# **核心源碼與物件實作解析** 本文件針對 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) 都是對這個物件進行操作。