Skillversitas

The Event System

Kivy's event system is how the app reacts: widgets emit events (on_press, on_text, on_touch_down), and handlers are attached either in the KV file (on_press: root.method()) or in Python (widget.bind(on_press=method)). Underneath, every event is an Event object dispatched through the widget tree, and the touch events are the primitive everything else is built on - a button press is a touch that the button interpreted. This chapter covers the two binding styles, the event flow, touch handling, and the dispatcher for app-wide events.

Binding: KV vs Python

from kivy.uix.button import Button
from kivy.uix.label import Label

class Panel:
    def __init__(self):
        self.btn = Button(text="Hi", size_hint=(0.4, 0.2))
        self.lbl = Label(text="idle")

        # style 1: bind() in Python - explicit, inspectable
        self.btn.bind(on_press=self.handle_press)

    def handle_press(self, instance):
        # `instance` is the widget that fired the event
        self.lbl.text = f"pressed at {instance.center}"

# style 2: in KV, the same binding is one line:
#   Button:
#       on_press: root.handle_press(self)

Two equivalent styles. The KV form (on_press: root.method(self)) is declarative and lives with the UI - the right choice for the standard "this button calls this screen method" wiring. The bind() form is Python and is the right choice when the wiring is dynamic (bind to a widget created at runtime, swap handlers, unbind on cleanup) or when the handler needs logic to decide what to attach. The handler signature is the convention: def handler(self, instance), where instance is the widget that fired - for touch events it is the widget and the event carries the touch. Unbinding is widget.unbind(on_press=handler), the cleanup that prevents a stale handler firing after a widget is replaced.

The Flow: How an Event Travels

A touch lands on a point, and Kivy walks the widget tree from the topmost widget at that point down, calling each widget's on_touch_down until one returns True (claiming the touch) - the propagation model. The widget that claims the touch then receives the subsequent on_touch_move and on_touch_up for that touch, which is how a drag works: the button claims the down, and the up is delivered to it regardless of where the finger drifts. A handler that returns None (or False) passes the touch to the next widget down; returning True stops the walk. Higher-level events are derived: on_press fires when a touch-down and touch-up happen on the same button, on_text fires on each keystroke in a TextInput. The practical read: buttons and fields handle their own derived events for you, and you drop to the raw touch events only when you need the geometry (position, movement) that the derived event does not expose.

Touch Handling: The Primitive

from kivy.uix.widget import Widget

class Canvas(Widget):
    def on_touch_down(self, touch):
        # only handle touches that are on me
        if self.collide_point(*touch.pos):
            print("down at", touch.pos, "id", touch.uid)
            touch.grab(self)      # I own this touch from now on
            return True
        return False

    def on_touch_move(self, touch):
        if touch.grab_current is self:
            print("move to", touch.pos)
            return True

    def on_touch_up(self, touch):
        if touch.grab_current is self:
            touch.ungrab(self)
            print("up at", touch.pos)
            return True

The Touch object carries pos (x, y), uid (the finger id - multi-touch means several Touch objects at once), grab_current (who owns it), and the button (touch.button == 'mouse'/'left'/'touch'). The pattern above is the complete touch handler: collide_point to accept only touches on the widget, grab to claim it so the moves and ups are delivered to you, and ungrab on release. This is the foundation for everything interactive that is not a stock widget - a draggable item, a drawing canvas, a pinch-to-zoom surface - and the custom-widgets chapter builds on it. The rule that keeps multi-widget UIs sane: only the widget under the finger should claim a down, and only the widget that grabbed should handle the rest of that touch's life.

App-Wide Events: The Dispatcher

from kivy.events import EventDispatcher

class MyWindow:
    def __init__(self):
        # app-wide: react to window resize wherever the handler lives
        from kivy.core.window import Window
        Window.bind(on_resize=self.on_win_resize)

    def on_win_resize(self, instance, width, height):
        print("window is now", width, "x", height)

# Kivy also has a global EventDispatcher for cross-cutting signals:
from kivy.event import EventDispatcher
class AppSignals(EventDispatcher):
    __events__ = ("on_data_loaded",)

    def dispatch_event(self, ev):
        return super().dispatch_event(ev)

signals = AppSignals()
signals.bind(on_data_loaded=lambda *a: print("loaded"))
signals.dispatch("on_data_loaded")   # fires every bound handler

Two app-wide mechanisms. The first is binding to the Window (or other core objects) directly - Window.bind(on_resize=...) - the standard way to react to the frame changing, the keyboard showing, or the app pausing (on_pause/on_resume on the App). The second is EventDispatcher, the base class behind every Kivy widget: it gives any object a set of declared events (__events__) with bind/dispatch, which is how you build a loose signal bus for cross-cutting notifications ("data loaded") without coupling the producer to the consumers. The widgets are EventDispatchers, so the same bind/dispatch vocabulary runs from a single button up to an app-wide signal - one mental model for all of it.

Pitfalls

  • Forgetting collide_point in a touch handler. Without it, a widget claims every touch on the screen, stealing input from its siblings.
  • Returning True from on_touch_down without grabbing. The widget gets the down but not the moves and ups - grab it to own the touch's life.
  • Handlers firing after a widget is gone. Unbind (or let the widget's death clean up) when you replace a widget, or the old handler runs on a dead reference.
  • Doing heavy work in a touch handler. Touch events fire at the input rate; the handler should record and return, with the real work deferred (a property, a Clock, a thread).

Summary

Events attach two ways - on_<event>: root.method() in KV for the standard wiring, widget.bind(on_press=handler) in Python for dynamic or logic-driven binding - and the handler takes (self, instance). The flow is propagation: a touch walks the tree top-down until a widget's on_touch_down returns True, and the widget that claims it (via grab) receives the rest of that touch. Derived events (on_press, on_text) are the stock-widget layer; the raw touch events are the geometry layer for custom interaction. App-wide reactions bind to the Window and the App, and EventDispatcher is the general signal mechanism the whole system is built on. Keep handlers fast, guard with collide_point, grab what you claim, and unbind what you replace.