Модели, виды и фреймы 

Джон Шемитц

В этой главе развивается творческий подход к функциональности программ, который позволяет внедрять одну форму внутрь другой. Новые интерфейсы Delphi 3 заметно упрощают эту задачу.

В число достоинств Delphi входит и упрощение многих аспектов работы с Windows API. В результате объемистый и неуклюжий код сокращается до простого оператора присваивания, а невообразимо сложное становится совсем тривиальным. Вероятно, самым знакомым примером для вас окажется свойство Canvas; кроме того, заслуживает внимания и свойство Parent. Задавая значение свойства Parent, вы сообщаете Windows о том, что элемент становится дочерним окном нового родителя Parent. Отныне он будет появляться на экране одновременно со своим окном-родителем. (Кстати, именно так работают диалоговые окна со вкладками: каждая страница фактически представляет собой панель. Все компоненты, находящиеся на ней, являются дочерними по отношению к вкладке. Когда вкладка переходит на передний план (поверх других вкладок), вместе с ней появляются и все ее компоненты.) Задание свойства Parent во время выполнения программы позволяет добиться разнообразных специальных эффектов — от динамического создания элементов типа вкладок до включения одной формы в пустую область другой.

Когда могут пригодиться внедренные формы? Рассмотрим четыре возможных сценария: 

  1. Вы занимаетесь созданием программ-мастеров (wizards), руководящих действиями пользователя при создании объектов. Кроме того, вы хотите предоставить пользователям список свойств объекта в виде диалогового окна со вкладками, чтобы позволить изменять любое свойство объекта, не проходя заново все этапы работы с мастером. Единственное отличие между мастером и списком свойств заключается в том, что мастер разрешает пользователю перейти только от текущей страницы к следующей (и лишь при условии ввода правильных данных), а вкладки списка свойств можно перебирать в произвольном порядке. Следовательно, вкладка списка свойств на рис. 10.1 содержит точно такой же набор данных объекта, как и страница мастера на рис. 10.2. Если страницы мастера будут обслуживаться тем же кодом, что и вкладки списка свойств, поведение этих объектов всегда будет согласованным. 

  2. Рис. 10.1. Мастер, использующий код для просмотра данных совместно 
    со списком свойств на рис. 10.2 

    Рис. 10.2. Список свойств, использующий код для просмотра 
    данных совместно с мастером на рис. 10.1 

  3. Ваши объекты могут отображаться в нескольких различных контекстах. Например, один и тот же человек может быть и подчиненным, и начальником. Если для просмотра информации о начальнике будет использо ваться тот же код, что и для информации о подчиненном, программа ста- нет более компактной, и в ней исчезнет вероятность рассогласования. 
  4. Вы участвуете в разработке большого и сложного диалогового окна со вкладками. Из-за сложности окна над ним трудится целая команда программистов, каждый из которых отвечает за одну или несколько вкладок. Вместо того чтобы объединять изменения в одном общем модуле, вы наверняка предпочтете хранить каждую вкладку в отдельном модуле. При этом участники команды не будут «наступать на пятки» друг другу, а программа станет более логичной и понятной. 
  5. Вы работаете с некой иерархией объектов, и вам нужна форма, с помощью которой пользователи могли бы просматривать и/или изменять любые объекты, входящие в иерархию. Некоторые действия применимы ко всем участникам иерархии, другие возможны только для некоторого подмножества объектов. Логичнее всего было бы создать единую форму с элементами, отвечающими за выполнение общих действий. Во время выполнения на форму можно добавлять специализированные элементы, относящиеся к определенному типу объектов, и изменять их при смене выбранного объекта.
Первые два варианта имеют много общего. У нас имеются объекты и стандартные способы для их просмотра и изменения — модели (models) и виды (views). Все, что вам теперь нужно — это фрейм. Фрейм (frame) может содержать любой вид. При отображении фрейма отображается и находящийся в нем вид. Один и тот же вид можно поместить в несколько фреймов. Виды, в свою очередь могут исполнять функции фрейма для видов внедренных или подчиненных объектов. 

Фреймы прекрасно подходят и для третьего сценария. Если на каждой вкладке окна будет находиться отдельный фрейм, у вас получится обобщенное диалоговое окно со вкладками, которое может содержать любые виды по вашему усмотрению. Из громоздкого набора ничем не связанных (по крайней мере потенциально) фрагментов модуль формы превращается в простой контейнер с кодом для заполнения каждого фрейма и, возможно, кодом OnChanging, в котором активный фрейм спрашивает у своего вида, можно ли переключиться на другую вкладку. Большое диалоговое окно со вкладками можно разделить на несколько модулей, чтобы каждый участник команды мог самостоятельно следить за обновлением своего кода. Вы забудете о таких неприятностях, как потеря обновлений или повторное появление уже исправленных ошибок из-за неаккуратного объединения версий. 

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

Практическая реализация видов 

До настоящего момента эта глава выглядела несколько абстрактно. День за днем вы работаете с объектами, компонентами, формами и обработчиками событий — никаких моделей, видов или фреймов. Однако эти абстракции полезны и даже необходимы. Стандартная абстракция «модель/вид» помогает избежать «ловушки RAD» (RAD — Rapid Application Development, быстрое создание приложений), то есть размазывания смыслового кода по многочислен ным обработчикам событий, которые трудно понять, изменить или повторно использовать. Концепция фрейма помогает избежать похожей ловушки и не привязывать вид к конкретному объекту-контейнеру (форме, панели или вкладке диалогового окна). И все-таки теорию следует оценивать по программам, написанным с ее помощью — итак, как же реализовать вид, который можно использовать в нескольких фреймах?

Шаблоны компонентов и составные компоненты 

Возможное решение заключается в использовании такой новинки Delphi 3, как шаблоны компонентов (component templates). Шаблоны позволяют объединить в группу взаимосвязанные компоненты (вместе с именами и обработчиками событий) и поместить ее в палитру компонентов для повторного использования. Превосходная идея — но не совсем то, что требуется в нашем случае, потому что в итоге мы получим просто набор компонентов на форме. Если нам потребуется разместить один и тот же вид на двух различных формах, станет ясно, что набор компонентов не является самостоятельным объектом и не может иметь своих методов — как же тогда приказать виду прочитать или записать свою модель? 

