NSIS是功能强大的安装包制作工具,但其高质量的教程稀少,大部分文章浅尝辄止,导致新手上手困难很难写出实际使用的安装包。且官方文档只罗列知识点,让人很难组合成实际用途。作者认为比起手册式写法,更需要从实际使用的角度出发的文章,在过程中逐渐引入概念和用法,更加符合人学习的思维。
1. 开始使用
1.1 简介
来自官方的介绍:
NSIS (Nullsoft Scriptable Install System) 是 Windows 下的一个安装程序创建工具,它允许程序员来创建这样的安装程序。
NSIS 创建的安装程序能够安装、卸载、设置系统设置、解压文件等等。 因为它基于脚本文件,你可以完全的控制安装程序的每一部分。 脚本语言支持变量、函数、字串操作,就像一个普通的程序语言一样 - 但是设计来创建安装程序。 即使有那么多的特性, NSIS 仍然是最小的安装程序系统。在默认选项下,它仅增加了 34 KB 的开销。
1.2 NSIS常用特性
- 脚本化,支持多文件的代码组织
- 注册表编辑、执行脚本、修改环境变量
- 较小的安装压缩大小
- 较为美观的界面,还支持自定义界面
- 插件系统
- 支持网络安装
- 支持做补丁安装
1.3 安装
在官网下载NSIS安装包。安装后将安装路径加入系统环境变量,如:C:\Program Files (x86)\NSIS。在命令行中输入nsis
或makensis
测试是否工作。
1.4 脚本示例项目
本文同时提供有一个示例项目,它包含在文档中内容的代码示例,包括几个示例项目,可以对照脚本与文章内容,会更容易理解到如何使用NSIS。
项目对应关系如下表:
项目 | 主文件 | 介绍 |
---|---|---|
base/ | base/installer.nsi | 包含安装脚本的主体结构,与本文章节3对应 |
install/ | install/installer.nsi | 在base的基础上增加安装与卸载的内容,与本文的章节4、5、6对应 |
2. 开始编写一个脚本
NSIS 可以通过多种方式编写:图形化Editor、脚本、三方工具。中文互联网上很多博客都是关于图形化Editor的,实际上这个东西做出来自己用用还行,距离商业化还是有些距离。本文是以编写脚本代码的方式来定义安装包的,原理如下:
创建代码文件
NSIS脚本文件的拓展名是.nsi,我们创建一个 installer.nsi,文件名可以随意取。一般为要打包的程序名称,本文示例为了不产生误解取为 installer。
.nsi文件可以使用任何文本编辑器打开,建议使用vscode打开,安装NSIS的vscode插件编写更加方便。
在文件中添加一个安装区段
installer.nsi
Section "Section1" SEC01
SectionEnd
这里引入一个区段
的概念,区段内包含安装或卸载过程的逻辑,用于组织不同的安装内容,有以下重要特点:
- 必须至少有一个区段,不然程序没有安装的内容
- 区段默认为安装区段,un.开头的区段为卸载区段,
- 区段可不设置区段名,或者在名称前加短横线
-
,这样让其在安装过程中不可见,悄悄安装 - 区段可以是一个空区段
区段由Section和SectionEnd组成,Section行中参数依次为:区段名、区段的唯一标识码(不是必须,可以不设置)。代码中Section1
为区段名,SEC01
为区段的唯一标识码,用于其他地方操作区段的标识使用。官方文档
添加输出文件和安装代码
installer.nsi
有了安装区段后,还必须设置一个输出文件,告诉NSIS编译器安装包的输出位置。在installer.nsi的开头加入下面这行代码:
OutFile "MyApp_Setup.exe"
Section "Section1" SEC01
SetOutPath "$INSTDIR"
File "..\myapp\MyApp.exe"
SectionEnd
这里使用了一个属性命令OutFile
,它是必须有的命令。指定生成的安装包的路径和名称,可以设置相对路径与绝对路径。当只有名称时,生成文件与.nsi文件同目录。
OutFile
是必须的命令,指定编译器输出安装包的路径。使用方式如下:
OutFile [路径]安装程序名称.exe
eg.
OutFile "MyApp.exe" ;只指定程序名称的情况下,安装文件输出到.nsi脚本同目录下
OutFile "D:\installer\MyApp.exe" ;指导路径的情况下,输出到指定路径下
💡Tip1: 这里用到了命令,命令分为安装程序属性命令和编译器命令
💡Tip2: 这里用到了
;
,它和#
一起在NSIS的脚本中作为注释符,后跟注释内容
安装程序属性命令用于指定安装包相关的属性值,编译器命令用于指定编译器在编译时的选项。
Section中使用了两个指令SetOutPath
和File
,前者用于指定后续指令的输出目录,后者用于指定安装包在打包时要包含进那些文件,且在安装时要将这些文件安装到前者指定的目录中去。
💡Tip: 这里用到的两个指令都是基础指令,指令还有很多种类型,它们都需要包含在区段或函数中才能运行。与命令的区别是,命令都在编译安装包时运行,指令在安装或卸载时运行。
👀 这里有一个情况需要注意,在官方文档或者一些论坛博客内,往往可能会混用命令与指令的概念。可以这样理解,命令分为编译时的命令和运行时的命令,前者在制作安装包时执行(由NSIS编译器执行),后者称之为指令在安装包运行时执行(由一个负责脚本解析的虚拟机执行)。
🌟知识点🌟
SetOutPath 使用了一个值 $INSTDIR,这是一个已经被声明的变量。变量用来临时存储一些值,它可以被改变,在变量名前面加上 $
符号使用,自定义的变量要自己声明。查看文档变量了解更多。
💡Tip: 这里有个注意点,$INSTDIR 前后用引号包起来了。这是因为在NSIS里,数值和字符串用法都是一样。区别的方式就是字符串值在使用时,用引号包起来,不然默认会被当成数值处理。
执行脚本编译安装包
这样就完成了一个最简单的脚本文件,在命令行中执行:
makensis ./installer.nsi
注:如果提示没有找到 makensis,那你可能是没有设置nsis的环境变量
3. 完善安装包主要结构
本章代码在示例项目中对应base
项目的installer.nsi,可以将本章内容与代码对照,会更容易理解到如何使用NSIS。
3.1 安装程序属性
安装程序属性主要控制安装程序的:程序信息、图标、默认安装目录、外观、安装界面文本、页面、包含的文件。
这里主要介绍常用到的属性(如下例),更多的属性设置在官方文档中可以查到。
# define const value
!define PRODUCT_NAME "My app"
!define PRODUCT_SHORT_NAME "MyApp"
!define PRODUCT_VERSION "1.0"
!define PRODUCT_BUILD_VERSION "1.0.0.0"
!define PRODUCT_PUBLISHER "My company, Inc."
!define PRODUCT_COPYRIGHT "Copyright (c) 2023 My company Inc."
!define PRODUCT_WEB_SITE "http://www.mycompany.com"
# info of installer
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" ; set name of installer execute
OutFile "MyApp_Setup.exe" ; set file name of compiler out
Icon "..\myapp\nsis.ico"
InstallDir "C:${PRODUCT_SHORT_NAME}" ; set the default install dir
# info of installer execute file
VIAddVersionKey ProductName "${PRODUCT_NAME} Installer" ; product name
VIAddVersionKey ProductVersion "${PRODUCT_VERSION}" ; product version
VIAddVersionKey Comments "${PRODUCT_NAME}" ; description
VIAddVersionKey CompanyName "${PRODUCT_PUBLISHER}" ; compnay name
VIAddVersionKey LegalCopyright "${PRODUCT_COPYRIGHT}" ; copyright
VIAddVersionKey FileVersion "${PRODUCT_BUILD_VERSION}" ; file version
VIAddVersionKey FileDescription "${PRODUCT_NAME} Installer" ; file description
VIProductVersion "${PRODUCT_BUILD_VERSION}" ; product verion(actual replace FileVersion)
常用的设置安装属性的命令都是编辑时命令,更多属性和特殊用法查看安装程序属性:
🌟基础语法🌟
!define
是定义常量值的命令,它是一个编译时命令。常量定义好后不能改变值,定义后的常量使用 ${常量名}
进行使用。如示例代码中的:${PRODUCT_NAME}
添加安装程序信息
Name
设置安装程序的名称。
Name "My Application"
Name "Jhon & Jack App" "Jhon && Jack App"
注意:当名称中使用了&
符号时,就需要传入第二个参数“双&名称”,将第一个参数名称中的&用两个&表示。
Icon
设置安装程序的图标,图标文件后缀名为.ico。
Icon [图标文件路径]
InstallDir
设置默认安装目录。
InstallDir [路径]
eg:
InstallDir "$PROGRAMFILES\YourApp"
InstallDir "C:\YourApp"
💡Tip: $PROGRAMFILES 是一个NSIS定义的变量的常量,说是常量其实就是NSIS的提供的系统环境变量,用于获取各种路径。使用方式与变量相同,查看变量的常量章节了解可以使用的值。
安装程序版本信息类
VIAddVersionKey
与VIProductVersion
共同决定安装程序的版本信息,前方示例代码设置的属性在文件信息上看就是这样了。官方文档
3.2 添加安装界面
大部分的安装包都是有着完善的安装过程,它往往由几个界面组成:
- 欢迎界面
- license界面
- 安装路径选择界面
- 安装过程界面
- 安装完成界面
3.2.1 古典(过气)安装界面
由于古典界面过于的古典,所以简单介绍一下,应该基本不会在当代的主流安装UI中被需要。
使用Page
命令指定安装过程要包含的界面,UninstPage
命令指定卸载过程界面,通过调整前后顺序来排列安装界面的顺序。中文文档
# 安装界面
Page license
Page components
Page directory
Page instfiles
# 卸载界面
UninstPage uninstConfirm
UninstPage instfiles
古典安装界面风格:
3.2.2 现代UI界面(MUI)
现代UI简称为MUI,是NSIS安装包最流行的UI库,文档链接
💡💁🗒️Tips: 后续文档中的UI不做特别说明,默认为MUI。
NSIS自带MUI,所以直接引用进来:
!include "MUI.nsh"
先进行基础设置,MUI_ABORTWARNING
常量标志关闭安装程序窗口时给出警告提示,MUI_ICON
常量是MUI安装程序和界面的logo图标,在作用上替代了前文中Icon 命令。
!define MUI_ABORTWARNING
!define MUI_ICON "..\myapp\nsis.ico"
再设置MUI的界面,使用 insertmacro
关键字导入不同界面的宏定义。
; Welcome page
!insertmacro MUI_PAGE_WELCOME
; License page
!insertmacro MUI_PAGE_LICENSE ".\myapp\license.txt"
; Components page
!insertmacro MUI_PAGE_COMPONENTS
; Directory page
!insertmacro MUI_PAGE_DIRECTORY
; Instfiles page
!insertmacro MUI_PAGE_INSTFILES
; Finish page
!insertmacro MUI_PAGE_FINISH
这里用到了insertmarco
关键字和macro(宏)
的概念,宏常用于组织安装与卸载的代码片段。
🌟Tips: 与区段不同的是,宏用于将可重复使用的脚本片段,在各个需要的地方用
!insetmarco
标记插入位置。宏命令是在编译时插入代码,这样你只需要写一份通用的代码,就可以多处的使用。
MUI的界面风格如下:
这样安装界面就定义完成了,可以根据自己的喜好删减和排列不同的界面。
但请注意,如果只有安装界面并不能成功编译出安装包,我们还需要定义卸载程序。
3.3 卸载程序属性
有安装程序,那么与之对应的就是卸载程序。卸载程序对应着我们的程序的卸载过程,对于常规软件来说,必提供卸载程序给用户方便卸载。
🌟Tips: 卸载属性必须在所有安装和卸载页面之前定义。
# uninstaller
!define MUI_UNICON "..\myapp\uninstall.ico"
MUI_UNICON
常量指定卸载程序使用的图标。
3.4 卸载程序
不同于安装程序,卸载程序包含在安装程序内被一起安装,在安装后运行进行卸载,所以NSIS要求我们在安装的Section中定义卸载程序,由编译器生成。
Section "Section1" SEC01
SetOutPath "$INSTDIR"
WriteUninstaller "$INSTDIR\uninstall.exe"
File "..\myapp\MyApp.exe"
SectionEnd
并且,需要至少编写一个卸载区段,作为卸载程序的执行内容。这里使用唯一特殊的卸载区段Uninstall
,这是一个特殊的卸载区段名称,可以不用加un.
的前缀。
Section -Uninstall
; your uninstall code here
SectionEnd
3.5 添加卸载界面
与安装界面同理,我们给卸载程序加入<组件卸载界面>和<卸载安装文件>界面。
; Uninstaller pages
!insertmacro MUI_UNPAGE_COMPONENTS
!insertmacro MUI_UNPAGE_INSTFILES
3.6 设置界面语言
最后设置界面使用的语言,🚨需要注意的是,语言设置必须放在所有安装和卸载界面之后,不然编译会出现一个错误,或别的未知错误。
; Language files
!insertmacro MUI_LANGUAGE "English"
🌟Tips: 这里涉及到一个代码顺序的问题,NSIS是顺序执行的,所以代码顺序十分重要。因为MUI在使用中包含宏代码,而它的宏代码中的变量又有前后关系,所以使用MUI的过程中会经常遇到一些因为代码顺序导致的问题。
4. 编写安装与卸载内容
在安装程序中,安装与卸载是伴生关系,一般编写了什么安装内容,就需要编写与之相应的卸载内容。
本章代码在示例项目中对应install
项目的installer.nsi,可以将本章内容与代码对照,更直观的理解如何编写安装与卸载内容。
👀 注意:文章和示例项目中提供的代码示例都以一个软件中一个可执行程序为例,实际项目中这方面没有任何限制。一个软件中可以包含任意多个各类可执行的文件如:.exe,.cmd,.bat 等等。你都可以为他们创建桌面和开始菜单的快捷方式,在安装过程中和完成后运行等可行的所有操作。
4.1 安装和卸载文件
从这里开始,我们将前文中Section1
中的安装内容移动到Sectionmyapp
中进行默认安装。与特定的卸载Section名称Uninstall
不同的是,安装Section并没有特定的Section名称,一般我们将程序默认安装内容的Section命名为程序的名称(如: myapp)或者Post
,也可以按自己意愿命名。
Section -myapp
SetOutPath "$INSTDIR"
SetOverwrite ifnewer
WriteUninstaller "$INSTDIR\uninstall.exe"
File "..\myapp\MyApp.exe"
File "/oname=$INSTDIR\repair.file" "..\myapp\s_win_repair.file"
SetOutPath "$INSTDIR\bin"
File /r "..\myapp\bin*.*"
SetOutPath "$INSTDIR\resources"
File "/oname=uninstallerIcon.ico" "${MUI_ICON}"
SectionEnd
Section -Uninstall
Delete "$INSTDIR\uninstall.exe"
Delete "$INSTDIR\repair.file"
Delete "$INSTDIR\MyApp.exe"
RMDir /r "$INSTDIR\bin"
Delete "$INSTDIR\resources\uninstallerIcon.ico"
RMDir "$INSTDIR\resources"
RMDir "$INSTDIR"
SectionEnd
我们在安装时增加了SetOverwrite
命令(文档),这是一个编译器标记命令,它指示编译器后续代码中File指令的覆盖方式(如果存在),ifnewer 值表示当文件更新时覆盖安装。
🌟Tips: NSIS中常常会用用到一些标记类的命令,他们经常需要在后续代码需要什么标记时进行设置,可以在多处设置不同的值。有成对的出现的情况,在设定非常规标记值后,在代码块后再将其设置回来,可以根据需要灵活搭配。
安装中包含了File
的常见的几种用法:
用法 | 解析 |
---|---|
File "文件路径" | 安装指定文件到当前 SetOutPath 指定的目录下 |
File "/onam=安装全路径" "文件路径" | 安装指定文件到指定路径下,并重命名文件 |
File "/onam=文件名" "文件路径" | 安装指定文件到当前 SetOutPath 指定的目录下,并重命名文件 |
File /r "文件夹和通配符" | 递归安装指定文件夹内所有的文件到当前 SetOutPath 指定的目录下,会保持内部的文件夹层级结构 |
注意:File
指令中的文件路径可以是绝对路径、相对路径或者文件名。安装位置的的相对路径和文件名相对于当前SetOutPath
指定的目录,需要安装文件的相对路径和文件名相对于当前执行脚本的目录。更多详实信息查看文档
与安装相对应的的卸载过程,在无特殊需求的情况下,卸载则需要将安装的文件卸载干净,SectionUninstall
中包含了卸载时常用的指令:
用法 | 解析 |
---|---|
Delete "绝对文件路径" | 删除指定路径的文件 |
RMDir "绝对文件夹路径" | 删除指定文件夹,必须为空文件夹,不然无法删除并设置错误标记(不影响流程) |
RMDir /r "绝对文件夹路径" | 递归删除指定文件夹 |
一般的做法是,与安装的顺序对应,依次删除安装的文件,最后删除整个安装目录。
4.2 程序快捷方式
绝大部分的安装程序都会往桌面和开始菜单中生成安装程序的图标,这是我们的程序安装后便于使用的重要形式。
安装桌面快捷方式
我们先往Section myapp尾部中加入下列代码:
CreateShortCut "$DESKTOP${PRODUCT_NAME}.lnk" "$INSTDIR${PRODUCT_SHORT_NAME}.exe"
使用CreateShortCut
指令创建前面使用File
指令安装的可执行程序的快捷方式到桌面上。
- $DESKTOP 常量表示执行安装的计算机的系统桌面路径,虽然官方文档里叫它常量,但是实际上从用法可以看出来它是个变量🤣
- ${PRODUCT_NAME} 常量表示前面用户自定义的产品名
- ${PRODUCT_SHORT_NAME} 常量表示前面用户自定义的产品短名称
CreateShortCut 生成的桌面图标默认采用指向的可执行程序的图标。如果之前生成过同名快捷方式,而图标不同,那么生成的新桌面快捷方式图标可能不会更新,需要刷新一下桌面或者注销用户重新登录才可以。
安装开始菜单快捷方式
再往Section myapp尾部中加入下列代码:
IfFileExists "$SMPROGRAMS${PRODUCT_NAME}" +2 0
CreateDirectory "$SMPROGRAMS${PRODUCT_NAME}"
CreateShortCut "$SMPROGRAMS${PRODUCT_NAME}${PRODUCT_NAME}.lnk" "$INSTDIR${PRODUCT_SHORT_NAME}.exe"
同理,创建开始菜单快捷方式也是使用CreateShortCut
在系统的指定目录下创建可执行程序的快捷方式。
这里我们在$SMPROGRAMS
常量表示的开始菜单目录下使用CreateDirectory
创建了产品名称的目录,再在其中创建快捷方式。
这种在开始菜单中创建产品目录,然后再在其中放置快捷方式的方式是常规做法。也可以直接将快捷方式直接创建在开始菜单根目录中,这里自由度很高,可以根据需要自行选择。
🌟知识点🌟
这里通过IfFileExists
指令来判断路径是否已经存在,它是一个流程控制指令,用法是:
IfFileExists 要检测的文件或路径 文件或路径存在时跳转的位置 [文件或路径不存在时跳转的位置]
位置可以指定行偏移或者标记,行偏移使用 +
/-
数值或者0,正数值代表从本行起向后的第几行,负数值代表从本行起向前的第几行,0代表从本行起继续执行。
如下例判断记事本程序是否已经安装在操作系统目录下,存在从改行顺序执行弹出 MessageBox。不存在则向后记2行开始执行,跳过MessageBox。
IfFileExists $WINDIR\notepad.exe 0 +2
MessageBox MB_OK "记事本已安装"
指定标记则使用标记名称,直接跳转到标记的代码行开始执行,例如:
IfFileExists $WINDIR\notepad.exe 0 notExists
MessageBox MB_OK "记事本已安装"
notExists:
Tips: 这样的判断指令跳转的方式在NSIS还有一些,他们都是同样的使用方式,效果与Goto一致。
卸载快捷方式
与安装文件同理,安装了快捷方式后同时也要编写卸载过程。使用Delete与RMDir指令删除桌面和开始菜单中的快捷方式。
; delete Desktop icon
Delete "$DESKTOP${PRODUCT_NAME}.lnk"
; delete start menu folder
Delete "$SMPROGRAMS${PRODUCT_NAME}${PRODUCT_NAME}.lnk"
RMDir "$SMPROGRAMS${PRODUCT_NAME}"
4.3 注册安装程序
在Windows中,正规的软件在安装后,都可以在win7的控制面板或者win10的安装的应用内找到,供用户很方便的可以查看安装信息和卸载。
但,这并不是Windows自动检测到安装并记录的,而是一个君子协定,需要安装程序自觉进行注册后才能在已安装的列表内找到。(如果你不注册也没人来打你😜
代码如下:
!define PRODUCT_UNINSTALL_KEY "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall${PRODUCT_NAME}"
Section -myapp
// ...
; Register the installed software
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "DisplayName" "$(^Name)"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "InstallDir" "$INSTDIR"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "UninstallString" "$INSTDIR\uninstall.exe"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "DisplayIcon" "$INSTDIR\resources\uninstallerIcon.ico"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
SectionEnd
💡Tips: $(^Name) 用于获取
Name
命令设定的值
这里使用指令WriteRegStr
添加注册表字符串值,它是一个注册表指令。基础语法如下:
WriteRegStr rootkey subkey key_name value
eg.
WriteRegStr rootkey "SOFTWARE\example" "key1" "Hello World!"
💡 注册表的rootkey
可以为下列内容,他们分别记录着windows中的不同内容,常用HKLM为本机内容(一般用来记录为所有用户的信息),HKCU为当前用户。
- HKCR 或 HKEY_CLASSES_ROOT,前者为缩写,下同。
- HKLM 或 HKEY_LOCAL_MACHINE
- HKCU 或 HKEY_CURRENT_USER
- HKU 或 HKEY_USERS
- HKCC 或 HKEY_CURRENT_CONFIG
- HKDD 或 HKEY_DYN_DATA
- HKPD 或 HKEY_PERFORMANCE_DATA
- SHCTX 或 SHELL_CONTEXT
💡subkey
为具体注册表项相对于rootkey的全路径,下图中的路径框中的 SOFTWARE\BiliBili 就是subkey。
🌟Tips: 如果子项并不存在,WriteRegStr 会将其创建。
在 SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall{程序名} 这个注册表项下创建指定的 DisplayName
、InstallDir
、UninstallString
、DisplayIcon
、DisplayVersion
、URLInfoAbout
、Publisher
这些值告诉Windows我们安装的程序信息。
🌟 由于 windows 在注册表内区分了32位和64位,默认的所有路径都是32位的,64位是后面加的。所以,在64位电脑上,安装实际设置的注册表路径应该为:SOFTWARE**WOW6432Node**\Microsoft\Windows\CurrentVersion\Uninstall{程序名}
然后,我们就可以在安装的程序列表内看到已安装的程序信息。
类似的添加注册表值的指令还有:WriteINIStr、WriteRegBin、WriteRegDWORD、WriteRegExpandStr。更多注册表操作可以参考文档。
与其他安装过程同理,安装写入了注册表,那么就需要在卸载的时候清理干净。
Section -Uninstall
// ...
; delete reg item
DeleteRegKey HKLM "${PRODUCT_UNINSTALL_KEY}"
SectionEnd
4.4 卸载完成自动关闭
受国产软件的荼毒,很多人不喜欢卸载完成之后还要点击完成,总觉得会被装上莫名其妙的软件。如果你也不喜欢这样,那么请在卸载的内容里设置卸载完成自动关闭。
Section -Uninstall
// ...
; close after finish
SetAutoClose true
SectionEnd
5. 定制安装向导
本章代码在示例项目中对应install
项目的installer.nsi。
5.1 基础界面
NSIS提供了一系列的命令用来设置基础界面信息,以下列举最常用的,更多的见文档。
窗口标题
Caption
自定义安装程序的窗口标题。安装程序标题默认为 {Name命令指定的名称} Setup
,如果你不喜欢可以自己设置。
# base ui info
Caption "${PRODUCT_NAME} Installer"
底部分割线文本
BrandingText
设置安装窗口内容底部分割线上的文本。如果设置空字符串(""),则显示下图中默认值;如果不想在这里显示文本,设置只有一个空格的字符串" "
就行。
BrandingText /TRIMLEFT "文本内容"
eg.
BrandingText /TRIMLEFT "${PRODUCT_NAME} ${PRODUCT_VERSION}"
退出警告
如果你需要在用户点击关闭按钮退出安装程序时进行提示,那么就加入下面这行代码:
!define MUI_ABORTWARNING
弹出提示窗:
顶部右侧图标
安装过程界面的顶部右侧可以设置图标,默认为NSIS的logo。我们自己程序的安装包肯定是是替换它的,通过定义下列常量来实现:
!define MUI_HEADERIMAGE ; Defining this value is the basis for the follow two definitions
!define MUI_HEADERIMAGE_BITMAP ".\resource\header_150x57.bmp"
!define MUI_HEADERIMAGE_BITMAP_STRETCH NoStretchNoCropNoAlign
!define MUI_HEADERIMAGE_RIGHT ; Display to right
MUI_HEADERIMAGE
作为开关量,必须定义后后续的常量才会生效MUI_HEADERIMAGE_BITMAP
常量定义使用的图片,格式限定为(.bmp),图片区域尺寸是15057,图片最好是15057的倍数,不然就会被拉伸变形。MUI_HEADERIMAGE_RIGHT
常量定义图片显示为右侧
效果图如下:
5.2 欢迎界面
定义MUI_WELCOMEFINISHPAGE_BITMAP
常量的值为图片路径,格式限定为(.bmp)。图片区域尺寸为164*314,推片最好是区域尺寸的倍数,不然可能会被裁剪。
; Welcome page
!define MUI_WELCOMEFINISHPAGE_BITMAP ".\resource\welcome_install.bmp"
!insertmacro MUI_PAGE_WELCOME
💁Tips: 一般在使用库时,定义其指定常量要在宏的前面,因为宏内代码会使用到其相关常量。
效果图如下:
5.3 License界面
一般License界面我们定义用户需要勾选确认,而不是直接点击Agree就进入下一步了。在导入Lincese界面宏之前,定义MUI_LICENSEPAGE_CHECKBOX
常量,设定必须点击同意选项了之后才能继续进行下一步。
; License page
!define MUI_LICENSEPAGE_CHECKBOX
!insertmacro MUI_PAGE_LICENSE "..\myapp\license.txt"
效果图如下:
5.4 组件界面
组件界面在实际使用中不是一个非必须的界面,在有一些可选的内容需要用户选择性安装时才需要这个界面,选择安装列表显示所有可见的Section。反之,不需要的话就移除Components page 。
组件界面的文字信息很多,我们一般定义顶部的标题(MUI_PAGE_HEADER_TEXT
)和文本(MUI_COMPONENTSPAGE_TEXT_COMPLIST
),与选择框的标题文本(MUI_PAGE_HEADER_SUBTEXT
)。
; Components page
!define MUI_PAGE_HEADER_TEXT "Component Selection"
!define MUI_COMPONENTSPAGE_TEXT_COMPLIST "Select components to install:"
!define MUI_PAGE_HEADER_SUBTEXT "Select the components you want to install."
!insertmacro MUI_PAGE_COMPONENTS
效果图如下:
但是在实际安装包中,我们需要调整Component界面的布局。因为它默认的这个界面布局实在是利用率很低,无关信息和空白间距太多了。
5.4.1 调整界面布局
我们可以通过NSIS提供的功能将默认的界面改变成下图的的样子:
主要使用的是GDI设置方式,如果对GDI开发比较熟悉的读者将会很容易理解。下面将逐步介绍调整过程:
(1) 定义界面加载函数
通过GDI调整界面的布局,首先需要界面先完成显示。定义MUI_PAGE_CUSTOMFUNCTION_SHOW
常量为一个函数ComponentsPageShow
,我们将GDI的操作代码写入这个函数中,它将在组件界面显示后调用。
🌟重点注意🌟:
MUI_PAGE_CUSTOMFUNCTION_SHOW
是一个MUI定义的自定义函数宏,它不特殊针对某一界面,而是在其定义后插入的第一个MUI界面宏中生效。所以,想要为一个界面设定界面显示后调用的函数,就请将这个常量定义在该界面宏的前面。同类的几个函数常量还有MUI_PAGE_CUSTOMFUNCTION_PRE
、MUI_PAGE_CUSTOMFUNCTION_LEAVE
、MUI_PAGE_CUSTOMFUNCTION_DESTROYED
,详见官网"Page Custom Functions"章节 。
!define MUI_PAGE_CUSTOMFUNCTION_SHOW ComponentsPageShow
!insertmacro MUI_PAGE_COMPONENTS
; When components page show
Function ComponentsPageShow
FunctionEnd
💡知识点一:函数 类似于区段,常用于组织可复用的安装过程。函数只能在区段中被调用,跟区段一样区分安装函数与卸载函数,不能混用。
💡知识点二:函数与宏的差别 宏的含义是可复用的代码片段,可以在不同的位置插入进行编译,它是编译时的。函数是运行时的,而且运行时分为安装和卸载,这两种情况下的方法不能互相调用,所以代码的复用要通过宏来实现。Updated in 2024.11
(2) 隐藏头部区域的次级文本
使用FindWindow
指令获取安装窗口的句柄,#32770 是安装程序的窗口在windows中注册的类名。
使用GetDlgItem
指令获取指定控件的句柄,使用ShowWindow
指令设置指定句柄控件的显示/隐藏。
# Variables
Var MyApp.Header.SubText
Function ComponentsPageShow
FindWindow $0 "#32770" "" $HWNDPARENT
GetDlgItem $MyApp.Header.SubText $HWNDPARENT 1038 ; header area subtext
ShowWindow $MyApp.Header.SubText ${SW_HIDE} ; Hide header area subtext
FunctionEnd
💡句柄来源:可以在NSIS安装路径下的 "Contrib\Modern UI 2\Pages" 中的代码文件中找到每个界面上的UI控件句柄。
结果:
(3) 隐藏内容区域的主文本
# Variables
Var MyApp.Component.MainText
Function ComponentsPageShow
FindWindow $0 "#32770" "" $HWNDPARENT
GetDlgItem $MyApp.Component.MainText $0 1006 ; The main text of the content area
ShowWindow $MyApp.Component.MainText ${SW_HIDE} ; Hide the main text of the content area
FunctionEnd
结果:
(4) 隐藏安装类型选择文本和控件
安装类型选择默认下没有显示,但是界面上空出了它的位置,为了避免调整UI受到干扰,我们需要将其一并隐藏。
💡安装类型在额外进行配置了后才会出现(用的比较少),常见于驱动程序安装时选择完整安全、典型安装还是自定义安装。
# Variables
Var MyApp.Component.InstallationType
Var MyApp.Component.InstallationTypeText
Function ComponentsPageShow
FindWindow $0 "#32770" "" $HWNDPARENT
GetDlgItem $MyApp.Component.InstallationTypeText $0 1021 ; Title text for the installation type
GetDlgItem $MyApp.Component.InstallationType $0 1017 ; installation type
ShowWindow $MyApp.Component.InstallationTypeText ${SW_HIDE} ; Hide header text for installation types
ShowWindow $MyApp.Component.InstallationType ${SW_HIDE} ; Hide installation type
FunctionEnd
结果:
(5) 隐藏描述标题文本
组件描述区域显示鼠标指向的组件列表中组件的描述文本,描述标题文本表示环绕描述文本的一个框和框上的标题文本。
💡标题文本本质上是一个GroupBox,但是它和描述文本只是前后叠在了一起,并没有包含关系。所以,隐藏了描述文本并不影响描述文本的显示。
# Variables
Var MyApp.Component.DescriptionTitleText
Function ComponentsPageShow
FindWindow $0 "#32770" "" $HWNDPARENT
GetDlgItem $MyApp.Component.DescriptionTitleText $0 1042 ; description title text
ShowWindow $MyApp.Component.DescriptionTitleText ${SW_HIDE} ; Hide the title text of the description
FunctionEnd
结果:
(6) 调整组件列表标题和组件列表
我们将组件列表文本和组件列表移动到左上角,并调整尺寸大小。
使用NSIS自带的 System.dll 库的 Call 函数调用 Windows 系统函数 SetWindowPos 设置GDI控件的位置和尺寸。System::Call 函数的用法详见文档,SetWindowPos 函数的用法详见MSDN文档。
NSIS可以调用C标准库中的函数,语法为 [dll名]::[函数名]
💡重点注意:SetWindowPos 从第3位参数到第6位为 x, y, cx, cy,x,y 为控件的左侧像素坐标和上侧像素坐标,相对原点为当前区域的左上角。cx,cy 为控件的像素宽高。
# Variables
Var MyApp.Component.ComponentListTitleText
Var MyApp.Component.ComponentList
Function ComponentsPageShow
FindWindow $0 "#32770" "" $HWNDPARENT
GetDlgItem $MyApp.Component.ComponentListTitleText $0 1022 ; Component list title text
GetDlgItem $MyApp.Component.ComponentList $0 1032 ; component list
System::Call "User32::SetWindowPos(i $MyApp.Component.ComponentListTitleText, i 0, i 0, i 0, i 270, i 13, i 0)" ; Sets the position of the component list title text
System::Call "User32::SetWindowPos(i $MyApp.Component.ComponentList, i 0, i 0, i 18, i 270, i 176, i 0)" ; Set the position of the component list
FunctionEnd
结果:
(7) 移动磁盘占用文本和描述文本
我们将组件列表文本和组件列表移动到左上角,并调整尺寸大小。
# Variables
Var MyApp.Component.DescriptionText
Var MyApp.Component.DiskSizeText
Function ComponentsPageShow
FindWindow $0 "#32770" "" $HWNDPARENT
GetDlgItem $MyApp.Component.DiskSizeText $0 1023 ; required disk size text
GetDlgItem $MyApp.Component.DescriptionText $0 1043 ; The content text of the description
System::Call "User32::SetWindowPos(i $MyApp.Component.DescriptionText, i 0, i 285, i 16, i 164, i 176, i 0)" ; Sets the position of the content text of the description
System::Call "User32::SetWindowPos(i $MyApp.Component.DiskSizeText, i 0, i 0, i 210, i 0, i 0, i 1)" ; Sets the position of the disk size text
FunctionEnd
结果:
5.5 安装文件过程界面
在选择安装路径后就要进入安装文件过程,一般只配置安装过程的详细信息是否默认显示还是隐藏。ShowInstDetails
指令设置是否显示安装文件的详细信息,详见文档。
💡 ShowInstDetails 指令值并不表意,nevershow 实际效果为隐藏详细内容;hide 实际效果为默认收起详细内容显示,可以点击按钮展开;show 实际效果与 hide 相反。
ShowInstDetails hide
!insertmacro MUI_PAGE_INSTFILES
5.6 完成界面
完成界面默认没有什么操作,只能点击完成结束安装程序,但更多时候我们需要在安装完成后启动我们的应用。
5.6.1 用户勾选启动项
MUI 提供了两种方式实现这一目的,根据需要进行选择:
(1) 方式一
在完成界面宏前定义MUI_FINISHPAGE_RUN
常量,设置完成界面需要运行的可执行程序路径。
!define MUI_FINISHPAGE_RUN "$INSTDIR${PRODUCT_SHORT_NAME}.exe"
!insertmacro MUI_PAGE_FINISH
完成界面上就显示运行选项,默认为勾选状态,文本默认为常量 PRODUCT_NAME
+PRODUCT_VERSION
。
如果想要更换显示的文本可以定义常量MUI_FINISHPAGE_RUN_TEXT
设置。
!define MUI_FINISHPAGE_RUN_TEXT "NB My App!"
🚨 问题:这种方式是通过cmd运行的程序,默认的工作路径为cmd的程序路径。如果要运行的程序需要正确的工作路径的话,请使用方式二。
(2) 方式二
定义常量MUI_FINISHPAGE_RUN
设置空值,执行动作由常量MUI_FINISHPAGE_RUN_FUNCTION
指定的函数来执行。在回调函数FinishRun
中我们就可以执行任意代码,比如可以用SetOutPath
设置正确的工作区。
!define MUI_FINISHPAGE_RUN
!define MUI_FINISHPAGE_RUN_TEXT "NB My App!"
!define MUI_FINISHPAGE_RUN_FUNCTION FinishRun
!insertmacro MUI_PAGE_FINISH
Function FinishRun
SetOutPath "$INSTDIR"
ExecShell "" "$INSTDIR${PRODUCT_SHORT_NAME}.exe"
FunctionEnd
5.6.2 后台自动启动
安装完成自动启动,不给用户拒绝的机会,这样的方式稍微流氓,请审慎后使用!
在不定义MUI_FINISHPAGE_RU
系列常量的情况下,我们实现函数.onInstSuccess
,这个函数是固定命名,它将会在点击完成后运行。这样完成界面上就没有了运行选项,在每次安装完成后都会执行函数内容。
Function .onInstSuccess
Call FinishRun
FunctionEnd
🌟回调函数🌟
NSIS的有一些内置的回调函数(或者叫生命周期函数),这些函数名称固定,只要在脚本中定义了就会在安装进行到对应时间点时被执行。回调函数分为安装回调和卸载回调,分别对应安装和卸载的过程。查看文档了解有哪些回调函数和他们被调用的时机。
5.7 卸载界面
卸载过程与安装过程对应,定制的方式都一致,只是需要注意定义卸载界面的常量,和卸载函数由un.开头。关于卸载界面的详细信息请参考文档。
6. 安装组件
一个完整的软件不仅仅包含应用程序本身,还可能包含驱动、运行时、插件、用户工具、文档等等。通常并不是所有的都一股脑让用户装上占用用户的硬盘,所以针对不同的类容要采用不同的策略进行安装。
- 对于驱动、运行时这种作为应用程序运行基础必须安装的内容,我们将他们配置为与应用程序的安装相同的隐藏区段
- 对于插件、用户工具、文档等可选内容,我们可以将他们配置为常规区段。这样它们就会在组件界面上以组件的形式呈现出来。
⚠️ 注意:本章后续中的安装文件 driver、plugins 等,只是笔者为了保持示例项目目录干净放置到 myapp 目录下的。其实可以在脚本中随意指定电脑上的任何路径位置,不一定非要将他们放置到需要打包的程序目录下,按照自己的实际情况和喜好来即可。
本章代码在示例项目中对应install
项目的installer.nsi。
6.1 驱动和运行时的安装
驱动和运行时是高度相似内容,我们以驱动安装为例来进行描述。
Section -driver
SetOutPath "$INSTDIR"
SetOverwrite ifnewer
File /r "..\myapp\driver"
; run install
nsExec::Exec "$INSTDIR\driver\install.bat"
SectionEnd
Section -un.driver
; run uninstall
nsExec::Exec "$INSTDIR\driver\uninstall.bat"
RMDir /r "$INSTDIR\driver"
SectionEnd
在代码中将myapp\driver
中驱动安装到我们的安装目录下,在安装完成后运行安装脚本进行安装,你可以在安装脚本里编写安装命令,设置安装的参数,比如最常见的静默安装参数。
🌟 提示:一般一个内容的安装区段与卸载区段同时编写是一个好的习惯,不要只管装不管卸载,留下垃圾遍地。不过在安装驱动或者运行时时,你可以根据自己的需要选择不编写卸载区段。
你也可以将驱动放置到 $TEMP 临时目录下,安装完成后就将他删除。
🌟知识点🌟
这里我们用到了一个 nsis 的插件 nsExec 的函数 Exec 来执行批处理脚本。
先介绍一下 nsis 的插件系统,nsis 支持拓展插件,插件实际上是一个接口导出为 C 的动态链接库。目录在C:\Program Files (x86)\NSIS\Plugins
下,我们可以在里面找到已经内置的插件文件,比如 nsExec 插件的dll文件 nsExec.dll。还有可以到官方论坛里找到一些你需要的三方插件下载下来,按类型放进不同的nsis的安装目录,就可以通过 [插件名]::[函数名] [参数列表]
的方式进行调用了。更多详情请看 官方文档。
除了 nsExec
还可以使用 Exec
或 ExecWait
,这里使用 nsExec 的原因:
- nsExec 是同步执行等待结果返回,Exec 是执行后不等待。
- nsExec 可以静默执行,不弹出命令行窗口,ExecWait 执行要弹出命令行窗口。
- nsExec 可以捕获命令行的输出并返回。
💡 自由拓展:nsExec的完整用法请看 插件文档
6.2 插件的安装
本节用 常规 和 工程化 两个层次来介绍,常规 小节包含常见博客形式的内容,工程化 小节包含实际使用时的技巧。通过对比,帮助读者认识到在互联网常见的学习内容距离实际使用的距离,而这部分基本上只能靠自己去找资料多次试错来获得。
6.2.1 常规
我们先安装两个插件 plugin1 和 plugin2,plugin1 是一个单的dll文件,plugin2是一个插件目录。
Section plugin1
SetOutPath "$INSTDIR"
SetOverwrite ifnewer
File "..\myapp\plugins\plugin1.dll"
SectionEnd
Section plugin2
SetOutPath "$INSTDIR\plugins"
SetOverwrite ifnewer
File /r "..\myapp\plugins\plugin2"
SectionEnd
这次在插件的名称前面没有了短横线 -
,它们就会出现在组件安装界面,让用户自行决定是否安装。安装效果界面如下:
Section 默认情况下首选是要被安装的,我们可以通过指定 /o
参数默认不选。
Section /o plugin2
效果:
Section 默认情况下在组件选择框内是正常粗细的文字,我们可以在 Section 名称前加上 !
让其在界面上加粗显示。
Section !plugin1
效果:
编写了安装 Section,别忘记还有卸载 Section。
Section !un.plugin1
Delete "$INSTDIR\plugin1.dll"
SectionEnd
Section /o un.plugin2
RMDir /r "$INSTDIR\plugins\plugin2"
SectionEnd
卸载界面如下:
💡 提醒:卸载界面没有进行自定义配置,配置方式与安装界面一致,只需要在下载组件界面定义前设置组件界面显示的函数,并将自定义布局的脚本放到一个宏内,这样只需要在安装和卸载两个函数中,插入这个宏就可以了。
!define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ComponentsPageShow
!insertmacro MUI_UNPAGE_COMPONENTS
; When components page show
Function ComponentsPageShow
!insertmacro RelayoutComponents
FunctionEnd
Function un.ComponentsPageShow
!insertmacro RelayoutComponents
FunctionEnd
!macro RelayoutComponents
; custom components layout scripts
!macroend
效果图如下:
一般的常规介绍类文章到这里就结束了,但是本文不会。
6.2.2 工程化
通过前面的常规过程我们添加了插件的安装卸载过程,但是这往往只能达到一个玩具的程度。接下来,就让我们来让插件安装变成一个可用于正式产品的环节。
设置安装大小
因为插件安装是用户可选的,所以有时对磁盘空间的占用就会是一个实际的问题。总所周知,很多人至今还将所有常规软件装在D盘里,而不是分盘时增加C盘的空间到200G以上。
我们通过在每个安装的 Section 中设置其安装后要实际占用的磁盘空间大小,组件界面会自动帮你计算后告诉用户。
现在 Section myapp 的开头添加它的大小,单位是 KB。
Section -myapp
; set the section disk space requirement, unit is KB.
AddSize 1024
这里我们添加的是1024KB,组件界面会就把它算上并显示出来。
然后,我们在每个插件 Section 上都设置它们的大小。
Section !plugin1
AddSize 512
Section /o plugin2
AddSize 512
在组件界面你选择对应的组件之后,就会发现占用空间的大小就会被自动累计上。
同理,在卸载 Section 中设置大小,卸载界面上也会如此。但是,卸载界面的文本也是 Space required,我觉得不是很合适。修改这个文字,是支线任务,这里不做介绍了。
建议读者可以举一反三研究一下,阅读5.4.1小节知道这个label的句柄,然后去发现nsis修改文字的命令完成修改。
添加插件的文本描述
在实际安装过程中,拥有详细的文字描述可以有效帮助用户在是否安装的抉择时做出合适的判断。
这里需要先设置对应 Section 的 ID 常量名,其他逻辑内对该 Section 对应的组件选项的操作都要通过这个 ID 来进行。
Section !plugin1 SEC_PLUGIN1_ID
Section /o plugin2 SEC_PLUGIN2_ID
🌟 帮助理解:Section 的 ID 常量的值会在打包脚本编译时被分配为 Section 的序号值,如果你的 Section 顺序发生变化它在新做的安装包内的值就会不同,但是在一个已经制作完成的安装包内它的值不会发生改变,所以它是一个常量。
然后,加入下面的脚本内容:
⚠️ 注意:常量的使用必须要在常量定义后,所以 Section ID 的使用代码需要编写在 Section 定义的代码后面。
; Set components descriptions
!define DESCRIPTION_PLUGIN1 "Plugin1 is a highly efficient tool designed to optimize users' workflows. It offers a wide range of features, including data processing, quick analysis, and intelligent operation suggestions. Whether used in personal projects or team collaborations, Plugin1 significantly boosts productivity. With its simple and user-friendly interface, users can easily get started with minimal effort."
!define DESCRIPTION_PLUGIN2 "Plugin2 is dedicated to providing comprehensive multi-functional extension support, suitable for various scenarios. Its powerful modular design allows users to flexibly configure features according to their needs, enabling higher productivity and streamlined workflow management. Plugin2 combines stability with flexibility, making it the ideal choice for meeting professional demands."
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_PLUGIN1_ID} "${DESCRIPTION_PLUGIN1}"
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_PLUGIN2_ID} "${DESCRIPTION_PLUGIN2}"
!insertmacro MUI_FUNCTION_DESCRIPTION_END
鼠标移动到组件名称上,右侧就会显示我们设置的介绍内容。效果如下:
这样我们就实现了添加插件的文本描述,但是别忘了,还有卸载界面。
首先,我们来设置卸载 Section 的 ID,你应该看起来理所应当是不,那是因为你理解了 ID 的原理。
⚠️⚠️⚠️ 这里有个非常需要注意的问题 ⚠️⚠️⚠️
往往我们在参考网络上其他人写的脚本文件或文章内容时,经常会看到一个现象,对卸载区段的操作里使用的是安装区段的 ID。所以,一般学到这种你就会陷入困惑,要么看不懂要么理解错。
这种写法能工作的原因是因为 ID 是Section在安装包中的序号,并不是一个唯一值。很多人在编写时 安装和卸载 Section 是成对编写的,所以只设置了安装 Section 的 id,其值刚好与它对应的卸载 Section 相同。这只是一个成对出现的话刚好序号一致的巧合而已,一旦你打乱了顺序加入和不对称的 Section,这个脆弱的巧合就会马上坏掉,导致对卸载 Section 的操作出现错乱。
所以,请不要沾染黑魔法,卸载 Section 也应该单独设置 ID。
Section !un.plugin1 SEC_UN_PLUGIN1_ID
Section /o un.plugin2 SEC_UN_PLUGIN2_ID
!insertmacro MUI_UNFUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_UN_PLUGIN1_ID} "${DESCRIPTION_PLUGIN1}"
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_UN_PLUGIN2_ID} "${DESCRIPTION_PLUGIN2}"
!insertmacro MUI_UNFUNCTION_DESCRIPTION_END
安装状态管理
如果你按照前面的操作走到这一步,一定已经发现了两个问题:
- 安装过 plugin1 后,再次安装我根本无法知道它有没有被安装。
- 即使没有安装 plugin2 卸载时还是能卸载它。
这个章节就是来解决这个问题:
首先,我们解决安装记录的问题。
插件安装后,我们可以记录下来它的安装状态。一般我们会将这个状态放在注册表内的软件信息下:
Var MyApp.InstalledPluginsCode
Function .onInit
; Read the current installed plugins code
ReadRegDWORD $MyApp.InstalledPluginsCode HKLM "${PRODUCT_UNINSTALL_KEY}" "InstalledPlugins"
IfErrors 0 +2
StrCpy $MyApp.InstalledPluginsCode 0 ; init to zero
FunctionEnd
!define PLUGIN1_CODE 1
!define PLUGIN2_CODE 2
Section !plugin1 SEC_PLUGIN1_ID
; ...
IntOp $MyApp.InstalledPluginsCode $MyApp.InstalledPluginsCode | ${PLUGIN1_CODE}
WriteRegDWORD HKLM "${PRODUCT_UNINSTALL_KEY}" "InstalledPlugins" $MyApp.InstalledPluginsCode
SectionEnd
Section /o plugin2 SEC_PLUGIN2_ID
; ...
IntOp $MyApp.InstalledPluginsCode $MyApp.InstalledPluginsCode | ${PLUGIN2_CODE}
WriteRegDWORD HKLM "${PRODUCT_UNINSTALL_KEY}" "InstalledPlugins" $MyApp.InstalledPluginsCode
SectionEnd
声明变量 MyApp.InstalledPluginsCode 来记录当前的插件编码,在安装的初始化函数中读取注册表中的值 InstalledPlugins。
💡知识点:IfErrors [有错误的跳转行数] [无错误的跳转行数] 用来判断前一条指令执行是否成功。第一个参数0代表如果未成功则执行 IfErrors 内的代码,第二个参数 +2 代表如果成功则向下数两行开始执行,因为 IfErrors 内只有一行代码,意味着跳过。
💡知识点:StrCpy [要设置值的变量] [值] 用来给变量赋值,虽然看名称好像是字符串Copy指令,但是实际上它可以为任何基础类型赋值。
我们用这个值来存储当前已安装的插件,这个值是一个 DWORD 值,可以认为它是一个 Int,在32位电脑上可以表示32位二进制,在64位电脑上可以表示64位二进制值。我们采用它的每一位二进制值表示一个插件,1 表示已安装,0 表示未安装。
所以,PLUGIN1_CODE 定义为 1 对应的二进制值就是 0001,PLUGIN2_CODE 定义为 2 对应的二进制值就是 0010。
我们在安装时只需要将当前安装插件的编码与已经安装的编码按位进行或计算,将计算结果写入注册表。
IntOp $MyApp.InstalledPluginsCode $MyApp.InstalledPluginsCode | ${PLUGIN1_CODE}
WriteRegDWORD HKLM "${PRODUCT_UNINSTALL_KEY}" "InstalledPlugins" $MyApp.InstalledPluginsCode
💡 知识点:IntOp 是一个数字运算计算指令,第一个参数是输出值,第二个参数是计算值1,第三个参数是操作符,第四个参数是计算值2。这里用来计算两个整型的或运算,详见 文档。
当只安装 plugin1 时注册表的值:
这时,我们就可以读取出插件的安装状态,将已安装的插件标记出来。
!define MUI_CUSTOMFUNCTION_GUIINIT GUIInit
!macro componentInstalled sectionID code name
Push $0
IntOp $0 $MyApp.InstalledPluginsCode & ${code}
${if} $0 == ${code}
SectionSetFlags ${sectionID} 0 ; uncheck section
SectionSetText ${sectionID} "${name} (Installed)" ; append text 'Installed'
${endif}
Pop $0
!macroend
; On UI init
Function GUIInit
!insertmacro componentInstalled ${SEC_PLUGIN1_ID} ${PLUGIN1_CODE} "plugin1"
!insertmacro componentInstalled ${SEC_PLUGIN2_ID} ${PLUGIN2_CODE} "plugin2"
FunctionEnd
💡 提示:常量 MUI_CUSTOMFUNCTION_GUIINIT 需要定义在任何MUI的界面定义之前,不然可能会报错
💡 提示:常量需要在定义的代码后再使用,不然会报错找不到。
效果如下:
因为有两个插件,所以我将同样的逻辑过程实现为一个带参数的宏 componentInstalled ,通过两次调用避免编写大量重复代码。实现带参数的宏很简单,只需要在宏名称后跟上参数的名称用空格隔开即可。使用时只需要按照参数顺序依次传入值即可。
🌟 知识点:条件判断 🌟
宏 componentInstalled 内使用到了条件判断 {endif},条件判断功能包含在 LogicLib.nsh
中,详见 逻辑结构。写法如下:
a. 单分支判断
!include LogicLib.nsh ;不包含好像也不会报错,建议加上
${if} condition
; ...
${endif}
b. 两个分支判断
${if} condition
; ...
${else}
; ...
${endif}
c. 多个分支判断
${if} condition
; ...
${elseif} condition
; ...
${endif}
d. 多个判断条件
${if} condition1
${AndIf} condition2
; ...
${endif}
c. 嵌套分支判断
${if} condition
${if} ${Edition} == "Community"
; ...
${elseif} ${Edition} == "Business"
; ...
${endif}
${elseif} condition
; ...
${endif}
然后,我们来实现卸载时检查已安装的插件。
卸载程序初始化时我们从注册表读取已安装的插件 code,在卸载界面初始化时将未安装的 section 名称设置为空值,它将不会被显示在组件列表内。
!define MUI_CUSTOMFUNCTION_UNGUIINIT un.UNGUIInit
Function un.onInit
; Read the current installed plugins code
ReadRegDWORD $MyApp.InstalledPluginsCode HKLM "${PRODUCT_UNINSTALL_KEY}" "InstalledPlugins"
IfErrors 0 +2
StrCpy $MyApp.InstalledPluginsCode 0 ; init to zero
FunctionEnd
!macro uncomponentInstalled sectionID code
Push $0
IntOp $0 $MyApp.InstalledPluginsCode & ${code}
${if} $0 != ${code}
SectionSetText ${sectionID} "" ; set white name to disapear section
${endif}
Pop $0
!macroend
Function un.UNGUIInit
!insertmacro uncomponentInstalled ${SEC_UN_PLUGIN1_ID} ${PLUGIN1_CODE}
!insertmacro uncomponentInstalled ${SEC_UN_PLUGIN2_ID} ${PLUGIN2_CODE}
FunctionEnd
💡 提示:常量 MUI_CUSTOMFUNCTION_UNGUIINIT 需要定义在任何MUI的界面定义之前,不然可能会报错
6.3 工具的安装
我们安装一个MySQL作为工具的示例,通过工作组将MySQL包括起来。
!define MYSQL_CODE 4
SectionGroup /e "Tools"
Section "MySQL" SEC_MYSQL_ID
AddSize 56265
SetOutPath "$LocalAppdata${PRODUCT_SHORT_NAME}\mysql"
SetOverwrite ifnewer
SetCompress off
DetailPrint "Install MySQL..."
SetDetailsPrint listonly
File "/oname=$PLUGINSDIR\mysql.7z" "..\resource\mysql.7z"
SetCompress auto
SetDetailsPrint both
Nsis7z::ExtractWithDetails "$PLUGINSDIR\mysql.7z" "Installing MySQL %s..."
IntOp $MyApp.InstalledPluginsCode $MyApp.InstalledPluginsCode | ${MYSQL_CODE}
WriteRegDWORD HKLM "${PRODUCT_UNINSTALL_KEY}" "InstalledPlugins" $MyApp.InstalledPluginsCode
; Do more.. add envriment, start server, etc.
SectionEnd
SectionGroupEnd
Function GUIInit
!insertmacro componentInstalled ${SEC_MYSQL_ID} ${MYSQL_CODE} "MySQL"
FunctionEnd
SectionGroup /e "un.Tools"
Section /o un.MySQL SEC_UN_MYSQL_ID
; Do more... remove envriment, stop server, etc.
RMDir /r "$LocalAppdata${PRODUCT_SHORT_NAME}\mysql"
SectionEnd
SectionGroupEnd
Function un.UNGUIInit
!insertmacro uncomponentInstalled ${SEC_UN_MYSQL_ID} ${MYSQL_CODE}
FunctionEnd
!define DESCRIPTION_MYSQL "This software utilizes a MySQL database to ensure reliable and efficient data management. With robust performance, scalability, and security, MySQL serves as the backbone for storing, retrieving, and processing application data, enabling seamless and stable operations."
!insertmacro MUI_UNFUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_UN_MYSQL_ID} "${DESCRIPTION_MYSQL}"
!insertmacro MUI_UNFUNCTION_DESCRIPTION_END
效果:
代码中的 SectionGroup 和 SectionGroupEnd 围起来的 Section 将会被分到一个组中,参数 /e
设置默认展开改组,去掉它默认不展开该分组。
这里我们将MySQL的目录提前压缩成了 mysql.7z,这里是一个新的用法。
🌟🌟 一个新的用法:打包预压缩文件 🌟🌟
之前我们都是直接引用要安装的原始文件,但是有时我们需要安装的软件内容文件夹很多,层级很深,小文件很多,如数据库、python这种。如果直接 File /r
这样打入安装包存在几个问题:
- 因为需要压缩大量文件,打包的时间稍长
- 压缩率不高导致安装包较大
- 安装时解压速度较慢
这时,我们将需要安装的大量小文件提前压缩就可以极大的环节上述几个问题。
所以,代码中使用 SetCompress off 指令暂时关闭了安装包对文件的压缩,直接包含 mysql.7z。DetailPrint 指令设置安装当前 Section 过程中界面显示的文本。SetDetailsPrint listonly 指令暂时关闭安装详细输出。
因为压缩包安装时要解压,所以 File 指令使用了 /oname
参数指定将 mysql.7z 安装时放置到零时的插件目录下,然后再用 Nsis7z 插件将其解压。
Nsis7z 插件是一个三方插件,可以从 Nsis7z插件地址 下载最新版 19.0.0,文档也在这个地址内,下载后将压缩包内的文件复制到NSIS的安装目录中就完成了安装。下面是示意图:
💡 笔者在网上找到 Nsis7z 目前有其他人编的 24.05 版本,速度更快,支持的文件更大,可解压压缩率更高的包。但是是未知来源无法判断安全性,所以不做推荐。
6.4 文档安装
现在我们安装的内容越来越多,组件列表也组件多起来。这种情况下,我们可以设置一些安装类型,供用户快速选择。
所以,首先我们先修改 RelayoutComponents 宏,将组件界面隐藏了的类型选择框放出来:
!macro RelayoutComponents
FindWindow $0 "#32770" "" $HWNDPARENT
GetDlgItem $MyApp.Header.SubText $HWNDPARENT 1038 ; header area subtext
ShowWindow $MyApp.Header.SubText ${SW_HIDE} ; Hide header area subtext
GetDlgItem $MyApp.Component.MainText $0 1006 ; The main text of the content area
ShowWindow $MyApp.Component.MainText ${SW_HIDE} ; Hide the main text of the content area
GetDlgItem $MyApp.Component.InstallationTypeText $0 1021 ; Title text for the installation type
GetDlgItem $MyApp.Component.InstallationType $0 1017 ; installation type
ShowWindow $MyApp.Component.InstallationTypeText ${SW_HIDE} ; Hide header text for installation types
; ShowWindow $MyApp.Component.InstallationType ${SW_HIDE} ; Hide installation type
GetDlgItem $MyApp.Component.DescriptionTitleText $0 1042 ; description title text
ShowWindow $MyApp.Component.DescriptionTitleText ${SW_HIDE} ; Hide the title text of the description
GetDlgItem $MyApp.Component.ComponentListTitleText $0 1022 ; Component list title text
GetDlgItem $MyApp.Component.ComponentList $0 1032 ; component list
GetDlgItem $MyApp.Component.DiskSizeText $0 1023 ; required disk size text
GetDlgItem $MyApp.Component.DescriptionText $0 1043 ; The content text of the description
; # MSDN https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos
System::Call "User32::SetWindowPos(i $MyApp.Component.ComponentListTitleText, i 0, i 0, i 6, i 140, i 13, i 0)" ; Sets the position of the component list title text
System::Call "User32::SetWindowPos(i $MyApp.Component.ComponentList, i 0, i 0, i 24, i 270, i 176, i 0)" ; Set the position of the component list
System::Call "User32::SetWindowPos(i $MyApp.Component.DescriptionText, i 0, i 285, i 20, i 164, i 176, i 0)" ; Sets the position of the content text of the description
System::Call "User32::SetWindowPos(i $MyApp.Component.DiskSizeText, i 0, i 0, i 210, i 0, i 0, i 1)" ; Sets the position of the disk size text
; System::Call "User32::SetWindowPos(i $MyApp.Component.InstallationTypeText, i 0, i 135, i 0, i 180, i 13, i 0)"
System::Call "User32::SetWindowPos(i $MyApp.Component.InstallationType, i 0, i 150, i 0, i 120, i 13, i 0)"
!macroend
效果如图:
然后,添加文档分组:
💡提示:这里笔者为了简化内容,就没有编写卸载,已安装判断等脚本。
InstType "Typical" ; type 1
InstType "Full" ; type 2
Section !plugin1 SEC_PLUGIN1_ID
SectionIn 1 2
; ...
SectionEnd
Section /o plugin2 SEC_PLUGIN2_ID
SectionIn 2
; ...
SectionEnd
Section "MySQL" SEC_MYSQL_ID
SectionIn 1 2
; ...
SectionEnd
SectionGroup /e "Docs"
Section /o "User Manual"
SectionIn 1 2
AddSize 12000
SetOutPath "$DOCUMENTS${PRODUCT_SHORT_NAME}"
SetOverwrite ifnewer
File "..\resource\docs\User Manual.doc"
SectionEnd
Section /o "Dec Docs"
SectionIn 2
AddSize 25000
SetOutPath "$DOCUMENTS${PRODUCT_SHORT_NAME}"
SetOverwrite ifnewer
File /r "..\resource\docs\Dev Docs"
SectionEnd
SectionGroupEnd
通过 InstType 指令设置类型名,类型编号按照设置顺序从1开始依次排序。
通过 SectionIn 指令设置当前 Section 从属与哪个类型,可以设置多个。当选中该类型时,其对应的 Section 都会被选中。
6.5 空安装
有时(无用的一节)可能会遇到特殊(奇怪)的需求,需要在组件安装列表中列出一些没有实际安装内容的项。
我们通过添加一个默认补选中的空 Section,并在GUI初始化时将它设置为 ${SF_RO}
不可用。
SectionGroup /e "Future"
Section /o "F1" SEC_F1_ID
SectionEnd
SectionGroupEnd
Function GUIInit
; ...
; Futures
SectionSetFlags ${SEC_F1_ID} ${SF_RO}
FunctionEnd
!define DESCRIPTION_F1 "This component provides a powerful solution for managing user authentication and authorization with advanced security measures. It supports multiple authentication methods, including OAuth, and ensures secure data access. With customizable configurations and a user-friendly interface, it simplifies the implementation of security protocols, making it suitable for modern web and mobile applications."
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
; ...
; Futures
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_F1_ID} "${DESCRIPTION_F1}"
!insertmacro MUI_FUNCTION_DESCRIPTION_END
7. 一些常用的语法和特性
7.1 代码换行
在 NSIS 脚本里每一行都作为一个命令处理, 如果这一行太长的话你可以使用 “\” 来分隔,编译器会自动地把下一行接到上一行来作为完整的一行,而不是看作新的行。例如:
Messagebox MB_OK|MB_ICONINFORMATION \
"本示例演示了在 NSIS 脚本里如何对长的命令进行断行处理"
7.2 字符串里需要使用双引号
- 字符串转义
"此操作将删除$"a.txt$",本操作不可逆"
2. 使用其他字符串引号
'此操作将删除"a.txt",本操作不可逆'
`此操作将删除"a.txt",本操作不可逆`
建议使用字符串引号方式,简化代码,避免阅读不解。
笔者注:入门内容规划的文章主题内容已基本完成,后续更新可能只是细节修改和局部内容完善。