Tkinter — notatki

Autor: Wojciech Muła
Dodany:1.12.2006
Aktualizacja:6.12.2006

Treść

Znaczenie event.state

Dla zdarzeń ButtonPress, ButtonRelease, Enter, KeyPress, KeyRelease, Leave, Motion pole state struktury Event jest liczbą. Kolejne bity oznaczają:

Ma to bardzo miłą konsekwencję, ponieważ nie trzeba osobno tworzyć procedur obsługi zdarzeń np. dla różnych modyfikatorów, ale w jednej po prostu sprawdzić stan niektórych bitów tego pola.

Funkcje TCL-owe

Łączenie kodu Pythonowego z TCL-owym polega w przypadku Tkintera na tworzeniu specjalnych funkcji TCL-owych. Tego rodzaju funkcje powstają gdy przy tworzeniu kontrolki podana zostanie opcja command jak również przy podpinaniu funkcji metodami bind, tag_bind oraz protocol (wm_protocol). Co więcej, wszystkie Tkinterowe funkcje zwracają właśnie nazwy TCL-owe, a nie referencje to pythonowych obiektów.

Czy to problem? Wyobraźmy sobie, że chcemy dodać, a nie nadpisać, nową funkcję do kontrolki (opcja command) albo protokołu (metoda protocol). Należałoby odczytać uprzednio przypisaną funkcję, a w nowej ją wywoływać... jakoś. Wystarczy użyć metody widget.tk.call i jako argument podać nazwę funkcji, np. metoda invoke (dostępna w kilku kontrolkach) może zostać zrealizowana następująco:

button.tk.call( button['command'] )

A takie bardziej praktyczne zastosowanie to dołączenie się do łańcucha funkcji obsługujących zdarzenia wysyłane przez menadżery okien:

from Tkinter import *

def foo():
        print "WM_DELETE_WINDOW"
        master.tk.call(prot)

master  = Tk()
prot    = master.protocol('WM_DELETE_WINDOW')
print prot # --> '-1210926108destroy' or similar
master.protocol('WM_DELETE_WINDOW', foo)

master.mainloop()

Obiekt Canvas

tagOrId, indeks najbliższego wierzchołka, kształt strzałek

W zasadzie nigdy nie spotkałem się z tą informacją w dokumentacji do Tkintera, ale może coś przeoczyłem.

Argument większości funkcji, tagOrId, wcale nie musi być pojedynczym tagiem — można łączyć wiele tagów i identyfikatorów za pomocą wyrażeń logicznych: && (and), || (or), ! (not) oraz ^ (xor), oraz podwyrażeń w nawiasach. Na przykład:

canvas.delete('red && !squere')

Usunie wszystkie obiekty z tagiem 'red' nie zawierające jednocześnie taga 'square'.

Prosty przykład

Indeks najbliższego wierzchołka

Tk umożliwia uzyskanie odpowiedzi, czy kursor znajduje się nad jakimś obiektem (ze statusem normal) — obiekt taki dostaje automatycznie tag 'current' (w Tkinterze stała CURRENT), wówczas wystarczy użyć metody find_withtag.

Ale dla obiektów takich jak wielokąty i łamane może istnieć potrzeba dowiedzenie się, który wierzchołek leży najbliżej kursora. (Wiem, taka funkcja jest trywialna, ale po co dublować funkcjonalność?).

Umożliwia to metoda index — można ją stosować dla wielokątów i łamanych, tj. obiektów tworzonych metodami create_polygon i create_line. Jeśli zostaną podane współrzędne punktu jako "@x,y" wówczas zostanie zwrócony indeks najbliższego wierzchołka:

index = canvas.index(tagOrId, "@%f,%f" % (x, y))

Uwaga, jest to indeks w liście Tk, która jest „spłaszczona” — jeśli pamiętacie wierzchołki jako pary, trzeba wynik podzielić przez 2.

Prosty przykład

Kształt strzałek

Kształt strzałki jest podawany w argumencie arrowshape i jest definiowany przez trzy liczby:

