简介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安装包那样好看”于是研究了一阵子 NSISDuilib 的组合方案最后封装出一个既能继承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 BOMDuilib要求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脚本一行不用动长期维护下来非常省心。本文还有配套的精品资源点击获取