標準 `CButton` 外觀固定,難以實現網頁超連結風格:平時像靜態文字,滑鼠懸停變藍+下劃線+手型游標,按下有陷入視覺回饋。本文實作 `CLinkButton` 繼承 `CButton`,設定 `BS_OWNERDRAW` 自繪,處理 `WM_MOUSEMOVE`/`WM_LBUTTONDOWN/UP`/`WM_TIMER`/`WM_SETCURSOR`/`WM_ERASEBKGND`,完整呈現三態視覺狀態,零外部依賴。
class CLinkButton : public CButton
{
DECLARE_DYNAMIC(CLinkButton)
public:
CLinkButton();
virtual ~CLinkButton();
protected:
CFont m_fontUnderline; // 下劃線字體
HCURSOR m_hHandCursor; // 手型游標
BOOL m_bHighlight = FALSE; // 滑鼠懸停
BOOL m_bLBtnDown = FALSE; // 按下狀態
// 核心繪製
virtual void DrawItem(LPDRAWITEMSTRUCT lpDIS);
// 訊息處理
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT msg);
afx_msg void OnTimer(UINT_PTR nIDEvent);
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
DECLARE_MESSAGE_MAP()
};
void CLinkButton::DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
CDC* pDC = CDC::FromHandle(lpDIS->hDC);
CRect rc(lpDIS->rcItem);
UINT state = lpDIS->itemState;
CString str; GetWindowText(str);
COLORREF clrText;
CFont* pOldFont = NULL;
// 背景透明
pDC->SetBkMode(TRANSPARENT);
if (state & ODS_DISABLED)
{
// 禁用狀態:灰白陰影文字
CBrush grayBrush(GetSysColor(COLOR_GRAYTEXT));
CSize sz = pDC->GetTextExtent(str);
int x = rc.left + (rc.Width() - sz.cx) / 2;
int y = rc.top + (rc.Height() - sz.cy) / 2;
rc.OffsetRect(2,2); pDC->SetTextColor(GetSysColor(COLOR_3DHILIGHT)); pDC->DrawText(str, rc, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
rc.OffsetRect(-2,-2); pDC->SetTextColor(GetSysColor(COLOR_GRAYTEXT)); pDC->DrawText(str, rc, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
}
else
{
if (m_bHighlight)
{
// 懸停/按下:繪製 3D 邊框
if (state & ODS_SELECTED) // 按下
pDC->Draw3dRect(rc, GetSysColor(COLOR_3DSHADOW), GetSysColor(COLOR_3DHILIGHT));
else // 懸停未按下
pDC->Draw3dRect(rc, GetSysColor(COLOR_3DHILIGHT), GetSysColor(COLOR_3DSHADOW));
// 藍字 + 下劃線
clrText = RGB(0,0,255);
if (m_fontUnderline.GetSafeHandle() == NULL)
{
CFont* pFont = GetFont(); LOGFONT lf; pFont->GetLogFont(&lf);
lf.lfUnderline = TRUE; m_fontUnderline.CreateFontIndirect(&lf);
}
pOldFont = pDC->SelectObject(&m_fontUnderline);
}
else
{
// 正常:普通按鈕文字色
clrText = GetSysColor(COLOR_BTNTEXT);
}
pDC->SetTextColor(clrText);
pDC->DrawText(str, rc, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
if (pOldFont) pDC->SelectObject(pOldFont);
}
}
BEGIN_MESSAGE_MAP(CLinkButton, CButton)
ON_WM_MOUSEMOVE()
ON_WM_SETCURSOR()
ON_WM_TIMER()
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_CREATE()
ON_WM_ERASEBKGND()
END_MESSAGE_MAP()
void CLinkButton::OnMouseMove(UINT nFlags, CPoint point)
{
SetTimer(1, 10, NULL); // 10ms 輪詢滑鼠是否移出
CButton::OnMouseMove(nFlags, point);
}
void CLinkButton::OnTimer(UINT_PTR nIDEvent)
{
if (nIDEvent != 1) return;
if (m_bLBtnDown) { KillTimer(1); return; } // 按下期間不檢測移出
CPoint pt; GetCursorPos(&pt);
CRect rc; GetWindowRect(rc);
if (!rc.PtInRect(pt))
{
// 滑鼠移出
if (m_bHighlight)
{
m_bHighlight = FALSE;
KillTimer(1);
Invalidate(); // 重繪恢復正常狀態
}
}
else
{
// 滑鼠在內部
if (!m_bHighlight)
{
m_bHighlight = TRUE;
Invalidate(); // 重繪進入懸停狀態
}
}
CButton::OnTimer(nIDEvent);
}
BOOL CLinkButton::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT msg)
{
if (m_bHighlight)
{
::SetCursor(m_hHandCursor);
return TRUE;
}
return CButton::OnSetCursor(pWnd, nHitTest, msg);
}
void CLinkButton::OnLButtonDown(UINT nFlags, CPoint point)
{
m_bLBtnDown = TRUE;
Invalidate(); // 進入按下視覺
CButton::OnLButtonDown(nFlags, point);
}
void CLinkButton::OnLButtonUp(UINT nFlags, CPoint point)
{
m_bLBtnDown = FALSE;
if (m_bHighlight) { m_bHighlight = FALSE; Invalidate(); }
CButton::OnLButtonUp(nFlags, point);
}
int CLinkButton::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CButton::OnCreate(lpCreateStruct) == -1) return -1;
// 載入手型游標
m_hHandCursor = AfxGetApp()->LoadStandardCursor(IDC_HAND); // 或 LoadCursor(IDC_CURSOR_HAND)
// 預建立下劃線字體
CFont* pFont = GetFont(); LOGFONT lf; pFont->GetLogFont(&lf);
lf.lfUnderline = TRUE; m_fontUnderline.CreateFontIndirect(&lf);
return 0;
}
BOOL CLinkButton::OnEraseBkgnd(CDC* pDC)
{
// 繪製稍深於 3D Face 的背景,避免與對話框背景同色顯得突兀
COLORREF cr = GetSysColor(COLOR_3DFACE);
int r = GetRValue(cr), g = GetGValue(cr), b = GetBValue(cr);
if (r > 1) r -= 2; if (g > 1) g -= 2; if (b < 253) b += 2;
pDC->FillSolidRect(CRect(0,0,0,0), RGB(r,g,b)); // 實際應用 GetClientRect
return TRUE; // 已處理背景,不呼叫基類
}
void CMyDialog::OnInitDialog()
{
CDialog::OnInitDialog();
m_linkBtn.SubclassDlgItem(IDC_BUTTON_LINK, this);
m_linkBtn.SetWindowText(_T("訪問官方網站"));
// 字體可在資源編輯器設定,或此處 SetFont
}



平時純文字,懸停變藍+下劃線+手型游標+凸起邊框,按下陷入邊框,釋放觸發 `BN_CLICKED` 命令。
`CLinkButton` 以 `BS_OWNERDRAW` + 狀態機實現超連結風格按鈕,核心邏輯:Timer 輪詢滑鼠位置 → m_bHighlight/m_bLBtnDown 驅動 DrawItem 三態繪製 → OnSetCursor 切換手型。無圖片資源依賴、純系統色適應主題、支援禁用狀態。可直接納入共用控制項庫,替代傳統 `CButton` 用於說明連結、啟動外部 URL、觸發精靈等動作,提升介面現代感。