Кроссплатформленная библиотека GTK+

Содержание

1. GTK = GIMP ToolKit

GTK+ — это кроссплатформленная библиотека элементов интерфейса. Мое знакомство с этим инструментарием началось летом в 2010 году. После 3 курса, когда были изучены такие технологии как WinAPI и MFC для Windows NT, мне захотелось писать родные для GNU\Linux приложения с графическим интерфейсом. Поскольку основным рабочим окружением я использовал GNOME, то выбор пал именно на GTK+. Так как GNOME использует именно этот инструментарий. Конкурентной библиотекой является Qt, она используется в рабочем окружении KDE. Но с Qt как-то не срослось поскольку сколько бы раз не пробовал KDE я снова и снова возвращался к GNOME. Писать родные приложения для него было интересней.

Изначально GTK+ являлось частью графического редактора GIMP. Что и поясняет расшифровку аббревиатуры GTK — GIMP ToolKit. Позже библиотека выросла в самостоятельный проект, который стал играть основную роль в другом проекте — GNOME.

С каждой новой версией библиотека включала в себя новые графические элементы, улучшения старых и новые особенности инструментария. Текущей актуальной версией библиотеки является 3.4. Переход с 2.0 на 3.0 состоялся совсем недавно и принес с собой много шумихи в обществе свободного ПО. Правда, не из-за самой библиотеки, а рабочего окружения GNOME, проекты ведь развиваются совместно. Версия третьего GNOME кардинально отличается от второго. Сделана большая работа в сторону интерфейса планшетов и многих это не устраивает. Сама же библиотека с третьей версии имеет другую систему отрисовки, абстракцию от X11 (X Window System), поддержку CSS-подобных тем и многое другое. Также нужно сказать про появление возможности запуска GTK+ приложений прямо в браузере посредством HTML5. До конца кому и зачем это нужно не понятно, но идея занятная.

2. Архитектура библиотеки

Библиотека написана на языке С, но это не мешает ей быть объектно-ориентированной. Для многих, наверное, это будет откровением, но ведь можно мыслить и писать ООПешно даже на языках не имеющих изначально ООП парадигмы. Одной из причин выбора языка С разработчиками было желание легкого написания обверток библиотеки для других языков программирования. Поэтому использовать GTK+ можно на C++, Python, Ruby, Java, Haskell, Lua, C# и многих других языках. Очень много программ с использованием GTK+ пишут на Python, мне же приводилось использовать C, C++ и Ruby. С Ruby, правда, не все так радужно как хотелось — поддерживается не последняя версия инструментария и документация на официальном сайте местами неполная.

Сам GTK+ использует несколько библиотек: GDK, Cairo, GLib, Pango. Они разрабатываются вместе с GTK+, но могут использоваться и отдельно. GDK отвечает за вывод на экран и может использовать для этого X Window System, WinAPI, функции Mac OS X, но большую часть его функционала перенесли на Cairo. Cairo используется для рендеринга векторной графики с независящим от оборудования API. GLib является низкоуровневой библиотекой, расширяющей возможности, предоставляемые стандартной библиотекой libc языка C. Pango — свободная библиотека для отображения текста на разных языках в высоком качестве. Сам GTK+ содержит графические элементы, если точнее в терминологии инструментария — виджеты.

Наиболее известными приложениями на GTK+ вне GNU\Linux являются: Chromium, Evolution, GIMP, Inkscape, Pidgin. Набор приложений намного больше чем этот короткий список. В рамках проекта GNOME фактически на любую задачу есть программа на GTK+.

3. Программирование GUI

Для примера программирования GUI с использованием GTK+ я приведу программу написанную мною на досуге — Судоку. Мне нравятся эти головоломки, но касаться ее реализации я не буду. Я уделю все внимание GTK+, описывая детально шаг за шагом создание интерфейса. Когда я начинал, то руководств на русском по GTK+ в интернете было не так уж и много. Сейчас у меня есть шанс немного исправить данную оплошность. С полным исходным кодом можно ознакомиться в репозитории на github, который я укажу в конце данного раздела.

Для написания интерфейса можно использовать программу Glade. После работы с ней единственно что нужно будет сделать это написать обработчики сигналов. Для создания главного окна необходимо на панели палиры слева выбрать раздел Toplevels и нажать на иконку Window. Как показано на рисунке 1.




