Drawing a graph with cairo in GTK3

image

An array of type float acts as input. The program organizes the display, stretching, scrolling of the chart.

The writing style is C with classes (without gtkmm). It turned out not perfect, with flowing abstractions. In particular, callback functions degrade encapsulation, a significant part of the variables have to be moved to the public section.
Basically, callback functions can be placed in a file along with the rest of the functions of the class, which I called graphic_parameters. In GTK, each type of widget has its own signals, some of them are inherited. For example, GtkEventBox has a “button-press-event” signal, but does not have the “configure-event” required to respond to widget resizing, since GtkEventBox always takes the size of the content. And the size of the content is set by hand. You could use the GtkFrame container.

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

In cairo_t, lines, labels are created that are displayed by the cairo_stroke function. When profiling, it turned out that cairo_stroke takes a lot of processor time, so it should be used as little as possible, and the execution time for functions
like cairo_move_to, cairo_line_to is quite small. After cairo_stroke, the contents of cairo_t are cleared and calling cairo_stroke (cr) again will not output anything. You can use
cairo_stroke_preserve to save the content and cairo_save / cairo_restore, but I did not use them.

If the dimensions are changed (by stretching with the mouse, the configure_event_cb signal), then for each drawing it is necessary to delete and recreate cairo_surface_t and cairo_t. If you rewind the schedule, then there is no need to recreate

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

Next, cairo_surface_t is translated into an image

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

This image is then inserted as follows

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);

I removed the prefixes, that is, for example, scrolledwindow is of type GtkScrolledWindow.
Attachment order briefly image-> scrolledwindow-> viewport-> box-> eventbox -> (Frame)
If you remove the scrolledwindow-> viewport containers, the graph will only increase, but not decrease. box adds scrolling. You may notice that there are 3, but 2 of them are not used and are needed only to initialize the necessary containers. In container widgets where 1 child widget fits, the gtk_container_add function is used to insert. g_object_set sets additional properties, in particular the lack of scroll bars for the scrolledwindow widget
. You can also set properties through 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);

Scrolling mechanism: the entire segment is divided by 100, and the schedule change is calculated in the callback function. 100 is taken from the numbers gtk_adjustment_new (0,0,110,1,5,10) as 100 = 110-10.

Further, about parameterization.

To parameterize text, we use the pango library to parameterize labels. It allows you to calculate the size of the text in pixels for a given font and its topographic size and export it to the cairo layer.

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;
}

As you can see, pango counts sizes in its own units. I highlighted a separate class
for the text and its parameters.

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);
};

The chart parameters form a separate class:

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);
};

This class joins the main application class.
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();
};

That is, the graphic_parameters class is created when the application starts, and the content is initialized as necessary by checking for NULL, 0.

The main difficulty was debugging all the transformations. Segfaults happened 3 times: 2 times missed return FALSE in callback functions and did not set the check for exit from the array in the redraw function. Qt has a ready-made QCustomPlot class for plotting, it has much more possibilities.

Link to github


All Articles