☰
NSIS+Duilib自定义安装程序:插件桥接与避坑实战
2026/9/26 11:38:32 网站建设 项目流程

简介:本资源面向Windows桌面开发与安装包制作人员,提供一套基于NSIS与Duilib的自定义安装程序完整工程,解决传统安装界面简陋、交互体验差的问题,适合具备一定脚本与C++基础的开发者进阶使用。压缩包共451个文件,约9.6MB,涵盖nsh脚本、nsi主脚本、xml界面布局、png与bmp皮肤图片、ico图标、dll动态库及exe工具等,另有bat构建脚本、chm帮助文档与多语言nlf文件,结构完整便于二次开发。资源围绕NSIS脚本编写、Duilib界面设计、DLL集成调用、快捷方式与开机启动项创建、快速启动栏处理、错误日志记录及多语言支持等关键知识点展开,并附带皮肤资源与构建批处理,可直接用于搭建美观且功能强大的安装程序。目前已有2942人学习下载,适合希望提升软件首屏体验与安装流程定制能力的开发者参考实践。

1. NSIS+Duilib 自定义安装程序:为什么老牌组合仍是桌面交付的硬通货

做 Windows 桌面交付的兄弟大概率都经历过这个场景:产品经理拿着竞品安装包过来,说“人家这个界面能换肤、能动画、能自定义按钮,我们那个 NSIS 默认向导太丑了,能不能改改?”你打开 NSIS 官方文档一看,内置页面就那几种,MUI2 换汤不换药,想搞个圆角按钮加背景图都费劲。这时候 NSIS+Duilib 自定义安装程序就是那条被反复验证过的路——用 NSIS 扛安装逻辑(文件释放、注册表、服务注册、卸载),用 Duilib 扛界面渲染(XML 布局、贴图、控件自绘),两者通过插件桥接,最终交付一个体积可控、界面完全自主的 setup.exe。

这套方案适合谁?适合需要品牌化安装体验的桌面软件团队,尤其是工具类、客户端类产品,安装包体积敏感(NSIS 脚本编译后极小),又不想引入 Electron 或 Qt 这种重型运行时。热搜词里 NSIS、Duilib、自定义安装程序三个词凑在一起,说明搜的人不是要学 NSIS 语法入门,而是已经决定用这套组合,卡在“怎么把 Duilib 塞进 NSIS 里跑起来”这一步。接下来按我实际落地的顺序,从架构选型到桥接实现再到避坑,一层层拆开讲。

2. 架构拆解:NSIS 与 Duilib 各自负责什么

2.1 为什么不是纯 NSIS 自绘,也不是纯 Duilib 打包

纯 NSIS 自绘界面不是不行,用nsDialogs配合CreateWindow能画出自定义窗口,但控件库太原始,按钮状态、列表滚动、图片缩放都要手写 GDI 代码,维护成本极高。纯 Duilib 做安装程序则要自己实现文件释放、注册表写入、卸载信息注册、静默安装参数解析,这些 NSIS 一行命令搞定的事,用 C++ 写要几百行且容易出兼容问题。

所以常见做法是分层:NSIS 作为宿主进程,负责安装业务逻辑和最终的文件落地;Duilib 编译成 DLL 插件,由 NSIS 在运行时加载,插件内部创建 Duilib 窗口并接管界面交互。NSIS 通过插件导出函数与 Duilib 通信,Duilib 通过回调或消息通知 NSIS 执行下一步。这样界面层和逻辑层解耦,改界面不用动安装脚本,改安装流程不用重编译界面库。

选型理由很直接:NSIS 的安装能力经过二十年验证,静默安装、权限提升、数字签名、多语言都成熟;Duilib 的界面能力在国产桌面软件里被大量使用,XML 布局加贴图机制能快速还原设计稿。两者结合,安装包体积增加通常在一到两兆,对大多数桌面产品可接受。

2.2 插件桥接的三种通信方式与选择依据

NSIS 调用 Duilib 插件,本质是 NSIS 的Plugin命令加载 DLL 并调用导出函数。通信方式我见过三种:

第一种是 NSIS 主动调用,Duilib 插件导出ShowInstallWizard之类的函数,NSIS 传参进去,插件内部跑消息循环,窗口关闭后函数返回,NSIS 继续执行后续安装步骤。这种方式最简单,适合界面和逻辑交替不频繁的场景。

第二种是 Duilib 回调 NSIS,插件在窗口事件里通过extra_parameters结构拿到 NSIS 的ExecuteCodeSegment能力,直接触发 NSIS 脚本里的函数。这种方式适合界面按钮直接驱动安装步骤,比如点击“开始安装”后 Duilib 通知 NSIS 执行文件释放。

第三种是共享内存或窗口消息,两者在不同线程时用PostMessage或命名共享内存同步状态。这种方式复杂,除非有异步进度回传需求,一般不用。

我一般选第二种为主、第一种为辅:主窗口由插件创建并跑消息循环,按钮事件通过回调触发 NSIS 函数,NSIS 执行完安装步骤后再回调插件更新进度条。这样界面响应和安装逻辑都在各自擅长的领域里跑。

2.3 最小可跑通的工程结构

在动手写代码前,先把工程目录定下来,后面编译和调试都依赖这个结构。我习惯这样组织:

project/ ├── installer.nsi # NSIS 主脚本 ├── setup_ui/ # Duilib 插件工程 │ ├── setup_ui.vcxproj │ ├── setup_ui.cpp # 插件导出函数与窗口逻辑 │ ├── setup_ui.h │ ├── ui_config.xml # Duilib 布局文件 │ └── res/ # 贴图与资源 ├── build/ # 编译输出 │ ├── setup_ui.dll │ └── setup.exe └── scripts/ └── build.bat # 一键编译脚本

关键点:Duilib 插件编译为 32 位 DLL(NSIS 是 32 位进程,即使安装包在 64 位系统运行,插件也必须是 32 位),输出到 build 目录;NSIS 脚本用!addplugindir指向 build 目录,编译时把 DLL 打包进安装包。资源文件通过 NSIS 的File命令释放到临时目录,Duilib 从临时目录加载 XML 和贴图。

3. 动手实现:从 Duilib 插件到 NSIS 脚本的完整链路

3.1 编写 Duilib 插件导出函数与窗口初始化

Duilib 插件对外暴露的入口是一个标准 C 导出函数,NSIS 通过Plugin命令调用它。函数签名要匹配 NSIS 插件规范:void __declspec(dllexport) __stdcall FunctionName(HWND hwndParent, int string_size, char *variables, stack_t **stacktop, extra_parameters *extra)。下面是一个最小实现,创建 Duilib 窗口并跑消息循环:

// setup_ui.cpp #include "stdafx.h" #include "setup_ui.h" #include <windows.h> // NSIS 插件规范需要的结构,简化声明 struct extra_parameters { unsigned int exec_flags; int (*ExecuteCodeSegment)(int, HWND); void (*validate_filename)(char *); // 其他字段省略 }; // 全局保存 NSIS 回调能力 static extra_parameters* g_extra = nullptr; static HWND g_parent = nullptr; // 导出函数:NSIS 调用此函数显示安装界面 extern "C" __declspec(dllexport) void __stdcall ShowInstallUI( HWND hwndParent, int string_size, char *variables, stack_t **stacktop, extra_parameters *extra) { g_parent = hwndParent; g_extra = extra; // 初始化 Duilib 窗口 CPaintManagerUI::SetInstance(GetModuleHandle(NULL)); // 资源路径从 NSIS 释放的临时目录读取,这里用环境变量传递 char resPath[MAX_PATH] = {0}; GetEnvironmentVariableA("SETUP_RES_PATH", resPath, MAX_PATH); CPaintManagerUI::SetResourcePath(resPath); CMainWnd* pWnd = new CMainWnd(); if (pWnd == nullptr) return; // 创建窗口,加载 XML 布局 pWnd->Create(NULL, _T("安装向导"), UI_WNDSTYLE_FRAME, 0L, 0, 0, 600, 400); pWnd->CenterWindow(); pWnd->ShowWindow(true); // 跑消息循环,窗口关闭后返回 CMessageLoop* pLoop = new CMessageLoop(); pLoop->AddMessageFilter(pWnd); pLoop->Run(); delete pLoop; delete pWnd; }

