Bài 7: Canvas & Đồ Họa
Giai đoạn 3 – Dự Án Thực Tế · Mục tiêu: Thành thạo Canvas để vẽ hình học, hiển thị ảnh và tạo animation cơ bản.
1. Canvas là gì?
Canvas là widget đặc biệt trong Tkinter cho phép:
- Vẽ hình học: đường thẳng, hình chữ nhật, hình tròn, đa giác
- Hiển thị văn bản và hình ảnh tùy ý vị trí
- Animation: di chuyển, xóa, thay đổi đối tượng
- Game 2D đơn giản
canvas = tk.Canvas(parent,
width=400, # Chiều rộng (pixel)
height=300, # Chiều cao (pixel)
bg="white", # Màu nền
bd=0, # Độ dày viền
highlightthickness=0 # Ẩn viền focus
)
2. Vẽ hình học cơ bản
2.1. Đường thẳng — create_line()
import tkinter as tk
root = tk.Tk()
root.geometry("500x300")
canvas = tk.Canvas(root, bg="white")
canvas.pack(fill="both", expand=True)
# Đường thẳng đơn giản
canvas.create_line(50, 50, 450, 50)
# Đường thẳng tùy chỉnh
canvas.create_line(50, 100, 450, 100,
fill="#E74C3C", # Màu
width=3, # Độ dày
dash=(10, 5) # Nét đứt: 10 pixel hiện, 5 pixel ẩn
)
# Đường gấp khúc (polyline)
canvas.create_line(50, 150, 150, 200, 250, 150, 350, 200, 450, 150,
fill="#3498DB", width=2, smooth=True) # smooth=True → đường cong
root.mainloop()
2.2. Hình chữ nhật — create_rectangle()
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=300, bg="#1A1A2E")
canvas.pack()
# (x1, y1, x2, y2)
canvas.create_rectangle(50, 50, 200, 150,
fill="#3498DB", # Màu nền
outline="#2980B9", # Màu viền
width=2 # Độ dày viền
)
# Hình chữ nhật không có fill
canvas.create_rectangle(250, 50, 450, 150,
fill="", # Trong suốt
outline="#E74C3C",
width=3,
dash=(8, 4)
)
# Bo góc (tags để tham chiếu sau)
canvas.create_rectangle(50, 170, 200, 260,
fill="#2ECC71", outline="",
tags="rect_green")
root.mainloop()
2.3. Hình oval / tròn — create_oval()
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=300, bg="#2C3E50")
canvas.pack()
# Hình ellipse
canvas.create_oval(50, 50, 200, 150, fill="#E74C3C", outline="white", width=2)
# Hình tròn (bounding box vuông)
canvas.create_oval(250, 50, 400, 200, fill="#F39C12", outline="")
# Hình tròn nhỏ
for i in range(5):
x = 50 + i * 90
canvas.create_oval(x, 200, x+50, 250, fill="#3498DB", outline="white", width=2)
root.mainloop()
2.4. Đa giác — create_polygon()
import tkinter as tk
import math
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=350, bg="#1A1A2E")
canvas.pack()
def polygon_points(cx, cy, r, n):
"""Tạo đa giác đều n cạnh, tâm (cx, cy), bán kính r."""
points = []
for i in range(n):
angle = math.radians(360 * i / n - 90)
points.append(cx + r * math.cos(angle))
points.append(cy + r * math.sin(angle))
return points
# Tam giác
canvas.create_polygon(polygon_points(100, 150, 70, 3),
fill="#E74C3C", outline="white", width=2)
# Ngũ giác
canvas.create_polygon(polygon_points(250, 150, 80, 5),
fill="#3498DB", outline="white", width=2)
# Hình sao 6 cánh
canvas.create_polygon(polygon_points(400, 150, 70, 6),
fill="#2ECC71", outline="white", width=2)
root.mainloop()
2.5. Văn bản — create_text()
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=300, bg="#2C3E50")
canvas.pack()
canvas.create_text(250, 80,
text="Canvas Text Demo",
font=("Arial", 24, "bold"),
fill="white",
anchor="center" # Điểm neo: center/nw/n/ne/w/e/sw/s/se
)
canvas.create_text(50, 150,
text="Căn trái (anchor=w)",
font=("Arial", 14),
fill="#3498DB",
anchor="w"
)
canvas.create_text(250, 200,
text="Nhiều dòng\nvới width",
font=("Arial", 12),
fill="#E74C3C",
width=200, # Tự động xuống dòng sau N pixel
justify="center"
)
root.mainloop()
3. Thao tác với đối tượng Canvas
3.1. Tags — Nhóm đối tượng
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=300, bg="white")
canvas.pack()
# Tạo với tags
canvas.create_rectangle(50, 50, 150, 150, fill="red", tags="circle_group red_items")
canvas.create_oval(200, 50, 350, 150, fill="red", tags="red_items")
canvas.create_rectangle(380, 50, 480, 150, fill="blue", tags="blue_items")
# Thao tác theo tags
def make_red_green():
canvas.itemconfig("red_items", fill="green") # Đổi màu tất cả "red_items"
def hide_blue():
canvas.itemconfigure("blue_items", state="hidden")
def show_all():
canvas.itemconfigure(tk.ALL, state="normal")
frame = tk.Frame(root)
frame.pack(pady=5)
tk.Button(frame, text="Red→Green", command=make_red_green).pack(side="left", padx=5)
tk.Button(frame, text="Ẩn xanh", command=hide_blue).pack(side="left", padx=5)
tk.Button(frame, text="Hiện tất", command=show_all).pack(side="left", padx=5)
root.mainloop()
3.2. Di chuyển và xóa
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=350, bg="white")
canvas.pack()
ball = canvas.create_oval(220, 150, 280, 210, fill="#E74C3C", outline="")
text = canvas.create_text(250, 280, text="Dùng phím mũi tên", font=("Arial", 12))
def move(dx, dy):
canvas.move(ball, dx, dy)
root.bind("<Left>", lambda e: move(-10, 0))
root.bind("<Right>", lambda e: move(10, 0))
root.bind("<Up>", lambda e: move(0, -10))
root.bind("<Down>", lambda e: move(0, 10))
# Xóa item
def delete_ball():
canvas.delete(ball) # Xóa 1 item
def clear_all():
canvas.delete("all") # Xóa tất cả
frame = tk.Frame(root)
frame.pack()
tk.Button(frame, text="Xóa bóng", command=delete_ball).pack(side="left", padx=5)
tk.Button(frame, text="Xóa tất cả", command=clear_all).pack(side="left", padx=5)
root.mainloop()
4. Animation — Chuyển động
4.1. Bóng nảy đơn giản
import tkinter as tk
root = tk.Tk()
root.title("Bóng nảy")
root.geometry("500x400")
canvas = tk.Canvas(root, bg="#1A1A2E", width=500, height=350)
canvas.pack()
# Bóng
ball = canvas.create_oval(230, 170, 270, 210, fill="#E74C3C", outline="")
dx, dy = 4, 3 # Vận tốc
def animate():
global dx, dy
canvas.move(ball, dx, dy)
x1, y1, x2, y2 = canvas.coords(ball)
# Nảy khi chạm biên
if x1 <= 0 or x2 >= 500: dx = -dx
if y1 <= 0 or y2 >= 350: dy = -dy
root.after(16, animate) # ~60 FPS
animate()
root.mainloop()
4.2. Animation nhiều bóng
import tkinter as tk
import random
root = tk.Tk()
root.title("Nhiều bóng nảy")
root.geometry("500x400")
W, H = 500, 350
canvas = tk.Canvas(root, bg="#1A1A2E", width=W, height=H)
canvas.pack()
colors = ["#E74C3C", "#3498DB", "#2ECC71", "#F39C12", "#9B59B6"]
balls = []
for i in range(8):
x = random.randint(20, W-20)
y = random.randint(20, H-20)
r = random.randint(15, 30)
color = random.choice(colors)
dx = random.choice([-1, 1]) * random.randint(2, 5)
dy = random.choice([-1, 1]) * random.randint(2, 5)
obj = canvas.create_oval(x-r, y-r, x+r, y+r, fill=color, outline="")
balls.append({"obj": obj, "dx": dx, "dy": dy})
def animate():
for b in balls:
canvas.move(b["obj"], b["dx"], b["dy"])
x1, y1, x2, y2 = canvas.coords(b["obj"])
if x1 <= 0 or x2 >= W: b["dx"] = -b["dx"]
if y1 <= 0 or y2 >= H: b["dy"] = -b["dy"]
root.after(16, animate)
animate()
root.mainloop()
4.3. Đồng hồ analog
import tkinter as tk
import math
from datetime import datetime
root = tk.Tk()
root.title("Đồng hồ Analog")
root.resizable(False, False)
W, H = 300, 300
canvas = tk.Canvas(root, width=W, height=H, bg="#1A1A2E")
canvas.pack()
CX, CY, R = W//2, H//2, 120 # Tâm và bán kính
def draw_clock():
canvas.delete("hands")
now = datetime.now()
sec = now.second
min_ = now.minute
hour = now.hour % 12
# Kim giây
s_angle = math.radians(sec * 6 - 90)
canvas.create_line(CX, CY,
CX + R*0.85 * math.cos(s_angle),
CY + R*0.85 * math.sin(s_angle),
fill="#E74C3C", width=1, tags="hands")
# Kim phút
m_angle = math.radians((min_ + sec/60) * 6 - 90)
canvas.create_line(CX, CY,
CX + R*0.75 * math.cos(m_angle),
CY + R*0.75 * math.sin(m_angle),
fill="white", width=3, tags="hands")
# Kim giờ
h_angle = math.radians((hour + min_/60) * 30 - 90)
canvas.create_line(CX, CY,
CX + R*0.5 * math.cos(h_angle),
CY + R*0.5 * math.sin(h_angle),
fill="white", width=5, tags="hands")
root.after(1000, draw_clock)
# Vẽ mặt đồng hồ (chỉ 1 lần)
canvas.create_oval(CX-R, CY-R, CX+R, CY+R, fill="#2C3E50", outline="#ECF0F1", width=3)
for i in range(12):
angle = math.radians(i * 30 - 90)
r_outer = R - 5
r_inner = R - 18 if i % 3 == 0 else R - 12
canvas.create_line(
CX + r_inner * math.cos(angle), CY + r_inner * math.sin(angle),
CX + r_outer * math.cos(angle), CY + r_outer * math.sin(angle),
fill="white", width=3 if i % 3 == 0 else 1)
canvas.create_oval(CX-5, CY-5, CX+5, CY+5, fill="white")
draw_clock()
root.mainloop()
5. Vẽ với chuột (Mini Paint)
import tkinter as tk
root = tk.Tk()
root.title("Mini Paint")
root.geometry("600x450")
# Toolbar
toolbar = tk.Frame(root, bg="#ECF0F1", pady=5)
toolbar.pack(fill="x")
color = ["#E74C3C"] # Dùng list để có thể thay đổi trong closure
size = [3]
colors_btns = ["#E74C3C", "#3498DB", "#2ECC71", "#F39C12", "#9B59B6", "#1A1A2E", "white"]
for c in colors_btns:
btn = tk.Label(toolbar, bg=c, width=3, cursor="hand2", relief="raised")
btn.pack(side="left", padx=2, pady=2)
btn.bind("<Button-1>", lambda e, c=c: color.__setitem__(0, c))
tk.Label(toolbar, text=" Cỡ:", bg="#ECF0F1").pack(side="left")
size_spin = tk.Spinbox(toolbar, from_=1, to=20, width=4, textvariable=tk.IntVar(value=3))
size_spin.pack(side="left", padx=5)
canvas = tk.Canvas(root, bg="white", cursor="crosshair")
canvas.pack(fill="both", expand=True)
last = [None, None]
def start_draw(event):
last[0], last[1] = event.x, event.y
def draw(event):
x, y = event.x, event.y
if last[0] is not None:
s = int(size_spin.get())
canvas.create_line(last[0], last[1], x, y,
fill=color[0], width=s, capstyle="round", smooth=True)
last[0], last[1] = x, y
def stop_draw(event):
last[0], last[1] = None, None
def clear_canvas():
canvas.delete("all")
canvas.bind("<ButtonPress-1>", start_draw)
canvas.bind("<B1-Motion>", draw)
canvas.bind("<ButtonRelease-1>", stop_draw)
tk.Button(toolbar, text=" Xóa", command=clear_canvas,
bg="#E74C3C", fg="white").pack(side="right", padx=10)
root.mainloop()
Bài tập thực hành
Trắc nghiệm
-
Hàm nào lấy tọa độ hiện tại của một object trên Canvas?
- A.
canvas.position(obj) - B.
canvas.coords(obj) - C.
canvas.location(obj) - D.
canvas.get_pos(obj)
- A.
-
Để xóa tất cả đối tượng trên Canvas, dùng lệnh nào?
- A.
canvas.clear() - B.
canvas.remove_all() - C.
canvas.delete("all") - D.
canvas.destroy_items()
- A.
-
root.after(16, func)tương đương với bao nhiêu FPS?- A. 16 FPS
- B. ~60 FPS
- C. 100 FPS
- D. 30 FPS
Thực hành
- Vẽ lá cờ Việt Nam: nền đỏ, ngôi sao vàng 5 cánh ở giữa.
- Tạo game bắt bóng: bóng rơi từ trên xuống, thanh trượt di chuyển ngang bắt bóng (dùng phím ←→).
- Tạo app vẽ đồ thị hình sin/cos: nhập tần số, biên độ → vẽ đường cong trên Canvas.
- (Nâng cao) Tạo Snake game đơn giản: rắn di chuyển, ăn mồi, tránh tường và thân mình.
Bài trước: Bài 6 – Menu & Hộp Thoại · Bài tiếp: Bài 8 – ttk & Style