Рисунок 1 — Создание окна в Glade

Правая панель показанная на рисунке 2 содержит панели структуры проекта и свойств выбранного виджета. Выберем в панели структуры проекта только что созданное окно и в панели свойств зададим новые параметры окна на вкладке Основные следующим образом: Название — main_window, Заголовок окна — Судоку, Позиция окна — Center. Первый параметр задает имя переменной отвечающей за виджет. Glade автоматически генерирует для добавляемых виджетов имена, но лучше их переименовывать. Второй задаст текст отображающийся в заголовке окна, третий параметр отвечает за позицию окна при его появлении на экране.




Рисунок 2 — Панели структуры проекта и свойств виджета

Теперь для главного окна необходимо добавить контейнер. В GTK+ контейнер очень удобная вещь. В одной ячейки контейнера может содержаться только один виджет и он будет упаковываться по заданным параметрам. То есть параметры выравнивания, изменения размера можно параметрически задать в Glade или же путем вызов в коде соответствующих функций и больше не думать о размещении и отображении виджета при изменении размера окна, за это все будет отвечать контейнер. В GTK+ два вида основных контейнеров GtkBox и GtkGrid. Первый будет хранить виджеты в последовательных ячейках расположенных вертикально или горизонтально, а второй в виде таблицы с заданным числом столбцов и строк. Также существуют такие контейнеры как: GtkNotebook, GtkPaned, GtkFixed, GtkScrolledWindow и прочие. Но они более сложные и используются в специфических целях.

В нашем случае добавим на главное окно контейнер GtkBox, находящийся в разделе Containers панели палитры, кликнув по иконке Box и по главному окну, и в появившемся диалоге задав количество элементов равным 2. В окне появится две ячейки разделенных горизонтальной линией. В первую ячейку добавим виджет GtkMenuBar, находящийся в том же разделе, а во вторую GtkDrawingArea из раздела Control and Display.В результате должна выйти такая же картинка как на рисунке 3.




Рисунок 3 — Главное окно с виджетом меню и областью рисования

GtkMenuBar — это виджет меню приложения, а GtkDrawingArea — виджет области рисования, где будет рисоваться наша головоломка Судоку. При создании меню в нем создалось меню по умолчанию. Нам необходимо его изменить. Для этого кликнув правой кнопкой по меню в панели иерархии проекта выберем пункт Edit. В появившемся окне переходим на вкладку Иерархия в субменю _Файл убираем лишние подпункты оставляя только первый Создать, подпункт разделитель и Выход. Оставшимся пунктам нужно задать параметр Название: для пункта Создатьmenu_create, Выходmenu_exit.

В субменю Правка удаляем все элементы и правым кликом по этому субменю добавляем два дочерних обычных элемента. Первому задать Названиеmenu_param и LabelПараметры игры. Второму Я лентяй! и menu_lazy соответственно. Это будет пункт меню по которому головоломка будет решаться сама.

Далее убираем субменю _Вид, а в _Справка единственному там элементу задаем Названиеmenu_about. После проделанных шагов окно редактирования иерархии меню должно выглядеть как на рисунке 4.




Рисунок 4 — окно редактирования иерархии виджета меню

Закроем окно и в панели иерархии выберем виджет области рисования. На вкладке Основные параметр Называние зададим draw_sudoku, а на вкладке Общие параметры Запрос на установку ширины, Запрос на установку высоты положим 500 и в списке события галочку на параметре Key Press. Последняя настройка позволит области рисования ловить сообщение нажатия клавиш на клавиатуре. Два параметра высоты и ширины зададут минимальный размер виджета в контейнере, что сразу должно отобразиться на макете главного окна.

Далее создадим окно О программе для этого в разделе Toplevels на панели палитры найдите иконку About Dialog и клацнете по ней. Появиться готовое окошко О программе которой нужно только на панели свойств в кладке Основные заполнить параметры. Параметр Название задайте как about_dialog, а остальные параметры заполните под себя. Результаты заполнения сразу отобразятся на макете.

Также создадим окошко параметров игры. Шаги его создания я пропущу скажу только что задайте Название виджета param_dialog. И добавте на него выпадающий список GtkComboBox с Названием combo_level. Вот его заполнение я опишу.

Для выпадающего списка необходимо создать модель его содержимого GtkListStore. Данная модель опишет какие элементы будут отображаться в строке выбора списка. В нашем случае там будут только строки с названием сложности, но вообще туда можно задать целую таблицу с графическими элементами. Для создания GtkListStore найдите на панели палитры в разделе Miscellaneous иконку List Store и кликнете по ней. В панели свойств на первой вкладке найдите таблицу Добавить или удалить столбцы. Задайте там одну строку с типом столбца — gchararray. Ниже вы увидите таблицу Добавить или удалить строки. Создайте там строки с текстом легкий, средний, сложный, очень сложный. В результате у вас должны получиться заполнение таблиц как на рисунке 5.




Рисунок 5 — Редактирование содержимого модели содержимого.

Теперь осталось задать созданную модель нашему выпадающему списку. Для этого в свойствах выпадающего списка на закладке Основные задайте параметр Модель элемента ComboBox кликнув по кнопке ... и в окне выбрав нашу созданную модель.

Вот и все с созданием интерфейса. У вас должна получиться иерархия проекта как на рисунке 6. Единственно у вас может отличаться содержание окна параметров. Когда я делал, то задал контейнером окна GtkGrid и создал там свои кнопки, выпадающий список и надпись (GtkLabel). Более лучшим вариантом было бы использовать GtkDialogBox. Еще было бы неплохо в свойствах виджетов на вкладке Сигналы задать имена обработчиков сигналов, тогда при подключении интерфейса в коде программы функции обработчики автоматически присоединились к виджетам. Но данную магию по некоторым причинам я делать не буду и обработчики сигнала подключу вручную в коде программы.




Рисунок 6 — Иерархия интерфейса программы Судоку

Сохраните проект в папку с будующей программой под именем sudoku.glade. Прелесть Glade состоит еще в том, что теперь данный файл теперь можно использовать в проекте на любом языке программирования где есть GTK+. Список данных языков я приводил выше.

Кстати, сохраненный файлик это обычный XML файл содержащий в себе структуру интерфейса и его атрибуты. Сейчас многие вспомнят про XAML, использующийся в WPF от компании Microsoft. Но WPF был с .NET Framework 3.0 в ОС Windows Vista, а первый выпуск Glade состоялся в далеком 1999 году. Ранние версии программы могли еще генерировать код для проекта. Но данную функцию исключили поскольку полученный код было трудно поддерживать и изменять.

Приступим теперь непосредственно к программированию.

Я использую среду разработки Eclipse с плагином CDT и компилятор gcc. Особенности процесса компиляции документирован тут. От себя скажу, что указанная там утилита pkg-config указывает компилятору где расположены заголовки файлов и библиотеки GTK+. Но если вы используете Eclipse, то советую в свойствах проекта вручную добавить данные файлы и библиотеки. Для этого то что выводит утилита по команде `pkg-config --cflags gtk+-3.0` записать в Свойства (правая кнопка по проекту) -> С/C++ Build -> Settings -> Cross G++ Compiler -> Includes, а вывод `pkg-config --libs gtk+-3.0` в Свойства -> С/C++ Build -> Settings -> Cross G++ Linker -> Libraries. Это гарантирует подсказки в Content Assist. Если сделать по руководству как на сайте документации, то данная удобность скорей всего не будет вам доступна в Eclipse.

Все приготовления закончены — приступим. Создадим файлы main.cpp, interface.h и interface.cpp. Если загляните в архив с исходным кодом, то увидеть там еще файлы game.h и game.cpp. В них описана сама механика судоку — генерация головоломки, решение и пара еще вспомогательных функций. Будем считать, что они уже написаны и содержаться в нашем проекте, а в ходе написания поведения интерфейса просто будем их вызывать.

Сперва заполним файл interface.h следующим кодом


#include <gtk/gtk.h>
#include "game.h"

