Graphics.FillEllipse()执行Task.Delay()后抛出参数无效异常求助
Graphics.FillEllipse()执行Task.Delay()后抛出参数无效异常求助
嗨,我来帮你捋清楚这个问题的根源和解决办法~
首先,你遇到的System.ArgumentException异常,核心原因是**Paint事件里的Graphics对象生命周期和异步等待的冲突**:
Paint事件触发时,系统会临时创建一个Graphics实例(也就是e.Graphics)给你用来绘制界面,一旦Paint事件处理方法执行完毕,系统就会自动回收这个Graphics对象,它就不再有效了。- 你在方法里用了
await Task.Delay(wait),这会让方法暂停执行,控制权交还给UI线程,Paint事件会提前结束——这时候e.Graphics已经被系统释放了。等Delay结束后,你再调用FillEllipse去操作这个已经失效的Graphics对象,自然就会抛出“参数无效”的异常。
另外,async void在UI事件里使用本身就有风险,一旦异步过程中出现异常,会直接触发未捕获异常,可能导致程序崩溃,所以尽量别在Paint这类UI事件里用异步等待。
给你两个可行的解决方案:
方案一:用Timer控制绘制时机(推荐)
把异步等待的逻辑移出Paint事件,改用System.Windows.Forms.Timer来定时触发重绘,Paint事件只负责同步绘制当前状态的内容:
- 先提前把所有星星数据加载到内存里(避免绘制时频繁读文件拖慢UI),然后用Timer每隔指定时间更新当前要绘制的帧,再触发重绘。
示例代码大概是这样:
// 定义成员变量存储数据和状态 private int currentFrame = 0; private Timer _drawTimer; private List<(double x, double z)> _allStarData; // 存储所有星星的坐标数据 private readonly int _totalFrames; // 总帧数 public Form1() { InitializeComponent(); // 提前加载所有星星数据 LoadAllStarData(); // 计算总帧数 _totalFrames = seconds * tickspersecond - 1; // 初始化Timer _drawTimer = new Timer { Interval = wait, // 你的等待毫秒数 Enabled = false }; _drawTimer.Tick += OnDrawTimerTick; // 可以在Form加载完成后启动Timer this.Load += (s, e) => _drawTimer.Start(); } private void LoadAllStarData() { _allStarData = new List<(double x, double z)>(); // 一次性读取所有坐标数据,提前乘上constant while (!readerx.EndOfStream && !readerz.EndOfStream) { double x = Convert.ToDouble(readerx.ReadLine()) * constant; double z = Convert.ToDouble(readerz.ReadLine()) * constant; _allStarData.Add((x, z)); } // 释放文件读取器(现在可以放心释放了) readerx.Dispose(); readerz.Dispose(); } private void OnDrawTimerTick(object sender, EventArgs e) { if (currentFrame < _totalFrames) { currentFrame++; Invalidate(); // 触发Paint事件重绘当前帧 } else { _drawTimer.Stop(); // 绘制完成,停止Timer } } // 修改后的Paint事件,只负责同步绘制 private void Form1_Paint(object sender, PaintEventArgs e) { e.Graphics.Clear(Color.White); // 计算当前帧要绘制的星星范围 int startIdx = currentFrame * starslenght; if (startIdx + starslenght > _allStarData.Count) return; // 绘制当前帧的所有星星 for (int i = startIdx; i < startIdx + starslenght; i++) { var star = _allStarData[i]; e.Graphics.FillEllipse(brush, (int)star.x + 960, (int)star.z + 540, 2, 2); } }
方案二:使用异步任务更新状态,触发重绘
如果必须保留异步读取的逻辑(比如数据量太大没法一次性加载),可以把文件读取放到异步任务里,每次读取一帧的数据后,调用Invalidate()触发重绘,而不是在Paint事件里等待:
private int _currentFrame = 0; private List<(double x, double z)> _currentFrameStars; private async void StartDrawing() { try { for (_currentFrame = 0; _currentFrame < seconds * tickspersecond - 1; _currentFrame++) { // 异步读取当前帧的星星数据 _currentFrameStars = new List<(double x, double z)>(); for (int i = 0; i < starslenght; i++) { double x = Convert.ToDouble(await readerx.ReadLineAsync()) * constant; double z = Convert.ToDouble(await readerz.ReadLineAsync()) * constant; _currentFrameStars.Add((x, z)); } Invalidate(); // 触发重绘 await Task.Delay(wait); } // 最后释放资源 readerx.Dispose(); readerz.Dispose(); } catch (Exception ex) { // 处理异常,避免程序崩溃 MessageBox.Show($"绘制出错:{ex.Message}"); } } private void Form1_Paint(object sender, PaintEventArgs e) { e.Graphics.Clear(Color.White); if (_currentFrameStars == null) return; // 同步绘制当前帧的星星 foreach (var star in _currentFrameStars) { e.Graphics.FillEllipse(brush, (int)star.x + 960, (int)star.z + 540, 2, 2); } }
额外注意点:
- 不要复用
Paint事件的Graphics对象,每次绘制都要用当前事件提供的e.Graphics。 - 文件读取操作尽量不要在UI线程执行,方案二里用了
ReadLineAsync来异步读取,避免阻塞UI。 - 记得在合适的时机释放
brush、文件读取器等资源,避免内存泄漏。
备注:内容来源于stack exchange,提问作者levvalentin
相关产品推荐
相关产品推荐

