Трюки с формой 2.1: Edit в заголовке окна

Не являюсь поклонником запихнуть что-нибудь в заголовок, но аргумент «жаль столько места пропадает» принимаю и поддерживаю. Поэтому продолжим размещать элементы редактирования в заголовке. Сейчас будем размещать Edit и ему подобных. Легально, без фокусов.

Что не так с предыдущим методом? К сожалению, размещая Edit подобным образом, мы теряем курсор внутри объекта. А без него выглядит и неуютно, и топорно. И самое главное, при таком методе мы теряем AeroSnap.

Описание проблемы

Как известно, Windows не позволяет размещать свои объекты в не-клиентской области окна. По крайней мере, нет такого API, которое позволило бы это сделать. Рисовать в заголовке можно без проблем, а назначить окну родителя с указанием разместиться в не-клиентской области нельзя. Все попытки взять заголовок под контроль оборачиваются в конечном счёте неким фокусом. В подавляющем большинстве случаев, это панель, которая притворяется заголовком.

В Delphi, начиная с 10.4, это фокус узаконили тем, что предоставили новый компонент TTitleBarPanel. Что вызвало ожидаемую радость: «TTitleBarPanel – это очередной пример простоты и элегантности разработки приложений с использованием Delphi». К 12-й версии он всё также плох.

К основным недостаткам TitleBarPanel можно отнести то, что он не дружит со стилями, не знает про тёмную тему, Label’ы и ряд других отображает хуже некуда. Ну и так далее, пишите в комментариях, что можно ещё предъявить.

Цели и направления

Конечно, хотелось бы, чтобы подобная фишка работала на всех ОС, но ограничимся 10, 11. Для реализации задуманного будем использовать DWM, а в Window 7 он весьма так себе. Чтобы работало на всех осях, просто делаем панель вместо заголовка и гордо уходим в закат.

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

Дельфовый TitleBarPanel системные кнопки полностью подменяет, рисует свои. Я не против хаков и фокусов, но window-заголовок меняется от версии к версии, и хочется, чтобы было меньше проблем и сейчас, и в будущем. Если делаем свой заголовок, то либо делаем нехилый определитель текущих системных цветов, либо на всё забиваем и всю палитру делаем свою. Если нам надо разместить в заголовке всего лишь пару контролов, то вся эта овчинка выделки не стоит.

Совершенно не хочется рисовать самостоятельно не-клиентскую область окна, обрабатывать WM_NCPAINT, ловить артефакты. Хочется максимально задействовать возможности Delphi и поменьше писать руками. Всё равно придётся, но давайте на это раз поменьше!

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

Пишем в XE 7, потом проверим в 12-ой Delphi. Так случилось, что XE 7 очень популярна, и люди неохотно переходят с неё на новые версии. Ситуация, как с Delphi 7 в своё время. Поддерживать исходники для Delphi 7 уже перестал, но XE 7 пока актуальна.

Начинаем

Предположим, у меня есть такая линейка компонент на форме и я хочу переместить их в заголовок. В наличии: TButtonEdit, наследник TCustomEdit, TComboBox и TButton. TButtonEdit взят, чтобы посмотреть, как будет отрисовываться правая кнопка. Обычно в заголовок уходит что-то, связанное либо с поиском, либо фильтром, а эта история всегда с кнопкой. TButton выбран по той причине, что обычно плохо отображается в не-клиентской области. Как работать с DWM и как определять его наличие, описал в этой статье. Сейчас постараемся поменьше обращаться к DwmApi и максимально использовать всё то хорошее, что есть в Delphi.

Заголовок в клиентской области

Если нельзя разместить TEdit в не-клиентской области, то пусть не-клиентская область станет частично клиентской. Этим занимается функция DwmExtendFrameIntoClientArea. Она расширяет рамку окна в клиентскую область.  Её надо вызывать всякий раз при наступлении определённых событий, читаем подобности по ссылке выше.

В Delphi за работу с DwmExtendFrameIntoClientArea отвечает базовый класс формы TCustomForm. Если у формы включён GlassFrame, то dwm-рамка окна корректно отработает, и нам не придётся ничего учитывать.

