简介「NSISDuilib 自定义安装程序」是一套面向 Windows 桌面软件开发者与安装包制作人员的实战素材聚焦用 NSIS 脚本驱动安装流程、以 Duilib 的 XML 布局与控件搭建美化安装界面的完整组合方案可解决传统安装界面简陋、交互与皮肤难以定制的问题适合已具备基础脚本能力、希望进阶到自定义皮肤与多语言安装的中级开发者参考。包体共 451 个文件、约 9.6MB以 113 个 png 皮肤图片、113 个 nsh 脚本头文件、58 个 nlf 多语言文件、35 个 ico 图标与 32 个 bmp 位图构成界面素材主体另有 29 个 dll、24 个 xml 布局、17 个 exe、2 个 nsi 主脚本及若干 bat 构建批处理覆盖皮肤资源、语言包与编译打包链路。已有 2945 人学习下载。读者可从中获取可直接复用的皮肤资源与 XML 布局结构、NSIS 脚本的组织方式安装目录选择、文件释放、注册表写入、快捷方式与开机启动等以及多语言文件与构建批处理的配置思路便于对照搭建并调试自己的安装包界面。1. 给桌面工具套一层自定义安装界面NSIS 与 Duilib 是最省事的组合也是最容易卡住的组合给一个桌面工具做安装包脚本层用 NSIS 基本没悬念体积小、中文文档多、卸载信息、注册表、快捷方式全都能覆盖。问题是 NSIS 自带的 MUI2 页面在用户眼里属于 2010 年的长相于是把 Duilib 拉进来做那层带皮肤的安装向导。NSIS 负责释放文件、写注册表、生成卸载项Duilib 负责画界面两者中间靠一个 32 位插件 DLL 连接。这套组合装不好会很麻烦makensis 编不过、插件 DLL 加载不到而报Invalid command、界面闪一下就白屏、x86 和 x64 库混着链进去直接 LNK1112。我前后重装过三次真正卡住时间的是位数和字符集两个开关代码本身没怎么费劲。下面按我最后一次成功安装的顺序写先定环境再装 NSIS再编 Duilib 和插件最后是装坏之后的补救记录和验证方法。2. 装前先确定环境NSIS 3.09 与 Duilib 的位数、字符集组合这一章不安装任何东西只把后面会反复用到的版本、位数和目录定死。安装程序这类项目最怕中途改位数改一次等于前面所有库全部重编。2.1 三层分工脚本层、界面层、编译层各自负责什么NSIS 这一层由.nsi脚本和一个编译产物makensis.exe组成。它做的事很固定解释脚本、把File指令引用的文件压缩进安装包、生成一个 exe 外壳。它不负责界面绘制只暴露页面钩子自带页面只有许可协议、安装目录、进度条、完成页这几种固定形态。Duilib 这层是纯 C 的窗口库负责画那套带背景图、自定义按钮、进度动画的安装向导。它不感知“安装”这件事唯一要做的就是把用户点击的“下一步”转换成返回值交回 NSIS 脚本。两者之间不能直接调用得经过 NSIS 的插件机制一个导出固定签名函数的 DLL。这个 DLL 是整篇文章的核心缓冲层Duilib 编译产物和 NSIS 脚本都围绕它对接。编译层就是 VS 的 msbuild。市面上很多教程直接把 Duilib 编成 DLL 再链接我不推荐安装包本体只需要一个插件 DLLDuilib 用静态库链进去更干净既省一个发布文件也避免安装包里出现两个 Dll 互相找不到。代价是静态库的字符集和位数必须和插件工程完全一致这也是后面两条踩坑记录的来源。2.2 版本对照表位数是重要决定因素NSIS 只有 32 位版本makensis.exe和它生成的安装包都是 32 位进程。这意味着加载进来的插件 DLL 必须是 32 位它依赖的 Duilib 静态库也必须是 32 位一路往下传递没有例外。第一次安装时我用 x64 编了 Duilib后面折腾了一晚上才定位到位数冲突问题根源就在这里。组件版本目标平台字符集说明NSIS3.09x86Unicode3.0 之后默认 Unicode 版不再提供 ANSI 主程序Duilib主干版本x86Unicode只编静态库 Release不编 DLLVS2019——平台工具集 v142Windows SDK10 系列——跟随 VS 安装即可msbuild随 VS——命令行编译避免手动点解决方案表里的 Duilib 我刻意没写具体版本号因为不同分支的目录结构和头文件名差异不小写死了反而容易让读者照着找不到文件。判断标准只有一条能编出DuiLib.lib这个静态库、头文件里能找到CWindowWnd和CPaintManagerUI就够了。字符集这一列比版本号重要。NSIS 3 的pluginapi.h在 Unicode 模式下popstring和pushstring收发的都是宽字符插件工程的字符集如果还停在“使用多字节字符集”字符串能编过运行时中文路径就会变成乱码属于最难查的一类问题。提示位数和字符集这两项建议在动手前就写进项目 README后面任何一个新同事接手都按这两条走不要凭印象改。2.3 目录规划先把产物路径固定下来安装类项目最容易乱的是中间产物Duilib 编出来的 lib 放在源码目录、插件 DLL 又扔在 Debug 目录、.nsi里写的是相对路径换一台机器立刻失效。我一般会在盘符根目录建一个工作区路径不带空格、不带中文因为 makensis 处理含空格路径时偶尔会在File指令上出问题虽然可以加引号绕过但没必要给自己找麻烦。# 工作区固定结构后面所有路径都从这里推 mkdir -p D:/dev/nsis-dui/src # Duilib 源码与插件工程 mkdir -p D:/dev/nsis-dui/build # 中间产物可整目录删除 mkdir -p D:/dev/nsis-dui/out/lib # Duilib 静态库最终落地处 mkdir -p D:/dev/nsis-dui/out/plugins # 插件 DLL 产物 mkdir -p D:/dev/nsis-dui/out/setup # 安装包输出这几行的作用是把“源码”“中间产物”“发布产物”三类东西隔开。build目录随时可以清空重来out目录里的东西则是安装脚本真正引用的.nsi里写路径时只写out这一层。这样做的直接好处是某天 Duilib 编译出错删build重编不会影响已经能跑通的安装脚本。接下来给工作区加一个系统变量DUISDK指向D:\dev\nsis-dui\out后面.nsi里用!define DUISDK或者环境变量读取都方便。变量值不要带结尾反斜杠.nsi里自己拼接\这是 NSIS 路径处理的老规矩。3. 安装 NSIS 3.09从 makensis 命令行到 x86-unicode 插件目录NSIS 本身的安装是整套流程里最省心的一步基本是下一步到底。真正需要动手的是安装完之后的三件事环境变量、插件目录规则、外加一次命令行编译验证。3.1 安装包选择与组件勾选下载 NSIS 3.09 的安装程序一路默认即可安装路径用C:\Program Files (x86)\NSIS。这个路径里有空格但 makensis 自己能处理不需要改。安装过程中会问要不要装几个可选组件我的选择是脚本示例装Examples目录里有很多现成的页面写法可以抄、语言文件装多语言安装界面要用、.nsh头文件必装源码包不用装。装完之后看一眼目录有四个位置后面会天天用到makensis.exe编译器本体命令行入口。Include\MUI2.nsh、LogicLib.nsh、FileFunc.nsh这些头文件都在这里。Plugins\x86-unicode\插件 DLL 的默认搜索目录只有这个目录对 Unicode 版生效。Stubs\安装包外壳的模板文件一般不动。为什么单独强调x86-unicode因为 NSIS 3 的安装包是 Unicode 版编译时它只在Plugins\x86-unicode\下找插件。老教程里写的Plugins\根目录是 NSIS 2 时代的规则照着放进去编译器不会报错但生成安装包运行时会提示Invalid command这种“编译通过、运行才报错”的失败最难查。3.2 环境变量配置与命令行首次编译装完之后不要急着写业务脚本先用一个三行脚本把命令行链路跑通。第一步是把 NSIS 目录加进 PATH让任意命令行窗口都能直接调makensis。推荐改用户级环境变量不要用setx /M改系统级 PATH因为系统 PATH 有长度上限被截断之后会有别的工具莫名失效这类事故我遇过不止一次。# 用户级追加路径只影响当前用户安全 setx PATH %PATH%;C:\Program Files (x86)\NSIS # 重新打开一个命令行窗口再验证setx 不会刷新已打开的窗口 makensis /VERSION第一行把 NSIS 目录追加到用户 PATH 后面不用%PATH%覆盖系统级内容。第二行的makensis /VERSION是最轻量的验证方式正常会打印版本号v3.09。如果提示“不是内部或外部命令”说明窗口没重开或者 PATH 写坏。确认能调起来之后写一个最小脚本验证编译链路; hello.nsi —— 最小可编译脚本只验证工具链 Unicode true Name 编译链路测试 OutFile D:\dev\nsis-dui\out\setup\hello.exe InstallDir $TEMP\hello-test RequestExecutionLevel user Page instfiles Section Main SetOutPath $INSTDIR FileOpen $0 $INSTDIR\mark.txt w FileWrite $0 ok FileClose $0 SectionEndUnicode true是第一行就必须写的指令它决定整个脚本的字符串按宽字符处理也决定后面插件必须按宽字符约定通信。RequestExecutionLevel user让安装包不弹 UAC测试阶段少一道点击。Page instfiles用自带进度页这段不涉及界面只为确认 makensis 能产出 exe。编译命令很简单cd /d D:\dev\nsis-dui makensis /V4 /WX hello.nsi/V4是详细输出级别能看到它引用了哪些头文件、压缩率多少/WX表示把警告当错误处理这个开关建议一直开着。安装脚本里最常见的警告是变量未使用和File指令引用的路径不存在前者无害后者往往是漏拷贝文件一旦放过装出来的包就是缺文件的半成品。3.3 插件搜索路径与第一次插件调用链路通了以后把插件目录规则一次性配好。有两个做法一是把插件 DLL 拷进C:\Program Files (x86)\NSIS\Plugins\x86-unicode\二是用!addplugindir在脚本里显式指定目录。前者的缺点是污染全局 NSIS 安装换台机器就得再拷一遍后者把依赖写进脚本谁拉代码谁都能编。; 在脚本顶部声明插件所在目录路径末尾不要加反斜杠 !addplugindir /x86-unicode D:\dev\nsis-dui\out\plugins ; 调用插件时写 插件名::函数名插件名即 dll 文件名去掉扩展名 Section CallPlugin duiui::ShowPage Pop $0 DetailPrint 插件返回$0 SectionEnd!addplugindir后面的/x86-unicode是必须带的修饰告诉编译器按 Unicode 插件去查找。duiui::ShowPage里duiui对应duiui.dllShowPage对应 DLL 里导出的函数名两者大小写不敏感但建议和代码保持一致。Pop $0取的是插件用pushstring压回栈的返回值这个栈是后进先出的插件里压了几个脚本里就要按相反顺序Pop几次否则后面的页面会读到脏数据。插件 DLL 还不会写的话这一节先跳过把!addplugindir那行注释掉等第 4 章编出 DLL 再回来接。4. 编译 Duilib 并封装 NSIS 插件Release x86 Unicode 三个开关这一章是整套安装里最容易出问题的部分。Duilib 编译本身不难难的是让它的产物和 NSIS 插件工程、NSIS 主程序三方在位数和字符集上完全对齐。4.1 Duilib 源码编译三个必须固定的开关源码拿到src目录下之后不要急着用 VS 打开解决方案点生成命令行编译更容易复现也更容易写进构建脚本。Duilib 的主干版本一般带一个.sln里面通常有 Debug、Release 两个配置平台默认是 Win32也就是 x86这一点刚好符合我们的要求。# 编译 Duilib 静态库Release x86 Unicode msbuild DuiLib.sln ^ /t:Rebuild ^ /p:ConfigurationRelease ^ /p:PlatformWin32 ^ /p:CharacterSetUnicode ^ /p:PlatformToolsetv142 ^ /m逐项说明/t:Rebuild强制全量重编避免旧 obj 干扰/p:PlatformWin32是必须的写x86有些工程不认/p:CharacterSetUnicode会覆盖工程文件里写死的字符集省得手动去改项目属性/p:PlatformToolsetv142对应 VS2019如果本机是 VS2022 就换成v143工具集和运行时库版本不匹配会报一堆LNK2038。编译完成后找到DuiLib.lib连同DuiLib头文件目录一起拷到out\lib。判断编对了没有不用看日志用一个命令查库文件的机器类型dumpbin /headers DuiLib.lib | findstr machine正常输出里应该是8664之外的14C也就是 x86。如果看到8664说明平台选错了这个库链进 NSIS 插件必然报LNK1112。4.2 写一个最小 NSIS 插件函数签名和栈约定NSIS 插件的本质是一个导出特定签名函数的 DLL函数名就是脚本里调用的名字。NSIS 3 提供了一个pluginapi.h里面定义了栈操作宏和EXDLL_INIT。这个头文件在 NSIS 安装目录的Include下找不到需要从 NSIS 源码包的Contrib\PluginAPI里拷出来很多人卡在这一步。// duiui.cpp —— 最小插件只负责弹出一个 Duilib 窗口并返回结果 #include windows.h #include tchar.h #include pluginapi.h // 从 NSIS 源码包 Contrib\PluginAPI 拷来 #include UIlib.h using namespace DuiLib; // 模态跑 Duilib 消息循环窗口关掉才返回 static int RunDuiWindow(HWND hParent) { CWindowWnd* pFrame new CInstallFrame(); // 父窗口传 hParent安装包主窗口会变成它的 owner pFrame-Create(hParent, _T(安装向导), UI_WNDSTYLE_DIALOG, WS_EX_WINDOWEDGE); pFrame-CenterWindow(); pFrame-ShowWindow(true); CPaintManagerUI::MessageLoop(); // 阻塞在这里直到窗口销毁 delete pFrame; return 1; } // 导出函数名字必须和 .nsi 里的 duiui::ShowPage 对应 extern C void __declspec(dllexport) ShowPage( HWND hwndParent, int string_size, TCHAR* variables, stack_t** stacktop) { EXDLL_INIT(); // 必须第一行它依赖上面三个形参名不能改 TCHAR szCaption[128] {0}; popstring(szCaption); // 取出脚本 push 进来的标题 int nRet RunDuiWindow(hwndParent); TCHAR szRet[16] {0}; _stprintf_s(szRet, _T(%d), nRet); pushstring(szRet); // 脚本用 Pop $0 接收这个值 }这段代码有三个必须遵守的约定。第一导出函数名ShowPage就是脚本里::后面的名字改成ShowPageW之类脚本立刻找不到。第二四个形参的名字不能改EXDLL_INIT()这个宏内部直接用string_size、variables、stacktop这三个标识符改成nSize之类会编不过。第三popstring和pushstring的顺序必须和.nsi严格对应脚本里 push 两个这边就要 pop 两个。工程配置上还有三个开关要固定字符集设为 Unicode、配置选 Release、平台选 Win32。链接器附加依赖项里加上DuiLib.lib附加库目录指向out\libC/C 附加包含目录指向 Duilib 的头文件目录。产物输出路径直接设成out\plugins\duiui.dll编完就是成品不需要手动挪。4.3 在 .nsi 脚本里接管页面流插件编好之后把它接到安装脚本里。NSIS 的自定义页面有两种写法用Page custom挂一个函数函数里调插件或者干脆让插件自己弹一个模态窗口脚本只等结果。第二种更简单也更符合 Duilib 的窗口模型。Unicode true !addplugindir /x86-unicode D:\dev\nsis-dui\out\plugins Name 某桌面工具 OutFile D:\dev\nsis-dui\out\setup\installer.exe InstallDir $PROGRAMFILES\SomeTool RequestExecutionLevel admin ; 欢迎页用 Duilib 接管返回 0 表示用户取消 Page custom PageWelcome PageWelcomeLeave Page directory Page instfiles Function PageWelcome Push 欢迎安装 duiui::ShowPage Pop $0 StrCmp $0 0 0 2 Abort ; 用户点了取消终止安装 FunctionEnd Function PageWelcomeLeave FunctionEndPage custom需要成对的两个回调进入页面时调用的PageWelcome离开页面时调用的PageWelcomeLeave后者留空即可。Abort是 NSIS 里终止安装流程的标准做法放在页面回调里会直接退出安装。Push 欢迎安装对应插件里的popstring参数数量必须一致少 push 一个的话插件会从栈上取到上一个页面的残留值表现是标题莫名其妙变成安装目录之类。编译时加/WX确认没有警告之后再上真机测试makensis /V4 /WX /DDUISDKD:\dev\nsis-dui\out installer.nsi/D用来在脚本里定义符号比如脚本里写!define DUISDK时可以用命令行覆盖方便 CI 上换路径而不改脚本。5. 避坑与排查五条装坏之后的补救记录下面这五条都是我实际遇到过的记录每条按“看到什么现象、为什么、怎么处理”写能对号入座基本就不用重装。5.1 编译期两个链接错误现象一LNK1112: 模块计算机类型“x64”与目标计算机类型“x86”冲突。原因是链接进去的 Duilib 静态库是 64 位而插件工程是 32 位。这种错误有时不做提示只在链接阶段突然报出来让人怀疑是不是工程配置被谁改了。解决方法是回第 4.1 节重编DuiLib.lib/p:Platform明确写Win32编完用dumpbin /headers DuiLib.lib | findstr machine确认是14C而不是8664。同类错误还有LNK2038: 检测到“RuntimeLibrary”的不匹配项那是 Release 库和 Debug 工程混用统一到 Release 即可。现象二LNK2001: 无法解析的外部符号 _DuiLib::CPaintManagerUI::MessageLoop。原因通常是库没加上或者加上了但库文件是空的比如编译时CharacterSet和源码里写死的不一致导致整个工程被跳过。检查链接器附加依赖项里有没有DuiLib.lib以及附加库目录是否指到了out\lib。还有一种少见情况库加对了但工程开了“全程序优化”静态库的符号被裁掉了把/GL关掉就好。5.2 运行期加载不到、白屏、闪退现象三运行安装包时提示Invalid command: duiui::ShowPage。编译阶段完全正常运行才报。原因只有一个插件 DLL 没放在Plugins\x86-unicode\下且脚本里没写!addplugindir /x86-unicode。NSIS 3 只在x86-unicode目录里找 Unicode 插件放到Plugins\根目录或者Plugins\x86-ansi\都找不到。解决办法是二选一把 DLL 拷进Plugins\x86-unicode\或者在脚本第一行加!addplugindir /x86-unicode 你的目录。我倾向后者因为依赖写在脚本里换机器不用重新配。现象四安装界面闪一下就不见了或者弹出空白窗口。原因是 Duilib 窗口对象在消息循环开始前就被析构了。常见写法是在插件函数里new一个窗口、Create、ShowWindow然后直接return函数返回时栈上的指针被销毁窗口跟着没了。正确做法是像 4.2 节那样在ShowWindow之后调用CPaintManagerUI::MessageLoop()把线程阻塞住等窗口自己关闭再往下走。另一种可能是父窗口没传Create的 first 参数传 NULL 时窗口没有 owner安装包主窗口会抢焦点看起来就像白屏。现象五静默安装/S模式下安装包卡死或者崩溃。原因是插件无条件弹窗而静默安装环境下没有用户交互窗口一直等不到点击。解决方法是插件入口先判断命令行参数NSIS 提供GetParameters或者更简单在脚本层用IfSilent跳过自定义页面Function .onInit IfSilent 2 0 ; 静默模式直接跳过欢迎页 FunctionEnd配合把Page custom用IfSilent包一层静默模式下就不进插件。这类问题在自动化部署脚本里出现概率很高本机双击测一百次都正常一进批量安装就翻车。5.3 发布期体积和拦截现象六安装包从 8MB 涨到 30MB或者被杀毒软件拦下。原因是 Duilib 静态链接时带进了调试信息和用不到的界面控件。解决办法插件工程选 Release、关闭“生成调试信息”、C/C 里把“优化”设为“使大小最小化”、链接器开启“消除未引用的代码”。做完这几项一般能压回去一半。另外不要给安装包外壳加 UPX 之类的压缩壳安装包本身已经有 LZMA 压缩再加壳不仅压不了多少还容易触发启发式拦截。6. 装完怎么验证三层检查跑一遍再交付环境装完、脚本编过不等于这套东西能用。我一般按三层验证每层都能单独定位问题出问题时不用在一整条链上瞎猜。第一层验工具链只跑makensis /VERSION和一次最小脚本编译确认 PATH 和头文件没问题。第二层验插件写一个只有Page custom的测试脚本不释放任何文件、不写注册表只调duiui::ShowPage看窗口能不能正常弹出、点“取消”能不能让安装终止、点“下一步”能不能走到进度页。第三层才验完整安装包在一个干净的系统环境里跑一遍全流程再跑一遍/S静默安装。检查项命令或动作通过标准编译器可用makensis /VERSION输出 v3.09脚本零警告makensis /V4 /WX installer.nsi无 warning 输出库位数正确dumpbin /headers DuiLib.libmachine 为 14C插件被识别运行测试包看是否报 Invalid command正常弹窗取消流程点欢迎页“取消”安装进程立即退出卸载残留安装后卸载看 Program Files 和注册表无残留目录与键值静默安装installer.exe /S无窗口退出码为 0静默安装那一行有个细节/S必须大写NSIS 只认大写小写会被当成普通参数忽略然后你会看到一个没人点的窗口挂在那里。测退出码可以用echo %ERRORLEVEL%正常安装返回 0用户取消返回 1 或 2构建脚本里靠它判断成败。最后补一个习惯每次改完.nsi我都会先把out\setup目录清空再编因为 makensis 不会自动删旧产物编译失败时旧 exe 还在原地很容易拿旧包去测试然后怀疑人生。那次装完之后我就把“清空输出目录、检查 DuiLib.lib 位数、跑一遍静默安装”这三步写进了发布脚本虽然麻烦但确实省下了不少事后排查的时间。希望这套流程能帮到你。本文还有配套的精品资源点击获取