1.2.4. Пример PanelMsg

1.2.4. Пример PanelMsg

Программа PanelMsg показывает, как можно перехватить оконные сообщения, поступающие компоненту, лежащему на форме. В данном случае этим компонентом будет TPanel. Для перехвата сообщений используется свойство WindowProc панели.

Мы будем обрабатывать два сообщения, приходящих с панели: WM_RBUTTONDBLCLK и WM_PAINT. Таким образом, наша панель получит возможность реагировать на двойной щелчок правой кнопки мыши, а также рисовать что-то на своей поверхности. С помощью одной только библиотеки VCL это сделать нельзя.

Примечание

Для рисования на поверхности панели, вообще говоря, существует более простой и правильный способ: нужно положить на панель компонент TPaintBox, растянуть его на всю область панели и рисовать в его событии OnPaint. Мы здесь используем более сложный способ перехвата сообщения WM_PAINT только в учебных целях.

При перехвате сообщения WM_PAINT любого компонента, на котором расположены неоконные визуальные компоненты, может возникнуть проблема с перерисовкой этих компонентов. Чтобы продемонстрировать способ решения этих проблем, разместим на панели компонент TLabel, который заодно будет показывать пользователю реакцию на двойной щелчок правой кнопкой мыши. В результате получается окно, показанное на рис. 1.9. При двойном щелчке правой кнопкой мыши на панели надпись Сделайте двойной щелчок правой кнопкой перемещается в то место, где находится курсор. Чтобы перехватить оконную процедуру панели, следует написать метод, который ее подменит, а адрес старого метода сохранить в предназначенном для этого поле. Сам перехват будем осуществлять в обработчике события OnCreate формы (листинг 1.29).

Рис. 1.9. Окно программы PanelMsg

Листинг 1.29. Перехват обработчика сообщений панели

type

 TForm1 = class(TForm)

 Panel: TPanel;

 Label1: TLabel;

procedure FormCreate(Sender: TObject);

 private

// Здесь будет храниться исходный обработчик сообщений

 // панели

FOldPanelWndProc: TWndMethod;

// Этот метод будет перехватывать сообщения,

// предназначенные панели

procedure NewPanelWndProc(var Msg: TMessage);

 end;

procedure TForm1.FontCreate(Sender: TObject);

begin

 FOldPanelWndProc:= Panel.WindowProc;

 Panel.WindowProc:= NewPanelWndProc;

end;

Сам перехватчик выглядит так, как показано в листинге 1.30.

Листинг 1.30. Метод-перехватчик сообщений панели

procedure TForm1.NewPanelWndProc(var Msg: TMessage);

var

 NeedDC: Boolean;

 PS: TPaintStruct;

 PanelCanvas: TCanvas;

begin

 if Msg.Msg = WM_RBUTTONDBLCLK then

 begin

Label1.Left:= Msg.LParamLo;

 Label1.Top:= Msg.LParamHi;

 Msg.Result:= 0;

 end

 else if Msg.Msg = WM_PAINT then

 begin

// Проверяем, был ли запрошен контекст устройства

 // обработчиком, стоящим раньше по цепочке, и если не

 // был, то запрашиваем его.

 NeedDC:= Msg.WParam = 0;

 if NeedDC then Msg.WParam:= BeginPaint(Panel.Handle, PS);

 // Вызываем старый обработчик WM_PAINT. Его нужно

 // вызывать обязательно до того, как мы начнем рисовать

 // на поверхности что-то свое, т. к. в противном случае

 // это что-то будет закрашено стандартным обработчиком.

 POldPanelWndProc(Msg);

// При использовании графических функций API самое

 // неудобное — это вручную создавать и уничтожать кисти,

 // карандаш и т. п. Поэтому здесь создается экземпляр

 // класса TCanvas для рисования на контексте устройства

 // с дескриптором, полученным при вызове BeginPaint.

 PanelCanvas:= TCanvas.Create;

 try

PanelCanvas.Handle:= Msg.WParam;

 FanelCanvas.Pen.Style:= psClear;

 PanelCanvas.Brush.Style:= bsSolid;

 PanelCanvas.Brush.Color:= clWhite;

PanelCanvas.Ellipse(10, 10, Panel.Width — 10, Panel.Height — 10);

 PanelCanvas.Brush.Color:= clYellow;

 PanelCanvas.Rectangle(100, 100, Panel.Width — 100, Panel.Height — 100);

finally

PanelCanvas.Free;

 end;

// В данном случае панель содержит визуальный неоконный

// компонент TLabel. Отрисовка неоконных компонентов

// происходит при обработке WM_PAINT родительского

// компонента, т. е. здесь она была выполнена при вызове

// стандартного обработчика. Таким образом, сделанный

// рисунок закрасил не только фон панели, но и

// неоконные компоненты. Чтобы компоненты были поверх

// рисунка, их приходится перерисовывать еще раз,

// вызывая protected-метод PaintControls. Это не очень

// эффективно, т. к. получается, что компоненты рисуются

// дважды: в стандартном обработчике и здесь. Но

