ARTICLE DETAIL

资讯详情

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

NSIS+Duilib 仿 QQ 安装包:界面与流程完整落地

NSIS+Duilib 仿 QQ 安装包:界面与流程完整落地 简介这份资源是一套基于NSIS与Duilib构建仿QQ风格安装包的完整工程面向希望掌握Windows桌面安装程序开发与自定义UI的中高级C开发者。它解决的是传统安装包界面粗糙、交互生硬的问题通过Duilib实现接近QQ的视觉与操作体验同时借助NSIS脚本完成文件复制、注册表操作、环境变量设置等安装逻辑。压缩包共206个文件约69.53MB以h头文件、cpp源文件、obj编译产物、png界面素材为主另含sln解决方案、vcxproj工程文件、nsi脚本、nsh头文件及第三方库压缩包工程按3party、samples、plugin、include等目录组织便于理解NSIS插件扩展与Duilib界面整合方式。已有1855人学习下载。读者可从中获得一套可编译运行的仿QQ安装包实例掌握NSIS脚本编写、Duilib界面设计、插件调用与安装流程优化等实用技能适合作为桌面应用安装程序开发的参考模板。1. NSISDuilib 仿 QQ 安装包从界面到流程的完整落地思路如果你做过 Windows 桌面软件的分发大概率绕不开一个尴尬NSIS 自带的界面太“上古”了而 QQ 安装包那种左侧大图、右侧滚动协议、底部一排按钮的布局用户早就看习惯了。用 NSIS 做安装逻辑用 Duilib 做界面渲染把两者拼起来就能得到一个既保留 NSIS 脚本能力、又有现代 UI 的安装包。这个方案适合谁适合需要自定义安装向导、又不想引入完整安装框架的 Windows 客户端团队。核心思路是NSIS 负责解压、写注册表、创建快捷方式Duilib 负责画窗口、响应鼠标、驱动页面切换。难点不在单个技术而在两者如何通信——NSIS 插件怎么把 Duilib 窗口嵌进去按钮点击怎么回调到 NSIS 脚本。下面按“先跑通最小窗口、再补交互、最后处理安装逻辑”的顺序拆开讲。2. 环境搭建与最小可运行窗口让 Duilib 在 NSIS 里先亮起来2.1 为什么选 Duilib 而不是 NSIS 自带的 MUI2NSIS 的 Modern UI 2 能改颜色、换图片但布局能力很弱。QQ 安装包那种左侧导航栏固定、右侧内容区滚动、底部按钮悬浮的布局用 MUI2 的页面模板很难精确控制。Duilib 是 DirectUI 思路所有控件都是自绘的XML 描述布局C 处理逻辑改起来自由得多。常见做法是把 Duilib 编译成一个 NSIS 插件 DLL在 NSIS 脚本里调用插件函数插件内部创建 Duilib 窗口并进入消息循环。这样 NSIS 的安装逻辑和 Duilib 的界面逻辑各管各的通过插件接口传参。选型上要注意Duilib 有多个分支建议用带完整 XML 解析和控件库的版本不要用只含核心的裁剪版否则后面加滚动条、富文本都要自己补。NSIS 用 3.x 版本插件接口用nsisplugin导出方式。2.2 用 NSIS 插件方式加载 Duilib 窗口的最小命令先建一个空的 NSIS 脚本调用一个自定义插件DuilibWnd.dll插件里创建窗口。NSIS 侧代码; installer.nsi OutFile QQSetup.exe InstallDir $PROGRAMFILES\MyApp Section ; 调用插件传入安装目录和窗口标题 DuilibWnd::ShowWnd $INSTDIR MyApp 安装向导 Pop $0 ; 返回值 0 表示用户点了“安装”1 表示取消 StrCmp $0 0 2 Abort SectionEnd插件侧 C 关键代码// DuilibWnd.cpp #include stdafx.h #include DuilibWnd.h // NSIS 插件导出函数参数从栈上取 extern C __declspec(dllexport) void ShowWnd(HWND hwndParent, int string_size, char *variables, stack_t **stacktop) { // 取 NSIS 传入的两个字符串参数 char installDir[MAX_PATH]; char title[MAX_PATH]; popstring(installDir); popstring(title); // 初始化 Duilib 窗口 CPaintManagerUI::SetInstance(GetModuleHandle(NULL)); CMainWnd* pWnd new CMainWnd(); pWnd-Create(NULL, title, UI_WNDSTYLE_FRAME, 0); pWnd-CenterWindow(); pWnd-ShowWindow(true); // 进入消息循环直到窗口关闭 CPaintManagerUI::MessageLoop(); // 把结果压回 NSIS 栈0 安装1 取消 pushstring(pWnd-IsInstallClicked() ? 0 : 1); delete pWnd; }逻辑说明NSIS 的DuilibWnd::ShowWnd调用会阻塞直到 Duilib 窗口关闭。插件内部用popstring取参数用pushstring回传结果。CPaintManagerUI::MessageLoop()是 Duilib 的标准消息泵窗口关闭后返回。参数installDir用于在界面上显示安装路径title是窗口标题。注意 NSIS 插件函数签名必须匹配nsisplugin规范否则调用会崩溃。2.3 编译插件时最容易翻车的三个配置项第一个是运行库。NSIS 插件 DLL 必须用/MT静态链接 CRT否则在没装 VC 运行库的机器上加载失败。第二个是导出函数名。NSIS 要求导出函数用__declspec(dllexport)且调用约定为__cdecl如果用了__stdcall会导致栈不平衡。第三个是字符集。NSIS 3.x 默认 Unicode插件里所有字符串处理要用TCHAR和_tcs系列函数不要混用char和wchar_t。我一般会在项目属性里把“C/C → 代码生成 → 运行库”设为“多线程 (/MT)”字符集设为“使用 Unicode 字符集”。3. 界面布局与交互用 XML 还原 QQ 安装包的视觉结构3.1 左侧导航 右侧内容区的 XML 布局写法QQ 安装包的结构可以拆成三块左侧固定宽度的导航栏放 logo 和步骤指示右侧内容区放协议、路径选择、进度条底部按钮栏安装/取消。Duilib 的 XML 用HorizontalLayout分左右右侧再套VerticalLayout分内容和按钮。核心 XML!-- skin\install_wnd.xml -- Window size700,500 caption0,0,0,32 mininfo700,500 HorizontalLayout bkcolor#FFFFFFFF !-- 左侧导航栏固定 180 宽 -- VerticalLayout width180 bkcolor#FF2B2B2B Label textMyApp textcolor#FFFFFFFF font18 height60 aligncenter / Label text步骤 1/3 textcolor#FFAAAAAA font12 aligncenter / /VerticalLayout !-- 右侧主区域 -- VerticalLayout !-- 内容区可滚动 -- RichEdit namecontent bkcolor#FFFFFFFF vscrollbartrue inset20,20,20,20 / !-- 底部按钮栏 -- HorizontalLayout height60 bkcolor#FFF5F5F5 Control / Button namebtn_install text安装 width100 height32 bkcolor#FF1E90FF textcolor#FFFFFFFF / Button namebtn_cancel text取消 width100 height32 bkcolor#FFE0E0E0 textcolor#FF333333 / /HorizontalLayout /VerticalLayout /HorizontalLayout /Window逻辑说明caption0,0,0,32表示窗口可拖动区域在顶部 32 像素这是 Duilib 自定义标题栏的常见写法。RichEdit控件用来显示协议文本并支持滚动vscrollbartrue开启垂直滚动条。按钮的name属性用于在 C 里绑定点击事件。参数inset控制内容区内边距避免文字贴边。3.2 按钮点击如何回调到 NSIS 脚本Duilib 的按钮事件在 C 里处理处理完需要把结果传给 NSIS。常见做法是在窗口类里保存一个状态变量窗口关闭后由插件函数pushstring回传。比如“安装”按钮点击后设置m_bInstall true然后Close()插件函数根据m_bInstall压入 “0” 或 “1”。NSIS 侧用Pop $0取结果再决定是否继续安装。// CMainWnd::Notify void CMainWnd::Notify(TNotifyUI msg) { if (msg.sType _T(click)) { if (msg.pSender-GetName() _T(btn_install)) { m_bInstall true; Close(); } else if (msg.pSender-GetName() _T(btn_cancel)) { m_bInstall false; Close(); } } __super::Notify(msg); }参数说明msg.sType是事件类型click表示点击msg.pSender是触发事件的控件指针通过GetName()匹配 XML 里的name。m_bInstall是自定义成员变量初始为 false。注意Close()之后消息循环会退出插件函数继续执行压栈操作。3.3 进度条与安装状态同步的两种做法安装进度需要从 NSIS 传到 Duilib 界面。两种做法一是 NSIS 在安装过程中通过插件导出函数UpdateProgress(int percent)主动通知 Duilib 窗口刷新进度条二是 Duilib 窗口启动一个定时器轮询共享内存里的进度值。第一种更直接但要注意 NSIS 安装线程和 Duilib UI 线程的同步。我一般用第一种在插件里暴露一个UpdateProgress函数NSIS 每解压一个文件就调用一次。; 在解压循环里调用 DuilibWnd::UpdateProgress 10 ; ... 解压文件 ... DuilibWnd::UpdateProgress 50插件侧extern C __declspec(dllexport) void UpdateProgress(HWND hwndParent, int string_size, char *variables, stack_t **stacktop) { char buf[16]; popstring(buf); int percent atoi(buf); if (g_pMainWnd) { // 通过 PostMessage 避免跨线程直接操作 UI ::PostMessage(g_pMainWnd-GetHWND(), WM_USER 100, percent, 0); } }逻辑说明g_pMainWnd是全局窗口指针在ShowWnd里赋值。PostMessage把进度值投递到 UI 线程的消息队列窗口过程里处理WM_USER 100消息并更新进度条控件。参数percent是 0 到 100 的整数。注意不要用SendMessage否则 NSIS 线程会阻塞等待 UI 线程可能导致死锁。4. 安装逻辑与 NSIS 脚本文件释放、注册表与快捷方式4.1 用 File 指令释放文件时的目录结构设计NSIS 的File指令把文件从安装包释放到$INSTDIR。QQ 安装包通常会把主程序、资源文件、插件分目录存放。建议在编译前把要打包的文件按目标目录组织好比如dist\MyApp.exe、dist\res\skin.xml、dist\plugins\然后在 NSIS 里用/r递归释放。SetOutPath $INSTDIR File /r dist\*.*参数说明SetOutPath设置后续File指令的目标目录/r表示递归包含子目录。注意File /r会保留源目录结构所以dist\res\skin.xml会释放到$INSTDIR\res\skin.xml。如果不想保留顶层目录可以在dist下直接放文件或者用File /r dist\*.*配合SetOutPath逐层设置。4.2 写注册表与创建快捷方式的必填项安装完成后需要写卸载信息、创建开始菜单和桌面快捷方式。QQ 安装包还会写文件关联和开机启动项但这里只列必填项。; 写卸载信息 WriteRegStr HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp \ DisplayName MyApp WriteRegStr HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp \ UninstallString $INSTDIR\uninst.exe WriteRegStr HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp \ DisplayIcon $INSTDIR\MyApp.exe ; 创建快捷方式 CreateShortCut $DESKTOP\MyApp.lnk $INSTDIR\MyApp.exe CreateDirectory $SMPROGRAMS\MyApp CreateShortCut $SMPROGRAMS\MyApp\MyApp.lnk $INSTDIR\MyApp.exe CreateShortCut $SMPROGRAMS\MyApp\卸载.lnk $INSTDIR\uninst.exe参数说明HKLM表示本地机器注册表需要管理员权限如果安装包不要求提权改用HKCU。DisplayIcon指向 exe 文件系统会自动提取图标。CreateShortCut的第一个参数是快捷方式路径第二个是目标路径。注意$DESKTOP和$SMPROGRAMS是 NSIS 内置变量分别对应当前用户桌面和开始菜单程序目录。4.3 卸载时清理残留的脚本写法卸载脚本要删除安装目录、快捷方式、注册表项。常见坑是卸载时没删干净导致重装后旧文件还在。Section Uninstall Delete $DESKTOP\MyApp.lnk Delete $SMPROGRAMS\MyApp\MyApp.lnk Delete $SMPROGRAMS\MyApp\卸载.lnk RMDir $SMPROGRAMS\MyApp DeleteRegKey HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall\MyApp RMDir /r $INSTDIR SectionEnd参数说明RMDir /r递归删除目录及内容慎用确保$INSTDIR是安装目录而不是根目录。DeleteRegKey删除整个注册表项。注意卸载时如果程序正在运行文件删除会失败常见做法是先调用nsProcess插件结束进程或者提示用户手动关闭。5. 避坑与排查NSISDuilib 组合里最容易翻车的五个点5.1 现象安装包启动后窗口一闪而过NSIS 直接退出原因插件函数没有正确阻塞消息循环或者Create失败后没有检查返回值就继续执行。Duilib 窗口创建失败通常是因为 XML 路径不对或资源没打包进去。解决在Create后加if (!pWnd-GetHWND()) { pushstring(1); return; }确保失败时回传取消。XML 路径用绝对路径或相对于 exe 的路径不要用工作目录相对路径。打包时确认skin目录被File /r包含。5.2 现象点击按钮没反应或者界面卡死原因Duilib 的消息循环被 NSIS 的某个同步操作阻塞或者按钮事件没有绑定到Notify函数。常见于在Notify里调用了SendMessage到 NSIS 线程。解决所有跨线程通信改用PostMessage。检查窗口类是否重写了Notify并调用了__super::Notify。按钮的name属性是否和 C 里匹配的字符串一致大小写敏感。5.3 现象进度条不刷新安装完成后才跳到 100%原因NSIS 安装循环里调用UpdateProgress后没有让 UI 线程有机会处理消息或者用了SendMessage导致 NSIS 线程等待。解决在 NSIS 每次调用UpdateProgress后加Sleep 10给 UI 线程留出处理时间。插件里用PostMessage投递进度窗口过程里更新控件后调用Invalidate强制重绘。5.4 现象在 Win7 上正常Win10 上窗口标题栏拖动失效原因Duilib 的caption区域设置和 Windows 10 的 DPI 缩放冲突导致鼠标坐标偏移。解决在插件初始化时调用SetProcessDPIAware()声明 DPI 感知或者在 manifest 里设置dpiAware。caption区域的高度按 96 DPI 设计高 DPI 下按比例缩放。我一般会在WinMain入口处调用SetProcessDPIAware()。5.5 现象卸载后重新安装旧版本文件残留原因卸载脚本没有删除所有文件或者RMDir /r因为文件占用失败。解决卸载前先结束进程用nsProcess::KillProcess或taskkill。RMDir /r后检查$INSTDIR是否还存在如果存在则提示用户手动删除。注册表项也要清理干净特别是Uninstall键否则控制面板里会有多个条目。6. 进阶技巧用 Duilib 动画和 NSIS 静默参数提升安装体验6.1 给页面切换加淡入淡出动画Duilib 支持控件动画可以通过SetFade或自定义定时器实现。QQ 安装包在步骤切换时有轻微的滑动效果。实现方式在Notify里捕获按钮点击后启动一个定时器逐步改变内容区的padding或透明度。// 在窗口类里定义动画定时器 void CMainWnd::OnTimer(UINT_PTR id) { if (id TIMER_ANIM) { m_nAnimStep; int offset 20 - m_nAnimStep * 2; // 从 20 递减到 0 if (offset 0) { KillTimer(GetHWND(), TIMER_ANIM); offset 0; } // 更新内容区 inset产生滑动效果 m_pContent-SetInset(0, offset, 0, 0); m_pContent-Invalidate(); } __super::OnTimer(id); }参数说明TIMER_ANIM是自定义定时器 IDm_nAnimStep记录动画步数SetInset调整内容区内边距。每 30 毫秒触发一次总共 10 步完成动画。注意动画期间禁用按钮避免重复触发。6.2 支持 /S 静默安装和 /D 指定目录NSIS 原生支持/S静默安装和/D路径指定目录。但 Duilib 窗口默认会显示需要在脚本里判断。; 检查是否静默安装 IfSilent silent_install ; 非静默调用 Duilib 窗口 DuilibWnd::ShowWnd $INSTDIR MyApp 安装向导 Pop $0 StrCmp $0 0 2 Abort Goto done silent_install: ; 静默模式直接安装不显示界面 done:参数说明IfSilent检测命令行是否带/S。静默模式下跳过 Duilib 调用直接执行安装逻辑。/D指定的目录会自动覆盖$INSTDIR不需要额外处理。注意静默安装时不要弹出任何 MessageBox否则会卡住自动化脚本。6.3 用日志验证安装流程是否按预期执行NSIS 可以写安装日志方便排查。在脚本开头加LogSet on指定日志文件路径。LogSet on !define LOGFILE $TEMP\MyApp_install.log然后在关键步骤加LogTextLogText 开始释放文件 File /r dist\*.* LogText 文件释放完成参数说明LogSet on开启日志LogText写入自定义文本。日志文件默认在$TEMP下文件名带安装包名称。排查时先看日志最后一行确认卡在哪一步。我一般会在插件调用前后都加日志这样能区分是 NSIS 逻辑问题还是 Duilib 窗口问题。6.4 一个我踩过的坑Duilib 窗口关闭后资源没释放早期版本里我在ShowWnd里new了窗口对象但窗口关闭后只delete了对象没有清理 Duilib 的全局资源。结果第二次调用安装包时XML 解析失败。后来在插件里加了CPaintManagerUI::RemoveAllFonts()和CPaintManagerUI::RemoveAllImages()在窗口销毁后调用。这个习惯一直保留到现在每次 Duilib 窗口生命周期结束手动清理全局资源避免残留状态影响下一次创建。希望帮到你。本文还有配套的精品资源点击获取
返回列表