struct app_sudoku
{
    // данные программы
    int n; // размер судоку
    int level; // уровень сложности
    int **sudoku; // матрица судоку
    int **sudoku_mask; // матрица маски судоку — элементы которые были изначально
    int set_x; // выделенный столбец матрицы
    int set_y; // выделенная строка матрицы
    int click_x; // координата x клика мышки по матрице
    int click_y; // координата y клика мышки по матрице
    // виджеты судоку
    GtkDrawingArea *draw_area; // область рисования
    GtkComboBox *combo_level; // выпадающий список
};
// функция создания нового судоку
void create_new_game(app_sudoku &app);
// обработчики сигналов и функции для области рисования
void on_drawingarea_event(GtkWidget *widget, cairo_t* cr, gpointer data);
void draw(GtkWidget *widget, cairo_t* cr, int **sudoku, int **sudoku_mask, int n, int set_x, int set_y);
void draw_numbers(GtkWidget *widget, cairo_t* cr, int **sudoku,int **sudoku_mask,int n,int Hline,int Vline);
void draw_select(GtkWidget *widget, cairo_t* cr, int set_x,int set_y,int Hline,int Vline);
void draw_wrong(GtkWidget *widget, cairo_t* cr, int **sudoku,int n,int Hline,int Vline);
gboolean button_press(GtkWidget *widget, GdkEventButton *event, gpointer data);
gboolean key_press(GtkWidget *widget,GdkEvent* event, gpointer data);
// обработчики сигналов меню
void on_menu_create_activate(GtkMenuItem* obj,gpointer data);
void on_menu_about_activate(GtkMenuItem* obj,gpointer data);
void on_menu_param_activate(GtkMenuItem* obj,gpointer data);
void on_menu_lazy_activate(GtkMenuItem* obj,gpointer data);
// обработчики сигналов окна параметров
void on_button_apply (GtkButton *widget, gpointer data);
void on_button_close(GtkButton *widget, gpointer data);
// обработчик сигнала закрытия диалога
void on_destroy_hide (GtkWidget *widget, GdkEvent *event, gpointer data);

Первая строчка подключает заголовочный файл всей библиотеки GTK+. Вторая заголовочный файл с реализацией механики судоку. После идет структура app_sudoku она содержит различные атрибуты игры, матрицу головоломки, маску головоломки (необходима для определения начальной матрицы при прорисовки) и некоторые графические элементы. Данная структура будет передаваться обработчикам сигналов для различных действий. После структуры указаны прототипы функций по их названиям и комментариям, я думаю, их предназначение понятно. Более детальное описание указанных обработчиков функций будет ниже.

Теперь напишем функцию main в файле main.cpp. Не забудьте подключить файл interface.h.


int main (int argc, char **argv)
{
    // инициализация GTK
    gtk_init(&argc, &argv);
    // задание начальных параметров судоку
    app_sudoku Sudoku;
    Sudoku.n = 9;
    Sudoku.level = 4;
    Sudoku.set_x = 0;
    Sudoku.set_y = 0;
    Sudoku.click_x = 0;
    Sudoku.click_y = 0;
    Sudoku.sudoku = NULL;
    Sudoku.sudoku_mask = NULL;
    create_new_game(Sudoku);
    // создание постройщика интерфейса из XML
    GtkBuilder *builder = gtk_builder_new ();
    // чтение интерфейса из XML файла
    gtk_builder_add_from_file (builder, "sudoku.glade", NULL);
    // создание виджетов из XML объявления
    // главное окно
    GtkWidget *main_window = GTK_WIDGET(gtk_builder_get_object(builder, "main_window"));
    // окно About
    GtkWidget *about_dialog = GTK_WIDGET(gtk_builder_get_object(builder, "about_dialog"));
    // окно параметров игры
    GtkWidget *param_dialog = GTK_WIDGET(gtk_builder_get_object(builder, "param_dialog"));
    // выпадающий список сложности игры
    Sudoku.combo_level = GTK_COMBO_BOX(gtk_builder_get_object(builder, "combo_level"));
    // кнопка отмены
    GtkButton *button_cancel = GTK_BUTTON(gtk_builder_get_object(builder,"button_cancel"));
    // кнопка принятия настроек
    GtkButton *button_apply = GTK_BUTTON(gtk_builder_get_object(builder,"button_apply"));
    // область рисования для судоку
    Sudoku.draw_area = GTK_DRAWING_AREA(gtk_builder_get_object(builder, "draw_sudoku"));
    // элемент меню "Создать"
    GtkMenuItem *menu_new = GTK_MENU_ITEM(gtk_builder_get_object(builder, "menu_create"));
    //элемент меню "О программе"
    GtkMenuItem *menu_about = GTK_MENU_ITEM(gtk_builder_get_object(builder, "menu_about"));
    //элемент меню "Я лентяй"
    GtkMenuItem *menu_lazy = GTK_MENU_ITEM(gtk_builder_get_object(builder, "menu_lazy"));
    //элемент меню "Параметры"
    GtkMenuItem *menu_param = GTK_MENU_ITEM(gtk_builder_get_object(builder, "menu_param"));
    //элемент меню "Выход"
    GtkMenuItem *menu_exit = GTK_MENU_ITEM(gtk_builder_get_object(builder, "menu_exit"));
    //установка обработчиков сигналов
    //установка сигнала нажатия клавишь в окне программы
    g_signal_connect(main_window,"key_press_event",G_CALLBACK(&key_press),&Sudoku);
    //установка сигнала нажатия кнопки мыши на виджет области рисования
    g_signal_connect(Sudoku.draw_area ,"button-press-event",G_CALLBACK(&button_press),&Sudoku);
    //установка сигнала отрисовки виджета области рисования
    g_signal_connect(Sudoku.draw_area ,"draw",G_CALLBACK(&on_drawingarea_event),&Sudoku);
    //установка сигнала выбора пункта меню "Создать"
    g_signal_connect(menu_new,"activate",G_CALLBACK(&on_menu_create_activate),&Sudoku);
    //установка сигнала выбора пункта меню "О программа"
    g_signal_connect(menu_about,"activate",G_CALLBACK(&on_menu_about_activate),about_dialog);
    //установка сигнала выбора пункта меню "Параметры"
    g_signal_connect(menu_param,"activate",G_CALLBACK(&on_menu_param_activate),param_dialog);
    //установка сигнала выбора пункта меню "Я лентяй"
    g_signal_connect(menu_lazy,"activate",G_CALLBACK(&on_menu_lazy_activate),&Sudoku);
    //установка сигнала нажатия кнопки применить окна параметров
    g_signal_connect(button_apply,"clicked",G_CALLBACK(&on_button_apply),&Sudoku);
    //установка сигнала нажатия кнопки отмена окна параматров
    g_signal_connect(button_cancel,"clicked",G_CALLBACK(&on_button_close),param_dialog);
    //установка сигналов закрытия окон
    g_signal_connect(menu_exit,"activate",G_CALLBACK(&gtk_main_quit),NULL);
    g_signal_connect(main_window,"destroy",G_CALLBACK(&gtk_main_quit),NULL);
    g_signal_connect(main_window,"delete_event",G_CALLBACK(&gtk_main_quit),NULL);
    g_signal_connect(about_dialog,"destroy",G_CALLBACK(&on_destroy_hide),NULL);
    g_signal_connect(about_dialog,"delete_event",G_CALLBACK(&on_destroy_hide),NULL);
    g_signal_connect(param_dialog,"destroy",G_CALLBACK(&on_destroy_hide),NULL);
    g_signal_connect(param_dialog,"delete_event",G_CALLBACK(&on_destroy_hide),NULL);
    //запуск приложения
    gtk_widget_show_all(main_window);
    gtk_main();
    return 0;
}

В приведенном листинге функция gtk_init выполняет инициализацию GTK+ по переданным параметрам функции main. GtkBuilder — структура предназначенная для построения интерфейса из XML документа. gtk_builder_new — возвращает указатель на новый объект GtkBuilder. gtk_builder_add_from_file — функция, считывающая интерфейс из XML документа, принимает первым параметром указатель на GtkBuilder, вторым путь к файлу, третьим указатель на GError. Функция gtk_builder_get_object возвращает указатель на созданный виджет из XML документа, на вход функции первым параметром является указатель на GtkBuilder, а вторым строка имени виджета указанная в проекте Glade. Функция gtk_builder_get_object возвращает указатель на GObject, поэтому его необходимо привести к необходимому типу. С этой целью используются макросы преобразования, например: GTK_WIDGET, GTK_BUTTON, GTK_MENU_ITEM и т.д. g_signal_connect — функция определяющая для сигнала виджета функцию-обработчика. Первым параметром g_signal_connect является указатель на виджет, вторым строка задающая имя сигнала, третьим хэндл call_back функции-обработчика, четвертый параметр принимает указатель на объект любой структуры. Если вы посмотрите на прототип функций-обработчика, указанных в файле interface.h, то заметите, что последний параметр имеет тип gpointer. Через этот параметр и будет передаваться объект указанный четвертым параметром функции g_signal_connect. В моем примере в качестве этого параметра передается указатель на переменную Sudoku, но в некоторые случаях этим выступают виджеты-окна.

