Сообщений 4 Оценка 160 Оценить |
Вступление TWinControl и с чем его едят… TCustomControl как строительная площадка для собственных оконных элементов управления. |
Данная статья – продолжение цикла о разработке собственных компонентов в среде Delphi. Как мы и обещали, в это раз речь пойдет об элементах управления (далее просто контролах), способных получать фокус ввода, а также имеющих свое собственное окно.
Собственное окно (более грамотно сказать – дескриптор окна) есть только у оконных контролов. Чем же отличается проектирование оконных компонентов от проектирования графических, рассмотренного в предыдущей статье? В контексте VCL это различие заключается в том, что графические контролы являются потомками TGraphicControl, а оконные – TWinControl. Благодаря стараниям коллектива фирмы Борланд разработка оконных контролов в общем случае очень похожа на разработку графических (и мы убедимся в этом немного ниже).
Большинство элементов управления в операционной системе Windows является окнами. С технической точки зрения, окно является записью во внутренней системной таблице, которая соответствует элементу, отображаемому на экране и, как следствие, обладающему связанным с ним исполняемым кодом.
Этот «код» обычно называют оконной функцией или оконной процедурой. Каждому окну ставится в соответствие так называемая оконная процедура, которая и определяет «поведение» окна. Когда происходит какое-либо событие, одному из окон, ответственному в данный момент за обработку событий данного типа, посылается сообщение. Окно, получившее это сообщение, должно, разумеется, его обработать. Эту обработку и осуществляет оконная функция. Класс TWinControl используется как базовый для создания компонентов, имеющих свое собственное окно. В связи с этим у него есть различные свойства, методы и события, присущие любому оконному элементу управления.
В первую очередь TWinControl предоставляет свойство Handle, которое является ссылкой на идентификатор окна базового элемента управления. Однако немаловажным фактом является то, что при создании экземпляра типа TwinControl автоматически создается соответствующий ему дескриптор окна. В действительности в Delphi используется инициализация с задержкой. Это означает, что элемент управления создается только тогда, когда в этом возникает необходимость. Обычно это происходит при обращении к вышеупомянутому свойству Handle. Далее происходит вызов цепочки методов HandleNeeded -> CreateHandle, и только после этого происходит вызов CreateWnd, который будет рассмотрен ниже.
Что же из этого следует? В первую очередь то, что существует возможность хранить в памяти элемент управления, дескриптор которого будет создаваться только тогда, когда данный элемент будет затребован, например, при установке Visible в true.
Эта идея наводит на мысль о создании экономичного контрола, который будет создавать себя как окно только при его отображении на экране. За основу берем обычную кнопку (TButton).При Visible:=False – уничтожаем дескриптор, при Visible:=True – создаем.
Код компонента приведен ниже:
type TmmEconomicButton = class(TButton) protected procedure WndProc(var Message: TMessage); override; end; implementation { TmmEconomicButton } procedure TmmEconomicButton.WndProc(var Message: TMessage); begin if Message.Msg = CM_VISIBLECHANGED then begin if not (csDesigning in ComponentState) then if Visible then HandleNeeded else DestroyHandle; end; inherited WndProc(Message); end; |
Кстати, сама попытка убедиться при помощи вызова метода Handle в том, что данная кнопка действительно уничтожает свое окно, будет свидетельствовать о невнимательном прочтении этой статьи :).
Итак, в Delphi любой из классов, порожденных от TWinControl, перекрывает метод WndProc, который и инкапсулирует вышеупомянутую процедуру окна. Того же эффекта можно достигнуть при назначении нового значения свойства WindowProc. Несмотря на то, что приведенный ниже или использованный в примере код может значительно упростить разработку компонента, желательно все-таки использовать специальные обработчики событий или то, что VCL преобразует события системы в события библиотеки VCL, т.е. работать с ними на высоком уровне.
Procedure TmyObject.WndProc(var Message:TMessage) begin Case Message.msg of WM_Paint:DoSomething; WM_Char:DoSomething; else Inherited wndProc(Mmessage) end end; |
Теперь рассмотрим метод CreateWnd, который вызывается каждый раз, когда необходимо создать окно и получить его идентификатор. При этом CreateWnd вызывает метод CreateParams для установки настроек создаваемого окна, а затем метод CreateWindowHandle – для создания окна и получения его идентификатора. Очень часто наследники TWinControl переопределяют методы CreateWnd и CreateParams.
Метод CreateParams переопределяется, если требуется установить специфические настройки окна компонента, отличные от стандартных. Наиболее часто настраиваются:
Caption | Заголовок окна. Как правило, совпадает со свойством Caption или Text |
---|---|
Style | Стиль окна. Массив битовых флагов, таких как WS_DISABLED. Полный список стилей можно найти в MSDN |
ExStyle | Расширенный стиль. См. Style |
X, Y | Координаты окна |
Width, Height | Ширина и высота окна |
Метод CreateWnd переопределяется, если при создании окна нужно произвести дополнительные действия. При переопределении методов будьте осторожны. Не забывайте вызвать inherited-метод, это может избавить вас от многих трудно устранимых ошибок. Ниже приведен пример кнопки с многострочным текстом в качестве надписи. Обратите внимание на использование метода CreateParams.
unit ummMultiLineButton; interface uses SysUtils, Classes, Controls, StdCtrls, Windows; type TMultyLineStyle = (mlSingle, mlMulti); TmmMultiLineButton = class(TButton) private FMultiLineStyle: TMultyLineStyle; procedure SetMultiLineStyle(const Value: TMultyLineStyle); { Private declarations } protected { Protected declarations } procedure CreateParams(var Params: TCreateParams); override; public { Public declarations } constructor Create(AOwner:TComponent);override; published { Published declarations } property MultiLineStyle:TMultyLineStyle read FMultiLineStyle write SetMultiLineStyle; property Width default 100; end; procedure Register; implementation procedure Register; begin RegisterComponents('Our components', [TmmMultiLineButton]); end; { TmmMultiLineButton } constructor TmmMultiLineButton.Create(AOwner: TComponent); begin inherited Create(AOwner); FMultiLineStyle:=mlMulti; Width:=100; end; procedure TmmMultiLineButton.CreateParams(var Params: TCreateParams); begin inherited; if MultiLineStyle = mlMulti then Params.Style:=Params.Style or BS_MULTILINE; end; procedure TmmMultiLineButton.SetMultiLineStyle( const Value: TMultyLineStyle); begin if FMultiLineStyle <> Value then begin FMultiLineStyle := Value; case FMultiLineStyle of mlSingle: SetWindowLong(Handle,GWL_STYLE, GetWindowLong(Handle,GWL_STYLE) and not BS_MULTILINE); mlMulti: SetWindowLong(Handle,GWL_STYLE, GetWindowLong(Handle,GWL_STYLE) or BS_MULTILINE); end; Invalidate; end; end; end. |
Еще одно очень важное свойство оконных компонент – возможность быть родительскими по отношению к другим элементам управления. Как было сказано в предыдущей статье, возможностью обрабатывать сообщения графические контролы, входящие в состав VCL, целиком и полностью обязаны TWinControl.
При создании элемента управления ему нужно передать указатель на родителя, который обязательно унаследован от TWinControl. Родитель отвечает за отображение контрола, перенаправление событий невизуальным контролам.
Что особенно интересно – родитель в смысле удаления принадлежащих ему элементов управления ведет себя так же, как и владелец, то есть удаляет все дочерние control-ы. Код деструктора TWinControl доказывает это в полной мере:
destructor TWinControl.Destroy; var I: Integer; Instance: TControl; begin Destroying; if FDockSite then begin FDockSite := False; RegisterDockSite(Self, False); end; FDockManager := nil; FDockClients.Free; if Parent <> nil then RemoveFocus(True); if FHandle <> 0 then DestroyWindowHandle; I := ControlCount; while I <> 0 do begin Instance := Controls[I - 1]; Remove(Instance); Instance.Destroy; I := ControlCount; end; FBrush.Free; if FObjectInstance <> nil then Classes.FreeObjectInstance(FObjectInstance); inherited Destroy; end; |
ПРИМЕЧАНИЕ Итак, при удалении элемента управления удаляются также все контролы, для которых он является родителем. |
Для доступа ко всем компонентам, для которых данный класс является родителем, используется массив controls. Именно его использует приведенный выше пример для освобождения ресурсов элементов управления, принадлежащих этому классу.
Так как любое окно в среде Windows может иметь дочерние окна, TWinControl предоставляет возможность задавать объекту своего класса родительское окно, не принадлежащее VCL-компоненту.
Для этого существует свойство ParentWindow, которое и определяет такое родительское окно для контрола. Вполне естественно, что родитель теперь контролу не нужен, поэтому Parent должен быть равен nil. В качестве родителя теперь и выступает это «внешнее» окно.
Для создания элемента управления с родительским окном в класс TWinControl введены два конструктора, объявленных следующим образом:
constructor CreateParented(ParentWindow: HWnd); class function CreateParentedControl(ParentWindow: HWnd): TWinControl; |
Рассмотрим следующий пример:
procedure TForm1.Button1Click(Sender: TObject); var VButton: TButton; begin VButton := TButton.Create(Self); InsertComponent(VButton); with VButton do begin ParentWindow:=GetDesktopWindow; Left:=100; Top:=100; Caption:=Кнопка на рабочем столе!'; ShowWindow(Handle, SW_show); end; end; |
Этот код размещает кнопку на рабочем столе Windows. Обратите внимание на вызов метода InsertComponent, который помещает созданный объект в список компонентов формы. Это сделано для уничтожения кнопки при завершении работы приложения.
Тем не менее, сколько не искать, но свойства Canvas и метода Paint в TWinControl не найти. Каким же образом тогда происходит прорисовывание элементов управления? Ответ на этот вопрос очень прост – элементы управления, напрямую порожденные от этого класса, умеют прорисовывать себя сами. Обычно это либо стандартные элементы управления Windows, либо ActiveX компоненты.
Вместо резюме к этому разделу заметим, что в TWinControl вводится множество интересных свойств и методов, среди которых – TabStop, TabOrder, DoEnter, DoExit, KeyDown и так далее. Для получения их полного списка и назначения обратитесь к справке по Delphi.
Все, что дает нам этот класс – это полотно для рисования и метод Paint, одним словом – все то, что нужно для того, чтобы проектировать оконные элементы управления подобно графическим.
Перед началом построения собственного компонента на базе TCustomControl рассмотрим некомпонентный класс TCanvas.
Класс TCanvas предоставляет удобные методы рисования. Он используется для отрисовки визуальными компонентами.
Основные свойства класса TCanvas:
Brush: TBrush; | Кисть, определяющая цвет и особенности заполнения фона и графических примитивов (shapes). |
---|---|
ClipRect: TRect | Прямоугольник, определяющий границы изображения. Изображение за пределами данного прямоугольника не выводится на экран. |
CopyMode: TCopyMode; | Способ копирования графических объектов. Используеться при использовании метода CopyRect, который копирует фрагмент рисунка из указанного полотна. |
Font: TFont; | Шрифт. |
Handle: HDC | Контекст устройства. Используется для прямого доступа к функциям рисования GDI Windows. |
LockCount: Integer; | Счетчик блокирования. Используется для организации рисования из нескольких потоков. |
Pen: TPen | Карандаш рисования. Определяет параметры рисования линий. |
PenPos: TPoint | Теущая позиция карандаша. |
Pixels[X, Y: Integer]: TColor | Массив точек полотна. |
TextFlags: LongInt; | Флаги режимов вывода текста. |
Свойство CopyMode может принимать следующие значения:
cmBlackness | Заполняет копируемую область черным. |
---|---|
cmDstInvert | Инвертирует изображение в копируемой области, игнорируя изображения источника копирования. |
cmMergeCopy | Накладывает копируемое изображение на Canvas, используя операцию AND. |
cmMergePaint | Накладывает копируемое изображение, используя OR. |
cmNotSrcCopy | Копирует инвертированное изображение на Canvas. |
cmNotSrcErase | Накладывает копируемое изображение на Canvas, затем инвертирует полученный результат. |
cmPatCopy | Копирует исходный шаблон на Canvas. |
cmPatInvert | То же, что cmPatCopy, результат инвертируется. |
cmPatPaint | Комбинирует изображение с шаблоном с использованием OR. Затем комбинирует результат c Canvas, также используя OR. |
cmSrcAnd | Комбинирует изображение на Canvas и копируемое изображение с использованием AND |
cmSrcCopy | Копирует изображение на Canvas. |
cmSrcErase | Инвертирует Canvas и комбинирует его с копируемым изображением с использованием AND. |
cmSrcInvert | Комбинирование изображения и Canvas по XOR. |
cmSrcPaint | -//- с использованием OR. |
cmWhiteness | Заполняет область Canvas белым цветом. |
Наиболее часто используемые методы TCanvas:
Arc | Рисование дуги |
---|---|
Chord | Рисование хорды |
CopyRect | Копирование части изображения из другого Canvas |
MoveTo | Перемещение пера в заданную точку |
LineTo | Рисование линии |
Rectangle | Рисование прямоугольника с заполнением его внутренней области |
FrameRect | Рисование прямоугольника без заполнения |
DrawFocusRect | Рисование прямоугольника фокуса Windows |
TextOut | Отображение текста в указанных координатах |
TextRect | Отображение текста с отсеканием заданным прямоугольником |
Pie | Рисование части круга |
Plygon | Рисование многоугольника с заданными вершинами |
Более подробное описание методов и свойств TCanvas можно найти во встроенной документации Delphi.
Все вышесказанное, в принципе, практически полностью иллюстрирует процесс создания оконного компонента, однако для более ясного понимания приведем два примера.
Итак, в качестве первого очень несложного примера, рассмотрим реализацию TCheckBox. Его внешний вид похож на обычную метку, только с пиктограммой слева. Если свойство установить свойство Checked установить в True, то пиктограмма будет отображаться цветом, заданным свойством OnColor, иначе – OffColor (см. рисунок 1).
Рисунок 1.
Кроме этого, вводится событие OnChecked.
Код компонента выглядит так:
unit mmLeg; interface uses SysUtils, Classes, Controls, Graphics, Math, Windows; type TmmLed = class(TCustomControl) private FOnColor, FOffColor, FDisabledColor : TColor; FChecked: Boolean; FOnChecked: TNotifyEvent; procedure SetChecked(Value: Boolean); procedure SetOnColor(Value: TColor); procedure SetOffColor(Value: TColor); procedure SetDisabledColor(const Value: TColor); protected procedure Paint; override; procedure DoEnter; override; procedure DoExit; override; procedure KeyDown(var Key: Word; Shift: TShiftState); override; procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; procedure DoChecked; dynamic; public constructor Create(AOwner: TComponent); override; published property Checked: Boolean read FChecked write SetChecked; property Enabled; property OnColor: TColor read FOnColor write SetOnColor default clLime; property OffColor: TColor read FOffColor write SetOffColor default clInactiveCaption; property DisabledColor: TColor read FDisabledColor write SetDisabledColor default clGrayText; property ParentShowHint; property PopupMenu; property ShowHint; property Hint; property Visible; property TabStop; property TabOrder; property Height default 14; property Width default 40; property Caption; property OnClick; property OnEnter; property OnExit; property OnChecked: TNotifyEvent read FOnChecked write FOnChecked; end; procedure Register; implementation {$R mmLed.dcr} procedure Register; begin RegisterComponents('Our components', [TmmLed]); end; constructor TmmLed.Create(AOwner: TComponent); begin inherited Create(AOwner); ControlStyle := ControlStyle + [csOpaque]; FOnColor := clLime; FOffColor := clInactiveCaption; FDisabledColor:=clGrayText; Width := 40; Height := 14; TabStop:=true; end; procedure TmmLed.Paint; var MajorExtent, SpotRadius, midXY: Integer; LedColor: TColor; begin MajorExtent := Min(Width, Height); midXY := MajorExtent div 2; Canvas.Font:=Font; if Enabled then begin if FChecked then LedColor := FOnColor else LedColor := FOffColor; end else LedColor:=FDisabledColor; if Visible or (csDesigning in ComponentState) then with Canvas do begin Brush.Color:=clBtnFace; Brush.Style:=bsSolid; FillRect(ClientRect); Brush.Color := LedColor; Pen.Color := clBtnShadow; Ellipse(0, 0, MajorExtent, MajorExtent); SpotRadius := Max(1, MajorExtent div 4); Pen.Color := clBtnHighlight; Brush.Color := clBtnHighlight; Ellipse(midXY - SpotRadius, midXY - SpotRadius, midXY, midXY); Brush.Style:=bsClear; if Enabled then Font.Color:=clBtnText else Font.Color:=clGrayText; TextRect(Clientrect, MajorExtent, midXY - (TextHeight(Caption) div 2), Caption); Brush.Color:=clBtnFace; if Focused then DrawFocusRect(ClientRect); // визуализация получения фокуса end; end; procedure TmmLed.SetDisabledColor(const Value: TColor); begin if Value <> FDisabledColor then begin FDisabledColor := Value; Refresh; end; end; procedure TmmLed.SetChecked(Value: Boolean); begin if Value <> FChecked then begin FChecked := Value; DoChecked; Refresh; end; end; procedure TmmLed.SetOnColor(Value: TColor); begin if Value <> FOnColor then begin FOnColor := Value; Refresh; end; end; procedure TmmLed.SetOffColor; begin if Value <> FOffColor then begin FOffColor := Value; Refresh; end; end; procedure TmmLed.DoEnter; begin inherited; Refresh; end; procedure TmmLed.DoExit; begin inherited; Refresh; end; procedure TmmLed.KeyDown(var Key: Word; Shift: TShiftState); begin inherited KeyDown(Key, Shift); if Key = VK_RETURN then Checked:=not Checked; end; procedure TmmLed.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); function LedClicked:boolean; begin Result:=((X <= Min(Width, Height)) and (Y <= Min(Width, Height))); end; begin inherited MouseDown(Button, Shift, X, Y); if Focused or LedClicked then Checked:=not Checked; end; procedure TmmLed.DoChecked; begin if Assigned(FOnChecked) then FOnChecked(Self); end; end. |
Последний пример этой статьи – реально используемый компонент, который представляет собой панель с CheckBox на верхней границе. Если Checked=False – тогда все контролы, которые лежат на панели, переходят в отключенное состояние (Enabled=False).
unit mmCheckPanel; interface uses Windows, ExtCtrls, Classes, Graphics, Controls, StdCtrls; type TmmCheckPanel = class(TPanel) private FCheckBox: TCheckBox; FCheckCaption: string; FOnCheck: TNotifyEvent; function GetChecked: Boolean; procedure SetChecked(const Value: Boolean); procedure SetCheckCaption(const Value: string); procedure SetOnCheck(const Value: TNotifyEvent); function GetCheckCaption: string; protected procedure Paint; override; procedure DoChecked(Sender: TObject); virtual; procedure DisableControls(const Disable: Boolean); public constructor Create(AOwner: TComponent); override; published property Checked: Boolean read GetChecked write SetChecked; property Caption: string read GetCheckCaption write SetCheckCaption; property OnCheck: TNotifyEvent read FOnCheck write SetOnCheck; end; procedure Register; implementation procedure Register; begin RegisterComponents('Our components', [TmmCheckPanel]); end; { TmmCheckPanel } constructor TmmCheckPanel.Create(AOwner: TComponent); begin inherited; inherited Caption := ' '; FCheckCaption := 'Включить'; FCheckBox := TCheckBox.Create(self); with FCheckBox do begin Checked := True; OnClick := DoChecked; Left := 5; Parent := Self; Caption := FCheckCaption; end; end; procedure TmmCheckPanel.DisableControls(const Disable: Boolean); var i: Integer; begin for i := 0 to ControlCount - 1 do // собственно код отключения контролов if Controls[i] <> FCheckBox then Controls[i].Enabled := Disable; end; procedure TmmCheckPanel.DoChecked(Sender: TObject); begin DisableControls(FCheckBox.Checked); if Assigned(FOnCheck) then FOnCheck(Self); end; function TmmCheckPanel.GetCheckCaption: string; begin Result := FCheckCaption; end; function TmmCheckPanel.GetChecked: Boolean; begin Result := FCheckBox.checked; end; procedure TmmCheckPanel.Paint; const Alignments: array[TAlignment] of Longint = (DT_LEFT, DT_RIGHT, DT_CENTER); var Rect: TRect; TopColor, BottomColor: TColor; FontHeight: Integer; procedure AdjustColors(Bevel: TPanelBevel); begin TopColor := clBtnHighlight; if Bevel = bvLowered then TopColor := clBtnShadow; BottomColor := clBtnShadow; if Bevel = bvLowered then BottomColor := clBtnHighlight; end; begin Rect := GetClientRect; Rect.Top := FCheckBox.Height div 2; if BevelOuter <> bvNone then begin AdjustColors(BevelOuter); Frame3D(Canvas, Rect, TopColor, BottomColor, BevelWidth); end; Frame3D(Canvas, Rect, Color, Color, BorderWidth); if BevelInner <> bvNone then begin AdjustColors(BevelInner); Frame3D(Canvas, Rect, TopColor, BottomColor, BevelWidth); end; with Canvas do begin Brush.Color := Color; FillRect(Rect); Brush.Style := bsClear; Font := Self.Font; FontHeight := TextHeight('W'); end; FCheckBox.Caption := FCheckCaption; FCheckBox.Width := GetSystemMetrics(SM_CXVSCROLL) + Canvas.TextWidth(FCheckCaption) + 5; end; procedure TmmCheckPanel.SetCheckCaption(const Value: string); begin FCheckCaption := Value; InValidate; end; procedure TmmCheckPanel.SetChecked(const Value: Boolean); begin FCheckBox.Checked := Value; DisableControls(FCheckBox.Checked); InValidate; end; procedure TmmCheckPanel.SetOnCheck(const Value: TNotifyEvent); begin FOnCheck := Value; end; end. |
Логика работы компонента очень проста. При создании компонента создается TCheckBox, которому динамически назначается обработчик события OnClick, запускающий процедуру включения/выключения контролов.
Вот, собственно, и все, что мы хотели рассказать о разработке оконных компонентов. Мы абсолютно не претендуем на полноту изложенного материала, однако очень надеемся, что данная статья в очередной раз подтвердит идею, положенную в цикл статей о разработке собственных компонентов Delphi, – компоненты – это просто!
Сообщений 4 Оценка 160 Оценить |