自定义MFC控件:从原理到实践

自定义MFC控件:从原理到实践

一、MFC控件体系概览

1.1 MFC控件继承层次

1
2
3
4
5
6
7
8
9
CObject
└── CCmdTarget
└── CWnd
├── CStatic
├── CButton
├── CEdit
├── CListBox
├── CComboBox
└── CMyCustomControl (我们的自定义控件)

1.2 MFC窗口创建机制

MFC控件的创建遵循Windows API的窗口创建流程,但通过MFC类进行封装:

1
2
3
4
5
// 窗口创建的核心流程
1. 注册窗口类 (RegisterClassEx)
2. 创建窗口实例 (CreateWindowEx)
3. 消息循环处理
4. 窗口销毁

二、创建自定义MFC控件

2.1 基础控件类定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// MyCustomControl.h
#pragma once

class CMyCustomControl : public CWnd
{
DECLARE_DYNCREATE(CMyCustomControl)

public:
CMyCustomControl();
virtual ~CMyCustomControl();

// 创建控件
BOOL Create(LPCTSTR lpszWindowName,
DWORD dwStyle,
const RECT& rect,
CWnd* pParentWnd,
UINT nID);

protected:
// 消息映射
DECLARE_MESSAGE_MAP()

// 重写虚函数
virtual void PreSubclassWindow();
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);

// 消息处理函数
afx_msg void OnPaint();
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);

// 自定义绘制函数
virtual void DrawBackground(CDC* pDC);
virtual void DrawContent(CDC* pDC);

// 自定义属性
COLORREF m_bkColor;
COLORREF m_textColor;
CString m_strText;
CRect m_clientRect;

// 状态标志
BOOL m_bMouseOver;
BOOL m_bPressed;

private:
// 初始化
void Initialize();
};

2.2 实现文件详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
// MyCustomControl.cpp
#include "stdafx.h"
#include "MyCustomControl.h"

IMPLEMENT_DYNCREATE(CMyCustomControl, CWnd)

// 消息映射表
BEGIN_MESSAGE_MAP(CMyCustomControl, CWnd)
ON_WM_PAINT()
ON_WM_SIZE()
ON_WM_MOUSEMOVE()
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_ERASEBKGND()
END_MESSAGE_MAP()

CMyCustomControl::CMyCustomControl()
{
Initialize();
}

CMyCustomControl::~CMyCustomControl()
{
// 清理资源
}

void CMyCustomControl::Initialize()
{
// 默认属性
m_bkColor = RGB(240, 240, 240);
m_textColor = RGB(0, 0, 0);
m_strText = _T("自定义控件");
m_bMouseOver = FALSE;
m_bPressed = FALSE;

// 注册窗口类
CString className = AfxRegisterWndClass(
CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW,
::LoadCursor(NULL, IDC_ARROW),
(HBRUSH)(COLOR_BTNFACE + 1),
NULL
);

// 保存类名供后续使用
m_strClassName = className;
}

BOOL CMyCustomControl::Create(LPCTSTR lpszWindowName,
DWORD dwStyle,
const RECT& rect,
CWnd* pParentWnd,
UINT nID)
{
// 调用基类CreateEx创建窗口
return CWnd::CreateEx(
WS_EX_CLIENTEDGE, // 扩展样式
m_strClassName, // 注册的窗口类名
lpszWindowName, // 窗口标题
dwStyle, // 窗口样式
rect, // 位置和大小
pParentWnd, // 父窗口
nID // 控件ID
);
}

void CMyCustomControl::PreSubclassWindow()
{
// 在窗口被子类化之前调用
// 可以在这里进行初始化工作
CWnd::PreSubclassWindow();

// 获取初始大小
GetClientRect(&m_clientRect);
}

BOOL CMyCustomControl::PreCreateWindow(CREATESTRUCT& cs)
{
// 在窗口创建之前调用
// 可以修改CREATESTRUCT结构
if (!CWnd::PreCreateWindow(cs))
return FALSE;

// 设置窗口类名
cs.lpszClass = m_strClassName;

// 确保有合适的样式
cs.style |= WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS;

return TRUE;
}

