...
...

Эффективное программирование 3D-приложений с помощью Irrlicht и Jython. Часть 10

В прошлый раз мы начали знакомство с реализацией ООП в python|jython и использовали полученные знания для организации самого простого взаимодействия irrlicht с пользователем — реакции на события клавиатуры. Сегодня продолжим эту тему и разберем, как обрабатывать события мыши, а также попробуем спроектировать интерфейс приложения с помощью стандартных компонентов GUI: кнопки, списки, диалоговые окна.

Первое задание сегодняшнего дня — разработать простой аналог mspaint, который будет уметь рисовать линию вслед за мышью. Вкратце идея кода такова: необходимо создать обработчик событий, приходящих от мыши. Каждый раз, когда кнопка мыши зажимается, мы устанавливаем специальную переменную "флажок". Когда отжимаем кнопку мыши, "флажок" сбрасывается. Тогда же, когда мышь просто перемещается, мы анализируем состояние "флажка" и рисуем линию из точки, где мышь находилась в предыдущий момент, в ту точку, где она находится сейчас. Методика создания класса обработчика событий мыши абсолютно идентична той, которая использовалась для обработки событий клавиатуры в предыдущей статье. Мы создаем собственный класс-наследник от IEventReceiver и переопределяем в нем метод OnEvent, в котором надо добавить анализ приходящих сообщений.

import java
import net.sf.jirr
from net.sf.jirr import dimension2di
from net.sf.jirr import position2di
from net.sf.jirr import recti
from net.sf.jirr import SColor
from net.sf.jirr import IEventReceiver

# класс обработчика событий должен наследоваться от IEventReceiver
class EvtHandler (IEventReceiver):
_driver = None # ссылка на драйвер устройства
start_paint = False # признак того, идет ли рисование
old_x = 0 # координаты предыдущего положения мыши
old_y = 0
def __init__ (self, _driver):
self._driver = _driver

def OnEvent (self, e):
# прежде всего проверяем, что событие относится к мыши
if (e.getEventType() == net.sf.jirr.EEVENT_TYPE.EET_MOUSE_INPUT_EVENT):
# получаем текущие координаты мыши
x = e.getMouseInputX ()
y = e.getMouseInputY ()

if (e.getMouseInputEvent () == net.sf.jirr.EMOUSE_INPUT_EVENT.EMIE_LMOUSE_PRESSED_DOWN):
# устанавливаем флажок — начало рисования
self.start_paint = True
self.old_x = x
self.old_y = y

if (e.getMouseInputEvent () == net.sf.jirr.EMOUSE_INPUT_EVENT.EMIE_LMOUSE_LEFT_UP):
# сбрасываем флажок рисования
self.start_paint = False

if (e.getMouseInputEvent () == net.sf.jirr.EMOUSE_INPUT_EVENT.EMIE_MOUSE_MOVED and self.start_paint):
# если мышь перемещается, и флажок рисования установлен, то рисуем линию
self._driver.draw2DLine (position2di(self.old_x, self.old_y), position2di(x, y), SColor (255,0,0,0))
self.old_x = x
self.old_y = y

java.lang.System.loadLibrary ('irrlicht_wrap')
# драйвер я сменил с directx на opengl из-за ошибок закрашивания, почему, непонятно
device = net.sf.jirr.Jirr.createDevice( net.sf.jirr.E_DRIVER_TYPE.EDT_OPENGL, dimension2di(800, 600), 32)
driver = device.getVideoDriver()
evt = EvtHandler (driver)
device.setEventReceiver (evt)

# первый раз рисуем сцену белым цветом
driver.beginScene(True, True, SColor(255,255,255,255))
driver.endScene ()

while device.run():
# очень важно, чтобы два первых параметра были False
# это значит, что не нужно стирать то, что было нарисовано раньше — историю линии
driver.beginScene(False, False, SColor(0,0,0,0))
driver.endScene ()
device.drop

Теперь мы рассмотрим, как создавать GUI. Давайте вспомним первые наши занятия по irrlicht — там в начале каждого файла я писал следующую строку кода:

java.lang.System.loadLibrary ('irrlicht_wrap')
device = net.sf.jirr.Jirr.createDevice(net.sf.jirr.E_DRIVER_TYPE.EDT_DIRECT3D9, dimension2di(800, 600), 32)
driver = device.getVideoDriver()
smgr = device.getSceneManager()
guienv = device.getGUIEnvironment()