gtk_widget_show_all — функция отображающая все содержимое виджета, передаваемого в виде параметра. Функция gtk_main запускает бесконечный цикл работы GTK+ приложения, работающий пока не будет вызван gtk_main_quit.

Теперь у нас есть весь разработанный интерфейс приложения под рукой с присоединенными к сигналам функциями-обработчиками. Я опишу только функции связанные с прорисовкой судоку, поскольку остальные обработчики менее интересны и просты. Если у кого-то они вызовут интерес, то они прокомментированы в исходном коде программы.

Рассмотрим функцию обработки нажатия кнопок мыши по области рисования:


gboolean button_press (GtkWidget* widget, GdkEventButton *event, gpointer data)
{
    //преобразование типа gpointer к app_sudoku*
    app_sudoku* sdk = static_cast<app_sudoku*>(data);
    if(sdk)
    {
        //запоминание координат клика
        sdk->click_x = event->x;
        sdk->click_y = event->y;
        switch(event->button)
        {
        //1 — первая кнопка мыши
        case 1:
            //GDK_BUTTON_PRESS — маска нажания кнопки
            if(event->type == GDK_BUTTON_PRESS)
            {
                //получение размеров области рисования
                int w = gtk_widget_get_allocated_width(widget);
                int h = gtk_widget_get_allocated_height(widget);
                //расчет размеров клеток судоку
                int Vline = (double)w/(double)sdk->n;
                int Hline = (double)h/(double)sdk->n;
                //нахождение в какой клетке произведен клик
                for(int i = 0, _x = 0; i < w; i += Vline, _x++)
                    for(int j = 0, _y = 0; j < h; j += Hline, _y++)
                        if(iclick_x && i+Vline>sdk->click_x && jclick_y && j+Hline>sdk->click_y)
                        {
                            //сохранения номера столбца и строки клика
                            sdk->set_col = _y;
                            sdk->set_row = _x;
                            //вызов сигнала прорисовки области рисования
                            gtk_widget_queue_draw(GTK_WIDGET(sdk->draw_area));
                        }
            }
            break;
        }
        return TRUE;
    }
    return FALSE;
}

Функция-обработчик нажатия кнопок мыши вторым параметром передает объект GdkEventButton. Данная структура содержит такую полезную информацию как координаты мыши относительно виджета, какая кнопка нажата, ее состояние и прочие. В приведенном примере обработчик отлавливает нажатие левой кнопки мыши.

Функции gtk_widget_get_allocated_width и gtk_widget_get_allocated_height используются для определения размеров области рисования, а gtk_widget_queue_draw вызывает сигнал отрисовки указанного в параметрах виджета.

Рассмотрим функцию обработки нажатия клавиш на клавиатуре.


gboolean key_press (GtkWidget* widget,GdkEvent* event, gpointer data)
{
    //преобразование типа gpointer к app_sudoku*
    app_sudoku* sdk = static_cast<app_sudoku*>(data);
    if(sdk)
    {
        guint key;
        //получение кода нажатой клавиши
        gdk_event_get_keyval(event,&key);
        //диапазон чисел
        if(key — 48 > 0 && key — 48 < 10)
        {
            //запись числа в судоку
            if(sdk->sudoku_mask[sdk->set_col][sdk->set_row] == 0)
                sdk->sudoku[sdk->set_col][sdk->set_row] = key — 48;
        }
        else
        {
            switch(key)
            {
            //клавиша delete
            case 65535:
                if(sdk->sudoku_mask[sdk->set_col][sdk->set_row] == 0)
                    sdk->sudoku[sdk->set_col][sdk->set_row] = 0;
                break ;
            //клавиша вправо
            case 65363:
                if(sdk->set_row < 8)
                    sdk->set_row++;
                else
                    sdk->set_row = 0;
                break ;
            //клавиша влево
            case 65361:
                if(sdk->set_row > 0)
                    sdk->set_row--;
                else
                    sdk->set_row = 8;
                break ;
            //клавиша вниз
            case 65364:
                if(sdk->set_col < 8)
                    sdk->set_col++;
                else
                    sdk->set_col = 0;
                break ;
            //клавиша вверх
            case 65362:
                if(sdk->set_col > 0)
                    sdk->set_col--;
                else
                    sdk->set_col = 8;
                break ;
            }
        }
        //вызов сигнала отрисовки области рисования
        gtk_widget_queue_draw(GTK_WIDGET(sdk->draw_area));
        return TRUE;
    }
    return FALSE;
}

