Из журнала Info Guide #10, Рязань, 05.2007 Построение графического пользовательского интерфейса Vitamin/CAIG Доброго времени суток, уважаемые чита- тели! Наш сегодняшний разговор пойдет о некоторых аспектах построения графического пользовательского интерфейса (GUI, если по-буржуйски). Надеюсь, заинтересованные в данном вопросе найдут для себя что-нибудь полезное. Возможно,кому-то статья покажет- ся излишне структурированной и "разложен- ной по полочкам",но,на мой взгляд,это луч- ший способ структурировать информацию. 0. Общие основы Что представляет собой интерфейс? Сово- купность средств для отображения информа- ции и обратной связи с пользователем.Иными словами, взаимодействия. Следовательно,вы- рисовываются три наиболее важные части системы: - средства ввода информации; - средства отображения информации; - ядро для организации взаимосвязи этих компонент. 1. Средства ввода Исторически сложилось,что для Спектрума используются 3 основных инструмента для пользовательского ввода информации: клави- атура, джойстик и мышь. Как наиболее уни- версальный компонент, клавиатура зачастую выступает и единственным средством ввода. Джойстик или мышь могут быть с разной степенью удобства проэмулированы на ней. Но для наиболее комфортного использования создаваемого программного продукта необхо- димо предусмотреть поддержку и этих уст- ройств (особенно мыши). Использование кла- виатуры для ввода информации может быть следующим: - ввод текстовой информации; - эмуляция джойстика; - комбинированное использование в разных вариантах. Ввод текстовой информации, думаю, не представляет собой сложности.Печатные сим- волы все жёстко закреплены,на функциональ- ные клавиши тоже существуют де-факто стан- дарты. Эмуляция джойстика основана на привязке различных клавишных комбинаций, соответст- вующих соответствующим комбинациям контак- тов джойстика. Есть несколько стандартных комбинаций: QAOPSpace, QAOPM, SXOPSpace, QCsOPSpace, Sinclair1/2, Cursor, etc. Обы- чно не представляет больших трудностей реализовать поддержку сразу нескольких стандартов, чтобы пользователь, привыкший к какому-то конкретному варианту, не испы- тывал неудобств. Комбинированное использование подразу- мевает использовавние клавиатуры и как джойстика, и для ввода текстовой информа- ции. Обычно это делается переключением ре- жимов (ввод-управление), поддержкой горя- чих клавиш. Работа с мышью тоже не представляет особых сложностей, за исключением наличия разных стандартов на её подключение. Но поддержка самого распространённого вари- анта Kempston Mouse позволит поддержать бОльшую часть имеющегося оборудования. Из других особенностей использования мыши стоит отметить определение функционала клавиш (фиксированное или "первая нажа- тая") и использование последнего писка мо- ды - ролика (см. статьи в DonNews ). 2. Средства отображения информации Главным (да и, пожалуй, единственным) средством отображения у нас является обыч- ный спектрумовский экран размером 6912 байт.В силу отсутствия честного текстового режима в "стандартном" Спектруме, всё, что рисуется на этом экране, будет являться графикой. Что и как рисовать - вопрос осо- бый. Беглый взгляд на парк существующего системного (и не только) программного обе- спечения позволяет выделить несколько сти- лей оформления пользовательского интерфей- са: - "текстовый" режим на функциональных клавишах (обычно это присуще текстовым ре- дакторам, IS-DOS ). Характеризуется унифи- цированным стилем отображения элементов интерфейса.Возможны вкрапления графических элементов; - "текстовый" режим с использованием графического / текстового курсоров. Очень похож на предыдущий режим, только горячие клавиши заменяются или дополняются возмож- ностью управления курсором. Сами элементы управления в "текстовом" стиле используют- ся, в основном, для упрощения построения самого интерфейса. Пример - ZXZip; - упрощённый графический режим. Исполь- зование графических элементов (рамок, зна- чков и т.д.) и текстовых полей с зачастую произвольной позицией печати (не привязан- ной к глобальной экранной сетке). Средства ввода могут быть самыми разными. Пример - Real Commander (особенно версии до 2.0); - более сложный графический режим. Испо- льзование перекрывающихся окон,графических и текстовых элементов управления и т.д. Типичное средство ввода - графический кур- сор. Возможна также поддержка горячих кла- виш. Текстовый ввод применяется по ситуа- ции. Типичный представитель - BGE (да и практически все графические редакторы - у них на роду такое написано). 2.1. Организация "текстового" режима Способов и тонкостей в реализации дан- ного режима существует огромное множество. Начиная от полноценной эмуляции текстового режима (текстовый буфер, перерисовка всего экрана при обновлении) и заканчивая печа- тью "по требованию" отдельных элементов. Второй способ обычно эффективнее,поскольку позволяет более рационально использовать процессорное время (стирать данные в экра- не вместо забивания символов пробелами и т.д.) и память и совмещать текстовый вывод с графическим. 2.2. Организация графического режима В организации данного режима тонкостей ещё больше. Обычная концепция - прямоуго- льные области (окна) с элементами управле- ния на них. Первые возникающие вопросы - как организовать 1. отрисовку и 2. восста- новление фона? На первый вопрос можно при- думать такие решения: - рисование окна и элементов управления происходит напрямую в экранную область; - рисование осуществляется в некий вне- экранный контекст, выводимый целиком на экран; - комбинированный вариант. Применяется при необходимости частого обновления бо- льшой области окон при основной работе с внеэкранным контекстом (некий аналог DirectDraw) На второй вопрос тоже есть ответы: - запоминать фон за окном перед отрисов- кой.При закрытии окна фон восстанавливать; - перерисовывать ранее невидимые части экрана при изменении ситуации. В качестве углубленного материала можно рассмотреть некоторые достоинства и недос- татки разных комбинаций приведенных выше решений (третий вариант организации отри- совки рассматриваться не будет,это по сути расширенный второй вариант). 1) Запоминание/восстановление фона,рисо- вание в экранную область: + достаточно простая реализация; + максимальное соотношение скорость·фун- кционал; - большие сложности при изменении поряд- ка отображения перекрывающихся окон. 2) Перерисовка невидимых частей, рисова- ние в экранную область: + иногда приводит к выигрышу при обнов- лении небольших участков; + самый экономный по памяти вариант; - обычно самый медленный вариант; - сложность реализации обновления элеме- нтов управления. 3) Запоминание/восстановление фона,рисо- вание в контекст: * все достоинства и недостатки рисования в контекст; - не самый быстрый вариант (следствие рисования в контекст); - большие расходы памяти (в 2 раза боль- ше варианта 1). 4) Перерисовка невидимых частей, рисова- ние в контекст: + самый функциональный вариант из всех перечисленных; * расходы памяти аналогичны варианту 1; * все достоинства и недостатки рисования в контекст; - самый медленный из перечисленных вари- антов (может, иногда быстрее варианта 2). Отдельно стоит рассказать о вышеупомя- нутых достоинствах и недостатках рисования в контекст: + на экране отображается фактически один объект; + сравнительная простота обновления фра- гментов окна; - сложности рисования элементов управле- ния - зависит от способа и атрибутов пред- ставления контекста в памяти; - достаточно медленно. Выбор конкретного способа организации ложится на плечи разработчика. От себя до- бавлю, что реализовывал варианты 1 и 4 и могу достаточно объективно судить об их достоинствах и недостатках. 3. Ядро пользовательского интерфейса Как стало ясно из вышеизложенного, ядро выполняет связующую функцию между устройс- твами ввода и устройствами отображения и предоставляет необходимый функционал для организации. Собственно, структура этого ядра в миниатюре повторит структуру всей системы пользовательского интерфейса: +----------------+ | Средства ввода | +----------------+ | v +--------------------------------+------+ | Процедуры опроса средств ввода | | +--------------------------------+ | | Логика взаимодействия | ЯДРО | +--------------------------------+ | | Процедуры работы с экраном | | +--------------------------------+------+ | v +-------+ | Экран | +-------+ Набор процедур опроса средств ввода за- висит исключительно от набора поддерживае- мых средств.Несколько рекомендаций по это- му поводу. 1) Одновременный опрос нескольких уст- ройств. Устройства,действующие по принципу джойстика (непосредственно джойстик и его аналоги на клавиатуре) лучше опрашивать процедурами, возвращающими статус нажатых кнопок. В таком случае обычное объединение результатов даст итоговый набор флагов: Driving call KJoy ;bit0 - left ;bit1 - right ;bit2 - up ;bit3 - down ;bit4 - fire ld c,a call sinc1 ;bit0 - '6' ;bit1 - '7' ;bit2 - '9' ;bit3 - '8' ;bit4 - '0' or c ld c,a call KBD ;bit0 - 'O' ;bit1 - 'P' ;bit2 - 'Q' ;bit3 - 'A' ;bit4 - 'Space' or c ld c,a call KBD1 ;bit2 - 'S' ;bit3 - 'X' ;bit4 - 'M' or c ld c,a call MouseFire ;bit4 - 'mouseL|R' or c ret 2) Также полезно завести ещё один бит под кнопку "отмена".Некоторые модели джой- стиков поддерживают не одну,а две кнопки - вторую можно задействовать на эти цели. На клавиатуре же такой кнопкой может быть лю- бое сочетание - Break, Edit, Extend etc. 3) Мышь опрашивается отдельно в силу своей специфики. Опрос её клавиш можно де- лать в цикле опроса клавиатуры (как пока- зано выше). Процедуры работы с экраном целиком и полностью зависят от используемой концеп- ции рисования (напрямую или через кон- текст) и обновления (целиком или по час- тям). Любой мало-мальски искушённый кодер напишет необходимый набор (или использует из набора готовых, благо их навалом в раз- ных изданиях). Так мы потихоньку добрались до самой интересной части ядра, отвечающей за логи- ку взаимодействия вышеперечисленных компо- нент.Типичный цикл работы такого ядра при- менимо к одному объекту-окну выглядит при- мерно следующим образом: 1) Создание окна; 2) Регистрация окна в ядре; 3) Построение окна; 4) Цикл опроса устройств ввода; 5) Выполнение активированных функций; 6) Переход на п.4, если процесс не заве- ршён; 7) Дерегистрация окна в ядре. Немного подробнее по каждому пункту: 1) С точки зрения ядра,окно представляет собой дескриптор в области памяти,описыва- ющий, что и как ядро должно отображать. В простейшем случае выполняется на этапе компиляции путем статического заполнения необходимых структур; 2) Дескриптор окна (копия или ссылка) регистрируется в системе.В простейшем слу- чае имеем стек открытых в данный момент окон. Если надо,можно организовать связный список; 3) Создание графической информации, не- посредственно показываемой пользователю - подложки и компонент окна. Подробнее будет рассмотрено ниже; 4) Получение информации о действиях по- льзователя - нажатие кнопок на клавиатуре, перемещение мыши и т.д. Подробнее будет рассмотрено ниже; 5) Непосредственно связано с п.3 - в зависимости от того, что создали, будем по-разному реагировать на действия пользо- вателя; 7) Выполнение действий,обратных п.2. Фи- зически дескриптор обычно продолжает суще- ствовать и может быть повторно использо- ван. Десктриптор окна обычно представляет из себя исчерпывающую для системы информацию об объекте: - геометрические размеры и позицию на экране; - информацию о стиле отображения (оформ- ления); - информацию об используемом(ых) контек- сте(ах); - куда окно сохраняет задний план (если сохраняет) и где хранит свой контекст (если хранит); - список размещаемых на окне компонент; - прочие параметры. Обычно используются следующие методы организации списка компонентов: 1) Резервирование указателей на массивы объектов соответствующих типов. Именно так работает SupremeGui из BGE - дескриптор окна содержит указатели на массивы надпи- сей, кнопок, флажков и т.д.; 2) Массив/список разнородных объектов с разными сигнатурами (идентификаторами). Процедуры обработки объектов пробегают все элементы по списку и,в зависимости от иде- нтификатора, по-разному обрабатывают даль- нейшую информацию в объекте; 3) Использование списка объектов с опре- делёнными в объекте реализациями методов. С первыми двумя методами все понятно - они достаточно просты в реализации,но име- ют один большой недостаток - слабую расши- ряемость. Добавление нового типа объектов потребует доработки (а то и переработки) основного кода ядра. Чтобы понять третий метод,следует иметь некоторое представление о полиморфизме (из области объектно-ориентированного програм- мирования - ООП).Суть его в следующем - за выполнение действий над объектом отвечает сам объект. Иными словами, помимо описания объекта (способа представления его в памя- ти) необходимо определить ряд функций объ- екта, которые будут вызываться системой в тех или иных ситуациях. В коде это может выглядеть примерно следующим образом: ;базовый описатель окна (и объекта окна): Window dw vfpt ;указатель на таблицу ;виртуальных функций db x,y,w,h ;размеры окна dw Flags ;флаги и т.д. dw Context ;указатель на контекст dw Sibling ;указатель на ;следующее окно в списке dw Childs ;указатель на список ;дочерних окон ... ;прочие данные vfpt dw Create ;указатель на функцию ;создания окна dw Show ;указатель на функцию ;показа окна dw Update ;указатель на функцию ;обновления окна dw OnInput ;указатель на функцию ;обработки ввода ... ;указатели на прочие функции И если система захочет обновить окно, оно выполнит такой код: ;IX-object SysUpdate ld hl,4 ;смещение в таблице ;виртуальных функций jp VCall ;вызов вирт. функции ... VCall ld a,(ix) add a,l ld l,a ld a,(ix+1) add a,h ld h,a ;HL указывает на адр.ф-ции ld a,(hl) inc hl ld h,(hl) ld l,a ;HL содержит адрес функции jp (hl) ;вызов функции Достаточно громоздко. Но! Взамен мы по- лучаем практически неограниченные возмож- ности расширения - можно создавать новые типы компонентов,абсолютно не трогая ядро! Главное - соблюдать базовый интерфейс. При таком подходе ядро существенно "ху- деет" - в нём остается только логический "скелет" вызова виртуальных функций, под- держка базовых компонент (соответствующие функции,которые вполне могут использовать- ся потомками - т.н.неполная перегрузка ви- ртуальных функций). Все остальные компоне- нты реализуются совершенно автономно, ядро и знать не знает, что они из себя в точно- сти представляют. Помимо решения проблемы создания и отображения объектов, снимается проблема реакции на пользовательский ввод - каждый компонент также в состоянии само- стоятельно отреагировать на возникающие "во внешнем мире" события. Внимательный читатель, наверное, заме- тил в примере описателя непонятное поле Sibling. Для чего оно?А это ещё одна идея, связанная с организацией иерархии объек- тов. Для тех, кто, может,не до конца понял суть происходящего, поясню: вышеуказанный объект описывает вообще любой объект в ра- мках оконного интерфейса - начиная от не- посредственно окон и заканчивая последней кнопочкой на этих самых окнах. Всё разли- чается исключительно реализацией конкрет- ных функций.Указанные поля - минимум,необ- ходимый для работы каркаса. Конкретный тип объекта имеет полное право расширить набор этих полей (замаскированно под "прочие данные") и использовать эти расширения. Главное - сохранить нетронутой базу. С то- чки зрения ООП данный процесс называется наследованием ("наследники" получают все свойства "предка") и, в некотором роде,ин- капсуляцией (про "прочие данные" ядро не знает, ими пользуются только те, кто знает "наследника"). Замечание. В дальнейшем слово "наслед- ник" будет упоминаться в контексте иерар- хии экземпляров объектов, а не в контексте понятия "наследование" из ООП. Следовательно,мы имеем возможность хра- нить объекты в "дереве" - иерархической структуре, что весьма облегчит нам жизнь. Первый уровень - непосредственно окна. Они образуют список - замена вышеупомянутому стеку окон. В качестве "детей" окна могут выступать вложенные иерархические структу- ры: ..-(sibling)-> Window -(sibling)->... |(child) v Frame->Button1->Button2 | v Radio1->Radio2->Checkbox Что на экране может выглядеть как: +-Window--------------+ | | | +-Frame------+ | | | o Radio1 | | | | o Radio2 | | | | v Checkbox | | | +------------+ | | | | [Button1] [Button2] | | | +---------------------+ Дочерние объекты наследуют свойства своих "родителей" - отсчитывают своё поло- жение относительно "родителя", наследуют общие свойства (заблокированность, стиль и т.д.) Любая системная процедура для работы с окнами и объектами будет работать с гори- зонтальным ярусом дерева иерархии - пере- ход между уровнями должен осуществляться непосредственно объектом. Поясню. Предпо- ложим,нам надо обновить (перерисовать) все окна. Некая системная процедура получает на вход список имеющихся в системе окон (на "дереве" - самая верхняя строчка). Она делает очень мало - для каждого объекта вызывает виртуальный метод для обновления и переходит к следующему элементу в спис- ке. При вызове метода элемент может рекур- сивно применить эту же процедуру для спис- ка своих "потомков" (предварительно имея возможность выполнить какие-то свои дейст- вия), которые, в свою очередь, также могут ещё более углубиться. Примерный код вызова виртуальной функции по цепочке: ;IX - start object, HL - vfpt offset ChainVCall push hl ld l,(ix+10) ;смещение до ld h,(ix+11) ;поля Sibling ex (sp),hl push hl call VCall pop hl,ix ld a,hx or lx jr nz,ChainVCall ret И если мы имеем примерно следующие структуры (описывающие пример, приведенный выше): Window1 dw WinVfpt db ... dw ... dw ... dw Window2 ;следующ.окно в цепочке dw Win1Childs ... Win1Childs Win1Frame dw FrameVfpt db ... dw ... dw ... dw Win1Button1 dw Win1FrameChld ... Win1FrameChld Win1Radio1 dw RadioVfpt db ... dw ... dw ... dw Win1Radio2 dw 0 ;у перекл-теля нет "потомков" ... Win1Radio2 dw RadioVfpt db ... dw ... dw ... dw Win1Checkbox dw 0 ;... ... Win1Checkbox dw CheckboxVfpt db ... dw ... dw ... dw 0 ;последнее окно в ярусе dw 0 ;нет "потомков" Win1Button1 dw ButtonVfpt db ... dw ... dw ... dw Win2Button2 dw 0 ;... ... Win1Button2 dw ButtonVfpt db ... dw ... dw ... dw 0 ;аналогично Win1Checkbox dw 0 ...то можно составить следующие таблицы виртуальных функций: WinVfpt dw WindowCreate ... FrameVfpt dw FrameCreate ... ButtonVfpt dw ButtonCreate ... RadioVfpt dw RadioCreate ... CheckboxVfpt dw CheckboxCreate ... В данном случае все функции в виртуаль- ных таблицах внутренние - это часть функ- ционала ядра по обслуживанию базовых ком- понент. Функции могут выглядеть так: ...Create ... ;сделать что-то полезное jp IterateChildren ... IterateChildren push hl ld l,(ix+12) ;берем "детей" ld h,(ix+13) ld a,l or h jr z,nochilds ex (sp),hl pop ix jp ChainVCall nochilds ;если у объекта нет "детей" pop hl ret Если объект гарантированно не может иметь "наследников", то вместо перехода на IterateChildren необходимо выполнить прос- то возврат. Теперь предположим,мы хотим вместо обы- чных кнопок реализовать свои. Очень просто - в описании объекта кнопки вместо указа- теля на стандартную таблицу виртуальных функций помещаем указатель на свою собст- венную таблицу: Win1Button1 dw MyButtonVfpt ... MyButtonVfpt dw MyButtonCreate ... ;не обязательно переопределять ... ;все функции, можно ... ;использовать и имеющиеся MyButtonCreate ... ;сделать что-то своё jp ButtonCreate ;и можно передать ;системе дальнейшее управление, а можно ;и всё делать самому - даже решать, ;передавать ли управление "потомкам" Не утомил? Не напугал? :) На самом деле всё достаточно просто. Главное - предста- вить всё это дело в тонкостях, достаточных для понимания происходящих процессов,но не перегружаться, чтоб не запутаться. Напоследок можно сказать несколько слов об эффективности.Приведённый пример реали- зации полиморфизма практически полностью скопирован из имеющихся реализаций на дру- гих платформах. Можно попробовать разные варианты: Window dw CallbackFunc db ... CallbackFunc and a jp z,Create dec a jp z,Show dec a jp z,Update ... И соответствующий системный вызов: VCall push hl ld l,(ix) ld h,(ix+1) ex (sp),hl ret Т.е.вместо указателя на таблицу функций помещаем указатель на функцию-диспетчер. Данный пример реализации более безопасен и может быть весьма эффективен при маленьком наборе виртуальных функций - несмотря на более короткую функцию виртуального вызо- ва, имеются накладные расходы на диспетче- ризацию. В качестве экзотического варианта можно вкратце рассмотреть вариант асинхронного (отложенного) вызова методов.Данный подход может применяться во многопоточных прило- жениях. (Да-да, это не опечатка - может быть, и будут такие рано или поздно, и не обязательно с появлением гипотетической ОС - приложение само по себе является ОС). Суть метода в том,что вместо вызова метода объекту посылается сообщение (помещается в очередь сообщений), и оно может быть обра- ботано позже в цикле обработки сообщений потока. На такой вот оптимистической ноте поз- вольте откланяться. Жду отзывов,вопросов и идей.