MFC基于CStatic自绘控件多轴+图样+标签的折线图控件

MFC基于CStatic自绘控件多轴+图样+标签的折线图控件

支持多 Y 轴、多数据序列绘制折线图,含刻度、网格、图例和自适应绘图区。

运行效果:

MyChartStatic.h

cpp 复制代码
#pragma once

#include <vector>
#include <afxwin.h>
#include <gdiplus.h>

#pragma comment(lib,"gdiplus.lib")

// ==============================
// 数据结构定义
// ==============================

/// <summary>
/// 曲线中的一个数据点(X,Y)
/// </summary>
struct AxisPoint
{
	double x;   // X轴值
	double y;   // Y轴值
};

/// <summary>
/// Y轴定义(支持多个)
/// </summary>
struct YAxis
{
	CString  label;       // Y轴标题
	double   minVal;      // 最小值
	double   maxVal;      // 最大值
	COLORREF color;       // 轴颜色
	int      majorCount;  // 主刻度数量
	int      minorCount;  // 次刻度数量(预留)
};

/// <summary>
/// 一条曲线(绑定到某一个Y轴)
/// </summary>
struct Series
{
	int axisIndex;                    // 绑定的Y轴索引
	CString name;                     // 曲线名称(用于图例)
	std::vector<AxisPoint> data;      // 曲线数据点
	COLORREF color;                   // 曲线颜色
};

/// <summary>
/// X轴定义(只有一个)
/// </summary>
struct XAxis
{
	double  minVal;      // X轴最小值
	double  maxVal;      // X轴最大值
	int     majorCount;  // 主刻度数量
	int     minorCount;  // 次刻度数量(预留)
	CString title;       // ★ X轴标题(只显示一行)
};

// ==============================
// 工具函数
// ==============================

/// <summary>
/// 将普通数组转换为 std::vector
/// </summary>
template<typename T>
std::vector<T> ArrayToVector(const T* arr, int count)
{
	std::vector<T> v;
	for (int i = 0; i < count; ++i)
		v.push_back(arr[i]);
	return v;
}

// ==============================
// 图表控件类
// ==============================

/// <summary>
/// 多Y轴折线图控件(派生自 CStatic)
/// </summary>
class CMyChartStatic : public CStatic
{
	DECLARE_DYNAMIC(CMyChartStatic)

public:
	CMyChartStatic();
	virtual ~CMyChartStatic();

	// ------------------------------
	// 对外接口
	// ------------------------------

	/// <summary>
	/// 添加一个 Y 轴
	/// </summary>
	void AddYAxis(const CString& label,
		double minVal,
		double maxVal,
		COLORREF color,
		int majorCount = 5,
		int minorCount = 4);

	/// <summary>
	/// 添加一条曲线(绑定到指定 Y 轴)
	/// </summary>
	void AddSeries(int axisIndex,
		const CString& name,
		const std::vector<AxisPoint>& data,
		COLORREF color);

	/// <summary>
	/// 设置 X 轴参数
	/// </summary>
	void SetXAxis(double minVal = 0.0,
		double maxVal = 10.0,
		int majorCount = 10,
		int minorCount = 5,
		const CString& title = _T("X刻度"));

protected:
	// ------------------------------
	// MFC 消息处理
	// ------------------------------
	afx_msg void OnPaint();
	afx_msg void OnSize(UINT nType, int cx, int cy);
	afx_msg BOOL OnEraseBkgnd(CDC* pDC);
	DECLARE_MESSAGE_MAP()

private:
	// ------------------------------
	// 数据成员
	// ------------------------------

	std::vector<YAxis>  m_yAxes;      // 所有Y轴
	std::vector<int>    m_axisWidths; // Y轴宽度(预留)
	std::vector<Series> m_series;     // 所有曲线
	XAxis               m_xAxis;      // X轴
	int                 m_leftMargin; // 左边距(预留)

	// GDI+ 句柄
	ULONG_PTR m_gdiplusToken;

	// ------------------------------
	// 绘图辅助函数
	// ------------------------------

