fix(installer): 安装位置识别与模式切换目录(真机四因根除)

主人真机复验:① 不能像原版那样自动识别「已安装程序的安装位置」;② 选「为所有用户安装」却装到
%LOCALAPPDATA% 而不是 Program Files。

真机打点(instrumented 副本 + WM_COMMAND/BM_CLICK/WM_SETTEXT 驱动)读出四个根因:
1. 切换安装模式单选钮只改状态位图,**不同步输入框**;
2. setInstallModePerAllUsers/PerUser 会按记忆/默认**重算 $INSTDIR**,把「框里定下的值」
   (用户手改、浏览选择、记忆)**无条件冲掉** —— 这是两条症状的共同根因
   (打点:after-normalize=[D:\SBCustom\Scoreboard Console] → after-mode=[C:\Program Files (x86)\…]);
3. 记忆读取 HKCU 优先、**不看安装模式** → 一条更早的用户级记忆顶掉刚装好的机器级目录;
4. 无人值守会话里 $LOCALAPPDATA 解析成 C:\ProgramData →「仅为我」默认成了 C:\ProgramData\Programs\…。

改法:
- 新增 ModeDefaultDir:「为所有用户」→ $PROGRAMFILES64\${APP_FILENAME}(本包是 x64);
  「仅为我」→ SHGetKnownFolderPath(FOLDERID_UserProgramFiles),失败才退回 $LOCALAPPDATA\Programs;
- 新增 PathTouched(框里的文本 != 程序设定的值 = 用户手改过)与 SyncPathToMode:
  切模式时**只有用户没改过**才把框与 $INSTDIR 同步成该模式默认目录(不冲掉用户刚填的);
- PageDirLeave 用 $pathKeep 保住路径,跑完上游两个宏后再恢复 —— 只让宏改模式与 SHELL_CONTEXT,不再改路径;
- .onInit 在 customInit 之后**按当前模式**重读自家记忆键(all → HKLM,否则 HKCU)。

