背景
在 WinForms 中使用 OxyPlot 绘制图表,通常的做法是直接拖入 PlotView 控件。但某些场景下(如多图表联动、自定义绘制、性能优化),直接使用 PlotView 会遇到限制。本文介绍如何通过重写 UserControl.OnPaint 的方式,手动管理 OxyPlot 渲染,实现更灵活的图表控件。
一、与 PlotView 的差别
1.1 PlotView 的限制
| 限制点 |
说明 |
| 控件隔离 |
每个 PlotView 是一个独立控件,多图表时需要多个控件,布局困难 |
| 裁剪复杂 |
无法轻松实现图表间共享坐标、联动缩放 |
| 自定义绘制难 |
想在图表上叠加自定义元素(如十字线、状态栏)需要侵入 PlotView 内部 |
| 性能瓶颈 |
多个 PlotView 各自维护渲染管线,无法统一优化 |
1.2 自定义 UserControl 的优势
| 优势 |
说明 |
| 统一渲染 |
所有图表在一个 OnPaint 中渲染,性能可控 |
| 灵活布局 |
完全自主控制每个图表的矩形区域、间距、比例 |
| 叠加绘制 |
可以在 OxyPlot 渲染后,继续用 GDI+ 绘制十字线、标注、状态栏等 |
| 事件统一 |
鼠标、键盘事件只需在一个控件处理,易于实现联动 |
二、核心技术
2.1 核心思路
UserControl.OnPaint
│
├── 1. 创建 GraphicsRenderContext (绑定到控件的 Graphics)
│
├── 2. 循环渲染每个 PlotModel
│ ├── 设置裁剪区域 (SetClip)
│ ├── 更新模型 (Update)
│ └── 渲染图表 (Render)
│
├── 3. 绘制分隔线、标题等 GDI+ 元素
│
└── 4. 绘制状态栏、性能信息等
2.2 关键代码
步骤 1:设置控件样式(双缓冲)
public class CustomChartControl : UserControl
{
public CustomChartControl()
{
// 启用双缓冲,避免闪烁
SetStyle(ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.ResizeRedraw, true);
}
}
步骤 2:重写 OnPaint
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillRectangle(Brushes.White, ClientRectangle);
// 创建 OxyPlot 渲染上下文
using (var rc = new GraphicsRenderContext(g) { RendersToScreen = true })
{
for (int i = 0; i < ChartCount; i++)
{
if (!chartVisible[i]) continue;
// 设置裁剪区域,限制在该图表范围内绘制
var prevClip = g.Clip;
g.SetClip(chartRects[i]);
// 更新并渲染
models[i].Update(true);
models[i].Render(rc, chartRects[i]);
g.Clip = prevClip;
}
}
// 绘制自定义元素
DrawSeparators(g);
DrawStatusBar(g);
}
步骤 3:计算图表布局
private void CalculateChartRects()
{
int chartAreaHeight = ClientSize.Height - StatusBarHeight;
int chartHeight = (chartAreaHeight - ChartGap * (ChartCount - 1)) / ChartCount;
for (int i = 0; i < ChartCount; i++)
{
int y = i * (chartHeight + ChartGap);
chartRects[i] = new Rectangle(0, y, ClientSize.Width, chartHeight);
}
}
三、完整实现示例
3.1 控件类
using System;
using System.Drawing;
using System.Windows.Forms;
using OxyPlot;
using OxyPlot.WindowsForms;
public class MultiChartControl : UserControl
{
private const int ChartCount = 4;
private const int StatusBarHeight = 30;
private const int ChartGap = 2;
private PlotModel[] models;
private Rectangle[] chartRects;
private bool[] chartVisible;
public MultiChartControl()
{
// 启用双缓冲
SetStyle(ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.ResizeRedraw, true);
models = new PlotModel[ChartCount];
chartRects = new Rectangle[ChartCount];
chartVisible = new bool[ChartCount] { true, true, true, true };
InitializeControl();
}
private void InitializeControl()
{
CreatePlotModels();
CalculateChartRects();
}
private void CreatePlotModels()
{
for (int i = 0; i < ChartCount; i++)
{
models[i] = new PlotModel { Title = $"图表 {i + 1}" };
}
}
private void CalculateChartRects()
{
int chartAreaHeight = ClientSize.Height - StatusBarHeight;
int chartHeight = (chartAreaHeight - ChartGap * (ChartCount - 1)) / ChartCount;
for (int i = 0; i < ChartCount; i++)
{
int y = i * (chartHeight + ChartGap);
chartRects[i] = new Rectangle(0, y, ClientSize.Width, chartHeight);
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillRectangle(Brushes.White, ClientRectangle);
using (var rc = new GraphicsRenderContext(g) { RendersToScreen = true })
{
for (int i = 0; i < ChartCount; i++)
{
if (!chartVisible[i] || chartRects[i].Width <= 0) continue;
var prevClip = g.Clip;
g.SetClip(chartRects[i]);
models[i].Render(rc, chartRects[i]);
g.Clip = prevClip;
}
}
DrawSeparators(g);
DrawStatusBar(g);
}
private void DrawSeparators(Graphics g)
{
using (var pen = new Pen(Color.FromArgb(200, 200, 200), 1))
{
for (int i = 0; i < ChartCount - 1; i++)
{
if (!chartVisible[i] || !chartVisible[i + 1]) continue;
int y = chartRects[i].Bottom + ChartGap / 2;
g.DrawLine(pen, 0, y, ClientSize.Width, y);
}
}
}
private void DrawStatusBar(Graphics g)
{
int y = ClientSize.Height - StatusBarHeight;
using (var bg = new SolidBrush(Color.FromArgb(240, 240, 240)))
g.FillRectangle(bg, 0, y, ClientSize.Width, StatusBarHeight);
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
CalculateChartRects();
Invalidate();
}
}
3.2 使用方法
在窗体中添加控件
public partial class MainForm : Form
{
private MultiChartControl chartControl;
public MainForm()
{
InitializeComponent();
chartControl = new MultiChartControl();
chartControl.Dock = DockStyle.Fill;
this.Controls.Add(chartControl);
}
}
添加数据到图表
// 向第一个图表添加折线
var lineSeries = new OxyPlot.Series.LineSeries { Title = "位置" };
lineSeries.Points.Add(new OxyPlot.DataPoint(0, 0));
lineSeries.Points.Add(new OxyPlot.DataPoint(100, 50));
lineSeries.Points.Add(new OxyPlot.DataPoint(200, 100);
chartControl.models[0].Series.Add(lineSeries);
chartControl.models[0].InvalidatePlot(true);
chartControl.Invalidate();
四、性能优化技巧
4.1 根据数据量动态调整质量
g.SmoothingMode = (models[0]?.DefaultSeries?.Points.Count > 2000)
? SmoothingMode.None // 大数据量关闭抗锯齿
: SmoothingMode.AntiAlias;
4.2 只在数据变化时重绘
public void UpdateData(List<DataPoint> newData)
{
// 更新数据...
// 只重绘需要更新的图表
models[0].InvalidatePlot(true);
this.Invalidate(); // 触发 OnPaint
}
五、总结
| 对比项 |
PlotView |
自定义 UserControl |
| 多图表布局 |
需多个控件,布局复杂 |
单一控件,自主控制 |
| 图表间联动 |
困难 |
容易(共享坐标映射) |
| 自定义叠加绘制 |
困难 |
简单(GDI+ 直接绘制) |
| 性能优化 |
受限 |
完全可控 |
| 学习成本 |
低 |
中等 |
适用场景:
- 需要多个图表联动显示(如位置/速度/加速度/Jerk 四图)
- 需要在图表上叠加自定义元素(十字线、标注、状态栏)
- 对性能有较高要求(大数据量渲染)
- 需要统一的事件处理(如鼠标拖动同步多个图表)