Bỏ qua

Image

TKINTER NÂNG CAO (PHẦN 2)

1. Ôn tập Tkinter cơ bản

1.1. Những gì đã học

  • Window: Cửa sổ chính với tk.Tk()
  • Label: Hiển thị văn bản tk.Label()
  • Button: Nút bấm với command tk.Button()
  • Entry: Nhập liệu một dòng tk.Entry()
  • Layout: pack(), grid() để sắp xếp widget

1.2. Cấu trúc ứng dụng Tkinter

import tkinter as tk

class UngDungCuaToi:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Tiêu đề")
        self.window.geometry("400x300")
        self.tao_giao_dien()

    def tao_giao_dien(self):
        # Tạo các widget ở đây
        pass

    def chay(self):
        self.window.mainloop()

app = UngDungCuaToi()
app.chay()

2. Widget Text - Nhập liệu nhiều dòng

2.1. Text widget cơ bản

import tkinter as tk

window = tk.Tk()
window.title("Text Widget Demo")
window.geometry("500x400")

# Label hướng dẫn
label = tk.Label(window, text="Nhập nội dung:", font=("Arial", 12))
label.pack(pady=10)

# Text widget
text_area = tk.Text(window, font=("Arial", 11), width=50, height=15)
text_area.pack(pady=10, padx=20)

# Nút để lấy nội dung
def lay_noi_dung():
    noi_dung = text_area.get(1.0, tk.END)  # Lấy từ dòng 1, ký tự 0 đến cuối
    print("Nội dung:", noi_dung)

btn_lay = tk.Button(window, text="Lấy nội dung", command=lay_noi_dung)
btn_lay.pack(pady=10)

window.mainloop()

2.2. Text với Scrollbar

import tkinter as tk

window = tk.Tk()
window.title("Text với Scrollbar")
window.geometry("600x450")

# Frame chứa text và scrollbar
text_frame = tk.Frame(window)
text_frame.pack(pady=20, padx=20, fill="both", expand=True)

# Text widget
text_area = tk.Text(text_frame, font=("Arial", 11), wrap="word")
text_area.pack(side="left", fill="both", expand=True)

# Scrollbar
scrollbar = tk.Scrollbar(text_frame, command=text_area.yview)
scrollbar.pack(side="right", fill="y")

# Liên kết scrollbar với text
text_area.config(yscrollcommand=scrollbar.set)

# Thêm nội dung mẫu
noi_dung_mau = """Đây là nội dung mẫu rất dài.
Bạn có thể cuộn lên xuống để xem toàn bộ.
Text widget rất hữu ích cho việc nhập văn bản nhiều dòng.

Một số tính năng của Text widget:
- Hỗ trợ nhiều dòng
- Có thể cuộn nội dung
- Hỗ trợ định dạng text
- Có thể chèn hình ảnh
- Hỗ trợ tag để định dạng"""

text_area.insert(1.0, noi_dung_mau)

window.mainloop()

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

3.1. Listbox cơ bản

import tkinter as tk

window = tk.Tk()
window.title("Listbox Demo")
window.geometry("400x350")

# Label
label = tk.Label(window, text="Danh sách môn học:", font=("Arial", 12))
label.pack(pady=10)

# Listbox
listbox = tk.Listbox(window, font=("Arial", 11), height=8)
listbox.pack(pady=10, padx=20)

# Thêm items vào listbox
mon_hoc = ["Toán", "Vật Lý", "Hóa Học", "Sinh Học", "Văn", "Anh", "Sử", "Địa"]
for mon in mon_hoc:
    listbox.insert(tk.END, mon)

# Hàm xử lý khi chọn
def xu_ly_chon():
    try:
        index = listbox.curselection()[0]  # Lấy index của item được chọn
        mon_chon = listbox.get(index)      # Lấy nội dung
        print(f"Bạn chọn: {mon_chon}")
    except IndexError:
        print("Chưa chọn môn học nào!")

# Button
btn_chon = tk.Button(window, text="Xem môn đã chọn", command=xu_ly_chon)
btn_chon.pack(pady=10)

window.mainloop()

3.2. Listbox với chức năng thêm/xóa

import tkinter as tk

class QuanLyMonHoc:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Quản lý môn học")
        self.window.geometry("500x400")
        self.tao_giao_dien()

    def tao_giao_dien(self):
        # Title
        title = tk.Label(self.window, text="QUẢN LÝ MÔN HỌC", 
                        font=("Arial", 16, "bold"), fg="blue")
        title.pack(pady=15)

        # Frame nhập môn mới
        input_frame = tk.Frame(self.window)
        input_frame.pack(pady=10)

        tk.Label(input_frame, text="Tên môn học:", font=("Arial", 11)).pack(side="left")
        self.entry_mon = tk.Entry(input_frame, font=("Arial", 11), width=20)
        self.entry_mon.pack(side="left", padx=10)

        btn_them = tk.Button(input_frame, text="Thêm", command=self.them_mon,
                            bg="green", fg="white", font=("Arial", 10))
        btn_them.pack(side="left", padx=5)

        # Listbox với scrollbar
        list_frame = tk.Frame(self.window)
        list_frame.pack(pady=20, padx=20, fill="both", expand=True)

        self.listbox = tk.Listbox(list_frame, font=("Arial", 11), height=12)
        self.listbox.pack(side="left", fill="both", expand=True)

        scrollbar = tk.Scrollbar(list_frame, command=self.listbox.yview)
        scrollbar.pack(side="right", fill="y")
        self.listbox.config(yscrollcommand=scrollbar.set)

        # Thêm dữ liệu mẫu
        mon_hoc_mac_dinh = ["Toán", "Vật Lý", "Hóa Học", "Sinh Học", "Văn", "Anh"]
        for mon in mon_hoc_mac_dinh:
            self.listbox.insert(tk.END, mon)

        # Buttons
        btn_frame = tk.Frame(self.window)
        btn_frame.pack(pady=10)

        btn_xoa = tk.Button(btn_frame, text="Xóa môn", command=self.xoa_mon,
                           bg="red", fg="white", font=("Arial", 11))
        btn_xoa.pack(side="left", padx=10)

        btn_sua = tk.Button(btn_frame, text="Sửa môn", command=self.sua_mon,
                           bg="orange", fg="white", font=("Arial", 11))
        btn_sua.pack(side="left", padx=10)

        btn_xoa_tat_ca = tk.Button(btn_frame, text="Xóa tất cả", command=self.xoa_tat_ca,
                                  bg="purple", fg="white", font=("Arial", 11))
        btn_xoa_tat_ca.pack(side="left", padx=10)

    def them_mon(self):
        ten_mon = self.entry_mon.get().strip()
        if ten_mon:
            # Kiểm tra trùng
            danh_sach_hien_tai = self.listbox.get(0, tk.END)
            if ten_mon not in danh_sach_hien_tai:
                self.listbox.insert(tk.END, ten_mon)
                self.entry_mon.delete(0, tk.END)
                print(f"Đã thêm môn: {ten_mon}")
            else:
                print("Môn học đã tồn tại!")
        else:
            print("Vui lòng nhập tên môn học!")

    def xoa_mon(self):
        try:
            index = self.listbox.curselection()[0]
            ten_mon = self.listbox.get(index)
            self.listbox.delete(index)
            print(f"Đã xóa môn: {ten_mon}")
        except IndexError:
            print("Vui lòng chọn môn cần xóa!")

    def sua_mon(self):
        try:
            index = self.listbox.curselection()[0]
            ten_cu = self.listbox.get(index)

            # Hiển thị tên cũ trong entry
            self.entry_mon.delete(0, tk.END)
            self.entry_mon.insert(0, ten_cu)

            # Xóa item cũ
            self.listbox.delete(index)
            print(f"Đang sửa môn: {ten_cu}")
        except IndexError:
            print("Vui lòng chọn môn cần sửa!")

    def xoa_tat_ca(self):
        self.listbox.delete(0, tk.END)
        print("Đã xóa tất cả môn học!")

    def chay(self):
        self.window.mainloop()

# Chạy ứng dụng
app = QuanLyMonHoc()
app.chay()

4. Widget Frame - Nhóm các widget

4.1. Frame để nhóm widgets

import tkinter as tk

window = tk.Tk()
window.title("Frame Demo")
window.geometry("600x400")

# Frame header
header_frame = tk.Frame(window, bg="blue", height=60)
header_frame.pack(fill="x", pady=(0, 10))
header_frame.pack_propagate(False)  # Không cho frame co giãn

title = tk.Label(header_frame, text="ỨNG DỤNG DEMO FRAME", 
                font=("Arial", 16, "bold"), fg="white", bg="blue")
title.pack(pady=15)

# Frame chính
main_frame = tk.Frame(window)
main_frame.pack(fill="both", expand=True, padx=20)

# Frame trái
left_frame = tk.Frame(main_frame, bg="lightblue", width=250)
left_frame.pack(side="left", fill="y", padx=(0, 10))
left_frame.pack_propagate(False)

tk.Label(left_frame, text="PANEL TRÁI", font=("Arial", 12, "bold"), 
         bg="lightblue").pack(pady=10)

tk.Button(left_frame, text="Button 1", width=15).pack(pady=5)
tk.Button(left_frame, text="Button 2", width=15).pack(pady=5)
tk.Button(left_frame, text="Button 3", width=15).pack(pady=5)

# Frame phải
right_frame = tk.Frame(main_frame, bg="lightgreen")
right_frame.pack(side="right", fill="both", expand=True)

tk.Label(right_frame, text="PANEL PHẢI", font=("Arial", 12, "bold"), 
         bg="lightgreen").pack(pady=10)

tk.Label(right_frame, text="Nội dung chính ở đây", bg="lightgreen").pack(pady=20)

# Frame footer
footer_frame = tk.Frame(window, bg="gray", height=40)
footer_frame.pack(side="bottom", fill="x")
footer_frame.pack_propagate(False)

tk.Label(footer_frame, text="Footer - Thông tin bản quyền", 
         fg="white", bg="gray").pack(pady=10)

window.mainloop()

5. Widget Menu - Tạo menu bar

5.1. Menu cơ bản

import tkinter as tk
from tkinter import messagebox

