Асинхронность при рисовании в 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 шт):

Автор решения: alexpanarin

Не знаю актуально еще или нет. Набросал код для вашего случая.

  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")
        ];
    
      }
    }
    
  2. Это сам элемент

     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);
         }
     }
    
  3. В вашем примере использование async/await не будет работать. Не проверял, но по тому как вы делаете Paint += ..., а этот event объявлен ка (delegate void PaintEventHandler(object? sender, PaintEventArgs e);), то callback будет выполняться синхронно. В противном случае он бы выдал вам ошибку при попытке обратиться к методам контрола не из основного потока. Поэтому решения без блокировки основного потока на OnPaint я не вижу. Даже если мы запустим Task и будем пытаться из нее рисовать, все равно придется маршалить вызов из вспомогательного потока в основной, используя Control.Invoke, что приведет к блокировке. Удачи!

  4. И еще один вариант с таской, - кстати получилось веселее. Это новый вариант контрола

     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 эту очередь разбираем. В общем блокировка основного потока происходит менее значительно, чем в первом варианте, хотя второй вариант "тяжелее"

→ Ссылка