void CMyCustomControl::OnPaint()
{
CPaintDC dc(this); // 设备上下文

// 双缓冲绘图防止闪烁
CRect rect;
GetClientRect(&rect);

CDC memDC;
CBitmap bitmap;
CBitmap* pOldBitmap = NULL;

memDC.CreateCompatibleDC(&dc);
bitmap.CreateCompatibleBitmap(&dc, rect.Width(), rect.Height());
pOldBitmap = memDC.SelectObject(&bitmap);

// 绘制背景
DrawBackground(&memDC);

// 绘制内容
DrawContent(&memDC);

// 复制到屏幕
dc.BitBlt(0, 0, rect.Width(), rect.Height(), &memDC, 0, 0, SRCCOPY);

// 清理
memDC.SelectObject(pOldBitmap);
bitmap.DeleteObject();
memDC.DeleteDC();
}

void CMyCustomControl::DrawBackground(CDC* pDC)
{
CRect rect;
GetClientRect(&rect);

// 根据状态绘制背景
if (m_bPressed)
{
// 按下状态
pDC->FillSolidRect(&rect, RGB(200, 200, 220));
pDC->Draw3dRect(&rect, RGB(100, 100, 120), RGB(220, 220, 240));
}
else if (m_bMouseOver)
{
// 鼠标悬停状态
pDC->FillSolidRect(&rect, RGB(220, 220, 240));
pDC->Draw3dRect(&rect, RGB(180, 180, 200), RGB(240, 240, 255));
}
else
{
// 正常状态
pDC->FillSolidRect(&rect, m_bkColor);
pDC->Draw3dRect(&rect, RGB(150, 150, 150), RGB(250, 250, 250));
}
}

void CMyCustomControl::DrawContent(CDC* pDC)
{
CRect rect;
GetClientRect(&rect);

// 设置字体
CFont* pOldFont = NULL;
CFont font;
font.CreatePointFont(100, _T("Arial"));
pOldFont = pDC->SelectObject(&font);

// 设置文本颜色
pDC->SetTextColor(m_textColor);
pDC->SetBkMode(TRANSPARENT);

// 居中绘制文本
pDC->DrawText(m_strText, &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

// 恢复原来的字体
pDC->SelectObject(pOldFont);
}

void CMyCustomControl::OnSize(UINT nType, int cx, int cy)
{
CWnd::OnSize(nType, cx, cy);
GetClientRect(&m_clientRect);

// 触发重绘
Invalidate();
}

void CMyCustomControl::OnMouseMove(UINT nFlags, CPoint point)
{
if (!m_bMouseOver)
{
m_bMouseOver = TRUE;
Invalidate();

// 设置鼠标追踪
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(tme);
tme.dwFlags = TME_LEAVE;
tme.hwndTrack = m_hWnd;
TrackMouseEvent(&tme);
}

CWnd::OnMouseMove(nFlags, point);
}

void CMyCustomControl::OnLButtonDown(UINT nFlags, CPoint point)
{
m_bPressed = TRUE;
SetCapture(); // 捕获鼠标
Invalidate();

CWnd::OnLButtonDown(nFlags, point);
}

void CMyCustomControl::OnLButtonUp(UINT nFlags, CPoint point)
{
if (m_bPressed)
{
m_bPressed = FALSE;
ReleaseCapture(); // 释放鼠标捕获

// 检查是否在控件内释放
CRect rect;
GetClientRect(&rect);
if (rect.PtInRect(point))
{
// 发送通知给父窗口
GetParent()->SendMessage(WM_COMMAND,
MAKEWPARAM(GetDlgCtrlID(), BN_CLICKED),
(LPARAM)m_hWnd);
}

Invalidate();
}

CWnd::OnLButtonUp(nFlags, point);
}

BOOL CMyCustomControl::OnEraseBkgnd(CDC* pDC)
{
// 返回TRUE表示已处理背景擦除,防止闪烁
return TRUE;
}

三、MFC控件工作原理深度解析

3.1 消息映射机制

MFC通过消息映射表将Windows消息路由到对应的处理函数:

1
2
3
4
5
6
7
8
9
10
// MFC内部消息路由流程
1. AfxWndProc() // 全局窗口过程

2. CWnd::WindowProc() // CWnd的窗口过程

3. CWnd::OnWndMsg() // 消息分发

4. CCmdTarget::OnCmdMsg() // 命令消息处理

5. 消息映射表查找对应处理函数

3.2 动态创建机制

1
2
3
4
5
6
7
8
9
10
// DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE宏展开
#define DECLARE_DYNCREATE(class_name) \
DECLARE_DYNAMIC(class_name) \
static CObject* PASCAL CreateObject();

#define IMPLEMENT_DYNCREATE(class_name, base_class_name) \
CObject* PASCAL class_name::CreateObject() \
{ return new class_name; } \
IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, \
wSchema, class_name::CreateObject)

