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

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

  1. 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)
  2. Để 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()
  3. 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

  1. Vẽ lá cờ Việt Nam: nền đỏ, ngôi sao vàng 5 cánh ở giữa.
  2. 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 ←→).
  3. Tạo app vẽ đồ thị hình sin/cos: nhập tần số, biên độ → vẽ đường cong trên Canvas.
  4. (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