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