В этом примере мы создадим "дырявое" окно. Те, кто уже знаком с функцией
SetWindowRgn
, знает, что сделать "дырку" в окне или придать ему какую-либо другую необычную форму не так уж и сложно. Но мы здесь пойдем дальше: у дырки в нашем окне будет рамка, и пользователь сможет изменять размеры и положение дырки так же, как он может изменять положение и размеры окна. Как это выглядит, показано на рис. 1.14.
Рассмотрим те средства, которые нам понадобятся для реализации этого.
1.3.3.1. Сообщение WM_NCHCHITTEST
Каждое окно в Windows делится на две области: клиентскую и не клиентскую. Клиентской называется та
область, в которой отображается содержимое окна. Неклиентская область — это различные служебные области окна: рамка, заголовок, полосы прокрутки, главное меню и т.п. Положение клиентской части окна относительно неклиентской определяет само окно при обработке сообщения
WM_NCCALCRECT
. Многие окна (особенно различные элементы управления) вообще не имеют неклиентской части.
Некоторые сообщения для клиентской части окна имеют аналоги для неклиентской. Например, перерисовка клиентской области осуществляется с помощью сообщения
WM_PAINT
, а неклиентской —
WM_NCPAINT
. Нажатие левой кнопки мыши над клиентской частью окна генерирует сообщение
WM_LBUTTONDOWN
, а над неклиентской —
WM_NCLBUTTONDOWN
и т.п. Неклиентская область неоднородна: в нее входит заголовок, кнопки сокрытия, разворачивания и закрытия окна, иконка системного меню, главное меню, вертикальная и горизонтальная полосы прокрутки и рамка. Рамка тоже неоднородна — она имеет левую, правую, верхнюю и нижнюю границы и четыре угла. Сообщение
WM_NCCALCSIZE
позволяет выяснить, какая область окна является неклиентской, но не позволяет узнать, где какая часть неклиентской области находится. Эта задача решается с помощью другого сообщения —
WM_NCHITTEST
. В качестве входных параметров
WM_NCHITTEST
получает координаты точки, а результат кодирует, к какой части окна относится эта точка (например,
HTCLIENT
означает, что точка принадлежит к клиентской части окна,
HTCAPTION
— к заголовку,
HTLEFT
— к левой границе рамки, меняющей размер, и т.п.).
Рис. 1.14. "Дырявое" окно
При любых событиях от мыши система начинает с того, что посылает окну сообщение
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
.
1.3.3.2. Регионы
Регионы — это особые графические объекты, представляющие собой области произвольной формы. Ограничений на форму региона нет, они даже не обязаны быть связными. Существует ряд функций для создания регионов простых форм (
CreateRectRgn
,
CreateEllipticRgn
,
CreatePolygonRgn
и т.п.), а также функция
СombineRgn
для объединения регионов различными способами. Все это вместе позволяет получать регионы любых форм. Область применения регионов достаточно широка. Ранее мы уже видели, как с помощью регионов можно ограничить область вывода графики. Здесь же мы будем с помощью функции
SetWindowRgn
изменять форму окна, придавая ему форму заданного региона.
1.3.3.3. Сообщения WM_SIZE и WM_SIZING
События
WM_SIZE
и
WM_SIZING
позволяют окну реагировать на перемещение его пользователем. В "классическом" варианте, когда пользователь начинает тянуть рамку окна, на экране рисуется "резиновый" прямоугольник, соответствующая сторона или угол которого движется за курсором мыши. Окно получает сообщение
WM_SIZING
при каждом изменении размера этого прямоугольника. Параметр
lParam
при этом содержит указатель на структуру
TRect
с новыми координатами прямоугольника. Окно может не только прочитать эти координаты, но и изменить их, блокировав тем самым нежелательные изменения размера. На этом, в частности, основано использование свойства
Constraints
: если размер окна при перемещении становится меньше или больше заданного, при обработке сообщения
WM_SIZING
размер увеличивается или уменьшается до необходимого. Параметр
wParam
содержит информацию о том, за какую сторону или угол тянет пользователь, чтобы программа знала, координаты какого из углов прямоугольника нужно смещать, если возникнет такая необходимость.
После того как пользователь закончит изменять размеры окна и отпустит кнопку мыши, окно получает сообщение
WM_SIZE
. При получении этого сообщения окно должно перерисовать себя с учетом новых размеров. (Окно получает сообщение
WM_SIZE
после изменения его размеров по любой причине, а не только из-за действий пользователя.)
Описанный "классический" вариант в чистом виде существует только в Windows 95. Во всех более поздних версиях по умолчанию включена опция отображения содержимого окна при перетаскивании и изменении размеров (начиная с Windows ХР эта опция не только включается по умолчанию, но и не отключается средствами пользовательского интерфейса). В таком режиме при изменении размеров окна вместо прямоугольника "резиновым" становится само окно, и любое перемещение мыши при изменении размеров приводит к перерисовке окна. В этом режиме окно получает сообщение
WM_SIZE
каждый раз после сообщения
WM_SIZING
, а не только при завершении изменения размеров. Но в целом логика этих сообщений остается прежней, просто с точки зрения программы это выглядит так, как будто пользователь изменяет размеры окна "по чуть-чуть".
1.3.3.4. А теперь — все вместе
Комбинация описанных достаточно простых вещей позволяет построить окно с дыркой, имеющей изменяемые размеры.
Для начала объявим несколько констант, которые нам потребуются при вычислении размеров дырки и т.п. (листинг 1.51).
Листинг 1.51. Константы примера WndHole
const
// минимальное расстояние от дырки до края окна
HoleDistance = 40;
// Зона чувствительности рамки панели - на сколько пикселов