	CRect CalcPlotRect();                              // 计算绘图区
	void DrawYAxes(CDC& dc, const CRect& plotRect);    // 绘制Y轴
	void DrawBorder(CDC& dc, const CRect& plotRect);   // 绘制边框
	void DrawGrid(CDC& dc, const CRect& plotRect);     // 绘制网格
	void DrawXAxis(CDC& dc, const CRect& plotRect);    // 绘制X轴 + 标题
	void DrawSeries(CDC& dc, const CRect& plotRect);   // 绘制曲线
	void DrawLegend(CDC& dc, const CRect& plotRect);   // 绘制图例
	void CreateChartFont(CFont& font);                 // 创建统一字体
};

MyChartStatic.cpp

cpp 复制代码
#include "stdafx.h"
#include "MyChartStatic.h"

IMPLEMENT_DYNAMIC(CMyChartStatic,CStatic)

BEGIN_MESSAGE_MAP(CMyChartStatic,CStatic)
	ON_WM_PAINT()
	ON_WM_SIZE()
	ON_WM_ERASEBKGND()
END_MESSAGE_MAP()

CMyChartStatic::CMyChartStatic() : m_leftMargin(0)
{
	m_xAxis.minVal=0;
	m_xAxis.maxVal=10;
	m_xAxis.majorCount=10;
	m_xAxis.minorCount=5;

	Gdiplus::GdiplusStartupInput gi;
	Gdiplus::GdiplusStartup(&m_gdiplusToken,&gi,NULL);
}

CMyChartStatic::~CMyChartStatic()
{
	Gdiplus::GdiplusShutdown(m_gdiplusToken);
}

void CMyChartStatic::AddYAxis(const CString& label,double minVal,double maxVal,
	COLORREF color,int majorCount,int minorCount)
{
	YAxis axis={label,minVal,maxVal,color,majorCount,minorCount};
	m_yAxes.push_back(axis);
}

void CMyChartStatic::AddSeries(int axisIndex,const CString& name,
	const std::vector<AxisPoint>& data,COLORREF color)
{
	if(axisIndex<0 || axisIndex>= (int)m_yAxes.size()) return;
	Series s={axisIndex,name,data,color};
	m_series.push_back(s);
}

void CMyChartStatic::SetXAxis(double minVal,double maxVal,int majorCount,int minorCount, const CString& title)
{
	m_xAxis.minVal = minVal;
	m_xAxis.maxVal = maxVal;
	m_xAxis.majorCount = majorCount;
	m_xAxis.minorCount = minorCount;
	m_xAxis.title      = title;
	Invalidate();
}

BOOL CMyChartStatic::OnEraseBkgnd(CDC* pDC)
{
	UNREFERENCED_PARAMETER(pDC);
	return TRUE;
}

void CMyChartStatic::OnPaint()
{
	CPaintDC dc(this);
	CRect rc; GetClientRect(&rc);

	CDC memDC; memDC.CreateCompatibleDC(&dc);
	CBitmap bmp; bmp.CreateCompatibleBitmap(&dc,rc.Width(),rc.Height());
	CBitmap* oldBmp=memDC.SelectObject(&bmp);
	memDC.FillSolidRect(&rc,RGB(255,255,255));

	CRect plotRect=CalcPlotRect();

	CFont font; CreateChartFont(font);
	CFont* oldFont=memDC.SelectObject(&font);
	memDC.SetBkMode(TRANSPARENT);

	DrawYAxes(memDC,plotRect);
	DrawBorder(memDC,plotRect);
	DrawGrid(memDC,plotRect);
	DrawXAxis(memDC,plotRect);
	DrawSeries(memDC,plotRect);
	DrawLegend(memDC,plotRect);

	dc.BitBlt(0,0,rc.Width(),rc.Height(),&memDC,0,0,SRCCOPY);

	memDC.SelectObject(oldFont);
	memDC.SelectObject(oldBmp);
	font.DeleteObject(); bmp.DeleteObject();
}

