Главная Заказать журнал Почта
Реклама
О журнале От редакции Архив Реклама
Авторам Ссылки Работа Обратная связь
Свежий номер  Вышел 5-й номер журнала за 2002 год. Анонсы номера и статьи размещены на сайте.
“Программист” Подписка 2002 Агентство “Роспечать” Каталог “Газеты. Журналы. Книги. Учебные пособия. Товары.” (Россия и СНГ) Индекс: 80467
“Программист” Подписка 2002 Объединенный каталог “Почта России 2001” (Россия) Индекс: 45775
В Москве на “Программист” можно подписаться через агентство “Интер-почта”
“Интер-почта” Удобно и выгодно! Не выходя из офиса! Подписка на профессиональный журнал “Программист” В Москве и регионах. Тел. (095) 921-1138, 921-1142, 925-07-94
Вы также можете:
ЗАКАЗАТЬ любой номер журнала с доставкой по Москве

TAction в С++ Builder

Вячеслав Ермолаев
yerm@mail.ru


 

В статье описан механизм предопределенных стандартных действий (Action), а так же показано, как можно расширять их возможности, создавая свои стандартные действия на их основе. Представленный материал может быть интересен программистам начального и среднего уровня, использующим в своей работе RAD-инструментарий фирмы Borland (Delphi, C++ Builder).

Одним из эффективных инструментов для централизованного управления кодом является список действий TActionList. Обоснованное применение списка значительно упрощает программирование пользовательского интерфейса. 
Судите сами: современные концепции пользовательского интерфейса предполагают, что пользователь может выполнять одно и то же действие различными способами: через пункт меню (главного и/или всплывающего), кнопку на инструментальной панели, нажатие комбинации клавиш и т.д. В зависимости от состояния программы, это действие в каждый момент времени может быть доступно или недоступно. Естественно, элементы интерфейса (пункты меню, кнопки) должны адекватно отражать доступность или недоступность действия. Класс TAction, обеспечивая связь между действием и элементами интерфейса, сводит весь процесс управления доступностью/недоступностью к строкам:

MyAction->Enabled = true;

или

MyAction->Enabled = false;

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

Как правило, код, описывающий само действие, «вешается» на событие TAction::OnExecute. Управление состоянием действия можно автоматизировать, поручив слежение за доступностью действия в конкретный момент событию TAction::OnUpdate, которое запускается каждый раз в момент простоя (idle) программы.

Внешне логика действий выглядит довольно простой: когда срабатывает элемент управления (нажатие кнопки, выбор пункта меню), вызывается метод TAction::Execute(), который возбуждает событие TAction::OnExecute, где и выполняется пользовательский код. В большинстве случаев, когда программист использует для задания действия класс TAction и ограничивается только определением события OnExecute, все так и происходит. 

Однако разработчики VCL и CLX (все сказанное относится и к новой кроссплатформенной библиотеке, появившейся в 6-ой версии C++ Builder) предусмотрели и более гибкий механизм, порождающий не одно, а целую цепочку событий. Кроме самого TAction, в ней могут принять участие: список действий TActionList, к которому принадлежит действие, приложение TApplication, активная форма, главная форма приложения (если активная форма не является главной) и активный элемент управления. Рассмотрим, каким же образом, и в каком порядке обеспечивается совместная работа всех задействованных объектов. Для начала ознакомимся с некоторыми особенностями реализации TAction. Иерархия классов, реализующих действия показана на рис. 1. В основе иерархии лежит TBasicAction, который является базовым классом для всех объектов, реализующих действия. Более подробно с ним можно ознакомиться по справочной системе, я же отмечу следующие моменты: 

1. довольно прозрачная реализация виртуальных методов Execute() и Update(), которые, как и ожидалось, просто вызывают соответствующие события OnExecute и OnUpdate, если они определены;

2. наличие дополнительных виртуальных методов ExecuteTarget(TObject* Target), HandlesTarget(TObject* Target) и UpdateTarget(TObject* Target), которым в качестве аргумента передается некий целевой объект, и которые пока ничего не делают. В качестве целевого объекта передается указатель на активный элемент управления или на компонент. Что конкретно будет передаваться, определяется реализацией виртуального метода TComponent::ExecuteAction(TBasicAction* Action).

Рисунок 1. Иерархия классов, реализующих действия (Actions)

