Версия для печати


"Шаманский метод Geo"
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=1367

George Judkin
дата публикации 30-06-2008 13:06

"Шаманский метод Geo"

Я испытываю определенную неловкость, начиная писать эту статью. С одной стороны, вроде, как обещал ее написать (давно уже обещал, больше двух лет тому назад). С другой, несколько странно писать про то, что уже давно хорошо известно (по крайней мере, на Королевстве Delphi). Давно обсудили, высказали все "За" и "Против" и даже название стихийно сложилось (вынесенное в заголовок данной статьи). Сначала хотел включить этот материал в большую публикацию по разработке собственных компонент. Но там и так материала выше крыши. А времени свободного все меньше и меньше, так что я просто боюсь уже браться за ту работу. В общем, решил подготовить эту небольшую публикацию, в которой собрать все результаты обсуждения воедино.

Итак, о чем, собственно, речь. Всех нас, наверное, когда-либо посещала мысль, переделать какие-либо стандартные компоненты. Ну, захотели, разобрались и сделали. Например, захотелось, чтобы стандартный TLabel имел цветную рамку (как и в большинстве моих примеров, содержательный смысл принесен в жертву простоте). Пишем что-то вроде такого:

unit Labels;

interface

uses
  StdCtrls,Graphics;

type
  TMyLabel = class(TLabel)
  private
    FBorderColor : TColor;
    procedure SetBorderColor(Val : TColor);
  protected
    procedure Paint; override;
  published
    property BorderColor : TColor read FBorderColor write SetBorderColor default clBlack;
  end;

procedure Register;

implementation

uses
  Classes;

procedure TMyLabel.SetBorderColor(Val : TColor);
begin
  if Val = FBorderColor then Exit;
  FBorderColor:=Val;
  Invalidate;
end;

procedure TMyLabel.Paint;
begin
  inherited;
  with Canvas do
    begin
    Brush.Color:=BorderColor;
    FrameRect(ClientRect);
    end;
end;

procedure Register;
begin
  RegisterComponents('Samples',[TMyLabel])
end;

end.

Пишем, компилируем, добавляем в палитру и, вуаля, можно бросать на форму и использовать новый компонент. Но у этого решения есть один минус: палитра компонент и так содержит много-много всякого разного. Хорошо, если сделали какой-то полезный универсальный компонент, который потом будем использовать постоянно. Тогда, вроде, и не жалко. А если это разовая разработка под конкретную задачу. А под следующий проект мы еще компонентов наделаем. А потом еще. Пройдет год-другой, и нам потребуется разрабатывать справочно-информационную систему, чтобы ориентироваться в панели компонент.

Имеется альтернативное решение: не регистрировать новый компонент, отказавшись от использования в design-time. Вместо этого будем создавать его в run-time, задавая значения свойств в коде. Это хорошо, но при условии, что дизайн формы прост, и сразу понятно, куда и как новый компонент должен быть вставлен. Если это не так, то невозможность визуального проектирования приведет к большим затратам времени на переписывание кода, компиляцию и просмотр того, что при этом получилось. К тому же, не знаю кого как, а меня сильно раздражает необходимость писать длинный-предлинный обработчик события создания формы, в котором создавать компоненты, а потом долго и нудно задавать значения их свойств.

Но оказывается, имеется возможность и компонент модифицированный в палитре не регистрировать, и сохранить (частично) возможности визуального проектирования. Вот тут и начинается "шаманизм". Бросаем на форму стандартный TLabel и TSpeedButton, задаем нужные свойства, а код модифицируем вот таким образом:

unit Unit1;

interface

uses
  Forms, Classes, Controls, StdCtrls, Buttons, Graphics;

type
  TLabel = class(StdCtrls.TLabel)
  private
    FBorderColor : TColor;
    procedure SetBorderColor(Val : TColor);
  protected
    procedure Paint; override;
  public
    property BorderColor : TColor read FBorderColor write SetBorderColor;
  end;

  TForm1 = class(TForm)
    Label1: TLabel;
    SpeedButton1: TSpeedButton;
    procedure SpeedButton1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TLabel.SetBorderColor(Val : TColor);
begin
  if Val = FBorderColor then Exit;
  FBorderColor:=Val;
  Invalidate;
end;

procedure TLabel.Paint;
begin
  inherited;
  with Canvas do
    begin
    Brush.Color:=BorderColor;
    FrameRect(ClientRect);
    end;
end;

procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
  if Label1.BorderColor = clRed
  then
    Label1.BorderColor:=clBlue
  else
    Label1.BorderColor:=clRed;
end;

end.

Здесь TSpeedButton добавлен, чтобы посмотреть, как будет меняться цвет рамки у модифицированного TLabel. StdCtrls – юнит, в котором расположен оригинальный TLabel. Использование имен с конкретизацией через имя модуля – стандартная возможность Паскаля. Свойство BorderColor перенесено из published-секции в public, так как оно не может быть изменено через Object Inspector, потому что тот знает только про свойства стандартного TLabel, в которых никакого BorderColor нет.

При этом в дизайнере мы видим обычный TLabel (левая картинка), а в запущенной программе – модифицированный (правая картинка).

Форма в design-time Форма в run-time

Теперь несколько слов, почему так происходит, и почему так можно делать.

Как работает дизайнер? Когда мы размещаем компоненты на форме и задаем их свойства, дизайнер делает две вещи: вставляет в определенные места кода нужные строки и создает DFM-файл с информацией о компонентах и их свойствах, по которому будет создаваться форма. В компиляции дизайнер не участвует, соответственно, компилятор ориентируется только на код. А в коде у нас используется другой класс – потомок первоначального – с тем же именем и тем же набором свойств. В Паскале использование одинаковых имен ошибкой не является, так как существуют формальные правила разрешения таких ситуаций.