void CMyChartStatic::OnSize(UINT nType, int cx, int cy)
{
	CStatic::OnSize(nType, cx, cy);

	// 只重绘,不修改任何参数
	if (cx > 0 && cy > 0)
	{
		Invalidate();  // 触发 OnPaint 重新绘制
	}
}

CRect CMyChartStatic::CalcPlotRect()
{
	CRect rc; GetClientRect(&rc);
	int leftMargin = 64*m_yAxes.size(); //固定宽度
	int rightMargin = 80;               //图例宽度
	int topMargin=40; int bottomMargin = 60;
	return CRect(rc.left+leftMargin,rc.top+topMargin,
		rc.right-rightMargin,rc.bottom-bottomMargin);
}

void CMyChartStatic::CreateChartFont(CFont& font)
{
	font.CreateFont(13,0,0,0,FW_NORMAL,FALSE,FALSE,0,
		ANSI_CHARSET,OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,
		DEFAULT_QUALITY,DEFAULT_PITCH|FF_SWISS,_T("Arial"));
}

void CMyChartStatic::DrawYAxes(CDC& dc,const CRect& plotRect)
{
	int offset=0;
	for(size_t i=0;i<m_yAxes.size();i++)
	{
		const YAxis& axis=m_yAxes[i];
		int axisX=offset+64;
		CPen pen(PS_SOLID,1,axis.color); CPen* oldPen=dc.SelectObject(&pen);

		int top=plotRect.top; int bottom=plotRect.bottom;
		float height=(float)(bottom-top);

		dc.MoveTo(axisX,top); dc.LineTo(axisX,bottom);

		for(int t=0;t<=axis.majorCount;t++)
		{
			double val=axis.minVal+t*(axis.maxVal-axis.minVal)/axis.majorCount;
			int y=bottom-(int)((val-axis.minVal)*height/(axis.maxVal-axis.minVal));
			dc.MoveTo(axisX-5,y); dc.LineTo(axisX,y);
			CString txt; txt.Format(_T("%.1f"),val);
			dc.SetTextColor(axis.color);
			CRect rcText(axisX-64+2,y-8,axisX-2,y+8);
			dc.DrawText(txt,&rcText,DT_RIGHT|DT_VCENTER|DT_SINGLELINE);
		}

		CSize szLabel=dc.GetTextExtent(axis.label);
		CRect rcLabel(axisX-szLabel.cx/2,plotRect.top-szLabel.cy-4,
			axisX+szLabel.cx/2,plotRect.top-4);
		dc.DrawText(axis.label,&rcLabel,DT_CENTER|DT_SINGLELINE|DT_VCENTER);

		dc.SelectObject(oldPen);
		offset+=64;
	}
}

void CMyChartStatic::DrawBorder(CDC& dc,const CRect& plotRect)
{
	CPen pen(PS_SOLID,2,RGB(0,0,0)); CPen* oldPen=dc.SelectObject(&pen);
	dc.Rectangle(plotRect); dc.SelectObject(oldPen);
}

void CMyChartStatic::DrawGrid(CDC& dc,const CRect& plotRect)
{
	CPen pen(PS_DOT,1,RGB(180,180,180)); CPen* oldPen=dc.SelectObject(&pen);
	int count=10;
	for(int i=0;i<=count;i++)
	{
		int y=plotRect.top+i*plotRect.Height()/count;
		dc.MoveTo(plotRect.left,y); dc.LineTo(plotRect.right,y);
		int x=plotRect.left+i*plotRect.Width()/count;
		dc.MoveTo(x,plotRect.top); dc.LineTo(x,plotRect.bottom);
	}
	dc.SelectObject(oldPen);
}

