☰
NSIS+Duilib自定义安装程序:原理、实现与避坑指南
2026/10/14 3:50:17 网站建设 项目流程

简介:这份资源面向需要制作 Windows 安装程序的开发者,尤其是希望摆脱默认安装界面、追求专业视觉效果的软件发布人员。它把 NSIS 的脚本化安装能力与 Duilib 的 XML 界面渲染结合起来,解决传统安装包界面简陋、交互生硬的问题,适合具备一定脚本与 C++ 基础的中级开发者参考。压缩包为 rar 格式,共 451 个文件,约 9.6MB,其中 png、bmp、ico 等图片资源用于界面皮肤,nsh、nsi 脚本承载安装逻辑,xml 描述布局,dll 与 exe 提供运行支撑,另有 nlf 多语言文件、chm 文档及 bat 构建脚本,结构完整。目前已有 2943 人学习下载。读者可从中获得一套可复用的安装界面皮肤方案、NSIS 与 Duilib 的集成调用思路、快捷方式与开机启动项写入方法,以及多语言与错误日志处理的实践参考,便于快速搭建美观且功能完整的自定义安装程序。

1. NSIS+Duilib 自定义安装程序:为什么原生界面总让用户觉得“不专业”

做过 Windows 桌面交付的同行大概都有过这种体验:功能都调通了,最后卡在安装向导上。NSIS 自带的 Modern UI 能跑,但界面是二十年前的审美,按钮灰扑扑,进度条像 Windows 98 的遗产。客户拿到安装包,第一反应不是“这软件能干什么”,而是“这玩意儿靠谱吗”。安装程序是用户接触产品的第一屏,第一屏掉链子,后面做得再好也先扣三分。

NSIS+Duilib 自定义安装程序,本质上是把 NSIS 的安装逻辑能力和 Duilib 的界面渲染能力拼在一起:NSIS 负责解压、写注册表、建快捷方式、做卸载记录,Duilib 负责画一个完全自定义的窗口,按钮、背景、动效、进度条全由你控制。适合谁?适合那些需要交付 Windows 桌面产品、又不想让安装界面拖后腿的团队。它不解决安装逻辑本身的问题,但能把“第一印象”这件事从及格线拉到能看。

这条路不是没有代价。NSIS 的插件机制、Duilib 的消息循环、两者之间的进程通信,每一层都有坑。下面按“先立住原理、再动手复现、最后排错”的顺序拆开讲。

2. 拆开 NSIS 与 Duilib:谁管安装逻辑,谁管界面渲染

2.1 NSIS 的插件机制与 Duilib 的接入点

NSIS 本身是一个脚本驱动的安装系统,核心是.nsi脚本,编译后生成单个 exe。它的界面能力来自nsDialogs和Modern UI,但这两者都是基于 Win32 原生控件的封装,想做出圆角、半透明、自定义字体渲染的界面,基本不可能。NSIS 真正留出的扩展口是插件(Plugin):一个符合 NSIS 插件接口的 DLL,导出特定签名的函数,NSIS 脚本通过plugin::function调用它。

Duilib 是一个基于 DirectUI 思想的 C++ 界面库,窗口内所有控件都是自绘的,不依赖系统主题。它的典型用法是:创建一个继承自WindowImplBase的窗口类,加载 XML 布局,绑定控件消息,然后跑消息循环。把 Duilib 编译成 NSIS 插件,意味着 NSIS 在安装过程中调用这个 DLL,DLL 内部启动 Duilib 窗口并进入消息循环,用户在这个窗口里完成交互,窗口关闭后控制权交回 NSIS。

这里有一个关键设计决策:Duilib 窗口是模态还是非模态。常见做法是做成模态——NSIS 调用插件函数后阻塞等待,Duilib 窗口关闭时函数返回,NSIS 继续执行后续安装步骤。这样安装流程的时序最清晰,不容易出现“界面还在但文件已经解压完”的错位。

2.2 为什么不用 NSIS 原生界面硬改

有人会想:能不能直接改 NSIS 的 Modern UI 皮肤?答案是能改一点,但改不深。Modern UI 的按钮、进度条、文字都是系统控件,换颜色可以,换形状不行;换字体可以,换渲染方式不行。更麻烦的是,NSIS 的界面线程和安装线程是同一个,你在界面上做任何耗时操作都会卡住整个安装流程。

Duilib 的优势在于它有自己的渲染线程和消息循环,界面刷新和后台安装可以解耦。你可以让 NSIS 在后台解压文件,Duilib 窗口同时播放一个进度动画,两者通过共享内存或命名管道通信。这种解耦带来的体验提升,是原生界面给不了的。

