編輯 | 究查 | 歷程 | 原始

核心源碼與物件實作解析

本文件針對 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) 都是對這個物件進行操作。