本報告針對 ActiveX 元件核心實作檔 CB_IMGPSScanImp.pas 進行功能解構,並分析其與第三方套件的綁定關係,以作為後續維護或現代化升級之參考。
根據原始碼結構,CB_IMGPSScanImp.pas 是一個將介面、硬體、網路與業務邏輯高度耦合的「上帝物件 (God Object)」。若要進行重構或拆件,建議依據以下五個核心模組進行解耦:
為達成上述複雜的業務需求,本專案引入了多個 Delphi 著名的第三方套件。以下為各套件的大致功能說明:
這是一套專為 Delphi 設計的高效能影像與 TWAIN 處理套件,在金融與保險業的文件電子化系統中非常常見。
由 EldoS (後被 /n software 收購) 開發的強大安全通訊與加密套件。由於早期 Delphi (D7~D2007) 內建網路庫缺乏完善的 SSL/TLS 支援,此套件被廣泛用於補強安全性。
本文件針對 CB_IMGPSScanImp.pas 中的核心方法與隱藏物件進行原始碼級別的邏輯拆解與推導。
在 Delphi 中,若要動態讀取編譯進 OCX 的 VS_VERSION_INFO 資源,標準作法是呼叫一系列的 Windows API。這段實作通常包含以下三個關鍵概念:
取得檔案路徑後,讀取版本資訊會經過三部曲,而 VerQueryValue 是最後負責「解析」的關鍵:
它會負責初始化全域變數、設定第三方元件的預設值、並建立暫存資料夾。
```pascal
procedure TCB_IMGPSScanX.ActiveFormCreate(Sender: TObject);
begin
// 1. 預設變數初始化
Def_DeviceDelete := True;
Def_ScannerReverse := False;
// 2. 建立本機暫存目錄
ForceDirectories('C:\Temp\IMGPSScan');
// 3. UI 語系初始化
InitialLanguage;
end;
```
這個方法會讀取網頁傳進來的參數(如 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;
```
源碼邏輯推導:
```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;三大核心資料物件解釋:
TMemoryStream:**記憶體串流**。它的作用像是一塊存在 RAM 裡的虛擬硬碟。掃描後的圖檔在還沒存到實體硬碟前,會先放在這裡進行格式轉換 (如轉成 JPEG) 或進行 MD5 計算,因為在記憶體中操作速度最快。
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這對應到 uses 區段中的 EnImgScr 套件。TImageScrollBox 是 Envision 提供的一個 UI 元件,專門用來顯示超出螢幕大小的掃描圖檔,支援平滑拖曳與縮放。這個方法通常用來把剛掃描完的 DibHandle 餵給畫面上的 ISB 元件顯示。
此方法透過迴圈尋找一個尚未被使用的目錄名稱(格式為「未配號 + 四位數字」)。
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;
這兩個方法是 OCX 與網頁 (JS) 溝通的雙向橋樑。
源碼實作解析:
程式一開始會先寫死一組標準值,例如 Def_ScanDpi := 300 (預設 300 DPI)、Def_DeviceDeleteSize := 3072 (空白頁判定門檻為 3KB)。
透過 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 的實作,這是因為它們屬於以下兩種情況:
本報告詳列了 CB_IMGPSScanImp.pas 檔案中定義的所有 IO 相關變數,以及執行目錄與檔案管理的關鍵方法,並定義了抽像化所需的介面原型。
程式中定義了多個全域或私有欄位,用來追蹤檔案在硬碟上的實體位置。
| 變數名稱 | 類型 | 組合邏輯 (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 伺育器上存放影像檔的起始位址。 |
| 變數名稱 | 類型 | 用途說明 |
|---|---|---|
| ScanSaveFilename | String | 下一張掃描影像預定的檔名(通常包含 FormID)。 |
| PEFileName | String | 在 PageEnd 階段確定的最終完整檔案路徑。 |
| AttName | String | 附件目錄名稱。根據 FIs_In_Wh 決定為 Attach 或 S_Attach。 |
| Ext | String | 副檔名。預設為 .tif,彩色/灰階模式下可能切換為 .jpg。 |
以下為需要進行抽像化設計的核心 IO 方法。
| 方法名稱 | 作用 | 應用場景 | 建議抽象 IO 介面 |
|---|---|---|---|
| _DelTree | 遞迴刪除整個目錄樹 | 上傳成功後清空暫存、模式切換初始化目錄 | RemoveDirRecursive(Path) | | Str2Dir / ForceDirectories | 確保路徑以 \ 結尾並建立路徑 | 初始化或掃描前建立資料夾 | EnsureDirExists(Path) | | GetNoNameCase | 搜尋尚未配號的臨時目錄 | 在本機搜尋如 未配號0001 到 9999 的可用路徑 | FindNextAvailablePath(Pattern) | | GetLocalAppDir | 取得 Windows %LocalAppData% 路徑 | 確保在現代 Windows 系統中有權限寫入語言檔與 Log | GetBaseStoragePath() | |
| 方法名稱 | 作用 | 應用場景 | 建議抽象 IO 介面 |
|---|---|---|---|
| FileExists | 檢查指定路徑檔案是否存在 | 在載入 .dat、.ini 或影像前進行安全檢查 | IsFilePresent(Path) |
| DirectoryExists | 檢查指定路徑資料夾是否存在 | 在建立掃描目錄或上傳前確認結構完整性 | IsDirPresent(Path) |
| FindFirst / FindNext | 搜尋符合特定格式的檔案 | 取得實體檔案大小 (FileRec.Size) 或遞迴搜尋檔案 | ListFiles(Path, Pattern) / GetFileInfo(Path) |
| 方法名稱 | 作用 | 應用場景 | 建議抽象 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) |
| 方法名稱 | 作用 | 應用場景 | 建議抽象 IO 介面 |
|---|---|---|---|
| DeleteCustomDocDir | 移除自訂文件定義 | 從 CustomDocNo.ini 中移除特定自訂文件的節點 | Storage.IniDeleteSection(Section) |
專案中使用 dnFile_Get 從伺服器端同步必要的環境檔案或規則定義。
| 抓取對象 (目標路徑) | 遠端請求 Action | 用途說明 | |
|---|---|---|---|
| CheckXmlPath + filename | GetCheckXml |
下載 OMR 檢核定義檔。同步伺服器端最新的 XML 座標規則,用於自動判斷影像區域。 | https |
| SamplePath + filename | GetSampleImg |
下載範本影像。用於在 UI 介面上提供給操作員對照的標準範例圖檔。 | https |
| SitePath + filename | GetSiteImg |
下載定位定義圖。下載與登打、校對位置相關的輔助影像。 | https |
| LngPath + 'Language.ini' | GetLanguage |
同步多國語言檔。確保用戶端介面文字(繁體、英文、越南語等)與伺服器同步。 | https |
專案中使用了大量的 .dat 與 .ini 檔案來記錄目錄內的結構資訊與使用者偏好。
| 檔案名稱 | 組合路徑邏輯 (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。 |
| 檔案名稱 | 路徑邏輯 | 回寫 (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 不自行修改。 |
源碼中頻繁使用 .LoadFromFile 與 .SaveToFile 來實現資料的持久化。
| 操作對象 | 讀取場景 (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) |
| 操作對象 | 讀取場景 (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;
本節針對 CB_IMGPSScanImp.pas 內部如何透過 Windows API 實作這些 IO 操作進行細部解說。
這個方法的主要作用是 「路徑標準化與實體資料夾建立」。
由於 Delphi 早期版本沒有提供單一函式來刪除包含檔案的資料夾,此方法透過 Windows 的搜尋機制實現:
這兩個方法是 Delphi 處理「集合式 IO 操作」(如掃描目錄、批次刪除)的基礎,通常搭配 TSearchRec 結構使用。
這是一個典型的 「可用名稱查表」 實作:
為了保證 UI 上顯示的頁碼順序與磁碟檔名一致,此方法採用「先暫存、後改名」的策略:
這是一個系統級別的 IO 位置定位方法:
當您將這些邏輯移植到 Web 時:
本節說明專案中哪些 .ini 檔案會由 OCX 主動更新資料及其觸發時機。
| 檔案名稱 | 回寫內容 (Content) | 觸發時機 (Trigger) |
|---|---|---|
| Scan.ini | DPI、Duplex、亮度、對比、影像模式 | 掃描前設定變更或啟動掃描時(SaveScanPara)。 |
| CustomDocNo.ini | 自訂文件名稱與 ID 映射 | 操作員新增自訂分類或呼叫 DeleteCustomDocDir 時。 |
| CB_IMGPSScan.ini | 版本號、伺服器 IP/Port | 自動更新完成後更新版號,或手動修改伺服器設定時。 |
本表條列了 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 是否要在特定時機執行螢幕截圖稽核。 |
本專案大量使用 SetSQLData 這種「一條字串走天下」的設計,這在早期 ActiveX 開發中非常流行,優點是規避了 COM 介面頻繁修改 _TLB.pas 的麻煩。您在轉換 TypeScript UI 時,可以將其封裝為一個 JSON 物件,最後再組合成字串傳給這個方法。
由於 ActiveX 事件 (Events) 的回傳有時在網頁端不夠穩定,開發者選擇了 GetSetInf 系列。宿主程式通常會在執行完某個長耗時動作(如 upFile)後,連續呼叫 GetSetInf3 (成功標記) 與 GetSetInf6 (錯誤訊息) 來決定下一步 UI 流程。
採用 「抽象介面層 (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();
}
}
}
原先 OCX 使用 SetSQLData 的「長字串」溝通是非常脆弱的(一項參數順序錯了就全毀)。在 TypeScript 抽象層中,請務必將其轉換為 強型別的 JSON 物件(如 ScanConfig 介面)。這樣你的 UI 邏輯在處理設定時會非常直覺。
ActiveX 的方法通常是同步阻塞的,而後端 API 是非同步的。
原本 GetSetInf1~7 主要是靠宿主程式主動去「問」OCX。
這是我之前提到的關鍵:由於 iframe 裡的 TypeScript 無法存取 C:\Temp,但 ActiveX 依舊會把檔案掃到那裡。
當您在 Delphi 宿主程式中使用內嵌瀏覽器時,JavaScript 取得 OCX_INSTANCE 的方式取決於底層的瀏覽器引擎:
這是最傳統的方式,Delphi 會透過實作 IDocHostUIHandler 介面,將一個自訂的 IDispatch 物件掛載到 window.external 上。
TCB_IMGPSScanX 實例或其包裝類別指派給瀏覽器的 External 屬性。window.external.StatrTwainScan() 呼叫。這是現代化的做法。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;
通常我們**不建議**直接將 TCB_IMGPSScanX 注入,原因如下:
TThread.Queue 確保掃描指令在正確的執行緒執行,避免瀏覽器 UI 凍結。AddHostObjectToScript),將 OCX 的操作介面「注入」到該網頁的 window 物件中。