Contents

 

Андрей Бреслав

С Borland Delphi на "ты"


Создание компонент

(Материалы проверены с Borland Delphi v5.0 Enterprise UP1)


Часть 1: Простые компроненты и редакторы свойств


1. Подготовка

Как мы знаем из истории, промышленный переворот происходит тогда, когда люди переходят к производству средств производства. Это занятие применительно к Delphi — не есть осознанная необходимость, но есть весьма полезное в практике умение, позволяющее не падать духом при виде несовершенства того, что уже написали программисты Borland или третьих фирм.

Итак, с чего начинается проектирование визуальных (и не только визуальных) компонент для Delphi? В принципе — со знания Object Pascal, но коль скоро это само собой разумеется, то создадим новый пакет (dpk.gif (409 bytes)File/New/Package(Object Repository/New)) назовём его как понравится, скажем, custom.dpk, в options.gif (111 bytes) опциях проекта установим описаниe (Description): "Custom components", сохраним. Дальнейшая работа будет проходить в пределах этого пакета — так удобнее локализовать данные.

00.jpg (7889 bytes)

Скажем немного о пакетах в Delphi. Пакет — это логическая единица (модуль, физически — файл), содержащая ссылки на другие модули и интегрируемая (устанавливаемя) в IDE при компиляции (Component/Install packages). Проще говоря, просто сборник файлов, компилируемых вместе и доступных во время проектирования. Наш пакет будет содержать файл с исходным кодом компоненты и два файла для среды: регистрационный модуль и ресурс (Delphi Component Resource — *.dcr).

Теперь создаём новый компонент (Component/New component), задаём родительский класс (Ancestor type) TComponent, имя класса (Class nameTMgsBox(это связано с характером проекта), страницу на палитре компонент (Palette page) Custom, сохраняем модуль в той же директории, где и пакет (не обязательно, но удобно) под именем, скажем, MsgBox.pas.

01.jpg (18339 bytes)

И получаем в итоге следующий код:

unit MsgBox;

interface

uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
TMsgBox = class(TComponent)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
published
{ Published declarations }
end;

procedure Register;

implementation

procedure Register;
begin
RegisterComponents('Custom', [TMsgBox]);
end;

end.

О сути увиденного мы будем говорить в самом конце, а сейчас не забудем addf.gif (137 bytes) добавить созданный файл в пакет и сообразим всё-таки, какого же чёрта мы будем делать.

2. Суть проблемы

Собственно, какую компоненту мы будем создавать? Класс мы наследовали от TComponent, это говорит о том, что наша первая компонента будет невизуальной, она просто будет оболочкой на функции MessageBox из глубин Win32 API. По чести сказать, глубокого практического смысла в такой компоненте нет, более того, нерационально писать компоненту ради оболочки на функцию — она занимает в стеке много больше места, однако мы возьмём эту задачу, как удобную для примера.

Функция MessageBox, экспортируемая из библиотеки user32.dll, имеет сигнатуру

function MessageBox(hWnh: HWND; lpText: PChar; lpCaption: PChar; uType: Cardinal): Integer;

и занимается выводом на экран сообщений типа

03.jpg (3181 bytes)

что очень полезно любой программе. Она обладает кандовым Borland'овским аналогом MessageDlg, но он уж очень левый, и к тому же кнопки подписывает всегда по-английски, а не на языке ОС.

Параметры функции MessageBox

hWnd Идентификатор вызывающего окна — удобно ставить в Application.Handle
lpText Указатель на строку с сообщением
lpCaption Указатель на строку с заголовком
uType Флаги (сумма целых констант), задающие поведение и содержание окна

Значение uType — сумма следующих констант:

MB_ABORTRETRYIGNORE The message box contains three push buttons: Abort, Retry, and Ignore.
MB_OK The message box contains one push button: OK. This is the default.
MB_OKCANCEL The message box contains two push buttons: OK and Cancel.
MB_RETRYCANCEL The message box contains two push buttons: Retry and Cancel.
MB_YESNO The message box contains two push buttons: Yes and No.
MB_YESNOCANCEL The message box contains three push buttons: Yes, No, and Cancel.
MB_ICONEXCLAMATION An exclamation-point icon appears in the message box.
MB_ICONWARNING An exclamation-point icon appears in the message box.
MB_ICONINFORMATION An icon consisting of a lowercase letter i in a circle appears in the message box.
MB_ICONASTERISK An icon consisting of a lowercase letter i in a circle appears in the message box.
MB_ICONQUESTION A question-mark icon appears in the message box.
MB_ICONSTOP A stop-sign icon appears in the message box.
MB_ICONERROR A stop-sign icon appears in the message box.
MB_ICONHAND A stop-sign icon appears in the message box.
MB_DEFBUTTON1 The first button is the default button. MB_DEFBUTTON1 is the default unless MB_DEFBUTTON2, MB_DEFBUTTON3, or MB_DEFBUTTON4 is specified.
MB_DEFBUTTON2 The second button is the default button.
MB_DEFBUTTON3 The third button is the default button.
MB_DEFBUTTON4 The fourth button is the default button.
MB_APPLMODAL The user must respond to the message box before continuing work in the window identified by the hWnd parameter. However, the user can move to the windows of other applications and work in those windows. Depending on the hierarchy of windows in the application, the user may be able to move to other windows within the application. All child windows of the parent of the message box are automatically disabled, but popup windows are not.MB_APPLMODAL is the default if neither MB_SYSTEMMODAL nor MB_TASKMODAL is specified.
MB_SYSTEMMODAL Same as MB_APPLMODAL except that the message box has the WS_EX_TOPMOST style. Use system-modal message boxes to notify the user of serious, potentially damaging errors that require immediate attention (for example, running out of memory). This flag has no effect on the user's ability to interact with windows other than those associated with hWnd.
MB_TASKMODAL Same as MB_APPLMODAL except that all the top-level windows belonging to the current task are disabled if the hWnd parameter is NULL. Use this flag when the calling application or library does not have a window handle available but still needs to prevent input to other windows in the current application without suspending other applications.
MB_DEFAULT_DESKTOP_ONLY The desktop currently receiving input must be a default desktop; otherwise, the function fails. A default desktop is one an application runs on after the user has logged on.
MB_HELP Adds a Help button to the message box. Choosing the Help button or pressing F1 generates a Help event.
MB_RIGHT The text is right-justified.
MB_RTLREADING Displays message and caption text using right-to-left reading order on Hebrew and Arabic systems.
MB_SETFOREGROUND The message box becomes the foreground window. Internally, Windows calls the SetForegroundWindow function for the message box.
MB_TOPMOST The message box is created with the WS_EX_TOPMOST window style.

Остальные флаги — только для Windows NT

Возвращаемые значения функции MessageBox

IDABORT Abort button was selected.
IDCANCEL Cancel button was selected.
IDIGNORE Ignore button was selected.
IDNO No button was selected.
IDOK OK button was selected.
IDRETRY Retry button was selected.
IDYES Yes button was selected.

Как видно, эта функция тоже не отличается особенным изяществом, но всё же будет весьма полезно её освоить.

3. Первый шаг

3. Общее место

С чего начнём? С концепции интерфейса нашей компоненты: нам нужно задать функции все четыре её параметра через поля объекта. Пишем:

protected
{ Protected declarations }
  fhWnd: HWND;   //Ссылка на окно
  fCaption: String;   //Заголовок окна
  fText: String;   //Текст сообщения
  fFlags: Integer;   //Флаги

Задаём эти поля как protected из уважения к несчастным, желающим что-нибудь наследовать от нашего объекта. Далее сразу устанавливаем свойства (properties), соответсвующие этим полям.

3.2 Свойства

Свойства (properties) объекта — это особое представление полей, для которого заданы определённый способ чтения и записи, а также другие интересные вещи. Только свойства (к которым относятся и события (events)) могут отображаться в Object Inspector. Да и то только те, которые описаны в разделе published и не являются свойствами только для чтения (read only). Для таких свойств генерируется RTTI (RunTime type information), о которой желающие могут почитать Help к Delphi на слово "published".

Свойство декларируется ключевым словом property

published
{Published declarations}
  property hWnd: HWND read fhWnd write fhWnd default 1;   //Ссылка на окно
  property Caption: String read fCaption write fCaption;   //Заголовок окна
  property Text: String read fText write fText;   //Текст сообщения

  property Flags: DWord read fFlags write fFlags default MB_APPLMODAL+MB_OK+MB_ICONERROR;

  //Флаги

Как видно, тип свойства необходимо указывать, как и тип поля (кроме наследуемых свойств, о которых скажем чуть ниже). Далее: за ключевым словом read указываем идентификатор поля или метода на запись свойства; метод должен быть процедурой, получающей параметр одного с полем типа. За ключевым словом write указываем идентификатор поля или метода на чтение объекта; метод должен быть функцией, возвращающей значение одного со свойством типа. Одна из этих директив может быть опущена, и тогда значение в поле нельзя будет читать или писать извне (свойство уже не попадёт в Object Inspector), того же эффекта можно добиться и создав пустой метод: тогда, если Delphi будет в хорошем настроении, сойство в Object Inspector попадёт.

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

3.3 Наследование

Теперь займёмся наследованными от родительского класса методами и свойствами. Собственно, нас интересуют конструктор, деструктор и published свойства нашего объекта. Таких свойств всего два (посмотрите Help): Name и Tag, их можно не указывать как наследованные, они отобразятся в Object Inspector и так, но если бы мы наследовали наш объект, скажем, от TActionList, то его свойство Images необходимо было бы внести в раздел published:

published
{Published declarations}
  property Images; //Не указывая ни тип, ни атрибуты чтения/записи — они указаны в родительском классе.

К такому свойству можно добавлять не указанные в родительском классе директивы (например default).

Теперь перейдём к методам, и в первую очередь — к конструктору. Наш конструктор не должен делать ничего выдающегося — просто создать экземпляр объекта. Но это неинтересно! И мы создадим ещё один конструктор: он создаёт обект с уже заданными свойствами Text,Caption и Flags и может сразу показать диалог. Надо сказать, что, если мы хотим использовать объект как компоненту, то сигнатура его конструктора (хотя бы одного) должна соответствовать стандартной:

constructor Create(AOwner: TComponent);

если хочется обязательно включить туда другие параметры, то AOwner должен остаться первым, а другие — быть необязательными:

constructor Create(AOwner: TComponent; Param1: Integer = 0; Param2: String = '');

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

public
{Public declarations}
constructor
CreateShow(AOwner: TComponent; Msg, Cpt: String; Flags: Integer = 0; Show: Boolean = false);

Деструктор мы вообще трогать не будем — убьёт объект, и пусть.

Реализуем конструкторы:

public
{Public declarations}
  constructor
Create(AOwner: TComponent); override;
  constructor CreateShow(AOwner: TComponent; Msg, Cpt: String;  Flags: Integer = 0; Show: Boolean = false);
{...}
constructor TMsgBox.Create(AOwner: TComponent);
begin
  inherited
Create(AOwner);
  fFlags:= MB_APPLMODAL+MB_OK+MB_ICONERROR;
  fhWnd:= 1;
end;
constructor TMsgBox.CreateShow(AOwner: TComponent; Msg, Cpt: String;  Flags: Integer = 0; Show: Boolean = false);
begin
 
Create(AOwner);
  fText:= Msg;
  fCaption:= Cpt;
  fFlags:= Flags;
  if
Show then Execute;
end;

В коде конструктора мы забежали вперёд: мы не декларировали пока функцию Execute, но у нас всё ещё впереди...

4. Переходим к рабочей части

4.1 Свойства DResult и TrueResults

Теперь займёмся реализаций рабочей части объекта. Собственно, об интересующем пользователя в первую очередь методе уже сказано: это функция Execute, возвращающая Boolean. Так, какое Boolean — это хитро: мы введём ещё одно свойство — TrueResults, в котором будет лежать список возвращаемых MessageBox значений, в случае которых Execute возвращает true. Реализуем TrueResults поле DResult, содержащее возвращённое MessageBox значение:

type
  ids = idOK..idHelp;
  TMBTRes = set of ids;

{...}
protected
  fDResult: Integer; //В действительности результат запросто укладывается  в Byte, но уважим Borland
  fTrueResults: TMBTRes;
public
  property
DResult: Integer read fDresult; //Read Only!
published
  property
TrueResult: TMBTRes read fTrueResults write fTrueResults default [idOK, idYes];

Как видно, поле DResult представлено свойством только для чтения (ради того и свойство), и декларировано свойство в разделе public, чтобы не нагружать Delphi не нужной работой по проверке на RTTI совместимость. Не забудем внести в текст конструктора инициализацию свойств по умолчанию: fTrueResults:= [idOk, idYes].

4.2 Метод Execute

Теперь реализуем главный метод объекта:

public
  function
Execute: Boolean;
{...}
function TMsgBox.Execute: Boolean;
begin
  fDResult:= MessageBox(fhWnd, PChar(fText), PChar(fCaption), fFlags);
  Result:= fDResult in fTrueResults;
end
;

К этому методу мы ещё вернёмся...

4.3 Дополнительные свойства — стиль

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

  1. Набор кнопок
  2. Тип сообщения
  3. Кнопка по умолчанмю
  4. Относительно чего модально окно
  5. Наличие кнопки "Помощь"
  6. Стиль окна
  7. Атрибуты текста

Задаём соответствующие свойства:

published
  property
Buttons: TMBBtns read GetButtons write SetButtons default btnOK;
  property Icon: TMBIcon read GetIcon write SetIcon default icoError;
  property DefaultButton: Integer read GetDefButton write SetDefButton default 1;
  property Modality: TMBModality read GetModality write SetModality default modApplModal;
  property Help: Boolean read GetHelp write SetHelp default false;
  property WinStyle: TMBWinStyle read GetWinStyle write SetWinStyle default [];
  property TextAttr: TMBTextAttr read GetTextAttr write SetTextAttr default [];

Вносим в type до класса необходимые типы:

type
  TMBBtns = (btnOK, btnOKCancel, btnAbortRetryIgnore, btnYesNo, btnYesNoCancel, btnRetryCancel);
  TMBTextAttr = set of (taRight, taRTLReading);
  TMBModality = (modApplModal, modSystemModal, modTaskModal);
  TMBWinStyles = (modSetForeground, modDefaultDesktopOnly, modTopMost);
  TMBWinStyle = set of TMBWinStyles;
  TMBIcon = (icoNone, icoError, icoQuestion, icoWarning, icoInfo);

Вносим в private декларации методов, а в implementation — их реализации:

private
{ Private declarations }
  function
GetButtons: TMBBtns;
  procedure SetButtons(Value: TMBBtns);
  function GetIcon: TMBIcon;
  procedure SetIcon(Value: TMBIcon);
  function GetDefButton: Integer;
  procedure SetDefButton(Value: Integer);
  function GetModality: TMBModality;
  procedure SetModality(Value: TMBModality);
  function GetHelp: Boolean;
  procedure SetHelp(Value: Boolean);
  function GetWinStyle: TMBWinStyle;
  procedure SetWinStyle(Value: TMBWinStyle);
  function GetTextAttr: TMBTextAttr;
  procedure SetTextAttr(Value: TMBTextAttr);
{...}
implementation
{...}
function TMsgBox.GetButtons: TMBBtns;
begin
{...Код, возврщаем значение} 
end;

procedure
TMsgBox.SetButtons(Value: TMBBtns);
begin
{...Код, записываем значение в поле} 
end;

{etc...}

Код для этих подпрограмм довольно громоздкий и однообразный, желающие вникнуть, читайте моё Руководство по Булевой Алгебе и, если хочется, вникайте в исходники. В принципе, содержание их имеет опосредованное отношение к делу, скажу только, что все эти свойства читают и пишут в fFlags какой-нибудь бит, задающий то или другое свойство.

Итак, настал момент, когда, скопировав из исходников код, можно откомпилировать проект, однако сперва — главная интерфейсная задача: нарисуем компоненте иконку...

5. Переходим к интерфейсу в IDE

5.1 Пиктограмма для панели компонент

Это делается просто: из меню Tools (можно и из меню ПУСК) запускаем iedit.gif (397 bytes)Borland Image Editor. Там выбираем пункт меню File/New.../Component Resource File(.dcr), и сохраняем созданный ресурс в каталоге с модулем и пакетом, под именем MsgBox.dcr. Теперь в контекстном меню выбираем New/Bitmap, и видим вот такой диалог:

04.jpg (8210 bytes)

главная задача — это выставить размер в 24x24, остальное — Ваше личное дело: хотите — 2, хотите — 256, а хотите — и 16 цветов, и воля для фантазии в рисовании... Созданную картинку называем командой Rename контекстного меню по имени компонента — TMSGBOX.

Теперь всё, что осталось — это включить в модуль директиву {$R}, добавляющую ресурс:

{$R *.dcr}

Теперь настало время remf.gif (159 bytes) удалить и пакета модуль, а потом снова addf.gif (137 bytes) добавить его — он будет включен уже с ресурсом.

Острый момент — comp.gif (125 bytes) компилируем пакет... Если не получилось — обратитесь к разработчику или проверьтесь на исходниках.

Должно получиться следующее:

  1. Появилась вкладка Custom (последняя) на политре компонент

  2. На ней (если этой вкладки раньше не было) одна msg.bmp (2406 bytes) пиктограмма — та, которую Вы нарисовали, если поленились, Delphi использует def.gif (162 bytes) значок по умолчанию

  3. Компонента при добавлении к форме работает.

5.2 Что дальше...

Теперь мы переходим к части, сулящей нам многократные компиляции и новые возможности. Собственно, возможностей две: контекстное меню для всей компоненты и редакторы её свойств. Ими и займёмся. Для этого создаём новый чистый модуль (File/New/Module(Object Repository/New)) и сохраняем его в каталоге с модулем MsgBox, под именем MsgReg.pas.

В этот модуль нужно перенести текст процедуры Register модуля MsgBox, не забыв включить MsgBox в раздел Uses. На будущее пропишем там модули времени проектирования — Dsgnintf и TypInfo, а также модули Classes, содержащий процедуры регистрации, Forms, Controls, Windows, Graphics, SysUtils, Dialogs — уверяю, они все нам пригодятся.

6. Редактор компоненты

6.1 Кратко о классах TComponentEditor и TDefaultEditor

В IDE Delphi что-нибудь редактировать позволяют разные EDITOR'ы, за компоненты отвечают TComponentEditor и TDefaultEditor — с их помощью можно контролировать поведение компоненты при DblClick'е в IDE и формирование контекстного меню компоненты. Вообще, для понимания всего этого не мешает заглянуть в Help по теме Making components avalible at design time, ибо там описано (хотя и бестолково), кроме всего выше и ниже сказанного, ещё, например, добавление Help'а компоненте.

Собственно, TDefaultEditor происходит от TComponentEditor, но это вовсе не говорит о нём ничего хорошего. Это редактор для компонент, для которых не зарегистрирован никакой редактор: в контекстном меню ветер свищет, а по DblClick'у — редактирует OnClick, если нет, то OnChange, если нет, то OnCreate, если нет, то первое попавшееся событие. От него можно что-нибудь наследовать, если хочется по DblClick'у редактировать чей-нибудь обработчик.

TComponentEditor — родительский класс для всех редакторов компонент, предоставляет все вышеописанные возможности. Делается это так наследуем новый класс TMsgBoxEditor от TComponentEditor, и начинаем переопределять ему разные методы...

TMsgBoxEditor = class(TComponentEditor)
  function GetVerbCount: Integer; override;
  function GetVerb(Index: Integer): String; override;
  procedure ExecuteVerb(Index: Integer); override;
  procedure Edit; override;
end;

6.2 Создаём меню компоненты

За контекстное меню компоненты отвечают три метода класса TComponentEditor, в имени которых присутствует слово Verb (в принципе, это "глагол", но можно предположить, что "действие"):

  1. GetVerbCount — сколько пунктов есть в меню
  2. GetVerb — возвращает по индексу текст пункта
  3. EvecuteVerb — совершает по индексу действие пункта

Вот этих троих мы и переопределяем (в принципе, если концепция ясна, можно не придерживаться текста примера):

function TMsgBoxEditor.GetVerbCount: Integer;
begin
  Result:= 1;
end;

function TMsgBoxEditor.GetVerb(Index: Integer): String;
begin
  case Index of
    0: Result:= '&View message';
  end;
end;

procedure TMsgBoxEditor.ExecuteVerb(Index: Integer);
begin
  case Index of
    0: //'&View message'
      TMsgBox(Component).Execute;
  end;
end;

Мы говорим среде, что в нашем меню один пункт, что зовут его View message и что по нажатию этого пункта следует запустить метод Execute редактируемой компоненты. Доступ к компоненте соуществляется через свойство Component класса TComponentEditor, естественно, для вызова специализированных методов надо сообщить объекту, что он TMsgBox, а не TComponent: TMsgBox(Component).

Регистрируем наш редактор процедуой RegisterComponentEditor, в процедуре Register:

procedure Register;
begin
  RegisterComponents('Custom', [TMsgBox]);
  RegisterComponentEditor(TMsgBox, TMsgBoxEditor);
end;

Теперь IDE знает, что для всех компонент TMsgBox нужно использовать редактор TMsgBoxEditor, а не TDefaultEditor.

Итак, как и обещали, comp.gif (125 bytes) компиляция... Контекстное меню должно выглядеть примерно так:
(Вообще, удобно создать форму, добавить в неё компроненту и проверять на ней действенность...) При выборе первого пункта появляется диалог (на нём можно проверить, работают ли настройки).
05.gif (1825 bytes)

6.3 Создаём обработчик DblClick'а компоненты

За DblClick отвечает один-единственный метод Edit класса TComponentEditor — по сути, это обработчик события, и мы пишем:

procedure TMsgBoxEditor.Edit;
begin
  TMsgBox(Component).Execute;
end;

Здесь мы по DblClick'у просто вызываем метод Execute (показываем диалог).

Снова comp.gif (125 bytes) компиляция, по DblClick'у — диалог.

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

7. Создаём редакторы свойств

7.1 Категории свойств

Если вспомнить контекстное меню Object Inspector, можно вспомнить и то, что в секци Arrange есть пункт By category, и если его выбрать. получится очень неудобный список свойств. Несмотря на эту его особенность, находятся люди, использующие именно такое расположение. Оно основано на категориях свойств, их всего 12 и каждая представляет собой класс, в котором нужно зарегистрировать свойство.

Название класса Строка в Object Inspector Описание
TActionCategory Action Разнообразные настройки времени выполнения: Enabled, Hint, Visible, HelpContext
TDatabaseCategory Database Всё, связанное с базами данных: DataBaseName, SQL, BeforeScroll, OnCalcFields
TDragNDropCategory Drag, Drop and Docking Всё про "оторви и брось:-)": DragKind, DragCursor, OnStartDrag
THelpCategory Help and Hints Всё про помощь итд: HelpContext, Hint, OnHelp
TLayoutCategory Layout Всё про отображение во время проектирования: Top, Left, Align, AutoScroll
TLegacyCategory Legacy Устаревшее: Ctl3D
TLinkageCategory Linkage Связи между компонентами: DataSource, DataSet, FileEdit
TLocaleCategory Locale Всё про локализацию в языковой среде: BiDiMode, taRTLReading
TLocalizableCategory Localizable То, что может меняться при локализации: всякие строки (Caption), размеры (Height, Width) и пр.
TMiscellaneousCategory Miscellaneous Всё, что не попало ни в какую другую категорию
TVisualCategory Visual Всё, что связано с отображением: Align, Visible, Autosize, BorderIcons
TInputCategory Input Всё, связанное с вводом: Enabled, ReadOnly, OnKeyPressed, OnClick

Как видно, события тоже делятся на категории, а те в свою очередь перекрываются: Enabled встречается и как Action, и как Input.

Все категории-классы наследованы от TPropertyCategory, следовательно, от него (или от одного из вышеперечисленных) можно наследовать что-то своё. Повторяю, классификация такого рода кажется мне очень неудобной, однако, по категориям свойства легко скрыть, чтобы они не мешались в Object Inspector (тут актуальна категория Legacy), и вообще, полезно поддерживать все возможности, предоставляемые IDE.

Итак, большинство свойств нашего объекта относятся к визуальным категориям. Delphi большую их часть по незнанию отправит в категорию Miscellaneous. Надо сказать, что свойства могут регистрироваться в категориях глобально, то есть любое свойство сименем Enabled любой компоненты обязательно попадёт в категорию Action, потому что при регистрации его не был указан класс, для которого оно регистрировалось. Регистрация может осуществляться двумя путями, у которых есть "подпути":

  1. Функция RegisterPropertyInCategory регистрирует одно свойство в одной категории за вызов
    RegisterPropertyInCategory(THelpCategory, TMyButton, 'HelpContext'); — данный класс: свойство по имени
    RegisterPropertyInCategory(TVisualCategory, 'AutoSize'); — глобально: свойство по имени
    RegisterPropertyInCategory(TVisualCategory, TypeInfo(Integer)); — глобально: свойства по типу
    RegisterPropertyInCategory(TVisualCategory, TypeInfo(Integer), 'Width'); — глобально: свойство по имени и типу
  2. Функция RegisterPropertiesInCategory регистрирует несколько свойств в одной категории за вызов
    RegisterPropertiesInCategory(THelpCategory, TMyButton, ['HelpContext', 'Hint', 'ParentShowHint', 'ShowHint']); — данный класс: свойства по именам
    RegisterPropertiesInCategory(THelpCategory, ['HelpContext', 'Hint', 'ParentShowHint', 'ShowHint']); — глобально: свойства по именам
    RegisterPropertiesInCategory(TLocalizableCategory, TypeInfo(String)); — глобально: свойства по типу
    RegisterPropertiesInCategory(TLocalizableCategory, ['Text', TEdit]); — глобально: свойства по именам, принадлежности к классу или типу (или имя 'Text' или это свойство класса TEdit)

Как видно, вариантов великое множество. Мы воспользуемся тем, что нужно в нашем случае: локальной регистрацией многих свойств. Снова процедура Register:

procedure Register;
begin
  RegisterComponents('Custom', [TMsgBox]);
  RegisterComponentEditor(TMsgBox, TMsgBoxEditor);
  RegisterPropertyInCategory(TCopyrightCategory, TMsgBox, 'About');
  RegisterPropertiesInCategory(TVisualCategory, TMsgBox, ['Buttons', 'DefaultButton', 'WinStyle']);
  RegisterPropertiesInCategory(TLocalizableCategory, TMsgBox, ['TextAttr', 'Modality', 'Flags']);
end;

Третья строчка в теле процедуры повергает в смятение: не бывает ни свойства About у TMsgBox, ни категории TCopyrightCategory вообще. Это не страшно: сейчас они появятся.

7.2 Создание новых категорий свойств

Как понятно, наследовать новый класс мы будем от TPropertyCategory. Собственно, ничего особенного переопределять не придётся, нас интересует только то, что будет написано в Object Inspector. Мы пишем:

TCopyrightCategory = class(TPropertyCategory)
  class function Name: String; override;
end;
{...}
implementation
{...}
class function TCopyrightCategory.Name: String;
begin
  Result:= 'Copyright, etc';
end;

Отмечаю, что функция Name является атрибутом класса, а не объекта, что говорит о способе использования категорий самой IDE.

Вообще, ввести такую категорию, зарегистрировать глобально для всех свойств типа About, Copyright итд, а потом исключить из показываемых в Object Inspector может быть удобно.

7.3 Добавление свойства About

Теперь добавим свойство About к нашему объекту (надо же как-то закрепить за собой авторство!).

private
  {...}
  fAbout: String;
{...}
published
  {...}
  property About: String read fAbout write SetAbout;
{...}
implementation
{...}
constructor TMsgBox.Create(AOwner: TComponent);
begin
  {...}
  fAbout:= 'TMsgBox by Andrew Breslav, 2000';
end;
{...}
procedure TMsgBox.SetAbout(Value: String);
begin end;

Применена хитрость, уже упоминавшаяся выше: чтобы свойство попало в Object Inspector, но его не могли редактировать нигде, создан пустой метод на запись SetAbout. К этому свойству мы ещё вернёмся.

7.4 Окончательный вариант компоненты

Теперь, перед тем, как заняться редакторами свойств,  допишем, наконец до конца нашу компоненту. Добавим три события: OnSuccess (если Execute = true), BeforeExecute и AfterExecute (до и после запуска) и свойство Dialog (показывать или не показывать диалог), а так же — модификацию свойства hWnd.

7.4.1 События

Событие — это свойство процедурного типа. Любое published-свойство, имеющее тип procedure(...) of object или function(...) of object, является событием. События, как правило, не имеют методов на чтение и запись — просто поля. Проверка на предмет, задан обработчик события или нет, осуществляется функцией Assigned(var P), возвращающей true, если задан, и false, если нет.

Итак, пишем:

private
  {...}
  fOnSuccess: TNotifyEvent;
  fBExecute: TNotifyEvent;
  fAExecute: TNotifyEvent;
{...}
published
  {...}
  property OnSuccess: TNotifyEvent read fOnSuccess write fOnSuccess;
  property BeforeExecute: TNotifyEvent read fBExecute write fBExecute;
  property AfterExecute: TNotifyEvent read fAExecute write fAExecute;
{...}
implementation
{...}
function TMsgBox.Execute: Boolean;
begin
  if Assigned(fBExecute) then fBExecute(Self);
  fDResult:= MessageBox(fhWnd, PChar(fText), PChar(fCaption), fFlags);
  Result:= fDResult in fTrueResults;
  if (Result) and (Assigned(fOnSuccess)) then fOnSuccess(Self);
  if Assigned(fAExecute) then fAExecute(Self);
end;

Зарегистрируйте события, как вам понравится, скажем в категории Action:

RegisterPropertiesInCategory(TActionCategory, TMsgBox, ['OnSuccess', 'BeforeExecute', 'AfterExecute']);

7.4.2 Свойства Dialog и hWnd

Не всегда оповещение должно быть визуальным, иногда нужен лишь звук. За звук в Windows API отвечает функция MessageBeep, параметром которой служат ICON-флаги функции MessageBox, этим и воспользуемся.

private
  {...}
  fDialog: Boolean;
{...}
published
  {...}
  property Dialog: Boolean read fDialog write fDialog default true;
{...}
implementation
{...}
function TMsgBox.Execute: Boolean;
var
  Wnd: THandle;
  i: Integer;
begin
  if
Assigned(fBExecute) then fBExecute(Self);
  if not fDialog then
    begin
      if GetIcon = icoNone then i:= MB_OK
      else i:= 16*GetNumBit(fFlags, 2);
      MessageBeep(i);
    end
  else begin
            Wnd:= fhWnd;
            if fhWnd <= 32 then
              case fhWnd of
                1: Wnd:= Application.Handle;
                2: Wnd:= GetDesktopWindow;
               end;
            fDResult:= MessageBox(Wnd, PChar(fText), PChar(fCaption), fFlags);
          end;
  Result:= fDResult in fTrueResults;
  if (Result) and (Assigned(fOnSuccess)) then fOnSuccess(Self);
  if Assigned(fAExecute) then fAExecute(Self);
end;

В тело функции уже внесена модификация свойства hWnd: если значение меньше 32 (ссылка фиктивная), то по значению 1 присвоить ссылку на приложение, а по значению 2 — ссылку на Desktop Windows. Далее мы используем эту модификацию.

7.5 Редакторы свойств

Редакторы свойств — пожалуй самая обширная из легкодоступных возможностей в IDE Delphi. Собственно, редактор — это всё, что находится правее названия свойства  в Object Inspector. Как Вы, наверное, заметили для большинства свойств нашего объекта Delphi назначила редакторы по умолчанию, но оин не всегда устраивают нас. Редактор свойства — это, как понятно, класс, наследованный от TPropertyEditor (описанный, как и все встречавшиеся ранее, в модуле Dsgnintf). Есть, как понятно несколько стандартных редакторов:

Имя класса Описание
TOrdinalProperty Базовый класс для редакторов свойств перечислимых типов
TIntegerProperty Редактор свойств целых типов
TCharProperty Редактор свойств символьных типов
TEnumProperty Редактор свойств перечислимых типов (btndown.gif (94 bytes) выпадающий список)
TFloatProperty Редактор свойств типов с плавающей точкой
TStringProperty Редактор свойств строковых типов
TSetElementProperty Редактор элементов подмножеств, как Boolean на вхождение в значение
TSetProperty Редактор свойств множественных типов (с использованием TSetEelementProperty)
TClassProperty Редактор свойств типов-классов (имя класса в строке и plus.gif (58 bytes) для свойств)
TMethodProperty Редактор свойств-методов (события, btndown.gif (94 bytes) выпадающий список)
TComponentProperty Редактор свойств-компонентов (btndown.gif (94 bytes) выпадающий список со всеми досягаемыми)
TColorProperty Редактор свойства Color
TFontNameProperty Редактор свойства FontName (btndown.gif (94 bytes) выпадающий список всех шрифтов объекта Screen)
TFontProperty Редактор свойства Font

Создавая свои редакторы свойств, обращайтесь к Borland'овским реализациям стандартных — все примеры налицо.

Теперь стоит подробно разобрать класс TPropertyEditor:

RT Метод/свойство Параметры Тип Описание
p Designer - IFormDesigner Интерфейс редактора формы (не понадобится)
p PrivateDirectory - String Каталог, где можно хранить данные, загружать библиотеки (HKEY_CURRENT_USER\Software\Borland\Delphi\x.0\Globals\PrivateDir)
p PropCount - Integer Сколько объектов, редактируемых этим редактора выделено на форме
p Value - String То, что отображено в строке Object Inspector (read GetValue write SetValure)
m Activate - - Обработчик события выбора свойства в Object Inspector. Переопределив, можно менять свойства редактора динамически.
m AllEqual - Boolean Показывает, у всех ли выбранных объектов значение свойства одинаково
m AutoFill - Boolean Говорит, может ли Object Inspector дополнять недовведённые значения из выпадающего списка
m Destroy - - Деструктор (самому вызывать противопоказано!)
m Edit - - Обработчик DblClick'а по свойству в ObjectInspector
m GetAttributes - TPropertyAttributes Возвращает среде параметры редактора. Лучше переопределять и настраивать заново
m GetComponent Index: Integer TPersistent Возвращает компонент с номером Index из всех выбранных
m GetEditLimit - Integer Возвращает среде количество знаков, которые можно ввести как значение свойства
m GetFloatValue - Extended Возвращает значение свойства типа с плавающей точкой (первой из выбранных компонент)
m GetFloatValueAt Index: Integer Extended Возвращает значение свойства типа с плавающей точкой (компоненты с номером Index)
m GetXXXValue - XXX Возвращает значение свойства типа XXX (первой из выбранных компонент)
m GetXXXValueAt Index: Integer XXX Возвращает значение свойства типа XXX (компоненты с номером Index)
m SetXXXValue Value: XXX - Усианавливает значение свойства типа XXX (всех выбранных компонент)

XXX может быть:

  1. Float — плавающая точка

  2. Int64 — тип Int64

  3. Method — процедурный тип

  4. Ord — перечислимый тип

  5. Str — строковый тип

  6. Var — тип Variant

m GetName - String Возвращает среде имя свойства, точнее — что написать в Object Inspector
m GetProperties Proc: TGetPropEditProc - Даёт среде знать, что создавать для подсвойств данного свойства (актуально для классов и множеств). Процедуру, переданную в параметре следует запустить с каждым редактором подсвойства
m GetPropInfo - PPropInfo Возвращает указатель на информацию о свойстве: тип, значение по умолчанию итд
m GetPropType - PTypeInfo Возвращает тип свойства (переопределять себе дороже)
m GetValue - String Read для Value — строковое представление значения
m SetValue Value: String - Write для Value — интерпретация строкового представления значения
m GetValues Proc: TGetStrProc - Даёт среде знать, что отображать в выпадающем списке. Переданную процедуру нужно вызвать для каждой строки
m GetVisualValue* - String Говорит Object Inspector, что вывести в строке свойства
m Initialize - - Обработчик события OnCreate-BeforeUsed вызывается перед использованием редактора. Можно переопределять, если нужно совершить что-нибудь исключительное перед использованием.
m ListDrawValue const Value: string; Canvas: TCanvas; Rect: TRect; Selected: Boolean - Рисует пункт в выпадающем списке — OwnerDraw чистой воды, но не верьте Help'у: никакого атрибута paOwnerDrawList в природе не бывает!
m ListMeasureHeight const Value: string; Canvas: TCanvas; var AHeight: Integer - В AHeight хранится значение высоты элемента выпадающего списка: его можно изменить.
m ListMeasureWidth const Value: string; Canvas: TCanvas; var AWidth: Integer - В AWidth хранится значение ширины элемента выпадающего списка: его можно изменить.
m Modified - - Процедура, вызов которой сообщает Object Inspector, что значение свойства изменилось.
m Revert - - Возвращение начальных значений свойства всем выбренным компонентам
m ValueAvailable - Boolean Если в ComponentStyle выбранных объектов есть csCheckPropAvail, определяет, для всех ли таких компонент есть значения свойства.
m PropDrawName* ACanvas: TCanvas; const ARect: TRect;
ASelected: Boolean
- Процедура рисует имя свойства в Object Inspector
m PropDrawValue* ACanvas: TCanvas; const ARect: TRect;
ASelected: Boolean
- Процедура рисует строку значения свойства в Object Inspector, когда она не активна

* — не документировано
m — метод
p — свойство

Теперь в дополнение — что может возвращать главная, на мой взгляд функция — GetAttributes, и что это значит:

Значение (эл-т множества) Описание
paValueList Редактор представляет собой btndown.gif (94 bytes) выпадающий список
paSortList Список значений нужно отсортировать
paSubProperties Свойство имеет plus.gif (58 bytes) подсвойства (актуально для классов и множеств)
paDialog Кнопка btnell.gif (92 bytes) вызова диалога
paMultiSelect Отображается, если выделено более одного объекта
paAutoUpdate Значение устанавливается после каждого изменения свойсьва, а не только по нажатию Enter
paReadOnly Нельзя редактировать значение
paRevertable Object Inspector может вернуть исходное значение (отменить изменение)
paOwnerDrawList

ВНИМАНИЕ: В ПРИРОДЕ НЕТУ!!! И НЕ НУЖНО!


Теперь теоритическую часть можно считать законченной, переходим к созданию этих самых редакторов свойств.

7.6 Первый  опыт

Как, видимо, стало понятно, основной метод создание чего-то нового — это переопределение параметральных методов старого.

Этим и займёмся. Первое, что мы сделаем будет редактор для свойства Flags, призванный просто запретить редактирование свойства во время проектирования — всё равно ничего не понятно.

Наследуем новый класс от TIntegerProperty и переопределяем метод GetAttributes:

TROIntProperty = class(TIntegerProperty)
  function GetAttributes: TPropertyAttributes; override;
end;
implementation
{...}
function TROIntProperty.GetAttributes: TPropertyAttributes;
begin
  Result:= inherited GetAttributes + [paReadOnly];
end;

Вот и всё. Удобно, не правда ли? Регистрируем созданный редактор в процедуре Register:

procedure Register;
begin
  RegisterComponents('Custom', [TMsgBox]);
  {...}
  RegisterPropertyEditor(TypeInfo(DWord), TMsgBox, 'Flags', TROIntProperty);
end;

comp.gif (125 bytes) Компиляция — свойство не редактируется!

7.7 Редактор свойства Icon

Теперь займёмся действительно созданием чего-то интерерсного, и сперва это будет редактор свойства Icon. Задача наша состоит в том, чтобы в списке отображались те иконки, которые впоследствии отобразятся на окне диалога. Скажем сразу, что эти иконки — стандартные, входят в число 6 стандартных пиктограмм модуля user32.dll ядра Windows. Достать их можно, если передать нулевую ссылку и предопределённый идентификатор функции LoadIcon Win32 API, которая с радосстью вернёт нам HICON. Впоследствии его можно рисовать где угодно с помощью функции того же API DrawIcon.

Итак, наследуем от TEnumProperty новый класс и переопределяем его методы:

TMBIconProperty = class(TEnumProperty)
  function GetAttributes: TPropertyAttributes; override;
  procedure ListDrawValue(const Value: string; ACanvas: TCanvas;
                                            const ARect: TRect; ASelected: Boolean); override;
  procedure ListMeasureWidth(const Value: string; ACanvas: TCanvas;
                                                 var AWidth: Integer); override;
  procedure ListMeasureHeight(const Value: string; ACanvas: TCanvas;
                                                 var AHeight: Integer); override;
  procedure PropDrawValue(ACanvas: TCanvas; const ARect: TRect;
                                              ASelected: Boolean); override;
end;

Реализуем GetAttributes: список, можно много выбирать, можно и отменить:

function TMBIconProperty.GetAttributes: TPropertyAttributes;
begin
  Result:= [paValueList, paMultiSelect, paRevertable];
end;

Реализуем ListMeasureHeight и ListMeasureWidth:

procedure TMBIconProperty.ListMeasureWidth(const Value: string; ACanvas: TCanvas; var AWidth: Integer);
begin
  AWidth:= AWidth+GetSystemMetrics(SM_CXSMICON);
end;

procedure TMBIconProperty.ListMeasureHeight(const Value: string; ACanvas: TCanvas; var AHeight: Integer);
begin
  if AHeight < GetSystemMetrics(SM_CYSMICON) then
    AHeight:= GetSystemMetrics(SM_CYSMICON);
end;

Смысл кода этих методов а том, чтобы всё, что мы хотим нарисовать, поместилось в пункт списка, и при этом уже определённые сваойства не пострадали. GetSystemMetrics возарщает размерв системных элементов (в данном случае маленьких иконок) — его мы и используемдля определения ширины и высоты области элемента.

Реализуем ListDrawValue:

procedure TMBIconProperty.ListDrawValue(const Value: string; ACanvas: TCanvas; const ARect: TRect; ASelected: Boolean);
var
  IDI: PChar;
begin
  IDI:= nil;
  if Value = 'icoError' then
    IDI:= IDI_ERROR
  else if Value = 'icoQuestion' then
            IDI:= IDI_QUESTION
  else if Value = 'icoWarning' then
            IDI:= IDI_WARNING
  else if Value = 'icoInformation' then
            IDI:= IDI_INFORMATION;
  with ACanvas do
    try
      if ASelected then Brush.Color:= clHighlight;
      FillRect(ARect);
      DrawIconEx(ACanvas.Handle, ARect.Left, Arect.Top, LoadIcon(0, IDI), GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), 0,                              0, DI_NORMAL or DI_DEFAULTSIZE);
    finally
      inherited ListDrawValue(Value, ACanvas, Rect(ARect.Left+GetSystemMetrics(SM_CXSMICON), Arect.Top, ARect.Right, ARect.Bottom), ASelected);
    end;
end;

Суть в том, чтобы в зависимости от значения рисуемого пункта, отобразить ту или другую иконку из числа стандартных (IDI_XXX — идентификаторы, определённые в модуле Windows), а потом — пусть Delphi рисует то, что хотела раньше, но чуть-чуть правее. Размеры картинки нам даёт небезызвестная GetSystemMetrics.

Теперь, всё, что мы хотим, кроме всеобщего счастья — это чтобы пиктограмма отображалась и тогда, когда списка нет. К сожалению, непосильной представляется задача рисовать иконку во время редактирования свойства, однако, в любое другое время — метод PropDrawValue всегда с нами:

procedure TMBIconProperty.PropDrawValue(ACanvas: TCanvas; const ARect: TRect;
ASelected: Boolean);
begin
  if
GetVisualValue <> '' then
    ListDrawValue(GetVisualValue, ACanvas, ARect, false)
  else
    inherited PropDrawValue(ACanvas, ARect, ASelected);
end;

(Этот метод честно скопирован с Borland'овского оригинала.) Здесь всё, что мы делаем, это говорим, что когда в строке значения свойства что-нибудь отображено (у всех выбранных объектов значение свойства одинаково), можно нарисовать на ней то же, что и при рисовании в списке невыбранного элемента (мы нигде ничего не говорим про фон — он остаётся по умолчанию).

Регистрируем созданный редактор в процедуре Register:

procedure Register;
begin
  RegisterComponents('Custom', [TMsgBox]);
  {...}
  RegisterPropertyEditor(TypeInfo(TMBIcon), TMsgBox, 'Icon', TMBIconProperty);
end;

comp.gif (125 bytes) Компиляция — всё должно работать.

7.8 Работа с ресурсами: редактор свойства Modality

Отличие редактора свойства Modality в том, что символы только двух из трёх его значений доступны как системные, третий символ придётся включить самим. Отличие составит только метод ListDrawValue. Картинку, в принципе, можно подгружать откуда угодно, но надёжнее всего — из прилинкованного к пакету ресурса.

Создайте ресурс чем угодно: Delphi Image Editor, Resources WorkShop, Symantec Resource Studio, etc...

Положите в него под именем "icoTaskModal" соответствующую идее иконку, я взял её из набора от MS Visual Studio (честное слово, кроме иконок, этот пакет представляет мало хорошего): icoTaskModal.gif (246 bytes). Теперь сохраним ресурс под именем MsgReg.res и прилинкуем его упоминавшейся директивой {$R}:

{$R *.res}

В код включено условное изменение ссылки на экземпляр приложения: 0 для стандартных и hInstance для ресурсной иконки:

procedure TMBModalProperty.ListDrawValue(const Value: string; ACanvas: TCanvas; const ARect: TRect; ASelected: Boolean);
var
  IDI: PChar;
  Inst: THandle;
begin
  IDI:= IDI_APPLICATION;
  Inst:= 0;
  if Value = 'modTaskModal' then
    begin
      IDI:= 'icoTaskModal';
      Inst:= hInstance;
    end
  else if Value = 'modSystemModal' then
            IDI:= IDI_WINLOGO;
  with ACanvas do
    try
      if ASelected then Brush.Color:= clHighlight;
      FillRect(ARect);
      DrawIconEx(ACanvas.Handle, ARect.Left, Arect.Top, LoadIcon(Inst, IDI), GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON),                              0,0, DI_NORMAL or DI_DEFAULTSIZE);
    finally
      inherited ListDrawValue(Value, ACanvas, Rect(ARect.Left+GetSystemMetrics(SM_CXSMICON), Arect.Top, ARect.Right, ARect.Bottom), ASelected);
    end;
end;

Надо сказать, что при работе с ресурсами вообще, а тем более — в пакетах, стоит соблюдать предельную осторожность, использовать как можно больше Win32 API, а из VCL — только специализированные методы. Это важно, потому что ошибка системного уровня в Вашем пакете немедленно вызовет критическую ошибку Delphi, и хорошо, если Вы, а не нервный заказчик, потеряете несохранённые данные.

7.9 Расслабимся и развлечёмся: создание редактора свойства About

Редактор свойства About — задача чисто эстетическая, не имеющая под собой практически никаких технических целей (только что, если Вы поставляете скомпилированный пакет, никто не сможет его украсть, да и то, я Вас уверяю, не перевелись ещё умельцы в земле Русской...). Поэтому здесь можно просто подурачиться и применить мало или узко применимые методы.

Сделаем так: пусть...

  1. в Object Inspector отображается не лаконичное "About", а развёрнутое "About this component"
  2. этот текст выводится красным цветом
  3. ему предшествует пиктограмма компоненты
  4. по DblClick'у показывается диалог с текстом
  5. вместо выпадающего списка демонстрируется Ваш логотип
TAboutProperty = class(TPropertyEditor)
  function GetAttributes: TPropertyAttributes; override;
  function GetName: String; override;
  procedure PropDrawName(ACanvas: TCanvas; const ARect: TRect;
                                             ASelected: Boolean); override;
  procedure Edit; override;
  procedure ListDrawValue(const Value: string; ACanvas: TCanvas;
                                           const ARect: TRect; ASelected: Boolean); override;
  procedure ListMeasureWidth(const Value: string; ACanvas: TCanvas;
                                                var AWidth: Integer); override;
  procedure ListMeasureHeight(const Value: string; ACanvas: TCanvas;
                                                var AHeight: Integer); override;
  function GetValue: string; override;
  procedure GetValues(Proc: TGetStrProc); override;
end;

GetAttributes: список, не редактируемо, выделяйте сколько угодно:

function TAboutProperty.GetAttributes: TPropertyAttributes;
begin
  Result:= [paValueList, paReadOnly, paMultiSelect];
end;

GetName: "About this component":

function TAboutProperty.GetName: String;
begin
  Result:= 'About this component...';
end;

PropDrawName: рисуем иконку, и сразу за ней текст (красный):

procedure TAboutProperty.PropDrawName(ACanvas: TCanvas; const ARect: TRect; ASelected: Boolean);
begin
  DrawIconEx(ACanvas.Handle, 0, ARect.Top, LoadIcon(hInstance, 'logo'), GetSysTemMetrics(SM_CXSMICON), GetSysTemMetrics(SM_CXSMICON),                          0,0,DI_NORMAL or DI_DEFAULTSIZE);
  ACanvas.Font.Color:= clRed;
  inherited PropDrawName(ACanvas, Rect(GetSystemMetrics(SM_CXSMICON), ARect.Top, ARect.Right, ARect.Bottom), ASelected);
end;

Edit: показываем незамысловатый диалог: ShowMessage (если не нравится, применяйте MessageBox):

procedure TAboutProperty.Edit;
begin
  ShowMessage('TMsgBox - Delphi VCL component'#13#10 +
                          'Created and used as an exaple'#13#10 +
                          'in the Tutorial of designing components'#13#10 +
                          '© Andrew Breslav, St.Petersburg, Russia, y. 2000');
end;

ListMeasureXXX: зависит от того, что Вы нарисовали себе в логотип. Я изобразил следующее:

logo.gif (2166 bytes)

имеет оно размеры 110x40, исходя из этого:

procedure TAboutProperty.ListMeasureWidth(const Value: string; ACanvas: TCanvas;
                                                                         var AWidth: Integer);
begin
  AWidth:= 110;
end;

procedure TAboutProperty.ListMeasureHeight(const Value: string; ACanvas: TCanvas;
                                                                         var AHeight: Integer);
begin
  AHeight:= 40;
end;

ListDrawValue: настал тот момент, когда вновь нужно обратиться к ресурсу (запихните туда BMP, назовите "About"), но здесь Win32 API уже мало полезен: придётся воспользоваться стандартным классом TBitMap из модуля Graphics и его методом LoadFromResourceName. Итак, заливаем фон, загружаем, центрируем, рисуем:

procedure TAboutProperty.ListDrawValue(const Value: string; ACanvas: TCanvas;
                                                                    const ARect: TRect; ASelected: Boolean);
var
  Bmp: TBitMap;
begin
  ACanvas.Brush.Color:= $0033FFFF;
  ACanvas.FillRect(ARect);
  Bmp:= TBitmap.Create;
  Bmp.LoadFromResourceName(hInstance, 'About');
  ACanvas.Draw((ARect.Right - 110) div 2,0,Bmp);
  Bmp.Free;
end;

GetValue: Чтобы видеть текст, нужно его вернуть: TPropertyEditor не предствляет, как это сделать:

function TAboutProperty.GetValue: string;
begin
  Result:= TMsgBox(GetComponent(0)).About;
end;

GetValues: чтобы список отображался, в нём должен быть хотя бы один элемент:

procedure TAboutProperty.GetValues(Proc: TGetStrProc);
begin
  Proc('');
end;

Регистрируем созданный редактор в процедуре Register:

procedure Register;
begin
  RegisterComponents('Custom', [TMsgBox]);
  {...}
  RegisterPropertyEditor(TypeInfo(String), TMsgBox, 'About', TAboutProperty);
end;

comp.gif (125 bytes) Компиляция — весёленький About!

**Кстати, если теперь посмотреть на категории свойств, нашей категории "Copyright, etc" мы не увидим: Delphi распознаёт свойства по именам, возвращаемым GetName редактора, а поскольку мы возвращаем не лаконичное "About", а развёрнутое "About this component", регистрация краткого имени для этого свойства недействительна, напишите:

procedure Register;
begin
  RegisterComponents('Custom', [TMsgBox]);
  {...} 
 
RegisterPropertyInCategory(TCopyrightCategory, TMsgBox, 'About this component...');
  {...}

  RegisterPropertyEditor(TypeInfo(String), TMsgBox, 'About', TAboutProperty);
end;

comp.gif (125 bytes) Компиляция — теперь лучше.

7.10 Усложнение задачи: создание редактора свойства hWnd

Как мы помним, для это свойство обладает некоторой спецификой: его значение интерпретируется перед тем, как быть передано в функцию MessageBox. Поскольку задать значене ссылки руками, то есть числом, и во время проектированя невозможно, не и смысла предоставлять такой интерфейс. Лучше красиво обработать три варианта значения: ноль, приложение, Desktop. Хорошо то, что для этих значений не нужно вводить типов и идентификаторов: они известны, и их число конечно: три.

Итак, что мы хотим:

  1. Свойство вручную не редактируемо
  2. Выпадающий список с пиктограммами
  3. Строки в списке: "Application", "Desktop", "NULL"
  4. По выбору строки свойство устанавливается соответственно в 1,2,0

Декларируем:

TMBHWndProperty = class(TIntegerProperty)
  function GetAttributes: TPropertyAttributes; override;
  procedure ListDrawValue(const Value: string; ACanvas: TCanvas;
                                            const ARect: TRect; ASelected: Boolean); override;
  procedure ListMeasureWidth(const Value: string; ACanvas: TCanvas;
                                                 var AWidth: Integer); override;
  procedure ListMeasureHeight(const Value: string; ACanvas: TCanvas;
                                                  var AHeight: Integer); override;
  procedure PropDrawValue(ACanvas: TCanvas; const ARect: TRect;
                                              ASelected: Boolean); override;
  function GetValue: String; override;
  procedure SetValue(const Value: String); override;
  procedure GetValues(Proc: TGetStrProc); override;
end;

С точки зрения рисования списка всё должно быть ясно. Займёмся делом.

GetAttributes: список, R/O, выбирайте хоть все, хочешь — отмени:

function TMBHWndProperty.GetAttributes: TPropertyAttributes;
begin
  Result:= [paValueList, paReadOnly, paMultiSelect, paRevertable];
end;

GetValues: те самые три строки:

procedure TMBHWndProperty.GetValues(Proc: TGetStrProc);
begin
  Proc('Application');
  Proc('Desktop');
  Proc('NULL');
end;

G|SetValue: интепретировать число в строку и обратно:

function TMBHWndProperty.GetValue: String;
var
  i: Integer;
begin
  for i:= 0 to PropCount - 1 do
    begin
      Result:= IntToStr(TMsgBox(GetComponent(i)).hWnd);
      case TMsgBox(GetComponent(i)).hWnd of
        1: Result:= 'Application';
        2: Result:= 'Desktop';
        else Result:= 'NULL';
      end;
    end;
end;

procedure TMBHWndProperty.SetValue(const Value: String);
var
  i: Integer;
begin
  for i:= 0 to PropCount - 1 do
    begin
      if Value = 'NULL' then TMsgBox(GetComponent(i)).hWnd:= 0
      else if Value = 'Application' then TMsgBox(GetComponent(i)).hWnd:= 1
      else if Value = 'Desktop' then TMsgBox(GetComponent(i)).hWnd:= 2
      else TMsgBox(GetComponent(i)).hWnd:= StrToInt(Value);
    end;
end;

Регистрируем созданный редактор в процедуре Register:

procedure Register;
begin
  RegisterComponents('Custom', [TMsgBox]);
  {...}
  RegisterPropertyEditor(TypeInfo(HWND), TMsgBox, 'hWnd', TMBHWndProperty);
end;

comp.gif (125 bytes) Компиляция — должно работать.

7.11 Высокий уровень сложности: свойство TrueResults

Вся сложность состоит в том что

  1. нет идентификаторов для элементов множества (кто не заметил — свойства до сих пор нет в Object Inspector)
  2. редактор должен состоять из двух: для множества и для элементов,  но класс TSetElementProperty написан безголово с точки зрения дальнейшего наследования: очень важное поле упрятано в private, следовательно придётся просто скопировать код этого класса и надставить своим... Занятие не отвечает концепциям ООП, но что делать?

Сперва решим вопрос с идентификаторами: создадим прямо в модуле MsgReg (в секции implementation) тип TRT:

type
  TRT = (idOK, idCancel, idAbort, idRetry, idIgnore, idYes, idNo, idClose, idHelp);

Этот тип заменит библиотеку имён.

Создаём редактор для множества:

TMBTRProperty = class(TSetProperty)
  function GetValue: String; override;
  procedure GetProperties(Proc: TGetPropEditProc); override;
end;

GetValue: идею честно крадём у Borland:

function TMBTRProperty.GetValue: String;
var
  S: TIntegerSet;
  TpInfo: PTypeInfo;
  I: Integer;
begin
  Integer(S):= GetOrdValue;
  TpInfo:= TypeInfo(TRT);
  Result := '[';
  for I := 0 to SizeOf(Integer) * 8 - 1 do
    if I in S then
      begin
        if Length(Result) <> 1 then Result := Result + ',';
        Result := Result + GetEnumName(TpInfo, I - 1);
      end;
  Result := Result + ']';
end;

Собственно, изменено в оригинальном коде очнь мало: вторая строчка в теле процедуры: сложное определение типа заменено его непосредственным идентификатором — мы подменяем любой тип, редактируемого свойства типом TRT. Суть Borland'овского кода в том, что если представить множество как TIntegerSet, можно получить доступ к его элементам через биты числа, хотя эта технология ограничена множествами с не более чем 32 элементами (иначе их не дают вносить в published).

GetProperties: тоже крадём и также реализуем подмену:

procedure TMBTRProperty.GetProperties(Proc: TGetPropEditProc);
var
  I: Integer;
begin
  with
GetTypeData(GetTypeData(GetPropType)^.CompType^)^ do
    for I := MinValue to MaxValue do
      Proc(TMBTResProperty.Create(Self, I));
end;

Код станет ясен до конца, когда мы реализуем TMBResProperty — для элементов.

Им и займёмся: из модуля Dsgnintf честно переписываем декларацию и реализацию TSetElementProperty (подчёркнуты привнесённые места):

TMBTResProperty = class(TNestedProperty)
protected
  FElement: Integer;
  constructor Create(Parent: TPropertyEditor; AElement: Integer); reintroduce;
public
  function AllEqual: Boolean; override;
  function GetAttributes: TPropertyAttributes; override;
  function GetName: string; override;
  function GetValue: string; override;
  procedure GetValues(Proc: TGetStrProc); override;
  procedure SetValue(const Value: string); override;
end;

Директива reintroduce связана с замещением конструктора TPropertyEditor в классе TNestedProperty — в целях самореализации. Новшеством в отношении FElement является его перенесение из private в protected (это и есть то самое поле!) — из жалости к ближнему.

constructor TMBTResProperty.Create(Parent: TPropertyEditor; AElement: Integer);
begin
  inherited Create(Parent);
  FElement := AElement;
end;

function TMBTResProperty.AllEqual: Boolean;
var
  I: Integer;
  S: TIntegerSet;
  V: Boolean;
begin
  Result := False;
  if PropCount > 1 then
    begin
      Integer(S) := GetOrdValue;
      V := FElement in S;
      for I := 1 to PropCount - 1 do
        begin
          Integer(S) := GetOrdValueAt(I);
          if (FElement in S) <> V then Exit;
        end;
    end;
  Result := True;
end;

function TMBTResProperty.GetAttributes: TPropertyAttributes;
begin
  Result := [paMultiSelect, paValueList, paSortList];
end;

function TMBTResProperty.GetName: string;
begin
  Result:= GetEnumName(TypeInfo(TRT), FElement - 1);
end;

function TMBTResProperty.GetValue: string;
var
  S: TIntegerSet;
begin
  Integer(S) := GetOrdValue;
  Result := BooleanIdents[FElement in S];
end;

procedure TMBTResProperty.GetValues(Proc: TGetStrProc);
begin
  Proc(BooleanIdents[False]);
  Proc(BooleanIdents[True]);
end;

procedure TMBTResProperty.SetValue(const Value: string);
var
  S: TIntegerSet;
begin
  Integer(S) := GetOrdValue;
  if CompareText(Value, 'True') = 0 then
    Include(S, FElement) else
    Exclude(S, FElement);
  SetOrdValue(Integer(S));
end;

Как видно, всё делалось исключительно ради метода GetName! Господа! Умоляю вас, пишите свои классы по-человечески!

Регистрируем созданный редактор в процедуре Register (обращаю внимание на то, что редактор для элементов регистрировать не надо):

procedure Register;
begin
  RegisterComponents('Custom', [TMsgBox]);
  {...}
  RegisterPropertyEditor(TypeInfo(TMBTRes), TMsgBox, 'TrueResults', TMBTRProperty);
end;

comp.gif (125 bytes) Компиляция — должно работать.

8. Конец первой части

Должен сообщить Вам приятную вещь: если прочитанное Вам не прошло бесследно, то Вы должны знать, как писать пока простые компоненты и редакторы свойств к ним (мы не разбирали свойства-классы (никак не удалось их запихать в TMsgBox) — если торопитесь, загляните вперёд, если чувствуете уверенность, то в модуль Dsgnintf: TFontProperty)). Далее, во второй части мы займёмся проектированием визуальной компоненты, и тут уже главным будет объект, а не редакторы свойств. Правда и всей широты вопроса охватить не удастся.

Hosted by uCoz