class UngDungVoiMenu:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Ứng dụng với Menu")
        self.window.geometry("500x400")
        self.tao_menu()
        self.tao_giao_dien()

    def tao_menu(self):
        # Tạo menu bar
        menubar = tk.Menu(self.window)
        self.window.config(menu=menubar)

        # Menu File
        file_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="File", menu=file_menu)
        file_menu.add_command(label="Mới", command=self.file_moi, accelerator="Ctrl+N")
        file_menu.add_command(label="Mở", command=self.file_mo, accelerator="Ctrl+O")
        file_menu.add_separator()  # Dòng phân cách
        file_menu.add_command(label="Lưu", command=self.file_luu, accelerator="Ctrl+S")
        file_menu.add_command(label="Lưu dạng...", command=self.file_luu_dang)
        file_menu.add_separator()
        file_menu.add_command(label="Thoát", command=self.thoat, accelerator="Ctrl+Q")

        # Menu Edit
        edit_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Edit", menu=edit_menu)
        edit_menu.add_command(label="Hoàn tác", command=self.edit_hoan_tac, accelerator="Ctrl+Z")
        edit_menu.add_command(label="Làm lại", command=self.edit_lam_lai, accelerator="Ctrl+Y")
        edit_menu.add_separator()
        edit_menu.add_command(label="Sao chép", command=self.edit_sao_chep, accelerator="Ctrl+C")
        edit_menu.add_command(label="Dán", command=self.edit_dan, accelerator="Ctrl+V")

        # Menu View
        view_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="View", menu=view_menu)
        view_menu.add_command(label="Phóng to", command=self.view_phong_to)
        view_menu.add_command(label="Thu nhỏ", command=self.view_thu_nho)
        view_menu.add_command(label="Kích thước gốc", command=self.view_kich_thuoc_goc)

        # Menu Help
        help_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Help", menu=help_menu)
        help_menu.add_command(label="Hướng dẫn", command=self.help_huong_dan)
        help_menu.add_command(label="Về chúng tôi", command=self.help_ve_chung_toi)

    def tao_giao_dien(self):
        # Nội dung chính
        self.text_area = tk.Text(self.window, font=("Arial", 12))
        self.text_area.pack(fill="both", expand=True, padx=10, pady=10)

        # Thêm nội dung mẫu
        self.text_area.insert(1.0, "Đây là ứng dụng demo với menu.\n\n"
                                   "Bạn có thể sử dụng các menu ở trên để thực hiện các chức năng.")

    # Các hàm xử lý menu File
    def file_moi(self):
        self.text_area.delete(1.0, tk.END)
        messagebox.showinfo("File", "Đã tạo file mới!")

    def file_mo(self):
        messagebox.showinfo("File", "Chức năng mở file!")

    def file_luu(self):
        messagebox.showinfo("File", "Đã lưu file!")

    def file_luu_dang(self):
        messagebox.showinfo("File", "Chức năng lưu dạng...")

    def thoat(self):
        if messagebox.askokcancel("Thoát", "Bạn có chắc muốn thoát?"):
            self.window.quit()

    # Các hàm xử lý menu Edit
    def edit_hoan_tac(self):
        messagebox.showinfo("Edit", "Chức năng hoàn tác!")

    def edit_lam_lai(self):
        messagebox.showinfo("Edit", "Chức năng làm lại!")

    def edit_sao_chep(self):
        messagebox.showinfo("Edit", "Đã sao chép!")

    def edit_dan(self):
        messagebox.showinfo("Edit", "Đã dán!")

    # Các hàm xử lý menu View
    def view_phong_to(self):
        current_font = self.text_area.cget("font")
        self.text_area.config(font=("Arial", 14))
        messagebox.showinfo("View", "Đã phóng to!")

    def view_thu_nho(self):
        self.text_area.config(font=("Arial", 10))
        messagebox.showinfo("View", "Đã thu nhỏ!")

    def view_kich_thuoc_goc(self):
        self.text_area.config(font=("Arial", 12))
        messagebox.showinfo("View", "Về kích thước gốc!")

    # Các hàm xử lý menu Help
    def help_huong_dan(self):
        messagebox.showinfo("Hướng dẫn", 
                           "Cách sử dụng:\n"
                           "1. Dùng menu File để tạo, mở, lưu file\n"
                           "2. Dùng menu Edit để chỉnh sửa\n"
                           "3. Dùng menu View để thay đổi hiển thị")

    def help_ve_chung_toi(self):
        messagebox.showinfo("Về chúng tôi", 
                           "Ứng dụng Demo Menu\n"
                           "Phiên bản 1.0\n"
                           "Tác giả: Học sinh Python")

    def chay(self):
        self.window.mainloop()

# Chạy ứng dụng
app = UngDungVoiMenu()
app.chay()

6. Messagebox - Hộp thoại thông báo

6.1. Các loại messagebox

import tkinter as tk
from tkinter import messagebox

window = tk.Tk()
window.title("Demo Messagebox")
window.geometry("400x500")

# Title
title = tk.Label(window, text="DEMO CÁC LOẠI MESSAGEBOX", 
                font=("Arial", 14, "bold"), fg="blue")
title.pack(pady=20)

# Các hàm demo messagebox
def demo_showinfo():
    messagebox.showinfo("Thông báo", "Đây là thông báo thông thường!")

def demo_showwarning():
    messagebox.showwarning("Cảnh báo", "Đây là cảnh báo!")

def demo_showerror():
    messagebox.showerror("Lỗi", "Đây là thông báo lỗi!")

def demo_askquestion():
    result = messagebox.askquestion("Câu hỏi", "Bạn có thích học Python không?")
    print(f"Kết quả: {result}")  # yes hoặc no

def demo_askokcancel():
    result = messagebox.askokcancel("Xác nhận", "Bạn có muốn tiếp tục?")
    print(f"Kết quả: {result}")  # True hoặc False

def demo_askyesno():
    result = messagebox.askyesno("Lựa chọn", "Bạn có muốn lưu file?")
    print(f"Kết quả: {result}")  # True hoặc False

def demo_askretrycancel():
    result = messagebox.askretrycancel("Thử lại", "Kết nối thất bại. Thử lại?")
    print(f"Kết quả: {result}")  # True hoặc False

# Các nút demo
buttons = [
    ("showinfo - Thông báo", demo_showinfo, "lightblue"),
    ("showwarning - Cảnh báo", demo_showwarning, "yellow"),
    ("showerror - Lỗi", demo_showerror, "lightcoral"),
    ("askquestion - Câu hỏi", demo_askquestion, "lightgreen"),
    ("askokcancel - OK/Cancel", demo_askokcancel, "orange"),
    ("askyesno - Yes/No", demo_askyesno, "lightpink"),
    ("askretrycancel - Retry/Cancel", demo_askretrycancel, "lightgray")
]

for text, command, color in buttons:
    btn = tk.Button(window, text=text, command=command, 
                   bg=color, font=("Arial", 11), width=30, height=2)
    btn.pack(pady=5)

window.mainloop()

7. Ví dụ tổng hợp - Ứng dụng tính điểm trung bình

7.1. Ứng dụng hoàn chỉnh

import tkinter as tk
from tkinter import messagebox, ttk
import json
import os

