Антон Григорьев дата публикации 14-10-2005 03:42 Обобщающие примеры работы с WinAPI. Пример №4 — "дырявое окно"
Более полный вариант этой статьи вошёл в книгу "О чём не пишут в книгах по Delphi"
В этом примере мы создадим "дырявое" окно. Те, кто уже знаком с функцией SetWindowRgn, знает, что сделать дырку в окне или придать ему какую-либо другую необычную форму не так уж и сложно. Но мы здесь пойдём дальше: у дырки в нашем окне будет рамка, и пользователь сможет изменять размеры и положение дырки так же, как он может изменять положение и размеры окна.
Рассмотрим те средства, которые нам понадобятся для реализации этого.
Каждое окно в Windows делится на две области: клиентскую и неклиентскую. Клиентской называется та область, в которой отображается содержимое окна. Неклиентская область - это различные служебные области окна: рамка, заголовок, полосы прокрутки, главное меню и т.п. Положение клиентской части окна относительно неклиентской определяет само окно при обработке сообщения WM_NCCalcRect. Многие окна (особенно различные элементы управления) вообще не имеют неклиентской части.
Некоторые сообщения для клиентской части окна имеют аналоги для неклиентской. Например, перерисовка клиентской области осуществляется с помощью сообщения WM_Paint, а неклиентской - WM_NCPaint. Нажатие левой кнопки мыши над клиентской частью окна генерирует сообщение WM_LButtonDown, а над неклиентской - WM_NCLButtonDown и т.п.
Неклиентская область неоднородна: в неё входит заголовок, кнопки сокрытия, разворачивания и закрытия окна, иконка системного меню, главное меню, вертикальная и горизонтальная полоса прокрутки и рамка. Рамка тоже неоднородна - она имеет левую, правую, верхнюю и нижнюю границы и четыре угла. Сообщение WM_NCCalcRect позволяет узнать, какая область окна является неклиентской, но не позволяет узнать, где какая часть неклиентской области находится. Эта задача решается с помощью другого сообщения - WM_NCHitTest. В качестве входных параметров WM_NCHitTest получает координаты точки, а результат кодирует, к какой части окна относится эта точка (например, HTClient означает, что точка принадлежит к клиентской части окна, HTCaption - к заголовку, HTLeft - к левой границы рамки, меняющей размер, и т.п.).
При любых событиях от мыши система начинает с того, что посылает окну сообщение WM_NCHitTest с координатами положения мыши. На основании результата система решает, что делать дальше. В частности, при нажатии левой кнопки мыши окну посылается WM_NCHitTest. Затем, если результат был HTClient, посылается сообщение WM_LButtonDown, в противном случае - WM_NCLButtonDown. При каждом перемещении мыши окно также получает WM_NCHitTest - это позволяет системе постоянно отслеживать, над какой частью окна находится курсор, при необходимости меняя его вид (как, например, при прохождении курсора над рамкой).
Что будет, если подменить обработчик WM_NCHitTest? Например, таким образом, чтобы при попадании точки в клиентскую часть окна он возвращал не HTClient, а HTCaption? Это приведёт к тому, что любые события от мыши над клиентской областью будут восприниматься так же, как над заголовком. Например, можно будет взять окно за клиентскую часть и переместить его, а двойной щелчок на ней приведёт к разворачиванию окна. Однако это полностью блокирует нормальную реакцию на мышь, потому что вместо клиентских "мышиных" сообщений окно будет получать неклиентские.
С практической точки зрения окно, которое можно таскать за любую точку, обычно не очень интересно (особенно это касается приложений, разработанных с помощью VCL: на мышь перестанет правильно реагировать не только само окно, но и расположенные на нём неоконные элементы управления). Однако обработчик WM_NCHitTest можно сделать более интеллектуальным и получить довольно интересные эффекты. Например, положив на форму панель и переопределив у панели обработчик WM_NCHitTest таким образом, чтобы при нахождении мыши около границ панели возвращался результат, соответствующий различным частям рамки с изменяемым размером, можно получить панель, размеры которой пользователь программы сможет менять: система будет реагировать на эту область панели как на обычную рамку, которую можно взять и потянуть (Пример такой панели можно увидеть в статье "Компонент, который меняет свои размеры в режиме run-time аналогично тому, как это происходит в design-time"). Фантазия может подсказать и многие другие способы получения интересных эффектов с помощью WM_NCHitTest.
Регионы - это особые графические объекты, представляющие собой области произвольной формы. Ограничений на форму региона нет, они даже не обязаны быть связными. Существует ряд функций для создания регионов простых форм (CreateRectRgn, CreateEllipticRgn, CreatePolygonRgn и т.п.), а также функция CombineRect для объединения регионов различными способами. Всё это вместе позволяет получать регионы любых форм.
Область применения регионов достаточно широка. В частности, с помощью функции SetWindowRgn можно изменить регион любого окна, придав ему непрямоугольную форму. Можно выбрать регион отсечения при использовании графики - тогда всё, что не попадает в выбранный регион, не будет отображаться.
События WM_Size и WM_Sizing позволяют окну реагировать на перемещение его пользователем. В "классическом" варианте, когда пользователь начинает тянуть рамку окна, на экране рисуется "резиновый" прямоугольник, соответствующая сторона или угол которого движется за курсором мыши. Окно получает сообщение WM_Sizing при каждом изменении размера этого прямоугольника. Параметр LParam при этом содержит указатель на структуру TRect с новыми координатами прямоугольника. Окно может не только прочитать эти координаты, но и изменить, блокировав тем самым нежелательные изменения размера. На этом, в частности, основано использование свойства Constraints: если размер окна при перемещении становится меньше или больше заданного, при обработке сообщения WM_Sizing размер увеличивается или уменьшается до необходимого. Параметр WParam содержит информацию о том, за какую сторону или угол тянет пользователь, чтобы программа знала, координаты какого из углов прямоугольника нужно смещать, если возникнет такая необходимость.
После того как пользователь закончит изменять размеры окна и отпустит кнопку мыши, окно получает сообщение WM_Size. При получении этого сообщения окно должно перерисовать себя с учётом новых размеров. (Окно получает сообщение WM_Size после изменения его размеров по любой причине, а не только из-за действий пользователя.)
В современных версиях Windows существует опция Show window contents while dragging (включается через Панель управления, раздел Display). Если она включена, при изменении размеров окна вместо прямоугольника "резиновым" становится само окно, и любое перемещение мыши при изменении размеров приводит к перерисовке окна. В этом режиме окно получает сообщение WM_Size каждый раз после сообщения WM_Sizing, а не только при завершении изменения размеров. Но в целом логика использования этих сообщений остаётся прежней, просто с точки зрения программы это выглядит так, как будто пользователь изменяет размеры окна "по чуть-чуть".
Комбинация описанных выше достаточно простых вещей позволяет построить окно с дыркой, имеющей изменяемые размеры.
На форму кладётся панель. С помощью функции SetWindowRgn устанавливается такая форма окна, чтобы от панели была видна только рамка, а на всю внутреннюю часть панели пришлась дырка. Рамку выбираем такую, чтобы панель выглядела утопленной - таким образом, края дырки будут выглядеть естественней.
Сообщения, поступающие панели, перехватываются через её свойство WindowProc (подробно эта технология рассмотрена в статье "Основы работы с Win API в VCL-приложениях"; здесь мы её касаться не будем). Сообщение WM_NCHitTest будем обрабатывать так, чтобы при попадании мыши на рамку панели возвращались такие значения, чтобы за эту рамку можно было тянуть. В обработчике сообщения WM_Size панели изменяем регион так, чтобы он соответствовал новому размеру панели. Всё, дырка с изменяемыми размерами готова.
Осталось только немного "навести красоту". "Красота" заключается в том, чтобы пользователь не мог уменьшить размеры дырки до нуля и увеличивать так, чтобы она вышла за пределы окна, а также уменьшить окно так, чтобы дырка оказалась за пределами окна. Первая из этих задач решается просто: добавляется обработчик сообщения WM_Sizing для дырки таким образом, чтобы её размеры не могли стать меньше, чем 10х10, а границы нельзя было придвинуть к границам окна ближе, чем на 40 пикселей. Вторая задача решается ещё проще: в обработчике WM_Size дырки меняем свойство Constraints формы таким образом, чтобы пользователь не мог слишком сильно уменьшить окно. Теперь окно с дыркой ведёт себя корректно при любых действиях пользователя.
Напоследок добавим к программе один бантик: красные стрелки по углам формы, за которые можно потянуть, чтобы изменить её размер. Очевидно, что делается это через обработчик WM_NCHitTest, только для формы. Вопрос только в том, как узнать, когда координаты мыши попадают внутрь нарисованной стрелки, т.к. стрелка - объект сложной формы, вычислить это не очень просто. Задача решается с помощью регионов: для каждой стрелки создаётся регион, а попадание координат курсора в этот регион отслеживается с помощью стандартной функции PtInRegion. Соответственно, следует пересчитывать координаты регионов, когда окно меняет размер (можно не пересчитывать только координаты левой верхней стрелки, но для простоты в примере пересчитываются и они).
Вот и всё. С помощью нескольких нехитрых приёмов мы получили окно, которое имеет такой необычный вид.
К материалу прилагаются файлы:
[Окна, оконные сообщения] [Изменение размеров компонент, нестандартная форма] [Перемещение контролов в run-time] [Регионы и траектории (Paths)] [WM_SIZE] [WM_NCHITTEST] [WM_SIZING]
Обсуждение материала [ 15-12-2005 01:50 ] 2 сообщения |