canvas.create_line(arrow=BOTH, arrowshape(d1, d2, h))
img/tkinter-arrows.png

Grot strzałki jest czworokątem — jeśli przyjąć, że odcinek jest poziomy i kończy się w punkcie (0,0), to kolejne wierzchołki mają następujące współrzędne:

  • (0, 0)
  • ( − d2, h)
  • ( − d1, 0)
  • ( − d2, − h)

Prosty przykład

Edycja łamanych i wielokątów

W przypadku łamanych i wielokątów istnieje możliwość edycji pojedynczych wierzchołków istniejącego obiektu. I w niektórych przypadkach może być to wygodniejsze; chociaż ja przeważnie podmieniam hurtem wszystkie wierzchołki metodą canvas.coords.

Uwaga co do indeksów: w Tkinterze te obiekty są reprezentowane jako lista „spłaszczona”, tj. [x0, y0, x1, y1, ...], a nie [(x0,y0), (x1,y1)]. Czyli jeśli chcemy zmienić i-ty element, należy podawać argument 2*i.

Jeśli podany zostanie niewłaściwy indeks (nieparzysty, spoza zakresu) Tkinter poradzi sobie; indeksy spoza zakresu zostaną odpowiednio przycięte, a nieparzyste zmniejszone o 1.

Usuwanie wierzchołków

Służy do tego metoda dchars. Jeśli podany zostanie tylko jeden argument, wówczas usuwany jest pojedynczy wierzchołek. Jeśli podane zostaną dwa argumenty, wówczas zostaną usunięte wierzchołki z zakresu przez nie określonego.

line = c.create_line(...)
c.dchars(line, index)
c.dchars(line, index1, index2)
c.dchars(line, index,  'end')

Wartość 'end' jest wartością specjalna, odnosi się do zakońcowego indeksu (nie do ostatniego, ale ostatniego plus 1).

Wstawianie wierzchołków

Służy do tego metoda insert. Nowy wierzchołek wstawiany jest przed podany indeks. Jeśli indeks ma wartość 'end' wówczas wierzchołek zostanie doklejony na koniec.

line = c.create_line(...)
c.insert(line, index, (x, y))
c.insert(line, 'end', (x, y))

Zmiana współrzędnych wierzchołka

line = c.create_line(...)
c.insert(line, index, (x, y))
c.dchars(line, index+2)
Indeks najbliższego wierzchołka

Pisałem już o tym kilka dni temu, więc tylko przypomnę:

line  = c.create_line(...)
index = c.index(line, "@%f,%f" % (x, y))
  • demo:
    • LBM — wstawienie nowego wierzchołka
    • Ctrl-LBM — dodanie wierzchołka na koniec
    • Shift-LBM — zmiana współrzędnych wierzchołka
    • RBM — usunięcie wierzchołka
    • Ctrl-RBM — usunięcie od bieżącego wierzchołka do końca

Metody tag_raise_top, tag_lower_bottom

Ku pamięci:

def tag_raise_top(canvas, tagOrId):
        canvas.tag_raise(tagOrId, 'all')

def tag_lower_bottom(canvas, tagOrId):
        canvas.tag_lower(tagOrId, 'all')
  • demo:

    • LBM na jasnoszarym kwadracie — tag_raise_top
    • RBM na jasnoszarym kwadracie — tag_lower_bottom

Rzeczywisty rozmiar, odwzorowane okno

Rzeczywisty rozmiar okna, nie tylko canvas, ale każdego innego trzeba odczytywać za pomocą metod winfo_width oraz winfo_height; można również użyć winfo_geometry, ale przeważnie potrzebujemy znać tylko wysokość i szerokość kontrolki, a poza tym akurat ta metoda zwraca łańcuch znaków, więc to niespecjalnie wygodne.

Jednak w przypadku canvas wysokość i szerokość nie mówią jeszcze o rzeczywistym widocznym obszarze. Należy odjąć jeszcze szerokość obramowania (borderwidth) oraz szerokość ramki wskazującej, czy kontrolka ma focus, czy nie (highlightthickness):

