Класс TComponent.

Создание невизуальных компонентов и диалогов. Создание графических компонентов.

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

Источник: RSDN Magazine #2-2003
Опубликовано: 26.07.2003
Версия текста: 1.0.1
TComponent
Компонент регулятор громкости звука.
Диалог поиска значения в наборе данных
TControl
TGraphicControl
Графические компоненты
Компонент X
Трехмерная метка
Заключение

TComponent

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

Одним из наиболее важных и часто используемых свойств TComponent является ComponentState. Оно отображает состояния компонента в контексте взаимодействия с интегрированной средой разработки и состояния потокового ввода/вывода свойств. В таблице 1 приведены возможные значения этого свойства:

csAncestor Компонент был размещен на форме, являющейся предком текущей формы. Устанавливается только если флаг csDesigning установлен.
csDesigning Компонент находится в режиме проектирования (design time). Данный флаг установлен при проектировании формы в IDE Delphi
csDestroying Компонент сейчас будет разрушен
csFixups Компонент связан с компонентом на другой форме, которая еще не загружена. Флаг сбрасывается после загрузки всех необходимых форм
csFreeNotification Один или несколько компонентов должны быть извещены при разрушении данного объекта. Флаг устанавливается вызовом метода FreeNotification.
csInline Компонент является контейнером и может редактироваться во время проектирования и внедрен в форму. Данный флаг используется для идентификации фреймов во время загрузки и сохранения формы.
csLoading Компонент загружается из файла формы
csReading Компонент считывает значения своих свойств из потока
csWriting Компонент записывает значения своих свойств в поток
csUpdating Компонент требует обновления, для актуализации изменений, произведенных с ним в родительском классе. Устанавливается только при установке флага csAncestor
csDesignInstance Данный компонент является корневым компонентом дизайнера форм. Например, фрейм в режиме проектирования. Однако, фрейм уже размещенный на форме, выступает как обычный компонент и не является корневым объектом.

Распространенной практикой использования свойства ComponentState является определение, манипулируют ли компонентом во время выполнения программы или во время ее проектирования. Например, компонент TImage использует это свойство для того, чтобы нарисовать пунктирную границу вокруг элемента управления на этапе проектирования. Следующий фрагмент кода иллюстрирует, как это происходит:

      if csDesigning in ComponentState thenwithinherited Canvas dobegin
    Pen.Style := psDash;
    Brush.Style := bsClear;
    Rectangle(0, 0, Width, Height);
  end;
…… {Продолжение рисования }

Класс TComponent развивает концепцию принадлежности (ownership), которая заложена в TPersistent и распространяется на все компоненты. Суть понятия принадлежности заключается в том, что определенный компонент отвечает за уничтожение других компонентов, которыми он владеет. Это освобождает разработчика от многих рутинных действий. Например, форма при уничтожении не «забывает» уничтожить все компоненты, которые на ней находятся. В свою очередь, заканчивая работу, приложение разрушает работы все свои формы.

Данная концепция реализуется с помощью двух свойств:

  1. Owner, который ссылается на другой компонент, как на своего владельца. В свою очередь любой компонент может сам выступать в роли владельца.
  2. Components – массив, содержащий ссылки на компоненты, которыми владеет данный класс.

Конструктор компонента принимает параметр Owner, указывающий на владельца. Если передаваемый в конструкторе владелец существует, то ссылка на созданный конструктором компонент сохраняется в массиве Components владельца. Этот механизм позволяет осуществлять разрушение компонентов, принадлежащих владельцу. Код для автоматического разрушения размещен в деструкторе TComponent, и, следовательно, используется всеми наследниками данного класса.

Компонент-владелец может быть не задан. В этом случае реализация корректного уничтожения компонента ложится на разработчика.

Еще одним довольно полезным методом является Notification (уведомление). Этот метод вызывается каждый раз, когда компонент вставляется или удаляется из списка владельца. Метод Notification перекрывается в потомках TComponent, чтобы обеспечить действительность ссылок на внешние компоненты.

Обеспечение метода Notification особенно актуально на этапе проектирования. В качестве примера приведем код метода Notification компонента метки. Данный код должен обеспечить корректировку свойства FocusControl метки. Свойство FocusControl указывает на оконный элемент управления, с которым ассоциирована метка.

      procedure TCustomLabel.Notification(AComponent: TComponent;
  Operation: TOperation);
begininherited Notification(AComponent, Operation);
  if (Operation = opRemove) and (AComponent = FFocusControl) then
    FFocusControl := nil;
end;

Данный код при вызове, обнаружив, что удаляется внешний компонент, на который ссылается свойство FocusControl, обнуляет указатель на него. Если этого не делать, то после удаления внешнего компонента свойство FocusControl будет содержать указатель на несуществующий компонент.

TComponent предоставляет возможность получения сообщений об удалении любого компонента. Для указания того, что ваш компонент должен получать извещение об удалении другого компонента, в TComponent существует метод FreeNotification. Единственный параметр метода AComponent:TComponent указывает на тот компонент, извещение об удалении которого вы хотите обработать. Другой метод – RemoveFreeNotification(AComponent: TComponent) – позволяет отписаться от получения извещений об удалении компонента, переданного в качестве параметра метода.

