Использование визуального наследования форм в Delphi

http://www.delphikingdom.com/asp/users.asp?ID=2079

Как читать исходный код dfm форм?

Вот пример dfm:

      object frmMain: TfrmMain
        Left = 223
        Top = 228
        Width = 387
        Height = 272
        Color = clBtnFace
        Font.Charset = DEFAULT_CHARSET
        Font.Color = clWindowText
        Font.Height = -11
        Font.Name = 'MS Sans Serif'
        Font.Style = []
        Menu = mmMain
        OldCreateOrder = False
        PixelsPerInch = 96
        TextHeight = 13
        object alMain: TActionList
          Left = 136
          Top = 96
          object acExit: TAction
            Caption = #1042#1099#1093#1086#1076
            OnExecute = acExitExecute
          end
        end
        object ilMain: TImageList
          Left = 168
          Top = 96
        end
        object mmMain: TMainMenu
          Images = ilMain
          Left = 40
          Top = 16
          object mmiFile: TMenuItem
            Caption = #1060#1072#1081#1083
            object miExit: TMenuItem
              Action = acExit
            end
          end
        end
      end

Что мы тут можем понять: У нас есть форма frmMain. у нее заданы свойства: Left, Right, Width, Height, Color, Font, Menu, OldCreateOrder, PixelsPerInch, TextHeight их значения заданы после знака равно. А остальные значения соответствуют значениям по умолчанию, по этому их здесь нет.

Более того можно определить, что у нас на форме сейчас 3 объекта, определить это очень просто у нас слово object c отступом в два пробела содержится 3 раза.

Более того мы можем без труда определить, что у нас на форму добавлено: alMain - TActionList; ilMain - TImageList; mmMain - TMainMenu;

У alMain - заданы свойства Left и Top. В этом ActionList содержится одно действие. Действие с именем (Name) acExit и каким-то Caption на русском языке, а на onExecute у нас выполняется метод acExitExecute. А поскольку это у нас не визуальный компонент, то Left и Top это положение компонента на форме во время Design-Time. Кстати обратите внимание, что свойство Image не задано.

У ilMain заданы свойства Left и Top. И в него еще не загружено никаких картинок, поскольку иначе было бы соответствующее свойство.

У mmMain заданы свойства Left, Top и Image. Данное меню содержит один верхний пункт меню с именем (Name) - mmiFile и текстом на русcком. В этом меню есть один пункт с именем (Name) - miExit и действием (Action) acExit.

Обратите внимание, что все отступы по два пробела. Т.е. степень вложенности очень легко посчитать. Свойства имеют аналогичное название, что и в Object Inspector, за исключением свойства Name, которое пишется сразу после слова object. Сложность с тем, что русский текст сохраняется так, что его очень тяжело читать. После окончания значений свойств или вложенных объектов идет слово end. То есть структура следующая: object Name: Type значения свойств, вложенные объекты end;

Соответствующая часть кода в файле pas выглядит следующим образом:

      TfrmMain = class(TForm)
        alMain: TActionList;
        ilMain: TImageList;
        mmMain: TMainMenu;
        acExit: TAction;
        mmiFile: TMenuItem;
        miExit: TMenuItem;
        procedure acExitExecute(Sender: TObject);
      ........
      end;

Должен возникнуть вопрос, что происходит если файлы неправильно отредактировать, т.е. то, что находится в pas расходится с тем, что в dfm. Тогда имеем проблему, что при компиляции Delphi не отловит ошибку, но когда вы запустите приложение и попробуете вызвать соответствующее окно, то будет ошибка и окно не откроется. Если же вы откроете это форму в Delphi, то Delphi будет как умеет приводить dfm и pas в соответствие в Delphi 6 и Delphi 7 файл pas приводится к dfm. Т.е., если в dfm вы удалите какой-то объект, то Delphi предложит вам удалить его из pas файла, а если вы удалите объект из pas файла, то Delphi предложит его восстановить. Собственно такой алгоритм работы создает основные неприятности при работе с визуальным наследованием. Приходится отслеживать соответствие между dfm и pas файлами самостоятельно, а это далеко не такое тривиальное занятие, как может показаться на первый взгляд.

