純 Win32 API 程式體積小、無執行階段依賴、適合驅動級/嵌入式/安全軟體。自繪按鈕核心在於子類化按鈕視窗程序,攔截 WM_PAINT、WM_MOUSEMOVE、WM_LBUTTONDOWN 等訊息,完全掌控繪製與狀態機。
class CWINButton {
HWND m_hBtn; // 按鈕句柄
HWND m_hParent; // 父視窗
WNDPROC m_oldProc; // 原視窗程序
WNDPROC m_thunk; // Thunk 存根 (實現 this 呼叫)
// 狀態旗標
BOOL m_bMouseOver, m_bPressed, m_bFocused, m_bEnabled;
// 外觀資源
HBITMAP m_bmpNormal, m_bmpHover, m_bmpPressed, m_bmpDisabled;
HICON m_hIcon;
HFONT m_hFont;
COLORREF m_clrText;
// Tooltip
HWND m_hTip;
TOOLINFO m_ti;
public:
BOOL Init(HWND hBtn); // 子類化進入點
BOOL Restore(); // 還原原視窗程序
void SetBitmaps(HBMP normal, HBMP hover, HBMP pressed, HBMP disabled);
void SetIcon(HICON); void SetFont(HFONT); void SetTextColor(COLORREF);
void SetTooltip(LPCTSTR);
// 靜態視窗程序 -> Thunk -> 實例成員函數
static LRESULT CALLBACK StaticProc(HWND, UINT, WPARAM, LPARAM);
LRESULT InstanceProc(UINT, WPARAM, LPARAM);
};
this 傳遞靜態視窗程序無法直接存取實例成員。Thunk 是一小段機器碼,將 HWND 映射到 CWINButton* (透過 SetProp/GetProp 或 SetWindowLongPtr GWLP_USERDATA),並調整 this 指標跳轉到成員函數。
// 簡易版:使用 GWLP_USERDATA (僅支援單實例/單執行緒)
BOOL CWINButton::Init(HWND hBtn)
{
m_hBtn = hBtn; m_hParent = GetParent(hBtn);
SetWindowLongPtr(hBtn, GWLP_USERDATA, (LONG_PTR)this);
m_oldProc = (WNDPROC)SetWindowLongPtr(hBtn, GWLP_WNDPROC, (LONG_PTR)StaticProc);
return TRUE;
}
LRESULT CALLBACK CWINButton::StaticProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
CWINButton* pThis = (CWINButton*)GetWindowLongPtr(hWnd, GWLP_USERDATA);
return pThis ? pThis->InstanceProc(msg, wp, lp) : DefWindowProc(hWnd, msg, wp, lp);
}
多實例/多執行緒安全版:使用 SetProp(hWnd, _T("CWINButton"), this) + 動態生成 Thunk 代碼 (x86: MOV ECX, pThis; JMP InstanceProc),參考 ATL::_AtlCreateWndProcThunk。
| 訊息 | 處理重點 |
|---|---|
WM_PAINT | 雙緩衝繪製:背景 → 邊框 → 圖示/文字 → 焦點虛線框。根據狀態旗標選擇位圖/顏色。 |
WM_MOUSEMOVE | TrackMouseEvent(TME_LEAVE) 追蹤離開,設定 m_bMouseOver,InvalidateRect 重繪。 |
WM_MOUSELEAVE | 清除 m_bMouseOver,重繪。 |
WM_LBUTTONDOWN/UP | 設定/清除 m_bPressed,SetCapture/ReleaseCapture,釋放時發送 BN_CLICKED 給父視窗。 |
WM_SETFOCUS/KILLFOCUS | 更新 m_bFocused,重繪焦點框。 |
WM_ENABLE | 同步 m_bEnabled,重繪灰態。 |
WM_DESTROY | 還原 m_oldProc,釋放 GDI 物件,RemoveProp。 |
MFC 會在父視窗處理 WM_DRAWITEM 繪製子控件。若父視窗為 CDialog,需在 OnDrawItem 中直接返回,不呼叫 Default(),否則雙重繪製導致關閉時 Crash。
void CMyDialog::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
// 若控件已被 CWINButton 子類化,這裡不做任何事
// CDialog::OnDrawItem(nIDCtl, lpDrawItemStruct); // 註釋掉!
}