none
Как можно ускорить кастомную отрисовку в WinForms? RRS feed

  • Общие обсуждения

  • Работаю на WinForms приложением, в котором нужно реализовать проверку правописания.

    Для перерисовки формы использую перехват Windows message. 
            /// <summary>
            /// Processes Windows messages.
            /// </summary>
            /// <param name="msg">
            /// A Windows Message object.
            /// </param>
            protected override void WndProc(ref Message msg)
            {
                switch (msg.Msg)
                {
                    case WM_PAINT:
                        Invalidate();
                        base.WndProc(ref msg);
    
                        if (IsSpellingAutoEnabled)
                        {
                            CustomPaint();
                        }
    
                        break;
    
                ...
    
                    default:
                        base.WndProc(ref msg);
                        break;
                }
            }

    Метод CustomPaint() выглядит так:
    private void CustomPaint()
            {
                Bitmap tempBitmap;
                Graphics bufferGraphics;
    
                tempBitmap = new Bitmap(Width, Height);
                bufferGraphics = Graphics.FromImage(tempBitmap);
                bufferGraphics.Clip = new Region(ClientRectangle);
    
                _textBoxGraphics = CreateGraphics();
                bufferGraphics.Clear(Color.Transparent);
    
                foreach (var wordStartIndex in UnderlinedSections.Keys)
                {
                    UnderlineWords(wordStartIndex, bufferGraphics);
                }
    
                _textBoxGraphics.DrawImageUnscaled(tempBitmap, 0, 0);
            }

    В UnderlineWords в буфер добавляются линии, которые нужно нарисовать с пом. такого метода:
    private void DrawWave(Graphics bufferGraphics, Point startOfLine, Point endOfLine)
            {
                startOfLine.Y--;
                endOfLine.Y--;
    
                var pen = Pens.Red;
    
                if ((endOfLine.X - startOfLine.X) >= 4)
                {
                    var points = new ArrayList();
                    for (int i = startOfLine.X; i <= (endOfLine.X - 2); i += 4)
                    {
                        points.Add(new Point(i, startOfLine.Y));
                        points.Add(new Point(i + 2, startOfLine.Y + 2));
                    }
    
                    var p = (Point[])points.ToArray(typeof(Point));
                    bufferGraphics.DrawLines(pen, p);
                }
            }

    При работе этого кода на тексте размером больше 500 символов, начинаются сильные тормоза и подвисания приложения. Как можно от этого избавиться? В какую сторону копать?

    25 апреля 2014 г. 7:27

