首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> VC++

MFC 子類化技術實戰:攔截視窗訊息實現自訂編輯框與超連結控制項

     編輯:VC++

核心概念:什麼是子類化?

子類化 是在視窗建立之後,將自訂的視窗程序 (Window Procedure) 插入到原始視窗程序之前,形成呼叫鏈:

系統訊息 → 新視窗程序 (攔截/修改/丟棄) → CallWindowProc → 原視窗程序 → 回傳結果

三種模式:

  • 實例子類化:針對單一控制項實例 (最常用)。
  • 全域子類化:針對同一視窗類別的所有實例 (需在 DLL 中實作)。
  • 超類化:以現有類別為基礎註冊新類別,再建立視窗 (不影響既有實例)。

C++ 繼承 vs. 視窗子類化:概念類比

// C++ 繼承:編譯期多型
class Parent { public: virtual void Func() { cout << "Parent"; } };
class Child : public Parent { public: void Func() override { cout << "Child"; } };

// 視窗子類化:執行期攔截
// 原視窗程序處理 WM_CHAR → 允許所有字元
// 新視窗程序攔截 WM_CHAR → 過濾非數字/小數點/退格 → CallWindowProc 原程序

實作步驟詳解:打造「僅允許浮點數」編輯框

步驟 1:對話方塊初始化時安裝子類化

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,避免指標截斷。

步驟 2:實作自訂視窗程序

// 需為靜態函數或全域函數,呼叫慣例 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);
}

步驟 3:對話方塊銷毀時恢復原視窗程序 (良好習慣)

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 封裝方式:CWnd::SubclassWindow

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 共享資料段
相容性風險系統更新可能改變內部訊息處理邏輯,導致子類化失效

現代替代方案

  • WPF/UWP/WinUI: 使用 Behavior、Attached Property、Custom Control 實現同樣功能,型別安全且支援 XAML 宣告。
  • WinForms: 繼承控制項並覆寫 OnKeyPress/WndProc,或使用 IMessageFilter。
  • Web (HTML/JS): 原生 ``、CSS `` 樣式、事件監聽器。

結語

子類化是 MFC/Win32 開發者手中的「手術刀」——精準、強大,但需極謹慎操作。掌握 SetWindowLongPtr/CallWindowProc 核心機制,配合 MFC CWnd::SubclassWindow 封裝,即可在不修改原始控制項原始碼的前提下,注入任意自訂行為。現代框架雖提供更安全抽象,但理解底層攔截機制對於除錯、相容舊代碼、跨層呼叫仍具不可替代價值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved