MSDN Magazine > Home > Issues > 2008 > Июнь >  Библиотека GUI. Привнесение простоты Windows Fo...
Библиотека GUI
Привнесение простоты Windows Forms в приложения в машинных кодах
Джон Торйо (John Torjo)

В этой статье рассматриваются следующие вопросы.
  • Трудности программирования GUI
  • Создание объектов-окон
  • Обработка событий и уведомлений
  • Формы и элементы управления
В данной статье использованы следующие технологии:
Win32 API, C++
Трудность программирования GUI в C++ заключается в том, что большинство библиотек является библиотеками слишком низкого уровня, и на программиста ложится слишком большая нагрузка. Эти библиотеки опираются на структуры типа C, или их классы оболочек скрывают сложность в недостаточной степени. Кроме этого, они не предлагают достаточно простых средств программирования событий, вынуждая программиста изучать базовые сообщения WM_.
В этой статье представлена eGUI++, написанная мною библиотека C++, которая обеспечивает программисту клиентов высокоуровневый интерфейс для работы с приложениями GUI. Она скрывает сложность, делая крайне простым программирование событий за счет того, что полностью избавляет от необходимости иметь сведения о сообщениях WM_. Исчезает необходимость иметь дело с какими-либо необработанными структурами типа C; программист всегда работает с классами. В итоге код клиента eGUI++ прост как при чтении, так и при написании.
eGUI++ предназначена только для Windows®. На самом деле я не верю в приложения GUI, рассчитанные на несколько платформ, за исключением случаев тривиального приложения, простой инфраструктуры для тестирования, прототипа или просто учебного приложения. Что более важно, я твердо убежден в том, что следует использовать все преимущества, предлагаемые базовой операционной системой. А в случае Windows XP и Windows Vista® их довольно много.

Встроенный и переносимый
Для тех, кто ищет код CLR, уже существует управляемый C++. Это хорошая платформа, и вносить усовершенствования в этой части нет необходимости. Те же, кто стремится иметь хорошую библиотеку для генерации машинного кода Windows для Windows 2000 и более новых операционных систем, стоит продолжить чтение статьи. Результат вам понравится; библиотека проста в использовании, поскольку использует преимущества операционной системы, на которую вы ориентированы. Вам совершенно не понадобится Microsoft® .NET Framework. Код, который будет написан, воспринимается как C++. Кроме того, для компилирования этого кода не требуется обязательно компилятор Visual C++. При желании его можно компилировать с помощью g++ (GNU C++ Compiler) 4.1. В сущности, если вы заключаете в оболочку Win32® API, нет никаких препятствий для написания переносимого кода.
Однако для нетривиального GUI вам понадобится хорошая среда IDE, например версия Visual Studio® 2005 или Visual Studio 2008 Express. Моя библиотека настроена для интегрирования в Visual Studio 2005 Express и более новые версии и для обеспечения еще более удобной работы с GUI. Я концентрировался на завершении кода, чтобы гарантировать, что IDE будет максимально полезен при создании нового класса GUI или расширении существующего.
Я стремлюсь получать удовольствие от написания приложений GUI. Поэтому моей целью при создании eGUI++ было облегчение чтения и написания кода GUI. Например, функции завершения кода были реализованы всюду, где это было возможно. Теперь программирование GUI является безопасным (в случае возникновения ошибки она обрабатывается на этапе компиляции, если только это возможно; в противном случае создается исключение на этапе выполнения). eGUI++ хорошо совместима с редактором ресурсов (она интегрируется с Visual Studio 2005 и более новыми версиями редактора ресурсов).

Никаких windows.h
Основной сложностью при включении windows.h являются слишком большие возможности для возникновения ошибок. Что должно остановить вас от отслеживания события, которое никогда не произойдет? Представим класс button, ожидающий, например, события, состоящего в нажатии клавиши на клавиатуре; предположим, что это событие не предвидится.
Почему, несмотря на это, вам необходимо иметь windows.h? Необходимо иметь возможность писать приложения Windows на C++ с использованием обычных классов C++. Соответственно, у вас не должно быть необходимости знать внутреннее устройство windows.h: никаких WM_LBUTTONDBLCLK, WM_LBUTTONUP и других длинных имен событий. Никаких LPNMITEMACTIVATE, NMHDR или каких-либо других неясных структур C. И никаких преобразований в стиле C. Основная функция хорошей библиотеки C++ GUI должна состоять в абстрагировании от Win32 API и предоставлении возможности иметь дело с классами.
Вы уже достаточно поработали с необработанными структурами C, подобными тем, что показаны на рис. 1? Лично мне это надоело. Поэтому теперь я пишу код следующего вида.
wnd<rebar> w = new_(parent);
rebar::item i(rebar::item::color | rebar::item::text);
w->add(i); 
// The old way 
hwndRB = CreateWindowEx(WS_EX_TOOLWINDOW,
  REBARCLASSNAME, NULL,
  WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|
  WS_CLIPCHILDREN|RBS_VARHEIGHT|
  CCS_NODIVIDER,
  0,0,0,0, hwndOwner, NULL, g_hinst, NULL);
...
rbi.cbSize = sizeof(REBARINFO);  
rbBand.cbSize = sizeof(REBARBANDINFO);
rbBand.fMask  = RBBIM_COLORS | RBBIM_TEXT |
  RBBIM_BACKGROUND;
rbBand.fStyle = RBBS_CHILDEDGE;
Как вы видите, eGUI++ скрывает сложность: вы имеете дело только с классами C++, вам не требуется помнить замысловатые функции API (подобные CreateWindowEx), имена констант (константы WS_*) или сложные структуры C, подобные REBARPARAMINFO.
eGUI++ предназначена для Windows 2000 и более новых версий. По умолчанию она настроена на Windows XP с пакетом обновления 2 (SP2). Однако ее можно настроить на другую операционную систему, так, чтобы оставалось доступным большее или меньшее число функций. Если требуется работать с другой операционной системой, просто задайте для EGUI_OS другую константу ОС перед включением каких-либо заголовков eGUI++ (см. рис. 2).
// code from eGUI++
struct os {
 typedef enum type {
 win_2k,
 win_2k_sp4,
 win_xp,
 win_xp_sp2,
 win_vista
 };
};

#ifndef EGUI_OS
#define EGUI_OS os::win_xp_sp2
#endif
Вы будете рады тому, что в коде очень мало #ifdefs, и поэтому его гораздо легче читать. Зато вы обратите внимание на то, что некоторые свойства зависят от ОС.
property<int,os::win_xp> some_prop;
Если вы имеете в виду раннюю версию ОС и пытаетесь использовать, например, упомянутое выше свойство, возникнет ошибка на этапе компиляции.

Работа с каждым окном
Обычно именно программист решает, как долго существует объект. Но серьезной проблемой при программировании GUI является то, что существует различие между отображаемым окном и объектом окна — и именно пользователь решает, когда закрыть окно. Значит, в вашем коде может присуствовать допустимый указатель или ссылка на объект окна, в то время как окно уже уничтожено пользователем. Это затрудняет обеспечение взаимнооднозначного соответствия, в котором окно на экране представляет экземпляр объекта и наоборот. Одно очевидное ограничение, вытекающее из такой ситуации, состоит в том, что нельзя иметь локальные экземпляры окон (которые уничтожались бы, когда существует область действия).
{
form f(...);
f.show();
...
}
Предположим, что форма f отображается на экране. Что должно происходить, когда вы покидаете область действия формы f? Есть два варианта действий: либо уничтожить форму (экранную), либо оставить форму на экране, в то время как соответствующий ей экземпляр C++ уничтожен. Ни один из вариантов не является разумным. В первом случае пользователь будет сбит с толку, пытаясь понять, куда подевалась форма. Во втором случае форма остается на экране, но она не будет откликаться ни на какие события, поскольку не существует соответствующего ей экземпляра C++. Кроме этого, пользователь, возможно, закрыл экранную форму, находясь в области действия f. Таким образом окажется, что вы работаете с объектом, который не существует на экране.
Решение: доступ к окну следует всегда получать посредством указателя (косвенного). Затем, когда пользователь закрывает экранное окно, соответствующий экземпляр C++ помечается как недействительный; при попытке получения к нему доступа создается исключение. Всегда можно выяснить, является окно действительным или нет, и уничтожить окно самостоятельно, например следующим образом.
wnd<> w = ...;
// is window valid?
if ( is_valid(w) ) w->do_something();
// is window valid?
if ( w) w->do_something();

// destroy the window
delete_(w);
Поскольку каждому окну на экране соответствует экземпляр C++, вы работаете с окнами с помощью класса шаблонов wnd<>, который представляет общий (ref counted) указатель на окно. Класс wnd<> имеет один необязательный аргумент: тип окна. По существу, это то, чего вы ожидаете. По умолчанию это window_base. Или это может быть класс окна такой как text, label, rebar, edit и т.д. На рис. 3 показано несколько примеров использования объектов окна и их взаимопреобразований.
// when constructing a window, you can specify its type
wnd<> w = new_<form>(parent);
w->bg_color( rgb(0,0,0));

