14.6 Графический интерфейс — Xt |
Предыдущая Содержание Следующая |
X Window System (X11) является стандартом де-факто для графических интерфейсов пользователя на UNIX и других системах. (* стандартный источник для получения информации о программировании под X11 - серия книг X Window System, опубликованная издательством O'Reilly and Associates. Справочный материал для текущего раздела может быть найден в томе 4, страницы руководства находятся в томе 5, ISBN 0-937175-58-7.) X11 управляет терминалом с растровым экраном, мышью и клавиатурой, и предоставляет средства ввода и вывода. Xlib — библиотека функций, реализующая протокол связи между прикладной программой и сервером X11, управляющим терминалом. Программирование X11 достаточно трудное, поскольку ожидается, что прикладные программы будут вести себя ответственно при совместном использовании средств сервера. Поэтому есть пакет X toolkit, небольшая иерархия классов, которая главным образом служит основой для библиотек графических объектов. Пакет реализован на C. Объекты пакета названы виджетами. Базовым классом пакета является Object, то есть надо будет изменить наш код, чтобы избежать этого имени. Другой важный класс в пакете это ApplicationShell: виджет от этого класса обычно служит основой для программы, использующей сервер X11. Сам пакет не содержит классы с виджетами, которые видимы на экране. Однако общедоступен Xaw, Athena Widgets, примитивное расширение иерархии классов пакета, который обеспечивает достаточную функциональность для демонстрации нашего калькулятора. Виджеты прикладной программы собираются в дерево. Корень этого дерева — объект ApplicationShell. Если мы работаем с Xaw, следующими будут виджеты Box или Form, так как они в состоянии управлять другими виджетами в своей экранной области. Для отображения нашего калькулятора на экране можно использовать виджет Label из Xaw, а кнопка может быть реализована с помощью виджета Command. На экране виджет Command появляется как фрейм с текстом внутри него. Если мышь входит или выходит из такого фрейма, она изменяет свой вид. Если внутри фрейма щёлкнуть кнопкой мыши, виджет вызовет функцию обратного вызова, которая предварительно должна быть зарегистрирована. Так называемые таблицы преобразования соединяют события, такие как щелчки мышью и нажатия клавиш на клавиатуре, с так называемыми функциям действия, соединенными с виджетом, на который в настоящее время указывает мышь. У Command есть функции действия, которые изменяют его визуальное представление и заставляют сделать вызов функции обратного вызова. Эти действия используются в таблице преобразования Command, чтобы реализовать реакцию на щелчок мыши. Таблицы преобразования определённого виджета могут быть изменены или увеличены, то есть мы можем решить, что нажатие клавиши 0 должно влиять на указанный виджет Command, как будто щелчок кнопкой мыши был в его фрейме. Так называемые акселераторы по существу представляют собой переназначение таблиц преобразования от одного виджета другому. Фактически, если мышь находится в виджете Box, и если мы нажимаем клавишу, например +, можно перенаправить это нажатие клавиши от виджета Box другому виджету Command внутри прямоугольника и распознать его, как будто кнопкой мыши щелкнули внутри в этот самом виджете Command. Подведём итоги: нам будет необходим виджет ApplicationShell в качестве среды разработки приложения; виджет Box или Form, чтобы содержать другие виджеты; виджет Label в качестве экрана; несколько виджетов Command с подходящими функциями обратного вызова для наших кнопок; и какие-то волшебные преобразования позволят нам организовать всё так, чтобы нажатия клавиш вызывали те же эффекты, что и щелчки мышью по определённым виджетам Command. При стандартном подходе надо создать собственные классы для коммуникации с классами иерархии пакета. Такие классы обычно упоминаются как обёртки для чужой иерархии или системы. Очевидно, что обёртки должны быть максимально независимы от любых других соображений, чтобы мы могли использовать их в произвольных проектах на основе данного пакета. Вообще-то следует обернуть всё, что есть в пакете; но чтобы не раздувать эту книгу, вот минимальный набор для калькулятора:
Эти классы очень просты в реализации. Они существуют главным образом, чтобы скрыть более ужасное шаманство X11 и пакета от наших собственных приложений. В проекте есть над чем поразмыслить. Например, setLabel() мог бы быть определён для XawLabel, а не для Xt, потому что новая метка значима только для XawLabel и XawCommand, но не для ApplicationShell, Box или Form. Однако, определяя setLabel() для Xt мы моделируем работу пакета: виджетами управляют так называемые ресурсы, которые могут быть предоставлены во время создания или позже вызовом XtSetValues(). Это доходит до виджета, если он знает определённый ресурс и решает действовать, когда получает для него новое значение. Способность отправлять значение является свойством пакета как такового, а не определённого виджета. Учитывая эту основу нам необходимо только два вида объектов: XLineOut получает строку и выводит её на экран, а XButton отсылает текстовые события, источниками которых являются щелчки мыши или ввод символов с клавиатуры. XLineOut — подкласс XawLabel, который ведёт себя как LineOut, то есть который должен сделать что-то вокруг gate().
Xt.d
% Class XLineOut: XawLabel { %}
Xt.dc
% XLineOut ctor { // new(XLineOut(), parent, "name", "text") struct XLineOut * self = super_ctor(XLineOut(), _self, app); const char * text = va_arg(* app, const char *);
gate(self, text); return self; }
Для создания XLineOut должны быть определены три параметра: конструктору суперкласса необходим родительский объект Xt, который предоставляет родительский виджет для иерархии виджета приложения; новому виджету необходимо дать имя для поиска ресурсов по названиям в файле приложения с настройками по умолчанию; и наконец, новому XLineOut надо изначально установить какой-нибудь текст, который может определять его размер на экране. Конструктор храбр и просто использует gate(), чтобы отправить себе этот первоначальный текст. Поскольку XLineOut не наследован от Ic, он не может непосредственно ответить на gate(). Однако, селектор перенаправит вызов; поэтому, мы перепишем forward() для выполнения работы, которая была бы сделана методом gate, если бы он мог быть у XLineOut:
% XLineOut forward { %casts if (selector == (Method) gate) { va_arg(* app, void *); setLabel((void *) self, va_arg(* app, const void *)); * (enum react *) result = accept; } else super_forward(XLineOut(), _self, result, selector, name, app); }
Мы не можем быть уверены, что каждый вызов XLineOut_forward() является по сути скрытым вызывом gate(). Каждый метод forward должен выполнять проверку и отвечать только на ожидаемые вызовы. Другие могут конечно же быть переданы вдоль цепочки наследования с помощью super_forward(). Так же как new(), forward() декларируется со списком с переменным числом аргументов; однако, селектор может только передать инициализированное значение va_list актуальному методу и селектор суперкласса должен получить такой указатель. Чтобы упростить совместное использование списка аргументов, в частности из уважения к конструктору метакласса, ooc генерирует код для передачи указателя на указатель списка аргументов, то есть на параметр va_list * app. Вот тестовая программа xhello с простым примером передачи сообщения gate() в XLineOut:
void main (int argc, char * argv []) { void * shell = new(XtApplicationShell(), & argc, argv); void * lineOut = new(XLineOut(), shell, 0, "hello, world");
mainLoop(shell); }
Эта программа выводит на экран окно с текстом hello, world и ожидает уничтожения сигналом. Здесь мы не дали виджету в объекте XLineOut имя явным образом, потому что не определяем никаких ресурсов. XButton представляет собой подкласс XawCommand, поэтому можно добавить функцию обратного вызова для получения щелчков мыши и нажатий клавиш:
Xt.d
% Class XButton: XawCommand { void * button; %}
Xt.dc
% XButton ctor { // new(XButton(), parent, "name", "text") struct XButton * self = super_ctor(XButton(), _self, app); const char * text = va_arg(* app, const char *);
self -> button = new(Button(), text); setLabel(self, text); addCallback(self, tell, self -> button); return self; }
У XButton такие же аргументы конструктора, как у XLineOut: родительский объект Xt, имя виджета и текст, который появится на кнопке. Имя виджета может отличаться от текста, например, операторы для нашего калькулятора пригодны в качестве компонентов имён путей ресурсов. Интересной частью является функция обратного вызова. Мы позволяем XButton владеть объектом Button и договориться о функции обратного вызова tell() для отправки ему нулевого указателя с помощью gate():
static void tell (Widget w, XtPointer client_data, XtPointer call_data) { gate(client_data, NULL); }
client_data регистрируется в функции обратного вызова виджета, чтобы передать его позже. Мы используем его, чтобы определить цель gate(). Мы вполне могли бы избежать внутренней кнопки, потому что можно было бы настроить соединение с другим объектом самого объекта XButton; client_data мог бы указывать на указатель на этот целевой объект и указатель на текст, а затем метод tell() мог бы отправить текст непосредственно целевому объекту. Однако, проще снова использовать функциональность Button, особенно потому, что это открывает возможность для XButton получать текст через перенаправляемый gate() и передавать его внутреннему объекту "кнопка" Button для фильтрации. Конечно же, передача сообщений также является ключом к XButton: внутренняя кнопка недоступна, но она должна ответить на вызов wire(), который первоначально направлен в XButton:
% XButton forward { %casts if (selector == wire) wire(va_arg(* app, void *), self -> button); else super_forward(XButton(), _self, result, selector, name, app); }
Сравнивая два методов forward мы замечаем, что forward() получает self со спецификатором const, но обходит его в случае XLineOut_forward(). Основная идея состоит в том, что конструктор gate() должен знать, безопасно ли это. Как и раньше, мы можем протестировать XButton с помощью простой программы xbutton. Эта программа помещает XLineOut и XButton в XawBox, а другую пару в XawForm. Оба контейнера упакованы в другой XawBox:
void main (int argc, char * argv []) { void * shell = new(XtApplicationShell(), & argc, argv); void * box = new(XawBox(), shell, 0); void * composite = new(XawBox(), box, 0); void * lineOut = new(XLineOut(), composite, 0, "-long-"); void * button = new(XButton(), composite, 0, "a");
wire(lineOut, button); puto(button, stdout); /* Box будет двигать свои дочерние объекты */
composite = new(XawForm(), box, "form"); lineOut = new(XLineOut(), composite,"lineOut", "-long-"); button = new(XButton(), composite, "button", "b");
wire(lineOut, button); puto(button, stdout); /* Form не будет двигать свои дочерние объекты */
mainLoop(shell); }
Результат выглядит на экране примерно следующим образом:
Once the button a is pressed in the top half, the XLineOut receives and displays the text a. The Athena Box widget used as a container will resize the Label widget, i.e., the top box changes to display two squares, each with the text a inside. The button with text b is contained in an Athena Form widget, where the resource Как только в верхней части нажата кнопка a, XLineOut получает и выводит на экран текст a. Виджет Athena Box, используемый в качестве контейнера, изменит размеры виджета Label, то есть верхний прямоугольник изменится, чтобы вывести на экран два прямоугольника с текстом внутри. Кнопка с текстом b содержится в виджете Athena Form, где размещением управляет ресурс
*form.button.fromHoriz: lineOut
Виджет Form поддерживает появление нижнего прямоугольника даже при нажатии b и в XLineOut появляется короткий текст b. Тестовая программа демонстрирует, что XButton может работать и с щелчками мыши, и с wire(); поэтому пора соединить калькулятор с xrun:
void main (int argc, char * argv []) { void * shell = new(XtApplicationShell(), & argc, argv); void * form = new(XawForm(), shell, "form"); void * lineOut = new(XLineOut(), form, "lineOut", "........"); void * calc = new(Calc()); static const char * const cmd [] = { "C", "C", "1", "1", "2", "2", "3", "3", "a", "+", "4", "4", "5", "5", "6", "6", "s", "-", "7", "7", "8", "8", "9", "9", "m", "*", "Q", "Q", "0", "0", "t", "=", "d", "/", 0 }; const char * const * cpp;
wire(lineOut, calc); for (cpp = cmd; * cpp; cpp += 2) { void * button = new(XButton(), form, cpp[0], cpp[1]);
wire(calc, button); } addAllAccelerators(form); mainLoop(shell); }
Эта программа даже проще, чем версия для curses, потому что таблица содержит только имя и текст каждой кнопки. Расположение кнопок управляется ресурсами:
*form.C.fromHoriz: lineOut *form.1.fromVert: lineOut *form.2.fromVert: lineOut *form.3.fromVert: lineOut *form.a.fromVert: C *form.2.fromHoriz: 1 *form.3.fromHoriz: 2 *form.a.fromHoriz: 3 ...
Файл ресурсов также содержит акселераторы, которые загружаются addAllAccelerators():
*form.C.accelerators: <KeyPress>c: set() notify() unset() *form.Q.accelerators: <KeyPress>q: set() notify() unset() *form.0.accelerators: :<KeyPress>0: set() notify() unset() ...
Если ресурсы находятся в файле Xapp, калькулятор может быть запущен, к примеру, такой командой оболочки Bourne:
$ XENVIRONMENT=Xapp xrun
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Предыдущая Содержание Следующая |