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

Có 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ố 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 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