Например, если в виде Employee должны присутствовать два внутренних вида с информацией о разных людях, то попытка размещения двух экземпляров шаблона на одной форме приведет к тому, что элементы второго экземпляра потеряют свои сохраненные имена и будут переименованы в Label1, Edit1 и т. д. В сценарии со сложным диалоговым окном все вкладки окажутся в одном модуле, а изменение шаблона не приведет к изменению созданного на его основе экземпляра. 

Похожий, но более «мощный» подход — превратить вид в составной (compound) компонент, который включает в себя другие визуальные компоненты в виде private-полей. Но если вам уже приходилось это делать, вы наверняка знаете, что создание и масштабирование такого компонента превращается в сущий кошмар. Вместо того чтобы просто разместить компоненты на форме, как мы все привыкли делать, приходится создавать каждый внутренний компонент на программном уровне. Вместо того чтобы перетаскивать объекты мышью и задавать их свойства в инспекторе объектов, пока не получится что-то приличное, приходится вручную задавать значения всех свойств. Конечно, теоретически это возможно, но программирование становится очень медленным, нудным и чреватым ошибками. В итоге получается большой объем кода, который очень трудно прочитать и/или изменить. Существует и другой, худший аспект — поскольку это занятие настолько тягостно, программист пытается ограничиться минимальным количеством свойств, и в результате на форме возникает уродливая и неудобная мешанина компонентов. Возможно, построенный подобным образом вид содержит все необходимые «фишки», но пользы от него оказывается немного. 

Мы могли бы избежать всех трудностей, связанных с ручным построением компонентов, если бы визуально сконструированную форму можно было преобразовать в компонент. На самом деле Delphi позволяет сделать это, однако не слишком простым или очевидным способом. Вам придется купить или построить специальный компонент, работающий только в режиме констру ирования, который задает недокументированное свойство формы IsControl, включить в форму нужный код и вручную исправить DFM-файл, чтобы изменить базовый класс объекта формы. Если вас заинтересует такая возможность, прочитайте книгу Рея Лишнера (Ray Lischner) «Secrets of Delphi 2» (Waite Group Press, 1996) — в ней приведен специальный компонент для работы с IsControl, а также содержатся подробные инструкции. Впрочем, хотя этот раздел книги произвел на меня огромное впечатление, я никогда не пользовался такой методикой, да и вам не рекомендую. Почему? Потому что вам придется повторять одни и те же действия при каждом создании нового вида или изменении существующего. Возможно, подобное превращение формы в компонент имеет смысл для создания истинно новых компонентов — например, объединения TMemo или TRichEdit с панелью инструментов — но не для видов. 

Наследование форм 

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

Листинг 10.1. Специальный конструктор для внедренных форм

type
  EmbeddedFormMode = (efmZoomed, efmTopLeft, 
  efmCentered);

function ALZ(Number: integer): Cardinal; 
// Проверка положительности
begin
  if Number > 0
    then Result := Number
    else Result := 0;
end;
constructor TEmbeddedForm.CreateEmbedded(
  _Owner: TComponent;
  Frame:  TWinControl;
  Mode:   EmbeddedFormMode );

begin
  Inherited Create(_Owner);

  Parent      := Frame;
  BorderIcons := [];
  BorderStyle := bsNone;

  case Mode of
    efmZoomed:   Align := alClient;
    efmTopLeft:  begin
                 Top  := 0;
                 Left := 0;
                 end; // efmTopLeft
    efmCentered: begin
Top  := ALZ((Frame.Height - Height) div 2);
Left := ALZ((Frame.Width  - Width)  div 2);
end; // efmCentered
    else         Assert(False);
    end; // case
  Visible := True;
end; // TEmbeddedForm.CreateEmbedded

В листинге 10.1 самой важной является строка Parent := Frame, которая назначает элемент Frame родителем внедренной формы. Именно это происходит «за кулисами» с обычными элементами управления при загрузке формы. Назначение родителя имеет три важных последствия. Во-первых, дочерний элемент отображается тогда, когда отображается его родитель. Следовательно, скрытие фрейма приводит к скрытию вида, а отображение фрейма или перевод его на передний план приводит к отображению вида. Во-вторых, дочерние элементы обрезаются по клиентской области родителя, поэтому большой вид автоматически вписывается в границы фрейма. Втретьих, дочерний элемент позиционируется относительно клиентской области своего родителя; свойства Top и Left для внедренной формы, как и для любого другого элемента, измеряются по отношению к содержащему его контейнеру. 

Последнее означает, что при масштабировании внедренной формы установкой свойства Align равным alClient форма ведет себя так же, как и любой другой элемент, выровненный подобным образом: она заполняет собой весь фрейм и автоматически масштабируется (с вызовом обработчика OnResize) при масштабировании фрейма. Без масштабирования вид сохраняет размеры, заданные в режиме конструирования, и даже может быть выровнен по центру или помещен в левый верхний угол фрейма. Начальный размер вида можно привести в соответствие с начальным размером фрейма, для этого следует задать свойства ClientHeight и ClientWidth в режиме конструирования. Можно, наоборот, изменять размеры окна фрейма в соответствии с размерами внедренных форм, как это происходит в мастерах и списках свойств (см. раздел «Редакторы моделей» этой главы). 

Пока листинг 10.1 находится под рукой, стоит пояснить смысл строк BorderIcons := [] и BorderStyle := bsNone. Они означают, что во время выполнения отображается лишь клиентская область формы вида — на ней нет ни заголовка, ни рамки, которые бы сообщали о том, что фрейм содержит независимую форму. Как видно из рис. 10.3 и 10.4, свойство Caption внедренной формы в режиме выполнения никак не проявляется. 

Рис. 10.3. В режиме конструирования вид ничем 
не отличается от обычной формы 

Рис. 10.4. Во время выполнения вид не похож на форму От внедренных форм к видам

Несомненно, возможность использования форм как элементов — хорошее начало. Теперь мы можем разместить форму там, где считаем нужным, и сделать сколько угодно копий. В объект формы можно включить методы, благодаря которым форма начинает «вести себя» как вид. 

Итак, как же ведут себя виды? 




Вид должен уметь читать данные от объекта модели и записывать их в этот объект. 
Вид должен уметь проверить свою правильность — мастер обычно не разрешает пользователю перейти к следующей странице при наличии неправильных данных на текущей, а список свойств не позволяет сохранить неверные данные. С другой стороны, это требование не является обязательным — например, Memo-поле Примечания может содержать любую информацию или вообще быть пустым. 
Вид должен иметь возможность сообщить своему фрейму об изменении свойства Valid, чтобы фрейм мог разрешить или запретить кнопки Next и OK. 
Виду может понадобиться такое отображение данных, при котором пользователь не сможет их редактировать. Список свойств может быть доступен только для чтения, если текущему пользователю не разрешается редактировать объект модели или просто потому, что пользователь не заблокировал объект и редактирование может привести к возникновению конфликтов.

Все эти «правила» отражены в листинге 10.2, который представляет собой выдержку из файла MODELS.PAS.

Листинг 10.2. Поведение модели, вида и фрейма

type
  TModel      = TObject;

  // И IView, и IModelEdit обладают свойством 
  ReadOnly
  IReadOnly =  interface
function  GetReadOnly: boolean;
procedure SetReadOnly(Value: boolean);
property  ReadOnly: boolean read  GetReadOnly
                            write SetReadOnly;
end;

  // Заполнить вид по данным модели 
  и записать изменения обратно;
  // взаимодействия фрейм/вид
  IFrame =     interface;
  IView =      interface (IReadOnly)
 procedure ReadFromModel(Model: TModel);
 procedure WriteToModel(Model: TModel);

function  GetValid: boolean;
 procedure SetValid(Value: boolean);
 property  Valid: boolean read GetValid
                         write SetValid;
 procedure AddNotifiee(   Notify: IFrame);
 procedure RemoveNotifiee(Notify: IFrame);
 end;
  IFrame =     interface
               procedure OnValidChanged(
                 ChangingObject: TObject;
                 View:           IView );
               end;

  // Мастера и списки свойств являются 
  «редакторами моделей»
  IModelEdit = interface (IReadOnly)
// Процедуры низкого уровня, которые позволяют
// приложению один раз подготовить редактор
// и несколько раз использовать его.
procedure Initialize;
function RunEditor(Model: TModel): boolean;
procedure Finalize;
// Initialize/RunEditor/Finalize
function EditModel(Model: TModel): boolean;
end;

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

Мы не пытаемся воспроизвести здесь полноценную архитектуру «Модель-Вид-Контроллер», где модель при внесении изменений в нее может приказать виду обновить себя, и т. д. Разумеется, реализация таких возможностей будет не столь уж сложной, но она лишь отвлечет нас от основной темы этой главы — внедренных форм. Кроме того, нашу упрощенную архитектуру «Модель-Вид -Фрейм» нельзя назвать слабой или примитивной. Я с большим успехом применял ее в нескольких проектах. 

Почему интерфейсы? 

Перед тем как следовать дальше, я хотел бы объяснить, почему я пользуюсь интерфейсами Delphi 3, вместо того чтобы просто определить классы вида и фрейма и создавать объекты на их основе. Не вызвано ли применение интерфейсов обычным желанием воспользоваться новой эффектной возможностью Delphi 3? 

Вовсе нет. Начнем с того, что виды и фреймы связаны циклической зависимостью. Фрейм должен сообщать виду о необходимости чтения или записи в модель; вид должен сообщать фрейму об изменении свойства Valid. Разумеется, подобную циклическую зависимость можно было бы реализовать с помощью опережающего объявления классов вместо опережающего объявления интерфейсов, но мне кажется, что интерфейсы делают эти взаимосвязи более понятными и избавляют их от груза посторонних свойствиметодов. Кроме того, хотя я и не использую такую возможность вEMBEDDEDFORMS.PAS, применение интерфейсов означает, что вид можно реализовать несколькими различными способами — он не обязан быть потомком TEmbeddedForm. Однако самая важная причина заключается в том, что вид сам может быть фреймом

Например, объект Employee (работник) может содержать ссылки на объекты типа People для самого работника и его руководителя, каждый из которых в свою очередь содержит сведения об имени и адресе. Вид объекта Employee может содержать внутренние виды для соответствующих полей. В Delphi не поддерживается множественное наследование, поэтому объект не может одновременно быть TView и TFrame, однако он легко может реализовать интерфейсыIView и IFrame

Перед тем как в Delphi появилась поддержка интерфейсов, для реализации чего-то наподобие протокола INotify обычно применялись процедурные типы:

type TOnValidChanged 
= procedure(ChangingObject: TObject) of object; 
procedure TViewAddNotifiee(Callback: 
TOnValidChanged);

Такая схема работала, но TOnValidChanged — это практически то же самое, что и TNotifyProc, и в каждой Delphi-программе такие процедурные типы встречаются в избытке. Вы можете передать процедуре AddNotifiee любой объект TNotifyProc, и компилятор никак не сможет предотвратить ошибочную передачу неверного параметра. При использовании интерфейсов процедура косвенного вызова (callback) фрейма должна иметь правильное имя и правильную сигнатуру, и притом она должна принадлежать объекту, реализующему протокол IFrame — это намного снижает вероятность случайных ошибок. 

Интерфейсные формы 

Когда я занялся реализацией интерфейсов из листинга 10.3, неожидан новозникли проблемы — моя система «зависала» при каждом вызове AddNotifiee(Self) из формы, реализующей IFrame. Хотя решение оказалось простым, мне пришлось в течение многих часов изобретать и проверять различные гипотезы. Чтобы вы лучше поняли суть происходившего, потребуется некоторая дополнительная информация. 

Документация Delphi 3 достаточно четко объясняет, что каждый объект, реализующий какой-то интерфейс, должен также реализовать интерфейс IUnknown, в котором производятся подсчет ссылок и запросы поддерживаемых интерфейсов. Если компилятор встретит следующее объявление:

type
  IFoo = interface
         procedure Foo;
         end;
  TFoo = class (TObject, IFoo)
         procedure Foo;
         end;

procedure TFoo.Foo; begin end; 

он пожалуется на наличие необъявленных идентификаторов QueryInterface, _AddRef и _Release. Вам придется явным образом реализовать IUnknown или создавать свой объект на базе TInterfaced, а не TObject. С другой стороны, следующий фрагмент не вызовет у компилятора никаких проблем: 

