» Кроссплатформенный анимированный осциллограф (GTK+, Cairo) с++ и с #. . . Блог программистов


Блог программистов




200916 Авг

Кроссплатформенный анимированный осциллограф (GTK+, Cairo)

Я решил сделать кроссплатформенную версию  этого импровизированного осциллографа и полностью переписал программу на GTK+. Данная статья может быть полезна всем, кто только начал или собирается изучать этот тулкит. Параллельно в уроке рассматривается использование некоторых функций мощной библиотеки двухмерной графики Cairo.
В примере показана компиляция программы в Linux, пользователи Windows могу скомпилировать программу, например, с помощью MinGW, так же скомпилировать программу могут и пользователи Mac OS X. Вопросы компиляции на платформах, отличных от Linux выходят за рамки данной статьи и рассматриваться не будут. Те кто желает — всегда сможет разобраться самостоятельно, используя поиск.

Но в начале немного теории.
GTK+ — это кроссплатформенная библиотека, предназначенная для создания приложений с графическим интерфейсом пользователя, предоставляет богатый набор функций на все случаи жизни. Изначально библиотека разрабатывалась для программы Gimp. Библиотека написана на языке С, но тем не менее сохраняет принципы объектно-ориентированного программирования.
Для использования возможностей библиотеки, в Вашей системе, должна быть установлена как сама библиотека, так и заголовочные файлы (dev, devel пакеты) к ней, все это Вы можете сделать с помощью менеджера пакетов Вашего дистрибутива.
После установки всех недостающих компонентов можно подключать заголовочные файлы библиотеки в свою программу

Код:

#include <gtk/gtk.h>

Основными объектами библиотеки, с которыми нам придется иметь дело, являются так называемые виджеты. Каждый элемент управления (кнопка, метка, поле ввода) является виджетом. Само окно так же является виджетом. Еще одно очень важное понятие GTK+ — сигналы. Их можно считать, своего рода, аналогами сообщений в Windows. Сигналы посылают различные элементы управления (нажата кнопка, передвинуто окно), а так же функции, при наступлении некоторого события (сработал таймер). Для обработки сигналов необходимо создавать функции-обработчики. Но функция-обработчик не будет взаимодействовать с сигналом сама по себе, для этого их необходимо связать. Это делается с помощью специальных методов GTK+. Обратите, так же, внимание на явное приведение типов аргументов функций.
Настало время рассмотрения программы.

В самом начале программы подключаем необходимые заголовочные файлы, объявляем необходимые переменные и функции. Комментарии, думаю, все поясняют.

Код:

#include <gtk/gtk.h>
#include <stdio.h>
#include <math.h>
//объявляем функцию, обработчик сигнала закрытия окна
static void destroy(GtkWidget*, gpointer);
//объявляем функцию, обработчки сигнала перерисовки окна
static void draw(GtkWidget*, GdkEventExpose*, gpointer);
//объявляем функцию, объявляющую область перерисовки недействительной
static gboolean time_handler(GtkWidget*);
//две переменные, смещение синусоиды и текущее положение точки графика
gint count = 0, t = 0;
double radianPerx = 2 * 3.14 / 90; //угол радиан, определяет расстояние между гребнями волн
const double uAmplit = 900; //амплитуда синусоиды

После объявлений — главная функция main, в самом объявлении функции нет ничего необычного. Рассмотрим что твориться в ней.
Первым делом мы должны объявить два виджета, это наше будущее окно и область, в которой мы будем рисовать

Код:

GtkWidget *window, *drawing;

Пока что это просто виджеты, объекты, без указания конкретного типа.
Далее — инициализация gtk

Код:

gtk_init(&argc, &argv);

функции передаются два аргумента — аргументы функции main.
Теперь создаем новое окно и задаем ему необходимые параметры.

Код:

//создаем новое, обычное окно
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
//задаем текст заголовка окна
gtk_window_set_title(GTK_WINDOW(window), "Осциллограф");
//запрещаем изменение размеров окна
gtk_window_set_resizable(GTK_WINDOW(window), FALSE);
//задаем размер окна
gtk_widget_set_size_request(window, 600, 400);
//задаем стартовую позицию - центр экрана
gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
//задаем отступы контейнера окна
gtk_container_set_border_width(GTK_CONTAINER(window), 10);

Тут стоит сказать, что в Gtk, элемент управления не может существовать произвольно, в любом месте. Его может содержать в себе некий невизуальный объект-контейнер. Окно приложения может выполнять функцию контейнера, но может содержать только один объект, для расположения в окне множества элементов в окне — следует расположить в контейнера окна другой контейнер. Сам контейнер может содержать сколь угодно много элементов. Функция gtk_container_set_border_width задает «поля» контейнера окна. Если не вызывать эту функцию — контейнер, содержащий в себе, в данном случае, поле рисования, будет равен размеру самого окна, без заголовка.

Следующей функцией мы создаем новый таймер, с интервалом срабатывания — 100 миллисекунд и привязываем к таким срабатываниями функцию time_handler.

Код:

g_timeout_add(100, (GSourceFunc) time_handler, (gpointer) window);

Далее мы создаем новую область для рисования и помещаем ее в контейнер окна

Код:

//создаем новую область для рисования
drawing = gtk_drawing_area_new();
//помещаем ее в контейнер окна
gtk_container_add(GTK_CONTAINER(window), drawing);

И самое интересное, в главной функции — связывание сигналов с обработчиками, это делается с помощью функции g_signal_connect.

Код:

//связываем сигнал уничтожения окна и функцию-обработчик
g_signal_connect(GTK_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL);
//связываем сигнал перерисовки области рисования и функцию-обработчик
g_signal_connect(G_OBJECT(drawing), "expose_event", G_CALLBACK(draw), NULL);

Первым параметром функции является объект, который мониторится, следующий параметр — название сигнала, третий параметр — функция-обработчик сигнала. Последний параметр, который мы не используем, в данном случае — дополнительный параметр, который может быть передан в функцию-обработчик.

Оставшиеся две функции выполняют отображение всех виджетов на экране и запуск основного цикла программы.

Код:

gtk_widget_show_all(window);
gtk_main();

Теперь рассмотрим собственно функции-обработчики сигналов. Пойдем от простого к сложному.

Функция-обработчик сигнала закрытия окна

Код:

static void destroy(GtkWidget* window, gpointer data)
{
	//завершаем главный цикл приложения
	gtk_main_quit();
}

Здесь все просто, при вызове этой функции происходит вызов gtk_main_quit() и завершение работы программы. Стоит заметить, что если не написать этот обработчик — после закрытия окна, процесс бы оставался висеть в памяти и ожидать сигналов.

Функция-обработчик сигнала таймера

Код:

static gboolean time_handler(GtkWidget* widget)
{
  //если передан нулевой параметр - возвращаем ложь
  if (widget->window == NULL) return FALSE;
  //увеличиваем значение переменной смещения графика
  count++;
  //обнуляем точку положения на графике
  t = 0;
  //заставляем окно перерисоваться
  gtk_widget_queue_draw(widget);
  return TRUE;
}

Здес так же нет ничего замудренного. Сначала выполняется корректность переданного параметра, если он нулевой — функция завершается, вернув FALSE;
Если функция не завершилась — происходит увеление значения переменной, задающей смещение рисуемого графика. Так же обнуляется вспомогательная переменная, указывающая текущую точку на графике. Следующий вызов gtk_widget_queue_draw(widget) объявляет, переданный ему виджет, визуально недействительным и требующим перерисовки.

И наконец самое интересное — функция, выполняющая отрисовку координатной сетки и синусоиды. Для рисования используется библиотека Cairo, «встроенная» в Gtk. Мы будем использовать следующие методы:

gdk_cairo_create(GtkWidget* ) — данная функция создает, на основе переданного ей виджета, новый контекст рисования Cairo.
cairo_rectangle (cr, x1, y1, x2, y2) — данная функция рисует, в контексте рисования Cairo прямоугольник по заданным координатам.
cairo_set_line_width (cr, 3) — данная функция задает ширину линии, которой производится рисования в указанном контексте
cairo_set_source_rgb (cr, r, g, b) — данная функция задает цвет линии, которой производится рисования в указанном контексте, параметры r, g, b определяют, соответственно, интенсивность красного, зеленого и синего цветов.
cairo_move_to(cr, x, y) — с помощью этой функции мы переходим в новую исходную точку, из которой будем рисовать.
cairo_line_to (cr, x, y) — задаем точку, куда следует нарисовать линию.
cairo_stroke (cr) — собственно выполнение рисования в контексте.
cairo_show_text(cr, buf*) — рисование текста в заданном контексте, в точке, предварительно заданной cairo_move_to.

Собственно это все функции, которые выполняют всю работу в данном обработчике. Рисование самой синусоиды производится методом, знакомым по предидущему уроку. Ниже приведен полный исходный код функции, с комментариями

Код:

static void draw(GtkWidget* drawarea, GdkEventExpose* event, gpointer data)
{
	int ya, xa; //переменные для хранения X и Y координат рисования
	char *buf; //текстовый буфер для хранения текста метки
	//создаем область для рисования Cairo
	cairo_t *cr = gdk_cairo_create(drawarea->window);
	//рисуем рамку, размером с область рисования
        cairo_rectangle (cr, event->area.x, event->area.y, event->area.width, event->area.height);
	//задаем толщину линии
	cairo_set_line_width (cr, 3);
	//задачем черный цвет
	cairo_set_source_rgb (cr, 0, 0, 0);
	//рисуем линию оси Y
  	cairo_move_to(cr, 40, 0);
	cairo_line_to (cr, 40, 350);
	cairo_stroke (cr);
	//рисуем линию оси X
	cairo_move_to (cr, 30, 340);
	cairo_line_to (cr, 600, 340);
	cairo_stroke (cr);
	//задаем толщину линии
	cairo_set_line_width (cr, 0.7);
	//рисуем риски на оси X, линии сетки и текстовые метки
	xa = 40;
	while(xa < 600)
	{
		//задаем черный цвет
		cairo_set_source_rgb (cr, 0, 0, 0);
		//рисуем риски
		cairo_move_to (cr, xa, 333);
		cairo_line_to (cr, xa, 347);
		cairo_stroke (cr);
		//преобразовывем значение координаты риски в строку	
		sprintf(buf, "%i", xa-40);
		//рисуем текст метки в нужной точке 
		cairo_move_to (cr, xa-2, 360);
		cairo_show_text(cr, buf);
		//задаем зеленый цвет
		cairo_set_source_rgb (cr, 0, 0.5, 0);
		//рисуем линии сетки
		cairo_move_to (cr, xa+40, 333);
		cairo_line_to (cr, xa+40, 0);
		cairo_stroke (cr);
		xa += 40;
	}
	//рисуем риски на оси Y, линии сетки и текстовые метки
	ya = 340;
	while(ya > 0)
	{
		//задаем черный цвет
		cairo_set_source_rgb (cr, 0, 0, 0);
		cairo_move_to (cr, 33, ya);
		cairo_line_to (cr, 47, ya);
		cairo_stroke (cr);
		//преобразовывем значение координаты риски в строку	
		sprintf(buf, "%i", 340-ya);
		//рисуем текст метки в нужной точке 
		cairo_move_to (cr, 12, ya+2);
		cairo_show_text(cr, buf);
		//задаем зеленый цвет
		cairo_set_source_rgb (cr, 0, 0.5, 0);
		//рисуем линии сетки
		cairo_move_to (cr, 47, ya-40);
		cairo_line_to (cr, 600, ya-40);
		cairo_stroke (cr);
		ya -= 40;
	}
	//рисуем синусоиду
	//задаем красный цвет и толщину линии
	cairo_set_source_rgb (cr, 1, 0, 0);
	cairo_set_line_width (cr, 4);
	//устонавливаем начальную точку синусоиды
 	cairo_move_to(cr, t+40, ((uAmplit * sin(t * radianPerx + count)) * 0.1 + 160));
	//рисуем синусоиду
	while(t < 600)
	{
		cairo_line_to(cr, t+40, ((uAmplit * sin(t * radianPerx + count)) * 0.1 + 160));
		t++;
	}
	cairo_stroke (cr);
}

Компиляция исходного текста в файле osc.c выполняется командой

Код:

gcc -Wall -g osc.c -o osc `pkg-config --cflags gtk+-2.0` `pkg-config --libs gtk+-2.0`

Здесь просходит подключение всех необходимых библиотек, с помощью pkg-config (у вас должен быть установлена эта утилита). Если компилятор будет ругаться — значит у вас не установлены необходимые пакеты с заголовочными файлами.

Скриншот итогового приложения:

Комментарии

  1. Январь 8th, 2011 | 16:31

    Извиняюсь, но где можно скачать все исходники этого приложения, собранные в одном месте?

  2. Oleg Kutkov
    Январь 8th, 2011 | 16:33
  3. Январь 8th, 2011 | 17:56

    Спасибо 🙂

  4. margop
    Август 14th, 2011 | 20:37

    поменял char * buf на
    char buf[128]; //текстовый буфер для хранения текста метки

    иначе ошибка сегментирования и привет

  5. margop
    Август 14th, 2011 | 21:05

    и память съедает всё за минуту
    static void draw(GtkWidget* drawarea, GdkEventExpose* event, gpointer data)
    {
    .
    .
    cairo_destroy(cr); //добавим освобождение ресурсов
    }

    зы. Чё-то многовато ошибок
    ззы. А можно две капчи поставить, а то одни спамеры тусуются