Статьи Королевства Дельфи

       

Менеджер объектов


В задачу менеджера входит организация взаимодействия между визуальным компонентом инспектора и инспектируемым объектом. Может возникнуть вопрос, для чего нужен посредник? Для ответа на этот вопрос можно выделить несколько моментов:

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


TGsvObjectInspectorObjectInfo = class public constructor Create; destructor Destroy; override; function ObjectName: String; virtual; function ObjectTypeName: String; virtual; function ObjectHelp: Integer; virtual; function ObjectHint: String; virtual; function PropertyInfo(Index: Integer): PGsvObjectInspectorPropertyInfo; procedure FillList(Info: PGsvObjectInspectorPropertyInfo; List: TStrings); virtual; procedure ShowDialog(Inspector: TComponent; Info: PGsvObjectInspectorPropertyInfo; const EditRect: TRect); virtual; function GetStringValue(Info: PGsvObjectInspectorPropertyInfo): String; virtual; procedure SetStringValue(Info: PGsvObjectInspectorPropertyInfo; const Value: String); virtual; function GetIntegerValue(Info: PGsvObjectInspectorPropertyInfo): LongInt; virtual; procedure SetIntegerValue(Info: PGsvObjectInspectorPropertyInfo; const Value: LongInt); virtual; property TheObject: TObject read GetObject write SetObject; end;
Можно заметить, что методы менеджера напоминают методы базового класса метаданных TGsvObjectInspectorTypeInfo. И это не случайно, ведь в большинстве случаев менеджер просто перенаправляет запрос соответствующему методу конкретного класса метаданных, то есть, играет роль диспетчера. Метод PropertyInfo напоминает метод ChildrenInfo метакласса - для каждого значения индекса функция возвращает указатель на метаданные свойства, а при завершении итерации по всем свойствам она возвращает nil. Наиболее существенное отличие от ChildrenInfo состоит в том, что PropertyInfo рекурсивно обходит все вложенные свойства и дополняет структуру TGsvObjectInspectorPropertyInfo несколькими динамически формируемыми полями. Здесь уместно упомянуть, что при описании записи TGsvObjectInspectorPropertyInfo мы опустили несколько полей, которые были неважны с точки зрения метаданных. Вот эти поля: HasChildren: Boolean; Level: Integer; Expanded: Boolean; TheObject: TObject; NestedObject: TObject;
  • HasChildren - указывает на наличие у данного свойства вложенных подсвойств,
  • Level - уровень свойства в полном дереве свойств,
  • Expanded - признак того, что вложенные свойства раскрыты и отображаются,
  • TheObject - объект или заместитель, которому принадлежит свойство,
  • NestedObject - объект или заместитель вложенного свойства.
Первые три поля используются только визуальным компонентом инспектора, а последние два поля - менеджером и метаклассами. Для доступа к метаданным менеджер обращается к реестру метаданных, используя при поиске имя типа инспектируемого объекта. Кроме того, менеджер обращается к реестру при рекурсивном обходе вложенных свойств. Назначение остальных методов:
  • FillList - перенаправляет запрос на заполнение списка перечислимых значений свойства конкретному метаклассу вложенного свойства,
  • ShowDialog - перенаправляет запрос на отображение диалога-мастера конкретному метаклассу вложенного свойства,
  • GetStringValue - получает значение свойства инспектируемого объекта в строковом виде на основе RTTI. Если свойство имеет вложенный метакласс, то используется его специализация (запрос перенаправляется метаклассу), а иначе выполняется стандартное преобразование, например, из типа Double в тип String,
  • SetStringValue - устанавливает значение свойства на основе заданного строкового значения,
  • GetIntegerValue и SetIntegerValue - подобны двум предыдущим методам, но специализированы не на строковом, а на целочисленном значении свойства.
Говоря о перенаправлении запросов от менеджера, нельзя не упомянуть о тех методах метаклассов, которых мы только коснулись в первом разделе статьи. В текущей версии инспектора определено несколько вспомогательных специализированных классов, порожденных от базового класса TGsvObjectInspectorTypeInfo. Это:
  • TGsvObjectInspectorTypeListInfo - предоставляет дополнительную функциональность при работе со свойствами, реализующими перечислимые типы. Такие свойства отображаются в инспекторе как выпадающие списки,
  • TGsvObjectInspectorTypeSetInfo - помогает описывать свойства-множества,
  • TGsvObjectInspectorTypeFontInfo - специализируется на описании свойства типа TFont и инкапсулирует стандартный Windows-диалог выбора шрифта,
  • TGsvObjectInspectorTypeColorRGBInfo - специализируется на описании простого свойства типа TColor и инкапсулирует стандартный Windows-диалог выбора цвета.
Все эти классы являются вспомогательными и уменьшают трудозатраты на описание конкретных классов метаданных. Для примера рассмотрим подробнее парочку из указанных вспомогательных классов.



