简介这是一份面向Windows桌面开发者的NSIS与Duilib整合实战工程目标是用脚本化安装系统搭配C界面库复刻腾讯QQ安装程序的界面风格与交互流程适合已具备一定C基础、希望深入定制安装包体验的开发者参考。压缩包共206个文件约69.53MB以h头文件与cpp源码为主体辅以obj、pdb等编译中间产物另有png界面素材、sln与vcxproj工程文件、nsi与nsh脚本、dll插件及7z、rar压缩组件覆盖从界面渲染到安装逻辑的完整链路。工程按3party、samples、plugin、include等目录组织分别承载第三方库、示例模板、NSIS扩展插件与公共头文件便于按模块研读。目前已有1866人学习下载。通过该工程可掌握Duilib界面元素与NSIS脚本的协同方式理解安装流程中的文件释放、注册表操作与插件调用并借鉴仿QQ风格的界面布局与交互设计为自研安装程序提供可复用的结构参考。1. 从“安装包像半成品”说起NSISDuilib 仿 QQ 安装包到底解决什么问题很多做 Windows 桌面交付的开发者都遇到过这种尴尬功能代码写得挺漂亮结果用户拿到的第一个界面——安装向导——还是那种灰底白字、按钮挤成一排的系统默认样式。用户还没打开主程序心里已经给这个软件打了“半成品”的标签。更麻烦的是默认 NSIS 界面能塞进去的东西太少想加个自定义协议勾选、想按组件分步安装、想在安装前做环境检测改起来全是nsDialogs里手写坐标维护成本高得离谱。这套 NSISDuilib 仿 QQ 安装包方案要解决的就是“安装过程本身也是产品体验”这件事。它用 NSIS 负责安装逻辑、文件释放、注册表写入和卸载登记用 Duilib 负责把安装向导画成接近 QQ 那种带左侧导航、圆角按钮、自定义复选框的现代界面。适合两类人一是正在做毕设或课程设计、需要交一个完整可运行安装包的同学二是手里有桌面产品、想把交付链路最后一段补齐的从业者。整套方案不依赖冷门工具链编译产物就是一个标准.exe双击就能跑。2. 需求拆解与技术选型为什么是 NSIS 加 Duilib而不是别的组合2.1 安装包的功能边界先画清楚在动手写脚本之前我一般会先把安装包要做的事列成一张表避免做到一半发现漏了卸载逻辑。这个仿 QQ 安装包通常覆盖以下功能点功能模块具体行为对应技术点欢迎页显示软件名称、版本、许可协议勾选Duilib 窗口 NSIS 页面注册路径选择默认C:\Program Files\xxx支持浏览NSISnsDialogs或 Duilib 自绘组件选择主程序、快捷方式、开机启动可勾选NSIS Section Duilib 复选框状态回传安装进度进度条、当前文件提示NSISSetDetailsPrint Duilib 进度控件完成页立即运行、创建桌面图标NSISExecCreateShortCut卸载控制面板可见、清理安装目录NSISWriteUninstaller 注册表 Uninstall 键这张表定下来后面写脚本和画界面就有依据不会出现“界面画了但脚本没对应 Section”的翻车情况。2.2 为什么不用纯 Duilib 或纯 NSIS纯 Duilib 做安装包意味着你要自己处理文件释放、注册表、卸载登记、UAC 提权工作量直接翻倍而且卸载入口很容易做不干净。纯 NSIS 默认界面又太丑nsDialogs虽然能自绘但每个控件都要算像素坐标改一版界面等于重写一遍。NSISDuilib 的分工是NSIS 当“后台工人”负责所有安装动作Duilib 当“前台门面”只负责渲染和收集用户输入。两者通过 NSIS 的插件机制通信——Duilib 编译成 DLLNSIS 用duilib::Create这类插件调用把窗口拉起来用户在界面上点的勾选、选的路径通过插件返回值传回 NSIS 脚本。这样界面改版不影响安装逻辑安装逻辑调整也不用动界面代码。2.3 环境准备与目录结构我一般会按下面的目录组织工程避免编译时找不到头文件project/ ├── installer/ │ ├── setup.nsi # NSIS 主脚本 │ ├── duilib_ui/ # Duilib 界面工程 │ │ ├── ui_main.cpp │ │ ├── ui_main.h │ │ └── skin/ # XML 皮肤与图片资源 │ └── plugins/ # 编译出的 Duilib 插件 DLL ├── app/ # 主程序编译产物 └── build.bat # 一键编译脚本setup.nsi里通过!addplugindir指向plugins目录Duilib 工程编译出的 DLL 放进去NSIS 编译时就能找到。这个结构的好处是主程序和安装包解耦主程序更新不影响安装包工程。提示Duilib 插件 DLL 的位数要和 NSIS 编译器一致。32 位 NSIS 配 32 位 DLL64 位同理否则运行时会直接报插件加载失败。3. 总体架构设计NSIS 脚本层与 Duilib 界面层的职责划分3.1 两层通信机制怎么跑通整个安装包运行时序是这样的用户双击setup.exeNSIS 先执行.onInit回调在这里调用 Duilib 插件创建欢迎页窗口。Duilib 窗口内部维护一个状态结构体记录用户勾选的组件、选择的路径、是否同意协议。用户点“下一步”时Duilib 把状态结构体序列化成字符串返回给 NSISNSIS 解析后决定后续执行哪些 Section。通信的数据格式我通常用简单的keyvalue拼接比如; NSIS 侧调用 Duilib 插件并接收返回值 duilib::ShowPage /NOUNLOAD welcome Pop $0 ; $0 返回类似 agree1|pathC:\Program Files\Demo|compmain,shortcut// Duilib 侧把界面状态拼成字符串返回 std::string BuildResult() { std::string ret; ret agree std::to_string(m_bAgree) |; ret path m_strInstallPath |; ret comp JoinComponents(m_vecComponents); return ret; }NSIS 拿到$0后用字符串函数切分agree1才继续comp里包含main才执行主程序 Section。这种设计的好处是界面和逻辑完全解耦以后想加一个“是否创建桌面快捷方式”的勾选只需要 Duilib 多返回一个字段NSIS 多解析一个 key。3.2 页面流转与状态保持仿 QQ 安装包的页面通常有欢迎页、路径选择页、组件选择页、安装进度页、完成页。NSIS 默认是线性页面流但 Duilib 自绘窗口后页面切换由 Duilib 内部管理NSIS 只负责在合适的时机调用duilib::ShowPage并传入页面标识。状态保持的关键是Duilib 窗口对象在页面切换时不能销毁。我一般把窗口做成单例页面切换只是隐藏/显示不同的布局容器。这样用户在路径页选了D:\Demo切到组件页再切回来路径还在。如果每页都重建窗口状态就得靠 NSIS 变量来回传代码会变得很啰嗦。3.3 安装进度与 NSIS 回调的配合进度页是最容易出玄学问题的地方。NSIS 执行File命令释放文件时默认会更新它自己的进度条但 Duilib 的进度条需要手动同步。通常的做法是在 NSIS 的 Section 里用GetFunctionAddress注册一个回调每释放一个文件就调用一次 Duilib 插件的UpdateProgress传入当前百分比。Section MainProgram SecMain SetOutPath $INSTDIR ; 注册进度回调 GetFunctionAddress $0 OnFileExtract duilib::SetCallback $0 File app\*.exe File app\*.dll SectionEnd Function OnFileExtract ; 每释放一个文件通知 Duilib 更新进度 duilib::UpdateProgress /NOUNLOAD FunctionEnd这里要注意SetOutPath必须在File之前否则文件会释放到错误目录。进度百分比的计算可以用File命令的总数做分母但更稳妥的方式是预先在脚本里定义文件清单按清单长度算。4. 关键模块实现从欢迎页到卸载登记的完整链路4.1 欢迎页与许可协议勾选欢迎页的 Duilib 布局用 XML 描述核心是一个CheckBox控件绑定“我已阅读并同意协议”。用户不勾选时“下一步”按钮置灰。这个逻辑在 Duilib 的OnClick回调里处理void CMainWnd::OnClick(TNotifyUI msg) { if (msg.sType _T(click)) { if (msg.pSender-GetName() _T(btn_next)) { if (!m_pCheckAgree-IsSelected()) { // 未勾选协议弹提示并返回 MessageBox(NULL, _T(请先同意许可协议), _T(提示), MB_OK); return; } // 勾选了切换到路径选择页 SwitchPage(PAGE_PATH); } } }NSIS 侧在.onInit里调用duilib::ShowPage welcome然后根据返回值决定是否继续。如果用户直接关窗口返回值里agree0NSIS 执行Abort终止安装。4.2 路径选择与磁盘空间检测路径选择页除了一个Edit控件显示路径还需要一个“浏览”按钮调起系统文件夹选择框。Duilib 本身没有文件夹选择对话框通常的做法是调用 Windows API 的SHBrowseForFoldervoid CMainWnd::OnBrowseFolder() { BROWSEINFO bi { 0 }; bi.lpszTitle _T(选择安装目录); LPITEMIDLIST pidl SHBrowseForFolder(bi); if (pidl ! NULL) { TCHAR path[MAX_PATH] { 0 }; SHGetPathFromIDList(pidl, path); m_pEditPath-SetText(path); // 检测磁盘剩余空间 ULARGE_INTEGER freeBytes; GetDiskFreeSpaceEx(path, freeBytes, NULL, NULL); if (freeBytes.QuadPart REQUIRED_SPACE) { MessageBox(NULL, _T(磁盘空间不足), _T(提示), MB_OK); } } }REQUIRED_SPACE根据主程序实际大小加一定余量设定通常的做法是主程序体积的 1.5 倍。这个检测放在用户选完路径后立即做比等到安装时才报错体验好得多。4.3 组件选择与 Section 映射组件选择页的每个复选框对应 NSIS 的一个 Section。Duilib 返回的comp字段里包含哪些组件被勾选NSIS 在安装时用SectionIn或条件判断决定是否执行Section 主程序 SecMain SectionIn 1 SetOutPath $INSTDIR File app\main.exe SectionEnd Section 桌面快捷方式 SecShortcut SectionIn 1 CreateShortCut $DESKTOP\Demo.lnk $INSTDIR\main.exe SectionEnd ; 安装时根据 Duilib 返回值决定是否跳过 Function .onInit duilib::ShowPage /NOUNLOAD components Pop $0 ; 解析 $0 中的 comp 字段 ${StrContains} $1 shortcut $0 StrCmp $1 skip_shortcut ; 包含 shortcut正常执行 Goto done skip_shortcut: ; 不包含跳过快捷方式 Section SectionSetFlags ${SecShortcut} 0 done: FunctionEnd这里用到了StrContains宏需要!include StrFunc.nsh并初始化。SectionSetFlags把 Section 标志设为 0 表示不执行这是 NSIS 里控制组件安装的标准做法。4.4 卸载登记与清理逻辑安装完成后必须写卸载登记否则控制面板里看不到卸载入口。NSIS 的WriteUninstaller和注册表写入通常在安装 Section 的最后执行Section 卸载登记 SecUninstall WriteUninstaller $INSTDIR\uninstall.exe WriteRegStr HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\Demo \ DisplayName Demo 应用 WriteRegStr HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\Demo \ UninstallString $INSTDIR\uninstall.exe WriteRegStr HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\Demo \ DisplayIcon $INSTDIR\main.exe WriteRegDWORD HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\Demo \ NoModify 1 WriteRegDWORD HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\Demo \ NoRepair 1 SectionEnd卸载 Section 里要清理安装目录、桌面快捷方式、开始菜单项和注册表键。注意RMDir /r $INSTDIR会递归删除整个目录执行前要确认$INSTDIR确实是安装目录避免用户把安装路径选到C:\导致误删。5. 避坑与常见问题排查安装包开发里最容易翻车的五个点5.1 界面显示乱码或字体发虚现象Duilib 窗口里的中文显示成方块或者字体边缘模糊。原因通常是资源文件编码不对或者 DPI 缩放没处理。Duilib 的 XML 皮肤文件要保存为 UTF-8 带 BOM图片资源用 PNG 而不是 JPG。DPI 方面在WinMain入口调用SetProcessDPIAware()让系统不做自动缩放然后 Duilib 内部按 96 DPI 设计高 DPI 下用DpiScale系数放大。我一般会在窗口创建前读一次系统 DPI把缩放系数存到全局变量里。5.2 NSIS 插件调用返回空值现象duilib::ShowPage执行后Pop $0拿到空字符串。原因多半是插件 DLL 没放对目录或者位数不匹配。排查步骤先确认!addplugindir路径正确再检查 DLL 是 32 位还是 64 位最后看 NSIS 编译日志有没有“plugin not found”警告。另一个常见原因是 Duilib 窗口创建失败但没返回错误码插件内部直接返回了空串。在插件入口加日志输出到临时文件能快速定位是加载失败还是逻辑失败。5.3 安装进度条卡在 100% 不结束现象文件都释放完了进度条停在 100% 但页面不跳转。原因是 NSIS 的File命令执行完后没有触发完成回调Duilib 还在等下一个进度更新。解决方式是在 Section 末尾显式调用一次duilib::UpdateProgress /NOUNLOAD 100然后调用duilib::ShowPage finish切到完成页。如果进度条是按文件数算的确保分母是实际File命令条数多一个少一个都会导致百分比对不上。5.4 卸载后残留文件或注册表项现象控制面板卸载完了安装目录还在或者注册表 Uninstall 键没删干净。原因通常是卸载 Section 里漏了某项或者DeleteRegKey的路径写错。排查时用regedit搜软件名看还有哪些键残留。我一般会在卸载脚本里按“文件→快捷方式→开始菜单→注册表”的顺序逐项清理每项后面加IfFileExists判断避免删不存在的文件报错。另外RMDir $INSTDIR只能删空目录如果还有残留文件要先Delete再RMDir。5.5 用户取消安装后临时文件没清理现象用户在安装过程中点了取消$TEMP目录下留了一堆临时文件。原因是 NSIS 的Abort不会自动触发清理逻辑。解决方式是在.onGUIEnd或自定义的取消回调里手动删除临时目录。通常的做法是在.onInit里用InitPluginsDir创建一个插件临时目录所有中间文件都放里面取消时RMDir /r $PLUGINSDIR一次性清掉。这个目录是 NSIS 自动管理的安装结束或取消后系统会尝试清理但手动清一次更保险。6. 验收与答辩怎么证明这套安装包真的能交付验收这套安装包我一般会走一遍“三机三场景”流程。三机是指一台干净虚拟机、一台已装过旧版本的机器、一台没有管理员权限的受限账户机器。三场景是指全新安装、覆盖安装、卸载后重装。每个场景下检查以下清单检查项通过标准常见失败表现界面渲染所有页面无乱码、无控件重叠中文方块、按钮错位路径选择默认路径正确浏览可改空间不足有提示默认路径指向系统盘根目录组件勾选取消勾选后对应 Section 不执行取消快捷方式仍创建桌面图标进度同步进度条与实际释放文件同步结束跳转完成页卡 99%、不跳转卸载清理安装目录、快捷方式、注册表全部清除残留uninstall.exe或注册表键权限处理非管理员账户弹出 UAC 提权静默失败无提示答辩时评委通常会问三个问题为什么选 NSIS 不选 Inno SetupDuilib 和 NSIS 怎么通信卸载不干净怎么办前两个在第二章和第三章已经讲清楚第三个就用上面的验收清单回答——把“卸载后残留检查”作为独立测试项现场演示regedit搜索无结果、安装目录已删除。还有一个容易被忽略的细节安装包的数字签名。毕设或课设不强制要求但如果要交付给真实用户没有签名的setup.exe会被 SmartScreen 拦截。通常的做法是申请一个代码签名证书用signtool在编译后签名。答辩时如果被问到“用户看到安全警告怎么办”这就是加分项。从那以后我每次做完安装包都会在干净虚拟机上先卸载再重装一遍确认没有残留才提交。这个习惯帮我省掉了好几次“答辩现场卸载失败”的尴尬。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?