一、什么是 Custom Control?
Custom Control(自定义控件) 是 MFC(Microsoft Foundation Classes)框架中提供的一种控件类型,用于实现自定义的外观和功能。当标准控件(例如 CEdit
、CButton
、CListCtrl
等)无法满足特定需求时,可以使用 Custom Control 来实现个性化的控件。
Custom Control 的核心特点:
- 基于
CWnd
类:通过继承CWnd
类,实现完全自定义的绘制和行为。 - 灵活性高:开发者可以自定义控件的绘制外观、消息响应、用户交互等。
- 适用于特殊需求:如自定义按钮、进度条、图表、绘图区域等。
二、Custom Control 的使用方法
1. 在资源编辑器中添加 Custom Control
- 打开 资源视图,编辑对应的对话框模板。
- 从工具箱中选择 Custom Control,并放置到对话框上。
- 在控件的 属性窗口 中:
- Class:指定自定义控件的类名(如
MyCustomControl
)。 - ID:设置控件的唯一标识符(如
IDC_MY_CUSTOM
)。
- Class:指定自定义控件的类名(如
2. 创建自定义控件类
要实现自定义控件,需要创建一个继承自 CWnd
的类,并重写消息处理函数。
示例代码:
头文件:MyCustomControl.h
#pragma once
#include "afxwin.h"
class CMyCustomControl : public CWnd
{
public:
CMyCustomControl();
virtual ~CMyCustomControl();
protected:
DECLARE_MESSAGE_MAP()
afx_msg void OnPaint(); // 自定义绘制
afx_msg void OnLButtonDown(UINT nFlags, CPoint point); // 鼠标点击事件
};
实现文件:MyCustomControl.cpp
#include "pch.h"
#include "MyCustomControl.h"
BEGIN_MESSAGE_MAP(CMyCustomControl, CWnd)
ON_WM_PAINT()
ON_WM_LBUTTONDOWN()
END_MESSAGE_MAP()
CMyCustomControl::CMyCustomControl() {}
CMyCustomControl::~CMyCustomControl() {}
void CMyCustomControl::OnPaint()
{
CPaintDC dc(this); // 设备上下文
CRect rect;
GetClientRect(&rect);
dc.FillSolidRect(rect, RGB(240, 240, 240)); // 背景颜色
dc.SetTextColor(RGB(0, 0, 255)); // 文本颜色
dc.DrawText(_T("自定义控件示例"), &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
void CMyCustomControl::OnLButtonDown(UINT nFlags, CPoint point)
{
AfxMessageBox(_T("自定义控件被点击!"));
CWnd::OnLButtonDown(nFlags, point);
}
3. 在对话框类中绑定 Custom Control
在对话框类中,将自定义控件绑定到资源中的 Custom Control。
示例代码:
头文件:MyDialog.h
#include "MyCustomControl.h"
class CMyDialog : public CDialogEx
{
...
private:
CMyCustomControl m_myCustomControl; // 自定义控件对象
};
DoDataExchange 绑定控件:
void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_MY_CUSTOM, m_myCustomControl); // 控件绑定
}
4. 在代码中动态创建 Custom Control
除了在资源编辑器中定义,Custom Control 也可以在代码中动态创建:
BOOL CMyDialog::OnInitDialog()
{
CDialogEx::OnInitDialog();
CRect rect(10, 10, 200, 50); // 控件位置和大小
m_myCustomControl.Create(NULL, WS_CHILD | WS_VISIBLE, rect, this, 1234);
return TRUE;
}
三、CGridCtrl 简介
CGridCtrl 是基于 Custom Control 的 MFC 自定义网格控件,通常用于显示表格数据。它是 CodeProject 社区提供的开源组件,可以实现类似 Excel 风格的表格功能。
CGridCtrl 官网:https://www.codeproject.com/Articles/8/MFC-Grid-control#History
CGridCtrl 的核心功能包括:
- 行列管理:动态添加、删除行列。
- 单元格管理:设置单元格文本、背景色、字体、边框等。
- 选择模式:支持单行选择、多行选择、单元格选择等。
- 数据编辑:单元格支持编辑功能,启用列表模式。
- 只读单元格:特定单元格可设置为只读,防止用户修改。
- 自适应列宽:支持自动扩展列宽适配内容。
- 高亮与排序:自动高亮选中行,并支持表格数据排序。
四、CGridCtrl 使用注意事项
1. SetItemText
与 SetRowCount
的关系
调用 SetItemText
函数设置单元格内容时,必须确保指定的行索引不超过当前的 SetRowCount
设置的行数。如果超出设置的行数范围,单元格内容将不会显示,也不会自动扩展行数。
2. 调用 ExpandColumnsToFit
和 ExpandLastColumn
在填充表格数据时,如果表格出现滚动条,默认情况下最后一列的宽度可能无法正确填充,导致内容显示不全或者异常。为确保显示正常,必须调用:
ExpandColumnsToFit
:调整所有列的宽度,使其适应当前表格的可视区域。ExpandLastColumn
:将最后一列扩展到表格的剩余宽度,确保视觉上的完整性。
五、CGridCtrl 特殊单元格类型使用
1. 设置下拉框单元格(CGridCellCombo)
通过设置单元格类型为 CGridCellCombo
,可以实现在单元格中显示下拉框,用户可选择选项。
示例代码:
if (m_gridUserManager.SetCellType(i, 3, RUNTIME_CLASS(CGridCellCombo))) {
CGridCellCombo* pCell = static_cast<CGridCellCombo*>(m_gridUserManager.GetCell(i, 3));
pCell->SetOptions(permissions); // 设置下拉框选项列表
pCell->SetStyle(CBS_DROPDOWNLIST); // 设置为只可选择的下拉框
}
SetOptions
:设置下拉框的选项内容。SetStyle(CBS_DROPDOWNLIST)
:设置为下拉选择模式,用户不能输入自由文本。
2. 设置复选框单元格(CGridCellCheck)
通过设置单元格类型为 CGridCellCheck
,可以在单元格中显示复选框,用户可勾选。
示例代码:
if (m_grid.SetCellType(nRowIndex, 1, RUNTIME_CLASS(CGridCellCheck))) {
auto* pCell = static_cast<CGridCellCheck*>(m_grid.GetCell(nRowIndex, 1));
pCell->SetCheck(TRUE); // 设置复选框为选中状态
}
SetCheck(TRUE)
:设置复选框的初始状态为选中。- 通过
SetCheck(FALSE)
可以将复选框状态设置为未选中。
注意事项:
- 在调用
SetCellType
之前,需要确保表格已设置足够的行列数。 - 通过
RUNTIME_CLASS
设置单元格类型时,需要确保所用的单元格类(如CGridCellCombo
和CGridCellCheck
)已经包含在项目中。
六、CGridCtrl 使用示例
1. 初始化表格控件
Custom Control 指定自定义控件的类名(MFCGridCtrl)。
以下代码展示如何初始化一个 CGridCtrl 控件,创建 4 列表格,并设置标题和基础样式:
void CRecipeListDlg::InitRecipeLise()
{
if (m_grid.GetSafeHwnd() == NULL) {
return;
}
int nRows = 1;
int nCols = 4;
int nFixRows = 1;
int nFixCols = 0;
int nRowIdx = 0;
int nColIdx = 0;
m_grid.DeleteAllItems();
m_grid.SetVirtualMode(FALSE);
m_grid.GetDefaultCell(TRUE, FALSE)->SetBackClr(g_nGridFixCellColor); // 设置固定行背景色
m_grid.GetDefaultCell(FALSE, TRUE)->SetBackClr(g_nGridFixCellColor); // 设置固定列背景色
m_grid.GetDefaultCell(FALSE, FALSE)->SetBackClr(g_nGridCellColor); // 设置单元格背景色
m_grid.SetFixedTextColor(g_nGridFixFontColor); // 设置固定行列字体颜色
m_grid.SetRowCount(nRows);
m_grid.SetColumnCount(nCols);
m_grid.SetFixedRowCount(nFixRows);
m_grid.SetFixedColumnCount(nFixCols);
// Col
m_grid.SetColumnWidth(nColIdx, 10);
m_grid.SetItemText(nRowIdx, nColIdx++, _T("No."));
m_grid.SetColumnWidth(nColIdx, 10);
m_grid.SetItemText(nRowIdx, nColIdx++, _T("名称"));
m_grid.SetColumnWidth(nColIdx, 50);
m_grid.SetItemText(nRowIdx, nColIdx++, _T("描述"));
m_grid.SetColumnWidth(nColIdx, 30);
m_grid.SetItemText(nRowIdx, nColIdx++, _T("创建时间"));
m_grid.SetFixedRowSelection(FALSE); // 设置固定行不可选中
m_grid.SetFixedColumnSelection(FALSE); // 设置固定列不可选中
m_grid.SetEditable(TRUE); // 设置单元格可编辑
m_grid.SetRowResize(FALSE); // 设置行不可调整大小
m_grid.SetColumnResize(TRUE); // 设置列可调整大小
m_grid.ExpandColumnsToFit(TRUE); // 自动调整列宽,适合固定表格大小并希望所有列均匀分布的情况
m_grid.SetListMode(TRUE); // 启用列表模式
m_grid.EnableSelection(TRUE); // 启用选择
m_grid.SetSingleRowSelection(TRUE); // 自动整行高亮(限制为单行选择)
m_grid.ExpandLastColumn(); // 最后一列填充网格
FillRecipeLise();
}
2. 填充数据到表格
FillRecipeLise
函数实现从文件夹和文本文件读取数据,并填充到表格:
void CRecipeListDlg::FillRecipeLise()
{
// 在设置行数和数据填充时,批量处理操作,避免逐行刷新表格。
// 开头调用 SetRedraw(FALSE),结束后调用 SetRedraw(TRUE)。
m_grid.SetRedraw(FALSE);
// 动态行数检查:在清空旧数据时,确保不会越界。
// 清除数据行,保留表头
for (int i = m_grid.GetRowCount() - 1; i > 0; --i) {
m_grid.DeleteRow(i);
}
// 1. 遍历文件夹下所有XML文件
std::string strRecipePath = CToolUnits::getRecipePath();
std::vector<CString> vecFile = CToolUnits::GetFileNamesInDirectory(strRecipePath.c_str(), _T(".xml"));
// 2. 读取 RecipeList.txt 文件
std::map<CString, std::pair<CString, CString>> recipeData; // {配方名, {描述, 创建时间}}
std::ifstream inFile(strRecipePath + "\\RecipeList.txt");
if (inFile.is_open()) {
std::string line;
while (std::getline(inFile, line)) {
if (line.empty()) continue; // 跳过空行
std::istringstream ss(line);
std::string name, description, createTime;
// CSV格式解析(逗号分隔)
if (std::getline(ss, name, ',') &&
std::getline(ss, description, ',') &&
std::getline(ss, createTime)) {
recipeData[CString(name.c_str())] = std::make_pair(CString(description.c_str()), CString(createTime.c_str()));
}
}
inFile.close();
}
// 3. 更新表格数据
int rowIdx = 1;
m_grid.SetRowCount(static_cast<int>(vecFile.size()) + 1);
for (const auto& fileName : vecFile) {
// 从 RecipeList.txt 数据中查找对应的描述和创建时间
CString description = _T("");
CString createTime = _T("");
auto it = recipeData.find(fileName);
if (it != recipeData.end()) {
description = it->second.first; // 配方描述
createTime = it->second.second; // 创建时间
}
// 填充表格数据
m_grid.SetItemText(rowIdx, 0, CString(std::to_string(rowIdx).c_str())); // No.
m_grid.SetItemText(rowIdx, 1, fileName); // 配方名称
m_grid.SetItemText(rowIdx, 2, description); // 配方描述
m_grid.SetItemText(rowIdx, 3, createTime); // 创建时间
// 禁止编辑
m_grid.SetItemState(rowIdx, 0, GVIS_READONLY);
m_grid.SetItemState(rowIdx, 1, GVIS_READONLY);
m_grid.SetItemState(rowIdx, 3, GVIS_READONLY);
++rowIdx;
}
// 适合内容不固定的情况,列宽自适应内容长度更自然。
m_grid.ExpandColumnsToFit(FALSE); // 自动调整列宽
m_grid.ExpandLastColumn(); // 最后一列填充网格
// 刷新网格控件
m_grid.SetRedraw(TRUE);
m_grid.Invalidate();
m_grid.UpdateWindow();
}
七、总结
- Custom Control 提供了实现自定义控件的基础能力,适合高度个性化需求。
- CGridCtrl 是一个基于 Custom Control 的表格控件,提供了灵活的表格展示与管理功能。
- CGridCtrl 支持设置特殊单元格类型,如下拉框(
CGridCellCombo
)和复选框(CGridCellCheck
)。