Ранее мы использовали только переменную driver. Объект driver умел рисовать линии, картинки из файла, выводить текст — в общем, все, что касается собственно 2d-рисования. Вторая и третья переменные нами никогда не использовались. Smgr — переменная, в которой хранится ссылка на менеджер сцены. Если вы решили создать настоящий трехмерный лабиринт, по которому будут бегать анимированные злодеи и стрелять спецэффектами, то SceneManager — именно то, что вам нужно. Guienv — отвечает за создание и размещение стандартных элементов управления GUI — нашей сегодняшней темы. Элементов управления на самом деле не очень много. А по сравнению с разнообразием компонентов для delpi|cbuilder/vcl/.net и их настраиваемостью, скажем прямо, irrlicht gui откровенно убог. Но с другой стороны вы же не будете использовать irrlicht для разработки офисных или финансовых приложений. Для игр и задач моделирования зачастую достаточно диалоговых окон сохранения и загрузки, пары ползунков для выбора параметров и кнопки запуска. Если же у вас возникла необходимость совместить 3d-графику со сложным интерфейсом, то вам лучше подойти с обратной стороны. Разработку интерфейса можно выполнить в среде delphi|cbulder, а затем выполнить отрисовку на их форме окна irrlicht. Если вас заинтересовала эта задача, то ключевые слова для поиска: createDeviceEx — функция создания устройства irlicht с расширенным набором параметров, среди которых есть и HWND-окна, на котором будет выполняться отрисовка. К проектированию пользовательского интерфейса можно подойти с двух сторон. Первый подход — создание элементов и их позиционирование с помощью вызовов специальных функций а-ля создать_кнопку, создать_меню. Второй же подход не столько упрощает проектирование интерфейса программистами, т.е. нами, сколько позволяет конечному потребителю выполнять его тонкую настройку под себя. Все серьезные приложения давно умеют менять свой вид: наборы показываемых панелей управления, их размещение и оформление. Даже игры (я говорю про хорошие игры) сейчас делают так, что выносят во внешние текстовые файлы описание положения определенных элементов интерфейса и особенности их внешнего вида. Это развязывает руки фанам, разрабатывающим дополнения или моды к игре, а, следовательно, способствует лучшим продажам продукта. Этот второй подход основан на существовании двух функций irrlicht: irr::gui::IGUIEnvironment::loadGUI и irr::gui::IGUIEnvironment::saveGUI. Эти функции получают только один параметр — имя файла, содержащего описание GUI. Теперь о формате, в котором эти данные хранятся.

В общем случае существуют два противоположных полюса, или концепции хранения информации. Это текстовые форматы и двоичные. У каждого из них есть свои плюсы и минусы. Двоичный формат хранит данные в виде, максимально близком к тому виду, которым эти данные представлены в памяти компьютера. Например, если у вас есть структура данных "машинка" с полями: модель, запас топлива, скорость …, и вы ее сохраняете в файл, который затем откроете с помощью блокнота, то, боюсь, вы не сможете разделить информацию (поля структуры будут записаны друг за другом без разделителей, длина полей будет зависеть от способа выделения памяти). Редактирование такого файла представляет собой нетривиальную задачу, и без точного знания, что именно в файле было сохранено, в каком порядке и в каком количестве, просто невозможно. Если же такая необходимость все-таки у вас возникла, то применяйте специальные hex-редакторы. Итак, минусы двоичных файлов — это непонятность внутреннего содержимого и невозможность его нормально редактировать без помощи специальных инструментов, созданных авторами этого двоичного формата. Плюсы естественным образом вытекают из минусов — как известно, "достоинства — это продолжения недостатков, и наоборот". Первый плюс — это скорость. Раз структура файла подобна структуре памяти, то чтение и запись выполняются без лишних преобразований методом прямого копирования. Также, если вы хотите, чтобы никто не смог прочитать ваши файлы без помощи специального программного продукта, двоичный формат — самое то. Текстовые форматы, наоборот, ориентированы на запись информации в форме, максимально понятной человеку, и доступны для редактирования почти любым простым текстовым редактором. В начале 2000 годов был настоящий бум универсальных текстовых форматов, их делали все кому не лень. Но дожили до сегодняшнего дня очень немногие. Прежде всего, это xml — (расширяемый язык разметки), который стал основой для множества других специальных языков. Документ xml содержит специальные теги, которые разделяют информацию на структурные части. Ниже я привожу пример простого файла xml, описывающего структуру этой статьи:

<?xml version="1.0" encoding="windows-1251" ?>
<article> <!—главный тег — статья содержит внутри себя три части -->
<part number="1">введение</part><!-- у каждой части есть атрибут: number и собственно название раздела -->
<part number="2">об xml и gui</part>
<part number="3">об обработке событий мыши</part>
</article>

Именно в этом формате хранит информацию об GUI irrlicht. Кроме GUI, irrlicht умеет загружать из xml-файлов информацию о 3d-окружении, моделях, освещении, системах частиц и т.д. Второй известный универсальный формат — yaml. Его начали разрабатывать в пику xml как облегченную версию и не столь громоздкую. Как вариант можно хранить информацию в виде ini-файлов, содержимое которых представляет набор пар: переменная = значение. Но этот формат слишком неудобен, если сохраняемая информация иерархична.

Итак, осталось только упомянуть плюсы/минусы текстовых форматов, и xml в частности. Минусы очевидны — это медленная скорость работы и огромный размер файлов. Размер одной и той же информации в виде xml и в двоичном формате может отличаться в десятки раз. В настоящее время фактор избыточного размера не столь важен, как раньше, но остается проблемой низкая скорость. Программа не может просто скопировать файл в память, как раньше. Она должна выполнить его синтаксический разбор. Конечно, для "настоящих программистов" это несложная задача, к тому же, существует множество библиотек функций, работающих с xml. Но сути дела это не меняет — скорость программ, хранящих данные в xml-формате, часто в десятки, если не сотни, раз медленнее, чем у их двоичных собратьев. Могу посоветовать только одно: разумно разделите хранящиеся данные на две категории и храните их раздельно. Также можно создать специальную утилиту, которая будет получать на вход xml-документ, а на выходе будет создан двоичный файл. При создании элементов управления мы вызываем различные функции объекта guienv. Общим для всех них является наличие параметра recti — задающего относительные координаты добавляемого элемента. Координаты задаются относительно родительского элемента — если данный параметр не указан, считается, что компонент добавляется на высший уровень иерархии элементов окна. Очень важно для каждого элемента интерфейса задать его уникальный идентификатор. Также возможно наличие некоторых специальных параметров, специфических для определенных элементов интерфейса. Например, для набора закладок таким параметром является признак, рисовать или нет границу вокруг этого элемента, для падающего списка — признак фоновой закраски области элемента, для кнопки на инструментальной панели (ToolBar) это изображение. Я, естественно, не могу перечислить все такие опции, т.к. их очень много, но о наиболее важных написал в примечаниях кода ниже:

import java
import net.sf.jirr
from net.sf.jirr import dimension2di
from net.sf.jirr import position2di
from net.sf.jirr import recti
from net.sf.jirr import SColor
from net.sf.jirr import IEventReceiver

# это код функции, выполняющей корректировку строки выводимого текста
# ее код идентичен тому, что мы узнали в части 7
def adapt (s, start_from_in_font):
rez = ""
for _c in s:
if ord(_c) >= 1040:
rez = rez + chr( ord(_c) — (1040 — start_from_in_font))
else:
rez = rez + _c
return rez

java.lang.System.loadLibrary ('irrlicht_wrap')
device = net.sf.jirr.Jirr.createDevice(net.sf.jirr.E_DRIVER_TYPE.EDT_DIRECT3D9, dimension2di(800, 600), 32)
driver = device.getVideoDriver()
guienv = device.getGUIEnvironment()

# далее идет объявление множества переменных, которые будут хранить идентификаторы всех элементов управления, используемых ниже
GUI_TOOLBAR = 50
GUI_SAVE = 51
GUI_LOAD = 52
GUI_EXIT = 53

GUI_PAGES_CONTROL = 100
GUI_PAGES_TAB_SOURCE = 101
GUI_PAGES_TAB_CHESS = 102
GUI_BUTTON_APPLE = 103
GUI_BUTTON_ORANGE = 104
GUI_TXT_FIO = 105
GUI_TXT_PHOTO = 106
GUI_BUTTON_FRUITS_LIST = 200
GUI_BUTTON_FRUITS_COMBO = 201

# переменная, в которой хранится множество картинок — как иконки для ToolBar, так и файл изображения шрифта
cur_dir = "F:/kolya/py/resources/"
user_font_arial = guienv.getFont(cur_dir + "book.bmp")
# замещаем шрифт новым с поддержкой русских символов
guienv.getSkin().setFont (user_font_arial)
# создаем элемент управления — набор кнопок ToolBar
# здесь и далее, если я использую какую-либо переменную из списка идентификаторов выше,
# значит, что для обращения к созданному элементу управления будет применяться именно этот идентификатор
tools = guienv.addToolBar (None, GUI_TOOLBAR)

