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

VC++ 全域 Hook 實現程式換膚 (按鈕為例)

     編輯:關於VC++

前言

傳統換膚需重寫每個控件類別 (如 CButtonEx, CEditEx),侵入性強、維護難。本文示範 **全域視窗鉤子 (WH_CALLWNDPROC)** 攔截系統建立標準控件 (Button/Edit/Combo/ListBox 等) 的瞬間,替換其 WndProc 為自訂繪製函數,實現 **零侵入、兩行代碼整合** 的全域換膚,原理同商業庫 SkinMagic/EasySkin。

核心架構


// 全域鉤子控制碼
HHOOK g_hHook = NULL;

// 啟動鉤子 (App::InitInstance)
BOOL IRStartup(HINSTANCE hMod, DWORD dwThreadId)
{
    g_hHook = SetWindowsHookEx(WH_CALLWNDPROC, IRCallWndProc, hMod, dwThreadId);
    return TRUE;
}

// 釋放鉤子 (App::ExitInstance)
BOOL IRComplete()
{
    UnhookWindowsHookEx(g_hHook);
    return TRUE;
}

鉤子回呼:攔截按鈕建立並替換 WndProc


LRESULT CALLBACK IRCallWndProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode < 0) return CallNextHookEx(g_hHook, nCode, wParam, lParam);

    CWPSTRUCT* pcs = (CWPSTRUCT*)lParam;
    HWND hWnd = pcs->hwnd;
    if (!hWnd) return CallNextHookEx(g_hHook, nCode, wParam, lParam);

    // 只處理標準 Button 類別
    char szClass[256];
    GetClassNameA(hWnd, szClass, 255);
    if (strcmp(szClass, "Button") != 0)
        return CallNextHookEx(g_hHook, nCode, wParam, lParam);

    // 只處理標準按鈕樣式 (BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP = 0x50010000)
    CWnd* pWnd = CWnd::FromHandle(hWnd);
    DWORD dwStyle = pWnd->GetStyle();
    if (dwStyle != 0x50010000)
        return CallNextHookEx(g_hHook, nCode, wParam, lParam);

    // 取得原始 WndProc
    WNDPROC oldProc = (WNDPROC)GetWindowLongPtr(hWnd, GWLP_WNDPROC);

    // 若已被我們替換過,跳過
    if (CButtonExt::s_oldProc && oldProc == CButtonExt::s_oldProc)
        return CallNextHookEx(g_hHook, nCode, wParam, lParam);

    // 替換為自訂 WndProc
    if (oldProc != CButtonExt::DefWindowProc) {
        CButtonExt::s_oldProc = oldProc; // 保存原始
        SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)CButtonExt::DefWindowProc);
    }

    return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

自訂按鈕繪製類 (CButtonExt)


class CButtonExt
{
public:
    static WNDPROC s_oldProc; // 原始 WndProc
    static UINT  s_nState;    // 狀態: NORMAL/HOVER/DOWN

    // 統一訊息分發入口
    static LRESULT CALLBACK DefWindowProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
    {
        CWnd* pWnd = CWnd::FromHandle(hWnd);
        switch (msg) {
            case WM_PAINT:      return OnPaint(pWnd);
            case WM_LBUTTONDOWN:return OnLButtonDown(pWnd, wp, lp);
            case WM_LBUTTONUP:  return OnLButtonUp(pWnd, wp, lp);
            case WM_LBUTTONDBLCLK:return OnLButtonDblClk(pWnd, wp, lp);
            case WM_MOUSEMOVE:  return OnMouseMove(pWnd, wp, lp);
            case WM_ERASEBKGND: return OnEraseBkgnd(pWnd, (CDC*)wp);
        }
        return CallWindowProc(s_oldProc, hWnd, msg, wp, lp);
    }

