編輯 | 究查 | 歷程 | 原始

相依套件分析報告

本報告針對 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;
    ```
  1. TFileStream:**檔案串流**。用來直接對硬碟讀寫資料。當確認影像無誤後,會透過 TFileStream 將 TMemoryStream 的內容真實地寫入 C:\Temp...\001.tif 中。

5) TCB_IMGPSScanX.FindISB2View

  • 功能描述:用來在畫面上尋找並更新用來預覽圖片的元件。
  • 名詞解釋:**ISB 代表 ImageScrollBox**。

這對應到 uses 區段中的 EnImgScr 套件。TImageScrollBox 是 Envision 提供的一個 UI 元件,專門用來顯示超出螢幕大小的掃描圖檔,支援平滑拖曳與縮放。這個方法通常用來把剛掃描完的 DibHandle 餵給畫面上的 ISB 元件顯示。

6) TCB_IMGPSScanX.GetNoNameCase

  • 功能描述:處理「無案號」或「未知歸屬」的掃描件。
  • 源碼實作逐行說明

此方法透過迴圈尋找一個尚未被使用的目錄名稱(格式為「未配號 + 四位數字」)。

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)過渡到現代化架構

/**
 * 掃描服務資料模型定義
 * 將原本 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<void>;
  startScan(): Promise<boolean>;
  uploadFiles(): Promise<boolean>;
  getStatus(): Promise<ScanStatus>;
  getVersion(): Promise<string>;
  downloadResource(action: string, fileName: string): Promise<void>;
}

/**
 * 實作 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<void> {
    // 將 JSON 轉回 OCX 慣用的管道分隔字串 (SetSQLData 邏輯)
    const sqlDataStr = `${config.serverIp}|${config.caseId}|${config.dpi}|${config.isColor ? 'C' : 'G'}`;
    this.ocx.SetSQLData(sqlDataStr);
  }

  async startScan(): Promise<boolean> {
    this.ocx.StatrTwainScan();
    return true; // 實際狀態需透過 GetSetInf 輪詢
  }

  async getStatus(): Promise<ScanStatus> {
    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<boolean> {
    this.ocx.upFile();
    return true;
  }

  async getVersion(): Promise<string> {
    return this.ocx.GetCurrentVersionNo();
  }

  async downloadResource(action: string, fileName: string): Promise<void> {
    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<void> {
    await fetch(`${this.apiUrl}/config`, {
      method: 'POST',
      body: JSON.stringify(config)
    });
  }

  async startScan(): Promise<boolean> {
    const res = await fetch(`${this.apiUrl}/scan`, { method: 'POST' });
    return res.ok;
  }

  async getStatus(): Promise<ScanStatus> {
    const res = await fetch(`${this.apiUrl}/status`);
    return await res.json();
  }

  async uploadFiles(): Promise<boolean> {
    const res = await fetch(`${this.apiUrl}/upload`, { method: 'POST' });
    return res.ok;
  }

  async getVersion(): Promise<string> {
    const res = await fetch(`${this.apiUrl}/version`);
    const data = await res.json();
    return data.version;
  }

  async downloadResource(action: string, fileName: string): Promise<void> {
    // 未來可能直接由後端處理,前端僅發送同步指令
    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 端執行

    // 將 OCX 物件以 'OCX_INSTANCE' 名稱注入
    EdgeBrowser.AddHostObjectToScript('OCX_INSTANCE', MyOcxWrapperObject);
    
  • JS 端取得: WebView2 會將物件放置在 window.chrome.webview.hostObjects 底下。

    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 相關套件分析