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. NSIS+Duilib 仿 QQ 安装包:从界面到流程的完整落地思路
如果你做过 Windows 桌面软件的分发,大概率绕不开一个尴尬:NSIS 自带的界面太“上古”了,而 QQ 安装包那种左侧大图、右侧滚动协议、底部一排按钮的布局,用户早就看习惯了。用 NSIS 做安装逻辑,用 Duilib 做界面渲染,把两者拼起来,就能得到一个既保留 NSIS 脚本能力、又有现代 UI 的安装包。这个方案适合谁?适合需要自定义安装向导、又不想引入完整安装框架的 Windows 客户端团队。核心思路是:NSIS 负责解压、写注册表、创建快捷方式,Duilib 负责画窗口、响应鼠标、驱动页面切换。难点不在单个技术,而在两者如何通信——NSIS 插件怎么把 Duilib 窗口嵌进去,按钮点击怎么回调到 NSIS 脚本。下面按“先跑通最小窗口、再补交互、最后处理安装逻辑”的顺序拆开讲。
2. 环境搭建与最小可运行窗口:让 Duilib 在 NSIS 里先亮起来
2.1 为什么选 Duilib 而不是 NSIS 自带的 MUI2
NSIS 的 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 size="700,500" caption="0,0,0,32" mininfo="700,500">
<HorizontalLayout bkcolor="#FFFFFFFF">
<!-- 左侧导航栏,固定 180 宽 -->
<VerticalLayout width="180" bkcolor="#FF2B2B2B">
<Label text="MyApp" textcolor="#FFFFFFFF" font="18" height="60"
align="center" />
<Label text="步骤 1/3" textcolor="#FFAAAAAA" font="12"
align="center" />
</VerticalLayout>
<!-- 右侧主区域 -->
<VerticalLayout>
<!-- 内容区,可滚动 -->
<RichEdit name="content" bkcolor="#FFFFFFFF"
vscrollbar="true" inset="20,20,20,20" />
<!-- 底部按钮栏 -->
<HorizontalLayout height="60" bkcolor="#FFF5F5F5">
<Control />
<Button name="btn_install" text="安装" width="100" height="32"
bkcolor="#FF1E90FF" textcolor="#FFFFFFFF" />
<Button name="btn_cancel" text="取消" width="100" height="32"
bkcolor="#FFE0E0E0" textcolor="#FF333333" />
</HorizontalLayout>
</VerticalLayout>
</HorizontalLayout>
</Window>
逻辑说明: caption="0,0,0,32" 表示窗口可拖动区域在顶部 32 像素,这是 Duilib 自定义标题栏的常见写法。 RichEdit 控件用来显示协议文本并支持滚动, vscrollbar="true" 开启垂直滚动条。按钮的 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. 避坑与排查:NSIS+Duilib 组合里最容易翻车的五个点
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 是自定义定时器 ID, m_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"
然后在关键步骤加 LogText :
LogText "开始释放文件"
File /r "dist\*.*"
LogText "文件释放完成"
参数说明: LogSet on 开启日志, LogText 写入自定义文本。日志文件默认在 $TEMP 下,文件名带安装包名称。排查时先看日志最后一行,确认卡在哪一步。我一般会在插件调用前后都加日志,这样能区分是 NSIS 逻辑问题还是 Duilib 窗口问题。
6.4 一个我踩过的坑:Duilib 窗口关闭后资源没释放
早期版本里,我在 ShowWnd 里 new 了窗口对象,但窗口关闭后只 delete 了对象,没有清理 Duilib 的全局资源。结果第二次调用安装包时,XML 解析失败。后来在插件里加了 CPaintManagerUI::RemoveAllFonts() 和 CPaintManagerUI::RemoveAllImages() ,在窗口销毁后调用。这个习惯一直保留到现在:每次 Duilib 窗口生命周期结束,手动清理全局资源,避免残留状态影响下一次创建。
希望帮到你。
更多推荐

所有评论(0)