Данный пост является дополнением к статье «Создание приложений на GTK+ с использованием среды Glade». Когда я начинал её читать, и наткнулся на слова о том, что пример будет на C++, то заранее обрадовался, так как на тот момент искал примеры связки Glade с gtkmm – обёрточной C++ библиотекой для GTK+. Каково же было моё разочарование, когда оказалось, что автор по непонятным для меня причинам код на C, использующий сишный API GTK+, поместил в ".cpp" файл и назвал это примером на C++. В итоге, я решил самостоятельно трансформировать сишный пример из той статьи на C++. Результат выносится на суд читателей.
Предполагается, что читатель знаком с базовыми понятиями библиотеки 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, работа с которым полностью аналогична оригинальному примеру.
На этом пост заканчивается. Большинство информации по использованию упомянутых библиотек взято из самозадокументированного исходного кода самих библиотек.
Автор: vagran
А как компилировать?