GDI + Paint Queue Problem

товарищи) Я обнаружил интересное поведение метода Invalidate в многопоточных приложениях. Надеюсь, вы поможете мне с проблемой ...

У меня проблемы, когда я пытаюсь аннулировать разные элементы управления одновременно: хотя они идентичны, один успешно перекрашивается, а другой - нет.

Вот пример: у меня есть форма (MysticForm) с двумя панелями (SlowRenderPanel) на ней. Каждая панель имеет таймер и с периодом 50 мс вызывается метод Invalidate (). В методе OnPaint я рисую номер текущего вызова OnPaint в центре панели. Но обратите внимание, что в методе OnPaint вызывается метод System.Threading.Thread.Sleep (50) для имитации длительной процедуры рисования.

Проблема в том, что панель, добавленная первой, перекрашивается гораздо чаще, чем другая.

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WindowsFormsApplication1 {
    static class Program {
        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MysticForm());
        }
    }

    public class MysticForm : Form {
        public SlowRenderPanel panel1;
        public SlowRenderPanel panel2;

        public MysticForm() {
            // add 2 panels to the form
            Controls.Add(new SlowRenderPanel() { Dock = DockStyle.Left, BackColor = Color.Red, Width = ClientRectangle.Width / 2 });
            Controls.Add(new SlowRenderPanel() { Dock = DockStyle.Right, BackColor = Color.Blue, Width = ClientRectangle.Width / 2 });
        }
    }

    public class SlowRenderPanel : Panel {
        // synchronized timer
        private System.Windows.Forms.Timer timerSafe = null;
        // simple timer
        private System.Threading.Timer timerUnsafe = null;
        // OnPaint call counter
        private int counter = 0;

        // allows to use one of the above timers
        bool useUnsafeTimer = true;

        protected override void Dispose(bool disposing) {
            // active timer disposal
            (useUnsafeTimer ? timerUnsafe as IDisposable : timerSafe as IDisposable).Dispose();
            base.Dispose(disposing);
        }

        public SlowRenderPanel() {
            // anti-blink
            DoubleBuffered = true;
            // large font
            Font = new Font(Font.FontFamily, 36);

            if (useUnsafeTimer) {
                // simple timer. starts in a second. calls Invalidate() with period = 50ms
                timerUnsafe = new System.Threading.Timer(state => { Invalidate(); }, null, 1000, 50);
            } else {
                // safe timer. calls Invalidate() with period = 50ms
                timerSafe = new System.Windows.Forms.Timer() { Interval = 50, Enabled = true };
                timerSafe.Tick += (sender, e) => { Invalidate(); };
            }
        }

        protected override void OnPaint(PaintEventArgs e) {
            string text = counter++.ToString();

            // simulate large bitmap drawing
            System.Threading.Thread.Sleep(50);

            SizeF size = e.Graphics.MeasureString(text, Font);
            e.Graphics.DrawString(text, Font, Brushes.Black, new PointF(Width / 2f - size.Width / 2f, Height / 2f - size.Height / 2f));
            base.OnPaint(e);
        }

    }

}

Информация об отладке:

1) Каждая панель имеет логическое поле useUnsafeTime (по умолчанию установлено значение true), которое позволяет использовать System.Windows.Forms.Timer (false) вместо System.Threading.Timer (true). В первом случае (System.Windows.Forms.Timer) все работает нормально. Удаление вызова System.Threading.Sleep в OnPaint также улучшает выполнение.

2) Установка интервала таймера на 25 мс или меньше вообще предотвращает перерисовку второй панели (пока пользователь не изменяет размер формы).

3) Использование System.Windows.Forms.Таймер приводит к увеличению скорости

4) Принудительное включение управления в контекст синхронизации (Invoke) не имеет смысла. Я имею в виду, что Invalidate (invalidateChildren = false) является «потокобезопасным» и, возможно, может иметь другое поведение в разных контекстах

5) Ничего интересного не обнаружено при сравнении этих двух таймеров с помощью IL ... Они просто используют разные функции WinAPI для установки и удалите таймеры (AddTimerNative, DeleteTimerNative для Threading.Timer; SetTimer, KillTimer для Windows.Forms.Timer), а Windows.Forms.Timer использует метод WndProc NativeWindow для увеличения события Tick

. Я использую аналогичный фрагмент кода в своем приложении и к сожалению, нет возможности использовать System.Windows.Forms.Timer) Я использую многопоточный рендеринг изображений двух панелей, и метод Invalidate вызывается после завершения рендеринга на каждой панели ...

Было бы здорово, если бы кто-то может помочь мне понять, что происходит за кулисами, и как решить проблему.

P.S. Интересное поведение, не правда ли? =)

1
задан Mikant 19 August 2010 в 20:27
поделиться

2 ответа

Хорошая демонстрация того, что идет не так, когда вы используете члены элемента управления или формы в фоновом потоке. Winforms обычно ловит это, но есть ошибка в коде метода Invalidate(). Измените его следующим образом:

 timerUnsafe = new System.Threading.Timer(state => { Invalidate(true); }, null, 1000, 50);

чтобы вызвать исключение.

Другая панель работает медленнее, потому что многие ее вызовы Invalidate() отменяются событием paint. Которое как раз достаточно медленно для этого. Классическая гонка потоков. Вы не можете вызвать Invalidate() из рабочего потока, синхронный таймер - очевидное решение.

1
ответ дан 2 September 2019 в 21:59
поделиться

Invalidate() делает недействительной клиентскую область или прямоугольник ( InvalidateRect() ) и "говорит" Windows, что в следующий раз Windows будет рисовать; обнови меня, покрась меня. Но это не вызывает или провоцирует сообщение о закраске. Чтобы вызвать событие paint, вы должны заставить окна рисовать после вызова Invalidate. Это не всегда необходимо, но иногда это то, что нужно сделать.

Для принудительной закраски нужно использовать функцию Update(). "Заставляет элемент управления перерисовать недействительные области в своей клиентской области."

В этом случае нужно использовать обе функции.


Edit: Обычная техника, позволяющая избежать подобных проблем, заключается в том, чтобы держать все процедуры рисования и все, что с ними связано, в одном (обычно главном) потоке или таймере. Логика может выполняться в другом месте, но фактические вызовы рисования должны быть в одном потоке или таймере.

Это делается в играх и 3D-симуляторах.

HTH

1
ответ дан 2 September 2019 в 21:59
поделиться
Другие вопросы по тегам:

Похожие вопросы: