简介:这是Qt Advanced Docking System(Qt高级扩展坞系统)的完整源码资源,面向希望构建类似Visual Studio等IDE风格可停靠界面的Qt开发者,适合具备C++与Qt基础、需要实现复杂窗口布局管理的中高级用户。包内为核心库源码及配套工程文件,可帮助理解停靠区域、浮动窗口、分组拖拽与透视切换等机制,并支持在Qt5/Qt6环境中集成使用。资源共包含若干源代码与配置类文件,压缩包大小约8.03MB,结构紧凑,便于直接查阅关键实现。目前已有2700余人学习下载,是研究高级窗口停靠方案时较具参考价值的开源项目。通过阅读源码,读者可掌握无中央小部件的自由停靠思路、浮窗内停靠逻辑及完整布局快速切换等设计细节,也可将其作为扩展自定义停靠功能的起点。 做一个 Qt 桌面工具设计多面板布局时,我最初是很依赖 QDockWidget 的,觉得"能拖出来、能停靠、能保存状态"已经够用。直到做一款带代码编辑、变量监视、渲染预览的调试器时,我才发现这套官方方案在复杂布局面前有多吃力。后来在 GitHub 上翻到 Qt-Advanced-Docking-System(下文简称 ADS),才算真正把多面板体验做到 IDE 级别。这篇文章就从我的真实接入视角出发,讲清楚这个高级扩展坞系统解决了什么问题、核心设计如何理解、怎么在 30 分钟内集成进项目,以及那些文档里不会写的坑。
1. 为什么项目用上 ADS:QDockWidget 的边界与诉求
1.1 官方停靠组件的三个"不够用"
先说结论:不是 QDockWidget 不能用,是它只适合"简单停靠"的场景。我踩过的具体限制主要有三条。
第一,QDockWidget 只能停靠到 QMainWindow 的四条边,中央区域永远是"剩余空间",不是一个可以参与停靠管理的 DockArea。这就导致你做不出"中央一个编辑器区域,左侧文件树,右侧属性面板,底部输出"这种标准 IDE 布局中"中央区域内部还能再堆叠标签页"的效果。你当然可以往 setCentralWidget 里塞 QSplitter 和 QTabWidget,但这些组件和 QDockWidget 的拖拽、浮动、状态持久化完全是两套体系,拼在一起非常别扭。
第二,QMainWindow 的 splitDockWidget 虽然能把同一个方向拆成多个停靠区,但拆分后的布局很难被用户自己重新组织。比如用户想把变量面板拖到代码面板的右侧,变成左右并排视图,QDockWidget 的拖拽逻辑经常只允许整体移动,不能精确指定"插入到某个区域内部的上方、下方、左侧、右侧"。对非技术用户来说,这种拖拽体验是反直觉的,对技术用户来说,它也不如 JetBrains 系工具那样自由。
第三,状态持久化不完整。QDockWidget 配合 QMainWindow::saveState 能记录基本浮停状态,但恢复到一半如果某个 widget 还没创建,或者布局里有浮动窗口,恢复结果常常错位。而且它没有"布局版本号"的概念,应用升级后旧布局文件直接失效的情况很常见。在我的项目里,用户最反感的就是每次升版都要重新摆一次窗口。
1.2 ADS 解决的是哪一类问题
ADS 的核心思路,是把"停靠系统"从 QMainWindow 中彻底独立出来。你可以把 CDockManager 当成一个全新的停靠容器,所有面板、编辑器、预览区都是里面的 DockWidget,它们可以在任意方向拆分、堆叠、浮动、自动隐藏,并且整套布局可以序列化保存,也能在不同 DPI 和窗口尺寸下尽量保持相对结构。
这个库在 GitHub 上维护周期很长,支持 C++11 和 Qt5/Qt6,许可证是 LGPLv2.1 加商业授权,所以无论是开源工具还是商业软件,接入成本都相对可控。ADS 解决的是一类非常典型的需求:当你的应用开始出现"多面板、多标签、可自由重组布局"的需求时,与其自己从零写一套拖拽算法,不如直接站在成熟方案上做功能裁剪。
我当时的判断很简单:与其花两周调 QDockWidget 的边缘 case,不如花两天把 ADS 接进来,再花半天做定制。事实证明这个判断是划算的。
2. 核心设计拆解:容器、区域、DockWidget 如何协作
2.1 四个核心类,先记名字
ADS 的类命名很直白,但第一次看容易混淆。我按从大到小的顺序梳理:
- CDockManager:整个停靠系统的顶层管理者,是一个 QWidget。你通常在 QMainWindow 里 new 一个 CDockManager,然后
setCentralWidget(m_DockManager)。所有拖拽、拆分、浮停的逻辑都由它统一调度。 - CDockContainerWidget:停靠系统的"根容器",内部装的是 CDockAreaWidget 和用于拆分的 CDockSplitter。CDockManager 内部其实就持有一个最外围的容器。
- CDockAreaWidget:一个带标签栏的区域,负责管理多个 CDockWidget 的堆叠显示。你可以把它理解为"一组标签页的外壳"。
- CDockWidget:对实际内容 QWidget 的包装,提供标题栏、关闭按钮、浮动按钮、自动隐藏按钮。你写业务面板时,真正往里面塞的是它。
这几个类的关系,可以粗暴类比成:CDockContainerWidget 是一个大房间,CDockAreaWidget 是房间里的一扇扇窗,每扇窗里面可以叠多张玻璃(CDockWidget),而 CDockManager 是管理整个房间布局和窗户位置的总控台。
2.2 添加与重组的基本动作
往 ADS 里加面板,核心 API 就一句话:
ads::CDockWidget *panel = new ads::CDockWidget("面板标题"); panel->setObjectName("myPanel"); panel->setWidget(new QTreeWidget); m_DockManager->addDockWidget(ads::LeftDockWidgetArea, panel);setObjectName这一步不是可有可无的,ADS 的布局保存与恢复严重依赖 objectName 来定位每个 dock widget。不设置会导致后面 restoreState 时找不到对应组件。
当你想让两个面板在同一个区域里变成标签页关系,只需要对同一个 DockWidgetArea 再次 addDockWidget。默认行为是和一个已有区域合并成多标签。
当用户拖拽一个标签离开原区域,ADS 会动态创建新的 CDockAreaWidget 或 CFloatingDockContainer,整个过程自动完成。你不需要写任何拖拽事件处理代码——这和我最初用 QDockWidget 时的体验是完全不同的,我只需要关心"哪些面板可以浮动、哪些可以关闭、哪些必须固定"。
2.3 所有权与生命周期,一次说清
ADS 有一个很容易被忽略的规则:你 new 出来的 CDockWidget 一旦交给 CDockManager,所有权就归停了靠系统管理,绝对不要手动 delete。
如果设置里带上了DockWidgetDeleteOnClosefeature,面板关闭时 ADS 会直接销毁这个 CDockWidget。这个特性对"临时窗口"很合适,但对"主界面常驻面板"非常危险——用户关掉面板后再从视图菜单点开,会发现对应对象已经没了。我的习惯是:所有主界面面板都不开启DockWidgetDeleteOnClose,只把临时预览窗口设成这个 feature。关闭后的面板只是隐藏,toggleView(true)可以再次唤出。
3. 30 分钟集成:从零搭一个带编辑器和面板的骨架
3.1 用 CMake 接入 ADS 源码
我推荐直接以源码或子模块方式集成,而不是强行装系统库。在 CMake 里写:
add_subdirectory(ads) target_link_libraries(your_app PRIVATE ads)ADS 开箱即用,它自己会带入依赖。如果你想用共享库,编译选项里会有 ADS_BUILD_SHARED 之类开关,静态和动态都能跑。我对大多数项目都偏好静态集成,这样部署时不用额外带一堆 DLL。
3.2 最小骨架代码
下面这段是我实际项目的最小原型,你可以直接抄来验证:
#include <QMainWindow> #include <QTreeWidget> #include <QTextEdit> #include <DockManager.h> #include <DockWidget.h> class MainWindow : public QMainWindow { public: MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) { m_DockManager = new ads::CDockManager(this); setCentralWidget(m_DockManager); // 左侧文件树 ads::CDockWidget *filePanel = new ads::CDockWidget("文件树"); filePanel->setObjectName("fileTreePanel"); auto *tree = new QTreeWidget; tree->setHeaderLabel("项目文件"); filePanel->setWidget(tree); m_DockManager->addDockWidget(ads::LeftDockWidgetArea, filePanel); // 底部输出 ads::CDockWidget *outputPanel = new ads::CDockWidget("输出"); outputPanel->setObjectName("outputPanel"); auto *output = new QTextEdit; output->setReadOnly(true); outputPanel->setWidget(output); m_DockManager->addDockWidget(ads::BottomDockWidgetArea, outputPanel); // 中央编辑器区 ads::CDockWidget *editorPanel = new ads::CDockWidget("编辑器"); editorPanel->setObjectName("editorPanel"); auto *editor = new QTextEdit; editorPanel->setWidget(editor); m_DockManager->addDockWidget(ads::CenterDockWidgetArea, editorPanel); } private: ads::CDockManager *m_DockManager = nullptr; };跑起来之后,你会看到左侧文件树、底部输出、中央编辑器三个区域。此时中央虽然只有一个编辑器,但它已经是 ADS 的 DockArea 了,后续再往中心区 add 第二个面板,会自动变成标签页。
3.3 给它加一个"视图菜单"
多面板应用除了界面本身,还需要一个能控制面板显隐的菜单。ADS 提供了toggleView(bool),可以直接接到 QAction 上:
auto *act = viewMenu->addAction("文件树"); connect(act, &QAction::triggered, filePanel, &ads::CDockWidget::toggleView);这里有个细节:toggleView的语义是"如果当前隐藏则显示,如果当前显示则隐藏",正好和菜单勾选状态联动。你还可以用isVisible()去同步菜单的 checkable 状态,就不用在业务代码里到处判断面板可见性了。
集成到这个程度,一套基础 IDE 布局已经成型。剩下的事情,就是让用户改过的布局能存下来。
4. 动态布局与状态持久化:别让用户每次重摆窗口
4.1 saveState / restoreState 的正确姿势
ADS 能把整个布局打包成一个 QByteArray,存到 QSettings 或数据库里都行。基本用法:
const int LAYOUT_VERSION = 3; void saveLayout() { QSettings settings("MyCompany", "MyTool"); settings.setValue("layout_version", LAYOUT_VERSION); settings.setValue("main_window_geometry", saveGeometry()); settings.setValue("dock_layout", m_DockManager->saveState(LAYOUT_VERSION)); } void restoreLayout() { QSettings settings("MyCompany", "MyTool"); if (settings.value("layout_version").toInt() != LAYOUT_VERSION) { // 版本不一致,说明布局结构可能变了,直接使用默认布局 return; } restoreGeometry(settings.value("main_window_geometry").toByteArray()); m_DockManager->restoreState(settings.value("dock_layout").toByteArray(), LAYOUT_VERSION); }saveState接受的版本号非常有用。当你在新版本里增加了面板、删除了某个区域,布局数据结构和旧版不兼容时,版本号不一致就可以直接放弃旧布局,而不是让 restoreState 硬去匹配一段结构已经对不上的数据。经验是:只要修改了 DockWidget 的 objectName、增删过面板类型或改变了停靠区域结构,就把版本号加一。
4.2 恢复布局后,那些面板去哪儿了
恢复布局有一个前提:一定要在先把所有 CDockWidget 创建完成并 addDockWidget 之后,再调用 restoreState。如果先恢复后创建,ADS 找不到 objectName 对应的 dock widget,布局匹配会失败或产生残留区域。正确的顺序是:
- 创建主窗口和 CDockManager
- 注册所有菜单、动作
- 创建全部面板并 addDockWidget
- 读取 QSettings 并 restoreState
- 最后显示主窗口
如果放在步骤 5 之后 restore,某些情况下浮动窗口的位置、分栏比例已经发生了闪烁,体验不好。我在实测中的感受是:"先恢复、再 show"是最稳的。
4.3 自动隐藏:把面板收进侧边栏
ADS 较新版本支持自动隐藏功能。调用很简单:
filePanel->setAutoHide(true);设置后,面板不再占据一个常驻区域,而是折叠到屏幕边缘的自动隐藏标签里,鼠标悬停时滑出,离开后缩回。这个能力在窄屏、多面板场景下非常实用。不过自动隐藏在不同版本里的行为差异比较大,有的版本还要求预先开启配置标志,建议先跑一次官方示例确认你安装版本的行为。
我个人只在"属性面板"这类低频使用但需要随时可查的面板上启用自动隐藏,高频面板仍然保持常驻。
5. 踩坑记录:拖拽、焦点链与样式表的实测复盘
5.1 坑一:拖拽后的残留空隙
用 ADS 拖拽时,如果用户把某个区域里的最后一个 DockWidget 拖走,ADS 理论上会自动清理空区域。但我实测发现,在某些组合操作下,比如"先自动隐藏,再拖出,又拖回去"的路径,会留下一个高度为几像素的空隙或空白容器。
排查下来,问题往往不是 ADS 本身坏了,而是我在布局变化后立刻保存了状态。ADS 保存布局时会把当前容器的拆分比例也写进去,如果那一刻容器里还有空的 QSplitter 占位,保存的状态里就带了一个"幽灵区域",下次恢复时它又出现了。
处理办法有两个:要么在保存布局前先调用一次CDockManager::removeDockWidget()把待销毁的区域清干净;要么在恢复到一半时主动检查容器里是否有空区域,有就调用区域内方法关闭。更省事的方案是:保存和恢复布局的入口都放在主窗口 show 事件之后,给 ADS 留出足够的布局修正时机。
5.2 坑二:浮动窗口关闭时,内容被直接销毁
这是我最开始踩得最疼的一个坑。用户点掉一个浮动面板的关闭按钮,过一会再从视图菜单打开,发现面板内容变成了空白。排查半天,根因就是我在创建面板时顺手 set 了DockWidgetDeleteOnClosefeature。
ADS 在关闭浮动容器时,会判断每个 DockWidget 是否带 DeleteOnClose 标记,如果有,就直接 delete。这个行为对临时弹出的"查找结果"窗口是合理的,但对主面板就是灾难。我的建议是:除非面板真的是"一次性的临时页",否则永远别设这个 feature。如果一定要设,请在显示面板的菜单触发逻辑里重新 new 一个新面板,并把它重新 addDockWidget。
5.3 坑三:全局样式表把 ADS 渲染搞花了
ADS 自身有一套精心设计的样式表,用来绘制标签栏、标题按钮、浮动窗口边框。如果你在 main.cpp 里给整个应用设置了一套全局 QSS,而且选择器写得很宽,比如直接设置QWidget { background: #333; },那么 ADS 内部控件的样式会受影响,出现图标错位、按钮背景异常、拖拽预览闪黑块等问题。
正确做法是把全局样式限制在业务控件范围,不要对停靠系统内部组件做无差别覆盖。如果要做深色主题,最好的方式是以 ADS 自带的样式表为基础,复制一份出来改色值,再通过样式表机制设给 CDockManager。另外,在高 DPI 显示器上,拖拽预览偶尔会有像素偏移,这通常是系统缩放和 QSS 中固定像素值共同作用的结果,建议所有边距、图标尺寸都用 QStyle 提供的 metrics 或相对单位。
5.4 坑四:高 DPI 与布局恢复的"默契"
Qt5 时代,很多 ADS 使用者在双屏不同缩放比的场景下会碰到恢复后的布局分栏比例异常。比如在 100% 缩放的屏幕上保存,换到 150% 缩放的屏幕上恢复,面板宽度可能变得很怪。
这个问题不完全是 ADS 的锅,而是你自己应用是否开启高 DPI 感知的问题。Qt5 里需要在 main 函数最早处调用:
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);Qt6 默认启用,但如果你用了自定义的 QSurfaceFormat 或某些 OpenGL 相关组件,最好还是显式确认一下。ADS 在保存布局时记录的是逻辑尺寸,只要应用自身完成了高 DPI 适配,恢复基本不会出大问题。
6. 选型判断:什么时候该用 ADS,什么时候原生就够
6.1 两张方案对比
| 维度 | QDockWidget | Qt-Advanced-Docking-System |
|---|---|---|
| 边缘停靠 | 支持 | 支持,且更灵活 |
| 中央区域作为可停靠区 | 不支持 | 支持 |
| 自由拆分、重组布局 | 弱,需配合 QSplitter | 强,拖拽直接生成 |
| 标签页堆叠 | 支持 | 支持 |
| 自动隐藏到侧边 | 无 | 支持 |
| 布局持久化 | 有限 | 完善,带版本控制 |
| 拖拽体验精细化 | 一般 | 好,可定制 |
| 集成成本 | 零成本,原生自带 | 需要引入第三方库 |
| 许可证 | LGPL | LGPLv2.1+ 或商业授权 |
6.2 我的选型建议
如果你的应用界面只有两三个固定面板,比如"左侧列表、右侧详情、顶部工具栏",而且用户基本不需要自定义布局,那用 QDockWidget 完全够了。加一个 QSplitter 做右侧拆分,比引入整套 ADS 体系更轻、更可控。
但只要你开始追求现代 IDE 的交互体验——多标签编辑器、面板可以自由浮动到第二块屏幕、布局可保存成多套方案、侧边自动隐藏——ADS 就是当前 Qt 生态里最成熟的答案。它把拖拽、拆分、布局序列化这些脏活都做了,你只需要聚焦业务面板本身。
另一个判断角度是维护成本。ADS 的 API 相对稳定,GitHub 上有完整示例和讨论区。相比你自己在 QDockWidget 上堆补丁,长期维护一个业务项目时,ADS 的投入产出比明显更高。我在项目里还额外在面板右上角加了自定义操作按钮,ADS 的接口对这类定制也友好,不会逼你去 hack 内部控件树。
最后说一句部署:用 windeployqt 打包时,除了 Qt 自带 DLL,记得把 ADS 编译出来的动态库一起放进去,或者干脆像我一样静态编译,省心很多。如果静态链接,CMake 里设置好ADS_STATIC之类标志后,整包部署只需要一个可执行文件加资源文件,对用户来说体验也更好。
本文还有配套的精品资源,点击获取