逻辑说明:ShowInstallUI是 NSIS 调用的入口,内部先设置 Duilib 的资源路径(由 NSIS 在调用前通过环境变量传入),然后创建主窗口并加载 XML。CMessageLoop跑起来后,窗口事件由 Duilib 处理,直到窗口关闭函数返回,NSIS 继续执行后续脚本。

参数说明:hwndParent是 NSIS 安装程序主窗口句柄,可作为 Duilib 窗口的父窗口;extra结构里的ExecuteCodeSegment是回调 NSIS 函数的关键,后面按钮事件会用到;SETUP_RES_PATH环境变量是 NSIS 和插件约定的资源路径传递方式,避免硬编码。

3.2 用 XML 定义安装向导界面与控件绑定

Duilib 的界面用 XML 描述,下面是一个安装向导的典型布局:顶部标题栏、中间进度显示区、底部按钮区。文件保存为ui_config.xml,放在资源目录下。

<!-- ui_config.xml --> <?xml version="1.0" encoding="UTF-8"?> <Window size="600,400" caption="0,0,0,32" mininfo="600,400"> <!-- 标题栏 --> <HorizontalLayout height="32" bkcolor="#FF2D2D2D"> <Label text="软件安装向导" textcolor="#FFFFFFFF" font="system_14" padding="10,0,0,0" /> <Control /> <Button name="btn_close" bkimage="close_normal.png" hotimage="close_hot.png" width="32" height="32" /> </HorizontalLayout> <!-- 内容区 --> <VerticalLayout bkcolor="#FFF5F5F5"> <Label name="lbl_status" text="准备安装..." textcolor="#FF333333" font="system_12" padding="20,20,20,10" /> <Progress name="progress_install" bkimage="progress_bg.png" foreimage="progress_fore.png" height="20" margin="20,0,20,0" /> <Label name="lbl_detail" text="" textcolor="#FF666666" font="system_10" padding="20,10,20,10" /> </VerticalLayout> <!-- 按钮区 --> <HorizontalLayout height="50" bkcolor="#FFFFFFFF"> <Control /> <Button name="btn_install" text="开始安装" bkimage="btn_normal.png" hotimage="btn_hot.png" pushedimage="btn_pushed.png" textcolor="#FFFFFFFF" width="100" height="32" margin="0,0,10,0" /> <Button name="btn_cancel" text="取消" bkimage="btn_normal.png" hotimage="btn_hot.png" pushedimage="btn_pushed.png" textcolor="#FFFFFFFF" width="80" height="32" margin="0,0,20,0" /> </HorizontalLayout> </Window>

逻辑说明:Window根节点定义窗口尺寸和标题栏拖拽区域;HorizontalLayout和VerticalLayout做布局;Button的name属性用于在 C++ 代码里绑定事件处理器;Progress控件用于显示安装进度。

参数说明:caption="0,0,0,32"表示标题栏可拖拽区域为顶部 32 像素;bkimage、hotimage、pushedimage分别对应按钮正常、悬停、按下三种状态的贴图;margin和padding控制间距,具体数值按设计稿调整。

在 C++ 窗口类里绑定按钮事件:

// MainWnd.cpp 片段 void CMainWnd::OnClick(TNotifyUI& msg) { if (msg.sender->GetName() == _T("btn_install")) { // 通知 NSIS 执行安装函数 if (g_extra && g_extra->ExecuteCodeSegment) { g_extra->ExecuteCodeSegment(0, m_hWnd); // 0 是 NSIS 函数索引 } } else if (msg.sender->GetName() == _T("btn_cancel")) { Close(); } else if (msg.sender->GetName() == _T("btn_close")) { Close(); } }

这里ExecuteCodeSegment的第一个参数是 NSIS 脚本里函数的索引,需要在 NSIS 侧用!insertmacro或函数注册机制对应上。常见做法是在 NSIS 脚本里定义Function .onInstallClick,然后在插件初始化时把函数索引传进来。

