Окна и шаблоны С++

Автор: Timoshenko D.
GameKeeper Ltd.

Источник: RSDN Magazine #1-2009
Опубликовано: 05.09.2009
Версия текста: 1.0
Введение
Концепция
Окна
Заключение
Литература

Говори, что хочешь сказать, просто и прямо.
Брайан Керниган

Исходные тексты к статье

Введение

Диалоговые окна – вещь достаточно тривиальная, а их разработка напоминает сгибание картонных коробочек. Достаточно скучная штука – разработка большого числа диалоговых окон. Я за свою жизнь «налепил» много окошек и «слепив» очередное, подумал о том, как бы сделать так, чтобы «сгибать» их было просто, легко и по возможность с минимумом ошибок. Повторное использование кода – лучшее средство от ошибок. Вот что у меня получилось.

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

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

Концепция

Окна

Какие бывают окна?

Примем, что у окна есть рамка – это окно-контейнер, вмещающий в себя окно-информационную панель. Рамка может содержать заголовок (header) и нижний колонтитул (footer). Как правило, в заголовке помещается какая-либо информация касательно смысла самого диалогового окна, а в нижней части окна обычно располагаются кнопки «Ok» и «Отмена». Информационная панель в свою очередь представляется диалоговой панелью, т.е. окном, имеющим лишь клиентскую область. Для VCL свойство формы BorderStyle следует установить в bsNone.

Таким образом, все окна данного стиля содержат одинакового типа рамку и информационные панели разных типов. Ниже показано, как это можно описать с помощью С++-шаблонов.

В качестве окна-рамки используется класс окна, который расширяет интерфейс TForm (например, CWnd для MFC). Соответственно, он наследуется от TForm. Это своего рода шаблон рамки, если нужна другая рамка – следует определить другой класс. Вот определение класса TDialogFrameForm:

ПРИМЕЧАНИЕ

Данная библиотека разработана с учетом особенностей Borland VCL, но адаптировать ее под любую другую оконную библиотеку не составляет труда.

        //---------------------------------------------------------------------------
        //template <typename T>
        class TDialogFrameForm : public TForm
{
__published: // IDE-managed Components
  TPanel *MainPanel;
  TButton *OkButton;
  TButton *CancelButton;
  TImage *DialogImage;
  TLabel *CaptionLabel;
  TLabel *HintLabel;
  TShape *Shape1;
  TImageList *DialogImageList;
  void__fastcall OkButtonClick(TObject *Sender);

private: // User declarations
  TInfoPanelFormBase *_infopanel;

public: // User declarations__fastcall TDialogFrameForm(TComponent* Owner);

private:
  template <typename T>
  void buildinaux(void)
  {
    Caption               = T::caption.c_str();
    CaptionLabel->Caption = T::caption.c_str();
    HintLabel->Caption    = T::hint.c_str();

//    boost::shared_ptr<Graphics::TBitmap> bmp(new Graphics::TBitmap());
    std::auto_ptr<Graphics::TBitmap> bmp(new Graphics::TBitmap());
    DialogImageList->GetBitmap(T::imageindex, bmp.get());

    DialogImage->Picture->Bitmap->Assign(bmp.get());
  }
  
public:
  TInfoPanelFormBase* panel(void) { return _infopanel; }

  template <typename T>
  void buildin(T *f)
  {
    Height -= MainPanel->Height - f->Constraints->MinHeight;
    MainPanel->Height = f->Constraints->MinHeight;
    // vcl will destroy the form
    _infopanel          = f;
    _infopanel->Parent  = MainPanel;
    _infopanel->Align   = alClient;
    _infopanel->Show();

    buildinaux<T>();
  }

  template <typename T>
  void setdata(consttypename T::Info_t &info)
  {
    static_cast<T*>(_infopanel)->setdata(info);
  }

  template <typename T>
  typename T::Info_t getdata(void) const
  {
    returnstatic_cast<T*>(_infopanel)->getdata();
  }
};

Давайте рассмотрим его подробнее. На рисунке 1 показан внешний вид получившегося окна.


Рисунок 1.

Для каждого типа диалога создается свой класс информационной панели. Он наследуется от базового класса информационной панели TInfoPanelFormBase, который объявляет виртуальный интерфейс и наследуется от TForm. Его определение представлено ниже.

        //---------------------------------------------------------------------------
        class TInfoPanelFormBase : public TForm
{
__published: // IDE-managed Componentsprivate: // User declarationsbool _drawHintFrame;

public:  // User declarations__fastcall TInfoPanelFormBase(TComponent* Owner);

public:
  void DrawHintFrame(bool v);

public:
  virtualvoid checkdata(void) const { /* empty */ }
  virtualvoid clear(void) { /* empty */ }
  virtualvoid readonly(bool readOnly) { /* empty */ }

///////////////////////////////////////////////////////////////////////////////// overrided functionsprotected:
  DYNAMIC void__fastcall Paint(void);
};

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

        virtual
        void checkdata(void) const { /* empty */ }

