Предисловие


Основная моя специализация — разработка ПО для систем реального времени на базе микроконтроллеров. Но иногда и на C# WinForms бывают разработки настольных приложений с весьма специфичными пользовательскими интерфейсами. Например, двоичное отображение слова по интерфейсу ДПК, представлено на рисунке ниже.

image

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

Хочу поделиться несколькими «эмпирическими» советами по разработке пользовательских компонентов, которые обеспечивают более быструю и организованную разработку оптимизированных визуальных (графически нагруженных) пользовательских компонентов.

Приведу несколько советов с практической демонстрацией.

Совет первый


Рендеринг (отрисовку) содержимого UserControl’а производить во внутренний визуальный буфер Bitmap.
Пример 1
       
        /// <summary>
        /// Общий визуальный буфер контрола
        /// </summary>
        Bitmap _imgControl;
        /// <summary>
        /// Визуальный буфер поля текст
        /// </summary>
        Bitmap _imgDPKText;
        /// <summary>
        /// поля значения Адрес
        /// </summary>
        Bitmap _imgDPKAdrVal;
        /// <summary>
        /// Визуальный буфер поля Адрес
        /// </summary>
        Bitmap _imgDPKFieldAdr;
        /// <summary>
        /// Визуальный буфер поля дата
        /// </summary>
        Bitmap _imgDPKFieldData;
        /// <summary>
        /// поля значения дата
        /// </summary>
        Bitmap _imgDPKDataVal;
        double TT = 0.2;
        double AT = 0.2;
        double AVT = 0.2;
        double DT = 0.2;
        double DVT = 0.2;
        /// <summary>
        /// Отрисовка визуального буфера контрола
        /// </summary>
        void Render()
        {
            if (_word == null) return;
            _imgControl = new Bitmap(_widthImgControl, _heightImgControl);
            using (Graphics g = Graphics.FromImage(_imgControl))
            {
                g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
                g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
                g.Clear(Color.White);
                int shiftY = 0;
                g.DrawImage(_imgDPKText, new Point(0, shiftY)); shiftY += (int)(_heightImgControl * TT);
                g.DrawImage(_imgDPKFieldAdr, new Point(0, shiftY)); shiftY += (int)(_heightImgControl * AT);
                g.DrawImage(_imgDPKAdrVal, new Point(0, shiftY)); shiftY += (int)(_heightImgControl * AVT);
                g.DrawImage(_imgDPKFieldData, new Point(0, shiftY)); shiftY += (int)(_heightImgControl * DT);
                g.DrawImage(_imgDPKDataVal, new Point(0, shiftY)); shiftY += (int)(_heightImgControl * DVT);
            } 
        }


А обработку события Paint организовывать таким образом, чтобы обеспечить только вывод визуального буфера с содержимым, а не производить рендеринг заново.
Пример 2
        /// <summary>
        /// Вывод визуального буфера на экран
        /// </summary>
        void DPKWordEdit_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.Clear(Color.White);
            if (_imgControl == null)
            {
                e.Graphics.DrawRectangle(new Pen(_clrBorder), new Rectangle(new Point(0,0), new Size(this.Width-1, this.Height-1)));
                return;
            }
            e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
            e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
            e.Graphics.DrawImage(_imgControl, new Rectangle(new Point(0,0), this.Size));
        }


Совет второй


Данный совет касается реализации масштабирования. Есть 2 пути:
1. Производить рендеринг содержимого компонента на основе новых размеров. (Это ресурсоёмко!)
2. Производить вывод визуального буфера без повторного рендеринга. (Масштабируя сам буфер Bitmap!)
Пример 3
        /// <summary>
        /// Обработка изменения размеров 
        /// </summary>
        void DPKWordEdit_Resize(object sender, EventArgs e)
        {
            Refresh();
        }	



        e.Graphics.DrawImage(_imgControl, new Rectangle(new Point(0,0), this.Size));


Совет третий


Данный совет касается рендеринга содержимого UserControl’a. Когда компонент получается «сложный», состоящий из множества различных элементов интерфейса, имеет смысл производить рендеринг в нескольких визуальных буферах (несколько логически разных области компонента).
Пример 4
        /// <summary>
        /// Общий визуальный буфер контрола
        /// </summary>
        Bitmap _imgControl;
        /// <summary>
        /// Визуальный буфер поля текст
        /// </summary>
        Bitmap _imgDPKText;
        /// <summary>
        /// поля значения Адрес
        /// </summary>
        Bitmap _imgDPKAdrVal;
        /// <summary>
        /// Визуальный буфер поля Адрес
        /// </summary>
        Bitmap _imgDPKFieldAdr;
        /// <summary>
        /// Визуальный буфер поля дата
        /// </summary>
        Bitmap _imgDPKFieldData;
        /// <summary>
        /// поля значения дата
        /// </summary>
        Bitmap _imgDPKDataVal;




        /// <summary>
        /// Отрисовка в визуальный буфер
        /// </summary>
        void DPKWordEdit_Rendering(object sender, EventArgs e)
        {
            RenderDPKText();
            RenderDPKFieldAdr();
            RenderDPKValAdr();
            RenderDPKFieldData();
            RenderDPKValData();
            Render();
            Refresh();
        }


