суббота, 6 августа 2011 г.

Анализ хака добавления "published" свойства без изменения класса

Это перевод Review: Delphi 2007 for Win32 (Beta) - part three. Автор: Hallvard Vassbotn.

Прим.пер.: в оригинале это была серия из трёх статей по обзору нововведений в Delphi 2007. Я оставил лишь одну часть, которая говорит не столько про нововведения, сколько про хаки - как часть серии "Хак №X".

В VCL (а равно и в RTL) появилось несколько исправлений багов, но мы не будем их рассматривать. Хотя Delphi 2007 остаётся совместимой с бинарными .dcu файлами предыдущей версии Delphi, CodeGear, тем не менее, удалось добавить новую функциональность и даже новые свойства в существующий класс TCustomForm. Как они это сделали?

Хак свойства GlassFrame
В частности, теперь все формы имеют свойство GlassFrame (с под-свойствами), которое позволяет расширить область "стекла" в Vista в клиентскую область. Allen Bauer писал об этой возможности в своём блоге в посте "Как добавить published свойство без нарушения совместимости DCU". Пожалуйста, прочтите сначала его пост - так будет проще понять этот пост.

Для начала: что представляет собой свойство GlassFrame и что оно делает? Представим, что оно было бы реализовано "чистым" образом (как оно, вероятно, и будет в следующем выпуске Delphi) - добавлением обычного свойства к TCustomForm и поднятием его до уровня published в TForm. Тогда изменения в коде выглядели бы как-то так:
type
  TCustomForm = class;

  TGlassFrame = class(TPersistent)
  public
    constructor Create(Client: TCustomForm);
    procedure Assign(Source: TPersistent); override;
    function FrameExtended: Boolean;
    function IntersectsControl(Control: TControl): Boolean;
    property OnChange: TNotifyEvent {...};
  published
    property Enabled: Boolean {...} default False;
    property Left: Integer {...} default 0;
    property Top: Integer {...} default 0;
    property Right: Integer {...} default 0;
    property Bottom: Integer {...} default 0;
    property SheetOfGlass: Boolean {...} default False;
  end;

  TCustomForm = class(TScrollingWinControl)
  public
    procedure UpdateGlassFrame(Sender: TObject);
    property GlassFrame: TGlassFrame {...} ;
  end;

  TForm = class(TCustomForm)
  published
    property GlassFrame;
  end;
Для упрощения я выкинул ненужный код. Заметьте, что новое свойство GlassFrame имеет тип TGlassFrame, который наследуется от TPersistent. Это означает, что все его свойства появляются в Инспекторе объектов как под-свойства GlassFrame. Свойства Left, Top, Right и Bottom определяют размеры области стекла на форме. Если вы установите SheetOfGlass в True, то вся форма станет "стеклянной". Наконец, свойство Enabled может быть использовано для быстрого включения и выключения эффекта расширения стекла. Заметьте, что эффект стекла, контролируемый свойством GlassFrame, является дополнением к обычному остеклению неклиентской области окна (заголовка и бордюра), предоставляемому самой Vista. На моём стареньком Acer не установлена Vista, так что вы можете пока посмотреть снимки экрана, сделанные Jeremy North в постах про эффект стекла здесь и здесь, чтобы увидеть, как визуально проявляется работа этого свойства в run-time.

Вызов, который сама себе бросила CodeGear, заключался в реализации свойства GlassFrame и его функциональности без изменения класса TCustomForm. И этот вызов был четырёхкратным:
  1. Свойство GlassFrame должно быть доступно в run-time и показываться в design-time в подсказках ко всем экземплярам форм. Это было решено использованием class helper-а TCustomFormHelper.
  2. Class helper-ы не могут добавлять новые поля в класс. Поэтому для свойства нужно как-то создать хранилище. Для этого был использован хак повторного использования существующего поля (FPixelsPerInch) для внесения в класс заглушки, указывающей на запись с расширенными полями класса.
  3. Хотя class helper-ы являются отличным способом реализации эффекта "миража": "обмана" вашего кода, убеждения его в том, что он видит искусственно внедрённое свойство класса, но они не помогают с RTTI класса и потому новое "свойство" не появляется само по себе в Инспекторе объектов. Как пояснил Allen в своей статье, они ввели в BDS 2006 новый интерфейс для редакторов выбора, называемый ISelectionPropertyFilter, который позволяет динамически добавлять и удалять свойства из инспектора объектов в design-time.
  4. Наконец, свойство GlassFrame нужно сериализировать как часть формы (в .dfm). Это не решается ни class helper-ом, ни интерфейсом фильтрации свойств. Решение заключается в использовании существующего механизма DefineProperties, но был необходим дополнительный трюк для поддержки имён вида Property.SubProperty определяемых свойств.
