Hlavní navigace

Klasický Hello World v PyGTK

Michal Smrž 4. 8. 2008

V dnešním druhém díle nového seriálu o vytváření aplikací pomocí PyGTK si ošetříme import knihoven, ukážeme si, jak správně zapisovat kód v PyGTK, co je to callback a vše si podrobně rozebereme. V závěru článku pak vylepšíme náš první program o nové vlastnosti a tak vytvoříme pořádný Hello World.

I když naše první aplikace funguje jak má, nachází se v ní dost neošetřených chyb. Navíc se použitý zápis (bez tříd) nepoužívá. V prvním díle byl využit kvůli krátkosti. Dnes již ale bude kód zapsán, jak má.

Vylepšený import

Na import gtk není nic špatného. Dokud…
Současná verze GTK má tvar 2.něco. Hlavní verze, ono první číslo, nám udává api. Z hlediska programování nás tedy ostatní verze moc nezajímají. Jelikož má GTK už verzi 2, je jasné, že zde byla i verze první a ty před ní. I na ně existoval Python binding. Proto bude nejdříve potřeba si verzi ověřit a v případě, že jich je v systému více, vyžádat si tu správnou. Dnes má v systému verzi 1.0 málokdo a 3.0 nikdo. Oficiální tutoriál doporučuje:

import pygtk
pygtk.require("2.0")

Jak je vidno, vyžádání verze a případnou chybu nám obstará funkce require() z modulu pygtk.
Na jiném místě ale vkládá tuto kontrolu do chráněného bloku, s tím, že v případě vyjímky se jde dál.

try:
    import pygtk
    pygtk.require("2.0")
except:
    pass

Nevím, nevím.
Pravda, když nám nesedí verze, nebo jiný možný důvod selhání pygtk.require(), tak to ještě nutně neznamená, že aplikace nebude fungovat, ale… Otestoval jsem to na Linuxu a na Windows. Někdy si udělám čas otestovat to i v Openmoko a Maemo, ale nikde jsem na problém nenarazil. Proto vyžádání verze a import gtk dám do jednoho chráněného bloku se společnou chybou.

Výsledek bude vypadat tedy nějak takto:

import sys

try:
    import pygtk
    pygtk.require("2.0")
    import gtk
except:
    print "Error: PyGTK and GTK 2.xx must be installed to run this application. Exiting"
    sys.exit(1)

Tak. Tento import mi přijde už dobře ochráněný.

Zápis kódu do třídy

To, jak jsem zapsal kód v prvním díle, do hlavní větve, lze-li to tak nazvat, a spustil jej bezpodmínečně na posledním řádku, není standardní zápis. Místo toho se vytváří třídy. Většinou co okno, to samostatná třída a samostatný soubor. Žádnou paniku, vše si ukážeme.

Zařekl jsem se, že nebudu popisovat Python, nicméně alespoň ve stručnosti. Třída (class) je zjednodušeně sdružení funkcí a proměnných, které se týkají stejného „tématu“ (těžko popsat, záleží na rozhodnutí programátora), do jednoho svazku. Ten pak lze přes instance tříd rozkopírovat a přiřadit kolikrát jen chceme.
Název třídy a názvy funkcí, správně „metody“, si volíme sami, vyjma jedné. Je to __init__ funkce, která je prováděna při inicializaci třídy. A právě v ní bude veškerý náš GTK kód.

Standardní vlastní třída

První program lze do třídy, pojmenované např prvni_okno, zapsat takto:

--- Vylepšený import, nebudu jej opisovat ---

class prvni_okno:

    def __init__(self):

        self.okno = gtk.Window()
        self.tlacitko = gtk.Button("Ahoj")

        self.okno.add(self.tlacitko)
        self.tlacitko.connect("clicked", gtk.main_quit)

        self.okno.show_all()

    def spust(self):

        gtk.main()

if __name__ == "__main__" :

    aplikace = prvni_okno()
    aplikace.spust()

Rozbor:

self
Je zástupný symbol pro proměnnou, jíž byla přiřazena instance třídy. Je předávána jako první parametr každé funkci. I když ji málokdy využijeme, je třeba ji v definici funkce vypsat jako první, jinak nám nebude souhlasit počet předaných a vstupních parametrů. Není to poslední předaný povinný parametr.
V horním příkladě platí, že self je rovno aplikace. To ale není jediný význam self. Jakákoli proměnná (náš objekt je vždy, pokud nezadáme jinak, lokální) exituje jen v dané funkci/metodě. Pokud ale vytvoříme objekt self.okno, je tato proměnná dostupná v celé třídě, v jakékoli metodě a je možné k ní přistoupit přes instance.proměn­ná. Oficiální tutoriál vkládá self před každé prvky, proto to dodržíme i my. Ovšem pokud v nějaké funkci, budeme mít pomocnou proměnnou pro onu funkci, která má ale význam jen v té funkci, budeme ji psát jako lokální (bez self).

if __name__ == "__main__":
Další trik vykoukaný z oficiálního tutoriálu. V Pythoní proměnné __name__ je uložen název, jak byl náš kód importován. Obvykle je to název souboru (bez přípony py). Pokud náš kód nikam importován nebyl, ale byl spuštěn přímo, je __name__ rovno „__main__“. Toto je dobré si osvojit, protože pak budeme moci tento program importovat do jiných programů a hned po importu nebude toto okno s tlačítkem spuštěno a zobrazeno, což by se jinak stalo.

aplikace = prvni_okno()
Samozřejmě, inicializace naší třídy. Právě v tuto chvíli (inicializace třídy) se provede __init__ funkce naší třídy. Po dokončení tohoto řádku jsou tedy prvky vytvořeny a předány k zobrazení. To se stane na dalším řádku aplikace.spust(), která spustí gtk.main().

Tento zápis, ač na to možná zatím nevypadá, nabízí více možností a ve výsledku přehlednější kód, a proto jej odteď budu využívat – i s trikem name == main. Při psaní tohoto článku jsem náhled do zdrojového kódu Gajimu (AFAIK nejznámější Python/PyGTK aplikace) a právě tento druh zápisu je tam použit. Krom triku name == main, autor zřejmě neplánoval možnost spustit jednotlivá okna samostatně.

Třída odvozená od jiné třídy

A konkrétně, odvozená od gtk.Window(). Tím pádem naše třída bude umět to, co umí gtk.Window(). Tedy show(), fullscreen() apod. Zkrátka se bude více tvářit jako naše vlastní okno, právě díky tomu, že lze přímo volat tyto jeho metody. To má v sobě i omezení, že naše vlastní metody se nesmí jmenovat stejně, jako se jmenují již metody existující (jejich seznam je v referenční příručce). Zápis je velice podobný:

--- Vylepšený import, nebudu jej opisovat ---

class prvni_okno(gtk.Window):

    def __init__(self):

        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)

        self.tlacitko = gtk.Button("Ahoj")

        self.add(self.tlacitko)
        self.tlacitko.connect("clicked", gtk.main_quit)

        self.show_all()

    def spust(self):

        gtk.main()

if __name__ == "__main__" :

    aplikace = prvni_okno()
    aplikace.spust()

class prvni_okno(gtk.Window):
Jako parametr se dává třída, od které se má naše třída odvodit.

gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
Ať už dělá gtk.Window() v __init__ cokoliv, měli bysme to udělat v našem __init__ také, a to hned na prvním místě. A pokud chceme zvolit typ okna, musíme to udělat právě zde. Proto jsem uvedl výchozí parametr gtk.WINDOW_TOPLEVEL (viz níže). Veškeré parametry předané třídě se dají odchytit jen na jednom místě a to je __init__, tam je je nutné převést na self proměnné.

self.add(self.tlacitko)
Všimněte si, že samotné self již není jen naše třída (náš init a funkce), ale i gtk.Window, tudíž self.add() a self.show_all(). Okno také není třeba vytvářet, protože naše třída je to „okno“.

