Использование GtkApplication. Особенности отрисовки librsvg

в 16:24, , рубрики: C, GTK+, GtkDrawingArea, GtkImage, librsvg, makefile, отрисовка ui компонентов, Программирование, Разработка под Linux

Аннотация статьи.

  • Использование 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 <gtk/gtk.h>

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

<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/example/YourApp">
<file preprocess="xml-stripblanks" compressed="true">window.glade</file>
</gresource>
</gresources>

main.c

<b>main.c</b>
#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, на другую:

Использование GtkApplication. Особенности отрисовки librsvg - 1

В результате изменится 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-2.0/librsvg/rsvg.h>. (Должны быть установлены пакеты 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), который чаще всего представляет собой
рисунок 1x1 с прозрачным пикселем. И потом на эту поверхность(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);
}

В результате получилось:

Использование GtkApplication. Особенности отрисовки librsvg - 2
Если в SVG в параметрах выставлены маленькие значения width и height, то картинка может получиться замыленной при экспорте в png.

Также можно программно изменять width и height. Для этого я создал отдельные файлы
svg_to_pixbuf_class.c и svg_to_pixbuf_class.h. То есть файл открывается в изменяется width, height.

Сохраняется в /dev/shm/. После экспорта информации в svg_handle нужно удалить сам файл и строку-путь к файлу. Дробные значения ширины/длины тоже поддерживаются.

svg_to_pixbuf_class.c


#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <gtk/gtk.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <math.h>
#include <stdbool.h>

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|%cn",char_to_digit(text[i]),text[i]);
			++whole_digits;
			++i;
		}
		else
		{
			if(char_to_digit(text[i])==-1)
			{   printf("fractional flag is truen");
				fractional_flag=TRUE;
				++i;
			}
			else
			{
				fractional_part[fractional_digits]=char_to_digit(text[i]);
				++fractional_digits;
				printf("frac_digit=%d|%cn",char_to_digit(text[i]),text[i]);
				++i;	
			}
		}
	}
	///вычисление непосредственно самого числа
	i=whole_digits;
	result=whole_part[whole_digits];
	while(i>0)
	{
		--i;
		printf("whole=%dn",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=%lfn",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|%cn",char_to_digit(text[i]),text[i]);
			++whole_digits;
			++i;
		}
		else
		{
			if(char_to_digit(text[i])==-1)
			{   printf("fractional flag is truen");
				fractional_flag=TRUE;
				++i;
			}
			else
			{
				fractional_part[fractional_digits]=char_to_digit(text[i]);
				++fractional_digits;
				printf("frac_digit=%d|%cn",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]!='')
	{++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("Размер файла: %ldn", buf.st_size);
        printf("Номер устройства: %lun", 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("Не хватает оперативной памяти для резмещения bodyn");
        }
        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,"<svg");
    int j=0;
    //анализируем содержимое файла
    if(start_search)
    {
		end_search=strstr(start_search,">");
		if(end_search)
		{
			///обработка параметра width
			width_start=strstr(start_search,"width");
			width_end=width_start+strlen("width");
			
			///переход от тега width к его значению
			while(width_end[j]==0x0A||width_end[j]==0x20) ++j;
			if(width_end[j]=='=') ++j;
			while(width_end[j]==0x0A||width_end[j]==0x20) ++j;
			if(width_end[j]!='"')
			printf("Ошибка анализа синтаксиса svg. Отсутсвует кавычки в параметре width=%cn",width_end[j]);
			else ++j; ///кавычка есть
			
			///вычисление количества символов, подлежащих удалению
			gssize size=count_of_digits_for_delete(width_end+j);
			///вычисление относительной позиции (1 позиция - 1 байт)
			gssize pos=width_end+j-body->str;
			///удаляем ненужное значение ширины и вставляем нужное
			g_string_erase(body,pos,size);
			char width_new[8];
			g_snprintf(width_new,8,"%d",width);
			g_string_insert(body, pos, width_new);
			
			///обработка параметра height
			height_start=strstr(start_search,"height");
			height_end=height_start+strlen("height");
			///переход от тега height к его значению
			j=0;
			while(height_end[j]==0x0A||height_end[j]==0x20) ++j;
			if(height_end[j]=='=') ++j;
			while(height_end[j]==0x0A||height_end[j]==0x20) ++j;
			if(height_end[j]!='"')
			printf("Ошибка анализа синтаксиса svg. Отсутсвует
			кавычки в параметре height=%c%c%cn",height_end[j-1],height_end[j],height_end[j+1]);
			else ++j; ///кавычка есть
			
			///вычисление количества символов, подлежащих удалению
			size=count_of_digits_for_delete(height_end+j);
			///вычисление относительной позиции (1 позиция - 1 байт)
			pos=height_end+j-body->str;
			///удаляем ненужное значение высоты и вставляем нужное
			g_string_erase(body,pos,size);
			char height_new[8];
			g_snprintf(height_new,8,"%d",height);
			g_string_insert(body, pos, height_new);
			
			
			///нужно открыть на запись файл в dev/shm/
			///записать изменённый массив
			write_string_to_file(writed_file,body->str,strlen(body->str));
			return writed_file;
			
			//g_free(writed_file);
			
			g_string_free(body,TRUE);
		}
		else
		printf("Ошибка анализа: нет закрывающей скобки у тега svg");
	}
}

void resized_svg_free(char *path)
{
    if (remove (path)==-1 )
    {
        printf("Не удалось удалить файл %sn",path);
    }
}

svg_to_pixbuf_class.h

#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]!=''&&i<1024)
	++i;
	while(argv0[i]!='/'&&i>0)
	--i;
	result[i]='';
	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=%sn",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=%sn",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=%sn",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).

Каждую из них поместим в конструкцию вида(не забывая подключить <time.h>)

clock_t tic = clock();
clock_t toc = clock();
printf("image1_draw_cb elapsed : %f secondsn", (double)(toc - tic) / CLOCKS_PER_SEC);

И в приложении htop видно, как приложение отъедает 20-30% от каждого ядра Athlon 2 X3 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 secondsn", (double)(toc - tic) / CLOCKS_PER_SEC);
return FALSE;
}

Как оказалось, GtkImage имеет свою собственную систему рендеринга, а содержимое image1_draw_cb можно только 1 раз проинициализировать. Закомментированные строки оказались лишними.

Использование GtkApplication. Особенности отрисовки librsvg - 3

Как видно, первый раз рендеринг идёт дольше у GtkImage, чем у GtkDrawingArea, но теоретически обновление картинки должно быть более быстрым. 4 миллиона процессорных циклов на каждую перерисовку изображения размером 220px*220px как-то многовато, а закешировать можно только через pixbuf (как минимум, мне другие способы не известны).

Спасибо за внимание.

Автор: SanyaZ7

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js