tkinter - Events

Events

Events sind etwa Mausklicks, oder Tastatureingaben. Diese werden mit Methoden an beispielsweise Widgets gebunden. Diese Bindung enthält den Empfänger des Events (z. B. ein Widget), das Ereignis (z. B. Mausklick) und eine Callback.

Ereignisse werden grundsätzlich von einem Widget zum Nächsten weitergereicht, bis zu einem Empfänger. Der Abschnitt Bindtags erläutert das genauer.

Empfänger von Event-Bindungen

Events werden mit Methoden an unterschiedliche Bereiche eines Programmes gebunden:

ZielErläuterung
App-BindungDer gesamten App wird ein Ereignis mit root.bind_all(…) zugeordnet
Class-BindungAllen Widgets, die eine gemeinsame class_-Option benutzen, kann mit bind_class(…) eine gemeinsame Menge an Events zugeordnet werden
Tag-BindungBeispiel Tags von Canvas- oder Treeviewitems: Werden mit Methoden wie w.tag_bind(…) erzeugt und beziehen sich alleine auf Tags. Siehe auch das Beispiel Tag Click im Treeview-Kapitel
Widget-BindungEinzelnen Widgets kann per w.bind(…) ein Ereignis zugeordnet werden

Das folgende Programm zeigt die obigen Bindungen. Das Label la wird als Ziel an das Ereignis '<Button-1>' (linker Mausknopf) gebunden, die Klasse foobar erhält eine Klassenbindung an den linken Mausknopf und die App wird an die Tastenkombination STRG-q gebunden.

Bild der Anwendung
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog as fd

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry('400x400')
        self._createWidgets()

    def _createWidgets(self):
        # Ereignis wird an genau das Label gebunden
        la = ttk.Label(self, text='Klick mich', relief=tk.SUNKEN )
        la.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES)
        la.bind('<Button-1>', self._label1Click)

        lf = ttk.Labelframe(self, text='Class Labels')
        lf.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES)

        # Ereignis wird an die Klasse (Gruppe) gebunden
        lb = ttk.Label(lf, class_='foobar', text='klickbar1', relief=tk.SUNKEN)
        lb.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES)
        lc = ttk.Label(lf, class_='foobar', text='klickbar2', relief=tk.SUNKEN)
        lc.pack(side=tk.RIGHT, fill=tk.BOTH, expand=tk.YES)
        lb.bind_class('foobar', '<Button-1>', self._label2Click)

        # Anzeige, wohin geklickt wurde
        self.labelInfoVar = tk.StringVar(value='Wer klickt was?')
        ld = ttk.Label(self, textvariable=self.labelInfoVar, relief=tk.SUNKEN)
        ld.pack(side=tk.TOP, fill=tk.X)

        b = ttk.Button(self, text="Ende!", command=self.destroy)
        b.pack(side=tk.BOTTOM)

        # Ereignis wird an App gebunden
        self.bind_all('<Control-KeyPress-q>', self._goodbye)

    def _label1Click(self, event):
        self.labelInfoVar.set('Label A wurde geklickt')

    def _label2Click(self, event):
        self.labelInfoVar.set('Gruppenklick, B oder C')

    def _goodbye(self, event):
        print('Googbye...')
        self.destroy()

if __name__ == '__main__':
    window = A()
    window.mainloop()

Weiterverarbeitung von Events abbrechen

Im folgenden Beispiel wird die Eingabe von '1' in ein Entry an eine Callback gebunden. Diese Callback soll die Eingabe verarbeiten, die Eins soll aber nicht im Entry erscheinen. Die Weiterverarbeitung der Eingabe im Entry wird unterbunden mit return 'break':

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry('200x200')
        self._createWidgets()

    def _createWidgets(self):
        self.entry = ttk.Entry(self)
        self.entry.pack()
        self.entry.bind('<KeyPress-1>', self._onOne)

    def _onOne(self, event):
        self.entry.delete(0, 'end')
        self.entry.insert(0, '1 gedrückt')
        return 'break'

if __name__ == '__main__':
    window = A()
    window.mainloop()

Eventtypen

Mausereignisse

Mausereignisse entstehen, wenn die Maus bewegt wird, über bestimmte Bereiche fährt oder Mausknöpfe betätigt werden.

EreignisnameBeschreibung
'<Button>'Ein Mausknopf wurde gedrückt
'<Button-1>', '<1>'Der erste Mausknopf wurde gedrückt
'<ButtonRelease>'Ein Mausknopf wurde losgelassen
'<ButtonRelease-1>'Der erste (linke?) Mausknopf wurde losgelassen
'<Enter>'Die Maus wird in einen Bereich, ein Widget hinein bewegt
'<Leave>'Die Maus wird aus einen Bereich, ein Widget heraus bewegt
'<Motion>'Die Maus über einem Bereich, einem Widget bewegt
'<MouseWheel>'Das Mausrad wurde bewegt

Hinweis: Unter Linux wird '<MouseWheel>' nicht erkannt. Hier ist das Ereignis stattdessen '<Button-4>' für Mausrad rauf und '<Button-5>' für Mausrad runter.

Das folgende Programm stellt einige Mausevents vor. Es wird dabei auch die String-Repräsentation der event-Parameter ausgegeben:

Bild der Anwendung
import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry('400x400')
        self._createWidgets()

    def _createWidgets(self):
        label = ttk.Label(self, text='Ereignisfeld', relief=tk.SUNKEN)
        label.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES)
        label.bind('<Button-1>', self._onClick)
        label.bind('<Enter>', self._onEnterLeave)
        label.bind('<Leave>', self._onEnterLeave)

        self.labelInfoVar = tk.StringVar(value='Beschreibung')
        labelInfo = ttk.Label(self, textvariable=self.labelInfoVar)
        labelInfo.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES)

    def _onClick(self, event):
        msg = F'Click: {event.x} {event.y} \n{event}'
        self.labelInfoVar.set(msg)

    def _onEnterLeave(self, event):
        if event.type == tk.EventType.Enter:
            msg = F'Enter: {event}'
            self.labelInfoVar.set(msg)
        else:
            msg = F'Enter: {event}'
            self.labelInfoVar.set(msg)

if __name__ == '__main__':
    window = A()
    window.mainloop()

Tastaturereignisse

Tastaturereignisse entstehen bei Tastendrücken auf der Tastatur. Manche Tasten, wie etwa STRG, NumLock oder ALT dienen dabei als "Modifier".

EreignisnameBeschreibung
'<KeyPress>'Eine Taste wurde gedrückt
'x'Die Taste x wurde gedrückt
'<Control-KeyPress-x>'Die Taste x wurde zusammen mit der STRG-Taste gedrückt
'<KeyRelease>'Taste wurde losgelassen

Tastaturereignisse liefern event.keysym (ein symbolischer Name für die Taste), event.keycode (Zahlencode der reinen Taste), und event.keysym_num (Zahlencode unter Berücksichtigung eventuell gedrückter Modifier) zurück. Zudem noch mit event.char das eigentliche Zeichen.

Das folgende Programm wertet allgemein den event-Parameter aus, indem Felder des Parameters untersucht werden:

Bild der Anwendung
import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry('400x400')
        self._createWidgets()

    def _createWidgets(self):
        self.bind_all('<KeyPress>', self._onKeypress)

        self.labelInfoVar = tk.StringVar(value='Beschreibung')
        labelInfo = ttk.Label(self, textvariable=self.labelInfoVar)
        labelInfo.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES)

    def _onKeypress(self, event):
        self.labelInfoVar.set(F'KeyPress\nkeysym={event.keysym}\n'
                              F'keycode={event.keycode}\n'
                              F'keysym_num={event.keysym_num}\n'
                              F'char={event.char}\n'
                              F'Modifier={event.state}')

if __name__ == '__main__':
    window = A()
    window.mainloop()

Weitere Ereignisse

EreignisnameBeschreibung
'<Configure>'Widget verändert seine Größe
'<Destroy>'Widget wird endgültig entfernt, weil beispielsweise die App beendet wird
'<Expose>'Wird aufgerufen, wenn Teile des Widgets/Fensters neu gezeichnet werden müssen
'<FocusIn>', '<FocusOut>'Ein Widget bekommt oder verliert den (Eingabe-)Fokus
'<Map>', '<Unmap>'Widget wird sichtbar, weil es in ein Layout eingefügt wird oder unsichtbar, weil es aus diesem wieder entfernt wird
'<Visibility>'Fenster wird sichtbar, weil es minimiert war oder gerade erst erzeugt wurde, oder maximiert

