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);
}
}
使用用法:
-
在对话框资源中拖拽一个CStatic控件;
-
在对话框的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
}