Как и в случае с обработчиком нажатия мыши обработчик нажатия клавиш на клавиатуре вторым параметром передает объект GdkEvent, содержащий различную информацию. В частности с помощью функции gdk_event_get_keyval мы можем узнать код нажатой клавиши. Что дает нам написать поведение программы соответствующим клавишам.

Теперь рассмотрим функцию-обработчика сигнала отрисовки области рисования. Саму реализацию рисования я выделил в отдельные функции. Я последовательно укажу их.


void on_drawingarea_event (GtkWidget *widget, cairo_t cr, gpointer data)
{
    //преобразование типа gpointer к app_sudoku*
    app_sudoku* sdk = static_cast<app_sudoku*>(data);
    if(sdk)
        //вызов функции рисования
        draw(widget,cr,sdk->sudoku,sdk->sudoku_mask,sdk->n,sdk->set_col,sdk->set_row);
}

void draw (GtkWidget *widget, cairo_t cr, int **sudoku, int **sudoku_mask, int n, int set_x, int set_y)
{
    //определение размеров области рисования
    int w = gtk_widget_get_allocated_width(widget);
    int h = gtk_widget_get_allocated_height(widget);
    //определение размеров ячейки
    int Vline = (double)w/(double)n;
    int Hline = (double)h/(double)n;
    //отрисовка белого фона
    cairo_set_source_rgb(cr,1,1,1);
    cairo_rectangle(cr,0,0,w,h);
    cairo_fill(cr);
    cairo_stroke(cr);
    //отрисовка вертикальных линий
    cairo_set_source_rgb(cr,0,0,0);
    for(int i = 1; i < n; i++)
    {
        if(i % 3 == 0)
            cairo_set_line_width(cr,2);
        else
            cairo_set_line_width(cr,0.5);
        cairo_move_to(cr,i*Vline,0);
        cairo_line_to(cr,i*Vline,h);
        cairo_stroke(cr);
    }
    //отрисовка горизонтальных линих
    for(int i = 1; i < n; i++)
    {
        if(i % 3 == 0)
            cairo_set_line_width(cr,2);
        else
            cairo_set_line_width(cr,0.5);
        cairo_move_to(cr,0,i*Hline);
        cairo_line_to(cr,w,i*Hline);
        cairo_stroke(cr);
    }
    //вызов функции отрисовки цифр
    draw_numbers(widget,cr,sudoku,sudoku_mask,n,Hline,Vline);
    //отрисовка выделенного элемента
    draw_select(widget,cr,set_x,set_y,Hline,Vline);
    //обведение неверных введенных элементов
    draw_wrong(widget,cr,sudoku,n,Hline,Vline);
}

void draw_numbers (GtkWidget *widget, cairo_t cr, int **sudoku,int **sudoku_mask,int n,int Hline,int Vline)
{
    //создание указателя на объект графического текста
    PangoLayout *layout = pango_cairo_create_layout(cr);
    //создание шрифтов
    PangoFontDescription *font_b = pango_font_description_from_string("sans bold 40");
    PangoFontDescription *font_i = pango_font_description_from_string("sans italic 40");
    //отрисовка цифер
    for(int i = 0; i < n; i++)
        for(int j = 0; j < n;j++)
            if(sudoku[i][j] != 0)
            {
                gchar *number = new gchar [1];
                number[0] = sudoku[i][j] + 48;
                //установка текста
                pango_layout_set_text(layout,number,1);
                //перемещение курсора контекста рисования
                cairo_move_to(cr,Vline*0.2 + Vline*j, Hline*i);
                //установка шрифта текста
                if(sudoku_mask[i][j] == 1)
                    pango_layout_set_font_description(layout,font_i);
                else
                    pango_layout_set_font_description(layout,font_b);
                //отрисовка текста
                pango_cairo_show_layout(cr,layout);
            }
}

