GObject inne podejście do programowania w Gtk+ w języku C
Cała biblioteka Gtk+ mimo, że została napisana w języku C pozwala programować obiektowo dzięki zastosowaniu GObject. Dodatkowa informacja, która Cię zaskoczy to ta, że sama biblioteka Gtk+ została napisana przy użyciu GObject.
Co to nam daje ? Pierwszą zaletą jak już na wstępie wspomniałem możliwość programowania obiektowego, każda kontrolka defakto jest obiektem. Drugą to szybkość programowania przez zaoszczędzenie ilości wpisanego kodu.
Ponieważ nie jest to opis biblioteki GObject, skupię się w jaki sposób używać tylko pewnych funkcji tejże biblioteki w zastosowaniu ich w programowaniu w Gtk+.
W klasycznym podejściu programując w języku C pod Gtk+ musisz znać nazwy funkcji tworzące kontrolkę (przyjmijmy, że kontrolka jest obiektem), przykładowo by stworzyć kontrolkę przycisku należało użyć funkcji
gtk_button_new, ewentualnie kiedy chcieliśmy utworzyć przycisk z tekstem
gtk_button_new_with_label. Żeby dobrze i sprawnie programować potrzebujesz znać bardzo wiele funkcji, których długość w znakach jest również różna. Poniżej przykład w klasycznym programowaniu w języku C biblioteki Gtk+ (wykorzystany przykład z kursu m4tx):
#include <gtk/gtk.h>
void zmien_etykiete( GtkWidget * widget )
{
if( gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON( widget ) ) )
gtk_button_set_label( GTK_BUTTON( widget ), "Wciśnięto!" );
else
gtk_button_set_label( GTK_BUTTON( widget ), "Nie wciśnięto!" );
}
int main( int argc, char * argv[] )
{
GtkWidget * okno;
GtkWidget * vbox;
GtkWidget * checkButton;
GtkWidget * toggleButton;
gtk_init( & argc, & argv );
okno = gtk_window_new( GTK_WINDOW_TOPLEVEL );
gtk_window_set_position( GTK_WINDOW( okno ), GTK_WIN_POS_CENTER );
gtk_window_set_default_size( GTK_WINDOW( okno ), 150, 75 );
gtk_window_set_title( GTK_WINDOW( okno ), "Kurs GTK+" );
gtk_container_set_border_width( GTK_CONTAINER( okno ), 5 );
vbox = gtk_vbox_new( FALSE, 1 );
gtk_container_add( GTK_CONTAINER( okno ), vbox );
checkButton = gtk_check_button_new_with_label( "Nie wciśnięto!" );
toggleButton = gtk_toggle_button_new_with_label( "Nie wciśnięto!" );
gtk_box_pack_start( GTK_BOX( vbox ), checkButton, TRUE, TRUE, 0 );
gtk_box_pack_start( GTK_BOX( vbox ), toggleButton, TRUE, TRUE, 0 );
g_signal_connect( G_OBJECT( okno ), "destroy", G_CALLBACK( gtk_main_quit ), NULL );
g_signal_connect( checkButton, "clicked", G_CALLBACK( zmien_etykiete ), NULL );
g_signal_connect( toggleButton, "clicked", G_CALLBACK( zmien_etykiete ), NULL );
gtk_widget_show_all( okno );
gtk_main();
return 0;
}
Poniżej przykład używający funkcji biblioteki GObject:
#include <gtk/gtk.h>
void zmien_etykiete( GtkWidget * widget )
{
gboolean i;
g_object_get( G_OBJECT( widget ), "active", & i, NULL );
if( i )
g_object_set( G_OBJECT( widget ), "label", "Wciśnięto!", NULL );
else
g_object_set( G_OBJECT( widget ), "label", "Nie wciśnięto!", NULL );
}
int main( int argc, char * argv[] )
{
GtkWidget * okno;
GtkWidget * vbox;
GtkWidget * checkButton;
GtkWidget * toggleButton;
gtk_init( & argc, & argv );
okno = g_object_new( GTK_TYPE_WINDOW, "window-position", GTK_WIN_POS_CENTER,
"default-width", 150,
"default-height", 75,
"title", "Kurs GTK+",
"border-width", 5, NULL );
vbox = g_object_new( GTK_TYPE_VBOX, "spacing", 1, NULL );
gtk_container_add( GTK_CONTAINER( okno ), vbox );
checkButton = g_object_new( GTK_TYPE_CHECK_BUTTON, "label", "Nie wciśnięto!", NULL );
toggleButton = g_object_new( GTK_TYPE_TOGGLE_BUTTON, "label", "Nie wciśnięto!", NULL );
gtk_box_pack_start( GTK_BOX( vbox ), checkButton, TRUE, TRUE, 0 );
gtk_box_pack_start( GTK_BOX( vbox ), toggleButton, TRUE, TRUE, 0 );
g_signal_connect( G_OBJECT( okno ), "destroy", G_CALLBACK( gtk_main_quit ), NULL );
g_signal_connect( checkButton, "clicked", G_CALLBACK( zmien_etykiete ), NULL );
g_signal_connect( toggleButton, "clicked", G_CALLBACK( zmien_etykiete ), NULL );
gtk_widget_show_all( okno );
gtk_main();
return 0;
}
Jak widać nie trzeba znać różnorodnych funkcji tworzących kontrolki tylko znajomość jednej funkcji
g_object_new i typy tworzonych obiektów / kontrolek. O wiele łatwiej jest zapamiętać typy niż funkcje poza tym tak jak to przystało na programowanie obiektowe możemy korzystać z cech istniejących obiektów. Tworząc nowy obiekt można wykorzystać już istniejący dodając do niego pewne cechy. Przykładowo kontrolka GtkToggleButton bazuje na kontrolce GtkButton, a nawet wykorzystuje jej pewne cechy dodając kilka swoich. W Gtk+ te cechy będziemy nazwać właściwościami.
Oznacza to: Kontrolka GtkCheckButton wywodzi się z kontrolki GtkToggleButton, GtkToggleButton wywodzi się GtkButton, GtkButton wywodzi się z obiektu GtkBin, ..., natomiast GInitiallyUnowned z objektu GObject. tak na marginesie wszystkie kontrolki Gtk+ wywodzą się z objektu GObject.
Tak, więc jeżeli mam kontrolkę niższego poziomu np. GtkToggleButton, której właściwościami według
http://developer.gnome.org/gtk/2.24/GtkToggleButton.html#GtkToggleButton.properties są:
Znamy powyższe właściwości, lecz jak zmienić np. tekst w GtkToggleButton ? Należy iść w górę w hierarchii kontrolek i szukać właściwości pozwalającej na ustawienie tekstu. Idąc w górę nadrzędną kontrolka jest GtkButton, więc zaglądamy w jej właściwości, np. w dokumentacji biblioteki zawartej na http://developer.gnome.org/gtk/2.24/GtkButton.html#GtkButton.properties
znajdujemy tam
"label" - eureka to jest to. Teraz tylko pozostaje ustawić tą właściwość odpowiednią wartością poprzez funkcję:
void g_object_set( gpointer object, const gchar * first_property_name,...);
Ustawia na obiekcie podanym jako pierwszy argument, właściwość podaną jako tekst (zdaje się, że wszyskie właściwości kontrolek Gtk+ są tekstami) odpowiednią wartość następującą po nazwie tej właściwości. Jest to funkcja wieloargumentowa, a co z tym się wiąże można ustawić wiele właściwości. Argumentem informującym funkcję o przetwarzaniu argumentów jest wartość NULL.
Przykładowe użycie:
g_object_set( G_OBJECT( przycisk ), "label", "_To jest przycisk", "use-underline", TRUE, NULL );
Ponieważ zwykle pierwszy argument nie jest typu GObject należy przekształcić makrem G_OBJECT ten argument. Ustawia na przycisk, który został przekształcony na G_OBJECT właściwość "label" na wartość "_To jest przycisk", czyli krótko mówiąc ustawi tekst w przycisku na "To jest przycisk". Następnie właściwość "use-underline" na wartość TRUE, co oznacza że pierwsza litera tekstu będzie skrótem do tego przycisku (Alt + t). Oczywiście na końcu wartość NULL.
Patrząc na funkcję
zmien_etykiete nie została zmniejszona ilość kodu, a nawet należało użyć dodatkowej zmiennej. Dlatego pokaże w jaki sposób można się dostać właściwości kontrolek:
void zmien_etykiete( GtkWidget * widget )
{
if( GTK_TOGGLE_BUTTON( widget )->active )
g_object_set( G_OBJECT( widget ), "label", "Wciśnięto!", NULL );
else
g_object_set( G_OBJECT( widget ), "label", "Nie wciśnięto!", NULL );
}
Skoro jest objektem dlaczego nie odwoływać się do składowych obiektu :) ? Uwaga nie wszystkie składowe są publiczne !
Ilość linii kodu identyczna co do wersji klasycznej. Jednak jest mała przewaga, funkcja
g_object_set ustawi za jednym zamachem wiele właściwości. W klasycznym podejściu należy użyć kilku funkcji, często różnych.
Dodawanie dodatkowych parametrów do obiektów / kontrolek
Kolejnym fajnym aspektem GObject jest to że kontrolką / obiektom można w dowolnej chwili dodawać dodatkowe dane za pomocą funkcji:
void g_object_set_data( GObject * object, const gchar * key, gpointer data );
Pierwszym argumentem jest wskaźnik na objekt, drugim tekst, który będzie identyfikował dołączaną zmienną, trzecim zmienna, która jest typu gpointer. Aby przekształcić konkretną zmienną na gpointer używa się najczęściej makr:
Chcąc pobrać dodatkową zmienną skorzystamy z funkcji:
gpointer g_object_get_data( GObject * object, const gchar * key );
Zwraca zmienną jak gpointer, czyli wskaźnik void*. Argumentami są wskaźnik na obiekt, z którego chcemy odczytać dane, oraz tekst identyfikujący zmienną ustawiony funkcją
g_object_set_data. W przypadku kiedy nie odnajdzie zmiennej skojarzonej z danym łańcuchem tekstowym zwróci NULL. Aby przekształcić zwrócony gpointer na zmienną odpowiedniego typu najczęściej używa się makr:
Ktoś może zastanawiać się po co dodatkowe dane dołączać do kontrolki ? Odpowiedź jest prosta, łatwość programowania, a nawet szybkość wykonywania programu, chociaż zwiększa się minimalnie ilość zajmowanej pamięci prze obiekt.
Dla przykładu załóżmy, że masz bardzo wiele przycisków, które skojarzone są z sygnałem clicked, który to sygnał obsługuje tylko jedna funkcja dla wszystkich przycisków.
Pierwsze co przychodzi do głowy to w funkcji obsługującej przycisk pobrać tekst przycisku i porównywać ten tekst z jakimiś łańcuchami tekstowi, i jeżeli np. są zgodne odpowiednio zareagować. Poniżej przykład w pseudo kodzie:
void wcisnieto_przycisk( GtkWidget * przycisk, gpointer dane )
{
gchar * tekst_przycisku;
tekst_przycisku = g_strdup( gtk_button_get_label( przycisk ) );
if( tekst_przycisku == "Przycisk A" )
else if( tekst_przycisku == "Przycisk B" )
[...]
else if( tekst_przycisku == "Przycisk Z" )
}
Porównywanie łańcuchów tekstowych jest stosunkowo czasochłonne, pól biedy kiedy nazwy przycisków były by tylko tekstami liczb ("1", "2"..., "100") wystarczyłoby użyć, którejś z funkcji zmieniającej łańcuch tekstowy na liczbę np.
g_strtod, a następnie porównywać wynik if'ami, bądź instrukcją switch.
Jednak łatwiej i przyjemniej skorzystać z funkcji
g_object_set_data i
g_object_get_data, poniżej przykład wykorzystujący powyższe funkcje:
#include <gtk/gtk.h>
void wcisnieto_przycisk( GtkButton * przycisk, gpointer dane )
{
GtkTextBuffer * bufor_tekstowy;
GtkTextIter iter_koniec;
gint numer_przycisku;
gchar * tekst;
bufor_tekstowy = GTK_TEXT_BUFFER( dane );
gtk_text_buffer_get_end_iter( bufor_tekstowy, & iter_koniec );
numer_przycisku = GPOINTER_TO_INT( g_object_get_data( G_OBJECT( przycisk ), "numer" ) );
tekst = g_strdup_printf( "Wciśnięto przycisk numer %d\n", numer_przycisku );
gtk_text_buffer_insert( bufor_tekstowy, & iter_koniec, tekst, - 1 );
g_free( tekst );
}
int main( int argc, char * argv[] )
{
GtkWidget * okno;
GtkWidget * hbox, * vbox;
GtkWidget * przycisk;
GtkWidget * textview;
GtkTextBuffer * bufor;
gchar str[ 2 ];
gchar * nazwa_przycisku;
gint i;
gtk_init( & argc, & argv );
okno = g_object_new( GTK_TYPE_WINDOW, "window-position", GTK_WIN_POS_CENTER,
"default-width", 150,
"default-height", 75,
"title", "Kurs GTK+",
"border-width", 5, NULL );
hbox = g_object_new( GTK_TYPE_HBOX, "spacing", 5, NULL );
gtk_container_add( GTK_CONTAINER( okno ), hbox );
textview = gtk_text_view_new();
bufor = gtk_text_view_get_buffer( GTK_TEXT_VIEW( textview ) );
vbox = g_object_new( GTK_TYPE_VBOX, "spacing", 1, NULL );
for( i = 0; i < 20; i++ )
{
str[ 0 ] = 'A' + i;
str[ 1 ] = '\0';
nazwa_przycisku = g_strdup_printf( "Przycisk %s", str );
przycisk = g_object_new( GTK_TYPE_BUTTON, "label", nazwa_przycisku, NULL );
g_object_set_data( G_OBJECT( przycisk ), "numer", GINT_TO_POINTER( i ) );
g_free( nazwa_przycisku );
gtk_box_pack_start( GTK_BOX( vbox ), przycisk, TRUE, TRUE, 0 );
g_signal_connect( przycisk, "clicked", G_CALLBACK( wcisnieto_przycisk ), bufor );
}
gtk_box_pack_start( GTK_BOX( hbox ), vbox, FALSE, TRUE, 1 );
gtk_box_pack_start( GTK_BOX( hbox ), textview, FALSE, TRUE, 1 );
g_signal_connect( G_OBJECT( okno ), "destroy", G_CALLBACK( gtk_main_quit ), NULL );
gtk_widget_show_all( okno );
gtk_main();
return 0;
}
Praktyczne zastosowanie g_object_set
Funkcja
g_object_set znajduje również nie zastąpione zastosowanie w kontrolce widoku drzewa GtkTreeView.
Jak już zapewne wiesz kontrolka GtkTreeView wyświetla dane na podstawie wartości zawartych w modelu GtkTreeModel. Wszystko jest pięknie dopóki korzystamy z "prostych" obiektów renderujących, kiedy zaprzęgniemy GtkCellRendererCombo do wyświetlania i wyboru opcji, standardowe funkcje obsługi GtkTreeView nie wystarczą.
Tak jak na rysunku poniżej, kolumna VIP, z wierszami które przyjmują wartości "TAK", "NIE", "NIEZNANY":
Spójrz na przykład:
#include <gtk/gtk.h>
enum {
KOLUMNA_LP = 0,
KOLUMNA_VIP,
ILOSC_KOLUMN
};
GtkWidget * treeview;
void zmieniono( GtkCellRendererCombo * combo, gchar * path_string, GtkTreeIter * new_iter, gpointer user_data )
{
GtkTreeModel * model =( GtkTreeModel * ) user_data;
GtkTreeIter iter;
GtkTreePath * sciezka;
gchar * numer_wiersza;
GtkListStore * lista;
gint nr;
lista = GTK_LIST_STORE( gtk_tree_view_get_model( GTK_TREE_VIEW( treeview ) ) );
sciezka = gtk_tree_path_new_from_string( path_string );
gtk_tree_model_get_iter( GTK_TREE_MODEL( lista ), & iter, sciezka );
gtk_tree_path_free( sciezka );
numer_wiersza = gtk_tree_model_get_string_from_iter( model, new_iter );
nr = g_ascii_strtoll( numer_wiersza, NULL, 0 );
g_free( numer_wiersza );
switch( nr )
{
case 0:
gtk_list_store_set( lista, & iter, KOLUMNA_VIP, "TAK", - 1 );
break;
case 1:
gtk_list_store_set( lista, & iter, KOLUMNA_VIP, "NIE", - 1 );
break;
case 2:
gtk_list_store_set( lista, & iter, KOLUMNA_VIP, "NIEZNANY", - 1 );
break;
}
}
int main( int argc, char * argv[] )
{
GtkWidget * okno;
GtkTreeViewColumn * kolumna;
GtkCellRenderer * komorka;
GtkListStore * lista_glowna, * lista_combo;
GtkTreeIter iter, iter2;
GtkTreeModel * model;
guint i;
GRand * los;
gtk_init( & argc, & argv );
okno = gtk_window_new( GTK_WINDOW_TOPLEVEL );
gtk_window_set_title( GTK_WINDOW( okno ), "TreeView - GtkCellRendererCombo" );
g_signal_connect( okno, "destroy", G_CALLBACK( gtk_main_quit ), NULL );
treeview = gtk_tree_view_new();
gtk_container_add( GTK_CONTAINER( okno ), treeview );
lista_glowna = gtk_list_store_new( ILOSC_KOLUMN, G_TYPE_UINT, G_TYPE_STRING );
komorka = gtk_cell_renderer_text_new();
kolumna = gtk_tree_view_column_new_with_attributes( "Lp", komorka, "text", KOLUMNA_LP, NULL );
gtk_tree_view_append_column( GTK_TREE_VIEW( treeview ), kolumna );
komorka = gtk_cell_renderer_combo_new();
kolumna = gtk_tree_view_column_new_with_attributes( "VIP", komorka, "text", KOLUMNA_VIP, NULL );
gtk_tree_view_append_column( GTK_TREE_VIEW( treeview ), kolumna );
los = g_rand_new();
for( i = 0; i < 10; i++ )
{
guint wylosowana;
gchar opcja[ 10 ];
gtk_list_store_append( lista_glowna, & iter );
wylosowana = g_rand_int_range( los, 1, 4 );
switch( wylosowana )
{
case 1:
g_strlcpy( opcja, "TAK", - 1 );
break;
case 2:
g_strlcpy( opcja, "NIE", - 1 );
break;
case 3:
g_strlcpy( opcja, "NIEZNANY", - 1 );
break;
}
gtk_list_store_set( lista_glowna, & iter, KOLUMNA_LP,( guint ) i + 1, KOLUMNA_VIP, opcja, - 1 );
}
lista_combo = gtk_list_store_new( 1, G_TYPE_STRING );
gtk_list_store_append( lista_combo, & iter2 );
gtk_list_store_set( lista_combo, & iter2, 0, "TAK", - 1 );
gtk_list_store_append( lista_combo, & iter2 );
gtk_list_store_set( lista_combo, & iter2, 0, "NIE", - 1 );
gtk_list_store_append( lista_combo, & iter2 );
gtk_list_store_set( lista_combo, & iter2, 0, "NIEZNANY", - 1 );
model = GTK_TREE_MODEL( lista_combo );
g_object_set( komorka, "model", model, "text-column", 0, "has-entry", FALSE, "editable", TRUE, NULL );
g_signal_connect( komorka, "changed", G_CALLBACK( zmieniono ), model );
gtk_tree_view_set_model( GTK_TREE_VIEW( treeview ), GTK_TREE_MODEL( lista_glowna ) );
g_object_unref( lista_glowna );
gtk_tree_view_set_grid_lines( GTK_TREE_VIEW( treeview ), GTK_TREE_VIEW_GRID_LINES_BOTH );
gtk_widget_show_all( okno );
gtk_main();
return 0;
}
Zwróć uwagę na:
g_object_set( komorka, "model", model, "text-column", 0, "has-entry", FALSE, "editable", TRUE, NULL );
bez tej linii nie wyobrażam sobie obsługi GtkCellRendererCombo. Wskazuje komórce, która jest obiektem GtkCellRendererCombo, skąd ma pobrać dane do wyświetlania oraz ustawia odpowiednie właściwości. Akurat tutaj
model, "text-column" wskazuje na nowy model, który składa się z trzech wierszy, dane do wyświetlania ma pobierać z 0-ej kolumny modelu. Nie ma mieć możliwości wpisywania danych
"has-entry", FALSE. Pozwala
"editable", TRUE na możliwość edycji w takim sensie, że po kliknięciu komórki wyświetlają się opcje wyboru w postaci listy rozwijanej tak jak w GtkComboBox.
Celowo w powyższym przykładzie użyłem "klasycznego" stylu pisania programów w Gtk+, aby pokazać że bez
g_object_set inaczej tego problemu nie można rozwiązać :)