void CMyChartStatic::DrawXAxis(CDC& dc, const CRect& plotRect)
{
	CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
	CPen* oldPen = dc.SelectObject(&pen);

	// X轴主线
	dc.MoveTo(plotRect.left, plotRect.bottom);
	dc.LineTo(plotRect.right, plotRect.bottom);

	// 刻度
	double step = (double)plotRect.Width() / m_xAxis.majorCount;
	for (int i = 0; i <= m_xAxis.majorCount; i++)
	{
		int x = plotRect.left + (int)(i * step);
		dc.MoveTo(x, plotRect.bottom);
		dc.LineTo(x, plotRect.bottom + 6);

		double val = m_xAxis.minVal +
			i * (m_xAxis.maxVal - m_xAxis.minVal) / m_xAxis.majorCount;

		CString txt;
		txt.Format(_T("%.1f"), val);

		CRect rcText(x - 20,
			plotRect.bottom + 8,
			x + 20,
			plotRect.bottom + 24);
		dc.DrawText(txt, &rcText, DT_CENTER | DT_SINGLELINE);
	}

	// ===== ★ X轴下面的一行文字(轴标题)=====
	if (!m_xAxis.title.IsEmpty())
	{
		CSize sz = dc.GetTextExtent(m_xAxis.title);
		int centerX = (plotRect.left + plotRect.right) / 2;

		CRect rcTitle(centerX - sz.cx / 2,
			plotRect.bottom + 32,   // ★ 在刻度文字下面
			centerX + sz.cx / 2,
			plotRect.bottom + 32 + sz.cy);

		// X轴标签颜色
		COLORREF	clrOld = dc.SetTextColor(RGB(255, 0, 0));
		dc.DrawText(m_xAxis.title,
			&rcTitle,
			DT_CENTER | DT_SINGLELINE | DT_VCENTER);
		dc.SetTextColor(clrOld);
	}

	dc.SelectObject(oldPen);
}


void CMyChartStatic::DrawSeries(CDC& dc,const CRect& plotRect)
{
	if(m_series.empty()) return;
	int left=plotRect.left,right=plotRect.right,top=plotRect.top,bottom=plotRect.bottom;
	float width = (float)(right-left);
	float height=(float)(bottom-top);

	Gdiplus::Graphics g(dc.m_hDC); g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);

	for(size_t si=0;si<m_series.size();si++)
	{
		const Series& s=m_series[si];
		if(s.data.size()<2) continue;
		const YAxis& axis=m_yAxes[s.axisIndex];
		Gdiplus::Color clr(GetRValue(s.color),GetGValue(s.color),GetBValue(s.color));
		Gdiplus::Pen pen(clr,2.0f);

		Gdiplus::PointF lastPt;
		bool first=true;
		for(size_t i=0;i<s.data.size();i++)
		{
			// 使用 AxisPoint 的 x、y
			float X = (float)(left + (s.data[i].x - m_xAxis.minVal) / (m_xAxis.maxVal - m_xAxis.minVal) * width);
			float Y = bottom - (float)((s.data[i].y - axis.minVal) / (axis.maxVal - axis.minVal) * height);

			if(first){ lastPt.X=X; lastPt.Y=Y; first=false; }
			else { Gdiplus::PointF pt(X,Y); g.DrawLine(&pen,lastPt,pt); lastPt=pt; }
		}
	}
}

void CMyChartStatic::DrawLegend(CDC& dc,const CRect& plotRect)
{
	int legendX=plotRect.right+10;
	int legendY=plotRect.top;
	int boxSize=12; int gap=4; int lineHeight=18;

	for(size_t i=0;i<m_series.size();i++)
	{
		const Series& s=m_series[i];
		CBrush brush(s.color); CBrush* oldBrush=(CBrush*)dc.SelectObject(&brush);
		dc.Rectangle(legendX,legendY+i*lineHeight,legendX+boxSize,legendY+i*lineHeight+boxSize);
		dc.SelectObject(oldBrush);

		CRect rcText(legendX+boxSize+gap,legendY+i*lineHeight,legendX+100,legendY+i*lineHeight+boxSize);
		dc.SetTextColor(RGB(0,0,0));
		dc.DrawText(s.name,&rcText,DT_LEFT|DT_SINGLELINE|DT_VCENTER);
	}
}

使用用法:

  1. 在对话框资源中拖拽一个CStatic控件;

  2. 在对话框的OnInitDialog照类似方法写:

cpp 复制代码
BOOL CDrawChartDlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// 将"关于..."菜单项添加到系统菜单中。

	// IDM_ABOUTBOX 必须在系统命令范围内。
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != NULL)
	{
		BOOL bNameValid;
		CString strAboutMenu;
		bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
		ASSERT(bNameValid);
		if (!strAboutMenu.IsEmpty())
		{
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}

	// 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动
	//  执行此操作
	SetIcon(m_hIcon, TRUE);			// 设置大图标
	SetIcon(m_hIcon, FALSE);		// 设置小图标

	// TODO: 在此添加额外的初始化代码

	// 绑定控件
	m_chart.SubclassDlgItem(IDC_CHART_STATIC, this);

	// ===============================
	// 1. 绑定 Chart Static 控件
	// ===============================
	m_chart.SubclassDlgItem(IDC_CHART_STATIC, this);

	// ===============================
	// 2. 添加 Y 轴
	// ===============================
	m_chart.AddYAxis(_T("温度 (℃)"), 0, 100, RGB(255, 0, 0), 5, 4);
	m_chart.AddYAxis(_T("压力 (Pa)"), 0, 50,  RGB(0, 0, 255), 5, 4);
	m_chart.AddYAxis(_T("浮力 (Pa)"), 0, 200,  RGB(0, 0, 255), 5, 4);

	// ===============================
	// 3. 构造测试数据
	// ===============================
	std::srand((unsigned int)time(nullptr));

	std::vector<AxisPoint> dataTemp1;
	std::vector<AxisPoint> dataTemp2;
	std::vector<AxisPoint> dataPress1;

	for (int i = 0; i <= 10; ++i)
	{
		AxisPoint pt;

		pt.x = i;
		pt.y = 20 + rand() % 60;
		dataTemp1.push_back(pt);

		pt.x = i;
		pt.y = 30 + rand() % 50;
		dataTemp2.push_back(pt);

		pt.x = i;
		pt.y = rand() % 50;
		dataPress1.push_back(pt);
	}

	// ===============================
	// 4. 添加曲线
	// ===============================
	m_chart.AddSeries(0, _T("温度1"), dataTemp1, RGB(255, 0, 0));
	m_chart.AddSeries(0, _T("温度2"), dataTemp2, RGB(0, 180, 0));
	m_chart.AddSeries(1, _T("压力1"), dataPress1, RGB(0, 0, 255));
	m_chart.AddSeries(2, _T("压力1"), dataPress1, RGB(0, 255, 0));

	// ===============================
	// 5. 设置 X 轴(关键:下面一行文字)
	// ===============================
	m_chart.SetXAxis(
		0,          // min
		10,         // max
		10,         // 主刻度
		5,          // 次刻度(暂未用)
		_T("单位采集时间 (s)")   // ★ X轴标题(只这一行)
		);

	// ===============================
	// 6. 初次刷新
	// ===============================
	m_chart.Invalidate();
	m_chart.UpdateWindow();

	return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
}
相关推荐
Yupureki4 小时前
《算法竞赛从入门到国奖》算法基础:入门篇-双指针
c语言·开发语言·数据结构·c++·算法·visual studio
淼淼7634 小时前
Qt工具栏+图页,图元支持粘贴复制,撤销,剪切,移动,删除
开发语言·c++·windows·qt
laocooon5238578864 小时前
C++中的安全指针(智能指针)
开发语言·c++
yuuki2332334 小时前
【C++】模板初阶
java·开发语言·c++
努力努力再努力wz4 小时前
【Linux网络系列】:网络+网络编程(UDPsocket+TCPsocket)
java·linux·c语言·开发语言·数据结构·c++·centos
scx201310044 小时前
20251210 DP小测总结
c++·动态规划
Blasit5 小时前
Qt C++ 编译 libevent静态库
开发语言·c++·qt
Aevget5 小时前
MFC扩展库BCGControlBar Pro v37.1——支持Visual Studio 2026
c++·mfc·bcg·界面控件·visual studio·ui开发
铅笔小新z5 小时前
【C++】 vector 全面解析:从使用到底层实现
开发语言·c++