MFC 為了在 C++ 靜態型別系統上構建動態的視窗物件模型,引入了兩大映射機制:
HWND ↔ CWnd*、HGDIOBJ ↔ CGdiObject* 等 —— 確保同一執行緒內「一個句柄對應一個物件」。本文剖析 MFC 原始碼關鍵片段,揭示其運作原理、快取優化與多執行緒陷阱。
struct AFX_MSGMAP
{
const AFX_MSGMAP* pBaseMap; // 基類對映表指標
const AFX_MSGMAP_ENTRY* lpEntries; // 對映項目陣列
// ... (DLL 版本使用 pfnGetBaseMap 函數指標)
};
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; // 訊息 ID (WM_XXX)
UINT nCode; // 控制項通知碼 (BN_CLICKED 等)
UINT nID; // 控制項 ID 範圍起始
UINT nLastID; // 控制項 ID 範圍結束
UINT nSig; // 簽名編碼 (參數類型、回傳類型)
AFX_PMSG pfn; // 處理函數指標
};
CWnd::OnWndMsg 核心邏輯 (簡化):
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
const AFX_MSGMAP* pMessageMap = GetMessageMap();
// ---- 快取查找 (最近一次命中) ----
UINT iHash = (LOWORD((DWORD_PTR)pMessageMap) ^ message) & (iHashMax - 1);
AfxLockGlobals(CRIT_WINMSGCACHE);
AFX_MSG_CACHE* pMsgCache = &_afxMsgCache[iHash];
if (message == pMsgCache->nMsg && pMessageMap == pMsgCache->pMessageMap)
{
// 快取命中
const AFX_MSGMAP_ENTRY* lpEntry = pMsgCache->lpEntry;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
if (lpEntry) goto Dispatch; // 找到處理函數
return FALSE; // 快記錄為「無對映」
}
// ---- 慢速路徑:遍歷對映表鏈 ----
pMsgCache->nMsg = message;
pMsgCache->pMessageMap = pMessageMap;
for (; pMessageMap; pMessageMap = pMessageMap->pBaseMap) // 或 pfnGetBaseMap()
{
// 防呆:避免循環繼承 BEGIN_MESSAGE_MAP(CMyWnd, CMyWnd)
ASSERT(pMessageMap != pMessageMap->pBaseMap);
if (message < 0xC000) // 標準視窗訊息
{
if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, message, 0, 0)) != NULL)
{
pMsgCache->lpEntry = lpEntry;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
goto Dispatch;
}
}
else // 已註冊訊息 (RegisterWindowMessage)
{
for (lpEntry = pMessageMap->lpEntries;
(lpEntry = AfxFindMessageEntry(lpEntry, 0xC000, 0, 0)) != NULL;
lpEntry++)
{
UINT* pnID = (UINT*)(lpEntry->nSig);
if (*pnID == message)
{
pMsgCache->lpEntry = lpEntry;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
goto DispatchRegistered;
}
}
}
}
// 完全找不到
pMsgCache->lpEntry = NULL;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
return FALSE;
Dispatch:
// 依簽名解碼參數並呼叫 pfn ...
}
| 特性 | 說明 |
|---|---|
| 查找策略 | 線性掃描 (非雜湊表),依訊息 ID 比對 |
| 繼承支援 | 鏈式遍歷基類對映表,衍生類優先 |
| 快取機制 | 單一全域雜湊表 _afxMsgCache,以 (MapPtr ^ MsgID) % Size 索引 |
| 執行緒安全 | 全域關鍵區 CRIT_WINMSGCACHE 保護快取讀寫 |
| 優化建議 | 常用訊息對映項目前置;避免大量動態註冊訊息 |
每個執行緒擁有獨立映射表 (TLS 儲存):
afxMapHWND():永久映射 (CWnd::FromHandlePermanent)。afxMapHWND() 臨時映射 (CWnd::FromHandle) —— GetDlgItem、SelectObject 回傳的臨時句柄包裝。// 取得父框架視窗
CFrameWnd* CWnd::GetParentFrame() const
{
if (GetSafeHwnd() == NULL) return NULL;
ASSERT_VALID(this);
CWnd* pParentWnd = GetParent(); // 向上遍歷父視窗
while (pParentWnd != NULL)
{
if (pParentWnd->IsFrameWnd()) // 虛擬函數判斷
return (CFrameWnd*)pParentWnd;
pParentWnd = pParentWnd->GetParent();
}
return NULL;
}
inline CWnd* CWnd::GetParent() const
{
ASSERT(::IsWindow(m_hWnd));
return CWnd::FromHandle(::GetParent(m_hWnd)); // API → 句柄映射 → 物件
}
LRESULT CALLBACK AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
if (nMsg == WM_QUERYAFXWNDPROC) return 1; // 識別標記
// 關鍵:從永久映射取得 CWnd*
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
ASSERT(pWnd != NULL);
ASSERT(pWnd->m_hWnd == hWnd);
if (pWnd == NULL || pWnd->m_hWnd != hWnd)
return ::DefWindowProc(hWnd, nMsg, wParam, lParam);
return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam); // 進入訊息對映
}
效能代價: 每個訊息到達皆需執行FromHandlePermanent查找映射表 (TLS 讀取 + 雜湊查找)。高頻訊息 (WM_MOUSEMOVE,WM_TIMER) 累積顯著。
同一 HWND 在不同執行緒查找可能得到不同 CWnd* (或 NULL),因映射表儲存於 Thread Local Storage (TLS)。
HWND → CWnd*。CWnd::FromHandle(hWnd) → 該執行緒映射表無紀錄 → 產生臨時物件 或 NULL。解決方案: 參考 KB Q147578 —— UI 物件專屬於建立執行緒,跨執行緒操作必須 PostMessage/SendMessage 封送。
| 映射類型 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 條件查找映射 | 動態分發、支援繼承覆寫、節省 vtable 空間 | 查找開銷、全域鎖爭用、除錯困難 | 訊息/命令/介面分發 |
| 句柄映射 | 封裝 C API、確保 1:1 綁定、支援臨時包裝 | TLS 隔離、每訊息查找開銷、多執行緒陷阱 | HWND/HGDI/菜單 物件化 |
DependencyObject 內建查找機制。MFC 的雙重映射機制是「C++ 封裝 C API」歷史妥協下的精妙產物:以空間換靈活性、以快取換速度、以 TLS 換執行緒隔離。理解其運作細節,不僅能避開多執行緒坑、優化高頻訊息路徑,更能啟發現代框架設計中「物件身分識別與分發機制」的權衡思考。