资讯动态

NSIS+DuiLib自定义安装界面:插件集成与整窗接管实践

发布时间:2026/10/13 1:44:50 来源:尧图企业网站定制
简介NSIS与Duilib结合的自定义安装程序开发素材包主要面向需要美化Windows安装界面的中高级开发者解决默认NSIS安装界面样式单一、难以贴合产品品牌视觉的问题。压缩包共451个文件体积约9.6MB类型覆盖113个PNG位图、113个NSH脚本片段、58个NLF多语言文件、35个ICO图标、32个BMP背景、24个XML布局文件、29个DLL动态库以及EXE、BAT辅助工具和NSI主脚本这些文件分别承担界面皮肤、安装逻辑、多语言、资源封装与编译打包等职责。包内还提供CHM帮助文档与多份示例工程便于对照学习NSIS脚本命令、Duilib控件与XML布局的配合方式以及快捷方式创建、注册表写入等常见安装功能的实现思路。内容还涉及安装目录选择、文件解压、界面事件绑定、错误处理与日志记录等环节有助于减少实际项目中的排错成本。已有2942人学习下载适合作为自定义安装程序从界面设计到功能集成的完整参考。1. NSISDuiLib 自定义安装程序用 NSIS 的壳装 Duilib 的魂第一次接触 NSISDuiLib 自定义安装程序多半是产品经理丢来一句话安装界面能不能做成我们自己的风格默认 NSIS 向导页虽然能用但在做品牌装机体验时确实撑不住按钮、进度条、背景图都受系统主题限制。把 DuiLib 接进来以后窗口由 DuiLib 的 XML 布局驱动NSIS 继续负责文件释放、注册表、快捷方式和卸载器。这个组合适合手里有 C 维护能力又想避免 Qt 那种十兆级运行时包袱的团队如果你只是换文案和 Logo直接改 NSIS 的 MUI 主题更划算。2. 为什么是 NSISDuiLib它把安装逻辑和界面分成两块而不是一团乱麻2.1 分工NSIS 管“安装”DuiLib 管“展示”NSIS 最值钱的部分不是它的默认界面而是脚本编译出来的安装骨架文件提取、目录选择、注册表写入、卸载器生成、版本判断这些能力经过十几年验证插件生态也完整。DuiLib 则是一个 DirectUI 界面库窗口上的控件不依赖系统控件而是由 DuiLib 自己绘制。它的 XML 布局文件能描述窗口大小、背景、按钮位置和字体颜色改样式时不用重新编译 C DLL只要替换资源和 XML 就行。所以我的分工策略很固定NSIS 的 Section 只写安装逻辑不在里面弹 MessageBoxDuiLib DLL 只做展示和收集用户选择不直接碰文件复制。两者通过一个很薄的接口通信——用户点了“安装”或“取消”DLL 往 NSIS 栈里压一个结果值NSIS 根据这个值决定继续 Section 还是 Abort。这种拆分让安装器变成可维护的工程而不是把 C 和 NSIS 混在一起调试到怀疑人生。2.2 页面插件与整窗接管两种接法两种代价实际落地时我看过两种主流接法。第一种是页面插件法把 DuiLib 窗口嵌到 NSIS 的 Page 流程里用Page custom指定一个自定义页面函数函数里调用 DuiLib 插件显示窗口。用户操作结束函数返回NSIS 继续执行后面的 Section。这个方案和 NSIS 的安装生命周期天然兼容卸载器也能照常生成改动最小。第二种是整窗接管法在.onGUIInit阶段直接把 NSIS 默认窗口隐藏创建一个无边框的 DuiLib 顶层窗口盖在上面。商业安装器经常这么干界面自由度最大但你要自己处理消息循环、窗体销毁和 NSIS 页面跳转否则很容易出现“点安装之后程序假死”的故障。新手我建议从页面插件法起步不要一上来就追求整窗接管。2.3 NSIS 插件入口和栈传参的最小约定如果你选择页面插件法需要把 DuiLib 窗口封装成一个 NSIS 插件 DLL。NSIS 插件不是普通的 Win32 DLL它有一套入口约定导出函数会收到父窗口句柄、参数指针和堆栈指针脚本里调用插件后插件可以把结果 push 到 NSIS 栈脚本再用Pop取出来。这个约定是 NSIS 所有插件的基础DuiLib 插件也逃不开。我一般会写一个名字很直白的入口比如Show。脚本侧看起来是这样的Function ShowDuiPage DuiUI::Show $HWNDPARENT Pop $0 ${If} $0 cancel Abort ${EndIf} FunctionEnd这段代码里的DuiUI::Show不是 NSIS 官方插件是我自己编译的 DuiLib UI 插件名。$HWNDPARENT是 NSIS 传给当前页面的父窗口句柄插件需要用这个句柄来决定窗口挂载关系。Pop $0取出插件压入栈的结果如果用户点取消Abort会让安装流程停下来。如果不想按 NSIS 插件规范写也可以把 DuiLib DLL 编成普通 DLL用System::Call显式调用。比如System::Call DuiUI.dll::ShowInstallWindow(i $HWNDPARENT, i 0) i .r0这里i表示整数参数.r0表示把返回值写进$0。普通 DLL 方式的好处是复用已有 DuiLib 代码更方便坏处是你要自己处理 DLL 释放路径和 stdcall 约定容易在路径上踩坑。我的建议是能走 NSIS 插件就走插件栈传参比手工调System::Call更稳。2.4 为什么不建议把安装逻辑全塞进 DuiLib DLL有时候团队会觉得反正界面是 DuiLib 的干脆文件复制、注册表写入也全用 C 做NSIS 只当压缩壳。这种思路能让进度条做到字节级精确但代价是要复活一套安装器该有的所有边缘逻辑覆盖已安装版本时备份文件、卸载时回滚注册表、处理被杀毒软件占用的文件、支持静默安装参数。这些能力 NSIS 已经趟过无数坑没必要重新发明一遍。所以我坚持让 NSIS 保留主流程。如果进度条精度不够用阶段进度来补偿如果 UI 需要实时反馈可以让 Section 每完成一个大步骤就调用一次插件上的SetProgress方法。这样既保住了 DuiLib 的观感又保住了 NSIS 的稳定性。3. 最小可跑工程NSIS 脚本、DuiLib DLL 和 XML 三件套3.1 目录结构与编译输出一个能实际跑起来的最小工程至少要有四块东西NSIS 脚本、DuiLib 插件 DLL、Duilib 资源目录、被安装的应用文件。我习惯于建这样一个目录路径作用script/install.nsiNSIS 安装脚本入口plugin/DuiUI/C 插件源码编译产出DuiUI.dllres/skin/layout.xmlDuiLib 窗口布局res/skin/img/按钮、背景、Logo 等图片资源payload/最终要装进用户机器的文件编译 DuiLib 插件时把产物放到 NSIS 的Plugins/x86-unicode目录下。这样 NSIS 编译器会自动把它打进安装包运行时自动解压到临时目录并加载。不用自己在脚本里File插件 DLL这是 NSIS 插件机制自带的能力。3.2 NSIS 脚本骨架把 DuiLib 页面挂进安装流程下面是一个可复现的脚本骨架只保留了最核心的流程Unicode true Name DemoApp OutFile DemoSetup.exe InstallDir $LOCALAPPDATA\DemoApp RequestExecutionLevel user Page custom ShowDuiPage Section Main SetOutPath $INSTDIR File payload\app.exe File payload\README.txt WriteUninstaller $INSTDIR\Uninstall.exe SectionEnd Section Uninstall Delete $INSTDIR\app.exe Delete $INSTDIR\README.txt Delete $INSTDIR\Uninstall.exe RMDir $INSTDIR SectionEnd Function ShowDuiPage DuiUI::Show $HWNDPARENT Pop $0 ${If} $0 cancel Abort ${EndIf} FunctionEndUnicode true是必须的尤其当 DuiLib 的 XML 里包含中文时ANSI 模式会出现乱码。Page custom ShowDuiPage把自定义页面插到安装流程最前面如果用户点了 DuiLib 窗口上的“取消”插件返回cancelAbort会让安装器直接退出。Section Uninstall是卸载器的逻辑WriteUninstaller会生成卸载程序。注意这里我用了user权限而不是admin因为示例安装到用户目录不需要提权。如果你的安装目录在Program Files或需要写系统服务改成admin但 UAC 弹窗会出现在 DuiLib 界面之前这一层交互要提前跟产品经理说清楚。3.3 插件 DLLDuiLib 消息循环怎么和 NSIS 握手下面是一段 C 伪代码展示 DuiLib 插件入口的基本形状#include duilib/UIlib.h #include nsis/pluginapi.h class CMainWnd : public CWindowWnd, public INotifyUI { public: LPCTSTR GetWindowClassName() const override { return _T(DuiDemoWnd); } void Notify(TNotifyUI msg) override { if (msg.sType DUI_MSGTYPE_CLICK) { if (msg.pSender-GetName() _T(btn_install)) { ::PostQuitMessage(0); } } } }; PLUGIN_EXPORT void Show(HWND hwndParent, int, char*, stack_t** stacktop, extra_parameters*) { CMainWnd* wnd new CMainWnd(); wnd-Create(hwndParent, _T(layout.xml), WS_POPUP, 0, 0); wnd-CenterWindow(); ::ShowWindow(wnd-GetHWND(), SW_SHOW); CPaintManagerUI::MessageLoop(); pushstring(stacktop, _T(ok)); delete wnd; }这里的重点有两个。第一Create的父窗口句柄直接用了 NSIS 传进来的hwndParent这样 DuiLib 窗口能作为页面嵌入 NSIS 容器。第二CPaintManagerUI::MessageLoop会进入 DuiLib 自己的消息循环直到窗口收到退出消息。用户点“安装”按钮时PostQuitMessage(0)让消息循环退出然后pushstring把结果压给 NSIS。这套流程最忌讳的是在按钮回调里写耗时的安装操作。因为你还在消息循环里界面会被卡住看起来像死掉。所以按钮回调只做两件事记录用户意图、退出消息循环。3.4 XML 布局DuiLib 的窗口样式从这里开始DuiLib 控件的摆放不靠代码写而是靠 XML 布局文件。一个最小布局长这样Window size500,360 caption0,0,0,0 shadowfalse VerticalLayout bkcolor#FFFFFF inset20,20,20,20 Label nametitle text安装 DemoApp aligncenter height48 font1/ Label namedesc text点击安装开始部署 height32/ HorizontalLayout height36 pos0,-56 childmargin10 Button namebtn_install text安装 width100/ Button namebtn_cancel text取消 width100/ /HorizontalLayout /VerticalLayout /WindowWindow标签决定窗口大小和是否显示系统标题栏。VerticalLayout和HorizontalLayout是 DuiLib 的容器子控件会按容器方向排列。name是控件在代码里的唯一标识上面 C 代码里FindControl就是靠这个名字来绑定按钮事件的。pos0,-56表示相对父容器右下角定位适合把按钮固定在窗口底部。XML 文件路径要传给 DuiLib 的窗口创建函数路径基准通常是插件 DLL 所在目录。强调一个坑不要在 XML 里写绝对路径否则换机器就崩。资源图片用相对路径并在 C 里通过CPaintManagerUI::SetResourcePath设置当前资源目录。4. 参数与资源文件把进度、取消、静默安装都接到 DuiLib 窗口上4.1 阶段进度别追求字节级精确先把流程打通NSIS 的File命令是压缩包解压流程它不提供逐字节回调。硬要在 DuiLib 进度条上显示“当前解压到第 3 个文件”会非常别扭。我一般用阶段进度Section Main SetOutPath $INSTDIR DuiUI::SetProgress 10 File payload\app.exe DuiUI::SetProgress 50 File payload\README.txt DuiUI::SetProgress 80 WriteUninstaller $INSTDIR\Uninstall.exe DuiUI::SetProgress 100 SectionEndDuiUI::SetProgress是插件暴露的另一个方法参数直接填 0 到 100 的整数。DLL 侧收到后把这个值转成进度条控件的百分比。这样每一步只跳一次进度用户看到的是“在动”而不是精确到字节的流畅动画。如果产品经理坚持要平滑进度唯一合理路径是把文件复制放到 DuiLib DLL 的工作线程里用CopyFileEx的回调函数更新 UI。但这意味着你要自己写文件存在性判断、目录创建、写失败回滚NSIS 的优势就废了。我的经验是先用阶段进度交付大多数用户根本不关心安装进度条精确不精确只关心它没卡住。4.2 卸载器也要复用同一套 DuiLib UI卸载器默认不会自动带上 UI 资源需要你在安装时把插件和 XML 放到固定位置或在卸载 Section 里重新释放。一个常见做法是这样Section Uninstall SetOutPath $PLUGINSDIR File plugin\DuiUI.dll File res\skin\layout.xml un.DuiUI::SetResourcePath $PLUGINSDIR un.DuiUI::Show uninstall Pop $0 Delete $INSTDIR\app.exe Delete $INSTDIR\README.txt Delete $INSTDIR\Uninstall.exe RMDir $INSTDIR SectionEndun.前缀是 NSIS 用来区分安装和卸载调用插件函数的约定。卸载 Section 里会先用File把资源释放到$PLUGINSDIR再加载 DuiLib 窗口。如果你不在安装时额外保存这些文件卸载器跑起来会找不到 layout.xml窗口直接黑屏或者闪退。有人会觉得这样做导致卸载程序体积变大但其实资源文件很小完全值得。还有一种做法是把 UI 相关文件装进$INSTDIR但卸载器在删除目录时会有文件占用问题$PLUGINSDIR是更安全的临时落点。4.3 静默安装和/S让 UI 在需要时消失企业批量部署通常要用静默安装NSIS 本身已经支持/S参数。问题在于Page custom函数在静默模式下也会被调用如果不处理用户看到的会是闪一下窗口然后退出。所以在ShowDuiPage函数开头要加一道判断Function ShowDuiPage IfSilent 0 2 Abort DuiUI::Show $HWNDPARENT Pop $0 ${If} $0 cancel Abort ${EndIf} FunctionEnd如果安装器收到/SNSIS 会进入静默状态IfSilent成立直接Abort跳过自定义页面。这样交互安装走 DuiLib 窗口静默安装走纯 NSIS 逻辑两边不冲突。如果静默安装还要指定安装目录可以在命令行里加/D参数NSIS 会把它解析到$INSTDIR。但要注意/D必须放在命令行最后这是 NSIS 的硬性规定写参数解析代码时别被这个坑绊倒。5. 避坑 / 常见问题DuiLib 窗口卡死、DPI 模糊和中文乱码的排查记录5.1 点击安装后整个程序无响应现象DuiLib 窗口正常显示点“安装”按钮后窗口卡住等十几分钟也没反应。如果按住按钮还能看到界面上的文字但安装流程没启动十有八九是消息循环堵住了。原因最常见的是在 DuiLib 按钮回调里直接调用了耗时操作或者尝试从 UI 线程启动 NSIS Section而 NSIS 还在等待Show函数返回。插件函数没返回NSIS 就不会继续跑安装但 UI 线程又被消息循环占住于是两边互等。解决严格遵循“UI 只收集意图”的原则。按钮回调里只记录结果并退出消息循环安装逻辑完全交给 NSIS Section。如果你需要安装过程中持续更新界面也不要直接在插件里开线程调 NSIS 内部 API而是用SetProgress这类消息驱动方式让 NSIS 在 Section 执行间隙通知插件。5.2 DPI 缩放导致界面模糊和控件错位现象同一台电脑上默认 100% 缩放时界面正常把系统分辨率改成 150% 后DuiLib 窗口看着像被拉伸过按钮之间缝隙忽大忽小文字还发虚。原因安装器没有声明 DPI 感知Windows 对它做了虚拟化缩放。DuiLib 内部按 96 DPI 计算布局坐标一旦系统缩放比例改变控件坐标就对不上了。解决在插件入口最早的地方调用SetProcessDpiAwarenessContext或声明 PerMonitorV2 的 manifest让系统不要拉伸。然后根据窗口的 DPI 值对字体和布局做缩放。至少保证安装器在 100%、125%、150% 三档常见缩放下不错位。这条经验很血泪因为大多数开发机是 100% 缩放交付给用户就翻车。5.3 XML 中文乱码现象layout.xml 里写的按钮文字“安装”在窗口上显示成“瀹夎”路径填写框输入中文目录后落盘路径也变成乱码。原因NSIS 脚本或 C 工程编译成了 ANSI 版本DuiLib 控件内部却用 Unicode 字符串两边编码不一致。还有一个隐蔽原因XML 文件本身没有声明 UTF-8也没有以 UTF-8 保存。解决NSIS 侧写Unicode trueC 工程统一使用/D_UNICODE /DUNICODEXML 文件保存为 UTF-8 with BOM。检查这三个地方后中文问题基本可以消除。如果是从老项目迁移还要看一下 DuiLib 控件SetText传进去的是_T还是L建议全工程只用宽字符。5.4 窗口阴影和圆角在部分系统上出现黑块现象安装界面在 Win10 上看着还行在一台 Win7 上窗口周围出现黑色边缘圆角区域变成黑底。原因阴影和圆角依赖分层窗口或透明区域裁剪有些老系统/远程桌面环境不支持或者 DuiLib 的默认绘制路径走了 GDI 而非硬件加速。解决在 XML 布局里把shadow强制设为falseroundcorner根据目标系统决定是否开启。如果产品上必须保留圆角可以用一张带透明通道的 PNG 背景来模拟而不是依赖窗口层。我一般会专门在虚拟机里用最旧的目标系统过一遍 UI黑块这种东西截图到测试单上很难解释。5.5 用户取消安装后注册表还是被写了现象点“取消”按钮DuiLib 窗口正常关闭安装流程也停了但某些注册表项已经写入到系统里残留了垃圾数据。原因ShowDuiPage函数返回后NSIS 并没有真正取消后续操作或者在取消前已经执行了部分 Section 逻辑。自定义页面插件的Abort只能在页面阶段阻止流程如果用户在文件复制完成之后才点取消Section 里的写入已经完成了。解决在ShowDuiPage里判断Pop $0后如果是取消立即Abort同时把 DuiLib 窗口的关闭按钮也映射成取消状态不要在窗口关闭时默认当“确定”。对于更彻底的回滚需要给 Section 加事务记录已写入的路径和注册表项取消时反向清理。普通场合作到“页面取消不继续执行”就够了别承诺用户取消后系统完全干净。6. 进阶在安装器里加一套命令行上下文让同一份包同时服务交互和静默分发如果安装器只会“下一步下一步”它就只能撑起最简单的场景。一旦进入区分版本、区分安装模式甚至要支持 CI/CD 自动打包就要给 DuiLib UI 和 NSIS 之间加一层“安装上下文”。我的做法是在 NSIS 脚本开头解析几个自定义参数比如/MODEdev或/MODEprod。解析结果存进 NSIS 变量然后在调用 DuiLib 插件之前通过插件接口传进去。代码示例如下${GetParameters} $R0 ${If} $R0 /MODEdev StrCpy $InstallMode dev ${Else} StrCpy $InstallMode prod ${EndIf} DuiUI::SetInstallMode $InstallMode DuiUI::Show $HWNDPARENT插件侧收到模式字符串后可以向 DuiLib 窗口设置不同的背景图片或按钮文案。这样一个安装包既能给内部测试用也能给正式用户用界面元素通过参数切换不用维护两套安装工程。最后的验证工作我会按表格逐项过一遍检查项通过标准全新安装首次安装无残留目录权限正确覆盖安装旧版本被替换不会同时存在两份配置取消安装不继续写注册表不释放后续文件静默安装/S参数下退出码为 0无 UI 闪现高分辨率150% 缩放下控件不重叠、文字不虚卸载能弹出 DuiLib 卸载界面删净主文件和目录当年我图省事直接做成整窗接管结果 NSIS 和 DuiLib 的消息循环互相打架一度以为这个方案根本跑不通。后来退回Page custom把插件当成一个“会画界面的函数”所有安装行为交回 NSIS问题才真正解决。现在我做这类安装器都先立这个规矩UI 只表达用户意图不越权干活进度可以粗糙流程必须稳定。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