MFC 自定义静态文本控件:增强型标签控件

在开发中,标准控件功能往往无法满足特定的业务需求。这篇博客实现了高度可定制的 MFC 自定义控件 CLabel,支持背景、文本、边框及圆角设置,提供更丰富的视觉效果和交互体验。

功能概览

CLabel 是基于 MFC CStatic 控件实现的增强型标签控件,支持以下功能:

  1. 文本样式设置

    • 自定义字体、颜色、大小、对齐方式等。
    • 支持加粗、斜体和下划线。
  2. 背景样式设置

    • 支持设置背景颜色。
  3. 边框设置

    • 支持设置边框颜色、宽度、样式,支持禁用边框。
  4. 圆角设置

    • 支持启用圆角,支持单独设置四个角的圆角半径。
  5. 交互特性

    • 支持鼠标事件处理,显示手型光标并触发点击事件回调。

代码实现

头文件:

以下是 CLabel 的完整类声明:

cpp 复制代码
#if !defined(AFX_LABEL_H__A4EABEC5_2E8C_11D1_B79F_00805F9ECE10__INCLUDED_)
#define AFX_LABEL_H__A4EABEC5_2E8C_11D1_B79F_00805F9ECE10__INCLUDED_

#if _MSC_VER >= 1000
#pragma once
#endif // _MSC_VER >= 1000

#include <functional> 

/
// CLabel window
enum FlashType { None, Text, Background };
enum TextAlign { AlignLeft, AlignCenter, AlignRight };

class AFX_EXT_CLASS CLabel : public CStatic
{
    DECLARE_DYNCREATE(CLabel) // 支持动态创建

public:
    // 构造与析构
    CLabel();
    virtual ~CLabel();

    void SetClickCallback(std::function<void()> callback); // 设置点击事件的回调函数

    // 属性设置接口
    CLabel& SetBkColor(COLORREF crBkgnd);                 // 设置背景颜色
    CLabel& SetTextColor(COLORREF crText);                // 设置文本颜色
    CLabel& SetText(const CString& strText);              // 设置文本内容
    CLabel& SetFontBold(BOOL bBold);                      // 设置字体加粗
    CLabel& SetFontName(const CString& strFont);          // 设置字体名称
    CLabel& SetFontUnderline(BOOL bSet);                  // 设置下划线
    CLabel& SetFontItalic(BOOL bSet);                     // 设置斜体
    CLabel& SetFontSize(int nSize);                       // 设置字体大小
    CLabel& SetAlignment(TextAlign alignment);            // 设置文本对齐方式
    CLabel& SetDynamicFont(BOOL bDynamic);                // 设置是否动态调整字体
    CLabel& FlashText(BOOL bActivate);                    // 闪烁文本
    CLabel& FlashBackground(BOOL bActivate);              // 闪烁背景
    CLabel& SetLink(BOOL bLink);                          // 设置是否启用超链接
    CLabel& SetLinkCursor(HCURSOR hCursor);               // 设置超链接光标
	CLabel& DisableBorder();							  // 禁用边框
	CLabel& SetBorderColor(COLORREF crBorder);            // 设置边框颜色
	CLabel& SetBorderWidth(int nWidth);                   // 设置边框宽度
	CLabel& SetBorderStyle(int nStyle);                   // 设置边框样式
	CLabel& SetDefaultCursor(HCURSOR hCursor);			  // 设置默认光标
    CLabel& SetHandCursor(HCURSOR hCursor);				  // 设置手型光标
    CLabel& SetRoundedCorners(BOOL bEnable, int nRadius); // 设置圆角及半径
    CLabel& SetCornerRadius(int nTopLeft, int nTopRight, int nBottomRight, int nBottomLeft); // 设置各角圆角半径

protected:
    // 工具函数
    void ReconstructFont();                                 // 重新构造字体
    void UpdateFontSize();                                  // 动态调整字体大小
	void CreateRoundedRegion(CRgn& rgn, const CRect& rect); // 创建圆角区域
    virtual void OnPaint();                                 // 自定义绘制文本

    // 属性
    COLORREF  m_crText;           // 文本颜色
    COLORREF  m_crBkColor;        // 背景颜色
    HBRUSH    m_hBrush;           // 背景画刷
    LOGFONT   m_lf;               // 字体信息
    CFont     m_font;             // 字体对象
    CString   m_strText;          // 文本内容
    BOOL      m_bState;           // 状态,用于闪烁
    BOOL      m_bTimer;           // 定时器状态
    BOOL      m_bLink;            // 是否为超链接
    BOOL      m_bDynamicFont;     // 是否动态调整字体大小
    TextAlign m_alignment;        // 文本对齐方式
    FlashType m_Type;             // 闪烁类型
    HCURSOR   m_hCursor;          // 超链接光标

    // 边框属性
    COLORREF m_crBorderColor;     // 边框颜色
    int m_nBorderWidth;           // 边框宽度
    int m_nBorderStyle;           // 边框样式(使用 GDI 样式:PS_SOLID, PS_DASH 等)

    // 圆角相关属性
    BOOL m_bRoundedCorners;       // 是否启用圆角
    int  m_nTopLeftRadius;        // 左上角圆角半径
    int  m_nTopRightRadius;       // 右上角圆角半径
    int  m_nBottomRightRadius;    // 右下角圆角半径
    int  m_nBottomLeftRadius;     // 左下角圆角半径