void draw_wrong (GtkWidget *widget, cairo_t cr, int **sudoku,int n,int Hline,int Vline)
{
    cairo_set_source_rgb(cr,1,0,0);
    cairo_set_line_width(cr,2);
    //обведение неверных цифер по строкам
    for(int i = 0; i < n; i++)
        for(int j = 0; j < n; j++)
            for(int t = 0; t < n; t++)
                if(sudoku[i][j] == sudoku [i][t] && sudoku[i][j] != 0 && t != j)
                    cairo_rectangle(cr,t*Vline,i*Hline,Vline,Hline);
    //обведение неверных цифер по столбцам
    for(int j = 0; j < n; j++)
        for(int i = 0; i < n; i++)
            for(int t = 0; t < n; t++)
                if(sudoku[t][j] == sudoku [i][j] && sudoku[i][j] != 0 && t != i)
                    cairo_rectangle(cr,j*Vline,i*Hline,Vline,Hline);
    //обведение неверных цифер по блокам
        for(int blh = 1; blh < 4; blh++)
            for(int blv = 1; blv < 4;blv++)
                for(int i = blh * n / 3 — n / 3; i < blh * n / 3; i++)
                    for(int j = blv * n / 3 — n / 3; j < blv * n / 3; j++)
                        for(int t1 = blh * n / 3 — n / 3; t1 < blh * n / 3; t1++)
                            for(int t2 = blv * n / 3 — n / 3; t2 < blv * n / 3; t2++)
                                if(sudoku[i][j] == sudoku [t1][t2] && sudoku[i][j] != 0 && t1 != i && t2 !=j)
                                    cairo_rectangle(cr,j*Vline,i*Hline,Vline,Hline);
    cairo_stroke(cr);
}

В функции on_drawingarea_event вторым параметром передается указатель на объект cairo_t. Это ничто иное как контекст рисования, являющийся технологией векторной графики Cairo. Рисовать наше судоку будем именно им. С помощью функции cairo_set_source_rgb контексту задается цвет в виде параметров RGB. cairo_rectangle — контексту рисования задается прямоугольник в указанных координатах и соответствующих размерами высоты и ширины. cairo_fill — заполняет последний нарисованных путь. То есть в нашем случае после вызова cairo_rectanglecairo_fill зарисует прямоугольник. cairo_stroke — рисует последние заданные контексту задачи. cairo_set_line_width — устанавливает ширину кисти линий. cairo_move_to — перемещает курсор рисования в определенную точку. cairo_line_to — задает контексту линию от текущей его точки к заданной в параметрах.

В функции draw_numbers с помощью Pango происходит отображение текста матрицы судоку в области рисования. Для этого необходимо создать layout с помощью pango_cairo_create_layout. С помощью pango_font_description_from_string можно создать необходимые шрифты. В моем примере их создается два. Один будет использоваться для выделения цифр введенных игроком, а второй будет выделять цифры, бывшие в головоломке изначально. pango_layout_set_text — устанавливает для layout текст. pango_layout_set_font_description — устанавливает шрифт. pango_cairo_show_layout — соответственно отображает текст в области рисования.

Готовое приложение приведено на рисунке 7. Проект в репозитории на github на расстоянии клика.




Рисунок 7 — Итоговый вид приложения Судоку

4. Документация и другие примеры по GTK+

  1. Полная документация на официальном сайте GTK
  2. Справочное руководство по Cairo
  3. Справочное руководство по Pango
  4. Русское руководство по использованию GTK и Glade
  5. Хорошая статья на хабре про GtkTreeView и GtkListStore
  6. Существует также книжка в переводе на русском Программирование Gnome/GTK+ Артура Гриффитса, ее можно читать для близкого ознакомления с библиотекой на примерах, но там описана старая вторая версия GTK+.
  7. Больше мне книжек в переводе не встречалось, но на английском их предостаточно: Foundations of GTK+ Development Andrew Krause, Gtk+ Programming in C Syd Logan, GTK+ /Gnome Application Development Havoc Pennington, Beginning GTK+ and GNOME Peter Wright. Правда, они тоже касаются второй версии библиотеки.