

## 2026-06-03

### 22:18 修正 AskDayWin 陣列越界 BASE/1132（nAlias STATIC 殘留 0）

**現象**：客戶端自動錯誤回報 `BASE/1132 Bound error: array access`，stack 頂 `ASKDAYWIN(1219)`（P113→PartsForm→DayWin→SearchDayWin→AskDayWin）。

**根因**：`DAYWIN.PRG:1219 aToggle[1, nAlias, 4]`，`aToggle[1]` 只有 4 元素，nAlias 須 1~4。`SearchDayWin` 的 `nAlias` 是 **STATIC**（初值 2）；當先做「查詢/國內訂單」類查詢走 ELSE 分支時 `nAlias := 0`（line 1451）呼叫 AskQIWin，0 殘留；之後做「進/銷貨」查詢走 IF 分支 `AskDayWin(@nAlias=0)` → 索引 0 越界。重現：先做詢價/國內訂單查詢，再做進/銷貨查詢。旁證：1440-1441 原有 `//nAlias:=2 //nType:=3` 重設保險被註解掉。潛在第二顆：`nType` 同源殘留 4~6 會讓 line 1238 `aToggle[2,nType,4]` 越界。

**解法**：AskDayWin 開頭、建 aToggle 前加防禦性夾值（保留「記住上次選擇」，只在非法時重設）：
`nAlias := IIF(nAlias==NIL .OR. nAlias<1 .OR. nAlias>4, 2, nAlias)`、`nType := IIF(nType==NIL .OR. nType<1 .OR. nType>3, 3, nType)`。

**踩坑**：PRG 為 BIG5，用 Python bytes-only 寫入（中文註解 BIG5），全 CRLF。

**修改檔案**：`source\DAYWIN.PRG`（備份 `.bak_nalias_20260603_2216`）。BUILD OK。**待**：實機驗證後刪備份。

### 12:20 STK_UPDATER 等待退出期間處理「誤進 STK32.EXE」的闖入者

**問題**：STK_UPDATER 建 FLG 後進入 wait_for_exit 等待退出。這期間若有人誤進（重啟）STK32.EXE：
1. 新實例啟動跑 ShutdownStartupClean() 把 FLG 當孤兒清掉 → 不會倒數、一直跑；
2. STK_UPDATER 偵測它仍在執行 → 等到 120s timeout → 被 KILL_STK 強殺（本該優雅倒數退出）。
根因＝「開機清孤兒 FLG」無法分辨孤兒 vs 更新器正在等待的活 FLG。

**解法（只改 stk_updater.py 的 wait_for_exit）**：
- **補建 FLG**：等待迴圈偵測到仍在執行的站台時，檢查它有沒有 SHUTDOWN.FLG，沒有就補建 → 闖入者下一次 CheckShutdown(每5秒)就看到倒數，正常退出。
- **延長等待（不重置、且只在不夠時才加）**：補建 FLG 時，**若現有剩餘 > 60s 表示時間已夠闖入者跑完倒數 → 不累加**；只有剩餘 ≤ 60s 才把 60s 加到現有剩餘往後延（deadline += 60，非重置成整個 120s）。例：剩 30s→變 90s、剩 5s→變 65s；剩 61s→維持 61s 不動。讓闖入者看完自己的 60s 倒數 UI 再退出，不被中途強殺、UI 不一致。
- 改用可延長的 `deadline` 取代固定 `timeout - elapsed`；INTRUDER_COUNTDOWN=60 須與 shutdown.prg 的 s_nRemain 一致。

**測試**：補建邏輯、deadline 延長（30+60=90）邏輯測試通過。**重 build** stk_updater.exe（x86-32，6.78MB）。

**修改檔案**：`stk_updater.py`、`dist\stk_updater.exe`。

### 12:00 方向2：消費端自我修復（重傳一定生效、不必刪 local_version.json）

**訴求**：先前「更新無效」根因是版本閘門 `has_update=False`（manifest.updated == local_version.json.updated）就整段跳過 distribute，要手動刪 `local_version.json` 才會重更。希望即使時間戳相同，只要站台實際 EXE 與更新包不符就自動重派、並自我修復漂移。

**做法（方案 X：靠 manifest 宣告的核心 EXE 大小，免下載即可判斷）**：
- **stk_uploader.py / ftp_upload.py（上傳端）**：manifest 多寫 `exe_sizes`＝核心執行檔（Stk32.exe/PrnAsk.exe/prn2xlsx.exe）大小。
- **stk_updater.py（消費端）**：新增 `check_station_drift()`——拿 manifest 的 `exe_sizes` 跟各站實際 EXE 大小比（純讀本地、不下載）；`need_update = has_update OR drift`，下載/SHUTDOWN/distribute 三處閘門改用 `need_update`。manifest 無 `exe_sizes`（舊版上傳）→ drift=False → 維持原行為（向下相容、零回歸）。排除 `.ini`（站台客製）與字型（本機重生）避免誤判，只比三個核心執行檔。

**測試**：單元測試三情境通過——相符→不重派、漂移(大小不符)→強制重派並印 `DRIFT:`、無 exe_sizes→False。

**重 build（兩支均 x86-32，依使用者指定）**：
- stk_updater.exe（伺服器端 console）→ **x86-32**，用 `E:\ConvertFW\Python312-32` build，6.78MB。（原 W:\ 那支是 x64，本次依使用者要求改成 x86-32 統一架構。）
- stk_uploader.exe（開發端 PyQt5）→ **x86-32**，用 `E:\ConvertFW\Python312-32`（含 PyQt5）build，31MB。
- 攜帶包 zip 已更新。

**啟用條件**：需上傳一次（新 uploader 寫含 exe_sizes 的 manifest + 派送新 stk_updater.exe），消費端換成新 exe 後下一輪即生效。

**修改檔案**：`stk_updater.py`、`stk_uploader.py`、`ftp_upload.py`、`dist\stk_updater.exe`、`stk_uploader.exe`、`stk_uploader_portable.zip`。

### 11:41 STK 更新上傳工具 GUI（PyQt5）打包成 32-bit EXE

**訴求**：原上傳靠 `pack_and_upload.bat` 背後呼叫 `ftp_upload.py`（使用者不知有此支），希望做成可視化 GUI，能臨場選蒐集來源與上傳比對條件。

**做法**：新增 `update_server\stk_uploader.py`（PyQt5）一條龍——蒐集來源(9 個可編輯路徑，含 stk_updater.exe dist 來源) → 勾選打包群組(EXE/ROOT/SET/TOOLS/SOCKET/WebView2/HDOS4，hover 顯示來源+派送目的地) → 選比對方式 ComboBox(時間戳/大小+時間/MD5/強制) → 打包到 update_pack\ → 上傳 FTP → 寫 manifest.json。上傳跑 QThread 不卡 UI；設定存 uploader_config.ini。

**踩坑/釐清**：
- 「更新無效 STK32.EXE 沒更新」根因＝消費端版本閘門：`local_version.json.updated == manifest.updated` → `has_update=False` → 跳過 distribute。刪 `W:\local_version.json` 強制重更成功。真因是沒重跑上傳（manifest 已被消費），非 bug；`ftp_upload.py:243` 本就 `datetime.now()` 每次 bump updated。
- FTP `getaddrinfo failed (Errno 11002)`＝動態 DNS `asiaware.tplinkdns.com` 偶發解析失敗（非 bug，打包成功只卡連線）→ 加自動重試 3 次(間隔 3s)+區分 DNS/登入/連線訊息。
- 打包：PyInstaller --onefile --windowed，**必用 `E:\ConvertFW\Python312-32`（含 PyQt5）**，輸出到 update_server\（與 ftp_config.ini 同目錄 exe 才讀得到）。產物 31MB，Machine 0x14c 確認 x86 32-bit。重建用 `build_uploader.bat`。

**部署必帶檔**：最精簡 `stk_uploader.exe` + `ftp_config.ini`（缺 ftp_config.ini → host 空 → getaddrinfo failed）；建議再帶 `uploader_config.ini`（記住來源/勾選/比對）；`.upload_hashes.json`、`update_pack\`、spec/bat/py 不用帶（自動產生或僅重建用）。已做攜帶包 `stk_uploader_portable.zip`（含三檔+README）。注意：開發端工具，來源指向開發機原始檔樹，搬機器要改「蒐集來源」路徑。

**現況**：✅ 實機打包上傳成功、✅ 32-bit exe 打包+啟動測試通過、✅ 功能報告 PDF 完成。

**修改檔案**：新增 `update_server\` 下 `stk_uploader.py`、`build_uploader.bat`、`stk_uploader.spec`、`stk_uploader.exe`、`stk_uploader_portable.zip`、`doc\_gen_uploader_pdf.py`；PDF `doc\STK更新上傳工具_20260603.pdf`（5 頁，DFKai）。

### 09:52 修正 SHUTDOWN.FLG/ACK 殘留造成更新 STK32.EXE 倒數計時時有時無

**問題**：更新 STK32.EXE 時，正在執行的 STK32 應跳出 60 秒倒數計時讓使用者存檔退出，但「有時候出現、有時候不出現」很混亂。

**根因（FLG/ACK 殘留 + 缺乏歸屬清理）**：
- 兩套機制共用同一組 `SHUTDOWN.FLG/ACK`：① Updater 更新 EXE（`stk_updater.py`，FLG 驅動倒數、靠 exe-lock 判退出、不讀 ACK）；② `DBF.PRG BroadcastShutdown` 索引/打包（讀 ACK 判在線）。
- **客戶端退出只刪 FLG 不刪 ACK**（`shutdown.prg` 倒數到 0 / 點立即 / OnClick）→ 每次倒數退出留孤兒 ACK。
- **孤兒 FLG**：STK32 被 KILL_STK/crash 在倒數結束前退出→FLG 沒被刪；`main.prg` 啟動又不清→下次開機第一個 idle 撞到殘留 FLG→**跳假倒數**（不該出現卻出現）。
- **跨機制清除競態**：`shutdown.prg` 每 5 秒才掃 FLG，這 5 秒內別台索引/打包的 `CleanupShutdownFlg()` 把本台 FLG 刪掉→**該出現卻沒出現**。
- stale ACK 還害 `BroadcastShutdown`（DBF.PRG:1652）誤判「在線等待」空等 90 秒。

**解決方法（1+2）**：
- **shutdown.prg**：新增 `STATIC FUNCTION ShutdownEraseFiles()`（同時刪 FLG+對應 ACK），三處 QUIT（倒數到 0、ShutdownTick 立即鈕、ShutdownOnClick 立即鈕）改呼叫它 → 堵住孤兒 ACK 源頭。
- **main.prg / shutdown.prg**：新增 `FUNCTION ShutdownStartupClean()`（用與 CheckShutdown 相同的 LOC_SYS + hb_DirBase 路徑解析，清自己這站殘留 FLG/ACK），在 `main.prg:356 hb_idleAdd(CheckShutdown)` 之前呼叫 → 開機先清孤兒，堵住假倒數。

**踩坑**：
- shutdown.prg 混合行尾（222 CRLF + 87 LF），三段替換實測為 CRLF；main.prg 主要 bare LF。bytes-only 比對須帶實際行尾。
- 中文註解整塊 `.encode('big5')`（ASCII 為 BIG5 子集，一次到位），禁用 Edit/Write 直寫 BIG5 PRG。

**修改檔案**：`compat/shutdown.prg`、`source/main.prg`（備份 `.bak_flgack_20260603_0949`）。BUILD OK，啟動煙霧測試通過。

**待驗證**：實機多站環境確認（a）更新 EXE 倒數穩定出現（b）不再開機跳假倒數（c）索引/打包不再被 stale ACK 拖 90 秒。可選後續：縮短 shutdown.prg 輪詢 5→1~2 秒。

### 09:46 KeyAltC(Ctrl+Keypad+)手動錯誤回報 無閃爍/不停住/成功提示

**問題**：GTWVT 文字模式下 Ctrl+Keypad+ 觸發 KeyAltC，畫面會把報告內容一行行噴在螢幕上、截圖/clip 閃 cmd 視窗、最後 InKey(0) 停住等使用者按鍵。

**解決方法**（ERRORSYS.PRG KeyAltC）：
- SET CONSOLE OFF / ON 包住 SET ALTERNATE+QOUT 區段 → 報告只寫進 .txt，不再噴螢幕（DefError 自動回報那段本來就有，只 KeyAltC 缺）
- clip 複製 hb_run(type|clip) → WIN_RUNCMD_HIDDEN（CreateProcess+CREATE_NO_WINDOW 無閃爍）
- 移除 InKey(0)（不停住），改成底部 SaveScreen+@SAY「>> 畫面擷取成功，已排程上傳 <<」+ Inkey(5) 5 秒自動消失 + RestScreen 清掉

**踩坑**：SET ALTERNATE TO 在檔案有兩處(DefError+KeyAltC)，直接 replace 會 count=2 失敗→改用「函式區段內 replace」精準只改 KeyAltC。

**修改檔案**：ERRORSYS.PRG（KeyAltC section）

## 2026-06-02

### 21:26 PrnToPdf 報表/標籤分頁修復（標題置中漂移、空白頁、溢出、分隔線對齊）

**問題：** PrnToPdf 轉 PDF 多項分頁錯誤：①無 `[?]` 報表每單元後夾空白頁、第2頁起標題「詢價單」從置中跑成靠左 ②有 `[?]` 估價單內容溢出頁面被切 ③單元尾部/中間版面空白被吃、=== 分隔線位置跑掉。

**根因鏈（逐一定位）：**
- 標題置中靠「前導空格」推位；舊 auto-break 在「行中（第一個非空格）」補頁 → 前導空格被消化 + NewPdfPage 重置 s_nX → 標題靠左。
- 換頁本質是 auto-break（跟 `~I` 數量無關），但補頁時機必須在「行首」（整行最開頭、前導空格之前）。
- 有 `[?]` 時 explicit 模式停用 auto-break → 內容超過 `[?]` 高度也不分頁 → 溢出。
- `[?]` 改 lazy 標記後在單元頭多換一頁（多餘 FF）→ 標題單獨成頁。
- PeekLineHasContent 只看「下一行」→ 把分隔線之前的版面空白也當填充跳過 → 分隔線上移。

**解決方法（patch5~8 漸進）：**
1. lazy 行首補頁：AdvanceLine 跌破只標記 `s_lNeedNewPage`，補頁移到 ParseAndRender 換行處（行首），保前導空格置中。
2. explicit 也走 auto-break（移除 auto-break 的 `!s_lExplicitPaperBreak`）→ 有 `[?]` 報表按 `[?]` 高度分頁、不溢出。
3. `[?]` 只設紙張尺寸、不觸發換頁 → auto-break 每紙張高度自然對齊單元邊界（44行=2半頁），標題落頁首。
4. PeekLineHasContent 掃到「檔尾」（非僅下一行）：空白行後面還有內容就佔頁面（分隔線位置固定），只有真檔尾空白才不開頁。

**踩坑：**
- **最大教訓：rebuild PrnAsk 後漏了 UPDATE（copy 到 HBSTK\build）**，一路在測舊 exe，導致多個實驗結論全錯（如「停 label~I 標籤仍34頁」其實是舊版 label~I 行為）。用 ProcLine 診斷才發現。
- DEBUG_LOG 行中補頁 vs 行首補頁：用 ADV/NEWPAGE/FLUSH log 逐頁反推才定位。
- 「空白行」分兩種：尾部填充（不開空白頁）vs 中間版面空白（該佔頁面）→ 用「後面到檔尾有無內容」區分。
- 每單元精算 DOT：Ty24+L21=45 DOT/行，44行=1980 DOT=11吋=2半頁（990 DOT/5.5吋/22行）。

**修改檔案：** `HBSTK\compat\prn_to_pdf.prg`（AdvanceLine/FlushText/ParseAndRender/新增 PeekLineHasContent），rebuild PrnAsk → UPDATE → 部署 `W:\S001\PrnAsk.exe`。

### 13:26 STK_UPDATER 在 S001 跑不到 10 行就閃退（ini BOM 害的）

**問題：** 在 S001 主機 `C:\STK\STK_UPDATER.EXE` 執行，畫面閃一下就關，且 `C:\STK\stk_updater.log` 完全沒產生。（開發機透過 `W:\` 對應 S001 的 `C:\STK`）

**根因：** S001 的 `stk_updater.ini` 開頭被加了 UTF-8 BOM（`EF BB BF`）。`load_config()` 用 `cfg.read(ini, encoding='utf-8')`（非 utf-8-sig），configparser 把第一行讀成 `﻿# ...`，不是合法 section header → 拋 `MissingSectionHeaderError`。此例外發生在 `main()` 設定 `LOG_FILE`（行1213）之前，所以 `log(FATAL)` 只 `print` 不寫檔 → console 閃退、無 log。開發端 3 份母本 ini 都正常無 BOM，只有 S001 那份壞掉（疑某次用編輯器存檔被加 BOM）。

**解決方法（救急+根治）：**
1. 救急：Python bytes-only 移除 `W:\stk_updater.ini` 的 BOM 前 3 bytes（內容不變），S001 立刻可跑。
2. 根治：`stk_updater.py:41` 的 `encoding='utf-8'` → `encoding='utf-8-sig'`，自動吃掉 BOM，任何工作站 ini 被加 BOM 也不再閃退。
3. PyInstaller rebuild（Python 3.14 / PyInstaller 6.19，64-bit, onefile spec），部署到 `W:\stk_updater.exe`（S001）+ `update_pack/SOCKET` 母本。

**踩坑：**
- 例外點在 `LOG_FILE` 設定之前 → 沒有 log 可查，是「閃退無 log」的關鍵特徵，要靠靜態重現 `load_config()` 才抓到。
- 一度誤以為是 PyInstaller 打包閃退；用「複製 exe 到臨時夾 + 假 FTP ini」零副作用測試，證明 exe 本體健康，排除打包問題。
- 路徑釐清：使用者說「進入 c:\stk 執行」指的是 S001 主機，開發機的 `W:\` = S001 的 `C:\STK`，不是開發機本機。

**修改檔案：**
- `build/update_server/stk_updater.py`（行41 utf-8 → utf-8-sig）
- `W:\stk_updater.ini`（移除 BOM）
- `W:\stk_updater.exe`、`build/update_server/dist/`、`update_pack/SOCKET/`（部署新 exe）

---

## 2026-06-01

### 23:50 STK_UPDATER 更新不到新版 + FLG/ACK 沒清乾淨（UNDO #14）

**問題：** STK_UPDATER 跑好幾輪，工作站 Stk32.exe 還是舊版；且 station 目錄殘留 SHUTDOWN.FLG / SHUTDOWN.ACK 沒清乾淨。

**根因（全在 `stk_updater.py`）：**
- `file_changed()` 只比 mtime：FTP 上 EXE 與工作站既有檔 mtime 剛好相同（沒重編就重打包、或 MFMT/時區換算進位到同一秒）時誤判「未變更」靜默跳過。
- `distribute()` 把「複製」與「unchanged 跳過」都算進 `results['ok']`，log 只印 `738 OK` → 看不出 Stk32.exe 到底有沒有被覆蓋。
- `save_local_version()` 存 `datetime.now()` 而非 `manifest['updated']`，且不論成敗都無條件 bump 版本 → 關鍵檔複製失敗仍 bump，下輪判定已最新而跳過，舊版鎖死。
- `cleanup_shutdown_flg()` 只刪 `.FLG`，從不刪 STK32 退出時寫的 `.ACK` → 累積殘留（現場找到 S001/SHUTDOWN.ACK）。

**解決方法：**
- `file_changed()` 加「檔案大小」比較，size 不同一律視為已變更。
- `distribute()` 分開統計 copied/unchanged/fail，對 EXE 目錄逐檔印 COPY/SKIP（附 size+mtime）。
- `save_local_version()` 改存 manifest 的 updated；main() 僅在無 fail 時才 bump 版本，失敗下輪自動重試。
- `cleanup_shutdown_flg()` 一併刪 `SHUTDOWN.ACK` 並逐檔記數、失敗印 WARNING。

**踩坑：**
- 表面「738 OK, 0 FAIL」會誤導為成功，實際關鍵 EXE 可能是 unchanged 靜默跳過 → 加 copied/skip 分類與逐檔 log 才看得出來。
- `save_local_version` 存 now() 反而讓版本判斷永遠不相等（每次全量重抓），是另一個隱藏 bug。

**修改檔案：** `build\update_server\stk_updater.py`（file_changed/distribute/save_local_version/cleanup_shutdown_flg/main 共 5 段）；rebuild `dist\stk_updater.exe`（PyInstaller）並部署到 `W:\stk_updater.exe`（備份 `.bak_removeack_filechanged_20260601_2350`）；清除殘留 `W:\S001\SHUTDOWN.ACK`。

### 23:34 功能提示 Alt+H「只開不關」修正（新增對稱 RemoveHintLabel）

**問題：** BUYSELL 等畫面第一次按 Alt+H 開啟功能提示，第二次按本應關閉卻「只開沒關」；即使畫面被還原，滑鼠移過原提示位置提示字又浮現。

**根因：** 功能提示用 `AddHintLabel()` 畫出，會在全域 `M->aMsReg` 註冊常駐滑鼠熱區（hover 重畫提示字、點擊注入按鍵）。`ShowXxxHelp()` 只有 `IF MEMVAR->lHelp`（.T.）分支去 AddHintLabel，缺 `.F.` 分支拆熱區 → 關閉時畫面雖由 Alt+H 呼叫端 RestScreen 還原，但 aMsReg 熱區殘留 → hover 又重畫。

**解決方法：**
- `compat\mouse_handler.prg` 新增對稱 `RemoveHintLabel( nRow, cLabel )`：以「列號+提示文字」反向掃 aMsReg，`ADel`+`ASize` 移除符合熱區；只動 aMsReg、不碰畫面；反向迴圈兼清重複熱區。
- TOOLS.PRG 共 9 個 ShowXxxHelp 補對稱關閉：7 個無 ELSE 者（BuySell/PayRece/Inquiry/Enquiry/Quot/FPurArr/DPurArr）新增 ELSE + RemoveHintLabel；2 個有 ELSE 者（PartsF9/AskParts）ELSE 先補 RemoveHintLabel 再保留原精簡提示。
- RemoveHintLabel 的標籤運算式逐字沿用 AddHintLabel 原 bytes，保證 runtime 比對一致。

**踩坑：**
- 誤判為畫面還原問題（一度想在 BottomMenu 加 SaveScreen/RestScreen）；真正根因是 aMsReg 熱區殘留。
- 「有 ELSE」不代表「有關閉」：PartsF9/AskParts 的 ELSE 只另畫精簡提示，仍須先拆 .T. 熱區。
- 以欄位定位會錯（.F. 無 SAY，Col() 鏈接無法重現），改以列號+標籤文字比對。
- BIG5 PRG 用 Python bytes-only 自動解析 AddHintLabel（逗號切第1欄 nRow、第3欄 cLabel）產生 RemoveHintLabel，中文註解 `.encode("big5")`。

**修改檔案：** `HBSTK\compat\mouse_handler.prg`（+RemoveHintLabel）、`HBSTK\source\TOOLS.PRG`（9 個 ShowXxxHelp +ELSE/RemoveHintLabel）

**報告：** `HBSTK\doc\功能提示開關修正_RemoveHintLabel_20260601.pdf`

---

### 22:36 標籤列印 LPT1 崩潰修正（漏轉換 PRN 導向）

**問題：** 標籤列印（P142 → MailForm → MailKey → LabelPrint）跳出 `TERM/2014 Create error: LPT1 (DOS Error 2)`，程式追蹤 QQOUT(0) → LABELPRINT(1410)。

**根因：** LABEL.PRG 的 `LabelPrint()` 漏做「轉換目標2」的 PRN 檔導向——只設 `SET(_SET_PRINTFILE,"LPT1")`，未呼叫 `OpenSpool`/`CloseSpool`。第 1410 行 `??`(QQOUT) 在 `SET(_SET_PRINTER,.T.)` 下去開實體 LPT1 裝置，現代 Windows 無 LPT1 → 建立失敗。

**解決方法：** 比照同檔已驗證的兄弟函式（LblFrm.Prn 路徑 ~793/1001）與 P26.PRG 的 PrintActFrm 範式，於 LabelPrint 補上兩行：
- 設定段 `lConsole := SET(...)` 後 → `OpenSpool( "Label.Prn", , @cPrintPort )`
- 收尾段 `SET(_SET_PRINTFILE,"")` 後 → `CloseSpool( "Label.Prn", , cPrintPort )`
- 保留 Bit14 LPT1 SET（`??` 延遲開檔，OpenSpool 隨後覆蓋 printfile，殘留無害）

**踩坑：**
- 兩個錯誤夾雜（CCODEHIGH + LPT1），以「發生時間」最新者（LPT1 22:28:50）為準才查對目標。
- `python -c` 內嵌中文被 shell 編碼弄亂 → big5 encode 失敗；改寫 `.py` 檔（UTF-8）執行，PRG 註解仍用 Python bytes-only `.encode("big5")`。
- 插入註解行後 `??` 行號位移；舊 log 顯示 1410 屬過期記錄，須用新 build 重測（build 時間 22:36 晚於所有錯誤記錄）。

**修改檔案：** `HBSTK\source\LABEL.PRG`（LabelPrint，+OpenSpool/+CloseSpool +BIG5 註解）

**報告：** `HBSTK\doc\Stk32_標籤列印LPT1修正_20260601.pdf`

---

### 07:30 IndexDbf/PackDbf Broadcast Shutdown + ACK Mechanism

**Problem:** IndexDbf/PackDbf need to notify other workstations to exit before reindexing/packing

**Solution:**
- `BroadcastShutdown()`: Write SHUTDOWN.FLG to W:\S001~S012 (skip self), poll for ACK/FLG status
- `CleanupShutdownFlg()`: Clean up all FLG/ACK after operation
- `CheckShutdown()`: Write SHUTDOWN.ACK when FLG detected (ACK = station is online)
- `CheckShutdown()`: FLG path priority: LOC_SYS first, hb_DirBase fallback

**Polling logic:**
- FLG gone = station exited
- ACK exists + FLG exists = waiting (station online, countdown active)
- No ACK after 8 sec = station offline, delete FLG, skip
- 90 sec timeout = force continue
- Bottom bar shows: `[等待工作站退出] NNN秒  S00X 狀態`

**Traps:**
- First version: polled ACK immediately after writing FLG → all stations marked offline (CheckShutdown runs every 5 sec)
- Fix: 8-second grace period before declaring "offline"
- CheckShutdown FLG path must match where FLG is written (LOC_SYS vs hb_DirBase)

**Files:**
- `HBSTK/source/DBF.PRG` (BroadcastShutdown, CleanupShutdownFlg)
- `HBSTK/compat/shutdown.prg` (CheckShutdown ACK + dual path)

### 00:13 Stk32 系統參數 PreviewMode/DEBUG_LOG 寫入 W:\S00X\System.ini（v49）

> （本條原文因編碼損壞，內文依 ASCII 骨架與記錄重建）

**問題**：`c_code/usrfont_api.c` 有 3 處 INI 路徑寫法（`BuildIniPath` + 2 處 inline），需統一改寫到 `W:\S00X\System.ini`。

**原理**：W:\S00X 是 master truth、C:\STK\S00X 是同步快取。系統參數（PreviewMode、DEBUG_LOG 等）統一寫回 W:\S00X\System.ini，再由 Agent 同步到各本機，避免本機與網路兩份不一致。

**配合 Agent v50 同步**：避免整檔 newest-wins 互蓋，改 per-key ownership merge；PreviewMode/DEBUG_LOG 等 key 各有擁有者，兩端各改不同 key 不會互相覆蓋。

**踩坑**：
- C 的 char literal `'\'`（4 chars：27 5C 5C 27）在 Python heredoc 內反斜線會被吃掉，寫入時要特別處理。
- `_compile_usrfont_c.bat` 編 `usrfont_c.c` 時會 #include `usrfont_api.c`；要把 api.c 一併交給 hbmk2。

**修改檔案**：
- `E:\ConvertFW\HBSTK\c_code\usrfont_api.c`（備份 `.bak_iniW_20260531_1820`）
- 重編 `E:\ConvertFW\HBSTK\build\Stk32.exe`

### 00:13 ini_editor 各站 schema 同步（v51）

> （本條原文因編碼損壞，內文依 ASCII 骨架與記錄重建）

**功能**：ini_editor.exe 對所有 S001~S020 工作站做統一管理，逐站比對每個 INI vs schema：
- 缺少的 key → ADD（用 schema 預設值補上）
- 多餘的 key → REMOVE
- MIGRATION_MAP 對應的 key → MIGRATE（搬到新 INI，保留原值）

介面用 Treeview，左側階層「工作站 / INI / Section / Key / 值」，可展開/折疊，並提供新增/刪除/遷移操作。

**MIGRATION_MAP**：
```python
MIGRATION_MAP = {
    ("stk.ini", "Debug", "DEBUG_LOG"): ("System.ini", "", "DEBUG_LOG"),
}
```

**STK_INI_KEYS 加入 `KeyPosEnable`**：每個 key 帶預設值（IndexDbf 用）、label「顯示名稱」、help「說明」。

**踩坑**：
- `IniFile.lines` 是 bytes（BIG5），不能直接用 `_strip_empty_sections` 的 string regex 處理，要先解碼。
- REMOVE 只 filter `entries` 沒刪 `lines`，寫回時 key 還在；新增 `IniFile.remove_key()` 同步刪 lines 才正確。

**修改檔案**：
- `E:\ConvertFW\HBSTK\build\ini_editor.py`
- 重編 `E:\ConvertFW\HBSTK\build\dist\INI_Editor.exe`，部署到 `C:\STK\TOOLS\ini_editor.exe`

## 2026-05-29

### 13:46 Relay 改 A/B 部署（仿 Stk_Agent 套路）

**設計改變**：放棄 `.new` staging，改成真正 A/B 部署，徹底解 file locked 問題。

**架構**：
```
W:\socket\relay\
  relay_server_A.exe    ← 兩份，watchdog 跑最新版
  relay_server_B.exe
  watchdog_relay.bat    ← 每 5 分鐘 schtasks，找最新 mtime；
                          有舊版在跑 → kill + start newest；
                          port 9530 不在 listen → 認定殭屍 → 重啟
  install_relay.bat     ← 改寫：註冊 STK_Relay_Watchdog（每 5 分鐘）
  update_relay.bat      ← Commander preset Ĳ發，等於跑一次 watchdog
```

**stk_updater deploy_socket 行為**：偵測 relay_server.exe 來源 → 比對 A/B 哪份鎖住（is_file_locked）→ 寫到沒鎖那份。一定成功。

**install_relay.bat 改寫重點**：
- 首次安裝若沒 A/B 自動從 relay_server.exe 複製出 A/B
- 註冊 schtasks `STK_Relay_Watchdog`（每 5 分鐘 + /ru SYSTEM）
- 保留 `STK_Relay_Server` task name 但 /tr 指向 watchdog_relay.bat（給手動 schtasks /run 用）
- 立即 call watchdog 啟動

**Commander preset 改名**：
- 從 `"Update Relay (SERVER only)"` 改成 `"Update Relay Now (select Relay host)"`
- 不再強制 SERVER；任何站只要有 W:\socket\relay\update_relay.bat 就能跑（但只有 Relay 主機有作用）

**為什麼 A/B 對 Relay 比 .new 好**：
- stk_updater 永遠寫得進去（A 鎖→寫 B，B 鎖→寫 A）
- 不需 Commander 人工介入
- 跟 Agent 一致，學習成本低
- watchdog 兼做殭屍偵測（process 在但 port 不 listen）

**修改檔案**：
- `E:\\ConvertFW\\SOCKET\\Relay\\watchdog_relay.bat`（新增）
- `E:\\ConvertFW\\SOCKET\\Relay\\install_relay.bat`（改寫）
- `E:\\ConvertFW\\SOCKET\\Relay\\update_relay.bat`（²化成Ĳ發 watchdog）
- `E:\\ConvertFW\\HBSTK\\build\\update_server\\stk_updater.py`（Relay A/B 邏輯）
- `E:\\ConvertFW\\HBSTK\\build\\update_server\\pack_and_upload.bat`（加 watchdog_relay.bat）
- `E:\\ConvertFW\\SOCKET\\StkCommander\\python\\stk_commander_qt.py`（preset 改名）

### 13:31 改用 Commander EXEC preset 處理 Relay 自我更新

**設計調整**：原本 stk_updater 內建 relay_self_update 邏輯被退掉，改由 Commander 用 EXEC preset Ĳ發。

**理由**：
- stk_updater 是無人值守 cron 跑的，自動重啟 Relay 風險高（剛好別人在用 Relay 會被切斷）
- Commander 是人工操作，由管理者選時機Ĳ發更安全

**架構**：
1. stk_updater 只負責下載新 relay_server.exe 到 C:\TEMP\UPDATE\socket\relay\（覆蓋 C:\STK\socket\relay\ 鎖住就讓它 FAIL，留待手動處理）
2. `update_relay.bat`（放在 C:\STK\socket\relay\）：taskkill → copy → schtasks /run 重啟，全程寫 update_relay.log
3. Commander 新 preset `"Update Relay (SERVER only)"` → 對 Server 站送 EXEC 跑 update_relay.bat
4. Relay 端 RELAY_SHUTDOWN 指令保留（Commander 將來可改用更優雅的方式）

