Artykuł opisuje w jaki sposób można korzystać z biblioteki GTK+ w języku C przy pomocy GObject. (artykuł)
Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Zarejestruj się!
dział serwisuArtykuły
kategoriaInne artykuły
artykułJak korzystać z biblioteki GTK+ w języku C przy pomocy GObject
Autor: 'Badman'

Jak korzystać z biblioteki GTK+ w języku C przy pomocy GObject

[artykuł] Artykuł opisuje w jaki sposób można korzystać z biblioteki GTK+ w języku C przy pomocy GObject.

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):
C/C++
#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:
C/C++
#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.

Hierarchia obiektów
Hierarchia obiektów

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ą:

  • "active" - informuje czy przycisk jest wciśnięty, bądz służy do programowego ustawienia wciśnięcia przycisku. Przyjmuje wartości TRUE / FALSE.
  • "draw-indicator" - informuje czy przełącznik przycisku jest wyświetlany, bądz ustawia programowego wyświetlania przełącznika. Przyjmuje wartości TRUE / FALSE.
  • "inconsistent" - ustawienie wartości TRUE spowoduje zachowanie się przycisku jak zwykłego GtkButton, poprostu przycisk nie zmienia swojego stanu. Można tą wartość także odczytywać. Przyjmuje wartości TRUE / FALSE.

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:
C/C++
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:

  • GINT_TO_POINTER - konwertuje gint na gpointer,
  • GUINT_TO_POINTER - konwertuje guint na gpointer.

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:

  • GPOINTER_TO_INT - konwertuje gpointer na gint,
  • GUINT_TO_POINTER - konwertuje gpointer na guint.

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:
C/C++
void wcisnieto_przycisk( GtkWidget * przycisk, gpointer dane )
{
    gchar * tekst_przycisku;
   
    tekst_przycisku = g_strdup( gtk_button_get_label( przycisk ) );
   
    if( tekst_przycisku == "Przycisk A" )
    // zrób to
    else if( tekst_przycisku == "Przycisk B" )
    // zrób to  i to
    [...]
    else if( tekst_przycisku == "Przycisk Z" )
    // wykonaj coś
}

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:
C/C++
#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:
C/C++
#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;
   
    /* Pobranie modelu z widoku drzewa przekształcając go GtkListStore */
    lista = GTK_LIST_STORE( gtk_tree_view_get_model( GTK_TREE_VIEW( treeview ) ) );
   
    /* Zamiana scieżki w postaci łańcucha tekstowego na GtkTreePath */
    sciezka = gtk_tree_path_new_from_string( path_string );
    /* Ustawienie iteratora iter na podstawie sciezka */
    gtk_tree_model_get_iter( GTK_TREE_MODEL( lista ), & iter, sciezka );
    /* Zwolnienie pamięci zajmowanej przez sciezka */
    gtk_tree_path_free( sciezka );
   
    /* Pobranie numeru wiersza w postaci łańcucha tekstowego */
    numer_wiersza = gtk_tree_model_get_string_from_iter( model, new_iter );
    /* Zamiana go na liczbe oraz zwolnienie pamięci zajmowanej przez numer_wiersza */
    nr = g_ascii_strtoll( numer_wiersza, NULL, 0 );
    g_free( numer_wiersza );
   
    /* Reakcja na wybraną odpowiedź */
    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 );
   
    /* Utworzenie okna */
    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 );
   
    /* Utworzenie okna  z dodaniem go do okna */
    treeview = gtk_tree_view_new();
    gtk_container_add( GTK_CONTAINER( okno ), treeview );
   
    /* Utworzenie magazynu danych listy składającego sie z 2 kolumn, 0-wa kolumna przechowuje wartości liczbowe całkowite bez znaku,
         1-sza łańcuchy tekstowe */
    lista_glowna = gtk_list_store_new( ILOSC_KOLUMN, G_TYPE_UINT, G_TYPE_STRING );
   
    /* Kolumna Lp */
    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 );
   
    /* Kolumna VIP */
    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 );
   
    /* Zaporzątkowanie generatora pseudolosowego */
    los = g_rand_new();
   
    /* Pętla dodajaca wiersze ustawiając 1-szą kolumnę modelu tekstem w zależności od wylosowanej liczby */
    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 );
    }
   
    /* Utworzenie magazynu listy danych 1 kolumna typ G_TYPE_STRING */
    lista_combo = gtk_list_store_new( 1, G_TYPE_STRING );
    /* Dodanie wartości */
    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 );
   
    /* Zamian magazynu danych na GtkTreeModel */
    model = GTK_TREE_MODEL( lista_combo );
    /* Ustawienie właściwości komórki/obiektu GtkCellRendererCombo. "model" na wczeeśniej stworzony model, wskazujący na kolumnę 0 tego modelu */
    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 );
   
    /* Ustawienie rysowania siatki w drzewie w pionie i pozimoie */
    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ć :)