Programování pro X Window System (6)

6. 5. 2004
Doba čtení: 9 minut

Sdílet

V tomto článku uzavřeme téma GTK+. Podíváme se dovnitř toolkitu, jak fungují widgety. Na závěr si ukážeme, jak definovat novou třídu widgetů.

Typy, třídy, objekty

Toolkit GTK+ má svůj vlastní typový systém, definovaný v knihovně GObject. Každý datový typ má unikátní numerický identifikátor typu (GType), přidělený při registraci typu. V rámci inicializace toolkitu ve funkci gtk_init se registrují fundamentální typy, tj. primitivní typy jazyka C (např. int má identifikátor G_TYPE_INT) a G_TYPE_OBJECT, označující základní objektový typ GObject. Všechny třídy v GTK+ jsou odvozené z GObject. Typový systém nepodporuje vícenásobnou dědičnost, místo ní má interfejsy. Třídy nezaregistrované v gtk_init se automaticky registrují v okamžiku vytvoření prvního objektu dané třídy. Pro každou třídu jsou definovány dvě struktury. Struktura pro instanci obsahuje data konkrétního objektu. Ve struktuře pro třídu jsou ukazatele na funkce. Je to obdoba tabulky virtuálních metod používané v C++. Ve struktuře pro třídu mohou být i data třídy (obdoba členů static v C++). Pro tento účel se ale častěji používají static proměnné definované v souboru .c implementujícím třídu. Prvním prvkem každé struktury je vždy struktura bázového typu. Tím je zajištěno, že bude korektně fungovat přetypování na předka. Např. pro typ GtkButtonjsou v souboru gtkbutton.h definice

typedef struct _GtkButton GtkButton;
typedef struct _GtkButtonClass GtkButtonClass;

struct _GtkButton
{
  GtkBin bin;
  ...
};

struct _GtkButtonClass
{
  GtkBinClass parent_class;
  ...
};

Pro každý typ widgetu WIDGET existuje funkce gtk_WIDGET_get_ty­pe, jež vrací identifikátor typu. Dále je definováno mnoho maker pro manipulaci s typy, např.:

  • GTK_TYPE_WIDGET … vrací identifikátor typu pro typ widgetů WIDGET
  • GTK_WIDGET … přetypuje parametr na ukazatel na objekt WIDGET; typ parametru musí být WIDGET nebo jeho potomek, jinak vypíše chybové hlášení
  • GTK_WIDGET_CLASS … přetypuje parametr na ukazatel na strukturu třídy pro typ WIDGET; kontroluje, zda je parametr struktura pro třídu WIDGET nebo pro odvozenou třídu
  • GTK_IS_WIDGET … otestuje, zda je parametr objekt typu WIDGET nebo objekt odvozené třídy
  • GTK_IS_WIDGET_CLASS … otestuje, zda je parametr struktura třídy pro typ WIDGET nebo pro odvozený typ

Vytvoření objektu

Objekt nějakého typu WIDGET se vytvoří funkcígtk_WID­GET_new. Pro některé typy widgetů existuje takových funkcí víc. Liší se jménem a parametry. Např. pro třídu GtkMenuItem jsou definovány funkce gtk_menu_item_new, gtk_menu_item_new_wit­h_label a gtk_menu_item_new_wit­h_mnemonic. Na obr. 1 je schematicky znázorněn postup vytvoření widgetu.

gtk_widget_new
Obr. 1: Vytvoření widgetu

Vytvoření widgetu typu WIDGET začíná zavoláním funkce gtk_WIDGET_new. Ta na začátku zjistí identifikátor typu voláním gtk_WIDGET_get_ty­pe. Pokud je tato funkce použita poprvé, musí zaregistrovat třídu v typovém systému knihovny GObject. To udělá pomocí funkce g_type_register_sta­tic, které předá identifikátor rodičovského typu, strukturu obsahující jméno nově registrované třídy, velikosti struktur pro třídu a pro instanci a ukazatele na inicializační funkce.

Identifikátor typu se předá funkci gtk_type_new, jež volá g_object_new. Tato funkce před vytvořením první instance daného typu inicializuje strukturu pro třídu voláním g_type_class_ref, která dále volá funkci type_class_init_Wm. Ta alokuje strukturu pro třídu a zkopíruje do ní – na začátek – strukturu rodičovské třídy. Jestliže nepostačuje bázovou třídu zkopírovat, lze pro třídu definovat funkci zajišťující další inicializaci kopie bázové třídy. Tyto inicializační funkce se volají postupně v celé posloupnosti bázových tříd ve směru od kořene hierarchie po rodiče právě inicializované třídy. Nakonec se provede inicializační funkce třídy, která především zajistí registraci properties a signálů.

