編輯 | 究查 | 歷程 | 原始

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) 取得該物件,實現「網頁點按鈕,掃描機開動」的效果。