**修改檔案**：
- `E:\\ConvertFW\\SOCKET\\Relay\\update_relay.bat`（新增）
- `E:\\ConvertFW\\SOCKET\\StkCommander\\python\\stk_commander_qt.py`（加 preset）
- `E:\\ConvertFW\\HBSTK\\build\\update_server\\stk_updater.py`（移除 relay_self_update）
- `E:\\ConvertFW\\HBSTK\\build\\update_server\\pack_and_upload.bat`（加 update_relay.bat 打包）

### 13:28 Relay 自我更新機制（解 file locked 死結）

**問題**：Relay EXE 每次 stk_updater 跑都是 `FAIL [WinError 32] file locked`，永遠裝不上新版 → KMSN 隔離形同虛設。

**解法**（雙端配合）：
1. `relay_server.py` 新增 `RELAY_SHUTDOWN` 指令：收到→回 ACK→另起 thread 1 秒後 `os._exit(0)` 自殺
2. `stk_updater.py deploy_socket()` 偵測到 `relay/relay_server.exe` 覆蓋失敗 + `WinError 32`：
   - 連 Relay 發 `RELAY_SHUTDOWN`
   - poll `is_file_locked` 等檔案解鎖（最多 15 秒）
   - `shutil.copy2` 覆蓋新版
   - `schtasks /run /tn "STK_Relay_Server"` Ĳ發 `install_relay.bat` 註冊的排程任務重啟（fallback: 直接 spawn）

**為什麼用 schtasks /run**：install_relay.bat 已用 `/sc onstart /ru SYSTEM` 註冊，schtasks /run 可以手動Ĳ發同一個 task，繼承 SYSTEM 權限不需要 stk_updater 自己 elevate。

**踩坑**：
- Relay 收到 SHUTDOWN 不能立刻 exit，否則 caller 還沒收到 ACK 就斷線。用 daemon thread sleep 1 秒再 exit
- `os._exit(0)` 而不是 `sys.exit()`——後者只結束目前 thread，daemon 結束時整個 process 才會走，但其他 client thread 可能還活著拖延

**修改檔案**：
- `E:\\ConvertFW\\SOCKET\\Relay\\relay_server.py`（加 RELAY_SHUTDOWN）
- `E:\\ConvertFW\\HBSTK\\build\\update_server\\stk_updater.py`（加 relay_self_update helper + deploy_socket hook）
- 兩支 EXE 重打包

### 13:23 log 加 EXE 編Ķ日期 + 印實際 target

- 三支 EXE 啟動時 log 印 `EXE: path  built=YYYY-MM-DD HH:MM:SS  size=N` 方便驗證版本
- PUSH_AND_UNZIP / SHUTDOWN log 改成印實際 target（含 KMSN 前綴），看 KMSN 隔離有沒有真的送出去

### 13:05 修補 COMMANDER 跨客戶污染（同一致命傷的第二處）

**問題**：複查發現 Commander 也有同類漏洞——而且因為 `TARGET_ALL="*"` 廣播比 Updater 更嚴重。

**漏洞清單**：
1. **致命**：`TARGET_ALL="*"`（PING/STATUS 全選/不選都走這條）→ Relay 無條件廣播到所有客戶 agent
2. **中**：`_send` SEL 模式 fallback `if s.kmsn else s.name` → 老 agent 沒 KMSN 就退化成純 station 名，打到別人
3. **小**：`_send_cmd`（PING 廣播用的較底層 sender）沒帶 `kmsn` field

**解法**：
- `relay_server.py`：`forward_to_agents()` 的 `*` 分支加 KMSN 過濾——packet 帶 `kmsn` 就只廣播給同 KMSN 的 agent；沒帶就 log ĵ告維持舊行為（老 Commander 相容）
- `stk_commander_qt.py _send_cmd`：補上 `'kmsn': self.app.kmsn` field
- `stk_commander_qt.py _send` SEL：移除 fallback，KMSN 為空時拒送 + log

**修改檔案**：
- `E:\\ConvertFW\\SOCKET\\Relay\\relay_server.py`
- `E:\\ConvertFW\\SOCKET\\StkCommander\\python\\stk_commander_qt.py`
- 三支 EXE 重打包：`stk_updater.exe`、`relay_server.exe`、`stk_commander.exe`

**教訓**：找到一個跨客戶污染漏洞 → 一定要把所有「會發指令到 Relay」的 client 全部複查（Updater、Commander、未來的 Web Console 等）。核心防線放在 Relay 端（`*` 廣播強制 KMSN 過濾）比靠每個 client 自律可靠得多。

### 12:55 修補 STK_UPDATER 跨客戶污染致命傷

**問題**：客戶 A 跑 STK_UPDATER 會把 B、C 客戶的 STK32 一起 SHUTDOWN/覆蓋（多客戶共用同一台 Relay 時）。

**根因**：
1. Relay `forward_to_agents()` 只用 station 名稱比對（A 客戶 S001 跟 B 客戶 S001 同名→廣播到兩邊）
2. Relay 自作聰明把 `kmsn:station` 前綴剝掉再比對，等於 KMSN 形同虛設
3. `stk_updater.py` PUSH_AND_UNZIP 的 target 沒加 KMSN 前綴
4. `stk_updater.py` agent filter 用 `or not a.get('kmsn','')`，把空 KMSN 的老 agent 全部拉進來

**解法**：
- `relay_server.py forward_to_agents()`：解析 `KMSN:STATION` target，比對時要求 station 與 kmsn 都符合；KMSN 不對就拒絕匹配；剝前綴改在匹配之後（保留老 agent 相容性）
- `stk_updater.py` PUSH_AND_UNZIP target 加上 `{kmsn}:{station}` 前綴
- `stk_updater.py` 嚴格 KMSN filter，移除空 KMSN 包容條款

**修改檔案**：
- `E:\\ConvertFW\\SOCKET\\Relay\\relay_server.py`（forward_to_agents 改寫）
- `E:\\ConvertFW\\HBSTK\\build\\update_server\\stk_updater.py`（target/filter）
- 兩支 PyInstaller 32-bit 重打包完成

**踩坑**：
- 之前在 Relay 加的 strip-kmsn hack 為了相容老 agent，但放在「比對前」就把整個 KMSN 隔離機制廢掉了。正解是：比對時保留 KMSN，**比對完成後**才在送往老 agent 的 payload 剝掉。

### 11:50 StuffString Alt+Z 數字/日期欄位 Argument Error 修正

**問題：**
P113 零件基本資料，在數字欄位按 Alt+Z（貼上期別）產生 `BASE/1073 Argument error: <`

**根因：**
`TLIB.PRG StuffString()` 對數字型(N)/日期型(D) GET 使用 `PadL(cPhase, Len(buffer))`，
把字串塞進數字欄位 → GET 系統 `PutMask` 用 `<` 比較時型別不匹配

**解法：**
- 數字型 "N"：`PadL(cPhase,...)` → `Val(cPhase)`
- 日期型 "D"：`PadL(cPhase,...)` → `CToD(cPhase)`

**修改檔案：** `HBSTK/source/TLIB.PRG` (StuffString, line 1760)

### 10:45 Agent 生存加固 + 配套改進 (10 個大項)

**主訴求**：Agent 經常死 + watchdog 失效 + TCP push 失敗 + INI 被覆蓋 + DPI 截圖太小 + 7 工作站全擠 S001。

**Agent 生存五層防護**：
| 層 | 機制 |
|---|---|
| L1.A | ExecuteCommandRelay BEGIN SEQUENCE/RECOVER (cmd 處理 crash 不傷主 loop) |
| L1.B | Main 外層 SEQUENCE 包整個 ListenLoop (任何位置 crash 自動 RECOVER) |
| L2 | crash → 5 秒重啟 RelayListenLoop (無限) |
| L3 | 90s 沒收到資料 → 強制 reconnect (解 TCP half-open zombie) |
| L4 | 30s 寫一次 .heartbeat |
| L5.A | watchdog 用 `%~dp0` 不依賴 HKCU (Task Scheduler SYSTEM 跑也找得到) |
| L5.B | watchdog `/dev/null` → `nul` (13 處) — Windows 標準 null device |

**9 個踩坑**：
| # | 坑 | 修法 |
|---|---|---|
| 1 | AgentErrorHandler BREAK 找不到 SEQUENCE → process 死 | Main 外層 SEQUENCE |
| 2 | TCP half-open 偵測不到 | 90s 沒 recv → reconnect |
| 3 | watchdog `/dev/null` 永遠沒生效 | 改 `nul` |
| 4 | install_agent.bat 沒 errorlevel | 加 LOG 到 W:\socket\install_log_{COMPUTERNAME}.txt |
| 5 | TCP push 失敗 (WinError 10060 firewall silent drop) | Agent 改從 I:\tools\ 自拉，繞 firewall |
| 6 | SyncNow 把使用者 INI 改的蓋掉 | bidirectional newest-wins for .ini |
| 7 | bidirectional 無限 ping-pong (mtime 寫入變 NOW) | content-first 比對，相同直接 skip |
| 8 | PowerShell 截圖 DPI 200% 變 1/4 | base64 + SetProcessDPIAware() |
| 9 | 7 工作站錯誤全傳 S001 (cTopWare 共用) | FTP 路徑改用 trigger 內 COMPUTER |

**修改檔案**：
- `SOCKET/StkAgent/source/stk_agent.prg` — 主 loop SEQUENCE + dead-detect + CheckLocalInstall + CheckIniSync + bidirectional sync + ProcessErrorPending COMPUTER 優先
- `SOCKET/StkAgent/build/watchdog_agent.bat` — nul + %~dp0
- `SOCKET/StkAgent/build/install_agent.bat` — LOG + errorlevel (INSTALL + UPDATE)
- `SOCKET/StkAgent/build/install_agent_silent.bat` — chcp 950 + LOG + station priority
- `HBSTK/build/update_server/stk_updater.py` — LIST_AGENTS 診斷 + 寬鬆 filter + shared_tools_dirs
- `HBSTK/source/ERRORSYS.PRG` — DPI-aware base64 截圖 + ARGS= 欄位

**新增 INI 設定 (stk_agent.ini)**：
- `LOCAL_PATH=C:\STK\S00X` — 啟用 SyncFilesFromNetwork 必填 (PrnAsk 從本機跑)

**新增 INI 設定 (stk_updater.ini)**：
- `[DeployZip] SHARED_TOOLS_DIR=D:\images\tools;I:\tools` — ZIP 推到共享路徑 (分號分隔多路徑)

**Build**：
- `Stk_Agent.exe` 1.09 MB (10:35)
- `Stk32.exe` 7.07 MB (10:43, 含 DPI fix + ARGS)
- `stk_updater.exe` 6.78 MB (07:39)

**報告**：[SOCKET/StkAgent/doc/Agent生存加固_20260529.pdf](../../SOCKET/StkAgent/doc/Agent生存加固_20260529.pdf)

---

## 2026-05-28

### 22:00 INI Editor auto-fill + PrnAsk Preview zoom save

**INI Editor:**
- Open older INI with missing keys -> auto-fill from meta defaults (section-aware)
- PRNASK_HOME_PATH ComboBox (C:\STK\S001~S020)
- Repackaged 32-bit EXE, deployed to C:\STK\TOOLS

**PrnAsk Preview zoom:**
- GDI Preview: bMouseWheel +/-2% scroll zoom
- GDI/WebView2: save PreviewZoom to System.ini on exit
- New FwSaveIniKey() generic INI key writer

**Files:** `HBSTK/build/ini_editor.py`, `PRNASK/source/fw_prnout.prg`

### 17:22 自動錯誤回報 完整版（雙截圖 + 剪貼簿 + Alt+C 升級）

**承接 15:00 條目** — 把 4 個小時的踩坑解決。

**完成項目**：
1. **雙截圖** `_before.bmp`（同步，DefError 第一行）+ `_after.bmp`（非同步，MessageBox 顯示 500ms 後拍）
2. **trigger 內容擴增**：加 ERR_DESC/ERR_OP/COMPUTER/USER/STACK 共 11 個欄位
3. **改用 hb_MemoWrit 一次性寫入**（取代 FCreate + 8x FWrite + FClose）
4. **`.txt` 改用 Error.TXT 完整 boxed 格式**：SET ALTERNATE TO (cTxtFile) 直接寫到 per-timestamp 檔
5. **`SET CONSOLE OFF`** 加到 ErrorMsg.TXT 段，止住畫面 spam
6. **Agent 改讀 trigger 的 FILES= 欄位**，不再寫死 .bmp/.txt 列表
7. **FtpUploadFile 修兩個 bug**：
   - 真檢查 STOR 回應（原本無條件 RETURN .T.）
   - STOR 後吃掉 150 preliminary reply（原本只 recv 一次拿到 150 誤判 FAIL）
8. **KeyAltC 升級**：截圖 + 寫 trigger + 內容塞剪貼簿（Ctrl+V 可貼）
9. **KeyAltC 改綁 `Ctrl + Keypad+`**（KP_CTRL_PLUS = 400）取代殭屍 K_ALT_6
10. **STK.PRG `aExeInfo` 抓 Stk32.exe**（原本抓不存在的 Stk.exe）

**9 個踩坑（詳見 PDF）**：
| # | 坑 | 解 |
|---|---|---|
| 1 | 相對路徑 `.\subdir\` Harbour FCreate/hb_MemoWrit 假成功 | 改絕對路徑 `hb_CurDrive()+":\"+CurDir()+"\\error_pending\\"` |
| 2 | 多次 FWrite 連續呼叫產生 0-byte 檔 | 改 hb_MemoWrit 一次性 |
| 3 | FtpUploadFile 無條件 RETURN .T. | 檢查 reply 開頭 "2" |
| 4 | FtpUploadFile 沒吃 150 preliminary reply | STOR 後 recv 一次吃 150 |
| 5 | Agent 寫死檔名清單 | 改讀 trigger FILES= 欄位 |
| 6 | ErrorMsg.TXT 段 QOUT 噴螢幕 | 加 SET CONSOLE OFF |
| 7 | SET ALTERNATE 跨進程 race | 同進程直接寫到目標位置 |
| 8 | KeyAltC 殭屍綁定（K_ALT_6 被下行覆蓋） | 改綁 KP_CTRL_PLUS |
| 9 | aExeInfo 抓 Stk.exe 拿空 | 改抓 Stk32.exe |

**修改檔案**：
- `HBSTK/source/ERRORSYS.PRG`（DefError + KeyAltC + GetStackStr）
- `HBSTK/source/STK.PRG`（Stk32.exe）
- `HBSTK/source/APPSETUP.PRG`（KP_CTRL_PLUS 綁定）
- `SOCKET/StkAgent/source/stk_agent.prg`（ProcessErrorPending + FILES= 解析）
- `SOCKET/shared/ftp_helper.prg`（STOR 流程修正）

**測試流程**：
- 自動：故意觸發錯誤 → 4 檔上傳 FTP `/H/stk-error/{KMSN}/{S00X}/`
- 手動：按 Ctrl+Keypad+ → 截圖+寫檔+上傳+剪貼簿
- 上傳成功本地清空；失敗保留下次重試

**報告**：[自動錯誤回報_完整版_20260528.pdf](自動錯誤回報_完整版_20260528.pdf)

---

### 15:00 自動錯誤回報（截圖+trigger+FTP）

**問題**：原 Clipper 設計只寫 Error.TXT + 顯示「請傳真到 (02)8209-2893」，現在客戶都不裝傳真機，畫面也早被使用者按掉看不出當時狀況。

**解決方法**：HBSTK 與 Agent 雙端配合，零客戶端設定。

**HBSTK 端**（`source/ERRORSYS.PRG`）：
- `DefError()` 進入第 1 件事：`hb_run` PowerShell 同步截圖到 `.\error_pending\YYYYMMDD_HHMMSS.bmp`（~300ms，使用者連 MessageBox 都還沒看到）
- 結尾寫 `.txt`（Error.TXT 副本）+ `.trigger`（純 ASCII 鍵值對：TIME/KMSN/STATION/ERR_SUBSYS/ERR_CODE/PROC/FILES）
- 資料來源：`MEMVAR->cKMSerialNo`（STK.PRG:17 從 STK.INI）、`MEMVAR->cTopWare`（SYSSETUP.PRG:60）

**Agent 端**（`SOCKET/StkAgent/source/stk_agent.prg`）：
- 主 loop 既有 2 秒 RelayRecv timeout 順手呼叫新增的 `ProcessErrorPending(s_cStkPath)`
- 掃 `{STK_PATH}\error_pending\*.trigger` → 收集同 timestamp 檔案 → 上傳 FTP `/H/stk-error/{KMSN}/{S00X}/` → 成功才 FErase
- 新增 `ParseTriggerFile()` (hb_ATokens + key=value parse)
- 既有 ERROR_REPORT case 的 FTP 路徑也補上 `/{s_cStation}` 後綴

**踩坑**：
| 坑 | 解 |
|---|---|
| 截圖時機（trigger file 太晚） | HBSTK 自己在 DefError 第一行先截圖，不等 Agent |
| INI 雙邊同步麻煩 | 不加 INI，雙方都用 `{STK_PATH}\error_pending\` 推導 |
| polling 多餘 timer | 併入既有 2 秒 RelayRecv timeout |
| BIG5 寫 trigger | 改純 ASCII，中文放在 .txt 副本 |
| FTP 多 station 混淆 | cFtpDir 加 `/{s_cStation}` |

**修改檔案**：
- `HBSTK/source/ERRORSYS.PRG` — LOCAL 宣告 + DefError 開頭截圖 + 結尾 trigger
- `SOCKET/StkAgent/source/stk_agent.prg` — FTP 路徑 + watcher + 新增 2 個 STATIC FUNCTION

**Build**：
- HBSTK: `_rebuild_claude.bat` → `Stk32.exe` 7.06MB
- Agent: `hbmk2 stk_agent.hbp -comp=bcc` → `Stk_Agent.exe` 1.06MB

**FTP 目錄結構**：
```
/H/stk-error/{KMSN}/{S00X}/
  ├ 20260528_073012.bmp     ← 當時螢幕截圖
  ├ 20260528_073012.txt     ← 錯誤訊息 (BIG5)
  └ 20260528_073012.trigger ← 元資料 (ASCII)
```

**報告**：[自動錯誤回報_20260528.pdf](自動錯誤回報_20260528.pdf)

---

### 11:40 Deploy ZIP 檢查機制修正 + 防火牆 + UAC 提權

**問題：**
- deploy_status.json 帳本導致目錄被刪後永不重推
- S001（本機）不在 Relay Agent 清單，拿不到 WebView2/HDOS4
- hb_DirExists 前綴匹配（C:\WEBVIEW2 匹配到 C:\WEBVIEW2WEBVIEW2）
- install_agent BAT 的 netsh 防火牆指令無管理員權限，TCP 9529 被擋

**解決方法：**
- stk_updater.py: 移除 already_deployed 跳過邏輯，每次對所有線上 Agent 發 PUSH_AND_UNZIP
- stk_updater.py: 加本機 local_dir 不存在時用 zipfile 自動解壓
- stk_agent.prg: hb_DirExists(path + Chr(92)) 加尾端反斜線精確匹配
- install_agent.bat / install_agent_silent.bat: schtasks /rl highest 靜默管理員提權
- BAT 加 netsh show allprofiles/currentprofile/findstr 驗證，移除 >nul 2>&1

**踩坑：**
- install_agent.bat UPDATE 段 socket\agent 路徑中 \a 被吃成 0x07（bell），用 Python bytes replace 修正
- Windows FindFirstFile 8.3 短檔名前綴匹配，必須加尾端 \ 才精確
- schtasks /rl highest 可靜默提權，Start-Process -Verb RunAs 會彈 UAC

**修改檔案：**
- E:\ConvertFW\HBSTK\build\update_server\stk_updater.py
- E:\ConvertFW\SOCKET\StkAgent\source\stk_agent.prg
- E:\ConvertFW\SOCKET\StkAgent\build\install_agent.bat
- E:\ConvertFW\SOCKET\StkAgent\build\install_agent_silent.bat

**報告：** doc\DeployZipCheck_20260528.pdf

### 03:30 PrnAsk Preview 強化 + INI Editor + Commander 細節

**WebView2 Preview 修正：**
- ESC 關閉：SetKey(27) 全域攔截（WebView2 吃掉 JS keydown）
- 右鍵關閉：HTML JS `contextmenu` event + `doClose()`
- 全螢幕：`FROM 0,0 TO GetSysMetrics(1)/16, GetSysMetrics(0)/8`
- 預設 zoom 230%：JS `sc=2.3`
- 滾輪縮放 ±2%：`{passive:false, capture:true}` 攔截 + 防抖 150ms
  - 根因：`CUR_PG` 變數名錯誤，應為 `cp`
  - 即時更新百分比顯示（`upd()`），150ms 後重新渲染（`goPg(cp)`）

**GDI Preview 修正：**
- 全螢幕：95% 螢幕寬高
- 預設 zoom 230%：`s_nPrvZoom := 230`
- ESC + 右鍵關閉（已有）
- 鍵盤 +/- 縮放 ±2%（SetKey 43/45）
- 滾輪被 scrollbar 攔截，無法繞過（暫用鍵盤替代）

**CheckWebView2Reg 修正：**
- 原只查 Registry（正常安裝的 EdgeWebView）
- 加 fallback：檢查 `C:\WebView2` 目錄（獨立安裝）
- 修正 PreviewMode=0(Auto) 無法切到 WebView2 的問題

**INI Editor：**
- System.ini 加 `PRNASK_HOME_PATH`（列印設定 group）
- stk_agent.ini 加 `[Sync] LOCAL_PATH`（C:\STK\S00X choices）
- 32-bit EXE 重新打包

**Commander 細節修正：**
- Dashboard 排序：KMSN + Station，每次刷新重排
- 欄位順序：STK 移到 STK32 旁邊
- 欄寬：Station +20, Computer +60, IP -20, 視窗 980px
- MemFree：≥1024MB 顯示 GB（如 1.5GB）
- Message：改 ComboBox，送出自動加歷史（最多 20 筆）
- Exec preset：加 `Run STK Updater (SERVER only)`、`Kill & Restart Agent`
- 32-bit EXE 打包（30MB）
- Relay：forward 前 strip kmsn: 前綴（相容舊版 Agent）

**unblock_all.bat：**
- 加 SmartScreen registry 停用（7 條 reg）
- 自動提權

**修改檔案：**
- `PRNASK/source/fw_prnout.prg` — Preview ESC/右鍵/全螢幕/zoom/滾輪
- `HBSTK/compat/gdi_print.prg` — CheckWebView2Reg fallback
- `HBSTK/build/ini_editor.py` — PRNASK_HOME_PATH + LOCAL_PATH
- `SOCKET/StkCommander/python/stk_commander_qt.py` — 排序/欄位/Message ComboBox/preset
- `SOCKET/Relay/relay_server.py` — strip kmsn: prefix
- `SOCKET/StkAgent/build/unblock_all.bat` — SmartScreen


## 2026-05-27



### 16:35 P113 車種代碼 -> 名稱轉換失效（DIALOG.PRG repaint 副作用）

**症狀：**
- P113 零件新增/修改, 在「車種/品名/品牌」欄輸入代碼後, 應顯示對應的中文名稱
- Clipper 版本 29 年正常, Harbour 版本顯示變成「代碼」而非「名稱」
- 機制: `aElement[9]` 設 `{.T., "Type"}` 表示走 CodeToName 從 Type DBF 查名稱

**根因：**
`DIALOG.PRG GetGeneralInput()` CASE nSysPara=4 結尾多了 3 行 repaint:
```harbour
/* repaint field with normal color after InputData (GET leaves N/BG residue) */
cString := AnyTypeToString( aElement[3,nIndex] )      <- 只用 raw 代碼, 沒走 CodeToName!
@ nInitialRow, ... SAY PadR(cString, nGetLength) COLOR ...
```
這 3 行**永遠執行**, 把上面 IF Eval 內已透過 CodeToName 顯示的「名稱」覆蓋成「代碼」。

**對照原版：**
| 版本 | 是否有這 3 行 |
|------|-------------|
| Clipper 原版 (900/DIALOG.PRG) | 沒有 |
| ClassStk/ | 沒有 |
| FWSTK/ FiveWin 版 | 沒有 |
| **HBSTK/ Harbour 版** | **多加了 3 行** |

時間軸:
- 2026-04-12 老 log 備份：沒有這 3 行
- 2026-05-26 17:30 (bak_quickbox)：已加入這 3 行
- 4/12~5/26 期間有人加入, 但**未寫 UpdateList**, 未留 PDF, 找不到原始動機文件
- 註解只說「GET leaves N/BG residue」(GET 系統殘留 N/BG 顏色)

**解法：**
先把 3 行**註解掉**(不刪除, 保留以便日後參考), 還原 Clipper 行為:
```harbour
// === 註解掉以下 3 行 (20260527): GET 殘留底色修補, 會覆蓋 CodeToName 機制 ===
// 原 Clipper 沒這 3 行 (E:/ConvertFW/900/DIALOG.PRG line 488 之後直接 ENDCASE)
// 副作用: 車種代碼輸入後, 名稱顯示變回代碼, 不走 CodeToName 轉換
// /* repaint field with normal color after InputData (GET leaves N/BG residue) */
// cString := AnyTypeToString( aElement[3,nIndex] )
// @ nInitialRow, nInitialCol+nTitleLen SAY PadR(cString, nGetLength) COLOR IIF(...)
```

**潛在副作用 (待觀察):**
原本 3 行是為了修「GET 殘留 N/BG 底色」, 註解掉後可能某些 GET 流程下出現底色殘留。
若出現, 改用方案 B (保留 repaint 但走 CodeToName)。

**Debug 關鍵手法:**
1. 查 doc / UpdateList 找不到記錄 -> 改用「對照其他 fork」(Clipper / ClassStk / FWSTK)
2. 三個 fork 都沒這 3 行 -> 確認 HBSTK 獨有
3. 對照時間軸: 4/12 沒有 / 5/26 有 -> 鎖定問題引入時間

**修改檔案:** `HBSTK\source\DIALOG.PRG` (line 696~698 區段)

**還未修但發現的相關問題:**
同函式 line 691 邏輯有誤:
```
IF ( M->nKeyStroke # K_ESC .OR. M->nKeyStroke # K_F12 ) .AND. !IsData(...)
```
此 OR 條件**永遠 .T.** (K_ESC ≠ K_F12, nKeyStroke 不會同時等於兩者).
Clipper 原版只有 `# K_ESC`. Harbour 為加 K_F12 處理但邏輯算錯, 應改 `.AND.`。
不影響本次 CodeToName 修正, 列為後續事項。


### 15:30 統一 WriteDebugLog（全專案唯一一份）

**訴求：**
ConvertFW 專案內 WriteDebugLog 定義散落 3 處：
- `HBSTK/compat/clipper_compat.prg` (Stk32 用)
- `PRNASK/source/fw_prnout.prg` (PrnAsk 用，覆蓋 debug_log.prg)
- `PRNASK/source/debug_log.prg` (dead code)

且 Stk32 端 79 個 WriteDebugLog 呼叫**全部失效**（Debug_Stk32.txt 完全沒 PRG 層訊息）。

**根因（Stk32 端 WriteDebugLog 失效）：**
`clipper_compat.prg::WriteDebugLog` 依 `GetIniValue("DEBUG_LOG")` 決定要不要寫，
但 `GetIniValue` Ū的是 **System.ini**，而 `DEBUG_LOG=2` 設在 **stk.ini**：
```
GetIniValue("DEBUG_LOG") -> 永遠返回 ""
Val("") = 0
s_nDebugLog = 0
WriteDebugLog -> RETURN NIL (不寫)
```

**解法：**
1. 新檔 `HBSTK/compat/writedebuglog.prg`，唯一的 WriteDebugLog 定義
2. 內部用 `WdlReadDebugLevel()` 直接解析 stk.ini，¶過 GetIniValue
3. 自動依 hb_ProgName() 選輸出檔（Stk32 -> Debug_Stk32.txt，PrnAsk -> DEBUG_PRNASK.TXT）
4. 移除 `clipper_compat.prg` 的 WriteDebugLog（line 47~92）
5. 移除 `PRNASK/source/fw_prnout.prg` 的 WriteDebugLog（line 20~44）
6. `PRNASK/source/debug_log.prg` 改名為 `.bak_unified_20260527`
7. `stk32.hbp` 加入新檔（在 clipper_compat.prg 前）
8. `prnask.hbp` 加入新檔（在 prn_to_pdf.prg 前）

**新檔 WdlReadDebugLevel() 邏輯：**
```harbour
STATIC FUNCTION WdlReadDebugLevel()
   LOCAL cIni := hb_DirBase() + "stk.ini"     /* 直接Ū stk.ini */
   LOCAL cContent, nPos, nEnd, cVal
   IF !hb_FileExists(cIni) ; RETURN 0 ; ENDIF
   cContent := hb_MemoRead(cIni)
   nPos := At("DEBUG_LOG=", cContent)
   IF nPos == 0 ; RETURN 0 ; ENDIF
   /* 解析數值 */
   ...
   RETURN Val(cVal)
```

**修改檔案：**
- 新增 `HBSTK\compat\writedebuglog.prg`
- `HBSTK\compat\clipper_compat.prg`（移除 WriteDebugLog）
- `PRNASK\sourcew_prnout.prg`（移除 WriteDebugLog）
- `PRNASK\source\debug_log.prg`（改名為 .bak_unified_20260527）
- `HBSTKuild\stk32.hbp`（加入 writedebuglog.prg）
- `PRNASKuild\prnask.hbp`（加入 writedebuglog.prg）

**踩坑：**
1. 第一次正規表達式吃掉太多：`.*?
?
\s+RETURN NIL
?
` 匹配到函式內部第一個 RETURN NIL，把函式後半留下 -> 編Ķ錯誤「ENDIF does not match IF」
2. bPainted codeblock 的尾逗號：移除 WriteDebugLog 後留下 `IIF(...), ;` 接 `), NIL) }` -> 編Ķ錯誤「syntax error at )」-> 手動拿掉逗號
3. .hbp 檔案有 LF 與 CRLF 兩種，要分別處理

**驗證：**
- PrnAsk + Stk32 雙雙 build OK
- 全專案 grep `FUNCTION WriteDebugLog` 只剩一個結果（writedebuglog.prg）

**後續：**
此修正後 stk32.ini `DEBUG_LOG=2` 才會生效，79 個 WriteDebugLog 呼叫終於能寫入 Debug_Stk32.txt。


### 13:14 到貨製單(DOMARR)表身增項焦點卡死修正（FreezeWindow 未配對解凍）

**症狀：**
- 到貨製單按「1.增項」→ AskPartsBox 選零件 → 零件回到表身輸入區
- **整片藍色，無焦點欄位可輸入，整個 UI 卡死**
- 必須**點滑鼠到 GTWVT 視窗外面再切回來**，紅色游標才會出現

**根因：**
- DOMARR.PRG 第 752 行 TYPE 欄位 WHEN 有 `FreezeWindow(.F.)` 解凍
- 第 781 行 READ 前有 `FreezeWindow(.T.)` 凍結
- KEYBOARD 只送 2 個 ENTER，但 cursor 在 ORGORDERNO 起始時沒解凍
- ORGORDERNO/REALNO 的 WHEN 可能 return .F. 不消化 ENTER → cursor 卡住沒到 TYPE
- TYPE 的 WHEN(FreezeWindow(.F.)) 從沒被呼到 → 視窗永久凍結
- 切窗時 Windows 強制重ø打破凍結 → 才看到游標

**歷史脈絡（從 gen_report_20260425.py 找出）：**
- 2026-04-25 加 FreezeWindow 機制，原本只處理 BUYSELL/QUOT/INQUIRY 的 nChoice=2（修改）
- 當時報告明確列「待處理：其他程式的 nChoice=1（新增模式）的 FreezeWindow 尚未處理」
- 後來有人嘗試把 DOMARR 套上 FreezeWindow 但**整合不完整、沒寫進 UpdateList**
- FORARR.PRG 完全沒用 FreezeWindow，正常運作（對照組）

**解決方法（A 方案 - 直接拿掉, 跟 FORARR 對齊）：**
1. DOMARR.PRG line 752: TYPE 欄位 WHEN 拿掉 `FreezeWindow( .F. ),`
2. DOMARR.PRG line 781: READ 前拿掉整行 `FreezeWindow( .T. )`

**為什麼選 A 方案：**
- 這是「半成品」狀態，沒被正式文件記錄
- FORARR.PRG 證明沒 FreezeWindow 也能正常 work
- 副作用只是 KEYBOARD 跳欄時 highlight 閃一下（比卡死好太多）

**修改檔案：**
- `E:\ConvertFW\HBSTK\source\DOMARR.PRG`

**FreezeWindow 知識補充：**
- 在 TLIB.PRG 用 Windows API `WM_SETREDRAW` 強制凍結視窗重ø
- 比 DispBegin/DispEnd 更底層（DispBegin 擋不住 Windows 自己的 paint）
- **必須成對使用**：`.T.` 凍結後，`.F.` 一定要呼到，否則永久凍結
- DOMARR 的問題就是「凍結後解凍呼不到」

