简介:NSIS与Duilib组合实现的一套仿QQ风格安装程序工程,面向Windows桌面应用开发者,也适合想摆脱传统向导式界面、学习自定义安装交互的中高级学员。压缩包约69.53MB,共206个文件,文件类型以Duilib界面库的45个.h与41个.cpp为主,另有26个png界面切图、NSIS安装脚本(.nsi/.nsh)、Visual Studio解决方案及插件工程,整体便于阅读、编译和二次改造。目前已有1843人学习下载。工程目录划分清晰,包含第三方依赖库、示例工程、NSIS插件和include头文件等模块,从界面到脚本均有完整演示;它展示了如何将Duilib自绘界面嵌入NSIS安装流程,并模拟腾讯QQ安装包的视觉风格与交互节奏,方便按需裁剪。通过研读源码和样例,可以掌握安装界面换肤、进度反馈、文件释放、系统环境配置等关键做法,并据此打造具有品牌感与良好体验的Windows安装包。 做过Windows客户端开发的朋友,应该都有过被安装包界面折磨的经历。默认的NSIS向导样式还是上个世纪的风格,拿去做产品演示分分钟被业务方吐槽“不够专业”。我之前接了内部工具分发的需求,领导点名要“做得像QQ安装包那样好看”,于是研究了一阵子 NSIS+Duilib 的组合方案,最后封装出一个既能继承NSIS强大的安装脚本能力、又能用Duilib绘制现代化界面的安装包模板。这篇就把这个方案完整拆开讲透。
这个方案的思路其实不复杂:NSIS负责干安装的脏活累活,Duilib负责把脸面撑起来。NSIS是Windows下老牌的开源安装包制作工具,脚本能力极强,生态成熟;Duilib则是开源界做DirectUI界面的老牌选手,国内很多桌面客户端的界面都是这种技术路线。两者结合,等于让安装包既有了强悍的“内芯”,又有了精致的“外表”。下面从方案选型、界面实现、脚本编写到联调踩坑,一条线讲清楚。
1. 项目缘起与方案选型
1.1 需求背景
这个需求其实很常见:公司内部有个工具软件要对外分发,老版本的安装包用的是NSIS默认向导,每隔一阵子就被销售同事吐槽“界面太土”。领导提了三个硬性要求:第一,界面要现代化,参考主流即时通讯软件的安装体验,说白了就是“像QQ安装包那样”;第二,安装逻辑要灵活,支持自定义安装路径、组件勾选,装完要有桌面快捷方式和卸载入口;第三,产物体积要小,不能为了界面好看就拖一个几十MB的运行时进去。
1.2 方案对比与选型理由
我列了几种方案做过对比:
| 方案 | 界面自定义能力 | 体积/运行时依赖 | 开发成本 | 结论 |
|---|---|---|---|---|
| Inno Setup + Pascal | 中等,可改但复杂 | 安装器约1MB | 自定义华丽界面费力 | 放弃 |
| C# / WPF | 很强 | 依赖.NET运行时 | 需要提前装运行时 | 放弃 |
| NSIS + 传统插件 | 弱,仅改控件样式 | 小 | 达不到QQ界面效果 | 放弃 |
| NSIS + Duilib | 很强,完全自绘 | 约1-2MB | 集成有一定门槛 | 采用 |
这个选型的关键点在于:NSIS脚本本身轻巧,编译出来的安装器只有几百KB,业务方接受的成本低;Duilib只要编一个DLL加一堆XML皮肤文件,整体体积可控;而且Duilib本来就是为桌面客户端而生,画QQ风格的界面属于它的老本行,控件布局全部由XML控制,后续想改文案、调按钮颜色,不用重新编译DLL,改配置文件就行,这对要配合业务方反复改样式的场景非常友好。
1.3 整体架构设计
整个安装程序我拆成了三层:
- 安装引擎层:NSIS脚本,负责文件释放、注册表写入、快捷方式创建、卸载逻辑。
- 界面层:Duilib DLL,内部包含XML定义的安装向导窗口,有欢迎页、路径选择页、进度页、完成页。
- 通信层:NSIS通过System插件直接调用DLL的导出函数,Duilib DLL以模态方式运行,用户操作完毕后再把结果返回给NSIS脚本。
这个架构下,NSIS仍然是安装程序的主体,Duilib只是作为一个“高级界面前端”存在。这样做的好处是:NSIS生态里已有的安装逻辑、插件、宏定义都能直接复用,不用自己造轮子;Duilib窗口关闭后,控制权完整交回NSIS,流程清晰,出问题也好定位。
2. Duilib前端:还原“QQ风”安装界面的实现要点
2.1 界面布局与XML设计
QQ安装包界面的核心特征我总结下来就四点:左侧或顶部有产品宣传图,右上区有“快速安装/自定义安装”的切换,底部是醒目的主按钮(绿色“立即安装”),安装过程中有直观的进度反馈。用Duilib实现这套布局,核心工作量在XML皮肤文件上。
我的XML结构大致是这样:窗口根节点用一个VerticalLayout,顶部放Banner图片控件,中间主体用HorizontalLayout分左右两栏,左侧放产品介绍文字或轮播图,右侧放路径编辑框和“浏览”按钮;底部再放一个HorizontalLayout,右下角放主安装按钮。控件层级清楚之后,后续调整间距、字体、颜色,全部在这个XML里改。
需要注意,Duilib的窗口是分层自绘的,字体渲染、按钮状态、圆角矩形的绘制都由渲染引擎完成,因此XML里要显式指定字体资源。为了跟QQ风格靠近,我直接用了微软雅黑,大小分成三级:标题18px、正文12px、辅助说明9px。所有颜色的定义集中放在XML的Global节点里,方便统一换主题。
2.2 DLL导出的核心接口
Duilib DLL不是独立运行的,它要被NSIS当做一个“黑盒”调用。我导出的核心函数设计如下:
extern "C" __declspec(dllexport) int WINAPI ShowInstallUI( HWND hParent, // 父窗口句柄,实际没用,保留兼容 const wchar_t* ini, // 配置文件路径,用于初始化默认路径和组件状态 const wchar_t* appName // 产品名称,用于窗口标题和提示文案 )函数内部创建Duilib窗口并进入模态消息循环,用户在界面上点击“立即安装”“取消”等操作后,窗口销毁,函数返回对应的结果码。比如我用0表示取消,1表示开始安装,2表示用户选择的是“自定义安装”选项。NSIS拿到返回值后,再决定走快速安装分支还是自定义安装分支。
这里有一个很关键的设计:安装进度条的更新。我的做法是让NSIS在释放文件过程中,通过Windows消息向Duilib窗口发送自定义的WM_APP + 100消息。所以我在Duilib DLL内部维护一个全局窗口指针,导出一个SetProgress(HWND hDlg, int percent, const wchar_t* hint)函数,NSIS通过System::Call把进度百分比和提示文字传进来。这样进度条就能实时反映文件释放的进度,而不是一个只会转圈的假动画。
2.3 DPI与界面适配的细节
这个坑我一开始没注意,后来在2K分辨率的机器上一跑,窗口直接糊成一团。Duilib本身对高DPI支持不算好,默认按96dpi设计,系统缩放是125%或150%时,控件位置会错乱。
解决办法是在DLL入口处手动处理DPI:
- 启动时调用
SetProcessDPIAware让进程感知系统DPI,避免系统对窗口做位图拉伸,这个对自绘窗口尤其重要,否则字会发虚。 - 在创建窗口时用
GetDpiForWindow获取当前DPI,动态调整窗口的初始宽高和字体字号。
如果业务对适配要求不高,有个取巧做法:直接按150%的DPI固定设计窗口,允许小字屏显示略小一点点,绝大多数用户能接受。但如果团队有专门的UI走查,还是老老实实做动态缩放。
3. NSIS后端:安装逻辑与脚本编写
3.1 NSIS脚本的整体结构
NSIS脚本虽然语法简单,但组织方式直接影响后续维护。我习惯的脚本骨架是这样的:
!include "MUI2.nsh" !include "LogicLib.nsh" !include "FileFunc.nsh" !include "x64.nsh" Name "${APP_NAME}" OutFile "MySetup_${VERSION}.exe" RequestExecutionLevel admin InstallDir "$PROGRAMFILES64\MyCompany\MyApp" !define APP_NAME "MyApp" !define VERSION "1.0.0" !define DUILIB_DLL "duilib_installer.dll"重点说几个参数:
RequestExecutionLevel admin:安装程序必须以管理员权限运行。我们产品要写注册表、装到Program Files,普通权限会直接失败。这个必须写在脚本前面,否则生成的安装包双击时会弹UAC,一旦用户取消,后面全是白搭。InstallDir:默认安装路径。为了模拟QQ那套“可以改路径”的交互,我会在Duilib端把编辑框的初始值设置成这里,用户改动后通过INI文件回传。
3.2 与Duilib DLL的通信机制
这是整个方案里最容易踩坑的部分。NSIS脚本里通过System::Call调用DLL,写法如下:
Section "Install" ; 前面初始化变量,然后调用Duilib窗口 StrCpy $0 "$INSTDIR" System::Call "${DUILIB_DLL}::ShowInstallUI(w, w, w) i (0, \"$PLUGINSDIR\config.ini\", \"$0\") .r1" ; $1 里就是DLL返回的结果码 ${If} $1 == 0 Quit ${EndIf} SectionEnd这里的执行模型是:NSIS执行到这行System::Call时,会同步等待DLL内部的模态窗口消息循环结束。也就是说,Duilib窗口关闭时这个函数才返回。这个特性非常重要,它意味着NSIS脚本不需要额外做异步处理,代码顺序就是安装流程顺序,心智负担很小。
进度条的通信则反过来,在NSIS的文件释放循环里更新:
!define WM_APP_PROGRESS 1024 Function UpdateProgress IntOp $3 $2 * 100 IntOp $3 $3 / $4 System::Call "user32::SendMessage(i, i, i, i) i ($5, ${WM_APP_PROGRESS}, $3, \"$1\")" FunctionEnd$5是Duilib窗口的句柄,这个句柄我会在页面初始化时通过另一个DLL导出函数GetInstallWnd()获取,作为全局变量保存。NSIS的变量都是字符串类型,传整数参数时要注意System::Call的格式匹配,传字符串必须加引号,否则DLL收到的是指针地址而不是内容。这个细节我调试时花了不少时间,后面在常见问题里会再提。
3.3 安装动作拆解与注册表、快捷方式处理
安装逻辑按标准流程来,我分成几个小段,每段一个职责:
- 释放主程序文件到安装目录
- 释放依赖DLL和资源文件
- 写入卸载注册表项
HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp - 创建桌面快捷方式和开始菜单快捷方式
- 可选:创建开机启动项(通过界面上的勾选框控制)
注册表卸载项的写法比较固定:
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp" "DisplayName" "MyApp" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp" "DisplayIcon" "$INSTDIR\MyApp.exe" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp" "UninstallString" "$INSTDIR\Uninstall.exe" WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp" "NoModify" 1 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp" "NoRepair" 1关于快捷方式,这里有个跨版本兼容的细节:老代码喜欢用CreateShortCut直接写开始菜单路径,但不同Windows版本开始菜单目录名可能不一样。稳妥做法是用$SMPROGRAMS变量,NSIS会自动映射到正确的路径。桌面快捷方式同理用$DESKTOP。
卸载程序我用NSIS自带的卸载器模式,在安装段末尾WriteUninstaller "$INSTDIR\Uninstall.exe",然后单独写一个Section "Uninstall",里面做反向操作:删文件、删注册表项、删快捷方式。这里要强调,卸载时不要图省事直接RMDir /r "$INSTDIR",万一用户自己往安装目录里塞了个人文件,会被一并清掉,风险太大。我一般只删除已知的目录列表,再判断目录为空才删。
4. 集成调试与实战避坑记录
4.1 联调流程与调试技巧
联调阶段最常见的困境是:Duilib DLL报错崩溃,但NSIS脚本没法像Visual Studio那样直接断点调试。我摸索出一套高效的排查流程:
首先,在Duilib DLL项目里加一个测试入口,比如导出RunStandalone函数,用一个普通EXE直接加载这个DLL,就可以在IDE里单步调试界面逻辑,跟纯Duilib项目开发体验一样。界面验证通过后,再走NSIS联调。
其次,在NSIS脚本里加足够的日志:
Section LogSet on DetailPrint "开始调用Duilib窗口" System::Call ... DetailPrint "返回值=$1" SectionEnd把进度日志输出到install.log,出问题时先看日志,能定位到是脚本问题还是DLL问题,避免双方互相推诿。
还有一个很重要的技巧:NSIS的System::Call调用DLL时,如果DLL还没有释放到本地,调用会失败。所以脚本里一定要先用File /oname=$PLUGINSDIR\duilib_installer.dll把DLL释放到临时目录,再从$PLUGINSDIR调用。直接引安装目录里的DLL会失败,因为那时文件还没解压。
4.2 常见问题与解决方案
| 问题 | 现象 | 根因与解决办法 |
|---|---|---|
| 回调函数收不到进度消息 | 进度条一直0% | 窗口句柄获取时机不对,NSIS在调用DLL时窗口还没创建完成,要先通过GetInstallWnd拿到句柄再发消息 |
| 中文显示乱码 | 按钮文案显示方块 | XML文件没有保存为UTF-8 with BOM,Duilib要求XML必须带BOM才能正确解析中文 |
| 杀毒软件误报 | 安装包被拦截 | 没有数字签名。内部分发可以申请测试证书,对外必须上正式的代码签名证书 |
| 管理员权限弹窗 | 双击后总是弹UAC | 某些开发机策略限制。正式对外还是保留UAC,但对内测试可以临时RequestExecutionLevel user |
| 路径包含空格 | 安装失败 | Duilib端回传路径时没有正确加引号,NSIS拼接命令行时路径要用"..."包裹 |
从这些坑里总结的规律是:NSIS脚本和Duilib DLL各自单测都没问题,问题往往出在两者交互的边界上,比如字符串编码、窗口句柄生命周期、参数类型不匹配。联调时重点盯这三点,能省很多时间。
4.3 体积与性能优化
NSIS默认使用zip压缩,对于已压缩过的DLL帮助不大。我最后的优化手段是:
- Duilib DLL编译时使用
/MT静态链接运行时,避免依赖VC运行时,同时能压掉几百KB。 - XML皮肤文件做一次精简,去掉用不到的控件定义和冗余的属性字段。
- 产品宣传图统一转成JPG压缩格式,尽量不要放PNG大图。QQ安装包那张左侧大图看着精致,实际用的是压缩率很高的JPG。
- NSIS用
SetCompressor /SOLID lzma整包压缩,实测DLL加皮肤加安装逻辑,最后生成的安装包控制在1.8MB以内,完全符合预期。
速度方面,安装过程大头在主程序文件的复制,我实测一个50MB的主程序,机械硬盘上大约6秒完成,SSD上2秒左右。由于进度消息本身是通过SendMessage同步发送的,不会有队列堆积,但要注意不要在消息里做重活,否则会卡UI线程。
4.4 素材版权与合规提醒
最后提一个容易被忽视的点:仿QQ安装包指的是交互方式和视觉风格,不能直接拿QQ安装包的图片、图标、Logo素材来用。我用的是自己设计的Banner图和图标,只是排版风格靠近主流即时通讯软件。企业内部自用问题不大,如果要对外发布商业产品,一定要确认素材版权归属,别给自己挖坑。
这个方案做完整套流程后,我最大的体会是:表面上是给安装包换了个皮肤,实际上是打通了“脚本引擎+自绘UI”这条组合路线。NSIS负责稳定可靠,Duilib负责好看好用,两者各干各擅长的活,分工明确。这套模板后来被部门复用了几次,换产品名、换Banner图、调整组件选项,十分钟就能生成一个新安装包,实用性很高。
最后再分享一个小技巧:如果后续有多个安装包需求,建议把Duilib DLL做成一个独立项目长期维护,版本号跟着安装器的版本走,不要每次拷贝源码改。这样DLL升级时只需要替换文件,NSIS脚本一行不用动,长期维护下来非常省心。
本文还有配套的精品资源,点击获取