標準 `CStatic` 功能單薄,無法滿足現代介面對標籤的豐富視覺需求。本文實作 `CLabelEx`,繼承 `CStatic`,整合三態背景圖片(正常/滑鼠懸停/點擊按下)、三態圖示標籤、文字顏色/下劃線/自動超連結效果(懸停變藍+下劃線)、可選邊框與背景填充。核心在於 `OnPaint` 手工繪製所有視覺層、`OnMouseMove` 配合 `SetCapture/ReleaseCapture` 實現懸停偵測。
class CLabelEx : public CStatic
{
DECLARE_DYNAMIC(CLabelEx)
public:
CLabelEx();
virtual ~CLabelEx();
// 背景圖片
void SetBGBitmap(UINT nIDResource);
void SetMouseOverBGBitmap(UINT nIDResource);
void SetClickedBGBitmap(UINT nIDResource);
// 標籤圖示(文字左側)
void SetLabelBitmap(UINT nIDResource);
void SetMouseOverLabelBitmap(UINT nIDResource);
void SetClickedLabelBitmap(UINT nIDResource);
// 文字樣式
void SetTextColor(COLORREF clr);
void SetUnderLine(BOOL bUnderline, COLORREF clrLine = RGB(0,0,255));
void SetAutoUnderLine(BOOL bEnable); // 懸停自動加下劃線
void SetHighLight(BOOL bEnable); // 懸停變藍
// 邊框與背景
void SetBorder(BOOL bEnable, COLORREF clrBorder = RGB(128,128,128));
void SetBkColor(COLORREF clrBk);
protected:
CBitmap m_bmpBG, m_bmpBGOver, m_bmpBGClicked;
CBitmap m_bmpLabel, m_bmpLabelOver, m_bmpLabelClicked;
COLORREF m_crText, m_crUnderLine, m_crBackText, m_crBorder, m_crBk;
BOOL m_bBorder, m_bUnderLine, m_bAutoUnderLine, m_bHighLight;
BOOL m_bOver, m_bClicked;
HCURSOR m_hHandCursor;
afx_msg void OnPaint();
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
DECLARE_MESSAGE_MAP()
};
void CLabelEx::OnPaint()
{
CPaintDC dc(this);
CRect rc; GetClientRect(&rc);
CDC memDC; memDC.CreateCompatibleDC(&dc);
CFont* pOldFont = dc.SelectObject(GetFont());
dc.SetBkMode(TRANSPARENT);
dc.SetTextColor(m_crText);
CPen penBorder, *pOldPen = NULL;
CBrush brushBk, *pOldBrush = NULL;
BITMAP bm;
int nTextLeft = 4, nTextTop = 0;
// 1. 邊框與背景填充
if (m_bBorder)
{
penBorder.CreatePen(PS_SOLID, 1, m_crBorder);
brushBk.CreateSolidBrush(m_crBk);
pOldPen = dc.SelectObject(&penBorder);
pOldBrush = dc.SelectObject(&brushBk);
dc.Rectangle(&rc);
dc.SelectObject(pOldPen); dc.SelectObject(pOldBrush);
rc.DeflateRect(1,1);
}
// 2. 貼背景圖片(優先級:Clicked > Over > Normal)
CBitmap* pBgBmp = NULL;
if (m_bClicked && m_bmpBGClicked.GetSafeHandle()) pBgBmp = &m_bmpBGClicked;
else if (m_bOver && m_bmpBGOver.GetSafeHandle()) pBgBmp = &m_bmpBGOver;
else if (m_bmpBG.GetSafeHandle()) pBgBmp = &m_bmpBG;
if (pBgBmp)
{
memDC.SelectObject(pBgBmp);
pBgBmp->GetBitmap(&bm);
dc.StretchBlt(rc.left, rc.top, rc.Width(), rc.Height(), &memDC, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY);
}
// 3. 貼標籤圖示(左側),計算文字左邊界
CBitmap* pLabelBmp = NULL;
if (m_bClicked && m_bmpLabelClicked.GetSafeHandle()) pLabelBmp = &m_bmpLabelClicked;
else if (m_bOver && m_bmpLabelOver.GetSafeHandle()) pLabelBmp = &m_bmpLabelOver;
else if (m_bmpLabel.GetSafeHandle()) pLabelBmp = &m_bmpLabel;
if (pLabelBmp)
{
pLabelBmp->GetBitmap(&bm);
double fScale = (double)bm.bmWidth / bm.bmHeight;
int nLabelW = (int)(rc.Height() * fScale);
memDC.SelectObject(pLabelBmp);
dc.StretchBlt(rc.left, rc.top, nLabelW, rc.Height(), &memDC, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY);
nTextLeft = rc.left + nLabelW + 4;
}
// 4. 輸出文字(垂直置中)
CString str; GetWindowText(str);
TEXTMETRIC tm; dc.GetTextMetrics(&tm);
nTextTop = rc.top + (rc.Height() - tm.tmHeight) / 2;
if (!str.IsEmpty()) dc.TextOut(nTextLeft, nTextTop, str);
// 5. 畫下劃線
if (m_bUnderLine && !str.IsEmpty())
{
CPen penLine(PS_SOLID, 1, m_crUnderLine);
CPen* pOld = dc.SelectObject(&penLine);
int nWidth = tm.tmAveCharWidth * str.GetLength();
dc.MoveTo(nTextLeft - 2, nTextTop + tm.tmHeight + 1);
dc.LineTo(nTextLeft - 2 + nWidth, nTextTop + tm.tmHeight + 1);
dc.SelectObject(pOld);
}
dc.SelectObject(pOldFont);
}
void CLabelEx::OnMouseMove(UINT nFlags, CPoint point)
{
CRect rc; GetClientRect(&rc);
if (m_bOver)
{
// 滑鼠移出判斷
if (!rc.PtInRect(point))
{
m_bOver = FALSE;
if (m_bAutoUnderLine) SetUnderLine(FALSE);
if (m_bHighLight) { m_crText = m_crBackText; Invalidate(); }
ReleaseCapture();
}
}
else
{
// 滑鼠移入
m_bOver = TRUE;
if (m_bAutoUnderLine) SetUnderLine(TRUE, RGB(0,0,255));
if (m_bHighLight) { m_crBackText = m_crText; m_crText = RGB(0,0,255); Invalidate(); }
SetCapture();
::SetCursor(m_hHandCursor ? m_hHandCursor : AfxGetApp()->LoadStandardCursor(IDC_HAND));
}
CStatic::OnMouseMove(nFlags, point);
}
void CLabelEx::OnLButtonDown(UINT nFlags, CPoint point)
{
m_bClicked = TRUE; Invalidate();
CStatic::OnLButtonDown(nFlags, point);
}
void CLabelEx::OnLButtonUp(UINT nFlags, CPoint point)
{
m_bClicked = FALSE; Invalidate();
// 這裡可發送自訂通知給父視窗:GetParent()->SendMessage(WM_LABEL_CLICK, GetDlgCtrlID());
CStatic::OnLButtonUp(nFlags, point);
}
BOOL CLabelEx::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
if (m_bOver) { ::SetCursor(m_hHandCursor); return TRUE; }
return CStatic::OnSetCursor(pWnd, nHitTest, message);
}
文中提到:在 `OnLButtonUp` 中若彈出模態對話框(`DoModal()`),滑鼠捕捉被對話框奪走,導致 `m_bOver`/`m_bClicked` 無法正確重置,標籤保持按下狀態。解決方案:
void CLabelEx::OnCaptureChanged(CWnd* pWnd)
{
if (pWnd != this) // 捕捉被其他視窗取走
{
m_bOver = m_bClicked = FALSE;
if (m_bAutoUnderLine) SetUnderLine(FALSE);
if (m_bHighLight) m_crText = m_crBackText;
Invalidate();
}
CStatic::OnCaptureChanged(pWnd);
}
void CMyDialog::OnInitDialog()
{
CDialog::OnInitDialog();
m_label.SubclassDlgItem(IDC_STATIC_LABEL, this);
m_label.SetBGBitmap(IDB_BG_NORMAL);
m_label.SetMouseOverBGBitmap(IDB_BG_HOVER);
m_label.SetClickedBGBitmap(IDB_BG_DOWN);
m_label.SetLabelBitmap(IDB_ICON_NORMAL);
m_label.SetAutoUnderLine(TRUE);
m_label.SetHighLight(TRUE);
m_label.SetTextColor(RGB(0,0,0));
m_label.SetWindowText(_T("點擊前往官網"));
}
`CLabelEx` 以純 GDI 繪製實現豐富視覺狀態,無外部依賴,輕量級適合大量使用。核心設計模式:狀態機驅動重繪 + 捕捉機制偵測懸停 + 位圖拉伸適應 DPI。已知模態對話框捕捉衝突可透過 `OnCaptureChanged` 或訊息轉發解決。建議將此控制項納入共用庫,統一專案中所有標籤、超連結、圖文按鈕的視覺語言。