Использование Boost.Lambda при программировании GUI на Gtk.

Библиотека Gtk является одной из двух самых популярных библиотек (или тулкитов) для написания пользовательских интерфейсов под Linux (кроме того, Gtk работает под Windows, под MacOS и др.). И хотя она написана на чистом C, у ней есть привязки/обертки на самых разных языках, включая C++, Python, Perl, Java и др. В данной статье я использую Gtkmm - это C++-обертка над Gtk.

Рассмотрим работу с Gtk на простом примере рисования: возьмем виджет1 Gtk::DrawingArea и нарисуем в нем 2 диагонали. Вот стандартный вариант реализации (скриншот внизу - что получаем в итоге):

   1 #include <gtkmm.h>
   2 
   3 class ExampleDA: public Gtk::DrawingArea
   4 {
   5     public:
   6    virtual bool  on_expose_event(GdkEventExpose* event);
   7 };
   8 
   9 bool ExampleDA::on_expose_event(GdkEventExpose* )//event)
  10 {
  11     int wdh = get_width(), hgt = get_height();
  12     Cairo::RefPtr<Cairo::Context> cr = get_window()->create_cairo_context();
  13 
  14     cr->move_to(0, 0);
  15     cr->line_to(wdh, hgt);
  16     cr->stroke();
  17 
  18     cr->move_to(wdh, 0);
  19     cr->line_to(0, hgt);
  20     cr->stroke();
  21 
  22     return true;
  23 }
  24 
  25 int main(int argc, char* argv[])
  26 {
  27     Gtk::Main kit(argc, argv); // инициализация Gtkmm
  28 
  29     ExampleDA da;              // наш виджет
  30 
  31     Gtk::Window win;           // вставляем его в окно
  32     win.add(da);
  33 
  34     win.show_all();    
  35     Gtk::Main::run(win);       // запускаем цикл сообщений
  36     return 0;
  37 }

Diagonals.png

Что мы видим? Нам пришлось породить класс ExampleDA от Gtk::DrawingArea и написать 2 функции: main() и ExampleDA::on_expose_event(). И если без функции main() нам не обойтись, то создание целого класса ради отрисовки двух линий слишком расточительно. Попробуем обойтись без создания класса:

  • удалим класс ExampleDA;
  • изменим название функции ExampleDA::on_expose_event() на DrawDiagonals()

  • в функции main() присоединим к сигналу "expose-event" функцию-обработчик DrawDiagonals()

Работает это по следующей схеме: в момент, когда требуется перерисовка, пройдет сигнал2 "expose-event", который и вызовет DrawDiagonals().

   1 bool DrawDiagonals(GdkEventExpose* )//event)
   2 {
   3     // ошибки компилятора - нет функций get_height(), get_width(), get_window()!
   4     int wdh = get_width(), hgt = get_height();
   5     Cairo::RefPtr<Cairo::Context> cr = get_window()->create_cairo_context();
   6 
   7     ...
   8     return true;
   9 }
  10 
  11 int main(int argc, char* argv[])
  12 {
  13     ...
  14     Gtk::DrawingArea da; //ExampleDA da;
  15     da.signal_expose_event().connect( &DrawDiagonals );
  16     ...
  17     return 0;
  18 }

Однако, в этом случае компилятор резонно укажет нам на отсутствие функций get_height(), get_width(), get_window(),- все они являются членами класса Gtk::DrawingArea, а сам объект da мы в функцию DrawDiagonals() не передали (раньше он был доступен через this). Более того, проблема гораздо шире,- в реальных условиях возникают потребности в передаче не только самого объекта Gtk, над которым производится обработка сигнала, но и другие, самые различные параметры; и наоборот, так как сигнатура функции DrawDiagonals() жестко определена сигналом "expose-event" (а именно, она равна bool(GdkEventExpose*) ), то нам пришлось "тащить" в эту функцию бесполезный аргумент "GdkEventExpose* event", который мы сразу и закомментировали (в данном случае мы его не используем, а вообще-то он полезен :) ).

Вот в таких ситуациях мы и применим библиотеку Boost.Lambda3. Вкратце, Lambda позволяет создавать функторы (классы со встроенной операцией '()') с нужной сигнатурой. Итак, изменим несколько строчек и получим работающий вариант программы:

   1 #include <boost/lambda/bind.hpp>
   2 
   3 bool DrawDiagonals(Gtk::DrawingArea& da)
   4 {
   5     int wdh = da.get_width(), hgt = da.get_height();
   6     Cairo::RefPtr<Cairo::Context> cr = da.get_window()->create_cairo_context();
   7     ...
   8 }
   9 
  10 int main(int argc, char* argv[])
  11 {
  12     ...
  13     da.signal_expose_event().connect( boost::lambda::bind(&DrawDiagonals, boost::ref(da)) );
  14     ...
  15 }

Замечание: с заявлением, что последний вариант кода скомпилируется, мы поторопились; дело в том, что функция connect() в библиотеке Gtkmm (а точнее, в sigc++) не может определить возвращаемый тип функтора (см. выше в функции main()), а потому просто считает, что он равен void (а в нашем случае надо возвращать bool). Решение - использовать специальную функцию wrap_return<R>(), описанную в заголовке src/mlib/sigc.h (см. исходники Bombono DVD). Однако, если сигнал не предполагает возвращение никакого значения, то спец. функции не нужны (например, сигналы "show", "map", "clicked").


Итого: цель достигнута - с помощью Boost.Lambda отрисовка виджета занимает всего одну функцию, и никакого вспомогательного кода не потребовалось. Окончательный вариант лежит тут: diagonals.cpp.

Автор: Илья Муравьев
2009


  1. Виджет - примитив GUI, такой как кнопка, меню, поле ввода, значок ... (1)

  2. Подробнее о сигналах в Gtk см. http://www.opennet.ru/docs/RUS/gtk_plus/x178.html (2)

  3. Документация по Boost.Lambda - http://www.boost.org/doc/html/lambda.html. (3)

Articles/GtkLambda (last edited 2009-06-11 16:49:15 by anonymous)