Каждое окно в 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;
// Зона чувствительности рамки панели - на сколько пикселов
// может отстоять курсор вглубь от края панели, чтобы его
// положение расценивалось как попадание в рамку.
BorderMouseSensivity = 3;
// Зона чувствительности угла рамки панели - на сколько пикселов
// может отстоять курсор от угла панели, чтобы его
// положение расценивалось как попадание в угол рамки.
CornerMouseSensivity = 15;
// Толщина рамки дырки, использующаяся при вычислении региона
HoleBorder = 3;
// Минимальная ширина и высота дырки
MinHoleSize = 10;
// Смещение стрелки относительно соответствующего угла
ArrowOffset = 8;
Теперь приступаем к созданию программы. На форму "кладем" панель. С помощью функции
SetWindowRgn
устанавливаем такую форму окна, чтобы от панели была видна только рамка, а на всю внутреннюю часть панели пришлась дырка. Рамку выбираем такую, чтобы панель выглядела утопленной, так края дырки будут выглядеть естественней. Для расчета региона используется метод
SetRegion
(листинг 1.52), он вызывается всегда, когда нужно изменить регион окна.