Метод Loaded – инструмент для корректной инициализации компонента после окончания чтения значений свойств из потока. Этот виртуальный метод вызывается сразу после того, как все значения свойств загружены в компонент. Вызов Loaded происходит раньше, чем форма отобразится. Это позволяет избежать ненужной перерисовки компонента. Особенно часто этот метод используется для компонентов манипулирования данными. В самом деле, до тех пор, пока не будет инициализированы все свойства, например DBGrid, смысла в отображении данных на нем абсолютно нет.

Удачным примером использования Loaded является метод пользовательского класса TCustomRadioGroup (элемент группы переключателей):

      procedure TCustomRadioGroup.Loaded;
begininherited Loaded;
  ArrangeButtons;
end;

В данном методе сразу после загрузки группы переключателей производится их выравнивание.

Теперь, рассмотрев свойства и методы класса TComponent, попробуем построить на его базе несколько реальных компонентов. Это так называемые невизуальные компоненты, назначение которых – инкапсулировать некоторый часто используемый код. Классическим примером такого компонента является всем известный TTimer. Псевдовизуальные компоненты (в их число входят диалоги, компоненты для настройки хинтов и т.д.) также являются подмножеством невизуальных компонентов и, следовательно, также являются прямыми потомками TComponent.

На этапе проектирования приложения они отображаются в виде пиктограммы на форме, а на этапе выполнения программы вообще не видны пользователю. Главная причина создания невизуальных компонентов – это возможность удобной визуальной настройки их свойств в Object Inspector. Внутри себя данные компоненты могут инкапсулировать практически любые участки кода. Основные типы задач, выполняемых невизуальными компонентами это:

С первого взгляда компоненты диалогов могут показаться визуальными, однако на самом деле они инкапсулируют формы диалогов и представляют удобный интерфейс настройки свойств этих форм.

В пределах раздела построения невизуальных компонентов мы рассмотрим два примера: компонент-регулятор уровня громкости звука и компонент-диалог поиска записи в наборе данных.

Компонент регулятор громкости звука.

Этот компонент предназначен для получения и установки уровня громкости звука CD-проигрывателя, MIDI и Wave. Он демонстрирует использование свойств нестандартных типов, которые потоковая система не «умеет» правильно сохранять.

Публичный интерфейс компонента содержит три свойства типа Byte: CDVolume, MIDIVolume и WaveVolume, соответствующие уровню звука различных источников. Код компонента приведен ниже.

        unit mgVolumes;

interfaceuses
  Windows, Messages, Classes, ExtCtrls, ComCtrls, MMSystem;

const
  CDVolume       = 0;
  WaveVolume     = 1;
  MidiVolume     = 2;

type
  TVolumeRec = recordcase Cardinal of
    0: (LongVolume: Longint);
    1: (LeftVolume,
        RightVolume : Word);
  end;

  TmgVolumeControl = class(TComponent)
  private
    FVol:TVolumeRec;
    FDevices     : array[0..2] of Integer;
    function       GetVolume(AIndex: Integer): Word;
    procedure      SetVolume(AIndex: Integer; aVolume: Word);
    procedure      InitVolume;
  protected{ Protected declarations }public{ Public declarations }constructor    Create(AOwner: TComponent); override;
  published{ Published declarations }property CDVolume: Word index 0 read GetVolume write SetVolume stored False;
    property WaveVolume: Word index 1 read GetVolume write SetVolume stored False;
    property MidiVolume: Word index 2 read GetVolume write SetVolume stored False;
  end;

procedure Register;

implementationprocedure Register;
begin
  RegisterComponents('Our components', [TmgVolumeControl]);
end;

function       TmgVolumeControl.GetVolume(AIndex: Integer): Word;
begin
  FVol.LongVolume := 0;
  if FDevices[AIndex] <> -1 thencase AIndex of
  0: auxGetVolume(FDevices[AIndex], @FVol.LongVolume);
  1: waveOutGetVolume(FDevices[AIndex], @FVol.LongVolume);
  2: midiOutGetVolume(FDevices[AIndex], @FVol.LongVolume);
  end;
  Result := FVol.LeftVolume;
end;

procedure      TmgVolumeControl.SetVolume(aIndex: Integer; aVolume: Word);
beginif FDevices[AIndex] <> -1 thenbegin
    FVol.LeftVolume := aVolume;
    FVol.RightVolume := FVol.LeftVolume;
    case AIndex of
        0: auxSetVolume(FDevices[AIndex], FVol.LongVolume);
        1: waveOutSetVolume(FDevices[AIndex],FVol.LongVolume);
        2: midiOutSetVolume(FDevices[AIndex], FVol.LongVolume);
    end;
  end;
end;


constructor    TmgVolumeControl.Create(AOwner: TComponent);
begininherited Create(AOwner);
  InitVolume;
end;

procedure      TmgVolumeControl.InitVolume;
var AuxCaps     : TAuxCaps;
  WaveOutCaps : TWaveOutCaps;
  MidiOutCaps : TMidiOutCaps;
  I         : Integer;
begin
  FDevices[0] := -1;
  for I := 0 to auxGetNumDevs - 1 dobegin
    auxGetDevCaps(I, @AuxCaps, SizeOf(AuxCaps));
    if (AuxCaps.dwSupport and AUXCAPS_VOLUME) <> 0 thenbegin
      FDevices[0] := I;
      break;
    end;
  end;
  FDevices[1] := -1;
  for I := 0 to waveOutGetNumDevs - 1 dobegin
    waveOutGetDevCaps(I, @WaveOutCaps, SizeOf(WaveOutCaps));
    if (WaveOutCaps.dwSupport and WAVECAPS_VOLUME) <> 0 thenbegin
      FDevices[1] := I;
      break;
    end;
  end;
  FDevices[2] := -1;
  for I := 0 to midiOutGetNumDevs - 1 dobegin
    MidiOutGetDevCaps(I, @MidiOutCaps, SizeOf(MidiOutCaps));
    if (MidiOutCaps.dwSupport and MIDICAPS_VOLUME) <> 0 thenbegin
      FDevices[2] := I;
      break;
    end;
  end;