Эту функцию следует переопределить в производном классе. В ней реализуется проверка корректности введенных пользователем данных. Если обнаружены некорректные данные, функция должна сгенерировать исключение. Тип исключения в данном конкретном случае не важен. Это может быть как VCL Exception, так и производный от C++ std::exception, к примеру.

        virtual
        void clear(void) { /* empty */ }

Данный метод должен «очищать» содержимое панели.

        virtual
        void readonly(bool readOnly) { /* empty */ }

Данный метод переключает панель в состояние «только для чтения» и наоборот, в зависимости от значения параметра.

Реализуя эти интерфейсы, каждая конкретная информационная панель реализует свою, присущую только ей функциональность. Динамический полиморфизм здесь очень кстати.

Рассмотрим теперь определение одного из конкретных классов информационной панели (в примере их три).

        //---------------------------------------------------------------------------
        class TUserInfoPanelForm : public TInfoPanelFormBase
{
__published: // IDE-managed Components
  TLabeledEdit *LNameEdit;
  TLabeledEdit *FNameEdit;
  TLabeledEdit *UserNameEdit;
  TLabeledEdit *PasswordEdit;
  TLabeledEdit *PasswordRepeatEdit;

private: // User declarationspublic:
  typedef test::UserInfo Info_t;

public: // User declarations  __fastcall TUserInfoPanelForm(TComponent* Owner);

public:
  void setdata(const Info_t &useri);
  Info_t getdata(void) const;

  void readonly(bool ro);
  void showpass(bool v);
  void checkpass(void) const;

public:
  void checkdata(void) const;

public:
  staticconst size_t imageindex;
  staticconst AnsiString caption;
  staticconst AnsiString hint;
};

Вот его внешний вид:


ПРИМЕЧАНИЕ

Важно: следует устанавливать свойство панели Constraints->MinHeight. Это значение используется для встравивания панели внутрь рамки. Если значение этого свойства оставить нулевым, панель в диалоговом окне будет скрыта, т.к. ее вертикальный размер будет равен значению этого свойства, т.е. нулю.

Каждый конкретный класс информационной панели должен предоставить три статических открытых константных члена. В представленном листинге их объявление выглядит так:

        public:
  staticconst size_t imageindex;
  staticconst std::string caption;
  staticconst std::string hint;    

Это уже статический полиморфизм – данные используются шаблоном диалогового окна. Шаблон класса диалогового окна представлен ниже.

Помимо этих трех констант, в классе информационной панели следует определить и предоставить синоним типа данных, объект которого хранит и позволяет редактировать данная панель. Это объявление выглядит следующим образом.

        typedef test::UserInfo Info_t;

Определение самого UserInfo выглядит так.

        /*
*******************************************************************************
* User Information Holder
*******************************************************************************
*/
        struct UserInfo 
{
private:
  AnsiString _fname;
  AnsiString _lname;
  AnsiString _username;
  AnsiString _password;

public:
  explicit UserInfo(const AnsiString &fn = "", const AnsiString &ln = "",
    const AnsiString &user = "", const AnsiString &pass = ""):
      _fname(fn),
      _lname(ln),
      _username(user),
      _password(pass)
  {
    // empty
  }

public:
  const AnsiString& fname(void) const { return _fname; }
  const AnsiString& lname(void) const { return _lname; }
  const AnsiString& username(void) const { return _username; }
  const AnsiString& password(void) const { return _password; }

  void fname(const AnsiString& s) { _fname = s; }
  void lname(const AnsiString& s) { _lname = s; }
  void username(const AnsiString& s) { _username = s; }
  void password(const AnsiString& s) { _password = s; }
};

Данное имя (Info_t) используется в шаблоне класса диалогового окна, в котором объявляется синоним этого типа:

        typedef
        typename PanelT::Info_t info_type;

        /*
*******************************************************************************
* Dialog Window.
*******************************************************************************
*/
        template <typename PanelT, typename FrameT>