def canvas_dimensions(canvas):
        w = canvas.winfo_width()
        h = canvas.winfo_height()

        l = int(canvas['highlightthickness'])
        b = int(canvas['borderwidth'])

        return (max(w-(l+b), 0.0),
                max(h-(l+b), 0.0))

Funkcja która zwróci współrzędne wyświetlanego prostokąta jest bardzo podobna:

def canvas_viewport(canvas):
        w = canvas.winfo_width()
        h = canvas.winfo_height()

        l = int(canvas['highlightthickness'])
        b = int(canvas['borderwidth'])

        x1 = canvas.canvasx(l+b)
        y1 = canvas.canvasy(l+b)
        x2 = canvas.canvasx(w - (l+b+1))
        y2 = canvas.canvasy(h - (l+b+1))

        return x1, y1, x2, y2

Widok — scan_mark, scan_dragto, see

Metody scan_mark i scan_dragto są używane wspólnie i służą do zmiany widocznego obszaru (widoku, okna) canvas.

Jak to działa: za pomocą scan_mark zaznaczany jest jakiś punkt Pm, natomiast metodą scan_dragto wybierany jest drugi punkt Pd i całe okno przesuwa się o różnicę: (Pd − Pm) ⋅ a. Czynnik a to przyspieszenie; radzę nie przesadzać z jego wartością.

Na początek dwie uwagi:

  1. Jeśli jest ustawiony scrollregion, wówczas możliwości przesuwania okna są ograniczone właśnie do tego prostokąta.
  2. Obie metody wymagają współrzędnych całkowitych.

Główne przeznaczenie obu metod to realizacja interaktywnego przesuwania zawartości płótna. Na ogół scan_mark woła się raz po kliknięciu myszką, a scan_dragto w funkcji obsługującej zdarzenie z rodzaju <Motion>.

Ale funkcje te mogą być użyte do realizacji funkcji see — funkcji, która tak przesuwa widok, aby wskazany obiekt stał się widoczny. Napisałem to w taki sposób, żeby obiekt znalazł się dokładnie na środku okna:

def see(canvas, item):
        x1, y1, x2, y2 = canvas.bbox(item)
        cx = (x1+x2)/2
        cy = (y1+y2)/2

        xo = canvas.canvasx(0)
        yo = canvas.canvasy(0)
        w  = canvas.winfo_width()
        h  = canvas.winfo_height()

        canvas.scan_mark(int(cx), int(cy))
        canvas.scan_dragto(int(xo + w/2), int(yo + h/2), 1)

Na początku wyznaczany jest środek obiektu (na podstawie pudełka otaczającego), potem środek okna, a na końcu płótno jest przesuwane, tak by oba punkty się pokryły.

  • demo:

    • LBM + ciągnięcie — przesuwanie zawartości płótna
    • LBM + (Ctrl+ciągnięcie) — j.w., większa wartość przyspieszenia
    • LBM na kwadracie — przesunięcie widoku tak, by wybrany kwadrat znalazł się na środku okna (zastosowanie funkcji see)

Stan obiektów, optyczne sprzężenie zwrotne

Obiekty umieszczone na canvas mogą znajdować się w czterech stanach:

  1. normal — normalnym (generują zdarzenia, mogą stać się aktywne);
  2. disabled — wyłączone (nie generują zdarzeń, nie są aktywowane);
  3. active — aktywne — najwyżej położony obiekt będący w stanie normal nad którym znajduje się kursor myszy; obiekt automatycznie dostaje tag current (w Tkinterze stała CURRENT);
  4. hidden — niewidoczne;

Stany normal, disabled, hidden mogą być ustawiana przez użytkownika poprzez przypisanie tych łańcuchów znaków do opcji state (uwaga: pusty łańcuch oznacza również stan normal). Natomiast o przypisanie stanu active dba Tk i jest to niezależne od użytkownika

Optyczne sprzężenie zwrotne — Tkinter umożliwia bardzo łatwe wyróżnianie obiektu w zależności od jego stanu, wyręczając użytkownika od wielu zbędnych czynności.