end;

end.

Объявления функций API для работы с мультимедийными устройствами содержатся в модуле VCL MMSystem. Поскольку мы используем Windows API, данный компонент будет работать только под Windows, поддержка мультиплатформенной версии компонента с использованием CLX требует серьезной переработки кода компонента и углубления в дебри работы со звуковыми устройствами. К сожалению, работа с мультимедиа-устройствами в Windows и Linux значительно различается, и обсуждение этих различий выходит за рамки этой статьи. Поэтому будет рассмотрен только VCL-вариант компонента.

Windows хранит уровень звука источника в виде значения LongInt. Значения громкостей для правого и левого канала хранятся в старшем и младшем словах (Word) соответственно. Для удобства хранения полученных уровней громкости используется вариантная запись типа TVolumeRec, объявленная как:

TVolumeRec = recordcase Integer of
    0: (LongVolume: Longint);
    1: (LeftVolume,
        RightVolume : Word);
  end;

Свойства для доступа к значениям громкостей звука объявлены как

        property CDVolume: Word index 0 read GetVolume write SetVolume stored False;
property WaveVolume: Word index 1 read GetVolume write SetVolume stored False;
property MidiVolume: Word index 2 read GetVolume write SetVolume stored False;

Первое, на что стоит обратить внимание – модификатор stored False в объявлении данных свойств. Значение модификатора false говорит о том, что значения данных свойств не будут сохранены в dfm-файл формы. Сделано это потому, что значение громкости может быть изменено не только нашим компонентом, но и стандартными средствами Windows (регулятор громкости) или другими программами, работающими со звуком (например, WinAmp). Установив в компоненте значения громкости, мы не можем быть уверены, что оно не изменится через некоторое время. При этом наш компонент совсем не обязательно будет оповещен об этих изменениях. По этой же причине хранить значения свойств CDVolume, WaveVolume и MIDIVolume во внутренних переменных компонента бессмысленно. При чтении значений громкости необходимо каждый раз запрашивать текущие значения громкости. Хранение громкости устройств в виде LongInt делает удобным объявление свойств отдельных громкостей в виде индексированного свойства. Получив LongInt-значение громкости, мы по индексу свойства можем определить значение, характеризующее уровень громкости конкретного устройства.

Начнем анализ компонента с его конструктора. Сразу после вызова конструктора предка вызывается метод InitVolume. Метод InitVolume пытается найти хотя бы одно устройство, поддерживающее регулирование звука в каждом классе CD, Midi, Wave. В данной реализации управление звуком осуществляется для первого подходящего устройства. В качестве упражнения можно после прочтения этой статьи написать версию компонента, поддерживающего регулирование звука для нескольких устройств одного вида. Код метода InitVolume специально не оптимизирован, дабы не вносить излишних сложностей. Он содержит три практически идентичных блока для инициализации каждого вида устройств-источников звука, поэтому достаточно будет разобрать только инициализацию Wave-устройств.

1   FDevices[1] := -1;
2   for I := 0 to waveOutGetNumDevs - 1 do
3   begin
4     waveOutGetDevCaps(I, @WaveOutCaps, SizeOf(WaveOutCaps));
5     if (WaveOutCaps.dwSupport and WAVECAPS_VOLUME) <> 0 then
6     begin
7       FDevices[1] := I;
8       break;
9     end;
10  end;

Массив FDevices хранит номера устройств каждого класса, громкость которых мы регулируем. Элемент 0 – CD устройства, 1 – Wave и 2 – Midi. В первой строке кода мы устанавливаем значение номера Wave устройства равным -1, что обозначает, что устройство не найдено. Количество Wave-устройств в системе определяется с помощью вызова функции API waveOutGetNumDevs. Мы опрашиваем каждое Wave-устройство чтобы узнать, поддерживает ли оно регулирование громкости (цикл в строках 2-10). Выяснить это можно с помощью анализа флага WAVECAPS_VOLUME в возвращаемом значении функции API waveOutGetDevCaps. Как только мы находим первое из устройств, поддерживающих управление громкостью, мы запоминаем его номер и прерываем цикл.

Следующим интересующим нас методом является метод получения громкости устройства GetVolume. В качестве параметра он получает индекс свойства, значение которого нужно вернуть. Свойство CDVolume объявлено с индексом 0, WaveVolume – 1, а MidiVolume – 2. Код метода приведен ниже.

        function       TmgVolumeControl.GetVolume(AIndex: Integer): Word;
begin
1  FVol.LongVolume := 0;
2  if FDevices[AIndex] <> -1 then
3  case AIndex of
4  0: auxGetVolume(FDevices[AIndex], @FVol.LongVolume);
5  1: waveOutGetVolume(FDevices[AIndex], @FVol.LongVolume);
6  2: midiOutGetVolume(FDevices[AIndex], @FVol.LongVolume);
7  end;
8  Result := FVol.LeftVolume;
end;

В строке 1 мы обнуляем значение громкости в структуре FVol, используемой для получения значения громкости. Далее, в зависимости от переданного индекса, определяющего вид устройства, производится вызов соответствующей функции API, которая и возвращает значение громкости.

