MFC 自定义网格控件

发布于:2024-12-18 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、什么是 Custom Control?

Custom Control(自定义控件) 是 MFC(Microsoft Foundation Classes)框架中提供的一种控件类型,用于实现自定义的外观和功能。当标准控件(例如 CEditCButtonCListCtrl 等)无法满足特定需求时,可以使用 Custom Control 来实现个性化的控件。

Custom Control 的核心特点:

  • 基于 CWnd:通过继承 CWnd 类,实现完全自定义的绘制和行为。
  • 灵活性高:开发者可以自定义控件的绘制外观、消息响应、用户交互等。
  • 适用于特殊需求:如自定义按钮、进度条、图表、绘图区域等。

二、Custom Control 的使用方法

1. 在资源编辑器中添加 Custom Control

  • 打开 资源视图,编辑对应的对话框模板。
  • 从工具箱中选择 Custom Control,并放置到对话框上。
  • 在控件的 属性窗口 中:
    • Class:指定自定义控件的类名(如 MyCustomControl)。
    • ID:设置控件的唯一标识符(如 IDC_MY_CUSTOM)。

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 的核心功能包括:

  1. 行列管理:动态添加、删除行列。
  2. 单元格管理:设置单元格文本、背景色、字体、边框等。
  3. 选择模式:支持单行选择、多行选择、单元格选择等。
  4. 数据编辑:单元格支持编辑功能,启用列表模式。
  5. 只读单元格:特定单元格可设置为只读,防止用户修改。
  6. 自适应列宽:支持自动扩展列宽适配内容。
  7. 高亮与排序:自动高亮选中行,并支持表格数据排序。

四、CGridCtrl 使用注意事项

1. SetItemTextSetRowCount 的关系

调用 SetItemText 函数设置单元格内容时,必须确保指定的行索引不超过当前的 SetRowCount 设置的行数。如果超出设置的行数范围,单元格内容将不会显示,也不会自动扩展行数。

2. 调用 ExpandColumnsToFitExpandLastColumn

在填充表格数据时,如果表格出现滚动条,默认情况下最后一列的宽度可能无法正确填充,导致内容显示不全或者异常。为确保显示正常,必须调用:

  • 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 设置单元格类型时,需要确保所用的单元格类(如 CGridCellComboCGridCellCheck)已经包含在项目中。

六、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();
}

七、总结

  1. Custom Control 提供了实现自定义控件的基础能力,适合高度个性化需求。
  2. CGridCtrl 是一个基于 Custom Control 的表格控件,提供了灵活的表格展示与管理功能。
  3. CGridCtrl 支持设置特殊单元格类型,如下拉框(CGridCellCombo)和复选框(CGridCellCheck)。