Оформим всю работу, связанную с получением информации и работе с заголовком, в отдельный класс TFormTitleInfo. Мало ли, понадобится ещё где-нибудь.

Если мы хотим, чтобы область заголовка «залезла» в клиентскую часть окна, необходимо установить значение отступа сверху (по сути, высоту заголовка) в свойство формы GlassFrame.Top и включить GlassFrame.Enabled.

Про текущую высоту заголовка спросим Windows. Функция GetAdjustWindowRect запрашивает прямоугольник отступов окна. Внутри использует AdjustWindowRectEx, которая вычисляет прямоугольник, полностью охватывающий клиентскую область. Если мы скормим ей нулевой прямоугольник, то получим значения отступов, включая и размер заголовка. Эта функция API не поддерживает DPI и не должна использоваться, если вызывающий поток поддерживает DPI. В этом случае необходимо использовать AdjustWindowsRectExForDPI

В XE 7 нет поддержки высокого разрешения. Начиная с 10.4, в Delphi появилась функция AdjustWindowRectExForWindow (Vcl.Controls), которая учитывает сказанное выше про DPI. Поэтому и мы будем учитывать версию компилятора:

Наблюдаем такое:

Область заголовка расширилась, компоненты на месте, кнопка ожидаемо глючит, иконки в ButtonEdit не видно. Всё круто!

Убираем лишний заголовок

Следующим шагом уберём верхнюю часть заголовка. Необходимо убрать не-клиентскую область сверху. Для этого нам понадобится обработать событие WM_NCCALCSIZE. Мероприятия, с этим связанные, описал в предыдущей статье. Внимание: в случае, когда действительно ограничиваем, inherited не вызываем.

FTitleInfo — это экземпляр нашего вспомогательного класса. У него появилось свойство FrameRect, в котором хранится рассчитанный прямоугольник. Метод доступа по чтению свойства выглядит так:

FOwner — это форма, с которой проводим манипуляции. Для определения области панели задач служит следующий метод (надо включить в предложение uses модуль Winapi.ShellAPI):

Получаем следующее:

Верхняя половина заголовка исчезла. Системные кнопки переместились в нижнюю часть, встали вровень с компонентами. Замечаем, что перетаскивать форму за заголовок, или изменить размер мышкой сверху, мы не можем. Также замечаем отсутствие всяческой реакции на действия мыши в области системных кнопок. Это замечательно!

Таскаем за заголовок

Мы прекрасно знаем, как заставить окно думать, что мышка на самом деле в области заголовка. Нам нужно обработать сообщение WM_NCHITTEST. Заодно решим проблему с изменением размера сверху. И давайте тут же разрешим показывать системное меню, то самое, когда кликаешь на иконку окна.

Чтобы определить область, где должна находиться иконка, в нашем вспомогательном классе предусмотрен метод GetIconRect:

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

Получив возможность таскать окно за заголовок, проверяем AeroSnap, и он работает. А вот компоненты и системные кнопки по прежнему не в лучшем виде. Всё просто зашибись!

Системные кнопки

Конечно, есть большой соблазн обработать координаты курсора и подсунуть в результат что-то типа HTMINBUTTON, HTMAXBUTTON, HTCLOSE. Но это даст только реакцию на клик, подсветки не будет. А хочется, чтобы системные кнопки подсвечивались так, как мы привыкли.

Для того, чтобы оживить системные кнопки, воспользуемся функцией DwmDefWindowProc из арсенала Dwm. В описании есть условия, при которых функция станет работать. Но мы считаем, что разработчики Delphi уже обо всём позаботились. В предложение uses надо добавить модуль Winapi.DwmApi. Чтобы воспользоваться данной функцией, переопределяем метод формы WndProc. Выглядеть он будет очень просто:

Что тут происходит. Перед анализом и рассылкой всех сообщений, мы скармливаем сообщение функции DwmDefWindowProc и если оно предназначалось Dwm и успешно обработано, то дальше не происходит ничего. Сообщение обработано, всё.

Теперь получаем привычную реакцию системных кнопок на мышь, подсказки и поведение. Кнопка подглючивает, но в целом отлично!

