Qt 应用一键部署打包助手(GUI 工具):图形化封装 windeployqt 部署流程,并可追加 Enigma Virtual Box 单文件便携打包,支持 Qt 5.15 / Qt 6 双版本、MinGW / MSVC 双编译器。
- 图形化配置:目标 EXE、Qt Bin 目录、输出目录、QML 目录、Enigma 控制台路径、VS 安装根目录,一键开始打包。
- 输出目录跟随目标 EXE:选定或填入目标 EXE 后,输出目录自动填成该 EXE 所在目录(手动输入路径同样生效),便于就地打包。
- 就地打包:输出目录与目标 EXE 目录相同时,跳过删除/复制主程序,跳过「清空旧目录」,不会误删目录内其它 DLL;目标 EXE 正是正在运行的本程序时,日志会提示运行中的 exe/DLL 被占用、同名文件不会被覆盖。
- 文件占用自愈:启动 windeployqt 前先把输出目录里的运行库一次性改名让位,避免它去删正在运行的程序所加载的 DLL;仍报
Cannot remove existing file ...时,再把被占用文件在同目录改名*.wdh-locked后重跑(最多重试 2 次),无需手动关程序。 - 选择对话框跟随输入框:文件/目录选择器自动定位到当前输入框已填的路径(无效才回退到用户主目录)。
- 打包前 Qt 版本校验:点「开始打包」后、动任何文件之前,先核对所选 Qt 与目标 EXE 是否配套——Qt 主版本(Qt5/Qt6)、位数(32/64)、编译器(MinGW/MSVC)三项,判定依据是目标 EXE 的 PE 导入表里真实依赖的
Qt5Core.dll/Qt6Core.dll、libgcc*/libwinpthread-1.dll、vcruntime140.dll等。不匹配直接报错中止、不产生任何文件改动;判不出的情况(静态链接 Qt 或运行库)只提示不阻断(详见技术要点)。 - MinGW 运行库来源固定到所选 Qt:windeployqt 是按 PATH 里的
g++.exe定位运行库来源的,Qt kit 的 bin 没有 g++ 时会命中系统里其它 MinGW(如 Strawberry Perl 自带的 2024 版工具链),版本对不上就会触发「先删后拷」。工具在部署前先让位被占用的运行库、把配套工具链前置 PATH,部署后再用所选 Qt bin 目录自带的运行库强制覆盖——哪个 Qt 版本就用哪个的 DLL(详见技术要点)。 - 复制 VC 运行库:MSVC 部署时随包带上
vcruntime140*.dll、msvcp140*.dll、concrt140.dll。源优先取 Visual Studio Redist,按 Qt Bin 里的msvc<年份>选年份最接近的版本;无 VS Redist 时回退C:/Windows/System32(详见技术要点)。 - 安全保护:
- 输出目录 = 源 EXE 所在目录时,跳过删除/复制,避免误删原主程序;
- 清空旧目录时强制保留源 EXE,并支持自定义保留规则(通配符)。
- 多 QML 目录:QML 目录可用英文分号分隔填多个,逐个传给
windeployqt --qmldir;「选择」按钮为追加而非覆盖,同一目录自动去重。 - windeployqt 参数支持:
--multimedia、--qmldir(可多次)、--no-plugins(精简插件)、--compiler-runtime(复制运行库)。 - Enigma 单文件打包:
- 自动生成 EVB 11.30 所需的 XML 工程文件(
.evb),递归打包输出目录全部文件; - 单文件命名:
<目标EXE基名>_Single-qt<版本>-<编译器><位数>.exe(如App_Single-qt6.8.3-MinGW64.exe、App_Single-qt5.15.2-MSVC64.exe),版本与编译器取自qmake -v识别结果,位数取自目标 EXE 的 PE 头(识别失败则不加位数),输出到输出目录内; - 打包时自动跳过已存在的单文件(
*_Single*.exe)、临时工程enigma_temp.evb与占用备份*.wdh-locked,防止旧产物被重复打入; - 自动跳过运行时数据目录
data(历史记录 db / 导出文件等由程序动态增删的文件),避免 Enigma 报File does not exist;运行时数据打进单文件本就无益(程序写入不回写 Single); - 封包走异步进程,封包期间界面保持可响应。
- 自动生成 EVB 11.30 所需的 XML 工程文件(
- 配置持久化:
config.ini保存/加载全部配置。 - 日志面板:输出 windeployqt / Enigma 完整过程,可保存为 txt。
注:界面中「打包后压缩」「自定义保留插件」「版本号」「附加DLL」为已放置的预留选项(随配置保存),尚未接入打包流程。其中压缩由 EVB 工程固定写入
<CompressFiles>True</CompressFiles>,勾不勾该开关结果一致。
| 依赖 | 说明 |
|---|---|
| Qt | 5.15 或 6.x(qmake 构建),C++17 |
| 编译器 | MinGW(win32-g++)或 MSVC(win32-msvc*) |
| windeployqt | 随 Qt 自带,位于所选 Qt Bin 目录 |
| Enigma Virtual Box | 11.30+(可选,勾选「启用Enigma单文件打包」时需要其 enigmavbconsole.exe) |
| Visual Studio | 可选,仅 MSVC 部署复制 VC 运行库时用于定位 Redist;未安装则从 System32 取 |
直接用 Qt Creator 打开 WindeployqtHelper.pro,选择对应 Kit(Qt 5.15 或 Qt 6,MinGW 或 MSVC)构建即可。
cd /d F:\WORK\QT_TEST\WindeployqtHelper\WindeployqtHelper
E:\Qt\6.8.3\mingw_64\bin\qmake.exe WindeployqtHelper.pro
E:\Qt\Tools\mingw1310_64\bin\mingw32-make.exe -j8输出目录由 QtCommonConfig/config.pri 统一拼接:
DESTDIR = $$clean_path($$PROJECT_DIR/../$${PROJECT_NAME}-qt$$[QT_VERSION]-$${DIR_COMPILER}-bin)
DIR_COMPILER 按 QT_ARCH 判定位数后取 MinGW64 / MSVC64,即生成到工程目录的上一级,例如:
WindeployqtHelper-qt6.8.3-MinGW64-bin\WindeployqtHelper.exeWindeployqtHelper-qt5.15.2-MSVC64-bin\WindeployqtHelper.exe
重新编译前先关掉正在运行的旧实例:Windows 不允许覆盖正在运行的 exe,否则 MSVC 会报
LNK1104: 无法打开文件 ...WindeployqtHelper.exe。
版本号形如 1.0.0.<编译日期>-<编译时间>,例如 1.0.0.20261109-1303,窗口标题显示为 Qt 部署助手 v1.0.0.20261109-1303。日期时间取自编译期的 __DATE__ / __TIME__,.pro 中 CONFIG *= version_stamp 会在每次构建结束删除 version.o,使版本号随每次编译自动更新。
- 目标EXE:选择待部署的 Qt 程序(Release 版)。输出目录会自动跟随到该 EXE 所在目录。
- Qt Bin目录:选择与目标 EXE 相同 Qt 版本/编译器的
bin目录(需含windeployqt.exe),例如E:\Qt\6.8.3\mingw_64\bin。 - 输出目录:部署产物输出位置;不填 Qt 相关依赖时也可直接作为单文件打包源目录。与目标 EXE 同目录即为就地打包。
- QML目录(可选):目标程序含 QML 时填写,多个目录用英文分号分隔,例如
D:/src/qml;D:/src/views。 - Enigma控制台(可选):勾选「启用Enigma单文件打包」后必填
enigmavbconsole.exe路径。 - VS安装目录(可选):复制 VC 运行库时用于定位 Redist;首次默认
C:/Program Files/Microsoft Visual Studio,VS 装在其它盘时改一次并保存即可。 - 点击 开始打包,流程:
- 校验所选 Qt 与目标 EXE 是否配套(不匹配直接中止,不改动任何文件)→ 复制主程序到输出目录(同目录时自动跳过)→ 提前让位被占用的运行库 → 执行 windeployqt(失败且为文件占用时改名让位后重试)→ 用所选 Qt bin 的运行库校正 →(勾选时)复制 VC 运行库 →(可选)Enigma 封包单文件。
打包完成后,输出目录内会生成 <目标EXE基名>_Single-qt<版本>-<编译器><位数>.exe 单文件便携版(例如 WindeployqtHelper_Single-qt6.8.3-MinGW64.exe),同一目录下不同 Qt 版本/编译器的产物可按文件名区分。
运行目录下自动生成,[Paths] / [Options] 两组配置,与界面一一对应:
[Paths]
exePath=f:\...\App.exe
qtBin=E:/Qt/6.8.3/mingw_64/bin
outputDir=f:\...\App-qt6.8.3-MinGW64-bin
qmlDir=D:/src/qml;D:/src/views
enigmaVbConsole=D:/Program Files (x86)/Enigma Virtual Box/enigmavbconsole.exe
vsRootDir=d:/Program Files/Microsoft Visual Studio
[Options]
chkCleanOld=false
chkMultimedia=false
chkZip=false
chkSlimPlugin=false
customKeepPlugins=
chkCopyVcRuntime=true
version=1.0.0
extraDlls=
chkEnigmaPack=true
keepFiles=config.ini选错 Qt 的 bin 目录,windeployqt 就会把不相关版本的 DLL 拷进输出目录,甚至覆盖掉程序真正需要的那份。所以点「开始打包」后、清空目录/复制主程序/让位运行库之前,先由 checkQtTargetMatch() 做三项核对:
| 维度 | 目标 EXE 侧 | 所选 Qt 侧 | 不符时 |
|---|---|---|---|
| Qt 主版本 | 导入表含 Qt5Core.dll 还是 Qt6Core.dll |
bin 目录里装的是哪一个(二者不会同时存在) |
报错中止 |
| 位数 | PE 头 Machine 字段(0x8664=64,0x014C=32) |
同上,读该 bin 里的 Core DLL | 报错中止 |
| 编译器 | 含 libgcc*/libstdc++-6.dll/libwinpthread-1.dll 判为 MinGW;含 vcruntime140*.dll/msvcp140*.dll 判为 MSVC |
qmake -v 的识别结果 |
报错中止 |
判定依据是真解析 PE 导入表(readImportDllNames():DOS 头 → 0x3C 处 e_lfanew → PE 头 → OptionalHeader 的 DataDirectory[1] → 节表把 RVA 换算成文件偏移 → 遍历 IMAGE_IMPORT_DESCRIPTOR 取 Name 字段读 DLL 名),而不是在整个文件里搜 DLL 名。后者在部署工具这类程序上必然失效——本程序源码里就写满了 libgcc_s_seh-1.dll、vcruntime140.dll 这些常量,字符串会原样进 exe,全文搜索会让一个 Qt5 MinGW 目标同时"命中" Qt5、Qt6、MinGW、MSVC 四项。
判不出来时只打 ⚠️ 不阻断:目标程序静态链接了 Qt(导入表里没有 QtXCore.dll),或运行库全静态(既无 libgcc/libwinpthread 也无 vcruntime140)。注意 MinGW 程序用 -static-libgcc -static-libstdc++ 后导入表只剩 libwinpthread-1.dll 一个 MinGW 特征,所以它也被算进编译器判定。
windeployqt 对 Qt 自身 DLL 会先比对内容,相同则输出 is up to date 跳过;但对编译器运行库(如 libgcc_s_seh-1.dll)是先删后拷。输出目录就是目标 EXE 所在目录、而该 EXE 正在运行时,这些文件被系统锁定、删不掉,windeployqt 直接以 exitCode=1 失败。工具从三个层面处理:
- 不传多余参数:就地打包且
libgcc_s_seh-1.dll、libstdc++-6.dll、libwinpthread-1.dll已在输出目录时,不再传--compiler-runtime——文件本就齐全,传了只会去删被占用的同名文件; - 改名让位 + 重试:仍出现
Cannot remove existing file <路径>时,解析出被占用文件,在同目录内改名为<原文件名>.wdh-locked(Windows 允许改名已加载的 DLL,进程继续使用旧文件、运行不受影响),随后用同一组参数重跑windeployqt,最多 2 次; - 清理备份:封包时跳过
*.wdh-locked;这些备份在部署完成后、封包结束后、程序退出时各尝试删除一次,仍被占用的(例如打包程序自身时锁在自己进程里)留到下次启动清理。
MSVC 版 exe 在未安装运行库的机器上会因缺少 vcruntime140.dll 等直接启动失败,因此勾选「复制VC运行库」时会:
--compiler-runtime交给 windeployqt(它可能自己取不到 Redist 路径);- 部署完成后由
copyVcRuntimeDlls()兜底,从以下位置按顺序找源目录:<VS根>/<年份>/<版本>/VC/Redist/MSVC/<号>/x64/Microsoft.VC*.CRT:用 Qt Bin 目录名里的msvc<年份>过滤,按年份数值距离挑最接近的(目标 2019 而本机只有 2022 / 2026 时选 2022,不盲目取最高版本);年份距离相同再比 Redist 版本号,有目标年份取更高版本、无年份信息取更低版本以兼容老机器;- 都找不到时回退
C:/Windows/System32(系统当前生效的那份运行库)。
- 复制时若目标文件已存在且大小一致则跳过;大小不同则先改名
*.wdh-locked让位再放入新文件,同样受「文件被运行中程序加载」影响。
onecore\x64、x86 等子目录里 vcruntime140_1.dll 可能缺失,故只取 x64 主目录下的 CRT。
windeployqt 复制 MinGW 运行库(libstdc++-6.dll、libgcc_s_seh-1.dll、libwinpthread-1.dll)时,并不是从 Qt kit 的 bin 取,而是顺着 PATH 找 g++.exe 所在的那套工具链。Qt kit 的 bin 里只有 DLL、没有 g++,所以只前置它也仍会命中系统里其它 MinGW——典型是 Strawberry Perl 自带的 D:\Strawberry\c\bin(740183 / 2304720 / 94355,2024 年文件),与 Qt5 kit 的 76288 / 1417216 / 52224 完全不同。
版本一旦不一致,windeployqt 每次判定都要「先删后拷」,撞上正在运行的程序锁定的 DLL 就 exitCode=1。工具分三步处理:
- 部署前让位:
preBackupRuntimeDlls()按编译器类型把输出目录中的同名 DLL 改名为*.wdh-locked(MinGW 三件套;勾选复制 VC 运行库时再加 VC 六件),windeployqt 此时就无需删除任何被占用文件;失败时restorePreBackups()全部改回原名,目录不会缺文件; - 前置 PATH:调用 windeployqt 前,把配套工具链
bin(findMinGwRuntimeDir()按libstdc++-6.dll大小与 kit 完全一致来挑选)插到 PATH 首位,让它从正确来源复制; - 固定来源校正:部署完成后
fixMinGwRuntimeDlls()以界面上所选 Qt 的bin目录为唯一来源覆盖运行库,不再推导、不再去系统里找同名 DLL。MSVC 的 kitbin里没有这些运行库,源文件不存在即跳过,不影响 MSVC 部署。
各版本自带值互不通用,可作为核对依据:
| 来源 | libgcc_s_seh-1 | libstdc++-6 | libwinpthread-1 |
|---|---|---|---|
Qt\5.15.2\mingw81_64\bin |
76288 | 1417216 | 52224 |
Qt\6.8.3\mingw_64\bin |
109056 | 2243072 | 53248 |
| Strawberry Perl 自带(错误来源) | 740183 | 2304720 | 94355 |
Enigma Virtual Box 11.30 的 .evb 工程文件为 XML 格式(旧版 INI 模板会被控制台拒绝:Project file is invalid!):
<Project>
<InputFile>...</InputFile>
<OutputFile>...</OutputFile>
<Files>
<Enabled>True</Enabled>
<DeleteExtractedOnExit>False</DeleteExtractedOnExit>
<CompressFiles>True</CompressFiles>
<Files>
<!-- 文件: <Type>2</Type>,虚拟目录: <Type>3</Type>(根目录名为 %DEFAULT FOLDER%) -->
</Files>
</Files>
</Project>源码内 escapeXml() 处理特殊字符转义,appendEvbFolder() 递归生成文件树,并在递归时跳过名为 data 的目录(大小写不敏感,日志输出 ℹ️ 跳过运行时数据目录:...)、跳过 *.wdh-locked。
代码使用两版本共有的 API(如 Qt::SkipEmptyParts,Qt 5.14+ 已提供),无需依赖 core5compat 模块;.pro 中按 QT_VERSION 区分构建,输出目录按 DIR_COMPILER 区分编译器与位数。
关闭窗口时先置内部 shuttingDown 标志,再 kill() 仍在运行的 windeployqt / enigmavbconsole 子进程(各等待最多 3 秒),两个进程回调遇到该标志直接返回。否则打包进行中点关闭,主进程会一直等子进程结束,表现为「界面已消失但进程仍在」、目录内 exe 与 DLL 一直被锁。
Warning: Cannot find any version of the dxcompiler.dll and dxil.dll:纯 QWidgets 程序无需 D3D12 渲染后端,可忽略(该警告仅与 Qt Quick 的 D3D12 后端相关)。- 日志出现
❌ 版本不匹配/❌ 位数不匹配/❌ 编译器不匹配:所选 Qt Bin 与目标 EXE 不配套,按日志里写的原因换成对应 Qt 版本/位数的bin目录。工具在动任何文件之前就中止,输出目录不会被污染。 - windeployqt 报
does not seem to be a Qt executable:同样是版本/编译器不匹配,但说明程序绕过了校验(例如目标程序静态链接 Qt、判不出主版本),换对应 Qt Bin 即可。 Cannot remove existing file ...或exitCode=1:目标目录里的同名 DLL 正被运行中的程序加载。工具会自动改名让位重试;若日志仍报失败,说明文件被其它进程独占,关掉对应程序后重试即可。- VC 运行库复制失败:同上,目标 DLL 被运行中的程序占用;工具已改为大小一致跳过、不一致改名让位。
- 重新编译报
LNK1104: 无法打开文件 ...WindeployqtHelper.exe:该 exe 正在运行(哪怕窗口已关,也可能因打包未结束而仍在)。结束对应进程后再编译。 - Enigma 报
File does not exist/Cannot open file:部署目录内的运行时数据目录(如data,含按天生成/清理的历史记录 db、导出文件)在打包瞬间文件可能已被删除或正被占用。工具已自动跳过名为data的目录;其它动态数据目录请勿放入部署目录。
WindeployqtHelper/
├── WindeployqtHelper.pro # 工程文件(Qt5/Qt6 双兼容、版本资源、构建开关)
├── QtCommonConfig/config.pri # 通用构建配置(编译模式/平台/DESTDIR 输出目录)
├── main.cpp
├── mainwindow.h # 主窗口声明
├── mainwindow.cpp # 主窗口实现(部署 + VC/MinGW 运行库 + Enigma 打包逻辑)
├── mainwindow.ui # 界面布局
├── version.h # 版本号常量与编译时间接口
├── version.cpp # 由 __DATE__ / __TIME__ 生成版本号
├── resources/WindeployqtHelper.rc # 自定义版本资源(UTF-8,避免中文乱码)
└── readme.md
MIT © netmankind