// when constructing a window, if you don't specify its type,
// it will guess it, based on who you assign it to
wnd<button> b = new_(w, rect(10,10,200,20) );
b->events.click += &my_func;

// destroying a window
delete_(b);

// casting - if it fails, it throws
wnd<form> f = wnd_cast(w);

// casting - if it fails, returns null
if ( wnd<edit> e = try_wnd_cast(e) )
  e->text = "not nullio";
Отметим, что окно создается с помощью функции new_, а удаляется функцией delete_ (и снова, как правило, именно пользователь закрывает окно). Кроме этого, если имеется окно типа X (по умолчанию window_base), и требуется выяснить, имеет ли оно также и тип Y, можно использовать преобразование. Преобразование всегда является явным. Существует два типа преобразований: wnd_cast, которое, если оно не проходит, создает исключение, и try_wnd_cast, которое в случае невыполнения возвращает окно null.
Иногда при разработке класса eGUI++, кроме получения производных от его базового класса, требуется наследовать некоторое внешнее поведение, например возможность изменять размер, оформление и т.д. В этом случае можно создать несколько повторно используемых классов поведения и дополнительно получать производные от этих классов.

Простой код
Вид кода GUI, который легко написать и прочитать, оказывает живительное действие. В книге я использовал все возможные приемы, чтобы добиться максимальной пользы от завершения кода. При работе с большой библиотекой GUI всегда есть то, что программист склонен забывать: имена свойств, имена событий, флаги и т.д. Я поработал с каждым из них. Для документации я использовал doxygen – он просто блистателен. Чем больше я им пользуюсь, тем больше он мне нравится.
Просмотр документации выполняется крайне просто. Для имени свойства достаточно набрать w->, и, как показано на рис. 4, будут отображены методы и свойства (свойства показаны как переменные-члены, поэтому их легко отличать). В случае имен событий легко вспомнить события, которые класс может обрабатывать; достаточно набрать class_name::ev::, и, после оператора области действия, вступает в игру завершение кода и отображает события. Где eGUI++ действительно блистает, так это при работе с флагами. Для каждого свойства, которое может быть сочетанием флагов, при выявлении доступных вариантов выбора флага достаточно добавить "." к свойству флага, и завершение кода снова придет на помощь. В качестве дополнительного подарка я добавил перегрузку операторов для свойств. Таким образом, следующий код является допустимым:
w->text = "hello";
w->text += " world";
w->style |= w->style.tiled;
Рис. 4 Завершение кода
И на тот случай, если вы забудете список элементов управления, которыми вы можете распоряжаться, добавлено пространство имен, названное по этому случаю egui::ctrl.

Элементы управления и формы
Если в прошлом вы занимались программированием Win32 GUI, то знакомы с диалоговыми окнами. Также вам известен тот факт, что API обрабатыавет создание диалогового окна (::CreateDialog) совсем не так, как создание окна (::CreateWindow[Ex]). Как программисту вам не потребуется помнить две сложные функции с очень разными форматами вызовов. Обе они являются типами окна. У eGUI++ имеется только один способ создания окна: функция new_.
С точки зрения разных типов окон имя «форма» является более выразительной, чем имя «диалог». Оно описывает окно, отображающее другие элементы управления, и эти элементы управления содержат данные. Я допускаю оба имени, но предпочитаю «форма». В действительности, в коде вы увидите следующее.
typedef form dialog;
Идеологически существуют только два типа окон: элементы управления и формы. Элемент управления представляет собой окно, в котором отображаются некоторые данные, которые пользователю может быть разрешено изменять. Каждый класс элементов управления является производным от класса «control». Форма является окном, в котором размещен один или несколько элементов управления и некоторая собственная логика (например, логика, позволяющая манипулировать некоторыми данными).
Доступный в каждом окне набор функций зависит от действий, предусмотренных для окна. Отметим, например, что форма позволяет перечислять свои дочерние элементы управления, в то время как элемент управления не предоставляет такой возможности; при таком подходе создаваемый код меньше подвержен ошибками. Кроме этого, крайне редко требуется создавать элементы управления; обычно они уже имеются в форме, поскольку вы помещаете их туда с помощью редактора ресурсов.
Сами формы бывают двух видов: модальные диалоговые окна и окна сообщений. Для создания модального диалогового окна достаточно добавить form::style::modal при создании формы. Для создания окна сообщения служит функция msg_box<>, указывающая кнопки в качестве аргументов шаблона.
if ( msg_box<mb::ok | mb::cancel>("q") == mb::ok)
    std::cout << "ok pressed";
Кроме этого, функция msg_box<> знает на этапе компиляции, работоспособно ли сочетание кнопок.
// ok
   msg_box<mb::yes | mb::no>("q");
   // compile-time error
   msg_box<mb::ok | mb::yes>("q");

Программирование форм
Повторю, что форма – это то, что называется диалоговым окном в Win32 API. В случае Windows Forms программирование форм оказалось успешной стратегией. В каждой форме имеются некоторые элементы управления, и каждая форма решает отдельную задачу. Вместо устаревшего и запутанного интерфейса SDI или интерфейса MDI можно использвать вкладки, на которых размещаются элементы управления или другие формы. Таким образом, вы будете избавлены от лицезрения каких бы то ни было CFrameWnd, CMDIChildWnd или чего-нибудь подобного; в них нет необходимости. Если в форме требуется разместить несколько форм, просто воспользуйтесь классом tab_form. Он позволяет добавлять дочерние формы, каждую на ее собственную вкладку.

Работа с формами
Так же сильно, как я ненавижу мастеров, я верю, что некоторые из них время от времени облегчают задачи программирования. В данном случае для создания формы я создал мастера новых классов. В окне просмотра классов выберите режим добавления класса и в пункте категорий выберите eGUI. Слева выберите Форма eGUI и выберите «Добавить». Укажите имя класса, и все готово (рис. 5). Мастер создает файл заголовка с именем <dlgname>.h, исходный файл с именем <dlgname>.cpp и дополнительный файл заголовка с именем <dlgname>_form_resource.h, который eGUI++ будет поддерживать внутренними средствами.
Рис. 5 Добавление класса (щелкните изображение для его увеличения)
В этом последнем файле заголовка содержатся все имена элементов управления, используемых в форме. Следовательно, вам не потребуется создавать дополнительные управлющие переменные и использовать обмен данными, как в MFC. Элементы управления будут использоваться напрямую. Предположим, что имеется диалоговое окно входа, содержащее два поля ввода (для имени пользователя и пароля) и две кнопки («OK» и «Отмена»), как показано на рис. 6.
Рис. 6 Поля ввода и кнопки
Будут сгенерированы следующие файлы:
// login.h
#pragma once
#include "login_form_resource.h"
struct login : form, 
 private form_resource::login {};

// login.cpp
#include "stdafx.h"
#include "login.h"
Обратите внимание, что код крайне прост; нет ни кода в стиле мастера, подобного «enum {IDD = ... }», ни сопоставлений сообщений. Вам не требуется обеспечивать пользовательский конструктор, если он вам не нужен; имеющийся по умолчанию будет прекрасно работать.
Класс login частным образом производится от класса form_resource::login, реализованного в файле login_form_resource.h (этот файл поддерживается библиотекой eGUI++); класс form_resource::login содержит сведения об элементах управления формы (их имена и тип, а также способность перехватывать уведомления от элементов управления); по своему усмотрению можно изменить тип доступности для получения производных, хотя я бы не советовал это делать. Точно так же, как данные члена класса обычно являются частными, то же самое верно для формы — ее элементы управления должны быть, как правило, частными.
Теперь сгенерированная форма form_resource::login выглядит примерно следующим образом.
// login_form_resource.h
#pragma once
struct form_resource::login {
 // ... (code to allow 
 // handling of notifications)
 wnd<edit> username;
 wnd<edit> passw;
 wnd<button> ok, cancel;
};
Это поможет легко манипулировать элементами управления формы. Предположим, что требуется гарантировать, что пароль имеет значение «secretword».
void login::on_button_click(ev::button_click &, ok_) {
 if ( passw->text == "secretword")
 { pass_ok = true; visible = false; }
}
Как видите, точно так же, как в Visual Basic®, для скрытия формы достаточно просто задать значение false для ее свойства visible.

Избавление от старых индентификаторов
Возможно, вы уже имели дело с редактором ресурсов. И вы сталкивались с разными типами префиксов ресурсов, например ID_, IDD_, IDC_, IDR_, IDS_ и т.д. Для редактора ресурсов префиксы удобны. Однако в коде они являются дополнительной информацией, которую вам не требуется запоминать и о которой не требуется заботиться. В приложении eGUI++ вам никогда не понадобится помнить эти префиксы, поскольку они полностью игнорируются.
Например, предыдущие имена (username, passw, ok, cancel) являются сокращениями из редактора ресурсов. Библиотека eGUI++ автоматически отсекает их префиксы ID*. Исходные имена могли быть следующими: IDC_username, IDC_passw, IDOK и IDCANCEL.

События и уведомления
Как уже упоминалось, нет необходимости помнить ни одного сообщения WM_. То есть, события сопротивляются приручению. Вокруг есть довольно много событий, и вам необходим простой способ выяснения, на какие события можно откликаться, и уметь откликаться на них без труда. Необходимы простые способы получать уведомления о событиях, происходящих в элементах управления вашей формы, возможность расширять элементы управления и добавлять свои собственные события.
Каждый класс окон (элемент управления или форма) может генерировать события. Для каждого из них существует обработчик событий, перехватывающий все события. Для каждого события определяется функция, обрабатывающая это событие; эта функция виртуальная и ее реализация ничего не делает. Каждая функция обработчика событий имеет один аргумент: данные события.
Для существующих элементов управления соответствующий класс событий называется handle_events::control_name. Каждый существующий класс окон eGUI++ window wnd_name уже является производным от handle_events::wnd_name. Если вы расширяете существующий класс окон, у вас всегда имеется возможность обрабатывать его события. (Для простоты все функции обработчика событий начинаются с «on_».) Например:
struct my_btn : button {
 void on_char(ev::char& e) {
 cout << "typed " << e.ch;
 }
};
Те, кто имел дело с другими библиотеками GUI, знают, что в жизни все не так просто, как может показаться на основе того, что вы только что увидели. Существующие элементы управления отправляют не события, а уведомления. Уведомления существуют в виде сообщений WM_COMMAND/WM_NOTIFY, и они отправляются родителю элемента управления, а не самому элементу управления. На первый взгляд это может показаться разумным: именно родителю элемента управления (форма) требуется уведомление. Однако это крайне затрудняет расширение классов элементов управления. Что если вам требуется создать дерево для визуализации текущей файловой системы? Вам потребовалось бы перехватывать события, например разворачивание элемента (TVN_ITEMEXPANDING), которые отправляются родителю элемента управления. Затем вам потребовался бы способ передачи уведомления ниже, до самого элемента управления.
В случае GUI++ уведомления являются событиями Таким образом, они всегда отправляются элементу управления, а затем родителю элемента управления. Когда класс элементов управления расширяется посредством наследования, все уведомления преобразуются в разные события. Например, если бы требовалось создать элемент управления списком, отображающий, когда пользователь редактирует первый столбец, поле со списком вместо поля ввода, код выглядел бы следующим образом.
struct list_with_combo : list {
 ...
 void on_begin_label_edit(
  ev::begin_label_edit & e) {
 e.allow_default = false;
 combo->rect(...);
 combo->visible = true;
 }
 wnd<combo_box> combo;
};
Для обработки события функция обработчика событий перегружается следующим образом.
struct my_btn : button {
 void on_char(ev::char& e);
};
В данном случае обеспечивается отклик на событие нажатия символа или, для любителей Win32 API, на сообщение WM_CHAR.
Не забывайте, что для функции on_my_event обработчика событий аргумент события всегда имеет тип ev::my_event. Все события, которые могут обрабатывать ваш класс, входят в структуру ev::. Простой набор ev:: приводит к тому, что код завершения отображает все события, которые ваш класс может обрабатывать (см. рис. 7). Отмечу, что самый простой способ получить сведения о событии состоит в том, чтобы набрать e. и позволить завершению кода отобразить все данные, относящиеся к событию (см. рис. 8).
Рис. 7 Завершение кода отображает события
Рис. 8 Получение информации о событии
Можно просматривать документы, относящиеся к событиям элемента управления: достаточно выбрать элемент управления, затем его класс ev, и будут отображены все его события. Библиотека может отправить одно и то же событие нескольким обработчикам событий (например, уведомления отправляются элементу управления, затем родителю элемента управления).
Все события обладают свойством .sender; это элемент управления, отправляющий событие (он удобен для уведомлений, в особенности для того, чтобы знать, кто отправил уведомление). Все события обладают свойством .handled; оно имеет два значения: handled_partially (по умолчанию) и handled_fully. Задавая для этого свойства значение handled_fully, можно остановить обработку события; даже если еще имеются обработчики событий, они не будут вызываться. Например, если бы выполнялось расширение класса edit и требовалось предотвратить получение родителем уведомления об изменении текста, нужно было бы написать следующий код.
struct independent_edit : edit {
                      void on_change(ev::change &e) {
                      e.handled = handled_fully;
                      }
                     };
Как было показано выше, расширение элементов управления выполняется просто. Однако обработка уведомлений в формах тоже должна быть несложной. При обработке уведомления требуется знать, кто его отправил (e.sender). Более того, требуется иметь возможность выполнять действия в соответствии с уведомлением из конкретного элемента управления. Следовательно, функция обработчика событий получает дополнительный аргумент: имя элемента управления, завершаемое символом подчеркивания (_). Например, для выяснения содержимого, вводимого пользователем в поле ввода имени пользователя, запускается на выполнение следующий код.
void login::on_change(
 edit::ev::change &e, username_) {
 cout << "name=" << e.sender->text;
}
Предположим, к примеру, что требуется выполнять преобразовaние денежных единиц между долларами США и евро. При вводе некоторого значения в поле EUR происходит обновление поля USD. Если вводится некоторое значение в поле USD, обновляется поле EUR, как видно из рис. 9. Далее идет код, реализущий это.
struct convert : form, form_resource::convert {
 double rate; 
 convert() : rate(1.5) {}
 int mul_str(const string& a, double b) { ... }
 void on_change(edit::ev::change&, eur_) {
 usd->text = mul_str ( eur->text, rate); }
 void on_change(edit::ev::change&, usd_) {
 eur->text = mul_str ( usd->text, 1/rate); }
};
Рис. 9 Преобразователь денежных единиц
Код понятен без объяснений; mul_str выполняет умножение double на string посредством преобразования string в double и умножения его на коэффициент конвертации.
Для обработки событий так, как это было сделано выше, нужно было проделать значительную работу. Предположим, что имеется форма с тремя полями ввода. Каждое поле ввода будет генерировать определенный набор событий. Для каждого такого события (например, on_change), можно было бы ли генерировать одну допускающую переопределение функцию для каждого элемента управления
void on_change(edit::ev::change& e, ctrlname_);
, либо генерировать одну переопределяемую функцию:
void on_change(edit::ev::change& e);
Я предпочитаю первое решение — код клиента гораздо проще (и гораздо больше похож на подход Visual Basic). Легко видеть, что именно обрабатывается (в противоположность второму решению, где в реализации события пришлось бы посредством e.sender вручную запрашивать, какой элемент управления сгенерировал событие).
Поэтому я реализовал первое решение. Однако в нем скрыт большой объем проделанной работы. eGUI++ отслеживает работу редактора ресурсов. Когда добавляется новый элемент управления, или элемент управления переименовывается, выполняется обновление всех файлов <dlgname>_form_resource.h. Отмечу, что для каждого файла <dlgname>_form_resource.h в классе form_resource::<dlgname> необходимо переопределить все уведомления из существующих элементов управления и, для каждого такого переопределенного уведомления, найти элементы управления, которые могут его отправить. На следующем шаге необходимо сгенерировать реализацию, которая будет перенаправлять к еще одной переопределяемой функции для каждого элемента управления. В качестве примера на рис. 10 показан код для формы входа с двумя полями ввода и двумя кнопками.
struct form_resource::login {
  wnd<edit> name;
  wnd<edit> passw;
  wnd<button> ok, cancel;

  typedef ... ok_;
  typedef ... cancel_;
  typedef ... name_;
  typedef ... passw_;

  virtual void on_change(edit::ev::change& e, name__) {}
  virtual void on_change(edit::ev::change& e, passw__) {}

  virtual void on_change(edit::ev::change& e) {
    if ( e.sender == name) on_change(e, name__());
    else if ( e.sender == passw) on_change(e, passw__());
  }
  // ... same for other edit notifications

  virtual void on_click(button::ev::click & e, ok__) {}
  virtual void on_click(button::ev::click & e, cancel__) {}
  virtual void on_click(button::ev::click & e) {
    if ( e.sender == ok) on_click(e, ok__());
    else if ( e.sender == cancel) on_click(e, cancel__() );
  }
  // ... same for other button notifications
};
Наконец, можно создать свои собственные события, производные от new_event<>. Отправляете ли вы существующие события или свои собственные, процедура одна и та же: используется функция send_event.
struct hover : new_event<hover> {
 int x,y; // position
 hover(int x,int y) : x(x),y(y) {}
};

w->send_event( hover(x,y) );
Библиотека безопасна с точки зрения потоков. Дополнительно отмечу, что каждое окно имеет переменную мьютекс m_cs (по существу, это CRITICAL_SECTION), которую я использую при каждом доступе к методу для обеспечения безопасности с точки зрения потоков. При расширении класса окон можно повторно использовать переменную m_cs или создавать свою собственную — это ваше дело.

Меню, сочетания клавиш и прочее
Если в прошлом вы занимались программированием GUI, вы знаете, что и нажатие команды меню, и нажатие клавиши приводит к отправке WM_COMMAND. Вследствие этого, когда вы получаете WM_COMMAND, трудно понять, пришло это событие от элемента управления или от меню (или вообще от сочетания клавиш). Первую часть этой проблемы eGUI++ решает, помещая меню непосредственно в форму (диалоговое окно). Если команда, отправленная форме, инициирована не кнопкой, значит, она пришла из меню.
Таким образом выполняется присмотр за командами меню. Обратимся теперь к сочетаниям клавиш. Проблема с сочетаниями клавиш заключается в том, что сочетание клавиш может быть нажато в любой момент времени (например, при нахождении в поле ввода). Сочетания клавиш (ускорители) направляются сначала в текущее окно, затем в форму, в которой размещено это окно, затем родителю формы и выше по иерархии, до достижения окна самого высокого уровня. При первой же встрече с обработчиком событий для этого сочетания клавиш обработка прекращается (заданное сочетание клавиш не обрабатывается двумя или несколькими окнами).
Остаются еще панели инструментов — они работают на равных с меню и сочетаниями клавиш. При нажатии кнопки на панели инструментов событие преобразуется в команду меню и направляется непосредственно в форму, в которой оно размещено — независимо от того, пришла команда из меню, от сочетания клавиш или кнопки панели инструментов.
Предположим, что реализуется форма для обработки команды меню, например следующая.
void on_menu_command( ev::menu&, 
 menu::some_menu_id) { ... }
Для обработки двух команд меню, new_file и open_file, создаются следующие обработчики.
void on_menu_command( ev::menu&,
    menu::new_file) { ... }
   void on_menu_command( ev::menu&,
    menu::open_file) { ... }

Элементы управления вкладки и формы
Вкладки являются очень популярной парадигмой GUI. Я расширил элемент управления вкладки таким образом, чтобы свойство tab_type имело значения normal или one_dialog_per_tab (в этом случае в элементе управления размещаются другие формы). В последнем случае можно добавлять новые формы следующим образом.
tab->add_form<form_type>( new_([args]) );
Для добавления формы входа, о которой шла речь, следовало бы написать следующий код.
tab->add_form<login>( new_() );
После того, как добавлена хотя бы одна форма, можно указать число вкладок, которое должна содержать данная форма вкладки.
tab->count = 5;
В данном случае имеется пять вкладок. Это достигается дублированием последней добавленной формы требуемое число раз. В предположении, что ранее имелась только одна вкладка, форма на первой вкладке будет клонирована еще четыре раза. Именно так. Вы может клонировать любое существующее окно!
До сих пор была продемонстрирована интрузивная обработка событий. Другими словами, вы расширяете класс окон и в конечном счете откликаетесь на его события (или, если вы реализуете форму, то отвечаете на ее уведомления). Иногда, однако, требуется реализовать поведение, которое применяется к нескольким окнам (вобщем-то, почти не связанным друг с другом).
Возьмем, например, возможность изменять размер и оформление. Их можно реализовать интрузивным способом (создавая некоторые классы, реализующие требуемое поведение, а затем получая из них требуемые классы GUI). Однако это усложняет код и не всегда выполнимо (возьмите, например, возможность изменять оформление). При реализации поведения не интрузивным способом его можно использовать повторно в других приложениях, а также без труда отключать.
Например, создайте неинтрузивный класс обработчика событий, создайте его экземпляр и зарегистрируйте его. При создании нового окна уведомляется экземпляр обработчика и вы можете принять решение отслеживать этот экземпляр. Для этого необходимо вручную указать, какие события требуется отслеживать, например следующим образом.
// monitor button clicks
struct btn_handler : non_intrusive_handler {
 void on_new_window_create(wnd<> w) {
 if ( wnd<button> b = try_cast(w)) {
  b->events.on_click += mem_fn(&on_click,this);
 }
 }
 void on_click(button::ev::click&) { ... }
};
Регистрация обработчика выполняется просто. Достаточно выполнить следующие действия.
btn_handler bh;
window_base::add_non_intrusive_handler(bh);

Возможность изменять размеры можно реализовать множеством способов в зависимости от приложения. Например, можно было бы переопределить событие on_size в каждой форме и обновлять положения элементов управления, исходя из нового размера формы (плохая идея, очень трудоемко). Или, в каждой форме, можно было бы создать связи между элементами управления, подобные «a.x = b.x + b.width + 4;» (это очень гибкий способ, но также трудоемкий).
В качестве альтернативы можно в каждой форме пометить элементы управления как масштабируемые или допускающие перемещение по всем осям. Если элемент управления помечен как масштабируемый по определенной оси, то при изменении размера формы соответствующий размер элемента управления будет обновлен; если элемент управления помечен как перемещаемый по определенной оси, тогда при изменении размера формы этот элемент управления будет перемещен. Для большинства приложений этого должно быть достаточно. Я позаимствовал эту идею из функции CResizeWindow библиотеки WTL и реализовал ее с помощью неинтрузивного обработчика. Допустим, что имеется диалоговое окно, подобное окну из рис. 11. Если требуется, чтобы при изменении размера оно выглядело, как на рис. 12, то следует использовать следующий код.
resize(name, axis::x, sizeable);
resize(desc, axis::x | axis::y, sizeable);
resize(ok, axis::x | axis::y, moveable);
resize(cancel, axis::x | axis::y, moveable);
Рис. 11. Диалоговое окно
Рис. 12. Диалоговое окно с измененным размером
Каждая операция GUI, закончившаяся сбоем, вызывает исключение. Таким образом вы узнаете о том, что есть какие-то неполадки. В режиме отладчика это приводит к генерации ошибочного утверждения и программа прерывается с выходом в режим отладчика. Это гораздо лучше, чем безмолвное игнорирование ошибки, визуальное обнаружение неполадки и последующий поиск ошибки.

Интеграция с Visual Studio 2005
Visual Studio является прекрасной интерактивной средой разрботки (IDE), и одним из ее главных преимуществ является то, что ее можно расширять. eGUI++ использует это преимущество; библиотека поступает с надстройкой, предоставляющей мастера нового класса форм (New Form Class Wizard). Она предоставляет также панель типа Visual Basic, позволяющую обрабатывать уведомления элементов управления в формах. Для этого достаточно выбрать элемент управления, после чего отображается список уведомлений, которые может генерировать данный элемент управления. Обратите внимание, что те, которые уже обработаны, отображаются полужирным шрифтом; выбор события приводит к добавлению обработчика, если его еще нет в списке — например, см. рис. 13.
Рис. 13 Обработка уведомлений элементов управления в формах(щелкните изображение для его увеличения)
Скрытым образом eGUI++ отслеживает работу редактора ресурсов, чтобы при каждом внесении изменений файлы _form_resource.h обновлялись при необходимости. Это работает наряду с завершением кода, как уже было подробно описано.

Реализация поведения
После создания собственного GUI, на следующем этапе следует реализовать поведение и разрешить привязку данных. Многие формы предназначены только для сбора данных. Для начала можно реализовать общий класс форм, который, при конструировании, получает данные для управления и привязывет их к элементам управления формы. Затем можно указать набор правил для проверки данных. При уничтожении, если правила проверки успешно выполнены, исходные данные обновляются значениями из элементов управления; в противном случае исходные данные остаются неизмененными. Таким образом, для каждой новой формы требуется только создать форму в редакторе ресурсов и затем указать набор правил, которые должны использоваться при проверке данных (в противополжность созданию нового класса форм и дублированию логики).
Заглядывая вперед, можно перекинуть мостик между массивами STL (Standard Template Library) и коллекциями, и элементами управления списком и элементами управления деревом. Я имею в виду массив, скажем, сотрудников и элемент управления списком. Массив можно привязать к элементу управления следующим способом.
list_ctrl->bind(employees);
Как можно ожидать, при этом обновится элемент управления списком. Более того, любое изменение в ячейке элемента управления списком будет автоматически синхронизировано с массивом сотрудников.
Конструируя eGUI++, я стремился создать современную библиотеку, с которой программирование GUI становится увлекательным. Если вы программируете на C++, я очень надеюсь, что вы со мной согласитесь. Исходный текст и двоичные файлы можно загрузить по адресу torjo.com.

Джон Торйо (John Torjo) программирует на C++ и, после 10 с лишним лет программирования, все еще в него влюблен. Джон обожает ведение журналов и GUI, а также сложные задачи. Если у вас есть такая задача, отправьте ему письмо по электронной почте. За более подробными сведениями обращайтесь на веб-узел по адресу torjo.com.

Page view tracker