拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

NSIS+Duilib自定义安装程序:桌面端分发实战与避坑指南

NSIS+Duilib自定义安装程序:桌面端分发实战与避坑指南 简介本资源面向Windows桌面开发与安装包制作人员提供一套基于NSIS与Duilib的自定义安装程序完整工程解决传统安装界面简陋、交互体验差的问题适合具备一定脚本与C基础的开发者进阶使用。压缩包共451个文件约9.6MB涵盖113个nsh脚本、113个png界面素材、58个nlf多语言文件、35个ico图标、32个bmp位图、29个dll动态库、24个xml布局及17个exe工具等另有nsi主脚本、bat构建批处理与chm帮助文档构成从皮肤资源到编译产物的完整链路。已有2942人学习下载。读者可从中获取Duilib皮肤XML布局与控件样式、NSIS脚本中调用DLL显示界面的集成方式、快捷方式与开机启动项写入、多语言适配及错误日志处理等关键实现并借助构建脚本快速生成带专业外观的安装程序提升软件首次安装的用户体验。1. NSISDuilib 自定义安装程序为什么老牌组合仍是桌面端分发的硬通货做 Windows 桌面分发的同行都清楚一个现实安装包是用户接触产品的第一道门而这道门往往被做成了一张毫无辨识度的灰底进度条。NSIS 负责打包逻辑Duilib 负责界面渲染两者拼在一起就能把那个「下一步下一步」的默认向导换成完全自定义的安装体验。这套组合不是新东西但在需要控制安装流程、嵌入品牌视觉、做静默参数分发的场景里它依然是投入产出比最高的方案之一。适合谁适合那些用 Inno Setup 觉得界面改不动、用 Electron 打包又嫌体积失控的团队。接下来我会把从环境搭建到界面联调、再到参数分发的完整路径拆开讲中间踩过的坑一并交代。2. 环境搭建与最小可运行骨架先把 NSIS 和 Duilib 接上2.1 为什么选 NSIS 做安装逻辑、Duilib 做界面NSIS 的核心优势在于脚本化安装流程和极小的运行时开销。它编译出来的是原生 Win32 可执行文件不依赖 .NET 或 Java 运行时安装包体积可以压到几百 KB。但 NSIS 自带的界面系统基于经典 Win32 控件想做出圆角、渐变、自定义按钮这些效果靠 NSIS 原生指令几乎是在跟自己较劲。Duilib 是一套轻量级 C 界面库基于 DirectUI 思想用 XML 描述布局、用图片和颜色定义外观渲染效率高适合做无边框窗口和自绘控件。把 Duilib 编译成静态库或 DLL在 NSIS 的自定义插件里调用它创建窗口就能绕开 NSIS 原生界面的限制。常见做法是NSIS 负责文件释放、注册表写入、快捷方式创建、卸载信息注册Duilib 负责安装向导的每一页 UI。两者通过 NSIS 插件机制通信——插件是一个导出特定函数的 DLLNSIS 脚本调用插件函数插件内部创建 Duilib 窗口并返回用户操作结果。提示Duilib 有多个分支版本选择时注意看是否支持 Unicode 和 x64 编译否则在 64 位系统上会出玄学问题。2.2 编译 Duilib 静态库并与 NSIS 插件工程对接第一步是把 Duilib 源码编译成静态库。以 Visual Studio 为例新建一个静态库工程把 Duilib 的 Core、Control、Layout、Render 等目录下的源文件加进去注意排除掉 UIDlgBuilder 之外的示例代码。# 目录结构建议 project/ ├── duilib/ # Duilib 源码 │ ├── Core/ │ ├── Control/ │ ├── Layout/ │ └── Render/ ├── nsis_plugin/ # NSIS 插件工程 │ ├── plugin.cpp │ ├── plugin.def │ └── ui_main.xml └── installer.nsi # NSIS 脚本编译参数上关键几项运行库选 /MT静态链接 CRT避免目标机器缺 VC 运行库字符集选 Unicode如果目标有 32 位系统平台选 Win32。编译产物是 duilib.lib。接下来建 NSIS 插件工程。插件需要导出 NSIS 约定的函数签名通常是HWND __declspec(dllexport) ShowInstPage(HWND hParent, int nPage)这种形式。在 plugin.cpp 里包含 Duilib 头文件链接 duilib.lib然后写窗口创建逻辑。// plugin.cpp 核心片段 #include stdafx.h #include UIlib.h using namespace DuiLib; class CInstWnd : public CWindowWnd, public INotifyUI { public: LPCTSTR GetWindowClassName() const { return _T(InstWnd); } UINT GetClassStyle() const { return UI_CLASSSTYLE_FRAME; } void OnFinalMessage(HWND) { delete this; } LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg WM_CREATE) { m_pm.Init(m_hWnd); CDialogBuilder builder; CControlUI* pRoot builder.Create(_T(ui_main.xml), NULL, NULL, m_pm); m_pm.AttachDialog(pRoot); m_pm.AddNotifier(this); return 0; } LRESULT lRes 0; if (m_pm.MessageHandler(uMsg, wParam, lParam, lRes)) return lRes; return CWindowWnd::HandleMessage(uMsg, wParam, lParam); } void Notify(TNotifyUI msg) { if (msg.sType _T(click)) { if (msg.pSender-GetName() _T(btn_install)) { // 通知 NSIS 开始安装 PostMessage(m_hWnd, WM_USER 100, 0, 0); } } } private: CPaintManagerUI m_pm; };这段代码做了三件事注册窗口类、在 WM_CREATE 时加载 XML 布局、通过 Notify 捕获按钮点击。m_pm.MessageHandler是 Duilib 的消息分发入口必须放在自定义消息处理之前。PostMessage(WM_USER 100)是给 NSIS 插件层发的自定义信号插件再通过回调通知 NSIS 脚本继续执行。参数说明UI_CLASSSTYLE_FRAME表示无边框窗口样式如果需要系统阴影可以换成UI_CLASSSTYLE_FRAME | CS_DROPSHADOW。XML 路径用相对路径时工作目录必须是插件 DLL 所在目录否则会加载失败。2.3 NSIS 脚本调用插件的最小闭环插件编译出 plugin.dll 后放到 NSIS 安装目录的 Plugins 子目录下。NSIS 脚本里这样调用; installer.nsi OutFile MyInstaller.exe InstallDir $PROGRAMFILES\MyApp RequestExecutionLevel admin Page custom ShowCustomPage !define MUI_PAGE_CUSTOMFUNCTION_LEAVE LeaveCustomPage Function ShowCustomPage ; 调用插件显示 Duilib 窗口 plugin::ShowInstPage $HWNDPARENT 0 Pop $0 FunctionEnd Function LeaveCustomPage ; 用户点击安装后跳转到实际安装页 FunctionEnd Section MainSection SetOutPath $INSTDIR File /r dist\*.* CreateShortCut $DESKTOP\MyApp.lnk $INSTDIR\MyApp.exe WriteUninstaller $INSTDIR\uninstall.exe SectionEndPage custom声明了一个自定义页面ShowCustomPage函数里通过plugin::ShowInstPage调用插件。$HWNDPARENT是 NSIS 向导窗口句柄传给插件作为父窗口。Pop $0取回插件返回值可以用来判断用户是点了「安装」还是「取消」。这个最小闭环跑通后你会看到一个由 Duilib 渲染的窗口嵌在 NSIS 向导流程里。但此时窗口和 NSIS 的页面切换还是割裂的下一步要解决的是状态同步和安装进度回传。3. 界面与逻辑解耦Duilib 页面栈与 NSIS 安装回调的配合3.1 用 XML 定义安装向导的多页布局Duilib 的布局全部写在 XML 里一个安装向导通常包含欢迎页、许可协议页、安装路径选择页、安装进度页、完成页。可以用一个 XML 文件定义多个 VerticalLayout通过显示隐藏来切换也可以每个页面一个 XML 文件动态加载。推荐做法是单 XML 多容器减少文件 IO。结构大致如下!-- ui_main.xml -- Window size600,400 caption0,0,0,32 mininfo600,400 VerticalLayout bkcolor#FFFFFFFF inset0,32,0,0 !-- 标题栏 -- HorizontalLayout height32 bkcolor#FF2D2D2D Label textMyApp 安装向导 textcolor#FFFFFFFF fontsystem_14 / Control / Button namebtn_close bkimageclose.png width32 height32 / /HorizontalLayout !-- 页面容器 -- TabLayout namepage_container count1 !-- 欢迎页 -- VerticalLayout namepage_welcome Label text欢迎使用 MyApp fontsystem_20 textcolor#FF333333 / Button namebtn_next text开始安装 / /VerticalLayout !-- 进度页 -- VerticalLayout namepage_progress visiblefalse Label namelbl_status text正在准备... / Progress nameprogress_bar min0 max100 value0 / /VerticalLayout /VerticalLayout /VerticalLayout /Window关键点TabLayout作为页面容器通过SelectItem切换页面。visiblefalse控制初始隐藏。进度条用Progress控件value属性绑定安装百分比。在 C 侧通过m_pm.FindControl(_T(page_container))拿到容器指针调用SelectItem(1)切到进度页。进度更新则通过FindControl(_T(progress_bar))拿到 Progress 指针调用SetValue(nPercent)。3.2 安装进度从 NSIS 回传到 Duilib 的三种方式NSIS 执行文件释放时需要把进度百分比实时传给 Duilib 窗口。三种常见方式第一种是共享内存。NSIS 插件里创建一块命名共享内存NSIS 脚本通过System::Call调用 Win32 API 写入进度值Duilib 侧起一个定时器读取。这种方式跨进程安全但需要处理同步。第二种是窗口消息。NSIS 脚本拿到 Duilib 窗口句柄后用SendMessage发送自定义消息wParam 传百分比。Duilib 在HandleMessage里拦截WM_USER 200更新进度条。这种方式最简单但 NSIS 的SendMessage需要插件导出窗口句柄。第三种是回调函数。插件导出一个SetProgressCallback函数NSIS 传入一个函数指针插件内部在收到进度消息时调用该指针。这种方式耦合度最低但 NSIS 脚本层面写回调比较绕。我一般用第二种够直接。插件里把窗口句柄存到一个全局变量导出GetInstWnd函数返回它。NSIS 脚本里; 获取窗口句柄 plugin::GetInstWnd Pop $1 ; 安装循环中更新进度 StrCpy $2 0 loop: IntOp $2 $2 1 ; 计算百分比 IntOp $3 $2 * 100 IntOp $3 $3 / $4 ; 发送进度消息 System::Call user32::SendMessage(i $1, i ${WM_USER200}, i $3, i 0) ; 释放一个文件 File dist\file_$2.dat Goto loop${WM_USER200}需要在 NSIS 里用!define定义和 Duilib 侧保持一致。$4是总文件数提前算好。SendMessage是阻塞的如果 Duilib 侧处理慢会拖慢安装速度所以进度更新逻辑要轻量只做SetValue和Invalidate。3.3 安装路径选择与注册表写入的联动安装路径选择页需要一个编辑框让用户输入路径再加一个「浏览」按钮调起文件夹选择对话框。Duilib 没有内置的文件夹选择框需要调用 Win32 的SHBrowseForFolder。// 浏览按钮响应 void CInstWnd::OnBrowse() { BROWSEINFO bi {0}; bi.hwndOwner m_hWnd; bi.lpszTitle _T(选择安装目录); bi.ulFlags BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE; LPITEMIDLIST pidl SHBrowseForFolder(bi); if (pidl) { TCHAR szPath[MAX_PATH] {0}; SHGetPathFromIDList(pidl, szPath); CEditUI* pEdit static_castCEditUI*(m_pm.FindControl(_T(edit_path))); if (pEdit) pEdit-SetText(szPath); CoTaskMemFree(pidl); } }拿到路径后需要回传给 NSIS。可以在点击「下一步」时插件把路径写到一个临时文件或共享内存NSIS 在LeaveCustomPage里读取。更直接的方式是插件导出一个GetInstallPath函数NSIS 调用后Pop出字符串。Function LeaveCustomPage plugin::GetInstallPath Pop $INSTDIR ; 写入注册表 WriteRegStr HKLM Software\MyApp InstallPath $INSTDIR FunctionEnd注意$INSTDIR是 NSIS 内置变量直接赋值即可。注册表写入放在LeaveCustomPage里确保用户确认路径后才落盘。如果用户点了取消LeaveCustomPage不会执行注册表不会被污染。注意SHBrowseForFolder在高 DPI 下可能显示模糊需要在 manifest 里声明 DPI 感知或者用IFileDialog替代。4. 避坑与排查NSISDuilib 联调时最容易翻车的五个点4.1 插件加载失败报「无法找到 plugin.dll」现象NSIS 编译通过运行安装包时弹出「无法找到 plugin.dll」或「插件调用失败」。原因NSIS 查找插件的路径是安装目录下的 Plugins 子目录且区分 32/64 位。如果 NSIS 是 32 位版本插件也必须编译成 32 位。另外插件 DLL 依赖的 Duilib 静态库如果用了 /MD 编译目标机器缺 VC 运行库也会导致加载失败。解决确认 NSIS 安装目录下Plugins文件夹存在且 plugin.dll 已放入。用 Dependency Walker 检查 DLL 依赖确保没有缺失的系统库。编译时统一用 /MT避免运行库依赖。4.2 Duilib 窗口创建成功但一片空白现象插件调用后窗口出现了但里面没有任何控件背景全白或全黑。原因XML 加载失败但没报错。常见原因是 XML 路径不对——Duilib 默认从工作目录找 XML而 NSIS 运行时的工作目录不一定是插件所在目录。另外XML 里的图片资源路径如果写的是相对路径也会加载不到。解决在CDialogBuilder::Create之前调用SetCurrentDir切换到 DLL 所在目录或者用绝对路径。图片资源建议打包进 DLL 资源节用res协议引用避免路径问题。4.3 进度条更新时界面卡死现象安装过程中进度条不动窗口无响应安装完成后才恢复。原因NSIS 的SendMessage是同步调用如果 Duilib 侧在消息处理里做了耗时操作比如重绘整个窗口会阻塞 NSIS 线程。另外如果 NSIS 脚本在循环里频繁发送消息消息队列堆积也会导致卡顿。解决Duilib 侧收到进度消息后只更新 Progress 控件的 value然后调用Invalidate标记重绘不要立即UpdateWindow。NSIS 侧控制发送频率比如每释放 10 个文件发一次而不是每个文件都发。4.4 卸载时残留注册表和文件现象卸载后注册表项还在安装目录里残留部分文件。原因NSIS 的卸载逻辑写在Uninstall段里如果安装时写入的注册表键没有在卸载段对应删除就会残留。另外如果安装过程中创建了临时文件或日志卸载段没有清理也会留下。解决安装段和卸载段成对写。每写一个WriteRegStr就在卸载段写一个DeleteRegKey或DeleteRegValue。文件释放用File /r的话卸载时用RMDir /r $INSTDIR整体删除但注意不要误删用户数据目录。4.5 高 DPI 下界面错位或模糊现象在 4K 屏幕上Duilib 窗口里的控件位置偏移文字模糊。原因Duilib 默认不处理 DPI 缩放XML 里的尺寸是按 96 DPI 写的在高 DPI 下系统会做位图拉伸导致模糊。控件位置偏移则是因为窗口大小和布局计算没有按 DPI 比例调整。解决在插件入口调用SetProcessDPIAware()声明 DPI 感知然后根据GetDpiForWindow获取实际 DPI按比例缩放 XML 里的尺寸。或者直接在 manifest 里声明dpiAware为true/PM让系统处理缩放但这样 Duilib 渲染的位图会模糊需要提供高分辨率资源。5. 进阶技巧静默安装参数与多语言切换的实战配置静默安装是分发场景里的刚需。NSIS 原生支持/S参数但自定义界面下需要额外处理当检测到/S时跳过 Duilib 窗口创建直接执行安装段。实现方式是在插件入口判断命令行参数。// plugin.cpp 入口判断 extern C __declspec(dllexport) void ShowInstPage(HWND hParent, int nPage) { if (IsSilentMode()) { // 静默模式直接返回不创建窗口 return; } CInstWnd* pWnd new CInstWnd(); pWnd-Create(hParent, _T(MyApp Installer), UI_WNDSTYLE_FRAME, 0L, 0, 0, 600, 400); pWnd-CenterWindow(); pWnd-ShowWindow(true); } bool IsSilentMode() { int argc 0; LPWSTR* argv CommandLineToArgvW(GetCommandLineW(), argc); bool silent false; for (int i 1; i argc; i) { if (_wcsicmp(argv[i], L/S) 0) { silent true; break; } } LocalFree(argv); return silent; }NSIS 脚本侧也要配合在.onInit里判断/S参数跳过页面声明Function .onInit ${GetParameters} $R0 ${GetOptions} $R0 /S $R1 IfErrors 2 0 SetSilent silent FunctionEndSetSilent silent会让 NSIS 自动跳过所有页面直接执行 Section。这样静默安装和界面安装共用同一套安装逻辑维护成本最低。多语言切换的思路类似XML 里用占位符标记需要翻译的文本运行时根据系统语言或命令行参数加载对应的语言文件替换占位符后重新渲染。常见做法是维护一个lang_zh-CN.ini和lang_en-US.ini用 Duilib 的CDialogBuilder加载 XML 后遍历控件树把text属性替换成对应语言的字符串。void CInstWnd::ApplyLanguage(const std::wstring lang) { std::mapstd::wstring, std::wstring dict LoadLangFile(lang); CControlUI* pRoot m_pm.GetRoot(); ApplyLangRecursive(pRoot, dict); } void CInstWnd::ApplyLangRecursive(CControlUI* pCtrl, const std::mapstd::wstring, std::wstring dict) { if (!pCtrl) return; std::wstring text pCtrl-GetText(); if (dict.find(text) ! dict.end()) { pCtrl-SetText(dict[text].c_str()); } for (int i 0; i pCtrl-GetCount(); i) { ApplyLangRecursive(pCtrl-GetItemAt(i), dict); } }这套逻辑的关键在于 XML 里的文本要写成 key 而不是直接写中文比如textwelcome_title然后语言文件里welcome_title欢迎使用。这样切换语言时只需要替换字典不用改 XML。验证安装包是否正常我习惯用三个检查一是用 7-Zip 打开安装包看文件列表是否完整二是在干净的虚拟机里跑一遍完整安装和卸载检查注册表和目录残留三是用/S参数跑静默安装确认退出码为 0。这三个检查做完基本能覆盖 90% 的翻车场景。最后说个血泪教训Duilib 的 XML 里控件 name 属性一定要唯一我曾经因为两个按钮重名导致点击事件绑到了错误的控件上排查了半天才发现。现在养成的习惯是 name 统一加前缀比如btn_、edit_、lbl_一眼就能看出控件类型。希望帮到你。本文还有配套的精品资源点击获取
返回列表