Создание приложений на GTK+/gtkmm с использованием среды Glade

в 7:01, , рубрики: c++, glade, GTK+, gtkmm, разработка, метки: , ,

Данный пост является дополнением к статье «Создание приложений на 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


  1. Василий:

    А как компилировать?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js