Перевод книги Julian’а Smart’а – Глава IX – Написание собственных диалогов – Часть I

Скачать PDF-версию (342 КБ)

Рано или поздно вам понадобится создать собственный диалог, будь то простой диалог, состоящий из текста и нескольких кнопок, или сложный диалог с вкладками, множеством панелей, собственными элементами управления, контекстной помощью и т.п. В этой главе мы рассмотрим основные принципы создания диалогов, а также передачу данных между переменными C++ и элементами управления. Также будет рассказано об использовании ресурсов, которые позволяют загружать диалоги и другие элементы интерфейса из XML-файлов.

Шаги для создания собственных диалогов

Самое интересное программирование начинается, когда вы начнете создавать свои собственные диалоги. Вот список шагов, которые необходимо для этого сделать:

  1. Создать наследника от wxDialog.
  2. Решить, где будут храниться данные диалога и как приложение получит к ним доступ.
  3. Написать код создания и размещения элементов управления.
  4. Добавить код, отвечающий за пересылку данных между данными диалога и элементами управления.
  5. Написать обработчики событий от элементов управления.
  6. Добавить обработку обновления пользовательского интерфейса, чтобы элементы управления всегда были в правильном состоянии.
  7. Добавить помощь, в частности всплывающие подсказки, контекстную помощь (данная возможность не реализована в Mac OS X) и справочную систему.
  8. Вызвать диалог из подходящего места вашего приложения.

Проиллюстрируем указанные шаги на конкретном примере.

Пример: PersonalRecordDialog

Как вы знаете из прошлой главы диалоги бывают двух видов: модальные и не модальные. Мы будем делать модальный диалог, так как это более распространенный и менее сложный в реализации тип. Приложение вызывает диалог с помощью ShowModal, а далее получает из диалога выбор пользователя. До того как ShowModal возвратит результат все взаимодействие с пользователем заключено в маленький мир внутри вашего диалога (и любых других модальных диалогов, которые ваш диалог может вызвать).

Многие шаги по созданию собственного диалога можно очень легко выполнить с использованием редакторов диалогов, таких как wxDesigner или DialogBlocks. С их помощью, в зависимости от сложности диалога, можно в автоматическом режиме выполнить очень большой объем работы по написанию кода. В этой главе исключительно для демонстрации базовых принципов мы все будем все делать вручную. Однако в реальной работе рекомендуется использовать такого рода инструменты для экономии кучи часов повторяющейся работы.

Мы проиллюстрируем шаги, необходимые для создания диалога с помощью диалога в котором пользователь вводит свое имя, возраст, пол и хочет ли он проголосовать. Диалог называется PersonalRecordDialog и показан на рисунке.

< рисунок пропущен>

Кнопка “Reset” (Сброс) устанавливает значение всех элементов управления в их начальные значения. Кнопка “OK” закрывает диалог и возвращает wxID_OK из ShowModal. Кнопка “Cancel” возвращает wxID_CANCEL и не обновляет содержимое переменных диалога. Кнопка “Help” (Помощь) выводит несколько строк с описанием диалога, (хотя в реальном приложении эта кнопка должна вызывать полноценную страницу помощи в справочной системе).

Хороший пользовательский интерфейс не должен позволять пользователю вводить данные, которые не имеют значения в текущей ситуации. Например, пользователь не должен иметь возможность использовать элемент управления “Vote” (Голосовать), если его возраст меньше чем допустимый для голосования возраст (18 лет для США или Великобритании). Поэтому необходимо убедиться, что при возрасте менее 18 лет чек-бокс Vote заблокирован для ввода.

Создаем новый класс-наследник

Далее приведено объявление нашего PersonalRecordDialog. Информация времени выполнения о типе вводится с помощью макроса DECLARE_CLASS, а добавление таблицы событий – с помощью DECLARE_EVENT_TABLE.

/*!
* Объявление класса PersonalRecordDialog
*/

class PersonalRecordDialog: public wxDialog
{
    DECLARE_CLASS( PersonalRecordDialog )
    DECLARE_EVENT_TABLE()

public:
    // Конструкторы
    PersonalRecordDialog( );
    PersonalRecordDialog( wxWindow* parent,
      wxWindowID id = wxID_ANY,
      const wxString& caption = wxT("Personal Record"),
      const wxPoint& pos = wxDefaultPosition,
      const wxSize& size = wxDefaultSize,
      long style = wxCAPTION|wxRESIZE_BORDER|wxSYSTEM_MENU );

    // Инициализация наших переменных
    void Init();