Das folgende Programm stellt einige dieser Ereignisse vor:

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry('400x400')
        self._createWidgets()

    def _createWidgets(self):
        labelEvent = ttk.Label(self, text='Ereignis', relief=tk.SUNKEN)
        labelEvent.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES)
        labelEvent.bind('<Configure>', self._onResize)
        labelEvent.bind('<Expose>', self._onExpose)
        labelEvent.bind('<Visibility>', self._onVisibility)

        # Um dieses Ereignis auszulösen springt man mit der Tab-Taste in das entry
        # und wieder hinaus in das entry2
        entry = ttk.Entry(self)
        entry.pack(side=tk.TOP, fill=tk.X, expand=tk.YES)
        entry.bind('<FocusIn>', self._onFocus)
        entry.bind('<FocusOut>', self._onFocus)
        entry2 = ttk.Entry(self)
        entry2.pack(side=tk.TOP, fill=tk.X, expand=tk.YES)

    def _onVisibility(self, event):
        print('Visibility ')

    def _onResize(self, event):
        print('Resize: ', event.width, event.height)

    def _onExpose(self, event):
        print('Expose: ', event.width, event.height)

    def _onFocus(self, event):
        print('Focus: %s' % ('bekommen', 'verloren')[int(event.type != tk.EventType.FocusIn)] )

if __name__ == '__main__':
    window = A()
    window.mainloop()

Übersicht Eventfelder

Der event-Parameter der Callbacks kennt die folgenden Felder, die je nach Ereignis gesetzt sind:

FeldBeschreibung
.charDas eigentlich Zeichen bei KeyPress, KeyRelease
.deltaMouseWheel: Änderung des Mausrades, plattformabhängig
.height, .widthConfigure: Neue Höhe/Breite des Widgets
.keycode, .keysym, .keysym_numKeyPress, KeyRelease: Beschreibung der gedrückten Taste
.numDer gedrückte Mausknopf
.serialSeriennummer
.stateBeschreibung der gedrückten Modifier bei Maus- und Tastaturereignissen. Plattformabhängig
.timeWird jede Millisekunde um 1 erhöht
.typeDer Event-Typ, kann verglichen werden mit tk.EventType.eventName
.widgetDas Widget, welches das Event verursacht hat
.x, .yMausposition relativ zum Widget
.x_root, .y_rootMausposition relativ zum Fenster

Übersicht Modifier

Die folgenden Modifier können ergänzend zu Maus- und Tastaturereignissen gesetzt werden, beispielsweise w.bind('<Alt-Button-1>', self.onClick):

ModifierBeschreibung
AltAlt-Taste wird gedrückt
ControlStrg-Taste
DoubleDoppelklick
LockShiftLock-Taste wurde gedrückt
ShiftShift-Taste
TripleDreifachklick

Virtuelle Events

Virtuelle Events erweitern das Konzept von Events durch Namen für bestimmte Aktionen, die nicht unmittelbar aus den physischen Ereignissen, wie beispielsweise Mausevents, hervorgehen. Virtuelle Events kommen von Widgets, können aber auch an diese gesendet werden.

Beispiele für solche virtuellen Ereignisse sind <<Cut>>, <<Copy>> und <<Paste>> in Textwidgets.

Das folgende Programm stellt erzeugt ein neues Ereignis namens <<TestEvent>>. Dieses Ereignis wird einerseits ausgelöst, indem der linke Mausknopf gedrückt wird, andererseits durch Drücken der Taste 1(Eins).

import tkinter as tk
from tkinter import ttk
from tkinter import filedialog as fd

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry('400x400')
        self._createWidgets()

    def _createWidgets(self):
        # Binde Taste "1" an _onKeyPressOne
        self.bind('1', self._onKeyPressOne)
        # Erstelle <<TestEvent>> auf der Basis von Event:Linke Maustaste
        self.event_add('<<TestEvent>>', '<Button-1>')
        # Binde <<TestEvent>> an _onTestEvent
        self.bind('<<TestEvent>>', self._onTestEvent)

    def _onKeyPressOne(self, event):
        print('_onKeyPressOne')
        # <<TestEvent>> wird in die Ereigniswarteschlange geworfen
        self.event_generate('<<TestEvent>>')

    def _onTestEvent(self, event):
        # Informationen über events ausgeben:
        print('_onTestEvent: ', self.event_info('<<TestEvent>>'), self.event_info() )


if __name__ == '__main__':
    window = A()
    window.mainloop()

Event-Methoden

Übersicht Event-Methoden

MethodeParameterRückgabewertBeschreibung
w.bind(sequenz, callback, add)sequenz: Ereignisname, callback Funktion mit einem event-Parameter (optional), add kann '+' sein, dann werden Ereignisse angehängt, sonst ersetzt-Ruft eine Callback auf, wenn Ereignis eintritt. Kann vorhandene Ereignisse ersetzen oder ergänzen
w.bind_all(sequenz, callback, add)wie w.bind(…)-Ereignis wird für alle Widgets in der Anwendung verarbeitet
w.bind_class(className, sequenz, callback, add)className Klasse (Gruppe) von Widgets, auf die sich die Verarbeitung bezieht, sonst wie w.bind(…)-Ereignis wird für alle Widgets der Klasse (Gruppe) in der Anwendung verarbeitet
w.bindtags(bindtags)Siehe Abschnitt Bindtags
w.event_add(virtualEvent, *physischesEvent)virtualEvent: neues virtuelles Event, physischesEvent: Liste physischer Events oder 'None'-Erzeugt ein neues virtuelles Event auf der Basis physischer vorhandener Events
w.event_delete(virtualEvent, *physischesEvent)wie w.event_add(…)-Entfernt ein physisches Event vom virtuellen Event. Wurde das letze Event entfernt, kann das virtuelle Event nicht mehr ausgelöst werden
w.event_generate(event, **kw)event: ein beliebiges Event, kw eine Liste von Schlüsselwörtern, die das Event beschreiben-Erzeugt ein neues Event mit den angegebenen Parametern aus kw
w.event_info(virtual=None)virtual: Optionales virtuelles Event-Ohne Parameter werden alle virtuellen Events ausgegeben. Ansonsten werden die physischen Events zum angegebenen virtuellen Events ausgegeben
w.mainloop()--Messageloop, verarbeitet alle Nachrichten und wird mit w.quit() verlassen
w.quit()--Verlässt die Messageloop, beendet damit zumeist das Programm
w.unbind(sequenz, callback)wie w.bind(…)-Entfernt die Ereignisbearbeitung
w.unbind_all(sequenz, callback)wie w.bind_all(…)-Entfernt Ereignisbearbeitung für sequenz für alle Widgets
w.unbind_class(className, sequenz)wie w.bind_class(…)-Entfernt Ereignisbearbeitung für sequenz für alle Widgets der Gruppe className
w.update()--Displayupdate, sollte nicht aus Ereignisbearbeitung aufgerufen werden
w.update_idletasks()--Idletasks werden bearbeitet: Displayupdate, Resizing

Klassenbindung

Mit w.bind_class(…) können Klassen, also Widgets mit gemeinsamer class_-Option, an Ereignisse gebunden werden. Widgets haben grundsätzlich voreingestellte Klassennamen, die sich ändern lassen.

Das folgende Beispiel bindet die Tab-Taste an ttk.Entry-Widgets, so dass Tabulatoren eingefügt werden. Das Standardverhalten ist sonst, den Fokus von einem Widget zum Nächsten zu bewegen. Die hier vorgestellte Implementierung überschreibt vorhandene Textmarkierungen (Textauswahl) nicht, das ist anders als bei der Standardimplementierung.

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry('200x200')
        self._createWidgets()

    def _createWidgets(self):
        edit1 = ttk.Entry(self)
        edit1.pack(fill=tk.BOTH, expand=tk.YES)
        edit2 = ttk.Entry(self)
        edit2.pack(fill=tk.BOTH, expand=tk.YES)

        self.bind_class('TEntry', '<KeyPress-Tab>', self._insertTab)

    def _insertTab(self, event):
        event.widget.insert(tk.INSERT, '\t')
        return 'break'


if __name__ == '__main__':
    window = A()
    window.mainloop()

Bindtags

Weitergabe von Ereignissen