2.3 最小可跑通的工程结构

一个能跑通的 NSIS+Duilib 安装程序,工程结构大致如下:

project/ ├── installer/ │ ├── setup.nsi # NSIS 主脚本 │ ├── ui_plugin/ # Duilib 插件工程 │ │ ├── ui_plugin.cpp │ │ ├── ui_plugin.h │ │ ├── main_window.h │ │ ├── main_window.cpp │ │ └── skin/ # XML 布局和图片资源 │ └── output/ # 编译产物 └── app/ # 被安装的应用程序

NSIS 脚本里调用插件的写法:

; setup.nsi !include "MUI2.nsh" Name "MyApp" OutFile "output\MyAppSetup.exe" InstallDir "$PROGRAMFILES\MyApp" Section "Main" SecMain ; 调用 Duilib 插件,弹出安装界面 ui_plugin::ShowInstallWizard Pop $0 ; $0 返回用户选择的安装路径或取消标志 StrCmp $0 "cancel" done SetOutPath $INSTDIR File /r "..\app\*.*" WriteUninstaller "$INSTDIR\uninstall.exe" done: SectionEnd

插件 DLL 的导出函数签名:

// ui_plugin.cpp #include <windows.h> #include "main_window.h" extern "C" __declspec(dllexport) void ShowInstallWizard(HWND hwndParent, int string_size, char *variables, stack_t **stacktop) { // 初始化 Duilib CPaintManagerUI::SetInstance(GetModuleHandle(NULL)); CPaintManagerUI::SetResourcePath(CPaintManagerUI::GetInstancePath() + _T("skin")); CMainWindow* pWnd = new CMainWindow(); // 创建模态窗口,阻塞直到关闭 pWnd->Create(NULL, _T("安装向导"), UI_WNDSTYLE_FRAME, 0L, 0, 0, 800, 600); pWnd->CenterWindow(); pWnd->ShowModal(); // 将用户选择写回 NSIS 栈 // 具体栈操作需按 NSIS 插件规范实现 delete pWnd; }

这段代码的逻辑说明:ShowInstallWizard是 NSIS 插件标准入口,参数由 NSIS 运行时传入。CPaintManagerUI::SetResourcePath指定皮肤资源目录,通常和安装包一起打包。Create创建窗口,ShowModal进入模态消息循环,窗口关闭后返回。返回值通过 NSIS 栈机制传回脚本,脚本根据返回值决定后续流程。

参数说明:UI_WNDSTYLE_FRAME表示带边框的窗口样式,如果要做无边框自定义标题栏,改成UI_WNDSTYLE_DIALOG并配合 XML 里的caption="0,0,0,32"属性。800, 600是窗口初始尺寸,实际布局由 XML 控制。

3. 从零搭一个可交互的安装向导:XML 布局与 NSIS 回调

3.1 Duilib 窗口类的创建与消息绑定

Duilib 的窗口类需要继承WindowImplBase,重写几个关键虚函数:

// main_window.h class CMainWindow : public WindowImplBase { public: CMainWindow(); virtual ~CMainWindow(); protected: virtual CDuiString GetSkinFolder() override { return _T("skin"); } virtual CDuiString GetSkinFile() override { return _T("install_wizard.xml"); } virtual LPCTSTR GetWindowClassName() const override { return _T("InstallWizard"); } virtual void Notify(TNotifyUI& msg) override; virtual void OnClick(TNotifyUI& msg) override; virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) override; private: CButtonUI* m_pBtnNext; CButtonUI* m_pBtnCancel; CProgressUI* m_pProgress; CRichEditUI* m_pLog; };

GetSkinFile返回 XML 文件名,Duilib 会自动从GetSkinFolder指定的目录加载。Notify处理所有控件事件,OnClick是按钮点击的快捷入口。HandleMessage用于拦截窗口消息,比如自定义拖拽、最小化到托盘等。

3.2 XML 布局:把安装向导拆成可复用的控件树

一个典型的安装向导 XML 结构:

<!-- install_wizard.xml --> <?xml version="1.0" encoding="UTF-8"?> <Window size="800,600" caption="0,0,0,32" mininfo="800,600"> <VerticalLayout bkcolor="#FF1E1E1E"> <!-- 自定义标题栏 --> <HorizontalLayout height="32" bkcolor="#FF2D2D2D"> <Label text="安装向导" textcolor="#FFFFFFFF" font="system_14" padding="12,0,0,0" /> <Control /> <Button name="btn_close" bkimage="close_normal.png" hotimage="close_hover.png" width="32" height="32" /> </HorizontalLayout> <!-- 内容区 --> <VerticalLayout inset="40,30,40,20"> <Label text="欢迎使用 MyApp 安装程序" textcolor="#FFFFFFFF" font="system_20" height="40" /> <Label text="安装程序将引导您完成安装过程。" textcolor="#FFAAAAAA" font="system_12" height="30" /> <!-- 进度条 --> <Progress name="progress_install" height="8" bkcolor="#FF3A3A3A" forecolor="#FF0078D4" value="0" max="100" margin="0,20,0,10" /> <Label name="label_status" text="准备安装..." textcolor="#FFAAAAAA" font="system_12" height="24" /> <!-- 日志区 --> <RichEdit name="log_detail" height="200" bkcolor="#FF252525" textcolor="#FFCCCCCC" font="system_12" vscrollbar="true" margin="0,10,0,0" /> </VerticalLayout> <!-- 底部按钮 --> <HorizontalLayout height="60" bkcolor="#FF2D2D2D" inset="20,0,20,0"> <Control /> <Button name="btn_cancel" text="取消" textcolor="#FFFFFFFF" bkcolor="#FF3A3A3A" width="100" height="36" margin="0,0,12,0" /> <Button name="btn_next" text="开始安装" textcolor="#FFFFFFFF" bkcolor="#FF0078D4" width="120" height="36" /> </HorizontalLayout> </VerticalLayout> </Window>

布局说明:caption="0,0,0,32"定义可拖拽区域为顶部 32 像素,配合自定义标题栏实现无边框窗口拖动。inset控制内边距,margin控制外边距。Progress控件的value和max决定进度百分比。RichEdit用于显示安装日志,vscrollbar="true"开启垂直滚动。

3.3 NSIS 与 Duilib 的进度同步:共享内存方案

安装进度需要从 NSIS 传到 Duilib 窗口。常见做法是共享内存:

// 在插件 DLL 中创建共享内存 HANDLE hMapFile = CreateFileMapping( INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, _T("MyAppInstallProgress")); LPVOID pBuf = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 4096); // 结构体定义 struct InstallProgress { volatile LONG percent; // 0-100 volatile LONG status; // 0=进行中, 1=完成, 2=失败 TCHAR message[256]; };

NSIS 侧通过插件函数更新进度:

; 在解压循环中调用 ui_plugin::UpdateProgress 30 "正在解压主程序..."

Duilib 窗口用定时器读取共享内存并刷新界面:

// 在 HandleMessage 中处理 WM_TIMER case WM_TIMER: if (wParam == TIMER_PROGRESS) { InstallProgress* p = (InstallProgress*)m_pSharedMem; m_pProgress->SetValue(p->percent); m_pLog->AppendText(p->message); if (p->status == 1) { m_pBtnNext->SetText(_T("完成")); KillTimer(m_hWnd, TIMER_PROGRESS); } return 0; } break;

参数说明:TIMER_PROGRESS是自定义定时器 ID,间隔建议 200-500ms,太短会频繁刷新导致界面卡顿,太长进度条会跳变。共享内存大小 4096 字节足够,结构体里用volatile防止编译器优化。

4. 避坑指南:NSIS+Duilib 安装程序最容易翻车的 5 个地方

4.1 插件 DLL 加载失败:路径与位数不匹配

现象:NSIS 脚本调用ui_plugin::ShowInstallWizard时直接报错,提示找不到插件或插件加载失败。

原因:NSIS 插件 DLL 必须放在 NSIS 安装目录的Plugins子目录下,或者和.nsi脚本同目录。更隐蔽的问题是位数:32 位 NSIS 只能加载 32 位插件,64 位 NSIS 只能加载 64 位插件。如果 Duilib 编译成 64 位而 NSIS 是 32 位,加载必然失败。

解决:确认 NSIS 和插件的目标平台一致。用makensis编译时加-DNSIS_PLUGIN_PATH指定插件目录。检查 DLL 导出函数名是否被 C++ 名称修饰破坏,插件入口必须用extern "C"包裹。

4.2 Duilib 窗口不显示或闪退:资源路径与初始化顺序

现象:插件被调用后窗口一闪而过,或者根本不出现,进程直接退出。

原因:CPaintManagerUI::SetResourcePath设置的路径不对,XML 加载失败后 Duilib 内部抛异常。另一个常见原因是CPaintManagerUI::SetInstance没有在窗口创建前调用,导致 Duilib 找不到资源句柄。

解决:在插件入口函数最开头就设置实例和资源路径,路径用绝对路径,不要用相对路径。资源目录必须和安装包一起打包,NSIS 的File命令要把skin目录整个复制到临时目录。可以在GetSkinFile返回前加日志,确认 XML 文件确实存在。

4.3 进度条卡死:消息循环阻塞

