前面的几篇文章介绍了 NSIS 的传统界面的安装包和现代界面的安装包的制作方法,也提到了 NSIS 支持自定义页面(即使用page custom)的特性,自定义页面需要用户自己创建对话框、控件、添加控件响应等等,虽然 NSIS 提供了nsDialogs.nsh来支持这些功能,但使用起来还是不太方便(需要专门了解这个插件诸多用法),而且不够灵活,所以本文介绍一种终极的自定义界面的安装包解决方案,即完全使用第三方界面库来绘制安装包界面。

该方案是对界面库没有限制的,可以使用其他任何界面库,如 MFC, Qt,WTL 等。通过这种方案可以很轻松的实现类似金山毒霸、QQ、360 安全卫士等软件的安装包界面。

一、原理

NSIS 自定义页面的语法:

1
page custom [创建函数] [离开函数] [标题]

使用第三方界面库完全定制安装包界面的基本原理就是:新建一个 dll 插件,在page custom[创建函数]中调用该插件中的函数来显示界面,这时界面上的按钮的响应就不再由 NSIS 控制了,完全由我们的代码控制。

二、难点问题

使用我们的插件 dll 完全替代 NSIS 界面之后,有几个问题需要解决:

  • 如何获取安装和卸载的进度
  • 如何从 C++回调 NSIS 函数

2.1 安装和卸载进度

NSIS 中的安装和卸载进度由!insertmacro MUI_PAGE_INSTFILESPage instfiles提供。

在完全使用自己的界面之后,这 2 个 NSIS 界面都不能使用了,这时我们需要自己获取安装(释放)和卸载(删除)的进度。

以安装进度为例,NSIS 中文件的安装时文件释放功能都是由File命令提供,但该命令没有提供释放进度,所以我们无法获取到实时的释放进度。在这里我们可以使用一个曲折的方法,我们将一个 7z 压缩包放入安装包中:

1
2
SetOutPath $INSTDIR
File "app\app.7z"

等安装包释放完这个压缩包之后(这段时间的进度无法显示),再使用 NSIS 官方提供的nsis7z插件来解压缩这个 7z 压缩包,由于 nsis7z 插件可以提供解压缩进度,所以我们可以将这个进度显示在安装进度页面上,解压完之后再删除 7z 压缩包。这个方案的一个弊端就是,7z 压缩包从安装包中释放到本地磁盘的过程需要时间,且这个时间无法准确的显示在进度页面。

1
2
3
4
5
6
7
Function ExtractFunc
SetOutPath $INSTDIR
File "app\app.7z"
GetFunctionAddress $R9 ExtractCallback
Nsis7z::ExtractWithCallback "$INSTDIR\app.7z" $R9
Delete "$INSTDIR\app.7z"
FunctionEnd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function ExtractCallback
Pop $1
Pop $2
System::Int64Op $1 * 100
Pop $3
System::Int64Op $3 / $2
Pop $0

nsDui::SetSliderValue "slrProgress" $0

${If} $1 == $2
nsDui::SetSliderValue "slrProgress" 100
nsDui::NextPage "wizardTab"
${EndIf}
FunctionEnd

写这篇文字的时候,发现现在的nsis7z已经太老了,新版的压缩软件生成的 7z 压缩包,该插件已经无法解压。可以使用 7za.exe 命令行工具来生成 7z 压缩文件,7za.exe 从此处下载:http://download.csdn.net/download/china_jeffery/10214464
7za 生成 7z 压缩包语法为:
7za.exe a app.7z app\*

2.2 从 C++回调 NSIS 函数

比如用户点击了我们自定义界面上的“取消”按钮,这时我们需要调用 NSIS 的Abort函数来取消安装。此时就需要解决如何从 C++环境回调到 NSIS 环境。

我们可以使用NSIS教程(8)-插件开发中介绍的PluginCommon.h来实现该功能。

大致原理是,在 NSIS 脚本中初始化自定义界面的控件与 NSIS 函数指针(整型)的绑定关系(如控件名–函数名),当用户点击控件之后,查找到该控件绑定的 NSIS 函数,然后调用extra_parameters::ExecuteCodeSegment函数(函数第一个参数就是 NSIS 函数指针)。

以 duilib 界面库为例,对 NSIS 暴露 OnControlBindNSISScript 接口,提供绑定控件与 NSIS 函数指针(整型)的功能:

1
2
3
4
5
6
7
8
9
NSISAPI OnControlBindNSISScript(HWND hwndParent, int string_size, char *variables, stack_t **stacktop, extra_parameters *extra)
{
char controlName[MAX_PATH];
ZeroMemory(controlName, MAX_PATH);

popstring(controlName);
int callbackID = popint();
g_pMainDlg->SaveToControlCallbackMap( controlName, callbackID );
}

在 NSIS 中调用OnControlBindNSISScript绑定控件与 NSIS 函数:

1
2
GetFunctionAddress $0 OnExitDUISetup
nsDui::OnControlBindNSISScript "btnFinishedClose" $0

在 duilib 的Notify按钮事件响应函数中调用ExecuteCodeSegment执行 NSIS 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CDlgMain::Notify( TNotifyUI& msg )
{
std::map<CDuiString, int >::iterator iter = m_controlCallbackMap.find( msg.pSender->GetName() );
if( _tcsicmp( msg.sType, _T("click") ) == 0 ){
if( iter != m_controlCallbackMap.end() )
g_pluginParms->ExecuteCodeSegment( iter->second - 1, 0 );
}
else if( _tcsicmp( msg.sType, _T("textchanged") ) == 0 ){
if( iter != m_controlCallbackMap.end() )
g_pluginParms->ExecuteCodeSegment( iter->second - 1, 0 );
} else {
WindowImplBase::Notify(msg);
}
}

可以参考我的NSIS-UI-Plugin 项目,基于该项目可以使用任意第三方界面库来定制安装界面。