Events werden grundsätzlich weitergereicht, bis sie zu ihrem Empfänger gelangt sind. Für ein Widget, das den Eingabefokus hat, ist diese Reihenfolge:

  1. das Widget schaut, ob es mit dem Event, beispielsweise einem Tastendruck, etwas anfangen kann. Das eigentliche Widget kann in der Regel nur dann etwas mit dem Ereignis anfangen, wenn per w.bind(…) eine Bindung erzeugt wurde.
  2. Kann das Widget nichts mit dem Ereignis anfangen oder bricht es nach dem Empfang die Weitergabe nicht ab (per return 'break'), dann landet es bei der Klasse (class_-Option oder Standardklasse). Die Klasse besteht unter Umständen aus vielen Widgets, die ihrerseits nun prüfen, ob das Ereignis für sie bestimmt ist. Dasjenige Widget, das den Eingabefokus hat und zur Klasse gehört erhält nun den Zuschlag. Dieses Widget kann seinerseits die Weitergabe abbrechen (per return 'break').
  3. Kann die Klasse nicht mit dem Ereignis anfangen oder wurde die Weitergabe nicht abgebrochen, wird es weitergereicht an die App. In der Regel endet die Weitergabe hier.
  4. Wurde das Ereignis mit w.bind_all(…) gebunden, wird das Ereignis weitergereicht, so dass die all-Bindung das Ereignis verarbeiten kann.

Für ttk.Entry und weitere Widgets ist die Klassenbindung verantwortlich für die Verarbeitung von Tastatureingaben. Bindet man das Ereignis per w.bind_class(…), dann wird unter Umständen das entsprechende Zeichen nicht im Editor angezeigt.

Das folgende Beispiel zeigt die Weitergabe eines Tastaturereignisses am Beispiel Tastendruck-x. Kommentiert man die Zeile entry.bind_class(…) aus, dann wird x im ttk.Entry dargestellt, sonst nicht.

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry('200x200')
        self._createWidgets()

    def _createWidgets(self):
        entry = ttk.Entry(self)
        entry.pack(fill=tk.BOTH, expand=tk.YES)

        entry.bind('<KeyPress-x>', self._printHelloEntry)
        entry.bind_class('TEntry', '<KeyPress-x>', self._printHelloClass)
        self.bind('<KeyPress-x>', self._printHelloApp)
        self.bind_all('<KeyPress-x>', self._printHelloAll)

    def _printHelloEntry(self, event):
        print('Hello Entry')

    def _printHelloClass(self, event):
        print('Hello Class')

    def _printHelloApp(self, event):
        print('Hello App')

    def _printHelloAll(self, event):
        print('Hello All')

if __name__ == '__main__':
    window = A()
    window.mainloop()

Bindtags

Die Reihenfolge, mit der Ereignisse bearbeitet werden, liefert die Methode w.bindtags(). Ruft man sie mit dem ttk.Entry aus obigem Beispiel auf, liefert sie etwa ('.!entry', 'TEntry', '.', 'all'). Die vier Elemente des Tupels bedeuten: Das Widget in Tcl-Schreibweise, die Klasse (TEntry), die App (in Tcl-Schreibweise nur ein Punkt) und die All-Bindung.

Toplevel-Fenster und die App haben nur drei Elemente, beispielsweise ('.', 'Tk', 'all'), der Name des Fensters ist identisch zum Punkt.

Die obengenannte Reihenfolge der Ereignisbearbeitung lässt sich verändern, beispielsweise umkehren. Ruft man w.bindtags(liste) mit einer Liste von Bindtags auf, dann gilt diese Reihenfolge. Folgendes Programm zeigt, wie man die Reihenfolge der Ereignisbearbeitung umkehrt:

import tkinter as tk
from tkinter import ttk

class A(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry('200x200')
        self._createWidgets()

    def _createWidgets(self):
        entry = ttk.Entry(self)
        entry.pack(fill=tk.BOTH, expand=tk.YES)

        print('Bindtags der App:', self.bindtags())
        print('Bindtags des Entry:', entry.bindtags())

        bt = list(entry.bindtags())
        bt.reverse()
        entry.bindtags(bt)
        print('Bindtags des Entry nach Umkehrung:', entry.bindtags())

        entry.bind('<KeyPress-x>', self._printHelloEntry)
        entry.bind_class('TEntry', '<KeyPress-x>', self._printHelloClass)
        self.bind('<KeyPress-x>', self._printHelloApp)
        self.bind_all('<KeyPress-x>', self._printHelloAll)

    def _printHelloEntry(self, event):
        print('Hello Entry')

    def _printHelloClass(self, event):
        print('Hello Class')

    def _printHelloApp(self, event):
        print('Hello App')

    def _printHelloAll(self, event):
        print('Hello All')

if __name__ == '__main__':
    window = A()
    window.mainloop()

Referenzen

Siehe Auch