Трюки с формой 1.0: Без заголовка с тенью и отзывчивой рамкой

Рамка - огонь!

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

Например, такой случай может возникнуть, когда заказчик требует поместить в заголовок формы ComboBox, календарь, разные checkbox‘ы, подписи в виде TLabel. Причем состав заголовка меняется в зависимости от того, что сейчас в этой форме отображается. При этом все должно выглядеть как обычно — заголовок, кнопки закрытия, максимизации, сворачивания, иконка и системное меню. Окно должно уметь менять размер мышкой. Иметь тень. Все атрибуты обычного окна, с необычным заголовком.

Немного про заголовок

В Delphi 10.4 Sydney появился «чудо-компонент» TTitleBarPanel. По поводу этого чуда, которое работает только под Windows 8 и 10, удобству использования и восторгов в его честь, выскажусь в отдельной статье.

А как же быть мне, любителю XE 7? А как же быть любителям Delphi 7, имя им — легион?

Сделать свой заголовок — это значить поместить свою панель вместо заголовка. Вот этим и займемся, неспеша и последовательно. В части 1 рассмотрим, как сделать тень, рамку и возможность менять размер мышкой при BorderStyle = bsNone. Максимально субъективно, по возможности внятно и коротко — как говорит Пивоваров А.В.

Для начала нам нужно избавиться от заголовка.

Без заголовка

Избавиться от заголовка можно, например, так. Сделать BorderStyle := bsSizeToolWin или bsSizeable, и в обработчике события OnCreate написать следующее:

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

Казалось бы, проблема решена. Но тут есть ряд нюансов.

Рис.1. Так выглядит лишенное заголовка sizeble окно в разных ОС

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

В то время, как BorderStyle = bsNone дает нам «чистый» холст — рисуй, что хочешь, обрабатывай не-клиентскую часть, как считаешь нужным. Это окно без заголовка, рамок, тени, страха и упрека.

Поэтому, для избавления от заголовка начисто, используем BorderStyle = bsNone.

Делаем тень

Тень — это дело Dwm. Тут есть немножко про DwmAPI. Рассмотрим вначале, как сделать тень в XE 5 и выше. Потом сделаем для Delphi 7.

Тень в XE 5 и выше

Подключим в uses модули:

Чтобы включить Aero тень, используем функцию DwmSetWindowAttribute следующим образом:

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

Включаем в обработчик OnCreate формы следующий фрагмент:

Что происходит. Инициализируем тень и выводим на форму сообщения об ошибках, если таковые возникли. Если вызов SetDwmArea неудачен, то есть красивое размытие не увидим, делаем тень топорным CS_DROPSHADOW. Какая-никакая, но тоже тень.

Необходимо обработать еще два DWM-события. Без них не будет тени. В private секции формы пишем следующее:

И где-то в implementation пишем реализацию:

Все. Симпатичная и привычная тень у нас теперь есть для окна без заголовка с BorderStyle = bsNone.

Рис.2. Окно bsNone без заголовка, рудиментов и с тенью

Тень в Delphi 7

Абсолютно тот же код, что для «XE 5 и выше» будет работать и в Delphi 7, при условии, что мы опишем псевдонимы некоторых функций из DwmApi. Для начала заключим блок с модулями Dwmapi в директивное условие.

Говорят, что новшество появилось в Delphi 2010. Проверить не могу, поэтому отсчет цивилизации начинается с XE 5.

Для Delphi 7 предлагаю такой блок:

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

Динамическая загрузка сделана потому, что Dwm существует, начиная с Vista. Под XP эту штуку еще не придумали. При запуске на XP ошибок не будет. Просто не будет тени.

Рамка окна

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

В приватной секции формы пишем такое объявление:

Задать размер не-клиентской области окна

Так делают суровые программисты

Что происходит. Здесь мы задаем отступы для не-клиентской области со всех сторон окна.

[свернуть]

Константа CNS_NC_SIZE объявлена как:

Директива WRITEABLECONST ON включает возможность присваивать типизированным константам другие значения в коде. Почему не можем использовать, как VAR переменную? Потому что «не должно быть глобальных переменных в грамотном коде«. Не буду говорить, чьи слова.

Программист должен быть в первую очередь грамотным и ленивым. Поэтому часть для «суровых» программистов заменяем в обработчике OnCreate формы на строку:

Остальное сделает TWinControl. В исходниках раздела СКАЧАТЬ обработчика WMNCCalcSize нет. Потому что, как люди мы — суровые, но как программисты — ленивые. Но WMNCCalcSize пригодится в дальнейшем, поэтому оставил в тексте статьи.

Рис.3. Окно без заголовка, с тенью и рамкой в 4 пикселя

Нарисовать не-клиентскую область окна

Зачем нам вообще ее рисовать? Если мы захотим свой цвет в будущем заголовке, то это будет резко диссонировать с системным цветом рамки.

Зачем нам рамка, ведь на рисунке 2 все чудесно? Об этом чуть позже.

Покрасим рамку в красный.

Какие тут есть нюансы.

Во-первых, в обработчике WMNCPaint контекст устройства DC получаем через GetWindowDC(Handle). Не надо использовать ни Canvas.Handle формы, ни что либо еще. Это неправильно.

Во-вторых, в методе NCPaint прямоугольник формы получаем, как GetWindowRect(Handle, rct). Если формировать прямоугольник от текущих значений свойств формы Left, Top, Width, Height, будет мрак.

Рис.4. Прямоугольник окна посчитан от свойств окна. Видны рудименты и глюки

В стандартном GDI API нет возможности нарисовать прямоугольник так, чтобы перо описывало его «изнутри». Поэтому, грани прямоугольника проходят по центральной оси «толстой» линии. В связи с чем, толщина пера равна CNS_NC_SIZE * 2 — 1. Если пытаться задать какое-либо другое значение, получим рудименты, как в клиентской области окна, так и в не-клиентской.

Рис.5. Ширина рассчитывается по советам некоторых авторов, как Round(CNS_NC_SIZE*1.9). Это неправильно

Менять размер мышкой

Снова код одинаков, что для Delphi 7, что для XE 5, XE 7, XE 10 и надеюсь XE 11.

Конечно, можно обрабатывать событие мыши OnMove или WM_MOUSEMOVE. По координатам определять в каком направлении менять размеры, выбрать соответствующий курсор, обработать нажатие мышкой с последующим перетаскиванием, посчитать ширину, высоту. Если таскабельная область оказалась слева или сверху, дополнительно менять Left и Top. Удовольствие так себе.

Можно сообразить, что если у нас появилась не-клиентская область окна, событие должно быть WM_NCMOUSEMOVE. Определившись с направлением, можем сделать такой ход. Определить параметр cmd, как сумму SC_SIZE + одна из констант, начинающихся на WMSZ_… Далее вызвать следующее:

Для такого cmd у меня даже набор констант есть:

Модифицирующие размер константы для WM_SYSCOMMAND

[свернуть]

Это, отчасти, верно. Одно но. Чтобы начать получать хоть что-то по не-клиентской области, вначале мы должны обработать событие WM_NCHITTEST.

Пишем в приватной части формы:

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

Для начала определимся с какой стороной или углом окна имеем дело:

Здесь происходит анализ координат и возвращается число от 0 до 8, где 0 — мимо всего, 1 — левый верхний угол, 2 — верх, 3 — правый верхний, 4 — правая сторона, 5 — нижний правый, 6 — низ, 7 — левый нижний, 8 — левая сторона.

У нас есть целый набор констант, которые говорят Windows, что ему делать и как реагировать на попадание курсора в конкретную область окна. В Message.Result необходимо вернуть понятную для Windows константу.

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

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

Подключаем GDI+

В предыдущей статье рассмотрено, как подключить GDIPlus для Delphi 7. Воспользуемся этой полезной информацией и подключим в опциях проекта для Delphi 7 каталог ..\GDIPPlus\ в поле Search Path. Для семейства XE этого делать не надо, в них уже все это есть.

Рис.6. Опции проекта Project-Options…

В предложение uses добавим такой код:

Добавим на форму ряд компонент, определяющих — рисуем ли с помощью GDI, или GDI+. Для режима GDI+ зададим три режима — просто рамка, градиентная рамка, огненная рамка.

