Chuyển tới nội dung chính

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ểmDùng khi nào
pack()Tự động xếp dọc/ngangLayout đơn giản, nhanh
grid()Lưới hàng × cộtForm, bảng, layout phức tạp
place()Tọa độ tuyệt đối/tương đốiOverlay, 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()
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ảnTrung bìnhTrung bình
ResponsiveTốtRất tốtKém
Chính xác vị tríKhóTốtRất chính xác
Phù hợpThanh công cụ, listForm, bảngOverlay, splash
Khi resizeThích ứng tốtThích ứng tốtVị trí cố định

Bài tập thực hành

Trắc nghiệm

  1. 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ì
  2. Tham số sticky="ew" trong grid() 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
  3. Để 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

Thực hành

  1. Dùng pack() tạo layout: Header (màu tối) + 2 cột (Sidebar trái, Content phải) + Footer.
  2. 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.
  3. 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.
  4. (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