Wyobraźmy sobie, że chcemy, aby aktywny obiekt zmieniał swój kolor, albo grubość linii. Wydawałoby się, że tym celu należałoby coś kombinować ze zdarzeniami (może <Enter>/<Leave>, prędzej <Motion>). Nic z tych rzeczy! Dla opcji: fill, outline, dash, width, stipple, image istnieją odpowiedniki activefill, activeoutline (itd.) oraz disabledfill, disabledoutline (itd.).

Jeśli obiekt jest w stanie normal to używane są zwykłe opcje, jeśli zostanie aktywny, to automatycznie użyte zostaną (o ile są ustawione) opcje active..., a jeśli użytkownik „wyłączy” obiekt, użyte zostaną opcje disabled....

Błąd w metodzie itemconfigure dla obiektów tekstowych

Na początek fragment kodu, dla zilustrowania problemu:

text  = "sample text with spaces"
id    = canvas.create_text(text=text)
text2 = cavnas.itemconfigure(id, 'text')
print text2

Na konsoli nieoczekiwanie pojawi się:

('sample', 'text', 'with', 'spaces')

Metoda nie zwraca przypisanego łańcucha, ale krotkę — wynik działania: tuple(string.split())! Problem pojawia się po stronie Tkintera.

Rozwiązanie jest dosyć proste, ale wymaga ominięcia Tkintera i odwołania się bezpośrednio do interpretera Tcl-a:

from Tkinter import TclError

def canvas_get_text(canvas, text_id):
        tk = canvas.tk
        try:
                result = tk.call(str(canvas), 'itemconfigure', text_id, '-text')
                return tk.splitlist(result)[-1]
        except TclError:
                return ”

Wyznaczanie pudełka otaczającego wygładzonych obiektów

Łamane i wielokąty mogą być „wygładzone” — należy ustawić opcję smooth na 1 lub bezier. (Można również definiować własne funkcje wygładzające, ale jest to tylko możliwe na poziomie języka C).

Metoda canvas.bbox zwraca pudełko otaczające wybranego obiektu, czy grupy obiektów; dokumentacja stwierdza, że The return value may overestimate the actual bounding box by a few pixels, czyli nie tak źle. Jednak dla obiektów wygładzonych zwracane jest pudełko punktów zwyczajnego, niewygładzonego obiektu, a nie krzywej, którą można podziwiać na ekranie.

Pudełko otaczające krzywej można jednak dość łatwo wyznaczyć.

Wbudowana metoda wygładzania opiera się, jak można się domyśleć po wartości bezier, na krzywych Beziera, a gdy spojrzeć w dokumentację lub źródła okazuje się, że chodzi o wielomianowe krzywe B-sklejane drugiego stopnia z równomiernym rozkładem węzłów wewnętrznych. A mówiąc po ludzku jest to ciąg krzywych Beziera danych za pomocą trzech punktów kontrolnych.

Aby otrzymać pudełko otaczające taką krzywą wystarczy wyznaczyć pudełka otaczające każdej z krzywych Beziera i na końcu je wszystkie ze sobą połączyć.

Pudełko otaczające pojedynczej krzywej Beziera 2. stopnia

Oznaczmy punkty kontrolne krzywej przez Pa, Pb, Pc.

Pudełko otaczające zależy od punktów:

  • Pa (czyli od xa i ya)
  • Pb
  • punktów P(t), gdzie t∈(0, 1) jest parametrem dla którego jeden z wielomianów x(t) lub y(t) ma ekstremum.

Wielomiany te są postaci f(t) = at2 + bt + c i wystarczy sprawdzić czy pochodna f'(t) = 0, czyli czy b/(2a) = 0.

Współczynniki wielomianu otrzymamy z następujących wzorów:

  • a = Pa − 2 ⋅ Pb + Pc
  • b = − 2 ⋅ Pa + 2 ⋅ Pb
  • c = Pa

Przedstawiona niżej funkcja wyznacza ekstrema osobno dla osi X i Y i zwraca listy wartości.

def qbezier_bounds((x0, y0), (x1, y1), (x2, y2)):
    """
    Returns extents of cubic bezier curve given by three points.
    """

    # cubic Bezier reprsented in polynomial base
    # f(t) = A*t^2 + B*t + C
    Ax =    x0 - 2*x1 + x2
    Bx = -2*x0 + 2*x1
    Cx =    x0

    Ay =    y0 - 2*y1 + y2
    By = -2*y0 + 2*y1
    Cy =    y0

    # find extremas:
    #   1) x(0) = x0
    #   2) x(1) = x2
    #   3) f(t_e), where f'(t_e)=0 and t_e in (0,1)
    x = [x0,x2]
    if abs(Ax) > 1e-10:
        t  = -Bx/(2*Ax)
        if 0.0 < t < 1.0:
            t2 = t*t
            x.append(Ax*t2 + Bx*t + Cx)

    y = [y0,y2]
    if abs(Ay) > 1e-10:
        t  = -By/(2*Ay)
        if 0.0 < t < 1.0:
            t2 = t*t
            y.append(Ay*t2 + By*t + Cy)

    return x, y

Punkty definiujące poszczególne krzywe

Niech obiekty będą określone przez ciąg punktów p0, p1, …, pn − 1. Punkty kontrolne krzywych są wówczas dane jako:

  • Pa = 0.5 ⋅ pi − 1 + 0.5 ⋅ pi
  • Pb = pi
  • Pc = 0.5 ⋅ pi + 0.5 ⋅ pi + 1

W przypadku łamanej krzywych jest n − 2 i są określone dla i = 2..n − 2. Dodatkowo dla i = 1 punkt Pa = p0, a dla i = n − 1 punkt Pc = pn. Jednak przedstawiony niżej generator tak modyfikuje pierwszy i ostatni punkt, żeby można było stosować powyższe wzory dla i = 1…n − 2.

def pt(points):
    x0, y0 = points[0]
    x1, y1 = points[1]
    p0     = (2*x0-x1, 2*y0-y1)

    x0, y0 = points[-1]
    x1, y1 = points[-2]
    pn     = (2*x0-x1, 2*y0-y1)

    p = [p0] + points[1:-1] + [pn]

    for i in xrange(1, len(points)-1):
        a = p[i-1]
        b = p[i]
        c = p[i+1]

        yield lerp(a, b, 0.5), b, lerp(b, c, 0.5)

W przypadku wielokątów jest n krzywych, indeksy we wzorach są brane modulo n, natomiast i = 0…n − 1.

def pt(points):
    p = points
    n = len(points)
    for i in xrange(n):
        a = p[(i-1) % n]
        b = p[i]
        c = p[(i+1) % n]

        yield lerp(a, b, 0.5), b, lerp(b, c, 0.5)

Implementacja

Uwaga: funkcje exact_line_bbox oraz exact_polygon_bbox zwracają pudełka otaczające naprawdę „dokładne”, tzn. zakłada się, że krzywe mają zerową szerokość. Można później te pudełka rozszerzyć o 1/2 aktualnej szerokości linii — w przypadku połączeń linii (line join) typu ROUND i takiego samego sposobu zakończania linii (line cap) będzie to wciąż dokładne. W przypadku innego sposobu łączenia/zakończania linii już nie.

screen

Zadania interakcyjne

Problem

Większość zadań interakcyjnych ma charakter typowo sekwencyjny, natomiast zdarzenia w Tkinterze są asynchroniczne — nie można przewidzieć kiedy i w jakiej kolejności nastąpią.

Weźmy prosty schemat rysowania prostokąta:

  1. Oczekiwanie aż użytkownik naciśnie lewy klawisz myszy i wskaże punkt — pierwszy narożnik prostokąta.
  2. Weź aktualną pozycję kursora jako drugi narożnik i dopóki użytkownik porusza myszką uaktualniaj obraz prostokąta. Dopiero gdy użytkownik ponownie naciśnie lewy klawisz myszy zapamiętaj bieżącą pozycję kursora jako drugi narożnik i zakończ procedurę.
  3. Jeśli użytkownik na etapie 1. lub 2. naciśnie klawisz ESC przerwij całą procedurę.

Oto szkic naiwnego rozwiązania:

class Dummy:
    def __init__(self, master):
        self.state  = 'wait'
        self.canvas = Canvas(root, master)
        self.canvas.bind('<Button-1>', self.click)
        self.canvas.bind('<Motion>',   self.motion)
        self.canvas.bind('<Escape>',    self.ESC)
    # [...]
    def draw_rect(self):
        # init drawing
        if self.state == 'active':
            self.cancel_draw()
        self.state = 'wait'

    def click(self, event):
        if self.state == 'wait':
            self.x1 = event.x
            self.y1 = event.y
            self.item = self.canvas.create_rectangle(
                self.x1, self.y1, self.x1, self.y1
            )
        else:
            self.canvas.itemconfig(
                self.item, self.x1, self.y1, event.x, event.y
            )
            self.state == 'wait'

    def motion(self, event):
        if self.state == 'active':
            self.canvas.itemconfig(
                self.item, self.x1, self.y1, event.x, event.y
            )

    def ESC(self):
        if self.state == 'active':
            self.cancel_draw()

    def cancel_draw(self):
        if self.state == 'active':
            self.canvas.delete(self.item)
            self.state = 'wait'

W porównaniu z przedstawionym na wstępie schematem postępowania, przedstawiony program jest kompletnie nieczytelny i na dobrą sprawę nie za bardzo wiadomo co się dzieje.

Więc ten sposób obsługi zadań interakcyjny wydaje się zupełnie chybiony. Bo teraz wyobraźmy sobie, że mamy zadanie, na które nie składają się ledwie dwa, czy trzy kroki, lecz dużo więcej. Oraz że mamy wiele różnych zadań do obsłużenia. Wówczas należałoby stworzyć dla każdego zadania osobny zestaw procedur obsługi zdarzeń i dynamicznie je podpinać w zależności od wybranego zadania, albo używać jednego zestawu, ale wówczas te funkcje rozdęłyby się do jakiś monstrualnych rozmiarów i de facto zadanie byłoby rozbite na kilka mniejszych (próbkę tego dałem wyżej), formalnie niepołączonych ze sobą funkcji.

A więc co chcemy osiągnąć:

  • Pojedyncze zadanie interakcyjne powinno mieścić się w jednej funkcji (lub też jednym bloku kodu).
  • Zaprogramowanie nowego zadania nie powinno pociągać za sobą tworzenia osobnych funkcji obsługi zdarzeń, ani modyfikowania już istniejących.
  • Musi istnieć mechanizm pozwalający na oczekiwanie na jakieś zdarzenie (zdarzenia). A więc na pewnym etapie wykonywania funkcji inne, nieistotne zdarzenia muszą być jakoś blokowane, ignorowane.

Zaczniemy od końca, bo to jak się okaże rozwiązanie tego problemu spowoduje automatycznie rozwiązanie dwóch wcześniejszych.

Oczekiwanie na zdarzenia

Na szczęście w Tkinterze dostępne są zmienne StringVar, BooleanVar, IntVar oraz DoubleVar, dla których istnieje mechanizm oczekiwania — wywołanie metody widget.wait_variable(var) powoduje wejście Tkintera w lokalną pętlę (bez blokowania głównej pętli) i przerwanie jej dopiero wtedy, gdy zmienna var zostanie zmieniona.

Ponieważ główna pętla działa bez przeszkód, zatem zdarzenia są normalnie obsługiwane i w procedurach ich obsługi można ustawiać odpowiednie zmienne, informując tym samym główny program o wystąpieniu zdarzenia.

Filtrowanie zdarzeń

Mamy więc mechanizm który umożliwia oczekiwanie na zdarzenia. W porządku, program czeka na zdarzenia, bo tyle ma akurat do roboty, ale gdy wreszcie się doczeka i zacznie wykonywać jakieś czasochłonne działania, co wtedy ze zdarzeniami, które w tym czasie nastąpią? Procedury obsługi zdarzeń mogą sobie zmienną ustawiać do woli, główny program nigdy tego nie zauważy.

Dlatego ze względów praktycznych lepiej jest stworzyć osobną kolejkę zdarzeń, która będzie wypełniana przez procedury obsługi zdarzeń.

Dzięki temu nawet jeśli program główny jest zajęty, to gdy będzie chciał sprawdzić czy były jakieś zdarzenia po prostu sięgnie do kolejki; a jeśli nie było, to po prostu poczeka.

Dodatkowym plusem tego rozwiązania jest to, że dodając nowe zadanie interakcyjne nie trzeba dodawać nowych funkcji obsługi zdarzeń. One cały czas robią to, co miały robić — dopisują dane do kolejki, a to ile funkcji czyta kolejkę i jak ją wykorzystuje nie ma znaczenia.

Program główny

Pisząc „program główny” mam na myśli aktualnie wykonywaną procedurę obsługującą pewne zadanie interakcyjne. Takich procedur może istnieć w programie wiele, ale ja rozwiązałem to w ten sposób, że jest rzeczywiście tylko jedna procedura główna, która wywołuje zadaną funkcję.

Program główny można uruchomić w osobnym wątku i wówczas zamiast tkinterowych zmiennych można wykorzystać muteksy, albo lepiej dostępny od Pythona 2.4 moduł Queue. Tkinter jest jednowątkowy i czytałem, że mogą występować problemy z jego wykorzystaniem w programach wielowątkowych. Jednak tutaj jest tylko jeden wątek, przetwarzane jest jedno zadanie interakcyjne — podczas testów nie zanotowałem problemów.

Ale program w ogóle nie musi korzystać z wątków — można zlecić Tkinterowi, by odpalał funkcje, gdy już nie będzie miał nic do roboty; służy do tego metoda after_idle. To w wielu przypadkach z pewnością wystarczy.

Implementacja — Tkinter Events Serializer

Te wszystkie rzeczy oprogramowałem w przedstawionym niżej module

(Po uruchomieniu python tkes.py można pobawić się prościutkim programem rysunkowym).

Problem, który przedstawiłem na początku da się teraz rozwiązać w ten sposób:

class Dummy:
    def __init__(self, master):
        self.canvas = Canvas(root, master)
        self.es.EventsSerializer('<Escape>',
            {self.canvas: ['<Button-1>', '<Motion>', '<Escape>']})

        def draw_rect(self):
            try:
                event  = self.es.wait_event('<Button-1>')
                x1, y1 = event.x, event.y
                rect = self.canvas.create_rectangle(x1, y1, x1, y1)

                for _, event in self.es.report_events(['<Motion>'], ['<Button-1>']):
                    x2, y2 = event.x, event.y
                    self.canvas.coords(rect, x1, y1, x2, y2)
            except FunctionInterrupted:
                try:
                    self.canvas.delete(rect)
                except UnboundLocalError:
                    pass

Skrótowy opis modułu. Każde zdarzenie ma nazwę, może to być nazwa Tkintera albo własna (niekoniecznie łańcuch znaków). Jest jedno wyróżnione zdarzenie, którego pojawienie się powoduje podniesienie wyjątku FunctionInterrupted; nie można tego zdarzenia zablokować.

Najważniejsze metody:

  • wait_events — oczekuje na kilka zdarzeń, może także zgłaszać dowolne wyjątki po wystąpieniu określonych zdarzeń; inne zdarzenia są ignorowane
  • report_events — generator, który działa dopóki pojawiają się zdarzenia z określonego zbioru; może także zgłaszać dowolne wyjątki po wystąpieniu określonych zdarzeń; działanie generatora kończy się, gdy pojawi się zdarzenie z określonego zbioru;
  • set_function — ustala funkcję, którą ma wykonywać program główny (jeśli działa jakaś inna, jest przerywana); funkcja jest wykonywana pętli, do czasu aż nie zostanie zmieniona przez inną;
  • unset_function — przerywa funkcję i ta funkcja już nie jest więcej wykonywana;
  • interrupt/reset — kończy działanie ustawionej funkcji, ale przy następnym przebiegu pętli funkcja jest wykonywana ponownie