NSIS + Duilib 仿 QQ 安装包:从零打造高颜值自定义安装向导
2026/9/9 20:05:28 网站建设 项目流程

简介:一套基于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 整体时序与配置约定

先建立一个全局画面,方便你理解后面的代码。

  1. 用户双击安装包,NSIS 运行.onInit
  2. NSIS 把wizard.exe和它的依赖文件释放到$PLUGINSDIR临时目录。
  3. NSIS 通过ExecWait启动 wizard.exe,并传入一个配置文件路径作为参数。
  4. Duilib 向导界面读取这个配置文件里的默认值,展示界面,用户操作完成后写入新的配置值,然后退出进程。
  5. NSIS 根据 wizard.exe 的返回值判断是否继续安装,读取配置文件里的安装路径、勾选项等。
  6. 如果用户点了“取消”或出错,NSIS 直接Abort中断安装。
  7. 安装完成后,NSIS 清理临时配置和临时文件。

配置文件我用最简单的 INI 格式,键值对足够用,NSIS 原生支持WriteINIStr/ReadINIStr,Duilib 的 C++ 侧也可以用WritePrivateProfileString/GetPrivateProfileString读写,两边都非常顺手。下面是一个约定好的示例配置内容。

[Wizard] InitialPath=C:\Program Files\DemoApp InstallPath=C:\Program Files\DemoApp DesktopShortcut=1 AutoRun=0

InitialPath是向导启动时展示的默认安装路径,InstallPath是用户最终选择并确认的路径,DesktopShortcutAutoRun是勾选状态。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 释放到$TEMP,NSIS 的$PLUGINSDIR是专门给插件和辅助程序用的,安装结束会自动清理。
  • File /r "skin\*.*"会把 skin 目录下的全部资源释放到$PLUGINSDIR,注意wizard.exe运行时需要的皮肤 XML、PNG 图片、DLL 依赖都要一起放过去。

2.3 关于 NSIS 的/D=参数坑

很多人在第一步就想当然地把安装路径通过/D=参数传给 wizard.exe,然后发现路径根本没传过去,或者传过去是错的。原因在于 NSIS 脚本编译为安装包后,/D=会被 NSIS 运行时用来指定安装目录,和你传给子进程的参数是两码事。应通过/cfg="path"这样的自定义参数传递配置文件路径,配置文件里再带上初始安装路径,避免被 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 version="1.0" encoding="utf-8"?> <Window size="820,560" caption="0,0,0,0" shadow="true" roundcorner="0,0"> <HorizontalLayout bkcolor="#FFFFFFFF"> <Control width="320" bkimage="file='left_bg.png'" /> <VerticalLayout inset="48,72,48,40"> <Label text="欢迎安装 DemoApp" font="1" textcolor="#FF202020" /> <Label text="安装前请关闭正在运行的 DemoApp" font="0" textcolor="#FF909090" margin="0,8,0,0" /> <Edit name="install_path" text="$INSTDIR" readonly="true" margin="0,36,0,0" height="36" /> <Button name="btn_browse" text="浏览..." margin="0,12,0,0" width="96" height="32" /> <CheckBox name="chk_desktop" text="创建桌面快捷方式" selected="true" margin="0,24,0,0" /> <HorizontalLayout height="44" margin="0,40,0,0"> <Button name="btn_install" text="立即安装" width="120" height="40" bkimage="file='btn_normal.png'" /> <Button name="btn_cancel" text="取消" width="100" height="40" margin="16,0,0,0" /> </HorizontalLayout> </VerticalLayout> </HorizontalLayout> <Button name="btn_close" text="" width="32" height="32" float="true" pos="780,8,0,0" bkimage="file='btn_close.png'" /> </Window>

几个细节说明:

  • shadow="true"可以让无边框窗口带阴影,观感接近原生窗口,但前提是系统配置了 DWM。如果追求 QQ 安装包那种“扁平成平板”的感觉,也可以关掉阴影。
  • caption="0,0,0,0"表示整个窗口都没有系统标题栏,所有按钮都由 XML 里的控件负责。
  • Edit控件设置readonly="true",避免用户手工乱改路径格式。浏览按钮点击后弹出文件夹选择对话框,选中结果回填到编辑框。
  • 按钮的bkimage建议准备三态图:normalhotdown,Duilib 也支持直接指定这几种状态的图片,交互手感会好很多。

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 写入一个Stage=1/2/3/4,Duilib 向导那边用一个定时器去读取这个值,把进度条按阶段推进。这种方式虽然做不到字节级精确,但用户感知上完全不突兀。