# теперь начинаем наполнять ToolBar кнопками — у кнопок нет надписи, но есть картинка
# метод добавить_кнопку вызывается от имени ToolBar
tools.addButton (GUI_SAVE, None, driver.getTexture(cur_dir + "tbtn_save.bmp"))
tools.addButton (GUI_LOAD, None, driver.getTexture(cur_dir + "tbtn_load.bmp"))
tools.addButton (GUI_EXIT, None, driver.getTexture(cur_dir + "tbtn_exit.bmp"))

# теперь добавляем набор закладок; параметр номер 2 задает ссылку на
# родительское окно: так, я указал специальное значение None, что значит
# элемент добавляется на высший уровень иерархии
pages = guienv.addTabControl (recti (0, 30, 980, 590), None, False,True, GUI_PAGES_CONTROL)
# и добавляем две закладки; также обратите внимание, что вызов методов addTab идет уже не от имени
# guienv, а именно от имени TabControl
tab_source = pages.addTab ("Tab Caption")
tab_chess = pages.addTab (adapt("Шахматы", 192))

# создаем различные элементы управления
# сначала создаем две кнопки
# последний параметр — это
btn_apple = guienv.addButton(recti(10,10,300,30), tab_source, GUI_BUTTON_APPLE, "Apple")
btn_orange = guienv.addButton(recti(10,50,300,80), tab_source, GUI_BUTTON_ORANGE, adapt("Апельсин", 192))
# затем список
lst_fruits = guienv.addListBox(recti(10,100,300,160), tab_source, GUI_BUTTON_FRUITS_LIST)
# теперь выпадающий список
combo_fruits = guienv.addComboBox(recti(10,180,300,204), tab_source, GUI_BUTTON_FRUITS_COMBO)
# создаем текстовое редактируемое поле
txt_fio = guienv.addEditBox("Vasyan Tapkin", recti(10,220,300,244), True, tab_source, GUI_TXT_FIO)
# и наконец элемент управления "Изображение"
image_foto = guienv.addImage(recti(10,260,310,560), tab_source, GUI_TXT_FIO)
# загружаем в созданный элемент файл картинки
image_foto.setImage (driver.getTexture(cur_dir + "portret2.jpg"))

# и наполняем список и падающий список элементами — текстовыми строками
captions = ("Apple", "Orange", "Grapes")
for i in captions:
lst_fruits.addItem (i)
combo_fruits.addItem (i)

# дальнейший код обычен — организуется цикл отрисовки
while device.run():
driver.beginScene(1, 1, SColor(255,220,241,240))
# именно здесь выполняется отрисовка всех тех элементов, которые были созданы выше
guienv.drawAll()
driver.endScene()
device.drop

Важное замечание: в примере был загружен пользовательский шрифт, с помощью которого мы выводим надписи на кнопках, закладках и падающих списках. Примененная методика идентична той, которую мы применяли в части 7. Обработка русских символов корректно работает как в выводе статических надписей (падающий список и заголовки кнопок), так и динамических (текстовые поля, в которые можно вводить русские символы). Следует, однако, уточнить, что материал данной статьи ориентируется на irrlicht 1.1. Буквально месяц назад вышла новая версия irrlicht 1.3 в которой сильно были изменены, переписаны и переделаны (без поддержки обратной совместимости) методы работы со шрифтами. Пока еще нет (по крайней мере, на момент написания данной статьи) версии jirr, поддерживающей работу с irrlicht 1.3. Но как только обновленные биндинги irrlicht -> java выйдут, я настоятельно рекомендую не затягивать и срочно переходить под новую версию irrlicht. В частности, методы чтения/записи описания GUI в xml-файлы доступны только под 1.3. Примеры, как выглядят эти файлы xml, я не привожу, т.к. они достаточно громоздки. Стоит отметить, что в irrlicht 1.3 появился визуальный редактор GUI-интерфейсов. В идеале вы таскаете на рабочую область компоненты, настраиваете их положение и внешний вид, а затем сохраняете во внешнем xml-файле (эдакий delphi для бедных). Увы, но, сказать по правде, более "глючного" программного продукта я не видел уже давно. Оправдывает разработчиков irrlicht только то, что эти возможности пока экспериментальные.

В следующий раз мы закончим с обработкой событий от элементов GUI и закроем большой подраздел, посвященный работе с 2d-графикой. Постарайтесь получить пару уроков по 3dsmax и заодно раздобудьте quake2|quake3. Мы попробуем создать приложение, загружающее карту уровня этой игры.

black zorro, black-zorro@tut.by

© Компьютерная газета

полезные ссылки
Аренда ноутбуков