    // 狀態機 + 圓角矩形繪製 (OnPaint)
    static LRESULT OnPaint(CWnd* pWnd) {
        CPaintDC dc(pWnd);
        CRect rc; pWnd->GetClientRect(&rc);
        CString txt; pWnd->GetWindowText(txt);

        // 依狀態選色
        COLORREF bg, border, text;
        if (s_nState == STATUS_DOWN)      { bg=RGB(160,160,160); border=RGB(100,100,100); text=RGB(50,50,250); }
        else if (s_nState == STATUS_HOVER) { bg=RGB(60,60,180);   border=RGB(0,0,0);     text=RGB(250,250,50); }
        else                               { bg=RGB(240,240,240); border=RGB(120,120,120); text=RGB(50,50,50); }

        CBrush br(bg); CPen pn(PS_SOLID,1,border);
        dc.SelectObject(&br); dc.SelectObject(&pn);
        dc.SetBkMode(TRANSPARENT); dc.SetTextColor(text);
        CPoint pt(2,2);
        dc.RoundRect(&rc, pt); // 圓角矩形

        // 區域裁剪 (可選)
        HRGN rgn = CreateRoundRectRgn(rc.left,rc.top,rc.right,rc.bottom,4,4);
        pWnd->SetWindowRgn(rgn, TRUE);
        DeleteObject(rgn);

        dc.DrawText(txt, &rc, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
        return 0;
    }

    // 滑鼠進入/離開/按下/釋放 狀態切換 + Invalidate
    static LRESULT OnMouseMove(CWnd* pWnd, WPARAM, LPARAM lp) {
        CPoint pt(LOWORD(lp), HIWORD(lp));
        HRGN rgn = CreateRectRgn(0,0,0,0);
        pWnd->GetWindowRgn(rgn);
        BOOL in = PtInRegion(rgn, pt.x, pt.y);
        DeleteObject(rgn);

        if (in) {
            if (s_nState == STATUS_DOWN) return 0;
            if (s_nState != STATUS_HOVER) { s_nState = STATUS_HOVER; pWnd->Invalidate(); pWnd->UpdateWindow(); pWnd->SetCapture(); }
        } else {
            if (s_nState == STATUS_HOVER) { s_nState = STATUS_NORMAL; pWnd->Invalidate(); pWnd->UpdateWindow(); ReleaseCapture(); }
        }
        return 0;
    }
    static LRESULT OnLButtonDown(CWnd* pWnd, WPARAM, LPARAM) {
        s_nState = STATUS_DOWN; pWnd->Invalidate(); pWnd->UpdateWindow(); return 0;
    }
    static LRESULT OnLButtonUp(CWnd* pWnd, WPARAM, LPARAM) {
        if (s_nState != STATUS_NORMAL) {
            s_nState = STATUS_NORMAL; pWnd->Invalidate(); pWnd->UpdateWindow();
            // 發送 WM_COMMAND 給父視窗模擬點擊
            ::SendMessage(GetParent(pWnd->m_hWnd), WM_COMMAND,
                MAKEWPARAM(pWnd->GetDlgCtrlID(), BN_CLICKED), (LPARAM)pWnd->m_hWnd);
        }
        return 0;
    }
    static LRESULT OnEraseBkgnd(CWnd*, CDC*) { return TRUE; } // 防閃爍
};

// 靜態成員定義
WNDPROC CButtonExt::s_oldProc = NULL;
UINT CButtonExt::s_nState = 0;

整合到專案 (兩行代碼)


// MyApp.cpp
BOOL CMyApp::InitInstance()
{
    // ... 原有初始化 ...
    IRStartup(GetModuleHandle(NULL), GetCurrentThreadId()); // 第 1 行
    return TRUE;
}

int CMyApp::ExitInstance()
{
    IRComplete(); // 第 2 行
    return CWinApp::ExitInstance();
}

效果與擴充

  • 專案中**所有**標準按鈕 (含對話框、屬性頁、訊息盒按鈕) 自動套用圓角、換色、Hover/Down 狀態。
  • 同理可擴充 CEditExt, CListBoxExt, CComboBoxExt 等,在 IRCallWndProc 中依 GetClassName 分派。
  • 皮膚資源 (顏色/圖片/圓角半徑) 可外置 XML/INI,CButtonExt 讀取配置實現多主題切換。
  • 64 位元專案需將 GetWindowLong/SetWindowLong 改為 GetWindowLongPtr/SetWindowLongPtr。

注意與風險

  • 全域鉤子影響**同執行緒**所有視窗;若專案多執行緒建 UI,需各執行緒各自安裝鉤子。
  • 替換 WndProc 可能與其他子類化庫 (如 MFC 內部、BCG、Skin++) 衝突,需測試相容性。
  • 效能:WH_CALLWNDPROC 只在訊息**發送前**通知,不阻塞;但大量視窗建立時會增加開銷。
  • 殺毒軟體可能誤報全域鉤子為惡意行為,需數位簽名與白名單。

完整原始碼含 IRHook.h/cpp, ButtonExt.h/cpp, 測試專案,VC6.0/VS2005+ 編譯通過。

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