Skip to content

.NET WinForms easy color scheme

NikolayHD edited this page Oct 14, 2019 · 12 revisions

Как-то ночью я писал код. Мониторы ярко светили, и, чтобы уменьшить яркость, я включил тёмную тему IDE. Стало лучше, но оставался второй монитор с открытым проводником Windows, поэтому пришлось включить тёмную тему Windows. Стало нормально, до того момента, как я не запустил WinForms программу, над которой в этот момент работал.

Какой она была до этого:

image

Что предстало моим глазам:

image

Так жизнь показала мне, почему не следует хардкодить цвета. Вместо этого нужно везде использовать...

Системные цвета

SystemColors.ControlText, а также системные кисти SystemBrushes.ActiveCaption, и карандаши.

Производные цвета

image

Как вы думаете, какой системный цвет соответствует розовой подсветке синтаксической ошибки на скрине выше? Никакой, в этом легко убедиться, взглянув в таблицу системных цветов Windows, источник stackoverflow. Розовых, оранжевых и зелёных цветов нет.

Чтобы в этих условиях избегать хардкода таких цветов, как розовый, придётся в буквальном смысле слова выкручиваться.

Иконки

Чёрные иконки были хороши на белом / почти белом фоне. На чёрном же фоне потребуются светлые.

К счастью, System.Drawing.Bitmap является изменяемым типом, поэтому ничто не мешает нам при изменении системных цветов перерисовать непосредственно используемый Bitmap.

Главное подобрать разумный алгоритм адаптации иконки под цветовую схему.

image

image

Казалось бы, пора поздравить себя с успехом и заняться другими делами, но не тут-то было. Мне нравится новый вид моего приложения, мысль о его потенциальном разнообразии греет душу, но чтобы пользоваться этим, нужно менять скин Windows. Это какой-то неправильный успех.

Скин

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

  • поддерживается DesignTime редактором в Visual Studio
  • используется самим .NET Framework и Win32 API

Таким образом, новые элементы, добавляемые в UI смогут учитывать цвета скина без дополнительных усилий при разработке.

System.Drawing.Color

Рассмотрим реализацию структуры System.Drawing.Color в исходном коде .NET Framework

Структура System.Drawing.Color реализует 2 способа поведения.

  • Хранить байты цвета непосредственно
  • Получать из таблицы KnownColorsTable по значению перечисления KnownColor

KnownColorsTable хранит значения системных цветов в приватном массиве colorTable, содержимое которого по мере необходимости обновляется с использованием запросов Win32 API.

Нам достаточно подменять значения в colorTable, чтобы для нашего приложения системные цвета имели такие значения, как нам нужно.

Вот как это было сделано.

На скриншоте ниже контекстное меню, которое сходу правильно отображается с изменёнными цветами:

image

Для быстрого редактирования цветовой схемы был реализован простой редактор:

image

Контрол для выбора цвета взят отсюда.

Нативно рисуемые контролы

Некоторые элементы UI из .NET Framework будут игнорировать подмену SystemColors, потому что они рисуют себя полностью за счёт запросов Win32 API. Характерный и распространённый пример - System.Windows.Forms.ScrollBar. То же отчасти относится и к ListView, TreeView, ComboBox и так далее.

ScrollBar

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

image

ComboBox

Большую часть недостатков отрисовки ComboBox можно решить переносом кода отображения в .NET framework, это можно сделать единообразно.

Но и в этих деталях есть дьявол - границу выпадающего списка, в том числе скролл выпадающего списка, так просто не победить.

image

RichTextBox

По-видимому, он имеет собственный кеш цвета, потому что простой Invalidate() не приводит к перерисовке с изменённым SystemColor. Также не помогает переустановка BackColor / ForeColor, потому что он умный и замечает, что как был SystemColors.WindowText, так и остался. Немного дополнительной настойчивости всё же заставит его склониться перед вашей волей.

Итоги

Ценой умеренных усилий, около 4 человеко-дней, удалось решить следующие задачи:

  • Весь UI отображается с учётом системных цветов Windows
  • Реализован простой, но достаточный UI для редактирования цветовой схемы приложения.
  • Подавляющая часть UI допускает применение цветов из редактируемой цветовой схемы приложения.
  • Монохромные иконки приложения адаптируются к цветовой схеме и хорошо различимы при условии адекватности самой схемы.
  • Как и раньше, можно редактировать цвета в DesignTime WinForms редакторе Visual Studio, не пришлось вводить отдельную сущность для цветовой схемы приложения отдельно от цветовой схемы Windows.
  • В моём приложении теперь есть своя Тёмная цветовая схема 😎

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

Постскриптум

Хотя описанный выше подход дал неплохие результаты в моём приложении mtgdb.gui, качество результата оказалось не идеальным, например не удалось правильно окрасить границу меню выпадающего списка (ComboBox), собственные скроллы контролов, того же ComboBox, RichTextBox. В конечном итоге, пришлось написать с нуля собственные суррогаты ComboBox, Button, Checkbox, TabControl, и т.д. RichTextBox так и остался с нативным скроллом.

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

И всё же, моя идея "цветовой схемы для бедняка" под .NET WinForms, получает продолжение в проекте GitExtensions. Потому что я им много пользуюсь, хочу темную тему, и не думаю, что он будет переписан на WPF, скажем к Новому Году.

Clone this wiki locally