type
TFoo = class (TForm, IFoo) procedure Foo; end; procedure TFoo.Foo; begin end; 

Это означает, что фирма Borland реализовала IUnknown где-то в недрах VCL и у нас стало одной заботой меньше, не так ли? 

Нет, не так. При передаче TForm в качестве интерфейсной ссылки VCL выдает ошибку защиты (GPF). Хотя класс TComponent и реализует методы IUnknown, это вряд ли поможет тем из нас, кто захочет воспользоваться интерфейсами в приложении. Вызовы IUnknown передаются FVCLComObject — указателю, значение которого задается лишь при вызове GetComObject для получения интерфейсной ссылки объекта. Более того, GetComObject задает значение FVCLComObject лишь в том случае, если вы использовали VCLCom в своем проекте. Если сделать это, GetComObject начинает жаловаться на то, что фабрика класса (class factory) не была зарегистрирована, и… на этом я прекратил свои исследования. Возможно, все это очень здорово, если вы собираетесь использовать COM-объекты совместно с другими приложениями, но совершенно не подходит, если нужно всего лишь добавить интерфейсы к формам. 

Намного проще будет заглянуть в реализацию TInterfacedObject и включить в TForm простую, независимую реализацию IUnknown, а затем порождать формы от TInterfacedForm вместо TForm

Листинг 10.3. Модуль INTERFACEDFORMS.PAS

unit InterfacedForms;

// Copyright © 1997 by Jon 
//Shemitz, all rights reserved.
// Permission is hereby granted to freely 
//use, modify, and
// distribute this source code PROVIDED 
//that all six lines of
// this copyright and contact notice are 
//included without any
// changes. Questions? Comments? Offers of work?
//mailto:jon@midnightbeach.com

// --------------------------------------------
// Добавление в TForm функциональной реализации 
IUnknown. 

interface

uses Classes, Forms;

type
  TInterfacedForm =
    class (TForm, IUnknown)
    private
      fRefCount: integer;
    protected
      function QueryInterface( const IID: TGUID;
Obj): Integer; stdcall;
function _AddRef: Integer;         stdcall;
      function _Release: Integer;  stdcall;
    public
      property RefCount: integer read 
      fRefCount write fRefCount;
    end;

implementation

uses Windows; // для E_NOINTERFACE

// Код IUnknown основан на исходном 
тексте TInterfacedObject

function TInterfacedForm.QueryInterface
( const IID: TGUID; out Obj): Integer;
begin
  if GetInterface(IID, Obj)
    then Result := 0
    else Result := E_NOINTERFACE;
end;

function TInterfacedForm._AddRef: Integer;
begin
  Inc(fRefCount);
  Result := fRefCount;
end;

function TInterfacedForm._Release: Integer;
begin
  Dec(fRefCount);
  Result := fRefCount;
  if fRefCount = 0 then Destroy;
end;

end.

Как видите, все очень просто. Оглядываясь назад, я не могу понять, почему мне потребовалось на это так много времени. Наверное, меня сбило с толку предположение о том, что программа, полученная при добавлении интерфейсов к форме, не будет компилироваться из-за своей потенциальной ненадежности. Впрочем, во время своих экспериментов я обнаружил еще одну проблему, связанную с реализацией интерфейсов в Delphi. О ней тоже следует рассказать перед тем, как идти дальше. 

Проблемы с подсчетом ссылок в Delphi 3 

Ссылки на интерфейсы, как и ссылки на длинные строки, подсчитываются. Каждый раз, когда вы создаете копию переменной, содержащей интерфейсную ссылку (непосредственным присваиванием или при передаче параметра процедуре), вызывается метод _AddRef объекта, который увеличивает значение счетчика ссылок. При каждом уничтожении ссылки на интерфейс (непосредственным присваиванием или при выходе за пределы области видимости) вызывается метод _Release объекта, который уменьшает значение счетчика ссылок. Когда значение счетчика достигает 0, объект удаляет себя. «Обычные» объектные ссылки никак не влияют на процесс подсчета ссылок

Данная схема прекрасно работает — если вы взаимодействуете с объектом только с помощью интерфейсных ссылок. Например, для следующего фрагмента:

type
  IFoo = interface
        procedure Foo;
         end;
  TFoo = class (TObject, IFoo)
         procedure Foo;
         end;

procedure TFoo.Foo; begin end;

prcedure Bar(InterfaceReference: IFoo); begin end;

begin
  Bar(TFoo.Create);
end.

ALIGN="JUSTIFY">объект TFoo, созданный вызовом Bar, автоматически уничтожается при выходе из Bar. Но давайте рассмотрим слегка измененный сценарий, в ором интерфейсные ссылки смешиваются со ссылками на объекты:

var
  ObjectReference: TFoo;

begin
  ObjectReference := TFoo.Create;
  try
    Bar(ObjectReference);
  finally ObjectReference.Free; end;
end.

Проблема заключается в том, что присваивание ObjectReference := TFoo.Create не влияет на счетчик ссылок объекта. Свойство RefCount продолжает оставаться равным 0, как и при создании объекта. Тем не менее при вызове процедуры Bar происходит неявное присваивание ее параметру InterfaceReference. При этом генерируется вызов _AddRef, в результате которого RefCount становится равным 1. При выходе из Bar заканчивается область видимости параметра InterfaceReference, поэтому генерируется вызов _Release. В результате RefCount снова обнуляется, что приводит к уничтожению объекта. Ссылка Object Reference становится недействительной! При следующем обращении к ней (в нашем случае — при вызове Free) возникает GPF. 

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

В подобных случаях следует принудительно увеличить счетчик ссылок объекта на 1 еще до получения первой интерфейсной ссылки на него. Например, класс TAbstractView из следующего раздела содержит следующий обработчик OnCreate:

procedure TAbstractView.FormCreate
(Sender: TObject);
begin
  inherited;
  _AddRef; // теперь Self можно передавать 
           // в качестве интерфейсной ссылки
end;

Явный вызов _AddRef означает, что при создании первой интерфейсной ссылки RefCount увеличится до 2 и в дальнейшем никогда не станет равным 0. Следовательно, объект никогда сам не уничтожится и не разрушит ваших объектных ссылок; он будет жить до тех пор, пока вы не освободите его с помощью Free

