上个月帮一个刚开始学Qt的朋友调界面,他问了个很典型的问题:为什么我的按钮、文本框都会用,一往主窗口里塞菜单、工具栏、状态栏、浮动窗口和对话框就乱套?其实这几个组件的用法本身都不难,难就难在它们共享同一个核心对象——QAction,以及主窗口对布局、停靠、显隐有一套默认管理逻辑,搞懂这套逻辑之后,Qt窗口开发基本就通了一半。
这篇文章我就按Qt Widgets开发的主线,把菜单、工具栏、状态栏、浮动窗口、对话框这几块逐一拆开讲清楚。内容主要面向正在学Qt Widgets、准备用QMainWindow做桌面工具或编辑器的朋友,我会把每个组件“为什么这么设计”“实际代码怎么写”“常见的坑在哪里”一次说透,最后用一个完整的骨架示例把它们串起来。
1. 为什么主窗口要用QMainWindow:五个区域的分工逻辑
很多新手分不清QWidget和QMainWindow的区别,甚至有人直接用QWidget开个窗口就往上面堆控件。这事本身不违法,但你一旦需要菜单栏、状态栏、可停靠面板,QWidget就完全管不过来了。QMainWindow之所以成为桌面应用主窗口的标准选择,是因为它在内部替你规划好了五个区域:
| 区域 | 对应类 | 管理入口 | 典型用途 |
|---|---|---|---|
| 菜单栏 | QMenuBar | menuBar() / setMenuBar() | 文件、编辑、视图等顶层菜单 |
| 工具栏 | QToolBar | addToolBar() | 常用动作的快捷入口 |
| 状态栏 | QStatusBar | statusBar() | 提示信息、坐标、进度反馈 |
| 浮动窗口 | QDockWidget | addDockWidget() | 素材列表、图层面板、属性面板 |
| 中央部件 | QWidget 及其子类 | setCentralWidget() | 主内容区,比如画布、列表、编辑器 |
1.1 QMainWindow帮你省掉哪些事
假如不用QMainWindow,纯用QWidget加布局,你得亲手拼一个“顶部菜单条 + 工具栏 + 中间内容 + 底部状态栏”的垂直布局。界面静态时看着还行,一旦用户想把工具栏拖出来、把面板停靠到左边、或者把状态栏的某个控件隐藏,所有这些重排逻辑都得自己写。而QMainWindow把这些都内建好了:菜单栏固定在顶部、状态栏固定在底部、工具栏可以拖到四周或悬浮、QDockWidget可以被用户自由拖拽成独立窗口。它天生就是“桌面应用主窗口”的完整骨架,所以不要拿QWidget硬扛。
1.2 中央部件的正确打开方式
QMainWindow本身不是一个可布置控件的容器,你必须通过setCentralWidget()指定主内容区。常见错误是试图给QMainWindow直接调用setLayout(),比如:
// 错误示范 auto *layout = new QVBoxLayout(this); // this是QMainWindow setLayout(layout);Qt会直接报错,因为QMainWindow不允许你再给它套一个顶层布局。正确的做法是新建一个QWidget作为中央部件,在它内部做布局,再把整个部件塞进去:
auto *central = new QWidget(this); auto *layout = new QVBoxLayout(central); layout->addWidget(someContent); setCentralWidget(central);如果不设置任何中央部件,窗口中央会是一片空白,而且整个窗口缩小到一定程度后,工具栏或浮动窗口很容易把中央区域挤成一条细缝。就算你暂时不打算放主内容,也建议先放一个QLabel占位,并给它一个合适的字符内容或最小尺寸,别让它空着。
1.3 一个容易忽略的初始化顺序
在构造函数里创建这些东西,建议按照“动作对象 → 菜单栏 → 工具栏 → 状态栏 → 中央部件 → 浮动窗口”的节奏来。这个顺序不是强制的,但你先定义好所有QAction,后面不管菜单还是工具栏都只是“把动作挂进去”,代码读起来非常顺。如果反过来先建菜单再定义动作,你会发现addAction需要传一个还不存在的对象,要么写成new QAction(...)临时创建,要么回头再改,很别扭。
2. 菜单栏与QAction:动作对象才是真正的幕后主角
菜单栏本身的结构很简单:QMenuBar下挂QMenu,QMenu下挂QAction。但真正值得你花时间理解的是QAction。它不只是“一个菜单项”,而是整个界面动作的中枢:文本、图标、快捷键、状态提示、可勾选状态、触发信号,全部打包在一个对象里。
2.1 三行代码搭出菜单树
QMenuBar *bar = menuBar(); QMenu *fileMenu = bar->addMenu(QStringLiteral("文件(&F)")); fileMenu->addAction(openAction);这里menuBar()会自动创建主窗口的菜单栏,你不需要手动new。addMenu返回的是QMenu指针,addAction挂上的是QAction。每一个顶层菜单、每一个下拉项,背后都是一个QMenu/QAction的组合。
菜单文本里的“(&F)”是Qt的访问键语法,显示时会出现带下划线的F,用户按Alt+F就能展开这个菜单。中文软件里很常见,注意别乱写,一个顶层菜单里最好只保留一个明确的访问键字符。
2.2 集中配置动作的细节
QAction最有价值的一点是“集中配置、多处复用”。一个打开动作可以这样定义:
openAction = new QAction(QStringLiteral("打开(&O)"), this); openAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); openAction->setShortcut(QKeySequence::Open); openAction->setStatusTip(QStringLiteral("打开一个图像文件")); connect(openAction, &QAction::triggered, this, &MainWindow::openFile);其中setShortcut不要自己写硬编码按键字符串“Ctrl+O”,直接用QKeySequence::Open这类标准语义名称。好处是跨平台默认键位正确,Windows和Linux上是Ctrl+O,mocOS上通常是Command+O,而且菜单右侧会自动显示这个快捷键,不需要额外代码。
statusTip是个很妙的机制:只要给QAction设了状态提示,当鼠标悬停在菜单项或者工具栏按钮上时,主窗口的状态栏会自动显示这段文字。你不需要手动捕获悬停事件,也不需要自己调用状态栏显示函数,框架全给你接好了。
2.3 checkable菜单项的一个易错点
菜单里如果放“显示网格”“锁定选区”这类开关,可以给QAction设置setCheckable(true),点击后菜单项前面会自动出现勾选标记。这里面有个小坑:这一类动作的勾选状态由框架自动维护,你的槽函数只需要根据isChecked()去执行逻辑,不要在槽里再手动setChecked()去设置状态。我之前调试一个Demo时,在槽里写了句:
action->setChecked(action->isChecked());看起来毫无意义,但实际因为信号触发时机在状态翻转之后,这种多余操作有时会把状态再翻回去,导致界面显示和实际状态不一致。如果你确实需要在某个时机改变勾选状态,用setChecked没问题,但要搞清楚触发顺序,别在triggered槽里反复改写。
另外一个常见需求是右键菜单。它和菜单栏没直接关系,但用的也是QAction和QMenu:
connect(tree, &QWidget::customContextMenuRequested, this, [=](const QPoint &pos) { QMenu menu(this); menu.addAction(addAction); menu.addAction(removeAction); menu.exec(tree->mapToGlobal(pos)); });使用exec()弹出时,菜单会进入一个本地事件循环。在菜单打开期间,主窗口的其它交互仍然可以继续,所以弹窗里的动作槽函数正常执行,不需要担心阻塞问题。唯一要注意的是,exec()返回后pos可能已经失效,不要保存mapToGlobal之后再用,直接当场使用即可。
3. 工具栏:同一个动作在多个入口复用的技术与雷区
工具栏和菜单的区别在于展示形态:菜单是文字列表,工具栏是按钮集合。但两者背后都是QAction,所以你做一次动作定义,可以同时出现在菜单和工具栏里,触发同一个槽函数,状态同步更新,这是Qt里很上瘾的设计模式。
3.1 创建工具栏并设置显示风格
QToolBar *toolBar = addToolBar(QStringLiteral("文件工具")); toolBar->setObjectName(QStringLiteral("fileToolBar")); toolBar->setMovable(true); toolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); toolBar->setIconSize(QSize(24, 24)); toolBar->addAction(openAction);这里特别提醒一下setObjectName。如果你希望窗口布局状态能保存和恢复,工具栏和浮动窗口都必须有唯一的objectName,因为saveState()和restoreState()就是靠它识别各个部件的。很多人在保存布局时发现还原不了,就是这个原因。
toolBar->setToolButtonStyle控制按钮显示方式,常用的有只显示图标、只显示文字、图标旁边显示文字。实际桌面软件里,纯图标更省空间,但需要配statusTip提示文字;文字旁边加图标对新手更友好,看按钮就知道干什么用的,调试阶段我习惯用Qt::ToolButtonTextBesideIcon,跑通后界面要精简再换成纯图标。
3.2 一个QAction挂两个入口的收益
你可能会想:菜单里已经有“打开”了,工具栏再放一个,是不是要把同一个槽函数connect两次?完全不需要。QAction只要connect过一次,你在菜单和工具栏都执行addAction,同一个动作的所有触发信号都会走到同一个槽函数:
fileMenu->addAction(openAction); // 菜单入口 toolBar->addAction(openAction); // 工具栏入口这种复用带来的好处是“一处改、多处动”。比如某段时间文件列表为空,你想禁用打开功能,只需要openAction->setEnabled(false),菜单项和工具栏按钮同时变灰。再比如打开功能正在处理一个文件,你想让所有入口同时显示忙碌状态,也依然是只操作这一个动作对象。这就是为什么我强烈建议把每个核心动作先定义为成员变量,而不是在菜单和工具栏里各自new一个QAction。
3.3 工具栏右键菜单和控件尺寸的坑
工具栏默认自带一个右键菜单,里面是所有工具栏的显隐列表。这个行为对用户很友好,但如果你又给ToolBar设置了自定义的context menu,两者会叠加,可能出现弹两个菜单的现象。我的做法是:工具栏的右键菜单保持默认,不在工具栏上额外设置customContextMenuRequested,自定义右键菜单只放在文档列表、画布这类内容控件上。
另一个细节更容易踩。QToolBar可以直接addWidget普通控件,比如把QSpinBox放进工具栏做缩放比例:
QSpinBox *zoomSpin = new QSpinBox(toolBar); zoomSpin->setRange(10, 800); toolBar->addWidget(zoomSpin);实际运行后你可能会发现这个SpinBox变得很窄,数字都显示不全。原因是QToolBar对普通控件不会主动给宽松的宽度策略,需要你手动设置minimumWidth或sizePolicy。我一般在addWidget之后补一句zoomSpin->setMinimumWidth(90),避免它在某些平台、某些拖拽布局下被压扁。
4. 状态栏:临时提示、常驻信息和进度反馈三类打法
状态栏看似不起眼,却是主窗口里“存在感”最强的部分。用户不一定会看,但一旦你需要给用户一个位置感、进度感或操作反馈,状态栏是最合适的位置。QStatusBar支持三类内容,用法完全不同,不要混在一起。
4.1 showMessage与statusTip的关系
showMessage是最基础的临时消息接口:
statusBar()->showMessage(QStringLiteral("正在保存文件..."), 2000);第二个参数是显示时长,单位毫秒,2秒后自动清空。如果把时长写成0,消息会一直显示到下次showMessage被调用。这个特性可以用来显示“就绪”这类长期状态。
很多新手在初始化时写statusBar()->showMessage(QStringLiteral("就绪"))却不给时长,以为它会一直挂在那里。实际上不带时长默认是0,也就是一直显示,直到被新消息覆盖,这个行为完全没问题。另外要记住:菜单或工具栏悬停提示由QAction::statusTip自动驱动,它内部也会调用状态栏的临时显示逻辑。所以你在槽函数里写showMessage时,不要在2秒后马上又被控件的悬停事件清掉——我自己通常遵循一个约定:悬停类的提示交给statusTip,不要额外写代码;而点击动作完成后的反馈用showMessage("成功", 2000),两者各管各的,不会冲突。
4.2 addWidget与addPermanentWidget的区别
除了临时文字,状态栏还可以放常驻控件,最常见的例子是图片查看器左下角的坐标、右下角的缩放比例:
QStatusBar *sb = statusBar(); QLabel *coordLabel = new QLabel(QStringLiteral("0, 0"), this); sb->addWidget(coordLabel); QLabel *zoomLabel = new QLabel(QStringLiteral("100%"), this); sb->addPermanentWidget(zoomLabel);addWidget添加的控件靠左排列,会按添加顺序依次排在已有内容后面;addPermanentWidget添加的控件靠右排列,在窗口宽度不够时应优先被压缩或省略。理解“永久”不是“一直存在”,而是“位置固定在右侧远端”。如果你的左侧控件和右侧控件都想常驻,注意别把窗口拉到太窄,否则右侧还是可能被挤出可见区域。
对QLabel这种控件,默认的sizePolicy一般够用。但如果你放的是QProgressBar、QComboBox这类有最小尺寸要求的控件,建议手动设置一次最小宽度,避免在不同系统下出现显示不全。
4.3 状态栏放进度条,线程归宿要注意
后台任务往状态栏塞一个进度条是常见做法:
QProgressBar *progress = new QProgressBar(this); progress->setRange(0, 100); progress->setValue(0); statusBar()->addPermanentWidget(progress);关键问题是:进度条更新千万不能在工作线程里直接调用setValue。QWidget不是线程安全的,在工作线程里直接操作界面控件,轻则界面不同步,重则崩溃。标准方案是让工作线程发信号、主线程槽函数更新进度条:
connect(worker, &Worker::progressUpdated, progress, &QProgressBar::setValue);信号槽机制检测到发信号的线程和接收对象所在的线程不同,会自动走队列连接,槽函数会安全地切回主线程执行。这里不需要额外加线程锁,例外情况是你在工作线程里自定义了lambda去捕获进度条指针并调用,那就要封装干净,不要裸操作控件。
5. 浮动窗口:可停靠面板的布局规则与用户习惯
标题里的“浮动窗口”,对应Qt里的QDockWidget。这个东西中文名容易误解,它的核心能力不只是“浮起来”,而是“可停靠、可拖动、可关闭、可重新组合面板”。素材列表、图层面板、属性编辑器,甚至聊天窗口,都是典型场景。
5.1 创建Dock部件并控制停靠区域
auto *layerDock = new QDockWidget(QStringLiteral("图层"), this); layerDock->setObjectName(QStringLiteral("layerDock")); QListWidget *list = new QListWidget(layerDock); list->addItem(QStringLiteral("背景层")); list->addItem(QStringLiteral("文字层")); layerDock->setWidget(list); layerDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); addDockWidget(Qt::RightDockWidgetArea, layerDock);setAllowedAreas控制用户能把面板拖到哪些区域停靠。这里我只允许左右两侧,是因为对“图层”这种纵向列表来说,停靠在顶部或底部非常反直觉,占空间又不好看。setAlloweAreas是面向用户的拖拽约束;而addDockWidget的第一个参数是初始停靠位置。
setFeatures是另一个值得关注的属性,由这些标志组合:
- DockWidgetClosable:允许用户关闭面板
- DockWidgetMovable:允许用户拖动它
- DockWidgetFloatable:允许用户把它拖成独立浮动窗口
默认三个全开,用户体验最好。如果你把Closable去掉,用户就没法通过面板右上角的X按钮关闭,有些初学者想让“面板不能关”就这么做,但要注意:即使关了X,用户还能在“视图”菜单里通过toggleViewAction切换显示,没有真正的“锁死”一说。
5.2 toggleViewAction:把面板显隐统一交给视图菜单
QDockWidget有一个很省心的接口toggleViewAction(),它会返回一个QAction,这个动作的勾选状态和面板实际显示状态自动同步。你只需要把它挂到“视图”菜单:
QAction *toggleLayerAction = layerDock->toggleViewAction(); toggleLayerAction->setText(QStringLiteral("图层")); viewMenu->addAction(toggleLayerAction);这个设计的好处是,你不用自己维护面板显隐状态,用户在菜单里勾选、或在面板上点X关闭、或拖动后浮动,所有状态的改变都会反映到同一个QAction上。工具栏也可以加toggleViewAction,效果一样,比如在你想让某个工具栏可显隐时。
多面板应用建议统一收集所有dock和toolbar的toggleViewAction,全部放到视图菜单里。这是桌面软件极为通用的约定,用户已经习惯了。
5.3 浮动状态的细节和经典翻车点
先注意splitDockWidget的使用。如果你把两个Dock都addDockWidget到同一个区域,默认行为是它们交错堆叠,而不是并排分割。实际画面经常是你添加了“图层”和“素材库”两个面板,都放在右边,结果它们相互挤压,每个都只显示一截。想要它们上下分割,用:
addDockWidget(Qt::RightDockWidgetArea, layerDock); addDockWidget(Qt::RightDockWidgetArea, materialDock); splitDockWidget(layerDock, materialDock, Qt::Vertical);splitDockWidget把第二个参数指定的部件放到第一个参数的指定方向,这样两个面板就能并排分割而不是堆叠,视觉整齐很多。
另一个坑是Dock内嵌控件的尺寸策略。我实际跑过一个Demo,Dock里放了一个带固定宽度的QListWidget,设置setFixedWidth(260),面板初始看起来没问题。一旦用户把它拖成浮动窗口,再停靠到另一边,整体宽度被固定,用户想拖边框调整窗口大小,却发现面板变得特别别扭。后来我把固定宽度改成minimumWidth,让Dock内部的Widget可以根据可用空间自由伸缩,问题迎刃而解。原则是:Dock内部尽量让布局和sizePolicy去管理尺寸,不要硬编码固定宽高。
6. 对话框:模态/非模态、标准弹窗和自定义表单的生命周期
对话框和前面几个组件不太一样,它通常不是常驻在主窗口里的,而是“按需弹出”。按交互方式可以分为模态和非模态,按来源可以分为标准对话框和自定义对话框。先把标准对话框用熟,再自己写自定义弹窗,能覆盖绝大多数场景。
6.1 消息框和文件选择的正确姿势
QMessageBox是最常用的标准对话框:
auto ret = QMessageBox::question(this, QStringLiteral("确认"), QStringLiteral("确定要删除这个图层吗?"), QMessageBox::Yes | QMessageBox::No); if (ret == QMessageBox::Yes) { // 执行删除 }注意返回值判断用的是QMessageBox::Yes,不是布尔值。判断错误是新手常犯的问题,写if(ret)这种代码不会报错,但逻辑很可能不对。
文件对话框同样简单:
QString filePath = QFileDialog::getOpenFileName(this, QStringLiteral("选择图片"), QStringLiteral(""), QStringLiteral("图片文件 (*.png *.jpg *.bmp)")); if (filePath.isEmpty()) return; // 用户点了取消QFileDialog的过滤条件里,显示名和通配符用空格连接,格式是“说明 (*.后缀)”,多个后缀用分号分隔。这个字符串写不对会导致文件过滤选项空白。另外,文件对话框返回的QString一旦为空,就代表用户取消了选择,千万别在空路径上继续操作。
6.2 自定义对话框:表单布局与返回值获取
自定义对话框本质是一个QDialog子类,在构造函数里用布局和控件搭出界面,通过accept/reject决定返回值。我给图片处理Demo写的一个模糊参数对话框,结构如下:
class FilterDialog : public QDialog { Q_OBJECT public: explicit FilterDialog(QWidget *parent = nullptr); double radius() const; private: QDoubleSpinBox *radiusSpin; QDialogButtonBox *buttonBox; }; FilterDialog::FilterDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(QStringLiteral("模糊参数")); auto *form = new QFormLayout(this); radiusSpin = new QDoubleSpinBox(this); radiusSpin->setRange(0.1, 100.0); radiusSpin->setValue(3.0); form->addRow(QStringLiteral("半径"), radiusSpin); buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); form->addRow(buttonBox); }调用侧:
FilterDialog dlg(this); if (dlg.exec() == QDialog::Accepted) { double r = dlg.radius(); applyBlur(r); }这个模式很常用,也很干净。exec()会阻塞直到对话框关闭,然后根据返回值判断用户是确认还是取消。因为dlg是栈对象,函数结束自动析构,不存在内存泄漏问题。
6.3 栈对象还是堆对象:对话框生命周期的取舍
多数情况直接用栈对象构造对话框就能搞定,但有些场景必须用堆对象。比如你想在主窗口里多次打开同一个对话框,而不是每次重新构造,或者对话框里有异步操作,你希望关闭后再做别的处理。这时常见的错误是:
auto *dlg = new FilterDialog(this); dlg->show(); // 错误示范:既不模态,又没设置自动销毁这样每次打开都会new一个对象,对话框关闭后对象没被delete,内存越用越多。正确的堆对象用法是配合deleteOnClose和finished信号:
auto *dlg = new FilterDialog(this); dlg->setAttribute(Qt::WA_DeleteOnClose); connect(dlg, &QDialog::finished, this, [this, dlg](int result) { if (result == QDialog::Accepted) { double r = dlg->radius(); applyBlur(r); } }); dlg->show();带WA_DeleteOnClose后,对话框关闭时自动delete自己,信号槽里访问dlg指针仍是安全的,因为finished信号在销毁前发出。这样既避免泄漏,又能做非模态弹窗。
还要强调模态不等于冻结。exec()启动的是本地事件循环,期间主窗口仍然可以重绘,仍能响应部分事件,但用户无法操作同级其它顶层窗口。如果槽函数里执行耗时操作,那个操作的代码是“卡在原地等待对话框返回”,不是界面卡死,但如果你在对话框里触发了另一个模态对话框,就会形成嵌套对话框栈。嵌套本身能正常工作,但一旦管理不好,会出现用户莫名其妙点不动主窗口的局面,所以尽量保持对话框层级简单。
7. 组装实例:菜单、工具栏、浮动窗口和对话框如何协作
前面把这些组件单独拆开了,现在把它们放进一个完整的QMainWindow骨架里。我以一个简单的“图片浏览 + 图层面板”模拟项目X为例,展示构造函数里的一次性装配过程。
7.1 构造函数里的一次性装配
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setWindowTitle(QStringLiteral("窗口组件演示")); // 1. 先定义动作 QAction *openAction = new QAction(QStringLiteral("打开(&O)"), this); openAction->setShortcut(QKeySequence::Open); openAction->setStatusTip(QStringLiteral("打开图像文件")); connect(openAction, &QAction::triggered, this, &MainWindow::openImage); QAction *exitAction = new QAction(QStringLiteral("退出(&X)"), this); exitAction->setShortcut(QKeySequence::Quit); connect(exitAction, &QAction::triggered, this, &MainWindow::close); // 2. 菜单栏 QMenu *fileMenu = menuBar()->addMenu(QStringLiteral("文件(&F)")); fileMenu->addAction(openAction); fileMenu->addSeparator(); fileMenu->addAction(exitAction); QMenu *viewMenu = menuBar()->addMenu(QStringLiteral("视图(&V)")); // 3. 工具栏 QToolBar *toolBar = addToolBar(QStringLiteral("文件工具")); toolBar->setObjectName(QStringLiteral("fileToolBar")); toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); toolBar->addAction(openAction); viewMenu->addAction(toolBar->toggleViewAction()); // 4. 状态栏 statusBar()->showMessage(QStringLiteral("就绪")); // 5. 中央部件 auto *centralLabel = new QLabel(QStringLiteral("图片显示区"), this); centralLabel->setAlignment(Qt::AlignCenter); centralLabel->setMinimumSize(400, 300); setCentralWidget(centralLabel); // 6. 浮动窗口 auto *layerDock = new QDockWidget(QStringLiteral("图层"), this); layerDock->setObjectName(QStringLiteral("layerDock")); auto *layerList = new QListWidget(layerDock); layerDock->setWidget(layerList); layerDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); addDockWidget(Qt::RightDockWidgetArea, layerDock); viewMenu->addAction(layerDock->toggleViewAction()); }这个骨架虽然简单,但把前面讲到的所有组件都串在了一起。菜单和工具栏共用同一个openAction,状态栏由statusTip自动驱动提示,视图菜单统一控制工具栏和图层面板的显隐。
7.2 信号连接上的两个高频翻车点
第一个是同一个QAction被connect了两次。有些程序员先建菜单时connect一次,后面建工具栏时又把触发信号connect一次,结果点一下按钮执行两次打开逻辑。QAction被addAction到多个容器只负责展示,信号连接只做一次。建议把QAction定义为成员变量,在构造函数一开始统一connect。
第二个是状态栏提示重复管理。既然QAction已设置了statusTip,你又在自己写的触发槽函数里调用statusBar()->showMessage,两者不是同一个时机会冲突,而是意义不同:悬停提示告诉用户“这个按钮干什么”,点击后的提示告诉用户“操作结果成功或失败”,两者都该有,但别写成互相覆盖的逻辑。我自己的习惯是:初始化“就绪”、点击反馈、悬停提示,三者分别用不同的机制去实现,不在槽函数里无条件覆盖状态栏长期内容。
7.3 我实际跑这个骨架时调整了什么
跑这个模拟项目时,两个细节让我印象很深。
第一次运行,我发现把“图层”面板拖到左侧后,想拖回右侧却拖不回去,后来一查是创建时写了setAllowedAreas(Qt::RightDockWidgetArea),只允许右侧。改成Left和Right两个区域,问题消失。这提醒我:写约束是为了防止用户做不合理拖拽,不是把所有可停靠区域全部砍掉。
另一个是添加了第二个Dock面板后,两个面板全堆在右侧,各自占一块区域,显示非常局促。用splitDockWidget竖直分割后好了很多,但如果你想让面板以标签页方式切换,还需要进一步调整才行。多面板程序建议一开始就设计好分区策略,是左右并列、上下分割,还是标签合并,而不是等用户拖乱了再补救。
如果让我重新搭一遍这个骨架,我会直接建立一个“动作中心”的初始化函数,把open、save、exit、togglePanel这类QAction全部集中创建和connect,再让菜单、工具栏、快捷键、状态栏提示都指向这个中心。Qt这套窗口组件的精髓就在这里:动作驱动界面,而不是界面各管各的。搞懂这一点,后面不管是做编辑器、图像工具还是企业级管理系统,窗口骨架这块就再也不会手忙脚乱了。