简介:本资源为基于 VS2019 与 Qt5.15.2 64 位环境编译的 KDDockWidgets 动态库及配套源码,面向需要在 Qt 项目中实现 QML 窗口停靠、可拖拽布局的开发者,尤其适合中高级 Qt 工程师直接集成使用。包内区分 Quick 与 QWidget 两套版本,各自对应不同的 include 头文件,不可混用;example 目录下提供两个演示 demo,每个 demo 均含 KDDockWidgets 文件夹,可通过将 KDDockWidgets.pri 添加进自有工程快速接入。资源共 956 个文件,以 h 头文件、cpp 源文件、png 图片、qml 界面文件、json 配置、dll 动态库与 lib 导入库等为主,压缩包约 51.56MB,目录结构完整,涵盖源码、示例与编译产物。目前已有 789 人学习下载,读者可借此省去自行编译与配置的繁琐过程,直接参考示例工程理解停靠窗口的布局保存、标题栏定制与多窗口嵌套等实现思路,快速落地到实际项目中。
1. 拿到 KDDockWidgets 的 VS2019 动态库:先搞清楚它到底解决什么
如果你正在用 Qt 做桌面端工具,尤其是那种需要多面板、可拖拽、可停靠、还能保存布局的工程软件,那你大概率绕不开 KDDockWidgets 这个名字。它是一套基于 Qt 的窗口停靠框架,支持 QWidget 和 QML 两套体系,能实现类似 Visual Studio 那种「拖出来变浮动窗口、拖回去自动吸附、布局可序列化」的交互。这次拿到的资源是 VS2019 + Qt5.15.2 64bit 编译好的动态库,附带源码和两个演示 Demo,Quick 版和 QWidget 版分开打包,头文件也各自独立。适合谁?适合不想从零编译、想直接集成进现有工程、又需要快速验证停靠效果的人。但注意,32bit 环境用不了,这是硬门槛。
2. 动态库选型与工程集成:Quick 版和 QWidget 版到底怎么选
2.1 两套库的本质差异与选型依据
KDDockWidgets 的 Quick 版和 QWidget 版不是简单的「接口不同」,而是底层渲染路径和事件处理机制完全不一样。QWidget 版走的是传统 QWidget 体系,停靠窗口本身就是 QWidget 的子类,适合纯 Widget 工程或者 Widget 为主的混合工程。Quick 版则是基于 QQuickWindow 和 QQuickItem 实现的,停靠区域、标题栏、浮动窗口都是 QML 场景里的 Item,适合 QML 为主或者纯 QML 工程。
选错了会怎样?最直接的表现是编译能过,但运行起来窗口不显示、拖拽无响应、或者直接崩溃。因为两套库依赖的 Qt 模块不同:QWidget 版依赖 QtWidgets,Quick 版依赖 QtQuick 和 QtQml。如果你的工程只链接了 QtWidgets,却硬塞 Quick 版的库,链接阶段就会报一堆未定义符号。
我一般会先看工程的主窗口类型。如果主窗口是 QMainWindow 或者 QWidget 派生类,直接选 QWidget 版;如果主窗口是 QQuickView 或者 QQmlApplicationEngine 加载的 QML 根对象,那就选 Quick 版。混合工程要特别注意,KDDockWidgets 官方文档里明确说了,同一个进程里不要同时加载两套库,否则符号冲突和事件循环混乱会让你查到头秃。
2.2 把 KDDockWidgets.pri 挂进现有工程
资源里的 example 目录下有两个 Demo,每个 Demo 下都有一个 KDDockWidgets 文件夹,里面包含 include、lib 和 KDDockWidgets.pri。最省事的集成方式就是把这个 pri 文件直接 include 到你的 .pro 工程里。
# 在你的 .pro 文件末尾添加 include($$PWD/KDDockWidgets/KDDockWidgets.pri) # 如果你用的是 Quick 版,确保工程里已经声明了 QT += quick qml # 如果是 QWidget 版,确保有 QT += widgets这个 pri 文件内部做了几件事:把 include 路径加到 INCLUDEPATH,把 lib 路径加到 LIBS,并且根据当前构建套件自动选择 debug 或 release 版本的库。逻辑说明:pri 文件里通常会有 CONFIG(debug, debug|release) 判断,分别链接带 d 后缀和不带 d 后缀的 dll。参数说明:如果你的工程输出目录和 Demo 不一样,可能需要手动改 pri 里的 LIB_PATH 变量,指向你实际存放 dll 和 lib 的目录。
提示:pri 文件里的路径默认是相对路径,如果你把 KDDockWidgets 文件夹挪到别的层级,记得同步改 pri 里的相对路径,否则 qmake 会报找不到库。
2.3 头文件包含与命名空间约定
Quick 版和 QWidget 版的头文件目录结构不同,不能混用。QWidget 版的核心头文件是 KDDockWidgets/KDDockWidgets.h,Quick 版则是 KDDockWidgets/Quick/Quick.h 之类的路径。实际写代码时,我一般会先包含总头文件,再按需包含具体类。
// QWidget 版典型包含方式 #include <KDDockWidgets/KDDockWidgets.h> #include <KDDockWidgets/DockWidget.h> #include <KDDockWidgets/MainWindow.h> // Quick 版典型包含方式 #include <KDDockWidgets/Quick/Quick.h> #include <KDDockWidgets/Quick/DockWidget.h>逻辑说明:总头文件里做了前置声明和宏定义,具体类头文件里才是完整的类定义。参数说明:如果你只包含具体类头文件而不包含总头文件,某些宏(比如 KDDOCKWIDGETS_QUICK)可能没定义,导致编译期条件判断走错分支。常见做法是总头文件必包含,具体类头文件按需包含。
3. 从 Demo 到自己的窗口:停靠布局的初始化与序列化
3.1 创建 MainWindow 和第一个 DockWidget
KDDockWidgets 的核心对象是 MainWindow 和 DockWidget。MainWindow 是停靠容器,DockWidget 是可停靠的窗口单元。下面这段代码是 QWidget 版的最小可运行示例。
#include <QApplication> #include <KDDockWidgets/MainWindow.h> #include <KDDockWidgets/DockWidget.h> int main(int argc, char *argv[]) { QApplication app(argc, argv); // 创建主窗口,作为停靠容器 auto mainWindow = new KDDockWidgets::MainWindow(QStringLiteral("MyMainWindow")); mainWindow->resize(1200, 800); // 创建一个 DockWidget,标题为 "面板A" auto dockA = new KDDockWidgets::DockWidget(QStringLiteral("PanelA")); dockA->setWidget(new QLabel(QStringLiteral("这是面板A的内容"))); // 把 DockWidget 添加到主窗口的左侧区域 mainWindow->addDockWidget(dockA, KDDockWidgets::Location_OnLeft); mainWindow->show(); return app.exec(); }逻辑说明:MainWindow 构造时传入一个唯一名称,这个名称在序列化布局时作为标识。DockWidget 构造时也传入唯一名称,setWidget 设置实际内容控件。addDockWidget 的第二个参数是停靠位置,常见的有 Location_OnLeft、Location_OnRight、Location_OnTop、Location_OnBottom、Location_OnCenter。参数说明:如果你想让多个 DockWidget 叠在同一个区域形成 Tab,就多次调用 addDockWidget 并传入相同的位置参数,KDDockWidgets 会自动把它们做成 Tab 页。
3.2 布局的保存与恢复:序列化成字节流
工程软件里用户拖完布局,下次打开要能恢复,这是刚需。KDDockWidgets 提供了 saveLayout 和 restoreLayout 两个接口,底层是 QByteArray 序列化。
// 保存布局到文件 QByteArray layoutData = mainWindow->saveLayout(); QFile file(QStringLiteral("layout.bin")); if (file.open(QIODevice::WriteOnly)) { file.write(layoutData); file.close(); } // 从文件恢复布局 QFile readFile(QStringLiteral("layout.bin")); if (readFile.open(QIODevice::ReadOnly)) { QByteArray saved = readFile.readAll(); mainWindow->restoreLayout(saved); readFile.close(); }逻辑说明:saveLayout 返回的 QByteArray 里包含了所有 DockWidget 的停靠位置、尺寸、浮动状态、Tab 顺序等信息。restoreLayout 会按照字节流里的描述重建布局。参数说明:restoreLayout 必须在所有 DockWidget 都已经创建并注册到 MainWindow 之后调用,否则会找不到对应的 DockWidget 而恢复失败。常见做法是在程序启动时先创建所有 DockWidget,再读文件恢复布局。
注意:saveLayout 不会保存 DockWidget 里的业务数据,只保存布局结构。业务数据需要你自己另外持久化。
3.3 QML 版停靠窗口的声明式写法
Quick 版的用法和 QWidget 版差异较大,需要在 QML 里声明 DockWidget 和 MainWindow 对应的 QML 类型。
import QtQuick 2.15 import QtQuick.Window 2.15 import KDDockWidgets 1.0 MainWindow { id: mainWindow width: 1200 height: 800 visible: true DockWidget { id: dockA title: "面板A" Rectangle { color: "#f0f0f0" Text { text: "QML 面板A"; anchors.centerIn: parent } } } Component.onCompleted: { mainWindow.addDockWidget(dockA, KDDockWidgets.Location_OnLeft) } }逻辑说明:QML 版里 MainWindow 和 DockWidget 都是 QML 类型,通过 import KDDockWidgets 1.0 引入。addDockWidget 在 Component.onCompleted 里调用,确保所有子项都已创建完毕。参数说明:QML 版的 Location 枚举和 C++ 版一致,但写法上要用 KDDockWidgets.Location_OnLeft 这种带命名空间的形式。如果你在 QML 里直接写 Location_OnLeft,会报未定义。
4. 避坑与排查:编译链接和运行时的五个血泪坑
4.1 32bit 环境直接翻车
现象:在 32bit Qt 套件下编译,链接阶段报大量未解析外部符号,或者编译通过但运行时报「不是有效的 Win32 应用程序」。原因:这份动态库是 64bit 编译的,32bit 进程无法加载 64bit dll。解决:确认你的 Qt 套件是 MSVC2019 64bit,在 Qt Creator 的「项目」→「构建套件」里检查。如果必须用 32bit,只能自己拿源码重新编译。
4.2 Quick 版和 QWidget 版头文件混用
现象:编译时报「未定义的类型」或者「无法解析的符号」,明明已经包含了头文件。原因:Quick 版和 QWidget 版的头文件目录结构不同,类名虽然相似但命名空间和宏定义不一样。解决:检查你的 INCLUDEPATH 里是否同时存在两套 include 路径。如果是,移除不需要的那套。在 .pro 里用 CONFIG 条件判断,只挂载对应版本的 pri。
4.3 Debug 和 Release 库混链
现象:Debug 模式下编译通过,但运行到停靠窗口创建时直接崩溃,堆栈指向 Qt 内部。原因:Debug 版工程链接了 Release 版的 dll,或者反过来。MSVC 的 Debug 和 Release 运行时库不兼容。解决:检查 pri 文件里的 CONFIG 判断是否正确,确保 Debug 构建链接带 d 后缀的库,Release 构建链接不带 d 后缀的库。手动检查 LIBS 变量里实际链接的库文件名。
4.4 restoreLayout 调用时机不对
现象:布局恢复后所有 DockWidget 都堆在中间,或者直接消失。原因:restoreLayout 在 DockWidget 创建之前调用了,框架找不到对应的 DockWidget 名称,只能忽略。解决:确保所有 DockWidget 都 new 出来并 addDockWidget 之后,再调用 restoreLayout。常见做法是在 MainWindow 的 showEvent 里做恢复,或者在所有初始化代码的最后一步做。
4.5 QML 引擎销毁顺序导致崩溃
现象:Quick 版程序退出时崩溃,堆栈显示 QQuickWindow 析构相关。原因:QML 引擎先于 KDDockWidgets 的 MainWindow 销毁,导致 MainWindow 析构时访问了已经释放的 QML 场景对象。解决:在 main 函数里手动控制销毁顺序,先 delete MainWindow,再让 QApplication 正常退出。或者把 MainWindow 的父对象设为 nullptr,在 aboutToQuit 信号里手动 delete。
5. 进阶技巧:用自定义标题栏和浮动窗口策略提升交互质感
5.1 自定义标题栏的两种接入方式
KDDockWidgets 默认的标题栏是框架自绘的,样式比较朴素。资源目录里有个 advancedCustomTitleBar 示例,展示了怎么替换成自定义标题栏。QWidget 版的做法是继承 KDDockWidgets::TitleBar 并重写 paintEvent,然后通过 DockWidget::setTitleBar 设置。Quick 版则是在 QML 里给 DockWidget 的 titleBar 属性赋值一个自定义 Component。
// QWidget 版自定义标题栏示例 class MyTitleBar : public KDDockWidgets::TitleBar { public: explicit MyTitleBar(KDDockWidgets::DockWidget *dockWidget) : KDDockWidgets::TitleBar(dockWidget) {} protected: void paintEvent(QPaintEvent *event) override { QPainter painter(this); painter.fillRect(rect(), QColor("#2d2d2d")); painter.setPen(Qt::white); painter.drawText(rect(), Qt::AlignCenter, dockWidget()->title()); } }; // 使用自定义标题栏 dockA->setTitleBar(new MyTitleBar(dockA));逻辑说明:TitleBar 的 paintEvent 里可以完全接管绘制逻辑,实现任意风格的标题栏。参数说明:dockWidget() 返回所属的 DockWidget 指针,通过它可以拿到标题、图标等信息。注意自定义标题栏的高度要通过 sizeHint 返回,否则布局会按默认高度算。
5.2 浮动窗口的初始尺寸和位置控制
默认情况下,DockWidget 拖出来变成浮动窗口时,尺寸和位置是框架自动算的。但有些场景下你需要指定浮动窗口的初始几何。KDDockWidgets 提供了 setFloatingGeometry 接口。
// 设置浮动窗口的初始几何 dockA->setFloatingGeometry(QRect(100, 100, 600, 400)); // 如果想让 DockWidget 一开始就是浮动的 dockA->setFloating(true);逻辑说明:setFloatingGeometry 只在 DockWidget 处于浮动状态时生效,如果它当前是停靠状态,调用后不会立即改变位置,等它下次浮动时才会用这个几何。参数说明:QRect 的坐标是屏幕坐标,不是主窗口坐标。如果你想让浮动窗口出现在主窗口旁边,需要先拿到主窗口的 geometry 再计算。
5.3 布局持久化的版本兼容处理
saveLayout 生成的字节流在不同版本的 KDDockWidgets 之间可能不兼容。如果你升级了库版本,旧布局文件可能恢复失败。我一般会在布局文件头部加一个版本号,恢复时先检查版本。
// 保存时写入版本号 QByteArray layoutData = mainWindow->saveLayout(); QDataStream stream(&layoutData, QIODevice::WriteOnly | QIODevice::Append); stream << QStringLiteral("1.5"); // 恢复时检查版本号 QFile file(QStringLiteral("layout.bin")); if (file.open(QIODevice::ReadOnly)) { QByteArray data = file.readAll(); QDataStream readStream(&data, QIODevice::ReadOnly); QString version; readStream >> version; if (version == QStringLiteral("1.5")) { mainWindow->restoreLayout(data.mid(readStream.device()->pos())); } else { // 版本不匹配,走默认布局 } }逻辑说明:QDataStream 的 Append 模式可以在已有字节流后面追加数据,读取时先读版本号,再读剩余布局数据。参数说明:data.mid(readStream.device()->pos()) 拿到的是去掉版本号之后的纯布局字节流。这种做法在资源目录的 Changelog 里也有体现,1.5 和 1.6 的布局格式有差异,跨版本恢复大概率会出问题。
从那以后我每次集成 KDDockWidgets 到新工程,都强制走一遍「确认位数 → 确认版本 → 确认 pri 路径 → 先跑 Demo → 再改业务代码」的流程,少一步都可能多花半天排查。希望帮到你。
本文还有配套的精品资源,点击获取