MFC 自定义控件 CJobSlotGrid

发布于:2025-05-25 ⋅ 阅读:(21) ⋅ 点赞:(0)

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_DYNAMICIMPLEMENT_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_DYNCREATEIMPLEMENT_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)
双缓冲绘制 防止闪烁,可用 CMemDCCBufferedPaintDC
键盘导航 支持箭头移动选中单元格

七、完整代码参考

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、点击事件、热区)
  • 提供导出、打印、缩放支持

网站公告

今日签到

点亮在社区的每一天
去签到