现象:安装过程中进度条不动,界面无响应,点击取消按钮也没反应。

原因:NSIS 在后台解压大文件时占用了主线程,Duilib 的消息循环被阻塞。或者共享内存的读取频率太高,界面线程一直在等锁。

解决:把耗时操作放到 NSIS 的独立线程或子进程中,Duilib 窗口只负责渲染。共享内存用无锁设计,只读写volatile变量,不要加互斥锁。定时器间隔调到 300ms 以上,给界面线程留出响应时间。

4.4 卸载时残留文件:卸载程序没有清理皮肤资源

现象:软件卸载后,安装目录下还残留skin文件夹和 XML 文件。

原因:NSIS 的卸载脚本只删除了主程序文件,没有删除 Duilib 运行时释放的临时资源。或者卸载程序本身也是 Duilib 写的,卸载时窗口还在运行,文件被占用无法删除。

解决:在卸载脚本里显式删除skin目录和所有临时文件。如果卸载程序用了 Duilib,先关闭窗口再执行删除,或者用Delete /REBOOTOK标记重启后删除。注册表里的卸载信息要写完整,包括安装路径和版本号。

4.5 高 DPI 下界面模糊:没有做 DPI 适配

现象:在 4K 显示器上,Duilib 窗口里的文字和图片模糊,按钮位置偏移。

原因:Duilib 默认按 96 DPI 渲染,系统缩放 150% 或 200% 时,窗口被系统拉伸导致模糊。

解决:在插件入口调用SetProcessDPIAware()声明进程支持 DPI。Duilib 的 XML 里用dp单位代替px,或者根据GetDpiForWindow动态调整布局尺寸。图片资源准备 2x 版本,根据 DPI 切换加载。

5. 进阶技巧:让安装程序看起来不像“安装程序”

5.1 用 Duilib 做无边框异形窗口

传统安装程序都是矩形窗口,Duilib 可以做出圆角甚至异形窗口。关键在 XML 的Window标签里设置roundcorner属性,配合 PNG 透明通道:

<Window size="800,600" roundcorner="12,12,12,12" caption="0,0,0,32" bktrans="true">

roundcorner四个值分别对应左上、右上、右下、左下圆角半径。bktrans="true"开启背景透明,窗口阴影由 Duilib 自动绘制。注意异形窗口在 Windows 7 上兼容性较差,如果目标用户还有 Win7,建议保留矩形窗口加圆角图片背景。

5.2 安装动画与音效的轻量集成

Duilib 支持 GIF 和 APNG 动画控件。在安装等待页面放一个 200x200 的加载动画,比静态进度条更有质感:

<Gif name="loading_anim" bkimage="loading.gif" width="200" height="200" autoplay="true" />

音效方面,Duilib 本身不处理音频,但可以在 NSIS 脚本里用nsisSound插件播放安装完成的提示音。注意音效文件要压缩到 100KB 以内,否则安装包体积会明显膨胀。

5.3 安装日志的实时回传与错误定位

安装失败时,用户往往只看到“安装失败”四个字,无法定位问题。在 Duilib 窗口里做一个可折叠的日志面板,把 NSIS 的详细输出实时显示出来:

; NSIS 侧写日志到共享内存 ui_plugin::AppendLog "正在写入注册表项..."
// Duilib 侧读取并显示 void CMainWindow::AppendLog(const CString& msg) { m_pLog->AppendText(msg + _T("\r\n")); // 自动滚动到底部 m_pLog->LineScroll(m_pLog->GetLineCount()); }

日志内容要包含时间戳、操作类型、目标路径。安装成功后日志可以折叠隐藏,安装失败时自动展开并高亮错误行。这个功能在用户反馈问题时能省掉大量来回沟通。

5.4 一个我踩过的坑:NSIS 变量在插件里的可见性

NSIS 的变量(如$INSTDIR)在插件 DLL 里不能直接访问。我一开始想当然地在 Duilib 窗口里读$INSTDIR,结果拿到的是空字符串。正确做法是通过插件函数的variables参数传递,或者用GetInstDir这样的自定义插件函数显式获取。NSIS 插件规范里,variables是一个指向变量表的指针,但解析起来比较繁琐,更简单的方案是在 NSIS 脚本里把需要的值写入共享内存或临时文件,插件再去读。

这个坑让我多花了大半天排查,最后发现是 NSIS 的变量作用域和插件隔离机制导致的。后来我养成了一个习惯:凡是 NSIS 和插件之间要传递的数据,一律走共享内存或临时文件,不依赖插件的参数解析。这样虽然多写几行代码,但调试起来省心得多。

希望帮到你。

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

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

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

立即咨询