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).
1.3.4. Обобщающий пример 4 — Линии нестандартного стиля
GDI позволяет рисовать линии разных стилей, но бывают ситуации, когда стандартных возможностей по изменению стиля линий не хватает. В этом разделе мы покажем, как рисовать линии произвольного стиля (начнем с прямых, потом перейдем к кривым Безье), а также сделаем "резиновую" линию, которую пользователь может тянуть мышью.
1.3.4.1. Получение координат точек прямой
Рисование нестандартных линий выполняется следующим образом: вычисляются координаты всех пикселов, составляющих данную прямую, а потом каждый из них (а при необходимости — и какая-либо его окрестность) раскрашиваются нужным цветом. Следовательно, возникает вопрос об определении координат пикселов.
Существует ряд алгоритмов вычисления этих координат. Наиболее известный из них — алгоритм Брезенхэма (Bresengham), который заключается в равномерном разбрасывании "ступенек" разной длины вдоль линии. В Windows используется алгоритм GIQ (Grid Intersection Quantization). Каждый пиксел окружается воображаемым ромбом из четырех пикселов. Если прямая имеет общие точки с этим ромбом, то пиксел рисуется.
Самостоятельно реализовывать один из таких алгоритмов нет необходимости — в Windows существует функция LineDDA, которая возвращает вызвавшей ее программе координаты линии. Эта функция в качестве параметра принимает координаты начала и конца линии, а также указатель на функцию, которой будут передаваться координаты пикселов. Данная функция должна быть реализована в программе. За время выполнения LineDDA эта функция будет вызвана столько раз, сколько пикселов содержит линия (как обычно в Windows, последний пиксел не считается принадлежащим прямой). Каждый раз при вызове ей будут передаваться координаты очередного пиксела, причем пикселы будут упорядочены от начала к концу прямой.
В примере Lines (рис. 1.15) с помощью LineDDA рисуется пять различных типов линий. Рассмотрим на примере самого сложного из реализуемых программой типов линии ("Зеленая елочка"), как это делается (листинг 1.58).
Рис. 1.15. Окно программы Lines
// константы для типа "Зеленая елочка"
const
// Угол отклонения "иголки" от направления линии
FirNeedleAngle = 30;
//Длина иголки
FirNeedleLength = 8;
var
Counter: Integer; // Счетчик точек линии
// Вспомогательные переменные для построения "елочки"
DX1, DY1, DX2, DY2: Integer;
// Линия в виде "елочки"
procedure LineDrawFir(X, Y: Integer; Canvas: TCanvas); stdcall;
begin
with Canvas do case Counter mod 10 of
0: begin
MoveTo(X, Y);
LineTo(X + DX1, Y + DY1);
end;
5:
begin
MoveTo(X, Y);
LineTo(X + DX2, Y + DY2);
end;
end;
Inc(Counter);
end;
procedure TLinesForm.Line(X1, Y1, X2, Y2: Integer);
var
Angle: Extended;
begin
case RGroupLine.ItemIndex of
...
4:
begin
Counter := 0;
Angle := ArcTan2(Y2 - Y1, X2 - X1);
DX1 := Round(FirNeedleLength *
Cos(Angle + Pi / 180 * FirNeedleAngle));
DY1 := Round(FirNeedleLength *
Sin(Angle + Pi / 180 * FirNeedleAngle));
DX2 := Round(FirNeedleLength *
Cos(Angle - Pi / 180 * FirNeedleAngle));
DY2 := Round(FirNeedleLength *
Sin(Angle - Pi / 180 * FirNeedleAngle));
LineDDA(X1, Y1, X2, Y2, @LineDrawFir, Integer(Canvas));
end;
end;
end;
Каждая "иголка" — это линия длиной FirNeedleLength пикселов, отклоняющаяся от направления прямой на угол FirNeedleAngle градусов. "Иголки" отклоняются попеременно то в одну, то в другую сторону от прямой. В процедуре Line сначала рассчитываются смещения координат конца "иголки" относительно начала и результаты помещаются в глобальные переменные DX1, DY1, DX2, DY2. Переменная Counter служит для определения номера точки. Перед вызовом LineDDA она инициализируется нулем. Затем вызывается функция LineDDA, в качестве одного из параметров которой передается указатель на функцию обратного вызова LineDrawFir. В результате этого функция LineDrawFir будет вызвана последовательно для каждого из пикселов, составляющих линию, начиная с (X1, Y1). LineDrawFir ведет подсчет пикселов, каждый раз увеличивая Counter на единицу. Если остаток от деления номера точки на 10 равен 0, рисуется "иголка", отклоняющаяся в положительном направлении, если 5 — в отрицательном. В остальных случаях не рисуется ничего. Так получается "елочка".
1.3.4.2. "Резиновая" линия и растровые операции
Теперь нужно дать пользователю возможность рисовать линии. Для этого мы используем стандартную "резиновую" линию: пользователь нажимает левую кнопку мыши и, удерживая ее, передвигает мышь. До тех пор, пока кнопка удерживается, за курсором тянется линия. Как только пользователь отпускает кнопку, линия "впечатывается" в рисунок.
Сама по себе реализация "резиновой" линии очень проста: при наступлении события OnMouseDown запоминаются координаты начала линии и взводится флаг, показывающий, что включен режим рисования "резиновой" линии. Также запоминаются координаты конца отрезка, который на данный момент совпадает с началом. В обработчике OnMouseMove, если включен режим рисования "резиновой" линии, стирается линия со старыми координатами конца и рисуется с новыми. При наступлении OnMouseUp программа выходит из режима рисования "резиновой" линии, рисуя окончательный ее вариант с текущими координатами конца.
Самое сложное в этой последовательности действий — стереть нарисованную ранее линию. Если бы у нас был однородный фон, можно было бы просто нарисовать старую линию еще раз цветом фона — это выглядело бы как ее стирание. Но поскольку фон не однородный, а составлен из нарисованных ранее линий, этот способ мы применить не можем.
Для решения этой задачи мы здесь рассмотрим самый простой метод — инверсное рисование (более сложный метод будет рассмотрен чуть позже). При этом каждая точка, принадлежащая линии, закрашивается не каким-либо фиксированным цветом, а инвертируется (т.е. к текущему цвету точки применяется операция not). Для стирания линии просто рисуем ее еще раз: двойная инверсия восстанавливает предыдущий цвет точек (not not X = X для любого X).
При рисовании пером и кистью GDI позволяет использовать различные растровые операции, которые определяют результирующий цвет каждого пиксела в зависимости от цвета фона и пера или кисти. По умолчанию применяется операция R2_COPYPEN, в которой цвет фона игнорируется, а результирующий цвет пиксела совпадает с цветом пера или кисти. Изменить растровую операцию можно с помощью функции SetROP2 (двойка в названии функции показывает, что устанавливаемая растровая операция имеет два аргумента — цвет рисования и цвет фона: при выводе растровых рисунков могут применяться растровые операции с тремя аргументами — см. функцию BitBlt). Нас будет интересовать операция R2_NOT, которая инвертирует фоновый цвет, игнорируя цвет пера или кисти.