3.3 控件数据交换(DDX/DDV)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 在对话框中使用自定义控件
class CMyDialog : public CDialogEx
{
DECLARE_DYNAMIC(CMyDialog)

public:
CMyDialog(CWnd* pParent = NULL);
virtual ~CMyDialog();

enum { IDD = IDD_MYDIALOG };

// DDX/DDV支持
virtual void DoDataExchange(CDataExchange* pDX);

// 控件变量
CMyCustomControl m_myControl;
CString m_strControlText;

protected:
virtual BOOL OnInitDialog();

private:
// 初始化控件
void InitializeControl();
};

// 在对话框的实现中
void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
// 可以添加自定义控件的DDX支持
DDX_Text(pDX, IDC_MYCONTROL_TEXT, m_strControlText);
}

BOOL CMyDialog::OnInitDialog()
{
CDialogEx::OnInitDialog();

// 创建自定义控件
CRect rect(10, 10, 200, 100);
m_myControl.Create(_T(""), WS_CHILD | WS_VISIBLE,
rect, this, IDC_MYCUSTOMCTRL);

return TRUE;
}

四、高级自定义控件特性

4.1 添加属性支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 属性访问方法
class CMyCustomControl : public CWnd
{
public:
// 属性设置方法
void SetBackgroundColor(COLORREF color)
{
m_bkColor = color;
Invalidate();
}

COLORREF GetBackgroundColor() const
{
return m_bkColor;
}

void SetText(const CString& strText)
{
m_strText = strText;
Invalidate();
}

CString GetText() const
{
return m_strText;
}

// 添加自定义消息
#define WM_MYCUSTOMCTRL_CLICKED (WM_USER + 100)

// 通知父窗口的方法
void NotifyClick()
{
CWnd* pParent = GetParent();
if (pParent)
{
pParent->SendMessage(WM_MYCUSTOMCTRL_CLICKED,
GetDlgCtrlID(), (LPARAM)m_hWnd);
}
}
};

4.2 添加键盘支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 键盘消息处理
BEGIN_MESSAGE_MAP(CMyCustomControl, CWnd)
ON_WM_KEYDOWN()
ON_WM_KEYUP()
ON_WM_GETDLGCODE()
END_MESSAGE_MAP()

UINT CMyCustomControl::OnGetDlgCode()
{
// 告诉对话框我们需要处理方向键和空格键
return DLGC_WANTARROWS | DLGC_WANTTAB | DLGC_WANTCHARS;
}

void CMyCustomControl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
switch (nChar)
{
case VK_SPACE:
m_bPressed = TRUE;
Invalidate();
break;
}

CWnd::OnKeyDown(nChar, nRepCnt, nFlags);
}

void CMyCustomControl::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)
{
switch (nChar)
{
case VK_SPACE:
if (m_bPressed)
{
m_bPressed = FALSE;
Invalidate();
NotifyClick();
}
break;
}

CWnd::OnKeyUp(nChar, nRepCnt, nFlags);
}

4.3 添加工具提示支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 工具提示支持
class CMyCustomControl : public CWnd
{
protected:
CToolTipCtrl m_toolTip;
BOOL m_bToolTipCreated;

afx_msg BOOL OnToolTipText(UINT id, NMHDR* pNMHDR, LRESULT* pResult);
};

BEGIN_MESSAGE_MAP(CMyCustomControl, CWnd)
ON_NOTIFY_EX(TTN_NEEDTEXT, 0, OnToolTipText)
END_MESSAGE_MAP()

void CMyCustomControl::PreSubclassWindow()
{
CWnd::PreSubclassWindow();

// 创建工具提示
if (!m_bToolTipCreated)
{
m_toolTip.Create(this);
m_toolTip.AddTool(this, m_strText);
m_toolTip.Activate(TRUE);
m_bToolTipCreated = TRUE;
}
}

BOOL CMyCustomControl::PreTranslateMessage(MSG* pMsg)
{
if (m_bToolTipCreated)
{
m_toolTip.RelayEvent(pMsg);
}

return CWnd::PreTranslateMessage(pMsg);
}

