C # - асинхронное рисование на панели - PullRequest
0 голосов
/ 01 декабря 2018

В моем приложении 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) {...}

Теперь, когда я пытаюсь запустить метод Calculate через Task.Run или когда я изменяю его тип возвращаемого значения на Task и запускаю его, я получаю ошибку: «Параметр недействителен» в следующей строке:

e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1);

Теперь вопрос, возможно ли рисовать на панели асинхронно, чтобы другие части приложения не были заблокированы?И если нет, есть ли способ воссоздать этот алгоритм, используя любой другой способ (не обязательно панель)?Приветствия.

Ответы [ 3 ]

0 голосов
/ 01 декабря 2018

Вы не должны рисовать в форме или элементе управления из другого потока.В режиме отладки WinForms вызовет исключение, если вы это сделаете.

Наилучшим подходом будет использование компонента Timer.На каждом такте таймера делайте один шаг от вашего цикла.Вам, конечно, придется переместить счетчик взгляда как глобальную переменную.

0 голосов
/ 01 декабря 2018

Проблемы, поднятые другими авторами, полностью действительны, но на самом деле это вполне достижимо, если немного изменить стратегию.

Хотя вы не можете нарисовать контекст пользовательского интерфейса Graphics в другом потоке, существуетничто не мешает вам рисовать не-пользовательский интерфейс.Итак, что вы могли бы сделать, так это иметь буфер, в который вы рисуете:

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();
}

Там вы увидите таймер;вы добавляете Timer к форме и устанавливаете ее в соответствии с желаемой частотой обновления чертежа;так что 25 кадров в секунду будет 40 мс.В событии таймера вы просто аннулируете панель:

private void timer1_Tick(object sender, EventArgs e)
{
    DrawingPanel.Invalidate();
}

Большая часть вашего кода просто перемещается как есть в метод DrawToBuffer(), и вы извлекаете Graphics из буфера, а не изЭлемент пользовательского интерфейса:

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() внизу.


Предостережение: вам нужно быть немного осторожнее с изменениями пользовательского интерфейса.Например, если есть возможность изменить размер вашего DrawingPanel на полпути через рендеринг буфера, вам нужно учесть это.Кроме того, вам нужно предотвратить одновременное обновление 2 буферов.

Могут быть и другие вещи, которые вам нужно учитывать;например, вам может потребоваться DrawImage() вместо DrawImageUnscaled() и т. д. Я не утверждаю, что приведенный выше код является идеальным, просто что-то, чтобы дать вам идею для работы.

0 голосов
/ 01 декабря 2018

Вы не можете сделать это асинхронно, потому что все рисунки должны быть выполнены в потоке пользовательского интерфейса.
Что вы можете сделать, это использовать Application.DoEvents(), чтобы сигнализировать пользовательскому интерфейсу, что он может обрабатывать ожидающие сообщения:

for (int i = 0; i < Amount; i++)
{
    // consider extracting it to a function and separate calculation\logic from drawing
    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)) // and this too
    {
        InCircle++;
        e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1); 
    }
    else
    {
        // just another one suggestion - you repeat your code, only color changes
        e.Graphics.FillRectangle(Brushes.Cyan, X, Y, 1, 1); 
    }

    Thread.Sleep(1); // do you need it? :)
    Application.DoEvents(); // 
}

Это сделает ваше приложение отзывчивым во время рисования.

Подробнее о Application.DoEvents() здесь:
Использование Application.DoEvents ()

...