Cырость первого билда релиза Delphi4 поразила. Раньше Borland себе такого не позволяла. Однако что касается новых средств для программиста, то они оказались нужными и удобными. О применении одного из них и пойдет речь.
Те, кому приходится заниматься созданием насыщенных пользовательских интерфейсов с большим количеством контекстно-зависимых элементов управления (кнопок, пунктов меню и т.п.), хорошо представляют себе проблему обновления их состояния при каждой передаче фокуса между объектами или при изменениях состояния объектов. В VCL 4.0 появился класс, называемый TAction (действие), и механизм обновления состояния объектов-действий в режиме простоя (on idle). Вышеуказанная проблема принципиально разрешилась одним махом поддержанным производителем кодом. Уже этого было достаточно, чтобы мириться с залипаниями билда 5.33 и ждать патчей (во всяком случае, для меня).
Если не вдаваться в детали, то все выглядит следующим образом. Классы объектов управления, посредством которых пользователь выполняет некие действия, имеют сходный набор свойств - Caption, Visible, Enabled и др. Теперь все они собраны в классе TAction. Многие из элементов управления имеют свойство Action, при указании которого (подписке на действие) происходит копирование всех сходных свойств из объекта класса TAction (или его потомка). Кроме этого, при изменении любого свойства объекта действия происходит автоматическое обновление сходных свойств для всех элементов управления, подписанных на это действие. Таким образом, программа может предоставлять пользователю выполнить операцию "Вставка" через главное меню, через кнопку на панели инструментов, через всплывающее меню (для каждого из множества MDI-окон). Разрешить ее выполнение можно тогда, когда в буфере обмена что-то есть. Теперь достаточно управлять одним объектом действия, а не всеми вышеперечисленными объектами, которые автоматически обновятся, поскольку подписаны на это действие.
Как это принято в Delphi, можно создавать новые классы действий и регистрировать их в системе. VCL предоставляет целый ряд таких классов (для описанного выше случая для компонентов редактирования текста - TEditPaste). Можно настроить поведение стандартного действия TAction с помощью обработчиков событий, но мы предположим, что действие настолько часто используемо, что оправдано создание для него отдельного класса. Попробуем создать его.
Допустим, что в нашей программе используются объекты, которые поддерживают выполнение некоторой операции, например, горения. Нам необходимо, чтобы интерфейсные элементы управления были активны всякий раз, когда "горящий" объект получает фокус. Создадим класс действия: (листинг 1.)
Допустим, что все "горящие" объекты происходят от одного базового класса TBurned, который имеет метод Fire. Первый метод определяет, тот ли это компонент, для которого нужно обрабатывать состояние действия: (листинг 2.)
Так как объект "безусловно горит", то действие доступно всегда: (листинг 3.)
Собственно процедура сжигания: (листинг 4.)
На практике, однако, приходится существующим компонентам придавать одинаковые свойства. Допустим, нужно сделать "горящими" строку ввода (TEdit), кнопку (TButton). При этом в силу отсутствия в Object Pascal множественного наследования нужно по-другому выполнять проверку "тот ли это объект". Для этого компоненты (TBurnEdit, TBurnButton) должны иметь специально опубликованные свойства (чтобы воспользоваться реестром RTTI -Run-Time Type Information - информация о типе времени выполнения VCL), по которым их можно опознать как "горящие". Соответственно и методы класса TBurnAction придется реализовать по-другому. Допустим, компоненты реализуют published свойство Burn: Boolean, которое осуществляет вызов метода Fire при присвоении ему любого значения. Тогда реализация методов действия следующая (не забыв упомянуть модуль TypInfo): (листинг 5.)
(Подробнее об использовании TypInfo можно посмотреть, например, у Рэя Лишнера в книге "Секреты Delphi2".)
Эту задачу можно решить и более элегантно, если воспользоваться COM-технологией (ее реализация как раз допускает множественное наследование, но интерфейсов!). Для этого нужно реализовать интерфейс IBurn для каждого "горящего" компонента: (листинг 6.)
Определение того, поддерживает ли объект "горение", сводится при этом к выяснению факта поддержки нужного интерфейса. Методы TBurnAction будут выглядеть следующим образом. (листинг 7.)
Если шагнуть дальше в сторону OLE и разместить объекты в объектах-контейнерах, использовав вызов QueryInterface вместо GetInterface, можно вообще отвязаться от одного модуля и одного компилятора и собрать работающую систему из объектов, расположенных в разных модулях - как в основном EXE, так и в серверах (например, в процессе, inproc server, DLL). При этом в качестве параметра Target в методах класса TBurnAction будет выступать контейнер, который предоставляет доступ к объекту, реализующему интерфейс IBurn. Обычно на таких принципах делаются не слишком маленькие и не слишком закрытые системы, и при этом нужно решить ряд "попутных" проблем не только из области OLE, однако это вполне возможно при определенных усилиях.
Подытоживая, можно отметить, что классы TAction позволяют централизовать управление доступом пользователя к сервисам системы. В этом качестве они являются прекрасным средством для сборки надежных и сложных настраиваемых программных конструкций. Что касается сервисов, доступом к которым удобно управлять с помощью объектов-действий, то это - тема для отдельного разговора.
Юрий А. СМАНЦЕР,
системный аналитик
(1) TBurnAction = class(TAction) public function HandlesTarget(Target: TObject): Boolean; override; procedure UpdateTarget(Target: TObject); override; procedure ExecuteTarget(Target: TObject); override; end; (2) function TBurnAction.HandlesTarget(Target: TObject): Boolean; begin Result := (Target is TBurned)) and TBurned (Target).Focused; end; (3) procedure TBurnAction.UpdateTarget(Target: TObject); begin Enabled := True; end; (4) procedure TBurnAction.ExecuteTarget(Target: TObject); begin TBurned(Target).Fire; end; (5) function TBurnAction.HandlesTarget(Target: TObject): Boolean; var PropInfo: PPropInfo; begin PropInfo := GetPropInfo(Target.ClassInfo, 'Burn'); Result := (PropInfo <> nil) and TWinControl(Target).Focused; end; procedure TBurnAction.ExecuteTarget(Target: TObject); var PropInfo: PPropInfo; begin PropInfo := GetPropInfo(Target.ClassInfo, 'Burn'); if PropInfo <> nil then SetOrdProp(Target, PropInfo, 0); end; (6) IBurn = interface(IUnknown) ['{FFFFFF01-FFFF-FFFF-FFFF-FFFFFFFFFFFF}'] procedure Fire; stdcall; function IsFocused: BOOL; stdcall; end; TBurnEdit = class(TEdit, IBurn) ... end; TBurnButton = class(TButton, IBurn) ... end; (7) function TBurnAction.HandlesTarget(Target: TObject): Boolean; var Obj: IBurn; begin Result := GetInterface(IBurn, Obj) and Obj.IsFocused; end; procedure TBurnAction.ExecuteTarget(Target: TObject); var Obj: IBurn; begin GetInterface(IBurn, Obj); Obj.Fire; end;
Горячие темы