LXF115:FLTK

Материал из Linuxformat.

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


FLTK

Содержание

Сверхскоростная графика

ЧАСТЬ 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() перекрывает неправильный, но повторять эту небрежность не следует.

Рис. 1 Рис. 1. OpenGL в программе FLTK.

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

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] в его заголовке. Сама функция обратного вызова выглядит просто:

Рис. 2 Рис. 2. Модальное диалоговое окно FLTK.

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

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию