ActiveX 控制項跨容器挑戰:標準不等於相容
雖然 ActiveX 控制項實作標準 OLE 介面 (IOleObject, IOleControl, IDispatch...),但**容器行為差異巨大**:VB6、VC++、IE、Office、Delphi、C# 各有獨特的載入時機、授權檢查、執行緒模型、訊息迴圈整合、鍵盤焦點管理。未處理差異會導致控制項在特定容器**無法載入、授權失效、死鎖、鍵盤失效、資源洩漏**。
策略一:授權機制靈活配置
設計時 vs 執行時授權
- 授權控制項:包含
IClassFactory2、ILicenseProvider,.LIC 檔驗證。僅授權使用者可在**設計時**放入容器 (VB/VC 設計模式)。
- 非授權控制項:任何人可設計時使用,適合免費、內部、網頁部署。
AppWizard 選項與 .LIC 檔
- 建立專案勾選 **"Control requires a license"** → 自動生成
VerifyUserLicense、GetLicenseKey、.LIC 檔模板。
- 修改
_szLicString[] 為唯一驗證字串 (GUID 建議)。
網頁部署授權難題:LPK 工具與樂觀授權
IE 載入授權控制項需 **.LPK 檔** (License Package):**
- 用
LPK_TOOL.EXE (IE SDK) 將 .LIC 加密為 .LPK。
- HTML 中
<PARAM NAME="LPKPath" VALUE="ctrl.lpk">。
- IE 讀取 .LPK 驗證授權。
替代方案:樂觀授權 — 覆寫 COleControl::GetControlFlags 移除 requiresLicense,或實作 IOleObject::GetUserClassID 等繞過。更穩健:**覆寫 AmbientUserMode 查詢**:
BOOL CMyCtrl::OptimisticAmbientUserMode()
{
BOOL bUserMode = TRUE; // 預設為執行模式 (允許)
// 嘗試查詢容器 Ambient UserMode
if (!GetAmbientProperty(DISPID_AMBIENT_USERMODE, VT_BOOL, &bUserMode)) {
// 容器不回應 (如 IE) → 樂觀假設為執行模式
bUserMode = TRUE;
}
return bUserMode; // TRUE=執行模式(允許), FALSE=設計模式(需授權)
}
在 VerifyUserLicense 中呼叫此函數,若為執行模式直接返回 TRUE。
策略二:執行緒模型選擇與資源共享
單執行緒 (Single-threaded) vs 單元模型
| 模型 | 註冊表標記 | 特性 | 適用場景 |
| 單執行緒 | ThreadingModel = "Apartment" (或無) | 所有實例同一 STA 執行緒,序列化存取 | 簡單控制項、非視覺元件、VB6 相容佳 |
| 單元模型 | ThreadingModel = "Free" | 任意執行緒可直接呼叫,**需執行緒安全** | 高效能、多執行緒容器 (IE、C#)、免序列化開銷 |
MFC COleControl 預設**單執行緒**。改為單元模型需:
1. 專案屬性 → Linker → **Register Output** → 勾選。
2. 覆寫 CMyCtrl::GetControlFlags() 返回含 threadingModelFree (或註冊表寫入 ThreadingModel = "Free")。
3. **所有成員變數存取、資源操作必須加鎖** (CCriticalSection)。
全域資源共享:單元模型下的同步挑戰
場景:多實例共享資料庫連線、硬體裝置句柄、GDI 物件。
// 靜態共享資源 + 關鍵區段
class CMyCtrl : public COleControl
{
static CSharedResource* s_pResource;
static CCriticalSection s_csResource;
static CSharedResource* GetResource()
{
CSingleLock lock(&s_csResource, TRUE);
if (!s_pResource) s_pResource = new CSharedResource;
return s_pResource;
}
static void ReleaseResource()
{
CSingleLock lock(&s_csResource, TRUE);
if (s_pResource && --s_pResource->m_nRefCount == 0) {
delete s_pResource; s_pResource = nullptr;
}
}
};
關鍵:引用計數 + 關鍵區段保護建立/釋放。單元模型下**兩個執行緒可能同時呼叫 GetResource**,無鎖會導致重複建立或洩漏。
策略三:內容驗證
容器儲存控制項狀態時 (IPersistStream/Storage/PropertyBag),可能呼叫 IPersistPropertyBag::Load 載入屬性。惡意/損壞資料可能導致崩潰。
- 覆寫
DoPropExchange (MFC) 或 IPersistPropertyBag::Load,**驗證每個屬性值範圍、型別、字串長度**。
- 捕獲例外,返回
E_FAIL 而非崩潰。
- 版本相容:
m_dwVersion 機制處理舊版本資料升級。
策略四:鍵盤導航與焦點管理
容器鍵盤訊息分發機制不同:VB6 容器轉發 WM_GETDLGCODE、IE 需 IOleInPlaceActiveObject::TranslateAccelerator。
- 對話框式控制項 (Child/Control 樣式):**必須實作
IOleInPlaceActiveObject**,MFC COleControl 已實作,但需確保對話框範本 **More Styles → Control = True**。
- Tab/Enter/Esc 鍵:覆寫
PreTranslateMessage 或 OnGetDlgCode 返回 DLGC_WANTALLKEYS | DLGC_WANTARROWS | DLGC_WANTTAB。
- 焦點進入/離開:處理
WM_SETFOCUS/WM_KILLFOCUS,呼叫 IOleInPlaceSite::OnInPlaceActivate/Deactivate 通知容器。
策略五:其他容器差異清單
| 容器 | 特殊行為 | 應對 |
| Internet Explorer | 不查詢 AmbientUserMode (永遠設計模式)、安全性區域限制、無訊息迴圈整合 | 樂觀授權、IObjectSafety、非同步載入、避免阻塞 UI |
| Visual Basic 6 | 設計時完整載入控制項、事件連接點早期建立、授權嚴格 | 正確實作授權、設計時不執行耗時操作 |
| Visual C++ / MFC | 測試容器、包裝類生成、動態建立 | 標準流程驗證 |
| C# / .NET (AxHost) | RCW 包裝、事件轉換、執行緒封送 | 單元模型免封送、介面簽名符合 COM Interop 規範 |
| Office (Word/Excel) | 文檔儲存時序列化、印刷預覽模式 | 正確 IPersistStream/Storage、處理 m_lpfnCloseProc |
測試矩陣:最小驗證集
- VB6 設計時放入、設定屬性、執行時事件。
- VC++ 測試容器:插入、方法調用、事件日誌、屬性頁。
- IE 網頁:
<OBJECT> 載入、腳本調用方法/屬性、事件綁定、跨域安全性。
- C# WinForms:工具箱加入、拖放表單、事件處理、屬性網格。
- 授權測試:未授權機器設計時放入 (應失敗)、授權機器設計/執行 (應成功)、網頁 .LPK 載入。
- 執行緒測試:多執行緒並發調用方法 (單元模型)、STA 容器調用 (單執行緒)。
總結
開發「一次編寫、到處運行」的 ActiveX 控制項,核心在於:**授權策略靈活 (LPK/樂觀授權)、執行緒模型明確選擇並做好同步 (單元模型+鎖)、資源共享引用計數化、屬性載入防禦性驗證、鍵盤焦點標準化實作、針對主流容器差異建立測試矩陣**。這不僅是技術實作,更是對 COM/OLE 生態系統深度理解的體現。
- 上一篇文章: MFC 控制項顏色自訂完全指南:父視窗 OnCtlColor vs 訊息反射 CtlColor 兩大方案深度對比
- 下一篇文章: CImageList 顏色失真問題:橙色變黃色的根因與 LoadResourceBitmap 完美解決方案
Copyright ©
程式碼之家 All Rights Reserved