Рисование графика при помощи cairo в GTK3

от автора

image

Стиль написания — Си с классами(без gtkmm). Получилось не идеально, с протекающими абстракциями. В частности функции обратного вызова ухудшают инкапсуляцию, значительную часть переменных приходится перемещать в секцию public.

В принципе, функции обратного вызова можно поместить в файл вместе с остальными функциями класса, который я назвал graphic_parameters. В GTK каждый тип виджета имеет собственные сигналы, какая-то часть из них наследуется. Например, GtkEventBox имеет сигнал «button-press-event», но не имеет «configure-event», необходимый для реакции на изменение размеров виджета, так как GtkEventBox всегда принимает размер содержимого. А размер содержимого задаётся руками. Можно было использовать контейнер GtkFrame.

cairo_surface_t  *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); cairo_t  *cr = cairo_create(surface); 

В cairo_t создаются, линии, надписи, которые выводятся функцией cairo_stroke. При профилировании выяснилось, что cairo_stroke занимает достаточно много процессорного времени, поэтому её следует использовать как можно реже, а время выполнения функций
типа cairo_move_to, cairo_line_to достаточно малое. После cairo_stroke содержимое cairo_t очищается и повторный вызов cairo_stroke(cr) ничего не выведет. Можно использовать
cairo_stroke_preserve для сохранения содержимого и cairo_save/cairo_restore, но я их не использовал.

Если изменяются размеры (растягиванием мышью, сигнал configure_event_cb), то на каждую отрисовку необходимо удалять и заново создавать cairo_surface_t и cairo_t. Если же перематывать график, то пересоздавать нет необходимости

    cairo_set_source_rgb(cr,0.8,0.8,0.8);     cairo_paint(cr); 

Далее cairo_surface_t переводится в изображение

void gtk_image_set_from_surface (GtkImage *image, cairo_surface_t *surface); 

Это изображение далее вставляется следующим образом

eventbox=gtk_event_box_new();     g_signal_connect(eventbox,"button-press-event", G_CALLBACK(eventbox_press_cb), this);     GtkAdjustment *adj_h=gtk_adjustment_new(0,0,100,1,5,10);     GtkAdjustment *adj_v=gtk_adjustment_new(0,0,100,1,5,10);     GtkWidget *viewport=gtk_viewport_new(adj_h, adj_v);     scrolledwindow=gtk_scrolled_window_new(adj_h, adj_v);     g_object_set(scrolledwindow, "hscrollbar-policy", GTK_POLICY_EXTERNAL, "vscrollbar-policy", GTK_POLICY_EXTERNAL, NULL);     gtk_container_add(GTK_CONTAINER(viewport), scrolledwindow);     gtk_widget_set_events(scrolledwindow, GDK_SCROLL_MASK);      g_signal_connect(scrolledwindow,"scroll-event",G_CALLBACK(eventbox_scroll_cb), this);     GtkWidget *box=gtk_box_new(GTK_ORIENTATION_VERTICAL,0);     adj=gtk_adjustment_new(0,0,110,1,5,10);     g_signal_connect(adj,"value-changed", G_CALLBACK(adj_changed_cb), this);     scrollbar=gtk_scrollbar_new(GTK_ORIENTATION_HORIZONTAL,adj);     gtk_box_pack_end(GTK_BOX(box),scrollbar, FALSE,FALSE,0);     image_from_surface=gtk_image_new_from_surface(surface);     gtk_container_add(GTK_CONTAINER(scrolledwindow),image_from_surface);     gtk_box_pack_start(GTK_BOX(box),viewport, TRUE,TRUE,0);     gtk_container_add(GTK_CONTAINER(eventbox),box); 

Я убрал приставки, то есть, для примера, scrolledwindow имеет тип GtkScrolledWindow.
Порядок вложений кратко image->scrolledwindow->viewport->box->eventbox->(Frame)
Если убрать контейнеры scrolledwindow->viewport, то график будет только увеличиваться, но не уменьшаться. box добавляет прокрутку. Можно заметить, что их 3, но 2 не используются и нужны только для инициализации нужных контейнеров. В тех виджетах-контейнерах, куда влезает 1 дочерний виджет, используется для вставки функция gtk_container_add. g_object_set устанавливает дополнительные свойства, в частности отсутствие полос прокрутки у виджета
scrolledwindow. Можно устанавливать также свойства через GValue

    GValue val = G_VALUE_INIT;     g_value_init(&val, G_TYPE_BOOLEAN);     g_value_set_boolean(&val, TRUE);     gtk_container_child_set_property(GTK_CONTAINER(data->notebook), gr, "tab-expand", &val); 

