1. 为什么 QDockWidget 值得单独拿出来聊
做桌面端工具软件,尤其是那种需要同时看多个面板、频繁切换视图的工程类软件,停靠窗口几乎是绕不开的交互形态。你打开任何一个稍微专业一点的 IDE、波形分析工具、三维建模软件,都会看到主窗口四周挂着一堆可以拖动、可以浮动、可以关闭再拉回来的面板。Qt Widgets 里负责这套机制的核心控件就是QDockWidget。
我最早接触 QDockWidget 是在做一个数据采集上位机的时候。主界面中间是一块实时曲线画布,左边要放设备树,右边要放属性编辑器,下边要放日志输出。一开始我用的是 QSplitter 硬切,结果用户抱怨说“日志面板我不想一直占着地方,能不能收起来”,还有人说“属性面板我想拖到右边单独放大看”。QSplitter 做不到这些,改起来还特别别扭。换成 QDockWidget 之后,这些问题基本是配置层面就解决了。
这篇文章面向的是已经会写基本 Qt Widgets 程序、但对停靠窗口这套体系还不够熟的开发者。我会把 QDockWidget 的核心机制、布局规则、状态保存、样式定制、以及实际项目里踩过的坑,按我自己的理解顺序讲一遍。你不需要提前了解太多,跟着走就行。核心关键词就几个:停靠窗口、浮动面板、布局区域、状态持久化、嵌套停靠。这几个词贯穿全文,理解了它们,QDockWidget 基本就通了。
需要先说明一点:QDockWidget 本身只是一个“容器外壳”,它负责的是停靠行为、标题栏、浮动/关闭按钮这些框架层面的东西,真正显示内容的还是你塞进去的 QWidget。很多人第一次用会困惑“为什么我的控件拖不动”,原因往往就是把逻辑搞反了——你要先有 QDockWidget,再把内容控件 setWidget 进去,而不是反过来。
2. QDockWidget 的整体设计与核心思路拆解
2.1 它到底解决了什么布局问题
普通布局管理器(QHBoxLayout、QVBoxLayout、QGridLayout)的特点是“静态占位”。控件一旦放进去,位置和比例就基本定死了,用户没法在运行时调整。而 QDockWidget 引入的是一套动态可重排的布局模型:每个停靠窗口都是一个独立单元,用户可以拖动它的标题栏,把它停靠到主窗口的上下左右任意一个边缘区域,也可以让它脱离主窗口变成浮动窗口,甚至拖到另一个停靠窗口上面形成标签页合并。
这套模型背后其实是 QMainWindow 在支撑。QDockWidget 必须挂在 QMainWindow 上才能工作,因为只有 QMainWindow 内部维护了停靠区域(dock area)的概念。你如果把它塞进一个普通 QWidget,停靠功能是失效的。这一点是设计上的硬约束,不是 bug。
从架构角度看,QDockWidget 把“内容”和“容器行为”做了分离。内容控件只管自己怎么画,停靠窗口只管自己怎么被摆放。这种分离带来的好处是:同一个内容控件,你可以今天放在左边停靠,明天让它浮动,代码几乎不用改。这在做可配置界面的时候非常关键。
2.2 停靠区域与允许区域的取舍逻辑
QMainWindow 有四个停靠区域:Qt::LeftDockWidgetArea、Qt::RightDockWidgetArea、Qt::TopDockWidgetArea、Qt::BottomDockWidgetArea。默认情况下,一个 QDockWidget 可以被拖到任意一个区域。但实际项目里,我们往往要限制它的活动范围。
比如日志面板,它天生适合放在底部,你让它停到左边会显得很怪,宽度不够,文字换行严重。这时候就要用setAllowedAreas()来限制。我一般的做法是:
- 设备树、工程结构树:只允许左右
- 属性编辑器:只允许左右
- 日志、控制台输出:只允许上下
- 工具栏类面板:四个方向都放开
这个限制不是可有可无的,它直接影响用户的使用直觉。你放开全部区域,用户拖来拖去最后拖出一个自己都找不到的布局,反而增加困惑。限制区域本质上是在用约束引导用户,这是交互设计里很常见的手法。
2.3 嵌套停靠:被低估的布局能力
很多人不知道 QDockWidget 可以嵌套。所谓嵌套,就是在一个停靠窗口内部再放一个 QMainWindow,然后在这个内层 QMainWindow 上再挂 QDockWidget。这样就能实现“面板里面还有面板”的复杂布局。
我做过一个信号分析工具,右侧需要同时显示“通道列表”和“通道详情”,而且这两个面板要能上下分割、各自独立调整。单层 QDockWidget 做不到这种内部再分割的效果,用嵌套就解决了:外层一个 QDockWidget 放在右侧,里面塞一个 QMainWindow,内层再挂两个 QDockWidget,一个在上一个在下。
嵌套的代价是状态保存会变复杂,因为你要分别保存外层和内层的状态。这个后面讲持久化的时候会细说。但如果你确实需要多层可调整布局,嵌套是目前最干净的方案,比手动拼 QSplitter 再自己写拖拽逻辑要省事得多。
2.4 浮动窗口与主窗口的关系
QDockWidget 浮动之后,它本质上变成了一个独立的顶层窗口,但它和主窗口之间还保持着逻辑关联。比如主窗口关闭时,浮动的停靠窗口也会跟着关闭;主窗口最小化时,浮动窗口的行为取决于平台。这个关联是通过 Qt 内部的父子关系和窗口标志管理的。
有一个细节值得注意:浮动窗口的标题栏是系统原生的,还是 Qt 自绘的,取决于平台和样式。在 Windows 上通常是原生标题栏,在部分 Linux 桌面环境下可能是 Qt 自绘。这会导致浮动窗口的外观在不同平台上不一致。如果你对视觉一致性要求很高,可以考虑禁用浮动功能(setFloatable(false)),或者接受这种平台差异。我的建议是,除非有明确需求,否则不要轻易禁用浮动,因为浮动是用户整理工作区的重要手段。
3. 核心细节解析与实操要点
3.1 创建一个停靠窗口的最小完整代码
先给一个能跑起来的最小例子,把关键调用都标出来。假设主窗口类叫 MainWindow,继承自 QMainWindow。
#include <QMainWindow> #include <QDockWidget> #include <QTextEdit> #include <QListWidget> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // 中间的主内容区 QTextEdit *central = new QTextEdit(this); central->setPlaceholderText("主编辑区"); setCentralWidget(central); // 左侧停靠窗口:设备列表 QDockWidget *dockLeft = new QDockWidget("设备列表", this); dockLeft->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); QListWidget *deviceList = new QListWidget(dockLeft); deviceList->addItems({"设备A", "设备B", "设备C"}); dockLeft->setWidget(deviceList); addDockWidget(Qt::LeftDockWidgetArea, dockLeft); // 底部停靠窗口:日志 QDockWidget *dockBottom = new QDockWidget("日志输出", this); dockBottom->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea); QTextEdit *logView = new QTextEdit(dockBottom); logView->setReadOnly(true); dockBottom->setWidget(logView); addDockWidget(Qt::BottomDockWidgetArea, dockBottom); }这段代码里几个点必须理解清楚。第一,setWidget()只能调用一次,重复调用会警告并且不生效。第二,addDockWidget()的第二个参数决定初始停靠位置,但最终位置还受setAllowedAreas()约束。第三,内容控件的父对象设成 dock 本身,这样 Qt 会自动管理生命周期,dock 销毁时内容也跟着销毁,不用手动 delete。
3.2 标题栏、按钮与特性开关
QDockWidget 的标题栏上默认有浮动按钮和关闭按钮。这两个按钮可以通过特性标志控制:
dockLeft->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable);三个标志分别对应:可关闭、可移动、可浮动。如果你希望某个面板固定不动,就把 Movable 去掉;希望它不能被关掉,就去掉 Closable。我一般会保留 Movable 和 Floatable,只对少数“必须常驻”的面板去掉 Closable。
这里有个容易踩的坑:去掉 Closable 之后,用户就没法通过界面关闭它了,但你自己代码里还是可以调用close()或者hide()。所以如果你做了“视图菜单”来统一控制面板显隐,去掉 Closable 是合理的,因为显隐入口统一到了菜单里,标题栏上再放个关闭按钮反而重复。
标题文字用setWindowTitle()设置。注意这个标题同时会用在浮动窗口的标题栏上,所以起名要兼顾两种场景,别起太长。
3.3 停靠窗口的初始尺寸控制
这是新手最容易困惑的地方:为什么我resize()了停靠窗口,它还是那么窄?原因是停靠窗口的尺寸最终由 QMainWindow 的布局决定,你单独 resize 它往往不生效。
正确的做法有两种。第一种是用resizeDocks(),这是 QMainWindow 提供的接口:
resizeDocks({dockLeft, dockBottom}, {280, 200}, Qt::Horizontal);注意这个调用要在窗口显示之后或者至少布局完成之后才有效,放在构造函数里可能不生效,通常放到showEvent或者用QTimer::singleShot(0, ...)延迟执行。
第二种是给内容控件设置最小尺寸和尺寸策略:
deviceList->setMinimumWidth(200); deviceList->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);我一般两种结合用。resizeDocks()定初始比例,内容控件的最小尺寸兜底,防止用户拖得太小导致内容显示不全。这里的最小尺寸不要设太大,否则在小屏幕上会挤压主内容区,适得其反。
3.4 标签页合并与拆分
当两个停靠窗口被拖到同一个区域时,Qt 默认会把它们叠成标签页。这个行为由setTabbedDockWidgets()相关机制控制。你可以用tabifyDockWidget()手动把两个 dock 合并成标签页:
tabifyDockWidget(dockLeft, dockAnother); dockLeft->raise(); // 让 dockLeft 显示在前面标签页模式在面板很多的时候特别有用。比如右侧有“属性”“样式”“历史”三个面板,全竖着排会占满整个高度,合并成标签页就清爽多了。用户点标签切换,比来回拖动高效。
反过来,如果你不希望自动合并,可以在拖动时按住某个修饰键(不同平台不一样),或者干脆通过setAllowedAreas限制它们不在同一区域。但我的经验是,标签页合并大多数时候是好事,不要刻意禁用。
3.5 样式定制:去掉标题栏、改颜色
QDockWidget 的标题栏样式在不同平台上差异很大,有时候产品要求统一视觉,就需要自己定制。最彻底的做法是去掉原生标题栏,自己画一个:
QDockWidget *dock = new QDockWidget(this); dock->setTitleBarWidget(new QWidget(dock)); // 空标题栏,等于隐藏但这样做之后,拖动功能就没了,因为拖动是靠标题栏触发的。你要自己实现鼠标事件来拖动,或者干脆放弃拖动。所以除非有很强的定制需求,我不建议去掉标题栏。
如果只是想改颜色和字体,用样式表更稳妥:
QDockWidget { titlebar-close-icon: url(:/icons/close.svg); titlebar-normal-icon: url(:/icons/float.svg); font-size: 13px; } QDockWidget::title { background: #2d2d2d; color: #e0e0e0; padding: 6px; text-align: left; }注意QDockWidget::title这个子控件选择器,它控制的就是标题栏区域。样式表能改的东西有限,比如标题栏高度不太好精确控制,浮动窗口的边框也可能不受样式表影响。这是 Qt 样式表在原生窗口装饰上的固有局限,心里有数就行。
4. 实操过程与核心环节实现
4.1 从零搭一个多面板工作区
假设我们要做一个“数据监控工作台”,需求是这样的:中间是实时曲线,左侧是设备树,右侧是属性面板和告警面板(标签页合并),底部是日志。四个面板都要能显隐、能保存布局。
第一步,搭主窗口骨架,设置中央控件。中央控件用 QCustomPlot 或者 QChart 都行,这里不展开,重点是停靠部分。
第二步,创建四个 QDockWidget,分别配置允许区域和特性。设备树只允许左右,属性面板只允许左右,告警面板只允许左右,日志只允许上下。
第三步,把属性和告警合并成标签页:
addDockWidget(Qt::RightDockWidgetArea, dockProp); addDockWidget(Qt::RightDockWidgetArea, dockAlarm); tabifyDockWidget(dockProp, dockAlarm); dockProp->raise();第四步,创建“视图”菜单,把每个 dock 的toggleViewAction()加进去:
QMenu *viewMenu = menuBar()->addMenu("视图"); viewMenu->addAction(dockDevice->toggleViewAction()); viewMenu->addAction(dockProp->toggleViewAction()); viewMenu->addAction(dockAlarm->toggleViewAction()); viewMenu->addAction(dockLog->toggleViewAction());toggleViewAction()是 QDockWidget 自带的,返回一个带勾选状态的 QAction,勾上就显示,取消就隐藏,完全不用自己写逻辑。这是 Qt 提供的一个非常贴心的接口,很多人不知道,白白自己造轮子。
第五步,加一个“重置布局”的菜单项,把布局恢复到初始状态。这个后面讲状态保存时一起说。
4.2 布局状态的保存与恢复
这是 QDockWidget 实战里最重要的功能之一。用户花时间把面板拖成自己喜欢的样子,下次打开软件又变回默认,体验会很差。Qt 提供了两个接口:saveState()和restoreState()。
// 保存 QSettings settings("MyCompany", "DataMonitor"); settings.setValue("mainWindow/geometry", saveGeometry()); settings.setValue("mainWindow/windowState", saveState()); // 恢复 QByteArray geo = settings.value("mainWindow/geometry").toByteArray(); QByteArray state = settings.value("mainWindow/windowState").toByteArray(); if (!geo.isEmpty()) restoreGeometry(geo); if (!state.isEmpty()) restoreState(state);saveState()保存的是所有停靠窗口的位置、大小、浮动状态、标签页顺序等信息,返回一个 QByteArray。restoreState()把它还原回去。注意saveGeometry()和saveState()是两个不同的东西,前者管主窗口自己的位置和大小,后者管停靠窗口的布局,两个都要存。
这里有个关键细节:restoreState()必须在所有 QDockWidget 都创建并 addDockWidget 之后才能调用。如果你在构造函数里先 restoreState 再创建 dock,恢复会失败,因为 Qt 找不到对应的 dock 对象。正确的顺序是:创建所有 dock → 调用 restoreState → 如果没有保存的状态就用默认布局。
还有一个版本兼容问题。saveState()保存的数据带有版本号,如果 Qt 版本升级导致格式变化,旧数据可能无法恢复。所以恢复失败时要能优雅降级到默认布局:
if (!restoreState(state)) { // 恢复失败,应用默认布局 applyDefaultLayout(); }4.3 嵌套停靠的完整实现
前面提到嵌套,这里给一个具体实现。需求:右侧一个“分析面板”,内部上下分割成“频谱”和“统计”两个子面板。
// 外层 dock QDockWidget *dockAnalysis = new QDockWidget("分析面板", this); dockAnalysis->setAllowedAreas(Qt::RightDockWidgetArea); // 内层 QMainWindow QMainWindow *inner = new QMainWindow(dockAnalysis); inner->setCentralWidget(nullptr); // 内层不需要中央控件 // 内层两个 dock QDockWidget *dockSpectrum = new QDockWidget("频谱", inner); QDockWidget *dockStat = new QDockWidget("统计", inner); dockSpectrum->setWidget(new QTextEdit("频谱内容", inner)); dockStat->setWidget(new QTextEdit("统计内容", inner)); inner->addDockWidget(Qt::TopDockWidgetArea, dockSpectrum); inner->addDockWidget(Qt::BottomDockWidgetArea, dockStat); dockAnalysis->setWidget(inner); addDockWidget(Qt::RightDockWidgetArea, dockAnalysis);这样内层的两个 dock 可以上下拖动调整,外层整体可以停靠或浮动。保存状态时,外层用主窗口的 saveState,内层要单独保存:
settings.setValue("innerState", inner->saveState());恢复时先恢复外层,再恢复内层。嵌套层数不建议超过两层,再多的话状态管理会变得很麻烦,用户也容易迷失。
4.4 动态添加与移除停靠窗口
有些场景下,停靠窗口不是固定的,而是根据数据动态生成。比如每打开一个文档就生成一个对应的面板。这时候要注意:动态创建的 dock 必须在关闭时正确销毁,否则会内存泄漏。
QDockWidget *dock = new QDockWidget(title, this); dock->setAttribute(Qt::WA_DeleteOnClose); // 关闭时自动删除 addDockWidget(Qt::RightDockWidgetArea, dock);设置WA_DeleteOnClose之后,用户点关闭按钮,dock 会被 delete。但如果你是通过toggleViewAction()隐藏它,不会触发删除,因为隐藏不等于关闭。这个区别要分清楚:close()会触发删除,hide()不会。
动态 dock 的另一个问题是状态保存。如果 dock 是运行时生成的,下次启动时还没生成就 restoreState,Qt 会忽略这些不存在的 dock。所以动态 dock 的状态保存要单独处理,通常是在生成 dock 时给它一个唯一的 objectName,然后手动记录它的存在与否。
4.5 objectName 的重要性
saveState()和restoreState()内部是靠 objectName 来匹配 dock 的。如果你不设置 objectName,Qt 会自动生成一个,但自动生成的名字在每次运行时可能不同,导致恢复失败。
所以每个 QDockWidget 都必须显式设置 objectName:
dockDevice->setObjectName("dockDevice"); dockProp->setObjectName("dockProp");这个 objectName 一旦确定就不要改,因为用户的配置文件里存的就是这个名字。改了名字等于用户之前的布局全部失效。我在项目里会把所有 dock 的 objectName 集中定义成常量,避免手写字符串出错。
5. 常见问题与排查技巧实录
5.1 停靠窗口拖不动或拖出去回不来
拖不动最常见的原因是setFeatures()里没开 Movable,或者setAllowedAreas()限制得太死,导致当前区域不在允许范围内。检查这两个地方基本能解决。
拖出去回不来,通常是因为浮动窗口被拖到了屏幕外,或者多显示器环境下显示器被拔掉了。这种情况用户自己很难处理,所以最好提供一个“重置布局”的入口。实现很简单:
void MainWindow::resetLayout() { // 先把所有 dock 恢复成非浮动 for (QDockWidget *dock : findChildren<QDockWidget*>()) { dock->setFloating(false); dock->show(); } // 重新应用默认布局 applyDefaultLayout(); }applyDefaultLayout()里重新调用 addDockWidget 和 resizeDocks,把布局摆回初始状态。这个功能一定要有,它是用户遇到布局混乱时的救命稻草。
5.2 恢复布局后尺寸不对
这是高频问题。原因通常是 restoreState 之后又调用了 resizeDocks,或者内容控件的最小尺寸在恢复时还没生效。我的处理办法是:restoreState 之后不要立即 resizeDocks,如果确实需要调整,用QTimer::singleShot(0, ...)延迟到事件循环下一轮再执行。
另一个原因是内容控件在恢复时还没填充数据,尺寸策略计算出错。比如一个 QTableWidget 还没插入行,它的 sizeHint 很小,恢复出来的宽度就不对。解决办法是给内容控件设一个合理的最小尺寸,或者在数据加载完成后再恢复布局。
5.3 标签页切换时内容不刷新
标签页合并后,非当前标签的内容控件可能不会收到 paintEvent,导致切回来时显示的是旧内容。这个问题在自定义绘制控件上尤其明显。解决办法是在 dock 的visibilityChanged信号里触发内容刷新:
connect(dockProp, &QDockWidget::visibilityChanged, this, [=](bool visible){ if (visible) { propWidget->update(); } });注意visibilityChanged在标签页切换时也会触发,因为非当前标签在视觉上是不可见的。这个信号比showEvent更可靠。
5.4 浮动窗口的关闭行为异常
浮动窗口关闭时,默认行为是隐藏而不是销毁。如果你希望关闭即销毁,要设WA_DeleteOnClose。但设了之后,toggleViewAction()的勾选状态会失效,因为对象已经没了。所以这两个东西要配合使用:要么用隐藏模式配合 toggleViewAction,要么用销毁模式自己管理菜单状态。我一般选前者,因为隐藏模式更简单,对象一直存在,状态管理不容易出错。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 停靠窗口拖不动 | Features 未开 Movable | 检查 setFeatures |
| 拖到某区域停不下 | AllowedAreas 限制 | 检查 setAllowedAreas |
| 恢复布局失败 | objectName 未设置或改变 | 检查 objectName |
| 恢复后尺寸不对 | 内容控件最小尺寸未生效 | 设 minimumSize 或延迟 resize |
| 标签切换内容不刷新 | 未响应 visibilityChanged | 连接信号触发 update |
| 浮动窗口关不掉 | 未设 Closable | 检查 setFeatures |
| 动态 dock 内存泄漏 | 未设 WA_DeleteOnClose | 检查属性设置 |
| 嵌套 dock 状态丢失 | 内层状态未单独保存 | 单独 saveState 内层 |
5.6 几个我踩过的坑
第一个坑:在构造函数里调用restoreState()之前就调用了show()。这会导致恢复的布局被 show 时的默认布局覆盖。正确顺序是 restoreState 之后再 show,或者干脆在 showEvent 里做恢复。
第二个坑:给 dock 设置了样式表里的border,结果浮动窗口的边框变得很奇怪。原因是样式表同时作用于停靠状态和浮动状态,而这两种状态的窗口装饰机制不同。解决办法是用选择器区分,或者干脆不在 dock 级别设边框,改在内容控件上设。
第三个坑:多个 dock 的 objectName 重复。Qt 不会报错,但恢复时只有一个能生效,另一个位置错乱。这个 bug 很隐蔽,因为编译运行都不报错,只有恢复布局时才暴露。所以 objectName 一定要保证全局唯一,最好用常量管理。
第四个坑:在closeEvent里保存状态,但用户是通过任务管理器强杀的,状态没保存。这个没法完全避免,但可以加一个定时自动保存,比如每 30 秒存一次布局,降低丢失概率。
6. 进阶技巧与性能考量
6.1 大量停靠窗口时的性能
如果一个界面有十几个停靠窗口,全部显示时布局计算会有开销。实测下来,超过 20 个 dock 同时可见,窗口 resize 时会有轻微卡顿。优化方向有两个:一是用标签页合并减少同时可见的数量,二是对不常用的 dock 延迟创建,用到时才 new。
延迟创建的做法是:菜单里先放一个占位的 QAction,用户点击时才创建真正的 dock 并 addDockWidget。这样启动时只创建常用的几个,启动速度明显提升。代价是状态保存要处理“未创建的 dock”,恢复时跳过它们即可。
6.2 自定义标题栏按钮
原生标题栏只有浮动和关闭两个按钮,有时候我们想加一个“设置”按钮或者“刷新”按钮。做法是自定义一个 QWidget 作为标题栏:
QWidget *titleBar = new QWidget(dock); QHBoxLayout *layout = new QHBoxLayout(titleBar); layout->addWidget(new QLabel("设备列表")); layout->addStretch(); QToolButton *btnRefresh = new QToolButton(titleBar); btnRefresh->setIcon(QIcon(":/icons/refresh.svg")); layout->addWidget(btnRefresh); dock->setTitleBarWidget(titleBar);但前面说过,自定义标题栏会失去拖动功能。解决办法是在自定义标题栏上实现 mousePressEvent 和 mouseMoveEvent,手动调用dock->setFloating(true)和移动窗口。这部分代码不难,但要注意处理双击标题栏浮动/停靠的默认行为,需要自己模拟。
6.3 停靠窗口与全屏模式的配合
全屏模式下,停靠窗口默认还是显示的。如果你希望全屏时隐藏所有面板,只留中央内容,可以在进入全屏前记录各 dock 的可见状态,退出全屏后恢复:
void MainWindow::toggleFullScreen() { if (isFullScreen()) { showNormal(); for (auto &pair : dockVisibility) { pair.first->setVisible(pair.second); } } else { dockVisibility.clear(); for (QDockWidget *dock : findChildren<QDockWidget*>()) { dockVisibility.append({dock, dock->isVisible()}); dock->hide(); } showFullScreen(); } }这个功能在演示或者专注模式下很有用,实现也不复杂,建议加上。
6.4 多显示器下的浮动窗口管理
多显示器环境下,用户可能把某个面板拖到副屏上浮动。如果副屏被拔掉,这个浮动窗口就跑到屏幕外了。Qt 本身不处理这种情况,需要自己监听屏幕变化:
connect(qApp, &QGuiApplication::screenRemoved, this, [=](QScreen *screen){ for (QDockWidget *dock : findChildren<QDockWidget*>()) { if (dock->isFloating() && !screen->geometry().intersects(dock->geometry())) { dock->setFloating(false); // 拉回主窗口 } } });这个逻辑不完美,但至少能防止窗口彻底丢失。更完善的做法是记录浮动窗口的位置,屏幕恢复时再放回去,但实现复杂度高,看项目需求决定要不要做。
7. 我个人的一些使用体会
QDockWidget 这套东西,刚上手会觉得接口不多、挺简单,但真正用起来,细节全在布局恢复和边界情况上。我做了几个带复杂面板的桌面工具之后,最大的体会是:objectName 和默认布局函数这两样东西,一定要在项目一开始就规范好。objectName 用常量统一管理,默认布局写成一个独立函数,恢复失败时能随时回退。这两件事做好了,后面加面板、改布局都是几分钟的事;做不好,每加一个面板都要重新调一遍恢复逻辑,非常痛苦。
另一个体会是,不要过度定制标题栏。原生标题栏虽然丑一点,但拖动、双击、右键菜单这些行为都是现成的,用户也熟悉。自己画标题栏,这些行为全要自己实现,而且不同平台表现还不一样,投入产出比很低。除非产品对视觉有硬性要求,否则我建议保留原生标题栏,最多用样式表改改颜色和图标。
最后分享一个小技巧:如果你想让某个面板默认是浮动的,可以在 addDockWidget 之后调用setFloating(true),但要注意这必须在窗口 show 之后调用才生效,否则会被布局覆盖。我一般放在showEvent里,配合一个标志位确保只执行一次。这个需求不常见,但遇到的时候不知道这个细节会卡很久。