Практически аналогично работает и метод установки значения громкости. Как и следует ожидать, он получает в качестве параметров индекс свойства и значение громкости.

        procedure TmgVolumeControl.SetVolume(aIndex: Integer; aVolume: Word);
beginif FDevices[AIndex] <> -1 thenbegin
    FVol.LeftVolume := aVolume;
    FVol.RightVolume := FVol.LeftVolume;
    case AIndex of
      0: auxSetVolume(FDevices[AIndex], FVol.LongVolume);
      1: waveOutSetVolume(FDevices[AIndex],FVol.LongVolume);
      2: midiOutSetVolume(FDevices[AIndex], FVol.LongVolume);
    end;
  end;
end;

Далее происходит заполнение структуры FVol, используемой в вызовах функций API установки громкости. В нашем компоненте задаются одинаковые значения громкости для правого и левого каналов. Далее, в зависимости от переданного индекса, определяющего тип устройства, производится вызов функции API установки значения громкости для конкретного типа устройства.

Диалог поиска значения в наборе данных

Данный диалог является оберткой вокруг формы, представленной на рисунке ниже:


Рисунок 1.

На его примере будет продемонстрировано создание диалогов-оберток форм Delphi. Технология построения таких компонентов состоит из двух этапов

Первый этап представляет собой обычное проектирование формы. Не будем заострять на нем внимание. Ниже приведен код полученной формы:

        unit uDialogForm;

interfaceuses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, Grids, DBGrids, StdCtrls, DB, Buttons;

type
  TDialogForm = class(TForm)
    Panel1: TPanel;
    DBGrid1: TDBGrid;
    dsLookup: TDataSource;
    edValue: TEdit;
    Label1: TLabel;
    Panel2: TPanel;
    BitBtn1: TBitBtn;
    BitBtn2: TBitBtn;
    SearchBtn: TBitBtn;
    procedure SearchBtnClick(Sender: TObject);
  private{ Private declarations }public{ Public declarations }
    SearchField: String;
  end;

var
  DialogForm: TDialogForm;

implementation{$R *.dfm}procedure TDialogForm.SearchBtnClick(Sender: TObject);
begin
  dsLookup.DataSet.Locate(SearchField, edValue.Text, 
    [loCaseInsensitive, loPartialKey]);
end;

end.

Очевидно, ничего сверхъестественного в нем нет.

Второй этап начинается с выделения свойств формы, которые должны настраиваться в компоненте-диалоге. В нашем случае выделены следующие свойства:

Чтобы сделать форму гибкой по отношению к набору данных, диалог должен содержать еще два свойства:

Все эти свойства объявлены в компоненте-диалоге, код которого приведен ниже.

        unit mgDBFindDialog;

interfaceuses
  SysUtils, Classes, Forms, Controls, DB, Graphics;

type
  TmgControl = class (TControl)
  publicproperty Font;
  end;

  TmgDBFindDialog = class(TComponent)
  private
    FBorderStyle: TFormBorderStyle;
    FCaption: TCaption;
    FDataset: TDataset;
    FFont: TFont;
    FHeight: integer;
    FWidth: integer;
    FSearchField: String;
    procedure SetFont(const Value: TFont);
    procedure SetDataset(const Value: TDataset);
    { Private declarations }protected{ Protected declarations }procedure Notification(AComponent: TComponent; 
      Operation: TOperation); override;
  public{ Public declarations }constructor Create(AOwner:TComponent); override;
    destructor Destroy; override;
    function Execute: boolean;
  published{ Published declarations }property BorderStyle: TFormBorderStyle
      read FBorderStyle write FBorderStyle default bsDialog;
    property Caption: TCaption read FCaption write FCaption;
    property Dataset: TDataset read FDataset write SetDataset;
    property Font: TFont read FFont write SetFont;
    property Height: integer read FHeight write FHeight default 300;
    property SearchField: String read FSearchField write FSearchField;
    property Width: integer read FWidth write FWidth default 450;

  end;

procedure Register;

implementationuses uDialogForm;

procedure Register;
begin
  RegisterComponents('Our components', [TmgDBFindDialog]);
end;

{ TmgDBFindDialog }constructor TmgDBFindDialog.Create(AOwner: TComponent);
begininherited Create(AOwner);
  FHeight:=300;
  FWidth:=450;
  FBorderStyle:=bsDialog;
  FCaption:='Найти';
  FFont:=TFont.Create;
  if (AOwner is TControl) then
    FFont.Assign(TmgControl(AOwner).Font);
end;

destructor TmgDBFindDialog.Destroy;
begin
  FFont.Free;
  inherited;
end;

function TmgDBFindDialog.Execute: boolean;
begin
  Result:=false;
  with TDialogForm.Create(Application) dotry
    BorderStyle:=FBorderStyle;
    Height:=FHeight;
    Width:=FWidth;
    Font:=FFont;
    Caption:=FCaption;
    dsLookup.DataSet:=FDataset;
    SearchField:=FSearchField;
    Result:= ShowModal = mrOk;
  finally
    Free;
  end;
end;


procedure TmgDBFindDialog.Notification(AComponent: TComponent;
  Operation: TOperation);
begininherited Notification(AComponent, Operation);
  if (Operation = opRemove) and (AComponent = Dataset) then
    FDataSet:=nil;
end;

