Pull to refresh

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

Reading time 10 min
Views 45K
Данный пост посвящен созданию приложений с использованием кроссплатформенной библиотеки GTK+. Ориентирован он в основном на новичков? поэтому какие-то вещи возможно для многих покажутся очень простыми и банальными, но я постарался максимально подробнее всё описать, чтобы было понятно для всех.

Немалым достоинством этой библиотеки является то, что она бесплатна для коммерческого использования. В интернете не так много документации и действительно качественных статей по работе с GTK+. В очень многих примерах интерфейс программы пишется «ручками», что порой очень неудобно. Я сам с этим столкнулся и довольно часто больше времени тратил на то, чтобы правильно расположить виджеты (объекты) на форме, в контейнеры, а не сосредоточиться на решении поставленной задачи.

Я продемонстрирую как можно очень быстро создать интерфейс для GTK+ с помощью приложения для визуального создания графических интерфейсов Glade и интегрировать его в вашу программу. Glade не является ни компилятором, ни отладчиком. Он позволяет лишь описать интерфейс и представить его в файлах XML-формата GladeXML.

Базовым интерфейсом для библиотеки GTK+ является язык C. Но я в данном примере буду ипользовать C++. Соответственно появятся небольшие особенности, о которых я обязательно расскажу. Дистрибутив Linux я использую Ubuntu 10.04.

Задача будет такая: написать небольшое приложение состоящее из холста, бокового меню (выбора того, что нарисовать на холсте), главного меню и строки состояния. При этом боковое меню и холст при изменении размера всего окна приложения должны оставаться постоянного размера. Но тот контейнер, в котором находится холст может изменять свой размер в зависмости от увеличения/уменьшения размеров главного окна. И при необходимости должны появляться полосы прокрутки.

План будет такой:
  • Установка и запуск Glade3
  • Создадим графический интерфейс нашего приложения в Glade3, расположив основные виджеты (объекты) в главном окне и контейнерах, определив события (Singnals, сигналы) для виджетов и их обработчик (Handler), т.е. процедуру в коде нашей программы, которая будет вызываться при наступлении данного сигнала
  • напишем код программы: интегрируем XML файл с графическим интерфейсом, пропишем обработчики и немного функционала для демонстрации
  • немного потестируем наше приложение

Установка и запуск Glade3


Предполагается, что у Вас уже установлен dev-пакет GTK+, если нет, то это можно сделать через консоль:
sudo aptitude install libgtk2.0-dev

Установить Glade можно через Ubuntu Update Center или снова воспользовавшись консолью:
sudo apt-get install glade-gnome

После установки запускаем через Applications — Programming — Glade Interface Designer
Устанавливаем все параметры как показано на рисунке. Версию инструментария (Toolkit Version) выбираем 2.16

image

Слева вы можете увидеть окно с набором различных виджетов. Те, кто работал с Windows Forms, или в среде разрабоки Delphi быстро разберутся. Справо два меню: одно показывает иерархию виджетов, а второе окно — окно свойств виджетов.

Создание графического интерфейса


Создание графического интерфейса для вашего GTK+ приложения начинается с задания главной формы — заглавного окна программы. Находим в левом меню в разделе TopLevels виджет Window.

Справа в окне Properties задаём свойства этого виджета:
Name: topWindow
Windows position: Centre
Default Width: 500
Default Height: 320

image

Переходим во вкладку Signals (сигнал, событие) и GtkObject ищем destroy, в столбце Handler (обработчик) пишем topWindow_destroy_cb или выбираем из списка. Это будет названием процедуры которая будет обрабатывать сигнал destroy — закрытие главной формы.

image

Далее мы планируем, чтобы в нашем приложении было главное меню, основная функциональная часть (это холст и меню выбора того. что нарисовать) и строка состояния. Берём виджет-контейнер Vertical Box в левом меню в разделе Containers и кладём его нашу главную форму. В появившемя диалоге выбираем Number of Items: 3. Это означает, что в создаваемый нами контейнер можно будет положить 3 виджета.

У нас в контейнере vbox1 есть 3, так называемых ячейки, или отсека. Во второй (по центру) отсек кладём Frame (раздел Containers меню виджетов). Смотрим справа на дерево виджетов. Внутри созданного виджета frame1 будут два виждета: alignment1 и label1 — их можно удалить. На этом пока немного отклонимся и обратимся к теории.

Понимание того, как в GTK+ происходит управление размером виджетов, является очень важными для создания правильных и эффективных графических интерфейсов. Этот процесс часто называют процессом согласования, состоящего из двух составляющих (2 этапа): запрашиваемый размер и выделяемый размер.

Этап запроса представляет собой некий рекурсивный вопрос: «Сколько места тебе нужно?». Главное окно задаёт его дочерним окнам, дочерние окна задают его своим дочерним окнам и так пока все виджеты, во всей этой иерархии не ответят.

Тут есть 2 важных момента:
  1. Дочерние виджеты ничего не знают о размерах виджетов-родителей
  2. Размер виджетов-родителей зависит от размеров дочерних виджетов

Теперь, когда главное окно знает сколько места нужно в идеале, принимается решение о том сколько места будет фактически доступно. Если на запрос пришло некоторое разумное значение — оно и является используемым по умолчанию. Но если размер окна устанавливается в ручную (например, используя gtk_window_setdefault_size) или как-то иначе, окно отбрасывает запрошенный размер и устанавливает другой.

На втором этапе (выделение размера) происходит команда от родителя к дочернему виджету: «Здесь у тебя есть некоторое место, сделай всё, чтобы уместиться в него». Если у виджета более одного дочернего виджета, то задача этого этапа должным образом разделить свободное место между дочерними виджетами.

Теперь переходим к главной части нашего приложения. Идём в меню виджетов и выбираем Horizontal Panes в разделе Containers и кладём его на frame1. Нам нужно чтобы наша левая часть была фиксированного размера, правая нет. Снова идём в меню виджетов и ищем там Scrolled Window. И кладём сначала один, а потом и второй Scrolled Window в левый и правый отсек hpand1 (Horizontal Panes). У левого Scrolled Window, который называется scrolledwindow1, устанавливаем свойство (во вкладке Common)Width reguеst равным 145 — это и будет означать запрашиваемый размер (для процесса согласования). Так же следует проверить, чтобы во вкладке Packing свойство Resize у scrolledwindow1 было No, а у scrolledwindow2 Yes.
Из раздела Containers кладём сначала первый виджет ViewPort, а потом и второй на оба наших scrolledwindow1 и scrolledwindow2 соответственно.

image

Займёмся боковым нашим меню. Добавляем во ViewPort1 Vertical Box с 3 отсеками. И в каждый отсек добавляем Radio Button. У каждого Radio Button устанавливаем свойство Expand No, чтобы они не растягивались при увеличении размера родительского виджета. Также во вкладке Common можно задать Width и Height request тогда они будут размера такого какой мы укажем, а не по умолчанию. Но в любом случае изменяться в размере они будут. Изменим их свойства во вкладке General:
Name соответственно на rbutRectangle, rbutEllipse, rbutTriangle
Label соответственно на Прямоугольник, Эллипс, Треугольник
и для rbutEllipse и rbutTriangle укажем в свойстве Group rbutRectangle с помощью специального диалога.

image

Должно получиться вот так:

image

Теперь необходимо прописать события для каждого Radio Button. Во вкладке Signals без разницы либо для события GroupChanged, либо для toggled (я сделаю для него) пропишем обработчик rbutton_toggled_cb:

image

Теперь обратим к правой части нашего окна. Добавляем в ViewPort2 вертикальный контейнер Vertical Box с двумя отсеками. В верхний кладём обычный Label, а в нижний Drawing Area — это будет холст. У Label1 в свойстве label пишем Холст размером 300 x 200. Переходим во вкладку Packing и устанавливаем Expand в No.
Теперь наш холст.
Name: drawingarea
Expend: No
Width request: 300
Height request: 200
Event: ставим галочку Exposure

В Signals ищем expose-event и пишем или выбираем из списка:
drawingarea_expose_event_cb

image

Теперь нам нужно добавить строку состояния (в самый нижний отсек) и главное меню (в самый верхний отсек). В самый верхний кладём Menu Bar (раздел Containers меню виджетов), В самый нижний отсек Status Bar (раздел Control and Display меню виджетов), про его добавлении появиться диалог, выберете Number of Items: 1.

