WinForms 自定义图表控件绘制方案:绕过 PlotView 的实现

WinForms 自定义图表控件绘制方案:绕过 PlotView 的实现

背景

在 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 四图)
  • 需要在图表上叠加自定义元素(十字线、标注、状态栏)
  • 对性能有较高要求(大数据量渲染)
  • 需要统一的事件处理(如鼠标拖动同步多个图表)
posted @ 2026-08-20 14:57  家煜宝宝  阅读(5)  评论(0)    收藏  举报