Кроссплатформенный анимированный осциллограф (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 (у вас должен быть установлена эта утилита). Если компилятор будет ругаться — значит у вас не установлены необходимые пакеты с заголовочными файлами.
Скриншот итогового приложения:
Извиняюсь, но где можно скачать все исходники этого приложения, собранные в одном месте?
Вот здесь: http://programmersforum.ru/attachment.php?attachmentid=14474&d=1249867402
Спасибо 🙂
поменял char * buf на
char buf[128]; //текстовый буфер для хранения текста метки
иначе ошибка сегментирования и привет
и память съедает всё за минуту
static void draw(GtkWidget* drawarea, GdkEventExpose* event, gpointer data)
{
.
.
cairo_destroy(cr); //добавим освобождение ресурсов
}
зы. Чё-то многовато ошибок
ззы. А можно две капчи поставить, а то одни спамеры тусуются