子類化 是在視窗建立之後,將自訂的視窗程序 (Window Procedure) 插入到原始視窗程序之前,形成呼叫鏈:
系統訊息 → 新視窗程序 (攔截/修改/丟棄) → CallWindowProc → 原視窗程序 → 回傳結果
三種模式:
// C++ 繼承:編譯期多型
class Parent { public: virtual void Func() { cout << "Parent"; } };
class Child : public Parent { public: void Func() override { cout << "Child"; } };
// 視窗子類化:執行期攔截
// 原視窗程序處理 WM_CHAR → 允許所有字元
// 新視窗程序攔截 WM_CHAR → 過濾非數字/小數點/退格 → CallWindowProc 原程序
BOOL CMyDialog::OnInitDialog()
{
CDialog::OnInitDialog();
// 取得編輯框控制項 HWND
HWND hEdit = GetDlgItem(IDC_EDIT_FLOAT)->GetSafeHwnd();
// 保存原視窗程序指標,設定新視窗程序
// GWL_WNDPROC 索引對應視窗程序指標
m_OldEditProc = (WNDPROC)SetWindowLongPtr(hEdit, GWLP_WNDPROC, (LONG_PTR)FloatEditProc);
// 若為 32 位專案亦可用 SetWindowLong
// m_OldEditProc = (WNDPROC)SetWindowLong(hEdit, GWL_WNDPROC, (LONG)FloatEditProc);
return TRUE;
}
注意: 64 位元專案必須使用SetWindowLongPtr/GetWindowLongPtr與GWLP_WNDPROC,避免指標截斷。
// 需為靜態函數或全域函數,呼叫慣例 CALLBACK (stdcall)
LRESULT CALLBACK FloatEditProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// 僅攔截字元輸入訊息
if (uMsg == WM_CHAR)
{
// 允許:數字 0-9、小數點 '.'、退格鍵 VK_BACK (0x08)
// wParam 為字元碼
if (wParam != '.' && (wParam < '0' || wParam > '9') && wParam != VK_BACK)
{
MessageBeep(MB_ICONASTERISK); // 提示音
return 0; // 丟棄訊息,不傳遞給原程序
}
}
// 其他訊息或合法字元:轉交原視窗程序
return CallWindowProc(g_OldEditProc, hWnd, uMsg, wParam, lParam);
}
void CMyDialog::OnDestroy()
{
if (m_OldEditProc)
{
HWND hEdit = GetDlgItem(IDC_EDIT_FLOAT)->GetSafeHwnd();
SetWindowLongPtr(hEdit, GWLP_WNDPROC, (LONG_PTR)m_OldEditProc);
m_OldEditProc = NULL;
}
CDialog::OnDestroy();
}
LRESULT CALLBACK LinkStaticProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
static HCURSOR hHandCursor = LoadCursor(NULL, IDC_HAND);
static HFONT hUnderlineFont = NULL;
static COLORREF clrBlue = RGB(0, 0, 255);
static COLORREF clrDefault = GetSysColor(COLOR_WINDOWTEXT);
switch (uMsg)
{
case WM_SETCURSOR:
if (LOWORD(lParam) == HTCLIENT)
{
SetCursor(hHandCursor);
return TRUE;
}
break;
case WM_CTLCOLORSTATIC:
{
HDC hdc = (HDC)wParam;
SetTextColor(hdc, clrBlue);
SetBkMode(hdc, TRANSPARENT);
if (!hUnderlineFont)
{
LOGFONT lf; GetObject(GetStockObject(DEFAULT_GUI_FONT), sizeof(lf), &lf);
lf.lfUnderline = TRUE;
hUnderlineFont = CreateFontIndirect(&lf);
}
SelectObject(hdc, hUnderlineFont);
return (LRESULT)GetStockObject(HOLLOW_BRUSH);
}
case WM_LBUTTONUP:
ShellExecute(NULL, _T("open"), _T("https://example.com"), NULL, NULL, SW_SHOWNORMAL);
break;
case WM_DESTROY:
if (hUnderlineFont) DeleteObject(hUnderlineFont);
break;
}
return CallWindowProc(g_OldStaticProc, hWnd, uMsg, wParam, lParam);
}
MFC 提供物件導向封裝,避免手動管理函數指標:
// 1. 定義衍生類
class CFloatEdit : public CEdit
{
DECLARE_MESSAGE_MAP()
afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
};
BEGIN_MESSAGE_MAP(CFloatEdit, CEdit)
ON_WM_CHAR()
END_MESSAGE_MAP()
void CFloatEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
if (nChar != '.' && (nChar < '0' || nChar > '9') && nChar != VK_BACK)
{
MessageBeep(MB_ICONASTERISK);
return; // 不呼叫基類,等同丟棄訊息
}
CEdit::OnChar(nChar, nRepCnt, nFlags); // 合法字元交給基類
}
// 2. 對話方塊中動態子類化
void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_FLOAT, m_FloatEdit); // 成員變數 CFloatEdit m_FloatEdit
}
// 或執行期手動子類化
m_FloatEdit.SubclassDlgItem(IDC_EDIT_FLOAT, this);
| 限制 | 說明 |
|---|---|
| 僅影響單一實例 | 實例子類化不影響同類別其他控制項 |
| 無法使用額外視窗記憶體 | 不知道原類別如何使用 cbWndExtra,貿然讀寫會導致崩潰 |
| 不會收到 WM_CREATE | 視窗已建立完成,初始化邏輯需自行在安裝後執行 |
| 全域子類化需 DLL | 影響所有進程同類別視窗,必須置於 DLL 共享資料段 |
| 相容性風險 | 系統更新可能改變內部訊息處理邏輯,導致子類化失效 |
Behavior、Attached Property、Custom Control 實現同樣功能,型別安全且支援 XAML 宣告。OnKeyPress/WndProc,或使用 IMessageFilter。子類化是 MFC/Win32 開發者手中的「手術刀」——精準、強大,但需極謹慎操作。掌握 SetWindowLongPtr/CallWindowProc 核心機制,配合 MFC CWnd::SubclassWindow 封裝,即可在不修改原始控制項原始碼的前提下,注入任意自訂行為。現代框架雖提供更安全抽象,但理解底層攔截機制對於除錯、相容舊代碼、跨層呼叫仍具不可替代價值。