1. 项目背景与核心需求拆解
1.1 为什么会有“带界面程序封装成DLL”这种需求
在Windows桌面开发里,Qt一直是个绕不开的选择。用Qt写个带界面的工具、配置面板、调试助手,开发效率高,跨平台也方便。但实际项目里经常遇到一种尴尬局面:主程序可能是用MFC、C#、Delphi甚至纯Win32写的,而某个功能模块却想用Qt来快速实现,尤其是那种带复杂交互界面的模块。这时候最直接的办法,就是把Qt写的界面程序封装成一个DLL,让主程序像调用普通动态库一样调用它。
这个需求背后通常有几类典型场景。第一类是遗留系统改造,主框架是十几年前的老代码,重写成本太高,但某个新功能需要现代化界面,于是单独用Qt做一个模块,编译成DLL嵌入。第二类是插件化架构,主程序定义好接口,各个功能模块以DLL形式动态加载,Qt界面模块只是其中一种插件。第三类是团队协作分工,界面组用Qt,核心业务组用其他技术栈,两边通过DLL边界解耦。
但这里有个关键问题:普通的Qt DLL(比如只包含算法、数据处理的动态库)封装起来很简单,导出几个函数就行。可一旦这个DLL里包含QWidget、QDialog这类界面元素,事情就变得复杂了。因为Qt的界面系统依赖QApplication实例、事件循环、平台插件等一系列运行时环境,不是简单导出个函数就能跑起来的。
1.2 核心难点到底在哪里
把带界面的Qt程序封装成DLL,难点集中在几个方面。首先是QApplication的归属问题。Qt的GUI程序必须有一个QApplication实例,而且整个进程只能有一个。如果主程序不是Qt写的,它没有QApplication,那DLL里就得自己创建。但如果主程序也是Qt写的,它已经有了QApplication,DLL里再创建就会出问题。这个判断逻辑必须处理好。
其次是事件循环的嵌套。主程序有自己的消息循环(比如Win32的GetMessage循环,或者MFC的消息泵),Qt的界面要正常响应,就需要Qt的事件循环也能运转。如果直接在DLL里调用QDialog::exec(),它会启动一个模态事件循环,这在某些主程序环境下会卡死或者行为异常。
第三是平台插件的路径问题。Qt的界面依赖qwindows.dll这个平台插件,DLL加载时如果找不到这个插件,界面就创建不出来,程序直接崩溃。而DLL的加载路径和EXE的加载路径往往不一致,这就导致插件搜索失败。
第四是资源释放与生命周期管理。DLL里的Qt对象什么时候创建、什么时候销毁,和主程序的生命周期怎么协调,稍不注意就是内存泄漏或者野指针崩溃。
1.3 本文的适用读者与预期目标
这篇内容适合几类人看。如果你正在做Qt开发,需要把某个界面模块交给其他技术栈的主程序调用,那这篇就是为你写的。如果你的主程序是Qt的,但想把某个界面做成插件动态加载,这里面的思路同样适用。即使你只是好奇Qt的DLL封装机制,想了解背后的原理,也能从中学到不少东西。
目标很明确:读完你能自己动手把一个带界面的Qt程序封装成DLL,知道每一步为什么这么做,遇到问题知道往哪个方向排查。我会尽量把原理讲透,同时给出可直接参考的代码结构和配置方法。
2. 整体方案设计与技术选型考量
2.1 两种主流封装思路的对比
把带界面的Qt程序封装成DLL,业内主要有两种思路。第一种是导出C风格接口,DLL只暴露几个简单的C函数,比如ShowDialog()、HideDialog(),内部自己管理QApplication和窗口对象。第二种是导出Qt风格接口,DLL导出的是Qt类或者Qt风格的函数,调用方也需要是Qt程序,双方共享QApplication。
这两种思路的适用场景完全不同。C风格接口的好处是调用方不需要是Qt程序,任何能加载DLL的语言都能用,C#通过P/Invoke、Python通过ctypes、Delphi通过外部函数声明,都能调。缺点是接口设计受限,不能直接传递Qt对象,复杂交互需要额外设计回调或者消息机制。Qt风格接口的好处是双方都是Qt,可以直接传递QWidget指针、信号槽连接,开发效率高。缺点是调用方必须也是Qt程序,而且QApplication的归属需要仔细处理。
实际项目里怎么选?如果主程序不是Qt写的,那基本只能走C风格接口。如果主程序是Qt写的,那Qt风格接口更自然。但即使是Qt主程序,我也倾向于至少把接口做成C风格的边界,内部再用Qt实现,这样耦合度更低,后续替换实现更容易。
2.2 为什么选择“显式加载”而非“隐式加载”
DLL的加载方式分隐式加载和显式加载。隐式加载是程序启动时由操作系统自动加载,需要在链接时指定.lib文件。显式加载是程序运行时用LoadLibrary主动加载,用GetProcAddress获取函数地址。
对于带界面的Qt DLL,我强烈建议用显式加载。原因有几个。第一,隐式加载时,如果DLL依赖的Qt库找不到,程序直接启动失败,连错误提示都没有,排查起来很痛苦。显式加载可以控制加载时机,加载失败时能给出明确的错误信息。第二,显式加载可以在不需要的时候卸载DLL,释放资源。第三,显式加载避免了DLL和EXE之间的Qt版本冲突问题,因为加载时机可控,可以先设置好环境再加载。
隐式加载唯一的优势是调用方便,不需要手动获取函数指针。但这个便利性在界面DLL场景下不值得,因为界面DLL的加载本来就不是高频操作,多写几行LoadLibrary的代码换来更好的可控性,完全值得。
2.3 QApplication归属问题的处理策略
这是整个方案里最核心的设计决策。QApplication实例在整个进程里必须唯一,所以DLL里不能无脑创建。我的处理策略是:DLL内部维护一个标志位,记录QApplication是否由本DLL创建。
具体逻辑是这样的。DLL初始化时,先检查QApplication::instance()是否为空。如果为空,说明当前进程还没有QApplication,那DLL就自己创建一个,并把标志位设为true。如果不为空,说明主程序已经创建了QApplication(主程序是Qt程序的情况),DLL就直接复用,标志位设为false。
DLL卸载或者界面关闭时,如果标志位是true,说明QApplication是DLL创建的,需要负责销毁。如果是false,则不能销毁,因为那是主程序的。
这个逻辑看起来简单,但实际写的时候有个坑:QApplication的创建必须在任何QWidget创建之前,而且创建QApplication时需要传入argc和argv。如果DLL是被主程序动态加载的,主程序的argc和argv可能已经不可用了,这时候需要自己构造一对假的参数传进去。
2.4 事件循环的嵌套与协调
主程序的消息循环和Qt的事件循环怎么协调,这是另一个关键设计点。如果主程序是Win32或者MFC,它有自己的消息泵,Qt的界面要响应,就需要Qt的事件循环也能被驱动。
最稳妥的做法是:DLL导出的显示界面函数,内部使用QDialog::exec()或者QEventLoop启动一个模态事件循环。这个事件循环会接管消息处理,直到界面关闭才返回。这样主程序在调用这个函数时,会阻塞在调用处,直到用户关闭界面。这种模式适合“弹出配置窗口”这类场景。
但如果需要非模态界面,比如主程序继续运行,DLL的界面作为一个浮动面板存在,那就不能阻塞。这时候需要把Qt的事件处理集成到主程序的消息循环里。一种做法是导出一个ProcessEvents()函数,主程序在消息循环里定期调用,驱动Qt的事件处理。另一种做法是使用QAbstractEventDispatcher,但那个更复杂,一般项目用不上。
我个人的经验是,大部分场景下模态对话框就够了。非模态的需求往往可以通过模态加回调的方式变通实现,比如界面关闭时通过回调通知主程序结果。这样DLL的实现简单,主程序的集成也简单。
3. 核心细节解析与实操要点
3.1 项目文件结构与关键配置
先看一个典型的项目结构。假设我们有一个Qt Widgets Application,主界面是MainDialog,现在要把它封装成DLL。项目文件需要做几处关键修改。
在.pro文件里,第一件事是把TEMPLATE = app改成TEMPLATE = lib,这告诉qmake我们要生成的是库而不是可执行文件。然后加上CONFIG += dll,明确生成动态库。如果不需要生成.lib导入库文件,可以加CONFIG += plugin,但一般我们还是需要.lib的,所以不加这个。
TEMPLATE = lib CONFIG += dll TARGET = WidgetModule接下来是导出宏的定义。Windows下DLL导出函数需要显式声明,通常用__declspec(dllexport)。但为了跨平台和代码整洁,一般定义一个宏:
#ifdef WIDGETMODULE_EXPORTS #define WIDGETMODULE_API __declspec(dllexport) #else #define WIDGETMODULE_API __declspec(dllimport) #endif然后在.pro里加上DEFINES += WIDGETMODULE_EXPORTS,这样编译DLL时导出,调用方包含头文件时导入。
还有一个容易忽略的点:如果DLL里用了Qt的元对象系统(信号槽、Q_OBJECT宏),需要确保moc正确处理。TEMPLATE = lib时qmake默认会处理moc,但如果你有自定义的信号槽在导出类里,要确认头文件被正确扫描。
3.2 导出接口的设计与实现
接口设计我建议遵循“最小暴露”原则。DLL外部只看到几个简单的C函数,所有Qt相关的类型都藏在内部。这样调用方不需要包含任何Qt头文件,也不需要链接Qt库。
一个典型的接口头文件大概长这样:
// WidgetModule.h #pragma once #ifdef WIDGETMODULE_EXPORTS #define WIDGETMODULE_API __declspec(dllexport) #else #define WIDGETMODULE_API __declspec(dllimport) #endif extern "C" { WIDGETMODULE_API int InitModule(); WIDGETMODULE_API int ShowMainDialog(void* parentHandle); WIDGETMODULE_API void CleanupModule(); }三个函数分别对应初始化、显示界面、清理资源。parentHandle是主程序窗口的句柄,传进来可以让Qt对话框以主程序窗口为父窗口,这样窗口层级和模态行为更正常。
实现文件里,InitModule负责创建QApplication(如果需要),ShowMainDialog负责创建并显示对话框,CleanupModule负责销毁QApplication和清理资源。
这里有个细节:ShowMainDialog的参数用void*而不是HWND,是为了避免在头文件里引入Windows头文件,保持接口的跨平台性。内部实现时再转换成HWND。
3.3 QApplication创建时机的精确控制
QApplication的创建时机很关键。不能在DLL加载时就创建,因为那时候不确定主程序是否已经有QApplication。也不能在每次显示界面时都检查,因为QApplication只能创建一次。
我的做法是在InitModule里做判断:
static QApplication* g_app = nullptr; static bool g_ownApp = false; int InitModule() { if (QApplication::instance() == nullptr) { static int argc = 1; static char* argv[] = { (char*)"WidgetModule", nullptr }; g_app = new QApplication(argc, argv); g_ownApp = true; } return 0; }注意这里用了static的argc和argv,因为QApplication会保存这两个参数的引用,如果传局部变量,函数返回后就成了悬空指针。这个坑我踩过,程序会在某些情况下莫名其妙崩溃。
还有一个细节:如果主程序是Qt程序,它可能已经创建了QApplication或者QCoreApplication。如果创建的是QCoreApplication(没有GUI),那DLL里再创建QApplication会出问题。这种情况下需要主程序改成QApplication,或者DLL里用QApplication::instance()判断后决定是否能用GUI。实际项目中,如果主程序是控制台程序但想弹Qt界面,主程序必须创建QApplication而不是QCoreApplication。
3.4 平台插件路径的配置方法
Qt界面程序运行时需要加载平台插件,Windows下就是qwindows.dll,放在plugins/platforms/目录下。DLL加载时,Qt会按照一定的搜索路径去找这个插件。如果找不到,程序启动就崩溃,错误信息通常是“This application failed to start because no Qt platform plugin could be initialized”。
搜索路径包括:Qt安装目录、应用程序目录、QT_PLUGIN_PATH环境变量指定的路径等。对于DLL来说,“应用程序目录”是主程序EXE所在的目录,不是DLL所在的目录。所以如果DLL和主程序不在同一个目录,插件就可能找不到。
解决办法有两种。第一种是在DLL初始化时,用QCoreApplication::addLibraryPath()手动添加插件路径:
QString dllPath = getDllDirectory(); QCoreApplication::addLibraryPath(dllPath + "/plugins");第二种是设置环境变量QT_PLUGIN_PATH,但环境变量是进程级的,可能影响主程序的其他部分,不太推荐。
我一般用第一种,在InitModule里根据DLL自身路径计算出插件目录,然后添加。获取DLL自身路径可以用GetModuleHandleEx配合GetModuleFileName,或者用Qt的QLibrary来获取。
注意:
addLibraryPath必须在QApplication创建之前调用,否则不生效。这个顺序不能错。
4. 完整实操流程与核心环节实现
4.1 从零搭建一个可封装的Qt界面模块
假设我们要做一个“设备配置面板”,主界面是一个QDialog,上面有几个输入框、下拉框和确定取消按钮。这个模块要封装成DLL给一个C#的主程序调用。
第一步,创建Qt Widgets Application项目,设计好界面。这里不展开界面设计细节,重点说封装相关的部分。界面类叫ConfigDialog,继承自QDialog,有Q_OBJECT宏,有自定义信号configChanged()。
第二步,修改.pro文件,改成lib模板,加上导出宏定义。同时要注意,如果DLL里用了Qt的静态资源(比如qrc里的图片),需要加上CONFIG += resources,确保资源被编译进DLL。
第三步,创建接口头文件和实现文件。接口头文件就是前面说的三个C函数。实现文件里包含ConfigDialog的头文件,实现三个函数。
第四步,处理ConfigDialog的创建和销毁。在ShowMainDialog里,用new创建对话框,设置Qt::WA_DeleteOnClose属性,这样关闭时自动销毁。然后用exec()显示模态对话框。
int ShowMainDialog(void* parentHandle) { HWND hwnd = static_cast<HWND>(parentHandle); ConfigDialog* dlg = new ConfigDialog(); dlg->setAttribute(Qt::WA_DeleteOnClose); if (hwnd) { dlg->setWindowModality(Qt::WindowModal); } int result = dlg->exec(); return result; }这里有个细节:setWindowModality(Qt::WindowModal)会让对话框只阻塞父窗口,而不是整个应用程序。如果主程序是多窗口的,这个设置更合适。如果希望阻塞整个程序,用Qt::ApplicationModal。
4.2 编译配置与依赖库处理
编译DLL时,需要链接Qt的库。在.pro里,QT += widgets会自动处理。但要注意,生成的DLL会依赖Qt5Widgets.dll、Qt5Core.dll、Qt5Gui.dll这些。发布时这些库要跟DLL放在一起,或者放在系统能找到的路径。
如果希望DLL尽量独立,可以考虑静态链接Qt。静态链接Qt需要自己编译Qt静态库,过程比较繁琐,而且静态链接Qt的DLL体积会很大(几十MB)。一般项目里,动态链接Qt更常见,发布时把需要的Qt DLL和插件一起打包就行。
发布时的目录结构大概是这样:
主程序目录/ ├── MainApp.exe ├── WidgetModule.dll ├── Qt5Core.dll ├── Qt5Gui.dll ├── Qt5Widgets.dll └── plugins/ └── platforms/ └── qwindows.dllWidgetModule.dll里通过addLibraryPath把plugins目录加进去。主程序不需要知道Qt的存在,只需要加载WidgetModule.dll并调用导出的函数。
4.3 主程序侧的调用示例
如果主程序是C#,调用方式大概是这样:
[DllImport("WidgetModule.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int InitModule(); [DllImport("WidgetModule.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int ShowMainDialog(IntPtr parentHandle); [DllImport("WidgetModule.dll", CallingConvention = CallingConvention.Cdecl)] public static extern void CleanupModule(); // 调用 InitModule(); ShowMainDialog(this.Handle); CleanupModule();注意调用约定要匹配。Qt默认是C++调用约定,但导出的是extern "C"函数,所以用CallingConvention.Cdecl。如果编译时用了__stdcall,这里要改成StdCall。
如果主程序是C++但不是Qt,用LoadLibrary和GetProcAddress:
typedef int (*InitModuleFunc)(); typedef int (*ShowMainDialogFunc)(void*); typedef void (*CleanupModuleFunc)(); HMODULE hDll = LoadLibrary(L"WidgetModule.dll"); if (hDll) { InitModuleFunc init = (InitModuleFunc)GetProcAddress(hDll, "InitModule"); ShowMainDialogFunc show = (ShowMainDialogFunc)GetProcAddress(hDll, "ShowMainDialog"); CleanupModuleFunc cleanup = (CleanupModuleFunc)GetProcAddress(hDll, "CleanupModule"); if (init && show && cleanup) { init(); show(hWnd); cleanup(); } FreeLibrary(hDll); }这里有个经验:GetProcAddress获取的函数名,如果DLL是32位的,导出名就是InitModule;如果是64位的,也是InitModule。但如果用了.def文件或者某些编译器选项,可能会变成_InitModule@0这种修饰名。用extern "C"可以避免C++名称修饰,但__stdcall在32位下还是会修饰。所以要么统一用__cdecl,要么在.def文件里显式指定导出名。
4.4 资源释放与生命周期管理
DLL里的资源释放要特别小心。CleanupModule里要做几件事:销毁所有Qt窗口对象、销毁QApplication(如果是由DLL创建的)、清理其他全局资源。
void CleanupModule() { if (g_ownApp && g_app) { delete g_app; g_app = nullptr; g_ownApp = false; } }但这里有个顺序问题:如果还有Qt窗口没关闭,直接delete QApplication会导致崩溃。所以CleanupModule应该在所有窗口都关闭之后调用。实际项目中,主程序可能在退出时才调用CleanupModule,那时候窗口早就关了,没问题。但如果主程序想中途卸载DLL,就必须确保先关闭所有DLL创建的窗口。
另一个坑是:如果DLL里用了QThread或者定时器,这些对象在QApplication销毁前必须停止。否则QApplication销毁后,线程还在跑,访问已销毁的对象就会崩溃。我的做法是在CleanupModule里先发一个全局信号,通知所有后台任务停止,等它们都退出了再销毁QApplication。
提示:可以在DLL里维护一个窗口列表,
CleanupModule时遍历列表关闭所有窗口,再销毁QApplication。这样更稳妥。
5. 常见问题与排查技巧实录
5.1 界面弹不出来或者程序直接崩溃
这是最常见的问题,表现是调用ShowMainDialog后程序无响应或者直接退出。原因通常有几个。
第一个原因是平台插件没找到。排查方法是设置环境变量QT_DEBUG_PLUGINS=1,然后运行程序,Qt会输出插件加载的详细日志。如果看到“Failed to load platform plugin”之类的信息,就是插件路径问题。解决办法就是前面说的addLibraryPath。
第二个原因是QApplication创建失败。如果主程序已经有一个QCoreApplication,DLL里再创建QApplication会失败。排查方法是检查QApplication::instance()返回的对象类型。如果主程序是Qt程序,确保它创建的是QApplication而不是QCoreApplication。
第三个原因是线程问题。Qt的GUI必须在主线程创建和操作。如果DLL是在子线程里被加载和调用的,创建QApplication和QWidget都会失败。排查方法是检查调用InitModule和ShowMainDialog的线程ID是否和主线程一致。
5.2 界面显示但无法交互
界面能弹出来,但按钮点不动,输入框不能输入。这通常是事件循环的问题。
如果用的是exec()模态显示,事件循环应该没问题。但如果主程序有自己的消息循环,而且这个循环不处理Qt的事件,就可能出现界面卡死。排查方法是看主程序的消息循环是否调用了TranslateMessage和DispatchMessage。如果主程序用的是PeekMessage而不是GetMessage,要确保在空闲时调用QApplication::processEvents()。
另一种可能是模态设置问题。如果对话框设置了Qt::ApplicationModal,但主程序的消息循环不标准,模态行为可能异常。试试改成Qt::WindowModal或者不设置模态,用非模态方式显示。
5.3 内存泄漏与重复加载问题
DLL反复加载卸载,或者界面反复打开关闭,可能出现内存泄漏。排查工具可以用Visual Studio的调试器,或者Qt自带的QML Profiler(虽然名字叫QML,但也能看C++对象)。
常见泄漏点包括:new了QWidget但没有设置WA_DeleteOnClose,关闭后对象没销毁;QApplication重复创建(虽然Qt会阻止,但逻辑上如果判断失误可能尝试创建多次);信号槽连接没有断开,导致对象无法释放。
我的经验是,所有DLL里创建的QWidget都设置WA_DeleteOnClose,所有new出来的非QObject派生类都用智能指针管理,信号槽连接尽量用Qt::UniqueConnection避免重复连接。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 程序启动崩溃,无提示 | 平台插件缺失 | 设置QT_DEBUG_PLUGINS=1看日志 | addLibraryPath添加插件路径 |
| 界面弹不出,函数直接返回 | QApplication创建失败 | 检查QApplication::instance() | 确保主程序用QApplication |
| 界面卡死无响应 | 事件循环未驱动 | 检查主程序消息循环 | 用exec()模态或定期processEvents |
| 关闭界面后内存不释放 | 未设置自动删除 | 用调试器看对象计数 | 设置WA_DeleteOnClose |
| 重复加载DLL崩溃 | QApplication重复创建 | 检查g_ownApp标志 | 确保QApplication只创建一次 |
| 界面样式异常 | 样式表或资源未加载 | 检查qrc资源是否编译进DLL | CONFIG += resources |
| 中文显示乱码 | 编码设置问题 | 检查源文件编码和QTextCodec | 统一用UTF-8,设置QApplication::setFont |
6. 进阶技巧与工程化建议
6.1 用接口类替代纯C函数导出
纯C函数导出虽然通用,但接口多了之后管理麻烦。如果调用方也是C++,可以考虑导出一个抽象接口类。DLL里实现这个接口,导出一个工厂函数返回接口指针。
class IWidgetModule { public: virtual ~IWidgetModule() {} virtual int Init() = 0; virtual int ShowDialog(void* parent) = 0; virtual void Cleanup() = 0; }; extern "C" WIDGETMODULE_API IWidgetModule* CreateWidgetModule(); extern "C" WIDGETMODULE_API void DestroyWidgetModule(IWidgetModule*);这样接口更清晰,扩展也方便。调用方拿到接口指针后,像用普通C++对象一样调用。缺点是调用方必须是C++,而且需要包含接口头文件。
6.2 支持多实例与线程安全
如果主程序可能同时打开多个DLL界面实例,或者从多个线程调用DLL函数,就需要考虑线程安全。QApplication本身不是线程安全的,所有GUI操作必须在主线程。但DLL的初始化、清理这些操作可能被多线程调用,需要加锁。
我的做法是用QMutex保护g_app和g_ownApp的访问,InitModule和CleanupModule都加锁。ShowMainDialog因为涉及GUI,必须在主线程调用,不加锁但加一个线程检查,如果不在主线程就返回错误码。
6.3 日志与错误码设计
DLL内部出问题时,调用方需要知道发生了什么。设计一套错误码,每个导出函数返回int,0表示成功,负数表示不同错误。同时DLL内部写日志文件,方便排查。
错误码可以这样定义:
enum ModuleError { ERR_OK = 0, ERR_APP_CREATE_FAILED = -1, ERR_PLUGIN_PATH_FAILED = -2, ERR_WRONG_THREAD = -3, ERR_ALREADY_INITIALIZED = -4, ERR_NOT_INITIALIZED = -5 };日志用QLoggingCategory或者简单的文件写入。注意日志文件路径要可配置,默认放在DLL同目录下的logs文件夹。
6.4 版本兼容与升级策略
DLL和主程序之间的接口一旦发布,就要考虑兼容性。新增函数可以,修改已有函数签名不行。如果必须修改,要么改函数名,要么用版本号区分。
我的做法是在接口里加一个GetVersion()函数,返回接口版本号。主程序加载DLL后先调这个,版本不匹配就提示用户升级。同时DLL的文件名里带版本号,比如WidgetModule_v2.dll,这样新旧版本可以共存,主程序按需加载。
注意:Qt的版本也要考虑。如果主程序链接了Qt5,DLL用的是Qt6,两者在同一个进程里会冲突。所以DLL和主程序的Qt版本必须一致,或者DLL静态链接Qt,避免符号冲突。
6.5 实际项目中的部署清单
最后列一个部署清单,发布时对照检查:
- DLL文件本身,确认是Release版本
- 依赖的Qt库:
Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll - 平台插件:
plugins/platforms/qwindows.dll - 样式插件(如果用了):
plugins/styles/ - 图片格式插件(如果加载了jpg、gif等):
plugins/imageformats/ - 资源文件(如果qrc没编译进DLL):对应的rcc文件
- 配置文件(如果有):放在DLL同目录或指定路径
- 日志目录:确保有写入权限
我一般会写一个部署脚本,用windeployqt工具自动拷贝依赖,然后手动补充DLL特有的文件。windeployqt对EXE很好用,对DLL需要指定--dir参数,而且它不知道DLL的插件需求,所以插件目录还是要手动确认。
这个方案我在几个项目里用过,主程序分别是MFC、C#和Delphi,DLL这边都是同一套Qt界面代码,改改接口就能复用。最深的体会是:接口越简单越好,DLL内部越自包含越好,对外部环境的假设越少越好。把QApplication的创建、插件路径的设置、资源的加载都封装在DLL内部,调用方只需要LoadLibrary和几个函数调用,集成成本最低。