    // Создание
    bool Create( wxWindow* parent,
      wxWindowID id = wxID_ANY,
      const wxString& caption = wxT("Personal Record"),
      const wxPoint& pos = wxDefaultPosition,
      const wxSize& size = wxDefaultSize,
      long style = wxCAPTION|wxRESIZE_BORDER|wxSYSTEM_MENU );

    // Создание элементов управления и сайзеров
    void CreateControls();
};

Заметим, что следуя принятому в wxWidgets соглашению позволять одно- и двушаговое конструирование мы предоставляем конструктор по умолчанию и функцию Create, а также расширенный конструктор.

Проектируем хранение данных

У нас есть четыре части данных, которые нужно хранить: имя (строка), возраст (целое), пол (логическое) и предпочтения по голосованию (логическое). Чтобы упростить использование элемента управления wxChoice мы будем использовать целочисленный тип для хранения логического типа для пола, но интерфейс класса будет представлять его как логическое значение: true для женского пола и false для мужского. Давайте добавим эти данные и методы для работы с ними в класс PersonalRecordDialog:

// Данные
wxString    m_name;
int         m_age;
int         m_sex;
bool        m_vote;

// Доступ к имени
void SetName(const wxString& name) { m_name = name; }
wxString GetName() const { return m_name; }

// Доступ к возрасту
void SetAge(int age) { m_age = age; }
int GetAge() const { return m_age; }

// Доступ к полу (мужской = false, женский = true)
void SetSex(bool sex) { sex ? m_sex = 1 : m_sex = 0; }
bool GetSex() const { return m_sex == 1; }

// Будет ли пользователь голосовать?
void SetVote(bool vote) { m_vote = vote; }
bool GetVote() const { return m_vote; }

Создание элементов управления и их размещение

Теперь добавим функцию CreateControls, которая будет вызываться из Create. CreateControls добавляет несколько элементов управления типа wxStaticText, wxButton, wxSpinCtrl, wxTextCtrl, wxChoice и wxCheckBox. Обратитесь к рисунку 9-1, чтобы посмотреть результирующий диалог.

Мы используем основанный на сайзерах макет для этого диалога, из-за чего код выглядит достаточно сложно для такого небольшого числа элементов управления (мы уже коротко описывали сайзеры в Главе 7 “Размещение элементов с помощью сайзеров”, где рассказывали, что они позволяются создавать диалоги, которые нормально выглядят на множестве платформ и которые легко адаптировать для перевода и изменения размера). Вы можете использовать и другие методы, такие как загрузка диалога из ресурсов wxWidgets (XRC-файлов).

Основной принцип основанного на сайзерах размещения – поместить управляющие элементы во вложенные боксы (сайзеры), которые распределят место между элементами управления или растянуться, чтобы стать достаточными для их размещения. Сайзеры не являются окнами, они формируют отдельную иерархию и поэтому элементы управления остаются детьми своих родителей, независимо от сложности иерархии сайзеров. Вы можете освежить свою память и посмотреть на схематический вид размещения сайзеров, который показан на рисунке 7-2 в Главе 7.

В CreateControls мы используем вертикальный сайзер (boxSizer), вложенный в другой вертикальный сайзер (topSizer), чтобы получить достаточный отступ между элементам управления и границами диалога. Один горизонтальный сайзер используется для размещения wxSpinCtrl, wxChoice и wxCheckBox, а другой (okCancelSizer) – для кнопок Reset, OK, Cancel и Help.

