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

Bài 5: Widget Nâng Cao

Giai đoạn 2 – Widget Nâng Cao · Mục tiêu: Thành thạo Listbox, Combobox, Checkbutton, Radiobutton, Scale và Spinbox để xây dựng form phong phú.


1. Checkbutton — Ô chọn nhiều

1.1. Cơ bản

import tkinter as tk

root = tk.Tk()
root.geometry("300x250")

# Biến lưu trạng thái
var_python = tk.BooleanVar()
var_java = tk.BooleanVar(value=True) # Mặc định tick
var_js = tk.BooleanVar()

tk.Label(root, text="Ngôn ngữ bạn biết:", font=("Arial", 11, "bold")).pack(pady=10)

tk.Checkbutton(root, text="Python ", variable=var_python).pack(anchor="w", padx=30)
tk.Checkbutton(root, text="Java ", variable=var_java).pack(anchor="w", padx=30)
tk.Checkbutton(root, text="JavaScript ", variable=var_js).pack(anchor="w", padx=30)

def show_result():
langs = []
if var_python.get(): langs.append("Python")
if var_java.get(): langs.append("Java")
if var_js.get(): langs.append("JavaScript")
print("Bạn biết:", ", ".join(langs) if langs else "Không có")

tk.Button(root, text="Xác nhận", command=show_result).pack(pady=15)
root.mainloop()

1.2. Checkbutton với trace (tự động phản hồi)

import tkinter as tk

root = tk.Tk()
root.geometry("350x200")

var = tk.BooleanVar()
label = tk.Label(root, text="", font=("Arial", 13))
label.pack(pady=20)

def on_change(*args):
if var.get():
label.config(text=" Đã đồng ý điều khoản", fg="#27AE60")
else:
label.config(text=" Chưa đồng ý", fg="#E74C3C")

var.trace_add("write", on_change)

tk.Checkbutton(root, text="Tôi đồng ý với điều khoản sử dụng",
variable=var, font=("Arial", 11)).pack()

root.mainloop()

2. Radiobutton — Chọn một trong nhiều

2.1. Cơ bản

import tkinter as tk

root = tk.Tk()
root.geometry("300x250")

# Một IntVar (hoặc StringVar) dùng chung cho tất cả Radiobutton trong nhóm
gender = tk.StringVar(value="nam")

tk.Label(root, text="Giới tính:", font=("Arial", 11, "bold")).pack(pady=10)

tk.Radiobutton(root, text="Nam ", variable=gender, value="nam").pack(anchor="w", padx=30)
tk.Radiobutton(root, text="Nữ ", variable=gender, value="nu").pack(anchor="w", padx=30)
tk.Radiobutton(root, text="Khác ", variable=gender, value="khac").pack(anchor="w", padx=30)

def show():
print("Giới tính:", gender.get())

tk.Button(root, text="Xác nhận", command=show).pack(pady=15)
root.mainloop()

2.2. Nhiều nhóm Radiobutton

import tkinter as tk

root = tk.Tk()
root.geometry("350x300")

# Mỗi nhóm dùng một biến riêng
level = tk.StringVar(value="trung_binh")
lang = tk.StringVar(value="python")

# Nhóm 1: Trình độ
frame1 = tk.LabelFrame(root, text="Trình độ", padx=10, pady=5)
frame1.pack(fill="x", padx=15, pady=8)

for text, val in [("Cơ bản", "co_ban"), ("Trung bình", "trung_binh"), ("Nâng cao", "nang_cao")]:
tk.Radiobutton(frame1, text=text, variable=level, value=val).pack(anchor="w")

# Nhóm 2: Ngôn ngữ
frame2 = tk.LabelFrame(root, text="Ngôn ngữ", padx=10, pady=5)
frame2.pack(fill="x", padx=15, pady=8)

for text, val in [("Python ", "python"), ("JavaScript ", "js"), ("Java ", "java")]:
tk.Radiobutton(frame2, text=text, variable=lang, value=val).pack(anchor="w")

def show():
print(f"Trình độ: {level.get()} | Ngôn ngữ: {lang.get()}")

tk.Button(root, text="Xác nhận", command=show).pack(pady=10)
root.mainloop()

3. Listbox — Danh sách lựa chọn

3.1. Cơ bản

import tkinter as tk

root = tk.Tk()
root.geometry("350x300")

tk.Label(root, text="Chọn môn học:", font=("Arial", 11, "bold")).pack(pady=8)

# Tạo Listbox
listbox = tk.Listbox(
root,
height=6,
font=("Arial", 11),
selectmode="single", # single / multiple / browse / extended
activestyle="dotbox"
)
listbox.pack(padx=15, fill="x")

# Thêm item
for mon in ["Toán", "Lý", "Hóa", "Sinh", "Văn", "Sử", "Địa", "Tin học"]:
listbox.insert(tk.END, mon)

def on_select(event):
indices = listbox.curselection() # Tuple các index được chọn
if indices:
selected = [listbox.get(i) for i in indices]
print("Đã chọn:", selected)

listbox.bind("<<ListboxSelect>>", on_select)
root.mainloop()

3.2. Listbox với Scrollbar & thao tác CRUD

import tkinter as tk
from tkinter import simpledialog, messagebox

root = tk.Tk()
root.title("Danh sách sinh viên")
root.geometry("400x400")

# Frame listbox
frame = tk.Frame(root)
frame.pack(fill="both", expand=True, padx=10, pady=10)

scrollbar = tk.Scrollbar(frame)
scrollbar.pack(side="right", fill="y")

listbox = tk.Listbox(frame, yscrollcommand=scrollbar.set, font=("Arial", 11),
selectmode="single")
listbox.pack(side="left", fill="both", expand=True)
scrollbar.config(command=listbox.yview)

for sv in ["Nguyễn Văn An", "Trần Thị Bích", "Lê Văn Cường", "Phạm Thị Dung"]:
listbox.insert(tk.END, sv)

# Buttons
btn_frame = tk.Frame(root)
btn_frame.pack(pady=5)

def add():
name = simpledialog.askstring("Thêm", "Nhập tên sinh viên:")
if name:
listbox.insert(tk.END, name)

def delete():
sel = listbox.curselection()
if sel:
listbox.delete(sel[0])
else:
messagebox.showwarning("Cảnh báo", "Chọn một mục trước!")

def edit():
sel = listbox.curselection()
if sel:
old = listbox.get(sel[0])
new = simpledialog.askstring("Sửa", "Tên mới:", initialvalue=old)
if new:
listbox.delete(sel[0])
listbox.insert(sel[0], new)

tk.Button(btn_frame, text=" Thêm", command=add, bg="#27AE60", fg="white", padx=10).pack(side="left", padx=3)
tk.Button(btn_frame, text=" Sửa", command=edit, bg="#F39C12", fg="white", padx=10).pack(side="left", padx=3)
tk.Button(btn_frame, text=" Xóa", command=delete, bg="#E74C3C", fg="white", padx=10).pack(side="left", padx=3)

root.mainloop()

4. Combobox — Danh sách thả xuống

Combobox nằm trong module tkinter.ttk:

import tkinter as tk
from tkinter import ttk

root = tk.Tk()
root.geometry("350x250")

tk.Label(root, text="Chọn tỉnh/thành:", font=("Arial", 11)).pack(pady=10)

# Danh sách lựa chọn
cities = ["Hà Nội", "TP Hồ Chí Minh", "Đà Nẵng", "Cần Thơ", "Hải Phòng"]

combo = ttk.Combobox(root, values=cities, width=25, font=("Arial", 11))
combo.set("Chọn tỉnh/thành") # Placeholder
combo.config(state="readonly") # Chỉ chọn, không gõ
combo.pack(pady=5)

result = tk.Label(root, text="", font=("Arial", 13), fg="#2980B9")
result.pack(pady=15)

def on_select(event):
result.config(text=f"Bạn chọn: {combo.get()}")

