Introduction
A paint app is the most fun way to learn event-driven drawing: the mouse moves, the app responds, lines appear. Tkinter’s Canvas widget does the heavy lifting — you bind three mouse events and a drawing app materializes. This build includes freehand drawing, a color picker, adjustable brush size, an eraser, and one-click clear, in about 80 lines.
It is the desktop cousin of the photo booth: both turn input devices into visuals, one via camera, one via mouse.
Features
- Freehand drawing — smooth lines that follow the mouse.
- Color picker — any color via the built-in chooser.
- Brush size slider — 1 to 30 pixels.
- Eraser mode — draws in the canvas background color.
- Clear canvas — instant reset.
Prerequisites
- Python 3.8+ with Tkinter (bundled on Windows/macOS;
sudo apt install python3-tkon Ubuntu).
Step 1: Create the Script
Save as paint_app.py:
import tkinter as tk
from tkinter import colorchooser
class PaintApp:
def __init__(self, root):
self.root = root
self.root.title("Paint")
self.color = "#10b981"
self.brush_size = 4
self.erasing = False
self.last_x = self.last_y = None
self.BG = "#ffffff"
toolbar = tk.Frame(root, bg="#f3f4f6")
toolbar.pack(fill="x")
tk.Button(toolbar, text="🎨 Color", command=self.pick_color).pack(side="left", padx=4, pady=4)
self.color_swatch = tk.Label(toolbar, bg=self.color, width=3, relief="solid")
self.color_swatch.pack(side="left", padx=2)
tk.Button(toolbar, text="🧽 Eraser", command=self.toggle_eraser, relief="raised").pack(side="left", padx=4)
self.eraser_btn = toolbar.winfo_children()[-1]
tk.Scale(toolbar, from_=1, to=30, orient="horizontal",
command=self.set_size, length=140).pack(side="left", padx=8)
tk.Button(toolbar, text="🗑 Clear", command=self.clear).pack(side="right", padx=4)
self.canvas = tk.Canvas(root, bg=self.BG, cursor="pencil")
self.canvas.pack(fill="both", expand=True)
self.canvas.bind("<B1-Motion>", self.draw)
self.canvas.bind("<ButtonRelease-1>", self.reset)
def pick_color(self):
chosen = colorchooser.askcolor(self.color)
if chosen[1]:
self.color = chosen[1]
self.color_swatch.config(bg=self.color)
self.erasing = False
self.eraser_btn.config(relief="raised")
def toggle_eraser(self):
self.erasing = not self.erasing
self.eraser_btn.config(relief="sunken" if self.erasing else "raised")
def set_size(self, value):
self.brush_size = int(value)
def draw(self, event):
color = self.BG if self.erasing else self.color
if self.last_x is not None:
self.canvas.create_line(
self.last_x, self.last_y, event.x, event.y,
fill=color, width=self.brush_size,
capstyle="round", smooth=True,
)
self.last_x, self.last_y = event.x, event.y
def reset(self, event):
self.last_x = self.last_y = None
def clear(self):
self.canvas.delete("all")
if __name__ == "__main__":
root = tk.Tk()
PaintApp(root)
root.mainloop()
Step 2: Run the App
python paint_app.py
Draw freely, switch colors, resize the brush, erase mistakes — the full paint loop in under a hundred lines.
How It Works
Drawing is three event bindings. <B1-Motion> fires continuously while the left button is held; each firing draws a short line segment from the previous mouse position to the current one. <ButtonRelease-1> fires on mouse-up and clears the stored previous position — without it, the next stroke would start with a long stray line from where the last one ended. That reset handler is the difference between a paint app and a bug.
The Canvas stores every segment as a line object — which is both the strength and the ceiling of this approach. Strength: each segment is a real widget you could manipulate. Ceiling: a long drawing session accumulates thousands of objects and slows down; a real paint program would draw into an offscreen image instead. For a utility app, the object model is perfect.
The eraser is honest about what erasing means here: it draws in the background color. On a plain white canvas that’s indistinguishable from true erasing — and one line of code instead of compositing logic.
smooth=True with capstyle="round" turns the polyline segments into what looks like one continuous stroke — free smoothing from Tkinter’s line renderer.
Common Errors & Fixes
- Stray lines between strokes — the release handler isn’t resetting
last_x/last_y, or you bound motion without the release binding. - Dots don’t appear on single click — a click without motion never fires
<B1-Motion>; draw a tiny circle on<Button-1>if dot-painting matters. - Eraser leaves colored trails — you forgot to use
self.BGas the draw color whenself.erasingis true. - Window resize clears the drawing — Canvas contents aren’t preserved across resizes by default; that’s the object-model ceiling — live with it or move to an image-based canvas.
Key Concepts
- Mouse event bindings — B1-Motion, ButtonRelease, and the state between them.
- Segment chaining — previous-to-current lines build freehand strokes.
- Canvas objects — every drawn thing is a manageable widget.
- Eraser-as-painter — background-color drawing as the simple eraser model.
What to Try Next
- Add shape tools — click-drag rectangles and ovals with
create_rectangle. - Add an undo stack — track canvas item ids per stroke and delete the last batch.
- Add save to PNG — postscript export via
canvas.postscript()plus Pillow conversion. - Combine with the digital clock for a drawing app with a live toolbar clock.
FAQ
Why does drawing get slow after a while?
Every segment is a Canvas object; thousands accumulate. Periodically flattening into an image (Pillow) or restarting the canvas keeps it snappy.
Can I use this on a touchscreen?
Yes — Tkinter maps touch to mouse events on most platforms, so finger painting works out of the box.
How do I make the eraser size independent?
Use a larger width when self.erasing is true — e.g. self.brush_size * 2 in the draw call.