Chuyển tới nội dung chính

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ệnCổ điển, đơn giảnNative, hiện đại
Tùy chỉnhTrực tiếp qua tham sốQua Style/Theme
WidgetĐầy đủSubset nhưng đẹp hơn
Cross-platformGiống nhau mọi OSTheo 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

  1. Widget ttk.Notebook dù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
  2. 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
  3. Theme "clam" có sẵn trong?

    • A. Cần cài pip
    • B. Tkinter ttk
    • C. PyQt
    • D. wxPython

Thực hành

  1. 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.
  2. 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.
  3. Tạo bảng style button: 3 nút (Primary, Success, Danger) theo style tùy chỉnh hoàn chỉnh.
  4. (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