А затем их объединять в визуальный буфер UserControl’а.
Пример 5
        /// <summary>
        /// Отрисовка визуального буфера контрола
        /// </summary>
        void Render()
        {
            if (_word == null) return;
            _imgControl = new Bitmap(_widthImgControl, _heightImgControl);
            using (Graphics g = Graphics.FromImage(_imgControl))
            {
                g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
                g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
                g.Clear(Color.White);
                int shiftY = 0;
                g.DrawImage(_imgDPKText, new Point(0, shiftY)); shiftY += (int)(_heightImgControl * TT);
                g.DrawImage(_imgDPKFieldAdr, new Point(0, shiftY)); shiftY += (int)(_heightImgControl * AT);
                g.DrawImage(_imgDPKAdrVal, new Point(0, shiftY)); shiftY += (int)(_heightImgControl * AVT);
                g.DrawImage(_imgDPKFieldData, new Point(0, shiftY)); shiftY += (int)(_heightImgControl * DT);
                g.DrawImage(_imgDPKDataVal, new Point(0, shiftY)); shiftY += (int)(_heightImgControl * DVT);
            } 
        }


Написан компонент на Visual Studio 2010 под .Net Framework 2.0 на WinForms.

Проект компонента и демонстрационный проект можно скачать по ссылке: cloud.mail.ru/public/JKhH/oGPrX4DFX.

Комментарии (12)


  1. xtraroman
    30.07.2015 14:06
    +5

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

    void DPKWordEdit_Rendering(object sender, EventArgs e)
            {
    ....
                Refresh();//!!
            }
    


    1.Это тяжелый вызов, как правило, его можно заменить на Invalidate().
    2. Вы повторили реализацию DoubleBuffer которая и так есть в Windows.Forms. Флики отрисовки можно побороть проще
    3. Текст обычно не ресайзят чтобы его всегда было удобно воспринимать.


    1. BlackEngineer Автор
      30.07.2015 22:14

      Спасибо за замечания.
      С первым полностью согласен.
      Со вторым нет — отрисовку в буфер я использую, чтобы потом при изменении размеров и обновлении экрана не рендерить на основе содержимого, а просто отрисовать один визуальный буфер Bitmap.
      С третьим частично согласен — у меня задача стоит максимально быстрой работы компонента, красота на дальний палн уходит


  1. vvovas
    30.07.2015 14:27
    +5

    Из своей разработки на WinForms (лет 5 назад) помню:
    1. Не используйте events там где есть override метод. Т.е. внутри контрола зачастую пишут Control_Resize вместо OnResize. Override гораздо быстрее.

    2. Делайте проверку на Debug Mode. Очень удобная вещь, чтобы ваш контрол нормально отображался в редакторе. Причем стандартный Debug mode работает неправильно(кажется не работает для контролов внутри контролов), если найду свою реализацию — напишу.

    3. Используйте аттрибуты для Toolbox. В нашем большом проекте, контролов было безумно много и все они по умолчанию попадали в Toolbox, который начинал долго грузиться. Есть специальные аттрибуты, которыми помечаются классы, чтобы не попадать в Toolbox

    4. Отписывайтесь от событий в Dispose. Это просто в обязательном порядке иначе контролы будут висеть в памяти.


    1. BlackEngineer Автор
      30.07.2015 22:19

      Благодарю за советы.
      Про первый и четвёртый сам дошёл, когда рефакторинг кода компонента сегодня производил.
      Про второй и третий — сроки поджимали — конец месяца… так конечно хорошо бы реализовать, но не критично, на мой взгляд.
      Компонент то я этот изобрёл во второй половине дня, после обеда, а потом когда первую релиз-версию коллегам отдал, то и подзабил на рефакторинг. Только сегодня подправил — по заказам трудящихся.


      1. vvovas
        31.07.2015 08:29

        Просмотрел исходники для второго пункта:
        все контролы имеют свойство DesignMode, но как я уже говорил — работает оно не совсем корректно и правильно. Правильнее использовать следующую проверку:
        (LicenseManager.UsageMode == LicenseUsageMode.Designtime);

        Это свойство дает вам возможность получить отображение контролов в дизайнере без ошибок. Сложные контролы, которые могут иметь какую-то логику в конструкторе или на событии OnLoad(например, загрузку данных из базы), не будут отображаться в дизайнере. Используя DesignMode можно пропустить эти шаги.

        Ну и аттрибут [ToolboxItem(false)], который скрывает класс из toolbox'a обычно мы им помечали всякие базовые контролы, которые никогда в дизайнере не будут использоваться и dataset'ы.


        1. BlackEngineer Автор
          31.07.2015 09:05

          Попробовал (LicenseManager.UsageMode == LicenseUsageMode.Designtime);
          Но это не работает… Контрол не понимает, что он в режиме дизайнера…
          Я лично использую такую конструкцию:
          If ((Site != null) && (Site.DesignMode))


          1. vvovas
            31.07.2015 09:45

            Да, там несколько есть способов. К сожалению, я работал с WinForms последний раз года 4 назад, так что просто скопировал нашу проверку из кода. У нас нареканий не было, возможно есть какие-то свои ограничения.


    1. QtRoS
      31.07.2015 11:10

      По пункту 4 можно поподробнее?


      1. vvovas
        31.07.2015 12:04

        Вообще все, конечно зависит от архитектуры приложения, но идея в том, что любая подписка на событи связывает 2 объекта и сборщик мусора их соберет только, если они оба будут удалены.
        Т.е. вы создаете контрол, который связывается событиями с формой или другим контролом, форма связывается с контроллером и т.д. Потом вы написали form.Close() и забыли про форму, а она со всеми контролами в памяти повисла, потому что от событий не отписались.


        1. QtRoS
          31.07.2015 14:51

          Все, понял Вашу мысль, спасибо — я еще прочитал неправильно, как «Отписывайтесь от события Dispose», что меняет смысл…


  1. KindDragon
    30.07.2015 18:13

    <>


  1. BlackEngineer Автор
    31.07.2015 09:02

    !