Files
Scoreboard/console/installer/installer.nsi
T
AskaEth 67eadd7fe6 fix(installer): 真机四条 UI 问题 + 提权改为 highestAvailable
主人真机实测报出四个问题,全部只有在真 Windows 上才暴露(wine 渲染/移动循环/UAC/插件卸载时序都不同):

1. **窗口拖不动**:原来把移动交给 DefWindowProc 的移动循环(ReleaseCapture + WM_NCLBUTTONDOWN HTCAPTION),
   我们摘掉了 WS_CAPTION,真机上那个循环不启动。改成自己算位移:按下时记锚点(GetCursorPos + GetWindowRect),
   之后每个 tick 用 SetWindowPos 跟光标走。注意起拖后不能因为光标甩出标题栏就停(否则变成「拖一下就断」)。
2. **原生按钮透出**:内层对话框只「盖住」了原生控件,鼠标划过时按钮自己重绘就露出来。现在每页显式隐藏
   11 个原生 ID(Next/Cancel/Back + 品牌字 + 页眉),并断言 IsWindowVisible 为假;隐藏后 BM_CLICK 翻页仍有效。
3. **P3 点下一步闪一下没反应**:不是目录校验,是提权失败分支末尾的 Abort(默认选中「为所有用户安装」时
   真机必然走到)。现在不再 Abort,降级为按用户安装并弹一句说明后继续。
4. **退出时弹 StdUtils::TimerCreate「Plug-in unloaded before all timers were destroyed」**:
   定时器从来没销毁过。加哨兵 $timerOn,在 .onGUIEnd 与 MUI_CUSTOMFUNCTION_ABORT 两条退出路径上幂等销毁;
   定时器全程只建一次(翻页不叠加)。对照实验:把销毁段掏空即复现该报错窗口。

**提权策略**(主人拍板「一启动就提权」):RequestExecutionLevel highest。
原先没写这条指令时 NSIS 默认是 requireAdministrator —— 受限账户机器上根本起不来;highest 在管理员机器上
行为与 admin 完全一致(启动即 UAC、全程提权、防火墙必然放行),但给受限账户留了降级路径。
同时去掉 P3 的中途 UAC_RunElevated(那正是「闪一下没反应」的来源)。

验证(wine):五页走完 + 点完成/点 ✕ 退出均无 StdUtils 报错;原生按钮 vis=000;拖动位移精确;
/S /currentuser /D=... 与 /S /D=...(非提权时降级按用户)都 exit 0 且写出真卸载器。
UAC 本身与四条观感须真机复验。
2026-09-13 16:49:20 +08:00