	// 鼠标事件相关属性
    BOOL    m_bMouseIn;           // 鼠标是否在控件上
    HCURSOR m_hHandCursor;        // 手型光标
    HCURSOR m_hDefaultCursor;     // 默认光标
    std::function<void()> m_clickCallback; // 点击事件的回调函数

protected:
    // MFC 消息映射
    virtual HBRUSH CtlColor(CDC* pDC, UINT nCtlColor);      // 背景和文本颜色设置
    afx_msg void OnTimer(UINT_PTR nIDEvent);                // 定时器事件
    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);  // 鼠标点击事件
    afx_msg void OnMouseMove(UINT nFlags, CPoint point);    // 鼠标移动事件
	afx_msg void OnMouseLeave();                            // 鼠标离开事件
    afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message); // 设置光标事件
    DECLARE_MESSAGE_MAP()
};

#endif // !defined(AFX_LABEL_H__A4EABEC5_2E8C_11D1_B79F_00805F9ECE10__INCLUDED_)

源文件:

以下是 CLabel 的完整类实现:

cpp 复制代码
#include "stdafx.h"
#include "Resource.h"
#include "Label.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static const char* THIS_FILE = __FILE__;
#endif

IMPLEMENT_DYNCREATE(CLabel, CStatic)

BEGIN_MESSAGE_MAP(CLabel, CStatic)
    ON_WM_CTLCOLOR_REFLECT()
    ON_WM_PAINT()
    ON_WM_TIMER()
    ON_WM_LBUTTONDOWN()
    ON_WM_MOUSEMOVE()
    ON_WM_MOUSELEAVE()
    ON_WM_SETCURSOR()
END_MESSAGE_MAP()

CLabel::CLabel()
    : m_crText(GetSysColor(COLOR_WINDOWTEXT)),
    m_crBkColor(GetSysColor(COLOR_3DFACE)),
    m_bState(FALSE),
    m_bTimer(FALSE),
    m_bLink(TRUE),
    m_bDynamicFont(TRUE),
    m_alignment(AlignCenter),
    m_Type(None),
    m_hCursor(NULL),
	m_crBorderColor(RGB(0, 0, 0)),
	m_nBorderWidth(1),
	m_nBorderStyle(PS_SOLID),
	m_bRoundedCorners(FALSE),
	m_nTopLeftRadius(0),
	m_nTopRightRadius(0),
	m_nBottomRightRadius(0),
	m_nBottomLeftRadius(0),
    m_bMouseIn(FALSE),
    m_hHandCursor(::LoadCursor(nullptr, IDC_HAND)),
    m_hDefaultCursor(::LoadCursor(nullptr, IDC_ARROW))
{
    ::GetObject(( HFONT ) GetStockObject(DEFAULT_GUI_FONT), sizeof(m_lf), &m_lf);
    m_font.CreateFontIndirect(&m_lf);
    m_hBrush = ::CreateSolidBrush(m_crBkColor);
}

CLabel::~CLabel()
{
    // 确保字体被删除
    if (m_font.m_hObject != NULL) {
        m_font.DeleteObject();
    }

    // 清理其他资源,如画刷等
    if (m_hBrush != NULL) {
        ::DeleteObject(m_hBrush);  // 确保画刷被释放
    }
}

void CLabel::SetClickCallback(std::function<void()> callback)
{
    if (callback)
    {
        LONG style = GetWindowLong(m_hWnd, GWL_STYLE);
        style |= SS_NOTIFY;
        SetWindowLong(m_hWnd, GWL_STYLE, style);
        m_clickCallback = callback;
    }
}

CLabel& CLabel::SetBkColor(COLORREF crBkgnd)
{
    if (m_hBrush)
        ::DeleteObject(m_hBrush);

    m_crBkColor = crBkgnd;
    m_hBrush = ::CreateSolidBrush(crBkgnd);
    RedrawWindow();
    return *this;
}

CLabel& CLabel::SetTextColor(COLORREF crText)
{
    m_crText = crText;
    RedrawWindow();
    return *this;
}

CLabel& CLabel::SetText(const CString& strText)
{
    SetWindowText(strText);
    RedrawWindow();
    return *this;
}

CLabel& CLabel::SetFontBold(BOOL bBold)
{
    m_lf.lfWeight = bBold ? FW_BOLD : FW_NORMAL;
    ReconstructFont();
    RedrawWindow();
    return *this;
}

CLabel& CLabel::SetFontName(const CString& strFont)
{
    _tcscpy_s(m_lf.lfFaceName, LF_FACESIZE, strFont);
    ReconstructFont();

    return *this;
}

CLabel& CLabel::SetFontUnderline(BOOL bSet)
{
    m_lf.lfUnderline = bSet;
    ReconstructFont();
    RedrawWindow();
    return *this;
}

CLabel& CLabel::SetFontItalic(BOOL bSet)
{
    m_lf.lfItalic = bSet;
    ReconstructFont();
    RedrawWindow();
    return *this;
}

CLabel& CLabel::SetFontSize(int nSize)
{
    m_bDynamicFont = FALSE; // 禁用动态字体调整
    m_lf.lfHeight = -nSize;
    ReconstructFont();
    RedrawWindow();
    return *this;
}

CLabel& CLabel::SetAlignment(TextAlign alignment)
{
    m_alignment = alignment;

    if (GetSafeHwnd()) {
        // 获取当前窗口样式
        LONG style = GetWindowLong(m_hWnd, GWL_STYLE);

        // 清除现有的对齐样式
        style &= ~(SS_LEFT | SS_CENTER | SS_RIGHT);

        // 根据对齐方式设置新的样式
        if (m_alignment == AlignLeft) {
            style |= SS_LEFT;
        }
        else if (m_alignment == AlignCenter) {
            style |= SS_CENTER;
        }
        else if (m_alignment == AlignRight) {
            style |= SS_RIGHT;
        }

        // 应用新的样式
        SetWindowLong(m_hWnd, GWL_STYLE, style);

        // 触发重绘
        RedrawWindow();
    }

    return *this;
}

