Skip to content

Canvas

Canvas is Tkinter's 2D drawing and interaction surface (tk.Canvas).

It's the foundation for many custom UI patterns:

  • diagrams and node editors

  • charts and visualizations

  • drag-and-drop surfaces

  • zoomable / pannable views

  • virtual scrolling for custom content

bootstack exposes Canvas as a first-class widget so you can build high-performance, interactive views with consistent theming and practical patterns.

Prefer higher-level widgets when they fit

If your UI is primarily structured data, prefer ListView, TableView, or TreeView. Use Canvas when you need custom drawing, freeform layout, or interaction that standard widgets can't express.


Quick start

Draw a few shapes:

import bootstack as bs

app = bs.App()

c = bs.Canvas(app, width=520, height=260, background="white")
c.pack(fill="both", expand=True, padx=20, pady=20)

c.create_rectangle(20, 20, 220, 120, fill="#0d6efd", outline="")
c.create_oval(260, 30, 480, 140, outline="#212529", width=2)
c.create_text(260, 190, text="Canvas", font="heading-lg")

app.mainloop()

When to use

Use Canvas when:

  • you need custom drawing or freeform layout

  • interaction is item-based (hit testing, dragging, linking)

  • you need zooming/panning or custom virtualization

Consider a different control when...


Appearance

Canvas has a large option surface. These are the ones you'll use most.

Size and coordinates

  • width, height - canvas size (screen units)

  • scrollregion - the "virtual space" you can scroll around

  • confine - constrain view to scrollregion when scrolling

c = bs.Canvas(app, width=600, height=400, scrollregion=(0, 0, 2000, 1200))

Resetting scrollregion

Setting scrollregion=() resets it to empty. Setting it to None may not clear it on all Tk builds.

Interaction

  • closeenough - hit test tolerance (pixels)

  • cursor - cursor over the canvas

  • state="normal" | "disabled"

Theme-critical colors

  • background (or bg)

  • selection colors: selectbackground, selectforeground

If your canvas items depend on theme colors, set item colors explicitly (e.g., fill=..., outline=...) rather than relying on widget-level defaults.


Examples and patterns

Canvas items

Canvas content is made of items, each with an integer id:

rect = c.create_rectangle(10, 10, 110, 60, fill="tomato", outline="")
text = c.create_text(60, 35, text="Hello")

Item ids are stable until deleted. You can also assign tags to items.

Common item types

  • create_line

  • create_rectangle

  • create_oval

  • create_polygon

  • create_text

  • create_image

  • create_window (embed a widget)

Tags

Tags are the best way to manage groups of items.

  • Apply styles to many items at once

  • Bind events to groups

  • Move/scale groups as a unit

c.create_rectangle(20, 20, 140, 80, fill="#198754", outline="", tags=("node", "ok"))
c.create_text(80, 50, text="Node", fill="white", tags=("node",))

# Move all items with the "node" tag
c.move("node", 200, 0)

Bind events to a tag

def on_click(_):
    print("node clicked")

c.tag_bind("node", "<Button-1>", on_click)

Coordinates and transforms

Reading and updating item coordinates

xy = c.coords(rect)            # list[float]
c.coords(rect, 20, 20, 180, 90)  # set new coords

Move items

c.move(rect, 10, 0)
c.move("node", 0, 20)  # move all items with tag

Scale for zoom

scale(tagOrId, xOrigin, yOrigin, xScale, yScale) scales coordinates around an origin.

def zoom(factor: float, origin=(0, 0)):
    c.scale("all", origin[0], origin[1], factor, factor)
    c.configure(scrollregion=c.bbox("all"))

Canvas scaling

scale() scales item coordinates. Text and line widths may need extra handling depending on the effect you want.

Scrolling

Canvas uses xscrollcommand/yscrollcommand and xview/yview like Text.

import bootstack as bs

app = bs.App()

frame = bs.Frame(app)
frame.pack(fill="both", expand=True, padx=20, pady=20)

c = bs.Canvas(frame, scrollregion=(0, 0, 2000, 1200))
c.grid(row=0, column=0, sticky="nsew")

ysb = bs.Scrollbar(frame, orient="vertical", command=c.yview)
ysb.grid(row=0, column=1, sticky="ns")

xsb = bs.Scrollbar(frame, orient="horizontal", command=c.xview)
xsb.grid(row=1, column=0, sticky="ew")

c.configure(yscrollcommand=ysb.set, xscrollcommand=xsb.set)

frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)

app.mainloop()

Convert screen coordinates to canvas coordinates

When the canvas is scrolled, event x/y are screen coordinates relative to the widget. Use canvasx and canvasy to convert:

def on_click(e):
    x = c.canvasx(e.x)
    y = c.canvasy(e.y)
    print("canvas coords:", x, y)

c.bind("<Button-1>", on_click)

Hit testing

Canvas provides "find" helpers:

  • find_closest(x, y)

  • find_overlapping(x1, y1, x2, y2)

  • find_withtag(tagOrId)

def on_click(e):
    x, y = c.canvasx(e.x), c.canvasy(e.y)
    hit = c.find_closest(x, y)
    if hit:
        print("hit item id:", hit[0], "tags:", c.gettags(hit[0]))

c.bind("<Button-1>", on_click)

Dragging (common pattern)

A minimal drag pattern:

drag = {"id": None, "x": 0, "y": 0}

def on_down(e):
    x, y = c.canvasx(e.x), c.canvasy(e.y)
    hit = c.find_closest(x, y)
    if not hit:
        return
    drag["id"] = hit[0]
    drag["x"], drag["y"] = x, y

def on_move(e):
    if drag["id"] is None:
        return
    x, y = c.canvasx(e.x), c.canvasy(e.y)
    dx, dy = x - drag["x"], y - drag["y"]
    c.move(drag["id"], dx, dy)
    drag["x"], drag["y"] = x, y

def on_up(_):
    drag["id"] = None

c.bind("<ButtonPress-1>", on_down)
c.bind("<B1-Motion>", on_move)
c.bind("<ButtonRelease-1>", on_up)

Behavior

Performance tips

  • Prefer tags for bulk operations instead of iterating every item id.

  • Avoid deleting/recreating everything each frame; update coordinates and styles in-place.

  • Limit the number of canvas items when possible; thousands are fine, tens of thousands may require careful design.

  • For "virtualized" surfaces, render only what's visible (use canvasx/canvasy + view bounds).


Additional resources

API reference

  • bootstack.Canvas