1569 lines
62 KiB
NSIS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
; =============================================================================
; 赛事积分榜 安装器 —— 完全自绘的五页向导
;
; 这是 electron-builder 的「自定义 installer 脚本」(package.json 的 build.nsis.script)。
; 只换 UI 层:安装/卸载/升级/快捷方式/注册表/防火墙全部复用 electron-builder 模板里的宏,
; 一个字节的安装逻辑都不重写 —— 现场装机成功率不被这次改版动摇。
;
; 为什么整窗去边框、铺满位图:
; 设计要求是 760×480 的固定版面(左侧品牌栏 / 顶部标题栏 / 右侧内容区 / 底部按钮条)。
; 系统标题栏和原生按钮条在不同 Windows 版本上高度都不一样,要对齐只能自己画。
; 所以:去掉窗口边框 → 内层对话框铺满整个客户区 → 位图当画布 → 标准按钮条被盖住,
; 但仍然是「程序化翻页」的工具(点我们的按钮 = 点被盖住的系统按钮)。
;
; 页面流程:P1 欢迎 → P2 许可协议 → P3 安装位置与安装模式 → P4 正在安装 → P5 完成。
; P1/P2/P3/P5 是 Page custom(nsDialogs 自建内层对话框,POC 已验证);
; P4 必须是**真正的** instfiles 页,原因有两个,都不是偏好问题:
; 1. 安装段(Section)只在 instfiles 页上执行。不用 instfiles 页就得把整段安装逻辑塞进
; 一个函数,静默安装、取消中断、进度刷新全得自己重写 —— 那才是真正的风险源;
; 2. WriteUninstaller / 卸载段的执行时机也依赖这条路径。
; 代价是 instfiles 页自带的内层对话框(原生进度条 + 详情列表)只能藏起来,
; 在上面再盖一层我们自己的对话框。
;
; 高 DPI(现场是 4K@200%):
; ManifestDPIAware true + 运行时读 LOGPIXELSX,所有坐标 = 1x 设计值 × dpi / 96,
; 一个绝对像素值都不许写死在控件上;素材出 1x/2x 两套,dpi ≥ 144 用 2x,
; 并且给 LoadImage 传目标 cx/cy,让系统在 125%/150% 这类中间档位上兜底缩放。
; 字号不加倍:NSIS 的 CreateFont 自己会按系统 DPI 把点数换成像素,再乘一次就双倍了。
; =============================================================================
; ── 素材位置 ────────────────────────────────────────────────────────────────
; 走 ${PROJECT_DIR}(electron-builder 注入的绝对路径)而不是相对路径:
; electron-builder 是把脚本喂给 makensis 的 stdin、并把 makensis 的工作目录设成它的模板目录,
; 相对路径落点不确定。
!define ART_SRC "${PROJECT_DIR}\installer\art"
; ── 版面常量 ────────────────────────────────────────────────────────────────
; 全部是 art-gen/gen_art.py 里的 1x 设计值,改素材就要回来改这里(两边必须一致)。
!define WIN_W 760
!define WIN_H 480
!define TB_H 40
!define SIDE_W 240
!define BRAND_H 440
!define PG_X 240
!define PG_Y 40
!define PG_W 520
!define PG_H 440
!define PAD_L 32
!define CONTENT_W 456
!define BTN_W 108
!define BTN_H 36
!define BTN_Y 416
!define BTN_PRIMARY_X 620
!define BTN_SECONDARY_X 500
!define BROWSE_W 96
!define BROWSE_H 34
!define TB_MIN_X 672
!define TB_CLOSE_X 716
!define TB_BTN_W 44
; 页面内控件 → 窗口坐标 = 页内坐标 + (PG_X, PG_Y),这里直接写成窗口坐标
!define LIC_EDIT_X 273
!define LIC_EDIT_Y 237
!define LIC_EDIT_H 174
!define LIC_EDIT_W 454
!define LIC_CHK_X 272
!define LIC_CHK_Y 432
!define PATH_EDIT_X 273
!define PATH_EDIT_Y 151
!define PATH_EDIT_W 357
!define PATH_EDIT_H 32
!define BROWSE_X 632
!define BROWSE_Y 150
!define RADIO1_X 272
!define RADIO1_Y 260
!define RADIO2_X 272
!define RADIO2_Y 300
!define SIZE_TXT_Y 336
!define STAGE_X 272
!define STAGE_Y 156
!define STAGE_W 456
!define STAGE_H 18
!define PROG_X 272
!define PROG_Y 180
!define PROG_W 456
!define PROG_H 8
!define FIN_CHK_X 272
!define FIN_CHK_Y 338
!define FIN_PATH_Y 376
; 颜色取自 gen_art.py 的配色常量(SetCtlColors 收 RRGGBB,不是 BGR)
!define CLR_TXT "0x16233a"
!define CLR_DIM "0x5a6b85"
!define CLR_BG "0xf6f9fd"
; ── 变量 ───────────────────────────────────────────────────────────────────
; electron-builder 的模板(assistedInstaller / installSection)引用了这些变量,
; 平时由 installer.nsi 主模板声明;我们用自定义脚本,得自己声明,名字必须一致。
Var newStartMenuLink
Var oldStartMenuLink
Var newDesktopLink
Var oldDesktopLink
Var oldShortcutName
Var oldMenuDirectory
Var appExe
Var launchLink
; 自绘界面状态
Var hPage ; 当前页的内层对话框
Var hBg
Var hBrand
Var hTitleBar
Var hMin
Var hClose
Var hNext
Var hBack
Var hCancel
Var hBrowse
Var hFinish
Var hCheck
Var hRadio1
Var hRadio2
Var hPath
Var hLicEdit
Var hStage
Var hProg
Var stMin
Var stClose
Var stNext
Var stBack
Var stCancel
Var stBrowse
Var stFinish
Var uiPage ; 0=无页面,1..5=当前页;定时器靠它判断还能碰哪些控件
Var timerId
Var timerOn ; 定时器是否已经创建过(销毁时只认这个哨兵,不靠 id 猜)
Var gDpi ; 系统 DPI(LOGPIXELSX)
Var gArt ; 本次要用的素材目录($PLUGINSDIR\1x 或 \2x)
Var gCx
Var gCy
Var gBw ; Track 宏把基准宽高放这里,转交给 SetBmp
Var gBh
Var tmpName
Var tmpPath
Var tmpTxt
Var /GLOBAL IsPowerShellAvailable
Var artReady
Var licOk ; 许可页勾选
Var runApp ; 完成页「立即运行」
Var modeAll ; 安装模式单选:1=为所有用户,0=仅为我
Var instStarted ; 安装流程是否已经跑起来(一次性,见 DoInstall 开头的守卫)
Var gIsAdmin ; CheckAdmin 的结果:1 = 当前进程已提权
Var dragCx ; 拖动锚点:按下瞬间的光标屏幕坐标
Var dragCy
Var dragWx ; 拖动锚点:按下瞬间的窗口左上角
Var dragWy
Var gDparam ; 命令行 /D= 指定的安装目录(要在 customInit 的记忆目录之后盖回去)
Var keepShortcuts ; 快捷方式保留策略,由安装流程写定(上游在安装段里声明,我们提前到全局)
Var dragging
; ── include ────────────────────────────────────────────────────────────────
; MUI2 不是为了它的页面,是为了 LangString / MUI_LANGUAGE:
; electron-builder 注入的 messages.nsh(以及 assistedMessages.nsh)用的是 MUI 的 LangString,
; 没有 MUI2 直接编译不过。我们的页面全是 Page custom,一个 MUI_PAGE_* 都不插。
!define MUI_DISABLE_INSERT_LANGUAGE_AFTER_PAGES_WARNING
!include "MUI2.nsh"
!include "nsDialogs.nsh"
!include "WinMessages.nsh"
!include "LogicLib.nsh"
!include "FileFunc.nsh"
!include "StrContains.nsh"
!include "common.nsh"
!include "multiUser.nsh"
!include "allowOnlyOneInstallerInstance.nsh"
!include "installUtil.nsh"
!include "installer.nsh"
; 安装模式初值逻辑(照抄 templates/nsis/assistedInstaller.nsh 的 initMultiUser):
; 上次怎么装的还怎么装;命令行 --allusers / --currentuser 压过它;
; 都没有时看 INSTALL_MODE_PER_ALL_USERS_DEFAULT(= package.json 的 selectPerMachineByDefault)。
; 我们不 include assistedInstaller.nsh(它会插一堆 MUI 原生页面),所以这个宏得自己带一份,
; 否则 uninstaller.nsh 的 un.onInit 里那句 !insertmacro initMultiUser 直接找不到宏。
!macro initMultiUser
!ifdef INSTALL_MODE_PER_ALL_USERS
!insertmacro setInstallModePerAllUsers
!else
; 静默安装跳过「内层进程 / 非管理员」这道判断:它俩走 UAC 插件的 UAC::_,
; 而静默模式下没有页窗口,插件会在这一句上不返回(本机 wine 实测卡死 60 秒)。
; 静默安装要提权时由安装流程开头那段($installMode == "all" + ${Silent})自己处理。
${IfNot} ${Silent}
${If} ${UAC_IsInnerInstance}
${AndIfNot} ${UAC_IsAdmin}
SetErrorLevel 0x666666
Quit
${EndIf}
${EndIf}
!ifndef MULTIUSER_INIT_TEXT_ADMINREQUIRED
!define MULTIUSER_INIT_TEXT_ADMINREQUIRED "$(^Caption) requires administrator privileges."
!endif
!ifndef MULTIUSER_INIT_TEXT_POWERREQUIRED
!define MULTIUSER_INIT_TEXT_POWERREQUIRED "$(^Caption) requires at least Power User privileges."
!endif
!ifndef MULTIUSER_INIT_TEXT_ALLUSERSNOTPOSSIBLE
!define MULTIUSER_INIT_TEXT_ALLUSERSNOTPOSSIBLE "Your user account does not have sufficient privileges to install $(^Name) for all users of this computer."
!endif
StrCpy $hasPerMachineInstallation "0"
StrCpy $hasPerUserInstallation "0"
ReadRegStr $perMachineInstallationFolder HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation
${if} $perMachineInstallationFolder != ""
StrCpy $hasPerMachineInstallation "1"
${endif}
ReadRegStr $perUserInstallationFolder HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation
${if} $perUserInstallationFolder != ""
StrCpy $hasPerUserInstallation "1"
${endif}
${GetParameters} $R0
${GetOptions} $R0 "/allusers" $R1
${IfNot} ${Errors}
StrCpy $hasPerMachineInstallation "1"
StrCpy $hasPerUserInstallation "0"
${EndIf}
${GetOptions} $R0 "/currentuser" $R1
${IfNot} ${Errors}
StrCpy $hasPerMachineInstallation "0"
StrCpy $hasPerUserInstallation "1"
${EndIf}
${if} $hasPerUserInstallation == "1"
${andif} $hasPerMachineInstallation == "0"
!insertmacro setInstallModePerUser
${elseif} $hasPerUserInstallation == "0"
${andif} $hasPerMachineInstallation == "1"
!insertmacro setInstallModePerAllUsers
${else}
!ifdef INSTALL_MODE_PER_ALL_USERS
!insertmacro setInstallModePerAllUsers
!else
!ifdef INSTALL_MODE_PER_ALL_USERS_DEFAULT
!insertmacro setInstallModePerAllUsers
!else
!insertmacro setInstallModePerUser
!endif
!endif
${endif}
!endif
!macroend
; ── 执行级别 ───────────────────────────────────────────────────────────────
; highest(= 清单里的 highestAvailable),不是硬 admin:
; 管理员账户的机器:一启动就弹 UAC、之后**全程**是提权态 —— 不再有「装到一半才提权」
; 那种闪退/卡住,防火墙规则也必然放行得掉;
; 受限账户的机器:不弹 UAC,按普通权限启动,走下面的降级路径装成「仅为我安装」,
; 至少装得上;硬 admin 在那种机器上直接起不来。
RequestExecutionLevel highest
; ── 编译期替 electron-builder 补的两处 ──────────────────────────────────────
; 1) 自定义脚本下 electron-builder 不做 BUILD_UNINSTALLER 那一趟,UNINSTALLER_OUT_FILE
; 被留成空值,而 include/installer.nsh 里有一条无条件的
; File "/oname=${UNINSTALL_FILENAME}" "${UNINSTALLER_OUT_FILE}"
; 空路径直接编译失败。真卸载器由安装段末尾的 WriteUninstaller 现写,
; 这里顶一个占位文件上去只为让编译过去(安装时会被立刻删掉再重写,见安装段)。
!ifdef UNINSTALLER_OUT_FILE
!define /redef UNINSTALLER_OUT_FILE "${PROJECT_DIR}\installer\uninstaller-stub.txt"
!else
!define UNINSTALLER_OUT_FILE "${PROJECT_DIR}\installer\uninstaller-stub.txt"
!endif
; 2) MUI_INSERT 是在第一个 MUI_LANGUAGE 处展开的,界面初始化钩子必须在那之前定义。
!define MUI_CUSTOMFUNCTION_GUIINIT "GuiInit"
; 用户中途取消/关窗时也把定时器收掉(先于 .onGUIEnd 跑,靠 $timerOn 幂等,不会重复销毁)
!define MUI_CUSTOMFUNCTION_ABORT "AbortCleanup"
; 当前进程是否已提权(结果放 $gIsAdmin)。
; 静默安装要靠它决定「为所有用户」还是退一步「仅为我安装」,而这条路上一个 UAC 插件调用都不能有:
; 静默模式没有页窗口,UAC::_ 会卡住不返回(本机 wine 实测),真机上 UAC_RunElevated 会弹框等人点 ——
; 无人值守时那两件事都等于装不完。查令牌问的是同一个问题,且不依赖任何窗口。
Function CheckAdmin
Push $0
Push $R1
Push $R2
Push $R3
Push $R4
Push $R5
Push $R6
Push $R7
StrCpy $gIsAdmin 0
System::Call 'kernel32::GetCurrentProcess() p .R1'
System::Call '*(p 0) p .R2'
System::Call 'advapi32::OpenProcessToken(p R1, i 0x0008, p R2) i .R3' ; TOKEN_QUERY
${If} $R3 != 0
System::Call '*$R2(p .R4)'
System::Call '*(i 0) p .R5'
System::Call 'advapi32::GetTokenInformation(p R4, i 20, p R5, i 4, p 0) i .R6' ; TokenElevation
${If} $R6 != 0
System::Call '*$R5(i .R7)'
${If} $R7 != 0
StrCpy $gIsAdmin 1
${EndIf}
${EndIf}
System::Call 'kernel32::CloseHandle(p R4)'
System::Free $R5
${EndIf}
System::Free $R2
Pop $R7
Pop $R6
Pop $R5
Pop $R4
Pop $R3
Pop $R2
Pop $R1
Pop $0
FunctionEnd
; 「应用还在跑吗」的检查。不查的话覆盖安装时 CopyFiles 会一路失败,
; 用户最后只看到一个「无法关闭,重试/取消」的框,根本不知道要先关程序。
; 逻辑取自 allowOnlyOneInstallerInstance.nsh 的 CHECK_APP_RUNNING,但不能直接插那个宏:
; 1. 它的宏体带 Var /GLOBAL CmdPath / PowerShellPath,一个脚本只能插一次,
; 而那一次已经被 un.checkAppRunning 占掉(我们 include 了 uninstaller.nsh);
; 2. 卸载器侧的 Call 只能指向 un. 开头的函数,安装器又调不了 un. 函数,
; 所以两边各要一个入口,共用下面这一份宏体。
; 另外 $CmdPath / $PowerShellPath / $IsPowerShellAvailable 都由 un.checkAppRunning
; 那次展开声明(所以本文件里这一整段必须在 !include "uninstaller.nsh" 之后)。
!macro AppRunningCheck
StrCpy $CmdPath "$SYSDIR\cmd.exe"
StrCpy $PowerShellPath "$SYSDIR\WindowsPowerShell\v1.0\powershell.exe"
!insertmacro ProbePowerShell
!insertmacro _CHECK_APP_RUNNING
!macroend
; IS_POWERSHELL_AVAILABLE 的宏体自带 Var /GLOBAL IsPowerShellAvailable,
; 两边各插一次会报「already declared」,所以那句 Var 已经提到文件开头的变量区,
; 探测逻辑原样抄在这里(十几行,抄一份比去改上游模板安全)。
!macro ProbePowerShell
; 先用一个必然返回 7 的命令确认这是「真的」PowerShell。
; 上游模板直接问 Get-Command Get-CimInstance,但假壳(wine 自带的 powershell.exe 就是,
; 任何参数都 exit 0)会让这个探测和后面那条 Get-CimInstance 查询一律「成功」,
; 于是安装器误判「程序正在运行」,去杀一个不存在的进程、卡在重试框里。
; 这类假壳在现场也可能出现(安全软件挂的 shell 代理),所以宁可多花一次子进程。
nsExec::Exec `"$PowerShellPath" -C "exit 7"`
Pop $0
${if} $0 != 7
StrCpy $IsPowerShellAvailable 1
${else}
nsExec::Exec `"$PowerShellPath" -C "if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }"`
Pop $0
${if} $0 == 0
nsExec::Exec `"$PowerShellPath" -C "if ((Get-ExecutionPolicy -Scope Process) -eq 'Restricted') { exit 1 } else { exit 0 }"`
Pop $0
${endIf}
${if} $0 != 0
StrCpy $0 1
${endIf}
StrCpy $IsPowerShellAvailable $0
${endIf}
!macroend
; ── 卸载段 ─────────────────────────────────────────────────────────────────
; 卸载逻辑与 un.onInit 直接用 electron-builder 的模板(uninstaller.nsh),一个字节都不改:
; 升级、快捷方式回收、防火墙规则回收的行为与上游完全一致。
; 卸载器的界面仍是原生外观 —— 素材只有安装器那一套、5 张页面位图全带「安装」字样,
; 硬套上去只会更难看。
;
; ⚠️ 为什么这里要自己给 BUILD_UNINSTALLER:
; electron-builder 平时编两趟(安装器一趟、卸载器一趟),靠 BUILD_UNINSTALLER 这个
; define 区分两边的代码路径(典型的是 getProcessInfo.nsh:安装器定义 _GetProcessInfo、
; 卸载器定义 un._GetProcessInfo)。自定义脚本只有一趟编译,安装器和卸载器的代码同处
; 一个文件里,所以只能给「卸载器这一段」自己划一个作用域:
; · 定义 BUILD_UNINSTALLER → 让 un. 侧拿到 Call un._GetProcessInfo(否则卸载器里的
; Call 不许指向非 un. 函数,直接编译不过);
; · include 完之后 !undef,安装器侧的代码照旧走 _GetProcessInfo。
; 顺带这也让 un.onInit 里的 initMultiUser 走到「卸载器需要提权」的那条分支,
; 与上游卸载器编译结果一致。
; ── 提权判断:把 ${UAC_IsAdmin} 换成自己的实现 ─────────────────────────────
; 理由只有一个:静默安装 / 静默卸载都不能碰 UAC 插件。没有页窗口时 UAC::_ 在本机 wine 下
; 直接不返回(真机上 UAC_RunElevated 也会弹一个没人点的框)—— 无人值守装机就卡在那儿装不完。
; 问的是同一个问题(进程令牌是否已提权),换掉之后连构建期生成的 installer-custom.nsh
; (netsh 放行/回收前后那段判断)也一起受益,不用去改生成器。
; 必须放在 !define BUILD_UNINSTALLER 之前:下面按那个 define 决定叫哪一个实现
; (卸载器里的 Call 只能指向 un. 开头的函数)。
!undef UAC_IsAdmin
!macroundef _UAC_IsAdmin
!define UAC_IsAdmin `"" UAC_IsAdmin ""`
!macro _UAC_IsAdmin _a _b _t _f
!ifdef BUILD_UNINSTALLER
Call un.CheckAdmin
!else
Call CheckAdmin
!endif
StrCmp $gIsAdmin "1" `${_t}` `${_f}`
!macroend
!macro customCheckAppRunning
Call un.CheckAppRunningImpl
!macroend
!define BUILD_UNINSTALLER
!include "uninstaller.nsh"
; getProcessInfo.nsh 的 un._GetProcessInfo 只在 BUILD_UNINSTALLER 那一趟里定义;
; 我们的单趟编译里补一个,宏体用的还是上游那份 FUNC_GETPROCESSINFO。
Function un._GetProcessInfo
!insertmacro FUNC_GETPROCESSINFO
FunctionEnd
Function un.CheckAppRunningImpl
!insertmacro AppRunningCheck
FunctionEnd
; 卸载器侧的提权判断(同一套令牌查询,只是名字要带 un. 前缀)
Function un.CheckAdmin
Push $0
Push $R1
Push $R2
Push $R3
Push $R4
Push $R5
Push $R6
Push $R7
StrCpy $gIsAdmin 0
System::Call 'kernel32::GetCurrentProcess() p .R1'
System::Call '*(p 0) p .R2'
System::Call 'advapi32::OpenProcessToken(p R1, i 0x0008, p R2) i .R3'
${If} $R3 != 0
System::Call '*$R2(p .R4)'
System::Call '*(i 0) p .R5'
System::Call 'advapi32::GetTokenInformation(p R4, i 20, p R5, i 4, p 0) i .R6'
${If} $R6 != 0
System::Call '*$R5(i .R7)'
${If} $R7 != 0
StrCpy $gIsAdmin 1
${EndIf}
${EndIf}
System::Call 'kernel32::CloseHandle(p R4)'
System::Free $R5
${EndIf}
System::Free $R2
Pop $R7
Pop $R6
Pop $R5
Pop $R4
Pop $R3
Pop $R2
Pop $R1
Pop $0
FunctionEnd
!undef BUILD_UNINSTALLER
!insertmacro addLangs
; ── 通用函数 ───────────────────────────────────────────────────────────────
Function CheckAppRunningImpl
!insertmacro AppRunningCheck
FunctionEnd
; 按 DPI 缩放基准值。栈:基准像素值 → 返回缩放后的像素值。
; 只用 $R8/$R9:所有调用方都用 $R0..$R7 放自己的临时值,S 不能踩它们。
Function S
Exch $R9
Push $R8
IntOp $R8 $R9 * $gDpi
IntOp $R8 $R8 / 96
StrCpy $R9 $R8
Pop $R8
Exch $R9
FunctionEnd
; 贴位图。栈:句柄 文件名 消息 基准宽 基准高。
; 文件名只有名字(如 base.bmp),目录由 $gArt 决定;cx/cy 用控件的目标像素尺寸,
; 于是 2x 素材在 150% 这类非整数倍下也能由系统缩到刚好。
Function SetBmp
Pop $R4 ; 文件名
Pop $R3 ; 句柄
Pop $R2 ; 消息
Pop $R1 ; 基准宽
Pop $R0 ; 基准高
StrCpy $tmpPath "$gArt\$R4"
Push $R1
Call S
Pop $R5 ; 目标宽
Push $R0
Call S
Pop $R6 ; 目标高
System::Call 'user32::LoadImageW(p 0, w "$tmpPath", i 0, i $R5, i $R6, i 0x0010) p .R0'
${If} $R0 == 0
Return ; 素材没加载出来就保持原图,宁可画面旧一点也不画成空白
${EndIf}
System::Call 'user32::SendMessageW(p $R3, i $R2, p 0, p $R0)'
; 立即重画:安装段执行期间消息泵不一定马上转到,进度不能等到空闲才更新
System::Call 'user32::InvalidateRect(p $R3, p 0, i 1)'
System::Call 'user32::UpdateWindow(p $R3)'
FunctionEnd
; 建控件。栈顺序见宏;类名/样式是编译期文本,坐标是 1x 设计值。
!macro NewCtl CLASS STYLE BX BY BW BH OUT
Push ${BX}
Call S
Pop $R0
Push ${BY}
Call S
Pop $R1
Push ${BW}
Call S
Pop $R2
Push ${BH}
Call S
Pop $R3
nsDialogs::CreateControl "${CLASS}" "${STYLE}" 0 $R0 $R1 $R2 $R3 ""
Pop ${OUT}
!macroend
; 贴位图(参数按 1x 设计值写,内部转成像素)
!macro SetBmpBase HWND ONAME W H
Push ${H}
Push ${W}
Push ${BM_SETIMAGE}
Push ${HWND}
Push ${ONAME}
Call SetBmp
!macroend
!macro SetBmpStatic HWND ONAME W H
Push ${H}
Push ${W}
Push ${STM_SETIMAGE}
Push ${HWND}
Push ${ONAME}
Call SetBmp
!macroend
; 位图按钮:创建 + 贴常态图
!macro NewBmpBtn BX BY BW BH OUT ONAME
!insertmacro NewCtl "Button" "${BS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${BX} ${BY} ${BW} ${BH} ${OUT}
!insertmacro SetBmpBase ${OUT} ${ONAME} ${BW} ${BH}
!macroend
; 悬停跟踪:栈里带齐「基准矩形 + 句柄 + 当前状态 + 位图基名」。
; 状态机 0=常态 1=悬停 2=按下,和 art/ 的三态位图一一对应。
!macro Track HWND STATE ONAME BX BY BW BH
StrCpy $gBw ${BW}
StrCpy $gBh ${BH}
Push ${BX}
Push ${BY}
Push ${BW}
Push ${BH}
Push ${HWND}
Push ${STATE}
Push "${ONAME}"
Call HoverBtn
Pop ${STATE}
!macroend
; 光标位置换算 + 命中测试 → 需要时换位图。栈:基准x 基准y 基准宽 基准高 句柄 状态 位图基名(base)。
; 用 $R0..$R6 干活、$R7 存结果 —— SetBmp 只踩 $R0..$R6,所以 $R7 能活到返回。
Function HoverBtn
Pop $R0 ; 位图基名
Pop $R1 ; 当前状态
Pop $R2 ; 句柄
Pop $R3 ; 基准高
Pop $R4 ; 基准宽
Pop $R5 ; 基准 y
Pop $R6 ; 基准 x
Push $R6
Call S
Pop $R6 ; x
Push $R5
Call S
Pop $R5 ; y
Push $R4
Call S
Pop $R4 ; w
Push $R3
Call S
Pop $R3 ; h
; 右/下边界(命中测试用半开区间)
IntOp $R4 $R4 + $R6
IntOp $R3 $R3 + $R5
StrCpy $R7 0
${If} $gCx >= $R6
${AndIf} $gCx < $R4
${AndIf} $gCy >= $R5
${AndIf} $gCy < $R3
StrCpy $R7 1
System::Call 'user32::GetAsyncKeyState(i 1) i .R8'
IntOp $R8 $R8 & 0x8000
${If} $R8 != 0
StrCpy $R7 2
${EndIf}
${EndIf}
${If} $R7 != $R1
${If} $R7 == 0
StrCpy $tmpName "$R0-normal.bmp"
${ElseIf} $R7 == 1
StrCpy $tmpName "$R0-hover.bmp"
${Else}
StrCpy $tmpName "$R0-pressed.bmp"
${EndIf}
Push $gBh
Push $gBw
Push ${BM_SETIMAGE}
Push $R2
Push $tmpName
Call SetBmp
${EndIf}
Push $R7
FunctionEnd
; 起拖:记下锚点(光标屏幕坐标 + 窗口左上角)
Function DragStart
Pop $R0
StrCpy $dragging 1
System::Call '*(i, i) p .R1'
System::Call 'user32::GetCursorPos(p R1)'
System::Call '*$R1(i .R2, i .R3)'
System::Free $R1
StrCpy $dragCx $R2
StrCpy $dragCy $R3
System::Call '*(i, i, i, i) p .R4'
System::Call 'user32::GetWindowRect(p $HWNDPARENT, p R4)'
System::Call '*$R4(i .R5, i .R6)'
System::Free $R4
StrCpy $dragWx $R5
StrCpy $dragWy $R6
FunctionEnd
; 拖动中:窗口位置 = 锚点窗口位置 + 光标位移
Function DragApply
Pop $R0
System::Call '*(i, i) p .R1'
System::Call 'user32::GetCursorPos(p R1)'
System::Call '*$R1(i .R2, i .R3)'
System::Free $R1
IntOp $R2 $R2 - $dragCx
IntOp $R3 $R3 - $dragCy
IntOp $R2 $R2 + $dragWx
IntOp $R3 $R3 + $dragWy
System::Call 'user32::SetWindowPos(p $HWNDPARENT, p 0, i $R2, i $R3, i 0, i 0, i 0x0015)'
FunctionEnd
; 定时器回调:光标 → 客户区坐标 → 逐控件做悬停/按下 → 标题栏拖动。
; 不做窗口子类化(POC 结论):60ms 轮询足够跟手,也没有子类化回调崩掉整个安装器的风险。
Function Tick
System::Call '*(i, i) p .R0'
System::Call 'user32::GetCursorPos(p R0)'
System::Call 'user32::ScreenToClient(p $HWNDPARENT, p R0)'
System::Call '*$R0(i .R1, i .R2)'
System::Free $R0
StrCpy $gCx $R1
StrCpy $gCy $R2
${If} $uiPage == 0
Return
${EndIf}
; 标题栏两个按钮每页都在
!insertmacro Track $hMin $stMin "tb-min" ${TB_MIN_X} 0 ${TB_BTN_W} ${TB_H}
!insertmacro Track $hClose $stClose "tb-close" ${TB_CLOSE_X} 0 ${TB_BTN_W} ${TB_H}
${If} $uiPage == 1
!insertmacro Track $hNext $stNext "btn-next" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
!insertmacro Track $hCancel $stCancel "btn-cancel" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
${ElseIf} $uiPage == 2
!insertmacro Track $hNext $stNext "btn-next" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
!insertmacro Track $hCancel $stCancel "btn-cancel" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
${ElseIf} $uiPage == 3
!insertmacro Track $hNext $stNext "btn-next" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
!insertmacro Track $hBack $stBack "btn-back" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
!insertmacro Track $hBrowse $stBrowse "btn-browse" ${BROWSE_X} ${BROWSE_Y} ${BROWSE_W} ${BROWSE_H}
${ElseIf} $uiPage == 4
!insertmacro Track $hCancel $stCancel "btn-cancel" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
; 安装在这里开工:等第一个 tick(窗口已经画完)再动手,然后完工后翻到 P5。
; 为什么不用 NSIS 自带的 instfiles 页:那一页在跑安装时只泵它自己的消息,
; 我们挂在 StdUtils 定时器上的悬停/进度刷新全都收不到回调(实测:P4 上按钮不悬停、
; 定时器不触发)。改成自绘页 + 定时器驱动,消息泵一直是通的。
${If} $instStarted == 0
Call DoInstall
GetDlgItem $0 $HWNDPARENT 1
SendMessage $0 ${BM_CLICK} 0 0
${EndIf}
${ElseIf} $uiPage == 5
!insertmacro Track $hFinish $stFinish "btn-finish" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
${EndIf}
; 标题栏拖动:按下时记锚点(光标屏幕坐标 + 窗口左上角),之后按位移 SetWindowPos。
; 不用系统的 WM_NCLBUTTONDOWN / HTCAPTION 移动循环 —— 它把移动交给 DefWindowProc,
; 真机上(窗口已经没有 WS_CAPTION 了)实测拖不动;自己算不依赖系统行为,wine 与真机一致。
; 两点关键:
; 1. 命中判定只管「起拖」;已经在拖的时候只要左键还按着就继续跟 —— 光标一旦甩出标题栏
; (窗口还没跟上来的那一瞬间)就停,正是「拖一下就断」的经典写法错误;
; 2. 起拖区里要扣掉标题栏按钮(x >= TB_MIN_X),否则点最小化/关闭会连带开始拖窗。
System::Call 'user32::GetAsyncKeyState(i 1) i .R3'
IntOp $R3 $R3 & 0x8000
${If} $dragging == 1
${If} $R3 != 0
Push true
Call DragApply
${Else}
StrCpy $dragging 0
${EndIf}
${Else}
${If} $gCy >= 0
${AndIf} $gCy < ${TB_H}
${AndIf} $gCx >= 0
${AndIf} $gCx < ${TB_MIN_X}
${AndIf} $R3 != 0
Push true
Call DragStart
${EndIf}
${EndIf}
FunctionEnd
; ── 素材嵌入 ───────────────────────────────────────────────────────────────
; 逐个文件写 File 而不是 File /r:素材缺一张就在编译期报错,
; 而不是运行期画出一块空白(装机现场没有回头补素材的机会)。
!macro ArtFiles SUB
SetOutPath "$PLUGINSDIR\${SUB}"
File /oname=base.bmp "${ART_SRC}\${SUB}\base.bmp"
File /oname=brand.bmp "${ART_SRC}\${SUB}\brand.bmp"
File /oname=titlebar.bmp "${ART_SRC}\${SUB}\titlebar.bmp"
File /oname=page-welcome.bmp "${ART_SRC}\${SUB}\page-welcome.bmp"
File /oname=page-license.bmp "${ART_SRC}\${SUB}\page-license.bmp"
File /oname=page-dir.bmp "${ART_SRC}\${SUB}\page-dir.bmp"
File /oname=page-progress.bmp "${ART_SRC}\${SUB}\page-progress.bmp"
File /oname=page-finish.bmp "${ART_SRC}\${SUB}\page-finish.bmp"
File /oname=btn-next-normal.bmp "${ART_SRC}\${SUB}\btn-next-normal.bmp"
File /oname=btn-next-hover.bmp "${ART_SRC}\${SUB}\btn-next-hover.bmp"
File /oname=btn-next-pressed.bmp "${ART_SRC}\${SUB}\btn-next-pressed.bmp"
File /oname=btn-install-normal.bmp "${ART_SRC}\${SUB}\btn-install-normal.bmp"
File /oname=btn-install-hover.bmp "${ART_SRC}\${SUB}\btn-install-hover.bmp"
File /oname=btn-install-pressed.bmp "${ART_SRC}\${SUB}\btn-install-pressed.bmp"
File /oname=btn-finish-normal.bmp "${ART_SRC}\${SUB}\btn-finish-normal.bmp"
File /oname=btn-finish-hover.bmp "${ART_SRC}\${SUB}\btn-finish-hover.bmp"
File /oname=btn-finish-pressed.bmp "${ART_SRC}\${SUB}\btn-finish-pressed.bmp"
File /oname=btn-cancel-normal.bmp "${ART_SRC}\${SUB}\btn-cancel-normal.bmp"
File /oname=btn-cancel-hover.bmp "${ART_SRC}\${SUB}\btn-cancel-hover.bmp"
File /oname=btn-cancel-pressed.bmp "${ART_SRC}\${SUB}\btn-cancel-pressed.bmp"
File /oname=btn-back-normal.bmp "${ART_SRC}\${SUB}\btn-back-normal.bmp"
File /oname=btn-back-hover.bmp "${ART_SRC}\${SUB}\btn-back-hover.bmp"
File /oname=btn-back-pressed.bmp "${ART_SRC}\${SUB}\btn-back-pressed.bmp"
File /oname=btn-browse-normal.bmp "${ART_SRC}\${SUB}\btn-browse-normal.bmp"
File /oname=btn-browse-hover.bmp "${ART_SRC}\${SUB}\btn-browse-hover.bmp"
File /oname=btn-browse-pressed.bmp "${ART_SRC}\${SUB}\btn-browse-pressed.bmp"
File /oname=tb-min-normal.bmp "${ART_SRC}\${SUB}\tb-min-normal.bmp"
File /oname=tb-min-hover.bmp "${ART_SRC}\${SUB}\tb-min-hover.bmp"
File /oname=tb-min-pressed.bmp "${ART_SRC}\${SUB}\tb-min-pressed.bmp"
File /oname=tb-close-normal.bmp "${ART_SRC}\${SUB}\tb-close-normal.bmp"
File /oname=tb-close-hover.bmp "${ART_SRC}\${SUB}\tb-close-hover.bmp"
File /oname=tb-close-pressed.bmp "${ART_SRC}\${SUB}\tb-close-pressed.bmp"
File /oname=check-off.bmp "${ART_SRC}\${SUB}\check-off.bmp"
File /oname=check-on.bmp "${ART_SRC}\${SUB}\check-on.bmp"
File /oname=radio-off.bmp "${ART_SRC}\${SUB}\radio-off.bmp"
File /oname=radio-on.bmp "${ART_SRC}\${SUB}\radio-on.bmp"
File /oname=progress-0.bmp "${ART_SRC}\${SUB}\progress-0.bmp"
File /oname=progress-1.bmp "${ART_SRC}\${SUB}\progress-1.bmp"
File /oname=progress-2.bmp "${ART_SRC}\${SUB}\progress-2.bmp"
File /oname=progress-3.bmp "${ART_SRC}\${SUB}\progress-3.bmp"
File /oname=progress-4.bmp "${ART_SRC}\${SUB}\progress-4.bmp"
File /oname=progress-5.bmp "${ART_SRC}\${SUB}\progress-5.bmp"
File /oname=stage-1.bmp "${ART_SRC}\${SUB}\stage-1.bmp"
File /oname=stage-2.bmp "${ART_SRC}\${SUB}\stage-2.bmp"
File /oname=stage-3.bmp "${ART_SRC}\${SUB}\stage-3.bmp"
File /oname=stage-4.bmp "${ART_SRC}\${SUB}\stage-4.bmp"
File /oname=stage-5.bmp "${ART_SRC}\${SUB}\stage-5.bmp"
!macroend
; 只解当前 DPI 需要的那一套:两套都解要写 26MB 临时文件,首次显示会明显卡一下。
Function EnsureArt
${If} $artReady == 1
Return
${EndIf}
StrCpy $artReady 1
InitPluginsDir
${If} $gDpi >= 144
StrCpy $gArt "$PLUGINSDIR\2x"
!insertmacro ArtFiles 2x
${Else}
StrCpy $gArt "$PLUGINSDIR\1x"
!insertmacro ArtFiles 1x
${EndIf}
FunctionEnd
; 系统 DPI。ManifestDPIAware 之后进程不会被人为放大,只能自己按这个数缩放版面。
Function PickDpi
System::Call 'user32::GetDC(p 0) p .R0'
System::Call 'gdi32::GetDeviceCaps(p R0, i 88) i .R1' ; LOGPIXELSX
System::Call 'user32::ReleaseDC(p 0, p R0)'
${If} $R1 < 96
StrCpy $R1 96
${EndIf}
StrCpy $gDpi $R1
FunctionEnd
; 藏一个外层对话框上的原生控件(控件不存在就跳过,$R7 是本宏专用的临时变量)
!macro HideStd ID
GetDlgItem $R7 $HWNDPARENT ${ID}
${If} $R7 != 0
ShowWindow $R7 ${SW_HIDE}
${EndIf}
!macroend
; 内层对话框铺满整个客户区:MUI 外层框架(页眉 / 品牌字 / 原生按钮条)全被盖住,
; 自绘位图才有完整的 760×480 画布。
Function NewPage
nsDialogs::Create 1018
Pop $hPage
${If} $hPage == error
Abort
${EndIf}
; 悬停/拖动全靠这个 60ms 轮询(POC 结论:比窗口子类化稳,代价是光标移动要等一个 tick)。
; 建在页面里而不是 .onGUIInit:StdUtils 的定时器要挂在页对话框上才跑得起来。
; ⚠️ 全程只建一个:它挂在 StdUtils 的隐藏窗口上,不随页面消失,而 NewPage 每页都会跑 ——
; 不加哨兵就会越翻页越多(越跑越卡,退出时还会漏掉没销毁的那些)。
${If} $timerOn == 0
StrCpy $timerOn 1
${StdUtils.TimerCreate} $timerId Tick 60
${EndIf}
Push ${WIN_W}
Call S
Pop $R0
Push ${WIN_H}
Call S
Pop $R1
System::Call 'user32::SetWindowPos(p $hPage, p 0, i 0, i 0, i $R0, i $R1, i 0x0004)'
; 把外层对话框上的原生控件**显式隐藏**。内层对话框只是盖在它们上面,鼠标划过那块区域时
; 原生按钮会自己重绘、从自绘层底下透出来(真机实测:Next/Back/Cancel 会闪出来)。
; NSIS 每页都重建内层对话框,所以每页(=每次 NewPage)都要重新藏一遍。
; ⚠️ 藏掉之后翻页仍然靠 SendMessage BM_CLICK 点那个被藏起来的 Next —— 隐藏窗口照样收得到
; 消息,本机实测翻页正常(这条一旦不成立,整套翻页就断了,所以改了要立刻验证)。
!insertmacro HideStd 1 ; Next
!insertmacro HideStd 2 ; Cancel
!insertmacro HideStd 3 ; Back
!insertmacro HideStd 1028 ; 底部品牌字底
!insertmacro HideStd 1256 ; 底部品牌字
!insertmacro HideStd 1034 ; 页眉底
!insertmacro HideStd 1035 ; 分隔线(标准)
!insertmacro HideStd 1045 ; 分隔线(满窗)
!insertmacro HideStd 1037 ; 页眉标题
!insertmacro HideStd 1038 ; 页眉副标题
!insertmacro HideStd 1046 ; 页眉图标
; 上一页的句柄全部作废:定时器靠这组变量判断「还能碰谁」
StrCpy $hBg 0
StrCpy $hBrand 0
StrCpy $hTitleBar 0
StrCpy $hMin 0
StrCpy $hClose 0
StrCpy $hNext 0
StrCpy $hBack 0
StrCpy $hCancel 0
StrCpy $hBrowse 0
StrCpy $hFinish 0
StrCpy $hCheck 0
StrCpy $hStage 0
StrCpy $hProg 0
StrCpy $stMin 0
StrCpy $stClose 0
StrCpy $stNext 0
StrCpy $stBack 0
StrCpy $stCancel 0
StrCpy $stBrowse 0
StrCpy $stFinish 0
FunctionEnd
; 全窗共用的底:背景 → 品牌栏 → 页面位图 → 标题栏 → 标题栏按钮。
; 栈:页面位图名(空字符串 = 该页不贴页面位图,比如安装中页自己控制进度)
Function Chrome
Pop $tmpName
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" 0 0 ${WIN_W} ${WIN_H} $hBg
!insertmacro SetBmpStatic $hBg "base.bmp" ${WIN_W} ${WIN_H}
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" 0 ${TB_H} ${SIDE_W} ${BRAND_H} $hBrand
!insertmacro SetBmpStatic $hBrand "brand.bmp" ${SIDE_W} ${BRAND_H}
${If} $tmpName != ""
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${PG_X} ${PG_Y} ${PG_W} ${PG_H} $hProg
!insertmacro SetBmpStatic $hProg "$tmpName" ${PG_W} ${PG_H}
${EndIf}
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" 0 0 ${WIN_W} ${TB_H} $hTitleBar
!insertmacro SetBmpStatic $hTitleBar "titlebar.bmp" ${WIN_W} ${TB_H}
!insertmacro NewBmpBtn ${TB_MIN_X} 0 ${TB_BTN_W} ${TB_H} $hMin "tb-min-normal.bmp"
!insertmacro NewBmpBtn ${TB_CLOSE_X} 0 ${TB_BTN_W} ${TB_H} $hClose "tb-close-normal.bmp"
${NSD_OnClick} $hMin OnMin
${NSD_OnClick} $hClose OnClose
FunctionEnd
; 自绘文字标签:对话框默认底色是灰的,不显式设色就会在页面上贴出一块灰,
; 所以一律用「底色 = 内容区底色」把标签融进位图。
; 栈(后压先弹):文本 字号 基准高 基准宽 基准y 基准x
!macro AddLabel BX BY BW BH SIZE TEXT
Push ${BX}
Push ${BY}
Push ${BW}
Push ${BH}
Push ${SIZE}
Push "${TEXT}"
Call Label
!macroend
Function Label
Pop $R0
Pop $R1
Pop $R2
Pop $R3
Pop $R4
Pop $R5
Push $R5
Call S
Pop $R5
Push $R4
Call S
Pop $R4
Push $R3
Call S
Pop $R3
Push $R2
Call S
Pop $R2
nsDialogs::CreateControl "Static" "${SS_LEFT}|${SS_CENTERIMAGE}|${WS_CHILD}|${WS_VISIBLE}" 0 $R5 $R4 $R3 $R2 ""
Pop $R6
SetCtlColors $R6 "${CLR_TXT}" "${CLR_BG}"
CreateFont $R7 "Microsoft YaHei UI" "$R1" "400"
SendMessage $R6 ${WM_SETFONT} $R7 1
${NSD_SetText} $R6 "$R0"
FunctionEnd
; 栈(后压先弹):样式 基准高 基准宽 基准y 基准x → 返回控件句柄
!macro NewEdit STYLE BX BY BW BH OUT
Push ${BH}
Push ${BW}
Push ${BY}
Push ${BX}
Push "${STYLE}"
Call MakeEdit
Pop ${OUT}
!macroend
Function MakeEdit
Pop $R5
Pop $R0
Pop $R1
Pop $R2
Pop $R3
Push $R0
Call S
Pop $R0
Push $R1
Call S
Pop $R1
Push $R2
Call S
Pop $R2
Push $R3
Call S
Pop $R3
nsDialogs::CreateControl "Edit" "$R5" 0 $R0 $R1 $R2 $R3 ""
Pop $R6
SetCtlColors $R6 "${CLR_TXT}" "0xffffff"
CreateFont $R7 "Microsoft YaHei UI" "12" "400"
SendMessage $R6 ${WM_SETFONT} $R7 1
Push $R6
FunctionEnd
; ── 按钮动作 ───────────────────────────────────────────────────────────────
; 标准按钮条被自绘层盖住了,但按钮还在:点它等于点系统按钮,翻页/取消都由 NSIS 自己走。
Function GoNext
GetDlgItem $0 $HWNDPARENT 1
SendMessage $0 ${BM_CLICK} 0 0
FunctionEnd
Function GoBack
GetDlgItem $0 $HWNDPARENT 3
SendMessage $0 ${BM_CLICK} 0 0
FunctionEnd
Function GoCancel
GetDlgItem $0 $HWNDPARENT 2
SendMessage $0 ${BM_CLICK} 0 0
FunctionEnd
Function OnMin
; 最小化走系统命令:自绘窗口没有系统按钮,但 ShowWindow 一样有效
System::Call 'user32::ShowWindow(p $HWNDPARENT, i 6)' ; SW_MINIMIZE
FunctionEnd
Function OnClose
Call GoCancel
FunctionEnd
Function OnLicCheck
; 用 != 1 而不是 == 0:LogicLib 的 == 是字符串比较,$licOk 初值是空串,"" == 0 为假
${If} $licOk != 1
StrCpy $licOk 1
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
${Else}
StrCpy $licOk 0
!insertmacro SetBmpBase $hCheck "check-off.bmp" 16 16
${EndIf}
FunctionEnd
Function OnLicNext
${If} $licOk != 1
; 没同意就不让走:这是许可页唯一的硬性要求
MessageBox MB_OK|MB_ICONINFORMATION|MB_TOPMOST "请先勾选「我已阅读并同意」再继续。"
Return
${EndIf}
Call GoNext
FunctionEnd
Function OnModeUser
StrCpy $modeAll 0
!insertmacro SetBmpBase $hRadio1 "radio-on.bmp" 16 16
!insertmacro SetBmpBase $hRadio2 "radio-off.bmp" 16 16
FunctionEnd
Function OnModeAll
StrCpy $modeAll 1
!insertmacro SetBmpBase $hRadio1 "radio-off.bmp" 16 16
!insertmacro SetBmpBase $hRadio2 "radio-on.bmp" 16 16
FunctionEnd
; 「浏览…」:系统文件夹选择框没有 NSIS 包装,用 System::Call 直接调 shell32。
; BROWSEINFO = { hwndOwner, pidlRoot, pszDisplayName, lpszTitle, ulFlags, lpfn, lParam, iImage }
; ulFlags = BIF_RETURNONLYFSDIRS(0x1) | BIF_NEWDIALOGSTYLE(0x40)
Function OnBrowse
System::Call 'ole32::CoInitialize(p 0)'
StrCpy $R6 $HWNDPARENT
System::Alloc 260
Pop $R7
System::Alloc 32
Pop $R0
System::Call '*$R0(p $R6, p 0, p $R7, p 0, i 0x41, p 0, p 0, i 0)'
System::Call 'shell32::SHBrowseForFolder(p R0) p .R1'
${If} $R1 != 0
System::Call 'shell32::SHGetPathFromIDList(p R1, t .R2)'
System::Call 'ole32::CoTaskMemFree(p R1)'
${If} $R2 != ""
StrCpy $tmpTxt $R2
${NSD_SetText} $hPath "$tmpTxt"
${EndIf}
${EndIf}
System::Free $R7
System::Free $R0
FunctionEnd
Function OnFinish
${If} $runApp == 1
Call StartAppNow
${EndIf}
; 结束安装器。这里踩过两个坑:页面回调里 Quit 不生效(窗口不退、反而被推去下一页),
; 对主窗口发 WM_CLOSE 也不生效;能真正退出的只有点系统那个「取消」按钮。
; 先把错误码归零,别让「正常装完」的退出码看起来像被取消。
SetErrorLevel 0
GetDlgItem $0 $HWNDPARENT 2
SendMessage $0 ${BM_CLICK} 0 0
FunctionEnd
Function OnFinCheck
${If} $runApp != 1
StrCpy $runApp 1
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
${Else}
StrCpy $runApp 0
!insertmacro SetBmpBase $hCheck "check-off.bmp" 16 16
${EndIf}
FunctionEnd
; ── 进度分段 ───────────────────────────────────────────────────────────────
; app-64.7z 是一次 Nsis7z::Extract 调用,拿不到百分比;所以进度按「宏观步骤」分段
; (解压 → 注册表 → 快捷方式 → 防火墙 → 收尾),不画假进度条。
; NSIS 的 Call 不收参数,用法统一走这个宏
!macro StageCall N
Push ${N}
Call Stage
!macroend
; 栈:段号 1..5
Function Stage
Pop $R0
${If} ${Silent}
Return
${EndIf}
${If} $uiPage != 4
Return
${EndIf}
; 段号先搬进全局变量:SetBmp 会踩掉 $R0..$R7(它的局部值都放那儿)
StrCpy $tmpTxt $R0
StrCpy $tmpPath "stage-$tmpTxt.bmp"
Push ${STAGE_H}
Push ${STAGE_W}
Push ${STM_SETIMAGE}
Push $hStage
Push $tmpPath
Call SetBmp
StrCpy $tmpPath "progress-$tmpTxt.bmp"
Push ${PROG_H}
Push ${PROG_W}
Push ${STM_SETIMAGE}
Push $hProg
Push $tmpPath
Call SetBmp
FunctionEnd
; ── P1 欢迎 ────────────────────────────────────────────────────────────────
Function PageWelcome
${If} ${Silent}
Abort
${EndIf}
StrCpy $uiPage 1
Call NewPage
Push "page-welcome.bmp"
Call Chrome
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hCancel "btn-cancel-normal.bmp"
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hNext "btn-next-normal.bmp"
${NSD_OnClick} $hCancel OnClose
${NSD_OnClick} $hNext GoNext
nsDialogs::Show
FunctionEnd
; ── P2 许可协议 ────────────────────────────────────────────────────────────
; 中文摘要烘焙在 page-license.bmp 里;英文原文(LICENSE)用真编辑框显示,
; 因为它比框大一截、必须能滚 —— 位图放不下整篇协议。
Function PageLicense
${If} ${Silent}
Abort
${EndIf}
StrCpy $uiPage 2
Call NewPage
Push "page-license.bmp"
Call Chrome
; 先按可写建,灌完文本再设只读:以只读样式创建的编辑框会被刷成灰底,
; 后期用 EM_SETREADONLY 设的只读框底色还是白的(SetCtlColors 再兜一层)。
!insertmacro NewEdit "${ES_MULTILINE}|${ES_AUTOVSCROLL}|${WS_VSCROLL}|${WS_CHILD}|${WS_VISIBLE}" ${LIC_EDIT_X} ${LIC_EDIT_Y} ${LIC_EDIT_W} ${LIC_EDIT_H} $hLicEdit
File /oname=$PLUGINSDIR\license.txt "${PROJECT_DIR}\..\LICENSE"
FileOpen $9 "$PLUGINSDIR\license.txt" r
lic_loop:
ClearErrors
FileRead $9 $8
IfErrors lic_done
SendMessage $hLicEdit ${EM_REPLACESEL} 1 "STR:$8$\r$\n"
Goto lic_loop
lic_done:
FileClose $9
; FileRead + EM_REPLACESEL 会把插入点一路推到末尾、视图跟着滚到底;
; 用户进这一页看到的应该是协议开头,所以显式回到顶部
SendMessage $hLicEdit ${EM_SETSEL} 0 0
SendMessage $hLicEdit ${WM_VSCROLL} 6 0 ; SB_TOP
SendMessage $hLicEdit ${EM_SETREADONLY} 1 0
!insertmacro NewBmpBtn 272 432 16 16 $hCheck "check-off.bmp"
${If} $licOk == 1
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
${EndIf}
!insertmacro AddLabel 296 430 300 20 12 "我已阅读并同意上述条款"
${NSD_OnClick} $hCheck OnLicCheck
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hCancel "btn-cancel-normal.bmp"
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hNext "btn-next-normal.bmp"
${NSD_OnClick} $hCancel OnClose
${NSD_OnClick} $hNext OnLicNext
nsDialogs::Show
FunctionEnd
Function PageLicenseLeave
StrCpy $uiPage 0
FunctionEnd
; ── P3 安装位置 + 安装模式 ─────────────────────────────────────────────────
Function PageDir
${If} ${Silent}
Abort
${EndIf}
StrCpy $uiPage 3
Call NewPage
Push "page-dir.bmp"
Call Chrome
!insertmacro NewEdit "${ES_AUTOHSCROLL}|${WS_CHILD}|${WS_VISIBLE}" ${PATH_EDIT_X} ${PATH_EDIT_Y} ${PATH_EDIT_W} ${PATH_EDIT_H} $hPath
${NSD_SetText} $hPath "$INSTDIR"
!insertmacro NewBmpBtn ${BROWSE_X} ${BROWSE_Y} ${BROWSE_W} ${BROWSE_H} $hBrowse "btn-browse-normal.bmp"
${NSD_OnClick} $hBrowse OnBrowse
!insertmacro NewBmpBtn ${RADIO1_X} ${RADIO1_Y} 16 16 $hRadio1 "radio-off.bmp"
!insertmacro NewBmpBtn ${RADIO2_X} ${RADIO2_Y} 16 16 $hRadio2 "radio-off.bmp"
${NSD_OnClick} $hRadio1 OnModeUser
${NSD_OnClick} $hRadio2 OnModeAll
; 初始勾选跟随 $installMode:initMultiUser 已经按「上次装法 / 命令行 / 默认」定好了模式
${If} $installMode == "all"
StrCpy $modeAll 1
!insertmacro SetBmpBase $hRadio2 "radio-on.bmp" 16 16
${Else}
StrCpy $modeAll 0
!insertmacro SetBmpBase $hRadio1 "radio-on.bmp" 16 16
${EndIf}
; 位图上的「需要空间 约 260 MB」是设计稿的占位数字,用真实体积盖掉
!ifdef ESTIMATED_SIZE
IntOp $R0 ${ESTIMATED_SIZE} / 1024
StrCpy $tmpTxt "需要空间 约 $R0 MB"
!insertmacro AddLabel ${PG_X} ${SIZE_TXT_Y} ${CONTENT_W} 20 11 "$tmpTxt"
!endif
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hBack "btn-back-normal.bmp"
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hNext "btn-next-normal.bmp"
${NSD_OnClick} $hBack GoBack
${NSD_OnClick} $hNext GoNext
nsDialogs::Show
FunctionEnd
Function PageDirLeave
StrCpy $uiPage 0
; 手改过就以输入框为准(浏览按钮也是写回这个框)
${NSD_GetText} $hPath $R1
${If} $R1 != ""
StrCpy $INSTDIR $R1
${EndIf}
StrCpy $R2 $INSTDIR 1 -1
${If} $R2 == "\"
StrCpy $INSTDIR $INSTDIR -1
${EndIf}
; 和 electron-builder 的目录页同一套规矩:路径必须以应用目录名结尾,
; 否则用户填 D:\Games 会把整个文件树倒进 D:\Games 而不是 D:\Games\Scoreboard_Console
${StrContains} $R3 "${APP_FILENAME}" $INSTDIR
${If} $R3 == ""
StrCpy $INSTDIR "$INSTDIR\${APP_FILENAME}"
${EndIf}
; 兜底:万一路径是空的(记忆为空 + 默认值也没算出来),给一个合法的按用户目录,
; 绝不把空 $INSTDIR 带进安装流程(空路径是「点了没反应 / 装到莫名其妙的地方」的经典来源)。
${If} $INSTDIR == ""
StrCpy $INSTDIR "$LOCALAPPDATA\Programs\${APP_FILENAME}"
${EndIf}
${If} $modeAll == 1
; 「为所有用户安装」。执行级别是 highest,所以**管理员账户的机器上启动时就提过权了**,
; 这里不需要(也不该)再走一次 UAC_RunElevated:中途提权会把窗口藏起来、让用户看见
; 「闪一下没反应」,真机上就是这么坏的(前几轮修过)。
; 受限账户的机器上 highest 拿不到管理员权限 —— 这时明确告知并降级成「仅为我安装」继续,
; 绝不停在这一页。
${If} ${UAC_IsAdmin}
!insertmacro setInstallModePerAllUsers
${Else}
MessageBox mb_IconExclamation|mb_TopMost|mb_SetForeground "当前账户没有管理员权限,无法「为所有用户安装」,已改为「仅为我安装」继续。"
StrCpy $modeAll 0
!insertmacro setInstallModePerUser
${EndIf}
${Else}
!insertmacro setInstallModePerUser
${EndIf}
FunctionEnd
; ── P4 正在安装 ────────────────────────────────────────────────────────────
; 自绘页:分段进度由 DoInstall 里的 Stage 调用推动(7z 一次解压拿不到百分比)。
; 不用 NSIS 自带的 instfiles 页,原因见 Tick 里的注释。
Function PageInstall
${If} ${Silent}
Abort
${EndIf}
StrCpy $uiPage 4
Call NewPage
Push "page-progress.bmp"
Call Chrome
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${STAGE_X} ${STAGE_Y} ${STAGE_W} ${STAGE_H} $hStage
!insertmacro SetBmpStatic $hStage "stage-1.bmp" ${STAGE_W} ${STAGE_H}
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${PROG_X} ${PROG_Y} ${PROG_W} ${PROG_H} $hProg
!insertmacro SetBmpStatic $hProg "progress-0.bmp" ${PROG_W} ${PROG_H}
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hCancel "btn-cancel-normal.bmp"
${NSD_OnClick} $hCancel OnClose
nsDialogs::Show
FunctionEnd
Function PageInstallLeave
StrCpy $uiPage 0
FunctionEnd
; ── P5 完成 ────────────────────────────────────────────────────────────────
Function PageFinish
${If} ${Silent}
Abort
${EndIf}
StrCpy $uiPage 5
Call NewPage
Push "page-finish.bmp"
Call Chrome
!insertmacro NewBmpBtn ${FIN_CHK_X} ${FIN_CHK_Y} 16 16 $hCheck "check-off.bmp"
${If} $runApp == 1
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
${EndIf}
!insertmacro AddLabel 296 336 300 20 13 "立即运行 赛事积分榜"
${NSD_OnClick} $hCheck OnFinCheck
; 台上的实际安装目录(位图里写的是设计稿的占位路径)
StrCpy $tmpTxt "安装位置:$INSTDIR"
!insertmacro AddLabel ${PG_X} ${FIN_PATH_Y} ${CONTENT_W} 18 11 "$tmpTxt"
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hFinish "btn-finish-normal.bmp"
${NSD_OnClick} $hFinish OnFinish
nsDialogs::Show
FunctionEnd
Function PageFinishLeave
StrCpy $uiPage 0
FunctionEnd
; ── 界面初始化 ─────────────────────────────────────────────────────────────
; 去系统边框 + 固定 760×480(按 DPI 缩放)+ 居中。
; MUI 的外层框架本身用不上,但它是 $HWNDPARENT、是标准按钮条的宿主,
; 所以不能不要,只能去边框、盖住、把标准按钮当程序化翻页的工具。
Function GuiInit
Call PickDpi
System::Call 'user32::GetWindowLong(p $HWNDPARENT, i -16) i .R0'
IntOp $R0 $R0 & 0xFF30FFFF ; 清 WS_CAPTION / BORDER / DLGFRAME / SYSMENU / MIN / MAX
System::Call 'user32::SetWindowLong(p $HWNDPARENT, i -16, i R0)'
Push ${WIN_W}
Call S
Pop $R0
Push ${WIN_H}
Call S
Pop $R1
System::Call 'user32::GetSystemMetrics(i 0) i .R2'
System::Call 'user32::GetSystemMetrics(i 1) i .R3'
IntOp $R2 $R2 - $R0
IntOp $R2 $R2 / 2
IntOp $R3 $R3 - $R1
IntOp $R3 $R3 / 2
${If} $R2 < 0
StrCpy $R2 0
${EndIf}
${If} $R3 < 0
StrCpy $R3 0
${EndIf}
System::Call 'user32::SetWindowPos(p $HWNDPARENT, p 0, i $R2, i $R3, i $R0, i $R1, i 0x0014)'
Call EnsureArt
FunctionEnd
; ── .onInit ────────────────────────────────────────────────────────────────
; 逐项对照 electron-builder 的 templates/nsis/installer.nsi,只去掉 setInstallSectionSpaceRequired
; (那是给组件页算体积用的,我们没有组件页)。
; 安装器 GUI 结束时收尾(正常点「完成」退出、以及中途关窗都走这里)。
; 不销毁的话 StdUtils 插件在卸载时会弹「Plug-in unloaded before all timers were destroyed」。
; 只认哨兵 $timerOn:对没建过 / 已销毁的 id 调 TimerDestroy 本身也是一种报错源。
Function AbortCleanup
${If} $timerOn == 1
StrCpy $timerOn 0
${StdUtils.TimerDestroy} $R0 $timerId
StrCpy $timerId 0
${EndIf}
FunctionEnd
Function .onGUIEnd
${If} $timerOn == 1
StrCpy $timerOn 0
${StdUtils.TimerDestroy} $R0 $timerId
StrCpy $timerId 0
${EndIf}
FunctionEnd
Function .onInit
StrCpy $timerId 0
StrCpy $timerOn 0
StrCpy $uiPage 0
StrCpy $instStarted 0
SetOutPath $INSTDIR
${LogSet} on
!insertmacro check64BitAndSetRegView
; 静默安装跳过单实例检查:这个宏的头一句是 BringToFront,而静默模式压根没有窗口,
; 实测在这种情况下会挂死(wine 下 45 秒不返回 —— 安装器就卡在这儿永远装不完)。
; 交互安装照常查:那才是「用户重复双击」真正会发生的地方。
${IfNot} ${Silent}
${AndIfNot} ${UAC_IsInnerInstance}
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
${EndIf}
; 命令行 /D= 的优先级最高:用户明说了装哪,就不该被「上次装到哪」的记忆改掉。
; initMultiUser 内部已经把 /D= 应用过一次,但紧随其后的 customInit(构建期生成)会拿
; 注册表里的记忆目录盖掉 $INSTDIR —— 这里先把 /D= 的值留一份,customInit 之后盖回去。
; 现场装机脚本就是靠 /D= 指定目录的(安装包.exe /S /currentuser /D=...),
; 记忆把它顶掉的话,脚本会以为装到了指定目录,其实文件落在别处。
; 注意 /D= 不在 ${GetParameters} 里(NSIS 自己把它从命令行里摘掉了),
; 所以用上游 multiUser.nsh 的 GetDParameter —— 它走 StdUtils.GetAllParameters 拿原始命令行再手工解析。
!insertmacro GetDParameter $gDparam
!insertmacro initMultiUser
!ifmacrodef customInit
!insertmacro customInit
!endif
${If} $gDparam != ""
StrCpy $INSTDIR $gDparam
${EndIf}
StrCpy $runApp 1
FunctionEnd
; ── 页面序列 ───────────────────────────────────────────────────────────────
Page custom PageWelcome
Page custom PageLicense PageLicenseLeave
Page custom PageDir PageDirLeave
Page custom PageInstall PageInstallLeave
Page custom PageFinish PageFinishLeave
; 空跑的 instfiles 页(放在页面序列最后):NSIS 要求「有安装段就必须有 instfiles 页」,
; 否则 -WX 下直接报 warning 8000 编译不过。在 PRE 回调里 Abort 掉、页面不会显示,
; 而 P5 的「完成」是直接结束安装器(见 OnFinish),根本走不到这一页。
PageEx instfiles
Caption " "
PageCallbacks SkipInstfiles "" ""
PageExEnd
Function SkipInstfiles
Abort
FunctionEnd
; ── 安装流程 ───────────────────────────────────────────────────────────────
; 「立即运行」用 electron-builder 的 StartApp 宏(common.nsh):它内部走 ExecShellAsUser,
; 以**登录用户**身份启动 —— 「为所有用户安装」那一趟进程是提权过的,直接 Exec 会让应用
; 继承管理员令牌,装完跑起来的会是个管理员身份的客户端(数据目录/开机自启都跑到别人家去)。
; 这个宏里带一句 Var /GLOBAL startAppArgs,整个脚本只能插入一次,所以包成一个函数给两处调。
Function StartAppNow
HideWindow
!insertmacro StartApp
FunctionEnd
; 安装流程本体。放在函数里而不是 Section 里,是为了让 P4 能是自绘页(原因见 Tick)。
; 顺序逐行对照 electron-builder 的 templates/nsis/installSection.nsh。
Function DoInstall
; 一次性守卫:交互安装由 P4 的定时器触发,静默安装由安装段触发,
; 两条路都从这里进,谁先到谁干活,绝不可能装两遍。
${If} $instStarted == 1
Return
${EndIf}
StrCpy $instStarted 1
; 静默安装没有 UI 可选安装模式,per-machine 这一趟要在这里补提权。
; 比上游多一个 $installMode == "all" 的条件:上游只在「已有 per-machine 安装」时提权,
; 而我们把默认模式改成了「为所有用户」,全新静默安装(/S)就会默认往 Program Files 写,
; 不提权必然写不进去 —— 现场用脚本装机时这一步比交互装机更要紧。
!ifndef INSTALL_MODE_PER_ALL_USERS
!ifndef ONE_CLICK
${if} $installMode == "all"
${andIf} ${Silent}
; 静默安装**绝不弹 UAC**:无人值守时没人点那个框,弹出即等于装不完。
; 所以这里完全不碰 UAC 插件(提权与判断都不做),只查一眼令牌:
; 已经是管理员 → 按「为所有用户」继续,装到 Program Files 并放行防火墙;
; 不是管理员 → 退回「仅为我安装」(%LOCALAPPDATA%\Programs\...),
; 不需要提权也能装完,退出码仍然是 0。
; 交互安装不提权的事在 P3 离开时按 multiUserUi 的范式处理,与这里无关。
Call CheckAdmin
${if} $gIsAdmin == 1
!insertmacro setInstallModePerAllUsers
${else}
!insertmacro setInstallModePerUser
${endIf}
${endIf}
!endif
!endif
InitPluginsDir
${IfNot} ${Silent}
SetDetailsPrint none
${endif}
StrCpy $appExe "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
# must be called before uninstallOldVersion
!insertmacro setLinkVars
; 非 one-click 时只在最外层进程里查:提权出来的内层进程和用户看到的可能是同一个应用,
; 两个进程同时抢着关它会互相打架(上游 installer.nsh / installSection.nsh 就是这么分的)。
; 静默安装没有窗口,那句 UAC_IsInnerInstance 走插件、会卡住不返回,所以直接查。
${If} ${Silent}
Call CheckAppRunningImpl
${Else}
${ifNot} ${UAC_IsInnerInstance}
Call CheckAppRunningImpl
${endif}
${EndIf}
StrCpy $keepShortcuts "false"
!insertMacro setIsTryToKeepShortcuts
${if} $isTryToKeepShortcuts == "true"
ReadRegStr $R1 SHELL_CONTEXT "${INSTALL_REGISTRY_KEY}" KeepShortcuts
${if} $R1 == "true"
${andIf} ${FileExists} "$appExe"
StrCpy $keepShortcuts "true"
${endIf}
${endif}
; 覆盖安装:先跑旧卸载器(它自己会带 --updated / --keep-shortcuts)
!insertmacro uninstallOldVersion SHELL_CONTEXT
!insertmacro handleUninstallResult SHELL_CONTEXT
${if} $installMode == "all"
!insertmacro uninstallOldVersion HKEY_CURRENT_USER
!insertmacro handleUninstallResult HKEY_CURRENT_USER
${endIf}
SetOutPath $INSTDIR
!insertmacro StageCall 1
!insertmacro installApplicationFiles
!insertmacro StageCall 2
!insertmacro registryAddInstallInfo
!insertmacro StageCall 3
!insertmacro addStartMenuLink $keepShortcuts
!insertmacro addDesktopLink $keepShortcuts
${if} ${FileExists} "$newStartMenuLink"
StrCpy $launchLink "$newStartMenuLink"
${else}
StrCpy $launchLink "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
${endif}
!ifmacrodef registerFileAssociations
!insertmacro registerFileAssociations
!endif
!insertmacro StageCall 4
!ifmacrodef customInstall
!insertmacro customInstall
!endif
!insertmacro StageCall 5
; 真卸载器在这里现写。先删掉 installApplicationFiles 落下来的那个占位文件,
; 免得「万一没写成功、$INSTDIR 里却留着一个假卸载器」这种最坏情况。
Delete "$INSTDIR\${UNINSTALL_FILENAME}"
WriteUninstaller "$INSTDIR\${UNINSTALL_FILENAME}"
!ifdef ONE_CLICK
!ifdef RUN_AFTER_FINISH
${ifNot} ${Silent}
${orIf} ${isForceRun}
Call StartAppNow
${endIf}
!else
${if} ${isForceRun}
Call StartAppNow
${endIf}
!endif
!insertmacro quitSuccess
!else
; 交互安装的「立即运行」在 P5 上处理;静默安装只能靠 --force-run
${if} ${isForceRun}
${andIf} ${Silent}
Call StartAppNow
${endIf}
!endif
FunctionEnd
; 安装段的两条路:
; 交互安装:安装段根本不会执行(页面序列里的 instfiles 页被 Abort 掉了),
; 安装流程由 P4 的定时器调 DoInstall 跑;
; 静默安装(/S):没有页面可走,段就是唯一入口 —— 这一步走的是 electron-builder
; 原生的「段在 instfiles 页上执行」,和上游同一条路。
; 之所以不把静默安装也塞进 .onInit:实测那样会在 wine 里挂死(.onInit 阶段还没有
; 页对话框,DoInstall 里的界面相关动作没有宿主)。段这条路是上游验证过的。
Section "install" INSTALL_SECTION_ID
; 静默安装(/S):没有任何页面会被创建,P4 的定时器永远不会触发,所以这里就是唯一入口。
; NSIS 在静默模式下照常执行 Section、只是不显示页面 —— 和上游 electron-builder 安装器
; 「段在 instfiles 页上执行」是同一条路(区别只是我们把 instfiles 页 Abort 掉了)。
; 交互安装时这一支不执行:那时安装段根本不会被走到(instfiles 页被 Abort),
; 安装由 P4 的定时器调 DoInstall(DoInstall 自带一次性守卫,不会装两遍)。
${If} ${Silent}
Call DoInstall
!insertmacro quitSuccess
${EndIf}
SectionEnd