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

Bài 4: Xử Lý Sự Kiện (Event & Binding)

Giai đoạn 2 – Widget Nâng Cao · Mục tiêu: Hiểu mô hình sự kiện Tkinter, dùng command và bind() để phản hồi thao tác người dùng.


1. Mô hình sự kiện trong Tkinter

Tkinter hoạt động theo mô hình Event-Driven (lập trình hướng sự kiện):

Người dùng → Hành động (click, gõ, di chuột) → Event → Handler function → Cập nhật UI

2 cách xử lý sự kiện:

CáchSyntaxDùng khi
command=Button(command=func)Button, Checkbutton, Radiobutton
bind()widget.bind("<Event>", func)Mọi loại sự kiện

2. command — Cách đơn giản nhất

2.1. Truyền hàm vào command

import tkinter as tk

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

def on_click():
label.config(text="Đã nhấn! ")

label = tk.Label(root, text="Nhấn nút bên dưới", font=("Arial", 12))
label.pack(pady=20)

# Truyền tên hàm (không có ()) vào command
btn = tk.Button(root, text="Nhấn tôi!", command=on_click)
btn.pack()

root.mainloop()

:::warning Lỗi phổ biến

# Sai — gọi hàm ngay lập tức, không đợi click
btn = tk.Button(root, text="Click", command=on_click())

# Đúng — truyền tham chiếu hàm
btn = tk.Button(root, text="Click", command=on_click)

:::

2.2. Lambda — Truyền tham số vào command

import tkinter as tk

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

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

def greet(name):
label.config(text=f"Xin chào, {name}! ")

# Lambda để truyền tham số
tk.Button(root, text="Chào Vũ", command=lambda: greet("Vũ")).pack(pady=3)
tk.Button(root, text="Chào Nam", command=lambda: greet("Nam")).pack(pady=3)
tk.Button(root, text="Chào Hoa", command=lambda: greet("Hoa")).pack(pady=3)

root.mainloop()

3. bind() — Linh hoạt cho mọi sự kiện

3.1. Cú pháp bind()

widget.bind("<EventPattern>", handler_function)
  • EventPattern là chuỗi mô tả sự kiện: "<Button-1>", "<KeyPress>", v.v.
  • handler_function nhận 1 tham sốEvent object

3.2. Sự kiện chuột

import tkinter as tk

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

label = tk.Label(root, text="Tương tác với chuột", font=("Arial", 13))
label.pack(pady=20)

info = tk.Label(root, text="", fg="#2980B9", font=("Arial", 11))
info.pack()

def on_left_click(event):
info.config(text=f"Click trái tại ({event.x}, {event.y})")

def on_right_click(event):
info.config(text=f"Click phải tại ({event.x}, {event.y})")

def on_double_click(event):
info.config(text="Double click! ")

def on_enter(event):
label.config(fg="#E74C3C")

def on_leave(event):
label.config(fg="black")

def on_motion(event):
root.title(f"Chuột: ({event.x}, {event.y})")

# Gán sự kiện
root.bind("<Button-1>", on_left_click) # Click trái
root.bind("<Button-3>", on_right_click) # Click phải
root.bind("<Double-Button-1>", on_double_click) # Double click
label.bind("<Enter>", on_enter) # Chuột vào
label.bind("<Leave>", on_leave) # Chuột rời
root.bind("<Motion>", on_motion) # Di chuyển chuột

root.mainloop()

3.3. Sự kiện bàn phím

import tkinter as tk

root = tk.Tk()
root.geometry("400x300")
root.title("Keyboard Events")

log = tk.Text(root, height=10, font=("Consolas", 11))
log.pack(fill="both", expand=True, padx=10, pady=10)

def on_key_press(event):
log.insert(tk.END, f"Nhấn: '{event.char}' | keycode={event.keycode} | keysym={event.keysym}\n")
log.see(tk.END) # Cuộn xuống cuối

def on_enter_key(event):
log.insert(tk.END, "--- ENTER nhấn ---\n")
log.see(tk.END)

def on_ctrl_s(event):
log.insert(tk.END, " Ctrl+S (Lưu)\n")
log.see(tk.END)
return "break" # Ngăn xử lý mặc định

# Gán sự kiện phím
root.bind("<KeyPress>", on_key_press) # Mọi phím
root.bind("<Return>", on_enter_key) # Phím Enter
root.bind("<Control-s>", on_ctrl_s) # Ctrl+S

root.mainloop()

3.4. Bảng sự kiện phổ biến

Event PatternMô tả
<Button-1>Click chuột trái
<Button-2>Click chuột giữa
<Button-3>Click chuột phải
<Double-Button-1>Double click trái
<ButtonRelease-1>Thả chuột trái
<Motion>Di chuyển chuột
<Enter>Chuột vào widget
<Leave>Chuột rời widget
<MouseWheel>Cuộn chuột
<KeyPress>Nhấn bất kỳ phím
<KeyRelease>Thả phím
<Return>Phím Enter
<Escape>Phím Escape
<Tab>Phím Tab
<BackSpace>Phím Backspace
<Delete>Phím Delete
<F1> đến <F12>Phím chức năng
<Control-c>Ctrl+C
<Alt-F4>Alt+F4
<Configure>Widget thay đổi kích thước/vị trí
<FocusIn>Widget nhận focus
<FocusOut>Widget mất focus

4. Event Object — Thông tin sự kiện

def handler(event):
event.widget # Widget phát sinh sự kiện
event.x # Tọa độ X chuột (trong widget)
event.y # Tọa độ Y chuột (trong widget)
event.x_root # Tọa độ X chuột (trên màn hình)
event.y_root # Tọa độ Y chuột (trên màn hình)
event.char # Ký tự được nhấn
event.keysym # Tên phím (ví dụ: "Return", "space")
event.keycode # Mã số phím
event.num # Số nút chuột (1/2/3)
event.delta # Độ cuộn (MouseWheel)
event.width # Chiều rộng mới (Configure)
event.height # Chiều cao mới (Configure)
event.type # Loại sự kiện

5. Hiệu ứng hover với bind()

import tkinter as tk

root = tk.Tk()
root.geometry("300x200")
root.configure(bg="#1A1A2E")

def make_hover_button(parent, text, normal_color, hover_color):
"""Tạo nút với hiệu ứng hover."""
btn = tk.Label(
parent,
text=text,
bg=normal_color,
fg="white",
font=("Arial", 11, "bold"),
padx=20, pady=10,
cursor="hand2"
)

btn.bind("<Enter>", lambda e: btn.config(bg=hover_color))
btn.bind("<Leave>", lambda e: btn.config(bg=normal_color))
btn.bind("<Button-1>", lambda e: print(f"Nhấn: {text}"))

return btn

make_hover_button(root, " Trang chủ", "#3498DB", "#2980B9").pack(pady=10, padx=20, fill="x")
make_hover_button(root, " Cài đặt", "#9B59B6", "#8E44AD").pack(pady=5, padx=20, fill="x")
make_hover_button(root, " Thoát", "#E74C3C", "#CB4335").pack(pady=5, padx=20, fill="x")

root.mainloop()

6. after() — Thực thi có trễ / lặp lại

import tkinter as tk
from datetime import datetime

root = tk.Tk()
root.title("Đồng hồ thời gian thực")
root.geometry("300x150")

label = tk.Label(root, font=("Courier", 36, "bold"), fg="#2C3E50")
label.pack(expand=True)

def update_clock():
now = datetime.now().strftime("%H:%M:%S")
label.config(text=now)
root.after(1000, update_clock) # Gọi lại sau 1000ms

update_clock() # Gọi lần đầu
root.mainloop()

7. unbind() — Gỡ bỏ sự kiện

import tkinter as tk

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

count = [0]
label = tk.Label(root, text="Click vào cửa sổ", font=("Arial", 12))
label.pack(pady=20)

def on_click(event):
count[0] += 1
label.config(text=f"Số lần click: {count[0]}")
if count[0] >= 5:
root.unbind("<Button-1>")
label.config(text="Đã vô hiệu hoá click!")

root.bind("<Button-1>", on_click)
root.mainloop()

8. Ứng dụng thực tế: Notepad mini

import tkinter as tk
from tkinter import messagebox

root = tk.Tk()
root.title("Notepad Mini — Chưa lưu")
root.geometry("500x400")

# Track thay đổi
modified = [False]

def on_text_change(event=None):
if not modified[0]:
modified[0] = True
root.title("Notepad Mini — *Chưa lưu")

def on_ctrl_s(event=None):
content = text.get("1.0", tk.END)
# Giả lập lưu
messagebox.showinfo("Lưu", f"Đã lưu {len(content)} ký tự!")
modified[0] = False
root.title("Notepad Mini — Đã lưu")
return "break"

def on_close():
if modified[0]:
if messagebox.askyesno("Thoát", "Có thay đổi chưa lưu. Thoát không?"):
root.destroy()
else:
root.destroy()

# Thanh trạng thái
status = tk.Label(root, text="Sẵn sàng | 0 ký tự", bd=1, relief="sunken", anchor="w")
status.pack(side="bottom", fill="x")

def update_status(event=None):
content = text.get("1.0", tk.END)
chars = len(content) - 1
lines = int(text.index(tk.END).split(".")[0]) - 1
status.config(text=f"Dòng: {lines} | Ký tự: {chars}")

# Vùng văn bản
text = tk.Text(root, font=("Consolas", 12), undo=True)
text.pack(fill="both", expand=True)
text.bind("<KeyPress>", on_text_change)
text.bind("<KeyRelease>", update_status)

root.bind("<Control-s>", on_ctrl_s)
root.protocol("WM_DELETE_WINDOW", on_close) # Xử lý nút X

root.mainloop()

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

Trắc nghiệm

  1. Hàm handler dùng với bind() phải có bao nhiêu tham số?

    • A. 0
    • B. 1 (event object)
    • C. 2
    • D. Tùy ý
  2. root.after(2000, func) có nghĩa là gì?

    • A. Lặp lại func mỗi 2 giây
    • B. Gọi func sau 2000 giây
    • C. Gọi func sau 2000 mili-giây (2 giây)
    • D. Dừng sau 2 giây
  3. Sự kiện nào được kích hoạt khi chuột di chuyển vào widget?

    • A. <Hover>
    • B. <MouseIn>
    • C. <Enter>
    • D. <Over>

Thực hành

  1. Tạo ứng dụng "Vẽ điểm" — click vào cửa sổ thì vẽ chấm tròn nhỏ màu ngẫu nhiên tại vị trí click.
  2. Tạo form nhập liệu: nhấn <Return> trong Entry tên → focus chuyển sang Entry email.
  3. Tạo app "Stopwatch" với 3 nút Start / Stop / Reset, hiển thị giờ:phút:giây, dùng after().
  4. (Nâng cao) Tạo app "Paint đơn giản" — click và kéo chuột để vẽ đường thẳng tự do trên Canvas.

Bài trước: Bài 3 – Quản Lý Bố Cục · Bài tiếp: Bài 5 – Widget Nâng Cao