简介:这是基于开源Qt-Advanced-Docking-System实现的高级窗口停靠系统示例资源,面向需要为Qt应用集成复杂可停靠窗口布局的开发者,解决原生QDockWidget在多区域、多窗口管理上的不足。它支持多区域灵活布局、内嵌与悬浮切换、弹出式窗口、布局保存恢复等功能,能提升多窗口应用的组织效率。资源共68个文件,压缩包仅253KB,以21个头文件和21个源文件为主体,涵盖DockManager、DockAreaWidget等核心类实现;另含2个UI界面、2个pro工程文件、样式表与SVG图标,以及可直接运行的exe与配套dll。已有571人学习下载,适合有一定Qt基础、希望扩展窗口管理能力的开发者。通过这份示例,可完整查看高级停靠系统的工程架构与关键实现,理解DockManager如何统一管理各类停靠区域,掌握创建可拖拽停靠窗口、动态调整布局、保存/恢复窗口状态等代码写法,为自研编辑器、IDE类工具提供可复用的界面基础。
1. 为什么我在放弃Qt原生Dock后转向Advanced Docking System
先交代一下背景。我之前做过一个工控类的桌面软件,界面复杂度不算低:左侧设备树、中间主视图区、右侧属性面板、底部还有日志输出。一开始图省事,直接用Qt自带的QDockWidget硬怼,结果做到中期就有点后悔了。
原生QDockWidget的痛点,用过的朋友应该都懂:默认没有标签页模式(Tab化需要自己处理tabifyDockWidget)、拖拽预览很粗糙、浮动窗口嵌套麻烦、布局状态保存恢复全靠手动序列化,而且界面一复杂,停靠区的拖拽逻辑就开始“任性”,同一个Dock在不同QMainWindow之间拖来拖去,经常出现焦点错乱。
后来调研了一圈,开源方案里真正成熟、社区活跃度高的就是Qt-Advanced-Docking-System(后面简称ADS)。这个库最初是给Qt Creator用的,后来拆出来独立维护,现在在GitHub上几千个star,商用友好(LGPLv2.1),在视觉上几乎复刻了Visual Studio那种现代IDE的停靠体验。
我需要的功能它基本都覆盖了:任意方向停靠、标签页停靠、拖拽预览、浮动窗口、嵌套布局、布局持久化、侧边栏收起展开,甚至能实现跨窗口拖拽。最关键的,它对现有代码的侵入性很低,核心只要求一个CDockManager替换QMainWindow的角色,其他业务Widget照常使用。
说白了,这是一个“换掉停靠框架,而不是重写界面”的方案,这也是我最终选择它的核心原因。
2. 编译集成:最容易被拦住的几个坎
ADS的集成不算复杂,但确实有几个细节不处理会浪费很多时间。我先说环境,我的开发环境是Windows 10 + Qt 5.15.2 + MSVC2019 64位,这套组合在ADS下跑得很稳。理论上Qt 5.12以上的版本都支持,但建议用较新的Qt版本,老版本编译时可能遇到C++17标准相关的问题。
2.1 源码获取与目录结构
直接把仓库克隆下来,或者下载release包。ADS的项目结构大概分三块:
src:核心源码,全部需要参与编译examples:官方示例,强烈建议先跑一遍tests:单元测试,一般不用管
我把整个ADS目录放到了项目third_party文件夹下,而不是安装在系统路径里,这样打包发布时不会遗漏依赖,团队协作时其他同事clone代码后直接编译即可,省去每个人都要手动配置的环境步骤。
2.2 qmake集成方式
如果你的项目仍然使用qmake,在.pro文件里这样写:
include($$PWD/../third_party/Qt-Advanced-Docking-System/src/ads.pri)ADS官方提供了一份ads.pri,它会把需要的源文件、头文件和编译宏一次性引进来。这里有一个关键宏必须要知道:ADS_STATIC。
官方源码默认可以被编译成动态库或静态库。如果你希望直接把ADS源码编译进你自己的可执行文件(即静态集成),需要在ads.pri之前定义这个宏:
DEFINES += ADS_STATIC如果不加这个宏,链接时会出现LNK2019这类符号无法解析的错误,因为默认的导出宏逻辑期望的是动态库方式。我一开始没注意这个细节,浪费了一个下午查链接错误,后来翻issue才反应过来。
2.3 CMake集成方式
现在很多新项目已经全面切到CMake,ADS也支持得很完善。直接在CMakeLists.txt里写:
add_subdirectory(third_party/Qt-Advanced-Docking-System) target_link_libraries(your_project PRIVATE QtAdvancedDockingSystem)如果你不想用add_subdirectory,也可以先单独编译安装ADS,然后通过find_package的方式引用。不过个人推荐add_subdirectory,因为方便调试、方便跟踪源码、方便修改,真正出了问题你还能直接断点到ADS内部去排查,而不用去符号服务器里找PDB。
2.4 编译期的高频报错
编译ADS时,有几个错误属于高频问题,我直接给出来:
C++17 standard not supported:ADS源码使用了std::optional等C++17特性,需要指定编译标准。MSVC2019默认比较保守,在.pro里加CONFIG += c++17,CMake里加set(CMAKE_CXX_STANDARD 17)即可。无法找到 QOpenGLWidget或QOpenGLFunctions相关头文件:ADS的浮动窗口预览用到了OpenGL相关模块,如果Qt安装组件时没勾选OpenGL模块,就会碰到这个问题。去Qt安装器里勾上Qt OpenGL相关组件,或者在.pro里加上QT += opengl widgets。ADS_DEBUG与ADS_RELEASE库混用:如果你先编译了动态库版本,又切到静态版本,构建缓存没清理干净,会出现新旧宏定义冲突。切换编译模式前,老老实实clean一次再做全量重编。
3. 核心概念与最小可用示例:先把DockManager跑起来
ADS里最重要的三个类:
CDockManager:整个停靠系统的管理器,类似QMainWindow的角色CDockWidget:被停靠的窗口容器,对应原来的QDockWidgetCDockAreaWidget:停靠区域,一个区域可以包含多个标签页
CDockManager用法上跟QMainWindow很像,但它并不继承QMainWindow,而是继承QWidget。所以你需要让主窗口自己继承QMainWindow,然后把一个CDockManager实例设成它的中央部件。
一个最小可运行的示例:
#include "DockManager.h" #include "DockWidget.h" // 主窗口构造函数 MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // 创建DockManager并设置为主窗口的中央部件 ADS::CDockManager::setConfigFlags(ADS::CDockManager::DefaultOpaqueConfig); m_dockManager = new ADS::CDockManager(this); setCentralWidget(m_dockManager); // 创建设备树面板 QWidget *deviceTreeWidget = createDeviceTreeWidget(); // 业务逻辑Widget ADS::CDockWidget *deviceDock = new ADS::CDockWidget("设备树"); deviceDock->setWidget(deviceTreeWidget); deviceDock->setFeature(ADS::CDockWidget::DockWidgetClosable, true); deviceDock->setIcon(QIcon(":/icons/device.png")); // 添加到左侧停靠区域 m_dockManager->addDockWidget(ADS::LeftDockWidgetArea, deviceDock); }这里重点说说setConfigFlags,它决定整个ADS的交互风格。有几个常用的配置分支:
DefaultOpaqueConfig:默认不透明拖拽预览,拖拽时直接用虚线框或透明窗口预览,所见即所得DefaultFlattenConfig:适合多层级嵌套的场景,拖拽时自动把嵌套层级压平DisableTranslucentFloatingWindow:禁用浮动窗口半透明效果,对性能较差的显卡更友好
这些配置可以按位或组合,比如:
ADS::CDockManager::setConfigFlags( ADS::CDockManager::DefaultOpaqueConfig | ADS::CDockManager::FocusHighlighting | ADS::CDockManager::DockAreaHasCloseButton);FocusHighlighting是我个人很推荐开的选项,它会在Dock获得焦点时给标签页一个高亮边框,在多窗口场景下你能一眼看出当前操作的面板是哪个,不用眯着眼找光标。
4. 进阶使用:布局持久化、侧边栏与多窗口协作
跑通最小示例之后,ADS真正的生产力优势才开始体现出来。
4.1 布局保存与恢复
桌面软件几乎都要求记住用户的面板布局,否则每次启动都重置,用户会抓狂。ADS布局持久化做得极其简单,接口就两个:
// 保存布局 QByteArray layout = m_dockManager->saveState(); QSettings settings; settings.setValue("main_window_layout", layout.toBase64());// 恢复布局 QSettings settings; QByteArray layout = QByteArray::fromBase64(settings.value("main_window_layout").toByteArray()); m_dockManager->restoreState(layout);这里有一个非常重要的隐含要求:saveState和restoreState保存的DockWidget是通过对象名(objectName)来匹配的,所以你在创建CDockWidget时必须显式设置objectName,而且不能重复。我早期的版本没设置objectName,结果保存恢复后所有面板全部堆在了同一块区域,完全错乱。
ADS::CDockWidget *deviceDock = new ADS::CDockWidget("设备树"); deviceDock->setObjectName("DeviceTreeDock"); // 必须设置,且保持不变如果需要针对不同用户保存不同的布局,可以在key里加上用户ID,比如layout_user_1001,互不影响。
4.2 侧边栏与折叠面板
ADS的侧边栏(CDockSideBar)可以做出类似Visual Studio那种图标化的收起面板。用法也很直接:
ADS::CDockWidget *outputDock = new ADS::CDockWidget("输出窗口"); outputDock->setObjectName("OutputDock"); // 添加到底部区域 m_dockManager->addDockWidget(ADS::BottomDockWidgetArea, outputDock); // 将Dock切换为侧边栏模式 m_dockManager->setDockWidgetFloating(outputDock); outputDock->toggleView(true); // 设置侧边栏位置和样式 m_dockManager->sidebar()->setSidebarPosition(ADS::CDockSideBar::SideBarBottom);侧边栏模式下,窗口默认是隐藏的,点击侧边栏图标才会弹出,鼠标离开后自动收起。这种交互很适合资源面板、图层管理器这类“偶尔用、但不需要一直占着屏幕”的界面。
4.3 嵌套布局与分组
ADS最强的地方是支持复杂的嵌套布局。你可以做到:左侧是一个大的停靠区域,这个区域内部再分上、下两层,每层还能继续分多列。这种结构的操作在运行期直接通过拖拽就能完成,不需要写一行代码,因为ADS的拖拽引擎会自动处理嵌套关系。
但如果你希望启动时直接呈现一个预设好的复杂布局,可以手动add多次,比如:
// 先把设备树放到左侧 m_dockManager->addDockWidget(ADS::LeftDockWidgetArea, deviceDock); // 把属性面板也放到左侧,并且与设备树上下排列 m_dockManager->addDockWidget(ADS::LeftDockWidgetArea, propertyDock, Qt::Vertical); // 把日志面板放到底部 m_dockManager->addDockWidget(ADS::BottomDockWidgetArea, logDock); // 把参数曲线放到右边 m_dockManager->addDockWidget(ADS::RightDockWidgetArea, curveDock);这里注意addDockWidget的第三个参数Qt::Orientation,它决定了当同一个Area里已经有Dock时,新的Dock是水平方向(Qt::Horizontal)排列还是垂直方向(Qt::Vertical)排列。默认情况下,同区域的Dock会自动合并成标签页,只有指定了排列方向,才会出现上下或左右分割的效果。
这个细节官方文档写得不明显,很容易被忽略,但实际上决定布局手感的关键就在这里。
4.4 跨主窗口拖拽
ADS还支持同一个CDockManager下的浮动窗口再拆分,甚至可以让不同的主窗口共享同一个CDockManager,从而实现跨窗口拖拽。这在多显示器场景下非常实用:用户可以把某个面板拖到第二个显示器浮起来用,也可以在需要时再拖回主窗口停靠。
实现方式是创建多个QMainWindow,但共用同一个CDockManager实例,然后为第二个窗口也setCentralWidget同一个CDockManager。虽然看起来像是把一个控件塞进两个窗口中,ADS内部会处理这种特殊关系。
不过说实话,这个场景的复杂度较高,我目前只在官方示例里体验过,还没有用到实际项目中。如果你确实有这种需求,建议先跑通官方MultiWindow示例再动手。
5. 实战中的坑与我的处理方案
我集成ADS到正式项目的过程中,前后花了大约一周时间,把主要功能框架搭完。这一节把踩过的坑集中列出来,希望能帮你省一些排查时间。
5.1 与QMainWindow原生菜单栏/工具栏的协调问题
这是最让我困惑的一个坑。因为CDockManager继承自QWidget,而不是QMainWindow,所以你仍然需要自己的QMainWindow来承载菜单栏、工具栏和状态栏。但不要再使用QMainWindow::addDockWidget方法去塞任何面板了,否则两者会互相干扰,界面上会出现一种“停靠内容被另外一面板挤压”的错位现象。
解决思路:菜单栏、工具栏、状态栏全部挂在你的QMainWindow上,面板内容全部交给CDockManager管理。中央部件区域彻底交给CDockManager,不要在里面再嵌套其他布局。简单说,QMainWindow当壳,ADS当芯。
5.2 高DPI缩放下的拖拽偏移
我的开发机是4K屏+125%缩放,DPI缩放打开后,ADS的拖拽预览框会出现明显的偏移现象——鼠标已经移动了一段距离,预览框却还停留在原地。
处理办法是在程序入口显式开启高DPI支持:
int main(int argc, char *argv[]) { #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); #endif QApplication app(argc, argv); // ... }如果你用的是Qt 6,高DPI已经默认开启,这个坑基本不会再遇到。但Qt 5的用户,建议保留这段代码。
5.3 布局恢复时Dock内容为空
第一次使用restoreState恢复布局时,我发现恢复出来的Dock窗口内容区域一片空白。排查了一下,原因是恢复布局时,对应的CDockWidget还没有来得及setWidget,或者setWidget传入的Widget还没有初始化完成。
解决方案:严格遵循“先创建DockWidget→setWidget→添加到DockManager→再restoreState”的顺序。不要试图在restoreState之后再去补setWidget,这样大概率会出现空窗口。如果实在有复杂的依赖关系,可以等所有Dock都创建并添加完毕之后,再在QTimer::singleShot(0, ...)里执行恢复操作,确保界面事件循环已经处理完一轮。
5.4 关闭按钮的语义与二次确认
默认情况下,CDockWidget右上角的关闭按钮会把Dock关闭(隐藏),但用户并不会知道它还能通过侧边栏或视图菜单重新打开。如果你的软件不希望出现“面板关了找不到”的体验,有两个方案:
- 去掉关闭按钮:
setFeature(ADS::CDockWidget::DockWidgetClosable, false) - 保留关闭按钮,但拦截
close信号,统一改为隐藏加提示
ADS提供了setCloseHandler的机制(或者重写CDockWidget子类的closeDockWidget),可以自定义关闭行为。我的建议是:核心功能面板(比如设备树)保留关闭按钮但二次确认,辅助面板(比如日志窗口)直接去掉关闭按钮,用侧边栏控制显隐。
5.5 多语言切换与标题刷新
ADS内部的标签页文本直接取自CDockWidget的标题,所以如果你的软件支持中英文切换,必须像刷新业务界面一样刷新Dock的标题,否则UI语言会一半中文一半英文。
我实现了一个简单的广播信号,语言切换后遍历所有CDockWidget,调用setWindowTitle和setTabText刷新标题文本。这里提醒一下,CDockWidget::setWindowTitle会同时影响浮动窗口的标题栏,setTabText才只影响标签页文本,两个方法都要调用。
6. 自定义样式与深色主题适配
Qt的QSS一定程度上可以作用于ADS内部的界面元素,但ADS的结构层次较深,有些控件名称必须通过objectName去精确匹配。分享一下我的深色主题适配思路。
ADS内部的关键控件objectName主要有:
DockWidget:单个Dock标签页DockAreaWidget:停靠区域DockManager:总管理器ClosableDockWidget:带关闭按钮的Dock标题栏
常见的QSS配置:
ADS__CDockWidget[objectName="DockWidget"] QLabel { color: #D0D0D0; background: transparent; } ADS__CDockAreaWidget { background: #2D2D30; border: 1px solid #3F3F46; } ADS__CDockWidgetTab { background: #2D2D30; color: #D0D0D0; padding: 4px 8px; } ADS__CDockWidgetTab[activeTab="true"] { background: #007ACC; color: #FFFFFF; }需要注意的是,ADS的QSS选择器对类名的写法比较严格,通常要带上ADS__前缀,这是Qt的命名空间转换规则。如果你在QSS里写CDockWidget不生效,试试带完整前缀的写法。
另外,拖拽预览窗口、浮动窗口标题栏的QSS样式也是可以自定义的。一个很实用的技巧:在DockWidget的标题栏里嵌入QToolButton,替换默认的关闭、浮动、停靠按钮图标,配合深色UI背景,视觉上更加统一。
我在实际项目中用的主题色就是典型的VS Code深色系背景(#252526),配合#CCCCCC的字体颜色,整体效果很干净。如果用了第三方的QSS主题库,比如QDarkStyleSheet,ADS的样式需要单独补充,因为QDarkStyleSheet没有覆盖ADS的控件名。
7. 我在实际选型时的几个判断维度
最后聊一聊“要不要用ADS”这个问题。网上常常有人问:ADS到底值不值得引入到正式项目里?我的判断维度大致有这几条:
- 是否重度依赖多面板交互:如果你的软件就是多面板协作型(像IDE、建模工具、数据看板),ADS带来的拖拽自由度和布局记忆能力价值很大
- 界面自定义需求高不高:ADS的默认样式偏向IDE风格,如果你要做的是一款极简风格工具,用ADS还要额外写不少样式覆盖代码,反而不如原生QWidget布局直接
- 团队是否愿意接受第三方依赖:ADS是LGPL协议,动态链接下商用很友好;如果你要静态链接,需要注意LGPL要求的重链接义务。团队内部最好提前确认这个合规问题
我在正式项目中使用ADS已经有大半年了,稳定性和性能都没有出现明显短板。偶尔遇到Bug,去GitHub issue搜一下基本能找到解决方案,作者维护频率也比较稳定。
如果你正在做一个面板复杂、停靠需求多的桌面应用,ADS值得你花一两天时间试用一下。先把官方示例跑通,再按我这篇文章的步骤集成到你的项目里,基本可以平稳落地。
本文还有配套的精品资源,点击获取