Журнал LinuxFormat - перейти на главную

LXF115:FLTK

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
(Новая: : '''Программируем с ''FLTK''''' Быстрый, легкий, поддерживающий OpenGL: выберите любые три! {{Цикл/FLTK}} ==Сверхск...)
 
 
Строка 13: Строка 13:
 
Как уже отмечалось, поддержка OpenGL была в свое время уникальной и крайне привлекательной чертой ''FLTK'', и даже сейчас с некоторыми проблемами вывода трехмерной графики эта библиотека справляется лучше, нежели другие наборы виджетов. Для работы с OpenGL ''FLTK'' предлагает нам два класса: '''GlWindow''' и
 
Как уже отмечалось, поддержка OpenGL была в свое время уникальной и крайне привлекательной чертой ''FLTK'', и даже сейчас с некоторыми проблемами вывода трехмерной графики эта библиотека справляется лучше, нежели другие наборы виджетов. Для работы с OpenGL ''FLTK'' предлагает нам два класса: '''GlWindow''' и
 
'''GlutWindow'''. Как нетрудно догадаться, они наследуют Window и реализуют специальные типы окон, у которых рабочая поверхность подготовлена для вывода графики OpenGL. В остальном, окна '''GlWindow''' и '''GlutWindow''' подобны окну Window – они могут содержать дочерние виджеты и обрабатывать сообщения, адресованные главному окну программы. Окно '''GlWindow''' предоставляет базовую функциональность, необходимую для работы с OpenGL, а окно '''GlutWindow''' вдобавок эмулирует функции библиотеки ''GLUT''.
 
'''GlutWindow'''. Как нетрудно догадаться, они наследуют Window и реализуют специальные типы окон, у которых рабочая поверхность подготовлена для вывода графики OpenGL. В остальном, окна '''GlWindow''' и '''GlutWindow''' подобны окну Window – они могут содержать дочерние виджеты и обрабатывать сообщения, адресованные главному окну программы. Окно '''GlWindow''' предоставляет базовую функциональность, необходимую для работы с OpenGL, а окно '''GlutWindow''' вдобавок эмулирует функции библиотеки ''GLUT''.
    Если вы интересуетесь программированием с OpenGL, то
+
 
наверняка уже знаете, что такое GLUT, и тем не менее я это
+
Если вы интересуетесь программированием с OpenGL, то наверняка уже знаете, что такое ''GLUT'', и тем не менее я это поясню. Интерфейс OpenGL разрабатывался как максимально платформо-независимый. Выразилось это, например, в том, что в OpenGL не были включены функции для обработки сообщений системы и взаимодействия с окнами. Вместе с тем, на практике OpenGL-программы разрабатываются, в основном, в графических многооконных средах, а значит, всем программистам нужен некий минимум средств для взаимодействия между OpenGL и оконной системой. Конечно, разработчики последних тоже не остались в стороне. Для ''X Window'' была разработана система ''GLX'', а для Windows GDI – ''WGL'' (Wiggle), но эти расширения были довольно сложны и несовместимы друг с другом. Свободную нишу заполнила разработанная Марком Килгардом [Mark J. Kilgard] библиотека ''GLUT'', которая отличалась от ''GLX/WGL'' простотой использования и кроссплатформенностью (фактически, ''GLUT'' на каждой платформе представляет собой надстройку над расширениями конкретной системы). Неудивительно, что в то время многие программисты предпочитали ''GLUT'' для разработки надежным кроссплатформенных программ.
поясню. Интерфейс OpenGL разрабатывался как максимально
+
 
платформо-независимый. Выразилось это, например, в том, что
+
Учитывая популярность ''GLUT'', разработчик ''FLTK'' Билл Спитцак [Bill Spitzak] принял мудрое решение – добавить поддержку интерфейса ''GLUT'' в свой набор виджетов. В результате авторы программ, использовавшие ''GLUT'', смогли без труда портировать свой код на ''FLTK ''(отметим в скобках, что если вы начинаете писать новую программу, нет никакого смысла использовать класс '''GlutWindow''', так как все то хорошее, что может дать вам библиотека ''GLUT'', реализовано в классе '''GlWindow'''). Поскольку библиотека ''GLUT'' не является открытым ПО (хотя исходные тексты доступны), Спитцак создал модуль поддержки ''GLUT'' с нуля, сохранив совместимость на уровне интерфейса. Но так как особенности ''GLUT'' нас не интересуют, мы остановимся на работе с окном '''GlWindow'''.
в OpenGL не были включены функции для обработки сообщений
+
 
системы и взаимодействия с окнами. Вместе с тем, на практике
+
Мы напишем минимальную программу, использующую OpenGL и ''FLTK'', в которой окно-потомок '''GlWindow''' будет главным и единственным окном приложения (исходный текст программы вы найдете на диске в архиве '''ogldemo1''').
OpenGL-программы разрабатываются, в основном, в графических
+
 
многооконных средах, а значит, всем программистам нужен некий
+
<source lang=c>
минимум средств для взаимодействия между OpenGL и оконной
+
системой. Конечно, разработчики последних тоже не остались
+
в стороне. Для X Window была разработана система GLX, а для
+
Windows GDI – WGL (Wiggle), но эти расширения были довольно
+
сложны и несовместимы друг с другом. Свободную нишу запол-
+
нила разработанная Марком Килгардом [Mark J. Kilgard] библио-
+
тека GLUT, которая отличалась от GLX/WGL простотой использо-
+
вания и кроссплатформенностью (фактически, GLUT на каждой
+
платформе представляет собой надстройку над расширениями
+
конкретной системы). Неудивительно, что в то время многие про-
+
граммисты предпочитали GLUT для разработки надежным кросс-
+
платформенных программ.
+
    Учитывая популярность GLUT, разработчик FLTK Билл Спитцак
+
[Bill Spitzak] принял мудрое решение – добавить поддержку интер-
+
фейса GLUT в свой набор виджетов. В результате авторы про-
+
грамм, использовавшие GLUT, смогли без труда портировать свой
+
код на FLTK (отметим в скобках, что если вы начинаете писать
+
новую программу, нет никакого смысла использовать класс
+
GlutWindow, так как все то хорошее, что может дать вам библиоте-
+
ка GLUT, реализовано в классе GlWindow). Поскольку библиотека
+
GLUT не является открытым ПО (хотя исходные тексты доступны),
+
Спитцак создал модуль поддержки GLUT с нуля, сохранив совме-
+
стимость на уровне интерфейса. Но так как особенности GLUT нас
+
не интересуют, мы остановимся на работе с окном GlWindow.
+
    Мы напишем минимальную программу, использующую OpenGL
+
и FLTK, в которой окно-потомок GlWindow будет главным и един-
+
ственным окном приложения (исходный текст программы вы най-
+
дете на диске в архиве ogldemo1).
+
 
  #include <fltk/GlWindow.h>
 
  #include <fltk/GlWindow.h>
 
  using namespace fltk;
 
  using namespace fltk;
Строка 57: Строка 29:
 
     void draw();
 
     void draw();
 
  };
 
  };
    В объявлении класса окна мы переопределяем конструктор и
+
</source>
виртуальный метод draw(). Не спрашивайте меня, что он делает,
+
 
я сам скажу: draw() выполняет отрисовку сцены. Давайте посмот-
+
В объявлении класса окна мы переопределяем конструктор и виртуальный метод '''draw()'''. Не спрашивайте меня, что он делает, я сам скажу: '''draw()''' выполняет отрисовку сцены. Давайте посмотрим на реализацию методов:
рим на реализацию методов:
+
 
 +
<source lang=c>
 
  #include <fltk/gl.h>
 
  #include <fltk/gl.h>
 
  #include "MyGLWindow.h"
 
  #include "MyGLWindow.h"
Строка 82: Строка 55:
 
     glEnd();
 
     glEnd();
 
  }
 
  }
    Как уже отмечалось, одной из проблем вывода графики OpenGL
+
</source>
является необходимость изменять параметры матрицы проекти-
+
 
рования при изменении размеров окна. В GlWindow вы можете
+
Как уже отмечалось, одной из проблем вывода графики OpenGL является необходимость изменять параметры матрицы проектирования при изменении размеров окна. В '''GlWindow''' вы можете совместить настройку матрицы проектирования и код, формирующий изображение, в одном методе '''draw()''', благодаря свойству '''valid()''' класса '''GlWindow''' (о понятии свойства в ''FLTK'' говорилось в [[LXF113-114:FLTK|LXF113/114]]). Свойство '''valid()''' принимает значение '''0''', если окно только что создано, если его размеры были изменены или произошло переключение графических контекстов. После завершения вызова метода '''draw()''' свойство '''valid()''' принимает ненулевое значение. Таким образом, мы можем организовать проверку значения '''valid()''' в начале метода '''draw()'''. Если свойство равно '''0''', значит, требуется перенастроить матрицу проектирования, в противном случае мы можем сразу приступить к выводу изображения.
совместить настройку матрицы проектирования и код, формиру-
+
 
ющий изображение, в одном методе draw(), благодаря свойству
+
У класса '''GlWindow''' есть метод '''resize()''', объявленный в разделе '''protected''', который вызывается при их изменении размеров окна, так что у вас может возникнуть соблазн переопределить его и разместить в нем код перенастройки проектирования. Не делайте этого! В результате вы получите совсем не то, чего ожидали. Переопределение метода '''resize()''' может понадобиться только в том случае, если окну''' GlWindow''' приходится иметь дело с не-OpenGL
valid() класса GlWindow (о понятии свойства в FLTK говорилось
+
элементами, например, с дочерними виджетами. Не могу не отметить, что в наборе примеров ''FLTK Cheats'' (http://seriss.com/people/erco/fltk#OpenGlSimpleWidgets), которыми часто пользуются для изучения ''FLTK'', допущена ошибка – код перенастройки проектирования вызывается и в методе '''draw()''', и в методе '''resize()''' (и, кроме того, добавлен в конструктор окна). Ошибка незаметна, так как «правильный» код в методе '''draw()''' перекрывает неправильный, но повторять эту небрежность не следует.  
в LXF113/114). Свойство valid() принимает значение 0, если окно
+
 
только что создано, если его размеры были изменены или прои-
+
{{Врезка|Содержание= [[Изображение: LXF115_81_1.jpg|300px| Рис. 1]] Рис. 1. OpenGL в программе ''FLTK''.| Ширина=300px}}
зошло переключение графических контекстов. После завершения
+
 
вызова метода draw() свойство valid() принимает ненулевое значе-
+
Для компиляции программы воспользуемся командой
ние. Таким образом, мы можем организовать проверку значения
+
 
valid() в начале метода draw(). Если свойство равно 0, значит, тре-
+
буется перенастроить матрицу проектирования, в противном слу-
+
чае мы можем сразу приступить к выводу изображения.
+
    У класса GlWindow есть метод resize(), объявленный в разделе
+
protected, который вызывается при их изменении размеров окна,
+
так что у вас может возникнуть соблазн переопределить его и
+
разместить в нем код перенастройки проектирования. Не делай-
+
те этого! В результате вы получите совсем не то, чего ожидали.
+
Переопределение метода resize() может понадобиться только в том
+
случае, если окну GlWindow приходится иметь дело с не-OpenGL
+
элементами, например, с дочерними виджетами. Не могу не отме-
+
тить, что в наборе примеров FLTK Cheats (http://seriss.com/people/
+
erco/fltk/#OpenGlSimpleWidgets), которыми часто пользуются для
+
изучения FLTK, допущена ошибка – код перенастройки проектиро-
+
вания вызывается и в методе draw(), и в методе resize() (и, кроме
+
того, добавлен в конструктор окна). Ошибка незаметна, так как
+
«правильный» код в методе draw() перекрывает неправильный,
+
но повторять эту небрежность не следует.
+
    Для компиляции программы воспользуемся командой
+
 
  g++ MyGLWindow.cpp main.cpp -lfltk2 -lfltk2_gl -lGL -o ogldemo
 
  g++ MyGLWindow.cpp main.cpp -lfltk2 -lfltk2_gl -lGL -o ogldemo
    Обратите внимание, что кроме стандартной библиотеки
+
 
OpenGL нам требуется подключить к файлу программы библио-
+
Обратите внимание, что кроме стандартной библиотеки OpenGL нам требуется подключить к файлу программы библиотеку '''libfltk2_gl'''. Теперь мы можем наслаждаться зрелищем радужного треугольника (рис. 1), который вы, конечно, уже видели бессчетное количество раз.
теку libfltk2_gl. Теперь мы можем наслаждаться зрелищем радуж-
+
 
ного треугольника (рис. 1), который вы, конечно, уже видели бес-
+
 
счетное количество раз.
+
В заключение перечислим несколько полезных функций класса '''GlWindow'''. Свойство '''context()''' позволяет управлять контекстами OpenGL. Оно имеет тип '''GLContext''', который на платформе ''X'' соответствует типу ''GLXContext'', а в среде GDI – '''HGLRC'''. Благодаря '''context()''' вы можете вызывать напрямую функции оконных расширений OpenGL для данной платформы. С помощью метода '''mode()''' можно указать ряд параметров OpenGL, таких как использование альфа-канала, двойной буферизации, буфера трафарета и т.п. Метод '''ortho()''' настраивает матрицу проектирования таким образом, что начало системы координат OpenGL совпадает с нижним левым углом окна, а точка в координатах OpenGL соответствует одному пикселю экрана. Этот режим особенно удобен, когда OpenGL используется для работы с двумерными изображениями. Метод '''swap_buffers ()''' управляет переключением буферов OpenGL.
    В заключение перечислим несколько полезных функций клас-
+
 
са GlWindow. Свойство context() позволяет управлять контекста-
+
===Обработка событий===
ми OpenGL. Оно имеет тип GLContext, который на платформе X
+
 
соответствует типу GLXContext, а в среде GDI – HGLRC. Благодаря
+
Вы помните времена, когда программист MS-DOS, желающий добавить в свою программу такой простой элемент интерфейса, как кнопку, должен был выполнять все операции по ее отрисовке, используя единый цикл обработки сообщений программы? Прелесть концепции виджетов заключается в разделении обязанностей. Большую часть времени виджеты сами заботятся о себе (поддерживают свой внешний вид, изменяют размеры и положение в зависимости от геометрии окна) и беспокоят вашу программу только тогда, когда им действительно «есть, что сказать». Сообщения, которые виджеты посылают программе, можно разделить на две категории, или, точнее, на два уровня. Сообщения низкого уровня обычно связаны с действиями устройств ввода (нажата клавиша на клавиатуре, переместился указатель мыши);
context() вы можете вызывать напрямую функции оконных рас-
+
высокоуровневые же сообщения, как правило, отражают логику работы виджета. Сообщения высокого уровня часто основаны на событиях низкого уровня, но могут и не зависеть от них (виджет может сообщать о событии, связанном с внутренней работой программы, а не с внешним действием).
ширений OpenGL для данной платформы. С помощью метода
+
 
mode() можно указать ряд параметров OpenGL, таких как исполь-
+
На первый взгляд может показаться, что система виджетов должна предоставлять программисту средства обработки исключительно высокоуровневых сообщений, но, поскольку ни один, даже самый тщательно продуманный набор виджетов не может удовлетворить всех программистских запросов, следует предусмотреть и возможность обработки сообщений низкого уровня. Примером двухуровневой системы обработки сообщений может служить система событий и сигналов в ''Qt''. События ''Qt'' соответствуют сообщениям низкого уровня, тогда как сигналы отражают функциональность виджетов. В ''FLTK'' обработка сообщений низкого уровня выполняется с помощью механизма событий, а обработка сообщений высокого уровня, порожденных виджетами – с помощью функций обратного вызова.
зование альфа-канала, двойной буферизации, буфера трафарета
+
 
и т.п. Метод ortho() настраивает матрицу проектирования таким
+
Для обработки событий ''FLTK'' классы-потомки '''fltk::Widget''' используют метод '''handle()''', объявленный как
образом, что начало системы координат OpenGL совпадает с
+
 
нижним левым углом окна, а точка в координатах OpenGL соот-
+
<source lang=c>
ветствует одному пикселю экрана. Этот режим особенно удобен,
+
когда OpenGL используется для работы с двумерными изображе-
+
ниями. Метод swap_buffers () управляет переключением буферов
+
OpenGL.
+
Обработка событий
+
Вы помните времена, когда программист MS-DOS, желающий
+
добавить в свою программу такой простой элемент интерфейса,
+
как кнопку, должен был выполнять все операции по ее отрисов-
+
ке, используя единый цикл обработки сообщений программы?
+
Прелесть концепции виджетов заключается в разделении обязан-
+
ностей. Большую часть времени виджеты сами заботятся о себе
+
(поддерживают свой внешний вид, изменяют размеры и поло-
+
жение в зависимости от геометрии окна) и беспокоят вашу про-
+
грамму только тогда, когда им действительно «есть, что сказать».
+
Сообщения, которые виджеты посылают программе, можно раз-
+
делить на две категории, или, точнее, на два уровня. Сообщения
+
низкого уровня обычно связаны с действиями устройств ввода
+
(нажата клавиша на клавиатуре, переместился указатель мыши);
+
высокоуровневые же сообщения, как правило, отражают логику
+
работы виджета. Сообщения высокого уровня часто основаны на
+
событиях низкого уровня, но могут и не зависеть от них (виджет
+
может сообщать о событии, связанном с внутренней работой про-
+
граммы, а не с внешним действием).
+
    На первый взгляд может показаться, что система виджетов
+
должна предоставлять программисту средства обработки исклю-
+
чительно высокоуровневых сообщений, но, поскольку ни один,
+
даже самый тщательно продуманный набор виджетов не может
+
удовлетворить всех программистских запросов, следует преду-
+
смотреть и возможность обработки сообщений низкого уровня.
+
Примером двухуровневой системы обработки сообщений может
+
служить система событий и сигналов в Qt. События Qt соответ-
+
ствуют сообщениям низкого уровня, тогда как сигналы отражают
+
функциональность виджетов. В FLTK обработка сообщений низ-
+
кого уровня выполняется с помощью механизма событий, а обра-
+
ботка сообщений высокого уровня, порожденных виджетами – с
+
помощью функций обратного вызова.
+
      Для обработки событий FLTK классы-потомки fltk::Widget
+
используют метод handle(), объявленный как
+
 
  int Widget::handle( int event)
 
  int Widget::handle( int event)
    В параметре этого метода передается численный идентифи-
+
</source>
катор события. Метод должен вернуть ненулевое значение, если
+
 
событие было обработано корректно, и 0 в противном случае.
+
В параметре этого метода передается численный идентификатор события. Метод должен вернуть ненулевое значение, если событие было обработано корректно, и '''0''' в противном случае. Хотя метод '''handle()''' вызывается для обработки всех событий, связанных с виджетом, мы, как правило, хотим обрабатывать
Хотя метод handle() вызывается для обработки всех событий,
+
самостоятельно только некоторые события, возложив все прочее на систему. Шаблон перегруженного метода '''handle()''' можно представить так:
связанных с виджетом, мы, как правило, хотим обрабатывать
+
 
самостоятельно только некоторые события, возложив все про-
+
<source lang=c>
чее на систему. Шаблон перегруженного метода handle() можно
+
представить так:
+
 
  int MyWidget::handle(int event) {
 
  int MyWidget::handle(int event) {
 
   switch(event) {
 
   switch(event) {
Строка 183: Строка 97:
 
   }
 
   }
 
  }
 
  }
    Интересующие нас события перехватываются в теле оператора
+
</source>
switch(), а для обработки остальных событий мы вызываем метод
+
 
handle() базового класса. Каким образом с помощью одного чис-
+
Интересующие нас события перехватываются в теле оператора '''switch()''', а для обработки остальных событий мы вызываем метод '''handle()''' базового класса. Каким образом с помощью одного числового параметра метода '''handle()''' программе передается информация обо всем многообразии событий, на которые должен реагировать виджет? На самом деле параметр '''event''' содержит информацию только о типе события – остальные сведения обработчик получает с помощью вспомогательных функций. Объявления различных констант и функций, необходимых для обработки событий, содержатся в заголовочном файле '''fltk/events.h'''. Давайте рассмотрим механизмы обработки некоторых распространенных типов событий более подробно.
лового параметра метода handle() программе передается инфор-
+
 
мация обо всем многообразии событий, на которые должен реа-
+
Манипуляции с мышью порождают одно из пяти событий: '''ENTER''' – указатель мыши вошел в область виджета, '''LEAVE''' указатель покинул область виджета, '''PUSH''' – нажата одна из кнопок мыши, '''DRAG''' – указатель мыши перетаскивается при нажатой кнопке (это событие генерируется периодически, до тех пор, пока кнопка не будет отпущена), '''RELEASE''' – кнопка отпущена. Код кнопки мыши, вызвавшей событие, можно получить с помощью функции '''event_key()''': значения 1, 2, 3 обозначают левую, среднюю и правую кнопки, соответственно. Положение указателя в момент возникновения события можно выяснить с помощью функций '''event_x()''' и '''event_y()'''. Любопытно отметить, как ''FLTK'' сигнализирует о прокрутке колесика мыши. Прокрутка колесика порождает
гировать виджет? На самом деле параметр event содержит инфор-
+
серию событий '''MOUSEWHEEL'''. Функция '''event_dy()''' возвращает количество единиц прокрутки (положительное значение для прокрутки вверх и отрицательное – для прокрутки вниз). Если прокрутка сопровождается удерживанием средней кнопки мыши, помимо события '''MOUSEWHEEL''' генерируется серия событий
мацию только о типе события – остальные сведения обработчик
+
'''RELEASE''' (без парных им сообщений '''PUSH'''). Функция '''event_key()''' при этом возвращает значение '''4''' (прокрутка вверх) или '''5''' (прокрутка вниз).
получает с помощью вспомогательных функций. Объявления
+
 
различных констант и функций, необходимых для обработки
+
Нажатие клавиши на клавиатуре порождает события '''KEY''' (клавиша нажата) и '''KEYUP''' (клавиша отпущена). Функция '''event_key()''' позволяет получить код клавиши (она работает для любой клавиши клавиатуры), а функция '''event_text()''' – код символа (для символьной клавиши). Значение, возвращаемое '''event_text()''', зависит от настроек локали и выбранной раскладки клавиатуры. С помощью функции '''event_key()''' можно связывать специальные действия с несимвольными клавишами. Кроме того, эта функция удобна, когда некоторое действие должно выполняться при нажатии на символьную клавишу независимо от выбранной раскладки клавиатуры (меня, например, бесят программы, в которых сочетания клавиш '''Ctrl+C, Ctrl+V''' и '''Ctrl+Z''' перестают работать при
событий, содержатся в заголовочном файле fltk/events.h. Давайте
+
переключении на русскую раскладку). Для многих кодов клавиш, возвращаемых функцией '''event_key()''', определены константымнемоники, например, '''EscapeKey, HomeKey, LeftKey, UpKey, RightKey, DownKey, PageUpKey, PageDownKey, EndKey, PrintKey'''.
рассмотрим механизмы обработки некоторых распространенных
+
 
типов событий более подробно.
+
Если нажать и удерживать клавишу на клавиатуре, генерируется серия событий '''KEY''' без соответствующих им событий '''KEYUP'''. В ходе своих экспериментов с обработкой событий ''FLTK'' я обнаружил одну странность: событие '''KEYUP''' генерируется не тогда, когда ранее нажатая клавиша отпущена, а в момент нажатия следующей клавиши (сразу за событием '''KEYUP''' генерируется событие '''KEY''', соответствующее нажатию новой клавиши). Не думаю, что разработчикам следует полагаться на своевременность события '''KEYUP''' в ''FLTK'' (в некоторых ситуациях это событие вообще может не случиться).
  Манипуляции с мышью порождают одно из пяти событий:
+
 
ENTER – указатель мыши вошел в область виджета, LEAVE – ука-
+
Любопытно отметить, что функции '''event_key()''', '''event_x()''' и им подобные не являются методами классов виджетов. Это самостоятельные функции, которые получают информацию о параметрах события из статических переменных, спрятанных в недрах ''FLTK''. Такой подход нельзя назвать особо элегантным с точки зрения объектно-ориентированного программирования. Кроме того, поскольку функции «не знают», для какого события они вызваны, обработка событий возможна строго в порядке их поступления.
затель покинул область виджета, PUSH – нажата одна из кнопок
+
 
мыши, DRAG – указатель мыши перетаскивается при нажатой
+
Хотя обычно источником событий являются устройства ввода, их можно генерировать и программно. Для этого служит метод '''send()''' класса '''fltk::Widget'''. Единственным аргументом метода должен быть численный идентификатор события. Метод '''send()''' представляет собой, по сути, обертку вокруг метода '''handle()''', однако перед тем как вызвать обработчик событий, '''send()''' выполняет некоторые полезные действия, например, сохраняет координаты '''x'''
кнопке (это событие генерируется периодически, до тех пор, пока
+
и '''y''' для событий, связанных с мышью. А что делать, если вы хотите эмулировать не только событие, но и его параметры, например, указать собственные координаты мыши? Для этого придется воспользоваться недокументированной возможностью – напрямую обратиться к тем самым статическим переменным, в которых
кнопка не будет отпущена), RELEASE – кнопка отпущена. Код
+
сохраняются параметры события. Имена переменных начинаются с префикса '''e_''', и их можно найти в файле '''fltk/events.h'''. Например, координаты указателя мыши хранятся в переменных '''e_x''' и '''e_y'''.
кнопки мыши, вызвавшей событие, можно получить с помощью
+
 
функции event_key(): значения 1, 2, 3 обозначают левую, среднюю
+
Вы можете установить глобальный обработчик для всех событий, которые не смогли обработать виджеты ''FLTK'' (необработанными считаются события, для которых метод '''handle()''' вернул значение '''0'''). Заголовок функции обработчика должен иметь вид
и правую кнопки, соответственно. Положение указателя в момент
+
 
возникновения события можно выяснить с помощью функций
+
event_x() и event_y(). Любопытно отметить, как FLTK сигнализи-
+
рует о прокрутке колесика мыши. Прокрутка колесика порождает
+
серию событий MOUSEWHEEL. Функция event_dy() возвращает
+
количество единиц прокрутки (положительное значение для про-
+
крутки вверх и отрицательное – для прокрутки вниз). Если про-
+
крутка сопровождается удерживанием средней кнопки мыши,
+
помимо события MOUSEWHEEL генерируется серия событий
+
RELEASE (без парных им сообщений PUSH). Функция event_key()
+
при этом возвращает значение 4 (прокрутка вверх) или 5 (про-
+
крутка вниз).
+
  Нажатие клавиши на клавиатуре порождает события KEY (кла-
+
виша нажата) и KEYUP (клавиша отпущена). Функция event_key()
+
позволяет получить код клавиши (она работает для любой клави-
+
ши клавиатуры), а функция event_text() – код символа (для сим-
+
вольной клавиши). Значение, возвращаемое event_text(), зави-
+
сит от настроек локали и выбранной раскладки клавиатуры.
+
С помощью функции event_key() можно связывать специальные
+
действия с несимвольными клавишами. Кроме того, эта функ-
+
ция удобна, когда некоторое действие должно выполняться при
+
нажатии на символьную клавишу независимо от выбранной рас-
+
кладки клавиатуры (меня, например, бесят программы, в которых
+
сочетания клавиш Ctrl+C, Ctrl+V и Ctrl+Z перестают работать при
+
переключении на русскую раскладку). Для многих кодов клавиш,
+
возвращаемых функцией event_key(), определены константы-
+
мнемоники, например, EscapeKey, HomeKey, LeftKey, UpKey,
+
RightKey, DownKey, PageUpKey, PageDownKey, EndKey, PrintKey.
+
    Если нажать и удерживать клавишу на клавиатуре, генериру-
+
ется серия событий KEY без соответствующих им событий KEYUP.
+
В ходе своих экспериментов с обработкой событий FLTK я обнару-
+
жил одну странность: событие KEYUP генерируется не тогда, ког-
+
да ранее нажатая клавиша отпущена, а в момент нажатия следую-
+
щей клавиши (сразу за событием KEYUP генерируется событие
+
KEY, соответствующее нажатию новой клавиши). Не думаю, что
+
разработчикам следует полагаться на своевременность события
+
KEYUP в FLTK (в некоторых ситуациях это событие вообще может
+
не случиться).
+
    Любопытно отметить, что функции event_key(), event_x() и им
+
подобные не являются методами классов виджетов. Это само-
+
стоятельные функции, которые получают информацию о пара-
+
метрах события из статических переменных, спрятанных в недрах
+
FLTK. Такой подход нельзя назвать особо элегантным с точки зре-
+
ния объектно-ориентированного программирования. Кроме того,
+
поскольку функции «не знают», для какого события они вызваны,
+
обработка событий возможна строго в порядке их поступления.
+
      Хотя обычно источником событий являются устройства ввода,
+
их можно генерировать и программно. Для этого служит метод
+
send() класса fltk::Widget. Единственным аргументом метода дол-
+
жен быть численный идентификатор события. Метод send() пред-
+
ставляет собой, по сути, обертку вокруг метода handle(), однако
+
перед тем как вызвать обработчик событий, send() выполняет
+
некоторые полезные действия, например, сохраняет координаты x
+
и y для событий, связанных с мышью. А что делать, если вы хоти-
+
те эмулировать не только событие, но и его параметры, например,
+
указать собственные координаты мыши? Для этого придется вос-
+
пользоваться недокументированной возможностью – напрямую
+
обратиться к тем самым статическим переменным, в которых
+
сохраняются параметры события. Имена переменных начинаются
+
с префикса e_, и их можно найти в файле fltk/events.h. Например,
+
координаты указателя мыши хранятся в переменных e_x и e_y.
+
      Вы можете установить глобальный обработчик для всех собы-
+
тий, которые не смогли обработать виджеты FLTK (необработан-
+
ными считаются события, для которых метод handle() вернул зна-
+
чение 0). Заголовок функции обработчика должен иметь вид
+
 
  int handler_name(int event, fltk::Window * window).
 
  int handler_name(int event, fltk::Window * window).
      В параметре event обработчику передается идентификатор
+
 
события, а в параметре window – указатель на объект-окно,
+
В параметре '''event''' обработчику передается идентификатор события, а в параметре '''window''' – указатель на объект-окно, которому оно предназначалось (поскольку речь идет о необработанных событиях, система не всегда может определить окнополучателя). Установка обработчика выполняется с помощью функции '''add_event_handler()'''.
которому оно предназначалось (поскольку речь идет о необра-
+
 
ботанных событиях, система не всегда может определить окно-
+
===Живой OpenGL===
получателя). Установка обработчика выполняется с помощью
+
 
функции add_event_handler().
+
Чтобы продемонстрировать обработку событий ''FLTK'' на практике, мы добавим в нашу программу элемент интерактивности (новый вариант вы найдете в архиве '''ogdemo2'''). Пользователь сможет перетаскивать треугольник в окне, «ухватившись» за него мышью. Для этого добавим в класс '''MyGLWindow''' метод '''handle()''' и несколько вспомогательных полей:
Живой OpenGL
+
 
Чтобы продемонстрировать обработку событий FLTK на прак-
+
<source lang=c>
тике, мы добавим в нашу программу элемент интерактивности
+
(новый вариант вы найдете в архиве ogdemo2). Пользователь
+
сможет перетаскивать треугольник в окне, «ухватившись» за него
+
мышью. Для этого добавим в класс MyGLWindow метод handle() и
+
несколько вспомогательных полей:
+
 
   class MyGLWindow : public GlWindow {
 
   class MyGLWindow : public GlWindow {
 
   public:
 
   public:
       MyGLWindow(int X, int Y, int W,int H, const char* L=0);
+
       MyGLWindow(int X, int Y, int W,int H, const char* L=0); private:
  private:
+
       int x1, y1, x2, y2, x3, y3, oldX, oldY;
       int x1, y1, x2, y2, x3,
+
  y3, oldX, oldY;
+
 
       bool moving, isFullScreen;
 
       bool moving, isFullScreen;
 
       void draw();
 
       void draw();
 
       int handle(int event);
 
       int handle(int event);
 
   };
 
   };
      Реализация метода handle() следует описанной выше схеме:
+
 
 +
Реализация метода '''handle()''' следует описанной выше схеме:
 +
 
 +
<source lang=c>
 
   int MyGLWindow::handle(int event) {
 
   int MyGLWindow::handle(int event) {
 
     switch(event) {
 
     switch(event) {
Строка 298: Строка 144:
 
       if (event_key() == 1) {
 
       if (event_key() == 1) {
 
           unsigned int pixel[3] = {0,0,0};
 
           unsigned int pixel[3] = {0,0,0};
           glReadPixels(event_x(), h() - event_y(), 1, 1, GL_RGB, GL_
+
           glReadPixels(event_x(), h() - event_y(), 1, 1, GL_RGB, GL_UNSIGNED_INT, &pixel);
UNSIGNED_INT, &pixel);
+
 
           if ((pixel[0] + pixel[1] + pixel[2]) != 0) {
 
           if ((pixel[0] + pixel[1] + pixel[2]) != 0) {
 
             moving = true;
 
             moving = true;
Строка 337: Строка 182:
 
   return GlWindow::handle(event);
 
   return GlWindow::handle(event);
 
  }
 
  }
    Я намеренно не останавливаюсь на особенностях работы
+
</source>
OpenGL в данной программе – на эту тему можно было бы напи-
+
 
сать отдельную статью. Мы обрабатываем события мыши PUSH,
+
Я намеренно не останавливаюсь на особенностях работы OpenGL в данной программе – на эту тему можно было бы написать отдельную статью. Мы обрабатываем события мыши '''PUSH, DRAG''' и '''RELEASE'''. Кроме них, в нашем методе '''handle()''' обрабатываются события клавиатуры: нажатие на клавишу '''Esc''' приводит к завершению работы программы, а кнопка '''F''' переключает ее между полноэкранным и оконным режимами, для чего используются методы '''fullscreen()''' и '''fullscreen_off()'''. Они реализованы в классе '''Window''', а не '''GlWindow''', но, как вы понимаете, при работе с трехмерной графикой они особенно полезны. Обратите внимание, что для идентификации клавиши '''F''' мы пользуемся значением функции '''event_key()''', то есть эта клавиша будет работать независимо от раскладки клавиатуры и состояния '''CapsLock'''.
DRAG и RELEASE. Кроме них, в нашем методе handle() обрабаты-
+
 
ваются события клавиатуры: нажатие на клавишу Esc приводит к
+
===Функции обратного вызова===
завершению работы программы, а кнопка F переключает ее между
+
 
полноэкранным и оконным режимами, для чего используются
+
Рассмотрим теперь механизм обработки событий высокого уровня. Как было сказано выше, для передачи сообщений программе виджеты ''FLTK'' используют функции обратного вызова. Попросту говоря, вы можете указать виджету ''FLTK'' функцию, которую следует вызвать тогда, когда с ним произойдет нечто, достойное
методы fullscreen() и fullscreen_off(). Они реализованы в классе
+
внимания программы. Для каждого виджета можно зарегистрировать только одну такую функцию. Это связано с убеждением разработчика ''FLTK'' в том, что каждый виджет может создавать только одно «интересное» событие. Интерфейс функций обратного вызова ''FLTK'' прост настолько, насколько это возможно: все функции обратного вызова имеют заголовок вида
Window, а не GlWindow, но, как вы понимаете, при работе с трех-
+
 
мерной графикой они особенно полезны. Обратите внимание, что
+
для идентификации клавиши F мы пользуемся значением функ-
+
ции event_key(), то есть эта клавиша будет работать независимо
+
от раскладки клавиатуры и состояния CapsLock.
+
Функции обратного вызова
+
Рассмотрим теперь механизм обработки событий высокого уров-
+
ня. Как было сказано выше, для передачи сообщений программе
+
виджеты FLTK используют функции обратного вызова. Попросту
+
говоря, вы можете указать виджету FLTK функцию, которую сле-
+
дует вызвать тогда, когда с ним произойдет нечто, достойное
+
внимания программы. Для каждого виджета можно зарегистри-
+
ровать только одну такую функцию. Это связано с убеждением
+
разработчика FLTK в том, что каждый виджет может создавать
+
только одно «интересное» событие. Интерфейс функций обрат-
+
ного вызова FLTK прост настолько, насколько это возможно: все
+
функции обратного вызова имеют заголовок вида
+
 
  void callback_fn(Widget *, void *)
 
  void callback_fn(Widget *, void *)
В первом параметре функции передается указатель на объект-
+
 
виджет, породивший событие, второй параметр представляет
+
В первом параметре функции передается указатель на объект-виджет, породивший событие, второй параметр представляет собой указатель на произвольный блок данных, определенный программистом. Установить его можно с помощью метода '''user_data()''', которым обладает каждый класс-потомок '''fltk::Widget'''.
собой указатель на произвольный блок данных, определен-
+
Для регистрации функции обратного вызова используется метод '''callback()''', который, опять же, есть у каждого класса, реализующего виджет.
ный программистом. Установить его можно с помощью метода
+
 
user_data(), которым обладает каждый класс-потомок fltk::Widget.
+
Вот, собственно, и все. Как вы можете видеть, функция обратного вызова не возвращает никаких значений. Дополнительные сведения, необходимые для обработки события, можно получить с помощью свойств виджета, вызвавшего функцию, а также с помощью тех функций, которыми мы пользовались для обработки событий низкого уровня. В частности, функция '''event()''', объявленная в файле '''fltk/events.h''', позволяет узнать, какое именно низкоуровневое событие заставило виджет сделать обратный вызов. В интерактивной программе OpenGL я добавил функцию обратного вызова для главного окна программы. Оно вызывает ее в одном-единственном случае – когда пользователь пытается закрыть это окно с помощью кнопки ['''x'''] в его заголовке. Сама функция обратного вызова выглядит просто:
Для регистрации функции обратного вызова используется метод
+
 
callback(), который, опять же, есть у каждого класса, реализую-
+
{{Врезка|Содержание= [[Изображение: LXF115_83_1.jpg|300px| Рис. 2]] Рис. 2. Модальное диалоговое окно ''FLTK''. | Ширина=300px}}
щего виджет.
+
 
    Вот, собственно, и все. Как вы можете видеть, функция обрат-
+
<source lang=c>
ного вызова не возвращает никаких значений. Дополнительные
+
сведения, необходимые для обработки события, можно получить
+
с помощью свойств виджета, вызвавшего функцию, а также с
+
помощью тех функций, которыми мы пользовались для обработ-
+
ки событий низкого уровня. В частности, функция event(), объ-
+
явленная в файле fltk/events.h, позволяет узнать, какое именно
+
низкоуровневое событие заставило виджет сделать обратный
+
вызов. В интерактивной программе OpenGL я добавил функцию
+
обратного вызова для главного окна программы. Оно вызывает
+
ее в одном-единственном случае – когда пользователь пытается
+
закрыть это окно с помощью кнопки [x] в его заголовке. Сама
+
функция обратного вызова выглядит просто:
+
 
  void exit_callback(Widget* widget, void*) {
 
  void exit_callback(Widget* widget, void*) {
 
   if (ask("Вы действительно хотите выйти?"))
 
   if (ask("Вы действительно хотите выйти?"))
 
     ((MyGLWindow*)widget)->hide();
 
     ((MyGLWindow*)widget)->hide();
 
  }
 
  }
    Функция ask() выводит на экран модальное диалоговое окно с
+
</source>
кнопками Yes и No (рис. 2).
+
 
    Возвращаемое функцией значение соответствует нажатой
+
Функция ask() выводит на экран модальное диалоговое окно с кнопками '''Yes''' и '''No''' (рис. 2).
кнопке. Если пользователь нажал Yes, мы закрываем главное
+
 
окно программы с помощью его метода hide(), что приводит к
+
Возвращаемое функцией значение соответствует нажатой кнопке. Если пользователь нажал '''Yes''', мы закрываем главное окно программы с помощью его метода '''hide()''', что приводит к завершению работы всей программы.
завершению работы всей программы.
+
 
    Последнее, что нам осталось сделать – зарегистрировать
+
Последнее, что нам осталось сделать – зарегистрировать функции обратного вызова в функции '''main()''':
функции обратного вызова в функции main():
+
 
 +
<source lang=c>
 
  MyGLWindow win(0, 0, 500, 300, "OpenGL Test App");
 
  MyGLWindow win(0, 0, 500, 300, "OpenGL Test App");
 
  win.callback(exit_callback);
 
  win.callback(exit_callback);
    Возможно, библиотека FLTK – не лучший выбор для соз-
+
</source>
дания больших и сложных приложений, но она хорошо под-
+
 
ходит для создания небольших программ, например, «демок»
+
Возможно, библиотека ''FLTK'' – не лучший выбор для создания больших и сложных приложений, но она хорошо подходит для создания небольших программ, например, «демок» OpenGL. Возможно также, что опыт ''FLTK'' пригодится вам, если когда-нибудь вы захотите написать собственный набор виджетов. '''LXF'''
OpenGL. Возможно также, что опыт FLTK пригодится вам, если
+
когда-нибудь вы захотите написать собственный набор вид-
+
жетов. LXF
+

Текущая версия на 14:26, 21 января 2010

Программируем с FLTK Быстрый, легкий, поддерживающий OpenGL: выберите любые три!


Содержание

[править] Сверхскоростная графика

ЧАСТЬ 2 Пусть Qt и GTK+ лучше подходят для сложных приложений – FLTK блистает там, где интерфейс должен быть незаметным: например, в «демках» OpenGL. Андрей Боровский напишет одну такую.

Какое звено является ведущим в связке потребностей и технологий? Я думаю, что все-таки потребности. Развитие графических адаптеров с миллионами поддерживаемых цветов и пикселей стимулировалось парадигмой WYSIWYG, а не наоборот, а ускорители 3D-графики для ПК появились благодаря трехмерным играм (первые из которых обходились вовсе без ускорителей). А вот внедрение в работающую схему новых технологий по принципу «зачем добру пропадать» редко приносит хорошие результаты. Вот, например, все современные рабочие столы обзавелись трехмерными «примочками» – а часто ли мы ими пользуемся? Тем не менее, раз уж OpenGL распространяется повсюду, то и обзор библиотеки виджетов не может без него обойтись.

[править] OpenGL в FLTK

Как уже отмечалось, поддержка OpenGL была в свое время уникальной и крайне привлекательной чертой FLTK, и даже сейчас с некоторыми проблемами вывода трехмерной графики эта библиотека справляется лучше, нежели другие наборы виджетов. Для работы с OpenGL FLTK предлагает нам два класса: GlWindow и GlutWindow. Как нетрудно догадаться, они наследуют Window и реализуют специальные типы окон, у которых рабочая поверхность подготовлена для вывода графики OpenGL. В остальном, окна GlWindow и GlutWindow подобны окну Window – они могут содержать дочерние виджеты и обрабатывать сообщения, адресованные главному окну программы. Окно GlWindow предоставляет базовую функциональность, необходимую для работы с OpenGL, а окно GlutWindow вдобавок эмулирует функции библиотеки GLUT.

Если вы интересуетесь программированием с OpenGL, то наверняка уже знаете, что такое GLUT, и тем не менее я это поясню. Интерфейс OpenGL разрабатывался как максимально платформо-независимый. Выразилось это, например, в том, что в OpenGL не были включены функции для обработки сообщений системы и взаимодействия с окнами. Вместе с тем, на практике OpenGL-программы разрабатываются, в основном, в графических многооконных средах, а значит, всем программистам нужен некий минимум средств для взаимодействия между OpenGL и оконной системой. Конечно, разработчики последних тоже не остались в стороне. Для X Window была разработана система GLX, а для Windows GDI – WGL (Wiggle), но эти расширения были довольно сложны и несовместимы друг с другом. Свободную нишу заполнила разработанная Марком Килгардом [Mark J. Kilgard] библиотека GLUT, которая отличалась от GLX/WGL простотой использования и кроссплатформенностью (фактически, GLUT на каждой платформе представляет собой надстройку над расширениями конкретной системы). Неудивительно, что в то время многие программисты предпочитали GLUT для разработки надежным кроссплатформенных программ.

Учитывая популярность GLUT, разработчик FLTK Билл Спитцак [Bill Spitzak] принял мудрое решение – добавить поддержку интерфейса GLUT в свой набор виджетов. В результате авторы программ, использовавшие GLUT, смогли без труда портировать свой код на FLTK (отметим в скобках, что если вы начинаете писать новую программу, нет никакого смысла использовать класс GlutWindow, так как все то хорошее, что может дать вам библиотека GLUT, реализовано в классе GlWindow). Поскольку библиотека GLUT не является открытым ПО (хотя исходные тексты доступны), Спитцак создал модуль поддержки GLUT с нуля, сохранив совместимость на уровне интерфейса. Но так как особенности GLUT нас не интересуют, мы остановимся на работе с окном GlWindow.

Мы напишем минимальную программу, использующую OpenGL и FLTK, в которой окно-потомок GlWindow будет главным и единственным окном приложения (исходный текст программы вы найдете на диске в архиве ogldemo1).

 #include <fltk/GlWindow.h>
 using namespace fltk;
 class MyGLWindow : public GlWindow {
 public:
     MyGLWindow(int X, int Y, int W,int H, const char* L=0);
 private:
     void draw();
 };

В объявлении класса окна мы переопределяем конструктор и виртуальный метод draw(). Не спрашивайте меня, что он делает, я сам скажу: draw() выполняет отрисовку сцены. Давайте посмотрим на реализацию методов:

 #include <fltk/gl.h>
 #include "MyGLWindow.h"
 MyGLWindow::MyGLWindow(int X,int Y,int W,int H,const char*L) :
 GlWindow(X,Y,W,H,L) {
 }
 void MyGLWindow::draw() {
      if (!valid()) {
         glLoadIdentity();
         glViewport(0,0,w(),h());
         glOrtho(-w(),w(),-h(),h(),-1,1);
      }
     glClear(GL_COLOR_BUFFER_BIT);
     glBegin(GL_TRIANGLES);
     glColor3f(1.0f,0.0f,0.0f);
     glVertex2f(w() - 10, h() - 10);
     glColor3f(0.0f,1.0f,0.0f);
     glVertex2f(10 -w() , 10 -h());
     glColor3f(0.0f,0.0f,1.0f);
     glVertex2f(10 -w() , h() - 10);
     glEnd();
 }

Как уже отмечалось, одной из проблем вывода графики OpenGL является необходимость изменять параметры матрицы проектирования при изменении размеров окна. В GlWindow вы можете совместить настройку матрицы проектирования и код, формирующий изображение, в одном методе draw(), благодаря свойству valid() класса GlWindow (о понятии свойства в FLTK говорилось в LXF113/114). Свойство valid() принимает значение 0, если окно только что создано, если его размеры были изменены или произошло переключение графических контекстов. После завершения вызова метода draw() свойство valid() принимает ненулевое значение. Таким образом, мы можем организовать проверку значения valid() в начале метода draw(). Если свойство равно 0, значит, требуется перенастроить матрицу проектирования, в противном случае мы можем сразу приступить к выводу изображения.

У класса GlWindow есть метод resize(), объявленный в разделе protected, который вызывается при их изменении размеров окна, так что у вас может возникнуть соблазн переопределить его и разместить в нем код перенастройки проектирования. Не делайте этого! В результате вы получите совсем не то, чего ожидали. Переопределение метода resize() может понадобиться только в том случае, если окну GlWindow приходится иметь дело с не-OpenGL элементами, например, с дочерними виджетами. Не могу не отметить, что в наборе примеров FLTK Cheats (http://seriss.com/people/erco/fltk#OpenGlSimpleWidgets), которыми часто пользуются для изучения FLTK, допущена ошибка – код перенастройки проектирования вызывается и в методе draw(), и в методе resize() (и, кроме того, добавлен в конструктор окна). Ошибка незаметна, так как «правильный» код в методе draw() перекрывает неправильный, но повторять эту небрежность не следует.


Для компиляции программы воспользуемся командой

g++ MyGLWindow.cpp main.cpp -lfltk2 -lfltk2_gl -lGL -o ogldemo

Обратите внимание, что кроме стандартной библиотеки OpenGL нам требуется подключить к файлу программы библиотеку libfltk2_gl. Теперь мы можем наслаждаться зрелищем радужного треугольника (рис. 1), который вы, конечно, уже видели бессчетное количество раз.


В заключение перечислим несколько полезных функций класса GlWindow. Свойство context() позволяет управлять контекстами OpenGL. Оно имеет тип GLContext, который на платформе X соответствует типу GLXContext, а в среде GDI – HGLRC. Благодаря context() вы можете вызывать напрямую функции оконных расширений OpenGL для данной платформы. С помощью метода mode() можно указать ряд параметров OpenGL, таких как использование альфа-канала, двойной буферизации, буфера трафарета и т.п. Метод ortho() настраивает матрицу проектирования таким образом, что начало системы координат OpenGL совпадает с нижним левым углом окна, а точка в координатах OpenGL соответствует одному пикселю экрана. Этот режим особенно удобен, когда OpenGL используется для работы с двумерными изображениями. Метод swap_buffers () управляет переключением буферов OpenGL.

[править] Обработка событий

Вы помните времена, когда программист MS-DOS, желающий добавить в свою программу такой простой элемент интерфейса, как кнопку, должен был выполнять все операции по ее отрисовке, используя единый цикл обработки сообщений программы? Прелесть концепции виджетов заключается в разделении обязанностей. Большую часть времени виджеты сами заботятся о себе (поддерживают свой внешний вид, изменяют размеры и положение в зависимости от геометрии окна) и беспокоят вашу программу только тогда, когда им действительно «есть, что сказать». Сообщения, которые виджеты посылают программе, можно разделить на две категории, или, точнее, на два уровня. Сообщения низкого уровня обычно связаны с действиями устройств ввода (нажата клавиша на клавиатуре, переместился указатель мыши); высокоуровневые же сообщения, как правило, отражают логику работы виджета. Сообщения высокого уровня часто основаны на событиях низкого уровня, но могут и не зависеть от них (виджет может сообщать о событии, связанном с внутренней работой программы, а не с внешним действием).

На первый взгляд может показаться, что система виджетов должна предоставлять программисту средства обработки исключительно высокоуровневых сообщений, но, поскольку ни один, даже самый тщательно продуманный набор виджетов не может удовлетворить всех программистских запросов, следует предусмотреть и возможность обработки сообщений низкого уровня. Примером двухуровневой системы обработки сообщений может служить система событий и сигналов в Qt. События Qt соответствуют сообщениям низкого уровня, тогда как сигналы отражают функциональность виджетов. В FLTK обработка сообщений низкого уровня выполняется с помощью механизма событий, а обработка сообщений высокого уровня, порожденных виджетами – с помощью функций обратного вызова.

Для обработки событий FLTK классы-потомки fltk::Widget используют метод handle(), объявленный как

 int Widget::handle( int event)

В параметре этого метода передается численный идентификатор события. Метод должен вернуть ненулевое значение, если событие было обработано корректно, и 0 в противном случае. Хотя метод handle() вызывается для обработки всех событий, связанных с виджетом, мы, как правило, хотим обрабатывать самостоятельно только некоторые события, возложив все прочее на систему. Шаблон перегруженного метода handle() можно представить так:

 int MyWidget::handle(int event) {
   switch(event) {
   ...
   default:
     return BaseWidget::handle(event);
   }
 }

Интересующие нас события перехватываются в теле оператора switch(), а для обработки остальных событий мы вызываем метод handle() базового класса. Каким образом с помощью одного числового параметра метода handle() программе передается информация обо всем многообразии событий, на которые должен реагировать виджет? На самом деле параметр event содержит информацию только о типе события – остальные сведения обработчик получает с помощью вспомогательных функций. Объявления различных констант и функций, необходимых для обработки событий, содержатся в заголовочном файле fltk/events.h. Давайте рассмотрим механизмы обработки некоторых распространенных типов событий более подробно.

Манипуляции с мышью порождают одно из пяти событий: ENTER – указатель мыши вошел в область виджета, LEAVE – указатель покинул область виджета, PUSH – нажата одна из кнопок мыши, DRAG – указатель мыши перетаскивается при нажатой кнопке (это событие генерируется периодически, до тех пор, пока кнопка не будет отпущена), RELEASE – кнопка отпущена. Код кнопки мыши, вызвавшей событие, можно получить с помощью функции event_key(): значения 1, 2, 3 обозначают левую, среднюю и правую кнопки, соответственно. Положение указателя в момент возникновения события можно выяснить с помощью функций event_x() и event_y(). Любопытно отметить, как FLTK сигнализирует о прокрутке колесика мыши. Прокрутка колесика порождает серию событий MOUSEWHEEL. Функция event_dy() возвращает количество единиц прокрутки (положительное значение для прокрутки вверх и отрицательное – для прокрутки вниз). Если прокрутка сопровождается удерживанием средней кнопки мыши, помимо события MOUSEWHEEL генерируется серия событий RELEASE (без парных им сообщений PUSH). Функция event_key() при этом возвращает значение 4 (прокрутка вверх) или 5 (прокрутка вниз).

Нажатие клавиши на клавиатуре порождает события KEY (клавиша нажата) и KEYUP (клавиша отпущена). Функция event_key() позволяет получить код клавиши (она работает для любой клавиши клавиатуры), а функция event_text() – код символа (для символьной клавиши). Значение, возвращаемое event_text(), зависит от настроек локали и выбранной раскладки клавиатуры. С помощью функции event_key() можно связывать специальные действия с несимвольными клавишами. Кроме того, эта функция удобна, когда некоторое действие должно выполняться при нажатии на символьную клавишу независимо от выбранной раскладки клавиатуры (меня, например, бесят программы, в которых сочетания клавиш Ctrl+C, Ctrl+V и Ctrl+Z перестают работать при переключении на русскую раскладку). Для многих кодов клавиш, возвращаемых функцией event_key(), определены константымнемоники, например, EscapeKey, HomeKey, LeftKey, UpKey, RightKey, DownKey, PageUpKey, PageDownKey, EndKey, PrintKey.

Если нажать и удерживать клавишу на клавиатуре, генерируется серия событий KEY без соответствующих им событий KEYUP. В ходе своих экспериментов с обработкой событий FLTK я обнаружил одну странность: событие KEYUP генерируется не тогда, когда ранее нажатая клавиша отпущена, а в момент нажатия следующей клавиши (сразу за событием KEYUP генерируется событие KEY, соответствующее нажатию новой клавиши). Не думаю, что разработчикам следует полагаться на своевременность события KEYUP в FLTK (в некоторых ситуациях это событие вообще может не случиться).

Любопытно отметить, что функции event_key(), event_x() и им подобные не являются методами классов виджетов. Это самостоятельные функции, которые получают информацию о параметрах события из статических переменных, спрятанных в недрах FLTK. Такой подход нельзя назвать особо элегантным с точки зрения объектно-ориентированного программирования. Кроме того, поскольку функции «не знают», для какого события они вызваны, обработка событий возможна строго в порядке их поступления.

Хотя обычно источником событий являются устройства ввода, их можно генерировать и программно. Для этого служит метод send() класса fltk::Widget. Единственным аргументом метода должен быть численный идентификатор события. Метод send() представляет собой, по сути, обертку вокруг метода handle(), однако перед тем как вызвать обработчик событий, send() выполняет некоторые полезные действия, например, сохраняет координаты x и y для событий, связанных с мышью. А что делать, если вы хотите эмулировать не только событие, но и его параметры, например, указать собственные координаты мыши? Для этого придется воспользоваться недокументированной возможностью – напрямую обратиться к тем самым статическим переменным, в которых сохраняются параметры события. Имена переменных начинаются с префикса e_, и их можно найти в файле fltk/events.h. Например, координаты указателя мыши хранятся в переменных e_x и e_y.

Вы можете установить глобальный обработчик для всех событий, которые не смогли обработать виджеты FLTK (необработанными считаются события, для которых метод handle() вернул значение 0). Заголовок функции обработчика должен иметь вид

int handler_name(int event, fltk::Window * window).

В параметре event обработчику передается идентификатор события, а в параметре window – указатель на объект-окно, которому оно предназначалось (поскольку речь идет о необработанных событиях, система не всегда может определить окнополучателя). Установка обработчика выполняется с помощью функции add_event_handler().

[править] Живой OpenGL

Чтобы продемонстрировать обработку событий FLTK на практике, мы добавим в нашу программу элемент интерактивности (новый вариант вы найдете в архиве ogdemo2). Пользователь сможет перетаскивать треугольник в окне, «ухватившись» за него мышью. Для этого добавим в класс MyGLWindow метод handle() и несколько вспомогательных полей:

  class MyGLWindow : public GlWindow {
  public:
      MyGLWindow(int X, int Y, int W,int H, const char* L=0);  private:
      int x1, y1, x2, y2, x3, y3, oldX, oldY;
      bool moving, isFullScreen;
      void draw();
      int handle(int event);
  };
 
Реализация метода '''handle()''' следует описанной выше схеме:
 
<source lang=c>
  int MyGLWindow::handle(int event) {
     switch(event) {
     case PUSH:
      if (event_key() == 1) {
           unsigned int pixel[3] = {0,0,0};
           glReadPixels(event_x(), h() - event_y(), 1, 1, GL_RGB, GL_UNSIGNED_INT, &pixel);
           if ((pixel[0] + pixel[1] + pixel[2]) != 0) {
             moving = true;
             oldX = event_x();
             oldY = event_y();
         }
      }
      return 1;
     case DRAG:
      if (moving) {
       x1 += (event_x() - oldX)*2;
       x2 += (event_x() - oldX)*2;
       x3 += (event_x() - oldX)*2;
       y1 -= (event_y() - oldY)*2;
       y2 -= (event_y() - oldY)*2;
       y3 -= (event_y() - oldY)*2;
       oldX = event_x();
       oldY = event_y();
       redraw();
    }
    return 1;
   case RELEASE:
      moving = false;
      return 1;
   case KEY:
    switch (event_key()) {
    case EscapeKey:
        destroy();
    case 102:
        isFullScreen ? fullscreen_off(0, 0, 500, 300) : fullscreen();
        isFullScreen = !isFullScreen;
        break;
    default: ;
    }
    return 1;
   default:
   return GlWindow::handle(event);
 }

Я намеренно не останавливаюсь на особенностях работы OpenGL в данной программе – на эту тему можно было бы написать отдельную статью. Мы обрабатываем события мыши PUSH, DRAG и RELEASE. Кроме них, в нашем методе handle() обрабатываются события клавиатуры: нажатие на клавишу Esc приводит к завершению работы программы, а кнопка F переключает ее между полноэкранным и оконным режимами, для чего используются методы fullscreen() и fullscreen_off(). Они реализованы в классе Window, а не GlWindow, но, как вы понимаете, при работе с трехмерной графикой они особенно полезны. Обратите внимание, что для идентификации клавиши F мы пользуемся значением функции event_key(), то есть эта клавиша будет работать независимо от раскладки клавиатуры и состояния CapsLock.

[править] Функции обратного вызова

Рассмотрим теперь механизм обработки событий высокого уровня. Как было сказано выше, для передачи сообщений программе виджеты FLTK используют функции обратного вызова. Попросту говоря, вы можете указать виджету FLTK функцию, которую следует вызвать тогда, когда с ним произойдет нечто, достойное внимания программы. Для каждого виджета можно зарегистрировать только одну такую функцию. Это связано с убеждением разработчика FLTK в том, что каждый виджет может создавать только одно «интересное» событие. Интерфейс функций обратного вызова FLTK прост настолько, насколько это возможно: все функции обратного вызова имеют заголовок вида

void callback_fn(Widget *, void *)

В первом параметре функции передается указатель на объект-виджет, породивший событие, второй параметр представляет собой указатель на произвольный блок данных, определенный программистом. Установить его можно с помощью метода user_data(), которым обладает каждый класс-потомок fltk::Widget. Для регистрации функции обратного вызова используется метод callback(), который, опять же, есть у каждого класса, реализующего виджет.

Вот, собственно, и все. Как вы можете видеть, функция обратного вызова не возвращает никаких значений. Дополнительные сведения, необходимые для обработки события, можно получить с помощью свойств виджета, вызвавшего функцию, а также с помощью тех функций, которыми мы пользовались для обработки событий низкого уровня. В частности, функция event(), объявленная в файле fltk/events.h, позволяет узнать, какое именно низкоуровневое событие заставило виджет сделать обратный вызов. В интерактивной программе OpenGL я добавил функцию обратного вызова для главного окна программы. Оно вызывает ее в одном-единственном случае – когда пользователь пытается закрыть это окно с помощью кнопки [x] в его заголовке. Сама функция обратного вызова выглядит просто:


 void exit_callback(Widget* widget, void*) {
   if (ask("Вы действительно хотите выйти?"))
     ((MyGLWindow*)widget)->hide();
 }

Функция ask() выводит на экран модальное диалоговое окно с кнопками Yes и No (рис. 2).

Возвращаемое функцией значение соответствует нажатой кнопке. Если пользователь нажал Yes, мы закрываем главное окно программы с помощью его метода hide(), что приводит к завершению работы всей программы.

Последнее, что нам осталось сделать – зарегистрировать функции обратного вызова в функции main():

 MyGLWindow win(0, 0, 500, 300, "OpenGL Test App");
 win.callback(exit_callback);

Возможно, библиотека FLTK – не лучший выбор для создания больших и сложных приложений, но она хорошо подходит для создания небольших программ, например, «демок» OpenGL. Возможно также, что опыт FLTK пригодится вам, если когда-нибудь вы захотите написать собственный набор виджетов. LXF

Персональные инструменты
купить
подписаться
Яндекс.Метрика