/*!
* Control creation for PersonalRecordDialog
*/
void PersonalRecordDialog::CreateControls()
{
    // Сайзер верхнего уровня

    wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL);
    this->SetSizer(topSizer);

    // Второй сайзер, чтобы получить больше пространства вокруг элементов

    wxBoxSizer* boxSizer = new wxBoxSizer(wxVERTICAL);
    topSizer->Add(boxSizer, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5);

    // Некоторый текст

    wxStaticText* descr = new wxStaticText( this, wxID_STATIC,
        wxT("Please enter your name, age and sex, and specify whether\
        you wish to\nvote in a general election."),
        wxDefaultPosition, wxDefaultSize, 0 );
    boxSizer->Add(descr, 0, wxALIGN_LEFT|wxALL, 5);

    // Отступ

    boxSizer->Add(5, 5, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5);

    // Метка для текстового элемента

    wxStaticText* nameLabel = new wxStaticText ( this, wxID_STATIC,
        wxT("&Name:"), wxDefaultPosition, wxDefaultSize, 0 );
    boxSizer->Add(nameLabel, 0, wxALIGN_LEFT|wxALL, 5);

    // Тестовый элемент для получения имени пользователя

    wxTextCtrl* nameCtrl = new wxTextCtrl ( this, ID_NAME, wxT("Emma"),
        wxDefaultPosition, wxDefaultSize, 0 );
    boxSizer->Add(nameCtrl, 0, wxGROW|wxALL, 5);

    // Горизонтальный сайзер, содержащий возраст, пол и флаг голосования

    wxBoxSizer* ageSexVoteBox = new wxBoxSizer(wxHORIZONTAL);
    boxSizer->Add(ageSexVoteBox, 0, wxGROW|wxALL, 5);

    // Метка для возраста

    wxStaticText* ageLabel = new wxStaticText ( this, wxID_STATIC,
        wxT("&Age:"), wxDefaultPosition, wxDefaultSize, 0 );
    ageSexVoteBox->Add(ageLabel, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);

    // Спиновый элемент для получения возраста

    wxSpinCtrl* ageSpin = new wxSpinCtrl ( this, ID_AGE,
        wxEmptyString, wxDefaultPosition, wxSize(60, -1),
        wxSP_ARROW_KEYS, 0, 120, 25 );
    ageSexVoteBox->Add(ageSpin, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);

    // Метка для пола

    wxStaticText* sexLabel = new wxStaticText ( this, wxID_STATIC,
        wxT("&Sex:"), wxDefaultPosition, wxDefaultSize, 0 );
    ageSexVoteBox->Add(sexLabel, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);

    // Создаем выпадающий список для выбора пола
    wxString sexStrings[] = {
        wxT("Male"),
        wxT("Female")
    };

    wxChoice* sexChoice = new wxChoice ( this, ID_SEX,
        wxDefaultPosition, wxSize(80, -1), WXSIZEOF(sexStrings),
            sexStrings, 0 );
    sexChoice->SetStringSelection(wxT("Female"));
    ageSexVoteBox->Add(sexChoice, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);

    // Добавляем растяжимый отступ, который перемещает элемент для
    // голосования направо

    ageSexVoteBox->Add(5, 5, 1, wxALIGN_CENTER_VERTICAL|wxALL, 5);

    wxCheckBox* voteCheckBox = new wxCheckBox( this, ID_VOTE,
       wxT("&Vote"), wxDefaultPosition, wxDefaultSize, 0 );
    voteCheckBox ->SetValue(true);
    ageSexVoteBox->Add(voteCheckBox, 0,
        wxALIGN_CENTER_VERTICAL|wxALL, 5);

    // Создаем разделяющую черту перед кнопками OK и Cancel

    wxStaticLine* line = new wxStaticLine ( this, wxID_STATIC,
        wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
    boxSizer->Add(line, 0, wxGROW|wxALL, 5);

    // Горизонтальный сайзер содержит кнопки Reset, OK, Cancel и Help

    wxBoxSizer* okCancelBox = new wxBoxSizer(wxHORIZONTAL);
    boxSizer->Add(okCancelBox, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5);

    // Кнопка Reset

    wxButton* reset = new wxButton( this, ID_RESET, wxT("&Reset"),
        wxDefaultPosition, wxDefaultSize, 0 );
    okCancelBox->Add(reset, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);

    // Кнопка OK

    wxButton* ok = new wxButton ( this, wxID_OK, wxT("&OK"),
        wxDefaultPosition, wxDefaultSize, 0 );
    okCancelBox->Add(ok, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);

    // Кнопка Cancel

    wxButton* cancel = new wxButton ( this, wxID_CANCEL,
        wxT("&Cancel"), wxDefaultPosition, wxDefaultSize, 0 );
    okCancelBox->Add(cancel, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);

    // Кнопка Help

    wxButton* help = new wxButton( this, wxID_HELP, wxT("&Help"),
        wxDefaultPosition, wxDefaultSize, 0 );
    okCancelBox->Add(help, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);
}

Пересылка данных и их проверка

Теперь у нас в диалоге есть несколько элементов управления, но они пока не соеденены с переменными из диалога. Как можно реализовать такую связь?

Когда диалог показывается в первый раз wxWidgets вызывает InitDialog, который в свою очередь генерирует событие wxEVT_INIT_DIALOG. Обработчик по умолчанию для данного события вызывает TransferDataToWindow для диалога. Чтобы переслать информацию из элементов управления обратно в переменные вы должны вызвать TransferDataFromWindow, когда пользователь подтвердит свой выбор. Тоже самое делает wxWidgets в обработчике по умолчанию для управляющего события wxID_OK, который вызывает TransferDataFromWindow перед вызовом метода EndModal (который непосредственно закрывает диалог).

