☰
QtPropertyBrowser 的 CMake 移植:在 MSVC 下编译 DLL 并集成到 VS
2026/10/15 7:03:00 网站建设 项目流程

如果你在 Windows 上用 Visual Studio 做 Qt 插件或者属性面板开发,早晚会遇到QtPropertyBrowser这个库。它原本是 Qt Solutions 时代留下的老牌组件,后来被拆到独立仓库维护,专门用来做那种“左侧属性树、右侧编辑控件”的界面,常见于 3D 编辑器、仿真软件、地图工具、关卡编辑器这类项目。这次我把它用 CMake 重新组织了一遍,在 MSVC 环境里编成 DLL,再集成到我自己的 VS 解决方案中,整个过程踩了不少坑,这里完整复盘一下。

先说结论:这个库本身不复杂,真正麻烦的是它年久失修、没有 CMake 工程、也没有像样的导出宏声明,直接拿原版.pro文件扔给 MSVC 编译会出现各种魔幻问题。下面记录的移植路线适合所有“拿到一个 Qt 组件源码,想快速变成 DLL 并接入 VS”的场景,不局限于 QtPropertyBrowser 这一个库。

1. 为什么要在 VS 里重新编译 QtPropertyBrowser

Qt 有 qmake、CMake、QBS 这些构建系统,但 Visual Studio 的工程体系是独立的.vcxproj。你在 VS 里干活时,通常不希望每次构建都手动跑一堆外部脚本,更不想把第三方库的源码一股脑编进主工程。我这次的目标很明确:把 QtPropertyBrowser 做成一个独立 DLL,主程序通过头文件和导入库链接,和 Qt 官方 DLL 的用法保持一致。

QtPropertyBrowser 本身的价值在于它帮你封装了 Qt 的QTreeWidget和QAbstractItemModel,对外提供一组现成的 PropertyManager。比如你要让用户编辑一个三维向量,不需要自己写QDoubleSpinBox的委托逻辑,直接用QtVector3DPropertyManager加一个QtProperty节点,控件增删、信号转发全部由库内部实现。

但在 Windows + MSVC 环境下,原生源码有几个明显障碍:

  • 源码是基于qmake工程的,古老的.pro写法不一定适配现代 CMake 或者 VS 的 IDE 集成。
  • 源码里几乎没有主动标注Q_DECL_EXPORT,在 Windows 下编 DLL 时会丢失符号导出,你链接时得到一堆LNK2019。
  • Qt 的 MSVC 版本和 MinGW 版本 ABI 不兼容,不能拿 MinGW 编出来的.a文件或者.dll直接给 MSVC 用。
  • 老代码可能在 Qt 5.15+ 和 Qt 6 下出现 API 废弃、编译警告升级成错误等问题。

所以要做的其实是三件事:整理出支持 CMake 的构建文件,补全 Windows 导出符号,最后在 VS 侧配置依赖路径和部署脚本。

2. 环境准备与构建工具链选型

2.1 工具链版本匹配思路

我用的环境是 Windows 10/11 加 Visual Studio 2019/2022,Qt 版本 5.15.2,Python 3 用来辅助处理脚本。这里最需要留意的是“Qt 编译工具链”和“VS 工具链”必须匹配:

  • Qt 安装时如果选了msvc2019_64,那你就得用 MSVC 2019 的编译器来编译第三方库。
  • 如果选了mingw73_64,那第三方库也得用 MinGW 编,VS 这边基本没法直接用。

我个人建议直接用 MSVC 套件,因为 VS 的调试器对 MSVC 编译的 Qt 库支持最好,pdb符号也能匹配上。在安装 Qt 组件时,选择对应 MSVC 版本,然后打开“x64 Native Tools Command Prompt for VS”,确认编译器版本和 Qt 路径后,剩下的构建步骤基本稳定。

2.2 源码目录与工程组织

先看 QtPropertyBrowser 源码的目录结构。它通常分为src和examples两部分,核心源码文件包括属性管理器、属性编辑器工厂、树形浏览器和表格浏览器:

  • qtpropertybrowser.h、qtpropertybrowser.cpp
  • qtvariantproperty.h、qtvariantproperty.cpp
  • qteditorfactory.h、qteditorfactory.cpp
  • qttreepropertybrowser.h
  • qtbuttonpropertybrowser.h
  • qtgroupboxpropertybrowser.h

编译第三方库时,别直接把源码丢到你的项目内部,否则一旦其他项目也想用这个库,代码就重复了。我建议建一个专门的third_party目录,然后把整个qtpropertybrowser源码副本放进去:

C:\work\MyProject\ ├── src\ ├── third_party\ │ └── qtpropertybrowser\ │ ├── src\ │ ├── docs\ │ └── CMakeLists.txt └── MyProject.sln

这样做的好处是,以后可以在 VS 里把 QtPropertyBrowser 作为独立项目加入解决方案,也可以单独用 CMake 构建,生成QtPropertyBrowser.dll放进统一输出目录。

2.3 选 CMake 而不是 qmake 的原因

老库原版给的是.pro文件。如果你开着 Qt 的 VS Tools 扩展,双击.pro也能生成 vcxproj,但生成结果往往带堆不兼容的问题,而且每次源码增删文件你还得重新跑一遍 qmake。CMake 则可以直接生成 VS 工程文件,也支持add_subdirectory,未来如果要切换 Qt 版本,只需要在 CMake 参数里改Qt5_DIR或Qt6_DIR路径,不用重写依赖配置。

注意:Qt 6 之后 QtPropertyBrowser 的兼容性更差,默认的模块划分发生了巨大变化,所以我这篇记录主要以 Qt 5.15 为例,但方法同样适用于 Qt 6,只需要相应修改 CMake 里的组件名。

3. QtPropertyBrowser 编译为 DLL 的关键改造点

3.1 先决定静态库还是动态库

如果你是自产自用,静态库最省事,直接把.lib链进主程序,运行时不需要额外分发 DLL。但这次需求是“生成 DLL”,一方面要让多个插件或者主程序同时复用,另一方面也是为了模拟真实发布环境中“共享 Qt 组件”的部署方式。

编 DLL 就必须处理导出符号问题。Windows 下 DLL 里的类、函数、全局变量,如果没有指定导出会默认隐藏,MSVC 在链接时会报警告C4251或者直接产生LNK2019。解决办法是提供一个统一的导出宏:

// qtpropertybrowser_export.h #ifndef QTPROPERTYBROWSER_EXPORT_H #define QTPROPERTYBROWSER_EXPORT_H #include <QtCore/qglobal.h> #if defined(QTPROPERTYBROWSER_STATIC_LIB) # define QTPROPERTYBROWSER_EXPORT #elif defined(QTPROPERTYBROWSER_LIBRARY) # define QTPROPERTYBROWSER_EXPORT Q_DECL_EXPORT #else # define QTPROPERTYBROWSER_EXPORT Q_DECL_IMPORT #endif #endif

然后在每个需要导出的类前面加上这个宏:

class QTPROPERTYBROWSER_EXPORT QtProperty { public: QtProperty(QObject *parent = nullptr); ... };

这里有一个容易被忽略的细节:Q_DECL_EXPORT其实展开后是__declspec(dllexport),Q_DECL_IMPORT展开后是__declspec(dllimport)。当你编译 QtPropertyBrowser 项目本身时,必须定义QTPROPERTYBROWSER_LIBRARY;当外部工程使用 DLL 时,不能定义这个宏,否则类会被当成导出对象,主程序里再链接一次就会出现重复符号。

3.2 改造 CMakeLists.txt

我新建的根 CMakeLists.txt 大约长这样:

cmake_minimum_required(VERSION 3.16) project(QtPropertyBrowser VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets) set(QTPROPERTYBROWSER_SOURCES src/qtpropertybrowser.cpp src/qtvariantproperty.cpp src/qteditorfactory.cpp src/qttreepropertybrowser.cpp src/qtbuttonpropertybrowser.cpp src/qtgroupboxpropertybrowser.cpp ) set(QTPROPERTYBROWSER_HEADERS src/qtpropertybrowser.h src/qtvariantproperty.h src/qteditorfactory.h src/qttreepropertybrowser.h src/qtbuttonpropertybrowser.h src/qtgroupboxpropertybrowser.h src/qtpropertybrowser_export.h ) add_library(QtPropertyBrowser SHARED ${QTPROPERTYBROWSER_SOURCES} ${QTPROPERTYBROWSER_HEADERS} ) target_include_directories(QtPropertyBrowser PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src> $<INSTALL_INTERFACE:include> ) target_link_libraries(QtPropertyBrowser PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets ) if(MSVC) target_compile_options(QtPropertyBrowser PRIVATE /W3) else() target_compile_options(QtPropertyBrowser PRIVATE -Wall -Wextra) endif()

