TkinterThe Complete Python GUI Reference
Everything you need to build desktop applications with Python's standard GUI toolkit — beautifully documented with real examples.
What is Tkinter?
Tkinter is Python's standard GUI library — a thin object-oriented layer on top of Tcl/Tk, a mature cross-platform windowing toolkit. It ships with every standard Python distribution. No extra installation required.
With Tkinter you can build windows, dialogs, forms, data-entry tools, visualizations, and full desktop applications that run on Windows, macOS, and Linux from a single codebase.
- Built directly into Tk
- Full option control
- Consistent cross-platform look
- Simpler mental model
- Native OS appearance
- Style / theme engine
- Extra widgets: Treeview, Notebook…
- Preferred for modern apps
Browse the Documentation
A Minimal Example
import tkinter as tk
from tkinter import ttk
def greet():
label.config(text=f"Hello, {name.get()}!")
root = tk.Tk()
root.title("Hello Tkinter")
root.geometry("300x120")
name = tk.StringVar()
ttk.Entry(root, textvariable=name).pack(pady=12)
ttk.Button(root, text="Greet", command=greet).pack()
label = ttk.Label(root, text="")
label.pack(pady=8)
root.mainloop()
This documentation targets Python 3.8+ with Tk 8.6. Most examples work on older versions, but some features (like ttk.Spinbox) require Tk 8.6.
Quick StartFrom zero to a working Tkinter application
Installation
Tkinter ships with Python. Verify it is available:
python -c "import tkinter; print(tkinter.TkVersion)"
# Should print: 8.6On Ubuntu/Debian, run: sudo apt-get install python3-tk
Your First Window
Every Tkinter app follows the same three-step pattern:
- Create the root window —
tk.Tk() - Add widgets inside it
- Start the event loop —
root.mainloop()
import tkinter as tk
root = tk.Tk()
root.title("My First App")
root.geometry("400x300") # Width x Height
root.mainloop()Complete App: Temperature Converter
import tkinter as tk
from tkinter import ttk
class TempConverter(tk.Tk):
def __init__(self):
super().__init__()
self.title("Temperature Converter")
self.resizable(False, False)
self.celsius = tk.StringVar(value="0")
self.fahrenheit = tk.StringVar(value="32")
self._build_ui()
self.celsius.trace_add("write", self._from_c)
self.fahrenheit.trace_add("write", self._from_f)
def _build_ui(self):
f = ttk.Frame(self, padding=20)
f.pack()
ttk.Label(f, text="°C").grid(row=0, column=1)
ttk.Entry(f, textvariable=self.celsius, width=10).grid(row=0, column=0)
ttk.Label(f, text=" = ").grid(row=0, column=2)
ttk.Entry(f, textvariable=self.fahrenheit, width=10).grid(row=0, column=3)
ttk.Label(f, text="°F").grid(row=0, column=4)
def _from_c(self, *_):
try: self.fahrenheit.set(f"{float(self.celsius.get())*9/5+32:.2f}")
except: pass
def _from_f(self, *_):
try: self.celsius.set(f"{(float(self.fahrenheit.get())-32)*5/9:.2f}")
except: pass
TempConverter().mainloop()Subclassing tk.Tk keeps your code organised and scales well as apps grow.
ArchitectureHow Python, Tkinter, Tcl, and Tk fit together
The Technology Stack
Tkinter is a binding — it exposes a Python API for the Tk GUI toolkit, written in C and controlled via the Tcl scripting language.
┌─────────────────────────────────┐
│ Your Python Application │ ← You write this
├─────────────────────────────────┤
│ tkinter (Python module) │ ← Python ↔ Tcl bridge
├─────────────────────────────────┤
│ Tcl/Tk (C library) │ ← Actual GUI engine
├─────────────────────────────────┤
│ OS windowing system │ ← Win32 / Cocoa / X11
└─────────────────────────────────┘The Tk Instance
Every Tkinter application has exactly one tk.Tk() instance. It represents the connection to the Tk interpreter and creates the main (root) window.
Never create more than one tk.Tk(). For additional windows use tk.Toplevel(). Multiple Tk() instances cause unpredictable behaviour.
Widget Hierarchy
Widgets form a parent-child tree. Every widget (except root) has a parent passed as the first positional argument.
root = tk.Tk() # "."
frame = ttk.Frame(root) # ".!frame"
label = ttk.Label(frame, text="Hi") # ".!frame.!label"
print(label.winfo_parent()) # ".!frame"
print(frame.winfo_children()) # [<Label .!frame.!label>]The Event Loop
Tkinter is event-driven. After mainloop(), Tkinter continuously: waits for an event → dispatches to handlers → redraws changed widgets → repeat. Long-running code in a callback blocks and freezes the UI — see Threading.
tk vs ttk
- Original widgets since the 1990s
- Configure every visual property directly
- Consistent cross-platform look (not native)
import tkinter as tk
- Introduced in Tk 8.5
- Uses OS native appearance where possible
- Extra widgets: Treeview, Notebook, Progressbar…
from tkinter import ttk
Prefer ttk widgets whenever they exist. Fall back to tk for Canvas, Text, Listbox, and other widgets with no ttk equivalent.
Geometry ManagersControlling widget size and position
| Manager | Best For | Model |
|---|---|---|
| pack | Simple linear layouts | Stack top/bottom/left/right |
| grid | Forms, tables | Row/column spreadsheet |
| place | Absolute positioning | x/y coords or fractions |
Don't use both pack and grid for widgets sharing the same parent — it causes an infinite geometry loop and freeze.
pack()
| Option | Values | Description |
|---|---|---|
| side | TOP, BOTTOM, LEFT, RIGHT | Which side to pack against |
| fill | NONE, X, Y, BOTH | Expand to fill allocated space |
| expand | True / False | Claim extra space as window grows |
| anchor | N S E W NE NW SE SW CENTER | Alignment when there's spare space |
| padx / pady | int or (int,int) | External padding in pixels |
container = ttk.Frame(root)
container.pack(fill=tk.BOTH, expand=True)
sidebar = ttk.Frame(container, width=120)
sidebar.pack(side=tk.LEFT, fill=tk.Y)
content = ttk.Frame(container)
content.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)grid()
Always call parent.columnconfigure(n, weight=1) on columns that should grow when the window is resized.
root.columnconfigure(1, weight=1)
ttk.Label(root, text="Username:").grid(row=0, column=0, sticky="e", padx=8, pady=6)
ttk.Entry(root).grid (row=0, column=1, sticky="ew", padx=8)
ttk.Label(root, text="Password:").grid(row=1, column=0, sticky="e", padx=8, pady=6)
ttk.Entry(root, show="*").grid (row=1, column=1, sticky="ew", padx=8)
ttk.Button(root, text="Login").grid(row=2, column=0, columnspan=2, pady=12)place()
# Center widget using relative coords
label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)Layout Recipes
App Skeleton (toolbar + content + statusbar)
toolbar = ttk.Frame(root, relief="raised")
toolbar.pack(side="top", fill="x")
content = ttk.Frame(root)
content.pack(fill="both", expand=True)
statusbar = ttk.Label(root, text="Ready", relief="sunken")
statusbar.pack(side="bottom", fill="x")Events & BindingsResponding to user interaction and system events
bind() Basics
label.bind("<Button-1>", lambda e: print(f"Click at {e.x},{e.y}"))
root.bind("<Control-s>", lambda e: save())
root.bind("<Escape>", lambda e: root.destroy())The Event Object
| Attribute | Type | Description |
|---|---|---|
| widget | Widget | The widget that received the event |
| x, y | int | Mouse position relative to widget |
| x_root, y_root | int | Mouse position relative to screen |
| num | int | Mouse button number (1, 2, 3) |
| keysym | str | Key name (e.g. "Return", "a", "F5") |
| char | str | Character typed (may be empty) |
| state | int | Modifier key bitmask (Shift, Ctrl, Alt) |
| delta | int | Scroll amount (MouseWheel events) |
| width, height | int | New size (Configure events) |
Mouse Events
| Sequence | Fires when… |
|---|---|
| <Button-1> | Left mouse button pressed |
| <Button-3> | Right mouse button pressed |
| <ButtonRelease-1> | Left button released |
| <Double-Button-1> | Left button double-clicked |
| <B1-Motion> | Mouse moved while left button held |
| <Motion> | Mouse moved (any button or none) |
| <Enter> / <Leave> | Mouse cursor entered/left widget |
| <MouseWheel> | Mouse wheel scrolled (event.delta) |
Keyboard Events
| Sequence | Description |
|---|---|
| <Key> | Any key pressed |
| <Return> | Enter key |
| <Escape> | Escape key |
| <Tab> / <BackSpace> / <Delete> | Tab / Backspace / Delete |
| <Up> <Down> <Left> <Right> | Arrow keys |
| <F1> … <F12> | Function keys |
| <Control-s> | Ctrl+S |
| <Control-Shift-Z> | Ctrl+Shift+Z |
Virtual Events
| Virtual Event | When Generated |
|---|---|
| <<Cut>> <<Copy>> <<Paste>> | Clipboard operations |
| <<ComboboxSelected>> | ttk.Combobox selection changed |
| <<NotebookTabChanged>> | ttk.Notebook tab switched |
| <<TreeviewSelect>> | ttk.Treeview row selected |
| <<ListboxSelect>> | Listbox selection changed |
after() — Timers
count = tk.IntVar(value=10)
label = ttk.Label(root, textvariable=count, font=("TkDefaultFont", 48))
label.pack(padx=40, pady=20)
def tick():
n = count.get()
if n > 0:
count.set(n - 1)
root.after(1000, tick)
else:
label.config(text="Done!")
root.after(1000, tick)Tk VariablesTwo-way data binding between Python and widgets
Variable Types
| Class | Holds | Default |
|---|---|---|
| StringVar | str | "" |
| IntVar | int | 0 |
| DoubleVar | float | 0.0 |
| BooleanVar | bool | False |
slider_val = tk.DoubleVar()
# Scale widget writes to slider_val
ttk.Scale(root, variable=slider_val, from_=0, to=100).pack(fill="x", padx=20)
# Label automatically shows current value
ttk.Label(root, textvariable=slider_val).pack()
# Entry also linked — typing moves the slider
ttk.Entry(root, textvariable=slider_val).pack(pady=4)trace_add() — Watch for Changes
mode is one of "read", "write", or "unset". The callback receives (name, index, mode).
ITEMS = ["apple", "apricot", "banana", "blueberry", "cherry"]
query = tk.StringVar()
ttk.Entry(root, textvariable=query).pack(pady=8)
listbox = tk.Listbox(root, height=6)
listbox.pack(pady=4)
def filter_items(*_):
q = query.get().lower()
listbox.delete(0, tk.END)
for item in ITEMS:
if q in item:
listbox.insert(tk.END, item)
query.trace_add("write", filter_items)
filter_items()Widget-Variable Links
| Widget | Option | Variable Type |
|---|---|---|
| Entry, Label, Button | textvariable= | StringVar |
| Checkbutton | variable= | BooleanVar or IntVar |
| Radiobutton | variable= | StringVar or IntVar |
| Scale | variable= | IntVar or DoubleVar |
| Spinbox / Combobox | textvariable= | StringVar |
LabelDisplaying text, numbers, and images
ttk.Label(root, text="Hello, World!").pack(pady=8)
tk.Label(root,
text="Styled", font=("Helvetica", 16, "bold"),
fg="white", bg="navy", padx=12, pady=6).pack(pady=8)
# Live clock with textvariable
time_var = tk.StringVar()
ttk.Label(root, textvariable=time_var, font=("TkFixedFont", 24)).pack(pady=20)
def tick():
from datetime import datetime
time_var.set(datetime.now().strftime("%H:%M:%S"))
root.after(1000, tick)
tick()Key Options
| Option | Description |
|---|---|
| text | The string to display |
| textvariable | StringVar — live updates |
| image | PhotoImage to display |
| compound | LEFT/RIGHT/TOP/BOTTOM — image + text arrangement |
| font | Font tuple: ("family", size, "bold italic") |
| fg / bg | Text/background color (tk only) |
| anchor | N/S/E/W/CENTER — alignment |
| wraplength | Wrap text at this pixel width |
| justify | LEFT/RIGHT/CENTER for multi-line text |
| relief / bd | Border style and width |
Always store PhotoImage objects as instance attributes (self.img = ... or label.image = ...). Python garbage-collects unreferenced images and the widget goes blank.
ButtonClickable actions and command triggers
ttk.Button(root, text="Click Me", command=on_click).pack(pady=8)
# Loading state pattern
def on_submit():
btn.state(["disabled"])
btn.config(text="Working…")
def done():
btn.config(text="Submit")
btn.state(["!disabled"])
root.after(2000, done) # simulate work
btn = ttk.Button(root, text="Submit", command=on_submit)
btn.pack(pady=12)Key Options
| Option | Description |
|---|---|
| text | Button label text |
| command | Function called on click (no arguments passed) |
| textvariable | StringVar providing the label |
| image / compound | Image and text arrangement |
| state | normal/active/disabled (tk); use state() for ttk |
| width | Width in characters |
Methods
EntrySingle-line text input with validation
username = tk.StringVar()
entry = ttk.Entry(root, textvariable=username, width=30)
entry.pack(pady=8)
entry.focus_set() # give keyboard focus
# Methods
entry.insert(0, "Hello") # insert at start
entry.insert(tk.END, " World") # append
value = entry.get() # "Hello World"
entry.delete(0, tk.END) # clearValidation — Integers Only
def only_integers(new_value):
return new_value == "" or new_value.lstrip("-").isdigit()
vcmd = root.register(only_integers)
ttk.Entry(root,
validate="key",
validatecommand=(vcmd, "%P")).pack(padx=20, pady=20)Validation Substitutions
| Code | Meaning |
|---|---|
| %P | Proposed value if change is allowed |
| %s | Current value before the change |
| %S | Text being inserted or deleted |
| %d | Action: 1=insert, 0=delete, -1=other |
| %i | Index of the change |
| %V | Trigger: key / focusin / focusout / forced |
Password Field
pw = tk.StringVar()
show = tk.BooleanVar(value=False)
pw_entry = ttk.Entry(root, textvariable=pw, show="●")
pw_entry.pack(pady=8)
ttk.Checkbutton(root, text="Show", variable=show,
command=lambda: pw_entry.config(show="" if show.get() else "●")).pack()TextMulti-line rich text editor with tags, marks, and images
Basic Usage
text = tk.Text(root, width=50, height=10, wrap=tk.WORD, undo=True)
text.pack(padx=8, pady=8)
text.insert("1.0", "Hello, World!
Second line.")
content = text.get("1.0", tk.END).rstrip("\n")Index Notation
| Index | Meaning |
|---|---|
| "1.0" | Line 1, column 0 (the very beginning) |
| "2.5" | Line 2, column 5 |
| "end" or tk.END | One past the last character |
| "insert" | Current insertion cursor |
| "sel.first" / "sel.last" | Start/end of selection |
| "1.0 lineend" | End of line 1 |
| "1.0 + 3c" | 3 characters after 1.0 |
Tags (Formatting)
text.tag_configure("bold", font=("TkDefaultFont", 11, "bold"))
text.tag_configure("red", foreground="red")
text.tag_configure("highlight",background="yellow")
text.tag_configure("link", foreground="blue", underline=True)
text.insert(tk.END, "Bold text", "bold")
text.tag_bind("link", "<Button-1>", lambda e: print("clicked!"))Scrolled Text
frame = ttk.Frame(root)
frame.pack(fill="both", expand=True)
frame.rowconfigure(0, weight=1); frame.columnconfigure(0, weight=1)
text = tk.Text(frame, wrap=tk.WORD)
sb = ttk.Scrollbar(frame, orient="vertical", command=text.yview)
text.configure(yscrollcommand=sb.set)
text.grid(row=0, column=0, sticky="nsew")
sb.grid (row=0, column=1, sticky="ns")Frame & LabelFrameContainers for grouping widgets
Frame — Layout Container
root.rowconfigure(0, weight=1); root.columnconfigure(0, weight=1)
main = ttk.Frame(root); main.grid(row=0, column=0, sticky="nsew")
main.rowconfigure(1, weight=1); main.columnconfigure(1, weight=1)
header = ttk.Frame(main, height=48)
sidebar = ttk.Frame(main, width=200)
content = ttk.Frame(main)
header.grid (row=0, column=0, columnspan=2, sticky="ew")
sidebar.grid(row=1, column=0, sticky="ns")
content.grid(row=1, column=1, sticky="nsew")
sidebar.grid_propagate(False) # freeze sidebar widthLabelFrame — Grouped Controls
group = ttk.LabelFrame(root, text="Connection Settings", padding=12)
group.pack(padx=16, pady=16, fill="x")
group.columnconfigure(1, weight=1)
ttk.Label(group, text="Host:").grid(row=0, column=0, sticky="e")
ttk.Entry(group).grid (row=0, column=1, sticky="ew", padx=4)
ttk.Label(group, text="Port:").grid(row=1, column=0, sticky="e")
ttk.Entry(group, width=8).grid (row=1, column=1, sticky="w", padx=4)pack_propagate
sidebar = ttk.Frame(root, width=200)
sidebar.pack_propagate(False) # don't shrink to children
sidebar.pack(side="left", fill="y")Canvas2D drawing surface for shapes, images, and custom widgets
Drawing Methods
canvas = tk.Canvas(root, width=400, height=300, bg="white", highlightthickness=0)
canvas.pack()
rect = canvas.create_rectangle(50, 50, 150, 100, fill="skyblue", outline="navy")
oval = canvas.create_oval (200, 50, 300, 150, fill="tomato")
line = canvas.create_line (0, 200, 400, 200, fill="gray", dash=(4, 2))
text = canvas.create_text (200, 250, text="Hello Canvas", font=("Arial", 14))All Draw Methods
| Method | Key Options |
|---|---|
| create_rectangle(x1,y1,x2,y2) | fill, outline, width |
| create_oval(x1,y1,x2,y2) | fill, outline |
| create_line(x1,y1,x2,y2,...) | fill, width, dash, arrow, smooth |
| create_polygon(x1,y1,...) | fill, outline, smooth |
| create_arc(x1,y1,x2,y2) | start, extent, style (ARC/CHORD/PIESLICE) |
| create_text(x,y) | text, font, fill, anchor, justify |
| create_image(x,y) | image (PhotoImage), anchor |
| create_window(x,y) | window (any widget!), anchor |
Modifying Items
Draggable Shapes
canvas.create_oval(40,40,100,100, fill="dodgerblue", tags="drag")
_d = {"x":0,"y":0,"item":None}
def start(e):
_d["item"] = canvas.find_closest(e.x, e.y)[0]
_d["x"], _d["y"] = e.x, e.y
def drag(e):
canvas.move(_d["item"], e.x-_d["x"], e.y-_d["y"])
_d["x"], _d["y"] = e.x, e.y
canvas.tag_bind("drag", "<Button-1>", start)
canvas.tag_bind("drag", "<B1-Motion>", drag)ListboxScrollable list of selectable items
Basic Usage
lb = tk.Listbox(root, height=6, selectmode=tk.EXTENDED)
lb.pack(padx=10, pady=10)
for fruit in ["Apple", "Banana", "Cherry", "Durian"]:
lb.insert(tk.END, fruit)
lb.bind("<<ListboxSelect>>",
lambda e: print([lb.get(i) for i in lb.curselection()]))Selection Modes
| Mode | Behavior |
|---|---|
| BROWSE | Single selection; click drags (default) |
| SINGLE | Single selection; no drag |
| MULTIPLE | Toggle multiple with click |
| EXTENDED | Range select with Shift+click, Ctrl+click |
Key Methods
ScrollbarThe two-way link pattern
Scrollbars link to a scrollable widget through a two-way command interface.
text = tk.Text(frame, wrap="word")
vbar = ttk.Scrollbar(frame, orient="vertical")
# TWO-WAY link
text.configure(yscrollcommand=vbar.set) # widget tells scrollbar position
vbar.configure(command=text.yview) # scrollbar tells widget to scroll
text.grid(row=0, column=0, sticky="nsew")
vbar.grid(row=0, column=1, sticky="ns")
frame.rowconfigure(0, weight=1); frame.columnconfigure(0, weight=1)Compatible widgets: Text Canvas Listbox Entry ttk.Treeview
CheckbuttonOn/off toggle with BooleanVar
notifications = tk.BooleanVar(value=True)
dark_mode = tk.BooleanVar(value=False)
ttk.Checkbutton(root, text="Enable notifications",
variable=notifications).pack(anchor="w", padx=12, pady=4)
ttk.Checkbutton(root, text="Dark mode",
variable=dark_mode,
command=lambda: print("Dark:", dark_mode.get())).pack(anchor="w", padx=12)
ttk.Button(root, text="Show values",
command=lambda: print(notifications.get(), dark_mode.get())).pack(pady=8)Key Options
| Option | Description |
|---|---|
| variable | BooleanVar (or IntVar) tracking state |
| onvalue / offvalue | Values for checked/unchecked (default True/False) |
| command | Callback on toggle |
| text / textvariable | Label text |
RadiobuttonMutually exclusive choices
All Radiobuttons sharing the same variable form a group — only one selectable at a time.
size = tk.StringVar(value="M")
for label, value in [("Small","S"),("Medium","M"),("Large","L")]:
ttk.Radiobutton(root, text=label, variable=size, value=value).pack(anchor="w", padx=20)
ttk.Button(root, text="Show",
command=lambda: print("Size:", size.get())).pack(pady=8)Key Options
| Option | Description |
|---|---|
| variable | StringVar or IntVar shared by the group |
| value | The value this button sets when selected |
| command | Callback when this button is selected |
| indicatoron | 0 = button-style (no radio dot) — tk only |
ScaleSlider for numeric values
r, g, b = tk.IntVar(), tk.IntVar(), tk.IntVar()
preview = tk.Label(root, width=20, height=3)
preview.pack(pady=8)
def update(*_):
preview.config(bg=f"#{r.get():02x}{g.get():02x}{b.get():02x}")
for label, var, fg in [("R",r,"red"),("G",g,"green"),("B",b,"blue")]:
f = tk.Frame(root)
f.pack(fill="x", padx=10)
tk.Label(f, text=label, fg=fg, width=2).pack(side="left")
tk.Scale(f, variable=var, from_=0, to=255,
orient="horizontal", command=update,
showvalue=False).pack(side="left", fill="x", expand=True)Key Options
| Option | Description |
|---|---|
| from_ / to | Min/max value (from_ has trailing underscore — "from" is a keyword) |
| orient | HORIZONTAL or VERTICAL |
| variable | IntVar or DoubleVar |
| resolution | Snap increment |
| tickinterval | Spacing between tick labels (tk only) |
| command | Callback called with new value string |
SpinboxNumeric or value list spinner
quantity = tk.IntVar(value=1)
ttk.Spinbox(root, from_=1, to=99, textvariable=quantity, width=6).pack(pady=8)
day = tk.StringVar(value="Monday")
ttk.Spinbox(root,
values=("Monday","Tuesday","Wednesday","Thursday","Friday"),
textvariable=day, width=12, state="readonly").pack(pady=4)Key Options
| Option | Description |
|---|---|
| from_ / to / increment | Numeric range and step |
| values | Tuple of strings to cycle through |
| wrap | True = cycle back to start/end |
| format | printf-style format, e.g. "%.2f" |
| state | normal / readonly / disabled |
ToplevelSecondary windows and modal dialogs
Basic Window
def open_window():
win = tk.Toplevel(root)
win.title("Secondary Window")
win.geometry("300x200")
ttk.Button(win, text="Close", command=win.destroy).pack(pady=20)Modal Dialog Pattern
class AskNameDialog:
def __init__(self, parent):
self.result = None
win = tk.Toplevel(parent)
win.grab_set() # capture all input events
win.transient(parent) # stay above parent
win.title("Enter Name")
name = tk.StringVar()
ttk.Label(win, text="Name:").pack(padx=16, pady=(16,4))
ttk.Entry(win, textvariable=name).pack(padx=16)
def ok():
self.result = name.get()
win.destroy()
ttk.Button(win, text="OK", command=ok).pack(pady=12)
parent.wait_window(win) # block until closed
dlg = AskNameDialog(root)
print(dlg.result) # available after dialog closesClose Protocol
from tkinter import messagebox
def on_close():
if messagebox.askokcancel("Quit", "Do you want to quit?"):
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_close)TTK OverviewNative-looking widgets with a powerful style engine
Importing
import tkinter as tk
from tkinter import ttktk vs ttk Key Differences
| Feature | tk widgets | ttk widgets |
|---|---|---|
| Appearance | Custom cross-platform | Native OS look |
| Color/font config | bg=, fg=, font= directly | Via ttk.Style |
| State system | state="disabled" | widget.state(["disabled"]) |
| Extra widgets | None | Treeview, Notebook, Progressbar, Combobox, Separator, Sizegrip |
Available Themes
style = ttk.Style()
print(style.theme_names()) # list available
print(style.theme_use()) # current theme
style.theme_use("clam") # switch themeState System
btn.state(["disabled"]) # set flag
btn.state(["!disabled"]) # clear flag (! = negate)
print(btn.instate(["disabled"])) # True/False
print(btn.state()) # e.g. ('focus',)All TTK Widgets
TTK StyleCustomising the appearance of themed widgets
configure()
style = ttk.Style(root)
style.theme_use("clam")
style.configure("TButton",
font=("Helvetica", 11, "bold"),
padding=(10, 6),
background="#2563eb",
foreground="white")map() — State-Based Styles
style.map("TButton",
background=[
("disabled", "#9ca3af"),
("active", "#1d4ed8"),
("pressed", "#1e3a8a"),
("!disabled","#2563eb"),
],
foreground=[("disabled","#e5e7eb"),("!disabled","white")],
)Named (Custom) Styles
# Name must end with .TClassName
style.configure("Danger.TButton", background="#dc2626", foreground="white")
style.configure("Success.TButton",background="#16a34a", foreground="white")
ttk.Button(root, text="Delete", style="Danger.TButton").pack()
ttk.Button(root, text="Confirm", style="Success.TButton").pack()Debugging
print(style.configure("TButton")) # current settings
print(style.lookup("TButton", "background"))# resolved value
print(style.layout("TButton")) # element layoutttk.NotebookTab-based multi-page container
nb = ttk.Notebook(root)
nb.pack(fill="both", expand=True, padx=8, pady=8)
tab1 = ttk.Frame(nb, padding=12)
tab2 = ttk.Frame(nb, padding=12)
nb.add(tab1, text="General")
nb.add(tab2, text="Advanced")
ttk.Label(tab1, text="General settings").pack()
ttk.Label(tab2, text="Advanced options").pack()Methods
Tab Change Event
def on_change(event):
sel = nb.select()
text = nb.tab(sel, "text")
print(f"Switched to: {text}")
nb.bind("<<NotebookTabChanged>>", on_change)ttk.TreeviewHierarchical data display and sortable tables
Table Mode
columns = ("name", "age", "city")
tree = ttk.Treeview(root, columns=columns, show="headings", height=8)
for col in columns:
tree.heading(col, text=col.title())
for row in [("Alice",30,"NYC"),("Bob",24,"London"),("Carol",35,"Tokyo")]:
tree.insert("", tk.END, values=row)
tree.pack(fill="both", expand=True)Tree Mode
tree = ttk.Treeview(root)
food = tree.insert("", tk.END, text="Food", open=True)
fruit = tree.insert(food, tk.END, text="Fruit")
tree.insert(fruit, tk.END, text="Apple")
tree.insert(fruit, tk.END, text="Banana")Sortable Columns
def sort_col(tree, col, reverse):
data = [(tree.set(iid, col), iid) for iid in tree.get_children()]
data.sort(reverse=reverse)
for i, (_, iid) in enumerate(data):
tree.move(iid, "", i)
tree.heading(col, command=lambda: sort_col(tree, col, not reverse))
for col in columns:
tree.heading(col, text=col.title(),
command=lambda c=col: sort_col(tree, c, False))ttk.ProgressbarDeterminate and indeterminate progress
progress = tk.IntVar(value=0)
# Determinate — shows exact %
bar = ttk.Progressbar(root, variable=progress, maximum=100, length=300)
bar.pack(pady=20)
# Indeterminate — animated "working" bar
busy = ttk.Progressbar(root, mode="indeterminate", length=300)
busy.pack(pady=8)
busy.start(15) # ms between stepsWith Background Thread
import threading, time
progress = tk.DoubleVar()
bar = ttk.Progressbar(root, variable=progress, maximum=100, length=300)
bar.pack(pady=20)
def work():
for i in range(101):
time.sleep(0.05)
root.after(0, progress.set, i)
threading.Thread(target=work, daemon=True).start()ttk.ComboboxDropdown selector with editable entry
country = tk.StringVar()
combo = ttk.Combobox(root,
textvariable=country,
values=["Canada", "France", "Germany", "Japan", "UK", "USA"],
state="readonly",
width=18)
combo.pack(pady=12)
combo.set("USA")
combo.bind("<<ComboboxSelected>>",
lambda e: print("Selected:", country.get()))Key Options
| Option | Description |
|---|---|
| values | List/tuple of dropdown options |
| state | "normal" (editable), "readonly", "disabled" |
| height | Max rows shown in dropdown |
| postcommand | Callback fired before dropdown opens (to refresh values) |
# Refresh values just before dropdown opens
def refresh():
combo["values"] = get_current_options()
combo.configure(postcommand=refresh)Standard Dialogsmessagebox, filedialog, colorchooser, simpledialog
messagebox
from tkinter import messagebox
messagebox.showinfo ("Done", "File saved.")
messagebox.showwarning("Warning", "Low disk space.")
messagebox.showerror ("Error", "File not found.")
if messagebox.askyesno("Confirm", "Delete this item?"):
delete_item()
if messagebox.askokcancel("Quit", "Unsaved changes. Quit anyway?"):
root.destroy()filedialog
from tkinter import filedialog
path = filedialog.askopenfilename(
title="Open File",
filetypes=[("Text files","*.txt"),("All files","*.*")])
if path: print(f"Opening: {path}")
save_path = filedialog.asksaveasfilename(defaultextension=".txt")
folder = filedialog.askdirectory(title="Select Folder")colorchooser
from tkinter import colorchooser
# Returns ((R,G,B), "#rrggbb") or (None, None) if cancelled
rgb, hex_col = colorchooser.askcolor(color="#ff0000", title="Pick Colour")
if hex_col:
label.config(bg=hex_col)simpledialog
from tkinter import simpledialog
name = simpledialog.askstring ("Name", "Enter your name:")
age = simpledialog.askinteger("Age", "Enter your age:", minvalue=0, maxvalue=150)
price = simpledialog.askfloat ("Price", "Enter price:", minvalue=0.0)ThreadingKeeping your UI responsive during long operations
Tkinter is not thread-safe. Only the main thread should call Tk methods. Background threads must send results back via root.after(0, callback, result) or a queue.Queue.
Background Thread + after()
import threading, time
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
status = ttk.Label(root, text="Idle")
status.pack(pady=8)
bar = ttk.Progressbar(root, mode="indeterminate", length=200)
bar.pack(pady=4)
def do_work():
time.sleep(3) # your long-running task here
result = "42"
root.after(0, on_done, result) # safe callback to main thread
def on_done(result):
bar.stop()
status.config(text=f"Result: {result}")
btn.state(["!disabled"])
def start():
btn.state(["disabled"])
status.config(text="Working…")
bar.start(10)
threading.Thread(target=do_work, daemon=True).start()
btn = ttk.Button(root, text="Start", command=start)
btn.pack(pady=8)
root.mainloop()Thread-Safe Queue Pattern
import threading, queue, time
msg_queue = queue.Queue()
def worker():
for i in range(10):
time.sleep(0.5)
msg_queue.put(f"Step {i+1}/10
")
msg_queue.put(None) # sentinel: done
def poll():
while True:
try:
msg = msg_queue.get_nowait()
except queue.Empty:
break
if msg is None: return
log.config(state="normal")
log.insert("end", msg)
log.config(state="disabled")
root.after(100, poll) # check again in 100ms
threading.Thread(target=worker, daemon=True).start()
root.after(100, poll)ImagesPhotoImage, BitmapImage, and Pillow integration
PhotoImage (Built-in)
Supports GIF, PGM, PPM, and PNG natively. For JPEG and other formats use Pillow.
img = tk.PhotoImage(file="photo.png")
label = ttk.Label(root, image=img)
label.image = img # ← MUST keep reference or image vanishes!
label.pack()
# Resize (integer factors only)
small = img.subsample(2, 2) # ÷2
big = img.zoom(3, 3) # ×3Always assign your PhotoImage to self.img or widget.image. Local variables get garbage-collected before Tk renders the image.
Pillow for JPEG & More
pip install Pillowfrom PIL import Image, ImageTk
pil_img = Image.open("photo.jpg")
pil_img = pil_img.resize((200, 200), Image.LANCZOS)
tk_img = ImageTk.PhotoImage(pil_img)
label = tk.Label(root, image=tk_img)
label.image = tk_img # keep reference
label.pack()Images on Canvas
canvas = tk.Canvas(root, width=400, height=300)
canvas.pack()
img = tk.PhotoImage(file="bg.png")
canvas.image = img
canvas.create_image(0, 0, image=img, anchor="nw")FontsUsing and customising fonts with tkinter.font
Specifying Fonts
from tkinter import font
# Tuple format (family, size, style...)
tk.Label(root, font=("Helvetica", 14, "bold italic"))
# Font object (most flexible)
my_font = font.Font(family="Helvetica", size=14, weight="bold")
tk.Label(root, font=my_font)Named System Fonts
| Name | Use |
|---|---|
| TkDefaultFont | Default for most widgets |
| TkTextFont | Entry, Text widgets |
| TkFixedFont | Monospace / code |
| TkMenuFont | Menu items |
| TkHeadingFont | Column headings |
| TkCaptionFont | Window titles |
Font Object Methods
# Change default font for ALL widgets at once
font.nametofont("TkDefaultFont").configure(family="Arial", size=11)
font.nametofont("TkTextFont").configure(family="Consolas", size=11)
# List all available font families
print(sorted(font.families()))Common PatternsReusable recipes for everyday Tkinter tasks
MVC Application Structure
class Model:
def __init__(self): self.items = []
def add(self, item): self.items.append(item)
class View(ttk.Frame):
def __init__(self, parent):
super().__init__(parent, padding=12)
self.entry_var = tk.StringVar()
ttk.Entry(self, textvariable=self.entry_var).pack(fill="x")
self.add_btn = ttk.Button(self, text="Add")
self.add_btn.pack(pady=4)
self.lb = tk.Listbox(self)
self.lb.pack(fill="both", expand=True)
def refresh(self, items):
self.lb.delete(0, tk.END)
for item in items: self.lb.insert(tk.END, item)
class Controller:
def __init__(self, model, view):
self.m, self.v = model, view
view.add_btn.configure(command=self._add)
def _add(self):
t = self.v.entry_var.get().strip()
if t: self.m.add(t); self.v.entry_var.set(""); self.v.refresh(self.m.items)Scrollable Frame
class ScrollableFrame(ttk.Frame):
def __init__(self, parent, **kw):
super().__init__(parent, **kw)
canvas = tk.Canvas(self, highlightthickness=0)
sb = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=sb.set)
sb.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
self.inner = ttk.Frame(canvas)
wid = canvas.create_window((0,0), window=self.inner, anchor="nw")
self.inner.bind("<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.bind("<Configure>",
lambda e: canvas.itemconfig(wid, width=e.width))
canvas.bind("<MouseWheel>",
lambda e: canvas.yview_scroll(-1*(e.delta//120), "units"))Tooltip
class Tooltip:
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tip = None
widget.bind("<Enter>", self._show)
widget.bind("<Leave>", self._hide)
def _show(self, event):
x = self.widget.winfo_rootx() + 20
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 4
self.tip = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry(f"+{x}+{y}")
tk.Label(tw, text=self.text, bg="#ffffe0",
relief="solid", bd=1, padx=4).pack()
def _hide(self, event):
if self.tip: self.tip.destroy(); self.tip = None
Tooltip(save_btn, "Save the current file (Ctrl+S)")Center Window on Screen
def center_window(win, width=400, height=300):
win.update_idletasks()
sw = win.winfo_screenwidth()
sh = win.winfo_screenheight()
x = (sw - width) // 2
y = (sh - height) // 2
win.geometry(f"{width}x{height}+{x}+{y}")
center_window(root, 600, 400)Debounced Entry (Search-as-you-type)
_after_id = None
def on_key(*_):
global _after_id
if _after_id:
root.after_cancel(_after_id)
_after_id = root.after(300, do_search) # 300ms delay
def do_search():
query = search_var.get()
# perform search...
search_var.trace_add("write", on_key)