Механизм прокрутки: весь отрезок делится на 100, и в функции обратного вызова высчитывается изменение графика. 100 берётся из чисел gtk_adjustment_new(0,0,110,1,5,10) как 100=110-10.

Далее про параметризацию.
Чтобы параметризовать текст, используем библиотеку pango для параметризации надписей. Она позволяет подсчитать размеры текста в пикселях при заданном шрифте и его топографическом размере и экспортировать его в слой cairo.

PangoLayout* get_width_height_of_text(char *text, char *font, float size, float *w, float *h) {     GdkScreen *screen = gdk_screen_get_default();     PangoContext *context = gdk_pango_context_get_for_screen (screen);     PangoLayout *layout = pango_layout_new (context);     if(g_utf8_validate(text,-1,0))     {         pango_layout_set_text(layout,text,-1);         PangoFontDescription *desc=pango_font_description_new();         pango_font_description_set_family(desc,font);         pango_font_description_set_size(desc,size*1024);         pango_layout_set_font_description (layout, desc);         int width=0,height=0;         pango_layout_get_size(layout, &width, &height);         *w=(float) width/1024;         *h=(float) height/1024;         pango_font_description_free(desc);     }     else     {         printf("Текст не является валидным в кодировке UTF8\n");     }     return layout; } 

Как видно, pango считает размеры в собственных единицах. Я выделил отдельный класс
под текст и его параметры.

class text_layout {     private:     int fontsize;     public:     GString *text;     GString *font;     PangoLayout *layout;     int width;     int height;     text_layout(char *text, char *font, int fontsize);     void change_text_font_fontsize(char *new_text, char *new_font, int new_fontsize);     ~text_layout();     text_layout(float num, char *font, int fontsize); }; 

Параметры графика образуют отдельный класс

class graphic_parameters {     private:      text_layout y_text=text_layout("Ось y","Liberation Serif", 14);      text_layout x_text=text_layout("Ось x","Liberation Serif", 14);      text_layout *number=0; ///текущее число для отображения      float max=0;      float min=0;      text_layout *max_=0;      text_layout *min_=0;      GtkAdjustment *adj;     GtkWidget *scrollbar;      float gap_x=25; ///зазор между надписью и осями      float gap_y=5; ///зазор между надписью и осями      void create_axes_and_xy_labels(void);      public:     cairo_t *cr;     float *massiv=0; ///массив для графика     int len=0; ///длина массива     int count_in_display=0; ///отображаемое количество элементов массива     float multiplier_x=6;     int offset=0;     float x_null=0;     float y_null=0;     int pos=0;/// вертикальная линия     float margin=16;      int callback_width;  ///новые размеры     int callback_height;     int widget_width;     int widget_height;     int scroll_height=0;     GtkWidget *eventbox;     GtkWidget *scrolledwindow;     GtkWidget *image_from_surface;     cairo_surface_t *surface;      graphic_parameters(int width, int height);     ~graphic_parameters();     void resize_graphic(int new_width, int new_height);     void create_one_dimensional_graphic(float *massiv, int size);     void update_graphic(int offset);     void change_graphic_adj(void);     void create_vertical_line(void); }; 

Этот класс присоединяется к освновному классу приложения

class externals {     public:     graphic_parameters *param;     externals(); };  class appdata : public externals {     public:     char *glade_name=(char*)"window.glade";     GtkApplication *app;     GtkWidget *win;     GtkNotebook *notebook;     GtkMenuBar *menubar;     appdata(); }; 

То есть класс graphic_parameters создаётся при запуске приложения, а содержимое инициализируется по мере необходимости проверкой на NULL, 0.

Основная сложность заключалась в отладке всех преобразований. Сегфолты случались 3 раза: 2 раза пропустил return FALSE в функциях обратного вызова и в функции перерисовки не поставил проверку на выход из массива. В Qt есть уже готовый класс QCustomPlot для построения графиков, возможностей у него существенно побольше.
Ссылка на репозиторий: github.com/SanyaZ7/graphic,

ссылка на оригинал статьи https://habr.com/ru/post/496792/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *