Как сделать чёрным системное меню элемента Windows

Есть мнение, что сделать чёрным системное меню какого-либо элемента Windows в Delphi не получится. Даже сменив тему всей ОС. Если меняем тему окна на тёмную, внутреннее меню TEdit останется светлым. Что делать? Как приручить дракона?

Во-первых, можно назначить собственный PopupMenu и отрисовать его. Но тогда придётся полностью продублировать пункты всех системных меню всех элементов (окно, все TEdit, TMemo и т.д.), сделать отрисовку для всех выпадающих меню в проекте. Чтобы продублировать, необходимо получить описатель меню того же TEdit. Для этого нужно осуществить ряд танцев с бубном, связанных с CBT-ловушкой (ниже). Короче, мрак.

Во-вторых, конечно можно использовать какой-нибудь хороший (платный) тёмный VCL-стиль. Но не хочется. Потому что разбухает исполняемый модуль (и платный), и совсем не всегда нужно прям всё из стиля.

Получается, что есть только один путь. Ставить CBT-ловушку (CBT-ловушка по-русски), отлавливать события HCBT_CREATEWND, HCBT_DESTROYWND, связанные с окном «#32768» (класс меню), подменять отрисовку, либо дублировать меню. Но когда я этим призанялся, вдруг стало так лень…

Возможно, это продолжение разговора про тёмную тему Windows. Как получится.

Ловушка: TSysPopupStyleHook

TSysPopupStyleHook — это ловушка для контекстного меню (Vcl.SysStyles). Является наследником TSysStyleHook (Vcl.Themes). Также существует класс TCustomStyleEngine (Vcl.Themes), который понимает список зарегистрированных наследников TSysStyleHook, и знает, для каких классов окон они предназначены. Если список не пуст, TCustomStyleEngine регистрирует CBT-ловушку, отслеживает создание или уничтожение нужных окон и взаимодействует с соответствующей стилевой ловушкой.

Таким образом, TCustomStyleEngine занимается ровно тем, чем пришлось бы заниматься руками. И чем заниматься лень. По сути, связка TCustomStyleEngine и TSysStyleHook — это инкапсуляция легального Winapi. А это основа архитектуры Delphi. Поэтому, решение через наследование TSysPopupStyleHook  — это изящный, лёгкий и легальный путь достижения цели.

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

Чёрное меню: TBlackSysPopupStyleHook

Создадим класс наследника от системной ловушки контекстного меню и переопределим два метода, отвечающих за отрисовку пункта и фона меню:

Рисуем фон: PaintBackground

Для отрисовки фона делаем очень простые манипуляции:

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

Тут можно было бы нарисовать какой-нибудь битмап на всю ширь окна меню. Фоновую картинку с логотипом компании. Или замостить кирпичами. Метод DrawItem придётся, правда, реализовывать несколько иным образом, чтобы не затирать изображение, но это тема на целую статью.

Рисуем пункт меню: DrawItem

Немного о параметрах в DrawItem:

Canvas: TCanvas
Холст, на котором должна происходить вся отрисовка.
Index: Integer
Индекс элемента меню внутри ловушки. Элемент доступен, как Items[Index]. Сам тип элемента спрятан в приватной секции ловушки: private type TSysPopupItem = class, поэтому объявлять экземпляр этого типа смысла нет. Зато у него есть ряд интересных свойств, доступ к которым можно получить, обращаясь через Items[Index].
ItemRect: TRect
Область отрисовки пункта меню
ItemText: String
Текст для отрисовки пункта меню
State: TSysPopupItemState
Множество текущих состояний пункта меню:
TSysPopupItemState = set of (isHot, isDisabled, isChecked, isDefault);
isHot — мышь над пунктом, надо подсветить.
Остальное можно получить из свойств Items[Index]
isDisabled — пункт неактивен, надо засерить;
isChecked — на пункте галочка, либо кружок (если он RadioItem);
isDefault — является пунктом меню по умолчанию, надо выделить жирным.
Style: TSysPopupItemStyle
Стиль пункта меню:
TSysPopupItemStyle = (isNormal, isSep, isDropDown)
Дубли информации из свойств Items[Index]
isSep — это разделитель, рисовать линию, подсвечивать на мышь не надо
isDropDown — есть подменю, рисуем треугольник, либо что-то подходящее
isNormal — не разделитель, нет подменю

Свойства для элемента Items[Index] ловушки:

Доступные свойства элемента типа TSysPopupItem

[свернуть]

В этой реализации метода DrawItem не учитывается отрисовка битмапов и BiDiMode. При желании это можно легко допилить. Но это очень сильно увеличит код. Поэтому, чтобы не усложнять жизнь, вот базовая реализация, которая покрывает почти все проблемы:

Регистрация ловушки

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

Предположим, что у нас есть на форме CheckBox1: TCheckBox, включённое состояние которого регистрирует ловушку, выключенное — убирает регистрацию.

Здесь мы регистрируем нашу ловушку для системного класса меню ‘#32768’. Следует отметить, что если кто-то потом зарегистрирует свою ловушку для этого класса, наша ловушка будет выкинута из списка. Аналогично и обратное, если кто-то уже занял место за этим классом, наша регистрация убьёт его.

Сделали, запускаем, не работает.

Особенности и хитрости

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

Можно зарегистрировать и сделать активным какой-то стиль, например Light. Тогда у нас появится стиль, ради которого надо регистрировать CBT-ловушку, потому что он несистемный. Но мы не хотим стилей вообще, и нас вполне всё устраивает, только меню хотим чёрное.

Механизм начинает работать, если у нас есть хотя бы один зарегистрированный стиль, отличный от системного. Без стилей механизм не работает. Стилей мы не хотим. Системный стиль уже есть.

Как быть?

Как приручить дракона?

Всё просто. Является ли стиль системным определяется простым сравнением с уже созданным внутри TStyleManager стилем по умолчанию. А давайте создадим ещё один системный стиль и установим его активным? Например, в конструкторе формы.

И всё волшебным образом взлетело! В интерфейсе не изменилось ровным счётом ничего, кроме цвета контекстного меню. Контекстное меню теперь стало тотально чёрным. Везде. Если снять галку с Black Popup Menu Enabled, то меню станет обычным, для сравнения.

Так теперь выглядит системное меню TEdit.

Так выглядит своё меню. Пункты меню имеют соответствующе галки, точки и жирность.

А так меню выглядит у значка в трее.

И, наконец, системное меню окна тоже чёрное.

Применение

Возможно, нам надо только слегка изменить вид контекстного меню. Заявив тем самым о своей неповторимости. Использовать стили не хотим, они легко узнаваемы.

Или нам не нужно менять всё, у нас просто другой подход к интерфейсу. Например, вместо иконок, мы используем Юникод или ещё более тотально — Font Awesome. Использовать символы вместо картинок хорошо тем, что их можно отрисовать любым цветом и начертанием. Однако, возможности их пропихнуть в меню на место иконки — нет. Тут и пригодится такой подход — переопределить системную стилевую ловушку.

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

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

Надеюсь, немного подвинул горизонты возможного. Остальное — дело творчества.


Скачать

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

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

Исполняемый файл (zip) 973 Kб (Скомпилирован в XE 7)


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

4 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Oleg

Благодарю! Кое что умел ранее по этой теме, но вижу, что есть чему еще поучиться!

Алексей

Здравствуйте! Как применить Ваш код для изменения меню в чужом окне? И как добавить иконки из ImageList для рисования в меню?