Трюки с формой 2.2.1: Тёмная тема Windows в Delphi 12

С возникновением тёмной темы Windows, появились вопросы, почему Delphi её не поддерживает. Вне зависимости от текущей темы, окошки серые, заголовки белые. Давайте попробуем разобраться, что такое тёмное тема в понимании Windows, и как её реализовать в Delphi малыми усилиями.

Тёмная тема в понимании Windows

Рекомендую ознакомиться со статьёй Microsoft: «Поддержка темной и светлой тем в приложениях Win32».

Исходя из текста статьи, под тёмной темой Microsoft считает нечто, диаметрально противоположное светлой теме. Философия в стиле Стетхама: «В жизни всегда есть две дороги: одна — первая, а другая — вторая». Забавно звучит, но это так. Нет API, которое бы по умолчанию закрасило окно и объекты Windows в цвета тёмной темы. Windows, вместо API, предоставляет рекомендации и глобальный параметр настройки, что дескать тема по умолчанию – тёмная (или светлая).

Из-за широкого разнообразия оформления пользовательского интерфейса в различных приложениях настройка определенного цветового режима, а также цвета переднего плана и фона является скорее рекомендацией, чем правилом

Всё, что у нас есть, это возможность определить, какая сейчас актуальная тема и установить заголовок, только заголовок, в тёмную(светлую) тему. Заголовок тёмной темы – это белые знаки системных кнопок и нулевой цвет для объединения по ИЛИ с желаемым цветом заголовка. И небольшая подсветка фона при утере фокуса.

Одним словом, реализацию тёмной темы, Windows целиком возлагает на нас. Ну, потому что из-за «широкого разнообразия оформления пользовательского интерфейса в различных приложениях», давать что-то стандартное, видимо, никто не решился.

Тёмная тема в Delphi 12

В 12-ой Delphi появились плюшки для Windows 11, типа перевода заголовка в тёмную тему и скруглённые углы формы, но без перевода всей остальной клиентской области. Исходя из доктрины Windows, относительно реализации тёмной темы, это уже не кажется таким удивительным.

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

Почему ошибочный. Потому что просто изменить цвета для ряда компонент не получится. Это не повлияет на их внешний вид. Попробуйте поменять цвет шрифта у TButton, или цвет TCheckBox. Это может сделать только стиль, стандартный или видоизменённый с помощью StyleHook. В конце концов мы даже этим немного займёмся, подменив ловушку для TCustomForm, а сейчас просто возьмём стандартный тёмный стиль.

При использовании стиля, в контексте размещения компонент в заголовке, возникает неприятный нюанс. Для стиля такой возможности не предусмотрено. Стиль полностью контролирует заголовок и про наши игры с DWM не в курсе.

Начинаем

Берём проект для 12-ой Delphi из предыдущей статьи и подключаем стиль Windows10 Dark.

Последующий запуск проекта даёт такую картинку:  

Тени нет. Стильный заголовок тут явно лишний. Убираем его:

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

Переключение между темами

Важно, чтобы приложение запускалось со светлой темы. Это позиция Windows в том числе. Цитата из статьи «Не все приложения Win32 поддерживают темный режим, поэтому по умолчанию Windows предоставляет приложениям Win32 светлую строку заголовка». Поэтому в окне выше в выпадающем списке стиля по умолчанию выбираем Windows. Должно быть так:

У нас в проекте из предыдущей статьи был предусмотрен выпадающий список стилей, тот который в центре. Сделаем его csDropDownList и обработаем событие OnChange:

Запускаем, выбираем светлую тему:

Где-то явно не обработан GlassFrame формы. Нужен какой-то обработчик, который будет срабатывать при смене темы, устанавливать заголовок в тёмный или светлый режим и что-то ещё. Для начала, «что-то ещё» , это работа с GlassFrame. Создадим метод DoStyleChange, который в конечном счёте станет ключевым во всей этой затее:

Теперь его надо правильно вызвать. За событие смены стиля отвечает сообщение CM_CUSTOMSTYLECHANGED. Оно распространяется среди видимых форм путём PostMessage, поэтому внедряемся в уже переопределённый метод WndProc формы.

Почему так замысловато. Вначале фиксируем, что это событие наше, потом отпускаем выполняться весь блок обработки сообщений, и только потом реагируем. Дело в том, что в процессе обработки этого сообщения, оно трансформируется в CM_STYLECHANGED. И нам нужно среагировать в самом конце всех промежуточных сообщений, которые будут посланы форме в процессе его обработки. Поэтому, сохраняем факт смены стиля, затем этот факт пережёвывает стилевой механизм, и только потом реагируем мы.

Теперь, при переключении в светлую тему, видим нормальный вид:

В тёмной теме пока не так хорошо, но это дело поправимое.

Установить заголовок в тёмную тему

Чтобы установить форму в тёмную тему в Delphi 12 существует метод формы EnableImmersiveDarkMode:

В комментарии:

В Windows 11 формы по умолчанию находятся в светлом режиме независимо от настроек системы. С помощью этого метода вы можете отрисовать рамку формы в цветах темного режима, когда включена настройка системы темного режима.

Но этот метод работает только для Windows 11. Хотя в 10-м тоже есть тёмная тема. Поэтому, чтобы не спорить с Delphi, напишем свой установщик для заголовка в нашем вспомогательном классе. Ну как напишем, позаимствуем.