**Debug 手法（延續 12:48 QUOT 修正的策略）：**
- 對比結構相似檔案（DOMARR vs FORARR）→ 找出唯一差異
- 善用歷史報告（gen_report_20260425.py）回溯機制設計原意
- 「半成品」狀態：沒寫進 UpdateList 的程式碼變動要特別小心


### 12:48 報價單(QUOT)增項無限閃爍修正（nLastKey 殘留死迴圈）

**症狀：**
- 報價單舊單，連續增項不閃；但**結束增項 → 光棒上移一筆 → 再增項** → 紅框(rows 19-21)無限空白閃爍
- AskPartsBox 根本沒出現，畫面卡死，必須強制 KILL 進程

**根因（程式邏輯 bug）：**
- QUOT.PRG `CASE nChoice = 1`（增項）入口缺 `nLastKey := 0`
- 上方 K_UP/K_DOWN CASE（line 686-694）把 `nLastKey` 設成 K_UP/K_DOWN，從未重置
- 進入增項 DO WHILE 後，下方 IF 條件：
  ```
  IF ( nLastKey = K_INS .AND. AskBarCodeBox(...) ) .OR.
     ( nLastKey = 0     .AND. AskPartsBox(...) )
  ```
- nLastKey = K_UP → 兩個條件都 false → AskPartsBox/AskBarCodeBox 都不會呼叫
- 走 ELSE → `LastKey() != K_ESC` → LOOP → 回頂端 ChkBotMsg 又清紅框 → **無限迴圈**

**為什麼 Clipper 29 年沒事：**
- 這 bug 本來就存在於原 Clipper 邏輯，但 BUYSELL.PRG line 735 已有 `nLastKey := 0`，QUOT.PRG 漏了
- Clipper 時代可能也卡，但畫面更新慢、使用者習慣 Ctrl+Break 跳出，沒被當「bug」回報
- Harbour+GTWVT 下 paint 速度快、閃爍明顯，問題才浮上來

**踩坑（兩次失敗嘗試）：**
1. **誤判為 GTWVT ø圖問題**，包整個 `ChkBotMsg()` DispBegin/End → 失敗
2. **改包 ELSE 分支兩次 Scroll** → 失敗
3. 使用者一句「**邏輯有問題的話 Clipper 應該也閃**」推½錯誤方向
4. 重新分析「AskPartsBox 沒出場 + 一直閃 + 要 KILL」→ 鎖定死迴圈
5. 對比 BUYSELL（同樣 IF 條件但已有 `nLastKey := 0`）→ 找到唯一差異

**解決方法：**
- QUOT.PRG `CASE nChoice = 1` ELSE 分支內，`ShowOrderBody(...)` 前加一行 `nLastKey := 0`
- 跟 BUYSELL.PRG line 735 完全相同的修法

**修改檔案：**
- `E:\ConvertFW\HBSTK\source\QUOT.PRG`（CASE nChoice = 1 入口）

**Debug 關鍵手法：**
- **「Clipper 邏輯一樣 → Harbour 應該也一樣」**：強力篩除 GTWVT ø圖層的胡亂猜測
- **對比結構相似的檔案**（QUOT vs BUYSELL）：找出唯一差異就是答案
- 觀察症狀的「能否自行結束」：能停 = ø圖問題；要 KILL = 死迴圈


### 10:30 Commander PyQt5 重寫 + Agent 更新 + Relay 強化 + 檔案傳輸



**Commander PyQt5 全新重寫：**

- 從頭重寫，解決舊版 threading.Thread 閃退問題（改用 QThread + pyqtSignal）

- 上下分割畫面：Dashboard + Log 同時可見

- 所有指令統一 CommandDialog：Target 列表 + 指令參數

- KMSN 全面支援：註冊、指令、log、Dashboard 全部帶 KMSN，避免同名站混淆

- Dashboard 原地更新（不再 clear+rebuild），checkbox 狀態穩定

- 截圖自動接收：Agent 回傳 JPG base64 → Commander 解碼存檔 → 自動開啟

- 檔案傳輸：支援單檔/資料夾（自動壓 ZIP）、分塊傳輸（6MB/chunk）、Agent 端自動解壓

- AgentVer 欄位：PING 後顯示每站的 Agent EXE 日期版本



**功能按鈕：**

Ping / Status / Shutdown / Kill STK / Message / Exec / Screen / Lock / File



**Exec 預設指令：**



| 預設 | 用途 |

|------|------|

| TeamViewer | 啟動遠端 TeamViewer |

| AnyDesk | 啟動遠端 AnyDesk |

| Install Agent | 安裝 Agent（互動） |

| Install Agent Silent | 靜默安裝 Agent |

| Install Agent Update | 更新模式安裝 |

| Deploy Agent Fix | 一鍵部署修復 |

| Kill & Restart Agent | 隱藏 kill+重啟 Agent |



**Agent 修正：**

- MatchTarget 支援 kmsn:station 格式

- SCREENSHOT 改用 PowerShell 截 JPG（隱藏執行）

- SCREENSHOT Relay 模式 base64 回傳（不再 SendFileTCP 直連造成斷線）

- FILE_RELAY 新指令：接收 base64 檔案 + 分塊組裝 + 自動解壓

- RelayRecv loop 讀取修正（大封包 TCP 分段問題）

- PING 回報加 agent_ver（EXE 日期時間）



**Relay Server v2 重寫：**

- 修正 finally 用錯 key 刪連線

- forward 失敗自動移除死連線

- timeout 60s→90s

- log thread-safe + 自動 rotate（>5MB）

- 每分鐘狀態報告

- max message 1MB→50MB



**FTP 上傳/下載統一用日期比較：**

- ftp_upload.py: EXE/SET/SOCKET 永遠上傳；TOOLS/.zip 比日期

- stk_updater.py: 拿掉 version 字串比較，全部改用日期比較



**stk_updater Agent 更新流程改善：**

- 先送 RESTART_AGENT（新版 Agent A/B 切換）

- 3 秒後送 EXEC _restart_agent.bat fallback（舊版不認識 RESTART_AGENT）

- 部署清單加 _restart_agent.bat



---



### BAT 檔案部署說明



#### 開發端（E:\ConvertFW\）



| BAT | 位置 | 用途 | 使用方法 |

|-----|------|------|----------|

| pack_and_upload.bat | HBSTK\build\update_server\ | 打包+上傳 FTP | 手動 CMD 執行 |

| _rebuild_claude.bat | HBSTK\build\ | taskkill+rebuild+start | AI 開發用 |

| do_rebuild.bat | HBSTK\build\ | 完整 rebuild（含 PrnAsk） | 手動完整重編 |



#### 客戶端 SERVER（W:\）



| BAT/EXE | 位置 | 部署方式 | 用途 | 使用方法 |

|---------|------|---------|------|----------|

| stk_updater.exe | W:\ | FTP 自動 | 從 FTP 下載更新並分發到工作站 | SERVER 手動或排程執行 |

| deploy_agent_fix.bat | W:\ | FTP 自動 | 一鍵 kill+更新+啟動 Agent | SERVER 手動執行 |



#### Relay（W:\socket\relay\）



| BAT | 部署方式 | 用途 | 使用方法 |

|-----|---------|------|----------|

| install_relay.bat | FTP 自動 | 安裝 Relay 排程+啟動（自動提權） | 雙擊，彈 UAC |

| uninstall_relay.bat | FTP 自動 | 移除 Relay 排程+停止（自動提權） | 雙擊 |

| start_relay.bat | FTP 自動 | 手動啟動 Relay | 雙擊 |

| install_service.bat | FTP 自動 | 註冊排程任務（自動提權） | 雙擊 |



#### 工作站（W:\S00X\）



| BAT | 部署方式 | 用途 | 使用方法 |

|-----|---------|------|----------|

| _restart_agent.bat | stk_updater | kill 所有 Agent + 啟動較新版 | Commander EXEC 遠端隱藏呼叫 |

| watchdog_agent.bat | stk_updater | 檢查 Agent 存活 + heartbeat 超時重啟 | 排程每 5 分鐘 |

| watchdog_agent.vbs | stk_updater | 隱藏執行 watchdog_agent.bat | 排程呼叫 |

| install_agent.bat | stk_updater | 安裝 Agent（互動式） | 手動在工作站執行 |

| install_agent_silent.bat | stk_updater | 靜默安裝 Agent | Commander EXEC 遠端呼叫 |



---



**修改檔案總覽：**

- stk_commander_qt.py — PyQt5 Commander 全新重寫

- stk_commander.ini — 加 KMSN、SCREENSHOT_DIR

- stk_agent.prg — MatchTarget/SCREENSHOT/FILE_RELAY/agent_ver

- relay_client.prg — RelayRecv loop + 50MB limit

- json_helper.prg — MatchTarget 支援 kmsn: 前綴

- relay_server.py — v2 重寫

- stk_updater.py — 日期比較 + _restart_agent.bat

- pack_and_upload.bat — 加 _restart_agent.bat + install_agent_silent.bat

- ftp_upload.py — 永遠上傳 EXE/SET/SOCKET

- _restart_agent.bat — 新增

- install_relay.bat / uninstall_relay.bat / install_service.bat — 自動提權





## 2026-05-26



### 22:10 SearchPreCost F9 + prn2xlsx + FTP 上傳修正



**SearchPreCost F9 查詢不移動 record：**

- 根因：TABLE.PRG LOOKBLOCK 處理後呼叫 goTop() 覆蓋了 dbSeek 結果

- 修正：移除 goTop()，僅 refreshAll + forceStable

- 檔案：`TABLE.PRG`



**prn2xlsx PRN轉Excel：**

- prn2xlsx.py 重寫：用 ====== 分隔線偵測欄位邊界 + BIG5 字寬處理

- 數字保持文字格式

- PyInstaller 打包成 prn2xlsx.exe (20MB)，客戶端不需 Python

- fw_prnout.prg: 優先用 .exe，fallback .py

- pack_and_upload.bat: 加入 prn2xlsx.exe



**FTP 上傳/下載修正：**

- ftp_upload.py: EXE/SET/SOCKET 永遠上傳；TOOLS/.zip 比日期

- stk_updater.py: 拿掉 version 字串比較，全部改用日期比較

- stk_updater.py 打包成 EXE (8.1MB)



### 20:30 TGet BIG5 輸入修正 + LOG 統合



**問題：**

- P113 零件新增 DIALOG 的規格/備註欄位，中文輸入會變形（保險桿→保＠情）

- Debug_Type.txt 沒有分流到 Debug_Stk32/Debug_PrnAsk

- WriteDebugLog PRG 層完全沒有輸出



**根因：**

- Harbour TGet:Input() OTHERWISE case 呼叫 `Transform(cChar, "X")` 對 byte >= 0x80 做 codepage 轉換

- C0 被轉成 A2 7C（BIG5 “＠”），EC 被轉成 A1

- WriteDebugLog s_nDebugLog STATIC 快取漏洞：首次 INI 未載入 → cache=0 → 永久關閉



**修正：**

1. `GETSYS.PRG`: nKey >= 128 時繞過 TGet:Overstrike/Insert，直接 Stuff 寫入 oGet:buffer

2. `usrfont_api.c`: C 層 WriteDebugLogC 改用 EXE 名稱分流

3. `PRNASK/debug_log.prg`: Debug_Type → Debug_PrnAsk

4. `clipper_compat.prg`: s_nDebugLog==0 時重讀 INI



### 13:00 Agent 防死 + stk_updater + DispEnd 缺口 + F9 + Commander



**Agent 防死機制：**

- Agent heartbeat (.heartbeat 每 30 秒) + AgentErrorHandler (靜默)

- HideConsole: SW_HIDE + WS_EX_TOOLWINDOW 不出現工作列

- Watchdog: 沒 .heartbeat = 舊版→kill; >5 分鐘 = 卡死→kill

- install_agent_silent.bat: 靜默安裝，可遠端 EXEC

- install_agent update: 加建排程+watchdog

- deploy_agent_fix.bat: 一鍵部署



**stk_updater 修正：**

- 版本比較加 updated 時間，不只看 version 字串

- Agent A/B force-kill + 補更新

- 部署 deploy_agent_fix + install_agent_silent

- ftp_upload 加檔案大小比較 + SET touch 日期



**DispBegin/DispEnd 缺口：**

- DOMCMP 5 處 + FORCMP 確認OK

- AppendByHistory DispEnd 從 IF 內移到函式最後



**Table F9：**

- FreshOrder → goTop + refreshAll + forceStable（解決首筆重複）



**Commander：**

- HB 版恢復穩定

- PyQt5 版開始開發（CheckComboBox + QThread）



### 23:30 批次操作效能優化 + Commander + Agent + 其他修正



**問題與解決：**



1. **批次操作效能（重大發現）**

   - DOMCMP/FORCMP AppendByHistory 前景跑 85~130 秒，背景跑 8~12 秒

   - 根因：GTWVT 前景 WM_PAINT 觸發 TrueType 渲染，佔 90% 時間

   - 解法：DispBegin()/DispEnd() 包住批次操作

   - 結果：前景 130秒 → 10秒

   - DOMCMP 3組 + FORCMP 3組 全部加上



2. **Commander XBROWSE**

   - Cell 高亮：MARQSTYLE_HIGHLROWRC + bClrSelFocus/bClrRowFocus

   - 光棒保持：Refresh 前保存 nArrayAt

   - TeamViewer 按鈕：自動偵測 x64/x86 路徑



3. **Agent 改進**

   - .heartbeat 檔 + watchdog 超 5 分鐘自動 kill+restart

   - AgentErrorHandler 靜默處理錯誤，只寫 LOG

   - install_agent.bat 讀取既有 INI 顯示預設值



4. **P74 背景變色**

   - CLEAR SCREEN 前 SetColor("N/N")



5. **Table F9 查詢**

   - LOOKBLOCK 成功後加 FreshOrder 刷新 browse



6. **BUYSELL 增項 debug**

   - 加 5 行 WriteDebugLog 追蹤卡住原因（等客戶回報）



7. **Theme 0 slot 7**

   - RGB(140,140,140) → RGB(130,130,130)



**修改檔案：**

- DOMCMP.PRG, FORCMP.PRG: DispBegin/DispEnd 批次優化

- stk_commander_fw.prg: Cell 高亮 + 光棒保持 + TeamViewer

- stk_agent.prg: .heartbeat + AgentErrorHandler

- watchdog_agent.bat: heartbeat 檢查

- install_agent.bat: 讀取既有 INI

- TABLE.PRG: F9 LOOKBLOCK FreshOrder

- P74.PRG: CLEAR SCREEN 前 SetColor

- BUYSELL.PRG: 增項 debug log

- color_theme.prg: slot7 RGB 調整



### 08:00 Commander 光棒改進 + Agent Watchdog 強化 + install_agent 改進



**問題：**

1. Commander XBROWSE 只有 Row 高亮，沒有 Cell 高亮

2. Agent 跑 16 小時後卡死（process 活著但不工作），watchdog 偵測不到

3. Dashboard 空白 → Agent 沒連上 Relay → keepalive 循環斷線

4. install_agent.bat 即使已有 INI 還是要求重新輸入



**解決方法：**

1. MARQSTYLE_HIGHLROWRC + bClrSelFocus(深藍) + bClrRowFocus(淺藍)

2. Agent 主迴圈每 30 秒寫 .heartbeat 檔，watchdog 檢查超過 5 分鐘就 kill+restart

3. RefreshDashboard 保存/恢復 nArrayAt 防止光棒跳回第一列

4. install_agent.bat 偵測既有 INI，顯示現有值作為預設



**修改檔案：**

- stk_commander_fw.prg: MARQSTYLE_HIGHLROWRC + bClrSelFocus/bClrRowFocus + nArrayAt 保持

- stk_agent.prg: .heartbeat 檔寫入

- watchdog_agent.bat: 檢查 .heartbeat 檔修改時間

- install_agent.bat: 讀取既有 INI 顯示預設值



**踩坑：**

- XBROWSE 沒有 nClrFocuFore/Invalidate/GoTo 方法，正確用 bClrSelFocus/bClrRowFocus/nArrayAt

- Dashboard 空白不是程式改壞，是 Agent 卡死沒連 Relay

- SetArray 每次呼叫會重設游標位置，改用 Len 比較只在數量變化時呼叫

- VERSION 用 datetime.now() 不是固定字串，顯示的是啟動時間不是檔案版本



### 22:30 INI Editor 工具



**內容：**

- Python tkinter 開發的 INI 設定管理工具

- 管理 System.ini / stk.ini / stk_agent.ini

- 工作站選擇 C:\STK\S001~S012

- ComboBox 輔助輸入（FontDirect/PreviewMode/StkQtyForm/ColorTheme/box_type 等）

- Cascade 連動（PRNMETHOD 切換 PrnAsk/DosPntGo 子設定）

- FontMap 字型從 Windows\Fonts 掃描 + 共用 ASCII 字型

- COLOR_THEME 14 主題完整編輯（PRG 預設值補充 9~12 號）

- Case-insensitive meta 比對（解決 RunCMD vs RunCmd 問題）

- 32-bit EXE 打包（Python312-32 + PyInstaller）

- 部署路徑：C:\STK\TOOLS\INI_Editor.exe



**修改檔案：**

- ini_editor.py（新增）

- pack_and_upload.bat（加入 INI_Editor.exe → TOOLS）



### 19:58 GDI字型分離+Unicode向量列印+LOG分離+INI字型調整



**問題：**

1. ~FB/~FP 等CJK字型的英文字重疊（比例寬度）

2. BIG5不存在的Unicode字列印品質極差（24x24 bitmap放大→點狀）

3. PrnAsk和Stk32的LOG混在同一個Debug_Type.txt

4. System.ini的GDI_FONT_ADJ/GDI_ASCII_SCALE讀不到



**解決方法：**

1. GdiFlushText分離：CJK用GetGDIFontForCJK()、ASCII用Courier New(nFontW=0自動寬)

2. GdiDrawUsrGlyph Priority 3改用GdiCreateFont+GdiTextOutW直接在DC上向量渲染

3. WriteDebugLog根據EXE名稱分檔：Debug_PrnAsk.txt / Debug_Stk32.txt

4. hb_iniRead的top-level keys在hIni["MAIN"]底下（不是root也不是""）



**踩坑：**

- Courier New/Courier字型視覺比CJK字型小→加GDI_FONT_ADJ+GDI_ASCII_SCALE INI參數

- hb_iniRead把沒有section header的key放在"MAIN" hash裡

- GdiTextOutUtf自建CreateFontW跟GdiCreateFont不一致→字太小

- LoadGdiAsciiAdj只放在GdiPrintPrn沒放PrnToPdf→PDF模式讀不到INI

- PrnAsk的LOG寫在RESULT.TXT不是Debug_Type.txt



**修改檔案：**

- prn_to_pdf.prg: GdiFlushText字型分離、GdiDrawUsrGlyph向量渲染、LoadGdiAsciiAdj讀INI

- clipper_compat.prg: WriteDebugLog分檔

- gdi_print_c.c: 新增GdiTextOutUtf、GdiTextOutW函式

- System.ini: 新增GDI_FONT_ADJ、GDI_ASCII_SCALE參數



**新增System.ini參數：**

- GDI_FONT_ADJ=N — 字型高度微調(px)，中英文都適用

- GDI_ASCII_SCALE=1.0 — ASCII字額外倍率(1.15=放大15%)

# STK32 更新紀錄（完整歷史）



---





## 2026-05-24









### 23:30 批次操作效能優化 + Commander + Agent + 其他修正



**問題與解決：**



1. **批次操作效能（重大發現）**

   - DOMCMP/FORCMP AppendByHistory 前景跑 85~130 秒，背景跑 8~12 秒

   - 根因：GTWVT 前景 WM_PAINT 觸發 TrueType 渲染，佔 90% 時間

   - 解法：DispBegin()/DispEnd() 包住批次操作

   - 結果：前景 130秒 → 10秒

   - DOMCMP 3組 + FORCMP 3組 全部加上



2. **Commander XBROWSE**

   - Cell 高亮：MARQSTYLE_HIGHLROWRC + bClrSelFocus/bClrRowFocus

   - 光棒保持：Refresh 前保存 nArrayAt

   - TeamViewer 按鈕：自動偵測 x64/x86 路徑



3. **Agent 改進**

   - .heartbeat 檔 + watchdog 超 5 分鐘自動 kill+restart

   - AgentErrorHandler 靜默處理錯誤，只寫 LOG

   - install_agent.bat 讀取既有 INI 顯示預設值



4. **P74 背景變色**

   - CLEAR SCREEN 前 SetColor("N/N")



5. **Table F9 查詢**

   - LOOKBLOCK 成功後加 FreshOrder 刷新 browse



6. **BUYSELL 增項 debug**

   - 加 5 行 WriteDebugLog 追蹤卡住原因（等客戶回報）



7. **Theme 0 slot 7**

   - RGB(140,140,140) → RGB(130,130,130)



**修改檔案：**

- DOMCMP.PRG, FORCMP.PRG: DispBegin/DispEnd 批次優化

- stk_commander_fw.prg: Cell 高亮 + 光棒保持 + TeamViewer

- stk_agent.prg: .heartbeat + AgentErrorHandler

- watchdog_agent.bat: heartbeat 檢查

- install_agent.bat: 讀取既有 INI

- TABLE.PRG: F9 LOOKBLOCK FreshOrder

- P74.PRG: CLEAR SCREEN 前 SetColor

- BUYSELL.PRG: 增項 debug log

- color_theme.prg: slot7 RGB 調整



### 08:00 Commander 光棒改進 + Agent Watchdog 強化 + install_agent 改進



**問題：**

1. Commander XBROWSE 只有 Row 高亮，沒有 Cell 高亮

2. Agent 跑 16 小時後卡死（process 活著但不工作），watchdog 偵測不到

3. Dashboard 空白 → Agent 沒連上 Relay → keepalive 循環斷線

4. install_agent.bat 即使已有 INI 還是要求重新輸入



**解決方法：**

1. MARQSTYLE_HIGHLROWRC + bClrSelFocus(深藍) + bClrRowFocus(淺藍)

2. Agent 主迴圈每 30 秒寫 .heartbeat 檔，watchdog 檢查超過 5 分鐘就 kill+restart

3. RefreshDashboard 保存/恢復 nArrayAt 防止光棒跳回第一列

4. install_agent.bat 偵測既有 INI，顯示現有值作為預設



**修改檔案：**

- stk_commander_fw.prg: MARQSTYLE_HIGHLROWRC + bClrSelFocus/bClrRowFocus + nArrayAt 保持

- stk_agent.prg: .heartbeat 檔寫入

- watchdog_agent.bat: 檢查 .heartbeat 檔修改時間

- install_agent.bat: 讀取既有 INI 顯示預設值



**踩坑：**

- XBROWSE 沒有 nClrFocuFore/Invalidate/GoTo 方法，正確用 bClrSelFocus/bClrRowFocus/nArrayAt

- Dashboard 空白不是程式改壞，是 Agent 卡死沒連 Relay

- SetArray 每次呼叫會重設游標位置，改用 Len 比較只在數量變化時呼叫

- VERSION 用 datetime.now() 不是固定字串，顯示的是啟動時間不是檔案版本



### 22:30 INI Editor 工具



**內容：**

- Python tkinter 開發的 INI 設定管理工具

- 管理 System.ini / stk.ini / stk_agent.ini

- 工作站選擇 C:\STK\S001~S012

- ComboBox 輔助輸入（FontDirect/PreviewMode/StkQtyForm/ColorTheme/box_type 等）

- Cascade 連動（PRNMETHOD 切換 PrnAsk/DosPntGo 子設定）

- FontMap 字型從 Windows\Fonts 掃描 + 共用 ASCII 字型

- COLOR_THEME 14 主題完整編輯（PRG 預設值補充 9~12 號）

- Case-insensitive meta 比對（解決 RunCMD vs RunCmd 問題）

- 32-bit EXE 打包（Python312-32 + PyInstaller）

- 部署路徑：C:\STK\TOOLS\INI_Editor.exe



**修改檔案：**

- ini_editor.py（新增）

- pack_and_upload.bat（加入 INI_Editor.exe → TOOLS）



### 19:58 GDI字型分離+Unicode向量列印+LOG分離+INI字型調整



**問題：**

1. ~FB/~FP 等CJK字型的英文字重疊（比例寬度）

2. BIG5不存在的Unicode字列印品質極差（24x24 bitmap放大→點狀）

3. PrnAsk和Stk32的LOG混在同一個Debug_Type.txt

4. System.ini的GDI_FONT_ADJ/GDI_ASCII_SCALE讀不到



**解決方法：**

1. GdiFlushText分離：CJK用GetGDIFontForCJK()、ASCII用Courier New(nFontW=0自動寬)

2. GdiDrawUsrGlyph Priority 3改用GdiCreateFont+GdiTextOutW直接在DC上向量渲染

3. WriteDebugLog根據EXE名稱分檔：Debug_PrnAsk.txt / Debug_Stk32.txt

4. hb_iniRead的top-level keys在hIni["MAIN"]底下（不是root也不是""）



**踩坑：**

- Courier New/Courier字型視覺比CJK字型小→加GDI_FONT_ADJ+GDI_ASCII_SCALE INI參數

- hb_iniRead把沒有section header的key放在"MAIN" hash裡

- GdiTextOutUtf自建CreateFontW跟GdiCreateFont不一致→字太小

- LoadGdiAsciiAdj只放在GdiPrintPrn沒放PrnToPdf→PDF模式讀不到INI

- PrnAsk的LOG寫在RESULT.TXT不是Debug_Type.txt



**修改檔案：**

- prn_to_pdf.prg: GdiFlushText字型分離、GdiDrawUsrGlyph向量渲染、LoadGdiAsciiAdj讀INI

- clipper_compat.prg: WriteDebugLog分檔

- gdi_print_c.c: 新增GdiTextOutUtf、GdiTextOutW函式

- System.ini: 新增GDI_FONT_ADJ、GDI_ASCII_SCALE參數



**新增System.ini參數：**

- GDI_FONT_ADJ=N — 字型高度微調(px)，中英文都適用

- GDI_ASCII_SCALE=1.0 — ASCII字額外倍率(1.15=放大15%)

# STK32 更新紀錄（完整歷史）



---





## 2026-05-21



### 04:45 Relay Server + FiveWin Commander + STK(DOS) 偵測



**Relay Server（TeamViewer 模式）**

- relay_server.py → relay_server.exe（32-bit，不需 Python）

- 部署到公司 SERVER，port forward 9530

- Agent/Commander 穿越防火牆，不需開 port

- 協議：4-byte length header + JSON，KEEPALIVE 保持連線

- 自動重連（斷線 10s 後重試）



**FiveWin Commander GUI**

- FOLDEREX 三頁：Dashboard / Commands / Log

- XBROWSE 顯示工作站狀態（含 STK 欄）

- 頂部按鈕列：Ping/Status/Shutdown/Kill/Message/Exec/Screen/Lock

- 目標選擇：光棒位置或 ALL（Alert 選擇框）

- LAN + Relay 雙模式（ini 設 RELAY_HOST 即切換）



**STK(DOS) 偵測與 Kill**

- 偵測：HYPERDOS.EXE / DOSBOX.EXE / NTVDM.EXE

- Kill 選擇：STK32 / STK(DOS) / Both

- KILL_STK_OLD 全殺三個 DOS 模擬器



**踩坑**

- FiveWin GetParam 函式名與 shared 衝突 → 改名 AgentGetParam

- HB_SETCODEPAGE 需要 REQUEST HB_CODEPAGE_BIG5

- FOLDEREX 內容要在 ON INIT 時建立（ACTIVATE 前 aDialogs 尺寸為 0）

- KEEPALIVE_ACK 沒有 station 欄位，UpdateStation 要跳過



---



## 2026-05-20



### 21:00 STK Agent RPC — Phase 2~4 全部完成



**新增指令（全部跨機器驗證 S001+S002）**

- STATUS：CPU/MEM/DISK/STK32 狀態，Dashboard 即時顯示

- MESSAGE：遠端彈出訊息框（VBS 實作）

- EXEC：遠端執行命令（visible/background 模式）

- HEARTBEAT：30 秒自動心跳，Dashboard 即時更新

- FILE_PUSH/FILE_PULL/LOG_COLLECT：雙向檔案傳輸 + 批次收集 LOG

- SCREENSHOT：遠端截圖（C code obj，DPI aware）

- LOCK_SCREEN：遠端鎖定螢幕（C code obj）

- DEPLOY_ZIP：遠端解壓 ZIP

- DISK_CLEANUP：遠端清理暫存

- CONFIG_PUSH/GET_CONFIG：遠端讀寫 INI 設定

- WAKE_ON_LAN：WOL magic packet

- ERROR_REPORT：截圖+LOG 自動上傳 FTP（/H/stk-error/{KMSN}/）



**Commander Dashboard**

- 即時顯示所有工作站狀態（Station/Status/STK32/MemFree/DiskFree/Computer/LastSeen）

- 多執行緒（-mt），背景 TCP 接收 heartbeat + 回報

- F1~F6 功能鍵操作



**FTP 上傳（ftp_helper.prg）**

- 純 Harbour socket 實作 FTP client（PASV mode）

- hb_socketResolveAddr() DNS 解析

- SimpleEncrypt/Decrypt（XOR+Base64）加密 INI 密碼



**踩坑**

- screenshot.c 獨立 obj 編譯需要 -tW -tWM 旗標（跟 usrfont 一樣）

- SetProcessDPIAware() 解決 S002 高 DPI 截圖只有 1/4

- hb_socketConnect 不支援 hostname，需先 hb_socketResolveAddr

- -w3 -es2 把 ambiguous reference 當 error，改 -w1 -es0

- `msg` 命令在某些 Windows 不存在，改用 VBS MsgBox

- TCPListen 的 hb_socketBind 返回 .T./.F.（踩兩次）

- EXEC stdout 含 BIG5 需 hb_StrToUTF8 轉換

- INI 的 CRLF 問題（Write tool 產生 LF only）



---



### 20:30 STK Agent RPC 通訊系統（Phase 1 完成）



**問題**：SERVER↔CLIENT 通訊依賴檔案輪詢（SHUTDOWN.FLG），延遲大，\\C$ 管理共享不通



**解決方法**：建立獨立的 UDP/TCP RPC 通訊系統



**Phase 1 — 基礎通訊（已驗證成功）**

- 新專案 `E:\ConvertFW\SOCKET\`（StkAgent + StkCommander）

- Harbour 內建 hb_socket*() API（PRG level，type=Pointer）

- UDP 9527 廣播指令，TCP 9528 回報結果

- PING：探測在線，回報 station/stk_running/uptime

- SHUTDOWN：Agent 建 SHUTDOWN.FLG 到 STK_PATH，STK32 偵測後倒數退出

- KILL_STK：Agent 執行 taskkill /F /IM STK32.EXE

- 跨機器驗證成功：開發機(180) → S001(22) → STK32 退出 → 回報 OK



**踩坑**

- hb_socketOpen 返回 Pointer (type P)，不能用 hb_ntos()

- hb_socketBind 返回 .T./.F.，不是 0/非0

- SHUTDOWN.FLG 要建在 STK_PATH（ini 設定），不是 Agent 的 hb_DirBase()

- GTWIN 代替 GTWVT（避免 usrfont unresolved symbols）

- SO_BROADCAST C code 造成 winsock2/windows.h 衝突，改用 subnet-directed broadcast



**修改檔案**

- `SOCKET/shared/protocol.ch`（新建）— 共用常數

- `SOCKET/shared/json_helper.prg`（新建）— JSON 封包建構/解析

- `SOCKET/shared/socket_helper.prg`（新建）— Socket 工具函式

- `SOCKET/StkAgent/source/stk_agent.prg`（新建）— CLIENT 端主程式

- `SOCKET/StkAgent/build/stk_agent.hbp + ini`（新建）

- `SOCKET/StkCommander/source/stk_commander.prg`（新建）— SERVER 端主程式

- `SOCKET/StkCommander/build/stk_commander.hbp + ini`（新建）



---



### 02:30 FTP 自動更新系統（Phase 1~3）



**問題**：目前更新客戶端全靠手動複製，多站（S001~S020）同步耗時又易出錯



**解決方法**：建立三層自動更新架構



**Phase 1 — STK32 CheckShutdown 機制**

- 新增 `shutdown.prg`：hb_idleAdd 每 5 秒檢查 SHUTDOWN.FLG

- 偵測到後在畫面角落顯示 `[60秒][延長][立即]`，不遮擋工作區

- 支援滑鼠 hover（白底紅字）+ 點擊 [延長]/[立即]

- 位置由 System.ini `SHUTDOWN_POS=TL/TR/BL/BR` 設定（預設左上）

- 倒數歸零自動 QUIT，延長最多 3 次



**Phase 2 — SERVER 端更新程式 (stk_updater.py)**

- 放 SHUTDOWN.FLG → 等待所有工作站退出 → 備份 C:\STK → FTP 下載 → 分發

- 分發規則：TOOLS 覆蓋（根目錄）、EXE/SET/BAT/TMP 覆蓋（各站）、FRM/LBL/MEM 只新增不覆蓋

- 所有覆蓋皆先比對日期/時間/大小，相同則跳過

- 設定檔 stk_updater.ini（路徑、FTP、工作站範圍、超時）



**Phase 3 — 開發端上傳工具 (ftp_upload.py)**

- 掃描目錄結構（EXE/SET/FRM/...），上傳到 FTP + 產生 manifest.json

- 支援 --version 參數、更新說明 notes



**踩坑**

- K_F12 在 main.prg 未 include inkey.ch → 用數值 -41 替代

- hb_idleAdd 的 idle block 不能用阻塞式對話框（會卡住使用者工作）

- FRM/LBL/MEM 是各工作站自訂內容，不可覆蓋



**修改檔案**

- `HBSTK/compat/shutdown.prg`（新建）

- `HBSTK/source/main.prg`（加 hb_idleAdd CheckShutdown）

- `HBSTK/build/stk32.hbp`（加 shutdown.prg）

- `HBSTK/build/System.ini`（加 SHUTDOWN_POS）

- `HBSTK/build/update_server/stk_updater.py`（新建）

- `HBSTK/build/update_server/stk_updater.ini`（新建）

- `HBSTK/build/update_server/ftp_upload.py`（改造）





### 01:30 P5141/P5142 F9 in-place refresh



**問題**：P5141/P5142 在 Table 內按 F9 想重新輸入查詢條件，原本「退出 Table → 外層 loop 重做 ReptForm」的做法多次踩坑：

1. codeblock 內 `@cBeginId` 對 detached local by-ref 失效 → 改用 carrier array 包裝

2. block 多語句 `{|| a:=X, b:=Y, .F.}` 在 nState=4 時 Table 沒退出 → 改用 helper function 強制 RETURN .F.

3. 外層用 `IF LastKey() == K_F9` 判斷需 LOOP，但 LastKey 被 AskBox 改成 1004 → 改用 lF9Refresh 旗標

4. 即使 Table 退出，畫面殘留舊 browse + 新 browse 疊出來



**最終解決方法（in-place refresh）**：

- LOOKBLOCK 改成呼叫 helper `F9Refresh5141/F9Refresh5142`

- helper 內：AskDomOrd → 關舊 alias → 重跑 ReptForm（P5142 含 ExtractParts）→ 重綁 dbSetRelation → `oBrowse:refreshAll() + forceStable()` → 回 `.T.`

- block 回 `.T.` 讓 Table 繼續執行（不退出），同一個 browse 視窗原地刷新成新資料

- 用 aQryRef array 傳遞所有需要的 LOCAL



**踩坑**：

- Harbour codeblock 不能對 detached local 用 `@` by-ref → carrier array

- block 多語句 + 寫 detached local 在某些路徑下 RETURN 值不可靠 → 封進 helper 單一 RETURN

- AskBox 完成後 LastKey() 是 1004（內部完成碼）不是 K_F9

- 同一 alias name "DomOrdR1" close+reopen 後 oBrowse column block 仍可指向新資料



**修改檔案**：

- `source/P5141.PRG`：F9Refresh5141 helper、aQryRef[8]、LOOKBLOCK 改用 helper

- `source/P5142.PRG`：F9Refresh5142 helper、aQryRef[11]、LOOKBLOCK 改用 helper

- `source/TABLE.PRG`：F9 case 加 LOOKBLOCK 處理路徑



---



## 2026-05-19




## 2026-05-18





### 23:50 P5141 BIG5 typo + ProcessBox 頻率調整



**問題**：

- P5141 ProcessBox 訊息 `整理資料中` 顯示亂碼，根因是 `¾E`（= `¾E` = ASCII `E`）的 BIG5 編碼破壞

- ProcessBox 每筆都更新太頻繁，整體進度顯示拖慢



**解決方法**：

- 改成正確 BIG5 `¾ã`（整）

- ProcessBox 改成 `IF nI % 100 == 0` 才更新



**修改檔案**：

- `source/P5141.PRG`：ProcessBox 訊息與頻率





### 23:30 P5141 跳過 Customer Extract + 改寫 ReptForm 反向 seek



**問題**：原本流程先 Extract Customer 全表（內含 `InKey(0.01)`），畫面空白繞很久才出現結果。



**解決方法**：

- 跳過 Extract，把 `cOutputAlias` 設成 dummy

- 重寫 ReptForm 為「直接反向 scan DomOrdM」+ 日期範圍 + ID 範圍過濾，直接 AAdd 進 `DomOrdR1`

- 開頭強制 `MEMVAR->lProcessBox := .T.` 確保進度顯示



**修改檔案**：

- `source/P5141.PRG`：ReptForm 全面改寫





### 23:00 P5141 nState typo 修正



**問題**：`nState = 1` 是比較不是賦值，導致 lAllData 路徑下 nState 永遠未設，後續邏輯異常。



**解決方法**：改成 `nState := 1`。





### 22:45 TopMessage / BottomMessage hover 不還原 + 選錯對象



**問題**：

- BottomMessage 滑鼠 hover 變綠後離開沒變回原色

- TopMessage 滑鼠 hover 完全沒變色

- TopMessage 選錯對象（hit-test 對到錯誤的 hint zone）



**根因**：

- hover 還原依賴 persist zone 內容，但有些路徑沒呼叫 MsHoverUpdate

- TopMessage 的 persist zone 沒納入 MsHitTest 的判斷範圍



**解決方法**：

- 加 `MsHoverUpdate` 補在 row click 之外的 idle 路徑

- 把 TopMessage 的 aTopReg 條目納入 `MsHitTest` 的搜尋範圍

- BottomMessage 離開時主動 RestScreen 該 zone



**修改檔案**：

- `compat/mouse_handler.prg`：MsHoverUpdate、MsHitTest





### 22:30 DIALOG.PRG hover 顯示舊文字



**問題**：使用滑鼠點 Row 1 toggle 後，Row 2 的 hover 顏色雖然有重畫，但 hover 文字殘留 Row 2 之前的舊狀態（如「銷貨/退貨」沒換成新值）。



**根因**：

- ReadKey 的 hover 還原依賴 `aMsReg[i][7]` 存的文字

- Toggle() 切換 Row 1 後，Row 2 的 aToggle[nIndex,3] 變了，但 aMsReg[i][7] 沒同步



**解決方法**：

- 把 hover 還原改成 `SaveScreen / RestScreen`，不依賴存的文字

- Toggle() nSysPara=1/3 時同步 `aMsReg[i][7] := aToggle[nIndex,3]`



**修改檔案**：

- `source/DIALOG.PRG`：ReadKey hover、Toggle()





### 22:00 QuickBox 進出 row 0 + aTopReg 還原



**問題**：QuickBox 出現/離開時 row 0 的 TopMessage persist zones 沒有正確恢復，導致進入 QuickBox 後的 TopMessage 殘缺。



**解決方法**：QuickBox 進入時 SaveScreen row 0、記錄 aTopReg 條目；離開時 RestScreen + 還原條目。



**修改檔案**：

- `compat/quickbox.prg`





### 21:45 RPTWIN / DAYWIN 進入隔離 + 離開徹底還原功能提示



**問題**：

- 進入 RPTWIN / DAYWIN 時，外層（含 BottomMessage）的功能提示仍顯示，造成視覺混亂與按鍵衝突

- 離開 RPTWIN / DAYWIN 後，所屬的功能提示沒被乾淨清掉，殘留到下一層



**解決方法**：

進入時：

- `MsPushReg()` + `MenuEnterIsolate(nKeepRow)` 隔離外層提示（保留 row 0 TopMessage，搭配前一條 TLIB 修改）

- 主動清掉 BottomMessage 的 aBottomReg 條目



離開時：

- `MenuLeaveRestore(aSaved)` 還原外層條目

- `MsPopReg()` 還原 RPTWIN/DAYWIN 自己 push 的 reg stack

- 主動 RestScreen 把 row 24 BottomMessage 區域畫回去



**修改檔案**：

- `source/RPTWIN.PRG`、`source/DAYWIN.PRG`：ENTER / LEAVE 區段





### 21:40 RPTWIN / DAYWIN SUB Browse 功能鍵 label 與 CASE 不一致



**問題**：SUB browse 底部 BottomMessage 顯示 `Ctrl-K:盤貨`（DAYWIN 另含 `Ctrl-R:銷貨`），但點/按 Ctrl-K 沒反應。

- ESC 正常（CASE 有定義）

- Ctrl-K / Ctrl-R hint 顯示但無功能



**根因**：SubApplyKey 的 CASE 寫的是 `K_ALT_K` / `K_ALT_R`，但 acMessage label 寫成 `KY_CTRL + "-K"` / `KY_CTRL + "-R"`，導致 MsKeyFromLabel parse 出 K_CTRL_K → hb_keyPut → CASE 找不到 → 啞掉。



**解決方法**：把 label 改成 KY_ALT + KY_K / KY_R（使用 CUSTOMCHAR.ch 的造字符號 define，跟 CASE 對齊）。



**修改檔案**：

- `source/RPTWIN.PRG` line 934：`KY_CTRL + "-K"` → `KY_ALT + KY_K`

- `source/DAYWIN.PRG` line 833：`KY_CTRL + "-K"` → `KY_ALT + KY_K`、`KY_CTRL + "-R"` → `KY_ALT + KY_R`





### 21:35 RPTWIN / DAYWIN 雙 Browse（MAIN + SUB）滑鼠分區處理



**問題**：RPTWIN / DAYWIN 上半部是 MAIN browse（訂單主檔），下半部是 SUB browse（明細），原本：

- 滑鼠點 SUB 區域，被誤判到 MAIN（or 反之）

- MAIN 點 row 切換時，SUB 沒跟著重新查、停留在前一筆主檔的明細

- 兩個 browse 都需要滾輪 + click + 右鍵 ESC，但邏輯只套到 MAIN



**解決方法**：

- 滑鼠 hit-test 分區：用 MRow() 對應 MAIN/SUB 的座標範圍判斷點到哪一個 browse

- 滾輪事件依當前 cursor 所在 browse 派發（MWFORWARD/BACKWARD → 對應 browse 的 K_UP/K_DOWN）

- MAIN row click 後算出 nDelta，呼叫 `Eval(oMainWin:cargo[SKIPBLOCK], nDelta)` 移到目標 row，然後 `hb_keyPut(K_DOWN)` 觸發 dbSetRelation 重查 SUB

- SUB row click 直接在 SUB browse 內 skip 不影響 MAIN



**修改檔案**：

- `source/RPTWIN.PRG`、`source/DAYWIN.PRG`：MainApplyKey / SubApplyKey 區段





### 21:30 RPTWIN / DAYWIN 滑鼠完整支援 + F4→Alt+F9



**問題**：

- 滑鼠滾輪、row click、右鍵都不支援

- master row click 時 sub browse 沒連動

- F4:列印 與 DOS 模式衝突，要改 Alt+F9

- F9 提示在沒有 LOOKBLOCK 時不應顯示



**解決方法**：

- 滾輪 MWFORWARD/MWBACKWARD → K_UP/K_DOWN

- row click 算 nDelta 公式：`(MRow() - browse:nTop - nHdrH) - (browse:rowPos - 1)`

- 點完用 `hb_keyPut(K_DOWN)` 觸發 master→detail 連動

- 右鍵 → ESC

- F4 → K_ALT_F9 + 提示文字

- F9 提示用 `IF oMainWin:cargo[LOOKBLOCK] != NIL` 條件顯示



**修改檔案**：

- `source/RPTWIN.PRG`、`source/DAYWIN.PRG`

- `compat/mouse_handler.prg`：相關 hint zone





### 21:00 APPSETUP ALT_2 / ALT_9 browse 滑鼠 + 完整提示



**問題**：ALT_2 設定 / ALT_9 報表 browse 缺滑鼠（cell click、row、滾輪），提示也不完整。



**解決方法**：

- 加 cell click + row 公式（同 RPTWIN）

- 加滾輪

- 提示套 AddHintLabel



**修改檔案**：

- `source/APPSETUP.PRG`





### 20:30 K_ALT_A fallback 加 10 個 PRG



**問題**：BUYSELL 等多個畫面，當使用者按 ALT+A 但權限不足時，畫面靜默無反應，使用者不知道為什麼。



**解決方法**：在 K_ALT_A 的 CASE 中加 fallback 提示（無權限時跳訊息），共 10 個檔案。



**修改檔案**：

- `source/BUYSELL.PRG`、`source/QUOT.PRG`、`source/INQUIRY.PRG`

- `source/DOMORD.PRG`、`source/DOMSCH.PRG`、`source/ENQUIRY.PRG`

- `source/DOMPUR.PRG`、`source/DOMARR.PRG`、`source/FORPUR.PRG`、`source/FORARR.PRG`





### 19:30 多個 ShowXxxHelp 改用 AddHintLabel



**問題**：原本 ShowXxxHelp 用 `@ SAY + MsAddKey` 雙寫法，維護不易，部分提示重疊。



**解決方法**：統一改用 `AddHintLabel(nRow, nCol, cLabel, cColor)`，內部自動解 MsKeyFromLabel + MsAddKey。



**改的函式**：

- ShowAskPartsHelp、ShowPartsF9Help、ShowPayReceHelp

- ShowInquiryHelp、ShowEnquiryHelp、ShowQuotHelp

- ShowFPurArrHelp（含 SH+F7 修正）、ShowDPurArrHelp、ShowBuySellHelp





### 18:00 TLIB MenuEnterIsolate 加 nKeepRow 參數



**問題**：原本 `MenuEnterIsolate()` 進 isolate 模式會把所有 row 的 persist 條目都清掉，但有些畫面（RPTWIN/DAYWIN 等）希望只清部分、保留某 row（如 row 0 的 TopMessage）的提示。



**解決方法**：MenuEnterIsolate 加可選參數 `nKeepRow`：

- `nKeepRow == NIL`：行為與原本相同，全清

- `nKeepRow == n`：清除所有 persist，但把屬於 row n 的條目 restore 回去

配套 `MenuLeaveRestore(aSaved)` 把當初存的條目全部還原。



**修改檔案**：

- `source/TLIB.PRG`：MenuEnterIsolate / MenuLeaveRestore





### 18:30 LISTBYKIND / PARTSLIST / CusAccWin / SupAccWin 套用 P32 isolate



**問題**：進入這些畫面時，外層的功能提示沒被遮蔽，畫面殘留前一層的提示按鈕，造成視覺混亂。



**解決方法**：套用 P32 的 ENTER/LEAVE 模式：

```

SAVE SCREEN

MsPushReg() + MenuEnterIsolate()

... 畫面內容 ...

MenuLeaveRestore() + MsPopReg()

RESTORE SCREEN

```



**修改檔案**：

- `source/LISTBYKIND.PRG`、`source/PARTSLIST.PRG`

- `source/CusAccWin.PRG`、`source/SupAccWin.PRG`





### 09:30 DOSPNTGO 每頁多一行 → `/LL66`



**問題**：DOSPNTGO.EXE 列印時每印完一頁多跑一行空白，造成版面整體錯位。



**追查過程**：

- 直接呼叫 DOSPNTGO 正常 → 內部 c:\hdos4 fallback 路徑作怪

- 用 ProcMon（E:\ConvertFW\ProcessMonitor）攔截 HyperDos → DosPntGo 呼叫命令列

- 確認 HyperDos 是用 `dospntgo XXX.txt /Caller=HDOS4 /LL66` 參數

- `/LL66` 是行數每頁設定，缺它就跑預設值多算一行



**解決方法**：補上 `/LL66` 旗標。



**修改檔案**：

- `compat/dospntask.prg`（或對應 DOSPNTGO 呼叫處）





### 08:00 dospntask.prg 紙張清單動態化



**問題**：紙張選單寫死，新加印表機/紙張格式時要改程式。



**解決方法**：透過印表機驅動 `DocumentPropertiesW` 動態列舉 PaperSize。



**修改檔案**：

- `compat/dospntask.prg`





### 07:30 WebView2 預覽紙張誤判



**問題**：使用 WebView2 預覽時紙張誤抓 A4（實際應該 9×11），版面設定也選錯。



**解決方法**：預覽流程改用「喜好設定 → tolerance match → fallback 自訂(??*??)」邏輯。



**修改檔案**：

- `PRNASK` 預覽相關（已併入 PrnAsk.exe build）





---



## 2026-05-17





### 07:00 GdiGetPrinterPrefPaper 改用 DocumentPropertiesW



#### 問題

GDI 預覽紙張錯誤為 A4，實際印表機偏好設定是 9×11。



#### 根因

`GdiGetPrinterPrefPaper` 用 `GetPrinterW level 2` 取系統層級 DEVMODE → 永遠回 A4，無視使用者偏好設定（per-user DEVMODE）。

標準紙張代碼（dmPaperSize 如 DMPAPER_9X11=44）的 dmPaperWidth/Length 為 0，舊版函式未處理 → 隱性 fallback A4。



#### 解法

[gdi_print_c.c:935 GDIGETPRINTERPREFPAPER](../c_code/gdi_print_c.c:935) 完全重寫：

- 改用 `DocumentPropertiesW(DM_OUT_BUFFER)` 讀 per-user DEVMODE

- Priority 1: 自訂紙張 (dmPaperWidth/Length)

- Priority 2: 標準紙張代碼 → `DeviceCapabilitiesW(DC_PAPERS/DC_PAPERSIZE)` 查表

- 新增 `GdiGetPrinterPaperList(cPrn)` → 回 `{ {code, name, wMil, hMil}, ... }`

- 新增 `GdiGetPrinterPrefPaperCode(cPrn)` → 回 dmPaperSize 代碼



#### 影響範圍

Stk32 + PrnAsk 都用同一份 C，兩邊都需 rebuild。Stk32 的 GDI 預覽、列印對話框、頁面設定都用到。



#### 修改檔案

- HBSTK\c_code\gdi_print_c.c（加 `#include "hbapiitm.h"`、3 個函式）





### 00:29 TABLE.PRG K_UP/K_DOWN refreshCurrent → refreshAll



#### 問題

Alt+F3 (StockWin) 上下移動時，舊列數據殘留為新列內容（例如 BBB 上移到 AAA 後，原 BBB 列變成 AAA）。



#### 根因

Harbour TBrowse 的 `refreshCurrent()` 不會自動清除舊高亮列，與 Clipper TBrowse 行為不同。



#### 解決

K_UP / K_DOWN 改用 `refreshAll()`，跟其他 K_xxx 一致。速度略慢但行為正確。對照 900 (Clipper) 原始碼確認此修補符合 Clipper 預期行為。



#### 修改檔案

- `source/TABLE.PRG` K_UP / K_DOWN



---



### 00:15 TABLE.PRG DoGet 編輯 GET 顏色用 colorSpec index 8



#### 問題

Alt+F3 進 cell 編輯時顏色不是「紅底白字」（hilite cell 色），跟 BROWSE 不一致。



#### 根因

DoGet line 894 GetNew 用 `oBrowse:colorSpec` 整串 → GET 取**第一段**（normal N/W），不是 cell hilite。



#### 解決

取 colorSpec **第 8 段**（= BROWSE_CELL_COLOR，對應 colorRect 的 {8,8} 索引）：

```

cHi := AllTrim( hb_tokenGet(oBrowse:colorSpec, 8, ",") )

IF Empty(cHi) ; cHi := "W+/R" ; ENDIF

oGet := GetNew(..., cHi + "," + cHi)

```



#### 踩坑

原本誤抓 index 2（row hilite W+/B），輸出非紅底白字。



#### 修改檔案

- `source/TABLE.PRG` DoGet()



---



### 00:00 ARRAY.PRG 滑鼠 cell click + row 公式 + HitTest 游標還原



#### 問題

1. ArrayBrowse 滑鼠點 record 行 → 跳到錯誤的行（公式錯一格）

2. 無法用滑鼠點 cell 切換 column（沒有 col click 邏輯）

3. 點底部 hint 後游標跑位（flash SAY 沒還原 cursor）



#### 解決

**A. row 公式修正**（line 336）：

```

舊: nDelta := (MRow() - oBrowse:nTop - nHdrH) - oBrowse:rowPos

新: nDelta := (MRow() - oBrowse:nTop - nHdrH) - (oBrowse:rowPos - 1)

```



**B. col click 邏輯**（從 TABLE.PRG line 226-240 移植）：走訪 colCount 累加 width 找出點到的欄位，設 `oBrowse:colPos`。



**C. MsHitTest 游標還原**（mouse_handler.prg）：HitTest 開頭 `LOCAL nSavRow := Row(), nSavCol := Col()`，4 處 flash 區結尾加 `SetPos(nSavRow, nSavCol)`。



#### 修改檔案

- `source/ARRAY.PRG` K_LBUTTONDOWN + LOCAL nMsI/nColX/nColW

- `compat/mouse_handler.prg` HitTest 4 處



---



## 2026-05-16





### 23:35 ErrorSys 整合：UsrFontErrorHandler 併入 DefError + DEBUG_LOG 等級 + Alert→MessageBox



#### 訴求

1. 整合 UsrFontErrorHandler 與 DefError，移除 ErrorBlock 替換鏈

2. 把 stk.ini `DEBUG_LOG=0/1/2` 真實接起來

3. DefError 的 Alert() 框線在 BIG5 環境下亂碼，改用 MessageBox



#### 解決方法

**clipper_compat.prg WriteDebugLog**：加 `nLevel` 參數（預設 1），讀 stk.ini DEBUG_LOG。s_nDebugLog == 0 或 nLevel > s_nDebugLog 跳過寫入。



**ERRORSYS.PRG DefError**：開頭加 `s_lInError` 守衛（防遞迴）+ 詳細錯誤日誌（args + stack 用 WriteDebugLog level=2）。`Alert(cMessage, aOptions)` 改用 `MessageBox(cMessage, 5, 0, MIDDLE_ALIGN, aOptions, 1)`，aFrame 為 NIL 時 fallback 回 Alert。



**usrfont.prg**：移除 `STATIC bPrevErrorBlock`、整個 `UsrFontErrorHandler` 函式、`ErrorBlock` 替換語句。



#### 副產品

- 消除 feedback_errorblock_break.md 那條陷阱（不再需要 ErrorBlock 暫存）

- debug_on.txt 開關被 DEBUG_LOG 取代（不刪檔不影響）



#### 修改檔案

- `compat/clipper_compat.prg` WriteDebugLog

- `source/ERRORSYS.PRG` DefError

- `compat/usrfont.prg` 移除 ErrorHandler



---



### 22:53 MsKeyFromLabel 補 KY_ALT+KY_N → K_ALT_N



#### 問題

QuickBox 上緣 `[Alt+N:英轉中]` 無法 hover 變色 / 無法點擊。



#### 根因

MsKeyFromLabel 的 DO CASE 沒列 KY_ALT+KY_N。



#### 解決

DO CASE 加入 `CASE cShort == KY_ALT+KY_N ; RETURN K_ALT_N`。



#### 踩坑

即使 patch 套用、build OK，Stk32.exe 仍是舊版 — 因為 Stk32 還在跑，linker 無法覆寫（『Could not open Stk32.exe』訊息被 BUILD OK 蓋過）。**rebuild 前必須 Stop-Process -Name Stk32**。



#### 修改檔案

- `compat/mouse_handler.prg` MsKeyFromLabel



---



### 22:49 QuickBox 互動模式 [Enter:選擇] 滑鼠熱區



#### 問題

按 Ctrl+↓ 進入 QuickBox picker 互動模式後，上緣顯示 `[Enter:選擇]` 但無法點擊。



#### 解決

ELSE 分支 SAY `KY_ENTER1+":選擇"` 之後補上 `MsAddPersist(nTop-1, nLeft, Col()-1, K_ENTER, ..., NIL)`。



#### 修改檔案

- `source/TOOLS.PRG` QuickBox



---



### 22:37 QuickBox 互動模式 SaveScreen / RestScreen 切換



#### 訴求

進入 picker 互動模式時上緣應顯示 `[Enter:選擇]`，原本的 `[Ctrl↓:進入][Ctrl↵:首筆][Alt+N:英轉中]` 應該清掉。離開互動模式時應該恢復。



#### 解決

**main.prg**：`PUBLIC cQuickBoxUpperBuf := NIL`



**TOOLS.PRG QuickBox nFlag=1**：首次呼叫 SaveScreen(nTop-1, nLeft-1, nTop-1, nRight+1) 存純邊框



**TOOLS.PRG QuickBox nFlag=2 (ELSE)**：ArrayBrowse 前 RestScreen 純邊框 + MsClearPersist + SAY "選擇"



**GETSYS.PRG GET 離開時**：清 cQuickBoxUpperBuf



#### 修改檔案

- `source/main.prg`、`source/TOOLS.PRG`、`source/GETSYS.PRG`



---



### 22:21 QuickBox 離 GET 時 MsClearPersist(nQuickBoxRow)



#### 問題

QuickBox aTopReg entries 跨 GET 殘留，aSayReg 在離開 QuickBox 後不恢復。



#### 解決

**main.prg**：`PUBLIC nQuickBoxRow := 0`



**TOOLS.PRG QuickBox**：兩處 FOR 之前 `MEMVAR->nQuickBoxRow := nTop-1`



**GETSYS.PRG GetReader**：oGet:KillFocus 之前 `MsClearPersist(M->nQuickBoxRow)` + 清 0



#### 修改檔案

- `source/main.prg`、`source/TOOLS.PRG`、`source/GETSYS.PRG`



---



### 21:47 aTopReg 優先：aSayReg 在重疊位置自動 disable



#### 問題

QuickBox aTitle 跟 BUYSELL SayReg 標頭重疊 → hover 兩個都變色、點擊行為混淆。



#### 解決

**mouse_handler.prg MsHoverUpdate**：aSayReg 掃描前加 `IF nNewHover == 0` 守衛。

**mouse_handler.prg SayRegClick**：進入時先掃 ::aTopReg 重疊位置，有重疊 RETURN 0 給 MsHitTest 接手。



#### 修改檔案

- `compat/mouse_handler.prg`



---



### 21:38 MsKeyFromLabel 補 KY_CTRL + 方向鍵



#### 問題

QuickBox aTitle `[Ctrl↓:進入]` 點不到。MsKeyFromLabel DO CASE 沒有 KY_CTRL+KY_DOWN/UP/LEFT/RIGHT。



#### 解決

DO CASE 補 8 個：

```

CASE cShort == KY_CTRL+KY_DOWN    ; RETURN K_CTRL_DOWN

CASE cShort == KY_CTRL+KY_UP      ; RETURN K_CTRL_UP

CASE cShort == KY_CTRL+KY_LEFT    ; RETURN K_CTRL_LEFT

CASE cShort == KY_CTRL+KY_RIGHT   ; RETURN K_CTRL_RIGHT

CASE cShort == KY_CTRL2+KY_DOWN   ; RETURN K_CTRL_DOWN

... (CTRL2 對應 4 個)

```



#### 修改檔案

- `compat/mouse_handler.prg`



---



### 21:29 QuickBox aTitle 剝除 [...] 框框給 MsKeyFromLabel



#### 問題

QuickBox aTitle 有些版本是 `[KY_CTRL+KY_DOWN+":進入]"`（含 [ ] 包圍）→ MsKeyFromLabel 取 prefix 含 `[` 不匹配 → 註冊失敗。



#### 解決

TOOLS.PRG 兩處 FOR 內加：

```

cLabel := aTitle[nI]

IF Left(cLabel,1) == "[" ; cLabel := SubStr(cLabel,2) ; ENDIF

IF Right(cLabel,1) == "]" ; cLabel := Left(cLabel,Len(cLabel)-1) ; ENDIF

nMsKey := MsKeyFromLabel( cLabel )    ← 用剝除後的 label

```

SAY 顯示仍用原樣（含框）。



#### 修改檔案

- `source/TOOLS.PRG` QuickBox



---



### 21:23 QuickBox aTitle 改用 MsAddPersist (aTopReg 持久層)



#### 問題

QuickBox 結束後 aTitle 熱區立刻消失（MsAddKey 寫到 aMsReg 受 MsPushReg/MsPopReg 影響）→ 點不到。



#### 解決

改用 `MsAddPersist`（寫到 aTopReg，不受 MsPushReg/MsPopReg 影響）。進入時 `MsClearPersist(nTop-1)` 清前次。



#### 修改檔案

- `source/TOOLS.PRG` QuickBox 兩處 FOR aTitle



---



### 20:55 PRINT.PRG Alt+F9 列印 BASE/1003 + BASE/1081 雙修補



#### 問題

1. Alt+F9 列印爆 `BASE/1081 Argument error: +`（PRINT.PRG:654 `?? + xValue +` 串接，xValue 是 NUMERIC/DATE 等非字串型別）

2. 改完上面後又爆 `BASE/1003 Variable does not exist: NINCAMT`（line 608 macro 解析時 SubForm 引用不存在的欄位）



#### 解決方法

- **Bug 1 (型別)**：IF/ELSE 區段結束後加 `IF ValType(xValue) != "C"` 正規化 (N→Str、D→DToC、L→.T./.F.)

- **Bug 2 (欄位不存在)**：4 種 macro 取值用 `BEGIN SEQUENCE / RECOVER` 包圍，配 `ErrorBlock({|e| BREAK(e)})` 暫存替換 (UsrFontInit 全域 ErrorBlock 會優先攔截，必須先替換才能 RECOVER 到)，RECOVER 時 xValue:="" 並 WriteDebugLog 記下 cFrmAlias/cLevel/cField/cAliasXXX 方便日後修報表



#### 踩坑

- BEGIN SEQUENCE 單獨用無效——UsrFontInit 設了全域 ErrorBlock 攔截，必須 LOCAL `bSavedErr := ErrorBlock({|e|BREAK(e)})` 暫存才能讓 RECOVER 攔到

- LOCAL bSavedErr 必須先宣告，否則編譯錯



#### 修改檔案

- `source/PRINT.PRG` (line 596 加 LOCAL bSavedErr，line 603-618 macro 取值 BEGIN SEQUENCE 包圍，line 641 後加 xValue 型別正規化)



---



### 20:43 P37 InvForm K_TAB 切換顯示模式加 DispBegin/DispEnd



#### 問題

TAB 切換 WINDOW_MODE / WINDOW_FORM_MODE 時畫面一行一行刷，明顯緩慢。



#### 解決方法

對照 WINFORM line 345-371 K_TAB case，補上：

- `DispBegin()` 開頭批次繪製

- 結尾補 `oBrowse:refreshAll()` + `WHILE !oBrowse:stabilize() ; END` + `DispEnd()` 一次性輸出



#### 修改檔案

- `source/P37.PRG` K_TAB case (line 432-453)



---



### 20:36 P37 InvForm BROWSE 區滑鼠點選 + 滾輪 + DispBegin/DispEnd



#### 問題

InvForm 主迴圈用原始 InKey + MsHoverCheck/MsClick，無法：

- BROWSE 區點擊資料列跳到該列

- 滑鼠滾輪上下捲動



#### 解決方法

對照 WINFORM line 36/56/98-134 改用 MsBrowseKey 統一處理：

- LOCAL 補 nMsRow, nMsDiff, nHdrH, nMsI

- DispBegin/DispEnd 包覆初始顯示 (WinOutLine → stabilize)

- 主迴圈 wait-for-key 用 `MsBrowseKey(0, oBrowse)` 取代 InKey

- 處理 K_LBUTTONDOWN: `MRow()-nTop-nHdrH` 算 rowDiff → up()/down()

- 處理 K_MWFORWARD/K_MWBACKWARD: 滾輪上下



#### 修改檔案

- `source/P37.PRG` InvForm 主迴圈



---



### 20:20 P37 InvForm 主迴圈補 MsHoverUpdate + MsHitTest 點擊



#### 問題

BottomMessage hint zone hover 變色後無法復原顏色（停在 hover 色）。



#### 解決方法

對照 WINFORM line 73，InvForm 兩處迴圈在 MsHoverCheck 後補：

```

IF Type("M->aMsReg") == "A" ; MsHoverUpdate() ; ENDIF

```

WINFORM 用 `MsHoverCheck(aMouse)` 處理 aMouse 區，再用 `MsHoverUpdate()` 處理 aMsReg/aTopReg 區。InvForm 只做前者，aMsReg hover 不會還原。

順便在 K_LBUTTONDOWN 後補 `MsHitTest()` 路徑處理 aMsReg 點擊。



#### 修改檔案

- `source/P37.PRG` InvForm 兩處 hover 迴圈 (line 205, 224)



---



### 17:15 P37 InvForm K_INS / K_RETURN ReadModal 滑鼠包圍 (參考 WINFORM)



#### 問題

P371/P372 (銷項/進項發票) 進入新增/修改模式後缺：

- BottomMessage hint 區點不到

- GET 欄位點不到跳欄

- SAY 標頭點不到 (這項需另外 SayReg() 註冊)



#### 解決方法

v1 順序錯 (MsPushReg 放在 BottomMessage 之後，hint zones 被立刻清空) → v2 改成嚴格 WINFORM 順序：