Как это выглядит визуальное наследование в исходных файлах *.dfm и *.pas

Предположим у нас есть обычная форма в которой не используется визуальное наследование форм. То интересующие места исходного кода будут выглядеть следующим образом

первая строка EditOrder.dfm

      object frmEditOrder: TfrmEditOrder

исходный код EditOrder.pas

      unit EditOrder;

      interface

      uses
      ........
      type
        TfrmEditOrder = class(TForm)
        .....
        end;
      ........
      implementation

      {$R *.DFM}
      .....
      end.

А вот так будет выглядеть код в случае, если мы использовали визуальное наследование. В качестве формы от которой будем производить визуальное наследование выберем EditOrder.

первая строка EditOrderFirm.dfm

      inherited frmEditOrderFirm: TfrmEditOrderFirm

исходный код EditOrderFirm.pas

      unit EditOrderFirm;

      interface

      uses
      ........
      type
        TfrmEditOrderFirm = class(TfrmEditOrder)
        .....
        end;
      ........
      implementation

      {$R *.DFM}
      .....
      end.

Обратите внимание, что вместо слова object в dfm у нас слово inherited это и есть обозначение, что происходит визуальное наследование. А также в pas вместо TForm у нас TfrmEditOrder. Если поменять в файле эти две вещи, то Delphi будет воспринимать данную форму, как наследника, поэтому даже, если вы в своем проекте не используете визуальное наследование, то приложив соответствующие усилия, ее можно добавить, при этом на самом деле все не так уж сложно.

Для того чтобы сделать визуальный наследник, если у вас еще нет форм, необходимо выбрать File-New-Other-Вкладка с именем вашего проекта- там будет выбор форм. Выбрать нужную форму и нажать OK. Пример на рисунке.


Окно выбора предка

Как и для чего можно использовать визуальное наследование?

  • Несколько уровней пользователя (Новичок, Опытный, Эксперт). Пользователь в одном месте выбирает свой уровень, и все формы соответствующим образом изменяют свое поведение.
  • Например глобальная настройка отображать кнопку "Применить" на формах или нет. Или любая другая глобальная настройка всей системы.
  • Проблему, что правили форму, слетели табы. Пользователь никогда об этом не узнает.
  • Настройка отображения формы в соответствии с правами пользователя и настройками. Какие данные показывать, давать редактировать и т.п.

Я расскажу о том, как я это использовал. И почему остался очень доволен. Но сейчас бы я хотел обратить внимание на, то чем придется платить.

Проблемы возникающие при использовании визуального наследования

Delphi проверяет соответствие между dfm и pas только при открытии формы в Delphi. Соответственно отсюда и лезут все проблемы.

  • Если вы добавляете какой-то элемент на форму родитель. То у вас этот элемент автоматически не добавляется во всех наследниках. И надо переоткрывать всех наследников. Тут бывают самые разные проблемы. Об этих проблемах чуть позже. Иногда все проходит гладко даже без переоткрывания всех форм, но это как правило только в очень простых случаях.
  • Если мы в форме наследнике, поменяли свойства у какого-то из объектов, то если аналогичное свойство поменялось в родители, то в наследнике оно останется неизменным.
  • Самая неприятная проблема, что перед финальным билдом нужно переоткрывать все формы, чтобы быть уверенным, что нет багов. Было бы просто супер, если бы такую проверку делал компилятор в момент сбора проекта.

Хочу обратить внимание, что в случае изменения исходного кода методов, не надо переоткрывать все формы, а это самая распространенное изменение.

Визуальное наследование в действии

Пример нашего исходного кода:

EditOrder.pas, базовый класс

      unit EditOrder;

      interface

      uses
        Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
        Dialogs, ExtCtrls, StdCtrls;

      type
        TfrmEditOrder = class(TForm)
          ButtonPanel: TPanel;
          btnOK: TButton;
          btnCancel: TButton;
          procedure FormShow(Sender: TObject);
        private
          { Private declarations }
        protected
          FVisiblebtnOK: boolean;
          procedure Customize; virtual;
        public
          { Public declarations }
          constructor Create(AOwner: TComponent); override;
        end;

      var
        frmEditOrder: TfrmEditOrder;

      implementation

      {$R *.dfm}

      { TfrmEditOrder }

      constructor TfrmEditOrder.Create(AOwner: TComponent);
      begin
        inherited;

        FVisiblebtnOK := True;
      end;

      procedure TfrmEditOrder.Customize;
      begin
        btnOK.Visible := FVisiblebtnOK;
      end;

      procedure TfrmEditOrder.FormShow(Sender: TObject);
      begin
        Customize;
      end;

      end.

EditOrderFirm.pas, класс наследник.

      unit EditOrderFirm;

      interface

      uses
        Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
        Dialogs, EditOrder, StdCtrls, ExtCtrls;

      type
        TfrmEditOrderFirm = class(TfrmEditOrder)
        private
          { Private declarations }
        protected
          procedure Customize; override;
        public
          { Public declarations }
          constructor Create(AOwner: TComponent); override;
        end;

      var
        frmEditOrderFirm: TfrmEditOrderFirm;

      implementation

      {$R *.dfm}

      constructor TfrmEditOrderFirm.Create(AOwner: TComponent);
      begin
        inherited;
      
        FVisiblebtnOK := False;
      end;

      procedure TfrmEditOrderFirm.Customize;
      begin
        inherited;

        if not FVisiblebtnOK then begin
          btnCancel.Caption := 'Exit';
          Self.Caption := 'Просмотр Заказа'  
        end;
      end;

      end.

Что происходит в базовом классе? Есть переменная отвечающее за то, чтобы не отображать кнопку btnOK. При создании этой формы эта переменная по умолчанию ставится в значение True. При этом это свойство специально помещено в protected, чтобы наследник имел к нему доступ. Далее на Show вызывается метод Customize, в котором в соответствии с этим свойством показывается или нет кнопка btnOK. При этом метод Customize сделан virtual, чтобы его можно было переопределить в наследниках.

Что происходит в наследнике? При создании этой формы переменная отвечающая за отображение кнопки btnOK ставится в False. Переопределяется метод Customize. Далее на Show вызывается метод Customize, в котором сначала вызывается, часть из базового класса, а затем еще наша дополнительная часть кода. Если бы мы просто каждый раз переопределяли метод Customize, то нам бы пришлось дублировать код и его было бы очень сложно переписывать.

Что важно. Сначала мы устанавливаем параметры в базовом классе, затем если надо переустанавливаем их в классе наследнике, затем вызываем часть ответственную за отображение в базовом классе, а затем часть ответственную за отображение в наследнике. При этом в наследнике мы с отображением можем делать, что угодно. Но параметры которые общие для всех форм (Показывать "Применить" или нет) у нас уже есть и мы их можем учитывать. Кажется, что я не учел, что при повторном показе формы снова вызовется метод Customize, но это легко решаемая проблема, поэтому я не стал усложнять пример.

Как результат повторно используется огромная часть кода, но при этом когда возникает необходимость реализовать какое-то особое поведения у нас с этим не возникает никаких проблем. Обычно достаточно просто в Create переопределить нужные параметры, а методы базового класса сделают все за нас. Автоматизацию можно довести вплоть до того, что задаете свойства IdName и TableName и получаете сразу редактирование таблицы. В том проекте, где я использовал визуальное наследование было более 200 форм, около трети всего исходного кода в базовых классах (их было несколько), и там был специальный компонент, в котором хранились все необходимые свойства и они редактировались в Delphi.

Как бы я рекомендовал это организовать сейчас:

  1. Компонент(ы), который(е) хранит(ят) все необходимые свойства, со значениями по умолчанию редактируемыми в Delphi.
  2. Компонент(ы), который(е) умеет(ют) загружать нужные свойства из ini, БД, реестра, откуда это необходимо.
  3. Компонент(ы), который(е) отображает(ют) уже все необходимое на форму, по сути аналог Layout в Java.

Проблемы, возникающие при изменении базового класса

Самая простая мы добавили новый элемент в базовый класс, с именем (Name), которое 100% отсутствует во всех наследниках. Нужно банально переоткрыть все формы и добится, того чтобы она там появилась. Делаем Поиск class(тип базовой формы). Можно не сомневаться мы найдем все формы. Теперь открываем их убеждаемся, что все ок, ставим и стираем где-нибудь пробел и сохраняем. Все будет OK.

Мы решили вынести какой-то уже существующий элемент из наследника в базовую форму. Тогда закрываем наследника. Открываем базовую форму и добавляем в нее нужный элемент. Теперь лезем в наследника не Delphi средствами в dfm, вместо слова object пишем inherited и по максимуму удаляем значения свойств иначе, потом их придется править руками, в pas удаляем строку в которой определен соответствующий объект. Теперь открываем форму наследник в Delphi, если все нормально значит мы все сделали правильно. Если возникли какие-то проблемы, то лучше закрыть не сохраняя. Довольно частая ситуация, что при такой операции меняется тип объекта, тогда нужно дополнительно его скорректировать в dfm и проверить соответствие свойств у этих типов объектов.

Мы добавили новый элемент в базовый класс, с именем (Name), которое 100% отсутствует во всех наследниках. Но при открытии, какого-то наследника у нас посыпались ошибки. Это значит, что на самом деле мы ошиблись и на этой форме уже есть такой элемент. Придется опять-таки править dfm и pas вышеописанным способом. Либо все откатывать....

Мы случайно, что-то сдвинули в наследники, что не надо было делать. Можно либо по правой кнопке выбрать Revert to Inherited, но тогда все свойства будут сброшены, либо залезть в dfm и удалить ненужное свойство.

Резюме

Не бойтесь проблем, вы очень быстро научитесь их решать, и они не будут вам казаться, каким-то шаманством. Это поможет научится работать с dfm, а это пригодится в дальнейшем. Например, очень удобно в случае контроля версий, можно сразу понять, что в форме изменили, не открывая Delphi, а сравнивая dfm, как текстовые файлы. Или еще довольно редко бывает, но очень полезно, нужно какой-то тип компонентов (TВutton) заменить на (TImageButton) во всем проекте. Просто делаем поиск или можно даже автозамену по dfm и pas. Дело почти сделано. Осталось только убедится, что мы, где-то как-то очень хитро не привязались к типу TButton. Простейший случай inherited(TCustomButton). Это просто пример. Вообще не бойтесь править dfm, это не какие-то магические буковки, это хорошо читабельный исходный код. Почему в .Net и Java до сих пор не сделано аналогично, мне не понятно. Специально перед написанием статью глянул NetBeans 6.1, чтобы не быть голословным. Искренне считаю, что то как реализовано визуальное наследование в Delphi делает его самым удобным инструментом для написания GUI-интерфейса. А не использовать его бессмысленно лишать себя преимуществ разработки GUI на Delphi. Даже если вы сейчас не используете визуальное наследование, то его не так сложно внедрить. Лично у меня это заняло на всем моем проекте, около двух недель с момента старта и до момента выпуска стабильной рабочей версии, и это при более 200 формах и при учете того, что я делал все в первый раз и у меня не было никакой теоретической подготовки.

Обращаю внимание, что я активно использовал визуальное наследование в Delphi 6 и 7. И не могу гарантировать, что в более новых версиях Delphi, что-то не изменилось.

Пример исходного кода на Delphi 7 прилагается.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=19350