procedure TmgDBFindDialog.SetDataset(const Value: TDataset);
begin
  FDataset := Value;
  if FDataset <> nilthen FreeNotification(FDataSet);
end;

procedure TmgDBFindDialog.SetFont(const Value: TFont);
begin
  FFont.Assign(Value);
end;

end.

Помимо свойств, компонент диалога должен иметь некий метод вызова формы диалога. По соглашению данный метод имеет имя Execute и возвращаемое значение типа Boolean, говорящее об успешном использовании пользователем функциональности диалога или отмене вызова диалога.

Конструктор компонента не представлял бы собой ничего сложного, если бы не пара строчек:

        if (AOwner is TControl) then
  FFont.Assign(TmgControl(AOwner).Font);

Данные строки всего лишь устанавливают шрифт по умолчанию, совпадающий со шрифтом владельца компонента. Обратите внимание, что после проверки того, что владелец является классом TControl (или реально наследуется от него), осуществляется копирование свойств Font владельца в свойство Font нашего компонента вызовом FFont.Assign. Далее возникает очень интересный вопрос: почему мы приводим тип владельца к какому-то TmgControl. Ответ так же прост: потому, что свойство Font объявлено в классе TControl как protected. Так как код нашего компонента расположен в другом модуле, нежели декларация TControl, то получить доступ к свойству Font обычными методами мы не можем. Однако существует один замечательный прием, позволяющий сделать это.

Назовем этот прием созданием Dummy-класса. Для получения доступа к protected-свойствам класса, объявленного в другом модуле необходимо в модуле объявить его наследника и переопределить нужное свойство в разделе public. В нашем случае:

TmgControl = class (TControl)
publicproperty Font;
end;

Так как при увеличении видимости свойства класса структура «внутренности» его не меняются, а лишь на свойство выставляется флаг, говорящий, что оно находится в секции public, то приведение базового типа к типу объявленного нами класса не вызывает никаких ошибок. Именно этим фактом мы и воспользовались, вызвав TmgControl(AOwner).Font для доступа к свойству Font класса TControl.

Такой способ обращения к защищенным методам класса требует от разработчика большой осмотрительности и ясного понимания, почему данный метод был помещен в секцию protected.

Еще один метод, требующий пояснений – метод Notification, приведенный ниже:

        procedure TmgDBFindDialog.Notification(AComponent: TComponent;
  Operation: TOperation);
begininherited Notification(AComponent, Operation);
  if (Operation = opRemove) and (AComponent = Dataset) then
    FDataSet:=nil;
end;

Как уже говорилось, он вызывается при удалении или вставке внешнего компонента. Обычной реакцией на удаление компонента, который связан с нашим, является установка в nil свойства нашего компонента, ссылающегося на удаляемый компонент. У нашего компонента есть свойство-ссылка DataSet. Рассылка извещений компонентам, владельцем которых является наш компонент, реализована в классе TComponent. Однако извещение не будет получено в том случае, если DataSet и наш компонент не имеют какого-либо общего владельца! Явным образом подписаться на рассылку извещения об удалении компонента можно, вызвав FreeNotification. Именно это и делается при установке значения свойства DataSet в методе SetDataSet.

И, наконец, рассмотрим код метода Execute. Он всего лишь создает экземпляр формы и модально показывает его пользователю. После закрытия формы анализируется модальный результат формы. Если была нажата кнопку ОК (то есть результат равен mrOk), то Execute возвратит значение true. В противном случае возвращается false, означающее, что пользователь отказался использовать диалог.

В случае создания обертки вокруг стандартных диалогов Windows, расположенных в COMMDLG.DLL, в качестве базового класса для компонента удобнее использовать класс TCommonDialog, переопределив его метод Execute. Примерами таких диалогов являются диалоги с закладки Dialogs. В CLX аналогом TCommonDialog является класс TCustomDialog.

TControl

После изучения назначения класса TComponet наступило время перейти к рассмотрению процесса создания пользовательских элементов управления.

Начнем с класса TControl, который является прямым потомком класса TComponent и определяет большую часть свойств, методов и событий, используемых визуальными компонентами в Delphi. Несмотря на это, сам класс не предоставляет никаких механизмов для реального функционирования элементов управления в целом. Его назначение – обобщение сущности control-ов.

Основные свойства и события TControl приведены в таблице 2.

Свойства
Свойства позиционирования Top, Left, Width, Height, Align, Anchors
Свойства клиентской области ClientHeight, ClientOrigin, ClientRect, ClientWidth
Свойства внешнего вида Visible, Enabled, Font, Color
Строковые свойства Caption, Text, Hint
События
События левой кнопки мыши OnClick, OnDblClick,
Общие события мыши OnMouseDown, OnMouseMove, OnMouseUp
Поддержка перетаскивания OnDragDrop, OnDragOver, OnEndDrag

Очень немногие из этих свойств объявлены как published. В результате порожденным классам предоставляется возможность определить, какие свойства будут отображаться в инспекторе объектов. Это является общим подходом при разработке компонент, так как переопределить свойство менее видимым в наследнике нельзя. Существует много классов, которые реализуют свойства, методы и события компонента, но не публикуют их в секции published. Они оставляют эту задачу своим потомкам. TControl – пример такого класса. Помимо TControl, в VCL определены многочисленные пользовательские классы (custom), они ведут себя сходным образом. Так, например, класс TCustomLabel обеспечивает все свойства и методы текстовой метки, но очень немногие из них видны в инспекторе объектов. Отображение в инспекторе производит класс TLabel.