class UngDungTinhDiem:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Ứng dụng tính điểm trung bình")
        self.window.geometry("800x600")
        self.window.configure(bg="lightgray")

        # Dữ liệu
        self.danh_sach_hs = []
        self.file_data = "diem_hoc_sinh.json"

        self.tao_menu()
        self.tao_giao_dien()
        self.tai_du_lieu()

    def tao_menu(self):
        menubar = tk.Menu(self.window)
        self.window.config(menu=menubar)

        # Menu File
        file_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="File", menu=file_menu)
        file_menu.add_command(label="Mới", command=self.file_moi)
        file_menu.add_command(label="Mở", command=self.tai_du_lieu)
        file_menu.add_command(label="Lưu", command=self.luu_du_lieu)
        file_menu.add_separator()
        file_menu.add_command(label="Thoát", command=self.thoat)

        # Menu View
        view_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="View", menu=view_menu)
        view_menu.add_command(label="Thống kê", command=self.hien_thi_thong_ke)
        view_menu.add_command(label="Làm mới", command=self.cap_nhat_bang)

        # Menu Help
        help_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Help", menu=help_menu)
        help_menu.add_command(label="Hướng dẫn", command=self.hien_thi_huong_dan)
        help_menu.add_command(label="Về ứng dụng", command=self.ve_ung_dung)

    def tao_giao_dien(self):
        # Header
        header = tk.Frame(self.window, bg="darkblue", height=60)
        header.pack(fill="x")
        header.pack_propagate(False)

        title = tk.Label(header, text="🎓 ỨNG DỤNG TÍNH ĐIỂM TRUNG BÌNH", 
                        font=("Arial", 16, "bold"), fg="white", bg="darkblue")
        title.pack(pady=15)

        # Main container
        main_frame = tk.Frame(self.window, bg="lightgray")
        main_frame.pack(fill="both", expand=True, padx=10, pady=10)

        # Left panel - Form nhập
        left_panel = tk.LabelFrame(main_frame, text="Nhập thông tin học sinh", 
                                  font=("Arial", 12, "bold"), bg="white", width=300)
        left_panel.pack(side="left", fill="y", padx=(0, 10))
        left_panel.pack_propagate(False)

        # Form fields
        tk.Label(left_panel, text="Họ tên:", bg="white").grid(row=0, column=0, sticky="w", padx=10, pady=5)
        self.entry_ten = tk.Entry(left_panel, width=25)
        self.entry_ten.grid(row=0, column=1, padx=10, pady=5)

        tk.Label(left_panel, text="Lớp:", bg="white").grid(row=1, column=0, sticky="w", padx=10, pady=5)
        self.entry_lop = tk.Entry(left_panel, width=25)
        self.entry_lop.grid(row=1, column=1, padx=10, pady=5)

        tk.Label(left_panel, text="Toán:", bg="white").grid(row=2, column=0, sticky="w", padx=10, pady=5)
        self.entry_toan = tk.Entry(left_panel, width=25)
        self.entry_toan.grid(row=2, column=1, padx=10, pady=5)

        tk.Label(left_panel, text="Văn:", bg="white").grid(row=3, column=0, sticky="w", padx=10, pady=5)
        self.entry_van = tk.Entry(left_panel, width=25)
        self.entry_van.grid(row=3, column=1, padx=10, pady=5)

        tk.Label(left_panel, text="Anh:", bg="white").grid(row=4, column=0, sticky="w", padx=10, pady=5)
        self.entry_anh = tk.Entry(left_panel, width=25)
        self.entry_anh.grid(row=4, column=1, padx=10, pady=5)

        # Buttons
        button_frame = tk.Frame(left_panel, bg="white")
        button_frame.grid(row=5, column=0, columnspan=2, pady=20)

        tk.Button(button_frame, text="Thêm", command=self.them_hoc_sinh,
                 bg="green", fg="white", width=10).pack(side="left", padx=5)
        tk.Button(button_frame, text="Xóa form", command=self.xoa_form,
                 bg="orange", fg="white", width=10).pack(side="left", padx=5)

        # Right panel - Bảng kết quả
        right_panel = tk.Frame(main_frame, bg="white")
        right_panel.pack(side="right", fill="both", expand=True)

        # Treeview
        self.tree = ttk.Treeview(right_panel, columns=("Ten", "Lop", "Toan", "Van", "Anh", "TB", "XL"), 
                                show="headings", height=20)

        # Headers
        self.tree.heading("Ten", text="Họ tên")
        self.tree.heading("Lop", text="Lớp")
        self.tree.heading("Toan", text="Toán")
        self.tree.heading("Van", text="Văn")
        self.tree.heading("Anh", text="Anh")
        self.tree.heading("TB", text="Điểm TB")
        self.tree.heading("XL", text="Xếp loại")

        # Column widths
        self.tree.column("Ten", width=120)
        self.tree.column("Lop", width=60)
        self.tree.column("Toan", width=50)
        self.tree.column("Van", width=50)
        self.tree.column("Anh", width=50)
        self.tree.column("TB", width=60)
        self.tree.column("XL", width=80)

        self.tree.pack(fill="both", expand=True, padx=10, pady=10)

        # Bottom buttons
        bottom_frame = tk.Frame(right_panel, bg="white")
        bottom_frame.pack(fill="x", padx=10, pady=5)

        tk.Button(bottom_frame, text="Xóa học sinh", command=self.xoa_hoc_sinh,
                 bg="red", fg="white").pack(side="left", padx=5)
        tk.Button(bottom_frame, text="Thống kê", command=self.hien_thi_thong_ke,
                 bg="blue", fg="white").pack(side="left", padx=5)
        tk.Button(bottom_frame, text="Lưu dữ liệu", command=self.luu_du_lieu,
                 bg="purple", fg="white").pack(side="right", padx=5)

    def them_hoc_sinh(self):
        try:
            ten = self.entry_ten.get().strip()
            lop = self.entry_lop.get().strip()
            toan = float(self.entry_toan.get())
            van = float(self.entry_van.get())
            anh = float(self.entry_anh.get())

            if not ten or not lop:
                messagebox.showwarning("Cảnh báo", "Vui lòng nhập đầy đủ thông tin!")
                return

            if not (0 <= toan <= 10 and 0 <= van <= 10 and 0 <= anh <= 10):
                messagebox.showerror("Lỗi", "Điểm phải từ 0 đến 10!")
                return

            diem_tb = (toan + van + anh) / 3
            xep_loai = self.tinh_xep_loai(diem_tb)

            hoc_sinh = {
                "ten": ten,
                "lop": lop,
                "toan": toan,
                "van": van,
                "anh": anh,
                "diem_tb": diem_tb,
                "xep_loai": xep_loai
            }

            self.danh_sach_hs.append(hoc_sinh)
            self.cap_nhat_bang()
            self.xoa_form()

            messagebox.showinfo("Thành công", f"Đã thêm học sinh {ten}!")

        except ValueError:
            messagebox.showerror("Lỗi", "Vui lòng nhập điểm hợp lệ!")

    def tinh_xep_loai(self, diem_tb):
        if diem_tb >= 8.5:
            return "Giỏi"
        elif diem_tb >= 6.5:
            return "Khá"
        elif diem_tb >= 5.0:
            return "Trung bình"
        else:
            return "Yếu"

    def cap_nhat_bang(self):
        # Xóa dữ liệu cũ
        for item in self.tree.get_children():
            self.tree.delete(item)

        # Thêm dữ liệu mới
        for hs in self.danh_sach_hs:
            self.tree.insert("", "end", values=(
                hs["ten"], hs["lop"], hs["toan"], hs["van"], 
                hs["anh"], f"{hs['diem_tb']:.1f}", hs["xep_loai"]
            ))

    def xoa_form(self):
        self.entry_ten.delete(0, tk.END)
        self.entry_lop.delete(0, tk.END)
        self.entry_toan.delete(0, tk.END)
        self.entry_van.delete(0, tk.END)
        self.entry_anh.delete(0, tk.END)
        self.entry_ten.focus()

    def xoa_hoc_sinh(self):
        selected = self.tree.selection()
        if not selected:
            messagebox.showwarning("Cảnh báo", "Vui lòng chọn học sinh cần xóa!")
            return

        index = self.tree.index(selected[0])
        ten_hs = self.danh_sach_hs[index]["ten"]

        if messagebox.askyesno("Xác nhận", f"Xóa học sinh {ten_hs}?"):
            del self.danh_sach_hs[index]
            self.cap_nhat_bang()

    def hien_thi_thong_ke(self):
        if not self.danh_sach_hs:
            messagebox.showinfo("Thống kê", "Chưa có dữ liệu!")
            return

        tong_hs = len(self.danh_sach_hs)
        diem_tb_chung = sum(hs["diem_tb"] for hs in self.danh_sach_hs) / tong_hs

        # Đếm xếp loại
        gioi = sum(1 for hs in self.danh_sach_hs if hs["xep_loai"] == "Giỏi")
        kha = sum(1 for hs in self.danh_sach_hs if hs["xep_loai"] == "Khá")
        tb = sum(1 for hs in self.danh_sach_hs if hs["xep_loai"] == "Trung bình")
        yeu = sum(1 for hs in self.danh_sach_hs if hs["xep_loai"] == "Yếu")

        thong_ke = f"""THỐNG KÊ TỔNG QUAN

Tổng số học sinh: {tong_hs}
Điểm TB chung: {diem_tb_chung:.2f}

Xếp loại:
• Giỏi: {gioi} HS ({gioi/tong_hs*100:.1f}%)
• Khá: {kha} HS ({kha/tong_hs*100:.1f}%)
• Trung bình: {tb} HS ({tb/tong_hs*100:.1f}%)
• Yếu: {yeu} HS ({yeu/tong_hs*100:.1f}%)"""

        messagebox.showinfo("Thống kê", thong_ke)

    def luu_du_lieu(self):
        try:
            with open(self.file_data, 'w', encoding='utf-8') as f:
                json.dump(self.danh_sach_hs, f, ensure_ascii=False, indent=4)
            messagebox.showinfo("Lưu dữ liệu", "Đã lưu thành công!")
        except Exception as e:
            messagebox.showerror("Lỗi", f"Không thể lưu: {e}")

    def tai_du_lieu(self):
        try:
            if os.path.exists(self.file_data):
                with open(self.file_data, 'r', encoding='utf-8') as f:
                    self.danh_sach_hs = json.load(f)
                self.cap_nhat_bang()
        except Exception as e:
            messagebox.showerror("Lỗi", f"Không thể tải: {e}")

    def file_moi(self):
        if messagebox.askyesno("File mới", "Xóa tất cả dữ liệu hiện tại?"):
            self.danh_sach_hs = []
            self.cap_nhat_bang()
            self.xoa_form()

    def hien_thi_huong_dan(self):
        huong_dan = """HƯỚNG DẪN SỬ DỤNG

1. Nhập thông tin học sinh vào form bên trái
2. Nhấn 'Thêm' để thêm vào danh sách
3. Chọn học sinh trong bảng và nhấn 'Xóa' để xóa
4. Dùng menu File để lưu/tải dữ liệu
5. Xem thống kê qua menu View hoặc nút Thống kê

Chú ý: Điểm phải từ 0 đến 10"""

        messagebox.showinfo("Hướng dẫn", huong_dan)

    def ve_ung_dung(self):
        messagebox.showinfo("Về ứng dụng", 
                           "Ứng dụng tính điểm TB\nPhiên bản 2.0\nTkinter Advanced Demo")

    def thoat(self):
        if messagebox.askokcancel("Thoát", "Lưu dữ liệu trước khi thoát?"):
            self.luu_du_lieu()
        self.window.quit()

    def chay(self):
        self.window.mainloop()

