简介一套基于NSIS与Duilib的仿QQ安装包完整工程面向Windows桌面应用开发者及安装程序封装人员旨在解决安装包界面简陋、交互生硬的问题。项目将NSIS的脚本化安装逻辑与Duilib现代化UI库深度结合完整还原QQ安装向导的视觉风格和操作流程适合有一定C和Windows编程基础的开发者学习参考。压缩包内共206个文件、大小约69.53MB包含45个头文件与41个C源文件构成的Duilib界面库、39个编译目标文件、26张PNG界面素材以及NSIS脚本、Visual Studio解决方案、动态链接库、静态库等并按第三方库、示例、插件、头文件四个目录组织结构清晰便于复用。目前已有1843人学习下载。研读这份工程开发者可以掌握NSIS脚本编写、Duilib界面定制、NSIS插件扩展及安装包整合优化的完整流程直接复用其中的界面控件、脚本模板和模块划分快速构建出具有QQ级质感的自定义安装程序。 如果你维护过几个 Windows 桌面应用大概率会跟我有同样的烦恼功能倒是不复杂但安装包界面始终停留在“下一步、下一步、完成”的老三样怎么看都跟产品气质不搭。后来我接触到 NSIS Duilib 这套组合直接用 Duilib 写出仿 QQ 安装包风格的自绘向导界面再用 NSIS 负责文件释放、注册表、快捷方式这些脏活累活两边的优势都能吃到。这套方案做出来的安装包体积小、启动快、界面完全可控尤其适合想让软件“第一眼”就比较体面的团队或个人开发者。无论你是刚接触 NSIS 打包的新手还是已经写过不少脚本但嫌默认界面太丑的老手这篇内容都可以直接当参考。核心思路不复杂NSIS 在一个临时目录里把 Duilib 写的向导 exe 拉起来向导收集用户的选择写入 ini等向导退出后 NSIS 继续干活。下文我会把整个链路拆开讲包括脚本骨架、Duilib 工程配置、界面资源处理以及我踩过的各种坑。1. 为什么用“NSIS Duilib”组合仿 QQ 安装包1.1 这个需求是从哪来的Windows 下做安装包大多数人第一反应是 NSIS、Inno Setup或者干脆用 Visual Studio 自带的 InstallShield。NSIS 胜在脚本灵活、体积小但默认的 MUI 向导界面也就那样想做出“左侧大图、右侧内容卡片、右上角关闭按钮”这种主流互联网产品的安装风格单靠 MUI2.nsh 和 nsDialogs 是相当吃力的。不是做不到而是控件层级和自绘能力有限改起来非常痛苦。后来我注意到一些常见软件的安装界面尤其是聊天工具类产品的安装器界面风格和安装逻辑其实是分离的界面上有明显的自绘痕迹圆角按钮、背景图、自定义字体这不是原生 NSIS 能轻松画出来的。拆开看会发现这类安装器普遍是“高级语言写界面 NSIS 干安装活儿”的组合Duilib 就是其中比较典型的界面层实现。1.2 主流方案的对比我自己试过几条路线这里直接给出横向对比方便你结合自己项目的情况做选型。方案界面可定制程度体积/资源占用开发成本适合场景原生 NSIS MUI低只能换图和文字极低低内部工具、快速发布Inno Setup 第三方皮肤中皮肤组件有限较低中需要相对完善卸载器NSIS nsDialogs 自绘中控件能力有限低中高简单自定义界面NSIS Duilib本文方案极高完全自绘低中高产品化安装体验、品牌定制Qt / Electron 安装器高高高已有跨平台框架的大型产品Duilib 的核心优势在于它是 DirectUI 思路用 XML 布局描述界面C 处理事件和逻辑绘制完全自己接管窗口可以做成无边框、异形、带阴影。安装器本身不需要太复杂的业务逻辑所以这套组合的投入产出比很高一个几百行的 XML 一个几百行的 C 工程就能跑起来。1.3 两边怎么分工明确一下职责边界这是整个方案能不能顺利跑起来的关键。Duilib 向导 exe 只做一件事显示安装引导界面让用户选择安装路径、勾选快捷方式等选项最后把结果写进配置文件里。NSIS 负责所有“重活”解压释放文件到目标目录、写卸载注册表、创建快捷方式、生成卸载器以及在安装完成后清理临时文件。这种“双进程”设计的另一个好处是独立调试。向导界面可以单独在 Visual Studio 里跑不需要每次都把整个安装包装一遍再测试NSIS 脚本也可以用“假配置”方式直接跑两边互不阻塞。这在开发期的效率提升非常明显也是我最终放弃“在 NSIS 页面里内嵌控件”这条路的主要原因。2. 跑通核心链路NSIS 与 Duilib 的协作机制2.1 整体时序与配置约定先建立一个全局画面方便你理解后面的代码。用户双击安装包NSIS 运行.onInit。NSIS 把wizard.exe和它的依赖文件释放到$PLUGINSDIR临时目录。NSIS 通过ExecWait启动 wizard.exe并传入一个配置文件路径作为参数。Duilib 向导界面读取这个配置文件里的默认值展示界面用户操作完成后写入新的配置值然后退出进程。NSIS 根据 wizard.exe 的返回值判断是否继续安装读取配置文件里的安装路径、勾选项等。如果用户点了“取消”或出错NSIS 直接Abort中断安装。安装完成后NSIS 清理临时配置和临时文件。配置文件我用最简单的 INI 格式键值对足够用NSIS 原生支持WriteINIStr/ReadINIStrDuilib 的 C 侧也可以用WritePrivateProfileString/GetPrivateProfileString读写两边都非常顺手。下面是一个约定好的示例配置内容。[Wizard] InitialPathC:\Program Files\DemoApp InstallPathC:\Program Files\DemoApp DesktopShortcut1 AutoRun0InitialPath是向导启动时展示的默认安装路径InstallPath是用户最终选择并确认的路径DesktopShortcut和AutoRun是勾选状态。NSIS 侧要读取的是后三者向导侧会修改后三者。2.2 NSIS 脚本骨架直接给一份可用性比较高的骨架脚本你把它当成起点去改。这里面有几个点我会在后面专门解释。Unicode true RequestExecutionLevel admin !include FileFunc.nsh Name DemoApp OutFile DemoAppSetup.exe InstallDir $PROGRAMFILES64\DemoApp Var WizardExe Var CfgIni Var InstallPath Var DesktopShortcut Function .onInit ; 把向导和皮肤资源释放到 NSIS 临时目录 SetOutPath $PLUGINSDIR File wizard.exe File /r skin\*.* ; 生成向导配置文件写入默认安装路径 StrCpy $CfgIni $PLUGINSDIR\wizard.ini WriteINIStr $CfgIni Wizard InitialPath $INSTDIR WriteINIStr $CfgIni Wizard InstallPath $INSTDIR WriteINIStr $CfgIni Wizard DesktopShortcut 1 WriteINIStr $CfgIni Wizard AutoRun 0 ; 运行向导并等待退出0 表示继续安装其他值表示取消或出错 ExecWait $PLUGINSDIR\wizard.exe /cfg$CfgIni $0 IntCmp $0 0 continueInstall Abort continueInstall: ; 读取用户在向导里的最终选择 ReadINIStr $InstallPath $CfgIni Wizard InstallPath ReadINIStr $DesktopShortcut $CfgIni Wizard DesktopShortcut StrCmp $InstallPath 0 2 StrCpy $InstallPath $PROGRAMFILES64\DemoApp FunctionEnd Section Main SetOutPath $InstallPath File /r app\*.* ; 卸载注册表信息 WriteRegStr HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\DemoApp DisplayName DemoApp WriteRegStr HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\DemoApp UninstallString $InstallPath\Uninstall.exe WriteUninstaller $InstallPath\Uninstall.exe ; 开始菜单和桌面快捷方式 CreateDirectory $SMPROGRAMS\DemoApp CreateShortcut $SMPROGRAMS\DemoApp\DemoApp.lnk $InstallPath\DemoApp.exe StrCmp $DesktopShortcut 1 0 2 CreateShortcut $DESKTOP\DemoApp.lnk $InstallPath\DemoApp.exe SectionEnd Section Uninstall ; 终止运行中的进程避免文件占用 nsProcess::_FindProcess DemoApp.exe Pop $R0 IntCmp $R0 0 0 skipKill nsProcess::_KillProcess DemoApp.exe skipKill: Delete $INSTDIR\*.* RMDir /r $INSTDIR DeleteRegKey HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\DemoApp DeleteRegKey HKCU Software\DemoApp SectionEnd几个需要注意的细节Unicode true一定要开。NSIS 3.0 之后可以原生支持 Unicode配置文件里的中文路径才不会乱码。RequestExecutionLevel admin决定了安装包运行时会触发 UAC 提权。如果你要往Program Files写入必须加这一句如果只是装到AppData可以改成user让体验更轻。不要把向导 exe 释放到$TEMPNSIS 的$PLUGINSDIR是专门给插件和辅助程序用的安装结束会自动清理。File /r skin\*.*会把 skin 目录下的全部资源释放到$PLUGINSDIR注意wizard.exe运行时需要的皮肤 XML、PNG 图片、DLL 依赖都要一起放过去。2.3 关于 NSIS 的/D参数坑很多人在第一步就想当然地把安装路径通过/D参数传给 wizard.exe然后发现路径根本没传过去或者传过去是错的。原因在于 NSIS 脚本编译为安装包后/D会被 NSIS 运行时用来指定安装目录和你传给子进程的参数是两码事。应通过/cfgpath这样的自定义参数传递配置文件路径配置文件里再带上初始安装路径避免被 NSIS 的保留逻辑干扰。3. Duilib 向导界面的落地要点3.1 工程配置与项目结构Duilib 向导作为一个独立 Win32 工程我习惯把工程结构划分成这样SetupWizard/ ├── skin/ │ ├── setup.xml │ ├── left_bg.png │ ├── btn_normal.png │ ├── btn_down.png │ └── ... ├── Main.cpp ├── SetupWnd.h / SetupWnd.cpp └── duilib/工程字符集必须设置成 Unicode和 NSIS 保持一致。Duilib 库的编译方式可以直接静态链接到向导 exe 里这样发布时少一个 dll 依赖杀软误报的概率也会低一些。编译时注意运行库选择/MT让向导 exe 在目标机器上不依赖 VC 运行库。Main.cpp里要做的事情非常少初始化 COM、创建消息循环、进入窗口消息处理。#include windows.h #include SetupWnd.h int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int) { HRESULT hr ::CoInitialize(nullptr); SetupWnd wnd; wnd.Create(nullptr, _T(DemoApp Setup), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE); wnd.CenterWindow(); wnd.ShowWindow(); CMessageLoop::Run(); ::CoUninitialize(); return 0; }3.2 一个可用的 XML 布局写法仿 QQ 安装包的核心视觉结构我把它抽象成三个区域左侧品牌视觉区、右侧内容操作区、右上角关闭按钮。下面是一个简化但完整的setup.xml你可以直接跑起来看效果。?xml version1.0 encodingutf-8? Window size820,560 caption0,0,0,0 shadowtrue roundcorner0,0 HorizontalLayout bkcolor#FFFFFFFF Control width320 bkimagefileleft_bg.png / VerticalLayout inset48,72,48,40 Label text欢迎安装 DemoApp font1 textcolor#FF202020 / Label text安装前请关闭正在运行的 DemoApp font0 textcolor#FF909090 margin0,8,0,0 / Edit nameinstall_path text$INSTDIR readonlytrue margin0,36,0,0 height36 / Button namebtn_browse text浏览... margin0,12,0,0 width96 height32 / CheckBox namechk_desktop text创建桌面快捷方式 selectedtrue margin0,24,0,0 / HorizontalLayout height44 margin0,40,0,0 Button namebtn_install text立即安装 width120 height40 bkimagefilebtn_normal.png / Button namebtn_cancel text取消 width100 height40 margin16,0,0,0 / /HorizontalLayout /VerticalLayout /HorizontalLayout Button namebtn_close text width32 height32 floattrue pos780,8,0,0 bkimagefilebtn_close.png / /Window几个细节说明shadowtrue可以让无边框窗口带阴影观感接近原生窗口但前提是系统配置了 DWM。如果追求 QQ 安装包那种“扁平成平板”的感觉也可以关掉阴影。caption0,0,0,0表示整个窗口都没有系统标题栏所有按钮都由 XML 里的控件负责。Edit控件设置readonlytrue避免用户手工乱改路径格式。浏览按钮点击后弹出文件夹选择对话框选中结果回填到编辑框。按钮的bkimage建议准备三态图normal、hot、downDuilib 也支持直接指定这几种状态的图片交互手感会好很多。3.3 事件处理与进度条步进在SetupWnd::Notify中处理点击事件核心逻辑就是从界面控件拿到用户选项写入 INI 文件然后关闭窗口。void SetupWnd::Notify(TNotifyUI msg) { if (msg.sType _T(click)) { if (msg.pSender-GetName() _T(btn_install)) { SaveConfig(); Close(IDOK); } else if (msg.pSender-GetName() _T(btn_cancel)) { Close(IDCANCEL); } else if (msg.pSender-GetName() _T(btn_close)) { Close(IDCANCEL); } else if (msg.pSender-GetName() _T(btn_browse)) { // 弹出目录选择对话框选择结果写回 install_path 编辑框 std::wstring path ShowFolderDialog(); SetControlText(_T(install_path), path); } } } void SetupWnd::SaveConfig() { CDuiString cfgPath GetCommandLineParam(_T(/cfg)); std::wstring installPath GetControlText(_T(install_path)); bool desktop GetControlSelected(_T(chk_desktop)); WritePrivateProfileString(_T(Wizard), _T(InstallPath), installPath.c_str(), cfgPath); WritePrivateProfileString(_T(Wizard), _T(DesktopShortcut), desktop ? _T(1) : _T(0), cfgPath); }关于安装进度的显示这里要坦白说一个 NSIS Duilib 双进程方案的现实问题NSIS 解压文件时默认只会给你一个内部进度条这个进度条状态没法直接推给另一个进程的 Duilib 界面。最实用的做法不是追求“实时精确进度”而是把安装过程人为拆成几个阶段比如“准备安装素材 / 复制主程序 / 写入配置信息 / 创建快捷方式 / 完成”每个阶段开始时向 INI 写入一个Stage1/2/3/4Duilib 向导那边用一个定时器去读取这个值把进度条按阶段推进。这种方式虽然做不到字节级精确但用户感知上完全不突兀。如果你坚持要精确进度也不是没有办法可以用 NSIS 插件把每个文件的解压完成事件通过命名管道或者共享内存推送出去但复杂度和稳定性风险都会增加不少我个人不推荐在安装器这种“用完即走”的项目里过度设计。3.4 高 DPI、字体与资源处理的三个细节高 DPI 是个大坑。新版 Windows 默认会做 DPI 虚拟化如果你的向导 exe 没有正确声明感知模式在 150% 缩放的屏幕上界面就会发虚。通过工程 manifest 声明 PerMonitorV2 感知模式可以解决模糊问题但要注意一点NSIS 安装包本身是旧的 UI 进程如果它的 DPI 感知模式和向导 exe 不一致两个窗口切换时可能肉眼可见地闪一下。最稳妥的做法是向导 exe 和安装包都走“系统 DPI 感知字体缩放适配”的路线界面各控件尺寸适当放大不要硬扛。字体方面Duilib 默认用的字体在中文环境下可能不太好看建议在 XML 里显式指定字体比如用font1并全局声明一个中文字体列表。我习惯用“微软雅黑, 14px”作为主字体小字号辅助说明用“微软雅黑, 12px, 灰色”这样整体观感比较接近互联网产品的现代风格。最后是图片资源。安装器界面用到的背景图、按钮图、Logo建议全部用 PNG 格式并且提前用工具做好 1x 和 1.25x/1.5x 两套或者在资源图尺寸上直接留出富余避免高 DPI 下被拉伸模糊。图片尽量控制在几 MB 以内体积越大NSIS 释放到$PLUGINSDIR的时间越长用户双击安装包到看到界面的等待时间就越长。4. 构建安装包时的高频问题与排查4.1 杀软误报问题NSIS 本身是无辜的但因为它太流行很多恶意软件也用它打包所以杀软对 NSIS 生成的安装包天然有更高的“警惕性”。如果你在向导里又用了 UPX 压缩误报概率会进一步上升。我踩过的坑是一开始为了缩小安装包体积给 NSIS 生成的 exe 加了 UPX结果本地测试直接报毒去掉 UPX 之后回归正常。建议能不压壳就不压壳NSIS 的 LZMA 压缩已经足够小。对外的正式安装包一定要做代码签名没有签名的 Windows 安装包在 SmartScreen 那里也过不了关用户看到“未知发布者”的提示安装转化率会很难看。4.2 UAC 与安装目录权限我在骨架脚本里用了RequestExecutionLevel admin这意味着用户双击安装包后会先看到 UAC 弹窗然后才进入你的 Duilib 界面。某些团队希望把 UAC 弹窗放在“用户点完立即安装”之后这也有实现思路把安装包的主体做成普通权限进程先跑 Duilib 向导收集选项等真正开始写文件前再去执行提权操作。但说实话这个流程会让代码复杂度上一个台阶而且 UAC 弹窗逻辑跟系统安全强绑定很容易出现问题。普通应用直接走admin提权是省事且稳妥的选择。4.3 中文路径 / 空格路径 / 特殊字符NSIS 脚本里的每个路径都要养成加引号的习惯ExecWait、CreateShortcut、WriteRegStr这些指令的参数里如果含有空格路径少一个引号就会行为异常。中文路径在启动向导、传递配置的时候容易出乱码如果你的 NSIS 还是 2.x 老版本请无论如何升级到 3.x并开启Unicode true。另外安装目录尽量限制不可包含特殊符号比如在 Duilib 的 browse 弹窗里可以做一个简单的路径校验检测到非法字符就提示用户重新选择。4.4 卸载器提示文件被占用这是非常典型的场景用户没关主程序就直接跑卸载器结果主程序 exe 被占用卸载过程要么失败要么残留一堆文件。我的处理方式是写一个独立的进程检测模块无论是卸载主程序还是向导安装时检测“是否需要先关程序”都用同一套逻辑。上面的 NSIS 骨架脚本里已经用了nsProcess插件来做检测和结束进程注意在Uninstall段开头执行并且在杀掉进程之后加一个短延迟让文件句柄完全释放。4.5 进度条卡住不走如果你采用了阶段式进度反馈比较容易出现的问题是某个阶段执行特别快比如“创建快捷方式”一眨眼就完了界面上的进度条直接从 40% 跳到 100%观感比较突兀。处理方法有两种一是在阶段切换之间加一个短时等待让杜比感更真实二是把进度条改成“不定进度”动画风格在整个安装期间只做循环动画安装完成直接显示完成页。QQ 安装包本身也是更偏向“整体动效”而不是精确到百分比这个思路可以参考。5. 最后再提三个小建议根据我实际跑这个方案的经验还有三个点想单独拿出来说。第一个是关于向导 exe 的调试。开发期不要每次都打完整安装包再测可以在 Visual Studio 里直接给向导工程设置命令行参数比如/cfgD:\dev\wizard.ini然后像调试普通程序一样断点调界面效率高很多。等界面和配置读写逻辑都稳定了再回到 NSIS 的构建流程里做集成测试。第二个是版本号与升级策略。安装包做得再好看也是给“第一印象”服务的但它同样影响后续的自动更新。最好在向导界面和配置里预留一个产品版本号字段后续如果有静默安装、增量升级的需求可以直接复用这套配置机制不用再返工。第三个是审美和细节。仿 QQ 风格的精髓其实不是某个具体的控件画法而是“克制”和“清晰”左侧主视觉给足品牌氛围右侧只放必要的信息和操作按钮主次分明文案短平快。安装器本身是个临时工具用户在你产品上的第一次体验可能就来自这十几秒值得多花点时间把细节打磨干净。本文还有配套的精品资源点击获取