Руководство разработчика приложений Zune
Предисловие
Что такое Zune?
Zune является объектно-ориентированным набором для разработки приложений с графическим интерфейсом пользователя (GUI). Это практически полный аналог MUI (Magic User Interface), как на уровне API, так и на уровне Look&Feel, самого популярного на Amiga shareware-интерфейса Стефана Штунтца. Таким образом, разработчики знающие MUI, почувствуют себя здесь "как дома", а остальные смогут изучить понятия и особенности, общие для обоих средств. Постулируется, что:
- Программист может затратить намного меньше времени при проектировании интерфейса: в Zune нет привязки элементов интерфейса к абсолютным значениям, среда чувствительна к кеглям шрифтов и сама адаптирует размеры и расположение любых окон в зависимости от шрифтов пользователя. Zune предоставляет семантический доступ к элементам проектируемого интерфейса, а его свойства (такие, как отступ элемента от края окна в пикселях) регулируются автоматически.
- Пользователю намного легче контролировать вид и поведение интерфейса, спроектированного программистом, он получает возможность специфической настройки параметров окружения Zune.
Zune основан на системе BOOPSI (Basic Object Oriented Programming System for Intuition), унаследованной от AmigaOS и используемой в объектно-ориентированном программировании на Си. Классы Zune не являются дочерними по отношению к существующим для элементов интерфейса классам BOOPSI (т.е., не являются простым расширением их возможностей). Напротив, базовым классом (в иерархии Zune) является класс Notify — дочерний относительно корневого класса BOOPSI.
Предпосылки
Для понимания концепции Zune, более чем приветствуется знание парадигмы объектно-ориентированного программирования (ООП). Вы можете воспользоваться Google для поиска и изучения образовательных материалов, посвящённых ООП.
Также, желательно, владение такими ключевыми понятиями AROS (и AmigaOS), как список тегов (taglist) и система BOOPSI. Хорошим подспорьем здесь, безусловно, является руководство "Amiga Reference Kernel Manuals" (известное как RKM).
Поскольку Zune является аналогом MUI, вся документация, имеющая отношение к MUI, применима и к Zune. В частности, последняя версия инструментария для разработчиков интерфейсов MUI доступна здесь. Среди прочих, в этом LHA-архиве есть 2 документа особенно рекомендуемых к прочтению:
- MUIdev.guide, документация MUI-программиста.
- PSI.c, исходный код приложения, демонстрирующего все современные методы
- проектирования и создания динамических объектов интерфейсов MUI.
Также этот архив содержит документацию (MUI autodocs), которая является описанием и для всех существующих классов Zune.
Реализация BOOPSI
Концепции
Класс
Класс (class), по сути, является лишь абстракцией, типом, описывающим объекты этого класса с общими структурой и поведением, и задаётся именем, родительским классом и диспетчером (dispatcher). Описание вводится путем указания типа и свойств класса:
- имя: в случае, если класс является общим (public) - это строка, характеризующая название класса и его область видимости, что делает его доступным любому приложению в системе. В случае, если класс является локальным (private) - отсутствует, и такой класс не может использоваться нигде, кроме одного-единственного приложения.
- родительский класс: все классы BOOPSI формируются в иерархическом порядке, и являются дочерними по отношению к корневому классу rootclass. Это позволяет каждому подклассу иметь собственную версию операции, производную от родительской, либо ту же самую, что и у родителя. Класс, содержащий подклассы, также называют базовым (base class) или суперклассом (super class).
- диспетчер: предоставляет доступ ко всем операциям класса, которые называют методами. Гарантирует, что каждая операция будет обеспечена исполнением соответствующего ей кода или будет передана родителю данного класса (суперклассу).
В BOOPSI типом класса является Class * или IClass.
Объект
Объект является структурной единицей (реализацией) класса. Каждый объект обладает своими свойствами (состоянием), но при этом поведение всех объектов одного класса одинаково. Объект относится к нескольким классам, если исчислять их от его класса до корневого класса rootclass, что определяется свойством наследования.
Для объекта типом BOOPSI является Object *. Он не содержит доступных напрямую полей.
Атрибут
Атрибут находится в связи с структурой данных (переменными состояния) каждого объекта: изменять эти данные непосредственно вы не можете. Возможно только установить или получить значения атрибутов объекта (также называемых свойствами) для изменения его внутреннего состояния. Атрибуты объекта (со стороны системы) ассоциированы с тегами (Tag) (со стороны программиста) (это значения, имеющие тип ULONG и связанные с TAG_USER).
Для изменения атрибутов объектов используются функции GetAttr() и SetAttrs().
Атрибуты (один или несколько) могут быть следующих видов:
- Назначаемый при установке (Initialization-settable)(I) : Атрибут может быть передан как параметр лишь при создании объекта.
- Доступный для изменения (Settable)(S) : Можно установить значение этого атрибута в любое время (не только при создании объекта).
- Доступный для чтения (Gettable)(G) : Вы можете лишь получить значение атрибута.
Метод
Методом в BOOPSI называется функция, которой в виде параметров передаются имя объекта, его класс и сообщение:
- объект: имя объекта, над которым производится действие.
- класс: класс, соответствующий объекту.
- сообщение: содержит идентификатор (ID) метода, определяющий функцию, вызываемую диспетчером и передаваемые ей параметры
Для отправки сообщения объекту используется функция DoMethod(). Сначала метод будет применён к указанному классу. Если в классе определен этот метод, то сообщение будет обработано. В противном случае, будут перебираться родительские классы до тех пор, пока сообщение не будет обработано одним из них, или не будет достигнут rootclass (в этом случае так и не определённое сообщение будет молча отвергнуто).
Примеры
Рассмотрим основные приёмы объектно-ориентированного программирования с BOOPSI:
Получение атрибута
Попробуем запросить данные объекта MUI String:
void f(Object *string) { IPTR result; GetAttr(string, MUIA_String_Contents, &result); printf("String content is: %s\n", (STRPTR)result); }
Здесь string - объект, MUIA_String_Contents - получаемый атрибут, &result - указатель на строку с результатом этой операции. К тому же:
- Object * является типом объектов BOOPSI.
- Для типизации возвращаемого значения должен использоваться тип IPTR,
- поэтому это значение может быть целым числом либо указателем. IPTR всегда сохраняется в памяти, и использование более ограниченного типа привело бы порче её содержимого!
- Когда мы запрашиваем атрибуты объекта MUI String: MUIA_String_Contents, как и любой другой атрибут, имеет тип ULONG (это тег).
В приложениях Zune вместо указанных функций часто используются макросы get() и XGET(). Например:
get(string, MUIA_String_Contents, &result); result = XGET(string, MUIA_String_Contents);
Установка атрибута
Слегка изменим приведенную выше строку:
SetAttrs(string, MUIA_String_Contents, (IPTR)"hello", TAG_DONE);
- Указатели, передаваемые в качестве аргументов должны иметь тип IPTR (указатель на целую переменную, может содержать адрес значений типа int) иначе компилятор будет выдавать предупреждения.
- Вслед за атрибутом, функции SetAttrs передаётся список тегов, поэтому перечисление должно заканчиваться на TAG_DONE.
Вам наверняка покажется полезным макрос set():
set(string, MUIA_String_Contents, (IPTR)"hello");
Однако, только с помощью SetAttrs() вы сможете установить несколько атрибутов за один раз:
SetAttrs(string, MUIA_Disabled, TRUE, MUIA_String_Contents, (IPTR)"hmmm...", TAG_DONE);
Вызов метода
Рассмотрим наиболее часто применяемый в программах Zune метод, метод обработки событий, вызываемый в основном цикле программы:
result = DoMethod(obj, MUIM_Application_NewInput, (IPTR)&sigs);
- Параметры функций метода не являются списком тегов, и не должны заканчиваться с TAG_DONE
- Указатели приходится типизировать как IPTR, чтобы избежать предупреждений, от чего смысл их применения не меняется
Hello world

С начала, так с начала. Эта программа не разочарует новичка.
Исходный код
Рассмотрим наш первый реальный пример:
// gcc hello.c -lmui #include <exec/types.h> #include <libraries/mui.h> #include <proto/exec.h> #include <proto/intuition.h> #include <proto/muimaster.h> #include <clib/alib_protos.h> int main(void) { Object *wnd, *app, *but; // Создание GUI: окна wnd, текста Hello world! и кнопки Ok app = ApplicationObject, SubWindow, wnd = WindowObject, MUIA_Window_Title, "Hello world!", WindowContents, VGroup, Child, TextObject, MUIA_Text_Contents, "\33cHello world!\nHow are you?", End, Child, but = SimpleButton("_Ok"), End, End, End; if (app != NULL) { ULONG sigs = 0; // Реакция на элемент закрытия окна и выход по клавише Escape DoMethod(wnd, MUIM_Notify, MUIA_Window_CloseRequest, TRUE, (IPTR)app, 2, MUIM_Application_ReturnID, MUIV_Application_ReturnID_Quit); // Реакция на нажатие кнопки выхода DoMethod(but, MUIM_Notify, MUIA_Pressed, FALSE, (IPTR)app, 2, MUIM_Application_ReturnID, MUIV_Application_ReturnID_Quit); // Открываем окно wnd set(wnd, MUIA_Window_Open, TRUE); // Проверяем, что окно wnd действительно было открыто if (XGET(wnd, MUIA_Window_Open)) { // Основной цикл приложения Zune while((LONG)DoMethod(app, MUIM_Application_NewInput, (IPTR)&sigs) != MUIV_Application_ReturnID_Quit) { if (sigs) { sigs = Wait(sigs | SIGBREAKF_CTRL_C); if (sigs & SIGBREAKF_CTRL_C) break; } } } // Уничтожаем наше приложение со всеми его объектами MUI_DisposeObject(app); } return 0; }
Комментарии
Замечание
Мы не открываем библиотеки вручную т.к. это делается за нас автоматически.
Создание GUI интерфейса
Мы использовали макросы для облегчения программирования интерфейса программы. Приложение Zune всегда имеет 1 (и только 1) объект Приложения (ApplicationObject):
: app = ApplicationObject,
Приложение может иметь 0,1 или более объектов окон WindowObject. Чаще всего окно - одно единственное:
: SubWindow, wnd = WindowObject,
Будет хорошо, если заголовок окна будет содержать название приложения:
: MUIA_Window_Title, "Hello world!",
Окно может иметь 1 (и только 1) дочерний объект (Child), обычно это группа (group). Наша группа будет вертикальной (VGroup), это означает, что все входящие в неё дочерние объекты (children) будут группироваться по вертикали:
: WindowContents, VGroup,
Группа должна иметь, как минимум 1 дочерний объект. В нашем случае, это будет обыкновенный текст (TextObject):
: Child, TextObject,
В Zune поддерживаются различные escape-коды (ниже, через 33c производится центрирование текста) и перевод каретки ( n ):
: MUIA_Text_Contents, "\33cHello world!\nHow are you?",
Макрос End должен завершать описание любого макроса вида xxxObject (в нашем случае, TextObject):
: End,
Теперь добавим в нашу группу второй дочерний объект, кнопку! Помимо мыши, она будет откликаться на комбинацию клавиш RAmiga + O (укажем на это символом подчёркивания до буквы "O"):
: Child, but = SimpleButton("_Ok"),
Завершаем описание группы:
: End,
Завершаем описание окна:
: End,
Завершаем описание программы:
: End;
И что, вы всё ещё нуждаете в графических инструментах для создания GUI ? :-)
Обработка ошибок
Если окажется невозможным создание любого из объектов в структуре описанной нами выше, Zune уничтожит все объекты (включая те, которые удалось создать) и возвратит код ошибки. В обратном случае, вы получите полностью рабочий Zune интерфейс приложения:
: if (app != NULL) : { : ...
Если работа приложения завершается, вызывается метод MUI_DisposeObject() с передачей указателя на созданный объект приложения. Это необходимо для уничтожения всех созданных объектов и освобождения всех использованных ресурсов:
: ... : MUI_DisposeObject(app); : }
Обработка сообщений
Обработка сообщений значительно упрощает задание реакции программы на возникающие события (такие как, нажатие кнопки). Принцип: мы получаем сообщение, когда определённый атрибут определённого объекта примет определённое значение:
: DoMethod(wnd, MUIM_Notify, MUIA_Window_CloseRequest, TRUE,
Здесь мы ожидаем, когда атрибут MUIA_Window_CloseRequest объекта нашего окна (wnd) будет установлен в TRUE (пользователь нажал кнопку), В этом случае объект приложения получит сообщение, предписывающее ему вернуть код MUIV_Application_ReturnID_Quit на следующей же итерации цикла обработки событий:
: (IPTR)app, 2, : MUIM_Application_ReturnID, MUIV_Application_ReturnID_Quit);
Поскольку в этом списке могут быть указаны любые параметры, необходимо указать число дополнительных параметров, передаваемых MUIM_Notify: в этом случае, 2 параметра.
В случае c кнопкой "Ok" мы ожидаем, когда атрибут MUIA_Pressed` окажется установленным в FALSE, что будет означать нажатую и отпущенную пользователем кнопку "Ok" (реакция на простое нажатие кнопки является плохой практикой, т.к. вы можете захотеть отпустить кнопку мыши вне кнопки, и таким образом отказаться от действия. К тому же, мы можем и просто захотеть увидеть, как она выглядит в нажатом состоянии). В остальном, всё аналогично предыдущему примеру (посылается сообщение):
: DoMethod(but, MUIM_Notify, MUIA_Pressed, FALSE, : (IPTR)app, 2, : MUIM_Application_ReturnID, MUIV_Application_ReturnID_Quit);
Открытие окна
Окно не будет открыто, пока вы не "попросите" Zune об этом:
: set(wnd, MUIA_Window_Open, TRUE);
Если объекты описанной нами выше структуры были созданы удачно, вы уже должны увидеть окно. Но и эта операция может завершиться с ошибкой! Таким образом, мы не должны забывать о проверке атрибута объекта окна, который должен быть установлен в TRUE:
: if (XGET(wnd, MUIA_Window_Open))
Цикл приложения
Дорогие друзья, позвольте представить вам идеальный цикл интерфейса Zune:
: ULONG sigs = 0;
Не забывайте обнулять сигналы (sigs) ... далее показан тестовый цикл приложения с использованием метода MUIM_Application_NewInput:
: ... : while((LONG) DoMethod(app, MUIM_Application_NewInput, (IPTR)&sigs) : != MUIV_Application_ReturnID_Quit)
Этому методу передаются сигналы событий, которые он должен обработать (сообщения от Wait(), или 0), значение указателя sigs будет изменяться, принимая значения ожидаемых Zune сигналов (очередных сообщений от Wait()) и в результате это значение будет возвращено. Поэтому обнуление sigs в цикле необходимо. Этот механизм возврата значений исторически был единственным способом реакции на события. Однако, поскольку он был слишком "неудобоварим", впоследствии от него стали отказываться в пользу создания отдельных классов и объектно-ориентированной структуры приложения.
Тело самого цикла приложения весьма простое. Здесь мы видим лишь ожидание сигналов и обработку нажатия Ctrl + С для обеспечения принудительного выхода из цикла:
: { : if (sigs) : { : sigs = Wait(sigs | SIGBREAKF_CTRL_C); : if (sigs & SIGBREAKF_CTRL_C) : break; : } : }
Заключение
Эта программа позволила вам начать изучение Zune и немного поработать на дизайном GUI приложения, но не более того.
Сборка
Чтобы собрать эту программу кросс-компилятором i386-aros-gcc, используйте следующую команду:
i386-aros-gcc -o hello -D__AROS__ hello.c -lmui
Реакция на события
Согласно комментариям к hello.c, приведенным выше, вы должны использовать MUIM_Notify для вызова метода при возникновении ожидаемого вами события. Если же требуется описать более специфичную реакцию программы на события, необходимо воспользоваться одним из следующих алгоритмов:
- MUIM_Application_ReturnID: можно возвращать некий идентификатор ID на последующих шагах цикла, и проверять это значение в цикле. Можно задать несколько таких идентификаторов и в зависимости от возвращаемого значения вызывать разные методы. Старый и дурацкий способ обхода цикла.
- MUIM_CallHook: использование стандартного для Amiga механизма вызова (callback) пользовательских функий при возникновении событий, управляемого структурой Hook. Способ, не имеющий ничего общего с ООП, но вполне допустимый.
- Использование ООП: вызов из тела цикла метода, принадлежащего одному из созданных вами ранее классов. Это лучшее решение с точки зрения объектно-ориентированной структуры программы, однако, оно мало подходит для новичков в ООП и программистов, не привыкших тратить много времени на "изящества".