# Chạy ứng dụng
app = UngDungTinhDiem()
app.chay()

8. Bài tập thực hành tại lớp

Bài 1: Text Editor đơn giản

Tạo ứng dụng soạn thảo văn bản với Text widget, có menu File (Mới, Mở, Lưu, Thoát).

Bài 2: Quản lý sách thư viện

Tạo form nhập thông tin sách (Tên, Tác giả, Năm XB) và hiển thị trong Listbox.

Bài 3: Máy tính với nhiều chức năng

Tạo máy tính có giao diện đẹp với Frame để nhóm các nút, có menu để chọn chế độ tính.

Bài 4: Ứng dụng ghi chú

Tạo app ghi chú với Text widget, có thể lưu/tải ghi chú từ file.

9. Bài tập về nhà

Bài 1: Ứng dụng quản lý chi tiêu cá nhân

Tạo app theo dõi thu chi với form nhập, bảng hiển thị và thống kê.

Bài 2: Game tic-tac-toe (X-O)

Tạo game cờ caro 3x3 với giao diện Button.

Bài 3: Ứng dụng từ điển Anh-Việt mini

Tạo từ điển tra cứu với Entry nhập từ và Text hiển thị nghĩa.

Bài 4: Máy tính BMI nâng cao

Tạo app tính BMI với lưu lịch sử, biểu đồ và lời khuyên chi tiết.

