简介:这份安装程序制作资料面向希望基于 NSIS 与 Duilib 开发 Windows 自定义安装界面的开发者,重点解决传统安装包界面简陋、交互与品牌风格难以统一的问题,也适合已有脚本基础、想进一步掌握皮肤集成与多语言安装流程的中级读者。压缩包共 451 个文件,约 9.6MB,包含 nsi/nsh 安装脚本、xml 界面布局、png/ico/bmp 皮肤图片、nlf 多语言文件、dll 运行库以及 bat 构建批处理等,覆盖界面资源、脚本基础和编译辅助材料。已有 2945 人学习,说明该主题在安装包定制场景中具有一定参考价值。读者可从中获得一套可拆解的安装界面资源组织方式,结合 Duilib 皮肤与 NSIS 安装逻辑理解快捷方式、注册表、多语言和构建脚本的配合关系,并借助示例资源与文档排查集成问题。
1. NSIS 加 Duilib 组合:为什么明明装好了却跑不起来
上个月帮一个做桌面工具的朋友排查安装包问题,现象很典型:NSIS 脚本编译通过,Duilib 的界面在开发机上双击一切正常,打包进安装程序后,进度条走到一半界面直接白掉,或者干脆闪退,日志里什么都没有。翻来覆去看了半天才发现,问题不在业务代码,而在环境本身——Duilib 依赖的那几个运行时库版本没对齐,NSIS 的插件目录少放了一个 dll,编译位数也没匹配。这类“编译通过、运行翻车”的情况,在 NSIS 加 Duilib 自定义安装程序这套组合里出现的频率比想象中高得多。
NSIS 负责安装流程,脚本、插件、文件释放、注册表、卸载逻辑都是它的活;Duilib 负责把默认那种灰扑扑的安装向导换成自己设计的界面。两者拼在一起,等于同时踩在脚本编译和 C++ 界面库两条链路上,任何一条链路的版本、位数、依赖没对齐,最后都会在用户机器的安装环节集中爆发。这套东西适合谁?适合那些不满 NSIS 默认界面、又不想换 Inno Setup 或 WiX 的桌面软件团队,尤其是需要品牌化安装向导、想在安装页里塞自定义控件和交互的场景。接下来的内容按“先备环境、再编译、再对接、最后验证”的顺序走一遍,每一步都能照抄。
2. 环境准备:NSIS 3.x 与 Duilib 的版本取舍和依赖清单
动手之前先把版本和位数定死,这一步省下的时间,后面能少踩一半的坑。NSIS 这一侧建议锁定 3.08 或 3.09 这类 3.x 主线版本,2.x 虽然还到处能见到,但 Unicode 支持和插件接口上差别不小,Duilib 对接时容易在字符串编码上出问题。Duilib 本身是源码库,没有“安装”这回事,真正要装的是它的编译环境:Visual Studio 的 C++ 桌面开发工作负载,以及它依赖的 Windows SDK。位数上要统一,安装程序是 32 位就全用 32 位编,别一边 x64 编 Duilib、一边用 32 位 NSIS 打包。
2.1 版本对照与安装包获取
先把三样东西的版本关系理清楚,这是后面所有步骤的前提。
| 组件 | 建议版本 | 位数 | 作用 | 备注 |
|---|---|---|---|---|
| NSIS | 3.08 / 3.09 | 32 位 | 安装脚本编译与打包 | 用 Unicode 版,避免中文乱码 |
| Visual Studio | 2019 / 2022 | — | 编译 Duilib 源码 | 勾选“使用 C++ 的桌面开发” |
| Windows SDK | 10.0.19041 及以上 | — | 提供 Win32 API 头文件与库 | 跟 VS 安装器一起装即可 |
| Duilib | 仓库主线源码 | 与安装程序一致 | 自定义安装界面 | 只取源码,不取预编译产物 |
NSIS 从官网下载安装包,装的时候有个细节容易被忽略:安装路径里不要带空格和中文。我一般固定装到C:\NSIS,这样后面脚本里引用${NSISDIR}和插件路径时不用处理转义。装完在命令行敲makensis /VERSION,能回显版本号才算装对。
Duilib 这边,把源码放到一个短路径下,比如D:\code\duilib,路径越短越好,MSBuild 对长路径的容忍度不高。源码里通常带一个DuiLib_Static或DuiLib的工程,具体名字取决于你拿的是哪份分支,打开后先别急着编,确认工程属性里的平台工具集和你的 VS 版本一致。
注意:网上流传的一些 Duilib 整合包会把第三方库一起塞进来,看着省事,实际很容易和你的 VS 版本对不上,报一堆 LNK2019。我一般只取纯源码,依赖自己补,可控。
2.2 依赖库和编译前检查
Duilib 的静态库依赖并不复杂,主要是 Windows 自带的几个系统库,工程属性里一般已经配好,但有几个点需要手动确认。打开工程属性,看“链接器 → 输入 → 附加依赖项”里有没有comctl32.lib、ole32.lib、oleaut32.lib、imm32.lib、winmm.lib这几项,缺哪个补哪个。这些库分别对应通用控件、COM 初始化、OLE 自动化、输入法支持和多媒体计时,Duilib 的控件渲染和消息循环都绕不开。
同时确认字符集设置。工程属性里“高级 → 字符集”要选“使用 Unicode 字符集”,和 NSIS 的 Unicode 版保持一致。如果不一致,界面上的中文可能变成问号,或者安装页的标题直接乱码,这种问题在运行期才暴露,排查起来很费劲。
编译之前还有一步:把 Duilib 的工程配置成静态库(.lib)输出,而不是动态库。静态链接能把依赖收进安装程序自己的 dll 里,用户机器上少一个找不到 dll 的风险点。做法是在“配置属性 → 常规 → 配置类型”里选“静态库(.lib)”,然后分别对 Debug 和 Release 各编一次,实际打包只用 Release。
编译命令可以直接在开发者命令行里走,避免打开 IDE 的图形界面:
# 进入 Duilib 源码目录,用 MSBuild 编译 Release 静态库 msbuild DuiLib.sln /p:Configuration=Release /p:Platform=Win32 /m # 编译完成后确认产物存在 dir DuiLib\Release\*.lib/p:Configuration=Release指定发布配置,/p:Platform=Win32强制 32 位,/m允许并行编译加快速度。产物一般在Release或bin子目录下,文件名类似DuiLib.lib。如果编译报错说找不到atlbase.h之类,说明 ATL 组件没装,回 VS 安装器里补上“C++ ATL”这一项。这一步编不过,后面全是空谈,所以务必先让这个 lib 文件实实在在躺在磁盘上。
3. Duilib 界面工程编译:从源码到可被 NSIS 调用的 dll
Duilib 静态库编出来只是半个身子,真正要被 NSIS 调起来,还得写一个宿主 dll,把界面创建、消息循环、和 NSIS 插件之间的通信包起来。这一章的重点是把“能编译”推进到“能被 NSIS 加载”。
3.1 工程配置与入口函数改造
新建一个 Win32 DLL 工程,命名成InstallUI之类,把 Duilib 的头文件目录和刚编出来的 lib 加进工程依赖。然后按 NSIS 插件的规范导出函数。NSIS 插件本质是一个导出特定签名函数的 dll,最常用的是NSISPluginInit和消息处理相关的导出,具体导出哪些取决于你打算在哪个安装页调用它。
一个最小可用的导出函数长这样:
// InstallUI.cpp - NSIS 插件入口,负责弹出自定义安装界面 #include "StdAfx.h" #include "DuiLib/UIlib.h" extern "C" __declspec(dllexport) void __stdcall ShowInstallUI(HWND hParent, int /*string_size*/, wchar_t* /*variables*/, wchar_t* /*stacktop*/) { // 初始化 COM,Duilib 的窗口和部分控件依赖它 ::CoInitialize(NULL); // 创建 Duilib 的窗口管理器并加载 XML 皮肤 CWindowManager* pManager = new CWindowManager(); if (pManager->LoadSkin(L"install_ui.xml")) { pManager->CreateMainWindow(hParent); // 挂到 NSIS 父窗口下 pManager->RunMessageLoop(); // 阻塞式消息循环 } ::CoUninitialize(); }extern "C"保证导出名不被 C++ 编译器修饰,否则 NSIS 按名字找不到函数。__stdcall是 NSIS 插件约定的调用约定,漏了它会直接崩在调用点。CoInitialize不能省,Duilib 里用到的一些控件和图片解码依赖 COM。hParent传的是 NSIS 安装窗口句柄,挂上去之后界面才算“嵌在”安装流程里,而不是弹一个独立窗口。LoadSkin的路径要相对于 dll 所在目录,或者用绝对路径,路径写错的表现是界面一片空白,不报错。
编译这个 dll 时同样锁定 Win32 和 Release,输出位置放在 NSIS 插件目录的旁边,方便后面打包引用。工程属性里记得把“C/C++ → 代码生成 → 运行库”设成“多线程(/MT)”,和 Duilib 静态库保持一致,否则会出现 CRT 版本冲突导致的堆损坏,这种崩溃往往随机出现,属于典型黑匣子问题。
3.2 皮肤 XML 与资源文件的组织
Duilib 的界面是用 XML 描述的,这份 XML 和配套的图片资源需要跟 dll 一起分发。目录结构我一般这么排:
InstallUI/ ├── InstallUI.dll # 编译产物 ├── install_ui.xml # 界面布局描述 └── res/ ├── bg.png # 背景图 ├── btn_next.png # 按钮常态 └── btn_next_hover.png # 按钮悬停态XML 里的资源路径统一写相对路径,比如res/bg.png,不要写D:\...这种绝对路径,用户机器上不存在你的开发目录。XML 本身用 UTF-8 编码保存,第一行带上<?xml version="1.0" encoding="utf-8"?>,中文文本才不会变成方块。图片资源尽量压一下,安装包体积敏感的场景,一张 1920 宽的背景图就能让安装包涨好几兆,用 png 压缩工具过一遍是常规操作。
资源清单要在 NSIS 脚本里用File指令一一列出来,不能靠通配符一把梭,通配符在跨目录时会漏文件。这一步列全了,后面验证阶段才不用反复补。
4. NSIS 脚本对接:插件调用、文件释放与安装流程编排
到这一章,dll 有了、资源齐了,剩下的是让 NSIS 把这个自定义界面接进安装流程,并且保证文件释放、注册表写入、卸载信息这些常规动作不出岔子。
4.1 插件声明与调用位置
NSIS 调用插件用installui::ShowInstallUI这种形式,前面是 dll 名,后面是导出函数名。前提是把 dll 放到 NSIS 安装目录的Plugins子目录下,或者在脚本里用!addplugindir指定目录。我倾向后者,工程自包含,换台机器不用重新往 NSIS 目录里拷东西。
; 指定插件所在目录,相对当前脚本 !addplugindir ".\plugin" ; 在欢迎页之后插入自定义界面 Function .onGUIInit ; 先让默认流程走完初始化 Call ShowCustomUI FunctionEnd Function ShowCustomUI ; 调用 InstallUI.dll 中导出的 ShowInstallUI installui::ShowInstallUI FunctionEnd!addplugindir是编译期指令,告诉 makensis 去哪找插件 dll,路径相对脚本文件本身。installui::ShowInstallUI是运行期调用,参数按导出函数的签名传,上面这个例子是无参形式,如果有参数要按 NSIS 插件规范的顺序压栈。调用位置很关键,放在.onGUIInit里能保证 NSIS 的主窗口已经创建,hParent有值;放在更早的阶段,父窗口句柄是空的,界面挂不上去。
注意:插件调用失败时 NSIS 不会给很明确的提示,常见表现是安装程序一闪而过或者卡在某一页。排查办法是在调用前后加
DetailPrint打日志,配合LogSet开日志文件,能定位到具体是哪一步没返回。
4.2 文件释放与卸载信息写入
自定义界面接管了交互,但文件释放还是得靠 NSIS 的File和SetOutPath。这里有个顺序问题:界面里如果做了“选择安装路径”的控件,路径结果要通过插件回传给 NSIS 变量,再在SetOutPath里用。回传的实现方式是在导出函数里操作 NSIS 的栈,或者更简单些,插件把路径写到一个临时文件,NSIS 用FileRead读回来。前者更规范,后者更好调试,看团队习惯选。
Section "Install" SetOutPath "$INSTDIR" ; 主程序与依赖,逐个列出便于排查缺失 File "build\app.exe" File "build\core.dll" File "build\config.ini" ; Duilib 界面资源,路径与 dll 内约定一致 SetOutPath "$INSTDIR\InstallUI" File "plugin\InstallUI.dll" File "plugin\install_ui.xml" SetOutPath "$INSTDIR\InstallUI\res" File "plugin\res\*.png" ; 写卸载信息,缺了它控制面板里删不掉 WriteUninstaller "$INSTDIR\uninst.exe" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp" "DisplayName" "MyApp" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp" "UninstallString" "$INSTDIR\uninst.exe" SectionEndSetOutPath改变后续File的释放目标,切换目录时一定要重新设一次,漏了会把文件全倒进上一个目录。WriteUninstaller生成卸载程序,必须在安装段里调用,放在界面流程之后更稳妥,避免用户中途取消留下半截卸载器。注册表项的DisplayName和UninstallString是控制面板识别卸载入口的最低要求,少了这两项,用户只能手动删目录。
编译脚本用命令行:
# 编译 NSIS 脚本,输出安装包 makensis /V3 /DINSTALL_DIR="C:\Program Files\MyApp" installer.nsi/V3是详细输出级别,编译时能看到每个 Section 的处理过程,出问题第一时间有线索。/D用来在编译期定义宏,脚本里用${INSTALL_DIR}引用,方便切换不同构建目标。
5. 常见问题排查:编译过了但安装包跑不起来的五种情况
这一章全是血泪经验,每条都按现象、原因、解决来。
现象一:安装程序启动后界面空白,只有边框。原因通常是 XML 皮肤没加载成功,要么路径不对,要么 XML 里有语法错误导致解析中断。解决:把LoadSkin的返回值打日志,确认文件是否被找到;用浏览器打开 XML 看是否能正常解析,标签没闭合是最常见的语法问题。
现象二:点“下一步”没反应,或者整个界面卡死。原因多半是消息循环被阻塞,常见于插件里做了同步的网络请求或大文件拷贝。解决:把耗时操作放到子线程,界面线程只负责渲染和消息分发;或者在 Duilib 的消息循环里用定时器分片处理。
现象三:开发机正常,用户机器上闪退。原因是运行时库缺失或位数不匹配,比如用户机器没有对应版本的 VC 运行库,或者安装程序是 32 位而插件 dll 编成了 64 位。解决:统一全链路位数,运行库用静态链接(/MT)收进 dll;发布前在一台干净虚拟机上跑一遍,这是最有效的兜底。
现象四:中文全部变成乱码或问号。原因是编码链路不一致,NSIS 用了 ANSI 版,或者 XML 存成了 GBK。解决:NSIS 换 Unicode 版,XML 统一 UTF-8 并带 BOM 或不带 BOM 保持团队一致,源文件里的中文字符串用L""宽字符。
现象五:卸载后残留文件和注册表项。原因是WriteUninstaller调用时机太早,或者卸载段里漏了Delete和DeleteRegKey。解决:把写卸载器放到安装流程末尾,卸载段按安装段的反向顺序逐项清理,注册表用DeleteRegKey删整个子树而不是逐个值。
6. 装完怎么验证:三步确认这套安装程序真的能交付
环境装完、脚本编过,最怕的是“我这能跑”就交出去。我现在的习惯是固定走三步验证,缺一步都不算完。
第一步,干净环境实测。找一台没装过 VS、没装过任何运行库的虚拟机,把安装包拷进去双击。重点看三个点:界面能不能正常渲染、进度能不能走完、装完的程序能不能启动。这一步能拦掉八成的依赖缺失和位数问题。虚拟机快照留一个干净的,每次发版前重置一遍再测,比在开发机上反复卸载重装靠谱得多。
第二步,日志核对。NSIS 编译时加/V3只是编译日志,运行期日志要靠在脚本里开:
; 在 .onInit 里开启安装日志,输出到临时目录 Function .onInit LogSet on StrCpy $0 "$TEMP\install_log.txt" LogText "install start: $0" FunctionEndLogSet on打开日志开关,LogText往日志里写自定义内容,配合每个 Section 前后的DetailPrint,装完之后打开日志文件,能清楚看到每个文件的释放结果和每一步的返回值。插件调用前后各打一条,哪一步没回来一目了然。
第三步,卸载回归。装完不卸载不算验证完,手动跑一遍卸载,然后检查三处:安装目录是否清空、注册表Uninstall子树是否删除、开始菜单和桌面快捷方式是否消失。这一步经常被跳过,结果用户卸载后残留一堆文件,回头还得单独发补丁。
提示:验证阶段把安装包放到一个带空格和中文的路径下再跑一遍,比如
D:\测试 目录\,很多路径拼接的 bug 只在这种环境下暴露。
从那以后我每次出安装包,都强制走一遍“干净虚拟机 + 日志核对 + 卸载回归”这三步,哪怕改动只有一行脚本。这套动作花不了二十分钟,但省下的返工和用户投诉远不止这点时间。希望这套流程能帮到你,少在环境上翻车。
本文还有配套的精品资源,点击获取