编译时我会提示QtPropertyBrowser是 SHARED 库,所以在qtpropertybrowser_export.h里需要定义QTPROPERTYBROWSER_LIBRARY。我一般直接写在 CMake 的target_compile_definitions:

target_compile_definitions(QtPropertyBrowser PRIVATE QTPROPERTYBROWSER_LIBRARY)

PUBLIC和PRIVATE的含义也值得说清楚:QtPropertyBrowser自己编译时需要Qt5::Widgets,头文件不依赖 Widgets 之外的东西,所以链接是PRIVATE。但头文件路径必须让外部项目看到,所以include路径用PUBLIC。

3.3 头文件路径混乱的规避

QtPropertyBrowser 的老源码喜欢把src下所有头文件放在一层,有些#include用的是相对路径。比如qteditorfactory.cpp里可能会写:

#include "qtpropertybrowser.h"

如果外部项目只引用了src,那没问题。但你把它放进third_party后,可能会出现下面这种神操作:

#include "../qtpropertybrowser/src/qtpropertybrowser.h"

这个写法非常脆弱,换台机器目录一挪就得改。改成 CMake 的target_include_directories之后,外部工程只需要写:

#include <qtpropertybrowser.h>

就能找到正确头文件。如果你遇到某些.cpp在 include 时带了../前缀,建议顺手整理掉,否则跨平台到 Linux 的 make 环境时可能直接报文件不存在。

4. 在 VS 工程中集成 DLL 的完整流程

4.1 用环境变量统一管理依赖路径

VS 工程里配置第三方库最怕硬编码路径。换台电脑或者把代码仓库 clone 到别的目录,所有 Include、Library 路径就全乱了。我习惯新建一个用户环境变量QTPROPERTYBROWSER_DIR,指向这个库的根目录:

QTPROPERTYBROWSER_DIR=C:\work\MyProject\third_party\qtpropertybrowser

同时在 CMake 或者在 VS 属性页里引用:

  • C/C++ → 常规 → 附加包含目录:$(QTPROPERTYBROWSER_DIR)\src
  • 链接器 → 常规 → 附加库目录:$(QTPROPERTYBROWSER_DIR)\build\lib\Release
  • 链接器 → 输入 → 附加依赖项:QtPropertyBrowser.lib

如果你的库输出目录和cmake --build的默认输出不一致,一定要统一到一个固定目录,比如:

build\bin\Release\QtPropertyBrowser.dll build\lib\Release\QtPropertyBrowser.lib

因为 VS 的运行目录默认是$(OutDir),如果你把 DLL 放到别的地方,运行时会提示找不到QtPropertyBrowser.dll。

4.2 通过 CMake 的 add_subdirectory 集成更省心

如果你的主工程本身就是 CMake 工程,那就不需要手动建一个.vcxproj了。在根CMakeLists.txt里加一行:

add_subdirectory(third_party/qtpropertybrowser)

然后在自己的目标上链接:

target_link_libraries(MyProject PRIVATE QtPropertyBrowser)

CMake 会把QtPropertyBrowser.dll作为构建依赖,自动处理编译顺序,不需要你去改 VS 的项目依赖关系。这是我最推荐的用法,比手搓 vcxproj 省事得多,而且 CMake 在 Windows 和 Linux 下都能工作,后面如果要迁到 CI 上也容易。

4.3 一个最小可用的调用示例

集成成功之后,我写了一个属性面板 Demo,效果类似:右侧一个QtTreePropertyBrowser,当用户选中三维物体时,把位置、旋转、缩放这些属性挂上去。

#include <QtWidgets/QMainWindow> #include <QtWidgets/QApplication> #include <QtWidgets/QTreeWidget> #include <qtpropertybrowser.h> #include <qtvariantproperty.h> #include <qttreepropertybrowser.h> #include <qteditorfactory.h> class MainWindow : public QMainWindow { public: MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) { setMinimumSize(960, 640); m_browser = new QtTreePropertyBrowser(this); setCentralWidget(m_browser); m_manager = new QtVariantPropertyManager(m_browser); QtVariantEditorFactory *factory = new QtVariantEditorFactory(m_browser); m_browser->setFactoryForManager(m_manager, factory); QtVariantProperty *root = m_manager->addProperty(QVariant::String, "Transform"); root->addSubProperty(m_manager->addProperty(QVariant::Double, "Position X")); root->addSubProperty(m_manager->addProperty(QVariant::Double, "Position Y")); root->addSubProperty(m_manager->addProperty(QVariant::Double, "Position Z")); m_browser->addProperty(root); } private: QtTreePropertyBrowser *m_browser; QtVariantPropertyManager *m_manager; };