如果你坚持要精确进度,也不是没有办法,可以用 NSIS 插件把每个文件的解压完成事件通过命名管道或者共享内存推送出去,但复杂度和稳定性风险都会增加不少,我个人不推荐在安装器这种“用完即走”的项目里过度设计。

3.4 高 DPI、字体与资源处理的三个细节

高 DPI 是个大坑。新版 Windows 默认会做 DPI 虚拟化,如果你的向导 exe 没有正确声明感知模式,在 150% 缩放的屏幕上界面就会发虚。通过工程 manifest 声明 PerMonitorV2 感知模式可以解决模糊问题,但要注意一点:NSIS 安装包本身是旧的 UI 进程,如果它的 DPI 感知模式和向导 exe 不一致,两个窗口切换时可能肉眼可见地闪一下。最稳妥的做法是向导 exe 和安装包都走“系统 DPI 感知+字体缩放适配”的路线,界面各控件尺寸适当放大,不要硬扛。

字体方面,Duilib 默认用的字体在中文环境下可能不太好看,建议在 XML 里显式指定字体,比如用font="1"并全局声明一个中文字体列表。我习惯用“微软雅黑, 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 脚本里的每个路径都要养成加引号的习惯,ExecWaitCreateShortcutWriteRegStr这些指令的参数里如果含有空格路径,少一个引号就会行为异常。中文路径在启动向导、传递配置的时候容易出乱码,如果你的 NSIS 还是 2.x 老版本,请无论如何升级到 3.x,并开启Unicode true。另外,安装目录尽量限制不可包含特殊符号,比如在 Duilib 的 browse 弹窗里可以做一个简单的路径校验,检测到非法字符就提示用户重新选择。

4.4 卸载器提示文件被占用

这是非常典型的场景:用户没关主程序就直接跑卸载器,结果主程序 exe 被占用,卸载过程要么失败要么残留一堆文件。我的处理方式是写一个独立的进程检测模块,无论是卸载主程序还是向导安装时检测“是否需要先关程序”,都用同一套逻辑。上面的 NSIS 骨架脚本里已经用了nsProcess插件来做检测和结束进程,注意在Uninstall段开头执行,并且在杀掉进程之后加一个短延迟让文件句柄完全释放。

4.5 进度条卡住不走

如果你采用了阶段式进度反馈,比较容易出现的问题是某个阶段执行特别快,比如“创建快捷方式”一眨眼就完了,界面上的进度条直接从 40% 跳到 100%,观感比较突兀。处理方法有两种:一是在阶段切换之间加一个短时等待,让杜比感更真实;二是把进度条改成“不定进度”动画风格,在整个安装期间只做循环动画,安装完成直接显示完成页。QQ 安装包本身也是更偏向“整体动效”,而不是精确到百分比,这个思路可以参考。

5. 最后再提三个小建议

根据我实际跑这个方案的经验,还有三个点想单独拿出来说。

第一个是关于向导 exe 的调试。开发期不要每次都打完整安装包再测,可以在 Visual Studio 里直接给向导工程设置命令行参数,比如/cfg="D:\dev\wizard.ini",然后像调试普通程序一样断点调界面,效率高很多。等界面和配置读写逻辑都稳定了,再回到 NSIS 的构建流程里做集成测试。

第二个是版本号与升级策略。安装包做得再好看,也是给“第一印象”服务的,但它同样影响后续的自动更新。最好在向导界面和配置里预留一个产品版本号字段,后续如果有静默安装、增量升级的需求,可以直接复用这套配置机制,不用再返工。

第三个是审美和细节。仿 QQ 风格的精髓其实不是某个具体的控件画法,而是“克制”和“清晰”:左侧主视觉给足品牌氛围,右侧只放必要的信息和操作,按钮主次分明,文案短平快。安装器本身是个临时工具,用户在你产品上的第一次体验可能就来自这十几秒,值得多花点时间把细节打磨干净。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询