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...
-
you're laying out regular widgets in a scrolling container - prefer ScrollView + Frame
-
the content is primarily structured records - prefer ListView, TableView, or TreeView
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 toscrollregionwhen 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(orbg) -
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
Related widgets
-
ScrollView / Scrollbar - scrolling primitives
-
Text - tag-based content editing
-
ListView - virtual scrolling for record lists
-
PageStack - complex view composition
API reference
bootstack.Canvas