BOOL CMyCustomControl::OnToolTipText(UINT id, NMHDR* pNMHDR, LRESULT* pResult)
{
TOOLTIPTEXT* pTTT = (TOOLTIPTEXT*)pNMHDR;

// 设置工具提示文本
_tcscpy_s(pTTT->szText, m_strText);
pTTT->hinst = AfxGetResourceHandle();

*pResult = 0;
return TRUE;
}

五、MFC控件设计的最佳实践

5.1 内存管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CMyCustomControl : public CWnd
{
protected:
// 使用智能指针管理GDI资源
std::unique_ptr<CBitmap> m_pBackgroundBitmap;
std::unique_ptr<CFont> m_pCustomFont;

// 清理资源
virtual void CleanupResources();
};

void CMyCustomControl::CleanupResources()
{
// 自动释放资源
m_pBackgroundBitmap.reset();
m_pCustomFont.reset();
}

5.2 线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 如果控件需要从工作线程更新
class CMyCustomControl : public CWnd
{
protected:
CCriticalSection m_csData;

// 线程安全的属性设置
void SetTextSafe(const CString& strText)
{
CSingleLock lock(&m_csData, TRUE);
m_strText = strText;

// 使用PostMessage避免跨线程问题
PostMessage(WM_UPDATE_DISPLAY);
}

// 更新显示的消息处理
afx_msg LRESULT OnUpdateDisplay(WPARAM wParam, LPARAM lParam);
};

BEGIN_MESSAGE_MAP(CMyCustomControl, CWnd)
ON_MESSAGE(WM_UPDATE_DISPLAY, OnUpdateDisplay)
END_MESSAGE_MAP()

LRESULT CMyCustomControl::OnUpdateDisplay(WPARAM wParam, LPARAM lParam)
{
Invalidate();
return 0;
}

六、测试和使用自定义控件

6.1 在对话框中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 测试对话框
class CTestDialog : public CDialogEx
{
public:
CTestDialog(CWnd* pParent = NULL);

enum { IDD = IDD_TEST_DIALOG };

CMyCustomControl m_customCtrl1;
CMyCustomControl m_customCtrl2;

protected:
virtual BOOL OnInitDialog();
afx_msg void OnCustomCtrlClicked(WPARAM wParam, LPARAM lParam);

DECLARE_MESSAGE_MAP()
};

BEGIN_MESSAGE_MAP(CTestDialog, CDialogEx)
ON_MESSAGE(WM_MYCUSTOMCTRL_CLICKED, OnCustomCtrlClicked)
END_MESSAGE_MAP()

BOOL CTestDialog::OnInitDialog()
{
CDialogEx::OnInitDialog();

// 创建多个自定义控件实例
CRect rect1(20, 20, 150, 80);
CRect rect2(180, 20, 310, 80);

m_customCtrl1.Create(_T("控件1"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP,
rect1, this, IDC_CUSTOM1);

m_customCtrl2.Create(_T("控件2"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP,
rect2, this, IDC_CUSTOM2);

// 设置不同属性
m_customCtrl1.SetBackgroundColor(RGB(200, 230, 200));
m_customCtrl2.SetBackgroundColor(RGB(200, 200, 230));

return TRUE;
}

LRESULT CTestDialog::OnCustomCtrlClicked(WPARAM wParam, LPARAM lParam)
{
UINT nID = (UINT)wParam;
CString msg;
msg.Format(_T("控件 %d 被点击"), nID);
MessageBox(msg, _T("提示"), MB_OK | MB_ICONINFORMATION);

return 0;
}

七、总结

通过创建自定义MFC控件,我们深入理解了:

  1. MFC窗口创建机制:从窗口类注册到窗口实例创建
  2. 消息映射系统:MFC如何将Windows消息路由到处理函数
  3. 绘图和双缓冲:如何高效绘制控件界面
  4. 状态管理:处理鼠标、键盘等交互
  5. 扩展性设计:如何添加自定义属性和消息

自定义控件的关键在于理解MFC的类层次结构和消息处理机制。通过继承CWnd或现有控件类,并重写相应虚函数,可以实现高度定制化的界面控件。

这种深入理解不仅有助于创建自定义控件,还能提升对MFC框架整体架构的认识,为更复杂的Windows桌面应用开发打下坚实基础。


自定义MFC控件:从原理到实践
https://www.psnow.sbs/2026/01/08/自定义MFC控件:从原理到实践/
作者
Psnow
发布于
2026年1月9日
许可协议