Данный пост является дополнением к статье «Создание приложений на GTK+ с использованием среды Glade». Когда я начинал её читать, и наткнулся на слова о том, что пример будет на C++, то заранее обрадовался, так как на тот момент искал примеры связки Glade с gtkmm – обёрточной C++ библиотекой для GTK+. Каково же было моё разочарование, когда оказалось, что автор по непонятным для меня причинам код на C, использующий сишный API GTK+, поместил в ".cpp" файл и назвал это примером на C++. В итоге, я решил самостоятельно трансформировать сишный пример из той статьи на C++. Результат выносится на суд читателей.
Предполагается, что читатель знаком с базовыми понятиями библиотеки GTK+. Также в этой статье я не буду повторятся, поэтому перед прочтением рекомендую ознакомится с содержанием оригинальной статьи.
Для использования C++ нам понадобится библиотека gtkmm, которая является обёрткой для библиотеки GTK+. При установке она сама потянет за собой зависимости, которые нам также понадобятся, например библиотеку cairomm, которая, по аналогии, является C++ обёрткой для библиотеки cairo (рендеринг 2D графики). Для debian-based дистрибутива установка производится командой:
Номер версии может отличаться для Вашего дистрибутива.
Ниже приведён полный исходный код программы, которая по функциональности полностью соответствует оригинальному примеру, но при этом написана на C++. Далее я поясню некоторые части кода по отдельности.
Всё начинается с инициализации библиотеки созданием объекта класса «Gtk::Main». Далее создаётся объект билдера, который инициализируется файлом с описанием графического интерфейса, полученного редактором Glade (см. оригинальный пример).
Обратите внимание на использование класса «Glib::RefPtr». Это реализация смарт-поинтеров в библиотеке glibmm – C++ обёртки вокруг низкоуровневой библиотеки glib. Указатель, который присвоен данному объекту, будет автоматически освобождён при разрушении объекта.
Для представления главного окна в нашем приложении мы будем использовать свой класс, который унаследован от стандартного класса окна в gtkmm – «Gtk::Window». Данный приём называется сабклассинг (subclassing) и, в частности, является одним из способов перехвата событий для виджета, что будет показано ниже.
Здесь мы создаём объект для главного окна по его описанию в билдере. Для получения объекта использующего сабклассинг, вызывается метод «get_widget_derived». Если используется стандартный класс (в данном случае это мог быть «Gtk::Window»), то следует использовать метод «get_widget» билдера.
Метод «run» получает аргументом объект окна, с которым он будет работать, и возвращает управление только после того, как оно будет скрыто. Обратите внимание, что, во избежание утечек памяти, объект окна должен быть удалён. Данное требование относится только виджетам высшего уровня (top-level), и не относится к вложенным виджетам (например, ко всем виджетам внутри нашего главного окна, которые ниже будут получены тем же методом, но не будут явно удалены).
Теперь перейдём к классу главного окна. Конструктор всех виджетов всегда имеет один прототип:
Первый аргумент является указателем на оригинальный сишный объект GTK+ – тип «BaseObjectType» всегда определён таким образом для всех классов gtkmm (в данном случае это будет GtkWindow). Его необходимо передать в конструктор базового класса. Вторым аргументом является объект билдера, который, в частности, можно использовать для получения объектов для вложенных виджетов описанным выше способом, что и делается ниже:
Далее сигналы для события нажатий на radiobutton'ы подключаются к методу «OnRadiobuttonClick» нашего класса:
Обратите внимание на использование метода «sigc::mem_fun» из библиотеки sigc++, которая является основным фреймворком для коммутации сигналов в gtkmm. Данный метод возвращает функтор для метода класса. Если нужно использовать функцию, не являющуюся членом класса, то можно воспользоваться методом «sigc::ptr_fun». Описанный способ привязки сигналов к обработчикам является единственным для виджетов, для которых не используется сабклассинг, как в нашем случае с radiobutton'ами.
Следующая трёх-этажная конструкция привязывает сигнал активации действия выхода из программы к методу «OnQuit» нашего класса:
В данном случае, в Glade, необходимо удостоверится, что создано действие «action_quit», на которое должен ссылаться элемент «Quit» в главном меню. В оригинальной статье момент с действиями был опущен, поэтому прокомментирую. Действиями в GTK+ называют, собственно, действия, которые могут быть выполнены по событиям от разных источников – пунктов меню, кнопок тулбара, горячим клавишам. В объекте действия также описываются общие атрибуты для его внешнего представления (например в меню и тулбаре) – метка, иконка, текст всплывающей подсказки. Действию в gtkmm соответствует класс «Gtk::Action». Чтобы получить его из билдера, следует использовать метод «get_object», который возвращает объект базового класса «Glib::Object», поэтому также приходится использовать метод «cast_dynamic» класса «Glib::RefPtr» для явного преобразования типа. Сам метод «OnQuit» предельно прост:
Как уже говорилось выше, чтобы выйти из метода «run» в функции «main», достаточно скрыть окно, переданное ему в качестве аргумент, что и делается в теле данного метода.
Следующий интересный момент – подкласс для класса «Gtk::DrawingArea», реализующий отрисовку фигур в соответствующем виджете. Из новых особенностей у нас метод «on_draw»:
Он является примером другого способа перехвата сигналов на события, который применим только к виджетам, использующим сабклассинг. Суть его заключается в том, что в каждом классе виджетов в gtkmm определены виртуальные методы для каждого поддерживаемого виджетом сигнала, которые вызываются при получении соответствующего сигнала. Подкласс может переопределить нужный виртуальный метод, таким образом перехватив обработку нужного сигнала, что и делается в данном примере.
Конкретно данным метод обрабатывает событие перерисовки содержимого виджета. В качестве аргумента ему передаётся контекст отрисовки библиотеки cairomm, работа с которым полностью аналогична оригинальному примеру.
На этом пост заканчивается. Большинство информации по использованию упомянутых библиотек взято из самозадокументированного исходного кода самих библиотек.
Предполагается, что читатель знаком с базовыми понятиями библиотеки GTK+. Также в этой статье я не буду повторятся, поэтому перед прочтением рекомендую ознакомится с содержанием оригинальной статьи.
Установка компонентов
Для использования C++ нам понадобится библиотека gtkmm, которая является обёрткой для библиотеки GTK+. При установке она сама потянет за собой зависимости, которые нам также понадобятся, например библиотеку cairomm, которая, по аналогии, является C++ обёрткой для библиотеки cairo (рендеринг 2D графики). Для debian-based дистрибутива установка производится командой:
sudo apt-get install libgtkmm-2.4-dev
Номер версии может отличаться для Вашего дистрибутива.
Исходный код
Ниже приведён полный исходный код программы, которая по функциональности полностью соответствует оригинальному примеру, но при этом написана на C++. Далее я поясню некоторые части кода по отдельности.
#include <gtkmm.h> #include <cairomm/cairomm.h> /** Main window class. */ class MainWindow: public Gtk::Window { private: /** Subclass for drawing area. */ class CDrawingArea: public Gtk::DrawingArea { public: typedef enum { SHAPE_RECTANGLE, SHAPE_ELLIPSE, SHAPE_TRIANGLE } shape_t; private: shape_t _curShape = SHAPE_RECTANGLE; /** Drawing event handler. */ virtual bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) { switch (_curShape) { case SHAPE_RECTANGLE: cr->rectangle(20, 20, 200, 100); cr->set_source_rgb(0, 0.8, 0); cr->fill_preserve(); break; case SHAPE_ELLIPSE: cr->arc(150, 100, 90, 0, 2 * 3.14); cr->set_source_rgb(0.8, 0, 0); cr->fill_preserve(); break; case SHAPE_TRIANGLE: cr->move_to(40, 40); cr->line_to(200, 40); cr->line_to(120, 160); cr->line_to(40, 40); cr->set_source_rgb(0.8, 0, 0.8); cr->fill_preserve(); cr->set_line_cap(Cairo::LINE_CAP_ROUND); cr->set_line_join(Cairo::LINE_JOIN_ROUND); break; } cr->set_line_width(3); cr->set_source_rgb(0, 0, 0); cr->stroke(); return true; } public: CDrawingArea(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& builder): Gtk::DrawingArea(cobject) { } void SetShape(shape_t shape) { if (_curShape != shape) { _curShape = shape; /* Request re-drawing. */ queue_draw(); } } }; Glib::RefPtr<Gtk::Builder> _builder; Gtk::RadioButton *_rbRect, *_rbEllipse, *_rbTriangle; CDrawingArea *_drawingArea; public: /** Signal handler which is called when any radio button is clicked. */ void OnRadiobuttonClick() { if (_rbRect->get_active()) { _drawingArea->SetShape(CDrawingArea::SHAPE_RECTANGLE); } else if (_rbEllipse->get_active()) { _drawingArea->SetShape(CDrawingArea::SHAPE_ELLIPSE); } else if (_rbTriangle->get_active()) { _drawingArea->SetShape(CDrawingArea::SHAPE_TRIANGLE); } } /** "quit" action handler. */ void OnQuit() { hide(); } MainWindow(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& builder): Gtk::Window(cobject), _builder(builder) { /* Retrieve all widgets. */ _builder->get_widget("rbRectangle", _rbRect); _builder->get_widget("rbEllipse", _rbEllipse); _builder->get_widget("rbTriangle", _rbTriangle); _builder->get_widget_derived("drawing_area", _drawingArea); /* Connect signals. */ _rbRect->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::OnRadiobuttonClick)); _rbEllipse->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::OnRadiobuttonClick)); _rbTriangle->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::OnRadiobuttonClick)); /* Actions. */ Glib::RefPtr<Gtk::Action>::cast_dynamic(_builder->get_object("action_quit"))-> signal_activate().connect(sigc::mem_fun(*this, &MainWindow::OnQuit)); } }; int main(int argc, char **argv) { Gtk::Main app(argc, argv); Glib::RefPtr<Gtk::Builder> builder = Gtk::Builder::create_from_file("sample.glade"); MainWindow *mainWindow = 0; builder->get_widget_derived("main_wnd", mainWindow); app.run(*mainWindow); delete mainWindow; return 0; }
Всё начинается с инициализации библиотеки созданием объекта класса «Gtk::Main». Далее создаётся объект билдера, который инициализируется файлом с описанием графического интерфейса, полученного редактором Glade (см. оригинальный пример).
Gtk::Main app(argc, argv); Glib::RefPtr<Gtk::Builder> builder = Gtk::Builder::create_from_file("sample.glade");
Обратите внимание на использование класса «Glib::RefPtr». Это реализация смарт-поинтеров в библиотеке glibmm – C++ обёртки вокруг низкоуровневой библиотеки glib. Указатель, который присвоен данному объекту, будет автоматически освобождён при разрушении объекта.
class MainWindow: public Gtk::Window
Для представления главного окна в нашем приложении мы будем использовать свой класс, который унаследован от стандартного класса окна в gtkmm – «Gtk::Window». Данный приём называется сабклассинг (subclassing) и, в частности, является одним из способов перехвата событий для виджета, что будет показано ниже.
MainWindow *mainWindow = 0; builder->get_widget_derived("main_wnd", mainWindow);
Здесь мы создаём объект для главного окна по его описанию в билдере. Для получения объекта использующего сабклассинг, вызывается метод «get_widget_derived». Если используется стандартный класс (в данном случае это мог быть «Gtk::Window»), то следует использовать метод «get_widget» билдера.
app.run(*mainWindow); delete mainWindow;
Метод «run» получает аргументом объект окна, с которым он будет работать, и возвращает управление только после того, как оно будет скрыто. Обратите внимание, что, во избежание утечек памяти, объект окна должен быть удалён. Данное требование относится только виджетам высшего уровня (top-level), и не относится к вложенным виджетам (например, ко всем виджетам внутри нашего главного окна, которые ниже будут получены тем же методом, но не будут явно удалены).
Теперь перейдём к классу главного окна. Конструктор всех виджетов всегда имеет один прототип:
MainWindow(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& builder): Gtk::Window(cobject), _builder(builder)
Первый аргумент является указателем на оригинальный сишный объект GTK+ – тип «BaseObjectType» всегда определён таким образом для всех классов gtkmm (в данном случае это будет GtkWindow). Его необходимо передать в конструктор базового класса. Вторым аргументом является объект билдера, который, в частности, можно использовать для получения объектов для вложенных виджетов описанным выше способом, что и делается ниже:
_builder->get_widget("rbRectangle", _rbRect); _builder->get_widget("rbEllipse", _rbEllipse); _builder->get_widget("rbTriangle", _rbTriangle); _builder->get_widget_derived("drawing_area", _drawingArea);
Далее сигналы для события нажатий на radiobutton'ы подключаются к методу «OnRadiobuttonClick» нашего класса:
_rbRect->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::OnRadiobuttonClick)); _rbEllipse->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::OnRadiobuttonClick)); _rbTriangle->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::OnRadiobuttonClick));
Обратите внимание на использование метода «sigc::mem_fun» из библиотеки sigc++, которая является основным фреймворком для коммутации сигналов в gtkmm. Данный метод возвращает функтор для метода класса. Если нужно использовать функцию, не являющуюся членом класса, то можно воспользоваться методом «sigc::ptr_fun». Описанный способ привязки сигналов к обработчикам является единственным для виджетов, для которых не используется сабклассинг, как в нашем случае с radiobutton'ами.
Следующая трёх-этажная конструкция привязывает сигнал активации действия выхода из программы к методу «OnQuit» нашего класса:
Glib::RefPtr<Gtk::Action>::cast_dynamic(_builder->get_object("action_quit"))-> signal_activate().connect(sigc::mem_fun(*this, &MainWindow::OnQuit));
В данном случае, в Glade, необходимо удостоверится, что создано действие «action_quit», на которое должен ссылаться элемент «Quit» в главном меню. В оригинальной статье момент с действиями был опущен, поэтому прокомментирую. Действиями в GTK+ называют, собственно, действия, которые могут быть выполнены по событиям от разных источников – пунктов меню, кнопок тулбара, горячим клавишам. В объекте действия также описываются общие атрибуты для его внешнего представления (например в меню и тулбаре) – метка, иконка, текст всплывающей подсказки. Действию в gtkmm соответствует класс «Gtk::Action». Чтобы получить его из билдера, следует использовать метод «get_object», который возвращает объект базового класса «Glib::Object», поэтому также приходится использовать метод «cast_dynamic» класса «Glib::RefPtr» для явного преобразования типа. Сам метод «OnQuit» предельно прост:
void OnQuit() { hide(); }
Как уже говорилось выше, чтобы выйти из метода «run» в функции «main», достаточно скрыть окно, переданное ему в качестве аргумент, что и делается в теле данного метода.
Следующий интересный момент – подкласс для класса «Gtk::DrawingArea», реализующий отрисовку фигур в соответствующем виджете. Из новых особенностей у нас метод «on_draw»:
virtual bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr)
Он является примером другого способа перехвата сигналов на события, который применим только к виджетам, использующим сабклассинг. Суть его заключается в том, что в каждом классе виджетов в gtkmm определены виртуальные методы для каждого поддерживаемого виджетом сигнала, которые вызываются при получении соответствующего сигнала. Подкласс может переопределить нужный виртуальный метод, таким образом перехватив обработку нужного сигнала, что и делается в данном примере.
Конкретно данным метод обрабатывает событие перерисовки содержимого виджета. В качестве аргумента ему передаётся контекст отрисовки библиотеки cairomm, работа с которым полностью аналогична оригинальному примеру.
На этом пост заканчивается. Большинство информации по использованию упомянутых библиотек взято из самозадокументированного исходного кода самих библиотек.