Отсюда следует и безопасность использования такого вот, вроде бы, "шаманского" приема. Мы обманываем только дизайнер, который выполняет функции частичной кодогенерации и формирования ресурсного файла, но не участвует в процессе компиляции. Конкретизация имени через указание юнита, в котором это имя определено, является документированной возможностью Паскаля с тех самых пор, как в нем было введено разбиение программ на юниты. Механизм, когда вместо одного класса подставляется экземпляр другого класса (потомка первоначального) называется позднее связывание и является одним из основных принципов всего объектно-ориентированного программирования.

Данный прием может привести к сбоям, если в будущих версиях: будет отменена поддержка конкретизации имени через дополнительное указание модуля (маловероятно), не будет поддерживаться позднее связывание для классов (еще менее вероятно), дизайнер будет в большей степени задействован в формировании исполняемого модуля (более вероятно, но пока этого нет).

Можно, кстати, провести еще такой эксперимент (эксперимент этот предлагаю все же проводить умозрительно, чтобы не пришлось потом возиться с восстановлением исходных настроек IDE). Давайте зафиксируем где-нибудь содержимое DFM-файла проекта, полученного с применением шаманизма. А потом возьмем честный вариант нового компонента (из первого листинга), переименуем класс в TLabel, скомпилируем и установим в палитру, убрав оттуда предварительно стандартный TLabel. Теперь если создать проект, в котором точно так же разместить новый TLabel, задать все те же значения свойств и не трогать в design-time свойство BorderColor, то мы получим в точности такой же DFM-файл. Для пущего эстетизма можно и компиляцию выполнить не из IDE, а непосредственно запустив компилятор и передав ему нужные параметры.

Подведем итоги. Преимущества данного метода заключаются в возможности доработки стандартных компонентов под нужды конкретной задачи и, сохранив возможности визуального проектирования, избежать "загаживания" палитры компонент одноразовыми поделками. Вопрос о преимуществах модификации стандартных компонент перед использованием сторонних компонент, перед использованием обработчиков событий, позволяющих зачастую изменять внешний вид стандартных компонент в достаточно широком диапазоне, а также зачем вообще объединять свой код в классы, а не ограничиться применением стандартных классов, выходит за рамки данной статьи. Хотя я готов обсудить и их тоже, если возникнет такой интерес.

Существенных недостатков (кроме естественного отвращения к применению читерских приемов) выделено два:

  1. В design-time мы видим старый вариант компонента. Если для нас имеет ценность только взаимное расположение компонент на форме, а модификация внешнего вида не очень велика и может быть понята через воображение, то все хорошо. Если мы в процессе визуального проектирования, например, пытаемся разработать цветовое решение формы для компонента с сильно модифицированным внешним видом, то ничего хорошего не получится. Так что применяйте данный прием с учетом данного ограничения.
  2. Не получится на одной форме использовать и оригинальные, и модифицированные компоненты. Только что-то одно.

Все это нужно принимать во внимание прежде, чем принимать решение об использовании данного метода.

Вариант дальнейшего развития данного метода был предложен Юрием Спектором (Ins). Предположим, нам нужно использовать модифицированный компонент не в одной форме проекта, а в нескольких. В этом случае можно вынести реализацию модифицированного класса в отдельный юнит и подключать этот юнит по мере необходимости через uses. Но тут имеется одна хитрость. Дело в том, что с большой степенью вероятности Вам в проектировании формы потребуются компоненты, которые определены в том же юните, где и оригинальный компонент. Чтобы использовался именно модифицированный компонент, нужно грамотно задать порядок подключаемых юнитов в разделе uses. По правилам языка если в uses есть два модуля, содержащих одно и то же имя, то будет использован элемент из того модуля, который в списке uses указан позже. Меняя порядок юнитов в uses, можно получать нужную комбинацию оригинальных и модифицированных компонент в форме. Однако все равно сохраняется ограничение, что в одной форме невозможно использовать и оригинальный компонент, и его модификацию.

Немного истории. Прием этот был придуман мной еще во времена Delphi 1 и успешно применялся задолго до моего первого появления на Королевстве Delphi. Но я не уверен, что был первым в мире, кто до такого додумался, так как прием достаточно очевиден (по крайней мере, если иметь опыт работы с объектно-ориентированным программированием и инструментами визуального проектирования). Так что на авторские права не претендую. На Круглом Столе этот прием впервые активно обсуждался в вопросе 35814. Но впервые я упомянул его несколько раньше – в вопросе 29175. Ну и еще было активное обсуждение здесь.

Напоследок хотелось бы, как водится, выразить благодарности.

Юрию Спектору. Мой тезка является главным популяризатором данного метода (причем, не только на Королевстве Delphi). К тому же, он – главный "пинатель" меня, чтобы я этот метод оформил в виде статьи.

Антону Григорьеву. Антон – главный оппонент. И в споре с ним данный прием, бывший до того некоей интуитивно понятной примочкой, обрел строгое обоснование.

Алексею Румянцеву. Алексей выдал однажды одну единственную фразу (про звездочки). Но эта фраза не давала мне спокойно жить, подталкивая к подготовке данной публикации.

А также всем-всем-всем, кто принимал участие в обсуждении данного приема. Независимо от того, являетесь вы сторонниками или противниками. Если спор ведется конструктивно, то от грамотного противника, зачастую, больше пользы, чем от десятка сторонников.



Специально для Королевства Delphi