CLabel& CLabel::SetDynamicFont(BOOL bDynamic)
{
    m_bDynamicFont = bDynamic;
    RedrawWindow();
    return *this;
}

CLabel& CLabel::FlashText(BOOL bActivate)
{
    if (m_bTimer) {
        SetWindowText(m_strText);
        KillTimer(1);
    }

    if ( bActivate ) {
        GetWindowText(m_strText);
        m_bState = FALSE;
        m_bTimer = TRUE;
        SetTimer(1, 500, NULL);
        m_Type = Text;
    }

    return *this;
}

CLabel& CLabel::FlashBackground(BOOL bActivate)
{
    if (m_bTimer)
        KillTimer(1);

    if (bActivate) {
        m_bState = FALSE;
        m_bTimer = TRUE;
        SetTimer(1, 500, NULL);
        m_Type = Background;
    }

    return *this;
}

CLabel& CLabel::SetLink(BOOL bLink)
{
	m_bLink = bLink;
	return *this;
}

CLabel& CLabel::SetLinkCursor(HCURSOR hCursor)
{
    m_hCursor = hCursor;
    return *this;
}

CLabel& CLabel::DisableBorder()
{
	m_nBorderWidth = 0;
	return *this;
}

CLabel& CLabel::SetBorderColor(COLORREF crBorder)
{
	m_crBorderColor = crBorder;
	return *this;
}

CLabel& CLabel::SetBorderWidth(int nWidth)
{
	m_nBorderWidth = nWidth;
	return *this;
}

CLabel& CLabel::SetBorderStyle(int nStyle)
{
	m_nBorderStyle = nStyle;
	return *this;
}

CLabel& CLabel::SetDefaultCursor(HCURSOR hCursor)
{
	m_hDefaultCursor = hCursor;
	return *this;
}

CLabel& CLabel::SetHandCursor(HCURSOR hCursor)
{
	m_hHandCursor = hCursor;
	return *this;
}

CLabel& CLabel::SetRoundedCorners(BOOL bEnable, int nRadius)
{
    m_bRoundedCorners = bEnable;
    m_nTopLeftRadius = nRadius;
	m_nTopRightRadius = nRadius;
	m_nBottomRightRadius = nRadius;
	m_nBottomLeftRadius = nRadius;
    Invalidate(); // 强制重绘
    return *this;
}

CLabel& CLabel::SetCornerRadius(int nTopLeft, int nTopRight, int nBottomRight, int nBottomLeft)
{
    m_nTopLeftRadius = nTopLeft;
    m_nTopRightRadius = nTopRight;
    m_nBottomRightRadius = nBottomRight;
    m_nBottomLeftRadius = nBottomLeft;
    Invalidate(); // 强制重绘
    return *this;
}

void CLabel::ReconstructFont()
{
    if (m_font.m_hObject!=NULL) {
        m_font.DeleteObject();
    }

    // 创建新的字体
    m_font.CreateFontIndirect(&m_lf);
    if (GetSafeHwnd()!=NULL) {
        SetFont(&m_font);
    }
}

void CLabel::UpdateFontSize()
{
    if (!m_bDynamicFont)
        return;

    CRect rect;
    GetClientRect(&rect);

    int fontSize = rect.Height() / 2; // 动态调整字体大小为高度的一半
    if (fontSize < 8) fontSize = 8;   // 最小字体大小
    if (fontSize > 30) fontSize = 30; // 最大字体大小

    m_lf.lfHeight = -fontSize;
    ReconstructFont();
}

void CLabel::CreateRoundedRegion(CRgn& rgn, const CRect& rect)
{
    // 防止像素偏移问题,增加1
    rgn.CreateRoundRectRgn(
        rect.left, rect.top,
        rect.right + 1, rect.bottom + 1,
        m_nTopLeftRadius, m_nTopLeftRadius
    );
}

void CLabel::OnPaint()
{
    CPaintDC dc(this);

    // 1. 获取控件区域
    CRect rect;
    GetClientRect(&rect);

    // 2. 获取对话框背景颜色并清除整个控件区域
    COLORREF dlgBkColor = ::GetSysColor(COLOR_BTNFACE);
    CBrush clearBrush(dlgBkColor);
    dc.FillRect(&rect, &clearBrush);

    // 3. 绘制边框
    if (m_nBorderWidth > 0) {
        CPen pen(m_nBorderStyle, m_nBorderWidth, m_crBorderColor);
        CPen* pOldPen = dc.SelectObject(&pen);

        // 计算边框的实际区域
        CRect borderRect = rect;
        int borderOffset = m_nBorderWidth/2;
        borderRect.DeflateRect(borderOffset, borderOffset);

        if (m_bRoundedCorners) {
            dc.RoundRect(&borderRect, CPoint(m_nTopLeftRadius, m_nTopLeftRadius));
        }
        else {
            dc.Rectangle(&borderRect);
        }

        dc.SelectObject(pOldPen);
    }

    // 4. 绘制背景
    CRect backgroundRect = rect;
    if (m_nBorderWidth>0) {
        backgroundRect.DeflateRect(m_nBorderWidth, m_nBorderWidth);
    }

    CBrush brush(m_crBkColor);
    if (m_bRoundedCorners) {
        CRgn rgn;
        CreateRoundedRegion(rgn, backgroundRect);
        dc.SelectClipRgn(&rgn);  // 设置裁剪区域为圆角
        dc.FillRect(&backgroundRect, &brush);
        dc.SelectClipRgn(nullptr); // 重置裁剪区域
    }
    else {
        dc.FillRect(&backgroundRect, &brush);
    }

    // 5. 绘制文本
    CRect textRect = backgroundRect; // 文本区域与背景区域一致
    CFont* pOldFont = dc.SelectObject(&m_font);
    dc.SetTextColor(m_crText);
    dc.SetBkMode(TRANSPARENT);

    UINT format = DT_SINGLELINE|DT_VCENTER|DT_END_ELLIPSIS;
    if (m_alignment == AlignCenter)
        format |= DT_CENTER;
    else if (m_alignment == AlignLeft)
        format |= DT_LEFT;
    else if (m_alignment == AlignRight)
        format |= DT_RIGHT;

    CString text;
    GetWindowText(text);
    dc.DrawText(text, &textRect, format);

    dc.SelectObject(pOldFont);
}

HBRUSH CLabel::CtlColor(CDC* pDC, UINT nCtlColor)
{
    if (nCtlColor == CTLCOLOR_STATIC) {
        pDC->SetTextColor(m_crText);
        pDC->SetBkMode(TRANSPARENT);
        return m_hBrush;
    }

    return NULL;
}

void CLabel::OnTimer(UINT_PTR nIDEvent)
{
    m_bState = !m_bState;

    switch (m_Type)
    {
    case Text:
        if (m_bState)
            SetWindowText(_T(""));
        else
            SetWindowText(m_strText);
        break;

    case Background:
        InvalidateRect(NULL, FALSE);
        UpdateWindow();
        break;
    }

    CStatic::OnTimer(nIDEvent);
}

void CLabel::OnLButtonDown(UINT nFlags, CPoint point)
{
    if (m_clickCallback) {
        m_clickCallback();
    }

    if (m_bLink) {
        CString strLink;
        GetWindowText(strLink);
        ShellExecute(NULL, _T("open"), strLink, NULL, NULL, SW_SHOWNORMAL);
    }

    CStatic::OnLButtonDown(nFlags, point);
}

void CLabel::OnMouseMove(UINT nFlags, CPoint point)
{
    CRect rect;
    GetClientRect(&rect);

    // 检测鼠标是否进入控件区域
    BOOL bMouseInNow = rect.PtInRect(point);
    if (bMouseInNow && !m_bMouseIn) {
        // 鼠标刚进入控件,设置手型光标
        m_bMouseIn = TRUE;
        ::SetCursor(m_hHandCursor);

        // 追踪鼠标离开事件
        TRACKMOUSEEVENT tme = { sizeof(TRACKMOUSEEVENT), TME_LEAVE, m_hWnd, 0 };
        TrackMouseEvent(&tme);
    }

    CStatic::OnMouseMove(nFlags, point);
}

void CLabel::OnMouseLeave()
{
    // 恢复默认光标
    m_bMouseIn = FALSE;
    ::SetCursor(m_hDefaultCursor);
    Invalidate();
}

BOOL CLabel::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
	// 如果设置了超链接光标,则使用该光标
    if (m_hCursor) {
        ::SetCursor(m_hCursor);
        return TRUE;
    }

	// 如果鼠标在控件上,则使用手型光标
    if (m_bMouseIn && m_clickCallback)
    {
        ::SetCursor(m_hHandCursor);
        return TRUE;
    }

	// 否则使用默认光标
    ::SetCursor(m_hDefaultCursor);

    return CStatic::OnSetCursor(pWnd, nHitTest, message);
}

示例用法

以下是使用 CLabel 控件的示例代码:

cpp 复制代码
CLabel myLabel;

// 设置属性
myLabel.SetText(_T("Hello, World!"))
       .SetTextColor(RGB(0, 0, 0))
       .SetBkColor(RGB(240, 240, 240))
       .SetBorderColor(RGB(255, 0, 0))
       .SetBorderWidth(2)
       .SetRoundedCorners(TRUE, 15)
       .SetAlignment(AlignCenter);

// 设置点击回调
myLabel.SetClickCallback([]() {
    AfxMessageBox(_T("Label clicked!"));
});

消息映射宏

在 MFC 中,消息映射机制使用了一系列的宏来处理 Windows 消息。这些宏将消息分发到对应的消息处理函数,从而实现特定的功能逻辑。以下是 CLabel 中使用的消息宏的详细说明,以及它们的功能与实现方式。

消息映射宏简介

  1. DECLARE_MESSAGE_MAP()

    • 定义一个类的消息映射表。
    • 通常放在类声明的 protectedprivate 部分。
    • 配合 BEGIN_MESSAGE_MAPEND_MESSAGE_MAP 使用。
  2. BEGIN_MESSAGE_MAP(类名, 基类名)

    • 声明消息映射的起始部分。
    • 指定当前类名和其基类名。
  3. **ON_ 系列宏***:

    • 用于将特定的 Windows 消息映射到处理函数。
  4. END_MESSAGE_MAP()

    • 声明消息映射的结束部分。

CLabel 的消息映射宏使用

以下是 CLabel 中的消息映射宏以及功能说明:

消息映射表

cpp 复制代码
BEGIN_MESSAGE_MAP(CLabel, CStatic)
    ON_WM_PAINT()               // 绘制消息
    ON_WM_CTLCOLOR()            // 控件颜色设置消息
    ON_WM_TIMER()               // 定时器消息
    ON_WM_LBUTTONDOWN()         // 鼠标左键按下消息
    ON_WM_MOUSEMOVE()           // 鼠标移动消息
    ON_WM_MOUSELEAVE()          // 鼠标离开控件区域消息
    ON_WM_SETCURSOR()           // 设置光标消息
END_MESSAGE_MAP()

消息宏功能说明