Když je registrován typ a inicializována struktura pro třídu, je možné vytvořit instanci. Funkce gtk_type_new zavolá konstruktor definovaný pro třídu, jejíž objekt se vytváří. Konstruktor nejprve spustí konstruktor rodičovské třídy. Konstruktor g_object_construc­tor společného předka – třídy GObject – alokuje paměť pro strukturu instance. Pak zavolá postupně inicializační funkce pro objekty bázových tříd a tím inicializuje první položku struktury objektu, která obsahuje zděděná data. Nakonec se zavolá funkce pro inicializaci nově definovaných položek. Tím volání gtk_WIDGET_new končí a vrací ukazatel na nově vytvořený objekt.

Properties a signály

Properties jsou hodnoty přiřazené objektům a identifikované pomocí řetězcových klíčů. Jméno property se zadává buď samostatně (např. „border_width“), nebo spolu se jménem třídy (např. „GtkContainer::bor­der_width“). Properties se dědí. Při čtení nebo nastavování hodnoty se property daného jména hledá od třídy objektu, s jehož property se pracuje, směrem ke GObject. Properties se nastavují funkcí g_object_set a čtou se funkcí g_object_get. Pro některé properties existují i specializované funkce, které s nimi manipulují. Např. gtk_container_set_bor­der_width má stejný efekt jako nastavení hodnoty property „GtkContainer::bor­der-width“. Funkce g_object_clas­s_list_proper­ties slouží ke zjištění jmen všech properties definovaných v nějaké třídě.

Properties představují jednotné rozhraní, přes které lze manipulovat s hodnotami uvnitř objektů. Díky tomu, že jména properties jsou řetězce, se dá manipulace s nimi naprogramovat i bez apriorní znalosti jmen všech properties, s nimiž se bude pracovat. To je výhodné při implementaci rozhraní zpřístupňujícího GTK+ z nějakého skriptovacího jazyka, nebo pro programy na grafický interaktivní návrh GUI, jako je Glade.

Properties se definují v inicializační funkci třídy voláním funkce g_object_clas­s_install_pro­perty. Ve třídě, která definuje vlastní properties, je nutné implementovat funkce pro čtení a nastavování properties definovaných v této třídě a ukazatele na ně přiřadit do ((GObject *)klass)->get_property a((GObject *)klass)->set_property, kde klass je struktura pro třídu. Není třeba se starat o zděděné properties, protože se pro ně automaticky volají příslušné funkce bázových tříd. Funkce pro manipulaci s properties mohou dělat i komplexnější věci než jen číst či měnit nějakou datovou položku objektu. Lze si např. představit dvojici properties, jedna bude obsahovat šířku widgetu v pixelech a druhá v milimetrech. V datech objektu bude uložena pouze šířka v pixelech. Šířka v milimetrech se bude dynamicky dopočítávat podle rozlišení obrazovky. Změna šířky navíc způsobí překreslení widgetu.

V inicializační funkci třídy se definují také signály. Ty se rovněž dědí. Při definici signálu funkcí g_signal_new se zadává

  • jméno signálu
  • identifikátor typu, pro nějž se signál definuje
  • pořadí, v jakém se spouští defaultní handler a handlery připojené pomocí g_signal_connect
  • offset ukazatele na defaultní handler ve struktuře pro třídu vzhledem k začátku struktury
  • akumulátor, do něhož se návratové hodnoty jednotlivých handlerů skládají, aby bylo možné získat jednu návratovou hodnotu vrácenou po zpracování všech handlerů
  • marshaller, tj. funkce pro převod pole argumentů typu GValue(univerzální struktura, jež může obsahovat hodnoty různých typů) na seznam parametrů funkce
  • typ návratové hodnoty nebo G_TYPE_NONE, pokud signál nic nevrací
  • počet a typy parametrů handleru

Vysvětlení asi potřebuje především marshaller. Při vygenerování signálu – např. funkcí g_signal_emit – se vytvoří pole parametrů ve formě struktur GValue. Handler ale má seznam parametrů konkrétních typů, stejně jako jakákoliv jiná funkce v jazyce C. Problém je, že C neposkytuje prostředek pro rozdělení pole do seznamu argumentů a volání funkce s těmito argumenty. Proto je nutné pro každou kombinaci typů parametrů definovat marshaller, což je funkce, která rozebere pole strukturGValue, předělá ho na seznam parametrů, zavolá funkci s těmito parametry a vrátí návratovou hodnotu volané funkce. GTK+ obsahuje marshallery pro mnoho kombinací typů parametrů. Pokud definujeme signál s parametry, pro něž není v GTK+ marshaller, musíme definovat příslušný marshaller sami.

Důležité metody widgetů

Zrušení widgetu začíná voláním g_object_unref nebo gtk_widget_destroy. Vlastní destrukce widgetu je rozdělena do tří fází. Metoda dispose zruší X okno widgetu a vyjme widget z rodičovského kontejneru. Pak emituje signál „destroy“, a tím zahájí druhou fázi. Handler destroy označí objekt jako„nepoužitelný“ a uvolní s ním asociované systémové zdroje vyjma samotné struktury instance. V případě, že widget je kontejner, zruší synovské widgety. Mělo by však jít stále volat veškeré veřejné metody (protože na objekt stále mohou existovat platné reference), ty sice už nebudou nic dělat, nicméně program nehavaruje. Poslední fáze – finalize – se provede, teprve když počet referencí na objekt klesne na nulu. Uvolní se struktura instance, a tím se definitivně dokončí zrušení widgetu.

Metoda realize ve třídě GtkWidget funguje pouze pro widgety, které nemají vlastní GDK/X okno. Pro ostatní widgety je nutno metodurealize předefinovat tak, že nastaví příznak GTK_REALIZED, vytvoří GDK okno voláním gdk_window_new, ukazatel na ně uloží do widget->window, k oknu přidá odkaz zpět na widget pomocí gdk_window_set_u­ser_data(widget->window, widget) a nastaví styl widgetu voláním gtk_style_attach.

Metoda unrealize odmapuje a zruší GDK okno widgetu (pokud widget GDK okno má). Jestliže je widget kontejner, zavolá nejprve unrealize pro všechny synovské widgety.

Metody map a unmap nastaví resp. zruší příznak widgetuGTK_WID­GET_MAPPED a zobrazí/schovají GDK okno widget->window. Než se widget objeví na obrazovce, je nutné nastavit jeho velikost s využitím metod size_request a size_allocate tak, jak to bylo popsáno v kapitole o kontejnerech ve třetím dílu tohoto seriálu.

Widget, který není jen neviditelným kontejnerem pro jiné widgety, se musí umět nakreslit na obrazovku. Potřeba překreslit widget může nastat z několika důvodů. Jestliže se neviditelná část okna widgetu stane viditelnou – např. protože se přesune nebo odmapuje okno, které ji zakrývalo – dostane widget od X serveru událost „expose-event“. Handler události zajistí kreslení. Někdy se k překreslení rozhodne samo GTK+ (při změně velikosti widgetu nebo při nastavení nové theme). Při změně svého stavu se widget překreslí sám od sebe. Obsah widgetů v GTK+ se kreslí s použitím kreslicích funkcí knihovny GDK. Ještě lepší než kreslit přímo pomocí GDK je využít funkce z theme, aby vzhled widgetu ladil s ostatními a dal se měnit změnou theme.

Definice nové třídy

Pro každou třídu widgetů se vytváří samostatná dvojice souborů gtkWIDGET.h a gtkWIDGET.c. Je třeba definovat struktury pro třídu a instanci GtkWIDGETClass a GtkWIDGET. Dále je nutné implementovat řadu metod pro:

bitcoin_skoleni

  • registraci typu gtk_WIDGET_get_ty­pe
  • vytvoření instance gtk_WIDGET_new
  • inicializaci třídy, která zajistí definování nových properties a signálů a nastavení defaultních handlerů signálů
  • inicializaci instance
  • zrušení objektu dispose, destroy,finalize
  • realizaci a mapování realize, unrealize, map, unmap
  • nastavení velikosti size_request, size_allocate
  • kreslení expose_event

widget gtkev
Obr. 2: Definice nového widgetu

Ukázka definice nového widgetu GtkEv je v souborech gtkev.h a gtkev.c. Tento widget má podobnou funkci jako program xev z distribuce X, tj. vypisuje informace o událostech, které dostává. Widget je zvláštní tím, že má dvě GDK okna. Widget je použit v programu gtk_ev_usage.c. Okno tohoto programu je na obr. 2. Widget GtkEv je upravený stejnojmenný widget z knihy Havoc Pennington: GTK+/GNOME Application Development.

Autor článku