Есть соблазн переключиться в тёмную тему там же, где и переключаем стили, но это неправильно. Артефакты на кнопках и кривой заголовок останутся. Поэтому, в DoStyleChange  дописываем установку тёмной (светлой) темы:

Артефакты ушли, заголовок отрисовался нормально. Но заголовок не стал тёмным и системных кнопок не наблюдается. Без использования стиля заголовок был бы чёрным. Если есть стиль, то нам надо перерисовать область под заголовком. Но мы не хотим ничего перерисовывать, поэтому допишем ещё строку в обработчик смены стиля, в которой установим текущий цвет формы:

Появились системные кнопки, которые ведут себя просто замечательно. И куда-то пропал заголовок. Заголовка не видно, потому что он рисуется по прежнему, как для светлой темы, тёмными буквами. В отрисовщике заголовка вспомогательного класса учтём наличие тёмной темы. Наличие тёмной темы, это просто наличие активного пользовательского стиля. Полный текст в листингах в конце статьи.

Казалось бы, всё. Но куда пропала надпись «Read more on IP76.RU…» снизу справа? Она уехала вниз (((

Странное поведение компонент при смене тем

Поставим кнопку и метку на форму, зададим обоим выравнивание:

После нескольких переключений тем, видим следующее:

Выровненные таким образом контролы уползают вниз. После долгих поисков нашёлся виновник. Им оказался обработчик WM_NCCALCSIZE. Оказывается, во время смены стиля, не надо задавать смещения, иначе ломается AlignControl.

Допишем в обработчик смены стилей признак того, что сейчас надо действительно сменить стиль:

А в обработчик сообщения WM_NCCALCSIZE добавим условие не выполнять вычислений, если стиль меняется. Также учтём признак видимости формы. На тот случай, когда стартуем в тёмной теме Windows. Полный текст в листингах в конце статьи.

Признак смены стиля гарантировано сбрасываем в DoStyleChange:

Поэтому мы не стали вешаться на CM_STYLECHANGED в WndProc. Нам нужно гарантированно сбросить признак изменения стиля, и что он там выдаст после всех обработок – фиг знает.

Определить текущую тему Windows

Дописываем в наш вспомогательный класс определитель текущей темы в ОС (источник тот же):

Пишем метод формы, определяющий и устанавливающий нужную тему.

И вызываем его, например, в обработчике OnCreate формы.

Реакция на изменение в настройках Windows

Вряд ли это будет происходить часто, но для полноты картины учтём и это. При изменении настроек, в том числе текущей темы, всем окнам в системе рассылается сообщение WM_SETTINGCHANGE. Обработчик пусть выглядит следующим образом:

Не чёрный заголовок, а тёмный

В парадигме Windows, изложенной выше, цвет заголовка не должен быть чёрным, он должен позволять себе быть тёмным, в контексте собственной темы. Поэтому, сменим кардинально чёрную тему Windows10 Dark на что-то не такое чёрное, например Carbon. Подключаем стиль, не забываем оставить Windows по умолчанию. В переключалке тем вместо Windows10 Dark пишем Carbon:

И видим совершенно функциональный заголовок нашей тёмной, но не чёрной, темы:

Эффект достигнут за счёт присваивания цвету формы текущего цвета стиля для окна. Нам не нужен кардинально чёрный заголовок. Windows не предоставляет тёмной темы, он предоставляет возможности для неё.

Нажмём кнопку с лупой. Увидим диалоговое окошко:

Мы не можем простым путём повлиять на заголовок этого окошка, и любых других стандартных диалоговых окошек. Они все будут иметь заголовок выбранной темы.

Кардинально чёрный заголовок не для того, чтобы присутствовать только в таком виде в каждом окне. Он таков, чтобы без проблем объединиться с текущим предложенным цветом. В приложении всё должно соответствовать дизайнерскому замыслу автора.

С кнопкой закрытия окна есть небольшое расхождение. Но есть другие стили, где кнопка закрытия более подходящая. Например, Windows10 SlateGray:

А если свойству Color формы будем присваивать другое значение цвета стиля, например границы окна, то добьёмся идеальной тёмной темы, где все заголовки для всех окон одного цвета:  

Опробуем тёмную тему от delphistyles:

Классная тёмная тема. В одной из следующих статей выложу бесплатный вариант этого стиля.

Range Check Error при bsNone

Это к тёмной теме не относится, но надо рассказать. При переводе рамки окна в стиль bsNone наблюдается ошибка:

Эта проверка обычно включена в дебаге и отключена в релизе. Поэтому, чтобы этого не возникало, можно отключить для дебага опцию Range checking:

Или же, чтобы не возникало лишних вопросов, отключим проверку на диапазон в конкретном месте директивой $RANGECHECKS. Полный текст в листингах в конце статьи.

Листинги

Традиционно привожу полные листинги, их снова немного.

Вспомогательный класс

TFormTitleInfo

[свернуть]

Модуль формы

TFmMain

[свернуть]

Тёмная тема в Delphi XE 7

Delphi XE 7 создавалась в эпоху Windows 7. Никакой тёмной темы не было и в помине. Однако, не всё так печально. Как сделать настоящую тёмную тему в Delphi XE 7 расскажу тут: Трюки с формой 2.2.2: Тёмная тема Windows в Delphi XE7


Скачать

Друзья, спасибо за внимание!

Исходник (zip) 373 Кб. Delphi XE 12

Исполняемый файл (zip) 1.44 Мб (Скомпилирован в XE 12)


5 3 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Не нашли ответ на свой вопрос? Задайте его здесь!...x