3.3 NSIS 脚本加载插件并驱动安装流程

NSIS 脚本负责把 Duilib 插件和资源打包,运行时释放到临时目录,调用插件显示界面,并在回调函数里执行实际安装步骤。

; installer.nsi !addplugindir "build" Name "MyApp" OutFile "build\setup.exe" InstallDir "$PROGRAMFILES\MyApp" RequestExecutionLevel admin ; 定义安装函数,供 Duilib 回调 Var /GLOBAL hwndParent Function .onInstallClick ; 这里执行文件释放 SetOutPath "$INSTDIR" File "app\*.*" ; 写注册表 WriteRegStr HKLM "Software\MyApp" "InstallPath" "$INSTDIR" ; 创建卸载程序 WriteUninstaller "$INSTDIR\uninstall.exe" ; 通知 Duilib 更新进度(通过插件导出函数) Plugin::Call "setup_ui.dll" UpdateProgress 100 FunctionEnd Section "MainSection" ; 释放 Duilib 资源到临时目录 InitPluginsDir SetOutPath "$PLUGINSDIR\ui_res" File /r "setup_ui\res\*.*" File "setup_ui\ui_config.xml" ; 设置环境变量传递资源路径 System::Call 'kernel32::SetEnvironmentVariable(t "SETUP_RES_PATH", t "$PLUGINSDIR\ui_res")' ; 调用 Duilib 插件显示界面 Plugin::Call "setup_ui.dll" ShowInstallUI ; 界面关闭后继续 SectionEnd

逻辑说明:!addplugindir告诉 NSIS 编译器插件 DLL 的位置;InitPluginsDir创建临时目录;File /r把 Duilib 资源释放到临时目录;System::Call设置环境变量传递路径;Plugin::Call调用插件导出函数。Function .onInstallClick是 Duilib 按钮回调时执行的 NSIS 函数,里面做实际的文件释放和注册表写入。

参数说明:RequestExecutionLevel admin确保安装程序有管理员权限;$PLUGINSDIR是 NSIS 自动管理的临时目录,安装结束自动清理;Plugin::Call的第二个参数是导出函数名,必须与 DLL 里__declspec(dllexport)的名字一致。

编译顺序:先用 Visual Studio 编译 Duilib 插件生成setup_ui.dll,再用 NSIS 编译器编译installer.nsi生成setup.exe。建议写一个build.bat把两步串起来,避免手动操作遗漏。

4. 避坑排查:NSIS+Duilib 安装程序最常见的五类翻车

4.1 插件加载失败,安装程序直接闪退

现象:运行setup.exe后窗口没出来,进程直接退出,事件查看器里能看到应用程序错误。

原因:最常见的是 DLL 位数不匹配。NSIS 编译出的安装程序是 32 位,如果 Duilib 插件编译成了 64 位,Plugin::Call加载时会失败。其次是 DLL 依赖的运行时库缺失,比如 Duilib 依赖的d3d9.dll或 VC 运行时没打包进去。

解决:确认插件工程配置为 Win32 平台;用 Dependency Walker 或dumpbin /dependents检查 DLL 依赖;把 VC 运行时静态链接(/MT)或随安装包释放。另外检查!addplugindir路径是否正确,编译时 NSIS 会提示找不到插件。

4.2 Duilib 窗口显示空白或 XML 加载失败

现象:窗口出来了,但里面没有控件,或者只有背景色没有贴图。

原因:资源路径不对。Duilib 从CPaintManagerUI::SetResourcePath设置的目录加载 XML 和贴图,如果 NSIS 释放资源的目录和插件读取的目录不一致,就会加载失败。另一个原因是 XML 文件编码问题,Duilib 对 UTF-8 BOM 敏感,带 BOM 的 XML 可能解析异常。

解决:在插件里加日志输出实际读取的路径,确认SETUP_RES_PATH环境变量值是否正确;XML 文件保存为无 BOM 的 UTF-8;贴图文件用 PNG 格式并确认 alpha 通道正常。如果用了 zip 打包资源,检查解压逻辑是否在窗口创建前执行。