Поэтому вы можете переопределить TransferDataToWindow и TransferDataFromWindow для пересылки ваших данных. Для нашего диалога код может выглядеть следующим образом:

/*!
* Посылаем данные в окно
*/

bool PersonalRecordDialog::TransferDataToWindow()
{
    wxTextCtrl* nameCtrl = (wxTextCtrl*) FindWindow(ID_NAME);
    wxSpinCtrl* ageCtrl = (wxSpinCtrl*) FindWindow(ID_SAGE);
    wxChoice* sexCtrl = (wxChoice*) FindWindow(ID_SEX);
    wxCheckBox* voteCtrl = (wxCheckBox*) FindWindow(ID_VOTE);

    nameCtrl->SetValue(m_name);
    ageCtrl->SetValue(m_age);
    sexCtrl->SetSelection(m_sex);
    voteCtrl->SetValue(m_vote);

    return true;
}

/*!
* Получаем данные из окна
*/

bool PersonalRecordDialog::TransferDataFromWindow()
{
    wxTextCtrl* nameCtrl = (wxTextCtrl*) FindWindow(ID_NAME);
    wxSpinCtrl* ageCtrl = (wxSpinCtrl*) FindWindow(ID_SAGE);
    wxChoice* sexCtrl = (wxChoice*) FindWindow(ID_SEX);
    wxCheckBox* voteCtrl = (wxCheckBox*) FindWindow(ID_VOTE);

    m_name = nameCtrl->GetValue();
    m_age = ageCtrl->GetValue();
    m_sex = sexCtrl->GetSelection();
    m_vote = voteCtrl->GetValue();

    return true;
}

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

FindWindow(ID_NAME)->SetValidator(
      wxTextValidator(wxFILTER_ALPHA, &m_name));
FindWindow(ID_AGE)->SetValidator(
      wxGenericValidator(& m_age));
