直接上手写。先说个结论:QGraphicsView这个组件,看着像是专门用来显示图片的,其实它是一个完整的图形场景框架。你要做的“选图、显示在GUI窗口里”只是它最基础的一个用法。搞清楚它背后那几个类的协作方式,后面做缩放、拖拽、批量浏览、标注工具,都是顺水推舟的事。
我最早接触PyQt5时也走过弯路,第一反应是用QLabel放图片,觉得简单。但当我把一个几千像素的大图塞进去、又想做框选和缩放之后,QLabel方案很快就撑不住了。后来换成QGraphicsView,才体会到“场景-视图-图元”这套设计到底解决了什么问题。这篇博文就围绕这个标题展开,从环境搭建、架构拆解,到选图、显示、自适应缩放,再到滚轮缩放和拖拽平移的增强交互,最后把一些实战中容易踩的坑统一列一遍。内容偏实操,代码可以直接抄,关键是理解每一步背后的原因。
1. 为什么是QGraphicsView,而不是QLabel
1.1 如果只是“显示图片”,QLabel确实够用,但也就到此为止
很多人一开始会这样写:
from PyQt5.QtWidgets import QLabel from PyQt5.QtGui import QPixmap label = QLabel() pixmap = QPixmap("image.jpg") label.setPixmap(pixmap)三行代码,图片出来了。窗口缩放时图片不跟着变,换图时直接setPixmap覆盖即可。在固定尺寸、无交互、显示小缩略图这种场景下,这套方案完全成立,代码量也最小。
但一旦需求变成“图片自适应窗口大小”“鼠标滚轮缩放”“拖动查看局部细节”,QLabel的短板就暴露了。QLabel本身不是为图形交互设计的,它没有坐标系、没有图元管理、没有事件分发机制。有人会硬写setScaledContents和事件过滤器去模拟缩放,结果就是坐标计算越来越混乱,代码越来越脆。我见过有的项目用QLabel做图像标注,鼠标坐标换算翻了三次方向,最后还是整体推倒重写了。
1.2 图形视图框架三件套:Scene、View、Item
QGraphicsView这套体系核心是三个类,分工非常清晰:
- QGraphicsScene:相当于一个“世界”,负责管理所有图元。它本身是一块无限大的抽象画布,承载坐标、碰撞检测、图元遍历这些逻辑,负责管理图元之间的关系。
- QGraphicsView:相当于一个“相机/窗口”,负责把Scene中值得看到的部分渲染到屏幕上。它决定视口大小、滚动范围、变换比例,用户看到的缩放、平移效果,本质是View对Scene观察方式的改变。
- QGraphicsItem:相当于放到Scene里的“物体”。图片、矩形、线条、文字、可拖拽的标注框,统统是它的子类。图片场景中主要用QGraphicsPixmapItem。
这三者的关系,可以理解为“演员在舞台上表演,观众通过取景框观察”。演员只要站好位置,舞台负责维护秩序,取景框切换角度和焦距,观众就看到不同效果。你不需要让图片知道自己在屏幕上的像素坐标,只需要把它放到Scene的正确位置即可,View会替你完成投影计算。
你自然会产生一个疑问:为什么要把简单的事情搞复杂?因为当你要做的事情从“显示一张图”变成“在图上做交互”时,这套分层架构的价值就体现出来了。缩放时,你只需要改变View的缩放比例,Scene里的图片数据原封不动,不重采样、不重载,计算量小而且精度有保证。拖拽时,你移动的是View的视口中心,而不是重新setPixmap,流畅度和内存开销完全不在一个量级。
1.3 选型对比:QLabel与QGraphicsView
| 对比项 | QLabel方案 | QGraphicsView方案 |
|---|---|---|
| 实现难度 | 极低,三五行代码 | 中等,需要理解Scene/Item概念 |
| 自适应缩放 | 需要手动计算并不断重建QPixmap | setSceneRect配合FitInView,一次搞定 |
| 平移查看大图 | 不擅长,需要容器滚动条或手工搬运图像 | 内置滚动条,还可用View变换做拖拽 |
| 鼠标事件与坐标映射 | 需要手工换算像素和控件坐标,易出错 | scene/view/item三级坐标映射,API成熟 |
| 图元叠加与标注 | 极难维护 | 天然支持,每个标注都是一个独立Item |
| 性能上限 | 适合静态小图 | 可支撑高清大图与批量交互 |
用QLabel图省事的下场是,一旦后续需求增长,你面临的是整个交互层的重写。而QGraphicsView初始化阶段多写几十行代码,后面扩展起来几乎是无痛的。如果你只是做一次性脚本工具,QLabel无所谓;但如果这个GUI是你准备长期迭代的产品,直接选QGraphicsView。
2. 搭好框架:环境配置与最小可视化程序
2.1 安装依赖
用pip直接装:
pip install PyQt5如果你是Python 3.9以上的环境,建议指定一个稳定版本:
pip install PyQt5==5.15.10装完之后可以简单验证一下版本:
from PyQt5.QtCore import QT_VERSION_STR from PyQt5.QtCore import PYQT_VERSION_STR print("Qt版本:", QT_VERSION_STR) print("PyQt版本:", PYQT_VERSION_STR)2.2 初始化三件套的标准流程
创建QGraphicsView的第一步不是直接new一个View,而是先把Scene建好,再把View和Scene关联起来,之后才能往Scene里加东西。顺序反了会导致图元“加进了Scene,但View不知道要看谁”的错位感。
from PyQt5.QtWidgets import QMainWindow, QGraphicsView, QGraphicsScene from PyQt5.QtWidgets import QVBoxLayout, QWidget, QPushButton from PyQt5.QtCore import QRectF class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("图片显示Demo") self.resize(900, 700) central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) self.view = QGraphicsView() self.scene = QGraphicsScene() self.view.setScene(self.scene) layout.addWidget(self.view)这段代码已经能看到一个空白的图形视图。Scene在这里没有设置sceneRect,View会用一个默认矩形作为可视区域。后续加上图片后,建议把sceneRect设为图片边界,否则可能会出现滚动范围计算错误的情况。
注意:QGraphicsScene和QGraphicsView的关联必须在往Scene中增加图元之前完成,或者至少保证view.setScene之后view知道同步刷新。如果你先addItem再setScene,某些版本会出现显示不及时的情况,虽然最终结果通常一致,但把顺序写对是更稳妥的习惯。
2.3 理解Scene的坐标系统
Scene的坐标系是逻辑坐标,与屏幕像素没有一一对应关系。它的原点默认在左上角,x轴向右,y轴向下。这个坐标系独立于任何窗口大小,你可以把一张w*h的图片放进坐标(0,0)位置,也可以放在(100,50)位置,Scene不会关心你的View此刻有多大。
理解这一点很重要。当你调用view.fitInView(scene.sceneRect())时,View会在自身尺寸允许的范围内把整个“矩形区域”缩放并居中显示。图片被缩放、平移,但Scene里图片的数据没有发生任何变化。你看到的是View对Scene的观测结果变化了,这跟PS里放大画布完全是两回事。
3. 实现“选择图片并显示”的核心步骤
3.1 从文件对话框获取图片路径
用QFileDialog.getOpenFileName弹原生对话框,让用户选择图片文件。
from PyQt5.QtWidgets import QFileDialog def choose_image(self): path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.webp)" ) if not path: return self.load_image(path)这里有个细节:getOpenFileName返回两个值,一个是文件路径,一个是过滤器文本。如果用户取消,路径为空字符串。在拿到非空路径后再执行加载逻辑,避免空路径出错。
关于文件过滤器的写法,看起来简单,但有个常见的坑:有人写成*.png *.jpg,在部分Linux桌面环境下,会导致文件名带空格或特殊字符的文件无法被正常识别。更推荐的写法是用分号分隔多种扩展名:
"图片文件 (*.png;*.jpg;*.jpeg;*.bmp;*.webp)"3.2 将图片转为QPixmap并放入Scene
拿到路径后,加载步骤如下:
- QPixmap(path)加载图片文件;
- 判断isNull,防止路径无效、文件损坏;
- 创建QGraphicsPixmapItem,调用scene.addItem添加;
- 设置sceneRect为图片边界。
from PyQt5.QtGui import QPixmap from PyQt5.QtWidgets import QGraphicsPixmapItem def load_image(self, path): pixmap = QPixmap(path) if pixmap.isNull(): self.statusBar().showMessage("图片加载失败:" + path) return self.scene.clear() # 清掉之前的内容 item = QGraphicsPixmapItem(pixmap) self.scene.addItem(item) # 直接用图片的矩形区域作为场景范围 self.scene.setSceneRect(0, 0, pixmap.width(), pixmap.height())3.3 自适应窗口:fitInView的时机与一个必踩的坑
图片显示出来了,但默认情况下图片是按原始像素尺寸呈现的。如果图片是4000x3000,你的View只有900x600,那你只能看到左上角一小块,得拖滚动条才能看全。这显然不符合“选完图就整体预览”的预期。
解决办法是调用fitInView。但这行代码有个隐蔽的坑:它依赖View当前的视口尺寸,而如果你在窗口还没完全显示时就调用它,拿到的宽高可能还是初始默认值,导致缩放比例不准。
正确做法是重写resizeEvent,或者使用QTimer.singleShot延迟到窗口显示后执行:
from PyQt5.QtCore import QTimer def load_image(self, path): # ... 前面的加载逻辑保持不变 ... QTimer.singleShot(50, self.fit_to_view) def fit_to_view(self): self.view.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio)fitInView的参数很简单,第一个是要适配的矩形区域,一般传sceneRect;第二个是宽高比策略。传KeepAspectRatio可以保证图片不变形,宁可四周留白也不会拉伸。这个参数必须写,不写的话默认视图会强行把区域铺满视口,宽高比会被破坏,图片会变形。
提示:如果你后续做了“自适应”和“实际大小”两个按钮,可以把fitInView封装成一个公共方法,并在resizeEvent中调用,这样窗口大小变化时图片也能保持合适预览状态。但要注意,resizeEvent中调用fitInView,用户正在缩放图片时窗口变化会打断交互。我的经验是只在图片切换时做一次fitInView,窗口变化时不做自动适配。
4. 进阶交互:滚轮缩放、拖拽平移与右键重置
选图和显示的问题解决之后,使用体验还差那么一口气。用户往往希望在看图时能放大看细节、拖动看周边的区域、快速回到最初的整体视图。三种操作加上去,这个图像查看器才算真正能日常用起来。
4.1 滚轮缩放:改造wheelEvent
最直观的缩放方式是滚轮。核心逻辑是:以鼠标光标位置为中心,对View的缩放比例进行动态调整。
from PyQt5.QtCore import QPointF from PyQt5.QtCore import Qt class ImageView(QGraphicsView): def __init__(self, parent=None): super().__init__(parent) self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) def wheelEvent(self, event): factor = 1.2 if event.angleDelta().y() > 0 else 1 / 1.2 self.scale(factor, factor)setTransformationAnchor(QGraphicsView.AnchorUnderMouse)是必不可少的一行。它设置了View在缩放时的锚点:缩放发生在鼠标光标位置,所以放大时你会感到“被放大的内容正好是光标指着的那部分”。没有这行时,缩放默认以View中心为锚点,鼠标指着一个局部,滚轮下去图片却从正中心变大,操作感觉非常奇怪。
4.2 拖拽平移:切换ScrollHandDrag
平移大图的实现,QGraphicsView已经内置了。核心是ScrollHandDrag模式:
def enable_drag(self): self.setDragMode(QGraphicsView.ScrollHandDrag) def disable_drag(self): self.setDragMode(QGraphicsView.NoDrag)这个模式的意思是:鼠标左键按住拖动时,视口会跟随移动,看起来就像在卓面上拖着纸看。它的实现原理是移动滚动条,所以图片数据不发生任何变换,性能很好。
但有个体验问题:如果拖拽和平移是全局开启的,左键就没办法用来做圈选、点击图元等交互。更合理的方案是按住空格键时临时开启拖拽模式:
def keyPressEvent(self, event): if event.key() == Qt.Key_Space: self.setDragMode(QGraphicsView.ScrollHandDrag) else: super().keyPressEvent(event) def keyReleaseEvent(self, event): if event.key() == Qt.Key_Space: self.setDragMode(QGraphicsView.NoDrag) else: super().keyReleaseEvent(event)这个方案非常推荐。空格按住的瞬间光标变成抓手,拖完松手立刻恢复其他交互,很多成熟的看图软件都是这么设计的。
4.3 右键菜单重置视图
缩放平移久了,用户想快速回到刚打开时的预览状态。右键菜单加一个“适应窗口”选项就行。
def contextMenuEvent(self, event): menu = QMenu(self) action_fit = menu.addAction("适应窗口") action_actual = menu.addAction("实际大小") action = menu.exec_(event.globalPos()) if action == action_fit: self.fitInView(self.scene().sceneRect(), Qt.KeepAspectRatio) elif action == action_actual: self.resetTransform()实际大小这个选项也值得做,它调用resetTransform()把View的所有变换全部复位,图片回到原始1:1像素显示。
4.4 渲染效果:两行代码的提升
放大图片后最明显的问题是锯齿、模糊、马赛克。这是因为默认的渲染方式没有开启平滑变换。在ImageView初始化时加两行设置:
self.setRenderHint(QPainter.Antialiasing) self.setRenderHint(QPainter.SmoothPixmapTransform)第一行让边缘平滑,第二行在缩放图片时进行插值而非最近邻采样。效果非常直观,放大之后明显细腻很多。这条优化几乎零成本,强烈建议直接加上。
5. 性能、坐标与内存层面的避坑指南
5.1 内存管理:大图的隐藏炸弹
多张图片在QGraphicsView中切换时,内存问题很容易被忽略。QGraphicsScene.clear()会释放场景内的图元,但不会自动释放QPixmap背后的缓冲区。如果加载图片的pixmap是临时变量,它的内存会在函数退出后回收;但如果代码里持有item引用,对象生命周期就可能被意外延长。
最常见的坑是反复加载图片时,旧图片的QPixmap数据仍被引用持有,内存持续上涨。简单有效的做法是每次load_image都先用self.scene.clear()把旧图元清掉,再手动将保存的item引用设为None。不要依赖自动垃圾回收来处理大图。
5.2 坐标换算:Scene坐标和View坐标别搞混
当你做鼠标悬停提示像素坐标、框选标注时,必须要理解坐标换算。View的鼠标事件给出的是视图坐标,也就是相对控件左上角的像素值;Scene坐标才是图片在“世界”中的真实位置。二者关系因为存在缩放比例和滚动偏移,不能直接相等。
用view.mapToScene(event.pos())可以把视图坐标转成场景坐标。下面是一个状态栏显示鼠标位置的小示例:
class ImageView(QGraphicsView): def mouseMoveEvent(self, event): pos_in_scene = self.mapToScene(event.pos()) print(f"场景坐标: ({pos_in_scene.x()}, {pos_in_scene.y()})") super().mouseMoveEvent(event)当你日后做“在图片上画框”这类功能时,务必用场景坐标存储框的位置,不要存视图坐标。否则一旦缩放,框和数据就会对不上。
5.3 常见问题速查表
| 现象 | 排查方向 | 解决方案 |
|---|---|---|
| 图片加载后显示一片黑/白,没有内容 | QPixmap加载失败,路径中含中文或空格,文件损坏 | 打印pixmap.isNull()结果,改用QFileInfo规范化路径,确认图片格式被Qt支持 |
| 图片变形拉伸 | fitInView没写KeepAspectRatio | 统一给fitInView传Qt.KeepAspectRatio |
| 第一次FitInView不准,显示不全 | 窗口未完成布局时调用fitInView | 用QTimer.singleShot延迟调用,或重写showEvent时触发 |
| 放大的图片模糊、边缘锯齿 | 未开启平滑渲染 | 设置Antialiasing和SmoothPixmapTransform |
| 滚轮缩放中心点不在鼠标位置 | 没有设置AnchorUnderMouse或设置被后续覆盖 | 设置后不要轻易改回AnchorViewCenter |
| 鼠标拖不动图片 | DragMode与当前交互冲突 | 检查是否在拖动事件中触发了其他Item的鼠标事件,Scene的item可能要设置setAcceptedMouseButtons |
| 反复加载图片内存飙升 | 旧item未被正确释放 | scene.clear()后显式置空引用,必要时调用gc.collect() |
5.4 大图的加载优化:先压缩再显示
如果你要处理几千万像素甚至上亿像素的摄影图片,直接把原图加载到QPixmap,内存占用可能巨大,UI直接卡死。UI卡顿的关键不是加载本身,而是步线程中解压JPEG/PNG会产生明显的阻塞。
初步优化方案是:先在任意线程读图片尺寸,再按合适的缩放系数加载缩略图。QImageReader提供了setScaledSize,可以先读尺寸再按比例缩小加载,从而避免加载整张大图。
from PyQt5.QtGui import QImageReader reader = QImageReader(path) reader.setAutoTransform(True) size = reader.size() if size.isValid(): target_width = 2048 ratio = max(1.0, size.width() / target_width) reader.setScaledSize( QSize(int(size.width() / ratio), int(size.height() / ratio)) ) pixmap = QPixmap.fromImage(reader.read())这个做法的效果立竿见影:一张8000万像素的图,内存占用能从几百MB下降到几十MB,打开速度明显加快。需要看原图细节时,再按需加载局部,这是另一个话题,暂时不展开。
6. 完整代码示例:一个最小可用的图片查看器
把前面所有内容整合成一份可直接运行的完整示例。这段代码我实测过,支持选图、自适应、滚轮缩放、拖拽平移、右键菜单。
import sys from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QPainter, QPixmap from PyQt5.QtWidgets import ( QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QVBoxLayout, QWidget, QPushButton, QFileDialog, QMenu ) class ImageView(QGraphicsView): def __init__(self): super().__init__() self.setRenderHint(QPainter.Antialiasing) self.setRenderHint(QPainter.SmoothPixmapTransform) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setDragMode(QGraphicsView.NoDrag) def wheelEvent(self, event): factor = 1.2 if event.angleDelta().y() > 0 else 1 / 1.2 self.scale(factor, factor) def contextMenuEvent(self, event): menu = QMenu(self) fit_action = menu.addAction("适应窗口") actual_action = menu.addAction("实际大小") clean_action = menu.addAction("清除图片") action = menu.exec_(event.globalPos()) if action == fit_action: self.fitInView(self.scene().sceneRect(), Qt.KeepAspectRatio) elif action == actual_action: self.resetTransform() elif action == clean_action: self.scene().clear() def keyPressEvent(self, event): if event.key() == Qt.Key_Space: self.setDragMode(QGraphicsView.ScrollHandDrag) else: super().keyPressEvent(event) def keyReleaseEvent(self, event): if event.key() == Qt.Key_Space: self.setDragMode(QGraphicsView.NoDrag) else: super().keyReleaseEvent(event) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("图片查看器") self.resize(1000, 750) central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) self.view = ImageView() self.scene = QGraphicsScene() self.view.setScene(self.scene) open_btn = QPushButton("打开图片") open_btn.clicked.connect(self.choose_image) layout.addWidget(open_btn) layout.addWidget(self.view) self.statusBar().showMessage("就绪") def choose_image(self): path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "图片文件 (*.png;*.jpg;*.jpeg;*.bmp;*.webp)" ) if path: self.load_image(path) def load_image(self, path): pixmap = QPixmap(path) if pixmap.isNull(): self.statusBar().showMessage("加载失败:请确认图片格式受支持") return self.scene.clear() self.scene.addPixmap(pixmap) self.scene.setSceneRect(0, 0, pixmap.width(), pixmap.height()) QTimer.singleShot(50, self.fit_to_view) self.statusBar().showMessage("已加载:" + path) def fit_to_view(self): self.view.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())加载图片部分用了scene.addPixmap这个便捷方法,它内部会创建一个QGraphicsPixmapItem并加入场景,对简单场景来说少写两行代码是纯收益。如果你需要后续持有item引用来做标注或其他修改,再用new出item并addItem的方式也不冲突。
7. 常见的坑与排查心得
7.1 加载图片路径带中文,偶尔打不开
不同平台行为不完全一致。Windows上路径没问题,但部分Linux发行版如果不设置utf8文件系统编码,可能读取失败。稳妥做法是读取路径后用QFileInfo确认文件存在,再用QImageReader去读,不用直接依赖QPixmap的隐式加载。
7.2 Scene的rect不更新,图显示不完整
如果加载新图后没有调用setSceneRect,有时View的滚动范围会沿用上一张图的尺寸,导致图片明明加载进去了,却看不到内容。每张图片尺寸都可能不一样,所以每次加载必须重设sceneRect。这个顺手做一下就能避免,不需要额外思考。
7.3 fitInView调用之后,图片还是一大一小
一种原因是View还没完成布局,此时视口尺寸可能是默认值100x30之类。延迟到事件循环后执行能解决。另一种原因是图片Item设置了旋转或变换,fitInView会尽量适配整个item边界,但如果你在item上叠加了其他变换,边界计算可能不包含那些变换。保持item本身transform为纯平移就够了,缩放交给我们前面封装的scale方法。
7.4 高清屏显示模糊
在Windows高分屏下,如果QApplication没有设置高分屏支持,整个界面可能模糊。在创建QApplication之前加上:
from PyQt5.QtCore import Qt QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)这段代码需要在QApplication实例化前执行,放在脚本最开头。
7.5 鼠标行为冲突:左键画框时会触发平移
如果你在滚动拖拽模式下又需要画标注框,就需要注意DragMode与Item鼠标事件的关系。ScrollHandDrag模式下,QGraphicsView会吞掉鼠标事件,Item收不到press消息,导致你画不了框。
常规解决思路:空格键临时切换拖拽模式;或者干脆不用内置DragMode,而是手写mouseMoveEvent平移,释放鼠标左键作为画框事件。后者可控性更高,但要处理光标样式、惯性、边界限制,代码量会明显增加。我体感上还是优先用“空格切换”这种优雅省事的方案。
我把这套框架在我自己的模拟项目X里持续迭代了几个月,从最初只用来显示产品图片,到后来叠加了区域标注、像素坐标读取、多图层切换,没有再出现推倒重写的情况。QGraphicsView这套体系一旦领会了,收益会持续很久。
8. 一些个人补充的实操思考
框架搭起来很快,但真正决定使用体验的往往是那些很不起眼的小点。我在实际使用中发现,图像查看类工具,最影响用户观感的不是基础功能,而是细节。
一个是状态栏信息。显示鼠标对应的场景坐标、当前缩放百分比,对使用者排查问题很有价值。缩放比例可以用view.transform().m11()取出来乘以100换算成百分比,放到状态栏实时刷新。这个改进成本极低,但明显提升工具的专业感。
另一个是拖入文件支持。QGraphicsView本身没有现成的拖入打开事件,但你可以重写dragEnterEvent和dropEvent。文件拖进来直接加载,这个交互很符合看图工具的习惯,代码也不复杂,建议顺手加上。
还有一个建议是尽早封装一个“加载图片”的公共入口,把加载、适配、状态提示这些逻辑都收拢。后续要扩展批量浏览、上一张下一张,只需要在这个入口基础上叠状态。千万别把每张图的加载逻辑散落在各个事件里,后面维护会非常痛苦。
这个组件的学习曲线其实不陡:理解了Scene、View、Item的三角关系之后,几乎所有GUI里关于图形图像的功能都能往这套框架上靠。滚轮缩放看起来是在“放大图片”,实际上只是View的变换矩阵变了,图片数据毫发无损。这种优雅的架构设计,一旦思路理顺,你以后再做任何图像演示类、标注类、可视化类工具,都会感谢当初选择QGraphicsView这个决定。