type TGsvObjectInspectorListItem = record Name: String; // имя элемента списка Data: LongInt; // значение элемента списка end; PGsvObjectInspectorListItem = ^TGsvObjectInspectorListItem; TGsvObjectInspectorTypeListInfo = class(TGsvObjectInspectorTypeInfo) protected class function ListEnumItems(Index: Integer): PGsvObjectInspectorListItem; virtual; public class procedure FillList(AObject: TObject; List: TStrings); override; class function IntegerToString(const Value: LongInt): String; override; class function StringToInteger(const Value: String): LongInt; override; end; class function TGsvObjectInspectorTypeListInfo.ListEnumItems( Index: Integer): PGsvObjectInspectorListItem; begin Result := nil; end; class procedure TGsvObjectInspectorTypeListInfo.FillList(AObject: TObject; List: TStrings); var i: Integer; p: PGsvObjectInspectorListItem; begin i := 0; p := ListEnumItems(0); while Assigned(p) do begin List.AddObject(p^.Name, TObject(p^.Data)); Inc(i); p := ListEnumItems(i); end; end; class function TGsvObjectInspectorTypeListInfo.IntegerToString( const Value: Integer): String; var i: Integer; p: PGsvObjectInspectorListItem; begin Result := ''; i := 0; p := ListEnumItems(0); while Assigned(p) do begin if p^.Data = Value then begin Result := p^.Name; Break; end; Inc(i); p := ListEnumItems(i); end; end; class function TGsvObjectInspectorTypeListInfo.StringToInteger( const Value: String): LongInt; var i: Integer; p: PGsvObjectInspectorListItem; begin Result := 0; i := 0; p := ListEnumItems(0); while Assigned(p) do begin if p^.Name = Value then begin Result := p^.Data; Break; end; Inc(i); p := ListEnumItems(i); end; end;
Как уже было сказано, класс TGsvObjectInspectorTypeListInfo предоставляет дополнительную функциональность при работе со свойствами - перечислимыми типами. Класс переопределяет методы IntegerToString, StringToInteger и FillList, а для задания списка перечислений вводит новый виртуальный метод ListEnumItems - этот метод напоминает ChildrenInfo базового класса, но возвращает не типовые метаданные, а свойства каждого элемента перечисления - его имя и ассоциированное с ним значение - эти параметры определены записью TGsvObjectInspectorListItem. Конкретный метакласс, описывающий свойства-перечисления может быть порожден от класса TGsvObjectInspectorTypeListInfo, причем достаточно будет переопределить только метод ListEnumItems. Метод FillList выполняет итерацию по всем перечислимым значениям, вызывая ListEnumItems с монотонно возрастающим индексом до тех пор, пока ListEnumItems не вернет значение nil. Результаты итерации передаются визуальному компоненту инспектора через параметр List. Для преобразования строкового вида значения перечисления к целочисленному виду и для обратного преобразования служат методы StringToInteger и IntegerToString, алгоритм которых очень похож - оба они итерируют список перечислений, но в первом случае критерием для поиска является строковое имя, а во втором случае - ассоциированное с ним значение. Очевидно, что такой базовый класс может быть использован для любых перечислимых типов, причем даже таких, в которых значения перечисления не образуют упорядоченную монотонную последовательность.



type TGsvObjectInspectorTypeFontInfo = class(TGsvObjectInspectorTypeInfo) public class procedure ShowDialog(Inspector: TComponent; Info: PGsvObjectInspectorPropertyInfo; const EditRect: TRect); override; class function ObjectToString(const Value: TObject): String; override; end; class procedure TGsvObjectInspectorTypeFontInfo.ShowDialog( Inspector: TComponent; Info: PGsvObjectInspectorPropertyInfo; const EditRect: TRect); var dlg: TFontDialog; fnt: TFont; begin if not Assigned(Info) then Exit; if not Assigned(Info^.NestedObject) then Exit; if not (Info^.NestedObject is TFont) then Exit; fnt := TFont(Info^.NestedObject); dlg := TFontDialog.Create(Inspector); try dlg.Font.Assign(fnt); if dlg.Execute then fnt.Assign(dlg.Font); finally dlg.Free; end; end; class function TGsvObjectInspectorTypeFontInfo.ObjectToString( const Value: TObject): String; begin if Assigned(Value) then if Value is TFont then with TFont(Value) do Result := Format('%s, %d', [Name, Size]); end;
Класс TGsvObjectInspectorTypeFontInfo демонстрирует способ создания метакласса для специфического редактора свойства, в данном случае, для свойства-шрифта, имеющего тип TFont. Здесь переопределяются два метода - ShowDialog и ObjectToString. Методу ShowDialog передаются три аргумента:
  • Inspector - родительский компонент для формы-диалога,
  • Info - метаданные свойства,
  • EditRect - прямоугольник, представляющий собой экранные координаты поля редактирования визуального компонента инспектора. Эти координаты можно использовать для того, чтобы расположить диалог, скажем, прямо под значением редактируемого свойства (подобно списку). Конечно, это имеет смысл только для небольших по размеру диалогов.
Для свойств, отображающих диалог, менеджер заполняет поле метаданных NestedObject - оно указывает на инспектируемый объект или его заместитель. В данном случае менежер увидит, что свойство-шрифт является объектом-классом и определит его адрес, используя адрес объекта верхнего уровня в дереве объектов-свойств и имя свойства. Если бы это было простое свойство, например, TColor, то менеджер заполнил бы поле NestedObject указателем на объект текущего уровня. После того, как мы определили, что инспектируемое свойство действительно является объектом нужного нам типа (в данном случае TFont), мы создаем диалог, инициализируем его данные текущим значением свойства, отображаем диалог и при успешном завершении переносим новое значение свойства в инспектируемый объект. Другой метод класса - ObjectToString определяет то, как будет выглядеть значение свойства в инспекторе. В данном случае мы считаем, что основные свойства шрифта - это его имя и размер. Такой способ отображения отличается от того, что мы видим в инспекторе Delphi - в качестве значения объекта Delphi отображает имя его типа.


Содержание раздела