不用纠结,选错工具顶多多花半小时,真正让人崩溃的是程序在别人电脑上双击没反应、提示缺 DLL、或者直接闪退。这篇东西就是来终结这种痛苦的。
我本身做 Qt 开发有好几年了,从 Qt 4 时代一路用到 Qt 6,Windows、Linux、嵌入式都折腾过。打包这件事,几乎每个项目都要踩一遍,而且奇怪的是——每个团队的打包方案都不一样,网上教程也是各说各话。所以我想把常用的 Qt 打包工具一次性讲清楚,做个横向对比,并且把我实际过程中的操作命令、参数、坑,全部摊开来说。
这篇文章适合这几类人:
- 刚学 Qt 的小白:程序写完了,不知道怎么发给别人用
- 做了几年但一直用同一种打包方式的开发:想看看别的方案到底好在哪里
- 需要给 Qt 程序做安装包的发布/运维同学:需要了解不同工具的适用场景
我会从打包原理开始讲,因为不懂原理,你换什么工具都会被 DLL 搞死。
1. 先搞清楚一个问题:Qt 程序为什么换台电脑就跑不起来
很多人第一次接触 Qt 打包,是从“把 exe 发给朋友,结果打不开”开始的。弹出的错误五花八门:缺 Qt5Core.dll、找不到 Qt platform plugin、程序初始化失败……但这背后其实是同一件事:你的程序不是一个孤立的 exe,它依赖一堆动态库和插件。
1.1 动态链接让程序变小,也让程序变“脆”
Qt 默认是动态编译的。你写的代码会链接到 Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll 这些动态库上。编译出来的 exe 本身可能只有几百 KB,但它运行时要到系统路径或 exe 同目录下去找这些 DLL。
这一点和 C 语言静态编译完全不一样,静态编译是把所有代码塞进一个 exe,体积大,但不担心缺库。Qt 动态编译则是“按需加载”,好处是灵活、升级方便,坏处就是——你少带一个 DLL,程序就罢工。
打个比方:你的 exe 像是一台主机,DLL 是外接显卡、声卡、电源,缺一个都开不了机。
1.2 Qt 的插件机制:坑最多的环节
光把 DLL 复制过去还不够。Qt 里大量功能通过插件(plugin)实现,典型的有:
- platforms 目录下的 qwindows.dll(Windows 平台插件,没有它就报"could not find or load the Qt platform plugin windows")
- styles 目录下的样式插件
- imageformats 目录下的图片格式插件(jpg、gif、webp 等)
- iconengines 目录下的图标主题插件
- tls 目录下的网络加密插件
这些插件分布在 Qt 安装目录的 plugins 子目录里,默认不会跟着你的 exe 走。如果你用的控件涉及这些功能,发布时没带上,就会出现“有的功能正常、有的功能报错”这种极其诡异的现象。
我见过最典型的案例:程序用 QWebEngine 加载网页,打包后双击闪退,查了半天发现是 QtWebEngineProcess.exe 和相关资源文件没带全。所以打包时,对插件目录的处理一定不能想当然。
理解了这层机制,再看接下来要讲的工具,思路会清晰很多——无非就是“把该带的库和插件找齐、放对位置”,区别只是自动化程度和附加功能不同。
2. 官方部署工具:windeployqt、macdeployqt、linuxdeployqt
Qt 官方非常清楚用户的打包痛点,所以从早期就开始提供配套的部署工具。它们的作用不是制作安装包,而是把 exe 依赖的 DLL、插件、资源文件自动复制到 exe 所在目录,让程序能独立运行。
2.1 windeployqt 的具体用法和关键参数
在 Windows 上,windeployqt 是最基础的一步。它的工作原理是通过解析 exe 的导入表,找到需要的 Qt 模块,然后把对应的 DLL 和插件拷到 exe 旁边。
基本用法:
windeployqt.exe D:\build\release\MyApp.exe它会自动创建 platforms、styles、imageformats 等目录,并把文件放好。但我一般不会裸跑,通常会带几个参数:
windeployqt.exe --release --no-translations --no-system-dll --skip-plugin-types qmlscene MyApp.exe参数说明:
--release:告诉工具你是 Release 构建,避免去匹配 Debug 库--no-translations:不拷贝 Qt 自带的翻译文件,省体积且避免和自研翻译冲突--no-system-dll:不拷贝系统 DLL(如 msvcp140.dll、vcruntime140.dll),这些通常由 VC++ 运行库提供,但如果你要“绿色版”全带上也可以去掉这个参数--skip-plugin-types:跳过某些不需要的插件类型
有一点要特别注意:windeployqt 只处理 Qt 自己的依赖,不处理第三方库。如果你的程序用了 OpenCV、FFmpeg、自研 DLL,这些得自己额外复制。还有,windeployqt 对 MSVC 和 MinGW 版本的 Qt 支持有差别,MinGW 的较老版本可能在识别编译器运行时上没那么准确,建议先跑一遍再手动检查。
2.2 macdeployqt 与 Linux 上的部署
macOS 上对应的工具是 macdeployqt,用法类似:
macdeployqt MyApp.app -dmg它会处理 app bundle 里的动态库路径,把 Qt 库拷进 Contents/Frameworks,并把依赖路径改成@executable_path相对路径。加-dmg可以直接生成可拖拽安装的 dmg 镜像,体验非常顺滑。
这里有个 macOS 特有的坑:如果你用了 Qt 的 WebEngine,即使跑了 macdeployqt,也需要手动确认Contents/Frameworks/QtWebEngineCore.framework/Helpers里的子进程文件是否完整,否则会出现程序启动了但网页一直空白的情况。
Linux 上则相对分裂。Qt 官方没有专门的一键命令,常用的是linuxdeployqt这个社区工具,配合 AppImage 格式使用:
linuxdeployqt MyApp.AppDir/usr/bin/MyApp -appimage不过 linuxdeployqt 对发行版有要求,推荐在 Ubuntu 16.04 或 18.04 等较老环境里打包,否则在更低版本的系统上运行会报 GLIBC 版本不兼容。这是 Linux 特有的“兼容性诅咒”,只能靠老环境编译或静态链接缓解。
2.3 官方工具的局限
官方工具解决了“程序能不能跑”的问题,但不管“用户怎么装”。你生成的是一个目录,里面躺着一个 exe 和一堆文件。这在内部工具、免安装软件、或者直接压缩包分发时没问题,但如果要给普通用户做一个带桌面快捷方式、开始菜单项、卸载入口的正式安装包,就还得借助下一节说的封装工具。
另外,官方工具没有自动更新、注册表、服务安装等功能,这些都是安装包工具的长处。所以官方工具和第三方封装工具并不是竞争关系,而是流水线上下游的关系——先 windeployqt 整理依赖,再交给安装包工具生成安装向导,这是最标准的流程。
3. 安装包封装工具:Inno Setup、NSIS、Qt Installer Framework 横向对比
到了这一层,核心诉求已经变成“怎么让用户方便地装进系统里”。这一领域工具不少,但针对 Qt 程序,我主要推荐三个:Inno Setup、NSIS、Qt 官方的 Qt Installer Framework(简称 QIF)。
先给一张综合对比表,下面的篇幅再逐一点评:
| 维度 | Inno Setup | NSIS | Qt Installer Framework |
|---|---|---|---|
| 上手难度 | 低,脚本接近自然语言 | 中高,语法类汇编,学习曲线陡峭 | 中,有可视化维护工具 |
| 脚本写法 | Pascal 脚本,可读性强 | 自定义 NSIS 脚本,灵活但难调 | XML + JavaScript 混合 |
| 默认 UI | 经典 Windows 向导样式 | 老旧风格 | 现代化界面,支持换肤 |
| 体积与资源占用 | 安装器较小 | 最小 | 安装器偏大,运行时占资源高 |
| 多平台支持 | 仅 Windows | 仅 Windows | 全平台,特别适合做套件安装 |
| 在线/离线更新 | 依赖第三方插件 | 依赖插件和脚本 | 原生支持在线软件源仓库 |
| 组件选择 | 支持,逻辑简单直接 | 支持,定制性强 | 支持,按包管理 |
| 社区与资料 | 极其丰富 | 极其丰富,教程多 | 官方文档为主,实战案例较少 |
3.1 Inno Setup——Windows 上最省心的选择
我自己在 Windows 上发 Qt 软件,90% 用 Inno Setup。原因很简单:脚本足够简单,静态编译出来的安装程序稳定性极好。
一个最小可用的 Inno Setup 脚本大概是这样的:
[Setup] AppName=MyQtApp AppVersion=1.0.0 DefaultDirName={autopf}\MyQtApp OutputDir=installer_output OutputBaseFilename=MyQtApp_Setup [Files] Source: "D:\build\release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{autoprograms}\MyQtApp"; Filename: "{app}\MyQtApp.exe" Name: "{autodesktop}\MyQtApp"; Filename: "{app}\MyQtApp.exe"这段脚本的意图很容易理解:
[Setup]段定义安装包的基本信息[Files]段把整个 release 目录下的所有内容(包括 plugins 目录)递归放进安装目录[Icons]段创建开始菜单和桌面快捷方式
你只需要把 windeployqt 生成的整个发布目录当作“原料”丢给 Inno Setup,剩下的就是改改名称、路径。Inno Setup 内置了卸载功能,卸载时会按反序删除安装时拷贝的文件,不用你管。
Inno Setup 另外一个很强的点是Pascal 脚本可以做在安装前/后执行各种操作,比如检测 VC++ 运行库是否安装,没装就弹提示;或者自动配置防火墙规则;又或者写注册表。这些在 Qt 桌面软件里经常用到。
3.2 NSIS——强大的脚本能力,但写起来费劲
NSIS 是另一款老牌工具,它的核心优势是脚本能力极强、生成体积很小。但它最大的问题也在这——语法太反人类了,纯过程式,像汇编一样一步步推栈。
一个简单的 NSIS 脚本:
Name "MyQtApp" OutFile "MyQtApp_Setup.exe" InstallDir "$PROGRAMFILES\MyQtApp" Section "Main" SetOutPath "$INSTDIR" File /r "D:\build\release\*.*" CreateShortcut "$DESKTOP\MyQtApp.lnk" "$INSTDIR\MyQtApp.exe" WriteUninstaller "$INSTDIR\uninst.exe" SectionEnd Section "Uninstall" RMDir /r "$INSTDIR" SectionEnd这个脚本能跑,但是要做安装类型选择、多语言、界面美化这些事情,代码量立刻成倍上涨。
NSIS 也有它的价值:如果团队里对 NSIS 已经很熟,且需要做非常复杂的自定义安装流程(比如多种安装模式、插件联动),那它是比 Inno Setup 更灵活的选择。但对于绝大多数 Qt 项目来说,这种复杂度没有必要,性价比不如 Inno Setup。
3.3 Qt Installer Framework——正统但有点“重”
QIF 的优势在于全平台统一体验,而且自带组件管理和在线更新能力——企业级软件,尤其是软件套件,用 QIF 会非常舒服。
QIF 的制作思路不太一样,它要先定义一个“配置包”,描述产品基本信息,然后定义子组件,每个组件对应一组文件。打包/做安装器时,它把这些组件拼装成类型的安装程序。
一个简化版的 QIF 配置文件结构:
<!-- config/config.xml --> <Installer> <Name>MyQtApp</Name> <Version>1.0.0</Version> <StartMenuDir>MyQtApp</StartMenuDir> </Installer> <!-- packages/org.example.myapp/meta/package.xml --> <Package> <DisplayName>MyQtApp 主程序</DisplayName> <Description>主程序组件</Description> <Version>1.0.0</Version> <ReleaseDate>2025-01-01</ReleaseDate> </Package>然后通过命令行工具生成安装器:
binarycreator.exe -c config/config.xml -p packages MyQtApp_Installer.exeQIF 的组件机制很强大,可以做功能勾选、增量更新。但相应地,生成出来的安装器体积比 Inno Setup 大,启动时有自带的运行环境,执行速度偏慢。如果只是一个简单的业务系统,用 QIF 属于“杀鸡用牛刀”。我通常只在需要做客户端自动升级、或者需要多产品统一安装入口的时候才会选它。
3.4 封装工具怎么选:给一个直接的结论
如果是 Windows + 常规桌面软件的标配,我的建议是:
- 工具简单、不做复杂操作 → Inno Setup,学 30 分钟就能上手
- 要做多语言安装向导、品牌定制 → 优先 Inno Setup,界面观感足够
- 要做软件套件安装、组件选择、在线升级 → QIF
- 团队已有 NSIS 积累,不愿意换 → NSIS 也行,但别高估它的必要性
搞定了依赖,又搞定了安装包脚本,接下来要解决的是另一类诉求:没有安装过程、双击就跑的绿色软件。
4. 单文件绿色发布:Enigma Virtual Box 和 BoxedApp Packer
有些场景下不适合传统安装包。比如你做的工具软件要发给客户临时评估,或者做的是远程给用户培训时临时要跑的 demo,又或者你希望程序放在优盘里插到哪台电脑都能用——绿色免安装、单文件显得极其重要。
4.1 单文件工具的运作原理
所谓“单文件打包”,并不是把所有 DLL 静态编译进 exe,而是做一个虚拟文件系统的壳。它把 Qt 的 DLL、插件目录和你的 exe 打包进一个宿主程序,运行的时候在内存中创建虚拟目录,让 exe 认为那些文件天然存在于某个路径下。这样一来,你发给用户的只有一个 .exe,但运行时的效果跟解压了一堆文件是一样的。
这里有两款常见工具:
- Enigma Virtual Box:免费、轻量,适合快速做单文件封装
- BoxedApp Packer:偏商业,API 能力强,支持虚拟注册表、虚拟服务,适合更复杂的场景
Enigma Virtual Box 的使用流程很简单:
- 打开工具,填写主程序的虚拟化后文件名(输出路径)
- 把整个发布目录(release 下的所有文件和文件夹,包括 plugins)拖进文件列表
- 默认勾选“压缩文件”,点击 Process
处理完成后就会生成单一 exe。之后你要做的只是把这个 exe 发给别人。
4.2 Enigma Virtual Box 的注意事项
用这类工具,有几点必须注意:
- 不要指望它能跨平台,这是 Windows-only 的解决方案
- 杀毒软件误报率偏高,因为单文件壳的行为特征和捆绑木马类似。如果你发的是商业软件,建议对生成物做代码签名,否则客户机器上弹出红色警告就尴尬了
- 不支持同时运行多个实例或对虚拟路径做写入操作时,可能产生问题。Qt 程序如果运行时需要在 exe 同目录写日志,最好把日志路径改到
%APPDATA%,否则虚拟文件系统里写入的内容在程序退出后就没了
我自己用 Enigma Virtual Box 主要是给客户做“按需演示版”,不作为正式发布通道。原因很现实:正式版本如果出问题需要热更新,单文件改动成本太高,必须重新生成整个文件发给用户。
4.3 绿色发布与安装包发布怎么搭配
比较稳妥的做法是“两条腿走路”:
- 完整安装包(Inno Setup/QIF)→ 面向正式用户,提供卸载入口和升级能力
- 单文件便携版(Enigma Virtual Box)→ 面向临时演示、技术支持反馈问题场景,方便对方不用安装直接跑
这两个发布物从同一套 release 目录出,只是后处理方式不同。搭配使用的效果很理想,尤其是远程协助时,直接扔给对方一个单文件,比让他在安装向导点击下一步等待快得多。
5. 依赖排查:程序还缺 DLL、崩溃一闪而过怎么办
工具说得再天花乱坠,真正做发布时一定会碰到“明明本机能跑、换个机器就挂”的情况。问题到底是缺 DLL、缺运行库、还是缺插件?你需要一套成体系的排查方法。这里分享我每次发布前的检查流程和心得。
5.1 权威工具:Dependencies、Process Explorer 与 ldd
Windows 上,我推荐用Dependencies(原名 Dependencies Walker 的继承者)和Process Explorer配合使用。
- Dependencies:把 exe 拖进去,它会列出所有依赖模块,标注缺失项。对于“还缺哪个 DLL”这类问题,一查便知。
- Process Explorer:让程序跑起来,然后看进程加载了哪些 DLL,可以确认某些插件或第三方库是否真的被加载。
用 Dependencies 查 DLL 缺失的方法是:
- 打开 Dependencies.exe
- 把目标 exe 拖进窗口
- 看右侧模块列表有没有红色高亮条目(缺失项)
- 记录缺失项名称,去对应的运行库或工具目录里找
Linux 上对应的是ldd,它对一个可执行文件打印出所有依赖的动态库路径,如果某个库路径显示not found,一目了然:
ldd ./MyApp不过要注意:ldd本身会被某些高安全策略限制执行,此时可以用objdump -p | grep NEEDED来查看。
5.2 常见缺失项排查对照表
结合 Qt 桌面开发的特点,我把常见的问题现象、产生原因和解决手段整理成了速查表:
| 症状 | 常见原因 | 解决方案 |
|---|---|---|
| 弹窗提示缺少 Qt5Core.dll / Qt6Core.dll | 发布目录没带 Qt 动态库 | 重新执行 windeployqt,确认 exe 同目录下出现对应 DLL |
| could not find or load the Qt platform plugin "windows" | platforms/qwindows.dll 缺失或路径不对 | 手动检查 plugins/platforms 是否存在;确保程序在 exe 同目录寻找插件 |
| 程序一闪而过,无任何提示 | 缺 DLL;或插件目录异常;或 Debug 版 Qt 未带对应运行库 | 用 Dependencies 查库,用 Process Explorer 看进程退出码;确认编译模式与 windeployqt 参数匹配 |
| 提示 msvcp140.dll / vcruntime140.dll 缺失 | 目标机器没有 VC++ 运行库 | 安装 VC++ Redistributable,或在打包时带上--no-system-dll并把对应 DLL 放进发布目录 |
| 程序能启动,但图片、图标不显示 | imageformats 插件没带 | 确认 release/plugins/imageformats 存在;或者直接全量 recuse 模式复制进入发布目录 |
| 部分中文乱码或功能按钮文字显示方块 | 字体/翻译资源缺失,或未加载 qtlite 资源 | 若用了翻译,检查 .qm 文件路径;否则考虑把字体文件一并发布到资源路径 |
| QtWebEngine 使用时报错或者子进程崩溃 | QtWebEngineProcess.exe、icu 相关文件缺失 | 完整复制 Qt 安装目录中相关 bin 下的 WebEngine 文件和 resources;确保位置正确 |
5.3 发布前的最后一道自检:换一台干净机器验证
所有工具跑完,所有 DLL 看似都齐了,也别忘了做终极验证。我的习惯是:
- 准备一台全新的虚拟主机或未装开发环境的机器(Windows 可以用 Windows Sandbox,Linux 可以用 docker 容器跑一个最小镜像)
- 把发布目录(或安装产物)复制过去,不做任何额外安装,直接运行主要功能
- 确认程序能启动、数据库能连接、图片能加载
- 如果涉及权限场景,用普通用户而非管理员账号再跑一遍
这一步看起来不起眼,却救了很多次发布事故。有几次我自信满满地把安装包发出去,结果客户机器上一运行就提示缺少运行库,无非就是 VC++ 运行库没装。用干净环境验证一次,这些问题全部能提前暴露。
6. 一套完整的打包流程配置参考:从编译到安装包
工具都介绍了,依赖排查也会了,现在把整个发布流程串一遍。下面是一个实际项目的完整配置参考,可直接复制思路。假设场景为:Windows 10/11 + Qt 5.15.2 MSVC2019 64 位 + 常规 Widgets 程序。
6.1 第一步:编译 Release 版本
打包的前提是编译 Release,不是 Debug。Debug 版本的依赖库是带调试符号的,体积巨大且目标机器没有对应运行库。在 Qt Creator 左下角切换到 Release,然后重新构建。
构建完成后,找到输出目录(一般在 build-项目名-Desktop_Qt_5_15_2_MinGW_64_bit-Release 或 build-项目名-Desktop_Qt_5_15_2_MSVC2019_64bit-Release 下),里面有一个 exe。
6.2 第二步:用 windeployqt 整理目录
打开 Qt 自带的命令行环境(开始菜单里可以找到“Qt 5.15.2 (MSVC 2019 64-bit)”这个命令行快捷方式),然后执行:
cd D:\build\release D:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe MyApp.exe --release --no-translations --no-system-dll执行完后,你会看到目录下出现了 Qt5Core.dll、Qt5Gui.dll 等一系列库,以及 platforms、styles 等插件目录。
如果程序用了 Qt 的 SQL 模块并连了 MySQL 或其他数据库,还需要检查 sqldrivers 目录下的驱动 DLL 是否齐备。同理,如果你用到了 Qt Network 的 TLS 加密,要把 tls 插件目录一起带全。
6.3 第三步:根据项目追加第三方依赖
这一步是纯手工活。我的发布目录一般长这样:
release/ MyApp.exe Qt5Core.dll Qt5Gui.dll Qt5Widgets.dll ... platforms/ styles/ imageformats/ sqldrivers/ libmysql.dll <- 第三方库,手工复制 MyAppEngine.dll <- 自研库,手工复制 config/ resources/自研库和第三方库(比如 OpenCV、FFmpeg)没有任何工具能自动检测,必须使用 Dependencies 人工确认,或者直接在开发机上搜索你调用的库文件路径,然后复制过来。
6.4 第四步:Inno Setup 脚本生成安装包
在 release 目录的父级建一个 scripts 目录,创建installer.iss:
#define MyAppName "MyQtApp" #define MyAppVersion "1.0.0" #define MyAppPublisher "My Studio" #define MyAppExeName "MyApp.exe" [Setup] AppId={{8A9F7C41-3D09-4C2F-B02E-8D69E9E2C10A} AppName={#MyAppName} AppVersion={#MyAppVersion} AppPublisher={#MyAppPublisher} DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes OutputDir=..\output OutputBaseFilename=MyQtApp-Setup-{#MyAppVersion} Compression=lzma2 SolidCompression=yes [Languages] Name: "chinesesimplified"; MessagesFile: "compiler:Default.isl,compiler:Languages\ChineseSimplified.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}" [Files] Source: "..\release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent在 Inno Setup 编译器中打开这个文件,点击 Build,就会在 output 目录生成安装包。
6.5 第五步:制作单文件便携版(按需)
如果需要单文件版本,在 windeployqt 完成后的 release 目录基础上,用 Enigma Virtual Box 按前文描述把整个目录虚拟化成一个 exe。
完成后,建议把安装包和便携版分别明确命名:
MyQtApp-Setup-1.0.0.exe // 正式安装包 MyQtApp-Portable-1.0.0.exe // 便携版到这里,从 release 构建到安装包/绿色版就全部完成了。
7. 参数计算与选型细节:为什么这么选,不这么选
这部分不是讲具体某个工具,而是帮你在做一个“全局决策”时能比较心里有数。因为很多新人会被五花八门的方式搞晕:有人建议全静态编译,有人建议用 Docker 交叉编译,有人建议直接拷整个 Qt 安装目录……到底听谁的?
7.1 静态编译 vs 动态编译 + 部署工具
静态编译意味着把 Qt 库编进 exe,最终产物只有一个 exe,很多“演示程序”都是用这种方式分发的。听起来很诱人对吧?但实际上静态编译在 Qt 里属于“非官方推荐路线”,它有几个硬伤:
- 体积巨大:一个最简 Qt Widgets 程序,静态编译后也经常超过 20MB,如果用了 WebEngine,直接破百
- 插件机制失效:Qt 很多功能靠插件动态加载,静态编译时需要手动把插件的源码编译进去,这会导致代码复杂化
- 版权合规风险:LGPL 协议要求动态链接时用户可以替换 Qt 库文件,静态链接则有额外的开源义务要求,商业闭源软件要慎重
所以绝大多数情况下,“动态编译 + windeployqt + 封装工具”反而是最正规、最通用、也最省事的选择。真正静态编译的场景,多半是嵌入式设备或对目标机极不友好的特殊环境。
7.2 为什么我推荐 Inno Setup 而不是 NSIS
这个选择其实不是“NSIS 不够好”,而是“Inno Setup 更适合 Qt 开发者的心智模型”。
Qt 开发者通常已经要维护 C++ 代码,再让他们去学一门安装脚本语言,太痛苦了。Inno Setup 的 Pascal 是高级语言,变量、函数、if-else 可读性都好。而且 Inno Setup 有非常完善的向导式界面,新手上手几乎没有心理门槛。
反观 NSIS,虽然插件生态丰富,但学习成本实在高。我见过的真实案例里,大部分项目用 NSIS 写的脚本维护起来都极其痛苦,而 Inno Setup 的脚本只要规范,半年后回来看依然一目了然。
7.3 什么时候必须用 Qt Installer Framework
如果一个软件有多个独立子程序组合成套件,例如“主程序 + 数据服务 + 报表工具 + 帮助文档”,并且各个组件之间存在版本依赖关系,那 QIF 就非常合适。它的在线仓库功能允许你发布一个“在线安装器”,用户安装后可以通过维护工具每次增量拉取新版本,不用重新下载完整安装包。
但反过来,如果只是一个单 exe 的简单工具,用 QIF 纯粹是给自己添堵。它的配置复杂度摆在那里,安装器启动还慢,没必要为简单场景引入这么大一套东西。
7.4 打包的本质是“可复现”
不管选哪条路,最终目标都是让发布产物可以稳定复现。我的做法是:把整个打包过程写成脚本或文档放进项目仓库。谁接收这个项目,按照文档一步步执行,就能产出完全一致的安装包。
如果项目规模稍大,还可以用 CI(如 GitHub Actions 或 Jenkins)在每次打 tag 时自动执行打包流程,在流水线里直接产出安装包。这样的好处是发布人员不需要在自己电脑上装 Qt 环境,减少因环境差异导致的奇怪问题。
8. 踩坑实录:那些文档里不会写的细节
最后这部分,用来记录一些句句带血的细节问题。这里没有体系化的知识,只有零散但是真实的坑,能帮你省下大量调试时间。
8.1 windeployqt 不负责病毒扫描,别把杀毒误报全甩给工具
windeployqt 本身不产生任何可执行代码,它只是复制文件,所以误报一般不在它身上。但如果你在发布目录里自带了一些壳工具或混淆过代码,杀毒软件误报的概率会大幅上升。建议正式发布前,把安装包丢到 VirusTotal 上扫一遍,确认没有引擎报毒再发给客户。
8.2 “缺 Qt5Core.dll” 不一定是真缺
有时候你明明把所有 DLL 都放进去了,客户机器依然提示缺少 Qt5Core.dll。这时优先怀疑的不是文件缺失,而是exe 启动目录不对。如果你在代码里调用了QApplication::setLibraryPaths()或手动修改了 PATH,程序可能没去 exe 同目录找库,导致找到系统目录里不存在的 Qt 库,于是报缺失。
同理,如果客户是从别的目录双击 exe 启动的,且你的 exe 启动后切换了工作目录,那相对路径加载插件也可能出问题。代码里写路径时,尽量基于QCoreApplication::applicationDirPath()而不是QDir::current()。
8.3 Qt6 与 Qt5 在打包上的差异
Qt6 的 windeployqt 逻辑和 Qt5 大体一致,但生成的插件结构和加载机制有变化。比如 Qt6 里很多模块被拆分得更细(Qt5Gui 拆成 Qt6Gui 和 Qt6OpenGL 等),带上 windeployqt 之后体积会更大。Qt6.2 以上版本对编译器运行库的依赖也更强,如果目标机器没有 VC++ 2019/2022 运行库,即使带上了 Qt 库也可能因为 msvcp 缺失而启动失败。
8.4 带 QML 的程序打包:一个额外的坑
如果你的程序用了 QML(QQuickView 或 QQmlApplicationEngine),w indeployqt 需要额外加--qmldir参数,指向你的 qml 源文件目录,它才能正确收集 QML 模块依赖:
windeployqt.exe MyApp.exe --qmldir D:\project\qml不加这个参数,很常见的现象是程序在开发机上正常,在客户机器上运行时报 “module QtQuick is not installed”。这个坑我踩过至少三次,所以单独提出来。
8.5 安装包崩溃在“卸载”阶段
Inno Setup 默认的卸载逻辑是把安装时记录的文件逐个删除。但如果程序在运行时往安装目录写了文件(比如动态生成的日志或数据库),卸载时会发现文件被占用或者目录不空,导致卸载不干净。解决办法是:
- 让程序不要把数据写到安装目录,统一放
%APPDATA% - 或者在 Inno Setup 的
[UninstallDelete]段里声明要删除的额外目录
[UninstallDelete] Type: filesandordirs; Name: "{app}\logs"8.6 关于代码签名的必要性
现在的 Windows 系统对未知发布者的程序越来越不友好。SmartScreen 会弹蓝屏警告,下载管理器会标记“未知发布者”。如果你的软件面向大众用户,建议尽早申请代码签名证书(OV 或 EV),对 exe 和安装包做 Authenticode 签名。签名可以:
- 消除 SmartScreen 的大部分警告
- 降低杀毒软件误报概率
- 提升用户安装时的信任感
签名命令也很简单:
signtool.exe sign /fd SHA256 /a /tr http://timestamp.digicert.com /td SHA256 /v MyQtApp-Setup-1.0.0.exe8.7 别忘记程序的“国籍”——多语言资源
Qt 程序做国际化时,翻译文件通常以 .qm 形式随发布物分发。如果你在代码里通过QTranslator动态加载,需要把生成的 .qm 文件放到正确路径。很多人的习惯是把翻译文件放在 exe 同目录的translations子目录下,打包时要注意把整个目录带进去。
还有一点:Qt 自带的控件翻译文件是 qtwidgets_zh_CN.qm。如果你用--no-translations参数运行了 windeployqt,记得手动从 Qt 安装目录的 translations 子目录里拷贝一份,否则你用了 QFileDialog、QMessageBox 这类内部控件,按钮文字会全部变成英文。
9. 最后的经验之谈
打包工具再多,回到第一性原理,无非是解决三个问题:依赖完整、位置正确、入口清晰。
依赖完整靠 windeployqt 和人工检查;位置正确靠发布目录结构统一;入口清晰靠安装包脚本或单文件封装。把这三件事理顺了,换哪个工具都只是换个脚本语法而已。
我个人的建议是:先花半天时间把 Inno Setup 学会,然后固定下来作为 Windows 发布的主力方案;再花一小时学会 Enigma Virtual Box,作为快速分发和现场演示的应急方案;QIF 只在你明确需要“在线升级、组件安装、多产品整合”时再研究。
踩过几轮坑之后,你会发现自己越来越不纠结工具本身,而是更关心发布流程能不能自动化、安装包能不能稳定复现、客户侧问题能不能快速定位。那才是打包这件事真正的价值所在。