You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C# WinForms中处理耗时OnPaint事件并支持中断重绘的正确方式?

解决WinForms绘图中断旧任务并立即绘制新数据的方案

这个问题我之前帮不少WinForms开发者解决过——UI线程单线程的特性导致大量绘制阻塞界面,还要实现中断旧绘制的话,核心是引入取消机制+把耗时绘制从UI线程剥离,给你两个实用的方案:

方案1:用CancellationToken在UI线程内中断绘制(简单易实现)

这个方案适合点数量不是特别夸张的场景,直接在Paint事件里插入取消检查,每次切换数据时取消当前的绘制任务:

步骤1:定义全局取消令牌和数据

在Form类里声明需要的变量:

private CancellationTokenSource _currentDrawCts;
private List<Point> _currentPoints;

步骤2:切换数据时触发取消与重绘

写一个切换数据的方法,每次切换时先取消正在进行的绘制,再更新数据并触发重绘:

private void SwitchToNewData(List<Point> newPoints)
{
    // 取消并清理旧的取消令牌
    _currentDrawCts?.Cancel();
    _currentDrawCts?.Dispose();
    
    // 更新数据并创建新的取消令牌
    _currentPoints = newPoints;
    _currentDrawCts = new CancellationTokenSource();
    
    // 触发控件重绘
    yourDrawingControl.Invalidate();
}

步骤3:在Paint事件中加入取消检查

在Paint事件处理程序里,每绘制一定数量的点就检查是否需要中断:

private void yourDrawingControl_Paint(object sender, PaintEventArgs e)
{
    if (_currentPoints == null) return;
    
    var g = e.Graphics;
    // 关闭抗锯齿、使用实心画笔等优化绘制速度
    g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
    var brush = Brushes.Black;
    
    for (int i = 0; i < _currentPoints.Count; i++)
    {
        // 每绘制100个点检查一次取消信号(可根据需求调整频率)
        if (i % 100 == 0 && _currentDrawCts.Token.IsCancellationRequested)
        {
            return; // 直接中断绘制流程
        }
        
        var point = _currentPoints[i];
        // 绘制点(这里用用小椭圆模拟,你可以换成自己的绘制逻辑)
        g.FillEllipse(brush, point.X - 1, point.Y - 1, 2, 2);
    }
}

方案2:后台绘制+双缓冲(推荐,完全避免UI卡顿)

如果点的数量特别大(上万甚至更多),UI线程绘制还是会有卡顿,这时候把绘制逻辑放到后台线程,绘制到Bitmap后再在UI线程显示,同时用取消令牌中断后台任务:

步骤1:定义全局变量

private CancellationTokenSource _currentDrawCts;
private List<Point> _currentPoints;
private Bitmap _backBuffer; // 后台绘制的缓存位图

步骤2:后台绘制并更新UI

用Task.Run在后台线程绘制,完成后回到UI线程更新缓存:

private async void SwitchToNewData(List<Point> newPoints)
{
    // 取消并清理旧任务
    _currentDrawCts?.Cancel();
    _currentDrawCts?.Dispose();
    
    _currentPoints = newPoints;
    var cts = _currentDrawCts = new CancellationTokenSource();
    
    try
    {
        // 在后台线程绘制到位图
        var newBuffer = await Task.Run(() =>
        {
            // 创建和控件大小一致的位图
            var bmp = new Bitmap(yourDrawingControl.Width, yourDrawingControl.Height);
            using (var g = Graphics.FromImage(bmp))
            {
                g.Clear(Color.White); // 清空背景
                g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
                var brush = Brushes.Black;
                
                for (int i = 0; i < newPoints.Count; i++)
                {
                    // 检查是否取消,若取消则释放位图并返回null
                    if (cts.Token.IsCancellationRequested)
                    {
                        bmp.Dispose();
                        return null;
                    }
                    
                    var point = newPoints[i];
                    g.FillEllipse(brush, point.X - 1, point.Y - 1, 2, 2);
                }
            }
            return bmp;
        }, cts.Token);
        
        // 回到UI线程更新缓存并触发重绘
        if (newBuffer != null && !cts.Token.IsCancellationRequested)
        {
            _backBuffer?.Dispose(); // 清理旧缓存
            _backBuffer = newBuffer;
            yourDrawingControl.Invalidate();
        }
    }
    catch (OperationCanceledException)
    {
        // 任务被取消,无需额外处理
    }
}

步骤3:Paint事件仅显示缓存位图

这时候Paint事件只需要把后台绘制好的位图画出来,几乎不消耗UI线程资源:

private void yourDrawingControl_Paint(object sender, PaintEventArgs e)
{
    if (_backBuffer != null)
    {
        e.Graphics.DrawImage(_backBuffer, 0, 0);
    }
}

额外注意:资源清理

记得在Form关闭时释放资源,避免内存泄漏:

private void YourForm_FormClosing(object sender, FormClosingEventArgs e)
{
    _currentDrawCts?.Dispose();
    _backBuffer?.Dispose();
}

一些优化小技巧

  • 关闭抗锯齿、使用实心画笔/钢笔可以大幅提升绘制速度;
  • 若不需要全量重绘,可以用Invalidate(Rectangle)指定重绘区域;
  • 对于极大量的点,可以考虑分块绘制,比如每次只绘制当前可见区域的点,减少绘制量。

内容的提问来源于stack exchange,提问作者Robo Maklen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:00:23