Разумеется, явный вызов _AddRef необходим лишь при смешивании объектных и интерфейсных ссылок. Если вы собираетесь взаимодействовать с объектом только через интерфейсные ссылки, к явным вызовам _AddRef следует относиться с большой осторожностью — вы можете нарушить всю систему подсчета ссылок и ваш объект не будет уничтожаться. И наоборот, при работе с «чисто интерфейсным» объектом никогда не создавайте объектных ссылок на него, иначе они станут недействительными после того, как счетчик интерфейсных ссылок упадет до 0 и объект самоуничтожится. Одна из простейших мер предосторожности состоит в том, чтобы поместить все интерфейсные методы в секцию protected — они останутся доступными через интерфейс, но раз вы не сможете обратиться к ним через объектные ссылки, исчезнет и повод эти ссылки создавать. 

Абстрактные, контролируемые и неконтролируемые виды 

Как видно из дерева наследования на рис. 10.5, интерфейсные формы используются в проекте EmbeddedForms для создания двух категорий видов: неконтролируемы х (TValidView), для которых свойство Valid всегда равно True, и контролируемы х (TFickleView), для которых свойство Valid может изменять ся. Неконтролируемые виды можно использовать для Memo-полей с произвольным содержимым или, например, начальной или завершающей панелей мастера. Контролируемые виды должны применяться каждый раз, когда пользователь может ввести неверные данные, не подлежащие сохранению, — например, дату 31 февраля, расходы, превышающие общую сумму бюджета, и т. д. Поскольку в обоих случаях реализуется интерфейс IView, можно воспользоваться универсальным кодом для работы с обобщенным набором видов. 

Рис. 10.5. Интерфейсные формы в проекте 

Обе категории видов происходят от класса TAbstractView (листинг 10.4). 

Листинг 10.4. Модуль VIEWS.PAS

unit Views;

// Copyright © 1997 by Jon Shemitz, 
//all rights reserved.
// Permission is hereby granted to 
//freely use, modify, and
// distribute this source code PROVIDED 
//that all six lines of
// this copyright and contact notice are 
//included without any
// changes. Questions? Comments? 
//Offers of work?
//mailto:jon@midnightbeach.com

// ----------------------------------------------
// Отображает соглашение IView на внедренную 
//форму. Виды обычно 
// порождаются от TValidView или TFickleView.

interface

uses
  Models, Embedded;

type
  TAbstractView = class(TEmbeddedForm, IView)
    procedure FormCreate(Sender: TObject);
  private
    fReadOnly: boolean;
  protected
    function  GetValid: boolean;              
    virtual; abstract;
    procedure SetValid(Value: boolean);       
    virtual; abstract;

    function  GetReadOnly: boolean;           
    virtual;
    procedure SetReadOnly(Value: boolean);    
    virtual;
  public
    procedure ReadFromModel(Model: TModel);   
    virtual;
    procedure WriteToModel(Model:  TModel);   
    virtual;

    procedure AddNotifiee(   Notify: IFrame); 
    virtual; abstract;
    procedure RemoveNotifiee(Notify: IFrame); 
    virtual; abstract;

    property Valid:    boolean read GetValid 
    write SetValid;
    property ReadOnly: boolean read  fReadOnly
                               write SetReadOnly;
  end;

  TViewClass = class of TAbstractView;

implementation

{$R *.DFM}

function  TAbstractView.GetReadOnly: boolean;
begin
  Result := fReadOnly;
end; // TAbstractView.GetReadOnly

procedure TAbstractView.SetReadOnly(Value: 
boolean);
begin
  fReadOnly := Value;
  Enabled   := not Value;
// Вид, доступный только для чтения, отображает 
// информацию, но не позволяет изменять ее;
// вы можете переопределить SetReadOnly,
// чтобы изменить визуальное представление таких 
видов.
end; // TAbstractView.SetReadOnly

procedure TAbstractView.ReadFromModel(Model: 
TModel);
begin
end; // TAbstractView.ReadFromModel

procedure TAbstractView.WriteToModel(Model: 
TModel);
begin
end; // TAbstractView.WriteToModel

procedure TAbstractView.FormCreate(Sender: 
TObject);
begin
  inherited;
  _AddRef;  // Чтобы Self можно было передавать 
            // как интерфейсную ссылку
end;

end.

TAbstractView разделяет протокол IView на три части — доступность только для чтения, проверка корректности, обмен данными с моделью — и обрабатывает каждую часть отдельно: 



реализация базовой функциональности Read-Only — пользователи не могут изменить данные на заблокированной форме, хотя на практике виды обычно переопределяют метод SetReadOnly, чтобы изменить визуальное представление видов, доступных только для чтения; 
вся реализация проверки возлагается на потомков TValidView и TFickleView
для ReadFromModel и WriteFromModel предоставляются фиктивные заглушки. Поскольку эти методы переопределяются в любом реальном объекте вида, желательно, чтобы виды всегда вызывали inherited.

Как нетрудно догадаться по названию, предполагается, что вы не станете непосредственно использовать класс TAbstractView или напрямую наследовать от него. Вместо этого следует пользоваться TValidView и TFickleView

Разумеется, все «абстрактные», «неконтролируемые» и «контролируемые» виды можно было свалить в единый класс TView. Разделение обладает двумя основными достоинствами: поскольку неконтролируемые виды игнорируют те части протокола IView, которые занимаются проверкой, программа работает немного быстрее и требует меньше памяти. Что еще важнее, при порождении конкретного вида от TValidView вместо TFickleView свойство Valid всегда остается равным True, даже если вы по неосторожности присвоите ему False (сравните листинги 10.5 и 10.6). 

Листинг 10.5. Методы проверки корректности из модуля VALIDVIEWS.PAS

function TValidView.GetValid: boolean;
begin
  Result := True;
end; // TValidView.GetValid

procedure TValidView.SetValid(Value: boolean);
begin
  // TValidView всегда корректен - 
  //игнорируем Value
end; // TValidView.SetValid

procedure TValidView.AddNotifiee(Notify: IFrame);
begin
  // TValidView всегда корректен - игнорируем 
  запрос на добавление
end; // TValidView.AddNotifiee

procedure TValidView.RemoveNotifiee(Notify: 
IFrame);
begin
  // TValidView всегда корректен - игнорируем 
  запрос на удаление
end; // TValidView.RemoveNotifiee

Листинг 10.6. Фрагмент модуля FICKLEVIEW.PAS

type
  TFickleView = class(TAbstractView)
  private
    { Private declarations }
    fValid:  boolean;
    fNotify: IFrame;
    // В данной реализации проверки 
    //корректности поддерживается 
    // всего один получатель уведомлений
  public
    { Public declarations }
    procedure AddNotifiee(   Notify: IFrame); 
    override;
    procedure RemoveNotifiee(Notify: IFrame); 
    override;

    function  GetValid: boolean;              
    override;
    procedure SetValid(Value: boolean);       
    override;
  end;

implementation

{$R *.DFM}

procedure TFickleView.AddNotifiee(Notify: IFrame);
begin
  fNotify := Notify;
end; // TFickleView.AddNotifiee
procedure TFickleView.RemoveNotifiee(Notify: 
IFrame);
begin
  fNotify := Nil;
end; // TFickleView.RemoveNotifiee

function  TFickleView.GetValid: boolean;
begin
  Result := fValid;
end; // TFickleView.GetValid

procedure TFickleView.SetValid(Value: boolean);
begin
  if Value <> fValid then
    begin
    fValid := Value;
    if Assigned(fNotify) then
      fNotify.OnValidChanged(Self, Self);
    end; // Value <> fValid
end; // TFickleView.SetValid

Редакторы моделей 

Мастера и списки свойств являются редакторами моделей — вы передаете им объект модели, они выполняются и затем возвращают управление. Если пользователь нажал кнопку OK и изменил модель, возвращаемый результат равен True; в противном случае — False. Абстрактные шаблоны из проекта EMBEDDEDFORMS.DPR позволяют создавать реальных мастеров и списки свойств, которые могут совместно использовать объекты моделей и виды. От вас требуется следующее: 




создайте новую форму путем наследования от TAbstractWizard или TAbstract PropertySheet
задайте ее заголовок; 
для мастеров — выберите изображение и отрегулируйте ширину графической панели. 
напишите небольшую процедуру Initialize, которая поставляет информацию о заголовках страниц и классах вида, как показано в следующем фрагменте файла TESTSHEET.PAS:
procedure TPropertySheet.Initialize;
begin
  InitializeSheet( ['Name/Supervisor', 
  'Birthday', 'Address'],
                   [TEmployeeIdView,
                    TBirthdayView,
                    TAddressView] );
end; // TPropertySheet.Initialize

Абстрактный мастер и абстрактный список свойств делают все остальное; оба автоматически масштабируются, чтобы вместить наибольший вид. Мастер управляется стандартными кнопками Prev/Next/OK; список свойств блокирует кнопку OK при наличии неверных данных на странице за исключением ситуации, при которой хотя бы одна страница была неверной еще до вызова EditModel. В обоих случаях на входе вызывается метод ReadFrom Model для всех видов, а на выходе — метод WriteToModel для всех видов, если пользователь нажал кнопку OK. Список свойств обладает свойством ReadOnly, поэтому вы можете разрешить пользователям просматривать объекты без возможности их изменения. И мастер, и список свойств являются «чисто интерфейсными» объектами, не имеющими public-методов, так что вам не придется беспокоиться о Free или try..finally. Например, в листинге 10.7 приведен фрагмент модуля MAIN.PAS, в котором создаются и запускаются примеры мастера и списка свойств. 

Листинг 10.7. Запуск редакторов моделей

procedure TTestForm.EditModel(Editor: 
IModelEdit; Model: TModel);
begin
  {$ifdef ReadOnly}
    Editor.ReadOnly := True;
  {$endif} // ReadOnly
  if Editor.EditModel(Model)
    then ShowMessage('OK!')
    else ShowMessage('Abort ...');
end; // TTestForm.EditModel

procedure TTestForm.RunWizard(Sender: TObject);
var
  Employee: TEmployee;
begin
  Employee := DataModel.NewEmployee;
  try
    EditModel(TWizard.Create(Self), Employee);
  finally Employee.Free; end;
end;

procedure TTestForm.RunSheet(Sender: TObject);
var
  Employee: TEmployee;
begin
  Employee := DataModel.LoadEmployee(3);
  try
    EditModel(TPropertySheet.Create(Self), 
    Employee);
  finally Employee.Free; end;
end;

Лично меня в реализации мастера и списка свойств поражает, как просто выглядит такой обобщенный код на Delphi. Ключевым здесь является аргумент-массив array of TViewClass, передаваемый InitializeSheet() и Initialize Wizard() (см. листинг 10.8). 

Листинг 10.8. Метод TAbstractPropertySheet.InitializeSheet 

// из файла PropertySheets.pas

procedure TAbstractPropertySheet.InitializeSheet(
  Captions:   array of string;
  Views:      array of TViewClass );
var
  MaxSpan:    TSpan;
  Index:      integer;
  Sheet:      TTabSheet;
  ActualView: TAbstractView;
begin
  Assert( fViews.Count = 0,
          'Should only call ' + Name + 
          '.InitializeSheet once' );
  Assert( High(Captions) >= Low(Captions), 
  // можно использовать 
          'Must have at least one tab' );  
          // Slice() для передачи
// пустых массивов
  Assert( High(Captions) = High(Views),
          'Must have same number of Captions 
          as of Views' );

  MaxSpan := Point(0, 0);

  for Index := Low(Captions) to High(Captions) do
    begin
    Sheet := TTabSheet.Create(Self);
    with Sheet do
      begin
      PageControl := Self.PageControl;
      Caption     := Captions[Index];
      end; // with Sheet
    ActualView := Views[Index].CreateEmbedded
    ( Self,
    Sheet,
    efmTopLeft );
    fViews.Add(ActualView);
    ActualView.AddNotifiee(Self);
    MaxSpan := UnionSpan(MaxSpan, ActualView.Span);
    end; // for

  Sheet  := PageControl.ActivePage;
  Width  := (Width  - Sheet.Width)  + MaxSpan.X;
  Height := (Height - Sheet.Height) + MaxSpan.Y;
end; // TAbstractPropertySheet.InitializeSheet

Три оператора Assert проверяют, что список свойств еще не настроен, что в нем имеется хотя бы один заголовок и что количество заголовков совпадает с количеством классов вида. Обожаю Assert — лишь после того, как необходимость в конструкции {$IfOpt D+} {$Endif} отпала, я понял, как громоздко она выглядит. Assert проще ввести, он компактен и легко читается. 

Габариты (spans) определяются в файле EMBEDDED.PAS. Они представляют собой обычную пару «ширина/высота», то есть BottomRight прямоугольника TRect, у которого Top и Left равны 0:

function TEmbeddedForm.Span: TSpan;
begin
  Result.X := Width;
  Result.Y := Height;
end; // TEmbeddedForm.Span

Функция UnionSpan очень похожа на функцию Windows API UnionRect за исключением того, что она работает с габаритами, а не с прямоугольниками. Присваивая MaxSpan пару (0, 0), мы готовимся к определению минимального прямоугольника, вмещающего все виды из массива Views.

Вся настоящая работа выполняется в цикле при переборе элементов массива Captions. Для каждого элемента массива мы создаем новую вкладку (TTabSheet), размещаем ее на элементе-странице (TPageControl) и задаем текст заголовка. Затем аргумент Views используется для создания нового вида. Мы добавляем новый вид в общий список (Tlist), приказываем ему обращаться к фрейму при каждом изменении Valid и настраиваем MaxSpan

После того как все виды будут включены в список, мы определяем, сколько места следует выделить «вокруг» MaxSpan для фрейма, заголовка, кнопок и корешков вкладок. Для этого мы вычисляем разность между габаритами формы и габаритами PageControl.ActivePage.

TAbstractWizard выглядит почти так же, но оказывается чуть более сложным, потому что вместо вкладок мы используем три панели: внешнюю панель, панель заголовка (прижатую к верхнему краю — top-aligned) и панель фрейма (заполняющую клиентскую область — client-aligned). При активизации конкретной страницы мы просто переводим на передний план нужную внешнюю панель (листинг 10.9). 

Листинг 10.9. Метод TAbstractVizard.SetCurrentPage 

// из файла Wizards.pas

property CurrentPage: integer read  fCurrentPage
write SetCurrentPage;
procedure TAbstractWizard.SetCurrentPage
(Value: integer);
var
  LastPage, PageIsValid: boolean;
begin
  Assert(TObject(fPanels[Value]) is TPanel);
  Assert(TObject(fViews[Value]) is TAbstractView);
  // Сочетание Assert(is) со 'слепыми' 
  преобразованиями типов
  // обеспечивает отладочную безопасность 
  конструкции "as"
  // без (особой) потери производительности

  fCurrentPage := Value;
  TPanel(fPanels[Value]).BringToFront;

  LastPage := Value = fPageCount;
  PageIsValid 
  := TAbstractView(fViews[Value]).Valid;
  PrevBtn.Enabled := Value > 0;
  NextBtn.Enabled := PageIsValid and 
  (not LastPage);
  OkBtn.Enabled   := PageIsValid and LastPage;
end; // TAbstractWizard.SetCurrentPage

Как видно из листинга 10.9, еще одна приятная особенность Assert заключается в том, что пара «Assert/слепое преобразование типа» обеспечивает полноценную проверку на совместимость типов при отладке, но не отражается на производительности окончательной (поставляемой заказчику) версии. Во всем остальном код несложен: мы задаем fCurrentPage и переводим соответствующую панель на передний план. Затем проверяем, является ли данная страница первой или последней и корректно ли она заполнена (Valid), после чего соответствующим образом задаем состояние кнопок Prev, Next и OK. 

Оставшийся код в файлах WIZARDS.PAS и PROPERTYSHEETS.PAS не содержит никаких хитростей. Хотя я буду рад и польщен, если вы сочтете его достойным изучения, для успешного использования в нем совершенно не обязательно разбираться. Поэтому я не буду переводить на него бумагу; если этот код вас действительно заинтересует, найдите его на CD-ROM. 

Пример модели 

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

Модуль Data представляет собой «скелет» модуля данных с методами для создания, «загрузки» и «сохранения» объектов Employee (см. листинг 10.10). Вероятно, в реальном приложении эти методы будут представлять собой оболочки для процедур, работающих с базами данных; в нашем случае метод загрузки лишь извлекает «зашитые» в программе фиктивные данные, а метод сохранения вообще ничего не делает. Объект Employee содержит ссылки на два объекта People с личными данными работника и его начальника. Вид Employee View, изображенный на рис. 10.1 и 10.2, позволяет выбрать начальника из раскрывающегося списка, а также отредактировать имя и налоговый код (TaxID) работника. 

Самое интересное заключается в том, что для отображения сведений о работнике и начальнике применяется один и тот же вид — для этого создается две различные копии одного объекта вида. В режиме конструирования оба вида выглядят как пустые панели (см. рис. 10.6). При создании формы мы создаем два экземпляра вида PersonIdView (см. листинг 10.7) и размещаем их на соответствующих панелях формы EmployeeIdview

Рис. 10.6. Вид, одновременно являющийся фреймом

Рис. 10.7. PersonIdView в режиме конструирования 

Листинг 10.10. Модуль EMPLOYEEIDVIEWS.PAS

unit EmployeeIdViews;

// Copyright © 1997 by Jon Shemitz, 
//all rights reserved.
// Permission is hereby granted to freely use, 
//modify, and
// distribute this source code PROVIDED that 
//all six lines of
// this copyright and contact notice are 
//included without any
// changes. Questions? Comments? Offers of work?
// mailto:jon@midnightbeach.com

// ----------------------------------------

// Это достаточно правдоподобная реализация 
вида Employee ID.
// Она позволяет вводить имя и налоговый код, 
а также 
// указывать начальника.
interface

uses
  Windows, Messages, SysUtils, Classes, 
  Graphics, Controls,
  Forms, Dialogs, StdCtrls, ExtCtrls,
  Models, Embedded, FickleViews, PersonIdViews;

type
  TEmployeeIdView = class(TFickleView, IFrame)
    SupervisorPnl: TPanel;
    SupervisorCaptionPnl: TPanel;
    SupervisorFrame: TPanel;
    SelectSupervisor: TComboBox;
    SupervisorLbl: TLabel;
    EmployeeIdFrame: TPanel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure SelectSupervisorChange(Sender: 
    TObject);
  private
    SupervisorView,
    EmployeeView:   TPersonIdView;
  protected
    procedure ReadFromModel(Model: TModel);   
    override;
    procedure WriteToModel(Model:  TModel);   
    override;
    procedure SetReadOnly(Value: boolean);    
    override;
    procedure OnValidChanged( ChangingObject: 
    TObject;
                              View:           
                              IView );
  end;

implementation

{$R *.DFM}

uses Data;

// Создание/уничтожение

procedure TEmployeeIdView.FormCreate(Sender: 
TObject);
var
  Index: integer;
begin
  inherited;
  SupervisorView :=
    TPersonIdView.CreateEmbedded( Self,
SupervisorFrame,
efmCentered );
  SupervisorView.ReadOnly := True;
  SupervisorView.AddNotifiee(Self);

  EmployeeView :=
    TPersonIdView.CreateEmbedded( Self,
EmployeeIdFrame,
efmCentered );
  EmployeeView.AddNotifiee(Self);

  with DataModel do
    for Index := 0 to SupervisorCount - 1 do
      SelectSupervisor.Items.Add(
        GetEmployeeName(Supervisor[Index]) );
end; // TEmployeeIdView.FormCreate

procedure TEmployeeIdView.FormDestroy(Sender: 
TObject);
begin
  inherited;
  SupervisorView.RemoveNotifiee(Self);
  SupervisorView.Free;

  EmployeeView.RemoveNotifiee(Self);
  EmployeeView.Free;
end; // TEmployeeIdView.FormDestroy

// Переопределения IView

procedure TEmployeeIdView.ReadFromModel(Model: 
TModel);
begin
  Assert(Model is TEmployee);

  with TEmployee(Model) do
    begin
    SupervisorView.ReadFromModel(Supervisor);
    EmployeeView.ReadFromModel(Employee);

    SelectSupervisor.ItemIndex :=
      DataModel.IndexOfSupervisor(Supervisor.ID);
    end; // with
end; // TEmployeeIdView.ReadFromModel

procedure TEmployeeIdView.WriteToModel(Model:  
TModel);
begin
  Assert(Model is TEmployee);

  with TEmployee(Model) do
    begin
    SupervisorView.WriteToModel(Supervisor);
    EmployeeView.WriteToModel(Employee);
    end; // with
end; // TEmployeeIdView.WriteToModel

procedure TEmployeeIdView.SetReadOnly(Value: 
boolean);
begin
  inherited;
  EmployeeView.ReadOnly  := ReadOnly;
  SelectSupervisor.Color 
  := ShowReadOnly_EditColors[ReadOnly];
end; // TEmployeeIdView.SetReadOnly

// Изменение начальника

procedure TEmployeeIdView.SelectSupervisorChange
(Sender: TObject);
var
  ID:         TPersonID;
  Supervisor: TPerson;
begin
  inherited;
  ID := DataModel.Supervisor
  [SelectSupervisor.ItemIndex];
  Supervisor := DataModel.LoadPerson(ID);
  try
    SupervisorView.ReadFromModel(Supervisor);
  finally Supervisor.Free; end;
end; // TEmployeeIdView.SelectSupervisorChange

// Уведомление фрейма

procedure TEmployeeIdView.OnValidChanged(
  ChangingObject: TObject;
  View:           IView );
begin
  Valid := SupervisorView.Valid and 
  EmployeeView.Valid;
end; // TEmployeeIdView.OnValidChanged

end.

Процедура FormCreate создает два вида TPersonIDView и регистрируется как их фрейм. Вид начальника доступен только для чтения, однако начальника можно сменить с помощью раскрывающегося списка. FormDestroy отменяет регистрацию (то есть освобождает интерфейсную ссылку) и уничтожает внедренные формы. 

ReadFromModel и WriteToModel, в сущности, перепоручают свою работу внедренным видам. Обычно рекомендуется, чтобы все функции ввода/вывода моделей следовали этому примеру и с помощью Assert проверяли, относится ли аргумент-модель к ожидаемому типу. В этом случае при передаче неверного типа модели редактору (или неверного типа вида — процедуре настройки редактора модели) возникает runtime-ошибка. 

Другие применения 

EMBEDDEFORMS.DPR демонстрирует лишь два первых сценария из четырех, описанных в начале этой главы, — использование одной и той же формы для мастера и списка свойств, а также использование форм как компонентов. Я не привел ни одного реального примера для двух последних сценариев, связанных с использованием внедренных форм для совместной разработки диалогового окна со вкладками или с построением универсального редактора, способного работать с любым объектом иерархии. Тем не менее я продемонстрировал всю методику, необходимую для реализации этих, более редких сценариев. 

Чтобы построить диалоговое окно из нескольких независимых форм, достаточно породить каждую из них от TEmbeddedForm. Создайте вкладку для каждой страницы и в обработчике OnCreate диалогового окна вызовите Create Embedded для формы каждой страницы. Обычно я стараюсь соблюдать общее правило «Сам создал — сам уничтожай» и аккуратно уничтожаю страницы в обработчике OnDestroy, но, строго говоря, без этого можно обойтись, так как при уничтожении диалогового окна уничтожаются все его дочерние компоненты. Если вам потребуется постраничная проверка корректности (например, чтобы пользователь не мог покинуть страницу с неверными данными), используйте TFickleView вместо TEmbeddedForm

Универсальный редактор можно построить на основе абстрактного редактора моделей — объект-контейнер содержит все стандартные элементы, а пустая панель-фрейм предназначена для размещения специализированных элементов. Для каждого члена иерархии объектов можно создать функцию класса (class function), которая возвращает TViewClass. Это позволит универсальному редактору заполнить фрейм правильным видом, соответствующим редактируемому объекту. 

За последний год мне пришлось довольно много возиться с внедренными формами. Они помогают существенно упростить программу, сделать ее более надежной и гибкой. Такая возможность всегда присутствовала в Windows, но она оставалась невероятно сложной до тех пор, пока среда Delphi не сделала ее простой.


 

Предыдущая Содержание Следующая

Используются технологии uCoz

Rambler's Top100 Rambler's Top100
Используются технологии uCoz