```

MsPushReg()                      ← 先清空

acMessage := {...}

BottomMessage(acMessage)         ← 寫到乾淨的 aMsReg/aMouse

FOR MsAddKey                     ← GET 熱區

SayRegEnable()

ReadModal(LocalGetList)

SayRegDisable()

MsPopReg()                       ← 還原父層

```

另補：

- PRIVATE aMsReg PRIVATE 宣告

- STATIC FUNCTION GetColWidth (從 WINFORM 複製，因 STATIC 不跨檔)

- GetColWidth 必須插在 P37.PRG 結尾 `\x1a` (DOS Ctrl+Z EOF) **之前**，否則 Harbour 不會解析到



#### 修改檔案

- `source/P37.PRG` InvForm + ApplyKey K_INS / K_RETURN



---



### 16:10 WINDOW/DBFWIN/TLIB 標題列加 [Alt+H] 熱區



#### 需求

視窗標題列 `[ESC]` 左側加 [Alt+H] 功能提示按鈕（紅框位置）。



#### 解決方法

三個檔同步加：

- WINDOW.PRG MainWin 標題列：cTitleBar Stuff KY_ALT+KY_H + MsAddKey K_ALT_H

- DBFWIN.PRG DbfMWin 同上

- TLIB.PRG TopMessage：cString Stuff + MsAddPersist K_ALT_H（持久層）

- TopMessage Alt+H 與 ESC 之間加 1 空白



按鈕送 K_ALT_H 同鍵盤模式（無 bAction）。



#### 修改檔案

- `source/WINDOW.PRG`, `source/DBFWIN.PRG`, `source/TLIB.PRG`



---



### 15:45 WindowMessage MEMVAR->lHelp 分支補 MsAddKey



#### 問題

WindowMessage 在 `lHelp=.T.` 分支（WINDOW.PRG:1152-1157）只 SAY 不註冊 MsAddKey → help 展開模式下視窗內提示無法點擊。BottomMessage 同位置有，WindowMessage 漏。



#### 解決方法

補 nColSav 抓 SAY 前位置 + MsKeyFromLabel + MsAddKey，沿用下方分支既有的 `Min(COL()-1, nWindowRight-1)` 邊界算法。



#### 修改檔案

- `source/WINDOW.PRG` WindowMessage lHelp 分支



---



### 05:39 P32 SayReg 功能提示 + 選單殘留修正



#### 問題

P32 (收/付款帳) 加 SayReg 功能提示後出現兩個現象：

1. 點客戶代號 SAY 第 1 次正常，第 2 次跑去收款單號

2. 點客戶代號與收款單號中間的空白也會觸發 SAY click 反應



BUYSELL 用相同 SayReg 模式卻完全正常。



#### 追查過程

1. 先懷疑 SayReg 邏輯本身 → 加 SayRegClick diag log 證實 aSayReg 4 個 entry 永遠正確

2. 注意到 GETSYS K_LBUTTONDOWN 還會跑 MsHitTest 並可能覆寫 nMsJumpPos

3. 加 HitTest diag log → 發現 aMsReg 有 7 個殘留 entries (R=2,3,5,6,8,9,11 C=22-40 data4=1..7)

4. 加 AddKey caller trace → 7 個 entries 來自 **PULLDOWNMENU(178)**

5. 那是下拉選單項目，選單關閉後 MENU.PRG 沒清 aMsReg



#### 根因

`source\MENU.PRG` line 223-227 呼叫子程式前後只清 aTopReg (`MsClearAllPersist`) 與 aSayReg

(`SayRegClear`)，沒清 aMsReg。下拉選單的 7 個項目 (key 1-7) 殘留。



點 客戶代號 SAY (row 2 col 30-41) 時：

- SayRegClick 正確返回 3 (ID GET)

- MsHitTest 命中 aMsReg[1] (R=2 C=22-40, K=1)，因為 K=1 剛好對應 GetList[1]=ACCNO，GETSYS 把

  nMsJumpPos 覆寫成 1 → 跳到收款單號



#### 解法

`source\MENU.PRG` line 223-227 加 `MsPushReg()` / `MsPopReg()` 包住 `&cProgString("StartUp")`：



```clipper

MsClearPersist(24)

MsPushReg()              // 隔離選單 aMsReg

&cProgString("StartUp")  // 呼叫子程式 (P32, P11, ...)

MsPopReg()               // 還原選單 aMsReg

MsClearAllPersist()

SayRegClear()

```



#### 影響

- 所有經 MENU.PRG 進入的子程式 (P11/P32/P411/...) 都受惠

- 子程式啟動時 aMsReg 乾淨，不會被選單殘留干擾

- 子程式結束回到選單後恢復選單項目熱區



#### PAYRECE.PRG 增補

`source\PAYRECE.PRG` head 區 4 個 GET 加 SayReg 註冊 (參考 BUYSELL.PRG 250-257 + 291-294)：

- 【收款單號】【收款日期】【客戶代號】【業.務.員】

- READ 前 SayRegEnable、READ 後 SayRegDisable



#### QuickBox/ArrayBrowse 增補

`source\TOOLS.PRG` QuickBox 與 `source\ARRAY.PRG` ArrayBrowse 加 MsPushReg/MsPopReg

(WINDOW.PRG line 200/240 模式)，防止子層 hint 污染外層。



#### 修改檔案

- `source\MENU.PRG` (主修正)

- `source\PAYRECE.PRG` (SayReg 註冊)

- `source\TOOLS.PRG` (QuickBox shield)

- `source\ARRAY.PRG` (ArrayBrowse shield)

- `compat\mouse_handler.prg` (diag log 已清除)

- 備份: `MENU.PRG.bak_msmsreg_shield_20260516_0530` 等





---





## 2026-05-15





### 19:46 Stage E EXE 端移除 BCACHE 資源



#### 問題

Stage E PRG 端完成後，EXE 內仍嵌 20 個 BCACHE 資源 (1024x768~1920x1200 各種解析度的

USRFONT/ASCFONT BCACHE)，啟動會 extract 到目錄。需要從 RC 移除，瘦身 EXE。



#### 解法

- `build\stk32.rc`: 刪除 Multi-BCACHE 區塊 (20 個 RCDATA, ID 201-210/301-310)

- `source\main.prg`:

  - 移除 `STK_EXTRACT_MULTI_BCACHE()` 啟動呼叫 (RC 已無資源可解)

  - 移除 FD=0 fallback 的 `STK_SELECT_BCACHE()` 呼叫，改為依賴 FD=4 self-grow

- `c_code\usrfont_api.c`: ExtractOneRes 表移除 103/106 兩個 BCACHE 項

- 保留: HB_FUNC 本體、`USRFONT_NOBCACHESAVE`、`s_bNoBcacheSave` (避免動 C code 觸發 BCC ICE)



#### 效果

Stk32.exe: **9013248 → 6774272 bytes (省 2.2 MB)**



#### 修改檔案

- `build\stk32.rc` (1429 → 291 bytes)

- `source\main.prg`

- `c_code\usrfont_api.c`

- 備份: `stk32.rc.bak_remove_bcache_20260515_1945` / `usrfont_api.c.bak_remove_bcache_20260515_1945`



#### 待辦 (記錄於 UNDO_LIST.md)

- 未來最終目標：徹底清除 BCACHE (刪 tlib_c.c 的 STK_*_BCACHE HB_FUNC、刪 usrfont_api.c 的 BCACHE save/load、刪 build/bcache/ 目錄)

- USRFONT.TTF subset 瘦身計畫 (可再省 1.15 MB)





### 19:30 Stage E PRG 端移除 FD=2 BCACHE



#### 問題

FontDirect 模式 0/1/2/3/4 中，FD=2 (BCACHE) 是過渡方案，FD=4 (UDAFONT 自帶+自我成長)

已穩定運作，FD=2 可移除。



#### 解法

- `compat\usrfont.prg` LoadFontByFontDirect:

  - Header comment 從 `0/1/2/3` 改 `0/1/3/4`

  - 刪除 `OTHERWISE` (FD=2 BCACHE) 分支，改為「未認識的 FD 全部 fallback 到 FD=4」

- `source\main.prg`:

  - 移除 `ELSEIF Val(GETE("FontDirect")) == 2` 分支

  - 頂部 comment 更新



#### 效果

System.ini 殘留 `FontDirect=2` 不會壞掉，自動 fallback 到 FD=4。



#### 修改檔案

- `compat\usrfont.prg`

- `source\main.prg`

- 備份: `usrfont.prg.bak_remove_bcache_20260515_1930` / `main.prg.bak_remove_bcache_20260515_1930`





### 19:20 ShowAsciiChart 滑鼠閃爍修復



#### 問題

ASCII 字元表 (Ctrl+F12 → 1) 滑鼠進入顯示區後畫面瘋狂閃爍。



#### 解法

同 ShowUsrFontChart 的 lNeedRedraw 守衛：只在 PgDn/PgUp 翻頁時才重畫，

滑鼠 hover 喚醒 InKey(0) 但 DO CASE 不命中 → 跳過重繪。



#### 修改檔案

- `source/TLIB.PRG` ShowAsciiChart 函式重寫





### 19:15 ShowUsrFontChart 滑鼠閃爍修復



#### 問題

USRFONT.TTF PUA 預覽（Ctrl+F12 → 2）滑鼠 hover 觸發 InKey(0) 返回 →

每次迴圈 CLS + 920 個 @SAY 全重畫 → 嚴重閃爍。



#### 解法

新增 `lNeedRedraw` 旗標，初值 .T.，繪製完設 .F.；只有 PgDn/PgUp/D 切換時

才重設 .T.。滑鼠 hover 觸發 InKey 返回但 flag 仍 .F. → 跳過重繪。



#### 修改檔案

- `source/TLIB.PRG` ShowUsrFontChart 函式重寫





### 19:08 USRFONT 預覽空 slot 顯示全形＿（含踩坑）



#### 問題

空 PUA slot 原顯示 ".."，視覺不對齊；改顯示全形字符。



#### 踩坑

- 先試全形「口」(BIG5 a466)，但 replace_all 誤傷 9 處「進口/印表口」等字串

- 根因：BIG5 字串中 \xa4\x66 與相鄰字元的低/高位元組產生假匹配

- 教訓：不可用 replace_all 改 BIG5 短字符，必須帶完整 statement context



#### 解法

用「＿」(BIG5 a1c4) 全形底線，且只對 `@ nRow, nCol SAY "..."` 完整 pattern 替換。



#### 修改檔案

- `source/TLIB.PRG` ShowUsrFontChart 兩處 placeholder





### 19:00 Ctrl+F12 字型選單 + USRFONT.TTF 預覽



#### 問題

- 既有 Ctrl+F12 只能看 ASCII chart，無法檢視 USRFONT.TTF 的 PUA glyph

- 造字後沒辦法快速確認 glyph 是否正確嵌入



#### 解法

- 新增 `ShowFontMenu()`：選單讓使用者選 ASCII / USRFONT

- 新增 `ShowUsrFontChart()`：兩種顯示模式 (D 切換)

  - 密集：920 字/頁 (40×23)，2 cols/字

  - 詳細：230 字/頁，0xXXXX + glyph，8 cols/entry

- 新增 `Num2Hex4()`：int → 4 位大寫 hex

- APPSETUP.PRG 改 Ctrl+F12 綁定到 ShowFontMenu



#### 踩坑

- TLIB.PRG 是 CRLF 行尾，patch 用 LF 第一次 matches=0

- 修正：Python 偵測 CRLF 自動 normalize



#### 修改檔案

- `source/TLIB.PRG` 新增 3 函式

- `source/APPSETUP.PRG` Ctrl+F12 綁定變更

- 備份：TLIB.PRG.bak_before_fontmenu_20260515_1856 / APPSETUP.PRG.bak_before_fontmenu_20260515_1856





---





## 2026-05-14



### 08:00 色彩編輯器 CTRL+F11 強化 + LOG 改善



#### 色彩編輯器（color_theme_class.prg）

- Tab 標籤改用 KY_* 鍵盤符號圖示（KY_1:Hint 取代 1.Hint）

- Tab hover/click：MsAddKey 註冊到 aMsReg，MsHoverUpdate 處理 hover 還原

- Help bar：WindowMessage + nKey override（K_LEFT/K_RIGHT/K_TAB/K_ENTER/K_F10/K_ESC）

- Help bar 標籤：Tab:[前景]/背景、逆調色、正調色、套用、存檔、離開

- MsPushReg/MsPopReg 隔離（不洩漏到外部）

- CLEAR 覆蓋 nMaxTop~nMaxBottom（所有頁面的最大範圍聯集）

- SaveScreen/RestScreen 框架+1（不用全螢幕）

- 前景/背景 active 時顯示 [] 指標（[W+] /R 或 W+ /[R]）

- 第 7 頁 Dialog 恢復（DlgNormColor、DlgHiliteColor、DlgHintColor、DlgShadowColor）

- MenuSelColor 恢復至第 3 頁 Menu

- 踩坑：MsHoverCheck（LOCAL）與 MsHoverUpdate（全域 aMsReg）雙重處理導致兩色，移除 MsHoverCheck 只保留 MsHoverUpdate



#### LOG 改善（clipper_compat.prg、fw_prnout.prg、main.prg）

- WriteDebugLog：加入電腦名稱+使用者名稱（STATIC 快取，hb_GetEnv）

- StkBuildTitle：加入電腦名稱+使用者名稱+編譯時間（STATIC 快取）

- SpoolOpenToFile/CloseAndPrint：入口 LOG（追蹤列印卡住問題）

- PrnAsk START LOG：含 EXE 檔案日期（版本識別）

- 修改檔案：clipper_compat.prg、fw_prnout.prg、main.prg（Stk32+PrnAsk）





#### 給後續開發者的建議：功能提示（Hover/Click）開發指南



**核心機制（前輩已建好，直接用）：**



1. **顯示功能提示**：用 `BottomMessage(acMessage)` 或 `WindowMessage(nTop, nLeft, nBottom, nRight, acMessage)`

   - `acMessage` 是字串陣列，每個元素格式：`KY_符號 + ":說明文字"`

   - 例：`{ KY_TAB + ":前景/背景", KY_ESC + ":離開" }`

   - KY_* 符號定義在 `CustomChar.ch`（KY_F1~KY_F12、KY_ESC、KY_ENTER1、KY_TAB、KY_LEFT、KY_RIGHT 等）

   - `MsKeyFromLabel()` 會自動解析 KY_* 符號並回傳對應的 InKey 代碼



2. **Hover 變色+還原**：用 `MsHoverUpdate()`（全域 aMsReg）

   - WindowMessage/BottomMessage 會自動透過 `MsAddKey()` 註冊到全域 `aMsReg`

   - 事件迴圈中 K_MOUSEMOVE 時呼叫 `MsHoverUpdate()` 即可

   - **不要同時用 `MsHoverCheck()`（LOCAL aMouse）和 `MsHoverUpdate()`（全域），會產生雙重變色**



3. **Click 閃爍+回傳按鍵**：用 `MsClick(aMouse, @nHover)`

   - `aMouse` 是 `WindowMessage()` 的回傳值

   - 回傳值不為 0 時，用 `hb_keyPut(nHit)` 注入按鍵讓主迴圈處理



4. **如果 MsKeyFromLabel 映射的 Key 不對**：在 WindowMessage 回傳後手動覆蓋 `aMouse[n][6]`

   - 例：`aMouse[2][6] := K_LEFT`（KY_LEFT 預設映射 K_SH_TAB，需改成 K_LEFT）



**隔離機制：**

- 進入子畫面前：`MsPushReg()` 保存外部狀態

- 離開子畫面後：`MsPopReg()` 恢復外部狀態

- 避免子畫面的 aMsReg 註冊洩漏到外部



**額外按鈕（非 BottomMessage/WindowMessage 管理的）：**

- 用 `MsAddKey(nRow, nColS, nColE, nKey, cSayStr, cNormColor)` 手動註冊到 aMsReg

- 例：Tab 頁面按鈕就是用 MsAddKey 手動註冊



**參考範例：**

- 標準用法：`DAYWIN.PRG`（BottomMessage + MsHoverCheck + MsClick）

- 自訂對話框：`color_theme_class.prg`（WindowMessage + MsHoverUpdate + MsAddKey + MsPushReg/MsPopReg）



**事件迴圈範本：**

```

PRIVATE aMouse := {}, nHover := 0

MsPushReg()

...

aMouse := WindowMessage( nTop, nLeft, nBottom, nRight, acHelpMsg )

nHover := 0

...

DO WHILE !lExit

   nKey := Inkey(0, INKEY_ALL)

   IF nKey == K_MOUSEMOVE

      MsHoverUpdate()        /* 全域 hover 處理 */

      LOOP

   ENDIF

   DO CASE

   CASE nKey == K_LBUTTONDOWN

      nHit := MsClick(aMouse, @nHover)

      IF nHit != 0 ; hb_keyPut(nHit) ; LOOP ; ENDIF

      /* 其他 click 處理... */

   CASE nKey == K_ESC

      lExit := .T.

   /* 其他按鍵... */

   ENDCASE

ENDDO

MsPopReg()

```



---



## 2026-05-13



### 22:00 PrnAsk GDI 預覽基礎建設 + 印表機紙張



#### RC 資源打包（printdlg.rc）

- 10 個工具列 BMP 圖示打包進 RC（TB_PRINTER~TB_EXIT，部署不需 icons/ 資料夾）

- 7 個 ComboBox 加 WS_VSCROLL 捲軸

- rc_rebuild.bat：一鍵 RC 編輯流程（fix_resedit_rc → rebuild → copy → run）



#### GDI 預覽（fw_prnout.prg）

- 工具列按鈕：FILENAME → RESOURCE（從 RC 載入，不依賴外部檔案）

- FwGdiPrvPrintDlg：對話框紙張傳遞給 GdiPrintPrn（原本傳 NIL）

- FwGdiPrintDialog fallback：GetWinPrinterPaperMil → FwGetPrnDefPaper（讀喜好設定）



#### 印表機紙張（gdi_print_c.c、prn_to_pdf.prg）

- GdiGetPrinterPrefPaper（新 C 函式）：GetPrinter level 2 讀 per-user DEVMODE（使用者喜好設定）

- FwGetPrnDefPaper：PowerShell → C 函式（讀真正的印表機喜好設定，非驅動預設）

- OpenPrinterWithTimeout：5 秒 timeout，安全放手不殺（不用 TerminateThread）

- 3 處 OpenPrinterW 全部改用 OpenPrinterWithTimeout

- 踩坑：.NET PrinterSettings.DefaultPageSettings 讀到的是驅動預設值，GetPrinter level 2 才正確

- 踩坑：TerminateThread 可能造成 winspool.dll 死鎖，改用安全放手方式



#### 列印基礎建設（print_spool.prg、main.prg）

- FwDirectPrint + /DIRECTPRINT 參數（保留，目前未使用）

- SpoolDirectPrint/CallDosPntGo：維持直接呼叫 GdiPrintPrn（有 OpenPrinterWithTimeout 保護）

- 修改檔案：gdi_print_c.c、prn_to_pdf.prg、fw_prnout.prg、main.prg、printdlg.rc、print_spool.prg



## 2026-05-12



### 10:30 cMenuSelColor 套用 + 主題切換同步

- **cMenuSelColor 套用**：LoadColorTheme() 結尾將 cMenuSelColor 塞入 cMenuBarColor/cMenuBoxColor/cOtherColor 的第二組色值

- **主題切換同步**：Alt+F12 上下鍵切換時，ApplyThemeN 後呼叫 hb_SetEnv+LoadColorTheme 同步所有 MEMVAR 顏色（5 處）

- **MENU.PRG 直接引用 MEMVAR**：SetColor(aColor[MENU_BAR/BOX_COLOR]) 改為 SetColor(MEMVAR->cMenuBarColor/cMenuBoxColor)

- **Debug 確認**：LoadColorTheme 正確更新 MenuSel，INI 覆蓋機制正常運作

- 限制：Alt+F12 預覽時無法看到 MenuSelColor 效果（對話框蓋住選單區）

- 修改檔案：color_theme.prg, MENU.PRG



### 07:20 Dialog 顏色併入佈景主題

- **新增 4 個顏色**：cDlgNormColor, cDlgHiliteColor, cDlgHintColor, cDlgShadowColor

- CTRL+F11 色彩編輯器新增第 7 頁「Dialog」（a7, 頁碼範圍 1-7, ">" 選取符號）

- DIALOG.PRG 硬編碼顏色改為 MEMVAR 引用

- Dialog 清除範圍修正（原本多清 1 行 1 列超出 BOX 框）

- Dialog 陰影支援（右邊+下方，預設 N/N=關閉）

- 色彩編輯器切頁框架清除問題已調查（還原為原始 Block 行為）

- 修改檔案：main.prg, color_theme.prg, color_theme_class.prg, DIALOG.PRG



### 06:40 Dialog Hover + 滑鼠點擊修正

- **Dialog hover**：OkButton/CancelButton/ToggleElement 的 MsAddBtn 加入 cSayStr+cNormColor 參數

- **ReadKey hover**：MouseMove 掃描 aMsReg 只處理有按鈕文字的項目

- **滑鼠點擊 bug 修正（原有問題）**：點擊輸入框時舊欄位沒有 dehilite，導致全部顯示青色

  - 根因：主迴圈 K_ENTER 路徑跳過 dehilite（只有鍵盤 OTHERWISE 路徑有做）

  - 修正：ReadKey 點擊處理在切換 nCursor 前先呼叫 nSysPara=3 dehilite 舊欄位

- **InputData 重繪**：GET 編輯結束後無條件用 cColorNorm 重繪欄位，清除 N/BG 殘留

- 修改檔案：DIALOG.PRG



### 01:00 cMenuSelColor + 色盤調整

- **cMenuSelColor**：新增每個主題的選單選取色

- **色盤調整**：14 個主題共 18 處 slot 調整（主題 0 不動）

  - 主題 8-12：slot 8=15 維持相同（深色主題刻意設計）

- 修改檔案：main.prg, color_theme.prg, color_theme_class.prg



## 2026-05-09



### 00:00 ColorTheme CLASS SaveIniSection Bug Fix



- **SaveIniSection v3**: block replacement approach (not line-by-line)

  - Normalize to LF, process, convert back CRLF

  - Find [SECTION] block start/end, replace entire block

  - Fix: no more empty sections, no more destroying other sections

  - Fix: no trailing blank lines

- **F10 save key**: F2 was intercepted by SetKey, changed to F10

- **MessageBox confirmation**: F10 save shows success message

- **cMsgShadowColor**: MessageBox shadow color separated from BLANK_COLOR

- **cScrollArrowColor/ThumbColor/TrackColor**: PickList scrollbar colors to COLOR_THEME

- **hStartup removed**: SaveToIni saves ALL colors (no delta), simpler and safer

- **debug cleanup**: removed CT debug logs, removed s_hDefaults/SetColorDefaults

- **INI corruption fix**: SaveIniSection no longer inserts inside existing sections

- **Files**: ini_loader.prg, color_theme_class.prg, color_theme.prg, main.prg, STK.PRG, TLIB.PRG, picklist.prg, APPSETUP.PRG



### 22:30 ColorTheme CLASS + INI Override + EditDialog



- **ColorTheme CLASS**: New/Apply/Revert/SaveToIni/EditDialog

- **INI Override**: LoadIniSection/SaveIniSection, [Color] + [Color_ThemeN]

- **EditDialog**: 6 tabbed pages, mouse support, left/right cycle, F2 save

- **21 color variables**: all centralized in COLOR_THEME

- **Scrollbar colors**: cScrollArrowColor/ThumbColor/TrackColor

- **Entry points**: P74 menu + Ctrl+F11 hotkey

- **SUBMENU.DBF**: item 4 changed to color editor

- **Checklist comment**: 5-step guide for adding new colors

- **Files**: color_theme_class.prg(new), ini_loader.prg, color_theme.prg, picklist.prg, main.prg, STK.PRG, TLIB.PRG, WINDOW.PRG, mouse_handler.prg, dbedit_mouse.prg, P74.PRG, P73.PRG, APPSETUP.PRG, stk32.hbp, SUBMENU.DBF



### 14:00 COLOR_THEME Centralize + SelectCopy Default + Multi-Theme Tuning



- **SelectCopy default FALSE**: gtwvt.c bBeginMarked/bSelectCopy HB_TRUE->HB_FALSE, AppendMenu dynamic

- **Default theme slot 7**: RGB(170,170,170)->RGB(140,140,140)

- **COLOR_THEME centralize**: 14 new variables moved from hardcoded to color_theme.prg

  - cTopHoverColor, cSayHoverColor, cTopMsgColor, cMenuBarColor, cMenuBoxColor

  - cMsgBoxColor, cMsgBoxHiColor, cBrowseRowColor, cBrowseCellColor

  - cOtherColor, cPickerBoxColor, cPickerBarColor, cPickerTitleColor, cBlankColor

- **MessageBox border/text separation**: cMsgBoxHiColor for border, cMsgBoxColor for text

- **aColor re-apply**: STK.PRG LoadColorTheme() post-apply block

- **dbEditEx defColor**: {3,4}->{3,2} to use cBrowseRowColor

- **Themes tuned**: 0,2,5,6,7,8,13 (hover, hint, menu, browse, msgbox colors)

- **Files**: gtwvt.c, color_theme.prg, main.prg, STK.PRG, TLIB.PRG, WINDOW.PRG, mouse_handler.prg, dbedit_mouse.prg



### 13:40 SelectCopy default FALSE



- **Problem**: System menu SelectCopy default is TRUE, should be FALSE

- **Root cause**: gtwvt.c bBeginMarked/bSelectCopy init = HB_TRUE, AppendMenu hardcoded MF_CHECKED

- **Fix**:

  - Line 474: bBeginMarked = HB_TRUE -> HB_FALSE

  - Line 489: bSelectCopy = HB_TRUE -> HB_FALSE

  - Line 3897: MF_CHECKED -> conditional (bBeginMarked ? MF_CHECKED : MF_UNCHECKED)

- **Files**: gtwvt_patch/gtwvt.c



### 06:00 GetPrePrice/GetStockQty Cache 加速 + MenuSel ShowHotKeys 修復 + WinForm refreshAll



#### 1. GetPrePrice / GetStockQty Cache

- **問題**：BUYSELL 表身新增時，AskPartsBox 零件瀏覽滾動明顯比 P113 慢

- **根因**：SubForm1.DBF 的 PartKey W 格式比 Y 格式多第 8 欄 PARTS->NPRICE1，cGetBlock 為 GetPrePrice()，每列做 2 次 dbSeek + 可能開關 NoSale/NoBuy DBF

- **解法**：在 GetPrePrice / GetStockQty 內部加 STATIC hash cache

  - ValType 區分控制模式（.T./.F.）與查詢模式（string）

  - .T. 啟用建新 hash，.F. 關閉並 NIL 釋放記憶體

  - stk.ini PPCache=Y 控制開關

  - 原有邏輯零修改

- **修改檔案**：P113.PRG（GetPrePrice）、TOOLS.PRG（GetStockQty）、BUYSELL.PRG、stk.ini



#### 2. MenuSel ShowHotKeys 修復

- **問題**：主選單點擊 HDD 有 hover 但點擊無效，無法觸發 ShowHotKeys

- **根因**：MsMenuSel 的 click 處理只比對 aItems（選單項目），沒檢查 aTopReg（Persist 熱區）

- **解法**：在 aItems 比對失敗後加入 HitTest() 檢查 aTopReg

- **修改檔案**：mouse_handler.prg



#### 3. WinForm refreshAll

- **問題**：P11 等 WinForm 第一次進入時畫面空白，按任意鍵後才正常

- **根因**：WinForm() 只有 forceStable() 沒有 refreshAll()，Table() 兩者都有

- **解法**：在 forceStable() 前加 refreshAll()

- **修改檔案**：WINFORM.PRG



### 07:00 DBFWIN/WINDOW 進出隱藏/還原 Status 功能提示

- **問題**：進入 DBFWIN/WINDOW 時 Status 列的 HDD ShowHotKeys 熱區應該隱藏，離開時應還原

- **解法**：

  - 進入：`MsClearPersist(24)` 隱藏 row 24 熱區

  - 離開：`Status()` 重建熱區

- **修改檔案**：DBFWIN.PRG、WINDOW.PRG



## 2026-05-08



## 2026-05-07



### 10:30 框選複製改進 + DBFWIN/WINDOW 游標還原

- **框選複製改進**：

  - 預設開啟（不需先點系統選單）

  - 系統選單「框選複製」改為 toggle 開關（有 ✓ 勾勾）

  - 拖動距離 > 5px 才進入框選模式，普通點擊不受影響

  - 框選完成後保持啟用狀態（不用每次重新開啟）

  - 選單文字從 "Mark and Copy" 改為中文「框選複製」

  - 不喜歡的使用者可從系統選單關閉

  - **修改檔案**：gtwvt.c、gtwvt.h

- **DBFWIN/WINDOW 游標消失修正**：

  - **問題**：BUYSELL 單號欄位按 F10 開啟 DBFWIN，回來後游標消失

  - **根因**：DBFWIN.PRG 和 WINDOW.PRG 進入時 SetCursor(SC_NONE) 但離開時未還原

  - **修正**：進入時 nLastCursor := SetCursor()，離開時 SetCursor(nLastCursor)

  - **修改檔案**：DBFWIN.PRG、WINDOW.PRG





### 12:30 PRN 預覽列印 全面改版



#### 訴求

- PRN 預覽只顯示 1 頁（tractor page 9×11 一頁塞兩頁 + CropBox 只顯示上半）

- 列印紙張大小不受 PRN 控制（GDI 用印表機預設 11 吋）

- 使用者改變紙張大小後預覽/列印不一致



#### 詭異現象與踩坑



**1. 預覽只顯示 1 頁**

- 原因：tractor page 加倍（nPH×2=11吋），add_cropbox.py 加 CropBox 只顯示上半 5.5 吋

- 修正：移除 tractor 加倍，每頁實際大小。auto-break 自然分頁



**2. [?100;H;W] 不是換頁指令**

- 踩坑：一度把 [?] 當作換頁觸發，造成額外分頁

- 真相：[?] 只設紙張，~I 只重設字型。換頁靠 AdvanceLine auto-break（Y < 0 時觸發）

- 修正：移除 [?] 換頁邏輯，恢復 auto-break



**3. ~W2 標題第一頁無效**

- 原因：s_nPageNum==0 時不 flush text，~W2 設了 s_nHScale=2，但 ~W1 在 flush 前就重設為 1

- FIX2：控制碼前建頁+flush。但 flush 空格造成多一行

- 最終：控制碼前只建頁不 flush。空格留到 LF 才 flush



**4. CloseSpool 多送 CRLF**

- 現象：PRN 每頁固定 22 行，但檔案尾部多一個 CRLF → 23 行 → 溢頁

- 原因：Harbour SET PRINTFILE "" 關檔時自動加 CRLF

- 修正：SpoolCloseAndPrint() 關檔後立即 strip 尾部 CRLF（從源頭修）



**5. GDI DEVMODE 不生效（紙張永遠 9×11）**

- 現象：GdiOpenPrinter 傳了 DEVMODE 自訂紙張，但 page 仍 1620×1980（9×11）

- 根因：gdi_print.prg 的 BEGINDUMP 有**舊版 GdiOpenPrinter**（無 DEVMODE），linker 用舊版

- 修正：移除 gdi_print.prg 的 BEGINDUMP 重複函式，統一用 gdi_print_c.c



**6. updatePrnInfo 非同步回調偷改紙張值**

- 現象：頁面設定開啟後，紙張從 9×5.5 變成 9×(11/3)，預覽變 4 頁

- 根因：getprninfo 回調 → updatePrnInfo → _updatingUI=false → onPrOrChg() → 直印模式 W>H 交換 prW/prH

- 修正：_updatingUI=false 移到 updatePrnInfo 最後（onPrOrChg + margins 設完後才 false）

- 教訓：非同步回調修改 UI 狀態必須全程鎖定



**7. ESC/P PASSTHROUGH 印出亂碼**

- 現象：每頁左上角出現 C╝ 和 C) 怪符號

- 原因：GdiSetPageLength 用 PASSTHROUGH 送 ESC(C 指令，驅動不支援，raw bytes 變可見文字

- C╝：C 是指令碼，╝(0xBC=188) 是 page length 低位元

- 修正：移除 GdiSetPageLength



**8. GDI 列印內容裁切不縮放**

- 現象：使用者改小紙張後，GDI 印出的內容被裁切而非縮放

- 原因1：s_lCallerPaper 沒在 GdiPrintPrn 設定 → [?] 覆蓋 UI 紙張

- 原因2：GdiSetScale(MM_ANISOTROPIC) 的 SetMapMode 被 StartPage 重設

- 原因3：內容排版用 UI 紙張（小紙）而非原始紙張 → 分頁錯誤

- 修正：分離 s_nUiW/H（DEVMODE 用）和 s_nCustomW/H（排版用），GdiSetScale 從原始→UI 縮放



**9. 預覽縮放（使用者改紙張）**

- 需求：改紙張大小，頁數不變，內容等比縮放到新紙張

- 做法：JS rescalePaper() 調整 canvas 比例 + ctx.scale(scW,scH)

- 寬度不變時 scW=1.0，高度壓縮 scH=newH/origH



**10. 雙頁模式顯示全部頁數**

- 踩坑：改成全頁捲動模式 → 雙頁模式看到所有頁面（4 頁全顯示）

- 使用者要的：雙頁一次顯示 2 頁，按下一頁看後 2 頁

- 修正：恢復 C1/C2 逐頁模式（單頁=1 canvas，雙頁=2 canvas，按鈕翻頁）



#### 紙張規則（三層優先）

1. PRN [?100;H;W] → 初始值（文件帶入，填入 UI）

2. 印表機預設 → PRN 沒設定時的 fallback

3. 使用者 UI 修改 → **最終主宰**



#### 修改檔案

- `prn_to_pdf.prg`：移除 tractor 加倍、auto-break、FIX2、[?] 處理、s_lCallerPaper、GDI 縮放

- `print_spool.prg`：SpoolCloseAndPrint strip 尾部 CRLF

- `gdi_print_c.c`：DEVMODE 紙張、GdiSetScale、移除 ESC/P PASSTHROUGH

- `gdi_print.prg`：移除 BEGINDUMP 重複函式

- `fw_prnout.prg`：viewer 逐頁模式、rescalePaper、psOK/confirmPrint 紙張傳遞、_updatingUI

- `stk32.hbp`：加入 gdi_print_c.c

- `prnask.hbp`：加入 gdi_print_c.c



### 14:30 空白尾頁 + GDI 列印修正



#### 問題

1. 多餘空白尾頁（PRN 尾部 CRLF 造成溢頁）

2. 延遲建頁（lazy page creation）破壞 GDI 多頁列印

3. ESC/P PASSTHROUGH 印出亂碼（C╝ 和 C)）



#### 解決

- **空白尾頁**：CloseSpool 源頭 strip + ParseAndRender 安全網 strip

- **GDI 多頁**：恢復直接 auto-break（不用延遲建頁）

- **ESC/P 亂碼**：移除 GdiSetPageLength（PASSTHROUGH 不可靠）

- **踩坑**：延遲建頁在 PDF 正常但 GDI 只印 1 頁（FlushText lazy check 時機問題）

- **教訓**：兩個問題（空白頁 + 多頁列印）必須同時解決，不能顧此失彼



#### 修改檔案

- `prn_to_pdf.prg`：ParseAndRender strip、恢復直接 auto-break、移除 GdiSetPageLength

- `print_spool.prg`：SpoolCloseAndPrint strip（源頭）



### 15:30 暫存檔路徑 + WebView2 fallback



#### 暫存檔路徑

- PRN/PDF 產生在 `GETE("LOC_TMP")` 目錄

- `LOC_TMP` 沒設或目錄不存在 → fallback 到 `hb_DirBase()`（PrnAsk 同目錄）

- PDF 跟 PRN 同目錄（`hb_FNameDir(cPrnFile)`），不需額外改

- **修改檔案**：`print_spool.prg`（GenPrnFileName）



#### WebView2 fallback

- `FwPrnPreview` 開頭偵測 `CheckWebView2Reg()`

- 沒有 WebView2 → 跳過預覽，直接 GDI 列印

- 有 WebView2 → 正常預覽流程

- 適用 Win10 x86 無法安裝 WebView2 的情況

- **修改檔案**：`fw_prnout.prg`（FwPrnPreview）













### 22:00 頁碼範圍修正（輸出頁碼制）



#### 問題

- PRN split 做法：segment ≠ 輸出頁（auto-break 跨頁時多印）

- 選 1-4 頁印出 5 頁（segment 1 auto-break 成 2 頁）



#### 修正

- 改為輸出頁碼制：跑完整 PRN，追蹤 s_nPageNum

- 選中的頁：GdiNewPage + 渲染

- 沒選的頁：跳過 GdiNewPage + 跳過渲染（s_lGdiSkipRender）

- 不浪費紙、不碰 GDI 座標、不破壞狀態

- 所見即所印



#### 修改檔案

- prn_to_pdf.prg：NewPdfPage GDI 頁碼追蹤、GdiFlushText skip、GdiStartDoc lStartPage



### 21:30 GDI 底線缺失 + PDF/GDI 頁高不一致 + 頁碼範圍 + 殘留清理



#### GDI 底線缺失

- **現象**：預覽有底線，GDI 列印沒有

- **根因**：GdiFlushText 從來沒有底線程式碼（只有 PDF 版 FlushText 有）

- **修正**：DrawHLine/DrawFullHLine 加 GDI 分支 + GdiFlushText 加底線繪製



#### PDF/GDI 頁高不一致（四頁變三頁）

- **現象**：PDF pgH=792，GDI pgH=842 → 分頁不同

- **根因**：ResetState 的 s_nPaper=2（寫死A4），GDI 沒讀印表機預設

- **修正**：s_nPaper := -1 + GetPaperPt OTHERWISE 用 s_nCustomW/H



#### 頁碼範圍改用 PRN 拆分

- PRN 用 FF 分割，只取選中頁面拼接送進 ParseAndRender

- 不浪費紙、不破壞 GDI 狀態



#### s_bSkipRender 殘留移除

- gdi_print_c.c 殘留的 s_bSkipRender 擋住 GdiDrawLine → 完全移除



#### GDI 列印縮放下架

- GdiSetScale(MM_ANISOTROPIC) 造成內容旋轉/變形 → 已移除，列入 UNDO LIST



#### 修改檔案

- prn_to_pdf.prg、gdi_print_c.c、fw_prnout.prg



### 21:30 GDI 底線缺失 + PDF/GDI 頁高不一致 + 頁碼範圍 + 殘留清理



#### GDI 底線缺失

- **現象**：預覽有底線，GDI 列印沒有

- **根因**：GdiFlushText 從來沒有底線程式碼（只有 PDF 版 FlushText 有）

- **修正**：DrawHLine/DrawFullHLine 加 GDI 分支 + GdiFlushText 加底線繪製



#### PDF/GDI 頁高不一致（四頁變三頁）

- **現象**：PDF pgH=792，GDI pgH=842 → 分頁不同

- **根因**：ResetState 的 s_nPaper=2（寫死A4），GDI 沒讀印表機預設

- **修正**：s_nPaper := -1 + GetPaperPt OTHERWISE 用 s_nCustomW/H



#### 頁碼範圍改用 PRN 拆分

- PRN 用 FF 分割，只取選中頁面拼接送進 ParseAndRender

- 不浪費紙、不破壞 GDI 狀態



#### s_bSkipRender 殘留移除

- gdi_print_c.c 殘留的 s_bSkipRender 擋住 GdiDrawLine → 完全移除



#### GDI 列印縮放下架

- GdiSetScale(MM_ANISOTROPIC) 造成內容旋轉/變形 → 已移除，列入 UNDO LIST



#### 修改檔案

- prn_to_pdf.prg、gdi_print_c.c、fw_prnout.prg



---



## 2026-05-06（驚心動魄的一天）



### 06:30 功能提示與 hover 修正

- **TopMessage 加 PgUp**：TLIB.PRG — [PgUp] [PgDn] 中間空 1 格

- **WINDOW/DBFWIN 標題列按鈕**：PadC+Stuff 整行 SAY，[ESC] [標題] [PgUp] [PgDn]，MsAddKey 非持久

- **SayReg 隔離**：WINDOW/DBFWIN/TABLE 加 MsPushReg/MsPopReg，防止上層 SayReg 干擾子視窗

- **hover 偵測修正**：mouse_handler.prg — HoverUpdate/HoverCheck/HitTest/MsClick 改用 Len(string)，解決造字字元 hover 範圍多 1 格

- **gtwvt col+2 孤兒色修正**：PutChar 清除 col+2 時保留原始顏色，防止 hover 色泄漏

- **踩坑**：COL()-1 算 hover 偏大、MsAddPersist 殘留、分開 SAY 造字溢色、SC_NONE 未定義、nCol/cBuffer 變數衝突

- **修改檔案**：TLIB.PRG、WINDOW.PRG、DBFWIN.PRG、TABLE.PRG、BUYSELL.PRG、STOCK.PRG、mouse_handler.prg、gtwvt.c



### 11:00 P32 / RecBillAction 修正

- **P32**：加 SetCursor(SC_NONE) / 還原，防止進出殘留游標

- **RecBillAction**：DBF macro 用 10 字元截斷名稱 RecBillAct，P412.PRG 加 wrapper 串接 RecBillAction()（Clipper 相容）

- **修改檔案**：P32.PRG、P412.PRG



### 10:47~16:02 ASCFONT CHR(20) 上箭頭偏右（從誤判到真相的完整故事）

- **10:47 發現問題**：CHR(20) ▲ 偏右被截半邊，CHR(127) ▼ 正常。華康字型中兩者 metrics 完全相同（LSB=66, RSB=64, AW=512），但 ASCFONT-STK.TTF 的 PUA 映射不同

- **10:50 加 debug**：CaptureGlyphAlpha 掃描 alpha mask bounding box。結果：U+EF02(▲) xMin=8（偏右），U+EF03(▼) xMin=0（靠左）。確認是 TTF 字型問題

- **11:00 查字型**：用字型編輯器確認 ASCFONT-STK.TTF 的 hmtx 表。EF02: LSB=225（應為 66），EF03: LSB=0（應為 66）。glyf 座標正確但 hmtx 不匹配 — 搬運時 LSB 搞錯了

- **11:05 fontTools 修正**：Python fontTools 將 hmtx LSB 改為 glyf xMin。▲▼ 居中正確！

- **11:10 災難**：修正後 MCol 座標全系統偏移（點 col 78 回傳 49）。以為 fontTools 改壞了字型內部結構（OS/2、hhea 等表）

- **11:15 還原 TTF**：恢復原版 ASCFONT.TTF。嘗試 CaptureGlyphAlpha runtime 自動置中（2×nW 寬 buffer 取樣再居中）

- **11:20 新問題**：全部 ASCFONT 字元自動置中 → 滾動條、選單邊框壞掉。改為只針對 EF02 → 有效但 BCC ICE 無法編譯

- **11:30 BCC ICE**：usrfont_c.c 單獨編會 ICE（Internal Compiler Error）。發現加 dummy.c 一起編可通過。但 obj 驗證發現修改未編入

- **11:40 再度還原**：放棄 runtime 修正，列入 UNDO LIST

- **16:02 真相大白**：修正 PickList scrollbar 問題時發現，MCol 偏移的真正原因是 **DragSelect**（gtwvt.c 中延遲 K_LBUTTONDOWN 的機制），不是 fontTools！移除 DragSelect 後，fontTools 修正的 TTF 完全正常運作。▲▼ 都居中，滾動條正常，MCol 正確

- **修改檔案**：ASCFONT.TTF（fontTools 修正 hmtx LSB：EF02 225→66, EF03 0→66）



### 14:00 游標尾巴（Caret Tail）根治

- **問題**：全系統 @ SAY 後 caret 殘留在字串尾端閃爍。double buffer 特有 — PaintText 已有 !CaretHidden 檢查，但 ScanAndDrawUda 無條件 ShowCaret

- **為什麼兩條路徑**：啟動初期（usrfont hook 安裝前）走 PaintText，hook 安裝後走 ScanAndDrawUda。兩條都要保護

- **四層修正**（缺一不可）：

  1. gtwvt PaintText：ShowCaret 加 !CaretHidden（非 double buffer 路徑）

  2. usrfont_scan ScanAndDrawUda：兩條路徑（no-UDA / UDA）ShowCaret 加 !g_bCaretHidden（double buffer 路徑）

  3. gtwvt Refresh：UpdateCaret 在 paint 之前執行，先更新 g_bCaretHidden 讓 ScanAndDrawUda 用最新值

  4. gtwvt UpdateCaret+ReadKey：caret 位置用 s_nCaretRow/Col 分離，只在 ReadKey 且游標可見(>0)時更新位置。@ SAY 移動 GT cursor 不影響 caret

- **第四層是關鍵**：ReadKey 的 `if(_s>0)` 判斷。只在游標樣式可見時才更新 caret 位置，SC_NONE 時不更新。caret 停留在上次 GET 的正確位置

- **輔助修正**：GAUGE 移除多餘 SetCursor(SC_NORMAL)、STK 啟動 SetCursor(0)、TopMessage/BottomMessage/Status/WindowMessage/SayKey 加 SetPos save/restore

- **6 次失敗歷程**：

  1. Refresh 移除 WM_MY_UPDATE_CARET → 離開 BUYSELL 殘留游標 → 還原

  2. g_bOutputMode flag（PutChar 設、ReadKey 清）→ timing 問題，GET 游標不出現 → 還原

  3. hb_gtGetCursor() 直接查詢 → BUYSELL/P32 游標不出現 → 還原

  4. ShowCaret + 立刻 HideCaret 抵消 → timing 問題 → 還原

  5. transition detection（hidden→visible 時更新位置）→ 抓到 SAY 尾端位置 → 還原

  6. s_bCaretReady guard（ReadKey 設 ready 才 ShowCaret）→ GET 游標延遲 → 還原

- **WinForm AWICursor 延遲**：第二次修改游標延遲 1-2 秒，確認是 AWICursor → SetCursor 觸發的。已 REMARK AWICursor，列入 UNDO LIST

- **修改檔案**：gtwvt.c、usrfont_scan.c、usrfont_c.c、GAUGE.PRG、STK.PRG、TLIB.PRG、WINDOW.PRG、WINFORM.PRG、mouse_handler.prg

- **文件**：doc/游標尾巴修正歷程_20260506.pdf、doc/功能提示與游標修正_20260506.pdf



### 15:54 PickList scrollbar 小方塊拖動修復（驚心動魄）

- **現象**：PickList scrollbar 小方塊無法拖動，上下箭頭和 track 點擊正常

- **追查過程**：

  1. 加 debug log 到 picklist → 發現 MCol=48/45/61（點 col 78 回傳錯誤值，每次不同）

  2. 以為是 caret 修改破壞了 PTEXTSIZE.x → 用二分法逐一加回修改（Change 1→2→3→4），每個都正常

  3. 加回 WM_MY_UPDATE_CARET 就壞 — 但原版也有！矛盾

  4. 用 20260505 的 gtwvt.lib 測試 → 正常！確認是 gtwvt.c 差異

  5. diff 比對 → 發現今天起點（bak_beforecaret）比正常版（20260505_1705）多了 **DragSelect 功能**

  6. DragSelect 分析：DragSelectStart 延遲 K_LBUTTONDOWN，DragSelectCancel 同時發送 K_LBUTTONDOWN + K_LBUTTONUP → picklist 拖動迴圈進入後立即收到 LBUTTONUP 退出

- **根因**：DragSelect（前一位 Claude 在 05-05 晚間加入），不是 caret 修改

- **修正**：從今天完整版 gtwvt.c 移除 DragSelect（6 處修改），恢復原始 K_LBUTTONDOWN 直接發送

- **連鎖解決**：移除 DragSelect 後，fontTools 修正的 ASCFONT.TTF 也恢復正常（MCol 不再偏移），ASCFONT CHR(20) 問題一併解決

- **picklist 修正**：移除拖動時 MCol 檢查（MCol()!=nRight EXIT）、移除未知事件中止（nDragKey!=0 EXIT）

- **教訓**：

  1. 二分法有效但前提是基底正確。基底不同時結論會錯

  2. 多個問題可能有共同根因（DragSelect 同時造成 scrollbar 壞 + ASCFONT MCol 偏移）

  3. 前一位 AI 的修改可能引入新問題，要仔細驗證

- **修改檔案**：gtwvt.c、gtwvt.h、picklist.prg



### 19:40 MessageBox 按鈕 Hover Color

- **問題：** MessageBox 的按鈕（如「Y.是 / N.否」）沒有滑鼠 hover 效果，滑鼠移上去不會高亮

- **修正：** TLIB.PRG MessageBox 函式的 `CASE IsMouseMove` 分支加入 hover 偵測，檢查滑鼠位置是否在按鈕範圍內，若是則設 nChoice 觸發重繪 highlight

- **踩坑：** `nBottom-2` 是浮點數（19.50），與 `MRow()` 整數比較永遠 false。加 `Int()` 包裝解決

- **修改檔案：** TLIB.PRG



### 19:30 DIALOG 右鍵 ESC

- **問題：** DIALOG.PRG 的 ReadKey 函式未處理滑鼠右鍵，右鍵點擊無效果

- **修正：** ReadKey 加入 K_RBUTTONDOWN → K_ESC 轉換，在 IsMouseClick 判斷之前攔截

- **修改檔案：** DIALOG.PRG



### 17:00~19:10 主選單滑鼠自由選取

- **功能 1：選單列跳轉** — 子選單開著時，直接點選單列任意項目，自動關閉所有子選單並切換

- **功能 2：父層跳轉** — 多層深時，點任意父層的項目，自動關到該層並切換

- **功能 3：點選 highlight** — 滑鼠點選子選單項目時先 highlight 再展開（跟鍵盤一致）

- **架構**：

  - MsHandler 加 DATA aMenuBar（Level 0 項目）、nMenuBarSwitch/nMenuBarLevel（切換目標）、aMenuStack（每層 aMenuItems 堆疊）

  - MsMenuSel IsClick 直接比對 aItems（不靠 HitTest/aMsReg），再比對 aMenuBar 和 aMenuStack

  - 命中父層 → 設 nMenuBarSwitch + nMenuBarLevel → RETURN 0 → 每層 PullDownMenu 檢查並逐層退出

  - 目標層 `nLevel == nMenuBarLevel` → LOOP 切換；中間層 `nLevel > nMenuBarLevel` → RETURN 傳遞

- **踩坑**：IF/ELSEIF 結構錯誤（Level 0 和 Level ≥ 1 混用）、堆疊 push/pop 時機（MsMenuSel 前後 vs DO WHILE 開始/RETURN 前）、LOCAL 不能在迴圈內宣告

- **修改檔案**：mouse_handler.prg、MENU.PRG



### 16:13 TABLE 滑鼠 COL 點選

- **問題**：TABLE 滑鼠只能選 ROW，無法選 COL

- **修正**：點擊時從 oBrowse:nLeft 累加每欄寬度（getColumn:width 或 Transform 計算），找到 MCol() 落在哪欄，設 oBrowse:colPos 並 refreshAll

- **修改檔案**：TABLE.PRG



---



## 2026-05-05



### 21:52 BUYSELL & STOCK 表身 TopMessage 功能提示

- **問題：** 表身模式下 TopMessage 不顯示 [ESC] [PgDn] 功能提示

- **解決：** BUYSELL.PRG:290、STOCK.PRG:272 的 `TopMessage(..., .T.)` 改為 `TopMessage(..., 0)`，移除 lHintMode 參數

- **修改檔案：** BUYSELL.PRG、STOCK.PRG



### 16:00 GTWVT 精簡重構

- **Phase 1: PaintText static → pWVT**: 5 個 static 變數搬進 pWVT（hdcPaintBuf/hbmPaintBuf/nPaintBufW/nPaintBufH/hBkBrush/crBkCached），New() 初始化、Free() 清理。所有狀態統一由物件管理，函式內不再有隱藏狀態

- **Phase 2A/2B: extern 統一**: 5 處 `{ extern ... }` compound statements 消除，extern 宣告統一到檔案頂端（g_nCellW/g_nCellH/g_nMarginLeft/g_nMarginTop/g_nFontLfWidth/g_bHookPaint/g_hPaintBuf）。PaintText 從 4 層巢狀 → 1 層扁平

- **⚠ 編碼規範：禁止使用 extern compound statements**

  - ❌ 禁止：`{ extern int g_xxx; g_xxx = val; }` — 散落在函式中間的 extern 宣告

  - ✅ 正確：在檔案頂端統一宣告 `extern int g_xxx;`，函式內直接使用 `g_xxx = val;`

  - 原因：compound statements 造成多層巢狀（曾到 4 層），導致大括號不平衡 bug，程式碼難以閱讀和維護

  - 同理禁止：`{ static HDC s_xxx = NULL; ... }` — 應搬進 pWVT 結構體統一管理

- **Phase 3A: HandleWideChar**: WM_CHAR/WM_IME_CHAR ~60 行重複的 Unicode→BIG5 轉換合併為 hb_gt_wvt_HandleWideChar() helper。修 bug 只要改一處

- **Phase 3B: WriteConChar**: WriteCon/WriteConW ~130 行重複的控制字元處理合併為 hb_gt_wvt_WriteConChar() helper。同時修正 WriteConW 遺漏的 TAB（case 9）支援

- **Phase 4: 清理**: 移除 PutChar 中殘留的 debug file I/O（fopen debug_trail.log）

- **Phase 5A: Info 拆分**: hb_gt_wvt_Info (876行) 拆出 InfoFont (~110行, 處理 FONTSIZE/WIDTH/NAME/WEIGHT/QUALITY/ATTRIBUTE/SEL) 和 InfoCustom (~35行, 處理 202/210/211)。Info 主體從 876 → ~680 行。未來加自訂功能碼只需改 InfoCustom

- **Phase 5B: DefineBoxChar 拆分**: hb_gt_wvt_DefineBoxChar (900行) 拆成 DefineBoxCharRC (~445行, 自訂字元：箭頭/捲軸/按鈕/數字) 和 DefineBoxCharStd (~445行, 標準 box drawing：填充/線條/角落)。原函式變成 8 行 dispatcher

- **PutChar CJK 孤兒修正（3 層問題）**:

  1. **Overwrite 半邊字**: Redraw 的 CJK 擴展檢查 CURRENT 內容（清理後已看不到舊 trail）→ 改為無條件擴展 ±1 cell

  2. **Insert/Delete 連鎖孤兒**: 寫 trail marker 到 col+1 時，若 col+1 原是 CJK lead，其 trail(col+2) 變孤兒 → 先清 col+2

  3. **移除 UpdateCaret InvalidateRect(NULL) 暴露的問題**: 之前全畫面重繪意外遮蓋了以上兩個 bug

- **踩坑**: PutChar 加 post-write orphan cleanup 太激進，把正常 CJK 字元清掉導致半邊字 → 已撤回。教訓：邏輯多繞幾次，確定後再動手

- **精簡成果**:

  - 移除重複程式碼 ~190 行（HandleWideChar 60 + WriteConChar 130）

  - 消除 5 處 compound statement（程式碼扁平化）

  - PaintText 隱藏 static → pWVT 物件管理

  - Info (876行) 拆分為 3 個獨立函式（InfoFont + InfoCustom + 主體）

  - DefineBoxChar (900行) 拆分為 3 個獨立函式（RC + Std + dispatcher）

  - debug 殘留清除

  - gtwvt.c 兩大怪獸（佔 31%）已馴服為可維護的子函式群

- **修改檔案**: gtwvt.h（加 6 個 paint buffer 欄位）、gtwvt.c（PaintText/New/Free/WndProc/WriteCon/WriteConW/PutChar/Redraw/Info/DefineBoxChar）

- **Redraw 修正**: 無條件擴展 ±1 cell（原本只在檢測到 CJK 時擴展，但 PutChar 清理後已看不到舊 CJK）

- **PutChar 連鎖孤兒修正**: 寫 trail marker 前檢查 col+1 是否有 CJK lead，有的話先清 col+2

- **備份**: gtwvt.h.bak_refactor_20260505_1430, gtwvt.c.bak_refactor_20260505_1430

- **PDF 報告**:

  - TrueDoubleBuffer_Caret_20260505.pdf（6 頁，7 張圖表 — 雙緩衝 + 游標 + 精簡重構）

  - GTWVT_Glossary_20260505.pdf（4 頁，7 張圖表 — 技術名詞圖解 + UDA/PUA/CJK 造字專章）



### 19:00 Windows 剪貼簿完整整合（Ctrl+C / Ctrl+V / 滑鼠拖選）

- **Ctrl+V 貼上**: WM_CHAR 攔截 ASCII 22，讀取 CF_UNICODETEXT（已轉 BIG5），逐 byte 送入 input queue

- **Ctrl+C 複製（GET 欄位）**: WM_KEYDOWN 攔截 VK_C+Ctrl，送自訂 key code 9001 到 app（避開 K_CTRL_C=K_PGDN=3 衝突）。GETSYS.PRG 收到 9001 → `hb_gtInfo(15, RTRIM(oGet:buffer))` 送到剪貼簿

- **滑鼠拖選複製（任意位置）**: 左鍵按下記錄位置，拖動 >3px 進入框選模式（InvertRgn 反色），放開複製到剪貼簿並還原顏色。未拖動則補送正常 K_LBUTTONDOWN+K_LBUTTONUP

- **踩坑**:

  - K_CTRL_C = K_PGDN = 3：PRG 層無法區分，必須在 WM_KEYDOWN 層用 VK 碼區分

  - hb_gt_winapi_getClipboard 回傳已轉碼的 BIG5 字串，不是 raw Unicode

  - HB_GTI_CLIPBOARDDATA 常數需用數值 15（GETSYS.PRG 沒有 include hbgtinfo.ch）

  - oGet 可能為 NIL（不在 GET 欄位時），需加保護

- **函式封裝**: 所有子功能從 WndProc/MouseEvent/KeyEvent 提取為獨立函式：

  - `hb_gt_wvt_ClipboardPaste()` — 讀剪貼簿送入 input queue（Ctrl+V 和 Shift+Insert 共用）

  - `hb_gt_wvt_DragSelectStart()` — 記錄拖選起點

  - `hb_gt_wvt_DragSelectCheck()` — 檢測拖動門檻 >3px → 進入框選

  - `hb_gt_wvt_DragSelectCancel()` — 未拖動 → 補送正常點擊

  - 原則：GTWVT 主體只保留骨幹和分派，邏輯封裝在函式中

- **修改檔案**: gtwvt.c（4 個新函式 + WM_KEYDOWN VK_C + WM_CHAR Ctrl+V + MouseEvent 拖選）、gtwvt.h（加 bDragPending/ptDragStart/crDragStart）、GETSYS.PRG（加 nKey=9001 handler）



### 13:50 TRUE Double Buffer + Caret Unification

- **問題**:

  1. BUYSELL 單據修改游標消失（實體 ENTER 看不到游標）

  2. winform() 游標半形↔全形來回閃爍

  3. 游標移動後消失約 1 秒才重新出現

  4. 兩個 buffer (s_hdcPaintBuf + s_hdcDblBuf) 浪費記憶體且增加複雜度

- **根因**:

  1. GTWVT UpdateCaret 和 ScanAndDrawUda 都在建立 caret，互相覆蓋搶控制權

  2. UpdateCaret 永遠用半形寬度(PTEXTSIZE.x)，ScanAndDrawUda 用全形寬度(2×PTEXTSIZE.x) → 閃爍

  3. DestroyCaret/CreateCaret 每次 WM_PAINT 重置 blink timer → 游標移動延遲

  4. InvalidateRect(NULL) 在 UpdateCaret 中每次寬度改變觸發全畫面重繪

  5. BitBlt 覆蓋螢幕上的 caret 像素但 Windows 不知道 → 需 2 個 blink cycle (~1秒) 才修正

  6. BeginPaint 只在 caret 位於 dirty rect 內才 HideCaret，否則 caret 被 BitBlt 蓋掉無法恢復

- **解法**:

  1. **TRUE double buffer**: 合併為 1 個 buffer (g_hPaintBuf)，移除 s_hdcDblBuf/s_hBmpDblBuf/s_nDblBufW/s_nDblBufH

  2. **g_bSharedPaint → g_bHookPaint**: 語意更清楚（hook 控制整個 paint 流程）

  3. **UpdateCaret 全形修正**: 加 isWideChar() 判斷游標位置字元，CreateCaret 使用正確寬度(1x或2x)，處理 trail marker(0xFFFE) col-1 fallback

  4. **ScanAndDrawUda caret 管理重構**: 移除 DestroyCaret/CreateCaret/SetCaretPos，改用 HideCaret + BitBlt + 2×ShowCaret（輕量級，不重置 blink timer）

  5. **移除重複 pre-scan**: 2 次完全相同的 UDA 掃描合併為 1 次

  6. **PaintText 跳過 ShowCaret**: g_bHookPaint=1 時不呼叫 ShowCaret，讓 ScanAndDrawUda 統一管理

  7. **SetCaretPos 加 nOffX/nOffY**: 修正最大化時 MarginLeft/MarginTop 偏移

  8. **移除 InvalidateRect(NULL)**: UpdateCaret 不再觸發全畫面重繪，INSERT 切換由 timer 9988 (50ms) 處理

  9. **isWideChar 前置宣告**: 加 forward declaration 讓 UpdateCaret 可呼叫

- **踩坑**:

  1. 只呼叫 ShowCaret（不 DestroyCaret/CreateCaret）→ 全部游標消失。原因：BeginPaint 可能不呼叫 HideCaret（caret 不在 dirty rect），此時 BitBlt 覆蓋 caret 像素但 XOR 保存區未更新

  2. DestroyCaret/CreateCaret 每次 WM_PAINT 都執行 → 游標出現但移動延遲 ~1 秒。原因：每次 CreateCaret 重置 Windows 內部 blink timer，游標永遠在 blink 的「顯示」階段

  3. InvalidateRect(NULL) 在 UpdateCaret 中 → 每次游標寬度改變觸發全畫面重繪，造成連鎖 WM_PAINT

  4. 正確解法：HideCaret（確保 caret 被正確保存）→ BitBlt → 2×ShowCaret（第 1 次平衡自己的 HideCaret，第 2 次平衡 BeginPaint 的 HideCaret。若 BeginPaint 沒有 HideCaret，第 2 次 ShowCaret 是 no-op）

- **使用技術**:

  - Windows Caret XOR 機制：ShowCaret 用 XOR 繪製 caret 並保存被覆蓋像素，HideCaret 用保存像素恢復

  - HideCaret/ShowCaret 計數器：HideCaret 累加隱藏計數，ShowCaret 遞減，計數=0 時可見

  - CreateCompatibleDC + CreateCompatibleBitmap：off-screen memory DC 雙緩衝

  - BeginPaint/EndPaint：WM_PAINT 驗證機制，BeginPaint 條件式隱藏 caret

  - SendNotifyMessage：同執行緒同步呼叫 WM_MY_UPDATE_CARET

  - isWideChar()：判斷 Unicode 字元全形/半形（含 UDA PUA E000-F8FF）

- **修改檔案**:

  - `gtwvt.c`: UpdateCaret 加 isWideChar 全形判斷 + isWideChar 前置宣告 + PaintText 跳過 ShowCaret + 移除 InvalidateRect(NULL) + g_bSharedPaint→g_bHookPaint

  - `usrfont_c.c`: g_bSharedPaint→g_bHookPaint + 移除 s_hdcDblBuf 等 4 個 static 變數

  - `usrfont_scan.c`: 移除 DestroyCaret/CreateCaret 改 HideCaret+2×ShowCaret + 移除第二 pre-scan + 移除 s_hdcDblBuf 建立區塊 + SetCaretPos 加 nOffX/nOffY + 早期返回路徑加 HideCaret+2×ShowCaret

  - `usrfont_wndproc.c`: g_bSharedPaint→g_bHookPaint

- **備份**: gtwvt.c.bak_truedblbuf_20260505_1000, usrfont_*.c.bak_truedblbuf_20260505_1000, gtwvt.c.bak_caretunify_20260505_1030, usrfont_scan.c.bak_caretunify_20260505_1030

- **PDF 報告**: TrueDoubleBuffer_Caret_20260505.pdf（含 5 張圖表）

- **進化路線：假 Double Buffer → 真 Double Buffer**:

  - 假 buffer 的思路：兩個人各做各的，做完再拼在一起

    ```

    PaintText 有自己的 buffer (s_hdcPaintBuf)

    ScanAndDrawUda 也有自己的 buffer (s_hdcDblBuf)

    各自 BitBlt 到螢幕 → 閃爍、caret 衝突

    ```

  - 真 buffer 的思路：兩個人在同一張畫布上畫

    ```

    PaintText 先畫底層 ASCII → g_hPaintBuf

    ScanAndDrawUda 在同一張畫布上疊加 UDA/ASCFONT → g_hPaintBuf

    一次 BitBlt 搬到螢幕 → 無閃爍

    ```

  - 進化 3 步驟：

    1. **合併 Buffer**: 移除 s_hdcDblBuf，ScanAndDrawUda 直接畫在 g_hPaintBuf 上（少一次記憶體配置、少一次 BitBlt capture）

    2. **統一 Caret 管理者**: UpdateCaret 加 isWideChar() 建立正確全形寬度；ScanAndDrawUda 不再 DestroyCaret/CreateCaret（消除兩個管理者搶控制權的閃爍）

    3. **HideCaret/ShowCaret 取代 DestroyCaret/CreateCaret**: BitBlt 前 HideCaret 保存 XOR 像素，BitBlt 後 2×ShowCaret 恢復（blink timer 不被重置，游標即時反應）

  - 假 buffer 流程：

    ```

    PaintText: BeginPaint → 畫 ASCII → EndPaint → ShowCaret（過早！）

    ScanAndDrawUda: HideCaret → 建 s_hdcDblBuf → pre-scan×2（重複）

      → 畫 UDA → BitBlt → DestroyCaret → CreateCaret(全形，搶控制) → ShowCaret

    ```

  - 真 buffer 流程：

    ```

    PaintText: BeginPaint → 畫 ASCII → EndPaint → [跳過 ShowCaret]

    ScanAndDrawUda: pre-scan×1 → 畫 UDA(同一 buffer)

      → HideCaret → BitBlt → ShowCaret×2（輕量級）

    ```