ON_WM_PAINT()

  • 映射 WM_PAINT 消息到 OnPaint 函数。
  • 功能 :当控件需要重绘时调用 OnPaint,实现自定义绘制逻辑。
cpp 复制代码
void CLabel::OnPaint()
{
    // 自定义绘制代码
}

ON_WM_CTLCOLOR()

  • 映射 WM_CTLCOLOR 消息到 CtlColor 函数。
  • 功能:设置控件的背景颜色和文本颜色。
cpp 复制代码
HBRUSH CLabel::CtlColor(CDC* pDC, UINT nCtlColor)
{
    pDC->SetTextColor(m_crText);       // 设置文本颜色
    pDC->SetBkColor(m_crBkColor);      // 设置背景颜色
    return m_hBrush;                   // 返回背景画刷
}

ON_WM_TIMER()

  • 映射 WM_TIMER 消息到 OnTimer 函数。
  • 功能:处理定时器事件,例如实现闪烁效果。
cpp 复制代码
void CLabel::OnTimer(UINT_PTR nIDEvent)
{
    if (nIDEvent == 1) // 假设定时器 ID 为 1
    {
        // 处理定时器事件
        FlashBackground(TRUE);
    }
    CStatic::OnTimer(nIDEvent);
}

ON_WM_LBUTTONDOWN()

  • 映射 WM_LBUTTONDOWN 消息到 OnLButtonDown 函数。
  • 功能:处理鼠标左键点击事件,调用点击回调函数。
cpp 复制代码
void CLabel::OnLButtonDown(UINT nFlags, CPoint point)
{
    if (m_clickCallback)
        m_clickCallback(); // 调用回调
    CStatic::OnLButtonDown(nFlags, point);
}

ON_WM_MOUSEMOVE()

  • 映射 WM_MOUSEMOVE 消息到 OnMouseMove 函数。
  • 功能:处理鼠标移动事件,当鼠标进入控件时切换光标。
cpp 复制代码
void CLabel::OnMouseMove(UINT nFlags, CPoint point)
{
    if (!m_bMouseIn) {
        m_bMouseIn = TRUE;
        TRACKMOUSEEVENT tme = { sizeof(TRACKMOUSEEVENT), TME_LEAVE, m_hWnd, 0 };
        TrackMouseEvent(&tme);      // 跟踪鼠标离开事件
        ::SetCursor(m_hHandCursor); // 切换为手型光标
    }
    CStatic::OnMouseMove(nFlags, point);
}

ON_WM_MOUSELEAVE()

  • 映射 WM_MOUSELEAVE 消息到 OnMouseLeave 函数。
  • 功能:处理鼠标离开控件事件,恢复默认光标。
cpp 复制代码
void CLabel::OnMouseLeave()
{
    m_bMouseIn = FALSE;
    ::SetCursor(m_hDefaultCursor); // 恢复默认光标
}

ON_WM_SETCURSOR()

  • 映射 WM_SETCURSOR 消息到 OnSetCursor 函数。
  • 功能:设置鼠标指针形状。
cpp 复制代码
BOOL CLabel::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
    ::SetCursor(m_bMouseIn ? m_hHandCursor : m_hDefaultCursor); // 根据鼠标状态设置光标
    return TRUE;
}

关键功能实现

CLabel 提供了多种自定义功能,这里详细介绍各功能的具体实现,包括关键代码和逻辑解释。

文本样式设置

实现功能:支持设置字体名称、大小、加粗、下划线、斜体等文本样式。

实现方法

  • 在类中定义 LOGFONTCFont 对象:
cpp 复制代码
LOGFONT   m_lf;    // 用于存储字体信息
CFont     m_font;  // 实际字体对象
  • 提供修改字体样式的接口:
cpp 复制代码
CLabel& CLabel::SetFontBold(BOOL bBold)
{
    m_lf.lfWeight = bBold ? FW_BOLD : FW_NORMAL;
    ReconstructFont();
    return *this;
}

CLabel& CLabel::SetFontItalic(BOOL bSet)
{
    m_lf.lfItalic = bSet;
    ReconstructFont();
    return *this;
}

CLabel& CLabel::SetFontName(const CString& strFont)
{
    _tcscpy_s(m_lf.lfFaceName, strFont);
    ReconstructFont();
    return *this;
}

CLabel& CLabel::SetFontSize(int nSize)
{
    m_lf.lfHeight = -MulDiv(nSize, GetDeviceCaps(GetDC()->m_hDC, LOGPIXELSY), 72);
    ReconstructFont();
    return *this;
}
  • 使用 ReconstructFont 更新字体:
cpp 复制代码
void CLabel::ReconstructFont()
{
    m_font.DeleteObject();                // 删除旧字体
    m_font.CreateFontIndirect(&m_lf);     // 创建新字体
    Invalidate();                         // 触发重绘
}

背景样式设置

实现功能:支持设置背景颜色,绘制时填充控件区域。

实现方法

  • 定义背景颜色属性:
cpp 复制代码
COLORREF m_crBkColor; // 背景颜色
  • 提供接口设置背景颜色:
cpp 复制代码
CLabel& CLabel::SetBkColor(COLORREF crBkgnd)
{
    m_crBkColor = crBkgnd;
    Invalidate();
    return *this;
}
  • OnPaint 中绘制背景:
cpp 复制代码
CBrush brush(m_crBkColor);
if (m_bRoundedCorners) {
    CRgn rgn;
    CreateRoundedRegion(rgn, rect);
    dc.FillRgn(&rgn, &brush);         // 绘制圆角背景
}
else {
    dc.FillRect(&rect, &brush);       // 绘制普通矩形背景
}

边框设置

实现功能:支持设置边框颜色、宽度和样式。

实现方法

  • 定义边框属性:
cpp 复制代码
COLORREF m_crBorderColor; // 边框颜色
int      m_nBorderWidth;  // 边框宽度
int      m_nBorderStyle;  // 边框样式(PS_SOLID 等)
  • 提供接口设置边框属性:
cpp 复制代码
CLabel& CLabel::SetBorderColor(COLORREF crBorder)
{
    m_crBorderColor = crBorder;
    Invalidate();
    return *this;
}

CLabel& CLabel::SetBorderWidth(int nWidth)
{
    m_nBorderWidth = nWidth;
    Invalidate();
    return *this;
}

CLabel& CLabel::SetBorderStyle(int nStyle)
{
    m_nBorderStyle = nStyle;
    Invalidate();
    return *this;
}

CLabel& CLabel::DisableBorder()
{
    m_nBorderWidth = 0;
    Invalidate();
    return *this;
}
  • OnPaint 中绘制边框:
cpp 复制代码
if (m_nBorderWidth > 0) {
    CPen pen(m_nBorderStyle, m_nBorderWidth, m_crBorderColor);
    CPen* pOldPen = dc.SelectObject(&pen);

    CRect borderRect = rect;
    borderRect.DeflateRect(m_nBorderWidth / 2, m_nBorderWidth / 2);
    if (m_bRoundedCorners) {
        dc.RoundRect(&borderRect, CPoint(m_nTopLeftRadius, m_nTopLeftRadius));
    }
    else {
        dc.Rectangle(&borderRect);
    }

    dc.SelectObject(pOldPen);
}

圆角设置

实现功能:支持启用圆角,并可单独设置每个角的半径。

实现方法

  • 定义圆角属性:
cpp 复制代码
BOOL m_bRoundedCorners;       // 是否启用圆角
int  m_nTopLeftRadius;        // 左上角半径
int  m_nTopRightRadius;       // 右上角半径
int  m_nBottomRightRadius;    // 右下角半径
int  m_nBottomLeftRadius;     // 左下角半径
  • 提供接口设置圆角:
cpp 复制代码
CLabel& CLabel::SetRoundedCorners(BOOL bEnable, int nRadius)
{
    m_bRoundedCorners = bEnable;
    m_nTopLeftRadius = m_nTopRightRadius = m_nBottomRightRadius = m_nBottomLeftRadius = nRadius;
    Invalidate();
    return *this;
}

CLabel& CLabel::SetCornerRadius(int nTopLeft, int nTopRight, int nBottomRight, int nBottomLeft)
{
    m_nTopLeftRadius = nTopLeft;
    m_nTopRightRadius = nTopRight;
    m_nBottomRightRadius = nBottomRight;
    m_nBottomLeftRadius = nBottomLeft;
    Invalidate();
    return *this;
}
  • 在绘制背景和边框时应用圆角:
cpp 复制代码
void CLabel::CreateRoundedRegion(CRgn& rgn, const CRect& rect)
{
    rgn.CreateRoundRectRgn(
        rect.left, rect.top,
        rect.right + 1, rect.bottom + 1,
        m_nTopLeftRadius, m_nTopLeftRadius
    );
}

鼠标事件与交互

实现功能:支持点击事件回调、手型光标显示等。

实现方法

  • 定义鼠标事件相关属性:
cpp 复制代码
BOOL    m_bMouseIn;                       // 鼠标是否在控件上
HCURSOR m_hHandCursor;                    // 手型光标
HCURSOR m_hDefaultCursor;                 // 默认光标
std::function<void()> m_clickCallback;    // 点击事件的回调函数
  • 提供接口设置点击回调和光标:
cpp 复制代码
void CLabel::SetClickCallback(std::function<void()> callback)
{
    m_clickCallback = callback;
}

CLabel& CLabel::SetHandCursor(HCURSOR hCursor)
{
    m_hHandCursor = hCursor;
    return *this;
}

CLabel& CLabel::SetDefaultCursor(HCURSOR hCursor)
{
    m_hDefaultCursor = hCursor;
    return *this;
}
  • 实现鼠标事件:
cpp 复制代码
void CLabel::OnMouseMove(UINT nFlags, CPoint point)
{
    if (!m_bMouseIn) {
        m_bMouseIn = TRUE;
        TRACKMOUSEEVENT tme = { sizeof(TRACKMOUSEEVENT), TME_LEAVE, m_hWnd, 0 };
        TrackMouseEvent(&tme);
        ::SetCursor(m_hHandCursor);
    }
    CStatic::OnMouseMove(nFlags, point);
}

void CLabel::OnMouseLeave()
{
    m_bMouseIn = FALSE;
    ::SetCursor(m_hDefaultCursor);
}

BOOL CLabel::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
    ::SetCursor(m_bMouseIn ? m_hHandCursor : m_hDefaultCursor);
    return TRUE;
}

void CLabel::OnLButtonDown(UINT nFlags, CPoint point)
{
    if (m_clickCallback)
        m_clickCallback();
    CStatic::OnLButtonDown(nFlags, point);
}

优化

抗锯齿

在 Windows 中,实现抗锯齿(平滑绘制圆角)需要使用 GDI+ ,因为传统的 GDI 不支持抗锯齿功能。GDI+ 提供了更强大的图形绘制能力,可以通过设置 SmoothingMode 来启用抗锯齿。

以下是绘制代码改造为使用 GDI+ 实现抗锯齿功能:

添加 GDI+ 初始化

在代码中需要包含 GDI+ 的头文件,并确保 GDI+ 已正确初始化:

cpp 复制代码
#include <gdiplus.h>
using namespace Gdiplus;

