Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров5.5K
image

В качестве входных данных выступает массив типа float. Программа организует отображение, растягивание, прокрутку графика.

Стиль написания — Си с классами(без 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 для построения графиков, возможностей у него существенно побольше.

Ссылка на гитхаб

Теги:
Хабы:
Всего голосов 5: ↑4 и ↓1+3
Комментарии0

Публикации

Истории

Работа

Программист С
43 вакансии
Программист C++
133 вакансии
QT разработчик
8 вакансий

Ближайшие события