NSIS 安装程序的界面结构

作者:贾可

一般由 NSIS 安装程序的界面样式如图 1 所示。它由以下几层界面构成:父窗口、父窗口上的控件、子窗口(每个单独的页面都是一个子窗口)、子窗口上的控件。虽然控件也是窗口,但是我们这里根据属性的不同在名称上作以区分。父窗口上的控件和子窗口都是父窗口的儿子,而子窗口上的控件是子窗口的儿子,父窗口的孙子。这层关系一定要先理清楚。下面对这些窗口的作用,以及它们在 NSIS 中的一些相关逻辑进行简单说明。

图 1 MUI 界面资源示例.jpg图 1 MUI 界面资源示例.jpg

以下所提及的 MUI 均代表 MUI 和 MUI2 的统称,并非特指 MUI 第一代。不包含 MUI 头文件的情况下,我们称之为经典 UI,即 CUI,对应的 UI 界面也相对较小,如图 2 所示。

图 2 CUI 界面资源示例.jpg图 2 CUI 界面资源示例.jpg

info:1. 父窗口

父窗口即最外层的窗口,是整个 NSIS 界面的基础。父窗口在 NSIS 的 UI 文件或安装程序的资源的 ID 为 105,在 NSIS 源码中,其被定义为宏名称 IDD_INST。在 CUI 中使用 ChangeUI命令替换对话框时,参数 IDD_INST 用于指定替换父窗口。在 MUI 中,头文件已经默认使用ChangeUI all 替换了全部对话框。

父窗口在整个安装界面过程中只初始化一次,因此,很多针对父窗口的操作也只需要在.onGUIInit 函数中进行一次(MUI 需要定义一个 MUI_CUSTOMFUNCTION_GUIINIT 函数来替代.onGUIInit)。当然,如果你要在每个页面上单独对父窗口做一些处理,可以在对应的页面显示函数中单独进行处理,通常不需要这么做。父窗口上的控件通常包括:上一步(ID 为 3)、下一步(ID 为 1)、取消(ID 为 2),以及 NSIS 版本文字和水平线。在 MUI 中,父窗口还包括顶部的标题(ID 为 1037)和副标题(ID 为 1038),以及安装程序图标或安装程序 logo 图片。父窗口还包括一个 ID 为 1018 的控件(IDC_CHILDRECT)。此控件默认样式是隐藏的,其唯一作用就是 NSIS 创建子窗口时会根据此控件的位置和大小来创建。即它只是一个起参考作用的控件,仅仅用来确定子窗口的位置和大小。使用 Resource Hacker之类的工具打开NSIS安装目录中的Contrib文件夹中UIs文件夹中的modern.exe这个界面演示文件来验证这个问题。可以看到 ID 为 102~104、106~109 的对话框的大小默认为 300×140,而 ID 为 105 对话框中的 1018 控件大小也是 300×140。

MUI 头文件引入了欢迎页面、开始菜单文件夹选择页面和完成页面。这些页面不属于内置页面,均由 MUI 头文件完成。同时,在 MUI 的界面资源中,父窗口又引入了一个 ID 为 1044 的控件。此控件默认样式同样也是隐藏的,其唯一作用就是在 MUI 中用作创建欢迎和完成页面子窗口的位置和大小参考。此外,MUI 头文件中还实现了在欢迎和完成页面隐藏 NSIS 版本文字和水平线的功能。总而言之,父窗口上的这些控件的 HWND、位置、大小等在整个安装过程是不变的,只有文字和显示状态发生改变。

父窗口自身的 HANDLE(即 HWND)值可以在 NSIS 脚本中通过$HWNDPARENT 变量来引用。除了.onInit 函数及其调用的函数,其他执行代码段中该值均有效。此 HWND 值可用于绝大多数 Windows API 如 GetClientRect、ShowWindow、SendMessage 等以获取或设置窗口的属性。部分的 API 在 NSIS 中已有对应的封装命令,如 ShowWindow 和 SendMessage 等。其他 NSIS 未提供对应命令的 API 可使用 System::Call 来调用。

要控制父窗口上的控件,我们通常使用 GetDlgItem 通过 ID 来获取对应控件的 HWND 值。由于父窗口上控件是不会变动的,所以只需要在.onGUIInit 中对这些控件做出调整即可。例如,在.onGUIInit 函数中,将标题和副标题控件的文字颜色设置为红色。


GetDlgItem $0 $HWNDPARENT 1037
SetCtlColors $0 0xFF0000 transparent
GetDlgItem $0 $HWNDPARENT 1038
SetCtlColors $0 0xFF0000 transparent
info:2. 子窗口

子窗口即上面图中虚线框区域所属的窗口,子窗口不是固定的窗口。每一个安装页面都是一个独立的子窗口,且同一时刻只存在一个子窗口。子窗口在 NSIS 的 UI 文件或安装程序的资源的 ID 为除 105(IDD_INST)和 111(IDD_VERIFY)之外的所有窗口资源。在 CUI 中使用 ChangeUI 命令替换对话框资源时,参数 IDD_LICENSE、IDD_DIR、IDD_SELCOM、IDD_INSTFILES、IDD_UNINST 用于指定替换对应的窗口。上面已经提到过,欢迎页面、开始菜单文件夹选择页面和完成页面由 MUI 实现,这些 ID 不包括这三个页面。在 MUI 中,头文件已经默认使用 ChangeUI all 替换了全部对话框。

当点击父窗口上的上一步、下一步时,NSIS 会销毁当前子窗口,并创建新的子窗口。所以,子窗口上的数据并不会保留,而是需要每次创建成功后,都会重新设置一次。所有内置界面的这些逻辑均由 NSIS 完成。如果创建自定义界面,则需要使用在整个安装期间值都能保持稳定的变量自行保存界面上的数据,在界面创建的时候更新对应的控件内容,在控件内容变化的时候及时更新对应的变量。

每个独立的页面都有一个内置的名称,如 directory、components、instfiles 等。在CUI 中使用这些页面的时候,可以为其指定三个回调函数。如:


Page directory DirPage.Pre DirPage.Show DirPage.Leave

这三个回调的顺序是固定的,但可以部分或者全部忽略,不需要的可以留空或者不指定。而在 MUI 中则使用如下的方式来指定这三个页面回调函数:


;指定对应的回调函数名(对下面最近的页面有效)
!define MUI_PAGE_CUSTOMFUNCTION_PRE DirPage.Pre
!define MUI_PAGE_CUSTOMFUNCTION_SHOW DirPage.Show
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE DirPage.Leave
;立即插入对应的页面
!insertmacro MUI_PAGE_DIRECTORY

这种方式对宏定义的顺序要求就没那么严格,对用的语句由头文件来实现。

在内置页面回调函数中可以进行一些基本操作,在 Pre 函数中,可以调用 Abort 跳过当前页面。在 Show 函数中,可以获取到当前对话框的 HWND,以便控制和操作界面上控件。在Leave 函数中,可以调用 Abort 停留在当前页面。而自定义界面只有两个回调函数:Create函数和 Leave 函数。这是因为在调用自定义界面插件的 Create 函数,如 nsDialogs::Show之前,可以调用 Abort 跳过自定义界面,也可以得到对话框的 HWND、控件的 HWND 以操作控件,即自定义页面的 Create 函数可以实现内置页面的 Pre 和 Show 这两个函数的逻辑。

由于子窗口上的控件属于子窗口,以及子窗口在切换页面时会销毁或创建,所以如果要控制内置子窗口上的控件,我们需要在内置页面的 Show 回调函数中先使用 FindWindow 命令获取子窗口的 HWND,然后再使用 GetDlgItem 通过子窗口上的控件 ID 来获取对应控件的 HWND 值,再对这些控件做出调整。示例代码如下,仅供参考。实际的控件 ID 可使用 Resource Hacker 之类的工具在对应的窗口资源中查找。


FindWindow $R0 "#32770" "" $HWNDPARENT
GetDlgItem $0 $R0 1000
SendMessage $0 ${WM_SETTEXT} 0 STR:$INSTDIR

自定义界面中则不需要这么麻烦。比如使用 nsDialogs 时,nsDialogs::Create 的返回值就是子窗口的 HWND,${NSD_Create*}的返回值则是控件的 HWND。

info:3. 自定义 UI

如果要使用自定义的对话框资源替换内置的对话框,需要注意几点:

每个对话框中都有一些必须保留的控件 ID,修改 UI 时只能调整它们的位置和大小,不能删除这些基本控件。如,安装过程页面必须保留进度条。参考 NSIS.chm 的 ChangeUI 命令。

所有的子窗口(除了 111 之外)的大小必须和父窗口(105)中的 1018 保持严格一致。如果大小不一致,可能窗口会显示不全,或者多出一些区域。

none
最后修改于:2021年02月02日 10:51

添加新评论