Инспектор объектов и метаданные

Класс TFont_INFO порожден от класса TGsvObjectInspectorTypeFontInfo, в котором переопределены методы ShowDialog и ObjectToString. Метод ShowDialog вызывает стандартный Windows-диалог выбора шрифта, а метод ObjectToString выводит в качестве значения свойства Font строку, включающую имя шрифта и его размер. Свойства стиля и цвета заданы собственными метаклассами:

type
 TGsvColor16_INFO = class(TGsvObjectInspectorTypeListInfo)
 protected
  class function ListEnumItems(Index: Integer):
  PGsvObjectInspectorListItem; override;
 public
  class function TypeInfo: PGsvObjectInspectorPropertyInfo;
  override;
 end;
 TFontStyles_INFO = class(TGsvObjectInspectorTypeSetInfo)
 public
  class function ChildrenInfo(Index: Integer):
  PGsvObjectInspectorPropertyInfo; override;
 end;
 class function TGsvColor16_INFO.ListEnumItems(Index: Integer):
  PGsvObjectInspectorListItem;
 const
  DSK: array[0..15] of TGsvObjectInspectorListItem = (
  ( Name: 'Черный'; Data: clBlack ),
  ( Name: 'Коричневый'; Data: clMaroon ),
  ( Name: 'Темнозеленый'; Data: clGreen ),
  ......
  ( Name: 'Розовый'; Data: clFuchsia ),
  ( Name: 'Голубой'; Data: clAqua ),
  ( Name: 'Белый'; Data: clWhite )
  );
 begin
  if Index <= High(DSK) then Result := @DSK[Index]
  else  Result := nil;
 end;
 class function TGsvColor16_INFO.TypeInfo:
  PGsvObjectInspectorPropertyInfo;
 const
  DSK: TGsvObjectInspectorPropertyInfo = (
  Caption: 'Цвет'; Kind: pkDropDownList
  );
 begin
  Result := @DSK;
 end;
 class function TFontStyles_INFO.ChildrenInfo(
  Index: Integer): PGsvObjectInspectorPropertyInfo;
 const
  DSK: array[0..2] of TGsvObjectInspectorPropertyInfo = (
  ( Name: 'Style'; Caption: 'Полужирный'; Kind: pkBoolean;
  Tag: Ord(fsBold) ),
  ( Name: 'Style'; Caption: 'Курсив'; Kind: pkBoolean;
  Tag: Ord(fsItalic) ),
  ( Name: 'Style'; Caption: 'Подчеркнутый'; Kind: pkBoolean;
  Tag: Ord(fsUnderline) )
  );
 begin
  if Index <= High(DSK) then Result := @DSK[Index]
  else  Result := nil;
 end;

Метакласс TGsvColor16_INFO порожден от TGsvObjectInspectorTypeListInfo, который переопределяет методы IntegerToString, StringToInteger и FillList, а для задания списка перечислений вводит новый виртуальный метод ListEnumItems - этот метод напоминает ChildrenInfo, но возвращает не типовые метаданные, а данные по каждому элементу перечисления - его имя и ассоциированное с ним значение. Метакласс TFontStyles_INFO порожден от TGsvObjectInspectorTypeSetInfo, переопределяющего метод IntegerToString. Вот каким получится вид инспектора при инспектировании объекта типа TLabel для определенных нами метаданных:
clip0270
Может показаться, что нам потребовалось довольно много описаний, но нужно учесть, что все определенные выше метаклассы могут быть использованы в большом числе других классов, создавая, таким образом, дерево классов метаданных. Например, если бы мы захотели теперь создать метаданные для TButton, то нам потребовалось определить всего один метакласс TButton_INFO.
Вы, вероятно, уже обратили внимание на то, как образуются имена метаклассов - к имени инспектируемого типа добавляется суффикс _INFO. Это основное соглашение об именовании метаклассов. Кроме него, можно вводить дополнительные соглашения. Если при инспектировании объектов предполагается учет категории пользователей, то имя метакласса может состоять из имени класса, категории и суффикса, например, TButton_EXPERT_INFO. Возможен и другой вариант, при котором метаклассы различных категорий пользователей располагаются в различных DLL.
Последний вопрос, который остался неосвещенным - это реестр метаданных. Для того, чтобы инспектор мог получить доступ к метаданным, инспектор должен на основе типа объекта, который передан ему для инспекции, сформировать имя соответствующего метакласса и запросить реестр о ссылке на метакласс. Метаклассы, в свою очередь, должны иметь возможность регистрировать себя в реестре. Для этого имеются три глобальных процедуры:

procedure GsvRegisterTypeInfo(AClass: TGsvObjectInspectorTypeInfoClass);

procedure GsvRegisterTypesInfo(AClasses:

  array of TGsvObjectInspectorTypeInfoClass);

function GsvFindTypeInfo(const ATypeName: String):

  TGsvObjectInspectorTypeInfoClass;

Процедура GsvRegisterTypeInfo регистрирует метакласс в реестре метаданных. Регистрируемый метакласс передается по ссылке на класс, которая определяется как:

TGsvObjectInspectorTypeInfoClass = class of

 TGsvObjectInspectorTypeInfo;

Вторая процедура подобна первой, но позволяет зарегистрировать сразу несколько метаклассов, например:

GsvRegisterTypesInfo([TLabel_INFO, TFont_INFO, TButton_INFO)];

Удобнее всего регистрировать метаклассы в секции initialization того программного модуля, в котором они определяются. Третья функция выполняет поиск метакласса в реестре на основе его имени, причем она самостоятельно добавляет к имени суффикс _INFO, например, поиск метакласса по имени инспектируемого типа может выглядеть так:
cls := GsvFindTypeInfo(obj.ClassName);
Здесь obj - это экземпляр инспектируемого класса, а cls - ссылка на его метакласс. Если метакласс не найден в реестре, то функция возвращает nil. Реализация реестра метаданных весьма проста:

var

 GsvTypesInfo: TStringList;

procedure GsvRegisterTypeInfo(AClass: TGsvObjectInspectorTypeInfoClass);

begin

 if not Assigned(GsvTypesInfo) then begin

  GsvTypesInfo := TStringList.Create;

  GsvTypesInfo.Duplicates := dupIgnore;

  GsvTypesInfo.Sorted := True;

 end;

 GsvTypesInfo.AddObject(AClass.ClassName, TObject(AClass));

end;

procedure GsvRegisterTypesInfo(aClasses:

 array of TGsvObjectInspectorTypeInfoClass);

var

 i: Integer;

begin

 for i := Low(AClasses) to High(AClasses) do

  GsvRegisterTypeInfo(AClasses[i]);

end;

function GsvFindTypeInfo(const ATypeName: String):

 TGsvObjectInspectorTypeInfoClass;

var

 i: Integer;

begin

 Result := nil;

 if Assigned(GsvTypesInfo) then

  if GsvTypesInfo.Find(ATypeName + '_INFO', i) then

  Result := TGsvObjectInspectorTypeInfoClass(GsvTypesInfo.Objects[i]);

end;

Фактически, реестр представляет собой объект сортированного списка строк TStringList. Этот объект создается при регистрации первого метакласса. Поскольку список сортирован, то поиск в нем выполняется достаточно быстро. Каждый элемент списка содержит имя метакласса и ассоциированную с ним ссылку на метакласс.
Объекты и их заместители
В предыдущем разделе речь шла только о типах инспектируемых объектов. В этом разделе "фокус ввода" перемещается на инспектируемые объекты. Как было сказано, инспектор получает доступ к значениям свойств на основе RTTI. Это означает, что инспектируемые классы должны содержать объявление и реализацию published-свойств. Если мы инспектируем классы визуальных компонентов, порожденных от TComponent, то это условие выполняется автоматически и никаких других усилий нам прикладывать не нужно. Если мы проектируем классы, специально рассчитанные на инспекцию, то мы можем удовлетворить этому требованию, если при объявлении классов укажем директиву {$M+} или будем порождать классы данных от TPersistent. Все свойства, доступные для инспекции, нужно объявить в секции published. В этом случае от нас также не требуется дополнительных усилий. Ситуация осложняется, если нам требуется инспектировать объекты, которые не содержат RTTI или вообще не являются Delphi-объектами. Такое может произойти, например, если: ·мы вводим инспектор объектов в уже существующий проект, в котором изначально не предполагалось наличие инспектора, ·требуется инспекция объектов, разработанных сторонними разработчиками, ·объекты реализуются на другом языке программирования или доступны только через их интерфейсы (например, COM-объекты), ·объекты размещаются в адресном пространстве другого процесса или на другой машине в локальной сети.
Для того, чтобы иметь возможность инспекции объектов различной природы и происхождения, вводится понятие "объект-заместитель" (proxy). Те, кто знаком с книгой Эриха Гамма и др. "Приемы объектно-ориентированного проектирования. Паттерны проектирования" сразу поймут, в чем дело. При инспекции объекта, который не содержит RTTI, динамически создается его заместитель, который, с одной стороны, имеет RTTI и соответствующие published-свойства, а, с другой стороны, содержит ссылку на инспектируемый объект и перенаправляет запросы на получение и изменение свойств соответствующим методам, интерфейсным входам или полям данных реального инспектируемого объекта. После инспекции объекта его заместитель просто уничтожается. Таким образом, для инспектора создается иллюзия, что он работает с родным Delphi-объектом. Способ создания proxy-объекта тесно связан с тем, как реализован сам инспектируемый объект. Естественно, что в каждом конкретном случае потребуется конкретное решение. Для примера предположим, что инспектируемый объект - прямоугольник, то есть, экземпляр записи типа TRect. Тогда реализация объекта-заместителя может быть такой:

type

 {$M+}

 TRect_Proxy = class

 public

  constructor Create(ARect: PRect);

 private

  FRect: PRect; // указатель на экземпляр записи

  function GetLeft: Integer;

  function Get: Integer;

  function GetWidth: Integer;

  function GetHeight: Integer;

  procedure SetLeft(const Value: Integer);

  procedure Set(const Value: Integer);

  procedure SetWidth(const Value: Integer);

  procedure SetHeight(const Value: Integer);

 published

  property Left: Integer read GetLeft write SetLeft;

  property : Integer read Get write Set;

  property Width: Integer read GetWidth write SetWidth;

  property Height: Integer read GetHeight write SetHeight;

 end;

 {$M-}

 constructor TRect_Proxy.Create(ARect: PRect);

 begin

  Assert(Assigned(ARect));

  FRect := ARect;

 end;

 function TRect_Proxy.GetLeft: Integer;

 begin

  Result := FRect^.Left;

 end;

 ...

 procedure TRect_Proxy.SetHeight(const Value: Integer);

 begin

  FRect^.Bottom := FRect^. + Value;

 end;

Для случая, когда инспектируемый объект находится, например, на другой машине локальной сети, реализация прокси-объекта будет сложнее и определится тем, как конкретно реализовано сетевое взаимодействие.
Менеджер объектов
В задачу менеджера входит организация взаимодействия между визуальным компонентом инспектора и инспектируемым объектом. Может возникнуть вопрос, для чего нужен посредник? Для ответа на этот вопрос можно выделить несколько моментов: ·желание отделить визуальную часть инспектора от какой бы то ни было связи с конкретными объектами и конкретными методами работы с ними. Тем, кто программировал на Microsoft Visual C++, прекрасно знакома методология "документ-вид", а программисты на SmallTalk сразу вспомнят "модель-контроллер-вид", ·желание предоставить потенциальную возможность конструирования информации для инспектирования различными способами, ·обеспечение независимости от способа предоставления метаданных. Например, для какого-то конкретного проекта мы предпочли бы описывать метаданные на XML или каким-то иным способом, ·потенциальная возможность использования визуального компонента для реализации клона Delphi-инспектора, используя только ту информацию, которую в виде RTTI формирует компилятор (без предоставления дополнительных метаданных). А кроме того, в этом случае нам потребовалось бы два различных представления - для свойств и для методов, ·потенциальная возможность групповой инспекции объектов.
Учитывая эти аргументы, введение посредника становится достаточно обоснованным. Основные задачи менеджера объектов можно сформулировать так: ·отделить визуальное представление инспектора от данных, с которыми он работает, ·представить инспектору свойства инспектируемого объекта в наиболее удобной для него форме, то есть, в виде древовидной структуры свойств, ·передавать инспектору значения свойств объекта и изменять значения свойств объекта при их изменении в инспекторе, ·взаимодействовать с метаданными и перенаправлять классам метаданных запросы на требования инспектора, например, на отображение какого-то специфического диалога, заполнение списка перечислимых значений свойства и так далее.

Отправить комментарий

Проверка
Антиспам проверка
Image CAPTCHA
...