Все компоненты Delphi являются частью иерархии, которая называется Visual Component Library (VCL). Общим предком всех компонентов является класс TComponent (рис. 9.1.1), в котором собран минимальный набор общих для всех компонентов Delphi свойств.
Свойство ComponentState содержит набор значений, указывающих на текущее состояние компонента. Приведем некоторые значения свойства:
csDesigning | компонент находится в режиме проектирования |
sDestroyingKOMnoHeHT | сейчас будет разрушен; |
csLoading | компонент загружается из файла формы; |
csReading | компонент считывает значения из файла формы; |
csWriting | компонент записывает значения своих свойств в поток; |
csUpdating | компонент вносит изменения, чтобы отразить изменения в родительской форме. |
Класс TComponent вводит концепцию принадлежности. Каждый компонент имеет свойство Owner (владелец), ссылающееся на другой компонент как на своего владельца. В свою очередь, компоненту могут принадлежать другие компоненты, ссылки на которые хранятся в свойстве Components. Конструктор компонента принимает один параметр, который используется для задания владельца компонента. Если передаваемый владелец существует, то новый компонент добавляется к списку Components владельца. Свойство Components обеспечивает автоматическое разрушение компонентов, принадлежащих владельцу. Свойство ComponentCount показывает количество принадлежащих компонентов, a Componentlndex — номер компонента в массиве Components.
|
|
В классе TComponent определено большое количество методов. Наибольший интерес представляет метод Notification. Он вызывается всегда, когда компонент вставляется или удаляется из списка Components владельца. Владелец посылает уведомление каждому члену списка Components. Этот метод переопределяется в порождаемых классах для того, чтобы обеспечить действительность ссылок компонента на другие компоненты. Например, при удалении компонента Tablel с формы свойство DataSet компонента DataSourcel, равное Tablel, устанавливается в Nil.
Процесс разработки компонента включает пять этапов:
выбор класса-предка;
создание модуля компонента;
добавление в новый компонент свойств, методов и событий;
тестирование;
регистрацию компонента в среде Delphi;
9.1. Выбор класса-предка
На рис. 9.1.1 изображены базовые классы, формирующие структуру VCL. В самом верху расположен TObject, который является предком для всех классов в Object Pascal. От него происходит TPersistent, обеспечивающий методы, необходимые для создания потоковых объектов. Потоковый объект — объект, который может запоминаться в потоке. Поток представляет собой объект, способный хранить двоичные данные (файлы). Поскольку Delphi реализует файлы форм, используя потоки, то TComponent порождается от TPersistent, предоставляя всем компонентам способность сохраняться в файле формы.
|
|
Класс TComponent представляет собой вершину иерархии компонентов и является первым из четырех базовых классов, используемых для создания новых компонентов. Прямые потомки TComponent — невизуальные компоненты.
9.1.1. Класс TControl
Вершину иерархии визуальных компонентов представляет класс TControl.
Класс TControl вводит понятие родительских элементов управления (parent control). Свойство Parent является окном, которое содержит элемент управления. Например, если компонент Panel 1 содержит Button 1, то свойство Parent компонента Button 1 равно Panel 1.
Свойство ControlStyle определяет различные стили, применимые только к визуальным компонентам, например:
csAcceptControls | элемент управления становится родителем любых элементов управления, помещенных на него во время проектирования. Применим только к оконным элементам управления; |
csCaptureMouse | элемент управления перехватываетсобытия мыши; |
сsFrames | элемент управления имеет рамку; |
csSetCaption | свойства Caption и Text элемента управления (если не заданы явно) устанавливаются так, чтобы совпадать со свойством Name; |
csOpaque | элемент управления скрывает все элементы позади себя. |
В классе TControl определено большинство свойств, используемых визуальными компонентами: свойства позиционирования (Align, Left, Top, Height, Width), свойства клиентской области (ClientHeight, ClientWidth), свойства внешнего вида (Color, Enabled, Font, ShowHint, Visible), строковые свойства (Caption, Name, Text, Hint), свойства мыши (Cursor, DragCursor, DragKind, DragMode).
Кроме того, класс TControl реализует методы диспетчеризации событий.
Все визуальные компоненты подразделяют на графические элементы управления и оконные элементы управления. Каждый тип представляет свою иерархию классов, происходящую соответственно от TGraphicControl и TWinControl. Главная разница между этими типами компонент состоит в том, что графические компоненты не поддерживают идентификатор окна, и, соответственно, не могут принять фокус ввода.
Оконные компоненты далее разбиваются на две категории. Прямые потомки TWinControl являются оболочками вокруг существующих элементов управления, реализованных в Windows (например, TEdit, TButton, и др.) и, следовательно, знают, как себя рисовать.
Для компонентов, которые требуют идентификатора окна, но не инкапсулируют базовых элементов Windows, которые бы обеспечивали возможность перерисовывать себя, имеется класс TCustomControl.
9.1.2. Класс TGraphicControl
Класс TGraphicControl является базовым для компонентов, которые не нуждаются в получении фокуса ввода и не служат в качестве родительских для других элементов управления (эти функции требуют наличия идентификатора окна).
По умолчанию объекты TGraphicControl не имеют собственного визуального отображения, но для наследников обеспечиваются виртуальный метод Paint (вызывается всегда, когда элемент управления должен быть нарисован) и свойство Canvas (используется как «поверхность» для рисования).
9.1.3. Класс TWinControl
Класс TWinControl используется как базовый для создания компонентов, инкапсулирующих соответствующие оконные элементы управления Windows, которые сами себя рисуют.
Класс TWinControl обеспечивает свойство Handle, являющееся ссылкой на идентификатор окна базового элемента управления. Кроме этого свойства класс реализует свойства, методы и события, поддерживающие клавиатурные события и изменения фокуса:
свойства фокуса | TabStop, TabOrder; |
свойства внешнего вида | Ctl3D, Showing; |
методы фокуса | CanFocus, Focused; |
методы выравнивания | AlignControl, EnableAlign, Re Align; |
оконные методы | CreateWnd, CreateParam, RecreateWnd, CreateWindowHandle, DestroyWnd; |
события фокуса | OnEnter, OnExit; |
события клавиатуры | OnKeyDown, OnKeyPress, OnKeyUp. |
Создание любого потомка этого класса начинается с вызова метода CreateWnd, который вначале вызывает CreateParams для инициализации записи параметров создания окна, а затем вызывает CreateWindowHandle для создания реального идентификатора окна, использующего запись параметров. Затем CreateWnd настраивает размеры окна и устанавливает шрифт элемента управления.
|
|
9.1.4. Класс TCustomControl
Класс TCustomControl представляет собой комбинацию классов TWinControl и TGraphicControl. Являясь прямым потомком класса TWinControl, TCustomControl наследует способность управления идентификатором окна и всеми сопутствующими возможностями. Кроме этого, как и класс TGraphicControl, класс TCustomControl обеспечивает потомков виртуальным методом Paint, ассоциированным со свойством Canvas.
Таким образом, в зависимости от того, какой компонент будет исходным (базовым) для создания нового класса, можно выделить 4 случая:
создание Windows-элемента управления (TWinControl);
создание графического элемента управления (TGraphic-Control);
создание нового элемента управления (TCustomControl); О создание невизуального компонента (TComponent).
9.2. Создание модуля компонента и тестового приложения
Определившись с выбором компонента, можно приступить к созданию модуля компонента. Для этого необходимо выполнить следующие шаги.
Выполните команду File/ New.../ Component или Component/ New Component.
В диалоговом окне New Component (рис. 9.2.1.) установите основные параметры создания компонента: Ancestor type (имя класса-предка), Class Name (имя класса компонента), Palette Page (вкладка палитры, на которой должен отображаться компонент) и Unit file name (имя модуля компонента).
После щелчка на кнопке ОК будет сгенерирован каркас нового класса.
По ходу процесса построения компонента необходимо тестировать его, не устанавливая в палитру компонентов. Тестовое приложение должно содержать код, который динамически помещает новый компонент на форму, изменяет его свойства и вызывает методы.
Упражнение 9.2.1. Разработайте новый компонент, который объединяет компоненты TEdit и TLabel. Компонент Label располагается выше поля редактирования (TEdit). При перемещении поля редактирования TLabel следует за ним. При удалении поля редактирования TLabel также удаляется.
|
|
Решение
В качестве предка класса нового компонента используем TEdit.
Выполните команду Component/ New component. Установите следующие значения параметров окна: Ancestor type TEdit
Class Name TLabelEdit
Palette Page Test
Unit file name...\LabelEdit\LabelEdit.pas
Щелкните на кнопке ОК, автоматически будет сгенерирован следующий код:
unit LabelEdit;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type
TLabelEdit = class(TEdit)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
published
{ Published declarations }
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Test', [TLabelEdit]);
end;
end.
В модуле описан каркас нового класса и написана процедура регистрации компонента (Register), которая помещает его на страницу Test. Сохраните файл модуля компонента.
Разработка тестового приложения
Создайте новый проект. Сохраните его файлы в папке...\LabelEdit: файл модуля — под именем Main.pas, файл проекта — Test Application, dpr.
Добавьте имя модуля разрабатываемого компонента в раздел Uses формы тестового приложения:
...
uses..., LabelEdit;
...
В общедоступный раздел класса TForml добавьте поле
le: TLabelEdit;
В обработчике события OnCreate формы динамически создайте новый компонент:
procedure TForml.FormCreate(Sender: TObject);
begin
le:=TLabelEdit.Create(Self);
with le do
begin
parent:=forml;
left:=10;
top:=10;
end;
end;
Сохраните файлы проекта.
Эксперимент. Убедитесь, что при запуске в левом верхнем углу формы появляется окно редактирования. ♦
9.3. Добавление свойств, методов и событий
Свойства, как и поля класса, являются атрибутом объекта. Но если поля являются простым хранилищем некоего значения, которое может быть прочитано и изменено, то со свойством связаны некоторые действия, осуществляемые при чтении и изменении его содержимого.
Добавление свойства происходит в три этапа.
1. Создание внутреннего поля класса для хранения значения свойства.
2. Описание и разработка методов доступа к значению свойства.
3. Описание свойства.
В классе TControl свойства Caption/Text, Parent и Hint определяются так:
TControl = class (TComponent)
private
FParent: TWinControl; {внутреннее поле свойства Parent}
FText: PChar; {внутреннее поле свойства Text/Caption}
FHint: string; {внутреннее поле свойства Hint}
function GetText: Tcaption; {метод чтения свойства Text/Caption}
function IsCaptionStored: Boolean;
function IsHintStored: Boolean;
procedure SetText(const Value: TCaption);
{метод записи свойства Text/Caption}
…
Protected
…
procedure SetParent{AParent: TWinControl); virtual;
property Caption: TCaption read GetText write SetText stored IsCaptionStored;
property Text: TCaption read GetText write SetText;
…
public
…
property Parent: TWinControl read FParent write SetParent;
…
published
property Hint: string read FHint write FHint stored IsHintStored;
…
end;
Объявление свойства имеет следующий синтаксис: property <имя свойства>: тип определители;
При объявлении свойства используется зарезервированное слово property, после которого указываются четыре ключевых фрагмента информации. Первый — имя свойства, этот идентификатор используется для ссылок на значение свойства. Таким образом, свойства получают внешний вид полей данных.
Каждое объявление свойства должно определять тип свойства, для этого используется символ двоеточие после имени свойства.
Для указания метода, который будет использоваться для осуществления выборки значения свойства, используется директива read. Метод должен быть функцией, чей возвращаемый тип является тем же самым, что и тип свойства.
Однако вместо метода доступа для чтения можно указать внутреннее поле хранения данных, как, например, при описании свойств Hint и Parent. Подобная форма записи приводит к тому, что значение свойства извлекается прямо из внутреннего поля данных.
За спецификацией метода чтения следует определитель метода записи, директива write определяет, какой метод будет использоваться для присвоения свойству значения. Метод должен быть процедурой, имеющей единственный параметр, тип которого должен совпадать с типом свойства.
При обращении к значению свойства происходит перенаправление на соответствующий метод. Например, оператор s: =Editl. Text; автоматически будет преобразован в оператор s: =Editl. GetText; а оператор Editl. Text: =' Test' — в оператор Editl.Text('Test').
Описание свойства должно содержать определитель read или write или сразу оба. Если описание свойства включает в себя только определитель read, то оно является свойством только для чтения. В свою очередь, свойство, чье описание включает в себя только определитель write, является свойством только для записи. При присвоении свойству, определенному с директивой только для чтения, какого-либо значения или при использовании в выражении свойства с директивой только для записи всегда возникает ошибка.
В отличие от внутренних полей хранения данных свойства не могут быть переданы в процедуру (или функцию) в качестве параметра переменной (параметр var), это объясняется тем, что свойство не существует в памяти.
Когда программист использует Инспектор объектов для изменения свойств формы или свойств компонентов, то результирующие изменения заносятся в файл формы. Файлы форм представляют собой файлы ресурсов Windows, и когда приложение запускается, то описание формы подгружается из этого файла. Для определения того, что должно сохраняться в файле формы, служат спецификаторы памяти — необязательные директивы stored, default и node-fault. Эти директивы влияют на информацию о типе во время выполнения, генерируемую для свойств published.
Директива stored управляет тем, будет или нет свойство действительно запоминаться в файле формы. За директивой stored должны следовать либо константы True или False, либо имя поля, имеющего тип Boolean, либо имя метода, у которого нет параметров, и возвращающего значение типа Boolean. Например,
property Hint: string read FHint write FHint stored IsHintStored;
Если свойство не содержит директиву stored, то оно рассматривается как содержащее ее с параметром True.
Директивы default и nodefault управляют значениями свойства по умолчанию. За директивой default должна следовать константа того же типа, что и свойство, например:
property Tag: Longint read FTag write FTag default 0;
Чтобы перекрыть наследуемое значение default без указания нового значения, используется директива nodefault. Директивы default и nodefault работают только с порядковыми типами и множествами, нижняя и верхняя границы которых лежат в промежутке от 0 до 31. Если такое свойство описано без директив default и nodefault, то оно рассматривается как с директивой nodefault. Для вещественных типов, указателей и строк значение после директивы default может быть только О, NIL и
(пустая строка) соответственно.
Когда Delphi сохраняет компонент, то просматриваются спецификаторы памяти published свойств компонента. Если значение текущего свойства отличается от default значения (или директива default отсутствует) и параметр stored равен True, то значение свойства сохраняется, иначе свойство не сохраняется.
Спецификаторы памяти не поддерживаются свойствами-массивами, а директива default при описании свойства-массива имеет другое назначение.
9.3.1. Простые свойства
Простые свойства — это числовые, строковые и символьные свойства. Они могут непосредственно редактироваться в Инспекторе объектов и не требуют специальных методов доступа.
Рассмотрим создание простого свойства Color, описанного в классе TContol (модуль controls.pas):
Type
TControl = class (TComponent)
private
FColor: TColor;
function IsColorStored: Boolean;
procedure SetColor(Value: TColor);
protected
property Color: TColor read FColor write SetColor stored IsColorStored default clWindow;
end;
function TControl.IsColorStored: Boolean;
begin
Result:= not ParentColor;
end;
procedure TControl.SetColor (Value: TColor);
begin
if FColor <> Value then
begin
FColor:= Value;
FParentColor:= False;
Perform(CM_COLORCHANGED, 0, 0);
{вызов Perform позволяет обойти очередь сообщений Windows и послать сообщение, в данном случае — изменить цвет, элементу управления}
end;
end;
9.3.2. Свойства перечислимого типа
Определенные пользователем перечислимые и логические свойства можно редактировать в окне инспектора объектов, выбирая подходящее значение свойства в раскрывающемся списке. Рассмотрим создание свойства перечислимого типа на примере компонента Shape (модуль extctrls.pas).
Туре
TShapeType = (stRectangle, stSquare, stRoundRect, stRoundSquare, stEllipse, stCircle);
{вначале необходимо определить новый тип — перечислить возможные значения}
Туре
TShape = class(TGraphicControl)
private
…
FShape: TShapeType;
procedure SetShape(Value: TShapeType);
published
…
property Shape: TShapeType read FShape write SetShape
default stRectangle;
end;
procedure TShape.SetShape{Value: TShapeType);
begin
if FShape <> Value then
begin
FShape:= Value;
Inva 1 idate; {гарантирует перерисовку компонента}
end;
end;
9.3.3. Свойства типа множества
Свойство типа множества при редактировании в окне Инспектора объектов выглядит так же, как множество, определенное синтаксисом языка Pascal. Простейший способ его отредактировать — развернуть свойство в Инспекторе объектов, в результате каждый его элемент станет отдельным логическим значением.
При создании свойства типа множества нужно создать соответствующий тип, описать методы доступа, после чего описать само свойство. В модуле Controls.pas свойсво Align описано следующим образом:
Туре
TAlign = (alNone, alTop, alBottom, alLeft, alRight, alClient);
TAlignSet = set of TAlign; TControl = class(TComponent)
private
FAlign: TAlign;
procedure SetAlign(Value: TAlign);
public
property Align: TAlign read FAlign write SetAlign default alNone;
end;
procedure TControl.SetAlign(Value: TAlign);
var OldAlign: TAlign;
begin
if FAlign <> Value then
begin
OldAlign:= FAlign;
FAlign:= Value;
Anchors:= AnchorAlign[Value];
if not (csLoading in ComponentState) and
(not (csDesigning in ComponentState) or (Parent <> NIL))
then
if ((OldAlign in [alTop, alBottom])=(Value in [alRight, alLeft])) and not (OldAlign in [alNone, alClient]) and not (Value in [alNone, alClient]) then SetBounds(Left, Top, Height, Width)
{изменение границ компонента}
else AdjustSize; {устанавливает заданные размеры компонента}
end;
Request Align; {нструктирует «родителя» переставить компонент
в соответствии со значением свойства Align }
end;
9.3.4. Свойство-объект
Свойства могут являться объектами или другими компонентами. Например, у компонента Shape есть свойства-объекты Brush и Реп. Когда свойство является объектом, то оно может быть развернуто в окне инспектора так, чтобы его собственные свойства также могли быть модифицированы. Свойства-объекты должны быть потомками класса TPersistent, чтобы их свойства, объявленные в разделе published, могли быть записаны в поток данных и отображены в инспекторе объектов.
Для определения объектного свойства компонента необходимо сначала определить объект, который будет использоваться в качестве типа свойства. В модуле graphics.pas описан класс TBrush:
TBrush = class(TGraphicsObject)
private
procedure GetData(var BrushData: TBrushData);
procedure SetData(const BrushData: TBrushData);
protected
function GetBitmap: TBitmap;
procedure SetBitmap(Value: TBitmap);
function GetColor: TColor;
procedure SetColor(Value: TColor);
function GetHandle: HBrush.;
procedure SetHandle(Value: HBrush);
function GetStyle: TBrushStyle;
procedure SetStyle(Value: TBrushStyle);
public
constructor Create; destructor Destroy; override;
procedure Assign(Source: TPersistent); override;
property Bitmap: TBitmap read GetBitmap write SetBitmap;
property Handle: HBrush read GetHandle write SetHandle;
published
property Color: TColor read GetColor write SetColor
default clWhite;
property Style: TBrushStyle read GetStyle write SetStyle
default bsSolid;
end;
Метод Assign предназначен для копирования значения свойств экземпляра TBrush:
procedure TBrush.Assign (Source: TPersistent);
begin
if Source is TBrush then
begin
Lock; {блокирует использование объекта}
try
TBrush(Source).Lock;
try
BrushManager.AssignResource(Self, TBrush(Source).FResource);
finally TBrush(Source).Unlock;
end;
finally Unlock; {завершает секцию кода, начатую методом Lock,
снимая блокировку объекта}
end;
exit:
end;
inherited Assign(Source);
end;
Чтобы определить свойство-объект, нужно определить внутреннее поле. Так как свойство представляет объект, его нужно создать, а по завершении — уничтожить, поэтому в код включены конструктор Create и деструктор Destroy. Кроме того, объявлен метод доступа SetBrush, предназначенный для записи свойства Brush.
TShape = class(TGraphicControl)
private
FBrush: TBrush;
procedure SetBrush(Value: TBrush);
public
constructor Create (AOwner: TComponent); overrider;
destructor Destroy; overrider;
published
property Brush: TBrush read FBrush write SetBrush;
end;
constructor TShape.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FBrush:= TBrush.Create;
FBrush.OnChange:= StyleChanged;
end;
destructor TShape.Destroy;
begin
FBrush. Freer-inherited Destroy;
end;
procedure TShape.SetBrush (Value: TBrush);
begin
FBrush.Assign(Value);
end;
9.3.5. Свойство-массив
Примерами свойств-массивов могут служить такие свойства, как TMemo.Lines, TScreen.Fonts, TStringGrid.Cells.
Особенности свойства-массива заключаются в следующем:
свойства-массивы объявляются с помощью индексных параметров, цель которых — указать количество и тип индексов, которые будут использоваться свойством;
спецификации методов чтения и записи должны ссылаться на методы доступа. Методом для определителя read должна быть функция, список параметров которой совпадает со списком параметров, описывающих индекс свойства, и возвращающей значение того же типа, что и свойство. В свою очередь, методом в определителе write должна быть процедура, список параметров которой совпадает со списком параметров, описывающих индекс свойства. Список параметров такой процедуры может содержать и дополнительные свойства.
TCanvas = class (TPersistent)
private
FHandle: HDC; {ссылка на контекст устройства, используется для отображения графической информации}
function GetPixel (X, Y: Integer): TColor; {метод чтения}
procedure SetPixel (X, Y: Integer; Value: TColor);
{метод записи}
protected
public
constructor Create;
destructor Destroy; override;
property Pixels[X, Y: Integer]: TColor read GetPixel write SetPixel;
end;
constructor TCanvas.Create;
begin
inherited Create;
CanvasList. Add (Self); {добавляет в список ссылки на объекты}
end;
destructor TCanvas.Destroy;
begin
CanvasList.Remove (Self); {удаляет из списка ссылки на объекты}
inherited Destroy; end;
function TCanvas.GetPixel (X, Y: Integer): TColor;
begin
RequiredState([csHandleValid]);
GetPixel:= Windows.GetPixel(FHandle, X, Y); end;
procedure TCanvas.SetPixel(X, Y: Integer; Value: TColor);
begin
Changing;
RequiredState([csHandleValid, csPenValid]);
Windows.SetPixel(FHandle, X, Y, ColorToRGB(Value));
Changed;
end;
Доступ к такому свойству-массиву осуществляется следующим образом:
Canvas.Pixels [10, 20]:=clRed; что означает:
Canvas.SetPixel (10, 20, clRed);
За описанием свойства-массива может следовать директива default. В данном случае это будет означать, что это свойство становится свойством по умолчанию для данного класса. Например:
type
TStringArray = class public property Strings[Index: Integer]: string...; default;
end;
Если у класса есть свойство по умолчанию, то доступ к этому свойству может быть осуществлен оператором
<имя компонента>[Index], который эквивалентен оператору
<имя компонента>.<имя свойства>[Index].
Класс может иметь только одно свойство по умолчанию. В связи с тем, что компилятор статически определяет свойство по умолчанию у объекта, то смена свойства по умолчанию или его скрытие в наследниках класса может привести к непредсказуемым последствиям.
9.3.6. Массив свойств
Определитель Index позволяет разным свойствам иметь один и тот же метод доступа. Его описание состоит из директивы index и последующей за ней константой целого типа в промежутке от -2147483647 до 2147483647. Если у свойства есть определитель Index, то определители read и write должны ссылаться на методы, а не на поля. Например:
type
TRectangle = class private
FCoordinates: array[0..3] of Longint;
function GetCoordinate(Index: Integer): Longint;
procedure SetCoordinate{Index: Integer; Value: Longint); public
property Left: Longint index 0 read GetCoordinate
write SetCoordinate; property Top: Longint index 1 read GetCoordinate
write SetCoordinate; property Right: Longint index 2 read GetCoordinate
write SetCoordinate; property Bottom: Longint index 3 read GetCoordinate
write SetCoordinate;
end;
Обращение к свойству, определенному с директивой index, например,
Rectangle.Right:= Rectangle.Left + 100;
{Rectangle: TRectangle}
автоматически преобразуется к вызову метода,
Rectangle.SetCoordinate (2, Rectangle.GetCoordinate(0) + 100);
9.3.7. Перекрытие и переопределение свойств
Описание свойства без указания типа называется перекрытием свойства. Самый простой способ перекрытия состоит в использовании зарезервированного слова property и идентификатора — имени свойства. Данный способ используется для смены видимости свойства.
Перекрытия свойств могут содержать директивы read, write, stored, default и nodefault. Перекрытие может заменить существующие наследуемые определители доступа, добавить недостающие, увеличить видимость свойства, но оно не может удалить существующий определитель или уменьшить видимость свойства. Следующий пример демонстрирует использование перекрытия свойств:
type
TAncestor = class
protected
property Size: Integer read FSize; property Text: String read GetText write SetText; property Color: TColor read FColor write SetColor stored False;
...
end;
TDerived = class(TAncestor)
...
protected
property Size write SetSize; published property Text; property Color stored True default clBlue;
end;
Перекрытие свойства Size добавляет определитель write, что позволяет редактировать свойство, а перекрытие свойств Text и Color меняет их видимость с protected на published. Перекрытие свойства Color указывает, что оно должно быть сохранено, если его значение отлично от clBlue.
Переопределение свойства включает указание его типа, оно скрывает наследуемое свойство. Это означает, что создается новое свойство с тем же именем, что и у предка. Любое описание свойства, которое содержит указание типа, должно быть завершенным и включать в себя как минимум один определитель доступа:
type TAncestor = class
property Value: Integer read Methodl write Method2; end;
TDescendant = class(TAncestor)
property Value: Integer read Method3 write Method4; end;
9.3.8. Создание событий
Событие — это любое происшествие, вызванное вмешательством пользователя, операционной системы или логикой программы. Событие связано с некоторым программным кодом, отвечающим на это происшествие. Совокупность события и кода, выполняющегося в ответ на это событие, называется свойством-событием и реализуется в виде указателя на некоторый метод. Метод, на который указывает это свойство, называется обработчиком события.
Свойства-события являются не более чем указателями на методы. В модуле Controls.pas определены стандартные свойства-события.
Описание свойства-события начинается с описания нового типа, который представляет собой процедуру, одним из параметров которой, является Sender типа TObject, а директива of object делает эту процедуру методом:
TMouseMoveEvent = procedure(Sender: TObject; Shift: TShiftState; X, Y: Integer) of object;
Когда происходит какое-либо событие, например, перемещение мыши, в систему Win32 посылается соответствующее сообщение, в нашем случае WM_MOUSEMOVE. Система Win32 передает это событие элементу управления, для которого оно предназначено и на которое он должен тем или иным способом ответить. Элемент управления может ответить на это событие, сначала проверив наличие кода, предусмотренного для выполнения. Для этого он проверяет, ссылается ли свойство-событие на какой-либо код. Если да, то элемент выполняет этот код, называемый обработчиком события. Операция по определению наличия метода, связанного с событием-свойством, возлагается на метод диспетчеризации. Эти методы объявляются как защищенные методы того компонента, которому они принадлежат.
Описание свойства-события состоит из двух частей: во-первых, событие требует внутреннего поля данных, которое используется для хранения указателя на метод; во-вторых, создается соответствующее свойство, которое во время проектирования дает возможность присоединения обработчиков событий:
TControl = class(TComponent) private
FOnMouseMove: TMouseMoveEvent; {внутреннее поле события} procedure WMMouseMove(var Message: TWMMouseMove); message WM_MOUSEMOVE;
protected
procedure MouseMove(Shift: TShiftState; X, Y: Integer);
dynamic; {метод диспетчеризации}
property OnMouseMove: TMouseMoveEvent read FOnMouseMove
write FOnMouseMove;
end;
Метод диспетчеризации определяет, существует ли свойство-событие на какой-нибудь метод, и если это так, то передает управление соответствующей процедуре:
procedure TControl.MouseMove(Shift: TShiftState; X, Y: Integer); begin
if Assigned (FOnMouseMove) then FOnMouseMove (Self, Shift, X, Y); end;
Чтобы обеспечить возможность переопределения обработки события, необходимо перехватить возникшее событие, обработать его стандартным образом и передать управление методу диспетчеризации:
procedure TControl.WMMouseMove(var Message: TWMMouseMove); begin inherited; if not (csNoStdEvents in ControlStyle) then
{включение csNoStdEvents во множество ControlStyle заставляет игнорировать стандартные события мыши, клавиатуры. Этот флаг позволяет ускорить запуск приложения, если оно при этом не нуждается в обработке этих событий} with Message do MouseMove(KeysToShiftState(Keys), XPos, YPos); end;
9.3.9. Создание методов
Добавление в компонент методов не отличается от добавления методов в любой другой класс. Однако следует придерживаться следующих правил:
исключить взаимозависимость методов;
метод, вызываемый пользователем, не должен приводить компонент в такое состояние, при котором другие методы не действуют;
метод должен иметь осмысленное имя.
Упражнение 9.2.1 (продолжение). Добавим в описание нового класса свойство объектного типа TLabel:
type
TLabelEdit = class (TEdit)
private
{ Private declarations }
FLabel: Tlabel; {внутреннее поле}
protected
{ Protected declarations }
function GetLabelCaption: string; virtual;
{метод чтения свойства Caption объектного свойства Label}
procedure SetLabelCaption(Const Value: String); virtual;
{метод записи свойства
Caption объектного свойства Label} public
{ Public declarations }
constructor Create(Aowner: TComponent); override;
destructor Destroy; overrider;
published
{ Published declarations }
property LabelCaption: string read GetLabelCaption write SetLAbelCaption; end;
В конструкторе необходимо создать экземпляр типа TLabel, сохранить ссылку на него во внутреннем поле и задать значение свойства Caption созданного объекта:
constructor TLabelEdit.Create(AOwner: TComponent); begin
inherited Create(Aowner);
FLabel:= TLabel.Create(NIL);
{владельца у свойства-объекта не существует}
FLabel.Caption:= 'Label for Edit'; end;
При разрушении компонента необходимо освободить ресурсы, занятые созданным объектным свойством:
destructor TLabelEdit.Destroy;
begin
if (FLabel <> NIL) and (FLabel.parent = NIL) Then FLabel.free;
inherited Destroy;
end;
Методы доступа чтения и записи соответственно считывают и записывают значение свойства Caption во внутреннее поле FLabel:
function TLabelEdit.GetLabelCaption: String;
begin
Result:= FLabel.Caption;
end;
procedure TLabelEdit.SetLabelCaption(Const Value: string);
begin
Flabel.Caption:= value;
end;
Эксперимент. Сохраните модуль компонента. Запустите тестовое приложение. Как отображается создаваемый компонент? ♦
По-прежнему отображается только компонент Edit. Это связано с тем, что не определено свойство Parent внутреннего компонента Label. Напомним, свойство Parent задает компонент, который отвечает за прорисовку принадлежащих ему компонентов.
Добавьте в раздел protected описания класса TLabelEdit процедуру
procedure SetParent(value: TWinControl); override;
В разделе implementation модуля компонента опишите код процедуры:
procedure TLabelEdit.SetParent{Value: TWinControl};
begin
if (Owner=NIL) or not(csDestroying in Owner.ComponentState)
{если владелец компонента не определен или владелец не разрушается}
then FLabel.Parent:=Value;
{устанавливаем владельца компонента}
inherited SetParent(Value);
end;
Эксперимент. Модифицируйте тестовое приложение в соответствии с рис. 9.3.1. Значения Value компонентов SpinEdit определяют положение компонента LabelEdit.
Запустите тестовое приложение.
Проверьте отображение компонента LabelEdit при различных значениях свойств Left и Тор. ♦
При перемещении компонента Edit надпись (Label) должна перемещаться за ним. Для этого необходимо перехватить событие перемещения — WM_MOVE. Опишите в разделе private описания компонента TLabelEdit заголовок обработчика события WMMove:
procedure WMMove(var Msg: TWMMove); message WM_MOVE;
Обработчик события WMMove, кроме стандартной обработки события, содержит операторы перемещения компонента Label:
procedure TLabelEdit.WMMove(var Msg: TWMMove); begin inherited;
if Flabel <> NIL then with FLabel do SetBounds(Msg.XPos, Msg.Ypos-Height-5, Width, Height);
{procedure SetBounds(ALeft, ATop, A Width, AHeight: Integer) устанавливает сразу все граничные свойства элемента управления} end;
Эксперимент. Сохраните код модуля компонента. Запустите тестовое приложение, убедитесь в правильности перемещения компонента. ♦
9.4. Регистрация компонента в среде Delphi
В процессе регистрации компонент помещается в палитру компонентов Delphi.
Рассмотрим процесс установки компонента на примере компонента TLabelEdit, разработанного в упр. 9.2.1.
Выполните команду Component/Install Component. В диалоговом окне Install Component в строке Unit file name укажите имя модуля нового компонента —...\LabelEdit\LabelEdit.pas (рис. 9.4.1). Щелкните на кнопке ОК.
Появится диалоговое окно Confirm с сообщением «Package dclusr50.bpl will be built then installed. Continue?» (Пакет dclusr50.bpl будет переустановлен. Продолжить?), щелкните на кнопке Yes.
Если нет ошибок в файле модуля нового компонента, то компонент будет зарегистрирован в палитре компонентов Delphi и будет отображено окно Information с сообщением «Package c:\program files\borland\delphis\Projects\Bpl\ dclusrSO.bpl has been installed. The following new component(s) have been registered: TLabelEdit» (). Щелкните на кнопке ОК.
В палитре компонентов на странице Test появится новый компонент (рис. 9.4.2).
Примечание. Чтобы изменить пиктограмму нового компонента, воспользуйтесь программой Image Editor. Выполните команду File/ New/ Component Resource File, затем Resource/ New/ Bitmap. В появившемся диалоговом окне Bitmap Properties установите размер рисунка 24x24 пикселя, установите цвет — VGA (16 color), нажмите OK и измените имя Bitmapl на имя компонента (в нашем случае TLABELEDIT, вводите обязательно прописными символами). Затем выполните команду Resource/ Edit и создайте нужный рисунок. После этого сохраните файл в каталоге, в котором хранится модуль компонента, под тем же именем, но с расширением.DCR, установите компонент еще раз.
В случае повторной установки компонента или же в случае наличия ошибок в модуле компонента будет отображен редактор пакета компонентов Package-dclusr50.dpk (рис. 9.4.3), который позволяет удалять, добавлять, компилировать пакет.
Для сохранения изменений, проведенных в пакете Dclusr50, щелкните на кнопке ОК в диалоговом окне Confirm «Save changes to project Dclusr50?» (Сохранить изменения в проекте DclusrSO?).
Упражнение 9.4.1. Разработайте компонент SimpleTree, отображающий структуру файловой системы в древовидной форме (рис. 9.4.4).
Решение
Создайте каталог SimpleTree.
Выполните команду File\New\Component. В диалоговом окне New Component установите основные параметры:
введите имя класса предка — TCustomControl, так как этот класс предоставляет возможность рисования на компоненте и разрешает получать фокус ввода;
имя создаваемого класса — TSimpleTree;
название страницы палитры компонентов, на которую будет помещен компонент — Test;
имя файла модуля, содержащего описания создаваемого класса, —...\SimpleTree\SimleTree.pas;
□ значение строки указания путей для поиска файла оставьте без изменения.
После щелчка на кнопке ОК откроется окно редактирования модуля...\SimpleTree\SimleTree.pas, который содержит описание класса TSimpleTree и процедуру Register.
В разделе public описания класса опишите конструктор Create: constructor Create{AOwner: TComponent); override;
в котором определим возможность обрабатывать события мыши и установим объем рамки компонента:
constructor TSimpleTree.Create(AOwner: TComponent); begin
inherited Create{AOwner);
ControlStyle:= [csFramed, csCaptureMouse, csDoubleClicks,
csClickEvents ];
{Свойство ControlStyle отвечает за различные атрибуты компонента: csFramed — элемент управления имеет рамку и нуждается в эффектах Ctrl 3D; csCaptureMouse — данный элемент перехватывает события мыши; csDoubleClicks — когда на элементе дважды щелкнули мышью, генерируется событие OnDlClick; csClickEvents — когда на элементе нажата и отпущена мышь, генерируется событие OnClick}
FBorcier:= bsSingle; Width:= 150; Height:= 150;
Tabs top:= True; {возможность перехода на компонент
при нажатии на клавишу Tab}
end;
Задание для самостоятельного выполнения 9.2. Переопределите деструктор класса TSimpleTree.
Чтобы предоставить возможность пользователю компонента изменять внешний вид компонента и его положение на форме, создайте свойство Border и выполните перекрытие свойств Align, Anchors, Color, Ctl3D, Font, TabOrder, TabStop:
...
private
FBorder: TBorderStyle
...
published property Align; property Anchors;
property Border: TBorderStyle read FBorder write SetBorder default bsSingle;
property Color; property Ctl3D; property Font; property TabOrder; property TabStop;
...
end;
...
procedure TSimpleTree.SetBorder(const Value: TBorderStyle);
begin
if FBorder Value then begin FBorder:=Value; RecreateWnd;
{разрушает существующее окно, после чего создает заново}
end; end;
Эксперимент. Сохраните файл компонента.
Создайте тестовое приложение. Сохраните файл модуля под именем Main.pas, файл проекта Test.dpr.
Положите на форму компонент Button (измените свойство Caption на «Создать компонент»), создайте обработчик события onClick кнопки:
procedure TForml.buttonlclick (Sender: TObject);
begin
Tree:=TSimpleTree.Create(Forml);
with Tree do begin Parent:= forml; Left:=5; Top:=5; end; end;
Опишите переменную Tree и подключите модуль SimpleTree.
Запустите тестовое приложение. После щелчка на кнопке на форме должен отобразиться экземпляр класса TSimpleTree. Закройте приложение, убедитесь, что при этом не происходит никаких ошибок. ♦
Добавим в компонент возможность вертикального скроллинга дерева (ScrollBar). Перекройте метод CreateParams, который вызывается перед созданием окна (перед вызовом функции Win API CreateWindow):
protected
procedure CreateParams(var Params: TCreateParams); override;
...
procedure TSimpleTree.CreateParams{var Params: TCreateParams);
begin
inherited CreateParams(Params); with Params do begin
if FBorder = bsSingle then Style:=Style or WS_BORDER; Style:«Style or WS_VSCROLL; end; end;
Эксперимент. Запустите тестовое приложение. Убедитесь в появлении вертикальной полосы прокрутки на создаваемом компоненте.
Используя справочную систему Delphi, определите, какие стили окон управления существуют и как каждый стиль влияет на функциональность окна управления. ♦
Перед отображением компонента вызывается метод Paint. Для рисования древовидной структуры файловой системы необходимо его переопределить:
protected
... procedure Paint; override;
...
procedure TSimpleTree.Paint;
begin
end;
Рассмотрим процесс добавления большого количества элементов (узлов) в дерево. При добавлении каждого узла (до того времени, когда они отобразятся в компоненте) происходит перерисовка дерева, вызывающая мигание. Чтобы предотвратить этот эффект, создадим механизм блокировки «отрисовки» при добавлении узлов в дерево, который будет содержать два метода BeginPaint (начало блокировки) и EndPaint (окончание блокировки):
...
private
...FUpdateCount: integer;
{в конструкторе задайте начальное значение равным нулю} public
procedure BeginUpdate;
procedure EndUpdate;
procedure TSimpleTree.BeginUpdate; begin
inc(FUpdateCount); end;
procedure TSimpleTree.EndUpdate; begin
Dec(FUpdateCount);
if FUpdateCount = 0 then Invalidate; end;
Для того чтобы не происходила перерисовка дерева в процессе добавления узлов, в метод Paint добавляем оператор:
if FUpdateCount > 0 then exit;
Для вычисления положения узла используем значение ширины и высоты символа «А». Введем два поля FCharWidth и FCharHeight, соответственно, длина и высота символа текста, обновление значений которых будет происходить в следующем методе:
procedure TSimpleTree.UpdateCharMetrics; begin
Canvas.Font:= Self.Font;
FCharHeight:= Canvas.TextHeight('A') + 2; FCharWidth:= Canvas.TextWidth('A1); end;
Метод UpdateCharMetrics будет вызываться в ответ на событие смены шрифта и размеров компонента:
private
procedure CMFontChanged(var Msg: TMessage);
message CM_FONTCHANGED; procedure WMSize(var Msg: TWMSize); message WM_SIZE;
procedure TSimpleTree.CMFontChanged(var Msg: TMessage); "negin
inherited;
UpdateCharMetrics; end;
procedure TsimpleTree.WMSize(var Msg: TWMSize); begin
inherited;
UpdateCharMetrics; end;
Вернемся к процедуре Paint. Рисование дерева каталогов будем осуществлять последовательно: сначала отобразим узлы дерева, а затем, если нужно, — линии. Определите свойство DrawLines логического типа, значение True которого задает необходимость рисования линий дерева, False — рисование дерева без линий.
property DrawLines: boolean read FDrawLines write SetDrawLines default True;
где
procedure TSimpleTree.SetDrawLines(const Value: boolean); begin
if FDrawLines <> Value then begin
FDrawLines:=Value; Repaint; end; end;
He забудьте в конструкторе определить начальное значение поля FDrawLines. После этого метод Paint можно записать следующим образом:
procedure TSimpleTree.Paint;
procedure DoDrawNodes;
begin
end;
procedure DoDrawLines;
begin
end;
begin
if FUpdateCount > 0 then exit;
DoDrawNodes; {рисуем узлы}
if FDrawLines then DoDrawLines; {рисуем линии}
end;
Процедура DoDrawNodes рисует узлы дерева каталогов. Однако в конкретный момент времени нужно нарисовать только раскрытые пользователем узлы дерева. Список узлов дерева будем хранить в защищенном (private) поле FDrawList типа TList класса TSimpleTree.
Задание для самостоятельного выполнения
9.3. В конструкторе класса TSimpleTree создайте FDrawList (экземпляра класса TList), а в деструкторе освободите память, ассоциированную с этой переменной.
Список FDrawList содержит указатели на узлы дерева. Каждый узел представляет собой экземпляр класса TSimpleNode:
TSimpleNode = class(TObject) private
FTree: TSimpleTree/ {указатель на дерево}
FParent: TSimpleNode; {родительский узел}
FChildren: TList; {список дочерних узлов}
FCaption: string; {текст для отображения}
FLevel: integer; {уровень узла}
FIndex: integer;
{индекс в списке дочерних узлов родительского узла}
FX, FY: integer;
{последние координаты, по которым рисовался узел}
FExpanded: boolean; {развернутли}
FAbsolutelndex: integer; {индекс узла в дереве}
procedure Redraw; {перерисовка узла по последним координатам}
procedure DrawAt(X, Y: integer);
{нарисовать узел по координатам X, Y}
function GetChildren(Index: integer): TSimpleNode;
function GetChildrenCount: integer;
function GetSelected: boolean;
procedure SetSelected(const Value: boolean);
procedure SetCaption(const Value: string);
procedure SetExpanded(const Value: boolean); public
constructor Create(ATree: TSimpleTree);
destructor Destroy; override;
procedure ClearChildren; {очистить все дочерние узлы}
property Children[Index: integer]: TSimpleNode read GetChildren;
property ChildrenCount: integer read GetChildrenCount;
property Caption: String read FCaption write SetCaption;
property Level: integer read FLevel;
property Selected: boolean read GetSelected write SetSelected;
{выбран ли узел}
property Absolutelndex: integer read FAbsolutelndex;
property Index: integer read FIndex;
property Expanded: boolean read FExpanded write SetExpanded; end;
Обновление названия каталога:
procedure TSimpleNode.SetCaption(const Value: String); begin
if reaction <> Value then
begin
rtaction:=Value; ГТгее.Invalidate;
Задания для самостоятельного выполнения
9.4. Реализуйте методы GetChildrenCount (возвращает количество элементов, содержащихся в списке FChildren) и GetChildren (возвращает элемент списка FChildren под номером Index) класса TSimpleNode.
Обратите внимание на то, что описание класса TSimpleTree содержит элемент типа TSimpleNode, и наоборот. Чтобы сообщить компилятору о существовании класса TSimpleTree в разделе Туре, опишите классы следующим образом:
type
TSimpleTree = class; TSimpleNode = class (TObject)
end;
TSimpleTree = class(TCustomControl)
…
end;
Реализуем методы класса TSimpleNode. Конструктор инициализирует значения полей, а также выделяет память под переменную, которая будет содержать ссылки на подкаталоги:
constructor TSimpleNode.Create(ATree: TSimpleTree); begin
inherited Create;
FTree:=ATree;
FParent:=nil;
FChildren:=TList.Create;
FLevel:=0;
FIndex:=-l;
FExpanded:=False; end;
Деструктор освобождает память, ассоциированную с переменной FChildren (список ссылок на дочерние каталоги):
destructor TSimpleNode.Destroy; begin
ClearChildren; FChildren. Freer-inherited Destroy; end;
Удаление всех дочерних подкаталогов выполняет рекурсивная процедура, которая освобождает память, занятую под хранение ссылок на дочерние подкаталоги текущего подкаталога:
procedure TSimpleNode.ClearChildren;
var i: Integer; begin
for i:=0 to FChildren.Count - 1 do
9.5. При отображении узлов дерева использовались следующие свойства класса TSimpleTree:
property TextColor: TColor index 0 read GetTreeColor
write SetTreeColor; property LinesColor: TColor...; property SelTextColor: TColor property SelBackColor: TColor...;
Используя массив свойств, реализуйте перечисленные выше свойства. Не забудьте в конструкторе Create установить начальные значения этих свойств.
К реализации методов GetSelected, SetSelected, SetExpanded вернемся немного позднее.
Таким образом, внутренняя процедура DoDrawNodes метода TSimpleTree.Paint, отображающая узлы дерева, должна выполнить такую последовательность операторов:
var i: Integer; begin
for i:=0 to FDrawList.Count - 1 do
TSimpleNode(FDrawList[i]).DrawAt (0, i * FCharHeight); end;
Метод NodelnView предназначен для проверки видимости узла (опишите в разделе private класса TSimpleTree):
function TSimpleTree.NodelnView (Node: TSimpleNode): Boolean; begin
Result:=FDrawList.IndexOf(Node) > -1; end;
Для прорисовки дерева в методе Paint осуществляется рисование линий) внутренняя процедура которого DoDrawLines метода Paint:
procedure DoDrawLines; var MaxLevel: integer; i: integer; j: integer; begin
MaxLevel:=0; Canvas.Pen.Color:=LinesColor;
{устанавливаем цвет рисования линий} for i:=0 to FDrawList.Count - 1 do
{просматриваем все узлы дерева} with TSimpleNode(FDrawList[i]) do if FLevel > 0 then
begin
Canvas.MoveTo(FX + FCharWidth, FY + FCharHeight div 2); Canvas.LineTo(FX, FY + FCharHeight div 2); if (Flndex > 0) and
(not NodeInView(FParent.Children[Flndex - 1])) then Canvas.LineTo(FX, 0) else
if FIndex=0 then Canvas.LineTo (FX, FY - FCharHeight div 2) else Canvas.LineTo (FX, FParent.Children[Flndex - 1].FY); if Flndex < FParent.ChildrenCount - 1 then if not NodelnView(FParent.Children[Flndex + 1]) then Canvas.LineTo(FX, ClientHeight); if MaxLevel < FLevel then MaxLevel:=FLevel; end;
for i:=l to MaxLevel do begin j:=0; while (j < FDrawList.Count) and
(TSimpleNode(FDrawList[j]).Level <> i) do Inc(j); if j = FDrawList.Count then begin
Canvas.MoveTo((i + 1) * FCharWidth, 0); Canvas.LineTo((i + 1) * FCharWidth, ClientHeight); end; end; end;
Задание для самостоятельного выполнения
9.6. Поясните каждый оператор метода TSimpleTree.DoDrawLmes. Приведите все возможные варианты выполнения метода DoDrawLines.
Сформируем список FNodes узлов. В описание класса TSimple-Тгее введем следующие элементы:
private
FNodes: TList; {глобальный массив всех узлов}
FStartlndex: Integer; {абсолютный индекс узла,
с которого начинаем рисовать. Начальное значение равно нулю} FMaxLinesInView: Integer;
{максимальное количество отображаемых узлов} FMaxLines: Integer; {максимальное количество видимых узлов}
function GetNode(Index: integer): TSimpleNode; function GetNodeCount: integer;
...
public
property Nodes[Index: integer]: TSimpleNode
read GetNode; default; {возвращает узел под номером Index} property NodeCount: integer read GetNodeCount;
{общее количество узлов}
end;
Задание для самостоятельного выполнения
9.7. Реализуйте методы GetNode и GetNodeCount. He забудьте выделить память под переменную FNodes в конструкторе, а в деструкторе — освободить.
Формирование списка узлов дерева осуществляется в методе UpdateDrawList, который будет вызываться в ответ на каждое из следующих событий:
изменение размеров компонента,
добавление новых узлов, а скроллинг,
сворачивание или разворачивание какого-либо узла.
procedure TSimpleTree.UpdateDrawList;
function ListFull: Boolean; {проверка на полноту списка}
begin
Result:=FDrawList.Count >= FMaxLinesInView; end;
procedure FormDrawList(Node: TSimpleNode);
{формирование списка} var i: Integer; begin
if not ListFull then FDrawList.Add(Node); Inc(FMaxLines);
if Node. FExpanded then begin {если узел раскрыт}
for i:=0 to Node.ChildrenCount — 1 do FormDrawList(Node.Children[i]); Inc(FMaxLines, Node.ChildrenCount); end; end;
var i, Min: Integer; begin
FMaxLinesInView:=(ClientHeight div FCharHeight) + 1; FDrawList.Clear; FMaxLines:=0;
if FStartlndex + FMaxLinesInView > GetNodeCount then Min:=GetNodeCount -FStartlndex else Min:=FMaxLinesInView; for i:=FStartIndex to FStartlndex + Min — 1 do
FDrawList.Add (FNodes [i]); {добавляем в список узлы}
for i:=0 to GetNodeCount — 1 do
{вычисляем максимальное количество видимых узлов} with Nodes [i] do
if FParent = nil then Inc(FMaxLines)
else if FParent.FExpanded then Inc(FMaxLines);
UpdateScrollBar; {обновляем состояние ScrollBar}
end;
Обновление состояния компонента ScrollBar осуществляет метод UpdateScrollBar:
procedure TSimpleTree.UpdateScrollBar; var Scrolllnfo: TScrollInfo;
{структура, которая содержит параметры отображения полосы прокрутки} begin
if FMaxLinesInView >= FMaxLines then ShowScrollBar(Handle, SBJVERT, False)
{спрятать вертикальную полосу прокрутки} else begin
FillChar(Scrolllnfo, SizeOf(TScrollInfo), 0); Scrolllnfo.cbSize:=SizeOf{TScrollInfo);
Scrolllnfo. fMask:=SIF_ALL; {ограничиваетразмер страницы пропорционально отображению полосы прокрутки, минимальное и максимальное значение для диапазона скроллинга } Scrolllnfo.nMax:=FMaxLines;
{максимальное количество отображаемых строк}
ScrollInfo.nPage:=FMaxLinesInView; {общее количество строк} ScrollInfo.nPos:=FStartIndex; ShowScrollBar(Handle, SB_VERT, True);
{показать вертикальную полосу прокрутки} SetScrollInfo(Handle, SB_VERT, Scrolllnfo, True);
{установить назначенные параметры} end; end;
В раздел private класса TSimpleTree добавьте описание методов UpdateDrawList и UpdateScrollBar.
Метод UpdateScrollBar вызывается также и на изменение размеров компонента. Для полноценной поддержки скроллинга необходимо обрабатывать сообщение WMVSCROLL:
private
procedure WMVScroll(var Msg: TWMVScroll); message WM_VSCROLL;
procedure TSimpleTree.WMVScroll(var Msg: TWMVScroll); begin
case Msg.ScrollCode of SBJTHUMBPOSITION: begin
{прокручивает на абсолютную позицию.
Текущая позиция определяется значением параметра npos}
SetScrollPos(Handle, SB_VERT, Msg.Pos, True);
FStartlndex:=Msg.Pos;
end;
SB_LINEUP: {вверх}
if FStartlndex > 0 then Dec (FStartlndex) else exit;
SB_LINEDOWN: {прокрутить на одну строку вниз}
if FStartlndex < FMaxLines — FMaxLinesInView + 1 then
Inc(FStartlndex) else exit; else exit; end;
UpdateDrawList; {обновление списка}
Invalidate; {перерисовка компонента}
end;
Эксперимент. Сохраните модуль компонента. Запустите тестовое приложение. Убедитесь, что при перемещении «бегунка» полосы прокрутки компонент ScrollBar исчезает. ♦
Добавим в обработчик события WMSize вызов методов обновления списка узлов и перерисовки полосы прокрутки:
procedure TsimpleTree.WMSize(var Msg: TWMSize); begin inherited;
UpdateCharMetrics; UpdateDrawList; UpdateScrollBar; end;
Эксперимент. Запустите тестовое приложение. Отображается ли полоса прокрутки? Объясните, почему. ♦
Создаваемое дерево состоит как минимум из одного узла. Главный узел хранится в поле FMainNode типа TSimpleNode и доступен через свойство только для чтения MainNode.
Свойство SelectedNode типа TSimpleNode является указателем на выбранный узел, для записи в этот узел вызывается метод SetSelectedNode.
Опишите перечисленные свойства и методы класса TSimpleTree. Метод SetSelectedNode реализуется следующим образом:
procedure TSimpleTree.SetSelectedNode(const Value: TSimpleNode); var OldNode: TSimpleNode; begin if FSelectedNode <> Value then begin
{если выделенным должен стать другой узел} OldNode:=FSelectedNode; FSelectedNode:= Value; if (OldNode <> nil) and NodelnView(OldNode) then
{если узел, который был выделен ранее, виден —
его следует перерисовать} OldNode.Redraw;
if NodelnView(FSelectedNode) then {если выделенный сейчас узел видим, его также следует перерисовать}
FSelectedNode.Redraw; end; end;
В конструктор TSimpleTree.Create добавьте следующие операторы:
FNodes:=TList.Create;
FMainNode:^TSimpleNode.Create(Self);
FSelectedNode:= FMainNode;
FNodes.Add(FMainNode);
FMainNode.FAbsolutelndex:= 0;
Чтобы обработать события мыши, выполним перекрытие метода MouseDown:
procedure TSimpleTree.MouseDown (Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var Node: TSimpleNode; begin
inherited MouseDown(Button, Shift, X, Y);
{вызываем обработчик события нажатия кнопок мыши по умолчанию}
Node:=NodeAt [X, Y]; {определяем узел}
if Node <> nil then SelectedNode:=Node;
if (Shift = [ssLeft, ssDouble]) and (FSelectedNode <> nil) then
if FSelectedNode. FExpanded {если выделенный узел раскрыт}
then Collap