# **相依套件分析報告** 本報告針對 ActiveX 元件核心實作檔 CB_IMGPSScanImp.pas 進行功能解構,並分析其與第三方套件的綁定關係,以作為後續維護或現代化升級之參考。 ## **壹、 依功能拆分模組與第三方套件綁定分析** 根據原始碼結構,CB_IMGPSScanImp.pas 是一個將介面、硬體、網路與業務邏輯高度耦合的「上帝物件 (God Object)」。若要進行重構或拆件,建議依據以下五個核心模組進行解耦: ### **1. 硬體掃描控制模組 (Scanner Controller)** - **功能描述**:負責與實體掃描機溝通,處理 TWAIN 介面初始化、設定掃描參數(DPI、色彩、雙面)、觸發掃描指令,以及接收掃描完成的記憶體影像 (DibHandle)。 - **關鍵方法**:initkscan、StatrTwainScan、OnAcquire、PageEnd、PageDone。 - **第三方套件綁定 (強綁定)**: - 依賴 **Envision Imaging SDK** (EnScan) 進行 TWAIN 驅動控制。 - 依賴 EnMisc 處理底層記憶體或雜項運算。 ### **2. 影像處理與轉換模組 (Image Processor)** - **功能描述**:負責對擷取到的原始影像進行後製處理,包括:裁切(如 A3 裁 A4)、去斜 (Deskew)、旋轉、二值化 (轉黑白)、格式轉換 (轉 TIFF/JPEG),以及讀取影像上的條碼。 - **關鍵方法**:ImageReSize_FormID、CheckNeedCrop、DeskewImg、ConvertToBW。 - **第三方套件綁定 (強綁定)**: - 依賴 **Envision Imaging SDK** (EnTifGr, EnDiGrph) 進行 TIFF (CCITT Group 4) 壓縮與 DIB 物件封裝。 - 依賴 **MPS Barcode** (mpsBarco, BarcodesFinder) 進行影像條碼掃描與解析。 ### **3. 安全傳輸與 API 通訊模組 (Transport Manager)** - **功能描述**:負責將掃描並壓縮後的檔案上傳至伺服器,或從伺服器下載檔案。同時處理與後端 Java Servlet 的參數交換(如校時、取得 FTP 資訊、更新案件狀態)。 - **關鍵方法**:upFile、dnFile_Get、ProcessServlet_Get、GetServerDate、GetftpInfo。 - **第三方套件綁定 (強綁定)**: - 依賴 **SecureBlackbox** (SBSimpleSSL, SBHTTPSClient, SBSimpleFTPS 等) 處理 HTTPS/FTPS 安全加密通訊。 - 依賴 **Indy** (IdHashMessageDigest) 產生檔案的 MD5 檢核碼。 - 依賴自訂/第三方加密庫 (Encryp) 處理字串加解密。 ### **4. 業務邏輯與資料解析模組 (Business Logic & Parser)** - **功能描述**:專案核心的領域邏輯,包含接收網頁端傳入的參數字串進行解析、定義文件歸類邏輯(由條碼推導文件類型)、OMR (光學標記辨識) 檢核定義等。 - **關鍵方法**:SetSQLData (負責解析傳入的長字串至 TStringList)、GetSQLData、FormCode2DocNo、OMRCheckCase。 - **第三方套件綁定 (中度綁定)**: - 依賴 **Xmltool** 解析 XML 格式的 OMR 座標定義。 - 依賴 **VCLZip** (VCLZip, VCLUnZip) 將多個影像檔打包成 ZIP 進行單一傳輸。 ### **5. UI 與介面呈現模組 (UI View)** - **功能描述**:負責 ActiveX 控制項的視覺化介面,包含左側的樹狀文件清單 (TreeView)、右側的影像預覽區 (ScrollBox),以及按鈕與多國語言切換。 - **關鍵方法**:InitialLanguage、各類 OnClick 事件。 - **第三方套件綁定 (輕度綁定)**: - 依賴 **Envision Imaging** (EnImgScr) 作為支援大影像拖曳的滾動視窗。 - 依賴 PJMenuSpeedButtons 作為自訂外觀的按鈕元件。 ## **貳、 第三方套件功能總覽** 為達成上述複雜的業務需求,本專案引入了多個 Delphi 著名的第三方套件。以下為各套件的大致功能說明: ### **1. Envision Imaging SDK (前綴 En)** 這是一套專為 Delphi 設計的高效能影像與 TWAIN 處理套件,在金融與保險業的文件電子化系統中非常常見。 - **EnScan**:封裝了 TWAIN 協定,讓程式能直接控制實體掃描機,控制進紙、解析度與雙面掃描。 - **EnDiGrph (TDibGraphic)**:封裝 Windows 底層的 DIB (Device Independent Bitmap) 記憶體句柄,讓開發者能像操作一般圖片一樣操作掃描出來的原始資料。 - **EnTifGr**:專門處理 TIFF 影像格式。銀行業愛用 TIFF 是因為它支援「多頁」且黑白壓縮率極高 (Group 4 Fax Compression)。 - **EnImgScr**:提供 TImageScrollBox 視覺元件,用來平滑顯示並縮放超大尺寸的掃描圖檔。 - **EnMisc**:提供影像處理所需的底層輔助數學或資料轉換函式。 ### **2. SecureBlackbox (前綴 SB)** 由 EldoS (後被 /n software 收購) 開發的強大安全通訊與加密套件。由於早期 Delphi (D7~D2007) 內建網路庫缺乏完善的 SSL/TLS 支援,此套件被廣泛用於補強安全性。 - **SBHTTPSClient / SBSimpleSSL**:實作了完整的 HTTPS 用戶端通訊協定,用來安全地上傳檔案或呼叫 Servlet API。 - **SBSimpleFTPS**:支援 FTP over SSL (FTPS) 的安全檔案傳輸。 - **SBWinCertStorage / SBX509**:處理 Windows 系統內的憑證儲存區,以及 X.509 數位憑證的驗證(例如確認銀行伺服器的 HTTPS 憑證是否合法)。 ### **3. MPS Barcode & BarcodesFinder** - **功能**:這是一套條碼識別引擎。 - **專案應用**:在掃描過程中(如 PageDone 或 OnAcquire 階段),它會掃描圖片特定區域尋找一維或二維條碼。讀取到的條碼值會被用來決定這張影像屬於哪一種「文件表單」(FormCode),進而實現自動分類歸檔。 ### **4. VCLZip (VCLZip, VCLUnZip)** - **功能**:老牌的 Delphi ZIP 壓縮元件。 - **專案應用**:為了節省網路頻寬與減少伺服器連線次數,掃描完成的多張 TIFF/JPG 檔案會被打包成一個 .zip 檔後,再一次性上傳至伺服器。 ### **5. Indy Components (前綴 Id)** - **IdHashMessageDigest / idHash**:Delphi 內建的 Internet Direct (Indy) 函式庫。 - **專案應用**:在此專案中被用來計算檔案或字串的 **MD5 Hash (雜湊值)**。通常用於確保檔案上傳過程沒有損壞,伺服器比對 MD5 一致才算上傳成功。 ### **6. 其他工具 (Tools & Utils)** - **Xmltool**:負責讀寫 XML 檔案。銀行經常將表單的 OMR (光學標記辨識) 檢核座標或參數寫成 XML 設定檔,透過此工具讀入系統。 - **Encryp**:這可能是開發商自訂或第三方的輕量級加密單元,用來加密本機設定檔 (.ini) 內的敏感資訊或混淆網頁傳入的參數。 # **核心源碼與物件實作解析** 本文件針對 CB_IMGPSScanImp.pas 中的核心方法與隱藏物件進行原始碼級別的邏輯拆解與推導。 ## **壹、 核心方法源碼說明** ### **1) TCB_IMGPSScanX.GetCurrentVersionNo** - **功能描述**:取得目前 OCX 元件的版本號。這通常提供給網頁端的 JavaScript 呼叫,用來判斷客戶端的 OCX 是否需要觸發更新下載。 - **實作推導與底層 API 說明**: 在 Delphi 中,若要動態讀取編譯進 OCX 的 VS_VERSION_INFO 資源,標準作法是呼叫一系列的 Windows API。這段實作通常包含以下三個關鍵概念: 1. **HInstance (實體句柄)**: - 這是 Delphi 提供的一個全域變數,代表目前載入記憶體中的模組(即這個 OCX 檔)的「實體句柄 (Instance Handle)」。 - 在 Windows 作業系統中,當一個執行檔 (EXE) 或動態連結檔 (DLL/OCX) 被載入記憶體時,會獲得一個記憶體基底位址,HInstance 就是指向這個位址的指標。Windows 需要這個值來識別「你要操作的是哪一個載入的模組」。 1. **GetModuleFileName 的作用**: - 這是一個 Windows API。它的作用是利用前面提到的 HInstance,向作業系統反查出這個模組在硬碟上的**「完整實體檔案路徑」**(例如:C:\Windows\Downloaded Program Files\CB_IMGPSScan.ocx)。 - 因為我們需要讀取檔案本身的版本資訊,程式必須先知道「我自己到底存放在硬碟的哪裡」。 1. **後半段 VerQueryValue 的運作邏輯**: 取得檔案路徑後,讀取版本資訊會經過三部曲,而 VerQueryValue 是最後負責「解析」的關鍵: - **Step 1 (GetFileVersionInfoSize)**:先問 Windows 這個檔案的版本資訊區塊有多大。 - **Step 2 (GetFileVersionInfo)**:把整塊版本資訊的二進位原始資料 (Raw Data) 完整讀進一段記憶體緩衝區中。 - **Step 3 (VerQueryValue)**:因為剛剛讀進來的是一包無法直接閱讀的二進位資料,VerQueryValue 的作用就是**「在這包資料中進行精準查詢」**。透過傳入特定的查詢路徑(例如傳入 \ 可以取得包含主版號、次版號的 VS_FIXEDFILEINFO 結構體;或是傳入特定語系代碼如 \StringFileInfo\040404E4\FileVersion),作業系統會幫你把二進位資料解析成我們需要的字串或數字版本號(如 1.0.3.5)。 ### **2) TCB_IMGPSScanX.ActiveFormCreate** - **功能描述**:這是 ActiveX 視覺化窗體 (Form) 的生命週期起點。當 IE 瀏覽器或應用程式載入這個 OCX 時,首先觸發的初始化事件。 - **實作推導**: 它會負責初始化全域變數、設定第三方元件的預設值、並建立暫存資料夾。 ```pascal procedure TCB_IMGPSScanX.ActiveFormCreate(Sender: TObject); begin // 1. 預設變數初始化 Def_DeviceDelete := True; Def_ScannerReverse := False; // 2. 建立本機暫存目錄 ForceDirectories('C:\Temp\IMGPSScan'); // 3. UI 語系初始化 InitialLanguage; end; ``` ### **3) TCB_IMGPSScanX.StatrTwainScan** - **功能描述**:啟動掃描機的 TWAIN 介面,開始進紙或掃描。(註:Statr 應為 Start 的早期拼字錯誤,這在舊專案中很常見)。 - **實作推導**: 這個方法會讀取網頁傳進來的參數(如 DPI、色彩),設定給 Scanner 物件,然後發出擷取指令。 ```pascal procedure TCB_IMGPSScanX.StatrTwainScan; begin // 設定解析度與顏色 Scanner.Resolution := StrToIntDef(Fimgdpi, 200); if Fscancolor = 'C' then Scanner.PixelType := ptColor else Scanner.PixelType := ptGray; // 啟動掃描 (會喚起實體掃描機) Scanner.Acquire; end; ``` ### **4) TCB_IMGPSScanX.OnAcquire 與資料流物件** - **功能描述**:這是 Scanner (EnScan) 最核心的事件。當掃描機成功掃完「一張」影像並傳入電腦記憶體時,會觸發此事件。 - **源碼邏輯推導**: ```pascal procedure TCB_IMGPSScanX.ScannerAcquire(Sender: TObject; DibHandle: THandle; var Handled: Boolean); var MyStream: TMemoryStream; MyGraphic: TDibGraphic; begin // 1. 將 DibHandle 轉換為 Delphi 影像物件 MyGraphic := TDibGraphic.Create; MyGraphic.Handle := DibHandle; // 2. 影像處理 (如去斜、黑白轉換) // 3. 存入記憶體串流或實體檔案 // ... end; ``` - **三大核心資料物件解釋**: 1. **DibHandle**:這是 Windows 底層的「全域記憶體句柄 (Global Memory Handle)」。掃描機驅動把圖片放在記憶體裡,給你這個代號。你必須透過 TDibGraphic 來「解讀」這塊記憶體。 2. **TMemoryStream**:**記憶體串流**。它的作用像是一塊存在 RAM 裡的虛擬硬碟。掃描後的圖檔在還沒存到實體硬碟前,會先放在這裡進行格式轉換 (如轉成 JPEG) 或進行 MD5 計算,因為在記憶體中操作速度最快。 - **DeleteStm : TMemoryStream 的逐行操作說明**: 在 `OnAcquire` 事件中,`DeleteStm` 主要用於「空白頁判定」。其源碼邏輯如下: ```pascal // 判定是否啟動空白頁刪除功能 if Def_DeviceDelete then begin // 1. 在 Heap 記憶體中建立一個暫存串流物件 DeleteStm := TMemoryStream.Create; try // 2. 將當前掃描到的影像物件 (MyGraphic) 寫入記憶體串流 // 這會根據 TIFF G4 等格式進行壓縮,產生實際的檔案位元組資料 MyGraphic.SaveToStream(DeleteStm); // 3. 核心判定:檢查該影像在記憶體中所佔用的位元組大小 (Size) // 若小於預設門檻值 (Def_DeviceDeleteSize, 如 3072 Bytes) if DeleteStm.Size < Def_DeviceDeleteSize then begin // 4. 若符合空白頁特徵,則不進行存檔動作,直接跳出此 Procedure // 並告知掃描控制項此頁已處理完畢 (Handled := True) Handled := True; Exit; end; finally // 5. 無論是否為空白頁,操作結束後必須釋放記憶體,避免 Memory Leak DeleteStm.Free; end; end; ``` - 3. **TFileStream**:**檔案串流**。用來直接對硬碟讀寫資料。當確認影像無誤後,會透過 TFileStream 將 TMemoryStream 的內容真實地寫入 C:\Temp\...\001.tif 中。 ### **5) TCB_IMGPSScanX.FindISB2View** - **功能描述**:用來在畫面上尋找並更新用來預覽圖片的元件。 - **名詞解釋**:**ISB 代表 ImageScrollBox**。 這對應到 uses 區段中的 EnImgScr 套件。TImageScrollBox 是 Envision 提供的一個 UI 元件,專門用來顯示超出螢幕大小的掃描圖檔,支援平滑拖曳與縮放。這個方法通常用來把剛掃描完的 DibHandle 餵給畫面上的 ISB 元件顯示。 ### **6) TCB_IMGPSScanX.GetNoNameCase** - **功能描述**:處理「無案號」或「未知歸屬」的掃描件。 - **源碼實作逐行說明**: 此方法透過迴圈尋找一個尚未被使用的目錄名稱(格式為「未配號 + 四位數字」)。 ```pascal Function TCB_IMGPSScanX.GetNoNameCase(Path:String):String; // 取未配號XXXX var i : Integer; // 宣告迴圈計數器 begin // 1. 啟動一個從 1 到 9999 的迴圈,尋找可用的索引值 for i := 1 to 9999 do begin // 2. 檢查特定路徑下的目錄是否已存在 // _Msg('未配號'):取得「未配號」字串(可能具備多國語言處理) // Add_Zoo(i, 4):將數字 i 補齊為 4 位數(例如 1 變為 0001) if Not DirectoryExists(Path + _Msg('未配號') + Add_Zoo(i, 4)) then begin // 3. 若該目錄不存在,代表此名稱可用,設定 Result 並跳出迴圈 Result := _Msg('未配號') + Add_Zoo(i, 4); Break; // 找到第一個可用的名稱後立即停止搜尋 end; end; end; ``` ### **7) TCB_IMGPSScanX.SetSQLData 與 8) GetSQLData** 這兩個方法是 OCX 與網頁 (JS) 溝通的雙向橋樑。 - **SetSQLData (網頁 -> OCX)**:接收由網頁傳來的設定字串(通常用 | 或 , 分隔,例如 192.168.1.1|L12345678|200|C),在內部將字串切開 (Split),並分別賦值給 ServerIP, FCaseID, Fimgdpi, Fscancolor 等內部變數。 - **GetSQLData (OCX -> 網頁)**:將 OCX 內部的處理結果(例如:UploadSuccess|3|0 代表上傳成功、共 3 頁、0 錯誤),組合成一個長字串後回傳給網頁端的 JS 進行後續的頁面跳轉或提示。 ### **8) TCB_IMGPSScanX.GetDefScanIni** - **功能描述**:取得並套用掃瞄參數的預設值與自訂設定。 - **源碼實作解析**: - **硬編碼預設值 (Hardcoded Defaults)**: 程式一開始會先寫死一組標準值,例如 Def_ScanDpi := 300 (預設 300 DPI)、Def_DeviceDeleteSize := 3072 (空白頁判定門檻為 3KB)。 - **動態配置覆蓋 (Dynamic Overriding)**: 透過 for i := 0 to WORK_INF_List.Count - 1 do 迴圈遍歷設定檔清單。使用 GetSQLData 取得參數代號 (PARA_NO) 與內容 (PARA_CONTENT)。 - **屬性設定對照表**: 下表整理了此方法中設定的所有關鍵屬性、其對應的參數代號及功能說明。 | **屬性名稱 (內部變數)** | **對應 PARA_NO** | **PARA_CONTENT 處理方式(GetSQLData)** | **功能說明** | | --- | --- | --- | --- | | **Def_DeviceDelete** | SCAN_BLANKDEL_USE | 'Y' -> True, Else -> False | 空白頁自動刪除開關。 | | **Def_DeviceDeleteSize** | SCAN_BLANKDEL_SIZE | StrToInt (若空則 0) | 空白頁判定門檻 (Bytes)。 | | **Def_ScannerReverse** | SCAN_REVERSE | 'Y' -> True, Else -> False | 影像是否反相。 | | **Def_BoardClear** | SCAN_BOARDCLEAR | 'Y' -> True, Else -> False | 是否清除影像黑邊。 | | **Def_ScanDpi** | SCAN_DPI | StrToInt (若空則 300) | 掃描解析度。 | | **Def_ScanDuplex** | SCAN_DUPLEX | 'Y' -> True, Else -> False | 是否啟用雙面掃描。 | | **Def_ScanRotate** | SCAN_ROTATE_MODE | '0'->0, '1'->270, '2'->180, '3'->90 | 掃描旋轉角度映射。 | | **Def_ScanDeskew** | SCAN_DESKEW | 'Y' -> True, Else -> False | 是否自動矯正傾斜。 | | **Def_ScanImgSetUse** | SCAN_IMGSET_USE | 'Y' -> True, Else -> False | 是否使用亮度/對比設定。 | | **Def_ScanBright** | SCAN_BRIGHT | StrToInt (限制 -255~255) | 亮度數值。 | | **Def_ScanContrast** | SCAN_CONTRAST | StrToInt (限制 -255~255) | 對比數值。 | | **Def_ScanImgShowMode** | SCAN_SHOW_MODE | '0','1','2' 對應模式 | 影像顯示/縮放模式。 | | **ScanDenialTime** | CASE_IN_TIME | 直接存為 String | 進件截止時間限制。 | | **ScanDenialHint** | SCAN_HINT | 直接存為 String | 掃描畫面提示字串。 | | **NoSaveBarCodeList** | NO_SAVE_FORM_ID | CommaText := Value | 不存檔之表單代號清單。 | | **ImagePath** | LOCAL_PATH | 直接存為 String | 本機端暫存路徑。 | | **GuideFormIDList** | GUIDEFORMID | CommaText := Value | 導引頁表單 ID 清單。 | | **DivPageFormIDList** | DIVPAGEFORMID | CommaText := Value | 分案頁表單 ID 清單。 | | **FJpgCompression** | FILE_COMPRESSION | StrToInt | JPG 轉 TIF 壓縮比。 | | **FMaxUploadSize** | MAX_UPLOAD_SIZE | 直接存為 String | 上傳大小限制 (MB)。 | ## **貳、 記憶體直接操作實作清單** 以下條列專案中直接涉及記憶體操作的程式區段: ### **記憶體操作對應位置表** | **記憶體操作對象** | **所屬 Function / Procedure** | **關鍵操作關鍵字 / 代碼** | **記憶體特性說明** | 註記 | | --- | --- | --- | --- | --- | | **HInstance** | GetCurrentVersionNo | GetModuleFileName(HInstance, ...) | 模組載入位址。 | 增API版本號 | | **資源緩衝區** | GetCurrentVersionNo | VerQueryValue, GetFileVersionInfo | 透過 Pointer 進行二進位搜尋。 | 增API版本號 | | **DibHandle** | onAcquire | MyGraphic.Handle := DibHandle; | Windows 全域記憶體句柄轉移。 | 改套件實作/Rust | | **TMemoryStream** | onAcquire | MemoryStream.Write, .Read | Heap Memory 內部二進位操作。 | | ## **參、 遺失物件反推與說明** 你提到在原始碼中找不到 _DelTree 和 Scanner 的實作,這是因為它們屬於以下兩種情況: ### **1) TCB_IMGPSScanX._DelTree** - **這是一個自訂的私有輔助函式 (Private Helper)**。 - **功能**:它的名稱源自 DOS 時代的指令 deltree(刪除整個目錄樹)。在 ActiveX 中,當上傳伺服器成功後,必須清空 C:\Temp\案件編號\ 裡的所有暫存圖檔與資料夾以釋放空間,並防止資料外洩。 - **實作原理**:由於早期的 Delphi 沒有內建遞迴刪除目錄的單一指令,開發者通常會自己寫一個 _DelTree,裡面使用 FindFirst, FindNext 找出所有檔案並用 DeleteFile 刪除,最後用 RemoveDir 砍掉資料夾。 ### **2) Scanner** - **這是一個視覺化/非視覺化的第三方元件實例 (Component Instance)**。 - **為何找不到宣告?** 因為在 Delphi 中,如果你是透過 IDE 的工具箱 (Tool Palette) 將 TEnScan 元件直接拖拉到窗體 (Form) 上,IDE 會自動把它寫在 .dfm 檔裡,並在 TCB_IMGPSScanX 類別的 published 區段自動產生一行 Scanner: TEnScan;。 - **功能**:它就是 **Envision SDK** 裡用來與 TWAIN 硬體溝通的靈魂核心。所有的掃描屬性 (Scanner.Resolution) 與方法 (Scanner.Acquire) 都是對這個物件進行操作。 --- # **IO 路徑與檔案操作分析** 本報告詳列了 CB_IMGPSScanImp.pas 檔案中定義的所有 IO 相關變數,以及執行目錄與檔案管理的關鍵方法,並定義了抽像化所需的介面原型。 ## **壹、 關鍵路徑與檔案變數 (Variables)** 程式中定義了多個全域或私有欄位,用來追蹤檔案在硬碟上的實體位置。 ### **1. 核心路徑變數與動態組合邏輯** | **變數名稱** | **類型** | **組合邏輯 (Combination Logic)** | **用途說明** | | --- | --- | --- | --- | | **ImagePath** | String | WORK_INF 的 LOCAL_PATH 參數值 | **本機基礎根路徑**。所有暫存資料的起點。 | | **ScaniniPath** | String | ImagePath + FWork_No + '\' + FUserUnit + '\' | **設定檔存放區**。區分作業別與單位。 | | **ImageSavePath** | String | 同 ImagePath (初期) 或隨 FMode 變動 | **影像儲存基準路徑**。 | | **ScanPath** | String | ImageSavePath + ScanCaseno + '\' + ScanDocdir + '\' | **當前掃描實體目錄**。指向最末層的影像資料夾。 | | **TransPath** | String | ImageSavePath + CaseID + '\Upload\' | **準備上傳區**。在傳送前將檔案結構扁平化組合於此。 | | **DisplayPath** | String | ImageSavePath + NowCaseNo + '\' | **預覽基準目錄**。用於 UI 點選樹狀圖時讀取特定案件資料。 | | **LngPath** | String | GetLocalAppDir(Handle) + 'MPS\CB_IMGPS\' | **應用程式資源路徑**。存放在使用者設定檔目錄以避開權限問題。 | | **CheckXmlPath** | String | ImagePath + 'OMRSITE\' | **OMR 規則暫存區**。存放從伺服器同步下來的 XML 定義。 | | **SamplePath** | String | ImagePath + 'Sample\' + FWork_No + '\' | **範本影像存放區**。按作業別區分。 | | **SitePath** | String | ImagePath + 'Site\' | **登打定位存放區**。 | | **FFtpRootPath** | String | 透過伺服器回傳值動態設定 | **FTP 伺服器根路徑**。指定在遠端 FTP 伺育器上存放影像檔的起始位址。 | ### **2. 檔案名稱變數 (Filenames)** | **變數名稱** | **類型** | **用途說明** | | --- | --- | --- | | **ScanSaveFilename** | String | 下一張掃描影像預定的檔名(通常包含 FormID)。 | | **PEFileName** | String | 在 PageEnd 階段確定的最終完整檔案路徑。 | | **AttName** | String | **附件目錄名稱**。根據 FIs_In_Wh 決定為 Attach 或 S_Attach。 | | **Ext** | String | 副檔名。預設為 .tif,彩色/灰階模式下可能切換為 .jpg。 | ## **貳、 關鍵 IO 操作方法 (Methods)** 以下為需要進行抽像化設計的核心 IO 方法。 ### **1. 目錄管理 (Directory Management)** | **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** | | --- | --- | --- | --- | | _DelTree | 遞迴刪除整個目錄樹 | 上傳成功後清空暫存、模式切換初始化目錄 | RemoveDirRecursive(Path) | | Str2Dir / ForceDirectories | 確保路徑以 \ 結尾並建立路徑 | 初始化或掃描前建立資料夾 | EnsureDirExists(Path) | | GetNoNameCase | 搜尋尚未配號的臨時目錄 | 在本機搜尋如 未配號0001 到 9999 的可用路徑 | FindNextAvailablePath(Pattern) | | GetLocalAppDir | 取得 Windows %LocalAppData% 路徑 | 確保在現代 Windows 系統中有權限寫入語言檔與 Log | GetBaseStoragePath() | ### **2. 存在性檢查與搜尋 (Existence & Search)** | **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** | | --- | --- | --- | --- | | FileExists | 檢查指定路徑檔案是否存在 | 在載入 .dat、.ini 或影像前進行安全檢查 | IsFilePresent(Path) | | DirectoryExists | 檢查指定路徑資料夾是否存在 | 在建立掃描目錄或上傳前確認結構完整性 | IsDirPresent(Path) | | FindFirst / FindNext | 搜尋符合特定格式的檔案 | 取得實體檔案大小 (FileRec.Size) 或遞迴搜尋檔案 | ListFiles(Path, Pattern) / GetFileInfo(Path) | ### **3. 檔案生命週期與刪除 (File Lifecycle & Deletion)** | **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** | | --- | --- | --- | --- | | ReSortFileName | 影像重新編序命名 | 刪除或插入影像後,修正 001_xxx 等序號連續性 | RenameFile(Old, New) | | DeleteImageFile | 刪除實體影像檔 | 單張影像刪除並觸發 .dat 內容同步更新 | DeleteFile(Path) | | DeleteDocNoFile | 刪除特定文件代號下所有影像 | 整批文件(DocNo)的物理刪除與序號重整 | DeleteFilesByPattern(Pattern) | | DeleteShowFile | 刪除 UI 顯示中的影像清單 | 根據介面選取狀態進行批次物理刪除 | DeleteFileList(List) | | RenameFile / MoveFile | 檔案搬移或更名 | 用於「分案」、「移動頁數」或「變更歸類」 | MoveFile(Src, Dest) | | CopyFile | 複製實體檔案 | 影像搬移至上傳區或引用舊案件影像 | CopyFile(Src, Dest) | ### **4. 資料紀錄與序列化 (Metadata IO)** | **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** | | --- | --- | --- | --- | | DeleteCustomDocDir | 移除自訂文件定義 | 從 CustomDocNo.ini 中移除特定自訂文件的節點 | Storage.IniDeleteSection(Section) | ### **5. 網路傳輸 IO 方法 (Network Retrieval - dnFile_Get)** 專案中使用 `dnFile_Get` 從伺服器端同步必要的環境檔案或規則定義。 | **抓取對象 (目標路徑)** | **遠端請求 Action** | **用途說明** | | | --- | --- | --- | --- | | **CheckXmlPath + filename** | `GetCheckXml` | **下載 OMR 檢核定義檔**。同步伺服器端最新的 XML 座標規則,用於自動判斷影像區域。 | https | | **SamplePath + filename** | `GetSampleImg` | **下載範本影像**。用於在 UI 介面上提供給操作員對照的標準範例圖檔。 | https | | **SitePath + filename** | `GetSiteImg` | **下載定位定義圖**。下載與登打、校對位置相關的輔助影像。 | https | | **LngPath + 'Language.ini'** | `GetLanguage` | **同步多國語言檔**。確保用戶端介面文字(繁體、英文、越南語等)與伺服器同步。 | https | ## **參、 資料紀錄與序列化 (Metadata IO)** 專案中使用了大量的 .dat 與 .ini 檔案來記錄目錄內的結構資訊與使用者偏好。 ### **1. .dat 檔案 (結構紀錄)** | **檔案名稱** | **組合路徑邏輯 (Path Logic)** | **內容與用途** | | --- | --- | --- | | **Context.dat** | ImageSavePath + CaseID + '\' + DocDir + '\Context.dat' | **影像清單**。紀錄該資料夾內所有影像檔名的正確順序。 | | **DocDir.dat** | ImageSavePath + CaseID + '\Upload\DocDir.dat' | **目錄對應表**。紀錄 Upload 下每個檔名所屬的原始 DocDir | | **CaseDocNo.dat** | ImageSavePath + CaseID + '\CaseDocNo.dat' | **文件目錄索引**。紀錄案件內已建立的實體資料夾名稱。 | | **CaseDocNo_Copies.dat** | ImageSavePath + CaseID + '\CaseDocNo_Copies.dat' | **份數紀錄**。對應 CaseDocNo.dat 中的資料夾分別有多少份。 | | **CaseIndex.dat** | ImageSavePath + CaseID + '\CaseIndex.dat' | **案件屬性**。如是否為「授信卷」(Case_loandoc)。 | | **EditedDocDir.dat** | ImageSavePath + CaseID + '\EditedDocDir.dat' | **異動清單**。紀錄當次操作中被修改過的文件目錄。 | | **Scan_Memo.dat** | DisplayPath + 'Scan_Memo.dat' | **使用者註記**。紀錄操作員手動輸入的備註內容。 | | **CaseList.dat** | ImageSavePath + 'CaseList.dat' | **案件總表**。紀錄目前本機快取中存在的所有 CaseID。 | ### **2. .ini 檔案 (設定與偏好)** | **檔案名稱** | **路徑邏輯** | **回寫 (Write-back)** | **內容與用途** | 寫入關聯 | | --- | --- | --- | --- | --- | | **Scan.ini** | ScaniniPath + 'Scan.ini' | **是 (頻繁)** | **掃描器硬體設定**。儲存最後一次選取的 DPI、雙面、色彩模式、亮度、對比等參數。 | | | **CustomDocNo.ini** | ScaniniPath + 'CustomDocNo.ini' | **是 (中等)** | **自訂文件代號**。儲存操作員定義的文件分類名稱與內部 ID 映射。 | DeleteCustomDocDir | | **CB_IMGPSScan.ini** | LngPath + 'CB_IMGPSScan.ini' | **是 (低頻)** | **環境配置**。儲存伺服器 IP、Port、自動更新版本資訊等全域參數。 | | | **Language.ini** | LngPath + 'Language.ini' | **否 (唯讀同步)** | **多國語言包**。僅透過 `dnFile_Get` 從伺服器抓取更新,OCX 不自行修改。 | | ## **肆、 串流與讀寫操作 (Stream & File I/O)** 源碼中頻繁使用 .LoadFromFile 與 .SaveToFile 來實現資料的持久化。 ### **1. 使用 TStringList 讀寫 .dat 結構** | **操作對象** | **讀取場景 (Load)** | **寫入場景 (Save)** | **建議抽象 IO 介面** | | --- | --- | --- | --- | | **結構清單 (.dat)** | LoadImgFile 載入以重建樹狀結構 | PageEnd 掃描完成或結構增刪後即時存檔 | ReadTextFile(Path) / WriteTextFile(Path, Content) | | **備註文件 (Memo)** | 進入備註編輯器時載入舊有內容 | WNoteBtnClick 完成編輯後儲存內容 | ReadTextFile(Path) / WriteTextFile(Path, Content) | | **上傳包定義** | N/A | CreateFormID_FormName 產生上傳包所需的定義檔 | WriteTextFile(Path, Content) | | | | | | ### **2. 使用 TImageScrollBox / TDibGraphic 讀寫影像** | **操作對象** | **讀取場景 (Load)** | **寫入場景 (Save)** | **建議抽象 IO 介面** | | --- | --- | --- | --- | | **掃描影像原始檔** | view_image_... 載入顯示於預覽窗 | OnAcquire 將記憶體影像 (Dib) 持久化至磁碟 | ReadBinaryFile(Path) / WriteBinaryFile(Path, Blob) | | **影像處理覆蓋** | ImageReSize_FormID 讀取進行定位縮放 | 處理完成後覆蓋原始影像檔 | ReadBinaryFile(Path) / WriteBinaryFile(Path, Blob) | | **旋轉/後製處理** | 點擊旋轉按鈕後從磁碟重新讀取 | 旋轉完成後保存更新內容回實體檔案 | ReadBinaryFile(Path) / WriteBinaryFile(Path, Blob) | ## **伍、 檔案路徑構造邏輯示例** 程式中典型的路徑構造方式如下(以 PageEnd 為例): // 1. 基礎路徑 ScanPath := ImageSavePath + ScanCaseno + '\'; // 2. 進入特定文件目錄 (DocDir) ScanPath := ScanPath + ScanDocdir + '\'; // 3. 確保實體目錄存在 Str2Dir(ScanPath); // 4. 構造最終影像路徑 PEFileName := ScanPath + Add_Zoo(PageCount, 3) + '_' + FormID + ext; ## **陸、 IO 方法具體實作邏輯細部說明 (Detailed Implementation Logic)** 本節針對 CB_IMGPSScanImp.pas 內部如何透過 Windows API 實作這些 IO 操作進行細部解說。 ### **1. Str2Dir(Path: string) 的運作機制** 這個方法的主要作用是 **「路徑標準化與實體資料夾建立」**。 - **字串處理**:它會先檢查傳入的 Path 字串最後一個字元是否為 \。如果不是,會自動補上。 - **建立資料夾**:**是的,它會在內部建立資料夾**。它會呼叫 Delphi 的 ForceDirectories(Path) 函式。 - **ForceDirectories 的特性**:與一般的 CreateDir 不同,ForceDirectories 會「遞迴」建立路徑中所有不存在的父目錄(例如若路徑為 C:\A\B\C,即使 A 和 B 都不存在,它也會一併建立)。 ### **2. _DelTree(DirName: string) 的遞迴邏輯** 由於 Delphi 早期版本沒有提供單一函式來刪除包含檔案的資料夾,此方法透過 Windows 的搜尋機制實現: - **遍歷搜尋**:使用 FindFirst 和 FindNext 找出資料夾內的所有物件。 - **檔案處理**:若搜尋到的是一般檔案,直接呼叫 DeleteFile。 - **遞迴處理**:若搜尋到的是子資料夾,則「自己呼叫自己」 (_DelTree) 先清空該子資料夾。 - **物理移除**:當資料夾內的所有內容都清空後,呼叫 RemoveDir 將空的資料夾從磁碟移除。 ### **3. FindFirst 與 FindNext 的搜尋與疊代邏輯** 這兩個方法是 Delphi 處理「集合式 IO 操作」(如掃描目錄、批次刪除)的基礎,通常搭配 TSearchRec 結構使用。 - **FindFirst(Path, Attr, SearchRec)**: - **動作**:啟動搜尋任務,將找到的第一個檔案資訊填入 SearchRec。 - **應用例**:在 PM104Click (匯入影像) 時,程式會先執行 FindFirst(FName, faAnyfile, FileRec) 來確認使用者選取的實體檔案是否存在,並藉由 FileRec.Size 取得位元組大小,用以判斷是否超過系統設定的匯入限制(例如 5MB)。 - **FindNext(SearchRec)**: - **動作**:繼續尋找下一個符合條件的檔案。 - **疊代模式**:通常包裹在 repeat ... until FindNext(SR) <> 0 迴圈中。 - **應用例**:在 _DelTree 實作中,程式利用此疊代邏輯將資料夾內「所有的」子目錄與檔案清空,直到 FindNext 回傳非零值(代表搜尋結束)。 - **FindClose(SearchRec)**: - **重要性**:釋放搜尋控制代碼(Handle)。若未呼叫,頻繁搜尋會導致系統資源洩漏。 ### **4. GetNoNameCase(Path: string) 的搜尋邏輯** 這是一個典型的 **「可用名稱查表」** 實作: - **迴圈範圍**:從 1 迭代到 9999。 - **格式組合**:使用 Add_Zoo 函式將數字轉為四位數(如 0001),並加上前綴字「未配號」。 - **磁碟檢查**:呼叫 DirectoryExists 檢查 Path + '未配號000x'。 - **結果回傳**:第一個回傳 False(代表該資料夾尚不存在)的名稱即為結果,程式會立即跳出迴圈並選定此名稱。 ### **5. ReSortFileName(Path: string) 的連續性維護** 為了保證 UI 上顯示的頁碼順序與磁碟檔名一致,此方法採用「先暫存、後改名」的策略: - **防止衝突**:先將目錄內所有符合格式的影像更名為帶有臨時前綴(如 @)的檔名。 - **按序更名**:讀取 Context.dat 內的清單,依照目前的順序,將檔案逐一更名為 001_..., 002_... 等格式。 - **同步索引**:最後將更名後的結果重新寫入 Context.dat。 ### **6. GetLocalAppDir(Handle: HWND) 的權限繞過** 這是一個系統級別的 IO 位置定位方法: - **API 呼叫**:呼叫 SHGetSpecialFolderPath (Windows Shell API)。 - **定位 CSIDL_LOCAL_APPDATA**:指向 C:\Users\使用者名稱\AppData\Local\。 - **必要性**:這能解決 OCX 程式被安裝在 Program Files 下時,因為 UAC 權限無法在安裝目錄寫入 Log 或語言檔的問題。 ### **🎯 移植至 Web 平台的建議** 當您將這些邏輯移植到 Web 時: - Str2Dir 應對應到 **IndexedDB** 的 Object Store 建立或 **File System Access API** 的 getDirectoryHandle(搭配 create: true)。 - FindFirst / FindNext 在 Web 端應對應到 Directory Handle 的 entries() 異步疊代器。 - .dat 序列化文件在 Web 端建議直接轉為 **JSON** 格式存儲於瀏覽器的 localStorage 或 IndexedDB 中。 ## **柒、 設定檔回寫邏輯 (Write-back Logic)** 本節說明專案中哪些 .ini 檔案會由 OCX 主動更新資料及其觸發時機。 | **檔案名稱** | **回寫內容 (Content)** | **觸發時機 (Trigger)** | | --- | --- | --- | | **Scan.ini** | DPI、Duplex、亮度、對比、影像模式 | 掃描前設定變更或啟動掃描時(SaveScanPara)。 | | **CustomDocNo.ini** | 自訂文件名稱與 ID 映射 | 操作員新增自訂分類或呼叫 `DeleteCustomDocDir` 時。 | | **CB_IMGPSScan.ini** | 版本號、伺服器 IP/Port | 自動更新完成後更新版號,或手動修改伺服器設定時。 | ## **捌、 注意事項** - **硬編碼路徑**:ActiveFormCreate 中存在寫死的 C:\Temp\IMGPSScan,適合作為最終保險。 - **權限問題**:作為 ActiveX,這些 IO 操作強烈依賴瀏覽器的「安全站台設定」或 IE 的「保護模式」。使用 GetLocalAppDir 是現代化重構中解決 UAC 權限問題的重要手段。 --- # **TCB_IMGPSScanX OCX 通訊介面與溝通機制分析** 本表條列了 Delphi 宿主環境(ActiveX[ocx]) 與 TCB_IMGPSScanX 元件之間主要的溝通管道、對應方法及其在系統架構中的作用。 ## **壹、 指令與資料交換介面表** | **通訊方法 (Method)** | **通訊方向** | **溝通內容 (Data Payload)** | **具體作用與目的** | | --- | --- | --- | --- | | **SetSQLData** | **宿主 -> OCX** | 以 ` | 或,` 分隔的長字串(如:IP, CaseID, DPI) | | **GetSQLData** | **宿主 <-> OCX** | 指定 Key (如 'PARA_NO') 取得對應 Value | **資料查詢通道**。宿主可詢問 OCX 目前快取清單中的特定數值,用於介面同步或邏輯判斷。 | | **GetSetInf1 ~ 7** | **宿主 <- OCX** | 單一字串值 (如 CaseID, 總張數, 成功標記 'Y') | **狀態出口 (Outlets)**。提供標準化的 7 個出口,讓 JavaScript 或宿主 App 輪詢(Polling)目前的執行結果或掃描統計。 | | **StatrTwainScan** | **宿主 -> OCX** | 無參數 (觸發動作) | **硬體啟動指令**。宿主程式按下「開始掃描」按鈕後呼叫此方法,通知 OCX 喚起 TWAIN 驅動程式。 | | **GetCurrentVersionNo** | **宿主 <- OCX** | 版本字串 (如 '1.0.3.5') | **版本識別**。宿主程式載入 OCX 後第一步通常會呼叫此處,用以比對伺服器版本並決定是否強制更新。 | | **upFile** | **宿主 -> OCX** | 無或部分參數 (觸發網路 IO) | **輸出指令**。通知 OCX 將本機暫存區的檔案(如 ZIP 包)依照先前設定好的路徑與權限上傳至伺服器。 | | **dnFile_Get** | **宿主 -> OCX** | 遠端 Action 關鍵字與本地檔名 | **同步指令**。通知 OCX 向伺服器抓取特定資源(如 XML 規則或語言檔)並存放到指定 IO 路徑。 | | **ActiveFormCreate** | **系統 -> OCX** | 視窗建立事件 | **環境初始化**。當宿主程式實體化 OCX 時,自動觸發此處執行 GetDefScanIni,確保掃描環境就緒。 | | **Property CaseID** | **宿主 <-> OCX** | WideString | **屬性直連**。透過 COM 屬性機制直接設定或讀取案件編號。 | | **Property ScreenSnap** | **宿主 -> OCX** | Boolean | **功能切換**。控制 OCX 是否要在特定時機執行螢幕截圖稽核。 | ## **貳、 溝通機制細部說明** ### **1. 參數化配置 (Config-by-String)** 本專案大量使用 SetSQLData 這種「一條字串走天下」的設計,這在早期 ActiveX 開發中非常流行,優點是規避了 COM 介面頻繁修改 _TLB.pas 的麻煩。您在轉換 TypeScript UI 時,可以將其封裝為一個 JSON 物件,最後再組合成字串傳給這個方法。 ### **2. 狀態回饋 (The Inf1~7 Pattern)** 由於 ActiveX 事件 (Events) 的回傳有時在網頁端不夠穩定,開發者選擇了 GetSetInf 系列。宿主程式通常會在執行完某個長耗時動作(如 upFile)後,連續呼叫 GetSetInf3 (成功標記) 與 GetSetInf6 (錯誤訊息) 來決定下一步 UI 流程。 ### **3. 雙向同步 (Bidirectional Sync)** - **同步設定**:宿主透過 GetDefScanIni 觸發,OCX 內部完成硬體設定。 - **同步資料**:宿主透過 dnFile_Get 觸發,OCX 完成本機與伺服器的檔案 IO 同步。 # **參、描服務抽象層實作** 採用 **「抽象介面層 (Abstraction Layer)」** 配合 **「適配器模式 (Adapter Pattern)」**,處理遺留系統(Legacy System)過渡到現代化架構 ```tsx /** * 掃描服務資料模型定義 * 將原本 OCX 雜亂的字串溝通 (SetSQLData) 結構化 */ export interface ScanConfig { serverIp: string; caseId: string; dpi: number; isColor: boolean; isDuplex: boolean; // ... 其他來自 GetDefScanIni 的參數 } export interface ScanStatus { caseId: string; totalPageCount: number; isUploadSuccess: boolean; lastBarcode: string; errorMessage: string; } /** * 掃描服務抽象介面 (IScanService) * UI 層僅與此介面溝通,不直接操作底層實作 */ export interface IScanService { initialize(config: ScanConfig): Promise; startScan(): Promise; uploadFiles(): Promise; getStatus(): Promise; getVersion(): Promise; downloadResource(action: string, fileName: string): Promise; } /** * 實作 A: ActiveX 適配器 (Legacy Adapter) * 用於目前環境:UI (TS) -> Delphi Browser -> ActiveX (OCX) */ export class ActiveXScanAdapter implements IScanService { private ocx: any; constructor() { // 透過 window.external 或 Delphi Bridge 取得 OCX 實例 this.ocx = (window as any).OCX_INSTANCE; } async initialize(config: ScanConfig): Promise { // 將 JSON 轉回 OCX 慣用的管道分隔字串 (SetSQLData 邏輯) const sqlDataStr = `${config.serverIp}|${config.caseId}|${config.dpi}|${config.isColor ? 'C' : 'G'}`; this.ocx.SetSQLData(sqlDataStr); } async startScan(): Promise { this.ocx.StatrTwainScan(); return true; // 實際狀態需透過 GetSetInf 輪詢 } async getStatus(): Promise { return { caseId: this.ocx.GetSetInf1(), totalPageCount: parseInt(this.ocx.GetSetInf2()), isUploadSuccess: this.ocx.GetSetInf3() === 'Y', lastBarcode: this.ocx.GetSetInf5(), errorMessage: this.ocx.GetSetInf6() }; } async uploadFiles(): Promise { this.ocx.upFile(); return true; } async getVersion(): Promise { return this.ocx.GetCurrentVersionNo(); } async downloadResource(action: string, fileName: string): Promise { this.ocx.dnFile_Get(action, fileName); } } /** * 實作 B: Backend Server 適配器 (Future Adapter) * 用於未來環境:UI (TS) -> REST API / WebSocket -> Rust/Go Backend */ export class BackendServerAdapter implements IScanService { private apiUrl = "http://localhost:8080/api/scanner"; async initialize(config: ScanConfig): Promise { await fetch(`${this.apiUrl}/config`, { method: 'POST', body: JSON.stringify(config) }); } async startScan(): Promise { const res = await fetch(`${this.apiUrl}/scan`, { method: 'POST' }); return res.ok; } async getStatus(): Promise { const res = await fetch(`${this.apiUrl}/status`); return await res.json(); } async uploadFiles(): Promise { const res = await fetch(`${this.apiUrl}/upload`, { method: 'POST' }); return res.ok; } async getVersion(): Promise { const res = await fetch(`${this.apiUrl}/version`); const data = await res.json(); return data.version; } async downloadResource(action: string, fileName: string): Promise { // 未來可能直接由後端處理,前端僅發送同步指令 await fetch(`${this.apiUrl}/sync?action=${action}&file=${fileName}`); } } /** * 服務工廠 (Scanner Factory) * 根據環境自動切換實作 */ export class ScannerFactory { static getService(): IScanService { if ((window as any).OCX_INSTANCE) { console.log("偵測到 ActiveX 環境,啟用 ActiveX 適配器"); return new ActiveXScanAdapter(); } else { console.log("現代瀏覽器環境,啟用後端 API 適配器"); return new BackendServerAdapter(); } } } ``` ### **1. 介面標準化(JSON 化)** 原先 OCX 使用 SetSQLData 的「長字串」溝通是非常脆弱的(一項參數順序錯了就全毀)。在 TypeScript 抽象層中,請務必將其轉換為 **強型別的 JSON 物件**(如 ScanConfig 介面)。這樣你的 UI 邏輯在處理設定時會非常直覺。 ### **2. 非同步處理(Promise/Async)** ActiveX 的方法通常是同步阻塞的,而後端 API 是非同步的。 - **建議**:在抽象介面(IScanService)中,所有方法都回傳 Promise。 - **原因**:這樣當你未來切換到後端伺服器(fetch API)時,UI 的調用邏輯(await service.startScan())不需要做任何修改。 ### **3. 實作「心跳」或「狀態輪詢」** 原本 GetSetInf1~7 主要是靠宿主程式主動去「問」OCX。 - **抽象層建議**:在 IScanService 中實作一個 subscribeStatus(callback) 的觀察者模式。 - **ActiveX 模式下**:在適配器內部啟動一個 setInterval 去輪詢 GetSetInf。 - **未來後端模式下**:適配器改用 **WebSocket** 接收後端主動推播的掃描事件。 - **UI 層感受**:UI 只需要處理 onStatusUpdate 事件,根本不用管底層是用輪詢還是 WebSocket。 ### **4. 資源路徑的轉義(The Bridge Proxy)** 這是我之前提到的關鍵:由於 iframe 裡的 TypeScript 無法存取 C:\Temp,但 ActiveX 依舊會把檔案掃到那裡。 - **策略**:你的 Delphi Browser 需要實作一個簡單的本地 Proxy。 - **實作**:當 TypeScript 需要顯示圖檔時,它請求 http://local.bridge/view/001.tif,Delphi Browser 攔截此請求並從實體路徑讀取檔案回傳。 - **好處**:未來改為 Backend Server 時,Server 也可以提供同樣的 URL 格式,UI 完全無縫接軌。 ## **肆、 Delphi 與 JavaScript 通訊橋樑 (Bridge) 實作說明** 當您在 Delphi 宿主程式中使用內嵌瀏覽器時,JavaScript 取得 `OCX_INSTANCE` 的方式取決於底層的瀏覽器引擎: ### **1. 舊版 IE 核心 (TWebBrowser / window.external)** 這是最傳統的方式,Delphi 會透過實作 `IDocHostUIHandler` 介面,將一個自訂的 `IDispatch` 物件掛載到 `window.external` 上。 - **Delphi 端**:將 `TCB_IMGPSScanX` 實例或其包裝類別指派給瀏覽器的 `External` 屬性。 - **JS 端**:直接使用 `window.external.StatrTwainScan()` 呼叫。 ### **2. 新版 Edge 核心 (TEdgeBrowser / WebView2 Host Objects)** 這是現代化的做法。Delphi 透過 WebView2 的 API 將物件直接「注入」到 JS 的命名空間中。 - **Delphi 端執行**: ```pascal // 將 OCX 物件以 'OCX_INSTANCE' 名稱注入 EdgeBrowser.AddHostObjectToScript('OCX_INSTANCE', MyOcxWrapperObject); ``` - **JS 端取得**: WebView2 會將物件放置在 `window.chrome.webview.hostObjects` 底下。 ```jsx const ocx = window.chrome.webview.hostObjects.OCX_INSTANCE; ``` ### **3. 為什麼需要 Wrapper (包裝類別)?** 通常我們**不建議**直接將 `TCB_IMGPSScanX` 注入,原因如下: 1. **安全性**:直接暴露 COM 元件可能會有安全漏洞。 2. **相容性**:OCX 的某些方法回傳型別(如 Delphi 的自訂 Enum)JavaScript 無法識別。 3. **執行緒優化**:Delphi 可以在 Wrapper 中使用 `TThread.Queue` 確保掃描指令在正確的執行緒執行,避免瀏覽器 UI 凍結。 ### **4. 溝通路徑總結** 1. **Delphi 宿主程式** 啟動並實體化 **OCX 控制項**。 2. **Delphi 宿主程式** 建立 **Browser 物件** 並載入 **TypeScript UI (HTML)**。 3. **Delphi 宿主程式** 呼叫瀏覽器 API(如 `AddHostObjectToScript`),將 OCX 的操作介面「注入」到該網頁的 `window` 物件中。 4. **TypeScript** 透過抽象層 (Adapter) 取得該物件,實現「網頁點按鈕,掃描機開動」的效果。 --- # Transport 相關套件分析 --- # Image Processor 相關套件分析