Трюки с формой 2.0: ComboBox в заголовке

ComboBox в заголовке

Как разместить ComboBox в заголовке формы? Или CheckBox? Или DateTimePicker? Если кнопку (вернее, эмуляцию кнопки) можно «запихать» в заголовок формы, то что делать с другими компонентами?

Есть несколько вполне рабочих трюков. Один из них сейчас рассмотрим.

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

Панель — заголовок

Итак, берем исходник из уже упомянутой статьи и немного модифицируем.

Во-первых, теперь мы рисуем только в GDI+. Модули, представленные в статье «Как подключить GDI+ для Delphi 7 и не иметь проблем в XE«, хорошо себя показали. В связи с чем, убираем галку «GDI+», ибо смысла в ней больше нет.

Во-вторых, кидаем Panel поверх PaintBox‘а, в котором рисуется версия Windows. Это будет заголовок окна. Ни выравнивания, ни Anchors не задаем. Так надо )

В-третьих, переносим Label «Close Alt+F4» на эту панель. Пока так, вместо системных кнопок. Тем более что, и компонент, и реакция на клик, уже есть.

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

Еще ряд косметических модификаций. Добавим иконку, заголовок окна. В дизайнере Delphi7 получилось следующее:

Рис.1. Да, по прежнему Delphi 7, потому что есть большой запрос на эту версию

В OnResize формы устанавливаем расположение и размер панели:

В обработчике события OnMouseDown панели заголовка pnlTitle пишем:

Вешаем на этот обработчик все компоненты панели, кроме избранных. На ComboBox вешать смысла нет никакого. Клик на иконке должен показать в будущем системное меню. Клик на Close Alt+F4 должен закрывать окно, а не таскать его.

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

Напомню константы:

Некоторые константы для WM_SYSCOMMAND

[свернуть]

Обработаем изменения в ComboBox‘е:

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

Реализация:

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

Рис.2. Заголовок формы пока не заголовок, а часть клиентской области

У формы меняется размер мышкой, таскается за заголовок, вроде все как надо. Но хочется, чтобы панель стала более «настоящим» заголовком.

Шаги в потустороннее. Non-Сlient Window Area

Путь в потустороннее начинается с CheckBox Non-client. Обработчик события OnChange выглядит так:

Обращаю внимание на строку pnlTitle.ParentBackground := CheckBox1.Checked, без не фокуса не получится. Если рисуем в не-клиентской части окна, панель должна стать «прозрачной».

Шаг 1: Отрицательные координаты

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

Модифицированный обработчик OnResize формы:

Шаг 2: Размер не-клиентской области

Суровые программисты работают с не-клиентской областью окна

Трюки с формой 1.0

В тексте цитируемой статьи присутствует листинг обработчика для события WM_NCCALCSIZE, а в исходнике его нет. Просто там он не нужен, а здесь пригодится. Вернем и модифицируем его. BorderWidth меняет размер не-клиентской области окна. Мы чуть-чуть поправим размер сверху на высоту заголовка.

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

Рис.3. Панель-заголовок формы уехал в не-клиентскую область

Шаг 3: Нарисовать заголовок

Конечно, процедура NCPaint претерпела серьезные изменения. Полный листинг в конце статьи. Если вкратце, теперь во всех трех режимах формируется битмап, на котором потом создается текстурная кисть. Далее формируется путь TGPGraphicsPath на пересечении двух прямоугольников и с помощью этой кисти выводится на контекст.

Чтобы нарисовать панель заголовка, будем использовать метод PaintTo и свойство ParentBackground, которое мы выставляем в обработчике OnChange чекбокса Non-client. Фрагмент из NCPaint, рисующий панель:

Рисуем, начиная с точки (BorderWidth, BorderWidth), потому что надо учитывать отступы не-клиентской части сверху и слева.

Рис.4. Образ панели-заголовка теперь виден

Образ панели получился «неживой», нереагирующий на мышь. Сейчас оживим.

Трансляция Non-Client событий

Понятно, что события не могут «пробиться» к элементам, находящимся в не-клиентской области окна. Они как бы в «сумраке»…

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

WM_NCHITTEST

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

Для определения того, что точка попала в заголовок, используем следующий метод:

Обработка движения мыши

За событие движения мыши в не-клиентской области окна отвечает WM_NCMOUSEMOVE.

Обработчик таков:

Для начала получаем и считаем позицию курсора в клиентских координатах окна. Если координата попадает в область заголовка, преобразуем Y координату в клиентскую для панели. Далее пытаемся по этим координатам найти подходящий WinControl на панели, который возможно обработает событие. И если таковых не нашлось, отправляем событие WM_MOUSEMOVE панели.

Поиск WinControl‘ов — кандидатов и отправка сообщения осуществляется следующим образом:

Обработка нажатий мыши

За нажатия мыши в нашем случае будут отвечать события WM_NCLBUTTONDOWN и WM_NCLBUTTONUP.

Обработчики по структуре абсолютно такие же, как представленный выше обработчик движения мыши. Единственное различие, рассылаются сообщения WM_LBUTTONDOWN и WM_LBUTTONUP соответственно.

ComboBox в заголовке

Итак, у нас получилось следующее:

Рис.5. ComboBox в заголовке работает

Как видим, события транслируются превосходно. Клики на метках работают. Перетаскивание за заголовок формы — все есть. Хотя никаких особых ухищрений для этого не потребовалось. Работают все те же OnMouseDown, OnClick, OnMouseMove на компонентах.

Почему ComboBox в заголовке вообще работает. Потому что его parent, панель-заголовок, в статусе видимости. Посылаем ComboBox событие WM_LBUTTONDOWN с правильными координатами. В итоге он работает, как будто лежит на форме в легальной зоне.

Специально для того, чтобы выпадающее окно выпадало правильно, в OnResize устанавливаем правильные координаты панели. Когда приходит сообщение WM_LBUTTONDOWN, ComboBox определяет экранные координаты для выпадающего окна и показывает его. А так как он лежит на самом деле ровно под тем местом, на котором его нарисовали, то и окно выпадает прям идеально.

То же самое будет и с выпадающим меню. Кинем на форму TPopupMenu. Сделаем в нем пару элементов и на событие OnClick изображения Image1, которое служит иконкой, напишем такой обработчик:

Супер-незамысловато! Однако, между тем, из non-client области он будет работать более чем адекватно. Мы преобразовали не-клиентские координаты в клиентские и в обработчик OnMouseDown приходят X и Y в системе координат Image1.

Рис.6. PopupMenu на иконке из не-клиентской области по клику левой кнопки мыши.

Оптимизация кода

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

Перепишем уже существующие обработчики:

Реакция на правую кнопку мыши

Добавим еще пару обработчиков:

И их схожая реализация, только еще короче:

Теперь, если установить в свойство Image1.PopupMenu наш только что созданный PopupMenu1, и убрать обработчик Image1.OnMouseDown := nil для чистоты эксперимента, то выпадение контекстного меню станет автоматическим по правой кнопке.

Да, мне кажется это круто. Нет, это не заготовка под системное меню.

Также замечательно работают календарь (TDateTimePicker) и прочие «выпадашки»: TColorBox, TToolButton со стилем tbsDropDown и установленным DropdownMenu.

Рис.7. Календарь вместо текста заголовка

Метод WndProc

Конечно, все можно было бы сделать через переопределение метода формы WndProc:

И весь код с мышиными обработчиками не-клиентской области сокращается до такой реализации:

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

Также, теперь мы обрабатываем еще и двойной клик в не-клиентской области окна. Исключение составил только WM_NCMOUSEMOVE, потому что в нем происходит проверка на текущее рисование пламени. Причина только в этом. Эту проверку вполне можно было бы дописать в WndProc, но хотелось оставить простоту и изящество «в одной строке». Эдакое одностишье со смыслом.

Когда стоит применять

Прекрасно подходит для элементов, управление которыми привычно через клик. Button, BitBtn, PopupMenu, Image, Label, CheckBox, ComboBox в заголовке будут смотреться и вести себя просто идеально.

Недостатки

Если приглядеться к надписям, или убрать «жирность» шрифта, увидим черные рудименты вокруг символов. Это работает сглаживание, в этих местах должна быть полупрозрачность, но ее, к сожалению, нет. Аналогичные рудименты видны вокруг красного круга иконки. В этих местах также должно быть легкое размытие.

Вывести элементы редактирования, типа TEdit, можно, но пользоваться ими будет некомфортно. Не видно курсора внутри, плохая реакция на отрисовку.

Если придраться к тому, что CheckBox не реагирует на смену цвета шрифта, то это проблема CheckBox‘а. В лоб проблема решается размещением на какой-нибудь панельке узкого чекбокса, так чтобы видна была только галка, и метки, на клик которой устанавливать или снимать свойство Checked. Если решать изящно, то хотелось бы верить, что разговор об этом состоится в недалеком будущем. Голосуем в комментариях, интересует или нет, как решить эту проблему изящно.

Что будет дальше

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

Также, дальше мы откажемся от «попсовости» в пользу «деловитости». Пламя отбирает прилично ресурсов. Для украшения — вполне себе применимо, для дела надо другой подход.

Также, будем размещать в заголовке Edit’ы, SpinEdit’ы и прочие полезные контролы.

Если есть заинтересованность в решении таких проблем, подписывайтесь на канал в телеге, комментируйте. Меня это весьма мотивирует. Если кто-то хочет поучаствовать в создании контента, регистрируйтесь на сайте и пишите статьи. Давайте делать мир понятней!

Листинги

Рисование всей не-клиентской области

Обратите внимание на строку idx := ComboBox1.ItemIndex. Все обращения к массивам, а также приведение типов (например, TFireColorMode(idx)) во всем проекте происходит через предварительную инициализацию переменной idx: Integer. Использование ComboBox1.ItemIndex в качестве индекса массива или типа в семействе XE приводит к неожиданным ошибкам в самых загадочных местах. Не зная про этот глюк, можно искать до посинения.

Таскать за заголовок

Изначально это был обработчик только для формы. Но быстро стал общим. На него кто только не повешен.

Скрины

Рис.8. ComboBox в заголовке для WIndows 10
Рис.9. ComboBox в заголовке для других версий WIndows

Скачать

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

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


Исходник (zip) 209 Кб. Delphi 7, XE 7, XE 10, XE 11

Для XE открываем файл .dpr и спокойно build’им. Путь из Search Path можно убрать, а можно и не убирать, модули из него никак не задействованы в XE из-за директивного условия.

Исполняемый файл (zip) 317 Кб.


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

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

Хотел написать про Edit-подобные контролы, что их тоже можно и нужно размещать в заголовке, но увидел в статье, что это планируется на будущее, что радует.
А за статью спасибо! Очень подробно, доступно, понятно. Впрочем, как и всегда! )

Oleg

Есть одна заковыка с этими «кастомными» заголовками. Они (как и следует ожидать) не реагируют на такие уже привычные для обычных win-окон действия, как: если ты тащишь окно за загривок (заголовок) кверху, то окно максимизируется. Если тащишь вправо — оно впишется в правую половину экрана, если влево — то в левую и т.д. и т.п. Мало того, есть еще аналог этих манипуляция с клавиатуры через сочетания: кнопка WIN + стрелки курсора. Было бы круто, если бы была возможность реализовать такое и с формами \ приложениями, где заголовок свой полностью, как в статье?

Михаил

Добрый день ! Классный пример, спасибо! Но вот обнаружил проблему: при растягивании размера формы по ширине в режиме отрисовки noclient заголовок не перерисовывается, не могли бы Вы подсказать что поправить ?

Screen.jpg
Михаил

Спасибо за оперативный ответ !
Использую Win7 без всяких тем, со стандартным квадратным интерфейсом чтобы смотреть что происходит с интерфейсом в таком кривом варианте винды, компилирую в XE2, enable runtime themes включено, перерисовка заголовка до нормальных размеров происходит когда мышь попадает в его область

Михаил

Снова с добрым днем, в дополнение к предыдущей проблеме обнаружил еще одну — при развороте формы на весь экран съедается заголовок

Михаил

Снова спасибо за оперативный отклик ! SendMessage помог решить проблему артефакта перерисовки в правом верхнем углу, а вот заголовок при развертке упс….., сам еще не копал эту тему

Михаил

Идею понял, переделал немного по другому, все получилось, спасибо !

12
0
Не нашли ответ на свой вопрос? Задайте его здесь!...x