---



## 2026-05-02









### 22:30 P77 印表機設定改善 + 通用 Persist Hotzone + CJK 半字修正 + USRFONT 瘦身

- **CJK 半字修正（成功）**: dbedit_mouse.prg forceStable 後再呼叫一次 CallUser(DE_IDLE)

  - 跟 WINFORM 的 BIG5 half-char fix 同樣原理：stabilize 後無條件重繪

  - 經歷 7 次失敗後，回到最簡單方法成功

- **通用 Persist Hotzone API**: MsAddPersist/MsClearPersist/MsClearAllPersist

  - mouse_handler.prg: 3 METHOD + 3 wrapper FUNCTION

  - TopMessage 改用 ClearPersist(nRow) 只清指定 row

  - Status bar HDD 區域加 ShowHotKeys 觸發（row 24）

  - MENU.PRG 用 ClearAllPersist() 清全部

  - HoverUpdate 改用 MEMVAR->cHoverColor（系統配色）

  - MenuSel 加 HoverUpdate 支援

- **P77 印表機設定改善**:

  - 印表機下拉選單位置修正（nTop+2, lFixHeight=.T.）

  - 寬度調整（nRight 64→57）、高度調整（nBottom 22→18）

  - 印表機/紙張選單加「離開」項目（防止 ESC 強迫選最後一項）

  - row 24 雜訊修正：P77 離開一律呼叫 Status()

  - row 24 殘影修正：StkSetPrnMode 的 @ SAY + Inkey(2) remark 掉

  - SaveScreen/RestScreen 擴展到 row 24

  - nPickRight 改用 nMenuRight（動態計算）

- **USRFONT.TTF 瘦身**: 23.6MB → 1.4MB（只保留 PUA 造字區 3,611 字）

  - Stk32.exe: 31MB → 8.6MB

- **Deploy 機制**: _rebuild_claude.bat 呼叫 deploy_stk32.bat（7 個目標路徑）

- **dbEdit → dbEditEx**: 15 個報表 PRG

- **修改檔案**: dbedit_mouse.prg, mouse_handler.prg, TLIB.PRG, MENU.PRG,

  P77.PRG, print_spool.prg, WINDOW.PRG, DBFWIN.PRG, gtwvt.c,

  BUYSELL.PRG, STOCK.PRG, P113.PRG, APPSETUP.PRG, QUOT.PRG,

  P123.PRG, DIALOG.PRG, picklist.prg, deploy_stk32.bat,

  _rebuild_clauede.bat + 15 個報表 PRG



### 20:50 CJK 半字修正（WINDOW.PRG / DBFWIN.PRG dbEditEx 底部資訊區）

- **問題**: dbEditEx 瀏覽時，底部 PartsMessage 的 CJK 字元每個字之間有空隙

- **根因**: dbEditEx 的 forceStable() 觸發 WM_PAINT，覆蓋了底部已繪製的 CJK 字元

- **修正**: forceStable() 之後再呼叫一次 CallUser(DE_IDLE)，重繪底部蓋回去

- **原理**: 跟 WINFORM 的 BIG5 half-char fix 一模一樣（stabilize 後無條件重繪）

- **經歷 7 次失敗嘗試後，回到最簡單的方法成功**

- **修改檔案**: dbedit_mouse.prg（加一行）



### 08:30 USRFONT.TTF 瘦身 + Deploy 機制建立



#### USRFONT.TTF PUA 瘦身

- **問題**: Stk32.exe 高達 31MB，啟動時提取 24MB USRFONT.TTF 到磁碟造成遲鈍

- **根因**: USRFONT.TTF 包含完整華康字型 36,996 個 glyph（23.6MB），但系統只用 PUA 造字區

- **PUA vs UDA 對照**:

  - PUA (Private Use Area): Unicode 標準保留的私人使用區域 U+E000~U+F8FF

  - UDA (User Defined Area): 華康造字區，Big5 FA40~FEFE，映射到 PUA U+E000~U+E310

  - 系統實際使用: U+E000~U+EE1A（3,611 個造字 glyph）

- **解法**: 用 Python fonttools 提取 U+E000~U+EE1A 的 glyph，生成精簡 TTF

- **結果**:

  - USRFONT.TTF: 23.6MB → 1.4MB（省 94%）

  - Stk32.exe: 31MB → 8.6MB（省 72%）

  - 啟動提取: 24MB → 1.4MB（快 17 倍）

- **stk32.rc 嵌入資源清單**:

  - 101 USRFONT.24M / 102 USRFONT.UMAP / 104 ASCFONT.15 / 105 ASCFONT.UMAP

  - 107 stk.ini / 108 system.ini / 109 USRFONT.TTF / 110 ASCFONT.TTF

  - 201~210 USRFONT BCACHE（10 種解析度）/ 301~310 ASCFONT BCACHE（10 種解析度）

- **BCACHE 用 LockResource 直接記憶體讀取（快），TTF 用 Extract 寫磁碟（慢）**

- **備份**: USRFONT.TTF.bak_full_24mb



#### Deploy 機制建立

- **架構**:

  - _rebuild_claude.bat: 開發用（Kill → Build → Deploy → Start）

  - deploy_stk32.bat: 佈署專用（只做檔案複製，不 build）

  - BUILD 成功後自動呼叫 deploy_stk32.bat silent，失敗不呼叫

- **佈署路徑（只在 deploy_stk32.bat 維護）**:

  - \\S001\STk\S001 / \\S001\STk\S002（網路站點）

  - \\S001\STK32 / \\S001\STK32\INSTALL（網路主目錄/安裝包）

  - E:\UPDATE\STK32 / E:\UPDATE\STK32\INSTALL（本機更新/安裝包）

  - C:\STK\S001（本機站點）

- **佈署內容**:

  - 全部目標: Stk32.exe + PrnAsk.exe + 字型檔

  - INSTALL 目標: 額外加 INI + DLL + Helper

- **路徑修改只改 deploy_stk32.bat，_rebuild_claude.bat 不需改**



#### BCC7732 編譯器限制

- -optim+（開啟優化）: ilink32 報錯，不相容

- -strip（去除符號）: 同上，不相容

- 結論: 瘦身靠 USRFONT.TTF 精簡（效果最大）



#### 修改檔案

- USRFONT.TTF, _rebuild_claude.bat, deploy_stk32.bat（新建）



### 08:01 ShowHotKeys 開發過程中發現的異常修正

- **PickList ESC 觸發最後一項**: picklist.prg ESC set nSelected=nCount instead of 0

  - 修正: nSelected := 0 + CLEAR TYPEAHEAD + 加入「離開」項目

- **PickList 殘影**: SaveScreen/RestScreen 範圍太小

  - 修正：各邊加 1 格 (top-1, left-1, bottom+1, right+1)

- **SayReg 外漏到其他視窗**: aSayReg 是 PUBLIC，離開後未清除

  - 修正: SayRegDisable() 清除 aSayReg（僅修改時有效）

  - 修正: PushReg 儲存 aSayReg 供 PopReg 恢復

  - 修正: MENU.PRG 文件返回時 SayRegClear()

- **TopMessage 滲透到主選單**: 子選單返回路徑未清除 oMs:aTopReg

  - 修正：子選單返回後加 oMs:aTopReg := {} + SayRegClear()

- **P123 AskParts1 滑鼠點確認變取消**: ToggleDone 用 AEval 重新初始化所有按鈕

  - 根因: AEval 沒設 nMsCurBtnIdx，所有按鈕被註冊為 idx=13(Cancel)

  - 修正: AEval 加入 nMsCurBtnIdx := nIdx

  - 僅 P123 受影響（唯一有 ToggleDone + AEval 重初始化的檔案）

- **dbEdit → dbEditEx**: 15 個檔案 (LABEL, P221, P241, P246, P25, P27, P351, P355, P36, P373, P415, P424, P47, P46, REPORT)

- **STOCK.PRG 表頭/表身**: 同 BUYSELL 修正（刪初始 TopMessage，.T. 對調）

- **P113.PRG TopMessage .T.**: 移除（瀏覽表單全程有 PgDn）

- **APPSETUP F12**: 改為 ShowHotKeys()

- **修改檔案**: picklist.prg, mouse_handler.prg, MENU.PRG, DIALOG.PRG, P123.PRG, STOCK.PRG, P113.PRG, APPSETUP.PRG, QUOT.PRG + 15 個報表 PRG





### 03:30 TopMessage 功能提示系統 (ShowHotKeys)

- **功能**: 點擊 TopMessage 標題 → PickList 鍵盤按鍵一覽表（平板/手機使用者）

- **設計原則**:

  - TopMessage 顯示 = 熱區存在，不顯示 = 不存在

  - oMs:aTopReg 不受 PRIVATE scope 遮蔽（穿透所有子功能）

  - PgDn/ESC 由 !lHintMode 控制：表頭有、表身沒有

  - 主選單永遠不呼叫 TopMessage（lTitle=.F.）→ 不需額外禁止

  - 文件返回主選單時清除 aTopReg + SayReg

  - SayReg 僅在 FORM 修改時有效，離開即清除

  - BrowseKey row 0 硬編碼已移除，統一由 aTopReg 處理

  - 子功能隔離用 MsPushReg/MsPopReg

- **場景處理規則**:

  - 主選單：PullDownMenu 傳 lTitle=.F. → 不呼叫 TopMessage → aTopReg 為空 → MenuBar 不受影響

  - 進入單據：單據呼叫 TopMessage() → aTopReg 建立 → 熱區生效

  - 進入子選單再進入單據：子選單的 PullDownMenu 遞迴呼叫也傳 lTitle=.F.

  - 文件返回主選單：MENU.PRG 在 &cProgString 和 PullDownMenu 返回後清除 aTopReg + SayReg

  - 進入子功能（F1/F3/F4 SetKey → Window）：oMs:aTopReg 不受 PRIVATE 遮蔽，持續有效

  - 離開子功能：PRIVATE aMsReg 恢復，oMs:aTopReg 不變

  - 表頭 READ：TopMessage 正常呼叫（不帶 .T.）→ PgDn/ESC 有

  - 表身 READ：TopMessage(.T.) → !lHintMode=.F. → PgDn/ESC 無

  - WINFORM/P113：不帶 .T. → PgDn/ESC 全程有

  - PartsList/ListByKind：MsPushReg/MsPopReg 隔離

- **碰牆與教訓**:

  - 把 PgDn/ESC 綁到 lHintMode → 違反「永遠顯示」原則，13 個 PRG 被迫修改後又還原

  - IF lHintMode 和 IF !lHintMode 反覆切換 → 搞混表頭/表身語意，浪費大量時間

  - 誤以為 .T. = 表身 → 實際是表頭 READ，導致邏輯全部反轉

  - BUYSELL 行 158 被加了多餘的 .T. → 還原時漏掉，反覆出錯

  - 未經許可擅自還原檔案 → 毀掉三小時成果，嚴重錯誤

  - PickList ESC 回傳 nCount 而非 0 → 觸發最後一項功能

  - aSayReg 是 PUBLIC → SayRegDisable 只停用不清除 → 外漏到其他視窗

  - BrowseKey 舊的 row 0 硬編碼沒清 → 在無 TopMessage 時仍觸發 hover/click

  - oMs:aTopReg := {} 放在 AAdd 之後 → PgDn 註冊完馬上被清空

  - PushReg 清除 aSayReg → FORM 的 SayReg 標題消失（需保留不清除）

  - P123 ToggleDone 用 AEval 重初始化 → nMsCurBtnIdx 沒重設 → 所有按鈕變 Cancel

  - Edit tool 寫中文到 BIG5 PRG → 亂碼（已禁用 Edit tool，改用 Python bytes-only）

  - UpdateList.md 是 UTF-8 → 誤用 BIG5 寫入 → 混合編碼

  - 需求沒弄清楚就動手改 → 反覆修改浪費時間，應先研究 50 遍再動手

  - BUYSELL 沒有 aMouse → ShowHotKeys 讀 aMouse 失敗 → 不是所有系統都用 BottomMessage

  - WINFORM TopMessage 在 WinForm() 之前呼叫 → 註冊到外層 scope → WinForm PRIVATE 遮蔽 → 熱區消失（這是 aTopReg 的誕生原因）

  - 子選單返回路徑沒清 aTopReg → 只清了直接執行路徑 → TopMessage 滲透到主選單

  - 反覆 rebuild 但沒確認上次 build 是否完成 → 測試的是舊版

  - 反覆修改 13 個 PRG 的 .T./.F. → 應該在 TopMessage 裡面處理，不動呼叫者

  - SaveScreen 範圍不夠 → PickList 關閉後有殘影

  - dbEdit 未改成 dbEditEx → 15 個報表檔案漏改

  - P113 保留了 .T. → WINFORM 類型全程需要 PgDn 但沒有

  - STOCK.PRG 跟 BUYSELL 同類型 → 忘了一起改

  - 文件檔（.md）是 UTF-8，程式碼（.PRG）是 BIG5 → 兩種規則不要搞混

- **架構**: oMs:aTopReg (MsHandler DATA) 持久化 row 0 熱區

- **機制**:

  - TopMessage 註冊 ESC/PgDn/標題/公司名稱到 oMs:aTopReg（不受 scope 影響）

  - HoverUpdate/HitTest 掃描 aTopReg（優先於 aMsReg）

  - nKey=NIL + codeblock: HitTest 直接 Eval，不走 hb_keyPut

  - BrowseKey row 0 硬編碼移除（統一由 aTopReg 處理）

  - MENU.PRG 文件返回時清除 aTopReg

- **ShowHotKeys PickList 內容**:

  - 第一類：PgDn/ESC（特殊按鍵）

  - 第二類：F1~F8（基本功能鍵，SetKey 檢查）

  - 第三類：ALT/CTRL 組合鍵（SetKey 檢查）

  - 顯示：KY_ 圖形鍵標（CustomChar.ch）

  - F12 綁定 ShowHotKeys (APPSETUP)

- **BUYSELL 表頭/表身**: 刪 158 行，.T. 從 286 移到 291（表頭有 PgDn，表身沒有）

- **隔離**: MsPushReg/MsPopReg 用於 PartsList/ListByKind (QUOT.PRG)

- **修改檔案**: mouse_handler.prg, TLIB.PRG, BUYSELL.PRG, MENU.PRG, APPSETUP.PRG, P113.PRG, QUOT.PRG, picklist.prg



## 2026-05-01



### 10:30 INSERT cursor position fix (BUYSELL/QUOT)

- **problem**: INSERT mode cursor at TOP of cell in BUYSELL/QUOT, WINFORM normal

- **root cause**: pWVT->iCaretInsertH=0 (GTWVT) vs s_nCursorInsertH=8 (ScanAndDrawUda) out of sync

  - ScanAndDrawUda full path uses s_nCursorInsertH=8 -> 8px from bottom (correct)

  - GTWVT UpdateCaret uses iCaretInsertH=0 -> full cell height from top (wrong)

  - WINFORM always has UDA -> full path -> always correct

  - BUYSELL GET fields may be ASCII-only rows -> early return -> GTWVT wrong caret stays

- **fix**: main.prg add hb_gtInfo(210/211) to sync GTWVT caret heights after UsrFont_SetCursorHeight

- **also**: reverted AWICursor diagnostic RETURN NIL

- **files**: main.prg, TLIB.PRG



### 01:00 WinForm TAB 切換逐行刷新修正

- **問題**：WinForm 按 TAB 切換模式時畫面一行一行從上刷到下

- **根因**：TAB handler 的 DispEnd 之後，主迴圈的 stabilize 逐行進行（每行一次 WM_PAINT）

- **解決方法**：DispEnd 前加 oBrowse:refreshAll() + oBrowse:forceStable()，批次完成穩定化

- **修改檔案**：WINFORM.PRG



### 00:30 Double Buffer 研究完成（21 輪 + 最終報告）

- **v4 full 為最終版**：1 buffer、0 複製、2 次螢幕操作（1R+1W）

- **v1/v2/v5/v6 失敗原因記錄**：bUseMemDC 判斷、buffer 殘留、dirty region 不完整

- **核心發現**：capture 成本趨近零（穩定狀態 = NOP），二合一收益不值得風險

- **文件**：Research v1-v21.md + FullDoubleBuffer_FinalReport_20260501.pdf



## 2026-04-30



### 16:30 BUYSELL F4 閃爍 Bug 修正

- **問題**：F4（FitList）執行複製/修改/刪除後回到銷貨單，按增項（nChoice=1）表身不停閃爍

- **根因**：nLastKey 殘留為 K_UP(5)，增項迴圈條件 nLastKey=K_INS 和 nLastKey=0 都不成立 → AskPartsBox 永遠不被呼叫 → ELSE → LOOP → 無限迴圈（每 63ms 一次）

- **解決方法**：CASE nChoice=1 開頭加 nLastKey := 0

- **修改檔案**：BUYSELL.PRG



### 14:00 Full Double Buffer v3 完成

- **問題**：PaintText 和 ScanAndDrawUda 各有獨立 buffer，兩次螢幕寫入有可見中間狀態

- **v1/v2 失敗**：WndProc 主導的架構造成「無法輸入」（BeginPaint/EndPaint 與 GetDC/BitBlt 衝突）

- **v3 解法**：最小改動 — PaintText 跳過 final BitBlt 匯出 buffer → ScanAndDrawUda 從 buffer 捕捉 → 一次寫螢幕

- **修改**：usrfont_c.c（2 變數）、usrfont_wndproc.c（2 行 flag）、gtwvt.c（跳過+匯出）、usrfont_scan.c（capture 來源切換）

- **效果**：螢幕只有一次 BitBlt，無閃爍



### 11:30 QUOT.PRG 表身抬頭半字修正

- **問題**：QUOT（報價單）表身抬頭「倉庫」「數量」「單價」顯示為半個字

- **根因**：QUOT 有兩組連續的表身抬頭 @ SAY 區塊（line 211-225 和 229-256）。第一組畫寬版佈局，第二組畫窄版覆蓋。窄版沒蓋掉寬版多出的 CJK trail marker → 半字

- **為何 BUYSELL 正常**：BUYSELL 只有一組（完整 4 分支），無雙重繪製衝突

- **解決方法**：將多餘的第一組（line 210-225）remark 掉

- **修改檔案**：QUOT.PRG



### 10:00 PickList CJK trail marker 殘留修正

- **問題**：快速功能選單捲動後（End/PgDn）部分中文字顯示為半字或亂碼

- **根因**：CJK trail marker（0xFFFE）在新舊內容間不變，Harbour 的 RedrawDiff 跳過重繪

- **解決方法**：Display() 重繪前先 Scroll() 清空 + DispBegin/DispEnd 包住避免閃爍

- **修改檔案**：picklist.prg



### 07:00 Browse DispBegin/DispEnd + aMouse Isolation



**DispBegin/DispEnd batch update for browse init**

- ARRAY.PRG, BRRAY.PRG, TABLE.PRG, BROWSE.PRG: wrap init section with DispBegin/DispEnd

- Eliminates flicker during browse initialization (frame drawn separately from data)

- P113.PRG, WINFORM.PRG already done (2026-04-28)



**PRIVATE aMouse/aMsReg/nHover isolation**

- ARRAY.PRG L38, BRRAY.PRG L36, TABLE.PRG L42, BROWSE.PRG L22

- PRIVATE aMsReg:={}, aMouse:={}, nHover:=0 shadows caller variables

- Prevents sub-window mouse hints from mixing with caller (e.g. ALT+F3/F4 from BUYSELL)

- TABLE.PRG already had PRIVATE aMsReg, added aMouse+nHover



### 06:30 VSCode Workspace + Debug Environment



**VSCode Workspace setup (ConvertFW.code-workspace)**

- 9 projects: HBSTK, PRNASK, UpdStk, DBX32, FileHead, FLAGSET, MEMSET, XDOS64, 900-Original

- files.encoding=big5, files.autoSave=off

- aperricone.harbour extension: syntax highlight, IF/ENDIF decorator, symbol navigation

- harbour.validating=true with proper include paths and -w0

- Disabled: CMake Tools, HBMK2 extensions (hijacked Ctrl+Shift+B)



**Build tasks for each project**

- Each project has .vscode/tasks.json with Normal Build + Debug Build

- Ctrl+Shift+B = HBSTK: Kill+Build+Run (default)

- Tasks: Run Task = select any project build

- All builds use .bat files (avoid PowerShell backslash escaping issues)



**Debug environment (F5 breakpoint debugging)**

- dbg_lib.prg (from aperricone.harbour extension) copied to all projects

- _rebuild_debug.bat: hbmk2 xxx.hbp dbg_lib.prg -b -cflag=-w-

- launch.json: type=harbour-dbg, program/workingDir/sourcePaths configured

- Workflow: Tasks:Run Task > Debug Build, set breakpoints, F5

- F10=step over, F11=step into, Shift+F11=step out, WATCH panel for variables

- BCC32 workaround: -cflag=-w- needed (treats some warnings as errors in dbg_lib.c)

- stk32_debug.hbp approach failed (-b not propagated to sub-project), fixed with command-line -b



**Pitfalls encountered**

- bash heredoc converts 2>/dev/null to 2>/dev/null (Windows bat broken)

- Python f-string interprets  as backspace (use \ for backslash in bytes)

- VSCode shell tasks use PowerShell by default, eats backslashes (use type=process + .bat)

- Multi-root workspace: ${workspaceFolder} may resolve empty, use absolute paths



**Files created/modified**

- ConvertFW.code-workspace (settings + harbour config)

- */. vscode/tasks.json (build tasks for each project)

- */.vscode/launch.json (debug launch config for each project)

- */_rebuild_debug.bat (debug build script for each project)

- */source/dbg_lib.prg (debugger library, copied to all projects)



## 2026-04-29



### 20:31 ArrayBrowse (ARRAY.PRG) FitList fixes



**Problem 1: INS/DEL not refreshing immediately (15s delay)**

- Root cause: Stabilize loop used `InKey(0, INKEY_ALL)` - blocks until mouse event

- 900 original uses `InKey()` (immediate return, no wait)

- Fix: Changed to `InKey(0.01, INKEY_ALL)` - 10ms timeout, no blocking



**Problem 2: LEFT/RIGHT after ENTER edit needs 2 presses**

- Root cause: Mouse noise events (LBUTTONUP/RBUTTONUP/NC) interrupt stabilize, key lost when oBrowse:Stable=FALSE

- Fix: Filter noise mouse events (1003/1005/>=1010) + forceStable() after EXIT



**Problem 3: Mouse click record off by one row**

- Root cause: Hardcoded +1/+2 offset in click row mapping, should use dynamic nHdrH

- Fix: nHdrH := IIF(Empty(headSep), IIF(Empty(heading),0,1), 2), nDelta=(MRow()-nTop-nHdrH)-rowPos



**Problem 4: ArrayBrowse aMsReg/aMouse merged with caller**

- Root cause: No isolation - WindowMessage hints mixed with upper layer (e.g. BUYSELL) aMouse

- Fix: PRIVATE aMsReg:={}, aMouse:={}, nHover:=0 at ArrayBrowse entry (shadows caller, auto-restores on exit)



**Problem 5: ALT+F10 (copy) no response**

- Root cause: NOT a code bug - NVIDIA GeForce Experience (NVIDIA Share.exe) registers ALT+F10 as global hotkey

- RegisterHotKey only intercepts GUI window Message Loop, not Console ReadConsoleInput

- This explains: STK16 (console) works, STK32 (GTWVT/GUI) fails on same machine; STK32 works on machine without NVIDIA

- Fix: Disable NVIDIA In-Game Overlay or change its ALT+F10 shortcut



**Modified files**: ARRAY.PRG (L38 PRIVATE shadow, L157 InKey timeout, L158-162 mouse filter, L164-166 forceStable, L329-332 click nHdrH)





## 2026-04-28



### 12:16 Big5 全形字覆蓋模式修正

- **問題**：非 INSERT mode 時，游標在全形字上輸入單位元組字元（如 'A'），只取代 HIBYTE，LOBYTE 殘留導致後面全部亂碼

- **根因**：GETSYS.PRG 的 `oGet:Overstrike(cKey)` 用 `Stuff(buffer, pos, 1, cKey)` 只替換 1 byte。Big5 全形字佔 2 bytes，LOBYTE 殘留成為下一個字的 HIBYTE，連鎖亂碼

- **解決方法**：在 Overstrike 前用 `Big5CharWidth()` 檢查。若為全形字，先 `oGet:Delete()` 兩次刪除完整全形字（後面字往前移），再 `oGet:Insert(cKey)` 插入單位元組字元

- **修改檔案**：GETSYS.PRG



### 22:00 Resize 後造字字寬被截 — 已解決（95%）

- **問題**：視窗 resize 後，USRFONT（全形造字）和 ASCFONT（半形框線符號）的字寬被截掉右半邊

- **根因**：CaptureGlyphAlpha 用 `CreateFontW(-nH, 0, ...)` 建字型，lfWidth=0（自然寬度）。resize 後字型的自然寬度不再匹配 GTWVT 的 PTEXTSIZE.x（cell 寬度）。正常字不受影響是因為 ExtTextOut 用 FixedSize 強制寬度。

- **解決方法**：

  - CaptureGlyphAlpha 依字型名稱決定 lfWidth：

    - **USRFONT（華康W5A）**：`lfWidth = nW`（1:1，actualW = nW）

    - **ASCFONT-STK**：`lfWidth = nW * 2`（1:2 比例，actualW = lfWidth/2）

  - 發現 ASCFONT-STK 的 lfWidth:actualW 比例是 2:1（量測 lfW=2~38 對應 actualW=1~19）

  - gtwvt.c PaintText 匯出 g_nCellW/g_nCellH/g_nMarginLeft/g_nMarginTop

  - usrfont_scan.c 用 g_nCellW 取代 rc.right/nCols，加 nOffX/nOffY margin offset

- **踩坑**：

  - lfWidth=nW/2 讓字變「細長單位元組」（太窄，雙位元組空間只填一半）

  - lfWidth=nW 對 USRFONT 正確但讓 ASCFONT「框線變兩層」（太寬）

  - lfWidth=nW*2 讓 USRFONT 「字疊在一起」（更寬）

  - 水平 nearest-neighbor 拉伸讓 1px 框線變 2px（不可用）

  - 加大字型高度等比放大再垂直壓縮也產生雙線（垂直縮放同樣問題）

  - 最終靠量測 lfWidth vs actualW 的關係找到正確值

- **剩餘 5%**：微調待做

- **修改檔案**：usrfont_cache.c、usrfont_scan.c、usrfont_c.c、gtwvt.c



### 19:00 GTWVT 效能優化 17 項 + usrfont 拆檔

- **usrfont_c.c 拆檔**：3801 行拆成 8 個檔案（header + #include 串接）

  - usrfont_int.h (92) / usrfont_c.c (727) / usrfont_cache.c (274) / usrfont_render.c (361) / usrfont_ascfont.c (685) / usrfont_scan.c (431) / usrfont_wndproc.c (182) / usrfont_api.c (1127)

- **A** dirty-range BitBlt：capture/writeback 只處理 dirty 行

- **B** DispBegin/DispEnd + WinFormShow：WinForm 初始化 + K_TAB + SHOWBLOCK 批次刷新

- **C** PaintText double buffer：快取 memory DC，一次 BitBlt 輸出

- **D** Pre-scan 無 UDA 跳過：純 ASCII 畫面零開銷

- **E1** DrawUdaAlphaBlend DIBSection 快取：每 glyph 省 4 次 GDI 建/銷

- **E1b** DrawAscFontFromCache DIBSection 快取：同上

- **E3** scan loop 只掃 dirty 行：減少 95% hb_gtGetChar

- **E4** cursor_debug.log 移除：WM_PAINT 內的 disk I/O

- **E5** RestScreen WriteDebugLogC 移除：hot path disk I/O

- **E6** WriteDebugLogC 路徑快取：每次呼叫省 GetModuleFileName syscall

- **E8** DrawDynUnicode/DrawOneGlyph 移除冗餘 CreateRectRgn

- **E9** CreateSolidBrush 快取：最大化/sizing 時不再每次建/銷

- **E10** usrfont_wcharLen CJK 早期返回：避免 785 筆線性掃描

- **E11** InvalidateRect(NULL) → caret 行 rect：游標變更不再全畫面重繪

- **E12** SetTextAlign 設一次：每次 WM_PAINT 只呼叫一次

- **E13** LoadUsrFontUmap 三重重複判斷移除

- **E14** SaveUsrFontUmap 記憶體比對：取代每次寫入前讀檔

- **E15** USRFONT_DEBUGMAP 停用：啟動時不再寫 log

- **E7 放棄** DestroyCaret/CreateCaret 快取：INSERT mode 左移游標異常（BeginPaint/EndPaint 干擾 caret 狀態）

- **踩坑**：

  - 移除 cursor_debug 時多留 `{` 導致括號不匹配 → 拆檔後 443 行小檔案一行行看找到

  - E7 caret 快取造成 INSERT mode 左移異常 → HideCaret/BeginPaint/EndPaint/ShowCaret 交互破壞 caret 狀態

  - 完整 double buffer（PaintText+ScanAndDrawUda 共用 buffer）畫面只顯示 dirty 區域 → buffer 需先從螢幕複製基底內容（待修正）

- **修改檔案**：gtwvt.c、usrfont_int.h、usrfont_c.c、usrfont_cache.c、usrfont_render.c、usrfont_ascfont.c、usrfont_scan.c、usrfont_wndproc.c、usrfont_api.c、usrfont.prg、WINFORM.PRG



### 17:30 （已合併至 19:00）



### 14:00 Resize 後 WINFORM 刷新變慢（已解決）

- **問題**：拖拉視窗縮小後，進入 WINFORM 肉眼可見逐行刷新

- **根因**：ScanAndDrawUda 的 BitBlt capture（screen→buffer）每次複製全畫面，resize 後耗時暴增 5 倍

- **解決方法**：

  - usrfont_c.c：BitBlt capture/writeback 改為只處理 dirty row 範圍

  - WINFORM.PRG：初始化 + K_TAB 模式切換加 DispBegin/DispEnd

  - WINFORM.PRG：新增 WinFormShow() wrapper（DispBegin/DispEnd 批次 SHOWBLOCK + GetList display）

  - 24 處 SHOWBLOCK 呼叫改用 WinFormShow()

- **修改檔案**：usrfont_c.c、WINFORM.PRG



### 11:00 印表機切換即時寫入 System.ini + 紙張同步

- **問題**：PrnAsk/PntGoAsk 切換印表機時 System.ini 未更新，Status() 無法反映最新印表機

- **解決方法**：

  - P77 更動印表機後呼叫 Status() 立即更新狀態列

  - fw_prnout.prg：FiveWin ComboBox ON CHANGE + FwGetPrnInfo + FwSelPrnReload + FwPageSetup + FwDoPrint 加入 FwSaveIniPrinter()

  - fw_prnout.prg：新增 FwSaveIniPrinter() / FwReadIniPrinter() 函式

  - fw_prnout.prg：啟動時從 System.ini 讀取 PRINTER=（取代 PrnGetName）

  - dospntask.prg：ON CHANGE 改用 DspOnPrnChange() 單一函式呼叫（避免 FiveWin 預處理器雙層包裝 code block 問題）

  - dospntask.prg：紙張 ComboBox `DllCall(SendMessageA, CB_SETCURSEL)` 改為 `oCboPaper:Select()`（DllCall 直接操作 Windows API 會觸發異常的 CBN_SELCHANGE 導致 dialog 關閉）

  - 預覽視窗：版面設定 psPrn 加上 onchange="onPsPrnChg()" + 新增 onPsPrnChg() JS 函式

  - 預覽視窗：updatePrnInfo 同步三個紙張選單（prPap + psPap + pdPap）

- **踩坑**：

  - FiveWin `ON CHANGE { || expr }` 被預處理成 `{|Self| { || expr } }` 雙層 block，內層不執行 → 改用裸表達式或單一函式呼叫

  - FiveWin ComboBox 的 `DllCall(CB_SETCURSEL)` 會觸發 CBN_SELCHANGE 通知導致 dialog 異常關閉 → 改用 FiveWin 的 `oCboPaper:Select()`

  - PrnAsk 預覽視窗的印表機/紙張 select 是 HTML/WebView2 元素，ON CHANGE 只影響 FiveWin ComboBox，不影響 WebView2 → 需在 FwGetPrnInfo() 中處理

- **修改檔案**：P77.PRG、TLIB.PRG、fw_prnout.prg、dospntask.prg、print_spool.prg



### 10:00 狀態列改進（HDD/RAM 單位 + 印表機顯示 + 機號長度）

- **問題**：HDD 顯示 509154M 不易閱讀、RAM 顯示 2048.00M、無印表機資訊、機號只顯示 4 碼

- **解決方法**：

  - HDD：>1024M 時以 G 為單位顯示（整數）

  - RAM：>1024M 時以 G 為單位顯示（整數）

  - 印表機：讀取 System.ini PRINTER 設定，緊接 HDD 垂直線後空一格顯示

  - 機號：改為最多 8 碼，不足則顯示實際長度

  - P77 更動印表機後立即呼叫 Status() 更新狀態列

  - stk32.rc / updstk.rc 移除 BCACHE 項目（解決 build 找不到檔案的問題）

