Bài 6: Menu & Hộp Thoại
Giai đoạn 2 – Widget Nâng Cao · Mục tiêu: Tạo menu bar chuẩn, popup menu chuột phải, và dùng các hộp thoại tích hợp sẵn.
1. Menu Bar — Thanh menu ứng dụng
1.1. Menu cơ bản
import tkinter as tk
root = tk.Tk()
root.title("Menu Demo")
root.geometry("500x350")
# Tạo thanh menu chính
menubar = tk.Menu(root)
# ── Menu Tệp ─────────────────────────────────────────────
file_menu = tk.Menu(menubar, tearoff=0) # tearoff=0: ẩn đường gạch ngang
file_menu.add_command(label="Mới", accelerator="Ctrl+N", command=lambda: print("Mới"))
file_menu.add_command(label="Mở...", accelerator="Ctrl+O", command=lambda: print("Mở"))
file_menu.add_command(label="Lưu", accelerator="Ctrl+S", command=lambda: print("Lưu"))
file_menu.add_separator() # Đường phân cách
file_menu.add_command(label="Thoát", accelerator="Alt+F4", command=root.quit)
menubar.add_cascade(label="Tệp", menu=file_menu)
# ── Menu Chỉnh sửa ────────────────────────────────────────
edit_menu = tk.Menu(menubar, tearoff=0)
edit_menu.add_command(label="Cắt", accelerator="Ctrl+X")
edit_menu.add_command(label="Sao chép", accelerator="Ctrl+C")
edit_menu.add_command(label="Dán", accelerator="Ctrl+V")
edit_menu.add_separator()
edit_menu.add_command(label="Chọn tất cả", accelerator="Ctrl+A")
menubar.add_cascade(label="Chỉnh sửa", menu=edit_menu)
# ── Menu Trợ giúp ─────────────────────────────────────────
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label="Tài liệu", command=lambda: print("Tài liệu"))
help_menu.add_separator()
help_menu.add_command(label="Giới thiệu", command=lambda: print("Phiên bản 1.0"))
menubar.add_cascade(label="Trợ giúp", menu=help_menu)
# Gắn menubar vào cửa sổ
root.config(menu=menubar)
tk.Label(root, text="Nội dung ứng dụng", font=("Arial", 14)).pack(expand=True)
root.mainloop()
1.2. Menu với checkmark và radiobutton
import tkinter as tk
root = tk.Tk()
root.geometry("400x300")
menubar = tk.Menu(root)
view_menu = tk.Menu(menubar, tearoff=0)
# Checkmark trong menu
show_toolbar = tk.BooleanVar(value=True)
show_status = tk.BooleanVar(value=True)
view_menu.add_checkbutton(label="Thanh công cụ", variable=show_toolbar,
command=lambda: print("Toolbar:", show_toolbar.get()))
view_menu.add_checkbutton(label="Thanh trạng thái", variable=show_status)
view_menu.add_separator()
# Radiobutton trong menu
theme = tk.StringVar(value="light")
view_menu.add_radiobutton(label="Sáng ", variable=theme, value="light")
view_menu.add_radiobutton(label="Tối ", variable=theme, value="dark")
menubar.add_cascade(label="Xem", menu=view_menu)
root.config(menu=menubar)
root.mainloop()
1.3. Menu con (submenu)
import tkinter as tk
root = tk.Tk()
root.geometry("400x300")
menubar = tk.Menu(root)
insert_menu = tk.Menu(menubar, tearoff=0)
# Submenu
shape_menu = tk.Menu(insert_menu, tearoff=0)
shape_menu.add_command(label="Hình chữ nhật")
shape_menu.add_command(label="Hình tròn")
shape_menu.add_command(label="Hình tam giác")
insert_menu.add_cascade(label="Hình dạng ", menu=shape_menu)
insert_menu.add_command(label="Ảnh...")
insert_menu.add_command(label="Bảng...")
menubar.add_cascade(label="Chèn", menu=insert_menu)
root.config(menu=menubar)
root.mainloop()
2. Popup Menu — Menu chuột phải
import tkinter as tk
root = tk.Tk()
root.title("Context Menu Demo")
root.geometry("400x300")
text = tk.Text(root, font=("Arial", 11))
text.pack(fill="both", expand=True, padx=10, pady=10)
text.insert("1.0", "Click chuột phải vào đây để xem menu...")
# Tạo popup menu
popup = tk.Menu(root, tearoff=0)
popup.add_command(label=" Cắt", command=lambda: text.event_generate("<<Cut>>"))
popup.add_command(label=" Sao chép", command=lambda: text.event_generate("<<Copy>>"))
popup.add_command(label=" Dán", command=lambda: text.event_generate("<<Paste>>"))
popup.add_separator()
popup.add_command(label=" Xóa tất cả", command=lambda: text.delete("1.0", tk.END))
popup.add_separator()
popup.add_command(label=" Chữ hoa", command=lambda: (
text.delete("1.0", tk.END),
text.insert("1.0", text.get("1.0", tk.END).upper())
))
def show_popup(event):
try:
popup.tk_popup(event.x_root, event.y_root)
finally:
popup.grab_release()
text.bind("<Button-3>", show_popup) # Chuột phải
root.mainloop()
3. MessageBox — Hộp thông báo
import tkinter as tk
from tkinter import messagebox
root = tk.Tk()
root.geometry("400x350")
tk.Label(root, text="Các loại MessageBox:", font=("Arial", 12, "bold")).pack(pady=10)
def info():
messagebox.showinfo("Thông tin", "Đây là hộp thông báo thông tin.")
def warning():
messagebox.showwarning("Cảnh báo", "Có điều gì đó cần chú ý!")
def error():
messagebox.showerror("Lỗi", "Đã xảy ra lỗi nghiêm trọng!")
def ask_yes_no():
result = messagebox.askyesno("Xác nhận", "Bạn có muốn thoát không?")
print("Kết quả:", result) # True hoặc False
def ask_ok_cancel():
result = messagebox.askokcancel("Xác nhận", "Tiếp tục thực hiện?")
print("Kết quả:", result)
def ask_retry_cancel():
result = messagebox.askretrycancel("Thử lại?", "Kết nối thất bại. Thử lại?")
print("Kết quả:", result)
def ask_yes_no_cancel():
result = messagebox.askyesnocancel("Lưu?", "Lưu file trước khi thoát?")
# True=Yes, False=No, None=Cancel
print("Kết quả:", result)
for text, cmd in [
("showinfo", info),
("showwarning", warning),
("showerror", error),
("askyesno", ask_yes_no),
("askokcancel", ask_ok_cancel),
("askretrycancel", ask_retry_cancel),
("askyesnocancel", ask_yes_no_cancel),
]:
tk.Button(root, text=text, command=cmd, width=20).pack(pady=3)
root.mainloop()
4. FileDialog — Hộp thoại file
import tkinter as tk
from tkinter import filedialog
root = tk.Tk()
root.geometry("500x350")
text = tk.Text(root, font=("Consolas", 10))
text.pack(fill="both", expand=True, padx=10, pady=10)
status = tk.Label(root, text="Sẵn sàng", bd=1, relief="sunken", anchor="w")
status.pack(fill="x")
def open_file():
filepath = filedialog.askopenfilename(
title="Chọn file",
initialdir=".", # Thư mục ban đầu
filetypes=[ # Bộ lọc định dạng
("File văn bản", "*.txt"),
("File Python", "*.py"),
("Tất cả", "*.*"),
]
)
if filepath:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
text.delete("1.0", tk.END)
text.insert("1.0", content)
status.config(text=f"Đã mở: {filepath}")
def save_file():
filepath = filedialog.asksaveasfilename(
title="Lưu file",
defaultextension=".txt",
filetypes=[("File văn bản", "*.txt"), ("Tất cả", "*.*")]
)
if filepath:
with open(filepath, "w", encoding="utf-8") as f:
f.write(text.get("1.0", tk.END))
status.config(text=f"Đã lưu: {filepath}")
def open_folder():
folder = filedialog.askdirectory(title="Chọn thư mục")
if folder:
status.config(text=f"Thư mục: {folder}")
# Menubar
menubar = tk.Menu(root)
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="Mở...", command=open_file, accelerator="Ctrl+O")
file_menu.add_command(label="Lưu...", command=save_file, accelerator="Ctrl+S")
file_menu.add_command(label="Mở thư mục", command=open_folder)
menubar.add_cascade(label="Tệp", menu=file_menu)
root.config(menu=menubar)
root.bind("<Control-o>", lambda e: open_file())
root.bind("<Control-s>", lambda e: save_file())
root.mainloop()
5. ColorChooser — Bộ chọn màu
import tkinter as tk
from tkinter import colorchooser
root = tk.Tk()
root.geometry("400x250")
color_var = tk.StringVar(value="#3498DB")
def pick_color():
color_code, color_str = colorchooser.askcolor(
title="Chọn màu",
color=color_var.get() # Màu khởi đầu
)
if color_str:
color_var.set(color_str)
preview.config(bg=color_str)
hex_label.config(text=color_str.upper())
preview = tk.Label(root, bg=color_var.get(), width=20, height=4)
preview.pack(pady=20)
hex_label = tk.Label(root, text="#3498DB", font=("Courier", 14, "bold"), fg="#2C3E50")
hex_label.pack()
tk.Button(root, text=" Chọn màu", command=pick_color,
font=("Arial", 11), padx=15, pady=5).pack(pady=10)
root.mainloop()
6. simpledialog — Hộp thoại nhập liệu
import tkinter as tk
from tkinter import simpledialog
root = tk.Tk()
root.geometry("350x200")
result_label = tk.Label(root, text="", font=("Arial", 13))
result_label.pack(expand=True)
def ask_string():
name = simpledialog.askstring("Tên", "Nhập tên của bạn:", initialvalue="Nguyễn")
if name:
result_label.config(text=f"Xin chào, {name}!")
def ask_int():
age = simpledialog.askinteger("Tuổi", "Nhập tuổi:", minvalue=1, maxvalue=120)
if age:
result_label.config(text=f"Tuổi: {age}")
def ask_float():
score = simpledialog.askfloat("Điểm", "Nhập điểm:", minvalue=0.0, maxvalue=10.0)
if score is not None:
result_label.config(text=f"Điểm: {score:.1f}")
frame = tk.Frame(root)
frame.pack()
tk.Button(frame, text="Hỏi chuỗi", command=ask_string).pack(side="left", padx=5)
tk.Button(frame, text="Hỏi số nguyên", command=ask_int).pack(side="left", padx=5)
tk.Button(frame, text="Hỏi số thực", command=ask_float).pack(side="left", padx=5)
root.mainloop()
7. Ứng dụng tổng hợp: Text Editor đơn giản
import tkinter as tk
from tkinter import filedialog, messagebox, colorchooser, font
class TextEditor:
def __init__(self, root):
self.root = root
self.root.title("Text Editor")
self.root.geometry("700x500")
self.current_file = None
self.modified = False
self._build_menu()
self._build_editor()
self._build_statusbar()
self.root.protocol("WM_DELETE_WINDOW", self.exit_app)
def _build_menu(self):
menubar = tk.Menu(self.root)
# File
fm = tk.Menu(menubar, tearoff=0)
fm.add_command(label="Mới", accelerator="Ctrl+N", command=self.new_file)
fm.add_command(label="Mở…", accelerator="Ctrl+O", command=self.open_file)
fm.add_command(label="Lưu", accelerator="Ctrl+S", command=self.save_file)
fm.add_command(label="Lưu thành…", command=self.save_as)
fm.add_separator()
fm.add_command(label="Thoát", command=self.exit_app)
menubar.add_cascade(label="Tệp", menu=fm)
# Format
fmt = tk.Menu(menubar, tearoff=0)
fmt.add_command(label="Màu chữ…", command=self.change_fg)
fmt.add_command(label="Màu nền…", command=self.change_bg)
menubar.add_cascade(label="Định dạng", menu=fmt)
self.root.config(menu=menubar)
self.root.bind("<Control-n>", lambda e: self.new_file())
self.root.bind("<Control-o>", lambda e: self.open_file())
self.root.bind("<Control-s>", lambda e: self.save_file())
def _build_editor(self):
frame = tk.Frame(self.root)
frame.pack(fill="both", expand=True)
sb = tk.Scrollbar(frame)
sb.pack(side="right", fill="y")
self.text = tk.Text(frame, yscrollcommand=sb.set,
font=("Consolas", 12), undo=True, wrap="word")
self.text.pack(side="left", fill="both", expand=True)
sb.config(command=self.text.yview)
self.text.bind("<KeyPress>", self._mark_modified)
def _build_statusbar(self):
self.status = tk.Label(self.root, text="Sẵn sàng",
bd=1, relief="sunken", anchor="w")
self.status.pack(fill="x", side="bottom")
def _mark_modified(self, event=None):
if not self.modified:
self.modified = True
title = self.root.title()
if not title.endswith("*"):
self.root.title(title + " *")
def new_file(self):
if self._confirm_discard():
self.text.delete("1.0", tk.END)
self.current_file = None
self.modified = False
self.root.title("Text Editor")
self.status.config(text="File mới")
def open_file(self):
if not self._confirm_discard(): return
path = filedialog.askopenfilename(filetypes=[("Text", "*.txt"), ("All", "*.*")])
if path:
with open(path, "r", encoding="utf-8") as f:
self.text.delete("1.0", tk.END)
self.text.insert("1.0", f.read())
self.current_file = path
self.modified = False
self.root.title(f"Text Editor — {path}")
self.status.config(text=f"Đã mở: {path}")
def save_file(self):
if self.current_file:
with open(self.current_file, "w", encoding="utf-8") as f:
f.write(self.text.get("1.0", tk.END))
self.modified = False
self.root.title(f"Text Editor — {self.current_file}")
self.status.config(text="Đã lưu")
else:
self.save_as()
def save_as(self):
path = filedialog.asksaveasfilename(defaultextension=".txt",
filetypes=[("Text", "*.txt"), ("All", "*.*")])
if path:
self.current_file = path
self.save_file()
def change_fg(self):
_, color = colorchooser.askcolor(title="Màu chữ")
if color: self.text.config(fg=color)
def change_bg(self):
_, color = colorchooser.askcolor(title="Màu nền")
if color: self.text.config(bg=color)
def _confirm_discard(self):
if self.modified:
return messagebox.askyesno("Chưa lưu", "Có thay đổi chưa lưu. Tiếp tục?")
return True
def exit_app(self):
if self._confirm_discard():
self.root.destroy()
root = tk.Tk()
app = TextEditor(root)
root.mainloop()
Bài tập thực hành
Trắc nghiệm
-
Hàm nào hiển thị hộp thoại xác nhận Yes/No?
- A.
messagebox.confirm() - B.
messagebox.askyesno() - C.
messagebox.showconfirm() - D.
messagebox.yesno()
- A.
-
tearoff=0trong Menu có ý nghĩa gì?- A. Tắt menu
- B. Ẩn đường gạch ngang cho phép tách menu ra
- C. Xóa menu
- D. Đóng menu tự động
-
Module nào chứa
askopenfilename()?- A.
tkinter.dialog - B.
tkinter.files - C.
tkinter.filedialog - D.
tkinter.io
- A.
Thực hành
- Tạo app "Notepad" đầy đủ: Menu File (Mới, Mở, Lưu, Thoát), Menu Chỉnh sửa (Undo/Redo), Popup menu chuột phải.
- Tạo app "Xem ảnh đơn giản": Menu Tệp → Mở ảnh (jpg/png) → hiển thị ảnh trong Label.
- Tạo màn hình "Cài đặt": Chọn màu chủ đề (ColorChooser), cỡ chữ (Spinbox), nút Áp dụng thay đổi toàn app.
- (Nâng cao) Tạo Text Editor với đếm số từ, tìm kiếm (Ctrl+F mở hộp thoại), thay thế văn bản.
Bài trước: Bài 5 – Widget Nâng Cao · Bài tiếp: Bài 7 – Canvas & Đồ Họa