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