Следующим в иерархии идет класс TСontainedAction, в котором добавлены свойства (в частности, свойство ActionList) и методы, необходимые для работы в составе списка действий TActionList и, самое главное, переопределен метод Execute. Теперь этот метод запускает цепочку вызовов, которая в нотации Object Pascal выглядит следующим образом

function TContainedAction.Execute: Boolean;
begin
   Result := (ActionList <> nil) and ActionList.ExecuteAction(Self) or Application.ExecuteAction(Self) or inherited Execute or (SendAppMessage(CM_ACTIONEXECUTE, 0, Longint(Self)) = 1);
end;

Затем идет TCustomAction, в котором введена поддержка для свойств и методов пунктов меню и элементов управления, и, собственно, сам TAction. Эти два класса ничего нового в рассматриваемый механизм не вносят. Иерархию замыкают классы так называемых стандартных предопределеных действий (standart pre-defined actions classes). Главное их отличие от TAction – то, что в них переопределены ранее неиспользуемые методы ExecuteTarget, HandlesTarget и UpdateTarget, которые и делают основную работу. ExecuteTarget и UpdateTarget подменяют собой события OnExecute и OnUpdate. Метод HandlesAction реализует механизм проверки типа и состояния целевого объекта на предмет разрешения или запрещения выполнения ExecuteTarget или UpdateTarget. Таким образом, предопределенные стандартные действия – это действия, поведение которых может зависеть также от типа и состояния элемента управления, находящегося в фокусе.

Пару слов об TActionList. Это контейнер, содержащий список действий и предназначенный для организации работы с действиями в дизайн-режиме и централизованного управления действиями. Например, для выполнения групповых операций типа:

for (int i = ActionList->ActionCount; i--; )
{
   TAction* Action = (TAction*) ActionList->Actions[i]; 
   if (Action->Category == AnsiString(“Category1”))
   Action->Enabled = false;
}

Пройдемся теперь по всей цепочке вызовов методов и событий. При срабатывании элемента управления вызывается виртуальный метод TAction::Execute, но поскольку Execute для TAction не переопределен, реально вызывается метод предка TContainedAction::Execute() (рис. 2). Этот метод запускает последующую цепочку вызовов, первым из которых является метод ExecuteAction списка действий TActionList, который генерирует событие TActionList::OnExecute. При этом событию передается два параметра: указатель на действие TBasicAction* Action, сгенерировшее событие, и ссылка на булево значение &Handled. Второй параметр определяет, будет ли продолжена цепочка вызовов дальше. Для продолжения цепочки вызовов либо Handled должен быть равен false, либо событие OnExecute не определено. 

Если цепочка была продолжена, следующим вызывается метод ExecuteAction приложения TApplication, который генерирует событие TApplication::OnActionExecute с теми же параметрами, что и у предыдущего события, и с теми же условиями продолжения цепочки. Если и в этом случае цепочка не прервалась, вызовется метод TBasicAction::Execute и собственно событие TAction::OnExecute.

Здесь цепочка, как правило, заканчивается. Продолжиться она может только в случае, если событие OnExecute не задано, а использование TAction c не заданным OnExecute не имеет смысла.

Рисунок 2. Порядок вызовов методов при выполнении действия (Action)

Небольшое отступление. Изложенный выше порядок вызовов методов и событий противоречит порядку, изложенному в документации, поставляемой с C++ Builder 5. Авторы справочного руководства почему-то уверены, что вызовы выполняются в обратном порядке: сначала TAction::OnExecute, а потом, если TAction::OnExecute не задан, выполняются TActionList::OnExecute и TApplication::OnActionExecute (см. Borland C++ Builder Help, описание метода TContainedAction::Execute, Developing Guide: Programming with C++ Builder, раздел Executing actions). В том, что это не соответствует истине, легко убедиться, взглянув на реализацию метода TContainedAction::Execute или просто написав тестовый пример. В документации 6-ой версии эта досадная неточность исправлена.

Вернемся к оставшейся части цепочки. Она предназначена для работы в предопределенных стандартных действиях и заключается в отправке приложению сообщения CM_ACTIONEXECUTE с помощью функции SendAppMessage для VCL, или сообщения QEventType_CMActionExecute с помощью функции QApplication_sendEvent для CLX. В качестве второго параметра передается указатель на действие. Приложение переадресует сообщение на активную форму, которая вызывает виртуальный метод ExecuteAction(TBasicAction* Action) активного элемента управления. В этом случае вступают в действие виртуальные методы bool TAction::HandlesTarget(TObject* Target) и TAction::ExecuteTarget(TObjeсt* Target). Первый определяет возможность выполнения данного действия для целевого объекта Target, второй содержит выполняемый код, ассоциированный с данным действием. По умолчанию в качестве целевого объекта передается указатель на компонент, чей метод ExecuteAction был вызван. Однако это не обязательно так. Что будет передаваться в качестве целевого объекта, определяется конкретной реализацией метода ExecuteAction.

Таким образом, если использовать механизм вызова метода TComponent::ExecuteAction, в распоряжении программиста окажется более гибкий метод, чем при использовании TAction. К сожалению, использовать напрямую его можно только «программным» (невизуальным) способом, порождая от TAction свои классы и переопределяя их виртуальные методы HandlesTarget, ExecuteTarget и UpdateTarget. Отметим, что для Update выполняется примерно такая же цепочка вызовов, что и для Execute: TActionList::UpdateAction -> TApplication::UpdateAction -> TBasicAction::Update -> посылка сообщения CM_ACTIONUPDATE -> TComponent::UpdateAction -> TAction::UpdateTarget.

Попытаемся все-таки обойти это ограничение, создав свой предопределенный класс действий. Разработчики С++ Builder предоставили нам несколько предопределенных стандартных действий, производных от TEditAction, TWindowAction, THelpAction и TDataSetAction (в 6-ой версии этот набор существенно расширен)

Последние, определяющие действия над TDataSet, меня, программиста, постоянно работающего с базами данных, заинтересовали прежде всего. В отличие от обычного TAction, у них появилось свойство DataSource. Назначение его понятно из названия. Отмечу лишь одну интересную особенность: если явно не указать DataSource, то используется тот объект DataSource, с которым связан активный в данный момент элемент управления. Кроме того, автоматически отслеживается доступность действия в зависимости от состояния TDataSet. Попробуем унаследовать свой класс действий от этого класса. Для начала выясним, чeм нас не устраивает стандартный TDataSetAction.

К сожалению, в дизайн-режиме нельзя непосредственно использовать TDataSetAction. Возможно лишь использование предопределенных действий TDataSetCancel, TDataSetDelete, TDataSetEdit, TDataSetFirst, TDataSetInsert, TDataSetLast, TDataSetNext, TDataSetPost, TdataSetPrior и TDataSetRefresh. Это несколько ограничивает возможности применения. Кроме того, если не указывать DataSource, невозможно задать свое событие OnExecute, поскольку отсутствует возможность определить активный TDataSet, над которым надо производить действия. Нежелательно также вставлять дополнительный код в OnUpdate. В этом случае механизм отслеживания состояния TDataSet, определенный для каждого события в UpdateTarget, перестанет работать. Цепочка вызовов прервется на TBasicAction::Update, и до вызова UpdateTarget дело просто не дойдет.

В связи с этими ограничениями хотелось бы иметь некий расширенный класс, назовем его TExtDataSetAction, в котором в дизайн-режиме можно было бы задавать состояния TDataSet, при которых это действие разрешено, а также прописывать требуемый код, используя события OnExecute и OnUpdate. При этом, естественно, должны сохраниться все преимущества базового класса. Определим, какими дополнительными свойствами должен обладать TExtDataSetAction, чтобы удовлетворять предъявляемым требованиям.

Первое – набор состояний DataSetStates, который определяет те состояния DataSet, при которых действие разрешено, и который имеет тип Set<TDataSetState, dsInactiv, dsOpening>. 

Второе – набор состояний курсора CursorStates, имеющий тип TCursorStates: 

typedef enum { csBof, csEof, csEmpty } TCursorState;
typedef Set<TCursorState,csBof, csEmpty> TcursorStates;

и определяющих состояния курсора, при которых действие также разрешено. Здесь csBof – действие разрешено, если курсор указывает в начало таблицы, csEof – если в конец и csEmpty – если таблица пуста.

Третье свойство IsModify (тип bool) – признак того, что данное действие может изменять содержимое таблицы.

Четвертое дополнительное свойство – DataSetTarget типа TDataSet*, в котором хранится указатель на текущий активный TDataSet. Будем использовать это свойство, если свойство TDataSetAction::DataSource не задано.

Для того чтобы дать возможность программисту запрещать действие вне зависимости от состояния TDataSet, переопределим свойство Enabled, а также переопределим события OnExecute и OnUpdate с целью переноса их из методов Execute и Update в методы ExecuteTarget и UpdateTarget соответственно. В событие OnUpdate добавим параметр bool& Allow, с помощью которого можно определять дополнительные условия доступности действия. Для того чтобы правильно реализовать методы ExecuteTarget и UpdateTarget, нужно выяснить, что им передается в качестве целевого объекта. Анализ исходных кодов показал, все компоненты Data Controls в своих методах ExecuteAction и ExecuteUpdate вызывают методы ExecuteAction и ExecuteUpdate объекта тип TDataLink, который в качестве целевого объекта передает указатель на объект TDataSource. Ниже приведен текст описания интерфейса и реализации методов.

ExtDataSetAction.h:
#ifndef ExtDataSetActionH
#define ExtDataSetActionH
//---------------------------------------------------------------------------
#include <SysUtils.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <Forms.hpp>
#include <ActnList.hpp>
#include <DBActns.hpp>
//---------------------------------------------------------------------------
typedef Set<TDataSetState, dsInactive, dsOpening> TDataSetStates;
typedef enum {csBof, csEof, csEmpty} TCursorState;
typedef Set<TCursorState, csBof, csEmpty> TCursorStates;
typedef void __fastcall (__closure *TUpdateActionEvent)(System::TObject* Sender, bool& Allow);
class PACKAGE TExtDataSetAction : public TDataSetAction
{
private:
   TDataSetStates FDataSetStates;
   TCursorStates FCursorStates;
   bool FEnabled;
   TNotifyEvent FOnExecute;
   TUpdateActionEvent FOnUpdate;
   TDataSet* FTarget;
   bool FIsModify;
   void __fastcall SetDataSetStates(TDataSetStates value);
   void __fastcall SetCursorStates(TCursorStates value);
   void __fastcall SetEnabled(bool value);
   void __fastcall SetTarget(TDataSet* value);
   void __fastcall SetIsModify(bool value);
protected:
   virtual void __fastcall ExecuteTarget(TObject* Target);
   void __fastcall UpdateTarget(TObject* Target);
public:
   __fastcall TExtDataSetAction(TComponent* Owner);
   __property TDataSet* TargetDataSet = { read=FTarget, write=SetTarget };

   __published:
   __property DataSource;
   __property TDataSetStates DataSetStates = { read=FDataSetStates, write=SetDataSetStates, default=0x0E};
   __property TCursorStates CursorStates = { read=FCursorStates, write=SetCursorStates, default=0 };
   __property bool Enabled = { read=FEnabled, write=SetEnabled, default=true };
   __property TNotifyEvent OnExecute = { read=FOnExecute, write=FOnExecute };
   __property TUpdateActionEvent OnUpdate = { read=FOnUpdate, write=FOnUpdate };
   __property bool IsModify = { read=FIsModify, write=SetIsModify, default=false };
};
//---------------------------------------------------------------------------
#endif

ExtDataSetAction.сpp:
#include <vcl.h>
#pragma hdrstop
#include "ExtDataSetAction.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
__fastcall TExtDataSetAction::TExtDataSetAction(TComponent* Owner)
   : TDataSetAction(Owner),
   FEnabled(true),
   FOnExecute(NULL),
   FOnUpdate(NULL),
   FTarget(NULL)
{
   FDataSetStates << dsBrowse << dsEdit << dsInsert;
}
//---------------------------------------------------------------------------
void __fastcall TExtDataSetAction::SetDataSetStates(TDataSetStates value)
{
   if(FDataSetStates != value) {
   FDataSetStates = value;
 }
}
//---------------------------------------------------------------------------
void __fastcall TExtDataSetAction::SetCursorStates(TCursorStates value)
{
   if(FCursorStates != value) {
   FCursorStates = value;
 }
}
//---------------------------------------------------------------------------
void __fastcall TExtDataSetAction::SetEnabled(bool value)
{
   if(FEnabled != value) {
   FEnabled = value;
   if (!FEnabled)
  TDataSetAction::Enabled = false;
 }
}
//---------------------------------------------------------------------------
void __fastcall TExtDataSetAction::SetTarget(TDataSet* value)
{
   if(FTarget != value) {
  FTarget = value;
 }
}
//---------------------------------------------------------------------------
void __fastcall TExtDataSetAction::SetIsModify(bool value)
{
   if(FIsModify != value) {
   FIsModify = value;
 }
}
//---------------------------------------------------------------------------
void __fastcall TExtDataSetAction::ExecuteTarget(TObject* Target)
{
   FTarget = GetDataSet(Target);
   if (FOnExecute) FOnExecute(this);
}
//---------------------------------------------------------------------------
void __fastcall TExtDataSetAction::UpdateTarget(TObject* Target)
{
   if (!FEnabled) return;
      FTarget = GetDataSet(Target);
      bool bAllow = false;
   if(FOnExecute && FTarget)
   {
   bAllow = FDataSetStates.Contains(FTarget->State);
   if(bAllow && FTarget->Active)
      bAllow = (FIsModify?FTarget->CanModify:true) &&
         (FCursorStates.Contains(csBof)?true:!FTarget->Bof) &&
         (FCursorStates.Contains(csEof)?true:!FTarget->Eof) &&
         (FCursorStates.Contains(csEmpty)?true:!FTarget->IsEmpty());
   if (FOnUpdate)
      FOnUpdate(this,bAllow);
   }
      TDataSetAction::Enabled = bAllow;
}
//---------------------------------------------------------------------------
namespace Extdatasetaction
{
   void __fastcall PACKAGE Register()
   {
      RegisterActions("ExtDataSet", &__classid(TExtDataSetAction),0,NULL);
   }
}
//---------------------------------------------------------------------------

Кроме того, о существовании этого класса надо каким-то образом известить среду разработки. Для этого разработчики C++ Builder предоставили функцию регистрации действий:

extern PACKAGE void __fastcall RegisterActions(constAnsiString CategoryName, TMetaClass* * AClasses, const int AClasses_Size, TMetaClass* Resource);

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

namespace Extdatasetaction
{
   void __fastcall PACKAGE Register()
   {
      RegisterActions("ExtDataSet", &__classid(TExtDataSetAction), 0, NULL);
   }
}

Результат нашей работы вставляем в проект нового package или добавляем в уже существующий. После компиляции и линковки инсталлируем созданный package. Внешне в палитре компонентов после инсталляции ничего не изменится, но при работе с TActionList при выборе пункта New Standard Action в всплывающем меню откроется диалоговое окно со списком стандартных действий, среди которых мы можем найти и TExtDataSetAction (рис. 3). 

Рисунок 3. Список стандартных действий

Теперь о том, как этим пользоваться. Попробуем определить действие «Переход в начало таблицы», которое не может быть выполнено, если таблица находится в режиме редактирования/вставки. В инспекторе для CursorStates устанавливаем csEof в true, а csBof и csEmpty в false, что будет означать, что действие запрещено, если таблица пуста и курсор позиционирован на первую запись. Также сбрасываем все флаги DataSetStates в false, за исключением dsBrowse – действие разрешено, когда таблица находится в состоянии просмотра.

Определим событие OnExecute:

void __fastcall TForm1::ExtDataSetFirstExecute(TObject *Sender)
{
   ((TExtDataSetAction*)Sender)->TargetDataSet->First();
}

Вот и все. Событие будет отслеживать заданные условия автоматически и либо разрешать, либо запрещать действие в соответствии с заданными ограничениями (рис. 4). На рис.5 показана реализация действия Post c наложением дополнительного условия. В принципе, на основе TExtDataSetAction можно создать более универсальный компонент, в котором программист в дизайн-режиме сможет определять, какой из механизмов выполнения действия использовать. Этого можно добиться, если не перекрывать методы OnExecute и OnUpdate, а дополнительно добавить OnTargetExecute и OnTargetUpdate.
Полностью код компонента TExtDataSetAction можно скачать с сайта журнала.

Рисунок 4. Реализация действия «В начало таблицы»


Рисунок 5. Реализация действия «Сохранить» (Post)

СКАЧАТЬ ИСХОДНИК К СТАТЬЕ

В начало

Голосование

Что такое программирование?

искусство
наука
ремесло
не знаю

Результаты предыдущих голосований


Форумы
 обсуждение журнала/ общение с редакцией
 web-дизайн/ программирование
 windows программирование
 unix программирование
 3D графика
 Hardware

Рассылка

Подпишитесь на нашу почтовую рассылку. подписаться
отписаться


125047, г. Москва, ул. Бутырский вал д.20 тел.: 250-3801 факс: 250-2121 e-mail: info@programme.ru
© ПРОГРАММИСТ 2000. Все права сохранены
Перепечатка материалов без письменного разрешения редакции запрещена.
Поддержка: mailto:webmaster@programme.ruДизайн: NETOFFICE-DESIGN

  Rambler's Top100 Rambler's Top100


Сайт управляется системой uCoz