Имитация заголовка

Осталось вывести иконку и заголовок. В пустое место слева разместим TPaintBox. Куда ж без него. Обзовём pbTitle.

В обработчике OnCreate формы зададим нужные размеры и положение для pbTitle. Также установим DoubleBuffered в True. Тем самым, мы вылечим отображение компонент в заголовке.

В нашем вспомогательном классе напишем два метода отрисовки иконки и текста заголовка. Для рисования иконки метод выглядит так:

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

Для отрисовки заголовка служит метод:

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

Обработчик события OnPaint нашего pbTitle выглядит так:

И вот что получилось:

В принципе, получили то, чего хотели. Теперь про красоту.

Cтиль рамки окна и серая полоса

Вряд ли в процессе работы одно и то же окно будет впадать в разные стили. Поэтому обрабатывать виндусовые сообщения не будем, просто добавим список возможных значений в крайний справа ComboBox и обработаем событие OnChange.

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

Вот она. Может проявиться на nonsizeable стилях рамки окна.

Или так. Едва различимая светло-серая линия в sizeable стилях. Некоторых бесит.

Чтобы решить проблему, немного изменим метод UpdateGlassFrame нашего вспомогательного класса.

Добавим настройки видимости системных кнопок, чтобы посмотреть на их поведение и отображение.

Если мы выставим свойство BorderIcons := [biSystemMenu, biHelp], то кнопка помощи будет видна и при стилях Single и Sizeable, не только в диалоге. Ну… так устроен Windows… Кнопка помощи появляется только тогда, когда нет кнопок минимизации и максимизации. Чтобы видеть весь комплект системных кнопок, люди и творят зло TitleBarPanel.

Реакция на смену иконки, текста и фокуса

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

Проверяем тем, что кликаем на картинки в форме

или по кнопке Apply:

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

Delphi 12

Выше мы предусмотрели тот факт, что Delphi, начиная с 10.4, всё лучше и лучше воспринимает высокое разрешение и всё, что с ним связано. Надо посмотреть, правильно ли мы предусмотрели, да и вообще, любопытно. Берём тот же самый исходник и компилируем в Delphi 12. И видим, что баги, наблюдаемые при TitleBarPanel, это баги не только TitleBarPanel.

TButton ведёт себя плохо. Но это можно вылечить. Надо либо выставить в инспекторе объектов, либо руками прописать следующее:

Ожидания по качественному тексту в заголовке на первый взгляд не оправдались. Идём в настройки проекта, смотрим Application — Manifest — DPI Awareness и видим, что там… пусто. Выбираем из списка Per Monitor v2, и вуаля! — заголовок стал выглядеть просто чудо как хорошо.

Недостатки

  1. В таком окне TMainMenu попадает туда же, куда и заголовок. То есть, в никуда. Его не видно. Но можно использовать TActionMainMenuBar. Вполне себе современное решение.
  2. В текущей реализации нельзя использовать стили. Сразу появляется стильный заголовок и всё портит. Чтобы стильный заголовок не появлялся, надо убрать seBorder из свойства StyleElements формы. Подробнее опишу в следующей статье.
  3. Только Windows 10 и выше. Под Windows 7 работает, но выглядит грустно. Поэтому, при запуске надо анализировать, какая ОС, и, в зависимости от этого, либо делать манипуляции с заголовком, либо оставить всё как есть.

Листинги

Привожу полные листинги, потому что копипастить удобней, чем качать, и всё вместе наглядней, чем фрагментарно. Получилось совсем немного, примерно по 250 строк на каждый модуль.

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

TFormTitleInfo

[свернуть]

Модуль формы

TFmMain

[свернуть]

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

Стили Vcl умеют закрашивать заголовок, но не позволяют разместить там компоненты. Форма не понимает тёмную тему Windows и заголовок остаётся белым. Как быть? Обо всём в следующей статье: Трюки с формой 2.2.1: Тёмная тема Windows в Delphi 12.


Скачать

Друзья, спасибо за внимание! Надеюсь, материал пригодится )))

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

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

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


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

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