10. Udalosti v grafickej ploche

Naučíme sa v našich programoch využívať tzv. udalosti, ktoré vznikajú v bežiacej grafickej aplikácii buď aktivitami používateľa (klikanie myšou, stláčanie klávesov) alebo operačného systému (tikanie časovača). Na úvod si pripomeňme, čo už vieme o grafickej ploche. Pomocou metód grafickej plochy Canvas (definovanej v module tkinter) kreslíme grafické objekty:

  • canvas.create_line() - kreslí úsečku alebo krivku z nadväzujúcich úsečiek

  • canvas.create_oval() - kreslí elipsu

  • canvas.create_rectangle() - kreslí obdĺžnik

  • canvas.create_text() - vypíše text

  • canvas.create_polygon() - kreslí vyfarbený útvar zadaný bodmi na obvode

  • canvas.create_image() - kreslí obrázok (prečítaný zo súboru .gif alebo .png)

Ďalšie pomocné metódy manipulujú s už nakreslenými objektami:

  • canvas.delete() - zruší objekt

  • canvas.move() - posunie objekt

  • canvas.coords() - zmení súradnice objektu

  • canvas.itemconfig() - zmení ďalšie parametre objektu (napr. farba, hrúbka, text, obrázok, …)

Ďalšie metódy umožňujú postupne zobrazovať vytváranú kresbu:

  • canvas.update() - zobrazí nové zmeny v grafickej ploche

  • canvas.after() - pozdrží beh programu o zadaný počet milisekúnd

Udalosť

Udalosťou (event) voláme akciu, ktorá vznikne mimo behu programu a program môže na túto situáciu reagovať. Najčastejšie sú to udalosti od pohybu a klikania myši, od stláčania klávesov, od časovača (vnútorných hodín OS), od rôznych zariadení, … V našom programe potom môžeme nastaviť, čo sa má udiať pri ktorej udalosti. Tomuto sa zvykne hovoriť udalosťami riadené programovanie (event-driven programming).

Používa sa na to mechanizmus obsluhy udalosti (ovládač udalosti, event handler), čo je funkcia v našej aplikácii, ktorá má na starosti spracovávanie príslušnej udalostí. Napríklad, ak budeme potrebovať v našom programe spracovávať udalosť kliknutie myšou do grafickej plochy, napíšeme ovládač udalosti (obyčajnú pythonovskú funkciu) a systému oznámime, aby ju zavolal vždy, keď vznikne táto udalosť. Takémuto nastaveniu (event handler) funkcie k nejakej udalosti budeme hovoriť zviazanie (binding).

Naučíme sa, ako v našich grafických programoch reagovať na udalosti od myši a klávesnice.

Aby grafická plocha reagovala na klikania myšou, musíme ju zviazať (bind) s príslušnou udalosťou (event).

metóda bind()

Táto metóda grafickej plochy slúži na zviazanie niektorej konkrétnej udalosti s nejakou funkciou, ktorá sa bude v programe starať o spracovanie tejto udalosti (event handler). Jej formát je:

canvas.bind(meno_udalosti, funkcia)

kde meno_udalosti je znakový reťazec s popisom udalosti (napr. pre kliknutie tlačidlom myši) a funkcia je referencia na funkciu, ktorá by sa mala spustiť pri vzniku tejto udalosti. Táto funkcia, musí byť definovaná s práve jedným parametrom, v ktorom nám systém prezradí detaily vzniknutej udalosti.

Klikanie a ťahanie myšou

Ukážeme tieto tri „myšacie“ udalosti:

  • kliknutie (zatlačenie tlačidla myši) - reťazec '<ButtonPress>'

  • ťahanie (posúvanie myšou so zatlačeným tlačidlom alebo bez zatlačeného tlačidla) - reťazec '<Motion>'

  • pustenie myši - reťazec '<ButtonRelease>'

Klikanie myšou

Kliknutie myšou do grafickej plochy vyvolá udalosť s menom '<ButtonPress>'. Ukážme ako vyzerá samotné zviazanie funkcie:

import tkinter

canvas = tkinter.Canvas()
canvas.pack()

canvas.bind('<ButtonPress>', print)

Druhý parameter metódy bind() musí byť referencia na funkciu, ale nie hocijakú, na funkciu, ktorá má jeden parameter. Tu sme použili štandardnú funkciu print a keďže do bind() treba poslať referenciu na túto funkciu, nesmieme za identifikátor print písať zátvorky ().

Keď teraz tento malý testovací program spustíte, objaví sa prázdne grafické okno a program čaká, čo sa bude diať (Ak budete tento program spúšťať mimo IDLE, nezabudnite na záver pripísať volanie mainloop(), napr. canvas.mainloop()). Keďže sme grafickej ploche udalosť kliknutie myšou zviazali s funkciou print(), každé kliknutie do plochy automaticky vyvolá túto funkciu. Pri klikaní dostávame nejaký takýto výpis (pri každom kliknutí do plochy sa vypíše jeden riadok):

<ButtonPress event state=Mod1 num=1 x=194 y=117>
<ButtonPress event state=Mod1 num=1 x=194 y=173>
<ButtonPress event state=Mod1 num=2 x=328 y=31>
<ButtonPress event state=Mod1 num=3 x=17 y=9>

Zviazanie udalosti s nejakou funkciou teda znamená, že každé vyvolanie udalosti (kliknutie tlačidlom myši do grafickej plochy) automaticky zavolá zviazanú funkciu. To, čo nám pritom vypisuje print(), je ten jeden parameter, ktorý tkinter posiela pri každom jeho zavolaní.

Vytvorme si teraz vlastnú funkciu, ktorú zviažeme s udalosťou kliknutia:

import tkinter

def klik(parameter):
    print('klik')

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)

Vytvorili sme funkciu klik(), ktorá sa bude automaticky volať pri každom kliknutí do plochy. Nezabudli sme do hlavičky funkcie pridať jeden formálny parameter, inak by Python pri vzniku udalosti protestoval, že našu funkciu klik() chcel zavolať s jedným parametrom a my sme ho nezadeklarovali. Ak by sme tento program spustili, pri každom kliknutí do plochy by sa do textovej plochy shellu mal vypísať text 'klik'.

Teraz k samotnému parametru v našej funkcii klik(): tento parameter slúži na to, aby nám tkinter mohol nejakým spôsobom posielať informácie o udalosti. My vieme, že funkcia klik() sa zavolá vždy, keď sa niekam klikne, ale nevieme, kde presne do plochy sa kliklo. Práve na toto slúži tento parameter: z neho vieme vytiahnuť, napr. x-ovú a y-ovú súradnicu kliknutého miesta. V nasledovnom príklade vidíme, ako sa to robí. Ešte sme tento parameter premenovali na event (udalosť po anglicky), aby sme lepšie rozlíšili to, že s týmto parametrom prišla udalosť. Preto tieto súradnice získame ako event.x a event.y:

import tkinter

def klik(event):
    print('klik', event.x, event.y)

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)

V tomto programe sa pri každom kliknutí vypíšu do shellu aj súradnice kliknutého miesta.

V ďalšom príklade ukážeme, ako využijeme súradnice kliknutého bodu v ploche:

import tkinter

def klik(event):
    x, y = event.x, event.y
    canvas.create_oval(x - 10, y - 10, x + 10, y + 10, fill='red')

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)

Teraz sa pri kliknutí nakreslí červený kruh a využijú sa pritom súradnice kliknutého miesta: stred kruhu je kliknuté miesto.

Akcia, ktorá sa vykoná pri kliknutí môže byť veľmi jednoduchá, napr. spájanie kliknutého bodu s nejakým bodom grafickej plochy:

import tkinter

def klik(event):
    canvas.create_line(100, 200, event.x, event.y)

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)

Napr.

_images/10_01.png

Ale môžu sa nakresliť aj komplexnejšie kresby, napr. 10 sústredných farebných kruhov:

import tkinter
from random import randrange

def klik(event):
    x, y = event.x, event.y
    for r in range(50, 0, -5):
        farba = f'#{randrange(256**3):06x}'
        canvas.create_oval(x - r, y - r, x + r, y + r, fill=farba)

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)

Napr.

_images/10_02.png

Vráťme sa k príkladu, v ktorom sme kreslili malé krúžky:

import tkinter

def klik(event):
    x, y = event.x, event.y
    canvas.create_oval(x - 5, y - 5, x + 5, y + 5, fill='red')

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)

Do tohto programu chceme pridať takéto správanie: tieto kliknuté body (červené krúžky) sa budú postupne spájať úsečkami (zrejme sa bude úsečka kresliť až od druhého kliknutia). Pridáme dve globálne premenné xx a yy, v ktorých si budeme pamätať predchádzajúci kliknutý bod. Pred prvým kliknutím sme do xx priradili None, čo bude označovať, že predchádzajúci vrchol ešte nebol kliknutý:

import tkinter

xx = yy = None

def klik(event):
    x, y = event.x, event.y
    canvas.create_oval(x - 5, y - 5, x + 5, y + 5, fill='red')
    if xx != None:
        canvas.create_line(xx, yy, x, y)
    xx, yy = x, y

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)

Žiaľ to nefunguje: po spustení a kliknutí sa dozvieme:

...
File ..., line 8, in klik
    if xx != None:
UnboundLocalError: local variable 'xx' referenced before assignment

Problémom sú tu globálne premenné. Používať globálne premenné vo vnútri funkcii môžeme, len dovtedy, kým ich nemeníme. Priraďovací príkaz vo funkcii totiž znamená, že vytvárame novú lokálnu premennú (v mennom priestore funkcie klik()). Takže Python to v tejto funkcii pochopil takto: do premenných xx a yy sa vo vnútri funkcie priraďuje nejaká hodnota, takže obe sú lokálne premenné. Keď ale príde vykonávanie funkcie na podmienený príkaz if  xx != None:, Python už vie, že xx je lokálna premenná, ktorá nemá zatiaľ priradenú žiadnu hodnotu. A preto nám oznámil túto chybovú správu: UnboundLocalError: local variable 'xx' referenced before assignment (chceme používať lokálnu premennú ‚xx‘ skôr ako sme do nej niečo priradili).

Takže s globálnymi premennými vo funkcii sa bude musieť pracovať nejako inak. Zrejme, kým do takejto premennej nepotrebujeme vo funkcii nič priradzovať, iba ju používať, problémy nie sú. Problém nastáva vtedy, keď chceme (pomocou priraďovacieho príkazu) meniť obsah globálnej premennej.

Toto nám pomôže vyriešiť nový príkaz global:

príkaz global

príkaz má tvar:

global premenná
global premenná, premenná, premenná, ...

Príkaz sa používa vo funkcii vtedy, keď v nej chceme pracovať s globálnou premennou (alebo aj s viac premennými), ale nechceme, aby ju Python vytvoril aj v lokálnom mennom priestore, ale ponechal len v globálnom.

Po doplnení tohto príkazu do predchádzajúceho príkladu všetko funguje tak, ako má:

import tkinter

xx = yy = -1

def klik(event):
    global xx, yy
    x, y = event.x, event.y
    canvas.create_oval(x - 5, y - 5, x + 5, y + 5, fill='red')
    if xx >= 0:
        canvas.create_line(xx, yy, x, y)
    xx, yy = x, y

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)

Po spustení dostávame takýto obrázok:

_images/10_03.png

Nebezpečné

Príkaz global umožňuje modifikovať globálne premenné vo funkciách, teda vlastne robiť tzv. vedľajší účinok (side effect) na globálnych premenných. Toto je ale veľmi nesprávny spôsob programovania (bad programming practice) a väčšinou svedčí o programátorovi začiatočníkovi, amatérovi.

Kým sa nenaučíme, ako to obísť, budeme to používať, ale veľmi opatrne. Neskôr to využijeme veľmi výnimočne, najmä pri ladení. Správne sa takéto problémy riešia definovaním vlastných tried a použitím atribútov tried.

Ťahanie myšou

Obsluha udalosti ťahanie myšou (pohyb myši bez zatlačeného alebo so zatlačeným tlačidlom) je veľmi podobná klikaniu. Udalosť má meno '<Motion>'. Pozrime, čo sa zmení, keď kliknutie '<ButtonPress>' nahradíme ťahaním '<Motion>':

import tkinter

def klik(event):
    x, y = event.x, event.y
    canvas.create_oval(x - 10, y - 10, x + 10, y + 10, fill='red')

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<Motion>', klik)      # '<Motion>' namiesto '<ButtonPress>'

Funguje to veľmi dobre: pri ťahaní sa na pozícii myši kreslia červené kruhy. Pri pomalom ťahaní sú kruhy nakreslené veľmi nahusto.

Ak vo funkcii klik pri ťahaní myšou zakaždým zmažeme grafickú plochu (zrušíme všetky nakreslené objekty), v ploche zostanú nakreslené len objekty, ktoré sa kreslili neskôr. Napr.

import tkinter

def klik(event):
    x, y = event.x, event.y
    canvas.delete('all')
    canvas.create_oval(x - 10, y - 10, x + 10, y + 10, fill='red')

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<Motion>', klik)

Tento program nakreslí červený krúžok, ktorý bude „prilepený“ na kurzor myši. Vyskúšajte namiesto krúžku vypisovať text:

canvas.create_text(x, y, text=(x, y))

Často budeme v našich programoch spracovávať obe udalosti: kliknutie aj ťahanie. Niekedy je to tá istá funkcia, inokedy sú rôzne a preto je dobre ich pomenovať zodpovedajúcimi názvami, napr.

import tkinter

def klik(event):
    x, y = event.x, event.y
    canvas.create_oval(x - 10, y - 10, x + 10, y + 10, fill='red')

def tahanie(event):
    x, y = event.x, event.y
    canvas.create_oval(x - 5, y - 5, x + 5, y + 5, fill='blue')

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)
canvas.bind('<Motion>', tahanie)

Pri posúvaní myši sa kreslia malé modré kruhy, pri kliknutí sa nakreslí červený kruh. Všimnite si, že ťahanie (kreslenie modrých kruhov) funguje aj so zatlačeným tlačidlom myši.

_images/10_04.png

Veľmi často budeme potrebovať, aby sa funkcia na ťahanie (event handler tahanie) zavolala len v prípade, že je súčasne s ťahaním zatlačené aj tlačidlo myši. Do mena udalosti '<Motion>' vtedy na začiatok pripíšeme B1-, čo bude označovať, že funkcia tahanie si bude všímať len ťahania so zatlačeným ľavým tlačidlom myši. Otestujte, ako sa bude predchádzajúci program správať, keď namiesto canvas.bind('<Motion>', tahanie) zapíšeme canvas.bind('<B1-Motion>', tahanie).

Na podobnom princípe môžeme upraviť aj kreslenie lúčov z bodu (100, 200) do pozície myši:

import tkinter

def klik(event):
    canvas.create_line(100, 200, event.x, event.y, fill='red', width=3)

def tahanie(event):
    canvas.create_line(100, 200, event.x, event.y)

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)
canvas.bind('<Motion>', tahanie)

Pri posúvaní myši alebo ťahaní sa nakreslia čierne úsečky, pri každom kliknutí sa nakreslí červená úsečka:

_images/10_05.png

Alebo malou zmenou kliknutím definujeme pozíciu (globálny bod (xx, yy)), z ktorého sa budú kresliť lúče:

import tkinter

def klik(event):
    global xx, yy
    xx, yy = event.x, event.y

def tahanie(event):
    canvas.create_line(xx, yy, event.x, event.y)

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)
canvas.bind('<B1-Motion>', tahanie)

Všimnite si, že tu sme použili '<B1-Motion>'. Zamyslite sa, prečo by tu pri '<Motion>' vznikla chyba. Po spustení dostávame takúto kresbu:

_images/10_06.png

Ďalej nadviažeme na program, v ktorom sme postupne spájali kliknuté body. Pri tomto programe sme využili nový príkaz global, aby sme sa dostali ku globálnym premenným. Tento nie najvhodnejší príkaz môžeme obísť, keď využijeme meniteľný (mutable) typ zoznam.

Budeme ťahať (ťahaním myši so zatlačeným tlačidlom) jednu dlhú lomenú čiaru, pričom si budeme ukladať súradnice prijaté z udalosti do zoznamu čísel:

import tkinter

zoznam = []

def klik(event):
    zoznam[:] = [event.x, event.y]

def tahanie(event):
    zoznam.extend([event.x, event.y])
    canvas.coords(ciara, zoznam)

canvas = tkinter.Canvas()
canvas.pack()
ciara = canvas.create_line(0, 0, 0, 0)
canvas.bind('<ButtonPress>', klik)
canvas.bind('<B1-Motion>', tahanie)

