Сообщений 4    Оценка 160        Оценить  
Система Orphus

Класс TWinControl и его наследники

Авторы: Евгений Веселов
Михаил Голованов

Источник: RSDN Magazine #3-2003
Опубликовано: 12.10.2003
Исправлено: 13.03.2005
Версия текста: 1.0
Вступление
TWinControl и с чем его едят…
TCustomControl как строительная площадка для собственных оконных элементов управления.

Вступление

Данная статья – продолжение цикла о разработке собственных компонентов в среде Delphi. Как мы и обещали, в это раз речь пойдет об элементах управления (далее просто контролах), способных получать фокус ввода, а также имеющих свое собственное окно.

Собственное окно (более грамотно сказать – дескриптор окна) есть только у оконных контролов. Чем же отличается проектирование оконных компонентов от проектирования графических, рассмотренного в предыдущей статье? В контексте VCL это различие заключается в том, что графические контролы являются потомками TGraphicControl, а оконные – TWinControl. Благодаря стараниям коллектива фирмы Борланд разработка оконных контролов в общем случае очень похожа на разработку графических (и мы убедимся в этом немного ниже).

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.

TCustomControl как строительная площадка для собственных оконных элементов управления.

Все, что дает нам этот класс – это полотно для рисования и метод 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, – компоненты – это просто!


Эта статья опубликована в журнале RSDN Magazine #3-2003. Информацию о журнале можно найти здесь
    Сообщений 4    Оценка 160        Оценить