Skillversitas

The Canvas and Graphics

Every Kivy widget has a canvas - a list of drawing instructions (a rectangle, a line, a colored background) that the render loop executes to paint the widget. The stock widgets build their own canvases for you, but when you need to draw something the toolkit does not ship - a progress arc, a chart, a custom shape, a game surface - you add instructions to a widget's canvas directly, or to a Canvas/CanvasInstruction you control. This chapter covers the canvas model, the core instructions (color, rectangle, line, ellipse), the Canvas widget for free drawing, and binding canvas state to properties so the drawing updates reactively.

The Canvas Model: Instructions, Not Pixels

A canvas is an ordered list of instructions, not a bitmap: a Color instruction sets the current fill/stroke color, a Rectangle fills a region with it, a Line strokes a path, and so on. The renderer walks the list in order, so the drawing is a program - later instructions paint over earlier ones, and there is no implicit clearing (you clear by removing and re-adding instructions, or by a ClearColor). The widget's canvas draws the widget's background and content; the canvas.before and canvas.after layers draw beneath and above it, which is how a widget puts an accent behind its content or an overlay on top. Because the instructions reference the widget's properties (self.width, a bound NumericProperty), a canvas can be reactive - change the property, and the next frame redraws with the new value, which is how a progress arc tracks a value without any manual redraw call.

The Core Instructions

from kivy.uix.widget import Widget
from kivy.graphics import Color, Rectangle, Line, Ellipse

class Card(Widget):
    def on_size(self, *args):
        self._redraw()

    def _redraw(self):
        self.canvas.clear()
        with self.canvas:
            # background: a rounded-ish card
            Color(0.95, 0.95, 0.97)
            Rectangle(pos=self.pos, size=self.size)
            # an accent bar on the left
            Color(0.2, 0.5, 0.9)
            Rectangle(pos=(self.x, self.y),
                      size=(6, self.height))
            # a line across the card
            Color(0.8, 0.8, 0.8)
            Line(points=[self.x, self.y + 40,
                         self.x + self.width, self.y + 40],
                 width=1)

The pattern: with self.canvas: groups the instructions (the context manager appends them and tracks them for clearing), and each instruction is an object with the geometry and the current color. Color(r, g, b, a) sets the fill; Rectangle(pos, size) fills; Line(points, width) strokes a polyline; Ellipse(pos, size) draws an oval. The on_size redraw is the standard lifecycle hook - the canvas is built from the widget's current dimensions, so when the size changes (a layout reflow, a window resize) the on_size fires and the canvas is rebuilt. canvas.clear() removes the previous instructions before the new ones are added; skipping it is the "my drawing is doubled/ghosted" bug, because the old instructions are still in the list.

The Canvas Widget and Free Drawing

from kivy.uix.widget import Widget
from kivy.graphics import Color, Line

class DrawingArea(Widget):
    def __init__(self, **kw):
        super().__init__(**kw)
        self._points = []
        with self.canvas:
            Color(0.1, 0.1, 0.1)

    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            self._points = [touch.x, touch.y]
            touch.grab(self)
            return True

    def on_touch_move(self, touch):
        if touch.grab_current is not self:
            return
        self._points.extend([touch.x, touch.y])
        self.canvas.clear()
        with self.canvas:
            Color(0.1, 0.1, 0.1)
            Line(points=self._points, width=3)

    def on_touch_up(self, touch):
        if touch.grab_current is self:
            touch.ungrab(self)

A plain Widget subclass with touch handlers is a free drawing surface: accumulate the touch points, and on each move rebuild the canvas's Line from the point list. This is the minimal sketch pad, and the structure - a point list, a Line instruction, a redraw on move - is the foundation for any custom 2D content: a signature pad, a chart that plots data, a game's play field. The redraw-on-every-move is fine for a simple line; for a fast drawing, batch the points and let the render loop catch up (the threading chapter covers keeping the draw off the input path). The Canvas instruction set (arcs via Arc, text via Text, images via InstructionCollection) extends the same model to the full 2D vocabulary.

Reactive Drawing: Binding the Canvas to a Property

from kivy.uix.widget import Widget
from kivy.properties import NumericProperty
from kivy.graphics import Color, Arc

class ProgressRing(Widget):
    value = NumericProperty(0)   # 0..1

    def __init__(self, **kw):
        super().__init__(**kw)
        self.bind(value=self._redraw)

    def _redraw(self, *args):
        self.canvas.clear()
        with self.canvas:
            cx = cy = self.width / 2
            r = min(self.width, self.height) / 2 - 4
            Color(0.85, 0.85, 0.85)
            Arc(pos=(cx - r, cy - r), size=(r * 2, r * 2),
                start_angle=0, end_angle=360)
            Color(0.2, 0.7, 0.3)
            Arc(pos=(cx - r, cy - r), size=(r * 2, r * 2),
                start_angle=0, end_angle=360 * self.value)

The reactive canvas is the payoff of the properties chapter applied to drawing: a NumericProperty (value) is bound to a redraw method, and the Arc's end_angle is a function of the value. Set ring.value = 0.7 and the next frame redraws the arc to 252 degrees - no manual canvas call, no event handler, just the property change propagating to the draw. This is how a progress ring, a gauge, or a live chart tracks its data: the data is a property, the canvas is a function of it, and the binding is the connection. The rule is the one from the properties chapter - keep the redraw cheap, because it runs on the UI thread at the rate the property changes - and for a fast-changing value, throttle the property updates (the threading chapter's Clock) so the canvas redraws at the frame rate, not the data rate.

Summary

A widget's canvas is an ordered list of drawing instructions - Color sets the fill, Rectangle/Line/Arc/Ellipse draw with it - rendered in order with no implicit clear, so canvas.clear() before a redraw is the rule that prevents ghosting. Build the canvas in on_size (the dimensions are the inputs), group instructions under with self.canvas:, and use a plain Widget with touch handlers as a free drawing surface (the sketch-pad pattern). The advanced form is the reactive canvas: bind a NumericProperty to a redraw so the drawing is a pure function of state - the progress ring whose arc follows value - which is how custom graphics stay in sync with the data without manual redraw calls. The model is a program, not a picture, and that is what makes it both flexible and reactive.