FindWindow(ID_SEX)->SetValidator(
      wxGenericValidator(& m_sex);
FindWindow(ID_VOTE)->SetValidator(
      wxGenericValidator(& m_vote);

Эти несколько строк в конце CreateControls заменяют две переопределенные функции. В качестве бонуса пользователю будет запрещено использовать цифры в строке с именем.

Валидаторы могут выполнять две работы. Кроме передачи данных они также могут проверять данные и показывать сообщение об ошибке, если данные не удовлетворяют заданным критериям. В нашем примере кроме небольшой проверки имени не делается других проверок. wxGenericValidator относительно простой класс предназначенный только для передачи данных. Однако он может работать с множеством стандартных базовых элементов управления. Другие валидаторы (например wxTextValidator) имеют более сложное поведение и могут даже перехватывать нажатия на клавиши, чтобы запретить ввод недопустимых символов. Для нашего примера мы используем стандартный стиль wxFILTER_ALPHA, но можно также явно определить какие символы должны или не должны считаться допустимыми с помощью методов SetIncludes и SetExcludes.

Изучим подробнее каким образом wxWidgets обрабатывает валидаторы. Как было упомянуто ранее стандартный обработчик OnOK вызывает TransferDataToWindow, но до этого он вызывает Validate, запрещая вызов TransferDataToWindow и EndModal, если вызов закончится неудачно. Вот стандартная реализация OnOK:

void wxDialog::OnOK(wxCommandEvent& event)
{
    if ( Validate() && TransferDataFromWindow() )
    {
        if ( IsModal() )
            EndModal(wxID_OK); // Если диалог модальный
        else
        {
            SetReturnCode(wxID_OK);
            this->Show(false); // Если диалог не модальный
        }
    }
}

Обычная реализация Validate перебирает всех детей диалога (и их детей, если определен дополнительный стиль окна wxWS_EX_VALIDATE_RECURSIVELY) и вызывает Validate для каждого wxValidator элементов управления. Если любой из этих вызовов возвратит ошибку, то проверка диалога закончится неудачно и диалог не будет закрыт. Ожидается, что сообщение об ошибке будет вызвано снаружи функции Validate, если процедура проверки закончится неудачей.

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

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

Так как двух представленных выше классов-валидаторов может быть не достаточно для ваших нужд (особенно если вы пишете свои собственные элементы управления), то у вас есть возможность унаследовать свой класс от wxValidator. Этот класс должен иметь конструктор копирования и функцию Clone, которая возвращает копию объекта-валидатора, а также реализацию для передачи данных и проверки. Валидатор обычно также хранит указатель на переменную языка C++. В файлах include/wx/valtext.h и src/common/valtext.cpp дистрибутива wxWidgets можно посмотреть как реализовать валидатор. Также обратитесь к разделу “Написание собственных элементов управления” в Главе 12 “Продвинутые классы окон”.

Обработка событий

Для нашего примера нам не потребуется писать обработчики для кнопок OK и Cancel, так как мы можем использовать стандартные обработчики. Для этого достаточно использовать стандартные идентификаторы wxID_OK и wxID_CANCEL для этих кнопок.

Однако, для нетривиальных диалогов вам возможно потребуется перехватывать и обрабатывать события от элементов управления. В нашем примере у нас есть кнопка Reset, которую можно нажать в любой момент, чтобы сбросить значения в диалоге в их начальные значения. Мы добавим обработчик OnResetClick и соответствующую запись в таблицу обработчиков событий. Реализация OnResetClick будет очень простой: вначале мы сбрасываем значение переменных, вызвав функцию Init (которую введена, чтобы сделать единую точку инициализации переменных), а далее вызываем TransferDataToWindow, чтобы отобразить эти данные.

BEGIN_EVENT_TABLE( PersonalRecordDialog, wxDialog )
    ...
    EVT_BUTTON( ID_RESET, PersonalRecordDialog::OnResetClick)
    ...
END_EVENT_TABLE()

void PersonalRecordDialog::OnResetClick( wxCommandEvent& event )
{
    Init();
    TransferDataToWindow();
}

Обработка обновлений интерфейса

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

В нашем примере мы должны отключить чек-бокс “Vote” когда возраст пользователя меньше 18 лет, так как в этом случае пользователь не может принимать решение об этом. Вашей первой мыслью, наверное, будет добавить обработчик событий от спина Age, который будет включать и выключать элемент Vote в соответствии со своим значением в спине. Хотя данное решение может хорошо работать для простых пользовательских интерфейсов, но представьте что будет, если состояние элемента зависит от множества факторов. Существуют более плохие ситуации, когда вы не можете перехватывать изменения некоторого параметра. Например, вам необходимо включать пункт меню “Вставить” в зависимости от доступности данных в буфере обмена. Перехват этого события очень сложен, так как данные могут поступать и из другого окна.

Чтобы решить такую проблему в wxWidgets существует специальный класс событий wxUpdateUIEvent, который посылается все окнам, когда программа простаивает, что происходит когда петля событий закончила обработку всех остальных событий. Вы можете добавить обработчик EVT_UPDATE_UI в ваш диалог, по одному для каждого элемента управления, состояние которых вы хотите контролировать. Соответствующий обработчик вычисляет текущее состояние и вызывает функции объекта-события (а не для элемента управления) для включения, выключения, установки или снятия галочки. Эта технология позволяет перенести логику обновления состояния элемента в одно место, вызывая обновления даже в то время, когда в приложение не приходят реальные события. И это хорошо, так как не может возникнуть ситуация, когда вы забудете обновить состояние элементов управления.

Далее приведен обработчик события об обновлении интерфейса для элемента “Vote”. Заметим, что мы не можем использовать переменную m_age, так как перенос данных из элементов управления в переменные происходит только после того, как пользователь нажмет кнопку “OK”.

BEGIN_EVENT_TABLE( PersonalRecordDialog, wxDialog )

EVT_UPDATE_UI( ID_VOTE, PersonalRecordDialog::OnVoteUpdate )

END_EVENT_TABLE()

void PersonalRecordDialog::OnVoteUpdate( wxUpdateUIEvent& event )
{
wxSpinCtrl* ageCtrl = (wxSpinCtrl*) FindWindow(ID_AGE);
if (ageCtrl->GetValue() < 18) { event.Enable(false); event.Check(false); } else event.Enable(true); } [/sourcecode] Не волнуйтесь слишком сильно об эффективности такого решения, так как оно занимает не очень много циклов работы процессора. Однако, если у вас достаточно сложное приложение и вы столкнулись с проблемами производительности по вине обновления интерфейса, то посмотрите документацию по классу wxUpdateUIEvent о функциях SetMode и SetUpdateInterval, которые можно использовать, чтобы уменьшить время, которое wxWidgets тратит на обработку таких событий.

Читать вторую часть главы “Написание собственных диалогов”.

2 comments

  1. Pingback: Перевод книги Julian’а Smart’а - Глава IX - Написание собственных диалогов - Часть II | Cross-Platform Programming with wxWidgets

  2. Эх, еще летом начал переводить главу про sizers, но так и не продолжил. Надо будет исправляться…

Leave a Reply

Your email address will not be published. Required fields are marked *

Please leave these two fields as-is: