Операционная система QNX (в реализациях RTP 6.XX) содержит в своём составе инструментарий визуального проектирования
приложений Photon Application Builder (PhAB). При всей общей приемлемости для высокопроизводительной разработки
приложений для OS QNX и её
графической оболочки Photon,
технология использования PhAB
(по крайней мере, так, как она описана в технической документации QNX) вызывает несколько вопросов:
Фирма разработчик QSSL ориентирована, в первую очередь, на
использование классического языка C. Это обстоятельство часто объясняют (при чём объясняет не сама
QSSL, а сторонние обозреватели)
ориентацией системы на построение встроенных (embedded) систем, и, как следствие - на
достижение максимальной производительности результирующих приложений.
С этим утверждением нельзя согласиться, и можно отнести его
только к бытующим “народным заблуждениям”. Приложения, созданные на C++ практически ни в чём не уступают
по производительности приложениям C. (Редким исключением является вызов виртуальных функций членов
классов, но и здесь одна избыточная косвенность вызова может не приниматься в
расчёт ввиду своей малости). По всем нюансам сравнительных производительностей
этих языков, я отсылаю всех заинтересовавшихся непосредственно к анализу,
произведённому автором C++
Бьярном Страуструпом, например, в двух работах, посвящённых не обучению
C++, а разъяснению того, что,
почему и как сделано [1,2].
Более того, GNU компилятор GCC,
используемый в QNX с 2000 года,
с одинаковой лёгкостью оперирует как с C, так и с C++.
Приверженность QSSL
классическому C (во всех их
примерах использования и фрагментах кода) можно отнести только к историческим
традициям фирмы с 20-ти летней историей.
И система QNX, и её графическая
подсистема Photon, и GUI приложения, создаваемые с помощью PhAB, в высшей
степени модульные, детерминированы и объектны. “Объектны” не в смысле
используемого инструмента, а в смысле организации и функционирования. Поэтому,
вдвойне огорчительно отсутствие в PhAB средств построения некоторых
“крупноблочных” компонент, инкапсулирующих в себе GUI отображения и программный
код, служащих повторно используемым строительным материалом для приложений.
Итак, задача: создание графических интерфейсных
GUI компонент, используя IDE
PhAB в операционной системе QNX RTP 6.x., с ориентацией на C++. Такие компоненты должны позволять
описывать достаточно объёмные графические объекты (система связных окон,
мнемосхема управляемого устройства и т.д.) с достаточно сложным поведением, и
обеспечивать:
- Создание динамических наборов (массивы, списки)
единообразных пользовательских компонент в приложении;
- Встраиваемость (переносимость) в новые приложения
компонент, подготовленных в ходе более ранних работ;
- Повторную используемость программного кода.
Решение частичное (предварительное). Это
решение было использовано в реализации нескольких реальных проектов на ранних
этапах. Оно описывается здесь потому, что оно:
Представляет самоценность в решении относительно простых, но чрезвычайно
часто возникающих в практике реального программирования задач;
Послужило отправной точкой для отработки более продвинутого решения,
описываемого ниже, описание которого становится существенно проще после
рассмотрения предварительного варианта.
Для нескольких приложений требовалось создать группы достаточно
сложных в поведении единообразных GUI-объектов (мнемосхемы управляемых
приборов), число групп определялось либо статически (из некоторой
конфигурационной записи при старте приложения), либо динамически (изменяясь в
ходе работы приложения).
Создадим объекты как класс C++, содержащий в своём составе
ссылку на главное окно (фрейм) GUI-объекта, содержащее графическое отображение
составляющих компонент. Для простоты изложения будем считать, что компонент
представляет собой окно (widget класса PtWindow с именем component, на который в
PhAB создана внутренняя ссылка), содержащее кнопку (класс PtButton с именем
button). Class Object { protected: PtWidget_t* pFrame; public: Object( void ) { pFrame = ApCreateModule( ABM_component, NULL, NULL ); // . . . здесь делаем ещё что-либо . . . }; ~Object( void ) { DestroyWidget( pFrame ); // . . . . . . . . . . . }; };
Отметим некоторые принципиальные особенности
класса:
Объект содержит ссылку на фрейм графического образа, отображающего объект
на экране;
Конструктор класса содержит динамическое создание (прорисовку)
графического образа на экране;
Деструктор, перед уничтожения Object, уничтожает образ его графической
компоненты на экране;
Всякое создание переменной класса Object (как локальной переменной
функции, или по оператору new) будет прорисовывать графический образ на
экране;
Всякое уничтожение переменной класса Object (например, завершение
порождающей функции и выход за её область видимости локальных объектов) будет
уничтожать изображение образа на экране.
Из таких объектов могут строиться композиции – массивы или
списки. Пример ниже описывает работу со списками (вообще, при некоторых
минимальных навыках использования, динамические списки всегда более гибки, чем
массивы, и я почти повсеместно использую такую структуризацию, хотя это – дело
вкуса). Структура такого списка показана на рис.1 :

рис.1
В исходном коде это может выглядеть, например, так: class Element : public Object { friend class ElementList; private: Element* pNext; public: Element( void ) : Object( void ) { pNext = NULL; };}; class ElementList { private: Element* pBegin; public: ElementList( void ) { pBegin = NULL; }; void Add( Element* pE ) { // обычно элементы добавляются в “хвост” списка, но нас // устраивает и более простой способ добавления в “голову” pE->pNext = pBegin; pBegin = pE; }; PtWidget_t* FindButton( PtWidget_t* pButton ) { PtWidget_t *pThisFrame = GetParrent( pButton, PtWindow ); Element *p = pBegin; while( p->pFrame != pThisFrame ) p = p->pNext; return p; }; };
В приложении должна создаваться
хотя бы одна переменная (список) типа ElementList: static ElementList *pMyList = new ElementList();
К моменту требуемой визуализации компонент на экране (например,
в количестве N экземпляров) мы должны сделать нечто такое: for( int i = 0; i < N; i++ ) pMyList->Add( new Element() );
В PhAB определяем callback на нажатие кнопки button как функцию
(code) OnButton, которая выглядит как ни будь так (скелет функции генерируется
PhAB): int OnButton( PtWidget_t *widget, ... ) { /* определяем вызвавший фрейм, в котором активизирована кнопка*/ PtWidget_t *pWin = pMyList->FindButton( widget ); /* выделив фрейм, требующий обслуживания, выполняем над ним некоторые действия*/ pWin->DoSomeThing(); /* естественно, этот метод DoSomeThing вы должны описать для своего пользовательского класса Object, и, так-же естественно, что только автор может знать, что там в его классе нужно делать . . . */ };
Всё. Естественно – это
только схема. И на первый взгляд может показаться усложнённой. Но - этот
фрагмент кода, прописанный один раз, может в неизменном виде перекочёвывать из
приложения в приложение. Для
callback-ов других элементов контейнера component должны быть написаны
соответствующие функции Find…, за образец которым может быть взята функция
FindButton.
Некоторым недостатком метода является необходимость поиска в
списке (перебор элементов) вызывающего компонента при поступлении любого
действия, требующего callback-реакции. Но он ослабляется тем, что графическая
система Photon удовлетворительно работает (по производительности) при числе
одноуровневых widget не более 20-30 (примерно эта цифра называется многими
пользователями), а поиск производится в оперативной памяти и чрезвычайно быстро
(это – операция не более 25-30 разыменований указателя).
Гораздо более существенным недостатком является то, что такая
конструкция позволяет удовлетворительно решить 1-ю из сформулированных в начале
3-х задач (он использован мною для этих целей в 3-х реальных завершённых
проектах), но ни на шаг не приближает нас к решению 2-х других. Решать их мы и
будем в оставшейся части этих заметок.
Более развитое и универсальное решение. Для
того, чтобы повысить степень повторной используемости кода, необходима более
плотная связь пары “объект кода” - “графический фрейм”, составляющей базовую
конструкцию рис.1. А именно, не только иметь указатель из программного кода на
связанный фрейм, но и ввести обратную ссылку из фрейма на объект реализующего
кода, как это показано на рис.2.

рис.2
Для этого должен использоваться какой-то из
ресурсов widget, реализующего контейнер component. Неплохим кандидатом может
стать P t_ARG_USER_DATA, но в структуре базового образующего класса для всех
widget (PtWidget) есть ещё один приемлемый ресурс, который и использован в
примерах: Pt_ARG_POINTER (Я,
откровенно говоря, не знаю предназначения встроенного ресурса Pt_ARG_POINTER, могу только предполагать, что
это – указатель “родителя” в динамической иерархии widget-ов в Photon. Обсуждаемое использование ресурса
приемлемо только постольку, поскольку наши компоненты создаются динамически и не
имеют “родителя” в иерархии. Так или иначе, но этот “трюк” не совсем корректен,
и, если требуется более гарантированное поведение объекта, предпочтительнее
является использование Pt_ARG_USER_DATA. Однако, это более громоздко, и, чтобы не перегружать
изложение, я буду везде использовать Pt_ARG_POINTER, тем более, что во всех известных мне случаях это -
работает).
Определяем 2 файла:
Файл “Object.h”:
class Object { private: PtWidget_t *pFrame; static PtWidget_t* getFrame( PtWidget_t* ); static Object* getCode( PtWidget_t* ); public: Object( void ); ~Object( void ); static int Object::OnButton( PtWidget_t *widget, ApInfo_t *apinfo, PtCallbackInfo_t *cbinfo ); void Object::OnDoSomeThing( void ); };
Файл “Object.cpp”: Object::Object( void ) { int flags = PtEnter( 0 ); pFrame = ApCreateModule( ABM_component, NULL, NULL ); PtSetResource( pFrame, Pt_ARG_POINTER, this, 0 ); PtLeave( flags ); }; Object::~Object( void ) { int flags = PtEnter( 0 ); PtDestroyWidget( pFrame ); PtLeave( flags ); }; PtWidget_t* Object::getFrame( PtWidget_t *widget ) { return PtGetParent( widget, PtWindow ); }; Object* Object::getCode( PtWidget_t* widget ) { unsigned long* pRes; PtGetResource( getFrame( widget ), Pt_ARG_POINTER, &pRes, 0 ); return (Object*)pRes; }; // заголовок для callback-а сгенерированный PhAB: int Object::OnButton( PtWidget_t *widget, ApInfo_t *apinfo, PtCallbackInfo_t *cbinfo ) { // определяем указатель программного объекта (this), // соответствующего фрейму вызвавшего компонента Object* pO = getCode( widget ); // вызов метода – это главный оператор, из-за которого всё и строилось! pO->OnDoSomeThing(); return Pt_CONTINUE; };
В PhAB определяем calback code на нажатие button как:
Object::OnButton@Object.cpp. Функция-член OnButton, естественно, должна быть
методом класса, а не метод экземпляра, как это очень удачно именуется в
терминологии Java, т.е. static методом!
Всё. Схема с рис.2 – построена. Дальше объекты типа Object
могут агрегироваться в списки или массивы (как показано в предыдущем примере),
или даже использоваться в более экзотическом для C++ контексте – без имени (или
указателя) доступа. Вот такой оператор (или несколько последовательно
операторов) без левой части присвоения:
for( int i = 0; i < N; i++ ) new Object();
Несколько “дикая” для С/ C++ конструкция? Дело в том, что
в этом случае мы сознательно теряем всяческие пути доступа из
программного кода к созданному объекту (индивидуальные объекты недоступны как по
имени, так и по указателям). Но к объекту сохраняется новый, дополнительный путь
по схеме: “внешнее событие” - “widget” - “фрейм компонента” - “программный
объект” - “метод реакции”. Такое решение (изоляция объектов) может оказаться
крайне полезным во избежание внесения лишних ошибок при работе со сложными
компонентами. (Не совсем корректно, но разрушение таких объектов мы оставляем
программному коду эпилога завершения приложения – при завершении приложения для
каждого из объектов будет выполнен определённый нами деструктор объекта,
инициированный неявным “delete
…”).
Весь реализующий реакцию на внешние события код, и код –
обеспечивающий сколь угодно сложного уровня “внутреннюю жизнь” GUI-компонента мы
локализовали в тексте одного класса, и, при необходимости – в отдельном файле
реализации класса. Т.е. этот комплекс GUI-компонент и реализующий его класс
могут: переноситься из приложения в приложение, создаваться во многих
экземплярах, наследоваться для порождения более сложных объектов, повторно
использоваться практически без изменений.
Маленькое
примечание: в конструкторе и деструкторе
примера операции не случайно заключены в “скобки” PtEnter( … ) … PtLeave(
… ).
Динамическое создание графического компонента (например, по new), если оно
происходит в отдельном потоке, может случиться на момент времени занятости
графической библиотеки (QSSL предупреждает, что вызовы Photon библиотеки –
нереентерабельны!). При отсутствии монопольного захвата графической библиотеки
(PtEnter … PtLeave) приложение может просто аварийно “немо” завершиться со
сложнейшей диагностикой причин происходящего. То же, в полной мере, относится и
первому из описанных примеров, но там я не хотел усложнять объяснения. Общее
правило (очень грубо) выглядит так: если мы вызываем конструктор из главного
потока приложения – не ставим
монопольный захват, в противном случае – ставим. Это – отдельный предмет,
достаточно подробно описанный в недрах HELP-подсистемы QNX (ф-ции PtEnter и
PtLeave).
Придаём компоненту некоторый
“шарм”. В первоначальном варианте, родившемся из
обсуждения предмета на сайте разработчиков QNX http://qnx.org.ru, построения компонента на этом и завершалось. Но
последующий опыт использования мною таких компонент показал: если GUI компонент и реализующий его поведение
C++ объект достаточно объёмны,
или, если число реакций (callback) компонента значительно, то даже такой “локализованный” код ещё
достаточно путанный. Опыт показал, что писать callback-реакцию удобнее как функцию объекта,
а точку вызова callback из
PhAB определять только как
статический интерфейс с тем же именем, который всего лишь определяет
вызвавший объект, и тут же вызывает для него реакцию (из-за различия в
параметрах, компилятор никогда не “спутает” вызов реакции, и её static интерфейса). Вот как это выглядит для
ранее описанного класса Object:
class Object { private: PtWidget_t *pFrame; static PtWidget_t* getFrame( PtWidget_t* ); static Object* getCode( PtWidget_t* ); void OnButton( void ); public: Object( void ); ~Object( void ); static int Object::OnButton( PtWidget_t *widget, ApInfo_t *apinfo, PtCallbackInfo_t *cbinfo ); };
Файл “Object.cpp”: void Object::OnButton( void ) { OnDoSomeThing(); };// заголовок для callback-а сгенерированный PhAB: int Object::OnButton( PtWidget_t *widget, ApInfo_t *apinfo, PtCallbackInfo_t *cbinfo ) { OnButton( getCode( widget ) ); return( Pt_CONTINUE ); };
И так для всего множества реакций, требуемых
GUI
компонентом.
Маленькое
примечание: для всех реакций их
static-обработчики будут иметь единый “шаблонный”
вид:
int OnXXX( PtWidget_t *widget, ... ) { OnXXX( getCode( widget ) ); }
Может показаться заманчивым, поместить все
объявленные в PhAB точки входа callback
обработчиков непосредственно в файл объявлений XXX.h, но, к сожалению, этот номер не
проходит. Дело в том, что тогда компилятор раскроет Ваши шаблоны (методом
“inline” подстановки) в точках
вызовов, и PhAB не сможет найти
точки передачи управления callback.
Замечания по сборке. Этот раздел обязан своим присутствием тому, что IDE PhAB,
преимущественно, ориентирован QSSL на сборку проектов, построенных на
классическом C. При генерации и сборке чуть более или менее сложных проектов C++
возникают определённые сложности. Затронутые в этой части вопросы гораздо полнее
рассматриваются в обсуждениях на форумах портала http://qnx.org.ru. Я
перечислю только их итоговые положения (которыми, по крайней мере, я успешно
пользуюсь):
1. При сборке нового C++ проекта
в PhAB в меню "Application->Application Startup Information" сразу забиваем (очищаем) флажок "Scan Source Files for Prototipes".
Это препятствует PhAB в генерации
файла прототипов proto.h (он его всё равно генерирует, но “пустой”, и мы ниже
этим воспользуемся). Для прототипов теперь будет использоваться файл
abimport.h.
2. В файле abevent.h (функции, реализующие
реакции на callback) появятся строки (после очередной перегенерации проекта)
вида:
{8,0,0L,0L,0L,NULL,NULL,"button",2009,Object::OnButton ...
};
и вы снова
получите ошибки сборки, всего лишь потому, что объявления класса Object
(файл “Object.h”) должны стать доступны компилятору раньше, чем в файле
abmain.cc появится строка #include "abevent.h".
3. Я обхожу эту проблему так: в “пустой” и не нужный теперь
файл proto.h
вписываю нечто типа “#include "Object.h"” для всех заголовков, содержащих объекты-компоненты. Это
работает, потому, что генератор проекта всё равно вставляет ссылку на
proto.h в abmain.cc. Неприятен
этот способ тем, что после всякой перегенерации проекта (в ходе работы над
стабилизировавшимся проектом это требуется крайне редко) - proto.h очищается.
Возможно, кто-то предложит лучшее решение.
4. Ещё один
полезный приём при сборке: все реализующие C++ файлы компонент (поскольку они повторно
используемые, и могут включаться в различные проекты) удобно сконцентрировать в
отдельном пользовательском каталоге, а не каталоге проекта, генерируемом
PhAB. (Я его назову только для
определённости /OBJECTY, но корневой каталог – это не лучшее место для этой
цели, и Вы себе выберете более удачное месторасположение). В каталоге проекта я
делаю символьные ссылки:
link /OBJECTY/Object.h Object.h link /OBJECTY/Object.cpp Object.cpp
Постскриптум: Предложенные реализации инкапсулированных объектов могут быть существенно
расширены далее и доведены до универсального фрагмента кода использованием,
например, техники шаблонов (templates) C++. Такая реализация позволила бы
определить GUI-объекты, базой для которых мог бы быть не только класс PtWindow,
но и PtDialog, и другие. Подобные опыты могут быть весьма продуктивны, и
оставляются для самостоятельного экспериментирования
читателям.
Литература:
[1] – Маргарет Эллис, Бьярн Строуструп “Справочное
руководство по языку программирования C++ с комментариями. Проект стандарта
ANSI” - М.: “Мир”, 1992,
стр.446.
[2] -
Бьерн Страуструп “Дизайн и эволюция C++” - М.: ДМК Пресс, 2000, стр.448.
[3] – HELP
подсистема операционной системы QNX RTP
6.1.
Олег И. Цилюрик, фирма “LOT Ltd.”, г. Харьков,
Украина, mail: olej@lot.kharkov.ua.
Ответить на любые дополнительные вопросы, или обсудить предложения автор всегда
готов по электронной почте. |