class DialogWindow
{
public:
  typedeftypename PanelT::Info_t info_type;

private:
  mutable boost::shared_ptr<FrameT> _window;

public:
  explicit DialogWindow(TComponent* Owner, const AnsiString &extracapt = "", const info_type& info = info_type()):
  _window(new FrameT(Owner))
  {
    _window->buildin(new PanelT(_window.get()));
    _window->Caption = _window->Caption + (extracapt != "" ? " - " + extracapt : extracapt);
    setdata(info);
  }

public:
  FrameT& framewindow(void) { return *_window; }
  PanelT& panelwindow(void) { returnstatic_cast<PanelT&>(*_window->panel()); }

public:
  TModalResult ShowModal(void) const
  {
    return _window->ShowModal();
  }

  void setdata(const info_type &info)
  {
    _window->setdata<PanelT>(info);
  }

  info_type getdata(void) const
  {
    return _window->getdata<PanelT>();
  }
};

Здесь используется boost::shared_ptr, но можно использвать std::auto_ptr. Это не имеет большого значения.

ПРИМЕЧАНИЕ

Следует помнить, что std::auto_ptr не может быть использован со стандартными контейнерами, т.к. семантика копирования в этом классе отличается от общепринятой. В std::auto_ptr реализована передача владения.

Если оконная библиотека позволяет создавать экземпляры окон, распределяя объекты статически, т.е. без использования оператора new (пр. MFC), указатель (FrameT*) можно заменить самим объектом.

Новые типы окон определяются так:

        // dialog types
        typedef DialogWindow<TUserInfoPanelForm, TDialogFrameForm> UserInfoDialogWindow;
typedef DialogWindow<TWeekDayInfoPanelForm, TDialogFrameForm> WeekInfoDialogWindow;
typedef DialogWindow<TRGBColorInfoPanelForm, TDialogFrameForm> RGBColorInfoDialogWindow;

А вот так это используется (качайте исходники):

        //---------------------------------------------------------------------------
        void
        __fastcall TMainForm::AddUserButtonClick(TObject *Sender)
{
  // create dialog window to add new info and pass (via default parameter - third)// data constructed by default constructor to it
  UserInfoDialogWindow w(this, "Добавление");
  if (w.ShowModal())
  {
    // user pressed ok// w.getdata() returns entered data
    _userInfoForm->setdata(w.getdata());
  }
}

//---------------------------------------------------------------------------void__fastcall TMainForm::EditUserButtonClick(TObject *Sender)
{
  // create dialog window and pass existing data to it
  UserInfoDialogWindow w(this, "Редактирование", _userInfoForm->getdata());

  if (w.ShowModal())
  {
    // use returned data w.getdata()
    _userInfoForm->setdata(w.getdata());
  }
}

Заключение

Что мы получили?

1. Мы вытащили функциональность рамки, то есть оформление (колонтитулы) и обработка кнопок «ОК» и «Отмена», в отдельный модуль, в данном случае С++-класс, и теперь, когда нам требуется новое окошко, следует лишь создать класс окна информационной панели, реализовать интерфейс и при помощи замечательного typedef объявить новый тип окна следующим образом:

      // dialog types
      typedef DialogWindow<TUserInfoPanelForm, TDialogFrameForm> UserInfoDialogWindow;
typedef DialogWindow<TWeekDayInfoPanelForm, TDialogFrameForm> WeekInfoDialogWindow;
typedef DialogWindow<TRGBColorInfoPanelForm, TDialogFrameForm> RGBColorInfoDialogWindow;

2. Функциональность информационных панелей также теперь отделена и может изменяться независимо от рамки. Объекты информационных панелей могут использоваться независимо.

3. Собирая панели и рамки, мы получаем количество различного типа окошек в комбинаторном порядке, т.е. количество типов новых окон, равное количеству типов панелей, умноженных на количество типов рамок.

СОВЕТ

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

4. И очевидный плюс повторного использования кода (как вследствие статического – шаблоны, так и динамического – наследование полиморфизма) – это минимизация ошибок.

5. Удобство использования с точки зрения клиентского кода.

Какие могут быть недостатки? Пока не вижу. Если видите Вы, буду признателен, если сообщите.

Литература

  1. Б. Страуструп «Язык программирования С++»;
  2. Решение сложных задач на С++ – Герб Саттер;
  3. Новые сложные задачи на С++ – Герб Саттер;
  4. Стандарты программирования на С++ – Герб Саттер, Андрей Александреску;
  5. Эффективное использование STL – Скотт Майерс;
  6. Эффективное программирование на С++ – Скотт Майерс;
  7. Современное программирование на С++ – Андрей Александреску.

Эта статья опубликована в журнале RSDN Magazine #1-2009. Информацию о журнале можно найти здесь