// 添加一个静态初始化器类(推荐在全局初始化)
class GdiplusInitializer
{
public:
    GdiplusInitializer()
    {
        GdiplusStartup(&m_gdiplusToken, &m_gdiplusStartupInput, nullptr);
    }
    ~GdiplusInitializer()
    {
        GdiplusShutdown(m_gdiplusToken);
    }
private:
    GdiplusStartupInput m_gdiplusStartupInput;
    ULONG_PTR m_gdiplusToken;
};

// 在应用程序入口初始化 GDI+:
static GdiplusInitializer gdiplusInit;

修改 OnPaint 使用 GDI+

将原始的 GDI 绘图逻辑改为 GDI+:

cpp 复制代码
void CLabel::OnPaint()
{
    CPaintDC dc(this);     // GDI 设备上下文
    Graphics graphics(dc); // GDI+ 图形对象

    // 启用抗锯齿
    graphics.SetSmoothingMode(SmoothingModeAntiAlias);

    // 1. 获取控件区域
    CRect rect;
    GetClientRect(&rect);

    // 2. 获取对话框背景颜色并清除整个控件区域
    COLORREF dlgBkColor = ::GetSysColor(COLOR_BTNFACE);
    SolidBrush clearBrush(Color(GetRValue(dlgBkColor), GetGValue(dlgBkColor), GetBValue(dlgBkColor)));
    graphics.FillRectangle(&clearBrush, rect.left, rect.top, rect.Width(), rect.Height());

    // 3. 绘制边框
    if (m_nBorderWidth > 0) {
        Pen borderPen(Color(GetRValue(m_crBorderColor), GetGValue(m_crBorderColor), GetBValue(m_crBorderColor)), m_nBorderWidth);

        if (m_bRoundedCorners) {
            GraphicsPath path;
            path.AddArc(rect.left, rect.top, m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, 180, 90); // 左上角
            path.AddArc(rect.right - m_nTopLeftRadius * 2, rect.top, m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, 270, 90); // 右上角
            path.AddArc(rect.right - m_nTopLeftRadius * 2, rect.bottom - m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, 0, 90); // 右下角
            path.AddArc(rect.left, rect.bottom - m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, 90, 90); // 左下角
            path.CloseFigure();

            graphics.DrawPath(&borderPen, &path);
        }
        else {
            graphics.DrawRectangle(&borderPen, rect.left, rect.top, rect.Width(), rect.Height());
        }
    }

    // 4. 绘制背景
    CRect backgroundRect = rect;
    if (m_nBorderWidth > 0) {
        backgroundRect.DeflateRect(m_nBorderWidth, m_nBorderWidth);
    }

    SolidBrush backgroundBrush(Color(GetRValue(m_crBkColor), GetGValue(m_crBkColor), GetBValue(m_crBkColor)));
    if (m_bRoundedCorners) {
        GraphicsPath path;
        path.AddArc(backgroundRect.left, backgroundRect.top, m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, 180, 90);
        path.AddArc(backgroundRect.right - m_nTopLeftRadius * 2, backgroundRect.top, m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, 270, 90);
        path.AddArc(backgroundRect.right - m_nTopLeftRadius * 2, backgroundRect.bottom - m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, 0, 90);
        path.AddArc(backgroundRect.left, backgroundRect.bottom - m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, m_nTopLeftRadius * 2, 90, 90);
        path.CloseFigure();

        graphics.FillPath(&backgroundBrush, &path);
    }
    else {
        graphics.FillRectangle(&backgroundBrush, backgroundRect.left, backgroundRect.top, backgroundRect.Width(), backgroundRect.Height());
    }

    // 5. 绘制文本
    CRect textRect = backgroundRect;
    CString text;
    GetWindowText(text);

    FontFamily fontFamily(L"Arial"); // 示例字体
    Font font(&fontFamily, 14, FontStyleRegular, UnitPixel); // 示例字体大小
    SolidBrush textBrush(Color(GetRValue(m_crText), GetGValue(m_crText), GetBValue(m_crText)));

    RectF layoutRect(static_cast<REAL>(textRect.left), static_cast<REAL>(textRect.top),
                     static_cast<REAL>(textRect.Width()), static_cast<REAL>(textRect.Height()));

    StringFormat format;
    if (m_alignment == AlignCenter)
        format.SetAlignment(StringAlignmentCenter);
    else if (m_alignment == AlignLeft)
        format.SetAlignment(StringAlignmentNear);
    else if (m_alignment == AlignRight)
        format.SetAlignment(StringAlignmentFar);

    format.SetLineAlignment(StringAlignmentCenter);
    graphics.DrawString(text, -1, &font, layoutRect, &format, &textBrush);
}

总结

通过对 CLabel 自定义控件的实现,我们探索了 MFC 中如何通过合理的设计和实现来满足复杂的视觉和交互需求。本控件支持 边框背景文字 的高度定制,且处理了多个技术细节,如绘制优先级、动态区域裁剪和鼠标交互等。以下是本控件的核心设计总结和实现过程反思。


功能优先级设计