Давайте погрузимся в каждый из этих пунктов и подробно их разберём. В конце концов, в этом блоге мы в основном говорим про хаки в Delphi, а свойство GlassFrame из Delphi 2007 является, пожалуй, самым громким и лучшим (?) хаком в истории Delphi :-)

Предупреждение: исходный код этой статьи служит только для иллюстрации идеи - он был взят из бэта-сборки 2007. Если вам нужен реальный код - загляните в модуль Forms.pas вашей версии Delphi 2007.

Class helper TCustomFormHelper
Вот как выглядит class helper, который обеспечивает работу свойства GlassFrame:
type
  TCustomFormHelper = class helper for TCustomForm
  private
    function GetGlassFrame: TGlassFrame;
    procedure ReadGlassFrameBottom(Reader: TReader);
    procedure ReadGlassFrameEnabled(Reader: TReader);
    procedure ReadGlassFrameLeft(Reader: TReader);
    procedure ReadGlassFrameRight(Reader: TReader);
    procedure ReadGlassFrameSheetOfGlass(Reader: TReader);
    procedure ReadGlassFrameTop(Reader: TReader);
    procedure SetGlassFrame(const Value: TGlassFrame);
    procedure WriteGlassFrameBottom(Writer: TWriter);
    procedure WriteGlassFrameEnabled(Writer: TWriter);
    procedure WriteGlassFrameLeft(Writer: TWriter);
    procedure WriteGlassFrameRight(Writer: TWriter);
    procedure WriteGlassFrameSheetOfGlass(Writer: TWriter);
    procedure WriteGlassFrameTop(Writer: TWriter);
  public
    procedure UpdateGlassFrame(Sender: TObject);
    property GlassFrame: TGlassFrame read GetGlassFrame write SetGlassFrame;
  end;
Он предоставляет public-свойство GlassFrame, которое магией class helper-ов появляется у TCustomForm и всех его наследников. Он также вводит метод UpdateGlassFrame, но он предназначен для внутреннего использования самой формой. Он назначается обработчиком события OnChange класса TGlassFrame, что приводит к перерисовке формы при изменении под-свойств GlassFrame. Наконец, мы видим множество методов ReadXXX и WriteXXX, используемых в методе TCustomForm.DefineProperties для сериализации GlassFrame в .dfm файлы и обратной десериализации. Мы скоро обсудим этот момент.

У TPersistent есть виртуальный метод DefineProperties. К счастью, TCustomForm уже заместила этот метод в BDS 2006 (для хранения псевдо-свойств PixelsPerInch, TextHeight и IgnoreFontProperty) так что добавить новое свойство GlassFrame было не сложно.

Хак хранилища FPixelsPerInch
Как вы можете видеть, class helper не имеет (да и не может иметь) полей. Но указатель на экземпляр TGlassFrame всё ещё нужно где-то хранить - и хранить для каждого экземпляра формы отдельно. Тут есть несколько решений. К примеру, можно использовать хэш-таблицу для организации проецирования формы на свойство, но это сложно и может быть медленным - и уж точно потребует изрядной координации, чтобы свойство освобождалось при удалении формы и т.п. А другое решение заключается в запихивании новой информации поверх (старых) существующих полей формы.

И вот что решили сделать в CodeGear. Они выбрали относительно редко используемое private поле, которое не публикует свой адрес никакими средствами (вы можете узнать, чем это чревато, тут):
type
  TCustomForm = class(TScrollingWinControl)
  private
    FPixelsPerInch: Integer;
  end;
Поле FPixelsPerInch объявлено как Integer, но теперь в implementation секции с ним обращаются как с указателем на запись:
{ Хак-запись для внедрения новых свойств в TCustomForm }
type
  PPixelsPerInchOverload = ^TPixelsPerInchOverload;
  TPixelsPerInchOverload = record
    PixelsPerInch: Integer;
    GlassFrame: TGlassFrame;
    RefreshGlassFrame: Boolean;
  end;
