Tkinter Crash Course: Desktop Development With Python
Tkinter Crash Course: Desktop Development With Python
A comprehensive guide from basic GUIs to modern web-wrapper apps, multi-threading, and distribution.
Table of Contents
1. Introduction & Setup
2. Core Concepts: Windows, Geometry, and Layout
3. Essential Widgets
4. Advanced Widgets
5. Styling with ttk and Themes
6. Event Handling & Bindings
7. Dialogs, Menus, and System Tray
8. Multi-threading & Responsiveness
9. Data Persistence: SQLite, JSON, Config Files
10. Modern HTML Apps with Web Wrappers
11. Packaging & Distribution
12. Real-World Project: A Full Task Manager App
Installation Check
import tkinter as tk
print([Link]) # Should print 8.6 or higher
[Link]().mainloop() # Opens a blank window
root = [Link]()
[Link]("My App")
[Link]("800x600") # WIDTHxHEIGHT
[Link]("800x600+100+50") # ...+X_OFFSET+Y_OFFSET
[Link](400, 300)
[Link](1920, 1080)
[Link](True, False) # (width_resizable, height_resizable)
root = [Link]()
btn1 = [Link](root, text="Top")
[Link](side=[Link], fill=tk.X, padx=10, pady=5)
[Link]()
root = [Link]()
[Link](1, weight=1) # Column 1 expands with window
[Link]()
root = [Link]()
[Link]("600x400")
# Sidebar
sidebar = [Link](root, bg="#2c3e50", width=150)
[Link](side=[Link], fill=tk.Y)
sidebar.pack_propagate(False) # Prevent frame from shrinking to
content
[Link]()
3. Essential Widgets
Label
import tkinter as tk
from tkinter import ttk
root = [Link]()
# Basic label
lbl = [Link](root, text="Hello World",
font=("Helvetica", 16, "bold"),
fg="#333333", bg="white",
wraplength=200, # Word-wrap at 200px
justify=[Link])
[Link](padx=20, pady=10)
# With image
img = [Link](file="[Link]")
img_label = [Link](root, image=img, text="Icon + Text",
compound=[Link]) # Image left of text
img_label.pack()
# Dynamic update
[Link](text="Updated!")
lbl["text"] = "Also works"
[Link]()
Button
import tkinter as tk
root = [Link]()
def on_click():
print("Clicked!")
# Standard button
btn = [Link](root, text="Click Me",
command=on_click,
bg="#3498db", fg="white",
font=("Arial", 11),
padx=15, pady=8,
relief=[Link],
cursor="hand2", # Pointer cursor on hover
activebackground="#2980b9",
activeforeground="white")
[Link](pady=10)
# Disable / enable
[Link](state=[Link])
[Link](state=[Link])
def toggle():
is_active.set(not is_active.get())
toggle_btn.config(
text="ON" if is_active.get() else "OFF",
bg="#2ecc71" if is_active.get() else "#e74c3c"
)
[Link]()
root = [Link]()
# Password field
pwd = [Link](root, show="*", font=("Arial", 12))
[Link](padx=20, pady=5)
# Trace changes
def on_change(*args):
print(f"Value: {name_var.get()}")
name_var.trace_add("write", on_change)
# Read / clear
value = [Link]()
[Link](0, [Link])
[Link]()
Text (Multi-line)
import tkinter as tk
root = [Link]()
# Read-only mode
[Link](state=[Link])
# To write to disabled widget:
[Link](state=[Link])
[Link]([Link], "...")
[Link](state=[Link])
[Link]()
root = [Link]()
# Checkbuttons
options = ["Python", "JavaScript", "Rust"]
vars_ = []
for opt in options:
var = [Link]()
vars_.append(var)
cb = [Link](root, text=opt, variable=var,
font=("Arial", 11),
command=lambda o=opt, v=var: print(f"{o}:
{[Link]()}"))
[Link](anchor="w", padx=20)
# Radiobuttons
lang_var = [Link](value="Python")
for lang in ["Python", "Go", "Rust"]:
rb = [Link](root, text=lang, variable=lang_var,
value=lang,
font=("Arial", 11),
command=lambda: print(lang_var.get()))
[Link](anchor="w", padx=20)
[Link]()
root = [Link]()
# Horizontal scale
volume = [Link](value=50)
scale = [Link](root, variable=volume,
from_=0, to=100,
orient=[Link],
length=300,
tickinterval=25,
label="Volume",
command=lambda v: print(f"Volume: {v}"))
[Link](pady=10)
# ttk Spinbox
spin_var = [Link](value=5)
spinbox = [Link](root, from_=1, to=100,
textvariable=spin_var,
width=10,
command=lambda: print(spin_var.get()))
[Link]()
# String spinbox
days = [Link](root, values=("Mon", "Tue", "Wed", "Thu", "Fri"),
width=8)
[Link]()
[Link]()
4. Advanced Widgets
Combobox (Dropdown)
import tkinter as tk
from tkinter import ttk
root = [Link]()
# Editable dropdown
selected = [Link]()
combo = [Link](root, textvariable=selected,
values=["Option A", "Option B", "Option C"],
state="readonly", # Prevent manual input;
remove for editable
width=25)
[Link]("Option A") # Default
[Link](padx=20, pady=10)
def on_select(event):
print(f"Selected: {[Link]()}")
[Link]("<<ComboboxSelected>>", on_select)
[Link]()
Listbox
import tkinter as tk
root = [Link]()
frame = [Link](root)
[Link](padx=10, pady=10)
listbox = [Link](frame,
selectmode=[Link], # SINGLE, BROWSE,
MULTIPLE, EXTENDED
font=("Arial", 11),
bg="#f5f5f5",
activestyle="dotbox",
height=8, width=30)
[Link](side=[Link])
# Selection handling
def on_select(event):
selection = [Link]()
selected = [[Link](i) for i in selection]
print(f"Selected: {selected}")
[Link]("<<ListboxSelect>>", on_select)
# Add / remove
[Link]([Link], "Mango")
[Link](0) # Remove first item
[Link]()
root = [Link]()
[Link]("700x400")
# Column headers
for col in columns:
[Link](col, text=[Link](),
command=lambda c=col: sort_column(tree, c, False))
# Insert data
data = [
(1, "Alice Johnson", 28, "New York"),
(2, "Bob Smith", 34, "London"),
(3, "Carol Williams", 22, "Bangalore"),
]
def delete_selected():
sel = [Link]()
if sel:
[Link](sel[0])
def edit_selected(new_values):
sel = [Link]()
if sel:
[Link](sel[0], values=new_values)
[Link]()
Notebook (Tabs)
import tkinter as tk
from tkinter import ttk
root = [Link]()
notebook = [Link](root)
[Link](fill=[Link], expand=True, padx=5, pady=5)
# Populate tabs
[Link](tab1, text="General Settings", font=("Arial",
14)).pack(pady=20)
[Link](tab2, text="Advanced Options", font=("Arial",
14)).pack(pady=20)
[Link]("<<NotebookTabChanged>>", on_tab_change)
# Programmatic switch
[Link](1) # Switch to tab index 1
[Link]()
Canvas — Drawing and Custom Widgets
import tkinter as tk
import math
root = [Link]()
# Shapes
canvas.create_rectangle(50, 50, 200, 150, fill="#3498db",
outline="#2980b9", width=2)
canvas.create_oval(220, 50, 370, 150, fill="#e74c3c", outline="")
canvas.create_line(50, 200, 400, 200, fill="#2ecc71", width=3,
dash=(10, 5))
canvas.create_polygon(250, 220, 200, 320, 300, 320,
fill="#f39c12", outline="#e67e22")
canvas.create_text(250, 360, text="Canvas Demo",
font=("Arial", 16, "bold"), fill="#333")
# Image on canvas
img = [Link](file="[Link]")
canvas.create_image(400, 100, image=img, anchor="nw")
# Click detection
def on_canvas_click(event):
items = canvas.find_overlapping(event.x-2, event.y-2, event.x+2,
event.y+2)
if items:
[Link](items[0], fill="purple")
[Link]("<Button-1>", on_canvas_click)
# Animation loop
def animate():
[Link](rect_id, 2, 0)
x1, _, x2, _ = [Link](rect_id)
if x2 > 500:
[Link](rect_id, -50, 100, 10, 160)
[Link](16, animate) # ~60fps
animate()
[Link]()
PanedWindow — Resizable Panes
import tkinter as tk
from tkinter import ttk
root = [Link]()
[Link]("700x500")
[Link](left, weight=1)
[Link](right, weight=3)
[Link](left, text="Sidebar").pack(pady=10)
[Link](right, text="Main Content").pack(pady=10)
[Link]()
root = [Link]()
st = ScrolledText(root, width=60, height=20, font=("Courier", 11))
[Link](padx=10, pady=10, fill=[Link], expand=True)
[Link]([Link], "This widget has a built-in scrollbar.")
[Link]()
root = [Link]()
style = [Link]()
# Style a Button
[Link]("[Link]",
background="#3498db",
foreground="white",
font=("Arial", 11, "bold"),
padding=(12, 6),
relief="flat",
borderwidth=0)
[Link]("[Link]",
background=[("active", "#2980b9"), ("disabled", "#bdc3c7")],
foreground=[("disabled", "#7f8c8d")])
# Style a Frame
[Link]("[Link]",
background="white",
relief="flat",
borderwidth=1)
# Style Treeview
[Link]("Treeview",
background="#ffffff",
fieldbackground="#ffffff",
rowheight=28,
font=("Arial", 10))
[Link]("[Link]",
background="#2c3e50",
foreground="white",
font=("Arial", 10, "bold"),
relief="flat")
[Link]("Treeview",
background=[("selected", "#3498db")],
foreground=[("selected", "white")])
# Usage
btn = [Link](root, text="Save", style="[Link]")
[Link]()
root = [Link]()
# Keyboard events
[Link]("<Return>", lambda e: print("Enter pressed"))
[Link]("<Escape>", lambda e: [Link]())
[Link]("<Control-s>", lambda e: save())
[Link]("<Control-z>", lambda e: undo())
[Link]("<F5>", lambda e: refresh())
# Mouse events
canvas = [Link](root, width=400, height=300, bg="white")
[Link]()
[Link]("<Button-1>", lambda e: print(f"Left click at {e.x},
{e.y}"))
[Link]("<Button-3>", lambda e: print("Right click"))
[Link]("<Double-Button-1>", lambda e: print("Double click"))
[Link]("<B1-Motion>", lambda e: print(f"Dragging to {e.x},
{e.y}"))
[Link]("<MouseWheel>", lambda e: print(f"Scroll delta:
{[Link]}"))
# Focus events
entry = [Link](root)
[Link]()
[Link]("<FocusIn>", lambda e: [Link](bg="#fffde7"))
[Link]("<FocusOut>", lambda e: [Link](bg="white"))
# Widget-level vs window-level
# [Link]() — only that widget
# [Link]() — root window only
# root.bind_all() — entire application
[Link]()
root = [Link]()
# Programmatic set
[Link]("John Doe")
print([Link]())
[Link]()
def update_clock():
now = [Link]().strftime("%H:%M:%S")
time_label.config(text=now)
[Link](1000, update_clock) # Schedule next call in 1000ms
update_clock()
[Link]()
root = [Link]()
# Message boxes
[Link]("Title", "Info message")
[Link]("Title", "Warning message")
[Link]("Title", "Error message")
result = [Link]("Confirm", "Are you sure?") # Returns
bool
result = [Link]("Confirm", "Proceed?")
# Simple input
name = [Link]("Input", "What is your name?",
parent=root)
age = [Link]("Input", "Your age:", minvalue=1,
maxvalue=120)
# File dialogs
path = [Link](
title="Open File",
initialdir="/home",
filetypes=[("Text files", "*.txt"), ("Python files", "*.py"),
("All files", "*.*")]
)
paths = [Link](...) # Multiple files
save_path = [Link](defaultextension=".txt")
folder = [Link](title="Select Folder")
# Color chooser
color = [Link](title="Pick a color",
initialcolor="#3498db")
# Returns: ((r, g, b), "#rrggbb") or (None, None) if cancelled
if color[1]:
print(f"Hex: {color[1]}")
Menu Bar
import tkinter as tk
root = [Link]()
menubar = [Link](root)
[Link](menu=menubar)
# File menu
file_menu = [Link](menubar, tearoff=0)
menubar.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="New", command=lambda: print("New"),
accelerator="Ctrl+N")
file_menu.add_command(label="Open...", command=lambda:
print("Open"), accelerator="Ctrl+O")
file_menu.add_separator()
file_menu.add_command(label="Exit", command=[Link])
# Edit menu
edit_menu = [Link](menubar, tearoff=0)
menubar.add_cascade(label="Edit", menu=edit_menu)
edit_menu.add_command(label="Undo", command=lambda: print("Undo"),
accelerator="Ctrl+Z")
edit_menu.add_command(label="Redo", command=lambda: print("Redo"),
accelerator="Ctrl+Y")
# Submenu
theme_menu = [Link](view_menu, tearoff=0)
view_menu.add_cascade(label="Theme", menu=theme_menu)
for t in ["Light", "Dark", "System"]:
theme_menu.add_command(label=t, command=lambda x=t: print(f"Theme:
{x}"))
[Link]()
def show_context(event):
context_menu.tk_popup(event.x_root, event.y_root)
[Link]("<Button-3>", show_context)
[Link]()
import tkinter as tk
import threading
from PIL import Image, ImageDraw
import pystray
class TrayApp:
def __init__(self):
[Link] = [Link]()
[Link]("Tray App")
[Link]("WM_DELETE_WINDOW", self.hide_window)
self.tray_icon = self._create_tray_icon()
tray_thread = [Link](target=self.tray_icon.run,
daemon=True)
tray_thread.start()
def _create_icon_image(self):
img = [Link]("RGB", (64, 64), "#3498db")
d = [Link](img)
[Link]([16, 16, 48, 48], fill="white")
return img
def _create_tray_icon(self):
menu = [Link](
[Link]("Show", self.show_window, default=True),
[Link]("Quit", self.quit_app)
)
return [Link]("myapp", self._create_icon_image(), "My
App", menu)
def hide_window(self):
[Link]()
def show_window(self):
[Link](0, [Link])
def quit_app(self):
self.tray_icon.stop()
[Link](0, [Link])
def run(self):
[Link]()
TrayApp().run()
class App([Link]):
def __init__(self):
super().__init__()
[Link]("Threading Demo")
[Link]("400x200")
[Link] = [Link]()
[Link](100, self.poll_queue)
def start_task(self):
[Link](state=[Link])
thread = [Link](target=self.background_task,
daemon=True)
[Link]()
def background_task(self):
"""Runs in background thread — never touch widgets here."""
for i in range(1, 11):
[Link](0.5) # Simulate
work
[Link](("progress", i * 10, f"Processing step
{i}/10"))
[Link](("done", 100, "Task complete!"))
def poll_queue(self):
"""Drains queue on main thread — safe to update widgets
here."""
try:
while True:
msg_type, value, text = [Link].get_nowait()
[Link]["value"] = value
[Link](text=text)
if msg_type == "done":
[Link](state=[Link])
except [Link]:
pass
[Link](100, self.poll_queue)
App().mainloop()
class App([Link]):
def __init__(self):
super().__init__()
self.result_var = [Link](value="...")
[Link](self, textvariable=self.result_var).pack(pady=20)
[Link](self, text="Fetch Data", command=[Link]).pack()
def fetch(self):
self.result_var.set("Loading...")
future = [Link](self._do_request)
[Link](100, lambda: self._check_future(future))
def _do_request(self):
r = [Link]("[Link]
return [Link]().get("current_user_url", "N/A")
App().mainloop()
class AsyncApp([Link]):
def __init__(self):
super().__init__()
[Link] = asyncio.new_event_loop()
self._start_async_loop()
def _start_async_loop(self):
def run():
asyncio.set_event_loop([Link])
[Link].run_forever()
[Link](target=run, daemon=True).start()
def run_async_task(self):
asyncio.run_coroutine_threadsafe(self._async_work(),
[Link])
AsyncApp().mainloop()
DB_PATH = "[Link]"
def init_db():
conn = [Link](DB_PATH)
[Link]("""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER DEFAULT 0
)
""")
[Link]()
[Link]()
def get_tasks():
conn = [Link](DB_PATH)
rows = [Link]("SELECT id, title, done FROM
tasks").fetchall()
[Link]()
return rows
init_db()
# Wire into a UI
root = [Link]()
tree = [Link](root, columns=("id", "title", "done"),
show="headings")
for c in ("id", "title", "done"):
[Link](c, text=[Link]())
[Link](fill=[Link], expand=True)
def refresh():
[Link](*tree.get_children())
for row in get_tasks():
[Link]("", [Link], values=row)
refresh()
[Link]()
CONFIG_PATH = "[Link]"
def load_config():
cfg = [Link]()
if [Link](CONFIG_PATH):
[Link](CONFIG_PATH)
else:
cfg["appearance"] = {"theme": "light", "font_size": "12"}
cfg["window"] = {"width": "800", "height": "600"}
save_config(cfg)
return cfg
def save_config(cfg):
with open(CONFIG_PATH, "w") as f:
[Link](f)
config = load_config()
theme = [Link]("appearance", "theme", fallback="light")
[Link]("appearance", "theme", "dark")
save_config(config)
import tkinter as tk
from tkinterweb import HtmlFrame
root = [Link]()
[Link]("HTML App")
[Link]("900x700")
frame.on_link_click = on_link_click
[Link]()
import webview
import threading
import json
class API:
"""Python methods callable from JavaScript via
[Link]"""
def get_data(self):
return {"message": "Hello from Python!", "version": "1.0"}
def show_dialog(self):
# Runs on separate thread — post to main thread if needed
result =
[Link][0].create_file_dialog(webview.OPEN_DIALOG)
return result[0] if result else None
api = API()
html = """
<!DOCTYPE html>
<html>
<head>
<title>pywebview App</title>
<style>
body { font-family: 'Segoe UI', sans-serif; padding: 30px;
background: #f5f5f5; }
button { padding: 10px 20px; background: #2196f3; color: white;
border: none; border-radius: 5px; cursor: pointer;
margin: 5px; }
#output { background: white; padding: 16px; border-radius: 8px;
margin-top: 20px; min-height: 60px; }
</style>
</head>
<body>
<h1>pywebview Demo</h1>
<button onclick="getData()">Get Data from Python</button>
<button onclick="openFile()">Open File</button>
<div id="output">Output appears here...</div>
<script>
async function getData() {
const result = await [Link].get_data();
[Link]('output').textContent =
[Link](result, null, 2);
}
# Create window
window = webview.create_window(
title="My App",
html=html,
js_api=api,
width=900,
height=700,
resizable=True,
frameless=False,
on_top=False
)
[Link](debug=False)
def main():
[Link] = [Link]
settings = {"windowless_rendering_enabled": False}
[Link](settings)
root = [Link]()
[Link]("CEF Browser")
[Link]("1000x700")
# Embed browser
browser_frame = [Link](root)
browser_frame.pack(fill=[Link], expand=True)
[Link]()
window_info = [Link]()
rect = [0, 0, browser_frame.winfo_width(),
browser_frame.winfo_height()]
window_info.SetAsChild(browser_frame.winfo_id(), rect)
browser = [Link](window_info,
url="[Link]
[Link]()
[Link]()
if __name__ == "__main__":
main()
class LocalServer([Link]):
def __init__(self, directory):
super().__init__(daemon=True)
[Link] = directory
def run(self):
[Link]([Link])
server = HTTPServer(("localhost", PORT),
SimpleHTTPRequestHandler)
server.serve_forever()
[project]
name = "my-gui-app"
version = "1.0.0"
description = "A tkinter GUI application"
requires-python = ">=3.10"
dependencies = [
"ttkbootstrap>=1.10",
"requests>=2.31",
"Pillow>=10.0"
]
[[Link]]
my-app = "my_app.main:main" # Installs a CLI entry point
Basic usage:
# One-folder bundle
pyinstaller [Link] --name "MyApp" --icon assets/[Link]
a = Analysis(
["[Link]"],
pathex=["."],
binaries=[],
datas=[
("assets/", "assets/"), # Include assets folder
("[Link]", "."), # Include config file
],
hiddenimports=[
"tkinter",
"ttkbootstrap",
"PIL._tkinter_finder",
],
hookspath=[],
runtime_hooks=[],
excludes=["pytest", "unittest"],
noarchive=False,
)
pyz = PYZ([Link])
exe = EXE(
pyz, [Link], [Link], [Link], [Link],
name="MyApp",
debug=False,
console=False, # False = no terminal window
icon="assets/[Link]",
upx=True, # Compress (requires UPX installed)
)
pyinstaller [Link]
Common PyInstaller gotchas: - Hidden imports for dynamic importlib usage — add to
hiddenimports - Data files (images, CSS, templates) need to be listed in datas - Use
sys._MEIPASS to get the bundle path at runtime:
import sys, os
def resource_path(relative):
"""Get absolute path to resource — works for dev and
PyInstaller."""
base = getattr(sys, "_MEIPASS", [Link]("."))
return [Link](base, relative)
icon_path = resource_path("assets/[Link]")
Method 3: cx_Freeze
Good alternative to PyInstaller, especially on Linux.
pip install cx_Freeze
[Link]:
from cx_Freeze import setup, Executable
import sys
build_exe_options = {
"packages": ["tkinter", "ttkbootstrap"],
"include_files": [("assets/", "assets/")],
"excludes": ["unittest", "pytest"],
}
setup(
name="MyApp",
version="1.0",
description="My tkinter App",
options={"build_exe": build_exe_options},
executables=[
Executable(
"[Link]",
base=base,
target_name="MyApp",
icon="assets/[Link]",
)
]
)
Flag
--standalone
--onefile
--enable-plugin=tk-inter
--enable-plugin=numpy
--include-data-dir=src=dst
--windows-disable-console
--lto=yes
Method 5: briefcase (cross-platform, app store ready)
pip install briefcase
briefcase new # Interactive project setup
briefcase dev # Run in dev mode
briefcase build # Build for current platform
briefcase package # Create installer/dmg/deb
briefcase run # Run the built app
Comparison Table
Method Platforms Output Startup Skill Level Best For
pip/PyPI All Package n/a Low Dev tools,
libraries
PyInstalle Win/Mac/ Folder Medium Low Most apps
r Linux or .exe
cx_Freeze Win/Mac/ Folder Medium Medium Windows
Linux installers
Nuitka Win/Mac/ Native Fast Medium-High Performance
Linux binary , obfuscation
briefcase Win/Mac/ Native Medium Medium App store
Linux/iOS/ app distribution
Android
DB_PATH = "[Link]"
def init_db():
with [Link](DB_PATH) as conn:
[Link]("""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
prio TEXT DEFAULT 'Medium',
done INTEGER DEFAULT 0,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
def db_get_all():
with [Link](DB_PATH) as conn:
return [Link](
"SELECT id, title, prio, done FROM tasks ORDER BY ts DESC"
).fetchall()
def db_delete(task_id):
with [Link](DB_PATH) as conn:
[Link]("DELETE FROM tasks WHERE id = ?", (task_id,))
def db_toggle(task_id):
with [Link](DB_PATH) as conn:
[Link]("UPDATE tasks SET done = 1 - done WHERE id = ?",
(task_id,))
class TaskManagerApp([Link]):
def __init__(self):
super().__init__()
[Link]("Task Manager")
[Link]("750x520")
[Link](500, 350)
self._setup_style()
self._build_ui()
self._queue = [Link]()
init_db()
self.refresh_list()
[Link](200, self._poll_queue)
def _setup_style(self):
style = [Link](self)
style.theme_use("clam")
BG = "#f0f2f5"
ACCENT = "#4f46e5"
[Link](bg=BG)
[Link]("TFrame", background=BG)
[Link]("TLabel", background=BG, font=("Segoe UI",
10))
[Link]("[Link]", font=("Segoe UI", 18, "bold"),
foreground=ACCENT, background=BG)
[Link]("[Link]",
background=ACCENT, foreground="white",
font=("Segoe UI", 10, "bold"),
padding=(10, 6), relief="flat", borderwidth=0)
[Link]("[Link]",
background=[("active", "#4338ca"), ("disabled",
"#a5b4fc")])
[Link]("[Link]",
background="#ef4444", foreground="white",
font=("Segoe UI", 10), padding=(10, 6),
relief="flat")
[Link]("[Link]", background=[("active",
"#dc2626")])
[Link]("Treeview", rowheight=32, font=("Segoe UI",
10),
background="white", fieldbackground="white")
[Link]("[Link]", font=("Segoe UI", 10,
"bold"),
background="#e0e7ff", foreground="#312e81")
[Link]("Treeview", background=[("selected", ACCENT)],
foreground=[("selected", "white")])
def _build_ui(self):
# ── Header
header = [Link](self, padding=(20, 15, 20, 10))
[Link](fill=tk.X)
[Link](header, text="Task Manager",
style="[Link]").pack(side=[Link])
self.count_label = [Link](header, text="", font=("Segoe
UI", 9),
foreground="#6b7280")
self.count_label.pack(side=[Link], padx=12)
self.task_var = [Link]()
entry = [Link](add_bar, textvariable=self.task_var,
font=("Segoe UI", 11))
[Link](row=0, column=0, sticky="ew", ipady=5, padx=(0, 8))
[Link]("<Return>", lambda e: self.add_task())
self.prio_var = [Link](value="Medium")
prio_combo = [Link](add_bar, textvariable=self.prio_var,
values=["High", "Medium", "Low"],
state="readonly", width=9)
prio_combo.grid(row=0, column=1, padx=(0, 8))
# ── Treeview
tree_frame = [Link](self, padding=(20, 0, 20, 10))
tree_frame.pack(fill=[Link], expand=True)
tree_frame.columnconfigure(0, weight=1)
tree_frame.rowconfigure(0, weight=1)
[Link].tag_configure("done", foreground="#9ca3af")
[Link].tag_configure("high", foreground="#dc2626")
[Link].tag_configure("medium", foreground="#d97706")
[Link].tag_configure("low", foreground="#16a34a")
[Link]("<Double-1>", lambda e: self.toggle_task())
# ── Action Buttons
btn_bar = [Link](self, padding=(20, 0, 20, 15))
btn_bar.pack(fill=tk.X)
[Link](btn_bar, text="✓ Toggle Done",
style="[Link]",
command=self.toggle_task).pack(side=[Link])
[Link](btn_bar, text="✕ Delete",
style="[Link]",
command=self.delete_task).pack(side=[Link], padx=8)
[Link](btn_bar, text="⟳ Refresh",
command=self.refresh_list).pack(side=[Link])
# ── Status Bar
self.status_var = [Link](value="Ready")
status_bar = [Link](self, textvariable=self.status_var,
relief=[Link], anchor="w",
font=("Segoe UI", 9),
foreground="#6b7280")
status_bar.pack(fill=tk.X, side=[Link])
def refresh_list(self):
"""Load tasks from DB in background thread."""
self.status_var.set("Loading...")
[Link](target=self._load_tasks, daemon=True).start()
def _load_tasks(self):
rows = db_get_all()
self._queue.put(("refresh", rows))
def add_task(self):
title = self.task_var.get().strip()
if not title:
[Link]("Input Error", "Please enter a task
title.")
return
prio = self.prio_var.get()
self.task_var.set("")
[Link](target=self._add_task_bg, args=(title, prio),
daemon=True).start()
def toggle_task(self):
sel = [Link]()
if not sel:
return
task_id = int(sel[0])
[Link](target=self._toggle_bg, args=(task_id,),
daemon=True).start()
def delete_task(self):
sel = [Link]()
if not sel:
return
task_id = int(sel[0])
title = [Link](sel[0], "values")[1]
if [Link]("Confirm", f'Delete task "{title}"?'):
[Link](target=self._delete_bg, args=(task_id,),
daemon=True).start()
def _poll_queue(self):
try:
while True:
msg = self._queue.get_nowait()
if msg[0] == "refresh":
self._render_tasks(msg[1])
elif msg[0] == "status":
self.status_var.set(msg[1])
except [Link]:
pass
[Link](100, self._poll_queue)
if __name__ == "__main__":
app = TaskManagerApp()
[Link]()