CLabel 的绘制优先级遵循严格的逻辑,确保视觉效果清晰且各元素独立:

  1. 清除控件区域

    • 在绘制任何内容之前,清除控件区域是必要的。这可以防止残留上一次的绘制内容,确保控件的外观整洁。
    • 使用对话框背景色 (COLOR_BTNFACE) 统一填充整个控件区域,使控件与周围环境保持一致。
  2. 绘制边框

    • 边框的绘制优先级高于背景和文字,目的是让边框包裹背景和文字,构成控件的外观。
    • 通过 DeflateRect 缩小边框区域,确保边框不会超出控件的显示范围。
    • 当启用圆角时,边框绘制逻辑采用 RoundRect 实现,以适配圆角形状。
  3. 绘制背景

    • 背景是文字的承载区域,其优先级高于文字但低于边框。
    • 通过裁剪区域 (CRgnSelectClipRgn) 实现圆角背景的绘制,同时保持与边框形状一致。
    • 背景区域进一步通过 DeflateRect 缩小,确保不会覆盖边框。
  4. 绘制文字

    • 文字是控件的核心信息,最终显示在背景之上。
    • 为防止文字越界,其绘制区域严格限制在背景范围内。
    • 使用透明模式 (SetBkMode(TRANSPARENT)) 确保背景不会覆盖文字,同时通过文本对齐设置 (如居中、左对齐等) 提高视觉效果。

技术细节优化

  1. 动态裁剪与区域管理

    • 使用 CRgn 对背景区域进行裁剪,从而实现圆角背景。
    • 同一裁剪逻辑被用于边框和背景,确保二者的形状和尺寸完全一致。
    • 区域的动态调整由 DeflateRect 完成,以适配边框宽度和圆角半径。
  2. 抗锯齿的应用

    • 圆角绘制采用 GDI 的 RoundRect,为提高圆角平滑度,可以结合 GDI+(如启用 SmoothingModeAntiAlias)。
    • 使用 GDI+ 的 GraphicsGraphicsPath,实现高质量的抗锯齿效果,进一步提升控件外观。
  3. 动态属性调整

    • 提供多个接口方法,用于动态设置控件的边框颜色、宽度、背景颜色、字体样式、文字对齐等属性。
    • 属性设置后自动触发 Invalidate,实现实时重绘,保证控件状态的动态响应。
  4. 鼠标交互与事件支持

    • 通过鼠标消息(如 WM_MOUSEMOVEWM_MOUSELEAVE),实现了动态光标切换和点击事件。
    • 提供回调接口,允许开发者绑定自定义点击逻辑,增强控件的交互能力。

绘制流程总结

CLabel 的绘制过程分为以下几个步骤,每一步都对应清晰的逻辑:

  1. 清除控件区域

    • 填充整个控件区域以清除上一次绘制内容。
    • 统一使用对话框背景颜色,确保控件与周围环境的融合。
  2. 绘制边框

    • 通过 DeflateRect 缩小边框区域,确保边框完全位于控件内。
    • 当启用圆角时,使用 RoundRect 绘制平滑的圆角边框。
  3. 绘制背景

    • 背景填充在边框内,绘制区域进一步缩小以适应边框。
    • 当启用圆角时,使用裁剪区域限制背景形状,确保背景与边框一致。
  4. 绘制文字

    • 文字绘制在背景之上,且严格限制在背景区域内。
    • 通过文本对齐设置(如左对齐、居中、右对齐)优化文字显示效果。

问题与解决方案

  1. 背景覆盖边框

    • 问题原因:背景区域未正确缩小,导致覆盖边框。
    • 解决方法:使用 DeflateRect 调整背景区域大小,使其小于边框区域。
  2. 文字越界

    • 问题原因:文字绘制区域与控件区域一致,可能超出背景范围。
    • 解决方法:限制文字绘制区域与背景区域一致,确保文字不会超出背景。
  3. 圆角边框和背景不匹配

    • 问题原因:边框和背景的圆角裁剪逻辑不一致。
    • 解决方法:统一使用 CRgn 和圆角半径属性 (m_nTopLeftRadius 等) 确保二者一致。
  4. 抗锯齿效果不足

    • 问题原因:传统 GDI 的绘制方法不支持抗锯齿。
    • 解决方法:引入 GDI+,启用抗锯齿模式 (SmoothingModeAntiAlias) 提升绘图质量。

优化与扩展方向

  1. 支持渐变背景

    • 可通过 GDI+ 的 LinearGradientBrush 支持渐变背景效果。
  2. 增强动画支持

    • 在现有闪烁功能基础上,增加边框或文字的动态变化(如颜色渐变动画)。
  3. 响应式控件

    • 在字体和控件大小调整时,动态调整文字、边框和背景的尺寸,提升响应式设计的适配能力。

最终效果与意义

  • CLabel 提供了高度可定制的标签控件解决方案,适用于需要复杂样式和交互功能的场景。
  • 通过合理的绘制优先级和区域管理,控件实现了边框、背景和文字的无缝匹配。
  • 引入抗锯齿技术,提升了控件的视觉质量,增强了用户体验。
  • 鼠标交互和点击事件的支持,使得控件不仅具备静态外观,还具备动态行为。

这篇文章既是 CLabel 开发的完整记录,也是 MFC 自定义控件开发的一个参考模板。通过这次实现,我们更深入地了解了 MFC 的绘图机制及其在复杂控件开发中的应用。

相关推荐
妈妈说名字太长显傻几秒前
【C++】string类
开发语言·c++
丢丢丢丢丢丢~3 分钟前
c++创建每日文件夹,放入每日日志
开发语言·c++
華華3556 分钟前
读程序题...
开发语言·c++·算法
Android_chunhui44 分钟前
向量检索原理
c++·搜索引擎·全文检索
一行玩python1 小时前
Xerces-C,一个成熟的 C++ XML 解析库!
xml·c语言·开发语言·c++
Octopus20772 小时前
【C++】AVL树
开发语言·c++·笔记·学习
荒古前2 小时前
小发现,如何高级的顺序输出,逆序输出整数的每一位(栈,队列)
数据结构·c++·算法
奶香臭豆腐3 小时前
C++ 泛编程 —— 函数模板(中)
开发语言·c++·学习
蜗牛hb3 小时前
C++是如何工作的?
开发语言·c++