Takto napsaná třída vypadá více jako okno, protože přijímá všechny metody gtk.Window. Mohu měnit titulek aplikace.set_title(), měnit velikost aplikace.resize(), zkrátka volat naší třídu všemi metodami gtk.Window.
Moc velké zjednodušení to ale není. K oknu ve vlastní třídě mohu přistupovat přes aplikace.okno­.metoda(), takže to není žádná vlastnost navíc, jen kratší volání. Dále jsem neviděl tento zápis moc používaný v praxi, ale pokud se vám zalíbil, proč ne.

Lépe definované okno a jeho stavy

Předtím jsme se spokojili s pouhým vytvořením okno = gtk.Window(). Dnes si řekneme něco více. Gtk.Window může mít jako parametr dva ukazatele. gtk.WINDOW_TOP­LEVEL, což je standardní okno, které se nechá obalit správcem oken – toto je výchozí, proto jej není potřeba psát.
Nebo gtk.WINDOW_POPUP, a to se správci oken již úspěšně brání. Je vhodný na vytvoření popup oznámení a jiných neokenních prvků.
Zůstaneme ale u standardního okna a závorky necháme prázdné. Ovšem použijeme další metody.
Nastavíme oknu titulek „Hello World“, okno umístíme doprostřed obrazovky, vytvoříme nevykreslovací rámeček, takže tlačítko tentokráte nebude přes celé okno, zabráníme změně velikosti okna a nakonec rozebereme jeho zavírání.
Málo toho není, vezměme to tedy popořadě:

self.okno.set_title("Hello World")
self.okno.set_position(gtk.WIN_POS_CENTER)
self.okno.set_border_width(10)
self.okno.set_resizable(False)

self.okno.connect("delete_event", self.pokus_o_zavreni_okna)
self.okno.connect("destroy", self.okno_se_zavira)

self.okno.set_title("Hello World")
def set_title(title)
Nastaví text do záhlaví okna. Parametr je klasický řetězec.

self.okno.set_position(gtk.WIN_POS_CENTER)
def set_position(po­sition)
Nastaví pozici doprostřed obrazovky. Jako argument bere jednu z těchto gtk.konstant. Konstantu poznáte tak, že je napsána velkými písmeny.

self.okno.set_border_width(10)
def set_border_wid­th(border_wid­th)
Toto není metoda gtk.Window(), ale metoda gtk.Container(). Ten obsahují všechny widgety, které umí pohlcovat (vkládat do sebe) další widgety. Tímto řádkem řekneme, že vložený widget nebude vyplňovat celý obsah rodičovského widgetu, v našem případě okno – gtk.Window(), ale jen prostor 10 pixelů od okraje.
Mimochodem, metoda add() pomocí níž vkládáme tlačítko do okna, je také metoda gtk.Container().

self.okno.set_resizable(False)
def set_resizable(re­sizable)
Parametr je buď True – výchozí nebo False, podle toho, zda chceme či nechceme, aby oknu bylo možné měnit velikost.

self.okno.connect("delete_event", self.pokus_o_zavreni_okna)
„delete_event“ je signál, který vyvolává přímo X signál. Všechny X signály jsou pojmenované něco_event, tak je snadno poznáme. Samotné „delete_event“ okno neuzavře. Pokud není „delete_event“ ošetřen, vrátí hodnotu False a je automaticky vyvolán „destroy“ signál, který okno již doopravdy uzavře.
Coby programátoři nejdříve musíme odchytit „delete_event“ a rozhodnout, zda okno může být zavřeno. Pokud má uživatel rozdělanou neuloženou práci apod., tak by rozhodně uzavřeno být nemělo. Naše obsluhující funkce, odborně callback, bude končit buďto return False, což je výchozí a vyvolá destroy widgetu, nebo return True, které odvrátí destroy a okno bude zobrazeno a funkční i nadále.
Protože se nejedná přímo o signál gtk objektu, je předán signál (gdk.Event) jako druhý parametr. K čemu je to dobré, to nevím, ale v callbacku je třeba na to pamatovat, jako na povinný parametr.

self.okno.connect("destroy", self.okno_se_zavira)
Od callbacku delete eventu máme zelenou, konkrétně False a okno mizí z obrazovky. Je volán další callback a to okno_se_zavira(). Proč nevolám v tuto chvíli rovnou gtk.main_quit()? To by sice šlo, ale oficiální tutoriál má vše v obsluhujících funkcích. Z toho důvodu to, alespoň zde, dodržím.

Callbacky

Každý callback dostává jako první (vynechám-li self, jako že ho vynechávat budu všude) parametr widget, co onen callback vyvolal. Porovnáváním lze tedy zjistit, který widget to byl, nebo přímo zavolat nějakou metodu onoho widgetu.
V případě, že signál byla X událost, jako druhý parametr je předán on.


def pokus_o_zavreni_okna(self, widget, event):

    if not self.smi_skoncit:
        return True

def pokus_o_zavreni_okna(self, widget, event):

    return not self.smi_skoncit

V prvním se musím rozhodnout, zda smí aplikace skončit. Pro příklad jsem se rozhodl, že budu uživatele nutit, aby alespoň jednou klikl na naše tlačítko. V initu si vytvořím proměnnou self.smi_skoncit s hodnotou False. Ta se změní na True po kliknutí na tlačítko.
Zde tedy ověřím, zda je True nebo False a podle toho umožním, či znemožním konec.
Podmínku jsem znegoval, takže zní „Pokud není pravda, že smí skončit…“ a vracím True. Větev s else a return False není potřeba, je to výchozí chování.

Diskuze: Vzhledem k tomu, že už samotná informace, zda uživatel smí skončit nebo ne, je typu bo­olean (True nebo False) a návrat funkce/callbacku též, je její ověřování pomocí if zcela zbytečné.
Stačí vrátit její znegovanou hodnotu.

def okno_se_zavira(self, widget):

        gtk.main_quit()

V druhém callbacku už není co řešit, zkrátka ukončíme gtk smyčku.

def stisknuto_tlacitko(self, widget):

        widget.set_label("Ahoj svete!")
        self.smi_skoncit = True

Na prvním řádku demonstruji, jak si mohu ovlivnit widget, aniž bych znal jeho absolutní jméno (self.tlacitko), díky tomu, že mi byl předán.
Na druhém řádku si zapisuji do třídní proměnné (takže si ji mohu přečíst i v pokus_o_za­vreni_okna() ), že okno smí být zavřeno.

Celý hello world

Tramtaradá, výsledek dnešního povídání, aneb již dobrý hello world.

#!/usr/bin/env python

import sys

try:
    import pygtk
    pygtk.require("2.0")
    import gtk
except:
    print "Error: PyGTK and GTK 2.xx must be installed to run this application. Exiting"
    sys.exit(1)

class prvni_okno:

    def __init__(self):

        self.smi_skoncit = False

        self.okno = gtk.Window()
        self.okno.set_title("Hello World")
        self.okno.set_position(gtk.WIN_POS_CENTER)
        self.okno.set_border_width(10)
        self.okno.set_resizable(False)

        self.okno.connect("delete_event", self.pokus_o_zavreni_okna)
        self.okno.connect("destroy", self.okno_se_zavira)

        self.tlacitko = gtk.Button("Alespon jednou na me klikni")
        self.tlacitko.show()

        self.okno.add(self.tlacitko)

        self.tlacitko.connect("clicked", self.stisknuto_tlacitko)

        self.okno.show()

    def spust(self):

        gtk.main()

    def stisknuto_tlacitko(self, widget):

        widget.set_label("Ahoj svete!")
        self.smi_skoncit = True

    def pokus_o_zavreni_okna(self, widget, event):

        return not self.smi_skoncit

    def okno_se_zavira(self, widget):

        gtk.main_quit()

if __name__ == "__main__" :

    aplikace = prvni_okno()
    aplikace.spust()
Hello World Linux

Výsledek v Linuxu…

Hello World Windows

… a ve Windows

Dokud nestisknete tlačítko, budete klikat na křížek v okně marně.
Také si všimněte, jak se jednotlivá prostředí zachovala na znemožnění změny velikosti okna. Linuxová verze tlačítko pro maximalizaci úplně vypustila, zatímco Windowsí jej jen deaktivovala.

show() u každého prvku
Málem bych zapomněl na rozdělené show().
Obecně uznávaný standard je zobrazovat každý objekt samostatně pomocí show() a show_all() nepoužívat. Náhodou jsem přišel i na druhý důvod a tím je rychlejší vykreslení v případě vysoké zátěže nebo pomalosti počítače. Na pozadí se mi dělala rozdílová aktualizace zdrojáků jádra (dost náročná operace) a bylo vidět, že show_all() zobrazí nejdříve okno (takže chviličku vidím prázdné okno) a pak teprve tlačítko a další prvky v okně.
Pokud ale dáte všechny prvky zobrazit a nakonec dáte zobrazit okno, k vykreslení dojde okamžitě, protože v paměti ty prvky již vykreslené jsou.

Diskuze: Toto ale není pravda a já se moc omlouvám za mystifikaci. Show_all() vykresluje prvky odspodu nahoru (ve stromu widgetů) a není tedy žádný důvod jej nevyužívat.

Příště:
Dojde na umísťování více prvků do okna a na způsoby jejich umístění a zarovnání.

Našli jste v článku chybu?

4. 8. 2008 10:15

K. (neregistrovaný)
No pánové, diskutujete na zajímavé téma, ale na špatném příkladě. Já osobně strhávám za konstrukce typu "IF podmínka THEN RETURN TRUE ELSE RETURN FALSE" docela dost bodů.

Jinak řečeno, mělo tam být něco jako:

def pokus_o_zavreni_okna(self, widget, event):

    return not self.smi_skoncit

4. 8. 2008 12:39

... (neregistrovaný)
Hello World musi byt ukazkovy priklad, plne optimalizovaneho kodu.
Podnikatel.cz: K EET. Štamgast už peníze na stole nenechá

K EET. Štamgast už peníze na stole nenechá

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Podnikatel.cz: Vládu obejde, kvůli EET rovnou do sněmovny

Vládu obejde, kvůli EET rovnou do sněmovny

Vitalia.cz: „Připluly“ z Německa a možná obsahují jed

„Připluly“ z Německa a možná obsahují jed

DigiZone.cz: Česká televize mění schéma ČT :D

Česká televize mění schéma ČT :D

Podnikatel.cz: 1. den EET? Problémy s pokladnami

1. den EET? Problémy s pokladnami

Lupa.cz: UX přestává pro firmy být magie

UX přestává pro firmy být magie

Lupa.cz: Google měl výpadek, nejel Gmail ani YouTube

Google měl výpadek, nejel Gmail ani YouTube

Lupa.cz: Babiš: E-shopů se EET možná nebude týkat

Babiš: E-shopů se EET možná nebude týkat

DigiZone.cz: Sony KD-55XD8005 s Android 6.0

Sony KD-55XD8005 s Android 6.0

Podnikatel.cz: Víme první výsledky doby odezvy #EET

Víme první výsledky doby odezvy #EET

DigiZone.cz: NG natáčí v Praze seriál o Einsteinovi

NG natáčí v Praze seriál o Einsteinovi

Podnikatel.cz: Prodává přes internet. Kdy platí zdravotko?

Prodává přes internet. Kdy platí zdravotko?

DigiZone.cz: ČT má dalšího zástupce v EBU

ČT má dalšího zástupce v EBU

120na80.cz: Jak oddálit Alzheimera?

Jak oddálit Alzheimera?

Vitalia.cz: To není kašel! Správná diagnóza zachrání život

To není kašel! Správná diagnóza zachrání život

Měšec.cz: Finančním poradcům hrozí vracení provizí

Finančním poradcům hrozí vracení provizí

Měšec.cz: Zdravotní a sociální pojištění 2017: Připlatíte

Zdravotní a sociální pojištění 2017: Připlatíte

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

120na80.cz: Rakovina oka. Jak ji poznáte?

Rakovina oka. Jak ji poznáte?