Класс TControl вводит концепцию родителя (parent) компонента. Родитель элемента управления является окном (именно окном, а не компонентом), в котором отображается элемент управления, следовательно, родители должны быть TWinControl или его потомками (т.е. компоненты, поддерживающие идентификатор окна). Свойство Parent содержит указатель на родителя. При установке свойства Parent ссылка на компонент будет сохранена в свойстве Controls родительского компонента. При уничтожении родительского компонента будут автоматически уничтожены все дочерние компоненты, несмотря на значение их свойства Owner. Сделано это потому, что при уничтожении родительского компонента уничтожается его окно (и дескриптор окна), поэтому его «дети» не смогут нарисовать себя, даже если их не уничтожить.

Несмотря на то, что существует некоторое сходство между концепциями "владельца" и "родителя" (особенно в контексте уничтожения дочерних компонентов), эти понятия нельзя путать. Для некоторого прояснения ситуации приведем пример: панель с помещенной на нее кнопкой в режиме разработки. Владельцем кнопки и панели будет форма. При этом родителем кнопки будет панель, а родителем панели – форма.

Еще одно важное свойство – ControlStyle. Это свойство указывает стили, применимые к визуальным компонентам. Тип его объявлен как:

      type TControlStyle = setof (csAcceptsControls, csCaptureMouse, 
  csDesignInteractive, csClickEvents, csFramed, csSetCaption, csOpaque, 
  csDoubleClicks, csFixedWidth, csFixedHeight, csNoDesignVisible, 
  csReplicatable, csNoStdEvents, csDisplayDragImage, csReflector, 
  csActionClient, csMenuEvents);

В таблице 3 приведено описание флагов типа.

csAcceptsControls Компонент может выступать в качестве родителя компонентов, размещенных на нем во время проектирования. Имеет смысл только для оконных компонентов.
csCaptureMouse Компонент перехватывает события мыши. (То есть событие MouseUp посылается даже если мышь освобождена за пределами визуального компонента)
csDesignInteractive Во время проектирования щелчки правой кнопкой мыши транслируются в щелчки левой кнопкой
csClickEvents Компонент может принимать и обрабатывать щелчки мыши посредством события OnClick
csFramed Элемент управления имеет рамку. Нуждается в эффектах Ctrl3D.
csSetCaption Свойства Caption и Text устанавливаются совпадающими со свойством Name, если не заданы явно
csOpaque Элемент управления скрывает все элементы управления позади себя. Т.е непрозрачен.
csDoubleClicks Генерация события OnDblClick при двойном щелчке на компоненте
csFixedWidth Масштабирование не оказывает влияния на ширину элемента управления
csFixedHeight Масштабирование не оказывает влияния на высоту элемента управления
csNoDesignVisible Элемент управления не виден во время проектирования
csReplicatable Элемент управления может выводить свое изображение на другое полотно рисования с помощью метода PaintTo
csNoStdEvents Не генерирует стандартных событий мыши, клавиатуры. Позволяет быстрее работать компонентам не нуждающимся в этих событиях.
csDisplayDragImage Некоторые компоненты имеют возможность отображать специфические изображения при перетаскивании над ними. Для них данный флаг должен быть включен.
csReflector Элемент управления обрабатывает сообщения фокуса, диалога, изменения размера, посылаемые OC. Используется, если элемент управления может выступать в роли ActiveX элемента управления.
csActionClient Элемент управления ассоциирован с объектом action. Устанавливается при установке свойства Action компонента.
csMenuEvents Элемент управления отвечает на команды системного меню.

Класс TControl инициализируется с флагами csCaptureMouse, csClickEvents, csSetCaption и csDoubleClicks. Свойство ControlStyle не может быть модифицировано во время выполнения.

Другое свойство, ControlState, позволяет узнать состояние элемента управления на этапе выполнения программы. В таблице 4 приведены флаги данного свойства.

csLButtonDown Левая кнопка мыши нажата и не отпущена.
csClicked То же что и csLButtonDown. Однако устанавливается только если установлен флаг csClickEvents в ControlStyle, т.е нажатия кнопок мыши интерпретируются как щелчки (clicks)
csPalette Системная цветовая палитра изменилась и элемент не успел обновиться
csReadingState Компонент читает свое состояние из потока
csAlignmentNeeded Элемент управления требует повторного выравнивания.
csFocusing Приложение хочет передать фокус элементу управления. Установка флага не гарантирует, что фокус будет получен, но позволяет избавиться от циклических вызовов
csCreating Элемент управления и/или его владелец и вложенные элементы начали создаваться. Флаг очищается после окончания процесса создания.
csPaintCopy Копия элемента управления может быть отрисована. Флаг устанавливается только при установленном флаге csReplicatable в ControlStyle
csCustomPaint Элемент управления получает пользовательские сообщения об отрисовке
csDestroyingHandle Окно элемента управления сейчас будет уничтожено.
csDocking Сейчас элемент управления будет перетаскиваться на другой dock.

TGraphicControl

Класс TGraphicControl является базовым для визуальных компонентов, не нуждающихся в получении фокуса ввода и не служащими родителями(Parent) для других компонентов. Обе задачи не требуют идентификатора окна. Класс является прямым потомком класса TControl.

Несмотря на то, что TGraphicControl и его наследники не имеют идентификатора окна, они могут отвечать на события мыши. Эта возможность реализуется с помощью родителя компонента. Естественно, что родителем должен быть TWinControl (или его наследник). Класс TGraphicControl не имеет собственного изображения по умолчанию (да и как себе можно это представить?), однако определяет виртуальный метод Paint, позволяющий наследникам отрисовывать свое изображение, и свойство Canvas, определяющее полотно для рисования.

Для наследника TGraphicControl можно установить флаг csAcceptControls в свойстве ControlStyle. Но, при попытке поместить на него другой компонент, идентификатор окна не будет обнаружен, и возникнет исключительная ситуация.

Графические компоненты

Графические компоненты – это наследники класса TGraphicControl. Компоненты данного типа не имеют дескриптора окна, и как следствие, не могут получать фокуса ввода. Для создания графического компонента достаточно переопределить виртуальный метод Paint, отвечающий за отрисовку компонента, и перенести необходимые свойства и события TGraphicControl в наследнике в секцию published.

Компонент X

В качестве первого примера графического компонента рассмотрим компонент, отображающий на экране значок Х. Цвет и толщина линии значка задаются свойствами Color и LineWidth, соответственно. Код компонента приведен ниже.

        unit umgXShape;

interfaceuses
  Classes, Graphics, Controls;

type
  TmgXShape = class(TGraphicControl)
  private{ Private declarations }
    FColor: TColor;
    FLineWidth: integer;

    procedure SetColor(Value : Tcolor);
    procedure SetLineWidth(Value : integer);
  protected{ Protected declarations }procedure Paint; override;
  public{ Public declarations }constructor Create(AOwner: TComponent); override;
  published{ Published declarations }{own properties}property Color: TColor read FColor write SetColor;
    property LineWidth: integer read FLineWidth write SetLineWidth;

    {  inherited properties }property Align;
    property Cursor;
    property Visible;
    property Width default 20;
    property Height default 20;

    property OnClick;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDrag;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
  end;

procedure Register;

implementationconstructor TmgXShape.Create;
begininherited Create(AOwner);
   FLineWidth := 3;
   FColor := clBlack;
   Width:=20;
   Height:=20;
end;


procedure TmgXShape.Paint;
beginwith Canvas dobegin
    Pen.Width := FLineWidth;
    pen.color := FColor;
    MoveTo(0,0);
    LineTo(Width,Height);
    MoveTo(0,height);
    LineTo(Width,0);
  end;
end;

procedure TmgXShape.SetColor(value : Tcolor);
beginif FColor <> value thenbegin
    FColor := value;
    Invalidate;
  end;
end;

procedure TmgXShape.SetLineWidth(value : integer);
beginif LineWidth <> value thenbegin
    LineWidth := value;
    Invalidate;
  end;
end;

procedure Register;
begin
  RegisterComponents('Our components', [TmgXShape]);
end;

end.

В этом компоненте переопределены свойства TGraphicControl Height и Width. В компоненте определяются также новые свойства: Color – цвет значка X, и LineWidth – ширина линии значка X. Оба свойства имеют методы установки значения. Помимо непосредственно установки значения свойства, эти методы вызывают перерисовку компонента, используя вызов Invalidate.

Конструктор компонента вызывает конструктор предка и устанавливает значения свойств по умолчанию.

Переопределенный метод Paint отвечает за отрисовку изображения компонента. Для отрисовки используются методы холста рисования (TCanvas).

Несмотря на то, что толку от этого компонента немного, он отлично и довольно наглядно демонстрирует методику построения графических элементов управления, реализация действительно полезного компонента будет показана ниже.

Трехмерная метка

Настало время создать действительно полезный компонент. Таким примером графического компонента будет трехмерная метка.

Для создания эффекта трехмерности достаточно отрисовывать копию текста метки с некоторым смещением относительно основного текста метки. Наш компонент будет поддерживать несколько стилей трехмерности:

Поддерживаемые значения стилей трехмерности объявлены в типе-перечислении TCaptionStyle.

Компонент трехмерной метки, помимо свойств, объявленных в TLabel, добавляет свойство CaptionStyle, предназначенное для установки вида эффекта трехмерности.

Так как размеры по умолчанию для трехмерной метки отличаются от размеров по умолчанию для обычной метки, то свойства Height и Width переобъявлены с новыми значениями директив default.

Наш компонент не должен скрывать никаких свойств TLabel, поэтому именно TLabel будет являться предком трехмерной метки. Код компонента приведен ниже. Рассмотрим его подробнее.

        unit umgLabel3D;
interfaceuses
  SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
  StdCtrls, DesignIntf;

type

  TCaptionStyle = (csNone, csRaised, csHeavyRaised, csRecessed, csHeavyRecessed);

  TmgLabel3D = class(TLabel)
  private
    FCaptionStyle: TCaptionStyle;
  protectedprocedure DoDrawText(var Rect: TRect; Flags: LongInt);override;
    procedure SetCaptionStyle(Value: TCaptionStyle);
  publicconstructor Create(AOwner: TComponent); override;
  publishedproperty CaptionStyle: TCaptionStyle read FCaptionStyle write SetCaptionStyle 
      default csRecessed;
    property Height default 16;
    property Width default 62;
  end;


  procedure Register;

implementation{$R *.dcr}constructor TmgLabel3D.Create(AOwner: TComponent);
begininherited Create(AOwner);
  FCaptionStyle := csRecessed;
  Height := 16;
  Width := 62;
end;

procedure TmgLabel3D.SetCaptionStyle(Value: TCaptionStyle);
beginif Value <> FCaptionStyle thenbegin
    FCaptionStyle := Value;
    Invalidate;
  end;
end;

procedure TmgLabel3D.DoDrawText(var Rect: TRect; Flags: LongInt);
var
  Text       : array[ 0..255 ] of AnsiChar;
  TmpRect    : TRect;
  UpperColor : TColor;
  LowerColor : TColor;
begin
  StrPCopy(Text, Caption);

  Canvas.Font := Font;

  if (FCaptionStyle = csRecessed) or (FCaptionStyle = csHeavyRecessed) thenbegin
    UpperColor := clBtnShadow;
    LowerColor := clBtnHighlight;
  endelsebegin
    UpperColor := clBtnHighlight;
    LowerColor := clBtnShadow;
  end;

  if FCaptionStyle 
    in [csRaised, csHeavyRaised, csRecessed, csHeavyRecessed] thenbegin
    TmpRect := Rect;
    if FCaptionStyle in [csRecessed, csHeavyRecessed, csHeavyRaised] thenbegin
      OffsetRect( TmpRect, 1, 1 );
      Canvas.Font.Color := LowerColor;
    end;
    if FCaptionStyle in [csRaised, csHeavyRecessed, csHeavyRaised] thenbegin
      OffsetRect( TmpRect, -1, -1 );
      Canvas.Font.Color := UpperColor;
    end;
    DrawText(Canvas.Handle, Text, StrLen(Text), TmpRect, Flags);
  end;

  Canvas.Font.Color := Font.Color;
  ifnot Enabled then
    Canvas.Font.Color := clGrayText;
  DrawText(Canvas.Handle, Text, StrLen(Text), Rect, Flags);
end;

procedure Register;
begin
  RegisterComponents( 'Our components', [TmgLabel3D] );
end;

end.

Начнем с конструктора:

        constructor TmgLabel3D.Create(AOwner: TComponent);
begininherited Create(AOwner);
  FCaptionStyle := csRecessed;
  Height := 16;
  Width := 62;
end;

Ничего необычного в конструкторе не происходит. Сначала вызывается конструктор предка, затем устанавливаются значения свойств по умолчанию. Свойство CaptionStyle в csRecessed, свойство Height – 16, Width – 62. Обратите внимание, что для установки свойства CaptionStyle мы используем в конструкторе прямой доступ к переменной, хранящей значение свойства. Это избавляет нас от повторной прорисовки компонента, так как при создании компонента он отрисуется автоматически.

Метод установки значения свойства CaptionStyle, помимо установки значения свойства, вызывает перерисовку метки (с помощью вызова Invalidate) при изменении стиля.

        procedure TmgLabel3D.SetCaptionStyle(Value: TCaptionStyle);
beginif Value <> FCaptionStyle thenbegin
    FCaptionStyle := Value;
    Invalidate;
  end;
end;

Основную работу по отрисовке метки выполняет метод DoDrawText. Почему же мы не перекрыли метод Paint, а использовали метод DoDrawText? Все станет понятно, если мы рассмотрим код метода TCustomLabel.Paint.

        procedure TCustomLabel.Paint;
const
  Alignments: array[TAlignment] of Word = (DT_LEFT, DT_RIGHT, DT_CENTER);
  WordWraps: array[Boolean] of Word = (0, DT_WORDBREAK);
var
  Rect, CalcRect: TRect;
  DrawStyle: Longint;
beginwith Canvas dobeginifnot Transparent thenbegin
      Brush.Color := Self.Color;
      Brush.Style := bsSolid;
      FillRect(ClientRect);
    end;
    Brush.Style := bsClear;
    Rect := ClientRect;
    { DoDrawText takes care of BiDi alignments }
    DrawStyle := DT_EXPANDTABS or WordWraps[FWordWrap] or Alignments[FAlignment];
    { Calculate vertical layout }if FLayout <> tlTop thenbegin
      CalcRect := Rect;
      DoDrawText(CalcRect, DrawStyle or DT_CALCRECT);
      if FLayout = tlBottom then OffsetRect(Rect, 0, Height - CalcRect.Bottom)
      else OffsetRect(Rect, 0, (Height - CalcRect.Bottom) div 2);
    end;
    DoDrawText(Rect, DrawStyle);
  end;
end;

Как видим, метод Paint подготавливает полотно рисования для вывода текста метки. Непосредственно вывод текста осуществляется методом DoDrawText. Разработчики Borland поступили мудро, объявив метод DoDrawText виртуальным. Это дает нам возможность использовать в нашем компоненте родительский метод Paint, обеспечивающий подготовительные операции, а основную работу определить в методе DoDrawText нашего компонента.

Рассмотрим код этого метода (см. выше) более подробно.

Сначала анализируется стиль трехмерности и вычисляется цвет тени метки. Затем осуществляется построение тени. Построение тени производится путем вывода текста метки с небольшим (в нашем случае +-1 пиксел) смещением относительно основного текста. Смещение выполняется с помощью вызова метода полотна рисования OffsetRect. Вывод текста в прямоугольник с рассчитанным смещением выполняется с помощью метода полотна рисования DrawText.

Заключение

На этом мы заканчиваем очередную главу из цикла разработки пользовательских компонентов. Мы очень надеемся, что нам удалось доказать, что построение собственных компонентов – несложный процесс при условии достаточного уровня понимания концепции ООП и деталей организации иерархии классов библиотеки VCL. В следующей статье мы расскажем о построении оконных элементов управления.


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