10. Ghi chú quan trọng

10.1. Widget nâng cao đã học

  • Text: Nhập liệu nhiều dòng, có scrollbar
  • Listbox: Danh sách lựa chọn, thêm/xóa items
  • Frame: Nhóm widgets, tạo layout phức tạp
  • Menu: Tạo menu bar, submenu
  • Treeview: Bảng dữ liệu dạng cây
  • Messagebox: Các loại hộp thoại

10.2. Layout và thiết kế

  • Sử dụng Frame để tạo layout phức tạp
  • Kết hợp pack(), grid() trong các Frame khác nhau
  • LabelFrame để tạo khung có tiêu đề
  • pack_propagate(False) để cố định kích thước Frame

10.3. Xử lý sự kiện nâng cao

  • Menu với accelerator (phím tắt)
  • Bind sự kiện cho widget (như double-click)
  • Xử lý nhiều loại messagebox khác nhau
  • Validation dữ liệu đầu vào

10.4. Quản lý dữ liệu

  • Lưu/tải dữ liệu JSON
  • Cập nhật giao diện khi dữ liệu thay đổi
  • Xử lý lỗi với try-except
  • Backup dữ liệu định kỳ

10.5. Best practices

  • Tách biệt logic và giao diện
  • Sử dụng class để tổ chức code
  • Validation đầu vào người dùng
  • Feedback rõ ràng qua messagebox
  • Code có thể tái sử dụng

10.6. Mở rộng thêm

  • Tìm hiểu thêm về ttk (themed widgets)
  • Sử dụng Canvas để vẽ đồ họa
  • Tích hợp với database
  • Tạo executable file (.exe)
  • Responsive design cho nhiều kích thước màn hình