4.3 按钮点击无响应,NSIS 回调不执行

现象:界面正常显示,但点击“开始安装”没反应,进度条不动。

原因:ExecuteCodeSegment的函数索引不对。NSIS 脚本里函数的索引是编译时分配的,如果插件里硬编码了索引值,脚本改动后索引偏移就会导致回调错位。另一个原因是extra_parameters结构体定义与 NSIS 版本不匹配,字段偏移错误导致函数指针无效。

解决:不要硬编码函数索引,改用 NSIS 的GetFunctionAddress在运行时获取地址,通过插件初始化参数传给 Duilib;或者用命名约定,插件通过extra->ExecuteCodeSegment时传函数名字符串,NSIS 侧做映射。检查 NSIS 头文件nsis\Include\pluginapi.h里的extra_parameters定义,确保插件里的结构体声明与之一致。

4.4 安装进度卡在某个百分比不动

现象:进度条走到一半停住,界面无响应,但进程还在。

原因:NSIS 执行文件释放时如果遇到大文件或磁盘 IO 慢,会阻塞主线程,而 Duilib 的消息循环和 NSIS 脚本执行在同一线程,导致界面卡死。另一个原因是进度回调用错了线程,Duilib 控件更新必须在 UI 线程执行。

解决:把耗时的文件释放放到 NSIS 的Section里分步执行,每步之间调用插件更新进度并处理消息队列;或者用 NSIS 的SetDetailsPrint配合DetailPrint输出进度,插件通过定时器读取。如果必须在插件里做异步,用PostMessage通知 UI 线程更新,不要跨线程直接操作控件。

4.5 卸载时残留文件或注册表项

现象:卸载后安装目录还在,或者控制面板里程序列表仍有条目。

原因:NSIS 的卸载程序只删除WriteUninstaller时记录的文件,如果安装过程中动态创建了文件或注册表项,卸载脚本里没有对应删除逻辑。Duilib 释放到$PLUGINSDIR的资源虽然会自动清理,但安装目录下的配置文件、日志、缓存不会自动删。

解决:在 NSIS 的Uninstall段里用RMDir /r "$INSTDIR"强制删除整个安装目录(确认没有用户数据需要保留);注册表项用DeleteRegKey逐项清理;如果安装了服务,先sc stop再sc delete。建议在安装时把关键路径和注册表项记录到卸载日志,卸载时按日志清理。

5. 进阶技巧:让安装程序更像“原生应用”的两个细节

第一个细节是窗口阴影和圆角。Duilib 默认窗口是直角矩形,如果设计稿要求圆角或阴影,需要用分层窗口(WS_EX_LAYERED)配合UpdateLayeredWindow实现。具体做法是在窗口创建时去掉标准边框,用一张带 alpha 通道的底图作为窗口背景,然后通过SetLayeredWindowAttributes或UpdateLayeredWindow渲染。这样窗口可以有不规则形状,但要注意点击穿透问题——透明区域需要返回HTTRANSPARENT给WM_NCHITTEST。

第二个细节是安装进度与 NSIS 的精确同步。NSIS 的DetailPrint输出的是文本日志,Duilib 要显示百分比进度,需要知道总文件数和已释放数。我一般会在 NSIS 脚本里先统计文件列表,把总数通过插件接口传给 Duilib,然后在每个File命令后调用UpdateProgress递增。更精确的做法是用GetFileSize累加总字节数,按字节进度更新,这样大文件和小文件混合时进度条不会跳变。

验证方法:在虚拟机里跑一遍完整安装和卸载,用 Process Monitor 监控文件系统和注册表操作,确认没有遗漏的写入点;用 Dependency Walker 检查安装包和插件的依赖链;在 32 位和 64 位系统上各测一次,确认权限提升和路径重定向行为一致。

我自己的习惯是每次改完 NSIS 脚本或 Duilib 布局,先跑一遍静默安装(setup.exe /S)确认逻辑没问题,再跑界面安装确认交互没问题。这个顺序能快速定位是逻辑 bug 还是界面 bug,省得在界面上瞎点半天。希望帮到你。

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

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

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

立即咨询