Практика
Технология подмены класса, часть 2

:: Меню ::
:: На главную ::
:: FAQ ::
:: Заметки ::
:: Практика ::
:: Win API ::
:: Проекты ::
:: Скачать ::
:: Секреты ::
:: Ссылки ::

:: Сервис ::
:: Написать ::

:: MVP ::

:: RSS ::

Яндекс.Метрика


Сегодня я хочу немного углубиться в тему статьи "Технология подмены класса". Этот прием идеально подходит в ситуациях, когда нужно расширить или модифицировать функциональность какого-либо класса, не порождая его потомков, и уж тем более не устанавливая в RAD Studio новых компонентов. В своей практике я довольно часто использую этот прием, примеры можно найти в материалах о: "PageControl", "MaskEdit", "PopupMenu". Но ни один из этих примеров не раскрывает тему статьи - подмену самого класса, так что пора исправить это упущение.

Давайте посмотрим на следующий код (пример надуман, зато нагляден):

type
  TCheckBox = class(TRadioButton)
  end;

  TDemo1 = class(TForm)
    CheckBox1: TCheckBox;
    CheckBox2: TCheckBox;
    CheckBox3: TCheckBox;
    CheckBox4: TCheckBox;
    CheckBox5: TCheckBox;
  private
  public
  end;

implementation

end.

Код предельно прост - класс TCheckBox переопределяется до его непосредственного использования, и это изменение будет применено ко всем объектам класса TCheckBox в модуле (правая картинка). Вот мы и произвели подмену класса! К недостаткам можно отнести то, что хоть визуально в run-time мы будем видеть RadioButton, их ClassType по-прежнему будет TCheckBox (будьте внимательны и учитывайте это). Этот пример находится в архиве в папке Demo1.


Пользоваться этим приемом нужно с осторожностью. Допустим мы положили на форму компонент Edit и переопределили класс TEdit следующим образом:

type
  TEdit = class(TButton)
  end;

  TForm1 = class(TForm)
    Edit1: TEdit;
    {...}
  end;

В процессе компиляции мы получим следующую ошибку:


Вполне ожидаемо, ведь по умолчанию компонент Edit, брошенный на форму, содержит текст, идентичный свойству Name. Заглянем в DFM:

object Edit1: TEdit
  Left = 8
  Top = 8
  Width = 121
  Height = 21
  TabOrder = 0
  Text = 'Edit1'
end

Но у TButton, которым мы подменили TEdit, свойства Text нет. Чтобы избавиться от этой ошибки нужно либо очистить свойство Text, либо реализовать подмену следующим образом (что мне кажется более правильным):

type
  TEdit = class(TButton)
  private
    function GetText: string;
    procedure SetText(const Value: string);
  published
    property Text: string read GetText write SetText;
  end;

implementation

function TEdit.GetText: string;
begin
  Result := Caption;
end;

procedure TEdit.SetText(const Value: string);
begin
  Caption := Value;
end;

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


Однако это не единственный способ реализации подобного трюка, посмотрим на следующий пример:

interface

uses
  {...,} Vcl.StdCtrls;

type
  TCheckBox = class(Vcl.StdCtrls.TCheckBox)
  public
    constructor Create(AOwner: TComponent); override;
  end;

  TDemo2 = class(TForm)
    CheckBox1: TCheckBox;
    CheckBox2: TCheckBox;
    CheckBox3: TCheckBox;
    CheckBox4: TCheckBox;
    CheckBox5: TCheckBox;
  private
  public
  end;

var
  Demo2: TDemo2;

implementation

constructor TCheckBox.Create(AOwner: TComponent);
type
  PClass = ^TClass;
begin
  inherited;
  PClass(Self)^ := TRadioButton;
end;

Здесь переопределение класса происходит не при его объявлении, а в момент создания экземпляра этого класса, в конструкторе. Это работает потому, что тип экземпляра объекта хранится в неявном поле экземпляра объекта в памяти - первые SizeOf(Pointer) байт экземпляра всегда содержат ссылку на TClass (реализованную как указатель на VMT класса), который был использован при создании объекта. Это зарезервированное поле инициализируется в классовом методе InitInstance, определённым в TObject:

class function TObject.InitInstance(Instance: Pointer): TObject;
begin
  FillChar(Instance^, InstanceSize, 0);
  PInteger(Instance)^ := Integer(Self);
  {...}
end;

Зная, что класс экземпляра объекта в run-time явно хранится в поле по нулевому смещению, становится понятно, что для изменения класса экземпляра нужно перезаписать его TClass-ссылку на другую. Этот способ, как и предыдущий, применятся ко всем объектам класса TCheckBox в модуле, однако теперь объекты не только выглядят в run-time как RadioButton, но и их ClassType равен TRadioButton. Этот пример находится в архиве в папке Demo2.

А если нужно подменить не все экземпляры класса TCheckBox, а лишь некоторые? Тогда нужно немного изменить подход.

type
  TDemo3 = class(TForm)
    CheckBox1: TCheckBox;
    CheckBox2: TCheckBox;
    CheckBox3: TCheckBox;
    CheckBox4: TCheckBox;
    CheckBox5: TCheckBox;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
  public
  end;

var
  Demo3: TDemo3;

implementation

procedure TDemo3.Button1Click(Sender: TObject);

  procedure PatchCheckBox(CheckBox: TCheckBox);
  type
    PClass = ^TClass;
  begin
    PClass(CheckBox)^ := TRadioButton;
    CheckBox.Perform(CM_RECREATEWND, 0, 0);
  end;

var
  i: Integer;
begin
  for i := 0 to ComponentCount-1 do
    if (Components[i] is TCheckBox) and (TCheckBox(Components[i]).Tag = 1) then
      PatchCheckBox(Components[i] as TCheckBox);
end;

Здесь все практически идентично предыдущему примеру. Отличие в том, что подмена происходит не в конструкторе класса, поэтому мы принудительно пересоздаем модифицированный объект. Этот пример находится в архиве в папке Demo3.

Все это конечно хорошо, но какой во всем этом может быть практический смысл? Представим что у нас имеется большой проект, в котором несколько сотен форм, на каждой из которых есть кнопка класса TButton, реализующая функционал сохранения изменений, произведенных пользователем на форме. И вот от руководства прилетает задача - срочно добавить на кнопку картинку, срок - вчера. Конечно, можно поменять кнопку на каждой форме, но теперь мы знаем, как справиться с этим гораздо быстрее! Создадим следующий модуль и добавим его в секцию uses нужных форм после модуля Vcl.Buttons.

unit BtnModify;

uses
  Vcl.Buttons;

type
  TButton = class(Vcl.Buttons.TBitBtn)
  public
    constructor Create(AOwner: TComponent); override;
  end;

implementation

constructor TButton.Create(AOwner: TComponent);
var
  PropInfo: PPropInfo;
begin
  inherited;
  Kind := bkOK;
end;

end.

.: Пример к данной статье :.


При использовании материала - ссылка на сайт обязательна