- **修改檔案**：TLIB.PRG、P77.PRG、stk32.rc、updstk.rc



---



## 2026-04-27



### 23:45 Ctrl+P 連結 P77 + SelectPrinter 整合 + 網路印表機加回

- **問題**：Ctrl+P 綁定舊的 SwitchPrnSelect，SelectPrinter 用 AChoice 跟 P77 重複

- **解決方法**：

  - Ctrl+P（APPSETUP.PRG:159）改為 `SetKey( K_CTRL_P, {|| P77()} )`

  - SelectPrinter()（print_spool.prg）改為 wrapper 呼叫 P77()

  - 所有印表機列舉改回 `STK_PRINTERLIST(.F.)`（含網路印表機），因延遲根因是 WebView2 不是印表機列舉

- **修改檔案**：APPSETUP.PRG、print_spool.prg、fw_prnout.prg、dospntask.prg、P77.PRG



### 23:20 P77.PRG 印表機設定對話框

- **問題**：列印設定分散在系統選單三個獨立項目，使用者難以一次總覽修改

- **解決方法**：

  - 新增 P77.PRG — Console popup 設定對話框（aFrame[4] 立體框 + 表頭分隔線 + 底部提示列）

  - 5 項設定：印表機、紙張、列印方式、列印詢問、輸出格式

  - 印表機清單：PickList（支援滑鼠、捲軸、右鍵=ESC）

  - 紙張清單：STK_PAPERLIST(cPrinterName)（DeviceCapabilities DC_PAPERNAMES）動態取得印表機支援的紙張

  - 紙張 PickList 用 lChangeHigh 強制固定高度（PickList 新增 DATA lChangeHigh）

  - 列印方式切換時自動重設 AUTOFMT=1（避免 PRNMETHOD=0 停在 AUTOFMT=4 造成異常）

  - 所有修改立即寫入 System.ini + 同步更新系統選單

  - PRNMETHOD/AUTOFMT 同步邏輯：

    - 外部(0)→內部(1)：PRN_AUTOFMT=1, PNTGO_AUTOFMT=1

    - 內部(1)→外部(0)：PNTGO_AUTOFMT=1, PRN_AUTOFMT=1

    - 外部改格式：PNTGO_AUTOFMT=n, PRN_AUTOFMT=映射值

    - 內部改格式：PRN_AUTOFMT=n, PNTGO_AUTOFMT=映射值

  - 滑鼠支援：點選、雙擊修改、滾輪上下、右鍵離開

- **踩坑**：

  1. Chr(196) 分隔線在造字模式下變亂碼 → 改用 aFrame[4] 的 cHorzChar

  2. PickList 自動計算高度，項目多往上擴展超出對話框 → lChangeHigh 屬性固定高度

  3. Inkey(0) 收滑鼠移動事件造成閃爍 → 只在 nSelRow 變化時重繪

  4. 列印方式切換時忘記同時更新兩邊的 AUTOFMT → 陷阱：PRNMETHOD=0 + AUTOFMT=4 會當掉

- **修改檔案**：P77.PRG（新）、picklist.prg（加 lChangeHigh）、printer_c.c（加 STK_PAPERLIST）、stk32.hbp



### 20:30 列印系統全面修正 + WebView2 離線安裝

- **問題**：

  1. S001 新電腦部署後列印功能多處異常

  2. PrnAsk 啟動延遲 30-60 秒（FwCheckWebView2 的 hb_run 阻塞下載）

  3. 造字區（PUA E000-EE1A）列印時空白（預覽可見、印表機輸出空白）

  4. WebView2 未安裝時無提示、無離線安裝機制

- **解決方法**：

  - **Stk32 列印修正**（print_spool.prg）：

    - FIX1: `win_printerList()` → `STK_PRINTERLIST()`（只列本機印表機，避免網路逾時）

    - FIX2: `InitPrintSpool()` 加 `STK_PRINTEREXISTS()` 驗證 printer.cfg 印表機是否存在

    - FIX3: `CallDosPntGo()` 檢查 dospntgo.exe 是否存在

    - FIX4: PrnAsk.exe 缺失時顯示 MessageBox 警告（3 處）

  - **printer_c.c 連結進 stk32.hbp + prnask.hbp**：

    - `STK_PRINTERLIST(lLocalOnly)` — EnumPrintersW PRINTER_ENUM_LOCAL

    - `STK_PRINTEREXISTS(cName)` — 快速驗證印表機名稱

    - `STK_CHECKSPOOLER()` — 檢查 Print Spooler 服務

  - **PrnAsk WebView2 延遲修正**：

    - 根因：`FwCheckWebView2()` 的 `hb_run(PowerShell)` 同步下載 → 卡 30-60 秒

    - 修正：移除阻塞下載，改為離線安裝機制

  - **造字區列印修正**（prn_to_pdf.prg）：

    - 根因：`GdiFlushText()` 無 PUA 檢測 → BIG5 FA-FE 直接送 TextOut → 空白

    - 新增 `GdiDrawUsrGlyph()` — 跟 PDF 的 DrawUsrGlyph 同邏輯，用 GdiDrawBitmap 輸出

    - `GdiFlushText()` 加 PUA 檢測（nAsc >= 0xFA）→ 走點陣圖路線

    - `DrawUsrGlyph` Priority 3 移除 `nCPFb < 0xE000` 限制 → PUA 也能渲染

    - `LoadUsrFontData()` 加載 USRFONT.TTF（GdiLoadFontFile）→ PUA 用華康楷書W5A 渲染

    - `RenderUnicodeGlyph` 的 `CP_ACP` → `950`（修正非繁中系統字型名解碼錯誤）

  - **WebView2 離線安裝**（PrnAsk main.prg + gdi_print.prg）：

    - 離線安裝包 `MicrosoftEdgeWebView2RuntimeInstallerX86.exe`（173MB）放 EXE 同目錄

    - `FwCheckWebView2()` 流程：偵測未安裝 → 找離線安裝包 → MsgRun 顯示進度 → ShellRunWait 安裝 → Registry 驗證

    - `CheckWebView2Reg()` — 用 Uninstall Registry key 驗證（比目錄檢查可靠）

    - `MsgBoxW()` / `MsgBoxWTimeout()` — BIG5→Unicode→MessageBoxW（解決非繁中系統亂碼）

    - `ShellRunWait()` — ShellExecuteExW 啟動安裝程式並帶到前景

    - `GdiLoadFontFile()` — AddFontResourceExW(FR_PRIVATE) 載入 TTF

- **踩坑**：

  1. S001 閃退 → stk.ini/System.ini 未部署

  2. PrnAsk 延遲根因不是印表機列舉而是 WebView2 下載（RESULT.TXT 的時間差揭露）

  3. Bootstrapper（1.7MB）需連線，真正離線安裝包是 Standalone Installer（173MB）

  4. `hb_DirExists()` / `GetFileAttributesW` 在某些環境偵測不到已安裝目錄 → 改用 Registry

  5. `CP_ACP` 在非繁中系統不是 950 → 字型名解碼錯誤 → 造字區空白

  6. FiveWin 的 MsgInfo/MsgAlert 中文亂碼 → 改用自製 MsgBoxW（BIG5→Unicode→MessageBoxW）

  7. MessageBox 被 STK32 視窗擋住 → 加 MB_TOPMOST | MB_SETFOREGROUND

  8. 安裝提示與安裝程式互相遮掩 → MsgRun 解決（顯示訊息同時執行安裝，完成自動消失）

- **修改檔案**：

  - `compat/print_spool.prg` — 4 項列印修正 + LOCAL 宣告

  - `compat/prn_to_pdf.prg` — GdiDrawUsrGlyph、GdiFlushText PUA、DrawUsrGlyph Priority 3、LoadUsrFontData 載入 TTF、RenderUnicodeGlyph CP_ACP→950

  - `compat/gdi_print.prg` — GdiLoadFontFile、MsgBoxW、MsgBoxWTimeout、ShellRunWait、CheckWebView2Reg

  - `source/main.prg` — EXTERNAL STK_PRINTERLIST/PRINTEREXISTS/CHECKSPOOLER

  - `build/stk32.hbp` — 加 printer_c.c

  - `PRNASK/source/main.prg` — FwCheckWebView2 離線安裝 + MsgRun

  - `PRNASK/source/fw_prnout.prg` — AGetPrinters→STK_PRINTERLIST

  - `PRNASK/source/dospntask.prg` — AGetPrinters→STK_PRINTERLIST

  - `PRNASK/build/prnask.hbp` — 加 printer_c.c + winspool



### 15:37 FontDirect=3 完整實作（USRFONT.TTF + ASCFONT.TTF）

- **問題**：沒有華康的電腦無法顯示造字和 ASCFONT 控制字元

- **解決方法**：

  - USRFONT.TTF：全形造字（E000-EE1A）向量字型，AddFontResourceEx(FR_PRIVATE)

  - ASCFONT.TTF：半形控制字元，字形映射到 PUA（EF00-EF0E），獨立字型名 "ASCFONT-STK"

  - chr(20)▲/chr(127)▼：從 Windows GDI 用 GetGlyphOutline 提取向量輪廓寫入 TTF（glyph ID 127/128，unitsPerEm=1024 縮放）

  - LoadFontByFontDirect(nFD, cFile)：統一管理所有 FontDirect 的載入邏輯

  - CaptureGlyphAlpha 加 szFontName 參數：USRFONT 用 "華康楷書W5A"，ASCFONT 用 "ASCFONT-STK"

  - dynamic switch 映射 s_aAscSrc → PUA（解決 LoadAscMap 順序差異）

  - Scrollbar：Chr(219)█/Chr(176)░ 改用配色方案替代（Big5 前導碼問題暫不改 codepage hook）

  - SaveUsrFontUmap WRITE-3 SKIP dirty 修正

- **踩坑**：

  1. fontTools subset 砍掉 GSUB → 字形變標準 ♠♥♦♣（華康自訂框線消失）

  2. FR_PRIVATE 字型在 memory DC 被系統字型搶走標準 Unicode → 改用 PUA 解決

  3. s_bNoBcacheSave 的 s_bLoaded 阻擋第二個字型載入

  4. GetGlyphOutline 提取座標需匹配 unitsPerEm（-256→小點，-1024→正確）

  5. C1 控制字元 U+009E/009F 字型 outline 為空，▲▼ 來自 Windows GDI 內建渲染

  6. LoadAscMap 重排 s_aAscByte 順序 → 固定陣列映射錯位 → 改用 dynamic switch

- **修改檔案**：usrfont_c.c, usrfont.prg, main.prg, stk32.rc, picklist.prg, USRFONT.TTF(新), ASCFONT.TTF(新)

- **內部技術細節**：

  - USRFONT.TTF 字型名：`華康楷書W5A`（與安裝版同名，CaptureGlyphAlpha 用此名查找）

  - ASCFONT.TTF 字型名：`ASCFONT-STK`（獨立名稱避免與系統字型衝突，CaptureGlyphAlpha 的 szFontName 參數傳入此名）

  - PUA 映射表（ASCFONT 標準 Unicode → PUA，避免 memory DC 被系統字型搶走）：



    | 原始 Unicode | PUA | 字形 |

    |-------------|-----|------|

    | U+00B6 ¶ | EF00 | 段落符號 |

    | U+2660 ♠ | EF01 | 框線元素 |

    | U+009E (C1) | EF02 | ▲ 三角形（GDI 提取） |

    | U+009F (C1) | EF03 | ▼ 三角形（GDI 提取） |

    | U+263A ☺ | EF04 | 框線元素 |

    | U+25CB ○ | EF05 | 框線元素 |

    | U+25AC ▬ | EF06 | 框線元素 |

    | U+263B ☻ | EF07 | 框線元素 |

    | U+00A7 § | EF08 | 框線元素 |

    | U+2666 ♦ | EF09 | 框線元素 |

    | U+2665 ♥ | EF0A | 框線元素 |

    | U+2663 ♣ | EF0B | 框線元素 |

    | U+203C ‼ | EF0C | 框線元素 |



  - chr(20)/chr(127) 的 ▲▼：字型檔 outline 為空（0 contours），Windows GDI 用 ETO_GLYPH_INDEX 內建渲染。透過 GetGlyphOutline(GGO_NATIVE) 提取向量輪廓寫入 ASCFONT.TTF（需匹配 unitsPerEm=1024）

  - Scrollbar Chr(219)█/Chr(176)░：Big5 模式下 0xDB/0xB0 是雙位元組前導碼，無法單獨渲染。暫用配色方案替代（空格+背景色），待後續加入 wcharGet codepage hook





### 視窗標題動態顯示 + 最大化按鈕

- **問題**：視窗標題固定 "STK"；右上角最大化按鈕被停用；最大化後造字殘影

- **解決方法**：

  - 視窗標題：`c_code/wintitle_c.c` 的 `STK_GETWINSIZE()` + `main.prg` 的 `StkBuildTitle()` → `Stk32(WxH@DESKWxDESKH)` + idle handler 即時更新

  - 最大化按鈕：`usrfont_c.c` 的 `USRFONT_APPLYWINSTYLE` 保留 `WS_MAXIMIZEBOX` + 保留 `SC_MAXIMIZE/SC_RESTORE`

  - 零 Margin 最大化：`WM_GETMINMAXINFO` 設定最大化尺寸為 `cellW×80 + 邊框`（80×25 完美整除），Windows 正常處理最大化/還原，還原按鈕正常顯示

  - 修正 `SaveUsrFontUmap` WRITE-3 SKIP 未清 `s_nUfntDirty` → 每幀 file I/O 迴圈（已修正）

- **核心原則**：GTWVT RESIZEMODE_FONT 的 FitSize 保持 80×25 零 margin（視窗回彈到 cell 整數倍），最大化也必須遵守

- **踩坑（11 次嘗試）**：

  1. `WS_MAXIMIZEBOX` only → 按鈕無效（Windows 需 THICKFRAME）

  2. `WS_THICKFRAME` → GTWVT 重算字型 → 亂碼

  3. `HB_GTI_MAXIMIZED(.T.)` → 自動最大化 → BCACHE 不符

  4. `HB_GTI_RESIZABLE` → 按鈕消失

  5. 真正最大化（SW_MAXIMIZE）→ margin 不為零 → USRFONT overlay 錯位 → 殘影

  6. gtwvt.c 導出 PTEXTSIZE/Margin → 對齊成功但 double buffer 殘影無法解決

  7. FillRect 清除背景 → 破壞整個畫面

  8. skip BitBlt when drawn=0 → 框線消失

  9. Fake Maximize（SetWindowPos）→ 零 margin 成功但沒有還原按鈕

  10. Fake Maximize toggle → 非標準操作介面

  11. **最終解法：WM_GETMINMAXINFO** — 告訴 Windows 最大化尺寸 = 零 margin 尺寸，完全標準 Windows 行為

- **修改檔案**：`c_code/usrfont_c.c`、`c_code/wintitle_c.c`（新）、`main.prg`、`stk32.hbp`





### FontDirect=3 嵌入向量造字字型

- **問題**：BCACHE 存固定像素點陣，縮放品質差；沒華康的電腦無法高品質渲染造字

- **解決方法**：

  - Python fontTools 從華康 W5A 提取 797 個造字向量輪廓 → `UDAFONT.ttf`（229KB）

  - 嵌入 EXE 資源（resource 109），啟動時解壓

  - `AddFontResourceEx(FR_PRIVATE)` 載入 → `CaptureGlyphAlpha` 用向量字型即時渲染

  - `s_bNoBcacheSave` 旗標：完全跳過 LoadUsrFontUmap / SaveUsrFontUmap / LoadAscFontUmap / SaveAscFontUmap

  - cell 尺寸改變時（最大化/還原）清空 alpha cache → `CaptureGlyphAlpha` 用新尺寸重新渲染 → 任何解析度完美

- **核心邏輯**：向量字型 = 數學曲線，任何尺寸重新渲染；BCACHE = 固定像素，縮放必劣化

- **踩坑**：

  1. 跳過 STK_SELECT_BCACHE 不夠 — LoadUsrFontUmap 仍會載入殘留 BCACHE

  2. FErase 刪 BCACHE 不夠 — SaveUsrFontUmap 又把新渲染結果存回去

  3. ASCFONT 路徑也要攔截（LoadAscFontUmap + SaveAscFontUmap）

  4. cell 尺寸改變時只更新 s_nUfntCellW/H 不夠 — 必須清空 s_aUfntAlpha[] 和 s_aAscCache[]

- **FontDirect 完整行為**：



  | FontDirect | 行為 | BCACHE | 品質 |

  |------------|------|--------|------|

  | 0 + 有華康 | 自動偵測 → 直接渲染 | 跳過+刪除 | 最佳（向量） |

  | 0 + 沒華康 | 自動偵測 → BCACHE | STK_SELECT_BCACHE | SCALE（次佳） |

  | 1 | 強制華康 | 刪除 | 最佳（需安裝） |

  | 2 | 強制 BCACHE | STK_SELECT_BCACHE | SCALE |

  | 3 | 嵌入向量字型 | 跳過+刪除 | 最佳（不需安裝） |



- **修改檔案**：`c_code/usrfont_c.c`、`compat/usrfont.prg`、`source/main.prg`、`build/stk32.rc`、`build/UDAFONT.ttf`（新）



### FontDirect=0 自動偵測修正

- **問題**：System.ini 說明 FontDirect=0 應自動偵測華康，有華康用字型渲染、沒華康用 BCACHE。但實際程式碼不管有沒有華康都載入 BCACHE，等同 FontDirect=2

- **解決方法**：

  - `usrfont_c.c`：`USRFONT_CHECKFONT` 從 `CreateFontW`（永遠 TRUE）改為 `EnumFontFamiliesExW`（正確偵測）

  - `main.prg`：FontDirect=0 呼叫 `USRFONT_CHECKFONT()`，有華康→跳過 `STK_SELECT_BCACHE`，沒華康→載入 BCACHE

  - `usrfont.prg`：FontDirect=0 + 有華康→刪除 USRFONT.BCACHE + ASCFONT.BCACHE + RESETCACHE（強制 `CaptureGlyphAlpha` 即時渲染）

- **完整行為表**：



  | 情境 | BCACHE | 渲染來源 |

  |------|--------|----------|

  | FontDirect=0 + 有華康 | 跳過+刪除 | CaptureGlyphAlpha（最佳品質） |

  | FontDirect=0 + 沒華康 | STK_SELECT_BCACHE | BCACHE SCALE（fallback） |

  | FontDirect=1（強制字型） | 刪除 | CaptureGlyphAlpha |

  | FontDirect=2（強制BCACHE） | STK_SELECT_BCACHE | BCACHE SCALE |



- **修改檔案**：`c_code/usrfont_c.c`、`source/main.prg`、`compat/usrfont.prg`



### SaveUsrFontUmap dirty 迴圈修正

- **問題**：WRITE-3 SKIP（舊字形數 > 新字形數時不覆寫）後沒清 `s_nUfntDirty`，導致每幀 WM_PAINT 都重複 file I/O → 畫面刷新變慢

- **解決方法**：SKIP return 前加 `s_nUfntDirty=0`

- **修改檔案**：`c_code/usrfont_c.c`





---



## 2026-04-26



### BCacheThreshold 即時銳化

- **問題**：BCACHE 縮放載入時字形模糊

- **解決方法**：

  - 新增 `c_code/threshold_c.c`：`s_nBCacheThreshold` + `ApplyThreshold()` + `HB_FUNC(USRFONT_SETTHRESHOLD)`

  - `usrfont_c.c` SCALE 路徑呼叫 `ApplyThreshold()`

  - System.ini：`BCacheThreshold=80`（0=不銳化，80=預設，120=更銳利）

- **踩坑**：`bcc32.cfg` 的 `-w!` 把 W8071 升級為 error；加 `(BYTE)` 轉型解決

- **修改檔案**：`c_code/threshold_c.c`（新）、`c_code/usrfont_c.c`、`compat/usrfont.prg`、`stk32.hbp`



### BCC ICE 徹底解決

- **問題**：BCC 7.73 頻繁 Internal Compiler Error

- **解決方法**：

  1. dinkumware C++ headers → `dinkumware_DISABLED`（佔 90%）

  2. `-w!` 升級 warning → 修正 C 碼消除所有 warning

  3. .c 檔 137KB 太大 → 拆分到獨立 .c 檔

- **修改檔案**：`E:\BCC7732\Include\dinkumware` → `dinkumware_DISABLED`



### C Code 從 BEGINDUMP 分離

- **問題**：usrfont.prg BEGINDUMP 含 3700 行 C 碼，難維護且觸發 ICE

- **解決方法**：提取到 `c_code/usrfont_c.c`；PRG 用 `EXTERNAL` 宣告；`.hbp` 引用 `.c` 檔

- **修改檔案**：`c_code/usrfont_c.c`（新）、`compat/usrfont.prg`、`stk32.hbp`



### 多解析度 BCACHE 系統

- **問題**：不同螢幕解析度需要不同 BCACHE；新電腦沒有華康字型

- **解決方法**：

  - 預製 10 套 BCACHE 打包進 EXE（stk32.rc 資源 201-310）

  - `STK_SELECT_BCACHE`：啟動時自動選最匹配 BCACHE

  - `STK_EXTRACT_MULTI_BCACHE`：解壓到 `bcache/` 目錄

  - BCACHE 保護機制 FIX-1~FIX-5 + overwrite=0 + 字形數保護

  - FontDirect（0=自動，1=強制字型，2=強制 BCACHE）

  - `GenBCacheAll`（P83.PRG）：自動切換解析度生成

- **修改檔案**：`source/TLIB.PRG`、`source/main.prg`、`source/P83.PRG`、`build/stk32.rc`、`build/System.ini`



### BCACHE 系統參考文件

- **產出**：`doc/BCACHE_System_Reference_20260426.pdf`（16 頁 13 章）



---



## 2026-04-25



### MenuBar 閃爍修復

- **問題**：選單導覽時畫面閃爍

- **解決方法**：FreezeWindow(.F.) 在錯誤位置造成「裸奔」區間；移除中間 unfreeze 保持連續 freeze

- **關鍵教訓**：問題在邏輯不在 API，用盡 API 也沒用

- **修改檔案**：`source/MENU.PRG`



### BCACHE 被歸零問題

- **問題**：新電腦啟動後 BCACHE 被清空，造字全部消失

- **解決方法**：追蹤 5 個清除點（CLEAR-1~5），加上 6 道保護（FIX-1~5 + overwrite=0）

- **修改檔案**：`c_code/usrfont_c.c`、`source/TLIB.PRG`、`build/stk32.rc`



### FreezeWindow 跳欄閃爍修復

- **問題**：BUYSELL nChoice=2 用 KEYBOARD 跳欄時逐欄閃爍

- **解決方法**：新增 C 函式 `FreezeWindow()` 用 `WM_SETREDRAW` API 凍結整個視窗重繪

- **修改檔案**：`source/TLIB.PRG`（BEGINDUMP）、`source/BUYSELL.PRG`、`source/QUOT.PRG`、`source/INQUIRY.PRG`



---



## 2026-04-23



### GAUGE 效能修復

- **問題**：WriteSys 退出時 GAUGE + USRFONT 重繪 = 3 秒延遲

- **解決方法**：移除 GAUGE，退出瞬間完成

- **修改檔案**：`source/SYSTEM.PRG`、`source/P86.PRG`



### 系統選單與 System.INI 對應

- **問題**：系統選單列印設定需與 System.ini 同步

- **解決方法**：Key 9997/9998/9999 → Toggle 函式 → 寫 System.ini → 更新選單文字

- **修改檔案**：`compat/print_spool.prg`、`source/main.prg`



---



## 2026-04-22



### GDI 列印與系統選單整合

- **問題**：GDI 直接列印模式需完整整合

- **解決方法**：PRNMETHOD=1 走 GDI + PrnAsk；系統選單切換列印方式

- **修改檔案**：`compat/print_spool.prg`、`compat/gdi_print.prg`



### UNICODE GDI 渲染

- **問題**：GDI 列印中文需正確的 Unicode 轉換

- **解決方法**：`MultiByteToWideChar(950)` CP950→Unicode；`CreateFontW` 指定中文字型

- **修改檔案**：`compat/gdi_print.prg`（BEGINDUMP C 碼）



### PRN 轉 PDF 完整開發

- **問題**：列印輸出需轉換為 PDF 格式

- **解決方法**：hbhpdf（libharu）引擎 + DosPntGo 控制碼解析 + 紙張/字型/顏色支援

- **修改檔案**：`compat/prn_to_pdf.prg`



### 連續紙列印修正

- **問題**：連續報表紙尺寸（9x5.5 等）需要正確處理

- **解決方法**：紙張尺寸對照表 + PRN_PAPER 設定

- **修改檔案**：`compat/prn_to_pdf.prg`、`build/System.ini`



### SetColor 修復

- **問題**：`SetColor(NIL)` 是 no-op，LOCAL 未賦值傳入造成顏色遺失

- **解決方法**：SaveScreen 後立即 `cOldColor := SetColor()` 取得當前色

- **修改檔案**：多個業務模組



### CPRNSEL 修復

- **問題**：列印選擇器顏色異常

- **解決方法**：修正 SetColor 呼叫順序

- **修改檔案**：`compat/print_spool.prg`



---



## 2026-04-21



### Fullwidth Tilde 修復

- **問題**：報表中 A1E3 顯示「口」而非「～」

- **解決方法**：`usrfont_wcharGet()` 攔截 A1E3 → U+FF5E（FULLWIDTH TILDE）

- **修改檔案**：`compat/usrfont.prg`（C 碼 wcharGet 區段）



### 列印輸出路徑修正

- **問題**：多重輸出模式（列印/PDF/Excel/預覽）分派不正確

- **解決方法**：`SpoolCloseAndPrint()` 根據 PRNMETHOD + SKIPASK + AUTOFMT 正確分派

- **修改檔案**：`compat/print_spool.prg`



---



## 2026-04-20



### 游標殘影修復

- **問題**：雙緩衝截圖捕獲 XOR 游標殘影；全形字游標寬度錯誤

- **解決方法**：游標高度/寬度計算修正；截圖前隱藏游標

- **修改檔案**：`compat/usrfont.prg`



### GTWVT ExtWideSymbol 擴展全形

- **問題**：數學符號/方塊符號（U+2200-26FF）顯示為半形，破壞版面

- **解決方法**：`isWideChar()` 擴展範圍 + System.ini `ExtWideSymbol=1` 開關

- **修改檔案**：重新編譯 gtwvt.lib、`build/System.ini`



### 中文註解回復

- **問題**：多個 PRG 檔的中文註解遺失或亂碼

- **解決方法**：完整閱讀程式碼後重新撰寫準確中文註解（Python bytes-only）

- **修改檔案**：多個 source/ 和 compat/ 下的 PRG 檔



---



## 2026-04-19



### UDA 渲染路徑修復

- **問題**：提示列圖示背景色不一致（部分黑色、部分橘色）

- **解決方法**：`EnsureUfntAlpha` 移除自我參照 guard → 一律走 `CaptureGlyphAlpha` alpha 混合

- **修改檔案**：`compat/usrfont.prg`



### BIG5 A1E3 字元對照發現

- **問題**：Harbour BIG5 與 Windows CP950 對照不一致

- **解決方法**：建立 CP950 vs Harbour 對照表，`usrfont_wcharGet()` 攔截差異項

- **修改檔案**：`compat/usrfont.prg`、`include/CustomChar.ch`



---



## 2026-04-18



### ASCFONT 系統完成

- **問題**：ASCII 控制字元（chr(0)~chr(31), chr(127)）需離線顯示

- **解決方法**：

  - 13 個字元（¶♠☺○▬☻§♦♥♣‼▲▼）bitmap cache

  - ASCFONT.UMAP + ASCFONT.BCACHE 格式

  - DirectDC 渲染 + alpha 混合 + 動態 cell 尺寸

- **修改檔案**：`compat/usrfont.prg`、`build/gen_ascfont_umap.py`（新）



---



## 2026-04-16~17



### MsHandler CLASS 重構（Phase 1 完成）

- **問題**：滑鼠系統分散在各模組，hover/click/SAY 註冊不統一

- **解決方法**：

  - MsHandler 單例 CLASS + 25+ METHOD

  - BrowseKey 統一方法

  - PushReg/PopReg 狀態隔離

  - dbEditEx（TBrowse 版 DBEDIT 替代品）

- **修改檔案**：`compat/mouse_handler.prg`（重寫）、`compat/dbedit_mouse.prg`（新）、11 個業務模組 SAY 註冊



### 滾輪捲動 + 資料列點選

- **問題**：DBEDIT/Browse 不支援滑鼠滾輪和資料列點選

- **解決方法**：Set EVENTMASK + DE_IDLE callback 中處理滑鼠事件

- **修改檔案**：`source/DBFWIN.PRG`、`source/WINDOW.PRG`、`source/TABLE.PRG`



---



## 2026-04-15



### Incremental 連續捲動（12 模組）

- **問題**：按住方向鍵/PgUp/PgDn 應平滑連續捲動

- **解決方法**：各 Browse 模組加入 incremental key-repeat 機制

- **修改檔案**：BUYSELL、QUOT、INQUIRY、ENQUIRY、DOMPUR、FORPUR、DOMARR、FORARR、FORCMP、DOMCMP、DOMORD、DOMSCH、DEPOSIT、STOCK（共 14 個 .PRG）



---



## 2026-04-14



### Color Theme 擴展

- **問題**：配色主題需要更多變化，淺色/深色主題

- **解決方法**：

  - 擴展 16 色 slot 定義（slot7/15 設計規則）

  - 黃金對比比值 5~7:1

  - Theme 0~8 變體

- **修改檔案**：`compat/color_theme.prg`



---



## 2026-04-13



### PrnAsk.exe 完成

- **問題**：需要完整的列印 UI 對話框（PDF 預覽 + 格式選擇）

- **解決方法**：

  - FiveWin 對話框 + WebView2 PDF 預覽

  - PDF.js 離線打包進 EXE（7.9MB 自包含）

  - DPI 自適應視窗

  - 批次模式 INI 處理

- **修改檔案**：`E:\ConvertFW\PRNASK\` 全部（source + build）



---



## 2026-04-12



### RestScreen 修復

- **問題**：RestScreen 恢復畫面時造字位置被清除

- **解決方法**：直接複製 `HB_SCREENCELL` 繞過 PutChar，不呼叫 UpdateWindow

- **修改檔案**：`compat/usrfont.prg`（GT_REST hook）



### USRFONT PUA 技術手冊

- **產出**：`doc/USRFONT_PUA_Technical_Manual.pdf`



---



## 2026-04-11



### GTWVT CJK 修正

- **問題**：GTWVT 中文顯示需要完整的 CJK 支援

- **解決方法**：USRFONT hook 系統建立（Layer 1~3）

- **修改檔案**：`compat/usrfont.prg`、`source/main.prg`



### DosPntGo 控制碼參考表

- **產出**：`doc/DosPntGo_控制碼參考表.pdf`



---



## 2026-04-10



### GTWVT CJK Fix（初始版）

- **問題**：Harbour GTWVT 無法正確顯示 Big5 造字區 UDA 字元

- **解決方法**：

  - Big5 codepage hook（FA-FE 範圍映射至 Unicode PUA）

  - GTWVT subclass hook（WM_PAINT 覆蓋繪製 UDA 字形）

  - USRFONT.24M 點陣字型載入（24×24 1-bit bitmap）

- **修改檔案**：`compat/usrfont.prg`（建立）

- **產出**：`doc/GTWVT_CJK_Fix_Report.pdf`（12 頁）



---



## 換機部署注意事項



### 必要檔案

| 檔案 | 說明 |

|------|------|

| Stk32.exe | 主程式 |

| PrnAsk.exe | 列印 UI |

| pdflib.dll | PDF 生成引擎 |

| WebView2Loader.dll | PDF 預覽 |

| System.ini | 系統設定（PRNMETHOD=1） |

| stk.ini | 應用設定 |

| USRFONT.* / ASCFONT.* | 造字字型 |

| bcache/ | 多解析度造字快取 |



### 不要搬的檔案

| 檔案 | 原因 |

|------|------|

| printer.cfg | 存了開發機的印表機名 |

| debug_on.txt | 開發用 |

| *.bak* | 備份檔 |



### 常見問題

| 問題 | 原因 | 解法 |

|------|------|------|

| 印表機選擇等 5 分鐘 | win_printerList() 網路印表機 timeout | 確認 Print Spooler 服務啟動 |

| 列印功能全部消失 | System.ini 缺少或 PRNMETHOD=0 | 部署完整 System.ini（PRNMETHOD=1） |

| 造字全部空白 | BCACHE 被清空或 FontDirect=1 但沒華康 | 確認 bcache/ 有檔案，FontDirect=0 |

| PrnAsk 無反應 | pdflib.dll 缺少 | 部署 pdflib.dll |

| 畫面亂碼 | WS_THICKFRAME 或 Edit tool 改 BIG5 | 不用 THICKFRAME；PRG 一律 Python bytes-only |

