c# - C# - 面板上的异步绘图
问题描述
在我的 Winforms 应用程序中,我试图重新创建蒙特卡洛方法来近似 PI。表单本身由一个框组成,用户在其中提供点的数量和一个面板,我想在其上进行绘制。不过,对于这个例子,我们假设金额是恒定的。
private int Amount = 10000;
private int InCircle = 0, Points = 0;
private Pen myPen = new Pen(Color.White);
private void DrawingPanel_Paint(object sender, PaintEventArgs e)
{
int w = DrawingPanel.Width, h = DrawingPanel.Height;
e.Graphics.TranslateTransform(w / 2, h / 2);
//drawing the square and circle in which I will display the points
var rect = new Rectangle(-w / 2, -h / 2, w - 1, h - 5);
e.Graphics.DrawRectangle(myPen, rect);
e.Graphics.DrawEllipse(myPen, rect);
double PIE;
int X, Y;
var random = new Random();
for (int i = 0; i < Amount; i++)
{
X = random.Next(-(w / 2), (w / 2) + 1);
Y = random.Next(-(h / 2), (h / 2) + 1);
Points++;
if ((X * X) + (Y * Y) < (w / 2 * h / 2))
{
InCircle++;
e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1);
}
else
{
e.Graphics.FillRectangle(Brushes.Cyan, X, Y, 1, 1);
}
//just so that the points appear with a tiny delay
Thread.Sleep(1);
}
PIE = 4 * ((double)InCircle/(double)Points);
}
这有效。可视化很棒。但是,现在我想异步重新创建它,这样当它在后台绘制时,应用程序仍然负责,用户可以做其他事情,甚至只是移动窗口。最初,我创建了第二种绘图方法,我从事件处理程序中调用它:
private double Calculate(PaintEventArgs e)
{
int w = DrawingPanel.Width, h = DrawingPanel.Height;
double PIE;
int X, Y;
var random = new Random();
for (int i = 0; i < Amount; i++)
{
X = random.Next(-(w / 2), (w / 2) + 1);
Y = random.Next(-(h / 2), (h / 2) + 1);
Points++;
if ((X * X) + (Y * Y) < (w / 2 * h / 2))
{
InCircle++;
e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1);
}
else
{
e.Graphics.FillRectangle(Brushes.Cyan, X, Y, 1, 1);
}
Thread.Sleep(1);
}
PIE = 4 * ((double)InCircle/(double)Points);
return PIE;
}
private void DrawingPanel_Paint(object sender, PaintEventArgs e)
{
int w = DrawingPanel.Width, h = DrawingPanel.Height;
e.Graphics.TranslateTransform(w / 2, h / 2);
var rect = new Rectangle(-w / 2, -h / 2, w - 1, h - 5);
e.Graphics.DrawRectangle(myPen, rect);
e.Graphics.DrawEllipse(myPen, rect);
var result = Calculate(e);
}
这也很好。直到我使事件处理程序异步。
private async void DrawingPanel_Paint(object sender, PaintEventArgs e) {...}
现在,当我尝试通过 Task.Run 运行 Calculate 方法时,或者当我将其返回类型更改为 Task 并启动它时,我在以下行中收到错误:“Parameter is not valid”:
e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1);
现在的问题是,是否可以在面板上异步绘制,从而使应用程序的其他部分不被锁定?如果没有,有没有办法使用任何其他方式(不一定是面板)重新创建这个算法?干杯。
解决方案
其他海报提出的问题是完全正确的,但如果你稍微改变一下策略,这实际上是完全可以实现的。
虽然您不能Graphics
在另一个线程上绘制到 UI 上下文,但没有什么可以阻止您绘制到非 UI 上下文。所以你可以做的是有一个你绘制的缓冲区:
private Image _buffer;
然后,您决定开始绘图的触发事件是什么;让我们假设这是一个按钮点击:
private async void button1_Click(object sender, EventArgs e)
{
if (_buffer is null)
{
_buffer = new Bitmap(DrawingPanel.Width, DrawingPanel.Height);
}
timer1.Enabled = true;
await Task.Run(() => DrawToBuffer(_buffer));
timer1.Enabled = false;
DrawingPanel.Invalidate();
}
你会在那里看到计时器;您将 a 添加Timer
到表单并将其设置为与您想要的绘图刷新率相匹配;所以 25 帧/秒将是 40 毫秒。在计时器事件中,您只需使面板无效:
private void timer1_Tick(object sender, EventArgs e)
{
DrawingPanel.Invalidate();
}
您的大部分代码只是按原样移动到DrawToBuffer()
方法中,并且您Graphics
从缓冲区而不是 UI 元素中获取:
private void DrawToBuffer(Image image)
{
using (Graphics graphics = Graphics.FromImage(image))
{
int w = image.Width;
int h = image.Height;
// Your code here
}
}
现在您只需将面板绘制事件更改为从缓冲区复制:
private void DrawingPanel_Paint(object sender, PaintEventArgs e)
{
if (!(_buffer is null))
{
e.Graphics.DrawImageUnscaled(_buffer, 0, 0);
}
}
复制缓冲区非常快;可能BitBlt()
在下面使用。
需要注意的是,您现在需要对 UI 更改更加小心。例如,如果可以在DrawingPanel
缓冲区渲染的中途更改大小,则需要满足这一点。此外,您需要防止同时发生 2 个缓冲区更新。
可能还有其他需要满足的事情;例如,您可能需要DrawImage()
代替DrawImageUnscaled()
等。我并不是说上面的代码是完美的,只是给您一个可以使用的想法。
推荐阅读
- sql - 对每个 UserId 实施限制的最快方法
- .net - 高 DPI 150%+ 上的 Graphics.DrawImage 仅绘制图像的一部分
- devise - ActiveAdmin 和 Warden 路由约束
- javascript - 如何在选择时将 CSS 样式添加到复选框?
- registry - 查询 HKCU 中显示的已安装应用程序值?
- ios - 如何使用 UIContextMenuConfiguration 给出的建议操作?
- javascript - 使用 React 从 FileStream 返回下载文件
- php - 如果where子句是零整数,为什么sql查询返回一个值?
- python - 将一个数字范围内的所有数字相乘
- javascript - 检查数组中链接的单击是否具有子菜单键值。IF true 运行一些代码 ELSE 运行其他代码