Все ответы

  • Не понятно, зачем здесь используется WndProc. Для приведённого кода вполне можно использовать метод OnPaint или событие Paint. При этом можно будет использовать в них готовый e.Graphics.

    Тут я вижу создание битмапа одного размера и следом он сразу же обрезается. Это лишняя работа. Почему бы не создать его сразу нужного размера?

    tempBitmap = new Bitmap(Width, Height); bufferGraphics = Graphics.FromImage(tempBitmap); bufferGraphics.Clip = new Region(ClientRectangle);

    Далее в метод UnderlineWords передаётся графикс, на нём, вероятно, что-то рисуется, после чего битмап с этим графиксом рисуется на  _textBoxGraphics. Почему бы не рисовать сразу на последнем, без промежуточных шагов?

    В методе DrawWave используется ArrayList. Где вы откопали эту древность? Замените на типизированный List. Благодаря этому производительность повысится. Можно будет избавиться от приведения типа (Point[])points, что работает медленно.

    А самое главное - используйте профайлер. Только он покажет места, где наихудшая производительность. Профайлер уже встроен в некоторые версии VS (смотря какую вы используете). Можно скачать отдельный.

    25 апреля 2014 г. 10:46
  • Вот ещё что.

    Если размер вашего приложения (а точнее, рабочего поля) не меняется в ходе работы, то можно все битмапы и графиксы не пересоздавать каждый раз заново (что очень медленно), а создать один раз в начале, и потом использовать повторно.

    В любом случае обязательно следует вызывать Dispose у всех IDisposable объектов! К ним относятся Bitmap и Graphics.

    Инвалидировать нужно не всю поверхность формы, а лишь прямоугольник (или регион) нужно размера.

    Ещё может иметь смысл установить двойную буферизацию у формы (или того контрола, на котором происходит прорисовка).

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

    25 апреля 2014 г. 10:56
  • Не понятно, зачем здесь используется WndProc. Для приведённого кода вполне можно использовать метод OnPaint или событие Paint. При этом можно будет использовать в них готовый e.Graphics.

    Тут я вижу создание битмапа одного размера и следом он сразу же обрезается. Это лишняя работа. Почему бы не создать его сразу нужного размера?

    tempBitmap = new Bitmap(Width, Height); bufferGraphics = Graphics.FromImage(tempBitmap); bufferGraphics.Clip = new Region(ClientRectangle);

    Далее в метод UnderlineWords передаётся графикс, на нём, вероятно, что-то рисуется, после чего битмап с этим графиксом рисуется на  _textBoxGraphics. Почему бы не рисовать сразу на последнем, без промежуточных шагов?

    В методе DrawWave используется ArrayList. Где вы откопали эту древность? Замените на типизированный List. Благодаря этому производительность повысится. Можно будет избавиться от приведения типа (Point[])points, что работает медленно.

    А самое главное - используйте профайлер. Только он покажет места, где наихудшая производительность. Профайлер уже встроен в некоторые версии VS (смотря какую вы используете). Можно скачать отдельный.

    WndProc используется, потому что UserControl размещен на форме, следовательно обращаться можно только к обработчику OnPaint непосредственно формы, чтоб в данном случае не допустимо по требованиям.
    28 апреля 2014 г. 9:49
  • Вы упорно цепляетесь за WndProc. На rsdn вам тоже говорят, что это вовсе не обязательно.

    Вот набросал на коленке код. Часть слов подчёркиваются зелёной, часть - красной волнистой линией.

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

    Сама прорисовка происходит в событии Paint.

    Я не могу знать, на какое именно событие Scroll нужно реагировать. Но совершенно несложно делать это как в юзерконтроле, так и в форме.

    Дополнительно приходится обрабатывать событие SizeChanged.

    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Windows.Forms;
    
    namespace WinForm_SpellCheck
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                //InitializeComponent();
    
                MyControl my = new MyControl
                {
                    Parent = this,
                    Dock = DockStyle.Fill,
                };
            }
        }
    
        class DoubleBufferedPanel : Panel
        {
            public DoubleBufferedPanel()
            {
                SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
            }
        }
    
        class MyControl : UserControl
        {
            DoubleBufferedPanel panel;
    
            public MyControl()
            {
                this.AutoScroll = true;
    
                panel = new DoubleBufferedPanel
                {
                    Parent = this,
                    Dock = DockStyle.Top,
                    Height = 500,
                    BackColor = Color.White,
                    BorderStyle = BorderStyle.FixedSingle
                };
    
                panel.Paint += panel_Paint;
    
                this.Scroll += MyControl_Scroll;
                this.SizeChanged += MyControl_SizeChanged;
            }
    
            void MyControl_Scroll(object sender, ScrollEventArgs e)
            {
                panel.Invalidate();
            }
    
            void MyControl_SizeChanged(object sender, EventArgs e)
            {
                panel.Invalidate();
            }
    
            void panel_Paint(object sender, PaintEventArgs e)
            {
                string text = "";
                for (int i = 0; i < 10; ++i)
                    text += "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ";
    
                e.Graphics.DrawString(text, Font, Brushes.Black, panel.Bounds);
    
                int temporLength = "tempor".Length;
                int ipsumLength = "ipsum".Length;
    
                int index = 0;
                var tempors = new List<CharacterRange>();
                while ((index = text.IndexOf("tempor", index + temporLength)) != -1)
                {
                    tempors.Add(new CharacterRange(index, temporLength));
                }
    
                index = 0;
                var ipsums = new List<CharacterRange>();
                while ((index = text.IndexOf("ipsum", index + ipsumLength)) != -1)
                {
                    ipsums.Add(new CharacterRange(index, ipsumLength));
                }
    
                Underscore(e.Graphics, text, tempors.ToArray(), Pens.Red);
                Underscore(e.Graphics, text, ipsums.ToArray(), Pens.Green);
            }
    
            void Underscore(Graphics g, string text, CharacterRange[] characterRanges, Pen pen)
            {
                StringFormat stringFormat = new StringFormat();
                stringFormat.SetMeasurableCharacterRanges(characterRanges);
    
                var ranges = g.MeasureCharacterRanges(text, Font, panel.Bounds, stringFormat);
    
                foreach (var range in ranges)
                {
                    var rect = range.GetBounds(g);
                    if (!g.IsVisible(rect))
                        continue;
    
                    var points = new List<PointF>();
                    bool flag = true;
                    for (float x = rect.Left; x < rect.Right; x += 3)
                    {
                        if (flag)
                        {
                            points.Add(new PointF(x, rect.Bottom - 2));
                            flag = false;
                        }
                        else
                        {
                            points.Add(new PointF(x, rect.Bottom + 2));
                            flag = true;
                        }
                    }
                    g.DrawCurve(pen, points.ToArray());
                }
            }
        }
    }