// другого способа решить проблему, видимо, нет. Если

// бы на панели лежали только оконные компоненты,

// вызывать PaintControls не понадобилось, поскольку то, что

// мы рисуем на панели, не может затереть поверхность

// лежащих на этой панели других окон.

TFakePanel(Panel).PaintControls(Msg.WParam, nil);

// Если мы получали контекст устройства, мы же должны

// освободить его.

 if NeedDC then

begin

EndPaint(Panel.Handle, PS);

Msg.WParam:= 0;

 end;

 end

 else FOldPanelWndProc(Msg);

end;

Так как в наш обработчик поступают все сообщения, передающиеся в оконную процедуру панели, начинается он с проверки того, какое сообщение пришло. Сначала реализуем реакцию на WM_RBUTTONDBLCLK просто перемещаем метку Label1 на то место, где пользователь щелкнул мышью. Затем обнуляем результат, давая понять системе, что сообщение полностью обработано. Вызов унаследованного обработчика в данном случае не выполняем, потому что никакая унаследованная реакция на данное событие нам не нужна. Обработка сообщения WM_PAINT сложнее. Сначала необходимо разобраться с контекстом устройства, на котором будет производиться рисование. Вообще говоря, обработчик WM_PAINT должен получать этот контекст с помощью функции BeginPaint. Но если до написанного нами кода сообщение WM_PAINT уже начало обрабатываться, то контекст устройства уже получен, а вызывать BeginPaint два раза нельзя. В этом случае контекст устройства передаётся через параметр сообщения WParam. Соответственно, обработка сообщения WM_PAINT начинается с того, что мы проверяем, равен ли нулю параметр wParam, и если равен, то получаем контекст устройства, а если не равен, используем то, что передано.

Унаследованный обработчик закрашивает всю панель целиком, поэтому его нужно вызывать до того, как мы нарисуем что-то свое, иначе он просто закрасит то, что мы нарисовали. Так что следующий шаг — это вызов стандартного обработчика сообщений панели, указатель на который мы сохранили в поле FOldPanelWndProc. Только после этого можно что-то рисовать.

Примечание

Перекрывая обработку сообщения WM_PAINT, мы лишаем код VCL возможности полностью контролировать процесс перерисовки. В частности, это означает что значение свойства DoubleBuffered будет игнорироваться, двойной буферизации не будет. Поэтому еще раз напоминаем, что программа PanelMsg — это учебный пример, помогающий разобраться с механизмами взаимодействия VCL и Windows API, но не являющийся образцом для подражания. Если в реальной жизни потребуется рисовать что-то непосредственно на панели, нужно порождать от класса TPanel наследника и перекрывать в нем метод Paint.

Теперь можно нарисовать что-то свое. Здесь мы рисуем большой белый круг, а на его фоне — желтый прямоугольник. Для этого используем класс TCanvas способом, который был продемонстрирован в листинге 1.17 (см. разд. 1.1.11). Если бы мы остановились на этом, то увидели бы интересную картину: нарисованные фигуры лежат поверх текста метки Label1. Объяснение этому очень простое: метка является неоконным визуальным компонентом и рисуется на поверхности своего родительского компонента при обработке его сообщения WM_PAINT. А поскольку стандартный обработчик у нас вызывается до того, как рисуются круг и прямоугольник, любой неоконный компонент будет перекрыт ими. К оконным компонентам это, разумеется, не относится, они лежат над родительской панелью, и то, что мы рисуем на этой панели, не может оказаться над ними.

Мы не можем вставить свой код между рисованием непосредственно поверхности панели и рисованием компонентов на ней. Поэтому после отработки нашего кода приходится рисовать неоконные компоненты еще раз. Проще всего это сделать, вызвав метод PaintControls, который и используется стандартным обработчиком. Конечно, получится, что неоконные компоненты рисуются дважды: в стандартном обработчике и в нашем, и это не очень хорошо. Но повторим еще раз, что программа PanelMsg — не образец для подражания, а что-то вроде зонда для исследования особенностей работы VCL.

Вызов метода PaintControls затруднен тем, что он объявлен в разделе protected, а потому не может быть вызван из метода NewPanelWndProc, который относится к классу формы. Чтобы обойти это ограничение, нужно породить наследника от TPanel — TFakePanel. Этот наследник ничего не добавляет к классу TPanel и ничего не переопределяет в нем. Но раз он объявлен в нашем модуле, все его protected-члены, в том числе и унаследованный метод PaintControls, становятся доступными в нем. После этого мы можем привести поле, содержащее ссылку на панель, к этому типу и вызвать PaintControls. Так как структуры типов TPanel и TFakePanel идентичны, это приведет к вызову нужного метода.

Для завершения обработки сообщения WM_PAINT осталось только вызвать EndPaint, разумеется, только в том случае, если BeginPaint вызывали мы сами.

И последнее, что мы должны сделать, — это передать все остальные сообщения стандартному обработчику. После этого программа PanelMsg готова.

Данный текст является ознакомительным фрагментом.