1.3.3.4. А теперь — все вместе
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), он вызывается всегда, когда нужно изменить регион окна.
Листинг 1.52. Метод SetRegion, устанавливающий регион окна
procedure TFormHole.SetRegion;
var
Rgn1, Rgn2: HRGN;
R, R2: TRect;
begin
// Создаем регион, соответствующий прямоугольнику окна
Rgn1:= CreateRectRgn(0, 0, Width, Height);
// Нам потребуются координаты панели относительно левого
// верхнего угла окна (а не относительно левого верхнего
// угла клиентской области, как это задается свойствами
// Left и Тор). Функций для получения смещения клиентской
// области относительно левого верхнего угла окна нет.
// Придется воспользоваться сообщением WM_NCCalcRect
R2:= Rect(Left, Top, Left + Width, Top + Height);
Perform(WM_NCCALCSIZE, 0, LParam(@R2));
// Переводим координаты полученного прямоугольника из
// экранных в координаты относительно левого верхнего
// угла окна
OffsetRect(R2, -Left, — Top);
// получаем координаты панели относительно левого
// верхнего угла клиентской области и пересчитываем их
// в координаты относительно верхнего левого угла окна
R:= Rect(0, 0, PanelHole.Width, PanelHole.Height);
OffsetRect(R, PanelHole.Left + R2.Left, PanelHole.Top + R2.Top);
// уменьшаем прямоугольник на величину рамки и создаем
// соответствующий регион
InflateRect(R, — HoleBorder, — HoleBorder);
Rgn2:= CreateRectRgnIndirect(R);
// вычитаем один прямоугольный регион из другого, получая
// прямоугольник с дыркой
CombineRgn(Rgn1, Rgn1, Rgn2, RGN_DIFF);
// уничтожаем вспомогательный регион
DeleteObject(Rgn2);
// Назначаем регион с дыркой окну
SetWindowRgn(Handle, Rgn1, True);
// обратите внимание, что регион, назначенный окну, нигде
// не уничтожается. После выполнения функции SetWindowRgn
// регион переходит в собственность системы, и она сама
// уничтожит его при необходимости
end;
Сообщения, поступающие с панели, перехватываются через ее свойство WindowProc (подробно эта технология описана в первой части данной главы, здесь мы ее касаться не будем). Сообщение WM_NCHITTEST будем обрабатывать так, чтобы при попадании мыши на рамку панели возвращались такие значения, чтобы за эту рамку можно было тянуть. В обработчике сообщения WM_SIZE панели изменяем регион так, чтобы он соответствовал новому размеру панели. Все, дырка с изменяемыми размерами готова. Теперь нужно научить "дырку" менять размеры при изменении размеров окна, если окно стало слишком маленьким, чтобы вместить в себя дырку. Осталось только немного "навести красоту". "Красота" заключается в том, чтобы пользователь не мог уменьшить размеры дырки до нуля и увеличить так, чтобы она вышла за пределы окна, а также уменьшить окно так. чтобы дырка оказалась за пределами окна. Первая из этих задач решается просто: добавляется обработчик сообщения WM_SIZING для дырки таким образом, чтобы ее размеры не могли стать меньше, чем MinHoleSize на MinHoleSize пикселов, а границы нельзя было придвинуть к границам окна ближе, чем на HoleDistance пикселов. Вторая задача решается еще проще: в обработчике WM_SIZE дырки меняем свойство Constraints формы таким образом, чтобы пользователь не мог слишком сильно уменьшить окно. Теперь окно с дыркой ведет себя корректно при любых действиях пользователя с дыркой. Получившийся в результате код обработчика сообщений панели приведен в листинге 1.53.
Листинг 1.53. Обработчик сообщений панели, образующей "дырку"
procedure TFormHole.PanelWndProc(var Msg: TMessage);
var
Pt: TPoint;
R: TRect;
begin
POldPanelWndProc(Msg);
if Msg.Msg = WM_NCHITTEST then
begin
// Вся хитрость обработки сообщения WM_NCHITTEST
// заключается в правильном переводе экранных координат
// в клиентские и в несколько муторной проверке попадания
// мыши на сторону рамки или в ее угол. Дело упрощается
// тем, что у панели нет неклиентской части, поэтому
// верхний левый угол окна и верхний левый угол клиентской
// части совпадают.
Pt:= PanelHole.ScreenToClient(Point(Msg.LParamLo, Msg.LParamHi));
if Pt.X < BorderMouseSensivity then
if Pt.Y < CornerMouseSensivity then Msg.Result:= HTTOPLEFT
else
if Pt.Y >= PanelHole.Height — CornerMouseSensivity then
Msg.Result:= HTBOTTOMLEFT
else Msg.Result:= HTLEFT
else
if Pt.X >= PanelHole.Width — BorderMouseSensivity then
if Pt.Y < CornerMouseSensivity then Msg.Result:= HTTOPRIGHT
else
if Pt.Y >= PanelHole.Height — CornerMouseSensivity then
Msg.Result:= HTBOTTOMRIGHT
else Msg.Result:= HTRIGHT
else
if Pt.Y < BorderMouseSensivity then
if Pt.X < CornerMouseSensivity then Msg.Result:= HTTOPLEFT
else
if Pt.X >= PanelHole.Width — CornerMouseSensivity then
Msg.Result:= HTTOPRIGHT
else Msg.Result:= HTTOP
else
if Pt.Y >= PanelHole.Height — BorderMouseSensivity then
if Pt.X < CornerMouseSensivity then
Msg.Result:= HTBOTTOMLEFT
else
if Pt.X >= PanelHole.Width — CornerMouseSensivity then
Msg.Result:= HTBOTTOMRIGHT
else Msg. Result:= HTBOTTOM;
end
else if Msg.Msg = WM_SIZE then
begin
// Пересчитываем регион SetRegion;
// Устанавливаем новые ограничения для размеров окна.
// учитывающие новое положение дырки
Constraints.MinWidth:=
Width — ClientWidth + PanelHole.Left + MinHoleSize + HoleDistance;
Constraints.MinHeight:=
Height — ClientHeight + PanelHole.Top + MinHoleSize + HoleDistance;
end
else if Msg.Msg = WM_SIZING then
begin
// Копируем переданный прямоугольник в переменную R,
// одновременно пересчитывая координаты из экранных
// в клиентские
R.TopLeft:= ScreenToClient(PRect(Msg.LParam)^.TopLeft);
R.BottomRight:= ScreenToClient(PRect(Msg.LParam)^.BottomRight);
// Если ширина слишком мала, проверяем, за какую
// сторону тянет пользователь. Если за левую -
// корректируем координаты левой стороны, если за
// правую — ее координаты
if R.Right — R.Left < MinHoleSize then
if Msg.WParam in [WMSZ_BOTTOMLEFT, WMSZ_LEFT, WMSZ_TOPLEFT] then
R.Left:= R.Right — MinHoleSize
else
R.Right:= R.Left + MinHoleSize;
// Аналогично действуем, если слишком мала высота
if R.Bottom — R.Top < MinHoleSize then
if Msg.WParam in [WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT] then
R.Top:= R.Bottom — MinHoleSize
else R.Bottom:= R.Top + MinHoleSize;
// Сдвигаем стороны, слишком близко подошедшие
// к границам окна
if R.Left < HoleDistance then R.Left:= HoleDistance;
if R.Top < HoleDistance then R.Top:= HoleDistance;
if R.Right > ClientWidth — HoleDistance then
R.Right:= ClientWidth — HoleDistance;
if R.Bottom > ClientHeight — HoleDistance then
R.Bottom:= ClientHeight — HoleDistance;
// Копируем прямоугольник R, переводя его координаты
// обратно в экранные
PRect(Msg.LParam)^.TopLeft:= ClientToScreen(R.TopLeft);
PRect(Msg.LParam)^.BottomRight:= ClientToScreen(R.BottomRight);
end;
end;
Остается еще одна ситуация, когда границы "дырки" могут подойти к границам окна слишком близко: когда пользователь меняет не границы "дырки", а границы самого окна. Чтобы этого не случилось, нужно отслеживать изменения размеров окна и соответствующим образом менять размеры дырки — для этого нам потребуется изменить размеры панели и пересчитать регион. Пересчет региона необходим и в случае увеличения размеров окна: если его не пересчитать, получится, что часть окна не будет попадать в регион и будет отрезана. Все перечисленные действия выполняются в обработчике сообщения WM_SIZE окна (листинг 1.54).
Листинг 1.54. Обработчик сообщения WM_SIZE главного окна
procedure TFormHole.WMSize(var Msg: TWMSize);
begin
inherited;
// При уменьшении размеров окна уменьшаем размер дырки,
// если границы окна подошли слишком близко к ней
if PanelHole.Left + PanelHole.Width > ClientWidth — HoleDistance then
PanelHole.Width:= ClientWidth — HoleDistance — PanelHole.Left;
if PanelHole.Top + PanelHole.Height > ClientHeight — HoleDistance then
PanelHole.Height:= ClientHeight — HoleDistance — PanelHole.Top;
// На случай увеличения окна пересчитываем его регион,
// иначе та часть, которая добавилась, окажется за его
// пределами и будет отрезана SetRegion;
// Пересчитываем координаты стрелок
CalculateArrows;
Invalidate;
end;
Напоследок добавим к программе один "бантик": красные стрелки по углам формы, за которые можно потянуть, чтобы изменить ее размер. Каждая стрелка будет представляться отдельным регионом. Координаты регионов должны быть привязаны к соответствующим углам окна, поэтому при изменении размеров окна эти координаты придется пересчитывать (в листинге 1.54 можно найти соответствующий вызов). Все стрелки выглядят одинаково, но являются зеркальным отражением друг друга. Чтобы рассчитывать координаты стрелок, мы зададим координаты одной нарисованной заранее стрелки в виде константы, а потом будем рассчитывать на основе этих данных координаты реальных стрелок, отражая их от нужной плоскости и добавляя требуемое смещение (листинг 1.55).
Листинг 1.55. Расчет координат стрелок
// координаты верхней левой стрелки, отсчитанные от точки
// (0,0). Для получения координат реальных стрелок эти точки
// будут смещаться и отражаться
const
ArrowTemplate: TArrowCoords = (
(X:0; Y:0), (X:24; Y:0), (X:17; Y:7), (X:29; Y:19),
(X:19; Y:29), (X:7; Y:17), (X:0; Y:24));
procedure TFomHole.CalculateArrows;
var
Arrow: TArrowCoords;
I: Integer;
begin
// Вычисление региона левой верхней стрелки
// Координаты просто смещаются на постоянную величину
for I:= 0 to High(Arrow) do
begin
Arrow[I].X:= ArrowTemplate[I].X + ArrowOffset;
Arrow[I].Y:= ArrowTemplate[I].Y + ArrowOffset;
end;
// При необходимости уничтожаем старый регион
if ArrowTopLeft <> 0 then DeleteObject(ArrowTopLeft);
ArrowTopLeft:=
CreatePolygonRgn(Arrow[0], Length(Arrow), WINDING);
// Вычисление региона правой верхней стрелки
// Координаты по X отражаются и смещаются
// на постоянную величину относительно правого края окна
for I:= 0 to High(Arrow) do
begin
Arrow[I].X:= ClientWidth — ArrowOffset — 1 — ArrowTemplate[I].X;
Arrow[I].Y:= ArrowTemplate[I].Y + ArrowOffset;
end;
if ArrowTopRight <> 0 then DeleteObject(ArrowTopRight);
ArrowTopRight:= CreatePolygonRgn(Arrow[0], Length(Arrow), WINDING);
// Вычисление региона левой нижней стрелки
// Координаты по Y отражаются и смещаются
// на постоянную величину относительно нижнего края окна
for I:= 0 to High(Arrow) do
begin
Arrow[I].X:= ArrowTemplate[I].X + ArrowOffset;
Arrow[I].Y:= ClientHeight — ArrowOffset — 1 — ArrowTemplate[I].Y;
end;
if ArrowBottomLeft <> 0 then DeleteObject(ArrowBottomLeft);
ArrowBottomLeft:= CreatePolygonRgn(Arrow[0], Length(Arrow), WINDING);
// Вычисление региона правой нижней стрелки
// Координаты по обеим осям отражаются и смещаются
// на постоянную величину относительно правого нижнего угла окна
for I:= 0 to High(Arrow) do
begin
Arrow[I].X:= ClientWidth — ArrowOffset — 1 — ArrowTemplate[I].X;
Arrow[I].Y:= ClientHeight — ArrowOffset — 1 — ArrowTemplate[I].Y;
end;
if ArrowBottomRight <> 0 then DeleteObject(ArrowBottomRight);
ArrowBottomRight:= CreatePolygonRgn(Arrow[0], Length(Arrow), WINDING);
end;
Следующий шаг — рисование стрелки на форме. Делается это очень просто (листинг 1.56).
Листинг 1.56. Рисование стрелок на форме
procedure TFormHole.FormPaint(Sender: TObject);
begin
// Закрашиваем регионы стрелок
Canvas.Brush.Style:= bsSolid;
Canvas.Brush.Color:= clRed;
FillRgn(Canvas.Handle, ArrowTopLeft, Canvas.Brush.Handle);
FillRgn(Canvas.Handle, ArrowTopRight, Canvas.Brush.Handle);
FillRgn(Canvas.Handle, ArrowBottomLeft, Canvas.Brush.Handle);
FillRgn(Canvas.Handle, ArrowBottomRight, Canvas.Brush.Handle);
Остался последний шаг — объяснить системе, что пользователь может, ухватив за стрелки, изменять размеры формы. Очевидно, что делается это через обработчик WM_NCHITTEST. Вопрос только в том, как узнать, когда координаты мыши попадают внутрь нарисованной стрелки, поскольку стрелка является объектом сложной формы, вычислить это не очень просто. Данная задача также решается с помощью регионов: попадание координат курсора в регион каждой из стрелок отслеживается с помощью стандартной функции PtInRegion (листинг 1.57).
Листинг 1.57. Обработчик WM_NCHITTEST формы
procedure TFormHole.WMNCHitTest(var Msg: TWMNCHitTest);
var
Pt: TPoint;
begin
// Чтобы правильно обрабатывать стандартную неклиентскую область,
// вызываем унаследованный обработчик
inherited;
// Не забываем, что параметры WM_NCHITTEST дают экранные,
// а не клиентские координаты
Pt:= ScreenToClient(Point(Msg.XPos, Msg.YPos));
// Проверяем координаты на попадание в регионы стрелок
if PtInRegion(ArrowTopLeft, Pt.X, Pt.Y) then
Msg.Result:= HTTOPLEFT
else if PtInRegion(ArrowTopRight, Pt.X, Pt.Y) then
Msg.Result:= HTTOPRIGHT
else
if PtInRegion(ArrowBottomLeft, Pt.X, Pt.Y) then
Msg.Result:= HTBOTTOMLEFT
else
if PtInRegion(ArrowBottomRight, Pt.X, Pt.Y) then
Msg.Result:= HTBOTTOMRIGHT;
end;
Вот и все. С помощью нескольких нехитрых приемов мы получили окно, которое имеет такой необычный вид (см. рис. 1.14).
Данный текст является ознакомительным фрагментом.