MFC 自定义控件 CJobSlotGrid
本文将对 MFC 框架下自定义控件 CJobSlotGrid
进行整理和分析,涵盖以下内容:
- MFC 的动态创建机制与宏的作用(
DECLARE_DYNAMIC
,DECLARE_DYNCREATE
等) - 如何设置可配置行列、颜色、文本、字体
- 如何查看 GUI GDI 资源、避免 GDI 资源泄露
- 推荐扩展方向:交互式控件增强、导出 CSV、Tooltip 等
一、控件简介
CJobSlotGrid
是一个基于 MFC CWnd
派生的子控件,内部自绘一个网格区域,用于展示状态矩阵(如“Job 存在”/“Job 空”状态)。具备以下核心功能:
- 支持动态设置网格行列数量(默认 12×16)
- 每个单元格有不同颜色(有/无 Job)
- 每个单元格可显示文本(默认为坐标)
- 支持设置字体名称和字号
二、MFC 类型识别宏的作用
1. DECLARE_DYNAMIC
与 IMPLEMENT_DYNAMIC
功能:支持 MFC 自有的运行时类型识别(RTTI)机制(非标准 C++ 的
typeid
)位置:
DECLARE_DYNAMIC(CJobSlotGrid)
放在类声明体内(如头文件)IMPLEMENT_DYNAMIC(CJobSlotGrid, CWnd)
放在类实现 CPP 文件
支持调用:
pWnd->IsKindOf(RUNTIME_CLASS(CJobSlotGrid));
pObj->GetRuntimeClass()->m_lpszClassName;
- ⚠️ 如果
IMPLEMENT_DYNAMIC
没有DECLARE_DYNAMIC
声明,会报 E0298 编译错误:不允许使用继承成员。
2. DECLARE_DYNCREATE
与 IMPLEMENT_DYNCREATE
- 功能:在
DECLARE_DYNAMIC
的基础上添加 MFC 对象的“动态创建”能力 - 用途:用于
CRuntimeClass::CreateObject()
创建对象实例 - 适用于控件需要工厂创建场景或子框架自动生成类实例
- 示例:
// .h
class CJobSlotGrid : public CWnd
{
DECLARE_DYNCREATE(CJobSlotGrid)
...
};
// .cpp
IMPLEMENT_DYNCREATE(CJobSlotGrid, CWnd)
✅ 如果你不需要动态生成(仅 Create 手动调用即可),
DECLARE_DYNAMIC
已足够。
三、控件接口功能清单
功能 | 方法名 | 说明 |
---|---|---|
设置行列数 | SetGridSize(int, int) |
初始化状态和文本数组结构 |
设置格子状态 | SetSlotStatus(int row, int col, bool) |
显示绿色/灰色(有/无 Job) |
设置格子文本 | SetSlotText(int row, int col, CString) |
默认格式为 (row+1,col+1) |
设置颜色 | SetColors(COLORREF, COLORREF) |
设置有/无状态颜色 |
设置字体 | SetTextFont(CString name, int sizePt) |
字号单位为 pt |
清空状态 | ClearAll() |
将所有状态设置为 false |
四、控件使用与 Create 创建注意事项
1. 使用 Create 创建控件
在对话框的 OnInitDialog()
中调用:
m_ctrlJobGrid.Create(
AfxRegisterWndClass(0), // 注册窗口类名
_T("JobSlotGrid"), // 窗口名(不显示)
WS_CHILD | WS_VISIBLE, // 控件样式
CRect(20, 20, 420, 320), // 控件区域
this, // 父窗口指针
1001 // 控件 ID
);
⚠️ 你必须传入合法的窗口类名,可以使用 AfxRegisterWndClass(0)
返回一个默认类名。
如果传 NULL,会导致崩溃或 Create()
返回 FALSE。
2. 初始化控件数据
m_ctrlJobGrid.SetGridSize(12, 16);
m_ctrlJobGrid.SetColors(RGB(0, 255, 0), RGB(200, 200, 200));
m_ctrlJobGrid.SetTextFont(_T("Arial"), 9);
m_ctrlJobGrid.SetSlotStatus(0, 1, true);
m_ctrlJobGrid.SetSlotText(0, 1, _T("OK"));
3. 效果图展示(建议):
你可以在 DrawGrid()
中使用颜色填充和 DrawText()
实现以下效果:
- 网格颜色根据状态变化
- 每个格子显示状态或位置编号
- 支持不同字号和字体
五、GDI 对象管理与资源泄露问题
1. 常见 GDI 类型及使用方式
类型 | MFC 类名 | 创建方式 | 释放方式 |
---|---|---|---|
字体 | CFont |
CreatePointFont(...) |
DeleteObject() |
画刷 | CBrush |
CreateSolidBrush(...) |
DeleteObject() |
画笔 | CPen |
CreatePen(...) |
DeleteObject() |
2. 查看 GDI 对象(Windows 平台)
- 打开任务管理器 -> 详细信息 -> 添加列 -> GDI 对象数量
- 每个窗口进程最多允许约 10,000 个 GDI 对象,超过容易导致崩溃
备注:鼠标右键点击表头弹出添加列的选项
3. 泄露现象
- 程序运行一段时间后,界面绘制卡顿或组件显示异常
- GDI 对象不断增长(重启恢复)
ASSERT(::GetObject(...) != 0)
触发,说明未正确初始化或未释放
4. 正确释放方式(析构中)
CJobSlotGrid::~CJobSlotGrid()
{
if (m_fontText.GetSafeHandle()) m_fontText.DeleteObject();
if (m_brushHasJob.GetSafeHandle()) m_brushHasJob.DeleteObject();
if (m_brushNoJob.GetSafeHandle()) m_brushNoJob.DeleteObject();
}
5. 更新颜色或字体时也要释放旧资源
void SetColors(...)
{
if (m_brushHasJob.GetSafeHandle()) m_brushHasJob.DeleteObject();
m_brushHasJob.CreateSolidBrush(newColor);
...
}
六、推荐扩展功能
推荐功能 | 说明 |
---|---|
点击切换状态 | 支持鼠标点击某个格子,切换 true/false |
Tooltip 提示 | 悬停显示 slot 描述、状态等信息 |
数据导出导入 | CSV 保存与加载状态与文本内容 |
绘制图标 | 在单元格中绘制小图标(OK/NG) |
双缓冲绘制 | 防止闪烁,可用 CMemDC 或 CBufferedPaintDC |
键盘导航 | 支持箭头移动选中单元格 |
七、完整代码参考
1. JobSlotGrid.h
#pragma once
#include <afxwin.h>
#include <vector>
class CJobSlotGrid : public CWnd
{
DECLARE_DYNAMIC(CJobSlotGrid)
public:
CJobSlotGrid();
virtual ~CJobSlotGrid();
void SetGridSize(int nRows, int nCols);
void SetColors(COLORREF colorHasJob, COLORREF colorNoJob);
void SetSlotStatus(int nRow, int nCol, bool bHasJob);
void SetSlotText(int nRow, int nCol, const CString& strText);
void SetTextFont(const CString& strFontName, int nPointSize);
void ClearAll();
protected:
afx_msg void OnPaint();
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
DECLARE_MESSAGE_MAP()
private:
int m_nRows;
int m_nCols;
CFont m_fontText;
COLORREF m_colorHasJob;
COLORREF m_colorNoJob;
CBrush m_brushHasJob;
CBrush m_brushNoJob;
std::vector<std::vector<bool>> m_vSlotStatus;
std::vector<std::vector<CString>> m_vSlotText;
void DrawGrid(CDC* pDC);
};
2. JobSlotGrid.cpp
#include "pch.h"
#include "JobSlotGrid.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
IMPLEMENT_DYNAMIC(CJobSlotGrid, CWnd)
BEGIN_MESSAGE_MAP(CJobSlotGrid, CWnd)
ON_WM_PAINT()
ON_WM_ERASEBKGND()
END_MESSAGE_MAP()
CJobSlotGrid::CJobSlotGrid() {
// 初始化默认行列数
int m_nRows = 12;
int m_nCols = 16;
// 初始化默认字体
m_fontText.CreatePointFont(60, _T("Arial"));
// 初始化默认颜色
COLORREF m_colorHasJob = RGB(0, 200, 0); // 默认绿色
COLORREF m_colorNoJob = RGB(220, 220, 220); // 默认灰色
// 初始化默认画刷
m_brushHasJob.CreateSolidBrush(m_colorHasJob);
m_brushNoJob.CreateSolidBrush(m_colorNoJob);
}
CJobSlotGrid::~CJobSlotGrid() {
if (m_fontText.GetSafeHandle()) {
m_fontText.DeleteObject();
}
if (m_brushHasJob.GetSafeHandle()) {
m_brushHasJob.DeleteObject();
}
if (m_brushNoJob.GetSafeHandle()) {
m_brushNoJob.DeleteObject();
}
}
void CJobSlotGrid::SetGridSize(int nRows, int nCols)
{
m_nRows = nRows;
m_nCols = nCols;
m_vSlotStatus.assign(nRows, std::vector<bool>(nCols, false));
// 初始化文本数组
m_vSlotText.assign(nRows, std::vector<CString>(nCols));
for (int i = 0; i < nRows; ++i) {
for (int j = 0; j < nCols; ++j) {
m_vSlotText[i][j].Format(_T("(%d,%d)"), i + 1, j + 1);
}
}
Invalidate();
}
void CJobSlotGrid::SetColors(COLORREF colorHasJob, COLORREF colorNoJob)
{
m_colorHasJob = colorHasJob;
m_colorNoJob = colorNoJob;
if (m_brushHasJob.GetSafeHandle()) {
m_brushHasJob.DeleteObject();
}
if (m_brushNoJob.GetSafeHandle()) {
m_brushNoJob.DeleteObject();
}
m_brushHasJob.CreateSolidBrush(m_colorHasJob);
m_brushNoJob.CreateSolidBrush(m_colorNoJob);
Invalidate();
}
void CJobSlotGrid::SetSlotStatus(int nRow, int nCol, bool bHasJob)
{
if (nRow >= 0 && nRow < m_nRows && nCol >= 0 && nCol < m_nCols) {
m_vSlotStatus[nRow][nCol] = bHasJob;
Invalidate();
}
}
void CJobSlotGrid::SetSlotText(int nRow, int nCol, const CString& strText)
{
if (nRow >= 0 && nRow < m_nRows && nCol >= 0 && nCol < m_nCols) {
m_vSlotText[nRow][nCol] = strText;
Invalidate();
}
}
void CJobSlotGrid::SetTextFont(const CString& strFontName, int nPointSize)
{
// 删除旧字体
if (m_fontText.GetSafeHandle()) {
m_fontText.DeleteObject();
}
// CreatePointFont expects size in 1/10 pt
m_fontText.CreatePointFont(nPointSize * 10, strFontName);
Invalidate();
}
void CJobSlotGrid::ClearAll()
{
if (m_vSlotStatus.empty()) {
return;
}
for (int i = 0; i < m_nRows; ++i) {
if (i < (int)m_vSlotStatus.size()) {
std::fill(m_vSlotStatus[i].begin(), m_vSlotStatus[i].end(), false);
}
}
Invalidate();
}
BOOL CJobSlotGrid::OnEraseBkgnd(CDC* pDC) {
return TRUE;
}
void CJobSlotGrid::OnPaint() {
CPaintDC dc(this);
DrawGrid(&dc);
}
void CJobSlotGrid::DrawGrid(CDC* pDC)
{
CRect rect;
GetClientRect(&rect);
int nCellWidth = rect.Width() / m_nCols;
int nCellHeight = rect.Height() / m_nRows;
CFont* pOldFont = pDC->SelectObject(&m_fontText);
for (int i = 0; i < m_nRows; ++i) {
for (int j = 0; j < m_nCols; ++j) {
CRect cellRect(j * nCellWidth, i * nCellHeight, (j + 1) * nCellWidth, (i + 1) * nCellHeight);
// 背景
CBrush* pBrush = m_vSlotStatus[i][j] ? &m_brushHasJob : &m_brushNoJob;
pDC->FillRect(&cellRect, pBrush);
// 边框
pDC->DrawEdge(&cellRect, EDGE_SUNKEN, BF_RECT);
// 文字(居中)
pDC->SetBkMode(TRANSPARENT);
pDC->SetTextColor(RGB(0, 0, 0));
pDC->DrawText(m_vSlotText[i][j], &cellRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
}
pDC->SelectObject(pOldFont);
}
八、结语
CJobSlotGrid
是一个最初版本的 MFC 自定义控件,旨在为网格型状态展示提供一个基础的可视化方案。当前已实现行列设置、颜色配置、文本显示、字体控制等核心功能,同时确保了资源管理的基本规范。
我们尽可能保证代码的健壮性和灵活性,但由于此控件仍处于初步阶段,使用中可能会存在边界情况、兼容性问题或功能不足之处,敬请大家理解。
如果你有改进建议或扩展功能想法,非常欢迎大家在此基础上进行二次开发,比如:
- 鼠标点击交互支持(切换状态)
- 键盘导航与选中焦点
- 状态导入导出(CSV/JSON)
- 图标绘制与动态动画
控件已适用于工业控制、设备面板、可视化调试等场景,后续也计划封装成 .lib
或 .dll
便于项目复用。感谢阅读与支持!CJobSlotGrid
是一个灵活且健壮的 MFC 自定义控件,实现了完整的绘制、配置和资源管理能力。具备如下优点:
- 支持行列、状态、颜色、文本、字体等全面自定义
- 遵循 MFC 宏机制,方便类型识别与扩展
- GDI 管理规范,无资源泄露
非常适合工业控制、设备状态面板、诊断工具等场景。如果你希望将其进一步产品化,建议:
- 拆分封装为静态库(
.lib
)或 DLL - 编写更完整交互接口(Tooltip、点击事件、热区)
- 提供导出、打印、缩放支持