本表條列了 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 物件中。