image

В menubar1 для нашего примера оставим только один пункт меню — выход из программы. Соответственно можно удалить: menuitem2, menuitem3, menuitem4. Потом раскрываем menuitem1, заходим в menu1 и удаляем так всё кроме imagemenuitem5. Это пункт Quit. Можно нажать на неё и посмотреть свойство во вкладке General окна свойств свойство Stock Item, в котором прописано gtk-quit. Там можно «поиграться». Можно вообще выбрать Custom label and image и самому всё задать. Это не так важно. Для imagemenuitem5 во вкладке Signals устанавливаем для сигнала activate (нажатие на пункт меню) точно такой же обработчик как и для закрытия формы: topWindow_destroy_cb.

Вот так должен выглядеть в итоге наш интерфейс. Сохраняем наш проект с именем mainForm. Файл будет называться mainForm.glade

image

Код нашей программы


Создадим файл Example1.cpp в той же папке куда сохранили mainForm.glade. Сначало я приведу целиком весь код, а потом объясню некоторые важные моменты.

/* Example1.cpp */
#include <cairo.h>
#include <gtk/gtk.h>
 
#define UI_FILE "mainForm.glade"

// описание виджетов
GtkBuilder *builder;
GtkWidget *topWindow;
GtkRadioButton *rbutRectangle, *rbutEllipse, *rbutTriangle;
GtkDrawingArea *drawingarea;

// описание обработчиков сигналов
extern "C" void topWindow_destroy_cb (GtkObject *object, gpointer user_data);
extern "C" gboolean drawingarea_expose_event_cb(GtkWidget *widget, GdkEventExpose *event, gpointer data);
extern "C" void rbutton_toggled_cb (GtkObject *object);
 
int main( int argc, char **argv )
{
    GError *error = NULL;

    // инициализация GTK+
    gtk_init( &argc, &argv );
 
   // создание нового GtkBuilder объекта
    builder = gtk_builder_new();

    // загрузка пользовательского интерфеса из файла, который мы создали в Glade 
    if( ! gtk_builder_add_from_file( builder, UI_FILE, &error ) )
    {
        g_warning( "%s", error->message );
        g_free( error );
        return( 1 );
    }
 
   // связывание наших виджетов с описаннимем виджетов в GladeXML
   topWindow = GTK_WIDGET(gtk_builder_get_object(builder, "topWindow"));
   rbutRectangle = GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "rbutRectangle"));
   rbutEllipse = GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "rbutEllipse"));
   rbutTriangle = GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "rbutTriangle"));
   drawingarea = GTK_DRAWING_AREA(gtk_builder_get_object(builder, "drawingarea"));

   // связываем сигналы с объектами графического интерфейса
   gtk_builder_connect_signals (builder, NULL);   

   // освобождение памяти
   g_object_unref( G_OBJECT( builder ) );
 
   // Показываем форму и виджеты на ней
   gtk_widget_show( topWindow ); 
 
   // запуск главного цикла приложения
   gtk_main();

   return( 0 );
}

// закрытие приложения
void topWindow_destroy_cb (GtkObject *object, gpointer user_data)
{
 // завершаем главный цикл приложения
 gtk_main_quit();
}

// перерисовка холста
gboolean drawingarea_expose_event_cb(GtkWidget *widget, GdkEventExpose *event, gpointer data)
{
  cairo_t *cr;

  cr = gdk_cairo_create (widget->window);
  cairo_set_line_width (cr, 7);
  cairo_set_source_rgb (cr, 0, 0, 0);

   // переключатель установлен на прямоугольник
   if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(rbutRectangle)))
   {   
    // рисуем прямоугольник
    cairo_rectangle (cr, 20, 20, 200, 100);
    cairo_stroke_preserve(cr);
    cairo_set_source_rgb(cr, 0, 0.8, 0); 
   }

   // переключатель установлен на эллипс
   if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(rbutEllipse)))
   {
    // рисуем эллипс
    cairo_arc(cr, 150, 100, 90, 0, 2 * 3.14);
    cairo_stroke_preserve(cr);
    cairo_set_source_rgb(cr, 0.8, 0, 0); 
   }

   // переключатель установлен на треугольник
   if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(rbutTriangle)))
   {
    // рисуем треугольник
    cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); 
    cairo_move_to (cr, 40, 40);
    cairo_line_to (cr, 200, 40);
    cairo_line_to (cr, 120, 160);
    cairo_line_to (cr, 40, 40); 
    cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); 
    cairo_stroke_preserve(cr);
    cairo_set_source_rgb(cr, 0.8, 0, 0.8); 
   } 

  cairo_fill(cr);

  cairo_destroy(cr);

  return FALSE;
}

void rbutton_toggled_cb (GtkObject *object)
{
 // перерисовка drawingarea
 gtk_widget_queue_draw (GTK_WIDGET(drawingarea));
}


Сначало мы объявляем те объекты. с которыми мы будем работать. Потом начинаем прописывать обработчики сигналов. Т.к. мы пишем на С++, пользуясь extern «C» объявляем topWindow_destroy_cb, drawingarea_expose_event_cb, rbutton_toggled_cb внешними функциями и указываем, что они должны связываться согласно порядку связывания в С. GtkBuilder содержит ссылки на все объекты из описания в Glade. Функция gtk_builder_get_object получает ссылку на объект с соответствующим именнем из GtkBuilder (в данном случае из builder). Функция gtk_builder_connect_signals служит для соединения функций-обработчиков в коде с прописанными соответственными обработчиками сигналов в нам GladeXML. Это происходит автоматически. Важно, чтобы имена в Glade и в коде совпадали. Приложение нормально скомпилируется, просто обрабатываться сигналы не будут, а если вы запустите через консоль программу, там будут отображены warning's, что невозможно найти обработчик для такого-то сигнала.

Так же, т.к. мы пишем на С++ очень часто приходится пользоваться явным приведением типов. Например
rbutRectangle = GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "rbutRectangle"));


если бы мы писали на C, то можно было бы обойтись и без явного привидения GTK_RADIO_BUTTON.

Для того, чтобы откомпилировать Вашу программу нужно использовать либо автоматическую систему сборки (например CMake), либо самому прописать makefile. Я на этом подробно останавливаться не буду. А лишь приведу пример описания makefile. В конце поста две самые последнии ссылки как раз касаются создания make file.


CC=g++
LDLIBS=`pkg-config --libs gtk+-2.0 gmodule-2.0`
CFLAGS=-Wall -g `pkg-config --cflags gtk+-2.0 gmodule-2.0`

Example1: Example1.o
	$(CC) $(LDLIBS) Example1.o -o Example1

Example1.o: Example1.cpp
	$(CC) $(CFLAGS) -c Example1.cpp

clean:
	rm -f Example1
	rm -f *.o


Компиляция происходит командой:
make


Небольшое тестирование нашей программы



Запускаем…

image

… а затем изменяем размер окна

image

Как мы видим боковое меню и холст не растянулись и остались в тех же размерах, а вот контейнер, в котором холст, размеры изменил. И причём всё свободное пространство, которое появилось при увеличении размера формы досталось ему. Это всё потому что мы у scrolledwindow1 установили Width reguеst = 145 и resize Yes. Вот пример появления полос прокрутки как главное окно стало меньших размеров

image

А вот теперь я зайду в Glade3 и изменю у label1 свойство Expand в Yes. Проект не нужно перекомпилировать. Достаточно просто сохранить mainForm файл. И вот результат:

image

Почему так? Холст у нас фиксированного размера. А всё пространство, которое получается при увеличении главного окна, отдаётся виджету label1. Вы можете «поиграться» со всем этим делом. Например, установив обратно Expand в No и указав точное значение Height, например 70. И тогда снова будет фиксированный размер, но уже не по умолчанию, а тот, который зададите вы.

На этом этот пост оканчивается. Если эта тема будет интересна, у меня есть желание писать еще посты в этом направлении.

Немного теории взято из Glade3 tutorial — Size negotiation.
Советую прочитать: Packing Widgets, The Cairo graphics tutorial, Makefiles by example, GNU `make'
Tags:
Hubs:
+50
Comments 34
Comments Comments 34

Articles