Запись даёт место для хранения как старого (замещённого) поля PixelsPerInch (объявленного в TCustomForm), так и новых свойств - свойства GlassFrame (внедряемого TCustomFormHelper) и ещё одного внутреннего private поля RefreshGlassFrame.

Конструктор и деструктор TCustomForm управляют временем жизни записи и помещают указатель на неё в поле FPixelsPerInch - как если бы оно имело бы тип PPixelsPerInchOverload:
constructor TCustomForm.CreateNew(AOwner: TComponent; Dummy: Integer);
begin
  Pointer(FPixelsPerInch) := AllocMem(SizeOf(TPixelsPerInchOverload));
  inherited Create(AOwner);
  //...
end;

destructor TCustomForm.Destroy;
begin
  //...
    FreeMem(Pointer(FPixelsPerInch));
    inherited Destroy;
  //...
end;
Весь доступ к полям PixelsPerInch и RefreshGlassFrame делегируется через встраиваемые (inline) геттеры и сеттеры вроде такого:
function GetFPixelsPerInch(FPixelsPerInch: Integer): Integer; inline;
begin
  Result := PPixelsPerInchOverload(FPixelsPerInch).PixelsPerInch;
end;
Наконец, методы class helper-а могут использовать этот же трюк для хранения данных свойства GlassFrame:
function TCustomFormHelper.GetGlassFrame: TGlassFrame;
begin
  Result := PPixelsPerInchOverload(FPixelsPerInch).GlassFrame;
end;

procedure TCustomFormHelper.SetGlassFrame(const Value: TGlassFrame);
begin
  PPixelsPerInchOverload(FPixelsPerInch).GlassFrame.Assign(Value);
end;
Классный хак, разве нет? ;)

Внедрение свойств в design-time
Следующий шаг - убедить Инспектор объектов, что у формы есть свойство GlassFrame. Это достигается использованием мало известного интерфейса ISelectionPropertyFilter. Этот интерфейс был введён в предыдущей версии Delphi (Delphi 2006) и там он использовался для реализации свойства ControlIndex, внедряемого во все компоненты, бросаемые на TFlowPanel или TGridPanel (а также для скрытия стандартных свойств этих компонентов по контролю их положения в контейнере). Эти компоненты документированы в EDN статье Ed Vander Hoek тут, а интерфейс ISelectionPropertyFilter описан Tjipke A. van der Plaats тут.

Интерфейс объявлен в модуле DesignIntf и выглядит так:
{ ISelectionPropertyFilter
This optional interface is implemented on the same class that implements
ISelectionEditor.  If this interface is implemented, when the property list
is constructed for a given selection, it is also passed through all the various
implementations of this interface on the selected selection editors.  From here
the list of properties can be modified to add or remove properties from the list.
If properties are added, then it is the responsibility of the implementor to
properly construct an appropriate implementation of the IProperty interface.
Since an added "property" will typically *not* be available via the normal RTTI
mechanisms, it is the implementor's responsibility to make sure that the property
editor overrides those methods that would normally access the RTTI for the
selected objects.

FilterProperties
Once the list of properties has been gathered and before they are sent to the
Object Inspector, this method is called with the list of properties.  You may
manupulate this list in any way you see fit, however, remember that another
selection editor *may* have already modified the list.  You are not guaranteed
to have the original list. }
  ISelectionPropertyFilter = interface
    ['{0B424EF6-2F2F-41AB-A082-831292FA91A5}']
    procedure FilterProperties(const ASelection: IDesignerSelections;
      const ASelectionProperties: IInterfaceList);
  end;
В Delphi нет исходного кода, но один из IDE пакетов регистрирует редактор выбора для TCustomForm (и наследников), используя процедуру DesignIntf.RegisterSelectionEditor:
type
{ TBaseSelectionEditor
  All selection editors are assumed to derive from this class. A default
  implemenation for the ISelectionEditor interface is provided in
  TSelectionEditor class. }
  TBaseSelectionEditor = class(TInterfacedObject)
  public
    constructor Create(const ADesigner: IDesigner); virtual;
  end;

  TSelectionEditorClass = class of TBaseSelectionEditor;

procedure RegisterSelectionEditor(AClass: TClass; AEditor: TSelectionEditorClass);
Вы можете видеть некоторые детали по использованию ISelectionPropertyFilter в модуле DesignEditors и функции GetComponentProperties. Эта функция вызывается IDE, когда ей нужно выделить форму и получить список зарегистрированных редакторов выбора для формы и вызвать методы FilterProperties каждого редактора, реализующего интерфейс ISelectionPropertyFilter. Всё вместе это позволяет вам удалять и добавлять свойства из списка, который в конечном итоге оказывается в Инспекторе объектов. Новый редактор выделения TCustomForm добавляет реализацию IProperty для свойства-призрака GlassFrame. Итоговый результат: с точки зрения Инспектора объектов GlassFrame выглядит как published свойство TCustomForm, даже хотя у этого класса нет RTTI для этого свойства, да и вообще такого свойства нет. Чувствуете магию? ;)

Определение сериализуемых свойств
Последняя часть головоломки под названием "иллюзия GlassFrame" заключается в склеивании кода с потоковой системой .dfm. Виртуальный метод DefineProperties класса TPersistent всегда был частью потоковой системы Delphi. Вы переопределяете его для хранения дополнительной информации, сверх published свойств. К счастью, TCustomForm уже перекрывает (замещает) DefineProperties (для хранения PixelsPerInch, TextHeight и IgnoreFontProperty). Это означает, что в TCustomForm может быть добавлен дополнительный код для сериализации свойства GlassFrame без изменения интерфейса класса. Вот новый метод DefineProperties:
procedure TCustomForm.DefineProperties(Filer: TFiler);
//...
begin
  inherited DefineProperties(Filer);
  Filer.DefineProperty('PixelsPerInch', {...});
  Filer.DefineProperty('TextHeight', {...});
  Filer.DefineProperty('IgnoreFontProperty', {...});
  Filer.DefineProperty('GlassFrame.Bottom', {...});
  Filer.DefineProperty('GlassFrame.Enabled', {...});
  Filer.DefineProperty('GlassFrame.Left', {...});
  Filer.DefineProperty('GlassFrame.Right', {...});
  Filer.DefineProperty('GlassFrame.SheetOfGlass', {...});
  Filer.DefineProperty('GlassFrame.Top', {...});
end;
Я закомментировал не существенные детали. Само чтение и запись свойств делегируются Read- и Write-методам TCustomFormHelper, про который мы говорили в самом начале статьи.

Особой вещью, которую хотелось бы тут отметить, является то, что свойства GlassFrame хранятся с использованием вложенных имён вида 'GlassFrame.SubProperty'. В старых версиях Delphi это не сработало бы, но новая потоковая подсистема содержит такой дополнительный код в Classes, который делает это возможным:
procedure TReader.ReadProperty(AInstance: TPersistent);
//...
        PropInfo := GetPropInfo(Instance.ClassInfo, FPropName);
        if PropInfo = nil then
        begin
          // Call DefineProperties with the entire PropPath
          // to allow defining properties such as "Prop.SubProp"
          FPropName := PropPath;
          { Cannot reliably recover from an error in a defined property }
          FCanHandleExcepts := False;
          Instance.DefineProperties(Self);
          FCanHandleExcepts := True;
          if FPropName <> '' then
            PropertyError(FPropName);
          Exit;
        end;
Причиной в хранении свойства именно таким образом являются планы на будущую реализацию свойства GlassFrame.

GlassFrame в будущем
В будущих "binary-breaking" релизах Delphi свойство GlassFrame будет внедрено в сам класс "правильным образом". Allen про это тоже говорил. Так что, наиболее вероятно, GlassFrame станет обычным свойством класса TCustomForm (с подъёмом видимости в TForm), а TCustomFormHelper и прочие обсуждаемые выше хаки пропадут. Так вот, приятной особенностью указанного выше способа хранения является то, что даже при такой смене кода, весь код и старые .dfm окажутся полностью совместимыми с новым кодом. Имена 'GlassFrame.Subproperty' записанных в .dfm свойств будут просто спроецированы непосредственно на сами (вложенные) свойства GlassFrame. Так что хотя за сценой меняется всё, но видимое поведение остаётся неизменным. Впечатляюще, вам так не кажется?!

Прим.пер.: остаток поста опущен, как говорящий про прочие нововведения Delphi, но не про хаки.

Комментариев нет:

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

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

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