资讯动态

QtPropertyBrowser编译DLL并集成VS工程的踩坑全记录

发布时间:2026/10/15 23:30:58 来源:尧图企业网站定制
去年给一个桌面工具软件加属性面板时翻遍了Qt官方控件库也没找到现成的属性表最后选定了QtPropertyBrowser这个开源的属性浏览框架在Windows MSVC环境下折腾了差不多一整天把它编译成DLL并集成到了VS工程里。整个过程不算复杂但坑是真的多源码版本选不对、pro文件不会改、导出宏缺一堆、运行时还缺Qt的依赖DLL……这篇文章就把我这次移植的完整记录整理出来包括选型理由、源码获取、DLL改造、编译报错排查、VS工程调用和最终部署希望能让后面的人少走弯路。QtPropertyBrowser适合谁用很简单你正在用Qt开发桌面程序需要一个类似Visual Studio属性窗口、或者三维软件里选中物体后右侧那群参数面板不想手写一整套树形表格加编辑控件的逻辑那这个库就是为你准备的。它有树形、分组、按钮三种浏览器形态而且属性数据和编辑控件完全解耦后续要扩展新的属性类型也很方便。下面从为什么要选它开始讲。1. 这个库到底是干嘛的属性面板的场景与选型理由1.1 手工实现属性面板有多痛苦很多项目做到中期会突然冒出来一个需求界面上加一个属性面板用户选中某个对象后右侧要把这个对象的各种参数列出来并且能直接修改。比如一个图像处理软件里选中一张图层要能改透明度、混合模式、是否可见一个工控配置工具里选中一个设备要能填IP地址、波特率、超时时间。如果自己用QTreeWidget或者QTableView去做第一轮可能觉得还行无非就是每一行左边放属性名、右边放编辑控件嘛。但深入下去就会发现事情没这么简单。属性类型多种多样整数需要QSpinBox浮点可能要QDoubleSpinBox或者带单位的输入框布尔值是QCheckBox枚举要QComboBox颜色要弹QColorDialog文件路径要带浏览按钮……光把这些编辑控件塞进树里就是个麻烦事。更麻烦的是数据同步用户在编辑控件里改了值怎么回调给业务层业务层改了数据怎么刷新控件显示撤销重做怎么做不同属性之间还有联动。这些逻辑写下来没有两三天根本出不了稳定版本。1.2 QtPropertyBrowser的核心设计QtPropertyBrowser从一开始就是奔着解决属性面板这个通用需求去的。它把整个问题拆成了三层属性模型层、编辑工厂层、浏览器视图层。属性模型层QtPropertyManager维护属性树的结构和值每种数据类型由一个Manager来管理比如整型数值由QtIntPropertyManager处理枚举由QtEnumPropertyManager处理颜色由QtColorPropertyManager处理。编辑工厂层QtEditorFactory负责根据属性类型创建对应的QWidget编辑控件工厂只负责造控件不关心属性值怎么存。浏览器视图层QtTreePropertyBrowser / QtGroupBoxPropertyBrowser / QtButtonPropertyBrowser负责把属性树渲染出来并把用户的操作转发给工厂和Manager。这三层解耦之后好处是明显的你想让某类属性换个编辑方式只需要改写或替换对应的Editor Factory完全不碰数据层和视图层。这个设计思路放在今天看依然很优雅这也是为什么就算官方不再维护社区里依然有人在持续跟进它的原因。1.3 为什么坚持生成DLL而不是直接编译进工程我知道有人会说既然这个库已经有源码直接把全部cpp加进工程不就行了何必费劲生成DLL我这次坚持做DLL主要有三个考虑。第一这个库在我手头好几个项目里都会用如果每个工程都把几十个cpp拖进去维护成本太高遇到Qt版本升级还得逐个工程改打成DLL之后每次Qt升级只需要重编一次库工程侧引用方式完全不变。第二QtPropertyBrowser依赖了部分非公开的内部头文件直接散落在工程里容易造成头文件污染。第三项目中还有其他语言编写的模块需要以动态库方式调用这套属性面板能力DLL形态最方便对接。2. 源码选型与编译环境决定编译成败的第一个坎2.1 源码分支别乱选QtPropertyBrowser最原始的版本出自Qt官方示例目录但官方后来不再随Qt发布和更新了现在能找到的源码基本都来自社区维护的分支。这里有个很容易踩的坑不同分支的代码状态差别非常大有的还停留在Qt4/Qt5早期的状态连Qt5Widgets模块拆分都没跟上有的则是专门为MSVC DLL场景打过补丁的。我的建议是先不要急着下载重点看三个点是否带CMakeLists.txt或者能直接适配qmake的pro文件源码里是否已经有类似QT_PROPERTYBROWSER_EXPORT这样的导入导出宏定义是否把QtDesigner模块相关的代码独立开了便于裁剪。我这次选择的版本就是社区中一个活跃维护的分支自带CMakeLists.txt而且源码里的导出宏体系已经搭好了只是默认情况下没有启用DLL构建而已。如果你拿到的分支没有这些后面第三步的改造量会大不少甚至要自己写脚本给所有公开类加导出宏这个心理准备要有。2.2 Qt版本锁定与工具链组合编译环境我建议用Qt 5.15 LTS的MSVC版本而不是现在就冲Qt 6。原因很简单QtPropertyBrowser里定义了一个全局类叫QtProperty而Qt 6把QProperty作为标准属性系统类加入了官方库两者在头文件层面就会发生命名冲突。虽然可以通过套namespace的方式规避但改动面大不划算。除非你确定自己的项目必须上Qt 6否则用Qt 5.15编译这个库性价比最高。工具链方面路径可以是两种用VS自带的MSVC编译器 qmake NMake全程在Qt命令行里操作用VS IDE Qt VS Tools插件在IDE里直接用MSVC编译pro。我这次用的是方式一更直观出问题也好定位。前提是你已经安装了对应版本的Qt套件比如Qt 5.15.2 (MSVC 2019 64-bit)。注意千万不能把MinGW版本的Qt和MSVC版本的Qt搞混两者生成的二进制完全不兼容。2.3 启动Qt编译环境的正确姿势如果你只是打开了普通的VS开发人员命令提示符直接敲qmake多半会报qmake不是内部或外部命令。原因是qmake所在的目录不在PATH里。正确做法是从开始菜单里启动Qt自带的命令行快捷方式名字通常是Qt 5.15.2 (MSVC 2019 64-bit)它会自动设置好QTDIR、PATH等环境变量。如果你一定要在普通命令提示符里做也可以手动设置set QTDIRC:\Qt\5.15.2\msvc2019_64 set PATH%QTDIR%\bin;%PATH%这个环境变量问题看着不起眼但一旦设置错了后面qmake生成的Makefile指向的Qt库版本就会乱编出来的库运行时对你一脸嫌弃。3. pro文件的DLL化改造导出宏与模块依赖的种种细节3.1 从pro文件看这个库的构成我先用文本编辑器打开源码根目录下的pro文件这个文件决定了整个库的编译范围。典型的原始pro文件会把大量.cpp文件全部列进去其中包括一些和QtDesigner耦合很深的文件。例如qtdesignerpropertymanager.cpp它是用来把属性浏览器嵌入到Qt Designer里做实时属性编辑的普通应用完全用不上但它会引入QtDesigner模块的头文件编译时如果没有这个模块就会直接报错。我的做法是先尝试用qmake生成Makefile如果因为QtDesigner相关代码报错就把这个文件从pro文件的SOURCES列表里摘掉。摘掉之后QtDesigner模块的依赖自然就没了。3.2 把TEMPLATE改成lib并加入DLL导出宏原始pro文件的TEMPLATE通常不明确指定输出类型或者直接是app模板改过来的要生成DLL必须明确写成lib并且定义动态库构建标记。改造后的核心内容大致是这个样子QT core gui widgets TEMPLATE lib CONFIG dll DEFINES QT_PROPERTYBROWSER_NO_QT_DESIGNER DEFINES QT_PROPERTYBROWSER_BUILD CONFIG(debug, debug|release): TARGET QtPropertyBrowserd CONFIG(release, debug|release): TARGET QtPropertyBrowser HEADERS \ qtpropertybrowser.h \ qtvariantproperty.h \ qteditorfactory.h \ qttreepropertybrowser.h \ qtgroupboxpropertybrowser.h \ qtbuttonpropertybrowser.h SOURCES \ qtpropertybrowser.cpp \ qtvariantproperty.cpp \ qteditorfactory.cpp \ qttreepropertybrowser.cpp \ qtgroupboxpropertybrowser.cpp \ qtbuttonpropertybrowser.cpp \ ...这里有两个关键点要解释。第一DEFINES QT_PROPERTYBROWSER_BUILD的含义。如果你的源码分支里类声明上写了类似QT_PROPERTYBROWSER_EXPORT的宏那么它的定义逻辑通常是#ifdef QT_PROPERTYBROWSER_BUILD # define QT_PROPERTYBROWSER_EXPORT __declspec(dllexport) #else # define QT_PROPERTYBROWSER_EXPORT __declspec(dllimport) #endif所以编译DLL时必须定义QT_PROPERTYBROWSER_BUILD这样源码里的类才会加上__declspec(dllexport)。如果这个宏体系在你的分支里不存在那你就得手动给每个需要导出的类前加上导出标记比如class QT_PROPERTYBROWSER_EXPORT QtTreePropertyBrowser : public QtAbstractPropertyBrowser这个活儿如果只剩几个核心类还好如果是几十个类就要想办法批量处理别手工一个个改容易漏。第二TARGET分成debug和release两套分别加d后缀。Windows下MSVC的调试版运行库和发布版运行库不通用Qt官方库本身也区分Qt5Widgetsd.dll和Qt5Widgets.dll所以我们自己的DLL也最好严格区分免得后面链接的时候混用。3.3 要不要带上designer相关头文件我建议默认不带。这个库的大部分常用功能集中在几个核心类上qtdesignerpropertymanager只服务于Qt Designer插件场景。带上了Designer模块编译出来的DLL会依赖Qt5Designer.dll这会让部署链条变长而且Qt5Designer.dll在你安装Qt时也不一定默认带了pdb调试信息。所以从pro文件里把designer相关文件剔除是性价比最高的选择。4. 编译报错实录从qmake到nmake的完整踩坑链路4.1 第一步qmake生成Makefile阶段进入源码目录执行mkdir build cd build qmake ..\QtPropertyBrowser.pro -spec win32-msvc这一步一般不会报错但如果你用的Qt版本和源码版本差距过大可能出现类似Project ERROR: Unknown module(s) in QT: designer那说明pro文件里还挂着designer模块回去把QT designer那行删掉即可。顺利的话会生成Makefile但真正的问题在nmake阶段接踵而来。4.2 第二步nmake编译阶段的海量报错我用nmake编译时遇到的第一个实质性问题长这样qtpropertybrowser.cpp(987): error C2065: register: undeclared identifier这个报错很有意思。register是C早期版本里的存储类关键字C17里已经被移除了而MSVC 2022默认编译标准是C14后来又默认C17如果编译器按C17处理就会因为register不是合法标识符而报错。老代码里经常会有这种古董语法残留。解决办法是去源码里把所有register int改成int或者直接在pro文件里加QMAKE_CXXFLAGS /std:c14不过更干净的方式还是直接把register去掉因为你不知道后面引用这个DLL的工程会用什么标准。第二个报错是头文件路径问题fatal error C1083: Cannot open include file: QtDesigner/QDesignerFormWindowInterface.h: No such file or directory这个就是我前面说的designer模块依赖。处理方法是把pro文件里qtdesignerpropertymanager.cpp从SOURCES和HEADERS里拿掉同时确认所有头文件的INCLUDEPATH设置正确。一般把源码的src目录加进INCLUDEPATH就行。第三类问题是Qt 5版本下QRegExp相关接口过期的警告运气好的话只是警告不影响编译。但如果你选的分支实在太老在Qt 5.15下可能直接编译失败比如正则表达式相关接口整个被挪走了。遇到这种情况别犹豫换分支。4.3 第三步DLL导出失败的经典链接错误编译通过、但nmake链接时蹦出LNK2019这个是整个过程中最容易让人懵的QtPropertyBrowserd.lib(QtTreePropertyBrowser.obj) : error LNK2019: unresolved external symbol public: virtual struct QMetaObject const * __cdecl QtTreePropertyBrowser::metaObject(void)这个错误的本质是类的Q_OBJECT元对象代码没有导出导致链接器在DLL边界外找不到metaObject符号。它通常发生在你只是有条件地把部分类加了导出宏、但漏了某个类或者Q_OBJECT类头文件没有完整包含在HEADERS列表里、导致moc没有为它生成元对象代码。排查思路是这样的先在生成的头文件列表里确认QtTreePropertyBrowser.h确实在pro文件的HEADERS里再检查这个类声明前的导出宏是否生效。如果还是不行可以用dumpbin查看生成的DLL导出了哪些符号dumpbin /exports QtPropertyBrowser.dll如果输出的导出表里全是C的std相关符号一个Qt类的metaObject都没有那说明导出宏定义没生效回到pro文件检查DEFINES那行到底写没写对。4.4 第四步编译通过但运行时报缺平台插件库本身编好之后我当时很兴奋地写了个测试程序去调用结果一运行就弹出This application failed to start because no Qt platform plugin could be initialized.这个报错其实和DLL本身没关系而是链接这个DLL的可执行程序没有把Qt的平台插件目录带上。Qt程序启动时需要一个platforms目录里面放着qwindows.dll如果这个目录没放在exe同级的platforms文件夹下Qt就会罢工。这个问题在部署章节再细说我当时是先在Qt的bin目录下找到了platforms文件夹复制到exe目录里临时解决的。5. 输出产物整理DLL、头文件与依赖库的目录规划5.1 应该保留哪些头文件编译成功后下一步是整理产物。QtPropertyBrowser的源码目录里有很多内部实现细节的头文件比如各种PropertyManager的私有实现头文件它们是给.cpp文件用的外部调用者完全不需要。真正要对外暴露的头文件其实只有十几个大致包括qtpropertybrowser.hqtvariantproperty.hqteditorfactory.hqttreepropertybrowser.hqtgroupboxpropertybrowser.hqtbuttonpropertybrowser.hqtvariantpropertymanager.h如果你拿不准有个方法看pro文件的HEADERS列表凡是名字和上面这些公开类没有直接对应关系的大概率是内部头文件不对外发布。5.2 目录结构建议我最终的项目目录是这样规划的third_party/ QtPropertyBrowser/ include/ # 对外头文件 lib/ Debug/ # QtPropertyBrowserd.lib QtPropertyBrowserd.dll Release/ # QtPropertyBrowser.lib QtPropertyBrowser.dll这样做的目的是让VS工程里的附加依赖目录能按Debug和Release配置分别设置避免把两个版本的lib混在一起。很多人图省事把Debug和Release的lib放同一个目录结果链接器优先找到Release的libDebug程序链接后运行时各种莫名崩溃排查起来非常痛苦。5.3 怎么确认这个DLL依赖了哪些Qt库有个很管用的工具叫Dependency Walker但它在较新系统上对64位程序的解析不太友好。我这边更推荐用Qt自带的dumpbin或者直接在运行目录里观察缺失的DLL提示。更科学的方法是看生成的Makefile里链接了哪些Qt库。一般来说如果你的DLL只用到了核心的widgets属性编辑能力那么它的依赖就是固定的几个Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll。如果你发现运行时还要求Qt5Designer.dll那八成是我前面说的designer相关代码没有剔干净。6. 在VS工程里调用DLL的落地步骤与常见坑6.1 VS工程属性配置现在进入最后一步在VS项目里正式引用这个DLL。如果你的VS工程是用Qt VS Tools建立的Qt Widgets Application那么Qt本身的模块依赖已经配置好了不需要额外处理QTDIR路径。你要做的只是把QtPropertyBrowser加进工程引用体系。打开工程属性按以下路径配置C/C - 常规 - 附加包含目录 进入 third_party\QtPropertyBrowser\include 链接器 - 常规 - 附加库目录 按配置填 third_party\QtPropertyBrowser\lib\Release 或 \Debug 链接器 - 输入 - 附加依赖项 QtPropertyBrowser.libDebug 对应 QtPropertyBrowserd.lib配置完成后在代码里直接包含头文件#include qtpropertybrowser.h #include qtvariantproperty.h #include qteditorfactory.h注意对外头文件里已经有了dllimport相关的宏分支所以只要编译DLL时定义了QT_PROPERTYBROWSER_BUILD使用方就不用再做任何额外处理编译器会自动按__declspec(dllimport)解析这些类。6.2 一个最小可运行的属性面板示例下面这个例子就是我最常用的最小验证代码创建一个树形属性浏览器里面放一个布尔开关、一个整数范围、一个枚举、一个颜色值#include QtWidgets/QApplication #include QtWidgets/QMainWindow #include qtpropertybrowser.h #include qtvariantproperty.h #include qteditorfactory.h int main(int argc, char *argv[]) { QApplication app(argc, argv); QtVariantPropertyManager* variantManager new QtVariantPropertyManager; QtVariantEditorFactory* variantFactory new QtVariantEditorFactory; QtTreePropertyBrowser* tree new QtTreePropertyBrowser; tree-setFactoryForManager(variantManager, variantFactory); QtVariantProperty* rootItem variantManager-addProperty( QtVariantPropertyManager::groupTypeId(), 显示设置); QtVariantProperty* enabledItem variantManager-addProperty(QVariant::Bool, 启用); enabledItem-setValue(true); rootItem-addSubProperty(enabledItem); QtVariantProperty* fontSize variantManager-addProperty(QVariant::Int, 字体大小); fontSize-setAttribute(minimum, 9); fontSize-setAttribute(maximum, 72); fontSize-setValue(12); rootItem-addSubProperty(fontSize); QtVariantProperty* modeItem variantManager-addProperty( QtVariantPropertyManager::enumTypeId(), 显示模式); QStringList enumNames; enumNames 正常 叠加 差值; modeItem-setAttribute(enumNames, enumNames); modeItem-setValue(1); rootItem-addSubProperty(modeItem); QtVariantProperty* colorItem variantManager-addProperty(QVariant::Color, 背景颜色); colorItem-setValue(QColor(Qt::white)); rootItem-addSubProperty(colorItem); tree-addProperty(rootItem); tree-show(); QObject::connect(variantManager, QtVariantPropertyManager::valueChanged, [](QtProperty* property, const QVariant value) { qDebug(%s %s, qPrintable(property-propertyName()), qPrintable(value.toString())); }); return app.exec(); }这段代码运行起来后你就能看到一个比较像样的属性面板了。修改属性值后valueChanged信号会带着property和value回调出来这个信号就是业务层与属性面板对接的主要入口。6.3 运行时三个高频坑第一个坑是Debug和Release混用。这个问题我在第5章提过但实战中还是经常看到有人把Debug的程序链Release的lib。Windows的MSVC运行时库在Debug版和Release版之间是不能混用的因为内存分配和基础容器的实现都不同。一旦混用轻则界面显示异常重则启动时直接崩溃。务必严格区分。第二个坑是Qt库DLL没带全。如果你的程序是纯VS原生工程没有用Qt VS Tools那你除了要把自己的QtPropertyBrowser.dll放在exe目录下还得把Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll一并放进去另外exe目录下还必须有一个platforms文件夹里面放qwindows.dll。否则程序会报无法加载平台插件。这个部署套路可以用Qt官方提供的windeployqt工具自动完成它会把所有需要的依赖都拷贝到exe目录下非常省事。第三个坑是DLL搜索路径。Windows下加载DLL的默认搜索顺序是exe所在目录、系统目录、PATH中的目录。如果工程运行时设置的“工作目录”不是exe所在目录且项目里又通过相对路径加载资源就可能导致DLL找不到。最稳妥的办法永远是把所有非系统DLL统一放到exe所在目录。如果你有洁癖想放到别的子目录那就必须显式调用SetDllDirectory或者QCoreApplication::addLibraryPath否则迟早会被这个问题坑一把。6.4 进阶自定义属性类型的编辑控件QtPropertyBrowser真正厉害的地方在于它的编辑工厂几乎可以无限扩展。比如默认情况下浮点数是用QDoubleSpinBox编辑的如果你想改成滑块加数值输入的组合只需要继承QtAbstractEditorFactory重写connectToManager和createEditor即可然后在浏览器上setFactoryForManager把自定义工厂替换掉就行。它的数据层和编辑层完全解耦意味着你可以为某一种自定义数据类型定制一套编辑控件比如三维向量、矩阵、文件路径列表这些。这个扩展机制是我当初决定选这个库而不是手写控件的重要原因因为业务场景里总会有那么几个独特的属性类型。7. 移植完成之后还有几个值得注意的收尾细节整套移植流程走完我最深的体会是这种老库移植到Windows VS平台核心难点从来不在代码本身而在构建链路。源码选对分支、pro文件把模块依赖和导出宏理顺、Debug和Release产物严格分离这三件事做好了后面就是水到渠成。最后再分享一个小技巧。官方原始pro文件默认不区分debug和release的库名建议一定像我第3章那样加上d后缀逻辑。否则你辛辛苦苦编译了半年某一天突然发现Debug工程里错误链接上了Release库排查问题的时间会远远超过编译库的时间。另外如果以后用到Qt 6建议先确认你用的分支是否已经处理了与QProperty的命名冲突我目前给新项目接Qt 6时会在代码头文件顶部直接添加宏定义来重命名冲突标识符实测可以跑通但需要额外测试信号槽连接这块不能直接拍胸脯保证没问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