combo.bind("<<ComboboxSelected>>", on_select)
root.mainloop()

5. Scale — Thanh trượt

import tkinter as tk

root = tk.Tk()
root.geometry("350x300")

tk.Label(root, text="Điều chỉnh độ sáng:", font=("Arial", 11, "bold")).pack(pady=10)

brightness_var = tk.IntVar(value=50)

scale = tk.Scale(
root,
variable=brightness_var,
from_=0, # Giá trị nhỏ nhất
to=100, # Giá trị lớn nhất
orient="horizontal",# horizontal / vertical
length=300,
tickinterval=25, # Khoảng cách giữa các vạch
resolution=5, # Bước nhảy
label="Độ sáng (%)"
)
scale.pack(pady=10)

label = tk.Label(root, text="50%", font=("Arial", 20, "bold"))
label.pack(pady=10)

def on_change(val):
label.config(text=f"{val}%")
# Thay đổi độ sáng label demo
hex_val = int(255 * int(val) / 100)
color = f"#{hex_val:02x}{hex_val:02x}{hex_val:02x}"
label.config(fg=color if int(val) < 80 else "#2C3E50")

scale.config(command=on_change)
root.mainloop()

6. Spinbox — Ô nhập số có mũi tên

import tkinter as tk

root = tk.Tk()
root.geometry("350x250")

tk.Label(root, text="Nhập số lượng:", font=("Arial", 11, "bold")).pack(pady=10)

qty_var = tk.IntVar(value=1)

spinbox = tk.Spinbox(
root,
from_=1,
to=99,
increment=1, # Bước nhảy
textvariable=qty_var,
width=8,
font=("Arial", 14),
justify="center"
)
spinbox.pack(pady=5)

# Spinbox với danh sách giá trị
tk.Label(root, text="Chọn cỡ áo:", font=("Arial", 11, "bold")).pack(pady=10)
size_spin = tk.Spinbox(root, values=("XS", "S", "M", "L", "XL", "XXL"),
width=8, font=("Arial", 14), justify="center", state="readonly")
size_spin.pack(pady=5)

def show():
print(f"Số lượng: {qty_var.get()} | Cỡ: {size_spin.get()}")

tk.Button(root, text="Thêm vào giỏ ", command=show).pack(pady=15)
root.mainloop()

7. Ứng dụng tổng hợp: Form đặt hàng

import tkinter as tk
from tkinter import ttk, messagebox

root = tk.Tk()
root.title("Form đặt hàng")
root.geometry("450x500")
root.resizable(False, False)

# ── Thông tin khách hàng ──────────────────────────────────
frame_info = tk.LabelFrame(root, text="Thông tin khách hàng", padx=10, pady=8)
frame_info.pack(fill="x", padx=15, pady=8)

tk.Label(frame_info, text="Họ tên:").grid(row=0, column=0, sticky="e", pady=4)
entry_name = tk.Entry(frame_info, width=28); entry_name.grid(row=0, column=1, padx=8, pady=4, sticky="ew")

tk.Label(frame_info, text="Tỉnh/Thành:").grid(row=1, column=0, sticky="e", pady=4)
cities = ["Hà Nội", "TP Hồ Chí Minh", "Đà Nẵng", "Cần Thơ"]
combo_city = ttk.Combobox(frame_info, values=cities, state="readonly", width=26)
combo_city.grid(row=1, column=1, padx=8, pady=4, sticky="ew")

# ── Sản phẩm ─────────────────────────────────────────────
frame_prod = tk.LabelFrame(root, text="Sản phẩm", padx=10, pady=8)
frame_prod.pack(fill="x", padx=15, pady=5)

products = ["Áo thun - 150,000đ", "Quần jeans - 350,000đ", "Giày sneaker - 500,000đ"]
listbox = tk.Listbox(frame_prod, height=3, selectmode="multiple", font=("Arial", 10))
listbox.pack(fill="x")
for p in products: listbox.insert(tk.END, p)

# ── Tùy chọn ─────────────────────────────────────────────
frame_opt = tk.LabelFrame(root, text="Tùy chọn", padx=10, pady=8)
frame_opt.pack(fill="x", padx=15, pady=5)

# Cỡ
tk.Label(frame_opt, text="Cỡ:").grid(row=0, column=0, sticky="e")
spin_size = tk.Spinbox(frame_opt, values=("S", "M", "L", "XL"), width=5, state="readonly")
spin_size.grid(row=0, column=1, padx=8, sticky="w")

# Số lượng
tk.Label(frame_opt, text="Số lượng:").grid(row=0, column=2, sticky="e")
spin_qty = tk.Spinbox(frame_opt, from_=1, to=10, width=5)
spin_qty.grid(row=0, column=3, padx=8, sticky="w")

# Giao nhanh
var_fast = tk.BooleanVar()
tk.Checkbutton(frame_opt, text="Giao nhanh (+20,000đ)", variable=var_fast).grid(
row=1, column=0, columnspan=4, sticky="w", pady=5)

# Thanh toán
frame_pay = tk.LabelFrame(root, text="Thanh toán", padx=10, pady=8)
frame_pay.pack(fill="x", padx=15, pady=5)

pay_var = tk.StringVar(value="cod")
for text, val in [("Tiền mặt (COD)", "cod"), ("Chuyển khoản", "bank"), ("Ví điện tử", "ewallet")]:
tk.Radiobutton(frame_pay, text=text, variable=pay_var, value=val).pack(side="left", padx=10)

# ── Nút đặt hàng ─────────────────────────────────────────
def order():
name = entry_name.get()
city = combo_city.get()
sels = [listbox.get(i) for i in listbox.curselection()]
if not name or not city or not sels:
messagebox.showwarning("Thiếu thông tin", "Vui lòng điền đầy đủ thông tin!")
return
msg = (f"Đặt hàng thành công! \n\n"
f"Khách: {name}{city}\n"
f"Sản phẩm: {', '.join(sels)}\n"
f"Cỡ: {spin_size.get()} | SL: {spin_qty.get()}\n"
f"Giao nhanh: {'Có' if var_fast.get() else 'Không'}\n"
f"Thanh toán: {pay_var.get().upper()}")
messagebox.showinfo("Đặt hàng", msg)

tk.Button(root, text=" Đặt hàng ngay", command=order,
font=("Arial", 12, "bold"), bg="#E74C3C", fg="white",
padx=20, pady=8).pack(pady=12)

root.mainloop()

Bài tập thực hành

Trắc nghiệm

  1. Widget nào dùng để chọn một trong nhiều tùy chọn?

    • A. Checkbutton
    • B. Radiobutton
    • C. Listbox
    • D. Spinbox
  2. Combobox nằm trong module nào?

    • A. tkinter
    • B. tkinter.ttk
    • C. tkinter.widgets
    • D. tk.combobox
  3. Sự kiện nào kích hoạt khi chọn item trong Listbox?

    • A. <Select>
    • B. <ListboxChange>
    • C. <<ListboxSelect>>
    • D. <ItemSelect>

Thực hành

  1. Tạo form "Khảo sát kỹ năng": Tên (Entry), Kỹ năng (Checkbutton: Python, JS, SQL, Excel), Kinh nghiệm (Spinbox 0-20 năm), nút Gửi.
  2. Tạo app "Bộ lọc màu sắc" với 3 Scale (R, G, B từ 0-255) — cập nhật màu nền cửa sổ theo thời gian thực.
  3. Tạo Listbox danh sách công việc với chức năng Thêm, Đánh dấu hoàn thành (gạch qua), Xóa.
  4. (Nâng cao) Tạo app "Quiz trắc nghiệm": 5 câu hỏi, mỗi câu dùng Radiobutton 4 đáp án, nút Nộp bài tính điểm.

Bài trước: Bài 4 – Xử Lý Sự Kiện · Bài tiếp: Bài 6 – Menu & Hộp Thoại