Bài 8: ttk & Style (Giao Diện Đẹp)
Giai đoạn 3 – Dự Án Thực Tế · Mục tiêu: Dùng ttk để tạo giao diện chuyên nghiệp hơn, tùy chỉnh Style và áp dụng Theme.
1. tk vs ttk — Sự khác biệt
| Tiêu chí | tkinter (tk) | tkinter.ttk |
|---|---|---|
| Giao diện | Cổ điển, đơn giản | Native, hiện đại |
| Tùy chỉnh | Trực tiếp qua tham số | Qua Style/Theme |
| Widget | Đầy đủ | Subset nhưng đẹp hơn |
| Cross-platform | Giống nhau mọi OS | Theo native của OS |
Nguyên tắc: Dùng ttk cho các widget phổ biến (Button, Label, Entry, Frame, Combobox...), dùng tk khi cần vẽ tùy chỉnh (Canvas, Text, Listbox).
2. Các ttk widget cơ bản
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("ttk Widget Demo")
root.geometry("400x500")
# ttk Button
ttk.Button(root, text="ttk Button").pack(pady=5)
# ttk Label
ttk.Label(root, text="ttk Label", font=("Arial", 12)).pack(pady=5)
# ttk Entry
ttk.Entry(root, width=25).pack(pady=5)
# ttk Combobox
ttk.Combobox(root, values=["Hà Nội", "HCM", "Đà Nẵng"]).pack(pady=5)
# ttk Checkbutton
ttk.Checkbutton(root, text="Đồng ý điều khoản").pack(pady=5)
# ttk Radiobutton
var = tk.StringVar(value="a")
ttk.Radiobutton(root, text="Lựa chọn A", variable=var, value="a").pack(pady=2)
ttk.Radiobutton(root, text="Lựa chọn B", variable=var, value="b").pack(pady=2)
# ttk Scale (thanh trượt)
ttk.Scale(root, from_=0, to=100, orient="horizontal").pack(pady=5, fill="x", padx=20)
# ttk Progressbar
progress = ttk.Progressbar(root, mode="determinate", value=65)
progress.pack(pady=5, fill="x", padx=20)
# ttk Separator (đường phân cách)
ttk.Separator(root, orient="horizontal").pack(fill="x", padx=10, pady=10)
# ttk Notebook (tab)
notebook = ttk.Notebook(root)
notebook.pack(fill="both", expand=True, padx=10, pady=5)
tab1 = ttk.Frame(notebook)
tab2 = ttk.Frame(notebook)
notebook.add(tab1, text="Tab 1")
notebook.add(tab2, text="Tab 2")
ttk.Label(tab1, text="Nội dung tab 1").pack(expand=True)
ttk.Label(tab2, text="Nội dung tab 2").pack(expand=True)
root.mainloop()
3. ttk.Notebook — Giao diện nhiều Tab
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Ứng dụng nhiều Tab")
root.geometry("500x350")
notebook = ttk.Notebook(root)
notebook.pack(fill="both", expand=True, padx=10, pady=10)
# ── Tab Thông tin ─────────────────────────────────────────
tab_info = ttk.Frame(notebook)
notebook.add(tab_info, text=" Thông tin")
for row, (label, widget_type) in enumerate([
("Họ tên:", "entry"),
("Email:", "entry"),
("Điện thoại:", "entry"),
]):
ttk.Label(tab_info, text=label).grid(row=row, column=0, padx=10, pady=8, sticky="e")
ttk.Entry(tab_info, width=25).grid(row=row, column=1, padx=10, pady=8, sticky="ew")
tab_info.columnconfigure(1, weight=1)
# ── Tab Cài đặt ──────────────────────────────────────────
tab_settings = ttk.Frame(notebook)
notebook.add(tab_settings, text=" Cài đặt")
theme_var = tk.StringVar(value="Tối")
ttk.Label(tab_settings, text="Chủ đề:").pack(pady=10)
for text in ["Sáng ", "Tối ", "Hệ thống"]:
ttk.Radiobutton(tab_settings, text=text, variable=theme_var,
value=text.split()[0]).pack(anchor="w", padx=30)
# ── Tab Thống kê ─────────────────────────────────────────
tab_stats = ttk.Frame(notebook)
notebook.add(tab_stats, text=" Thống kê")
for i, (label, val) in enumerate([("Tổng bản ghi", 128), ("Hoàn thành", 94), ("Đang xử lý", 34)]):
ttk.Label(tab_stats, text=f"{label}:", font=("Arial", 10)).grid(row=i, column=0, padx=15, pady=8, sticky="w")
ttk.Label(tab_stats, text=str(val), font=("Arial", 12, "bold")).grid(row=i, column=1, padx=10, pady=8)
bar = ttk.Progressbar(tab_stats, value=int(val/128*100), mode="determinate", length=150)
bar.grid(row=i, column=2, padx=10, pady=8)
# Sự kiện khi chuyển tab
def on_tab_change(event):
tab = notebook.tab(notebook.select(), "text")
root.title(f"App — {tab}")
notebook.bind("<<NotebookTabChanged>>", on_tab_change)
root.mainloop()
4. ttk.Treeview — Bảng dữ liệu
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Danh sách sinh viên")
root.geometry("600x350")
# Tạo Treeview
columns = ("ma_sv", "ho_ten", "lop", "diem")
tree = ttk.Treeview(root, columns=columns, show="headings", height=10)
# Đặt tiêu đề cột
tree.heading("ma_sv", text="Mã SV")
tree.heading("ho_ten", text="Họ tên")
tree.heading("lop", text="Lớp")
tree.heading("diem", text="Điểm TB")
# Đặt chiều rộng cột
tree.column("ma_sv", width=80, anchor="center")
tree.column("ho_ten", width=180)
tree.column("lop", width=100, anchor="center")
tree.column("diem", width=80, anchor="center")
# Thêm dữ liệu
students = [
("SV001", "Nguyễn Văn An", "CNTT-01", 8.5),
("SV002", "Trần Thị Bích", "CNTT-01", 9.2),
("SV003", "Lê Văn Cường", "CNTT-02", 7.8),
("SV004", "Phạm Thị Dung", "CNTT-02", 8.0),
("SV005", "Hoàng Văn Em", "CNTT-03", 6.5),
]
for sv in students:
tree.insert("", tk.END, values=sv)
# Màu xen kẽ (zebra stripes)
tree.tag_configure("odd", background="#F2F2F2")
tree.tag_configure("even", background="white")
for i, item in enumerate(tree.get_children()):
tree.item(item, tags=("odd" if i % 2 else "even",))
# Scrollbar
sb = ttk.Scrollbar(root, orient="vertical", command=tree.yview)
tree.configure(yscrollcommand=sb.set)
tree.pack(side="left", fill="both", expand=True, padx=(10,0), pady=10)
sb.pack(side="right", fill="y", pady=10, padx=(0,10))
# Sự kiện chọn row
def on_select(event):
sel = tree.selection()
if sel:
values = tree.item(sel[0])["values"]
print(f"Chọn: {values}")
tree.bind("<<TreeviewSelect>>", on_select)
root.mainloop()
5. ttk.Style — Tùy chỉnh giao diện
5.1. Khái niệm Style
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry("400x300")
style = ttk.Style()
# Xem danh sách theme có sẵn
print(style.theme_names()) # ('winnative', 'clam', 'alt', 'default', 'classic', ...)
# Áp dụng theme
style.theme_use("clam") # Hoặc 'alt', 'default', 'classic'
root.mainloop()
5.2. Tùy chỉnh widget với Style
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry("400x350")
style = ttk.Style()
style.theme_use("clam")
# Tạo style tùy chỉnh cho Button
style.configure(
"Primary.TButton", # Tên style: CustomName.WidgetType
font=("Arial", 11, "bold"),
foreground="white",
background="#3498DB",
borderwidth=0,
focusthickness=3,
focuscolor="none",
padding=(15, 8)
)
style.map("Primary.TButton",
background=[("active", "#2980B9"), ("disabled", "#BDC3C7")],
foreground=[("disabled", "#7F8C8D")]
)
style.configure("Danger.TButton",
font=("Arial", 11, "bold"),
foreground="white",
background="#E74C3C",
padding=(15, 8)
)
style.map("Danger.TButton",
background=[("active", "#CB4335")]
)
style.configure("Success.TButton",
font=("Arial", 11, "bold"),
foreground="white",
background="#27AE60",
padding=(15, 8)
)
style.map("Success.TButton",
background=[("active", "#229954")]
)
# Tùy chỉnh Label
style.configure("Title.TLabel",
font=("Arial", 18, "bold"),
foreground="#2C3E50"
)
style.configure("Subtitle.TLabel",
font=("Arial", 11),
foreground="#7F8C8D"
)
# Tùy chỉnh Frame
style.configure("Card.TFrame",
background="white",
relief="raised",
borderwidth=1
)
# Áp dụng
ttk.Label(root, text="Dashboard", style="Title.TLabel").pack(pady=15)
ttk.Label(root, text="Chào mừng bạn quay trở lại!", style="Subtitle.TLabel").pack()
frame = ttk.Frame(root, style="Card.TFrame", padding=20)
frame.pack(padx=20, pady=15, fill="x")
ttk.Button(frame, text="Xác nhận", style="Primary.TButton").pack(side="left", padx=5)
ttk.Button(frame, text="Xóa", style="Danger.TButton").pack(side="left", padx=5)
ttk.Button(frame, text="Lưu", style="Success.TButton").pack(side="left", padx=5)
root.mainloop()
5.3. Tùy chỉnh Progressbar
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry("400x200")
style = ttk.Style()
style.theme_use("clam")
style.configure("Green.Horizontal.TProgressbar",
troughcolor="#ECF0F1",
background="#27AE60",
thickness=20
)
style.configure("Blue.Horizontal.TProgressbar",
troughcolor="#ECF0F1",
background="#3498DB",
thickness=20
)
var1 = tk.IntVar(value=70)
var2 = tk.IntVar(value=40)
ttk.Label(root, text="Hoàn thành: 70%").pack(pady=5)
ttk.Progressbar(root, variable=var1, style="Green.Horizontal.TProgressbar",
maximum=100).pack(fill="x", padx=20, pady=3)
ttk.Label(root, text="Đang xử lý: 40%").pack(pady=5)
ttk.Progressbar(root, variable=var2, style="Blue.Horizontal.TProgressbar",
maximum=100).pack(fill="x", padx=20, pady=3)
# Tiến trình indeterminate (không biết %%)
ttk.Label(root, text="Đang tải...").pack(pady=5)
loading = ttk.Progressbar(root, mode="indeterminate")
loading.pack(fill="x", padx=20, pady=3)
loading.start(10)
root.mainloop()
6. Giao diện tối (Dark Mode) tự xây
import tkinter as tk
from tkinter import ttk
DARK = {
"bg": "#1E1E2E",
"surface": "#2A2A3E",
"primary": "#7C3AED",
"text": "#CDD6F4",
"muted": "#6C7086",
"success": "#A6E3A1",
"danger": "#F38BA8",
"border": "#45475A",
}
root = tk.Tk()
root.title("Dark Mode App")
root.geometry("480x400")
root.configure(bg=DARK["bg"])
style = ttk.Style()
style.theme_use("clam")
# Cấu hình style tổng thể
style.configure(".",
background=DARK["bg"],
foreground=DARK["text"],
fieldbackground=DARK["surface"],
selectbackground=DARK["primary"],
selectforeground=DARK["text"],
bordercolor=DARK["border"],
darkcolor=DARK["surface"],
lightcolor=DARK["surface"],
troughcolor=DARK["surface"],
font=("Segoe UI", 10)
)
style.configure("TEntry",
fieldbackground=DARK["surface"],
foreground=DARK["text"],
insertcolor=DARK["text"],
bordercolor=DARK["border"]
)
style.configure("TButton",
background=DARK["primary"],
foreground=DARK["text"],
borderwidth=0,
padding=(12, 6)
)
style.map("TButton", background=[("active", "#6D28D9")])
style.configure("TLabel", background=DARK["bg"], foreground=DARK["text"])
style.configure("TFrame", background=DARK["bg"])
# ── UI ────────────────────────────────────────────────────
tk.Label(root, text=" Dark Mode Login",
font=("Segoe UI", 18, "bold"),
bg=DARK["bg"], fg=DARK["text"]).pack(pady=25)
card = tk.Frame(root, bg=DARK["surface"], padx=30, pady=25)
card.pack(padx=40, fill="x")
for label, show in [("Email", ""), ("Mật khẩu", "*")]:
tk.Label(card, text=label, bg=DARK["surface"], fg=DARK["muted"],
font=("Segoe UI", 9)).pack(anchor="w", pady=(8,2))
e = tk.Entry(card, show=show, bg=DARK["surface"], fg=DARK["text"],
insertbackground=DARK["text"],
relief="flat", highlightthickness=1,
highlightbackground=DARK["border"],
highlightcolor=DARK["primary"],
font=("Segoe UI", 11))
e.pack(fill="x", ipady=6)
ttk.Button(root, text="Đăng nhập").pack(pady=20, ipadx=20, ipady=4)
tk.Label(root, text="Chưa có tài khoản? Đăng ký ngay",
bg=DARK["bg"], fg=DARK["primary"],
cursor="hand2", font=("Segoe UI", 9, "underline")).pack()
root.mainloop()
Bài tập thực hành
Trắc nghiệm
-
Widget
ttk.Notebookdùng để làm gì?- A. Hiển thị dữ liệu dạng bảng
- B. Tạo giao diện nhiều tab
- C. Tạo thanh cuộn
- D. Tạo thanh tiến trình
-
style.map()dùng để làm gì?- A. Đặt style cho một widget
- B. Đặt style theo trạng thái (active, disabled...)
- C. Copy style từ widget này sang widget khác
- D. Xóa style
-
Theme "clam" có sẵn trong?
- A. Cần cài pip
- B. Tkinter ttk
- C. PyQt
- D. wxPython
Thực hành
- Tạo app "Dashboard" với ttk.Notebook: 3 tab (Tổng quan, Chi tiết, Báo cáo), mỗi tab có style khác nhau.
- Tạo Treeview danh sách sản phẩm (Mã, Tên, Giá, Số lượng), có thể click để chọn và hiển thị chi tiết.
- Tạo bảng style button: 3 nút (Primary, Success, Danger) theo style tùy chỉnh hoàn chỉnh.
- (Nâng cao) Tạo "Settings Panel" cho phép người dùng chọn theme (clam/alt/default) và áp dụng ngay lập tức toàn app.
Bài trước: Bài 7 – Canvas & Đồ Họa · Bài tiếp: Bài 9 – OOP với Tkinter