简介NSIS 与 Duilib 结合的仿 QQ 安装包工程面向需要自定义 Windows 安装程序的开发者旨在解决安装包界面简陋、交互生硬的问题。工程将 NSIS 的脚本化安装逻辑与 Duilib 的高质量 UI 结合模仿 QQ 安装流程帮助读者快速掌握安装界面定制与安装行为设计。资源共 206 个文件约 69.53MB以 h/cpp 源码、png 界面素材、obj/tlog 编译输出、sln/vcxproj 工程文件为主体同时包含 nsi/nsh 脚本、NSIS 插件及相关第三方库压缩包目录分为 3party、samples、plugin、include结构清晰便于按模块查阅。目前已有 1869 人学习下载。通过该工程可系统了解 NSIS 脚本编写、Duilib 控件布局与窗口消息处理以及两者在安装流程中的协作方式压缩包内附示例代码、插件和第三方库适合动手复现也可作为二次开发的起点尤其适合从零搭建安装程序界面的入门者。工程还附带编译中间文件和调试符号方便对比静态库与源码逻辑进一步降低阅读门槛。1. NSISDuilib仿QQ安装包为什么我放弃MFC和高价商业工具给内部工具做安装包时我一开始用的是 MFC 对话框套壳丑且难维护后来试过商业安装工具授权费又贵得离谱。直到把 NSIS 脚本和 Duilib 界面库拼在一起才做出那种“下一步、下一步”的丝滑向导外观和交互都向主流即时通讯软件的安装包看齐。这个组合的核心思路是用 NSIS 负责文件释放、注册表写入和快捷方式创建用 Duilib 接管窗口渲染和用户交互两者通过自定义插件或命令行参数通信。适合给 Windows 平台工具做定制安装界面的开发者也适合想把安装流程做成品牌化而非千篇一律的团队。这篇笔记围绕实际工程展开参数和坑都按我遇到的版本写清楚。2. 选型拆解NSIS 脚本能力和 Duilib 界面渲染怎么分工2.1 NSIS从临时脚本到发布级安装程序的边界NSIS 本质是一个脚本驱动的安装程序生成器它把压缩后的文件数据和安装逻辑打包进一个 exe。很多人误以为 NSIS 只能做那种带系统默认界面的安装包其实它的脚本语言支持页面自定义、节Section、函数、宏定义和插件调用。我在实践中把它当作一个“执行引擎”而不是界面框架。典型的多节安装逻辑长这样!include MUI2.nsh !include x64.nsh Name MyTool Setup OutFile dist\\MyTool_Setup.exe InstallDir $PROGRAMFILES64\\MyTool !define MUI_ABORTWARNING !define MUI_ICON res\\install.ico !insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH !insertmacro MUI_LANGUAGE SimpChinese Section MainApp SecMain SetOutPath $INSTDIR File /r build\\*.* WriteUninstaller Uninstall.exe WriteRegStr HKLM Software\\MyTool InstallPath $INSTDIR SectionEnd Section Uninstall SecUn Delete $INSTDIR\\Uninstall.exe RMDir /r $INSTDIR DeleteRegKey HKLM Software\\MyTool SectionEnd这段脚本里!include MUI2.nsh引入现代用户界面库x64.nsh用来处理 64 位系统下的注册表和路径重定向。File /r build\\*.*表示把 build 目录下所有文件递归打进安装包。WriteRegStr写的是 32 位视角的注册表位置如果目标机器是 64 位系统且安装程序没有做SetRegView 64写入位置会被系统自动重定向到 Wow6432Node。这个细节非常容易踩坑后面专门说。NSIS 脚本的变量分为用户变量和全局变量$INSTDIR是安装目录$PROGRAMFILES64是 64 位程序目录。如果你希望用户能改安装路径就用MUI_PAGE_DIRECTORY让用户在界面上选选完的值会存回$INSTDIR。在工程里我不会把所有逻辑堆在一个 nsi 文件里而是拆成env.nsh、paths.nsh、sections.nsi三份通过!include引入。这样当安装包需要同时支持多个子组件时只需在sections.nsi里增加节不动公共逻辑。2.2 Duilib 的接入方式DLL 封装与消息泵Duilib 是一个直接基于 Win32 的界面库它的核心思路是把控件和布局用 XML 描述然后用一个窗口类接管消息循环。直接把它接进 NSIS 是不行的因为 NSIS 生成的安装程序有自己的窗口和消息循环两者会抢焦点。常见做法是把 Duilib 界面封装成一个独立的 DLL由 NSIS 在自定义页面里加载。这个 DLL 内部要做三件事第一创建窗口和消息循环。第二暴露一组 C 接口比如ShowInstallUI(HWND parent, int width, int height)。第三把安装进度通过回调或者共享内存传给主程序。extern C __declspec(dllexport) int ShowInstallUI(HWND parent, int nWidth, int nHeight) { CPaintManagerUI paintManager; CWindowWnd* pWnd new CWindowWnd(); pWnd-Create(parent, LInstallUIClass, WS_CHILD, WS_EX_TOOLWINDOW); pWnd-SetWindowText(L); HWND hWnd pWnd-GetHWND(); SetWindowPos(hWnd, NULL, 0, 0, nWidth, nHeight, SWP_NOZORDER); // 加载 XML 布局 CResourceManager::GetInstance()-LoadResource(Lres\\InstallUI.xml); CDialogBuilder builder; CControlUI* pRoot builder.Create(LInstallUI.xml, NULL, NULL, paintManager); pWnd-SetRoot(pRoot); pWnd-ShowWindow(); // 进入消息循环 MSG msg; while (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return 0; }这段代码的核心在于WS_CHILD窗口嵌入到 NSIS 的页面容器里而不是创建一个独立顶层窗口。这样用户在安装向导的每一步看到的都是同一个宿主窗口内的切换视觉上更贴近 QQ 那种单窗口内分步向导的风格。CPaintManagerUI负责位图资源和控件的绘制XML 里写什么控件窗口里就渲染什么。CDialogBuilder 的Create可以指定XML 文件路径和父控件。需要注意LoadResource这一步如果 XML 里引用了 PNG 或者字体文件资源加载路径不对界面会直接白屏或者控件丢失。实际工程里我会把图片资源统一放到一个res目录并在 XML 里用相对路径引用。2.3 选型对比MFC、Qt、Electron 的取舍用 NSISDuilib 不是因为它最流行而是在“安装包”这个特定场景里它能做到体积小、启动快、界面可控。MFC 写安装包界面编译出来的 exe 体积倒是不大但控件样式老旧想做成圆角、半透明、标签切换这些效果要自己画工作量极大。Qt 的安装包框架很成熟但运行时依赖多打出来的安装包自带一堆动态库体积动不动就是几十兆。Electron 做装机界面更是夸张一个安装程序里塞一个完整浏览器引擎。Duilib 的优势在于它只依赖 DirectUI 合成和 GDI 绘制不引入重量级框架。同样一个安装界面NSIS 本身的脚本引擎加 Duilib 的渲染层打包出来的主程序体积大概只有 Qt 方案的三分之一。缺点是排错手段少界面布局出问题时要靠打日志看坐标不像 Qt 有完善的调试工具。如果目标是做一个跨平台的安装程序那 NSISDuilib 这套组合就不合适因为 NSIS 只面向 Windows。但在 Windows 环境里它算是“体积和颜值”平衡得比较好的一套方案。3. 动手复现仿 QQ 安装界面的安装程序骨架3.1 工程结构把 NSIS 脚本和 Duilib 资源分开工程目录我推荐这样组织installer/ |-- build/ # 待打包的文件 |-- duilib_dll/ # Duilib 封装工程 | |-- InstallUI.cpp | |-- InstallUI.h | |-- res/ | |-- InstallUI.xml | |-- bg.png | |-- btn_normal.png | |-- btn_hover.png |-- nsis/ | |-- env.nsh | |-- paths.nsh | |-- sections.nsi | |-- installer.nsi |-- dist/ # 最终生成的安装包build目录放的是安装程序要释放的原始文件duilib_dll是界面库的工程nsis目录放脚本。这种拆分的好处是duilib_dll可以独立编译和调试不需要每次都跑一遍 NSIS。我一般先把 Duilib 窗口做成一个独立的 Demo exe验证 XML 布局没问题之后再把宿主窗口改成WS_CHILD模式接入 NSIS。installer.nsi作为主入口只负责包含和调用其他文件!include env.nsh !include paths.nsh !include sections.nsi Function .onInit SetRegView 64 ; 检查是否有旧版本安装 ReadRegStr $0 HKLM Software\\MyTool InstallPath StrCmp $0 done StrCpy $INSTDIR $0 done: FunctionEnd这个onInit函数在安装程序启动时执行是做环境判断和旧版本检测的好地方。SetRegView 64放在这里是为了让后续所有的注册表操作都基于 64 位视角避免被系统重定向。3.2 定制造型窗口从 XML 布局到安装进度回传Duilib 界面的核心在 XML 文件。我做一个仿 QQ 安装向导的典型布局包含顶部 Banner、中间内容区、底部按钮栏和进度条Window size480,360 caption0 roundcorner8,8 shadowtrue HorizontalLayout Control bkimagefilebg.png width140/ VerticalLayout Label text欢迎安装 MyTool fonttitle height60 aligncenter/ Progress nameinstall_progress height20 value0 min0 max100/ HorizontalLayout height50 Button namebtn_prev text上一步 width80/ Button namebtn_next text下一步 width80/ Button namebtn_cancel text取消 width80/ /HorizontalLayout /VerticalLayout /HorizontalLayout /Window关键点在于roundcorner8,8定义了窗口圆角shadowtrue给窗口加上阴影这两个属性是仿 QQ 界面“现代感”的主要来源。caption0表示不显示系统标题栏窗口的移动和关闭行为全部由 Duilib 接管。NSIS 这边需要把安装进度传给 Duilib。我采用的方法是在 NSIS 页面里设置一个自定义页面页面加载时创建 Duilib 子窗口然后通过SendMessage往子窗口发自定义消息。Function ShowDuilibPage ; 创建子窗口 System::Call duilib_dll\\ShowInstallUI::ShowInstallUI(i $HWNDPARENT, i 480, i 360) ? u ; 把当前安装路径传给界面显示 System::Call duilib_dll\\ShowInstallUI::SetInstallPath(t $INSTDIR) ? u FunctionEnd使用System::Call调用 DLL 是 NSIS 的常规做法?u表示调用后不检查返回值避免 DLL 内部出错时 NSIS 直接崩溃。如果 DLL 需要回传进度我建议在 DLL 里抛一个 Windows 消息比如自定义消息WM_APP101NSIS 在页面循环里监听。case WM_APP 101: { HWND hProgress FindWindow(NULL, Linstall_progress); SendMessage(hProgress, PBM_SETPOS, wParam, 0); break; }进度条控件的刷新频率会影响安装包的流畅感。每安装完一个文件就刷一次进度会导致界面卡顿我一般在每节完成时统一更新一次进度。如果你的安装包要释放几千个小文件逐文件回调反而会让安装时间变长。3.3 安装逻辑文件解压、注册表、快捷方式三步走安装逻辑的主体在 NSIS 的 Section 里。除了文件释放和注册表写入快捷方式创建和卸载信息登记也是必须的Section Install SecInstall SetOutPath $INSTDIR File /r build\\*.* ; 桌面快捷方式 CreateShortCut $DESKTOP\\MyTool.lnk $INSTDIR\\MyTool.exe ; 开始菜单快捷方式 CreateDirectory $SMPROGRAMS\\MyTool CreateShortCut $SMPROGRAMS\\MyTool\\MyTool.lnk $INSTDIR\\MyTool.exe ; 卸载信息 WriteUninstaller $INSTDIR\\Uninstall.exe WriteRegStr HKLM Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MyTool DisplayName MyTool WriteRegStr HKLM Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MyTool UninstallString $INSTDIR\\Uninstall.exe WriteRegDWORD HKLM Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MyTool NoModify 1 WriteRegDWORD HKLM Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MyTool NoRepair 1 SectionEndCreateShortCut的第一个参数是快捷方式路径$DESKTOP和$SMPROGRAMS是 NSIS 内置的目录宏。卸载信息这一节必不可少否则用户从“控制面板-程序-卸载”里看不到这个软件会显得很不专业。写注册表时要注意WriteRegDWORD和WriteRegStr的区别前者写数值后者写字符串。NoModify和NoRepair置 1 表示卸载页面只有“删除”按钮没有“修改”和“修复”的入口。对于安装到 Program Files 目录的场景SetOutPath会自动创建目录但如果目录名含空格File语句里的源路径必须加双引号。NSIS 对路径中的空格比较敏感我早期踩过不少次。4. 避坑指南安装包在真机上的六个翻车现场4.1 进度条全程不动现象安装过程要两分钟但界面上的进度条一直停在 0%直到最后一步才跳到 100%。原因进度条更新消息发送的频率太低或者消息循环被 NSIS 的文件释放操作阻塞。NSIS 在释放大文件时是同步操作期间没有消息泵转起来SendMessage发出的进度消息全积压在队列里。解决把文件释放拆成多个小块每释放完一个子目录就调用一次RefreshUI给 Duilib 一个处理消息的机会。同时把进度条更新逻辑从“每文件更新”改成“每目录更新”减少消息数量。4.2 静默安装参数被忽略现象在命令行加/S想静默安装结果弹出了安装界面。原因NSIS 默认支持/S静默参数但如果你自定义过页面且在.onInit里调用了InitPluginsDir或File操作这些操作在静默模式下依然会执行但页面显示逻辑没被禁用界面依然会闪出。解决在.onInit里增加判断IfSilent如果是静默模式就不加载 Duilib 界面直接跳到安装节Function .onInit ${If} ${Silent} ; 静默模式直接走安装逻辑 Call JumpToInstallPage ${Else} ; 正常模式加载界面 Call LoadDuilibUI ${EndIf} FunctionEnd4.3 64 位系统注册表写到 Wow6432Node现象用WriteRegStr HKLM Software\\MyTool写完后在 RegEdit 里看到的是Wow6432Node\\Software\\MyTool。原因32 位安装程序在 64 位系统上运行时系统会自动做注册表重定向把 32 位软件的写入路径重定向到 Wow6432Node。如果你的软件是 64 位必须告诉 NSIS 不要做重定向。解决在.onInit和主 Section 开头都加上SetRegView 64。如果软件同时包含 32 位和 64 位组件需要分别写在两个节里并交替调用SetRegView 32和SetRegView 64。4.4 安装目录有中文名导致 Duilib 加载失败现象用户选择把软件装到D:\软件发布\MyTool安装后软件界面白屏。原因Duilib 的LoadResource在解析 XML 里的图片和字体路径时如果路径包含非 ASCII 字符内部某些版本的 GDI 绘制会解析失败。解决安装路径用英文是一种解决方案但不够根治。更好的办法是 DLL 内部用GetModuleFileName动态定位资源目录而不是依赖相对路径。4.5 杀毒软件误报安装包现象安装包在部分机器上被杀毒软件直接隔离。原因NSIS 自解压结构加上自定义 DLL 加载确实容易被启发式查杀。尤其是 DLL 里用了SendMessage跨进程操作这种模式在一些安全软件里属于敏感行为。解决不解决查杀而是降低误报概率。做法是避免在安装包内直接释放 DLL 到临时目录改为在安装过程中正常写入 Program Files避免在脚本里使用ExecDos这类容易被怀疑的命令。正式对外发布时建议提交给主流安全软件做白名单申请。4.6 Window 系统缩放导致界面模糊现象4K 屏、150% 缩放下安装界面文字发虚。原因NSIS 生成的窗口没有启用 Per-Monitor V2 DPI 感知Windows 用位图拉伸方式放大了界面文字边缘模糊。解决在.onInit里调用系统 API 设置 DPI 感知System::Call user32::SetProcessDPIAware() i这行代码在安装程序启动时调用一次即可。Duilib 侧还需要在创建窗口时设置WM_DPICHANGED消息处理否则缩放比例变化时界面布局会错乱。5. 进阶验证用命令行日志确认你的安装包不是黑匣子安装包做出来以后我很少直接双击测试而是固定走一套命令行验证流程。第一步是静默安装测试确认没有任何界面弹出且返回码为 0MyTool_Setup.exe /S /DC:\TestInstall echo %ERRORLEVEL%这里的/D参数指定安装路径必须放在最后。如果返回码不是 0说明脚本里某个 Section 中途出错。NSIS 默认遇错会直接终止安装流程但很多错误并不会弹对话框所以看返回码比看界面更可靠。第二步是验证注册表和文件完整性。装完后我会用一个小的对比工具把build目录里的文件列表和C:\TestInstall对比确认没有漏文件。注册表用reg query手动看重点检查InstallPath和卸载信息是否存在。第三步是卸载验证。运行C:\TestInstall\Uninstall.exe /S然后再确认目录是否被清空。我遇到过卸载后Uninstall.exe自身还在目录里残留的情况原因是卸载程序运行时文件自己还没完全退出Delete语句执行时机太早需在卸载 Section 最后加一行Sleep 300再执行RMDir /r。第四步是界面自动化冒烟测试。我会写一个小脚本用 Windows UI Automation 框架读取安装窗口的控件树确认 Duilib 渲染出来的按钮和进度条控件都被正确识别。这一步能发现 XML 布局里的控件 ID 冲突问题。以上流程每次发布新版本都走一遍。从那以后我做安装包就顺手多了遇到日志里异常返回码第一反应不是猜而是翻回installer.nsi和InstallUI.xml逐行核对。希望这篇笔记里的选型逻辑和排错路径能帮你少走几步弯路。本文还有配套的精品资源点击获取