真机六条全过(含落盘复核):进 P3 显示记忆目录;点「为所有用户」→ C:\Program Files\Scoreboard_Console;
点「仅为我」→ %LOCALAPPDATA%\Programs\…;点回去再正确切换;手塞自定义路径以它为准;改过后再切模式不被冲掉。
静默无回归:/S 6.6s exit=0 落记忆目录;/S /D= 6.4s exit=0 且 /D= 优先。
wine 回归:人手速度拖动、原生控件隐藏、P3→P5 自动翻页、✕ 退出无 StdUtils 报错、旧版在跑时 /S 关掉旧进程。
This commit is contained in:
AskaEth committed 2026-09-13 23:32:00 +08:00
1 parent 9951a69938
commit a57df78f99
1 file changed
+99 -2
+99 -2
View File
@@ -162,6 +162,8 @@ Var dragWy
Var gDparam ; 命令行 /D= 指定的安装目录(要在 customInit 的记忆目录之后盖回去)
Var keepShortcuts ; 快捷方式保留策略,由安装流程写定(上游在安装段里声明,我们提前到全局)
Var dragging
Var pathSet ; 我们最后一次写进「安装位置」输入框的值(用来判断用户有没有手改过)
Var pathKeep ; 计算路径时临时存一份(上游那两个宏会重写 $INSTDIR,用它盖回来)
; ── include ────────────────────────────────────────────────────────────────
@@ -1183,16 +1185,82 @@ Function OnLicNext
Call GoNext
FunctionEnd
; 当前安装模式对应的「默认目录」(结果放 $R0)。
; 「为所有用户」= 公共目录:本包是 x64 应用,所以优先 $PROGRAMFILES64(真机期望就是 C:\Program Files\…),
; 32 位系统上退回 $PROGRAMFILES;「仅为我」= %LOCALAPPDATA%\Programs\…(与上游 setInstallModePerUser 一致)。
Function ModeDefaultDir
${If} $modeAll == 1
StrCpy $R0 "$PROGRAMFILES"
!ifdef APP_64
${If} ${RunningX64}
StrCpy $R0 "$PROGRAMFILES64"
${EndIf}
!endif
StrCpy $R0 "$R0\${APP_FILENAME}"
${Else}
; 与上游 setInstallModePerUser 同源:优先问 shell 要「用户程序目录」(FOLDERID_UserProgramFiles)。
; 不能直接用 $LOCALAPPDATA:在提权/无人值守(session 0,没有加载用户配置)的会话里,
; NSIS 的 $LOCALAPPDATA 会解析成 C:\ProgramData —— 真机实测就是装到了
; 「C:\ProgramData\Programs\Scoreboard_Console」,那不是用户的程序目录。
StrCpy $R0 "$LOCALAPPDATA\Programs"
Push $0
Push $1
Push $2
StrCpy $2 0
System::Call 'SHELL32::SHGetKnownFolderPath(g "${FOLDERID_UserProgramFiles}", i ${KF_FLAG_CREATE}, p 0, *p .r2) i .r1'
${If} $1 == 0
System::Call 'KERNEL32::lstrcpynW(w .r0, p r2, i ${NSIS_MAX_STRLEN}) p'
StrCpy $R0 "$0"
${EndIf}
${If} $2 != 0
System::Call 'OLE32::CoTaskMemFree(p r2)'
${EndIf}
Pop $2
Pop $1
Pop $0
StrCpy $R0 "$R0\${APP_FILENAME}"
${EndIf}
FunctionEnd
; 用户有没有手改过「安装位置」框?结果放 $R9(1 = 改过)。
; 判据是「框里的文本 != 我们最后一次写进去的值」——不需要额外的控件通知,浏览/手输都盖得住。
Function PathTouched
${NSD_GetText} $hPath $R1
StrCpy $R9 0
${If} $R1 != $pathSet
StrCpy $R9 1
${EndIf}
FunctionEnd
; 切模式时把路径与输入框同步到该模式的默认目录。
; 策略(说得通的理由):**用户没手改过**才跟着模式变;用户改过就保持他填的,
; 免得「改完路径再点一下单选」把用户刚填的东西悄悄冲掉。
; 之前这里只改单选状态位图、不动框 —— 于是「选了为所有用户,框里还是用户目录」,
; 真机复验的两条症状(记忆目录看不出、选公共安装却像在装用户目录)都是它引起的。
Function SyncPathToMode
Call PathTouched
${If} $R9 == 0
Call ModeDefaultDir
StrCpy $INSTDIR $R0
${If} $hPath != 0
${NSD_SetText} $hPath "$INSTDIR"
StrCpy $pathSet "$INSTDIR"
${EndIf}
${EndIf}
FunctionEnd
Function OnModeUser
StrCpy $modeAll 0
!insertmacro SetBmpBase $hRadio1 "radio-on.bmp" 16 16
!insertmacro SetBmpBase $hRadio2 "radio-off.bmp" 16 16
Call SyncPathToMode
FunctionEnd
Function OnModeAll
StrCpy $modeAll 1
!insertmacro SetBmpBase $hRadio1 "radio-off.bmp" 16 16
!insertmacro SetBmpBase $hRadio2 "radio-on.bmp" 16 16
Call SyncPathToMode
FunctionEnd
; 「浏览…」:系统文件夹选择框没有 NSIS 包装,用 System::Call 直接调 shell32。
@@ -1213,6 +1281,8 @@ Function OnBrowse
${If} $R2 != ""
StrCpy $tmpTxt $R2
${NSD_SetText} $hPath "$tmpTxt"
; 浏览选出来的目录也算用户改过:别让切模式把它冲掉
StrCpy $pathSet ""
${EndIf}
${EndIf}
System::Free $R7
@@ -1365,7 +1435,11 @@ Function PageDir
Call Chrome
!insertmacro NewEdit "${ES_AUTOHSCROLL}|${WS_CHILD}|${WS_VISIBLE}" ${PATH_EDIT_X} ${PATH_EDIT_Y} ${PATH_EDIT_W} ${PATH_EDIT_H} $hPath
; 进页面时框里显示的就是当前 $INSTDIR:initMultiUser + customInit 已经把「上次装到哪」
; (我们自己的记忆键,HKCU 优先、再 HKLM)读进来了,所以这里就是「记忆目录」。
; $pathSet 记下我们写的这份值,之后 SyncPathToMode / PageDirLeave 靠它判断用户有没有手改过。
${NSD_SetText} $hPath "$INSTDIR"
StrCpy $pathSet "$INSTDIR"
!insertmacro NewBmpBtn ${BROWSE_X} ${BROWSE_Y} ${BROWSE_W} ${BROWSE_H} $hBrowse "btn-browse-normal.bmp"
${NSD_OnClick} $hBrowse OnBrowse
@@ -1414,12 +1488,17 @@ Function PageDirLeave
StrCpy $INSTDIR "$INSTDIR\${APP_FILENAME}"
${EndIf}
; 兜底:万一路径是空的(记忆为空 + 默认值也没算出来),给一个合法的按用户目录,
; 兜底:万一路径是空的(记忆为空 + 默认值也没算出来),按当前模式给一个合法默认目录,
; 绝不把空 $INSTDIR 带进安装流程(空路径是「点了没反应 / 装到莫名其妙的地方」的经典来源)。
; 走 ModeDefaultDir 而不是直接拼 $LOCALAPPDATA —— 后者在无人值守会话里会解析成 C:\ProgramData。
${If} $INSTDIR == ""
StrCpy $INSTDIR "$LOCALAPPDATA\Programs\${APP_FILENAME}"
Call ModeDefaultDir
StrCpy $INSTDIR "$R0"
${EndIf}
; 先把定下来的路径存一份:下面这两个上游宏会按注册表记忆/模式默认**重新算 $INSTDIR**,
; 而这一页的规矩是「框里是什么就装什么」(记忆、模式默认、用户手改、/D= 都已经体现在框里了)。
StrCpy $pathKeep "$INSTDIR"
${If} $modeAll == 1
; 「为所有用户安装」。执行级别是 highest,所以**管理员账户的机器上启动时就提过权了**,
; 这里不需要(也不该)再走一次 UAC_RunElevated:中途提权会把窗口藏起来、让用户看见
@@ -1436,6 +1515,8 @@ Function PageDirLeave
${Else}
!insertmacro setInstallModePerUser
${EndIf}
; 盖回框里定下的路径($installMode / SHELL_CONTEXT 已经在宏里设好了,这里只保路径)
StrCpy $INSTDIR "$pathKeep"
FunctionEnd
; ── P4 正在安装 ────────────────────────────────────────────────────────────
@@ -1572,6 +1653,22 @@ Function .onInit
!ifmacrodef customInit
!insertmacro customInit
!endif
; 记忆目录要跟**安装模式**对齐,这才是「原版安装器」的行为:initMultiUser 读的就是该模式对应的键
; (为所有用户 → HKLM,仅为我 → HKCU)。构建期生成的 customInit 是「HKCU 的自家记忆优先」,
; 于是一条更早的用户级记忆会把刚装好的机器级目录顶掉 —— 真机实测:机器上最后一次装在
; C:\Program Files\Scoreboard_Console,安装位置页却填着更早的 D:\Program Files\Scoreboard_Console。
; 这里按当前模式再读一遍自家记忆键,有值就盖回去;两个 hive 都空时保留 initMultiUser/customInit 的结果。
${If} $installMode == "all"
ReadRegStr $R9 HKLM "${SCOREBOARD_INSTALL_KEY}" "InstallLocation"
${If} $R9 != ""
StrCpy $INSTDIR $R9
${EndIf}
${Else}
ReadRegStr $R9 HKCU "${SCOREBOARD_INSTALL_KEY}" "InstallLocation"
${If} $R9 != ""
StrCpy $INSTDIR $R9
${EndIf}
${EndIf}
${If} $gDparam != ""
StrCpy $INSTDIR $gDparam
${EndIf}