Предисловие
Основная моя специализация — разработка ПО для систем реального времени на базе микроконтроллеров. Но иногда и на C# WinForms бывают разработки настольных приложений с весьма специфичными пользовательскими интерфейсами. Например, двоичное отображение слова по интерфейсу ДПК, представлено на рисунке ниже.
![image](http://s020.radikal.ru/i720/1507/a2/88c8592b0053.png)
Вот такой компонент пришлось изобрести для привычного пользователям отображения данных.
Хочу поделиться несколькими «эмпирическими» советами по разработке пользовательских компонентов, которые обеспечивают более быструю и организованную разработку оптимизированных визуальных (графически нагруженных) пользовательских компонентов.
Приведу несколько советов с практической демонстрацией.
Совет первый
Рендеринг (отрисовку) содержимого UserControl’а производить во внутренний визуальный буфер Bitmap.
/// <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 организовывать таким образом, чтобы обеспечить только вывод визуального буфера с содержимым, а не производить рендеринг заново.
/// <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!)
/// <summary> /// Обработка изменения размеров /// </summary> void DPKWordEdit_Resize(object sender, EventArgs e) { Refresh(); }
e.Graphics.DrawImage(_imgControl, new Rectangle(new Point(0,0), this.Size));
Совет третий
Данный совет касается рендеринга содержимого UserControl’a. Когда компонент получается «сложный», состоящий из множества различных элементов интерфейса, имеет смысл производить рендеринг в нескольких визуальных буферах (несколько логически разных области компонента).
/// <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’а.
/// <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)
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. Это просто в обязательном порядке иначе контролы будут висеть в памяти.BlackEngineer Автор
30.07.2015 22:19Благодарю за советы.
Про первый и четвёртый сам дошёл, когда рефакторинг кода компонента сегодня производил.
Про второй и третий — сроки поджимали — конец месяца… так конечно хорошо бы реализовать, но не критично, на мой взгляд.
Компонент то я этот изобрёл во второй половине дня, после обеда, а потом когда первую релиз-версию коллегам отдал, то и подзабил на рефакторинг. Только сегодня подправил — по заказам трудящихся.vvovas
31.07.2015 08:29Просмотрел исходники для второго пункта:
все контролы имеют свойство DesignMode, но как я уже говорил — работает оно не совсем корректно и правильно. Правильнее использовать следующую проверку:
(LicenseManager.UsageMode == LicenseUsageMode.Designtime);
Это свойство дает вам возможность получить отображение контролов в дизайнере без ошибок. Сложные контролы, которые могут иметь какую-то логику в конструкторе или на событии OnLoad(например, загрузку данных из базы), не будут отображаться в дизайнере. Используя DesignMode можно пропустить эти шаги.
Ну и аттрибут [ToolboxItem(false)], который скрывает класс из toolbox'a обычно мы им помечали всякие базовые контролы, которые никогда в дизайнере не будут использоваться и dataset'ы.BlackEngineer Автор
31.07.2015 09:05Попробовал (LicenseManager.UsageMode == LicenseUsageMode.Designtime);
Но это не работает… Контрол не понимает, что он в режиме дизайнера…
Я лично использую такую конструкцию:
If ((Site != null) && (Site.DesignMode))vvovas
31.07.2015 09:45Да, там несколько есть способов. К сожалению, я работал с WinForms последний раз года 4 назад, так что просто скопировал нашу проверку из кода. У нас нареканий не было, возможно есть какие-то свои ограничения.
QtRoS
31.07.2015 11:10По пункту 4 можно поподробнее?
vvovas
31.07.2015 12:04Вообще все, конечно зависит от архитектуры приложения, но идея в том, что любая подписка на событи связывает 2 объекта и сборщик мусора их соберет только, если они оба будут удалены.
Т.е. вы создаете контрол, который связывается событиями с формой или другим контролом, форма связывается с контроллером и т.д. Потом вы написали form.Close() и забыли про форму, а она со всеми контролами в памяти повисла, потому что от событий не отписались.QtRoS
31.07.2015 14:51Все, понял Вашу мысль, спасибо — я еще прочитал неправильно, как «Отписывайтесь от события Dispose», что меняет смысл…
xtraroman
Это здорово что на WinForms еще пишутся новые вещи, но у меня есть несколько замечаний.
1.Это тяжелый вызов, как правило, его можно заменить на Invalidate().
2. Вы повторили реализацию DoubleBuffer которая и так есть в Windows.Forms. Флики отрисовки можно побороть проще
3. Текст обычно не ресайзят чтобы его всегда было удобно воспринимать.
BlackEngineer Автор
Спасибо за замечания.
С первым полностью согласен.
Со вторым нет — отрисовку в буфер я использую, чтобы потом при изменении размеров и обновлении экрана не рендерить на основе содержимого, а просто отрисовать один визуальный буфер Bitmap.
С третьим частично согласен — у меня задача стоит максимально быстрой работы компонента, красота на дальний палн уходит