Všimnite si, že v týchto dvoch funkciách používame 3 globálne premenné:

  • canvas - referencia na grafickú plochu

  • ciara - identifikátor objektu čiara, potrebujeme ho pre neskoršie menenie postupnosti súradníc príkazom coords()

  • zoznam - zoznam súradníc je meniteľný objekt, teda môžeme meniť obsah zoznamu bez toho, aby sme do premennej zoznam priraďovali (priraďujeme buď do rezu alebo voláme metódu extend(), t. j. prilep nejakú postupnosť na koniec zoznamu)

    • modifikovanie zoznamu vo funkcii, pričom zoznam nie je parametrom funkcie, tiež nie je najvhodnejším spôsobom programovania, tiež je to nevhodný vedľajší účinok podobne ako príkaz global, zatiaľ to inak robiť nevieme, tak je to dočasne akceptovateľné

Ťahanie čiary v predchádzajúcom príklade žiaľ kreslí jedinú čiaru: každé ďalšie kliknutie a ťahanie začne kresliť novú čiaru, pričom stará čiara zmizne. Vyriešime to tak, že pri kliknutí začneme kresliť novú čiaru a tú starú necháme tak:

import tkinter

zoznam = []

def klik(event):
    global ciara
    zoznam[:] = [event.x, event.y]
    ciara = canvas.create_line(0, 0, 0, 0)

def tahanie(event):
    zoznam.extend([event.x, event.y])
    canvas.coords(ciara, zoznam)

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)
canvas.bind('<B1-Motion>', tahanie)

Poexperimentujeme:

_images/10_07.png

Malou zmenou dosiahneme veľmi zaujímavý efekt. Vyskúšajte a poštudujte:

import tkinter
from random import randrange

zoznam = []

def klik(event):
    global poly
    zoznam[:] = [event.x, event.y]
    farba = f'#{randrange(256**3):06x}'
    poly = canvas.create_polygon(0, 0, 0, 0, fill=farba)

def tahanie(event):
    zoznam.extend([event.x, event.y])
    canvas.coords(poly, zoznam)

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)
canvas.bind('<B1-Motion>', tahanie)
_images/10_08.png

Udalosti od klávesnice

Aj každé zatlačenie nejakého klávesu na klávesnici môže vyvolať udalosť. Základnou univerzálnou udalosťou je '<Key>', ktorá sa vyvolá pri každom zatlačení nejakého klávesu. Môžeme otestovať:

import tkinter

def test(event):
    print(event.keysym)

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind_all('<Key>', test)

Všimnite si, že sme museli zapísať bind_all() namiesto bind(). Každé zatlačenie nejakého klávesu vypíše jeho reťazcovú reprezentáciu, napr.

a
Shift_L
A
Left
Right
Up
Down
Next
Escape
Return
F1

Pritom každý jeden kláves môže vyvolať aj samostatnú udalosť. Ako meno udalosti treba uviesť meno klávesu (jeho reťazcovú reprezentáciu) v '<...>' zátvorkách alebo samostatný znak, napr.

import tkinter

def test_vlavo(event):
    print('sipka vlavo')

def test_a(event):
    print('stlacil si klaves a')

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind_all('a', test_a)
canvas.bind_all('<Left>', test_vlavo)

Tento program reaguje len na stláčanie klávesu 'a' a šípky vľavo. Všetky ostatné klávesy ignoruje.

Často sa samostatné udalosti pre jednotlivé šípky použijú podobne, ako v tomto príklade:

import tkinter

x, y = 200, 200
zoznam = [x, y]

def kresli(dx, dy):
    global x, y
    x += dx
    y += dy
    zoznam.extend((x, y))
    canvas.coords(ciara, zoznam)

def udalost_vlavo(event):
    kresli(-10, 0)

def udalost_vpravo(event):
    kresli(10, 0)

def udalost_hore(event):
    kresli(0, -10)

def udalost_dolu(event):
    kresli(0, 10)

canvas = tkinter.Canvas()
canvas.pack()

ciara = canvas.create_line(0, 0, 0, 0)        # zatiaľ prázdna čiara
canvas.bind_all('<Left>', udalost_vlavo)
canvas.bind_all('<Right>', udalost_vpravo)
canvas.bind_all('<Up>', udalost_hore)
canvas.bind_all('<Down>', udalost_dolu)

Kreslenie pomocou tohto programu môže pripomínať detskú hračku „magnetická tabuľka“:

_images/10_09.png

Časovač

Pripomeňme si, ako by sme doteraz v grafickej ploche kreslili krúžky na náhodné pozície s nejakým časovým pozdržaním (napr. 100 ms):

import tkinter
from random import randrange

def kresli():
    while True:
        x = randrange(350)
        y = randrange(260)
        canvas.create_oval(x - 10, y - 10, x + 10, y + 10, fill='red')
        canvas.update()
        canvas.after(100)

canvas = tkinter.Canvas()
canvas.pack()

kresli()
print('hotovo')

Použili sme tu nekonečný cyklus a preto sa príkaz print('hotovo') za volaním kresli() s nekonečným while-cyklom už nikdy nevykoná.

Metóda grafickej plochy after(), ktorá pozdrží výpočet o nejaký počet milisekúnd, je oveľa všestrannejšia: môžeme pomocou nej štartovať, tzv. časovač:

metóda after()

Metóda after() grafickej plochy môže mať jeden z týchto tvarov:

canvas.after(milisekundy)
canvas.after(milisekundy, funkcia)

Prvý parameter milisekundy už poznáme: výpočet sa pozdrží o príslušný počet milisekúnd. Lenže, ak je metóda zavolaná aj s druhým parametrom funkcia, výpočet sa naozaj nepozdrží, ale pozdrží sa vyvolanie zadanej funkcie (parameter funkcia musí byť referencia na funkciu, teda väčšinou bez okrúhlych zátvoriek). Táto vyvolaná funkcia musí byť definovaná bez parametrov.

S týmto druhým parametrom metóda after() naplánuje (niekedy v budúcnosti) spustenie nejakej funkcie a pritom výpočet pokračuje normálne ďalej na ďalšom príkaze za after() (bez pozdržania).

Tomuto mechanizmu hovoríme časovač (naplánovanie spustenia nejakej akcie), po anglicky timer. Najčastejšie sa používa takto:

def casovac():
    # príkazy
    canvas.after(cas, casovac)

V tomto prípade funkcia naplánuje spustenie samej seba po nejakom čase. Môžete si to predstaviť tak, že v počítači tikajú nejaké hodiny s udanou frekvenciou v milisekundách a pri každom tiknutí sa vykonajú príkazy v tele funkcie.

Najprv jednoduchý test:

import tkinter

def casovac():
    print('tik')
    canvas.after(1000, casovac)

canvas = tkinter.Canvas()
canvas.pack()
casovac()

Časovač každú sekundu vypíše do textovej plochy reťazec 'tik'.

Predchádzajúci program s náhodnými červenými krúžkami teraz prepíšeme s použitím časovača:

import tkinter
from random import randrange

def kresli():
    x = randrange(350)
    y = randrange(260)
    canvas.create_oval(x - 10, y - 10, x + 10, y + 10, fill='red')
    #canvas.update()
    canvas.after(100, kresli)

canvas = tkinter.Canvas()
canvas.pack()
kresli()             # naštartovanie časovača
print('hotovo')

Po spustení funkcie kresli() (tá nakreslí jeden kruh a zavolá after(), t. j. naplánuje ďalšie kreslenie) sa pokračuje ďalším príkazom, t. j. sa vypíše print('hotovo'), program končí a v shelli môžeme zadávať ďalšie príkazy. Pritom ale stále beží náš rozbehnutý časovač. V shelli by sme mohli napr. zapísať:

>>> canvas.delete('all')

Tento príkaz vymaže grafickú plochu, ale bežiaci časovač (rozbehnutá funkcia kresli()) do nej bude pokračovať v kreslení červených krúžkov.

Keďže počas behu časovača môže program vykonávať ďalšie akcie, môže spustiť hoci aj ďalší časovač. Zapíšme:

import tkinter
from random import randrange

def kresli():
    x = randrange(350)
    y = randrange(260)
    canvas.create_oval(x - 10, y - 10, x + 10, y + 10, fill='red')
    canvas.after(100, kresli)

def kresli1():
    x = randrange(350)
    y = randrange(260)
    canvas.create_rectangle(x - 10, y - 10, x + 10, y + 10, fill='blue')
    canvas.after(300, kresli1)

canvas = tkinter.Canvas()
canvas.pack()
kresli()
kresli1()
print('hotovo')

Program teraz spustí oba časovače: kreslia sa červené krúžky a modré štvorčeky. Keďže druhý časovač má svoj interval 300 milisekúnd, teda „tiká“ 3-krát pomalšie ako prvý, kreslí 3-krát menej modrých štvorčekov ako prvý časovač červených krúžkov, napr.

_images/10_10.png

Zastavovanie časovača

Na zastavenie časovača nemáme žiaden príkaz. Časovač môžeme zastaviť len tak, že on sám v svojom tele na konci nezavolá metódu canvas.after() a tým aj skončí. Upravíme predchádzajúci príklad tak, že zadefinujme dve globálne premenné, ktoré budú slúžiť pre oba časovače na zastavovanie:

import tkinter
from random import randrange

bezi = bezi1 = True

def kresli():
    x = randrange(350)
    y = randrange(260)
    canvas.create_oval(x - 10, y - 10, x + 10, y + 10, fill='red')
    if bezi:
        canvas.after(100, kresli)

def kresli1():
    x = randrange(350)
    y = randrange(260)
    canvas.create_rectangle(x - 10, y - 10, x + 10, y + 10, fill='blue')
    if bezi1:
        canvas.after(300, kresli1)

canvas = tkinter.Canvas()
canvas.pack()
kresli()
kresli1()
print('hotovo')

Teraz bežia oba časovače, ale stačí zavolať, napr.

>>> bezi = False

V tomto momente sa prvý časovač zastaví a beží iba druhý, teda sa kreslia len modré štvorčeky. Ak by sme chceli znovu naštartovať prvý časovač, nesmieme zabudnúť zmeniť premennú bezi a zavolať kresli():

>>> bezi = True
>>> kresli()

Opäť bežia súčasne oba časovače.

Globálne premenné môžeme využiť aj na iné účely: môžeme nimi meniť „parametre“ bežiacich príkazov. Napr. farbu krúžkov, ale aj interval tikania časovača, napr.

import tkinter
from random import randrange

bezi = True
farba = 'red'
cas = 100
vel = 10

def casovac():
    x = randrange(350)
    y = randrange(260)
    canvas.create_oval(x - vel, y - vel, x + vel, y + vel, fill=farba)
    if bezi:
        canvas.after(cas, casovac)

canvas = tkinter.Canvas()
canvas.pack()
casovac()

Počas behu tohoto časovača ho môžeme nielen zastavovať, ale meniť mu rýchlosť (zmenou premennej cas), farbu aj veľkosť krúžkov, napr.

>>> vel, farba = 30, 'blue'

Od tohto momentu sa kreslia modré ale väčšie krúžky.

>>> cas = 30

Zrýchli časovač na 30 milisekúnd.

Ďalšia ukážka bude hýbať dvoma obrázkami autíčok (napr. auto1.png a auto2.png) rôznou rýchlosťou:

import tkinter

def pohyb():
    canvas.move(auto1, 4, 0)
    canvas.move(auto2, -5, 0)
    canvas.after(30, pohyb)

canvas = tkinter.Canvas(width=600)
canvas.pack()

obr_auto1 = tkinter.PhotoImage(file='auto1.png')
obr_auto2 = tkinter.PhotoImage(file='auto2.png')

auto1 = canvas.create_image(0, 150, image=obr_auto1)
auto2 = canvas.create_image(600, 150, image=obr_auto2)

pohyb()

Program rozbehne dve autíčka proti sebe, ale nekontroluje, či sa zrazia:

_images/10_11.png

Aby sa autá nerozbehli už pri štarte programu, ale až po kliknutí do plochy, zapíšeme:

import tkinter

def start(event):
    canvas.coords(auto1, 0, 150)
    canvas.coords(auto2, 600, 150)
    pohyb()

def pohyb():
    canvas.move(auto1, 4, 0)
    canvas.move(auto2, -5, 0)
    canvas.after(30, pohyb)

canvas = tkinter.Canvas(width=600)
canvas.pack()

obr_auto1 = tkinter.PhotoImage(file='auto1.png')
obr_auto2 = tkinter.PhotoImage(file='auto2.png')

auto1 = canvas.create_image(0, 150, image=obr_auto1)
auto2 = canvas.create_image(600, 150, image=obr_auto2)

canvas.bind('<ButtonPress>', start)

Teraz chceme pridať kontrolu, či sa už obe autá zrazili. Vtedy zastavíme časovač a vypíšeme nejakú správu. Keďže si nikde neuchovávame momentálnu pozíciu áut, využijeme metódu coords(), ktorá okrem zmeny súradníc grafického objektu, dokáže zistiť momentálne jeho súradnice. Napr. ak počas behu časovača zapíšeme:

>>> canvas.coords(auto1)
[164.0, 150.0]
>>> canvas.coords(auto2)
[395.0, 150.0]

Vidíme, že táto funkcia nám vracia polohu autíčka (súradnice stredu obrázka), preto môžeme zastaviť časovač testovaním x-ových súradníc autíčok:

import tkinter

def start(event):
    canvas.coords(auto1, 0, 150)
    canvas.coords(auto2, 600, 150)
    pohyb()

def pohyb():
    canvas.move(auto1, 4, 0)
    canvas.move(auto2, -5, 0)
    x_auto1 = canvas.coords(auto1)[0]
    x_auto2 = canvas.coords(auto2)[0]
    if x_auto1 > x_auto2 - 140:
        canvas.create_text(200, 50, text='BUM', fill='red', font='arial 40 bold')
    else:
        canvas.after(30, pohyb)

canvas = tkinter.Canvas(width=600)
canvas.pack()

obr_auto1 = tkinter.PhotoImage(file='auto1.png')
obr_auto2 = tkinter.PhotoImage(file='auto2.png')

auto1 = canvas.create_image(0, 150, image=obr_auto1)
auto2 = canvas.create_image(600, 150, image=obr_auto2)

canvas.bind('<ButtonPress>', start)

Pri stretnutí autíčok dostávame:

_images/10_12.png

Hoci tento program funguje dobre, má niekoľko malých nedostatkov:

  • ak počas pohybu autíčok (beží časovač pohyb()) znovu klikneme do plochy (vyvoláme udalosť start()), autíčka skočia do svojich štartových pozícií a znovu sa vyvolá časovač pohyb(); teraz to ale vyzerá, že autíčka idú dvojnásobnou rýchlosťou - totiž teraz bežia naraz dva časovače pohyb() aj pohyb(), ktoré oba pohnú oboma autíčkami - hoci je to zaujímavé, budeme sa snažiť tomuto zabrániť

  • ak autíčka do seba nabúrajú, vypíše sa text 'BUM', ktorý tam bude svietiť aj po opätovnom naštartovaní autíčiek

Vylepšíme funkciu start() takto:

  • keďže táto štartuje časovač pohyb(), zablokujeme opätovné kliknutie tým, že zrušíme zviazanie udalosti klik s funkciou start()

  • ak svieti text 'BUM', tak ho vymažeme

Využijeme nový príkaz unbind(), pomocou ktorého vieme rozviazať nejakú konkrétnu udalosť s funkciou:

metóda unbind()

Metóda zruší zviazanie príslušnej udalosti:

canvas.unbind(meno_udalosti)

V časovači (vo funkcii) pohyb() pri výpise správy 'BUM', keďže zastavujeme časovač, opätovne zviažeme kliknutie do plochy s udalosťou start():

import tkinter

text = None

def start(event):
    canvas.unbind('<ButtonPress>')              # zruší klikaciu udalosť
    canvas.coords(auto1, 0, 150)
    canvas.coords(auto2, 600, 150)
    canvas.delete(text)
    pohyb()

def pohyb():
    global text
    canvas.move(auto1, 4, 0)
    canvas.move(auto2, -5, 0)
    x_auto1 = canvas.coords(auto1)[0]
    x_auto2 = canvas.coords(auto2)[0]
    if x_auto1 > x_auto2 - 140:
        text = canvas.create_text(200, 50, text='BUM', fill='red', font='arial 40 bold')
        canvas.bind('<ButtonPress>', start)     # obnoví klikaciu udalosť
    else:
        canvas.after(30, pohyb)

canvas = tkinter.Canvas(width=600)
canvas.pack()

obr_auto1 = tkinter.PhotoImage(file='auto1.png')
obr_auto2 = tkinter.PhotoImage(file='auto2.png')

auto1 = canvas.create_image(0, 150, image=obr_auto1)
auto2 = canvas.create_image(600, 150, image=obr_auto2)

canvas.bind('<ButtonPress>', start)

Zhrnutie udalostí od myši

Udalosť '<ButtonPress>' reprezentuje kliknutie ľubovoľným tlačidlom myši. Väčšinou má každá myš tri tlačidlá: ľavé, stredné a pravé. Priamo v názve udalosti môžeme určiť, aby sa udalosť vyvolala len pri konkrétnom tlačidle. Vtedy bude názov udalosti:

  • '<ButtonPress-1>' pre zatlačenie ľavého tlačidla

  • '<ButtonPress-2>' pre zatlačenie stredného tlačidla

  • '<ButtonPress-3>' pre zatlačenie pravého tlačidla

Môžeme zapísať napr.

import tkinter

def klik_lavy(event):
    canvas.create_text(event.x, event.y, text=1, font='Arial 30', fill='blue')

def klik_stredny(event):
    canvas.create_text(event.x, event.y, text=2, font='Arial 30', fill='green')

def klik_pravy(event):
    canvas.create_text(event.x, event.y, text=3, font='Arial 30', fill='red')

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress-1>', klik_lavy)
canvas.bind('<ButtonPress-2>', klik_stredny)
canvas.bind('<ButtonPress-3>', klik_pravy)

Každá funkcia sa stará o kliknutie svojho tlačidla. V parametri event okrem súradníc event.x a event.y dostávame aj poradové číslo tlačidla event.num, ktorým sme práve klikli. Predchádzajúci program môžeme zapísať aj takto:

import tkinter

def klik(event):
    farba = ['blue', 'green', 'red'][event.num-1]
    canvas.create_text(event.x, event.y, text=event.num, font='Arial 30', fill=farba)

canvas = tkinter.Canvas()
canvas.pack()
canvas.bind('<ButtonPress>', klik)

Je to na programátorovi, ktorú z týchto možností preferuje.

Meno udalosti '<ButtonPress-1>' existuje aj v skrátenej forme a to buď '<Button-1>' alebo dokonca '<1>' (zrejme to funguje aj s 2 aj 3). Opäť je na programátorovi, ktorý zápis preferuje a pritom neznižuje čitateľnosť kódu.

Už poznáme meno udalosti pre ťahanie myši '<Motion>'. Táto pomenovaná udalosť zavolá príslušnú funkciu so zatlačeným aj bez zatlačeného tlačidla. Poznáme aj variant mena udalosti '<B1-Motion>', vďaka čomu spracovávame len ťahanie so zatlačeným ľavým tlačidlom myši. Podobne ako pri klikaní môžeme špecifikovať zatlačené tlačidlo číslom od 1 do 3, bude to fungovať aj pri ťahaní, preto:

  • '<B1-Motion>' ťahanie so zatlačeným ľavým tlačidlom

  • '<B2-Motion>' ťahanie so zatlačeným stredným tlačidlom

  • '<B3-Motion>' ťahanie so zatlačeným pravým tlačidlom

Neskôr uvidíme využitie udalosti aj od pustenia tlačidla myši '<ButtonRelease>'. Aj táto udalosť funguje pre varianty s konkrétnym tlačidlom myši, napr. '<ButtonRelease-1>' spracováva len pustenie ľavého tlačidla myši.



Cvičenia

L.I.S.T.

  1. Napíš program, ktorý pri každom kliknutí do grafickej plochy zmení farbu nakresleného obdĺžnika. Najprv zadefinuj štyri premenné (x1, y1, x2, y2 = 100, 50, 200, 100) a pomocou nich nakresli obdĺžnik.

    1. každé kliknutie zmení farbu obdĺžnika na náhodnú farbu

    2. každé kliknutie cyklicky strieda jednu zo 4 farieb, napr. zadefinuj farby = ['blue', 'red', 'green', 'yellow']

      • pripomeň si funkciu posun(zoznam) z predchádzajúcich cvičení

    • klikania by nemali vytvárať nové obdĺžniky (create_rectangle), ale mali by meniť farbu len jednému nakreslenému obdĺžniku

    • nepoužívaj global


  1. Predchádzajúci program, v ktorom sa pri klikaní cyklicky menila farba obdĺžnika, oprav tak, aby zmena farby fungovala len pre kliknutie do vnútra obdĺžnika.


  1. Napíš program, ktorý pri každom kliknutí do grafickej plochy vypíše na toto miesto poradové číslo kliknutia, teda postupne čísla 1, 2, 3, 4, …

    1. poradové číslo kliknutia bude v globálnej premennej a jej hodnotu budeš meniť pomocou global

    2. poradové číslo bude prvým prvkom nejakého zoznamu (globálna premenná), jej hodnotu môžeš meniť bez príkazu global


  1. Predstav si, že celá grafická plocha je štvorcová sieť, ktorej štvorčeky majú veľkosť 50x50. Napíš program, ktorý pri každom kliknutí do grafickej plochy nakreslí príslušný štvorček (pomocou create_rectangle()) tejto siete s náhodnou farbou. Nepoužívaj príkaz global.


  1. Najprv do stredu grafickej plochy vypíš nejaký zväčšený text. Každé kliknutie na ľubovoľné miesto plochy zmení farbu textu na náhodnú a tiež veľkosť tohto textu (náhodné číslo od 10 do 50).


  1. Najprv nakresli kruh so stredom (x0, y0) a polomerom r (napr. pre r, x0, y0 = 120, 150, 130). Potom každé kliknutie do vnútra kruhu zmení farbu výplne na odtieň šedej - čím bližšie do stredu tým tmavšie (v strede kruhu čierne), ku okraju svetlejšie (na obvode biele). Zrejme pri kliknutí vypočítaš vzdialenosť od stredu kruhu a toto číslo potom prepočítaš na celé číslo od 0 do 255 (napr. int(255 * vzd / r)). Z tohto čísla vyrobíš šedý odtieň pre farbu kruhu. Nepoužívaj príkaz global.


  1. Napíš program, ktorý pri každom kliknutí do grafickej plochy nakreslí 20 červených bodiek (červené kruhy s polomerom 2) na náhodných pozíciách. Pre všetky tieto bodky sú x-ové aj y-ové súradnice v náhodnej vzdialenosti od x a y kliknutého bodu z intervalu (-20, 20) (napr. x + randint(-20, 20)). Takto by mal vzniknúť efekt spreja.


  1. Doplň predchádzajúci príklad tak, že pri kliknutí sa ešte nič nekreslí len sa zvolí nejaká náhodná farba (v globálnej premennej) a až ťahanie myšou bude kresliť sprejový efekt zvolenou farbou.


  1. Gumená úsečka: kliknutie naštartuje vytváranie úsečky (najprv prvý aj druhý bod úsečky je samotný kliknutý bod, t. j. jej veľkosť je 0), ťahanie aktualizuje jej druhý bod (použi metódu canvas.coords())

    • každé ďalšie kliknutie a ťahanie vytvára ďalšiu úsečku


  1. Gumený obdĺžnik: kliknutie naštartuje vytváranie obdĺžnika (jeden vrchol je kliknutý bod a veľkosť je zatiaľ 0x0), ťahanie aktualizuje jeho veľkosť, t.j. protiľahlý vrchol

    • kliknutie nastaví náhodnú farbu výplne tohto obdĺžnika

    • každé ďalšie kliknutie a ťahanie vytvára ďalší obdĺžnik

    • otestuj tento program s „gumenou elipsou“:- namiesto create_rectangle() daj create_oval()


  1. V ploche sa nachádza jeden červený štvorček veľkosti 50x50. Keď klikneme do jeho vnútra (počíta sa aj obvod), môžeme ho ťahať, teda posúvať po ploche pomocou pohybov myši (inak ťahanie s kliknutím mimo štvorček nerobí nič).

    • daj pozor, aby aj malé posunutie myši počas ťahania neurobilo jeho neúmerne veľký skok (môžeme ho chytiť a ťahať napr. aj za ľubovoľný vrchol)

    • pri kliknutí do vnútra štvorčeka si môžeš niekde zapamätať posunutie kliknutého bodu od ľavého horného rohu štvorčeka a toto posunutie využiješ pri prekreslení štvorčeka na nových pozíciách (pomocou canvas.coords() alebo canvas.move())


  1. Predchádzajúci príklad uprav tak, aby fungoval aj pre 2 rôzne veľké štvorce: jeden červený veľkosti 50x50, druhý modrý veľkosti 100x100

    • vedel by si tento program upraviť tak, aby fungoval pre ľubovoľný počet štvorcov v ploche?


  1. Najprv nakresli niekde v strede plochy autíčko, ktoré je zložené z dvoch farebných obdĺžnikov a dvoch kruhov. Napíš program, ktorý bude toto autíčko posúvať po ploche pomocou šípok na klávesnici.


  1. Stláčaním malých a veľkých písmen abecedy sa tieto vypisujú nejakým väčším fontom vedľa seba. Využi jeden grafický objekt pre text (create_text) a tomuto budeš pri stláčaní písmen pridávať vypisovaný text (pomocou canvas.itemconfig()). Program by mohol akceptovať aj stláčanie medzery a Enteru (do textu vloží '\n').

    • použi metódu bind_all('<Key>', ...) pričom vo viazanej funkcii pracuj s hodnotou event.char


  1. Do grafickej plochy nakresli kružnicu (napr. pre r, x0, y0 = 100, 150, 120). Potom naprogramuj časovač, ktorý bude rovnomerne posúvať červenú bodku (napr. kruh s polomerom 5) na obvode tejto kružnice (napr. po každom tiknutí časovača posunie jeho pozíciu o uhol na kružnici o 10 stupňov). Posúvanie kruhu budeš robiť pomocou canvas.coords()


  1. Do predchádzajúceho príkladu pridaj ešte jeden pohybujúci sa modrý kruh, ktorého uhol posunu bude iný, napr. 15 stupňov.

    • experimentuj s rôznymi rýchlosťami pohybu oboch kruhov po kružnici (prípadne má jeden z nich opačný smer)

    • ak by sa farebné kruhy pohybovali po rôznych kružniciach, mohli by sme simulovať pohyb planét okolo slnka


  1. Nechaj bežať na obrazovke veľké digitálky: čas je zobrazený v tvare '9:22:34.5' a mení sa každú 0.1 sekundu

    • použi jeden textový objekt (create_text()), ktorému pomocou itemconfig() meníš zobrazovanú hodnotu


  1. Naprogramuj takúto hru na postreh:

    • každých interval milisekúnd sa farebný kruh s polomerom r presunie na náhodnú pozíciu v ploche

    • keď klikneme do plochy a trafíme do vnútra kruhu, ku nášmu skóre sa pripočíta 10

    • keď klikneme do plochy, ale netrafíme do kruhu, skóre sa zníži o 1

    • aktuálne skóre sa vypisuje niekde v rohu obrazovky (ako grafický objekt create_text())

    • interval a r sú nejaké globálne premenné, napr. s hodnotami 1000 a 20


  1. Na mieste kliknutia myšou sa nakreslí kruh s polomerom 50 a s náhodnou farbou výplne, ďalších 50 krokov sa jej polomer zmenší o 1 s časovým intervalom 0.1 sekundy, keď bude polomer 0, časovač túto kružnicu zruší

    • zabezpeč, aby aj viac kliknutí tesne za sebou na rôzne miesta plochy paralelne vytvorilo viac kruhov a aby sa všetky postupne zmenšovali o 1 (každé kliknutie vytvorí nový časovač s vlastnou kružnicou - treba dať pozor na lokálne a globálne premenné)