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
Có 2 cách xử lý sự kiện:
| Cách | Syntax | Dù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ố là
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 Pattern | Mô 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
-
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 ý
-
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
-
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>
- A.
Thực hành
- 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.
- Tạo form nhập liệu: nhấn
<Return>trong Entry tên → focus chuyển sang Entry email. - Tạo app "Stopwatch" với 3 nút Start / Stop / Reset, hiển thị giờ:phút:giây, dùng
after(). - (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