[Из песочницы] Использование GtkApplication. Особенности отрисовки librsvg
Аннотация статьи.
- Использование GtkApplication. Каркас приложения. Makefile.
- Отрисовка библиотекой librsvg.
- Экспорт изображения в GtkImage и его масшабирование.
- Масштабирование SVG самописными функциями.
- Получение полного пути в приложениях.
- Тесты быстродействия GtkDrawingArea vs GtkImage.
Ранее были статьи (не мои) в хабе GTK+, использующие в примерах функцию void gtk_main (void); класс GtkApplication позволяет явно выделить функции обратного вызова application_activate и application_shutdown. C gtk_main нужно явно подцеплять gtk_main_quit для того, чтобы при нажатии на крестик происходило завершение приложения. GtkApplication завершает приложение при нажатии на крестик, что более логично. Сам каркас приложения состоит из файлов main.h, Makefile, string.gresource.xml, main.c.
main.h
#ifndef MAIN_H
#define MAIN_H
#include
typedef struct{
GtkApplication *restrict app;
GtkWidget *restrict win;
GtkBuilder *restrict builder;
}appdata;
appdata data;
appdata *data_ptr;
#endif
Makefile
здесь универсальный, позволяет компилировать все файлы исходников без указания конкретных имён файлов, но если в папке будут лишние файлы, компилятор будет ругаться.
Можно также использовать CC = g++ -std=c++11, но в функциях обратного вызова поставить
extern «C».
CC = gcc -std=c99
PKGCONFIG = $(shell which pkg-config)
CFLAGS = $(shell $(PKGCONFIG) --cflags gio-2.0 gtk+-3.0 librsvg-2.0) -rdynamic -O3
LIBS = $(shell $(PKGCONFIG) --libs gio-2.0 gtk+-3.0 gmodule-2.0 librsvg-2.0 epoxy) -lm
GLIB_COMPILE_RESOURCES = $(shell $(PKGCONFIG) --variable=glib_compile_resources gio-2.0)
SRC = $(wildcard *.c)
GEN = gresources.c
BIN = main
ALL = $(GEN) $(SRC)
OBJS = $(ALL:.c=.o)
all: $(BIN)
gresources.c: string.gresource.xml $(shell $(GLIB_COMPILE_RESOURCES) --sourcedir=. --generate-dependencies string.gresource.xml)
$(GLIB_COMPILE_RESOURCES) string.gresource.xml --target=$@ --sourcedir=. --generate-source
%.o: %.c
$(CC) $(CFLAGS) -c -o $(@F) $<
$(BIN): $(OBJS)
$(CC) -o $(@F) $(OBJS) $(LIBS)
clean:
@rm -f $(GEN) $(OBJS) $(BIN)
string.gresource.xml
cлужит для включения ресурсов в исполняемый файл, в данном случае это файл описания интерфейса window.glade
window.glade
main.c
main.c
#include "main.h"
GtkBuilder* builder_init(void)
{
GError *error = NULL;
data.builder = gtk_builder_new();
if (!gtk_builder_add_from_resource (data.builder, "/com/example/YourApp/window.glade", &error))
{
// загрузить файл не удалось
g_critical ("Не могу загрузить файл: %s", error->message);
g_error_free (error);
}
gtk_builder_connect_signals (data.builder,NULL);
return data.builder;
}
void application_activate(GtkApplication *application, gpointer user_data)
{
GtkBuilder *builder=builder_init();
data_ptr=&data;
data.win=GTK_WIDGET(gtk_builder_get_object(builder, "window1"));
gtk_widget_set_size_request(data.win,360,240);
gtk_application_add_window(data.app,GTK_WINDOW(data.win));
gtk_widget_show_all(data.win);
}
void application_shutdown(GtkApplication *application, gpointer user_data)
{
g_object_unref(data.builder);
}
int main (int argc, char *argv[])
{
gtk_init (&argc, &argv);
gint res;
data.app = gtk_application_new("gtk.org", G_APPLICATION_FLAGS_NONE);
g_signal_connect(data.app, "activate", G_CALLBACK(application_activate), NULL);
g_signal_connect(data.app, "shutdown", G_CALLBACK(application_shutdown), NULL);
res = g_application_run(G_APPLICATION(data.app), 0, NULL);
return 0;
}
В первом аргументе функции gtk_application_new можно разместить любой текст, но без точки у меня не работало. В этом примере также опущен файл window.glade, который можно создать в UI редакторе Glade.
Разделим окно контейнером GtkBox на 2 части, в одну из них поместим GtkDrawingArea, на другую:
В результате изменится appdata
typedef struct{
GtkApplication *restrict app;
GtkWidget *restrict win;
GtkBuilder *restrict builder;
GtkDrawingArea *restrict draw;
GtkImage *restrict image;
GtkEventBox *restrict eventbox1;
RsvgHandle *restrict svg_handle_image;
RsvgHandle *restrict svg_handle_svg;
GdkPixbuf *pixbuf;
cairo_t *restrict cr;
cairo_surface_t *restrict surf;
}appdata;
И соответственно инициализация.
void application_activate(GtkApplication *application, gpointer user_data)
{
GtkBuilder *builder=builder_init();
data_ptr=&data;
data.win=GTK_WIDGET(gtk_builder_get_object(builder, "window1"));
data.draw=GTK_DRAWING_AREA(gtk_builder_get_object(builder, "drawingarea1"));
data.image=GTK_IMAGE(gtk_builder_get_object(builder, "image1"));
gtk_widget_set_size_request(data.win,640,480);
gtk_application_add_window(data.app,GTK_WINDOW(data.win));
gtk_widget_show_all(data.win);
}
Добавим путь #include. (Должны быть установлены пакеты librsvg и librsvg-dev).
Имена функций обратного вызова берутся из файла .glade, за это отвечает функция
gtk_builder_connect_signals (data.builder, NULL);
gboolean
drawingarea1_draw_cb (GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
if(!data.svg_handle_svg)
{
data.svg_handle_svg=rsvg_handle_new_from_file("compassmarkings.svg",NULL);
}
gboolean result=rsvg_handle_render_cairo(data.svg_handle_svg,cr);
if(result&&cr)
{cairo_stroke(cr);}
else
printf("Ошибка отрисовки\n");
return FALSE;
}
В кое-каких ситуациях (например, HMI) может потребоваться изменение размеров SVG. Можно
менять параметры width и height в SVG файле. Или перевести в GtkPixbuf и там уже произвести масштабирование. Так как GtkImage не наследуется от GtkBin, то не может иметь собственные события типа ButtonClick (события, связанные с курсором). Для этого имеется пустой контейнер — GtkEventBox. А саму непосредственно отрисовку можно повесить прямо на GtkImage.
gboolean
image1_draw_cb (GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
if(!data.svg_handle_image)
{
data.svg_handle_image=rsvg_handle_new_from_file("compassmarkings.svg",NULL);
data.surf=cairo_image_surface_create_from_png("2.png");
data.pixbuf=rsvg_handle_get_pixbuf(data.svg_handle_image);
}
if(data.pixbuf)
{
cairo_set_source_surface(cr,data.surf,0,0);
GdkPixbuf *dest=gdk_pixbuf_scale_simple (data.pixbuf,250,250,GDK_INTERP_BILINEAR);
gtk_image_set_from_pixbuf (data.image,dest);
g_object_unref(dest);
cairo_paint(cr);
}
}
В этой функции загружается фоновый рисунок (2.png), который чаще всего представляет собой
рисунок 1×1 с прозрачным пикселем. И потом на эту поверхность (surface) рендерится рисунок (pixbuf) и далее происходит масшабирование и экспорт в картинку (image).
И нельзя забывать про очистку памяти.
void application_shutdown(GtkApplication *application, gpointer user_data)
{
cairo_surface_destroy(data.surf);
g_object_unref(data.svg_handle_image);
g_object_unref(data.svg_handle_svg);
g_object_unref(data.pixbuf);
g_object_unref(data.builder);
}
В результате получилось:
Если в SVG в параметрах выставлены маленькие значения width и height, то картинка может получиться замыленной при экспорте в png.
Также можно программно изменять width и height. Для этого я создал отдельные файлы
svg_to_pixbuf_class.c и svg_to_pixbuf_class.h. То есть файл открывается в изменяется width, height.
Сохраняется в /dev/shm/. После экспорта информации в svg_handle нужно удалить сам файл и строку-путь к файлу. Дробные значения ширины/длины тоже поддерживаются.
#include
#include
#include
#include
#include
#include
#include
#include
#include
int char_to_digit(char num)
{
switch(num)
{
case '0': return 0;
case '1': return 1;
case '2': return 2;
case '3': return 3;
case '4': return 4;
case '5': return 5;
case '6': return 6;
case '7': return 7;
case '8': return 8;
case '9': return 9;
case '.': return -1;
default: return -2;
}
}
//считывает число с позиции указателя text
double read_num_in_text(char* text)
{
double result=0;
int i=0;
bool fractional_flag=FALSE;
char whole_part[16]={0};
char whole_digits=0;
char fractional_part[16]={0};
char fractional_digits=0;
while(char_to_digit(text[i])!=-2)
{
if(char_to_digit(text[i])!=-1&&!fractional_flag)
{
whole_part[whole_digits]=char_to_digit(text[i]);
printf("text_num=%d|%c\n",char_to_digit(text[i]),text[i]);
++whole_digits;
++i;
}
else
{
if(char_to_digit(text[i])==-1)
{ printf("fractional flag is true\n");
fractional_flag=TRUE;
++i;
}
else
{
fractional_part[fractional_digits]=char_to_digit(text[i]);
++fractional_digits;
printf("frac_digit=%d|%c\n",char_to_digit(text[i]),text[i]);
++i;
}
}
}
///вычисление непосредственно самого числа
i=whole_digits;
result=whole_part[whole_digits];
while(i>0)
{
--i;
printf("whole=%d\n",whole_part[i]);
result=result+pow(10,whole_digits-i-1)*whole_part[i];
}
i=0;
while(i<=fractional_digits)
{
result=result+pow(0.1,i+1)*fractional_part[i];
++i;
}
printf("result_read_num=%lf\n",result);
return result;
}
//подситывает количество символов, которые надо удалить
//
int count_of_digits_for_delete(char* text)
{
int i=0;
bool fractional_flag=FALSE;
char whole_part[16]={0};
int whole_digits=0;
char fractional_part[16]={0};
int fractional_digits=0;
while(char_to_digit(text[i])!=-2)
{
if(char_to_digit(text[i])!=-1&&!fractional_flag)
{
whole_part[whole_digits]=char_to_digit(text[i]);
printf("text_num=%d|%c\n",char_to_digit(text[i]),text[i]);
++whole_digits;
++i;
}
else
{
if(char_to_digit(text[i])==-1)
{ printf("fractional flag is true\n");
fractional_flag=TRUE;
++i;
}
else
{
fractional_part[fractional_digits]=char_to_digit(text[i]);
++fractional_digits;
printf("frac_digit=%d|%c\n",char_to_digit(text[i]),text[i]);
++i;
}
}
}
if(fractional_flag)
return whole_digits+1+fractional_digits;
else
return whole_digits;
}
//создаёт пустой файл в каталоге рамдиска /dev/shm
//с именем совпадающим с названием файла
char* create_dump_file(char *file_with_path)
{
char *file=NULL;
int i=0;
while(file_with_path[i]!='\0')
{++i;}
while(file_with_path[i]!='/'&&i>0)
{--i;}
file=file_with_path+i;
GString *string=g_string_new("test -f /dev/shm");
g_string_append(string,file);
g_string_append(string,"|| touch /dev/shm/");
g_string_append(string,file);
system(string->str);
///нужно сформировать строку-полный путь
GString *full_path=g_string_new("/dev/shm");
g_string_append(full_path,file);
char *result=g_string_free(full_path,FALSE);
return result;
}
//result must be freed with g_string_free
GString* read_file_in_buffer(char *file_with_path)
{
FILE *input = NULL;
struct stat buf;
int fh, result;
char *body=NULL; //содержимое
GString *resultat=g_string_new("");
fh=open(file_with_path, O_RDONLY);
result=fstat(fh, &buf);
if (result !=0)
printf("Плох дескриптор файла\n");
else
{
printf("%s",file_with_path);
printf("Размер файла: %ld\n", buf.st_size);
printf("Номер устройства: %lu\n", buf.st_dev);
printf("Время модификации: %s", ctime(&buf.st_atime));
input = fopen(file_with_path, "r");
if (input == NULL)
{
printf("Error opening file");
}
body=(char*)calloc(buf.st_size+64,sizeof(char)); //дополнительная память для цифр
//проверяем хватило ли памяти
if(body==NULL)
{
printf("Не хватает оперативной памяти для резмещения body\n");
}
int size_count=fread(body,sizeof(char),buf.st_size, input);
if(size_count!=buf.st_size)
printf("Считался не весь файл");
resultat=g_string_append(resultat,body);
free(body);
}
fclose(input);
return resultat;
}
void* write_string_to_file(char* writed_file, char* str_for_write, int lenght)
{
FILE * ptrFile = fopen (writed_file ,"wb");
size_t writed_byte_count=fwrite(str_for_write,1,lenght,ptrFile);
//if(writed_byte_count>4) return TRUE;
//else return FALSE;
fclose(ptrFile);
}
//возвращаемый результат нужно удалить при помощи g_free
char* get_resized_svg(char *file_with_path, int width, int height)
{
char *writed_file=create_dump_file(file_with_path);
//открываем файл и копируем содержимое в буфер
GString *body=read_file_in_buffer(file_with_path);
char *start_search=NULL;
char *end_search=NULL;
char *width_start=NULL;
char *width_end=NULL;
char *height_start=NULL;
char *height_end=NULL;
start_search=strstr(body->str,"
#ifndef SVG_TO_PIXBUF_CLASS_H
#define SVG_TO_PIXBUF_CLASS_H
void resized_svg_free(char *path);
char* get_resized_svg(char *file_with_path, int width, int height); //result must be freed with g_free()
#endif
Теперь изменим размер левой части (которая GtkDrawingArea)
gboolean
drawingarea1_draw_cb (GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
if(!data.svg_handle_svg)
{
char* path=get_resized_svg("/home/alex/svg_habr/compassmarkings.svg", 220, 220);
data.svg_handle_svg=rsvg_handle_new_from_file(path,NULL);
resized_svg_free(path);
g_free(path);
}
gboolean result=rsvg_handle_render_cairo(data.svg_handle_svg,cr);
if(result&&cr)
{cairo_stroke(cr);}
else
printf("Ошибка отрисовки\n");
return FALSE;
}
Как видим, здесь есть неприятная особенность — полный путь. То есть стоит переместить папку, как левая часть (которая GtkDrawingArea) перестанет отображаться. Это же касается всех ресурсов, которые не вошли в исполняемый файл. Для этого я написал функцию, которая вычисляет полный путь к запускаемому файлу вне зависимости от способа запуска.
//результат экспортируется в data.path
void get_real_path(char *argv0)
{
char* result=(char*)calloc(1024,sizeof(char));
char* cwd=(char*)calloc(1024,sizeof(char));
getcwd(cwd, 1024);
int i=0;
while(argv0[i]!='\0'&&i<1024)
++i;
while(argv0[i]!='/'&&i>0)
--i;
result[i]='\0';
while(i>0)
{
--i;
result[i]=argv0[i];
}
/*alex@alex-System-Product-Name:~/project_manager$ ./manager.elf
argv[0]=./manager.elf
path=/home/alex/project_manager*/
if(strlen(result)<=strlen(cwd)) //путь слишком короткий
{
free(result);
strcpy(data.path,cwd);
strcat(data.path,"/");
//printf("path_cwd=%s\n",cwd);
free(cwd);}
else
{
/*alex@alex-System-Product-Name:/home$ '/home/alex/project_manager/manager.elf'
argv[0]=/home/alex/project_manager/manager.elf
path=/home*/
free(cwd);
strcpy(data.path,result);
strcat(data.path,"/");
//printf("path_result=%s\n",result);
free(result);
}
}
В самом коде есть 2 примера того, как можно запустить файл manager.elf. Ещё нужно в начало функции main () поместить
char cwd[1024];
getcwd(cwd, sizeof(cwd));
get_real_path(argv[0]);
Функция отрисовки примет следующий вид
gboolean
drawingarea1_draw_cb (GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
if(!data.svg_handle_svg)
{
char image_path[1024];
strcat(image_path,data.path);
strcat(image_path,"compassmarkings.svg");
printf("image_path=%s\n",image_path);
char* path=get_resized_svg(image_path, 220, 220);
data.svg_handle_svg=rsvg_handle_new_from_file(path,NULL);
resized_svg_free(path);
g_free(path);
}
gboolean result=rsvg_handle_render_cairo(data.svg_handle_svg,cr);
if(result&&cr)
{cairo_stroke(cr);}
else
printf("Ошибка отрисовки\n");
return FALSE;
}
Тесты быстройдействия.
У нас есть 2 функции отрисовки (GtkDrawingArea и GtkImage).
Каждую из них поместим в конструкцию вида (не забывая подключить
clock_t tic = clock();
clock_t toc = clock();
printf("image1_draw_cb elapsed : %f seconds\n", (double)(toc - tic) / CLOCKS_PER_SEC);
И в приложении htop видно, как приложение отъедает 20–30% от каждого ядра Athlon 2×3 2.5 ГГц.
Ошибка нашлась быстро.
gboolean
image1_draw_cb (GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
clock_t tic = clock();
if(!data.svg_handle_image)
{
data.svg_handle_image=rsvg_handle_new_from_file("compassmarkings.svg",NULL);
data.surf=cairo_image_surface_create_from_png("2.png");
data.pixbuf=rsvg_handle_get_pixbuf(data.svg_handle_image);
//}
//if(data.pixbuf)
// {
cairo_set_source_surface(cr,data.surf,0,0);
GdkPixbuf *dest=gdk_pixbuf_scale_simple (data.pixbuf,250,250,GDK_INTERP_BILINEAR);
gtk_image_set_from_pixbuf (data.image,dest);
g_object_unref(dest);
//cairo_paint(cr);
}
clock_t toc = clock();
printf("image1_draw_cb elapsed : %f seconds\n", (double)(toc - tic) / CLOCKS_PER_SEC);
return FALSE;
}
Как оказалось, GtkImage имеет свою собственную систему рендеринга, а содержимое image1_draw_cb можно только 1 раз проинициализировать. Закомментированные строки оказались лишними.
Как видно, первый раз рендеринг идёт дольше у GtkImage, чем у GtkDrawingArea, но теоретически обновление картинки должно быть более быстрым. 4 миллиона процессорных циклов на каждую перерисовку изображения размером 220 px*220 px как-то многовато, а закешировать можно только через pixbuf (как минимум, мне другие способы не известны).
Спасибо за внимание.