강의노트 Canvas
Canvas
Canvas는 다이어그램/네트워크 토폴로지, 데이터 시각화, 간단한 게임/애니메이션, 이미지 뷰어, 그리기 앱 등 2D 그래픽 (선, 도형, 텍스트, 이미지)를 그릴 수 있는 영역을 나타내는 위젯이다.
그래픽 영역은 좌표계를 사용하는데 좌상단이 (0,0)이고 x축은 오른쪽으로 증가하고 y축은 아래쪽으로 증가한다.
모든 도형의 속성 변경, 이동, 삭제가 용이하게 아이템 ID또는 태그(tag)로 관리한다.
창에 그림을 그리기위해 사용한다. 캔버스 안에 다양한 위젯을 그릴 수 있다. 캔버스는 두개의 축을 시스템을 갖고 있다. 윈도우 축과 캔버스 축을 가지고 있다.
기본 예제
켄버스에 도형 그리기
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Canvas 기본")
# ttk 프레임으로 레이아웃, 실제 캔버스는 tk.Canvas
wrap = ttk.Frame(root, padding=10)
wrap.pack(fill="both", expand=True)
cv = tk.Canvas(wrap, width=400, height=250, bg="white")
cv.pack(fill="both", expand=True)
# 기본 도형들
line_id = cv.create_line(20, 20, 200, 20, width=2, fill="black")
rect_id = cv.create_rectangle(20, 40, 150, 120, outline="blue", width=2, fill="light blue")
oval_id = cv.create_oval(200, 60, 360, 180, outline="green", width=2, fill="light green")
text_id = cv.create_text(200, 210, text="Hello Canvas", font=("Arial", 14, "bold"))
root.mainloop()
태그로 아이템 묶어 제어하기
import tkinter as tk
from tkinter import ttk
def func():
cv.itemconfig("node", outline="red")
root = tk.Tk(); root.title("Canvas 태그")
cv = tk.Canvas(root, width=360, height=220, bg="white"); cv.pack(padx=10, pady=10)
# 공통 태그 'node' 부여
for x in (60, 160, 260):
cv.create_oval(x-25, 60-25, x+25, 60+25, fill="gray", outline="dark gray", width=2, tags=("node",))
cv.create_text(180, 160, text="모든 원에 공통 태그 'node' 적용")
# 모든 node의 외곽선 색상을 한번에 변경
bnt = tk.Button(root,text='Click me',command=func)
bnt.pack()
root.mainloop()
![]() |
![]() |
캔버스 안의 타원들을 "node"라는 tag로 묶어서 제어한다.
마우스 이벤트로 인터랙션(드래그와 드롭)
import tkinter as tk
from tkinter import ttk
root = tk.Tk(); root.title("드래그 예제")
cv = tk.Canvas(root, width=400, height=260, bg="#fff"); cv.pack(padx=10, pady=10)
rect = cv.create_rectangle(50, 80, 150, 150, fill="light gray", outline="green", width=2)
drag = {"item": None, "x":0, "y":0}
def on_press(e):
# 클릭한 좌표의 최상위 아이템 찾기
item = cv.find_closest(e.x, e.y) #1
drag["item"] = item[0]
drag["x"], drag["y"] = e.x, e.y
def on_drag(e):
if drag["item"]:
dx, dy = e.x - drag["x"], e.y - drag["y"]
cv.move(drag["item"], dx, dy) #2
drag["x"], drag["y"] = e.x, e.y
def on_release(e):
drag["item"] = None
line_id = cv.create_line(20, 20, 200, 20, width=2, fill="black")
rect_id = cv.create_rectangle(20, 40, 150, 120, outline="blue", width=2, fill="light blue")
oval_id = cv.create_oval(200, 60, 360, 180, outline="green", width=2, fill="light green")
text_id = cv.create_text(200, 210, text="Hello Canvas", font=("Arial", 14, "bold"))
cv.bind("" , on_press) #3
cv.bind("" , on_drag)
cv.bind("" , on_release)
root.mainloop()
- find_closest(x,y)는 (x,y)의 위치에 있는 아이템을 찾는다.
- cv(canvas)안의 아이템을 (dx,dy)만큼 움직인다.
- 이벤트를 처리한다.
애니메이션
import tkinter as tk
root = tk.Tk(); root.title("애니메이션")
cv = tk.Canvas(root, width=420, height=240, bg="#0b1020"); cv.pack(padx=10, pady=10)
ball = cv.create_oval(20, 20, 60, 60, fill="#34c759", outline="")
vx, vy = 3, 2
def tick():
global vx, vy
cv.move(ball, vx, vy)
x1, y1, x2, y2 = cv.coords(ball) #1
if x1 < 0 or x2 > 420: vx = -vx
if y1 < 0 or y2 > 240: vy = -vy
root.after(16, tick) # 2
tick()
root.mainloop()
- coords(아이템)은 아이템의 현 좌표를 돌려준다.
- after(ms, func)는 ms주기로 func을 업데이트한다.
스크롤과 스크롤바를 결합
import tkinter as tk
from tkinter import ttk
root = tk.Tk(); root.title("스크롤/줌")
wrap = ttk.Frame(root); wrap.pack(fill="both", expand=True) #1
xscroll = ttk.Scrollbar(wrap, orient="horizontal")
yscroll = ttk.Scrollbar(wrap, orient="vertical")
cv = tk.Canvas(wrap, bg="white", scrollregion=(0,0,2000,2000),
xscrollcommand=xscroll.set, yscrollcommand=yscroll.set) #2
xscroll.config(command=cv.xview); yscroll.config(command=cv.yview) #3
cv.grid(row=0, column=0, sticky="nsew") #4
yscroll.grid(row=0, column=1, sticky="ns") #5
xscroll.grid(row=1, column=0, sticky="ew") #6
wrap.columnconfigure(0, weight=1); wrap.rowconfigure(0, weight=1)
# 큰 격자 그리기
for i in range(0, 2000, 40):
cv.create_line(i, 0, i, 2000, fill="#eee")
cv.create_line(0, i, 2000, i, fill="#eee")
cv.create_rectangle(100,100,400,300, outline="blue", width=3, fill="light blue")
# Ctrl+마우스휠로 줌
scale = 1.0
def on_wheel(e):
global scale
if (e.state & 0x0004): # 3 Ctrl 키
factor = 1.1 if e.delta > 0 else 1/1.1
scale *= factor
x = cv.canvasx(e.x); y = cv.canvasy(e.y)
cv.scale("all", x, y, factor, factor)
# 스케일 후 스크롤영역 업데이트(대략적으로 크게 설정)
cv.configure(scrollregion=cv.bbox("all"))
else:
# 일반 스크롤(Windows/Mac delta 차이 주의)
cv.yview_scroll(-1 if e.delta > 0 else 1, "units")
cv.bind("" , on_wheel) # Windows/Mac
cv.bind("" , lambda e: cv.yview_scroll(-1, "units")) # 7
cv.bind("" , lambda e: cv.yview_scroll(1, "units")) # 8
root.mainloop()
- scroll영역을 지정한다.
- 캔버스에 스크롤을 지정
- 스크롤의 표현을 캔버스로 지정
- 캔버스를 프레임안의 지정 nsew로 확장
- yscroll을 ns방향으로 확장
- xscroll을 ew방향으로 확장
- 마우스 휠을 돌리면 좌표를 올려준다
- 마우스 휠을 아래로 돌리면 좌표를 내림
이미지 삽입
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
root = tk.Tk(); root.title("이미지/위젯 삽입")
cv = tk.Canvas(root, width=420, height=260, bg="#fff")
cv.pack(padx=10, pady=10)
# 이미지 삽입 (PhotoImage는 참조 유지 필요)
img_orig = Image.open("images.jpg")
img_photo = ImageTk.PhotoImage(img_orig)
cv.create_image(0, 0, image=img_photo, anchor="nw")
# Canvas 안에 ttk 위젯을 붙이기
btn = ttk.Button(root, text="Root Button")
cv.create_window(300, 150, window=btn) #1
root.mainloop()
- create_window(x,y, window = 다른 위젯, option)는 캔버스 위젯의 메서드로 다른 위젯을 캔버스의 (x,y)위치에 삽입한다.
옵션으로는 anchor, width, height, state, tags를 갖는다.
anchor는 캔버스의 (x,y)를 다른 위젯의 어디에 맞추는가를 나타낸다. tk.CENTER(기본값), tk.N, tk.S, tk.E, tk.W, tk.NW, tk.NE, tk.SW, tk.SE 등이 가능하다. 예를 들어, anchor=tk.NW는 다른 위젯의 왼쪽 상단 끝이 (x,y)가 되도록 만든다.
width, height은 캔버스의 width와 height의 공간을 다른 위젯을 위해 비워둔다. 만약 이 값들이 생략되면 다른 위젯의 크기가 들어간다.
state는 다른 위젯의 보여지는 것을 제어한다. tk.NORMAL(기본값), tk.DISABLED(다른 위젯이 동작하지 않게 만듬), tk.HIDDEN(안보이게 만듬)이 가능하다.
tags는 태그들로 쉽게 제어하기 위해 사용한다.
좌표 변환
윈도우 좌표와 캔버스 좌표의 변환
오류 해결
현장에서 자주 부딪히는 문제를 원인 → 해결로 정리했고, 바로 복붙 가능한 스니펫도 넣었습니다.
이미지가 안 보이거나 사라짐
원인 : PhotoImage
참조가 GC로 사라짐(지역변수로만 보관).
해결 : 전역/인스턴스 속성에 반드시 보관.
self.img = tk.PhotoImage(file="image.png")
self.cv.create_image(0, 0, image=self.img, anchor="nw")
스크롤/줌 후 클릭 위치가 어긋남
원인: e.x, e.y
(뷰 좌표)를 그대로 사용.
해결: 항상 canvasx/canvasy
로 변환.
cx, cy = self.cv.canvasx(e.x), self.cv.canvasy(e.y)
배경이 클릭을 가로막음(히트테스트 문제)
원인: 배경/격자 레이어가 앞쪽(z-order)으로 올라와 있음.
해결: 배경에 태그를 주고 항상 아래로.
grid = self.cv.create_line(..., tags=("grid",))
self.cv.tag_lower("grid")
리사이즈해도 캔버스가 안 늘어남
원인: 레이아웃 옵션 미설정.
해결: pack(fill="both", expand=True)
또는 grid의 row/columnconfigure(..., weight=1)
.
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
cv.grid(row=0, column=0, sticky="nsew")
성능 저하(수천 개 아이템)
원인: 아이템 수 과다, 잦은 재그리기.
해결 팁
- 태그 일괄 스타일 변경:
itemconfig("group", ...)
- 선을 폴리라인/폴리곤으로 묶기
- 보이지 않는 영역은 생성/갱신 최소화
- 빈번한 재그리기는
after
주기 조절로 통제
마우스 휠 OS별 차이
문제: Windows/Mac은
, Linux는
.
해결 스니펫:
def bind_wheel(widget, handler): # handler(step: +1/-1)
ws = widget.tk.call("tk", "windowingsystem")
if ws == "x11":
widget.bind("" , lambda e: handler(+1))
widget.bind("" , lambda e: handler(-1))
else: # win32, aqua
widget.bind("" , lambda e: handler(+1 if e.delta > 0 else -1))
# 사용 예
bind_wheel(cv, lambda step: cv.yview_scroll(-step, "units"))
선택/상태 관리가 복잡
권장 패턴: 태그 기반 상태머신(조건 분기 대신 태그를 붙였다 뗐다!)
def toggle_select(item):
if "selected" in cv.gettags(item):
cv.dtag(item, "selected")
cv.itemconfig(item, width=2)
else:
cv.addtag_withtag("selected", item)
cv.itemconfig(item, width=4)
디버깅 도우미
경계 확인: print(cv.bbox(id_or_tag))
레이어 확인: cv.tag_raise(id)
, cv.tag_lower(id)
좌표 그리드 임시 표시로 시각 점검: 얇은 회색 선을 타일링
for i in range(0, 2000, 50):
cv.create_line(i, 0, i, 2000, fill="#eee", tags=("debug_grid",))
cv.create_line(0, i, 2000, i, fill="#eee", tags=("debug_grid",))
# 완료 후 cv.delete("debug_grid")
연습문제
- 드래그로 선을 그리고 색상 변경하는 버튼과 지우는 버튼을 갖는 그림판을 만든다.
- 사각형 3개를 만들고 클릭하여 외관선의 굵기를 변경하고 방향키로 이동시킨다.
로그인 하면 댓글을 쓸 수 있습니다.