这段代码在链接时会用到QtVariantPropertyManager、QtTreePropertyBrowser等符号,如果没有导出宏,它就会变成LNK2019。编出 DLL 后,这个 Demo 可是非常直观的验证项目。

编译之后,我遇到过一个很隐蔽的问题:QtVariantPropertyManager返回的QtProperty*需要在内部注册QtVariantProperty类型。如果你在 Release 下链接,但是 Debug 下运行,Qt 的容器类型和动态类型系统会直接闪退,这就是最常见的 Debug/Release ABI 混用问题。

5. 常见问题排查与经验避坑

5.1 经典 LNK2019 / LNK2001 错误

症状是链接时报:

LNK2019: 无法解析的外部符号 "public: __cdecl QtVariantPropertyManager::QtVariantPropertyManager(class QObject *)"

一般原因有 4 种,我按出现频率排一下:

  • 编译 QtPropertyBrowser 时没有定义QTPROPERTYBROWSER_LIBRARY,导致所有类只是声明了Q_DECL_IMPORT,但你链接的是静态.lib,里面没有任何导出符号。
  • 主程序里把QTPROPERTYBROWSER_LIBRARY也定义了,这会导致类被__declspec(dllexport)包装,链接库时可能不匹配。
  • 你链接的库是 Release 版QtPropertyBrowser.lib,但主程序是 Debug 配置,或者反过来。Qt 对象模型对 Debug/Release 的 ABI 要求很严格。
  • 头文件版本和.lib文件版本不一致,比如头文件是新拉的代码,.lib 却是旧的编出来的。

排查技巧:用dumpbin /headers QtPropertyBrowser.dll看 DLL 的 Machine 类型,用dumpbin /exports QtPropertyBrowser.dll看有没有导出QtVariantPropertyManager相关符号。如果确实没导出,回 CMake 检查宏定义。

5.2 运行时提示找不到 DLL

这个问题在 VS 里最常见的表现是:能编译,但点击调试后弹窗“由于找不到 QtPropertyBrowser.dll,无法继续执行代码”。

解决思路:把 DLL 复制到主程序的输出目录,或者把 DLL 所在的目录加入PATH。但我不建议直接把C:\qt\5.15.2\msvc2019_64\bin写进系统PATH,这样容易干扰其他项目。正确做法是在 VS 调试会话环境里加路径:

  • 项目属性 → 调试 → 环境
  • 填入:PATH=$(QTPROPERTYBROWSER_DIR)\build\bin\Release;%PATH%

如果要打包给别人,还得带上 Qt 运行库。QtPropertyBrowser 通过 CMake 链接了Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll,发布时最少要用 windeployqt 或者手动把这些 DLL 放到可执行文件旁边:

windeployqt --release --no-translations MyProject.exe

windeployqt会自动检测用到的 Qt 模块,把对应的 DLL 拷过去。QtPropertyBrowser 自己是普通 DLL,把它和 Qt 的运行库一起放好就行。

5.3 编译警告当成错误的坑

老代码在 MSVC 下会出现一些无害警告,比如:

  • C4996: 'QDateTime::currentTime' was marked deprecated
  • C4251: 'QtPropertyBrowserPrivate::...' needs to have dll-interface

如果目标平台是 Windows,C4251通常不用太紧张,它只是说类成员包含 STL/Qt 类型,DLL 边界上的接口会暴露模板类型。只要调用方还在同一个 ABI 下,基本不会崩。但为了干净,我习惯把C4251设为警告级别 3 以下或者直接忽略。

在 Qt 5.15 下最典型的是QFontMetrics::width被废弃导致的C4996。原库的绘制代码可能还在用这些老 API,在 CMake 里我建议只在 MSVC 下增加:

target_compile_options(QtPropertyBrowser PRIVATE /wd4996 /wd4251)

不要全局禁用安全警告,那样会掩盖真正有问题的代码。

5.4 访问冲突和内存崩溃排查

这类问题绝大多数出现在 Qt 对象生命周期管理上。属性浏览器的 manager 和 editor factory 在析构时如果顺序不对,很容易崩溃。

之前我在一个插件工程里,把QtVariantPropertyManager和QtVariantEditorFactory直接new到堆上,但没有指定父对象,这样在主窗口销毁时它们不会自动释放,可能先释放 browser,再释放 manager,内部属性树已经空了,就会触发空指针访问。正确写法是给它们传父对象:

m_manager = new QtVariantPropertyManager(m_browser); QtVariantEditorFactory *factory = new QtVariantEditorFactory(m_browser);

另外如果你在主程序里使用QObject::connect连接QtPropertyBrowser的属性变更信号,需要确保连接在setValue之前建立,否则第一次初始化属性时会丢失信号。QtPropertyBrowser 的 bug 历史和这个问题很相关。

5.5 Release / Debug 配置混用导致的不稳定

这个问题无法靠改代码解决,只能从构建配置上卡死。我在 CI 脚本里直接约定:

  • Debug 构建必须用 Debug 的 Qt 库,链接 Debug 的 QtPropertyBrowser.lib。
  • Release 构建必须用 Release 的 Qt 库,链接 Release 的 QtPropertyBrowser.lib。
  • 禁止在 Release 配置里链接 Debug 库。

如果你不想维护两套 QtPropertyBrowser 构建,也可以用/MD和/MDd统一运行时,但 Qt 官方版本本身就是按/MD编的,所以主程序最好也匹配。CMake 在 MSVC 下默认会生成/MD或/MDd,只要不手动改成/MT就行。

5.6 编译时找不到 Qt 模块

如果你直接用 CMake 命令行构建,会遇到:

Could not find a package configuration file provided by "Qt5Widgets"

这时需要指定你的 Qt 安装路径:

cmake -S . -B build -DCMAKE_PREFIX_PATH=C:/Qt/5.15.2/msvc2019_64 cmake --build build --config Release

如果你在 VS 里写代码时用 Qt VS Tools 扩展,也要保证扩展设置的 Qt 版本和 CMake 指向的版本一致,否则 IDE 的 IntelliSense 可能认为找不到头文件。

6. 一个更顺手的 CMake 集成样例

最后放一个可以“抄作业”的完整 CMake 工程骨架,这是我实际用过的最小结构,省掉了很多反复配置。

项目根目录CMakeLists.txt:

cmake_minimum_required(VERSION 3.16) project(MyQtPluginApp LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets) add_subdirectory(third_party/qtpropertybrowser) add_executable(MyQtPluginApp src/main.cpp src/MainWindow.cpp src/MainWindow.h ) target_link_libraries(MyQtPluginApp PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets QtPropertyBrowser ) if(WIN32) set_target_properties(MyQtPluginApp PROPERTIES WIN32_EXECUTABLE ON RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) endif()

在src/MainWindow.cpp里直接 include<qtvariantproperty.h>和<qttreepropertybrowser.h>,然后编译。CMake 会因为add_subdirectory(third_party/qtpropertybrowser)自动把 DLL “液化”成一个构建目标,VS 在生成时会自动等待它先编译。

如果你不想每次都在命令行里敲-DCMAKE_PREFIX_PATH,可以在根 CMakeLists.txt 里加一个缓存变量:

set(CMAKE_PREFIX_PATH "C:/Qt/5.15.2/msvc2019_64" CACHE PATH "Qt 安装路径")

注意这个写法会让工程在别的电脑上失去灵活性,一般只适合固定开发机。

7. 最后的几个真实体会

这次移植 QtPropertyBrowser 到 VS 的时候,我试过直接改.pro文件然后用 qmake 生成工程,发现它生成的 vcxproj 文件虽然能编译,但每次新增源码都得重新生成,而且输出文件名和 Qt 依赖路径不够可控。后来转成 CMake,整个集成过程才稳定下来。

如果你也在做类似的第三方 Qt 组件移植,我的建议是:首先不要迷信“直接读原版 qmake 文件”,学会用 CMake 的SHARED库目标去重新描述依赖关系;其次导出宏一定要单独拆一个头文件,不要塞进qtpropertybrowser.h里让用户侧跟着乱;最后就是给 QtPropertyBrowser 单独创建一个构建目录,这样不同编译器、不同 Qt 版本可以并排产出不同的 DLL 和 lib,不至于互相覆盖。

还有一个小技巧:我把QtPropertyBrowser的输出目录设置为和主程序一致的bin目录,然后用 VS 的“生成事件”把Qt5Core.dll等 Qt 运行库复制到同一个目录。这样做调试时不需要修改PATH,发布时也只要把整个bin目录打包就行。后续迭代时,如果 QtPropertyBrowser 源码有更新,不用动主工程,只在third_party/qtpropertybrowser里拉代码然后重启构建就好。这套流程基本就是我在现有项目里持续使用的状态。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询