Асинхронность при рисовании в windowsForms
Мне нужно отрисовать линейный список по этапам, параллельно выводя сопровождающий текст на каждом этапе, для этого я попытался добавить либо ожидание с помощью
await Task.Delay(2000);
Либо
Thread.Sleep(2000);
Оно происходит в непонятном порядке.
Пробовал я через ожидание нажатия кнопки, в таком случае программа крашится после первого же ожидания при первом вызове отрисовки. В интернетах я увидел, что асинхронность в отрисовке невозможна, но неужто нет никакого выхода?
Код правок:
namespace DataStucturesVisualizer
{
public partial class Form1 : Form
{
private LinkedList<int> linkedList = new LinkedList<int>();
public Form1()
{
InitializeComponent();
listPanel.Paint += (s, e) => DrawLinkedListAsync(e.Graphics);
}
private async Task DrawNodeAsync(Graphics g, int x, int y, string value, bool isLastNode)
{
int width = 80;
int height = 50;
int splitHeight = height / 2;
g.DrawRectangle(Pens.Black, x, y, width, height);
AddCommandToFlowLayoutPanel("new(p)");
await Task.Delay(1000);
g.DrawLine(Pens.Black, x, y + splitHeight, x + width, y + splitHeight);
AddCommandToFlowLayoutPanel("read(f, p^.inf)");
await Task.Delay(1000);
g.DrawString(value, new Font("Arial", 10), Brushes.Black, x + 5, y + 5);
AddCommandToFlowLayoutPanel("read(f, p^.inf)");
await Task.Delay(1000);
if (isLastNode)
{
g.DrawString("nil", new Font("Arial", 10), Brushes.Black, x + 5, y + splitHeight + 5);
AddCommandToFlowLayoutPanel("p^.next := nil");
await Task.Delay(1000);
}
}
private void AddCommandToFlowLayoutPanel(string command)
{
Label commandLabel = new Label
{
Text = command,
AutoSize = true,
Font = new Font("Arial", 12),
ForeColor = Color.Black,
Padding = new Padding(5)
};
commandFlowLayoutPanel.Controls.Add(commandLabel);
}
private async Task DrawArrowAsync(Graphics g, int x1, int y1, int x2, int y2)
{
g.DrawLine(Pens.Black, x1, y1, x2, y2);
g.DrawLine(Pens.Black, x2, y2, x2 - 5, y2 - 5);
g.DrawLine(Pens.Black, x2, y2, x2 - 5, y2 + 5);
AddCommandToFlowLayoutPanel("p^.next := nil");
await Task.Delay(2000);
}
private async Task DrawLinkedListAsync(Graphics g)
{
int x = 10, y = 10;
const int nodeSpacing = 100;
int visibleXStart = -listPanel.AutoScrollPosition.X;
int visibleXEnd = visibleXStart + listPanel.Width;
var current = linkedList.First;
while (current != null)
{
string value = current.Value.ToString();
bool isLastNode = current.Next == null;
if (x + 80 >= visibleXStart && x <= visibleXEnd)
{
await DrawNodeAsync(g, x, y, value, isLastNode);
if (!isLastNode)
{
int arrowX1 = x + 40;
int arrowY1 = y + 50;
int arrowX2 = x + nodeSpacing;
int arrowY2 = y + 50;
await DrawArrowAsync(g, arrowX1, arrowY1, arrowX2, arrowY2);
}
}
x += nodeSpacing;
current = current.Next;
}
}
private static async Task WaitForButtonClick(Button button)
{
var tcs = new TaskCompletionSource<bool>();
EventHandler handler = null;
handler = (s, e) =>
{
tcs.SetResult(true);
button.Click -= handler;
};
button.Click += handler;
await tcs.Task;
}
private void UpdateVisualization()
{
int totalWidth = linkedList.Count * 100;
listPanel.AutoScrollMinSize = new Size(totalWidth, listPanel.Height);
listPanel.Refresh();
}
private async void listPanel_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.TranslateTransform(listPanel.AutoScrollPosition.X, listPanel.AutoScrollPosition.Y);
await DrawLinkedListAsync(g);
}
private void addNode(object sender, EventArgs e)
{
if (int.TryParse(inputTextBox.Text, out int value))
{
linkedList.AddFirst(value);
UpdateVisualization();
}
}
private void removeNode(object sender, EventArgs e)
{
if (linkedList.Count > 0)
{
linkedList.RemoveFirst();
UpdateVisualization();
}
}
}
}
Ответы (1 шт):
Не знаю актуально еще или нет. Набросал код для вашего случая.
Это контрол, на котором будем отрисовывать ваши элементы.
namespace WinFormsDraw { public partial class DrawPannel : UserControl { private ManualResetEventSlim m_event = new ManualResetEventSlim(true); public DrawPannel() { InitializeComponent(); } protected override void OnPaint(PaintEventArgs e) { int x = 10, y = 10; const int nodeSpacing = 100; base.OnPaint(e); // Отрисовка базовых вещей //Debug.WriteLine("On Paint"); foreach (var item in Items) { while (item.State != DrawState.None) { m_event.Reset(); item.Draw(e.Graphics, x, y); m_event.Wait(TimeSpan.FromSeconds(1)); // Блокируем основной поток } x += nodeSpacing; } } public List<DrawItem> Items = [ new DrawItem("Item one"), new DrawItem("Item two"), new DrawItem("Item three") ]; } }
Это сам элемент
namespace WinFormsDraw { public enum DrawState { None, Rect, Line, String } public class DrawItem { private readonly string value; private readonly int width; private readonly int height; public DrawState State { get; set; } public DrawItem(string value, int width = 80, int height = 50) { this.value = value; this.width = width; this.height = height; State = DrawState.Rect; } public void Draw(Graphics g, int x, int y) { if (State == DrawState.Rect) { DrawRect(g, x, y); State = DrawState.Line; } else if (State == DrawState.Line) { DrawLine(g, x, y); State = DrawState.String; } else if (State == DrawState.String) { DrawString(g, x, y); State = DrawState.None; } } public void DrawRect(Graphics g, int x, int y) => g.DrawRectangle(Pens.Black, x, y, width, height); public void DrawLine(Graphics g, int x, int y) => g.DrawLine(Pens.Black, x, y + (height / 2), x + width, y + (height / 2)); public void DrawString(Graphics g, int x, int y) => g.DrawString(value, new Font("Arial", 10), Brushes.Black, x + 5, y + 5); } }
В вашем примере использование async/await не будет работать. Не проверял, но по тому как вы делаете Paint += ..., а этот event объявлен ка (delegate void PaintEventHandler(object? sender, PaintEventArgs e);), то callback будет выполняться синхронно. В противном случае он бы выдал вам ошибку при попытке обратиться к методам контрола не из основного потока. Поэтому решения без блокировки основного потока на OnPaint я не вижу. Даже если мы запустим Task и будем пытаться из нее рисовать, все равно придется маршалить вызов из вспомогательного потока в основной, используя Control.Invoke, что приведет к блокировке. Удачи!
И еще один вариант с таской, - кстати получилось веселее. Это новый вариант контрола
using System.Collections.Concurrent; namespace WinFormsDraw { public partial class DrawPannel : UserControl { private ConcurrentQueue<DrawItem> m_queue = new ConcurrentQueue<DrawItem>(); private volatile int m_index = 0; public DrawPannel() { InitializeComponent(); m_queue.Enqueue(Items[m_index]); ThreadPool.QueueUserWorkItem(async state => { while (true) { await Task.Delay(1000); var control = (Control)state!; // Качество кода так себе... var item = Items[m_index]; if( item.State == DrawState.None) { if (++ m_index == Items.Count) break; m_queue.Enqueue(Items[m_index]); } control.Invoke(control.Refresh); } }, this) ; } protected override void OnPaint(PaintEventArgs e) { int x = 10, y = 10; const int nodeSpacing = 100; base.OnPaint(e); foreach (var drawItem in m_queue.ToArray()) { var index = Items.IndexOf(drawItem); var dx = x + (index * nodeSpacing); drawItem.Draw(e.Graphics, dx, y); } } public List<DrawItem> Items = [ new DrawItem("Item one"), new DrawItem("Item two"), new DrawItem("Item three") ]; } }
И элемент
namespace WinFormsDraw
{
public enum DrawState
{
None,
Rect,
Line,
String
}
public class DrawItem
{
private readonly string value;
private readonly int width;
private readonly int height;
public DrawState State { get; set; }
public DrawItem(string value, int width = 80, int height = 50)
{
this.value = value;
this.width = width;
this.height = height;
State = DrawState.Rect;
}
public void Draw(Graphics g, int x, int y)
{
if (State == DrawState.Rect)
{
DrawRect(g, x, y);
State = DrawState.Line;
}
else if (State == DrawState.Line)
{
DrawRect(g, x, y);
DrawLine(g, x, y);
State = DrawState.String;
}
else if (State == DrawState.String
|| State == DrawState.None)
{
DrawRect(g, x, y);
DrawLine(g, x, y);
DrawString(g, x, y);
State = DrawState.None;
}
}
public void DrawRect(Graphics g, int x, int y) =>
g.DrawRectangle(Pens.Black, x, y, width, height);
public void DrawLine(Graphics g, int x, int y) =>
g.DrawLine(Pens.Black, x, y + (height / 2), x + width, y + (height / 2));
public void DrawString(Graphics g, int x, int y) =>
g.DrawString(value, new Font("Arial", 10), Brushes.Black, x + 5, y + 5);
}
}
В общем смысл такой. Создаем очередь в которую по таймеру пихаем элементы, а на Paint эту очередь разбираем. В общем блокировка основного потока происходит менее значительно, чем в первом варианте, хотя второй вариант "тяжелее"