在开发中,标准控件功能往往无法满足特定的业务需求。这篇博客实现了高度可定制的 MFC 自定义控件 CLabel
,支持背景、文本、边框及圆角设置,提供更丰富的视觉效果和交互体验。
功能概览
CLabel
是基于 MFC CStatic
控件实现的增强型标签控件,支持以下功能:
-
文本样式设置:
- 自定义字体、颜色、大小、对齐方式等。
- 支持加粗、斜体和下划线。
-
背景样式设置:
- 支持设置背景颜色。
-
边框设置:
- 支持设置边框颜色、宽度、样式,支持禁用边框。
-
圆角设置:
- 支持启用圆角,支持单独设置四个角的圆角半径。
-
交互特性:
- 支持鼠标事件处理,显示手型光标并触发点击事件回调。
代码实现
头文件:
以下是 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
中使用的消息宏的详细说明,以及它们的功能与实现方式。
消息映射宏简介
-
DECLARE_MESSAGE_MAP():
- 定义一个类的消息映射表。
- 通常放在类声明的
protected
或private
部分。 - 配合
BEGIN_MESSAGE_MAP
和END_MESSAGE_MAP
使用。
-
BEGIN_MESSAGE_MAP(类名, 基类名):
- 声明消息映射的起始部分。
- 指定当前类名和其基类名。
-
**ON_ 系列宏***:
- 用于将特定的 Windows 消息映射到处理函数。
-
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
提供了多种自定义功能,这里详细介绍各功能的具体实现,包括关键代码和逻辑解释。
文本样式设置
实现功能:支持设置字体名称、大小、加粗、下划线、斜体等文本样式。
实现方法
- 在类中定义
LOGFONT
和CFont
对象:
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
的绘制优先级遵循严格的逻辑,确保视觉效果清晰且各元素独立:
-
清除控件区域:
- 在绘制任何内容之前,清除控件区域是必要的。这可以防止残留上一次的绘制内容,确保控件的外观整洁。
- 使用对话框背景色 (
COLOR_BTNFACE
) 统一填充整个控件区域,使控件与周围环境保持一致。
-
绘制边框:
- 边框的绘制优先级高于背景和文字,目的是让边框包裹背景和文字,构成控件的外观。
- 通过
DeflateRect
缩小边框区域,确保边框不会超出控件的显示范围。 - 当启用圆角时,边框绘制逻辑采用
RoundRect
实现,以适配圆角形状。
-
绘制背景:
- 背景是文字的承载区域,其优先级高于文字但低于边框。
- 通过裁剪区域 (
CRgn
和SelectClipRgn
) 实现圆角背景的绘制,同时保持与边框形状一致。 - 背景区域进一步通过
DeflateRect
缩小,确保不会覆盖边框。
-
绘制文字:
- 文字是控件的核心信息,最终显示在背景之上。
- 为防止文字越界,其绘制区域严格限制在背景范围内。
- 使用透明模式 (
SetBkMode(TRANSPARENT)
) 确保背景不会覆盖文字,同时通过文本对齐设置 (如居中、左对齐等) 提高视觉效果。
技术细节优化
-
动态裁剪与区域管理:
- 使用
CRgn
对背景区域进行裁剪,从而实现圆角背景。 - 同一裁剪逻辑被用于边框和背景,确保二者的形状和尺寸完全一致。
- 区域的动态调整由
DeflateRect
完成,以适配边框宽度和圆角半径。
- 使用
-
抗锯齿的应用:
- 圆角绘制采用 GDI 的
RoundRect
,为提高圆角平滑度,可以结合 GDI+(如启用SmoothingModeAntiAlias
)。 - 使用 GDI+ 的
Graphics
和GraphicsPath
,实现高质量的抗锯齿效果,进一步提升控件外观。
- 圆角绘制采用 GDI 的
-
动态属性调整:
- 提供多个接口方法,用于动态设置控件的边框颜色、宽度、背景颜色、字体样式、文字对齐等属性。
- 属性设置后自动触发
Invalidate
,实现实时重绘,保证控件状态的动态响应。
-
鼠标交互与事件支持:
- 通过鼠标消息(如
WM_MOUSEMOVE
和WM_MOUSELEAVE
),实现了动态光标切换和点击事件。 - 提供回调接口,允许开发者绑定自定义点击逻辑,增强控件的交互能力。
- 通过鼠标消息(如
绘制流程总结
CLabel
的绘制过程分为以下几个步骤,每一步都对应清晰的逻辑:
-
清除控件区域:
- 填充整个控件区域以清除上一次绘制内容。
- 统一使用对话框背景颜色,确保控件与周围环境的融合。
-
绘制边框:
- 通过
DeflateRect
缩小边框区域,确保边框完全位于控件内。 - 当启用圆角时,使用
RoundRect
绘制平滑的圆角边框。
- 通过
-
绘制背景:
- 背景填充在边框内,绘制区域进一步缩小以适应边框。
- 当启用圆角时,使用裁剪区域限制背景形状,确保背景与边框一致。
-
绘制文字:
- 文字绘制在背景之上,且严格限制在背景区域内。
- 通过文本对齐设置(如左对齐、居中、右对齐)优化文字显示效果。
问题与解决方案
-
背景覆盖边框:
- 问题原因:背景区域未正确缩小,导致覆盖边框。
- 解决方法:使用
DeflateRect
调整背景区域大小,使其小于边框区域。
-
文字越界:
- 问题原因:文字绘制区域与控件区域一致,可能超出背景范围。
- 解决方法:限制文字绘制区域与背景区域一致,确保文字不会超出背景。
-
圆角边框和背景不匹配:
- 问题原因:边框和背景的圆角裁剪逻辑不一致。
- 解决方法:统一使用
CRgn
和圆角半径属性 (m_nTopLeftRadius
等) 确保二者一致。
-
抗锯齿效果不足:
- 问题原因:传统 GDI 的绘制方法不支持抗锯齿。
- 解决方法:引入 GDI+,启用抗锯齿模式 (
SmoothingModeAntiAlias
) 提升绘图质量。
优化与扩展方向
-
支持渐变背景:
- 可通过 GDI+ 的
LinearGradientBrush
支持渐变背景效果。
- 可通过 GDI+ 的
-
增强动画支持:
- 在现有闪烁功能基础上,增加边框或文字的动态变化(如颜色渐变动画)。
-
响应式控件:
- 在字体和控件大小调整时,动态调整文字、边框和背景的尺寸,提升响应式设计的适配能力。
最终效果与意义
CLabel
提供了高度可定制的标签控件解决方案,适用于需要复杂样式和交互功能的场景。- 通过合理的绘制优先级和区域管理,控件实现了边框、背景和文字的无缝匹配。
- 引入抗锯齿技术,提升了控件的视觉质量,增强了用户体验。
- 鼠标交互和点击事件的支持,使得控件不仅具备静态外观,还具备动态行为。
这篇文章既是 CLabel
开发的完整记录,也是 MFC 自定义控件开发的一个参考模板。通过这次实现,我们更深入地了解了 MFC 的绘图机制及其在复杂控件开发中的应用。