# **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; } /** * 掃描服務抽象介面 (IOCXService) * UI 層僅與此介面溝通,不直接操作底層實作 */ export interface IOCXService { 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 IOCXService { 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 IOCXService { 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(): IOCXService { 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 是非同步的。 - **建議**:在抽象介面(IOCXService)中,所有方法都回傳 Promise。 - **原因**:這樣當你未來切換到後端伺服器(fetch API)時,UI 的調用邏輯(await service.startScan())不需要做任何修改。 ### **3. 實作「心跳」或「狀態輪詢」** 原本 GetSetInf1~7 主要是靠宿主程式主動去「問」OCX。 - **抽象層建議**:在 IOCXService 中實作一個 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) 取得該物件,實現「網頁點按鈕,掃描機開動」的效果。