Bài 3: Quản Lý Bố Cục (Layout Manager)
Giai đoạn 1 – Nhập Môn · Mục tiêu: Thành thạo 3 layout manager — pack(), grid(), place() — và biết khi nào dùng loại nào.
1. Tổng quan Layout Manager
Tkinter có 3 cách sắp xếp widget trong cửa sổ:
| Manager | Đặc điểm | Dùng khi nào |
|---|---|---|
pack() | Tự động xếp dọc/ngang | Layout đơn giản, nhanh |
grid() | Lưới hàng × cột | Form, bảng, layout phức tạp |
place() | Tọa độ tuyệt đối/tương đối | Overlay, vị trí chính xác |
:::warning Không trộn lẫn Không được dùng cả pack() và grid() trong cùng một Frame! Có thể gây lỗi hoặc treo ứng dụng. Nên chọn một loại cho mỗi container. :::
2. pack() — Đơn giản & nhanh
2.1. Các tham số chính
widget.pack(
side="top", # Hướng xếp: top/bottom/left/right
fill="none", # Lấp đầy: none/x/y/both
expand=False, # Mở rộng chiếm không gian còn lại
padx=0, # Khoảng cách ngoài theo chiều ngang
pady=0, # Khoảng cách ngoài theo chiều dọc
ipadx=0, # Khoảng cách trong theo chiều ngang
ipady=0, # Khoảng cách trong theo chiều dọc
anchor="center" # Căn chỉnh trong không gian được cấp
)
2.2. side — Hướng xếp
import tkinter as tk
root = tk.Tk()
root.geometry("400x100")
colors = ["#3498DB", "#E74C3C", "#2ECC71", "#F39C12"]
texts = ["LEFT", "LEFT", "RIGHT", "RIGHT"]
sides = ["left", "left", "right", "right"]
for text, side, color in zip(texts, sides, colors):
tk.Label(root, text=text, bg=color, fg="white",
font=("Arial", 11, "bold"), width=8).pack(side=side, padx=5, pady=20)
root.mainloop()
2.3. fill & expand
import tkinter as tk
root = tk.Tk()
root.geometry("400x300")
# fill="x" — kéo dài theo chiều ngang
tk.Label(root, text="fill='x'", bg="#3498DB", fg="white").pack(fill="x", pady=2)
# fill="y" — kéo dài theo chiều dọc (cần side=left/right)
tk.Label(root, text="fill='y'", bg="#E74C3C", fg="white").pack(fill="y", side="left", padx=2)
# fill="both" + expand=True — chiếm toàn bộ không gian còn lại
tk.Label(root, text="fill='both' + expand=True",
bg="#2ECC71", fg="white", font=("Arial", 14)).pack(fill="both", expand=True, padx=2)
root.mainloop()
2.4. Layout Header + Content + Footer
import tkinter as tk
root = tk.Tk()
root.title("Layout pack()")
root.geometry("400x350")
# Header
header = tk.Frame(root, bg="#2C3E50", height=50)
header.pack(fill="x")
header.pack_propagate(False)
tk.Label(header, text="HEADER", fg="white", bg="#2C3E50",
font=("Arial", 13, "bold")).pack(expand=True)
# Sidebar + Content
body = tk.Frame(root)
body.pack(fill="both", expand=True)
sidebar = tk.Frame(body, bg="#ECF0F1", width=100)
sidebar.pack(side="left", fill="y")
sidebar.pack_propagate(False)
tk.Label(sidebar, text="SIDEBAR", bg="#ECF0F1", fg="#7F8C8D").pack(expand=True)
content = tk.Frame(body, bg="white")
content.pack(side="left", fill="both", expand=True)
tk.Label(content, text="CONTENT", font=("Arial", 12)).pack(expand=True)
# Footer
footer = tk.Frame(root, bg="#BDC3C7", height=35)
footer.pack(fill="x", side="bottom")
footer.pack_propagate(False)
tk.Label(footer, text="FOOTER", bg="#BDC3C7", fg="#7F8C8D").pack(expand=True)
root.mainloop()
3. grid() — Lưới hàng × cột
3.1. Các tham số chính
widget.grid(
row=0, # Hàng (bắt đầu từ 0)
column=0, # Cột (bắt đầu từ 0)
rowspan=1, # Trải dài bao nhiêu hàng
columnspan=1, # Trải dài bao nhiêu cột
sticky="", # Căn chỉnh: n/s/e/w hoặc kết hợp "nsew"
padx=0, # Padding ngoài ngang
pady=0, # Padding ngoài dọc
ipadx=0, # Padding trong ngang
ipady=0, # Padding trong dọc
)
3.2. Form đăng nhập với grid()
import tkinter as tk
root = tk.Tk()
root.title("Đăng nhập")
root.geometry("350x200")
root.resizable(False, False)
# Cấu hình cột (cột 1 mở rộng)
root.columnconfigure(1, weight=1)
# Hàng 0: Tên đăng nhập
tk.Label(root, text="Tên đăng nhập:").grid(row=0, column=0, padx=10, pady=10, sticky="e")
entry_user = tk.Entry(root)
entry_user.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
# Hàng 1: Mật khẩu
tk.Label(root, text="Mật khẩu:").grid(row=1, column=0, padx=10, pady=5, sticky="e")
entry_pass = tk.Entry(root, show="*")
entry_pass.grid(row=1, column=1, padx=10, pady=5, sticky="ew")
# Hàng 2: Nút
tk.Button(root, text="Đăng nhập", bg="#3498DB", fg="white",
command=lambda: print(f"User: {entry_user.get()}")).grid(
row=2, column=0, columnspan=2, pady=15, ipadx=10, ipady=5)
root.mainloop()
3.3. rowspan & columnspan
import tkinter as tk
root = tk.Tk()
root.geometry("400x300")
# Cấu hình trọng số để các ô mở rộng
for i in range(3): root.columnconfigure(i, weight=1)
for i in range(3): root.rowconfigure(i, weight=1)
colors = ["#3498DB", "#E74C3C", "#2ECC71", "#F39C12", "#9B59B6"]
# Ô chiếm 2 cột
tk.Label(root, text="columnspan=2", bg=colors[0], fg="white",
font=("Arial", 10, "bold")).grid(row=0, column=0, columnspan=2, sticky="nsew", padx=2, pady=2)
tk.Label(root, text="(0,2)", bg=colors[1], fg="white").grid(row=0, column=2, sticky="nsew", padx=2, pady=2)
# Ô chiếm 2 hàng
tk.Label(root, text="rowspan=2", bg=colors[2], fg="white",
font=("Arial", 10, "bold")).grid(row=1, column=0, rowspan=2, sticky="nsew", padx=2, pady=2)
tk.Label(root, text="(1,1)", bg=colors[3], fg="white").grid(row=1, column=1, sticky="nsew", padx=2, pady=2)
tk.Label(root, text="(1,2)", bg=colors[4], fg="white").grid(row=1, column=2, sticky="nsew", padx=2, pady=2)
tk.Label(root, text="(2,1)", bg="#1ABC9C", fg="white").grid(row=2, column=1, sticky="nsew", padx=2, pady=2)
tk.Label(root, text="(2,2)", bg="#E67E22", fg="white").grid(row=2, column=2, sticky="nsew", padx=2, pady=2)
root.mainloop()
3.4. sticky — Căn chỉnh trong ô
# sticky giống như "dán" widget vào cạnh nào của ô
# "n" = bắc (trên), "s" = nam (dưới), "e" = đông (phải), "w" = tây (trái)
# "nsew" = kéo dài ra 4 phía (lấp đầy ô)
# "ew" = kéo dài theo chiều ngang
widget.grid(row=0, column=0, sticky="ew") # Kéo ngang
widget.grid(row=0, column=0, sticky="nsew") # Lấp đầy ô
widget.grid(row=0, column=0, sticky="w") # Căn trái
widget.grid(row=0, column=0, sticky="e") # Căn phải
3.5. columnconfigure & rowconfigure (responsive)
import tkinter as tk
root = tk.Tk()
root.geometry("400x300")
root.title("Responsive Grid")
# Cột 1 (index 1) có weight=1 → mở rộng
root.columnconfigure(0, weight=0) # Cố định
root.columnconfigure(1, weight=1) # Mở rộng
# Hàng 0,1,2 cố định; hàng 3 mở rộng
root.rowconfigure(3, weight=1)
tk.Label(root, text="Nhãn:", anchor="e").grid(row=0, column=0, padx=5, pady=5, sticky="e")
tk.Entry(root).grid(row=0, column=1, padx=5, pady=5, sticky="ew")
tk.Label(root, text="Email:", anchor="e").grid(row=1, column=0, padx=5, pady=5, sticky="e")
tk.Entry(root).grid(row=1, column=1, padx=5, pady=5, sticky="ew")
# Vùng Text mở rộng
tk.Label(root, text="Ghi chú:", anchor="ne").grid(row=2, column=0, padx=5, pady=5, sticky="ne")
txt = tk.Text(root, height=5)
txt.grid(row=2, column=1, padx=5, pady=5, sticky="nsew")
root.rowconfigure(2, weight=1)
tk.Button(root, text="Gửi").grid(row=3, column=1, pady=10, sticky="e", padx=5)
root.mainloop()
4. place() — Tọa độ tuyệt đối
4.1. Các tham số chính
widget.place(
x=0, # Tọa độ X tuyệt đối (pixel từ trái)
y=0, # Tọa độ Y tuyệt đối (pixel từ trên)
relx=0.0, # Tọa độ X tương đối (0.0 → 1.0)
rely=0.0, # Tọa độ Y tương đối (0.0 → 1.0)
width=None, # Chiều rộng tuyệt đối
height=None, # Chiều cao tuyệt đối
relwidth=None, # Chiều rộng tương đối
relheight=None,# Chiều cao tương đối
anchor="nw" # Điểm neo: nw/n/ne/w/center/e/sw/s/se
)
4.2. Ví dụ place()
import tkinter as tk
root = tk.Tk()
root.geometry("400x300")
root.configure(bg="#1A1A2E")
# Vị trí tuyệt đối
tk.Label(root, text="Tọa độ (50, 50)", bg="#3498DB",
fg="white", padx=5).place(x=50, y=50)
# Vị trí tương đối (căn giữa)
tk.Label(root, text="Căn giữa (relx=0.5, rely=0.5)",
bg="#E74C3C", fg="white", padx=5).place(
relx=0.5, rely=0.5, anchor="center")
# Kết hợp tương đối và tuyệt đối
tk.Label(root, text="Góc dưới phải", bg="#2ECC71",
fg="white", padx=5).place(relx=1.0, rely=1.0, anchor="se", x=-10, y=-10)
root.mainloop()
4.3. place() cho overlay / splash screen
import tkinter as tk
root = tk.Tk()
root.geometry("400x300")
# Ảnh nền (dùng Frame giả lập)
bg = tk.Frame(root, bg="#2C3E50")
bg.place(relwidth=1, relheight=1)
# Card ở giữa (dùng place)
card = tk.Frame(bg, bg="white", padx=20, pady=20)
card.place(relx=0.5, rely=0.5, anchor="center")
tk.Label(card, text=" Chào mừng!", font=("Arial", 18, "bold")).pack()
tk.Label(card, text="Nhấn nút để tiếp tục", fg="gray").pack(pady=5)
tk.Button(card, text="Bắt đầu", bg="#3498DB", fg="white",
padx=20, pady=5).pack(pady=10)
root.mainloop()
5. So sánh 3 layout manager
| Tiêu chí | pack() | grid() | place() |
|---|---|---|---|
| Độ phức tạp | Đơn giản | Trung bình | Trung bình |
| Responsive | Tốt | Rất tốt | Kém |
| Chính xác vị trí | Khó | Tốt | Rất chính xác |
| Phù hợp | Thanh công cụ, list | Form, bảng | Overlay, splash |
| Khi resize | Thích ứng tốt | Thích ứng tốt | Vị trí cố định |
Bài tập thực hành
Trắc nghiệm
-
Layout manager nào không được dùng chung với
pack()trong cùng một Frame?- A.
place() - B.
grid() - C. Cả A và B
- D. Không có gì
- A.
-
Tham số
sticky="ew"tronggrid()có nghĩa là gì?- A. Widget căn giữa
- B. Widget kéo dài theo chiều dọc
- C. Widget kéo dài theo chiều ngang
- D. Widget ở góc trái trên
-
Để một widget chiếm toàn bộ không gian còn lại, dùng gì trong
pack()?- A.
fill="all" - B.
expand=True, fill="both" - C.
stretch=True - D.
grow=True
- A.
Thực hành
- Dùng
pack()tạo layout: Header (màu tối) + 2 cột (Sidebar trái, Content phải) + Footer. - Dùng
grid()tạo form "Thông tin liên hệ" gồm: Tên, Email, Số điện thoại, Địa chỉ (Text), nút Gửi. - Dùng
place()tạo một "Card đăng nhập" nằm chính giữa cửa sổ với nền màu xanh đậm. - (Nâng cao) Kết hợp: Dùng
pack()cho layout tổng thể,grid()cho form bên trong một Frame.
Bài trước: Bài 2 – Widget Cơ Bản · Bài tiếp: Bài 4 – Xử Lý Sự Kiện