neděle 18. listopadu 2012

PyGObject - Aktualizace GUI z callbacku, časovače, i vlákna

Už je to nějaká doba, co jsem se začal vrtat v GTK+ pomocí Pythonu a tak začnu trousit nějaké poznámky související s tímto toolkitem, což mám od začátku v plánu, jen jsem se doposud neodhodlal. Nebude to mít žádný řád, budou to jen útržky kódu, které se budu snažit co nejsrozumitelněji okomentovat a mělo by jít především o věci, které mi chvilku trvalo najít, poskládat a tak.

Začnu něčím základním, na co začátečníci narazí skoro vždy, než si uvědomí, jak jejich GUI vlastně funguje a kdy se aktualizuje okno a jeho součásti. ...


Začínal jsem s PyGTK, které má velmi dobrou dokumentaci, ale jelikož tu už nějaký ten pátek máme GTK+ 3, přesedlal jsem na něj a s tím souvisí i změna statického PyGTK bindingu céčkových GTK knihoven na binding dynamický, založený na GObject Introspection - PyGObject. GTK+ 3 má dokumentaci zatím trochu horší, něco k PyGObject je zde, ale velmi často budete i při použití Pythonu odkázáni na základní dokumentaci pro C, což není až taková překážka, když si uvědomíte rozdíly dané především syntaxí obou jazyků a neobjektovostí C. Dokumentace PyGTK se dá stále částečně využívat, protože ne všechno je úplně jinak, ale spoléhat se na ni nedá. Navíc GTK+ 3 stále podporuje plno objektů ze dvojky, které jsou již zastaralé a mají náhradu, která by se měla používat místo nich. Ovšem je pravda, že ne vždy je to pokrok a v některých případech setrvám u těch původních co to půjde.

Mé příklady tedy budou psané v Pythonu 3 s GTK+ 3, přičemž pro Python 2 v tom nebudou defakto žádné rozdíly v samotném kódu, občas se jen jinak jmenují importované moduly, nejčastěji jen ve změně malých/velkých písmen a v případě změn syntaxe Python 2 v posledních verzích sežere skoro vše z Pythonu 3, případně se dá kompatibilita s vyšší verzí ještě zlepšit moduly z future. Celkem jednoduše jsem přepsal svou GTK aplikaci založenou na Pythonu 2 o čtyřech tisících řádcích tak, že šla spustit jak ve dvojce, tak ve trojce, musel jsem ošetřit jen pár importů a pak hlavně problémy s kódováním a důsledně všechny řetězce překládat na unikód. Pak jsem se na to vykašlal a přešel na čistý Python 3, protože mi vadila zbytečná režie a problémy s kódováním mi tím odpadly úplně, což je neskutečná úleva, když používáte znaky mimo ASCI sadu, třeba češtinu. Python 3 už je v některých distribucích výchozím a čeká to i Ubuntu, ačkoliv jeho vývojové nástroje jako Quickly ještě stále trojku nepodporují a zatím ve standardní instalaci Ubuntu žádnou Python 3 aplikaci nenajdete.

Ale k věci..

Ukážu jeden typický začátečnický příklad - "efektní" vypsání textu znak po znaku do prvku okna. Napíšete si smyčku ve které onen text po písmenech sypete na výstup, přičemž v každém cyklu pozdržíte vypsání dalšího znaku o pár milisekund. Zkusíte si to v terminálu (se spuštěným interpreterem Pythonu)

from time import sleep

txt = 'Hůůůůůůůůůůůááááááááááá'
for i in range(1, len(txt)+1):
   print(txt[0:i])
   sleep(.1)

a je to ok. V GTK aplikaci to pak nebudete cpát do printu ale do nějakého labelu, který ten text zobrazí. Proženete takovou smyčku a budete se divit, že GTK to v onom labelu zobrazí až celé a vaše snaha není vidět, když v terminálu to přece funguje.

Vezmu to od začátku. Hlavní smyčka GTK čeká na události, které přichází z X serveru, identifikuje jejich spojitost s konkrétními prvky GUI, které jste si předem nadefinovali a poskládali a vyvolává tzv signály, v závislosti na vlastnostech daného prvku. Na tyto signály můžete napojit tzv. callbacky, tedy funkce, které budou volány, pokud událost signál vyvolá. Tak můžete reagovat třeba na stisknutí tlačítka.
Hlavní smyčka zavolá vaši funkci a teprve až tato skončí, může pokračovat v krasojízdě a zpracovávat další události, co se mezitím přihodily - například se x krát změnil popisek tlačítka. To, že vy jste si ve smyčce dělali nějaké pauzy ji nezajímá, o tom nemá páru.

Vyřešit se to dá několika způsoby:
  • zavoláte po každé změně Gtk a necháte zpracovat všechny nahromaděné události
  • použijete časovač, který bude v zadaných intervalech volat funkci, která postupně úkol splní a v intervalech se může aplikace věnovat dalším věcem
  • spustíte další vlákno a budete do GUI sahat odtud

Efektní text poprvé, aneb spíme a brzdíme, ale kreslíme:


#!/usr/bin/env python3

from gi.repository import Gtk
from time import sleep

class Funny_Button(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, title="Veselé tlačítko") 
        self.set_size_request(220, 80)
        self.btn = Gtk.Button("Klikni na mě")
        self.btn.connect("clicked", self.on_click)
        self.add(self.btn)

    def on_click(self, w):
        txt = " Hůůůůůůůůůůůááááááááááá"
        for i in range(1,len(txt)+1):
            self.btn.set_label(txt[0:i])
            while Gtk.events_pending():
                Gtk.main_iteration_do(False)
            sleep(0.1)

win = Funny_Button()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()

Finta v tomto případě spočívá v tom, že po každé změně textu na tlačítku necháte ve smyčce zpracovat všechny události, které se zatím nahromadily ve vašem okně. Zkuste si kód spustit a na tlačítko kliknout před vypsáním celého řetězce a uvidíte jak každý další klik přeruší smyčku vypisující text a začne od začátku vlastní, přičemž ta přerušená se vrátí opět ke slovu, aby dokončila co začala, teprve až ta nová skončí. A pokud kliknete znovu, přeruší se i tato, atd.

I když necháváte v jednotlivých cyklech Gtk zpracovat události, stejně aplikaci zdržujete ve sleepech. Může to být jedno, ale také tím můžete zhoršovat odezvu vaší aplikace, když zatímco si hrajete s textem, uživatel již očekává vyřízení dalších požadavků. Pak je dobré použít časovač a smyčku s vypisováním textu rozložit na více akcí, které budou volány v požadovaném časovém intervalu.

Efektní text podruhé, aneb efektivně porcujeme s časovačem


#!/usr/bin/env python3

from gi.repository import Gtk, GObject
from time import sleep

class Funny_Button(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, title="Veselé tlačítko") 
        self.set_size_request(220, 80)
        self.btn = Gtk.Button("Klikni na mě")
        self.btn.connect("clicked", self.on_click)
        self.add(self.btn)

    def on_click(self, w):
        txt = " Hůůůůůůůůůůůááááááááááá"
        self.i = 0
        GObject.timeout_add(100, self.print_txt, txt)

    def print_txt(self, txt):
        try: txt[self.i]
        except IndexError: return False
        else:
            self.i += 1
            self.btn.set_label(txt[0:self.i])
            return True

win = Funny_Button()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()

Časovač spouští v zadaném intervalu funkci print_txt do té doby, než se mu vrátí False. V příkladu to nastane tehdy, když už nebudou další znaky k tisku, tedy pomocná proměnná self.i bude ukazovat na neexistující část řetězce. V tomto provedení se ovšem stane při kliknutí na tlačítko před dopsáním textu to, že se spustí další časovač a tím, že sdílí self.i, dojde k restartování vykreslování textu a zároveň zdvojnásobení rychlosti, neboť tak začnou oba časovače spolupracovat. Řešením by bylo uložit si id časovače do proměnné (timeout = GObject.timeout_...) a při každém stisku tlačítka nejprve ten starý zrušit pomocí GObject.source_remove(timeout).

Efektní text potřetí, aneb konečně trocha vlákniny

Zakládat další vlákno (thread) je to poslední, co je doporučováno a asi první, co začátečníky napadne použít. Většinou skončí neúspěchem, protože do vláken se dá docela slušně zaplést. Jedním z přístupů je ten, že do GUI bude zasahovat jen hlavní vlákno aplikace, které si bude vyzvedávat aktualizovaná data z dalšího vlákna v pravidelných intervalech, druhou možností je to, že dovolíte všem vláknům kecat do GUI. Já ukážu příklad té druhé cesty, která je víc o držku, jak se říká. Ostatně to první řešení je v našem příkladu nesmysl, časovač už máme za sebou, přidávat k němu vlákno se hodí jen pro obsluhu časově náročnějších záležitostí.

#!/usr/bin/env python3

from gi.repository import Gtk, Gdk, GObject
from time import sleep
from threading import Thread
GObject.threads_init()
Gdk.threads_init()

class Funny_Button(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, title="Veselé tlačítko") 
        self.set_size_request(220, 80)
        self.btn = Gtk.Button("Klikni na mě")
        self.btn.connect("clicked", self.on_click)
        self.add(self.btn)

    def on_click(self, w):
        txt = " Hůůůůůůůůůůůááááááááááá"
        t = Print_In_Thread(self.btn, txt)
        t.start()

class Print_In_Thread(Thread):
    def __init__ (self, btn, txt):
        Thread.__init__(self)
        self.btn = btn
        self.txt = txt

    def run(self):
        for i in range(1,len(self.txt)+1):
            Gdk.threads_enter()
            try:
                self.btn.set_label(self.txt[0:i])
            finally: Gdk.threads_leave()
            sleep(0.100)

win = Funny_Button()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gdk.threads_enter()
Gtk.main()
Gdk.threads_leave()

Gtk skrze Gdk podporuje vlákna, ale musíte je inicializovat jak v GObject, tak Gdk a pak každého účastníka GUI radovánek obalit dvojicí funkcí Gdk.threads_enter() a Gdk.threads_leave(). Tedy obalíte hlavní vlákno Gtk.main() a pak pokaždé, když chcete z jiného vlákna přistupovat ke Gtk objektům, musíte nejprve do hlavního vlákna vstoupit (vlastně ho přerušit) a pak z něj opět vystoupit. Konstrukce try: / finally: zajišťuje vystoupení z hlavního vlákna i v případě, že při provádění kódu nastane chyba. Jinak se vám zcela podle očekávání celá aplikace zasekne, protože hlavní vlákno zůstane zastaveno.
V tomto konkrétním případě použití vlákna moc smyslu nedává, režie bude vyšší, než s časovačem, který úkol splní také a možná lépe s ohledem na to, že je v jeho případě velmi jednoduché zrušit původní požadavek při předčasném přijetí dalšího.
Pokud ale čekáte, že mohou nastat zdržení, například při přístupu do databáze, nebo čekání na stažení dat z webu, je jasné, že se vláknu nevyhnete. Pokud ovšem nebudete potřebovat sahat z vlákna do GUI, vynecháte řádky, které jsem označil tučně, přičemž otestovat to můžete tak, že místo nastavování popisku tlačítka, použijete print pro výpis do konzole.

Pokud máte výhrady, dotazy, připomínky, tak tu máme komentáře ;)

Žádné komentáře:

Okomentovat

Zkuste prosím při komentováni používat místo volby Anonymní volbu Název/adresa URL, kde vyplníte nějakou přezdívku, adresu zadávat nemusíte. Vědět, které příspěvky jsou od jednoho člověka, je fajn. Díky.

Pokud by se vám náhodou odeslaný komentář na stránce nezobrazil, vytáhnu ho z koše hned jak si toho všimnu. I Google spam filter se občas sekne.