Зачем нам GDI+. Во-первых, есть возможность указать, что ширина линии направлена внутрь фигуры. Во-вторых, GDI+ дает множество визуальных возможностей.

Простая рамка

Рис.7. Красная рамка нарисованная GDI+ в Delphi 7

Градиентная рамка

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

Фрагмент в NCPaint:

Рис.8. Градиентная рамка нарисованная GDI+ в Delphi 7

Огненная рамка

Это не видео, не набор битмапов. Мы генерируем битмап, используя этот алгоритм. Затем получившийся битмап (FBkgBmp: TGPBitmap) используем при создании текстурной кисти.

Так как не по теме статьи, генерацию огня спрячу в спойлер. По теме эффектов планируется цикл статей. Если это интересно, пишите комментарии, подписывайтесь на телегу, участвуйте в создании познавательного контента! )))

Создать эффект огня

[свернуть]

Что получилось, продемонстрировано ниже. «Горит» не только рамка, но и «внутренности» текстов. Как сделано — в исходниках.

Рис.9. Гиф так себе, но общее представление дает
Рис.10. Работа под разными осями. Также показан вывод ошибок. Например, для XP

NCPaint целиком

Выводы

Модифицированные модули GDI+ вполне рабочие для Delphi 7.

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

Зная, как все работает и зачем это нужно, можно обходится без дополнительных компонент. Большинство из которых, в настоящее время, всего лишь красочная упаковка того же анальгина. Только стоит раз в 10 дороже.


Скачать

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

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


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

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

Внутри исходников есть еще всякие интересные штуки, для которых не нашлось места в статье.

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


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

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

Отличная статья, читается как хороший детектив. И интрига насчёт эффектов даже есть )
Так вот, по эффектам по типу той же огненной рамки — безусловно интересно, по крайней мере, мне ) Потому жду дальнейшего раскрытия темы, так сказать. Ну, по мере сил и возможностей, разумеется.

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

Ну и если посмотреть на многие текущие приложения для десктопа, то очень много где вообще рисуют свой интерфейс полностью, заменяя системные элементы управления на отрисованные картинки, и все это под управлением html/js и с использованием css, и в итоге вообще порой это выглядит как вкб-приложение, ни разу не нативно. Это не плохо, если все сделано с умом, со вкусом и этим удобно пользоваться. Но всего хорошо в меру )
Впрочем, как использовать те или иные инструменты — это дело каждого программиста. А автору — благодарность за то, что он такие вот инструменты описывает, рассказывает, показывает. Делает в общем. Спасибо!

Гостья

7: Message.Result := HTBOTTOMRIGHT; заменить на 7: Message.Result := HTBOTTOMLEFT; а остальном весьма любопытный пример, спасибо за публикацию.

Гостья

А насчёт плюсов «родного» заголовка, конечно, заблуждение: от него отказываются и идут на различные ухищрения как раз именно потому, что он не поддерживает даже тёмную тему Windows 11 (он всегда белого цвета).

Гостья

Не верю. У меня наоборот все спрашивают, почему в моём приложении есть это бельмо, от которого я никак не могу избавиться.

0.gif
Жора

Случилось чудо: наконец-то хоть кто-то догадался всё это систематизировать и собрать и показать в одном месте!
Спасибо!

Жора

Если в примере включить GDI+, Red Border = true и Border Width = 1, то правая и нижняя границы не отображаются и не реагируют на наведение курсора. Это как-то можно починить?

0.gif
Жора

У вас всё работает? Странно. У меня в демонстрационном примере, если включить всю связку:
1) GDI+
2) Red Border = true
3) Border Width = 1
то правая и нижняя границы вообще на мышь не реагируют (курсор не меняется). И их даже не видно.

Никаких изменений конечно в пример мною не вносилось.

Последний раз редактировалось 1 год назад Жора ем
Жора

А можно исходники, пожалуйста, если не затруднит? Мне песочницу долго заводить для проверки исполняемого 🙁

Жора

Спасибо за обновление! К сожалению, что с GDI+, что без — правой и нижней границ в 1 пиксель не видно ни мне, ни мышке.

border.gif
Жора

Да, спасибо, а я на десятке пока не могу проверить, у меня временно ноут на восьмёрке.

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