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

LXF115:FLTK

Материал из Linuxformat
Версия от 13:59, 21 января 2010; Crazy Rebel (обсуждение | вклад)

(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск
Программируем с 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() следует описанной выше схеме:
 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

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