Прим.пер.: в оригинале это была серия из трёх статей по обзору нововведений в 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
. И этот вызов был четырёхкратным:
- Свойство
GlassFrame
должно быть доступно в run-time и показываться в design-time в подсказках ко всем экземплярам форм. Это было решено использованием class helper-аTCustomFormHelper
. - Class helper-ы не могут добавлять новые поля в класс. Поэтому для свойства нужно как-то создать хранилище. Для этого был использован хак повторного использования существующего поля (
FPixelsPerInch
) для внесения в класс заглушки, указывающей на запись с расширенными полями класса. - Хотя class helper-ы являются отличным способом реализации эффекта "миража": "обмана" вашего кода, убеждения его в том, что он видит искусственно внедрённое свойство класса, но они не помогают с RTTI класса и потому новое "свойство" не появляется само по себе в Инспекторе объектов. Как пояснил Allen в своей статье, они ввели в BDS 2006 новый интерфейс для редакторов выбора, называемый
ISelectionPropertyFilter
, который позволяет динамически добавлять и удалять свойства из инспектора объектов в design-time. - Наконец, свойство
GlassFrame
нужно сериализировать как часть формы (в .dfm). Это не решается ни class helper-ом, ни интерфейсом фильтрации свойств. Решение заключается в использовании существующего механизмаDefineProperties
, но был необходим дополнительный трюк для поддержки имён видаProperty.SubProperty
определяемых свойств.
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" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.