Python tkinter Canvas画布完全攻略

      Tkinter 提供了 Canvas 组件来实现绘图。程序既可在 Canvas 中绘制直线、矩形、椭圆等各种几何图形,也可绘制图片、文字、UI 组件(如 Button)等。Canvas 允许重新改变这些图形项(Tkinter 将程序绘制的所有东西统称为 item)的属性,比如改变其坐标、外观等。

      Canvas 组件的用法与其他 GUI 组件一样简单,程序只要创建并添加 Canvas 组件,然后调用该组件的方法来绘制图形即可。如下程序示范了最简单的 Canvas 绘图:

from tkinter import *
# 创建窗口
root = Tk()
# 创建并添加Canvas
cv = Canvas(root, background='white')
cv.pack(fill=BOTH, expand=YES)
cv.create_rectangle(30, 30, 200, 200,
outline='red', # 边框颜色
stipple = 'question', # 填充的位图
fill="red", # 填充颜色
width=5 # 边框宽度
)
cv.create_oval(240, 30, 330, 200,
outline='yellow', # 边框颜色
fill='pink', # 填充颜色
width=4 # 边框宽度
)
root.mainloop()

       上面程序先创建并添加了 Canvas 组件,分别绘制了矩形和椭圆。运行上面程序,可以看到如图 1 所示的效果。

Python tkinter Canvas画布完全攻略

 

   图 1 最简单的 Canvas 绘图

 

      从上面程序可以看到,Canvas 提供了 create_rectangle() 方法绘制矩形和 create_oval() 方法绘制椭圆(包括圆,圆是椭圆的特例)。实际上,Canvas 还提供了如下方法来绘制各种图形:

      create_arc:绘制弧。

      create_bitmap:绘制位图。

      create_image:绘制图片。

      create_line():绘制直线。

      create_polygon:绘制多边形。

      create_text:绘制文字。

      create_window:绘制组件。

      Canvas 的坐标系统是绘图的基础,其中点 (0,0) 位于 Canvas 组件的左上角,X 轴水平向右延伸,Y 轴垂直向下延伸。

      绘制上面这些图形时需要简单的几何基础:

      在使用 create_line() 绘制直线时,需要指定两个点的坐标,分别作为直线的起点和终点。

      在使用 create_rectangle() 绘制矩形时,需要指定两个点的坐标,分别作为矩形左上角点和右下角点的坐标。

      在使用 create_oval() 绘制椭圆时,需要指定两个点的坐标,分别作为左上角点和右下角点的坐标来确定一个矩形,而该方法则负责绘制该矩形的内切椭圆,如图 2 所示。

Python tkinter Canvas画布完全攻略 

图 2 内切椭圆

 

      从图 2 可以看出,只要矩形确定下来,该矩形的内切椭圆就能确定下来,而 create_oval() 方法所需要的两个坐标正是用于指定该矩形的左上角点和右下角点的坐标。

      在使用 create_arc 绘制弧时,和 create_oval 的用法相似,因为弧是椭圆的一部分,因此同样也是指定左上角和右下角两个点的坐标,默认总是绘制从 3 点(0)开始,逆时针旋转 90° 的那一段弧。程序可通过 start 改变起始角度,也可通过 extent 改变转过的角度。

      在使用 create_polygon 绘制多边形时,需要指定多个点的坐标来作为多边形的多个定点;在使用 create_bitmap、create_image、create_text、create_window 等方法时,只要指定一个坐标点,用于指定目标元素的绘制位置即可。

      在绘制这些图形时可指定如下选项:

      fill:指定填充颜色。如果不指定该选项,默认不填充。

      outline:指定边框颜色。

      width:指定边框宽度。如果不指定该选项,边框宽度默认为 1。

      dash:指定边框使用虚线。该属性值既可为单独的整数,用于指定虚线中线段的长度;也可为形如(5,2,3)格式的元素,此时5 指定虚线中线段的长度,2 指定间隔长度,3 指定虚线长度……依此类推。

      stipple:使用位图平铺进行填充。该选项可与 fill 选项结合使用,fill 选项用于指定位图的颜色。

      style:指定绘制弧的样式。该选项仅对 create_arc 方法起作用。该选项支持 PIESLICE(扇形)、CHORD(弓形)、ARC(仅绘制弧)选项值。

      start:指定绘制弧的起始角度。该选项仅对 create_arc 方法起作用。

      extent:指定绘制弧的角度。该选项仅对 create_arc 方法起作用。

      arrow:指定绘制直线时两端是否有箭头。该选项支持 NONE(两端无箭头)、FIRST(开始端有箭头)、LAST(结束端有箭头)、BOTH(两端都有箭头)选项值。

      arrowshape:指定箭头形状。该选项是一个形如 "20 20 10" 的字符串,字符串中的三个整数依次指定填充长度、箭头长度、箭头宽度。

      joinstyle:指定直接连接点的风格。仅对绘制直线和多向形有效。该选项支持 METTER、ROUND、BEVEL 选项值。

      anchor:指定绘制文字、GUI 组件的位置。该选项仅对 create_text()、create_window() 方法有效。

      justify:指定文字的对齐方式。该选项支持 CENTER、LEFT、RIGHT 常量值,该选项仅对 create_text 方法有效。

      下面程序示范了通过不同的方法来绘制不同的图形,这些图形分别使用不同的边框、不同的填充效果:

from tkinter import *
# 创建窗口
root = Tk()
root.title('绘制图形项')
# 创建并添加Canvas
cv = Canvas(root, background='white', width=830, height=830)
cv.pack(fill=BOTH, expand=YES)
columnFont = ('微软雅黑', 18)
titleFont = ('微软雅黑', 20, 'bold')
# 使用循环绘制文字
for i, st in enumerate(['默认', '指定边宽', '指定填充', '边框颜色', '位图填充']):
cv.create_text((130 + i * 140, 20),text = st,
font = columnFont,
fill='gray',
anchor = W,
justify = LEFT)
# 绘制文字
cv.create_text(10, 60, text = '绘制矩形',
font = titleFont,
fill='magenta',
anchor = W,
justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, None, None, None),
(4, None, None, None),
(4, 'pink', None, None),
(4, 'pink', 'blue', None),
(4, 'pink', 'blue', 'error')]
# 采用循环绘制5个矩形
for i, op in enumerate(options):
cv.create_rectangle(130 + i * 140, 50, 240 + i * 140, 120,
width = op[0], # 边框宽度
fill = op[1], # 填充颜色
outline = op[2], # 边框颜色
stipple = op[3]) # 使用位图填充
# 绘制文字
cv.create_text(10, 160, text = '绘制椭圆',
font = titleFont,
fill='magenta',
anchor = W,
justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, None, None, None),
(4, None, None, None),
(4, 'pink', None, None),
(4, 'pink', 'blue', None),
(4, 'pink', 'blue', 'error')]
# 采用循环绘制5个椭圆
for i, op in enumerate(options):
cv.create_oval(130 + i * 140, 150, 240 + i * 140, 220,
width = op[0], # 边框宽度
fill = op[1], # 填充颜色
outline = op[2], # 边框颜色
stipple = op[3]) # 使用位图填充
# 绘制文字
cv.create_text(10, 260, text = '绘制多边形',
font = titleFont,
fill='magenta',
anchor = W,
justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, "", 'black', None),
(4, "", 'black', None),
(4, 'pink', 'black', None),
(4, 'pink', 'blue', None),
(4, 'pink', 'blue', 'error')]
# 采用循环绘制5个多边形
for i, op in enumerate(options):
cv.create_polygon(130 + i * 140, 320, 185 + i * 140, 250, 240 + i * 140, 320,
width = op[0], # 边框宽度
fill = op[1], # 填充颜色
outline = op[2], # 边框颜色
stipple = op[3]) # 使用位图填充
# 绘制文字
cv.create_text(10, 360, text = '绘制扇形',
font = titleFont,
fill='magenta',
anchor = W,
justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, None, None, None),
(4, None, None, None),
(4, 'pink', None, None),
(4, 'pink', 'blue', None),
(4, 'pink', 'blue', 'error')]
# 采用循环绘制5个扇形
for i, op in enumerate(options):
cv.create_arc(130 + i * 140, 350, 240 + i * 140, 420,
width = op[0], # 边框宽度
fill = op[1], # 填充颜色
outline = op[2], # 边框颜色
stipple = op[3]) # 使用位图填充
# 绘制文字
cv.create_text(10, 460, text = '绘制弓形',
font = titleFont,
fill='magenta',
anchor = W,
justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, None, None, None),
(4, None, None, None),
(4, 'pink', None, None),
(4, 'pink', 'blue', None),
(4, 'pink', 'blue', 'error')]
# 采用循环绘制5个弓形
for i, op in enumerate(options):
cv.create_arc(130 + i * 140, 450, 240 + i * 140, 520,
width = op[0], # 边框宽度
fill = op[1], # 填充颜色
outline = op[2], # 边框颜色
stipple = op[3], # 使用位图填充
start = 30, # 指定起始角度
extent = 60, # 指定逆时针转过角度
style = CHORD) # CHORD指定绘制弓
# 绘制文字
cv.create_text(10, 560, text = '仅绘弧',
font = titleFont,
fill='magenta',
anchor = W,
justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, None, None, None),
(4, None, None, None),
(4, 'pink', None, None),
(4, 'pink', 'blue', None),
(4, 'pink', 'blue', 'error')]
# 采用循环绘制5个弧
for i, op in enumerate(options):
cv.create_arc(130 + i * 140, 550, 240 + i * 140, 620,
width = op[0], # 边框宽度
fill = op[1], # 填充颜色
outline = op[2], # 边框颜色
stipple = op[3], # 使用位图填充
start = 30, # 指定起始角度
extent = 60, # 指定逆时针转过角度
style = ARC) # ARC指定仅绘制弧
# 绘制文字
cv.create_text(10, 660, text = '绘制直线',
font = titleFont,
fill='magenta',
anchor = W,
justify = LEFT)
# 定义列表,每个元素的5个值分别指定边框宽度、线条颜色、位图填充、箭头风格, 箭头形状
options = [(None, None, None, None, None),
(6, None, None, BOTH, (20, 40, 10)),
(6, 'pink', None, FIRST, (40, 40, 10)),
(6, 'pink', None, LAST, (60, 50, 10)),
(8, 'pink', 'error', None, None)]
# 采用循环绘制5个弧
for i, op in enumerate(options):
cv.create_line(130 + i * 140, 650, 240 + i * 140, 720,
width = op[0], # 边框宽度
fill = op[1], # 填充颜色
stipple = op[2], # 使用位图填充
arrow = op[3], # 箭头风格
arrowshape = op[4]) # 箭头形状
# 绘制文字
cv.create_text(10, 760, text = '绘制位图\n图片、组件',
font = titleFont,
fill='magenta',
anchor = W,
justify = LEFT)
# 定义包括create_bitmap, create_image, create_window三个方法的数组
funcs = [Canvas.create_bitmap, Canvas.create_image, Canvas.create_window]
# 为上面3个方法定义选项
items = [{'bitmap' : 'questhead'}, {'image':PhotoImage(file='images/fklogo.gif')},
{'window':Button(cv,text = '单击我', padx=10, pady=5,
command = lambda :print('按钮单击')),'anchor': W}]
for i, func in enumerate(funcs):
func(cv, 230 + i * 140, 780, **items[i])
root.mainloop()

      上面程序示范了 Canvas 中不同的 create_xxx 方法的功能和用法,它们可用于创建矩形、椭圆、多边形、扇形、弓形、弧、直线、位图、图片和组件等。在绘制不同的图形时可指定不同的选项,从而实现丰富的绘制效果。

      运行上面程序,可以看到如图 3 所示的效果。

Python tkinter Canvas画布完全攻略

 

 

图 3 使用Canvas 绘制图形

 

      掌握了上面的绘制方法之后,实际上已经可以实现一些简单的游戏了。比如前面介绍的控制台五子棋,之前程序是在控制台打印游戏状态的,实际上程序完全可以在界面上绘制游戏状态,这样就能看到图形界面的五子棋了。

      此外,该五子棋还需要根据用户的鼠标动作来确定下棋坐标,因此程序会为游戏界面的 <Button-1>(左键单击)、<Motion>(鼠标移动)、<Leave>(鼠标移出)事件绑定事件处理函数。下面程序示范了实现图形界面的五子棋:

from tkinter import *
import random
BOARD_WIDTH = 535
BOARD_HEIGHT = 536
BOARD_SIZE = 15
# 定义棋盘坐标的像素值和棋盘数组之间的偏移距。
X_OFFSET = 21
Y_OFFSET = 23
# 定义棋盘坐标的像素值和棋盘数组之间的比率。
X_RATE = (BOARD_WIDTH - X_OFFSET * 2) / (BOARD_SIZE - 1)
Y_RATE = (BOARD_HEIGHT - Y_OFFSET * 2) / (BOARD_SIZE - 1)
BLACK_CHESS = "●"
WHITE_CHESS = "○"
board = []
# 把每个元素赋为"╋",代表无棋
for i in range(BOARD_SIZE) :
row = ["╋"] * BOARD_SIZE
board.append(row)
# 创建窗口
root = Tk()
# 禁止改变窗口大小
root.resizable(width=False, height=False)
# 修改图标
root.iconbitmap('images/fklogo.ico')
# 设置窗口标题
root.title('五子棋')
# 创建并添加Canvas
cv = Canvas(root, background='white',
width=BOARD_WIDTH, height=BOARD_HEIGHT)
cv.pack()
bm = PhotoImage(file="images/board.png")
cv.create_image(BOARD_HEIGHT/2 + 1, BOARD_HEIGHT/2 + 1, image=bm)
selectedbm = PhotoImage(file="images/selected.gif")
# 创建选中框图片,但该图片默认不在棋盘中
selected = cv.create_image(-100, -100, image=selectedbm)
def move_handler(event):
# 计算用户当前的选中点,并保证该选中点在0~14之间
selectedX = max(0, min(round((event.x - X_OFFSET) / X_RATE), 14))
selectedY = max(0, min(round((event.y - Y_OFFSET) / Y_RATE), 14))
# 移动红色选择框
cv.coords(selected,(selectedX * X_RATE + X_OFFSET,
selectedY * Y_RATE + Y_OFFSET))
black = PhotoImage(file="images/black.gif")
white = PhotoImage(file="images/white.gif")
def click_handler(event):
# 计算用户的下棋点,并保证该下棋点在0~14之间
userX = max(0, min(round((event.x - X_OFFSET) / X_RATE), 14))
userY = max(0, min(round((event.y - Y_OFFSET) / Y_RATE), 14))
# 当下棋点没有棋子时,才能下棋子,用户才能下棋子
if board[userY][userX] == "╋":
cv.create_image(userX * X_RATE + X_OFFSET, userY * Y_RATE + Y_OFFSET,
image=black)
board[userY][userX] = "●"
while(True):
comX = random.randint(0, BOARD_SIZE - 1)
comY = random.randint(0, BOARD_SIZE - 1)
# 如果电脑要下棋的点没有棋子时,才能让电脑下棋
if board[comY][comX] == "╋": break
cv.create_image(comX * X_RATE + X_OFFSET, comY * Y_RATE + Y_OFFSET,
image=white)
board[comY][comX] = "○"
def leave_handler(event):
# 将红色选中框移出界面
cv.coords(selected, -100, -100)
# 为鼠标移动事件绑定事件处理函数
cv.bind('<Motion>', move_handler)
# 为鼠标点击事件绑定事件处理函数
cv.bind('<Button-1>', click_handler)
# 为鼠标移出事件绑定事件处理函数
cv.bind('<Leave>', leave_handler)
root.mainloop()

 

      上面程序先绘制了五子棋的棋盘,该棋盘就是一张预先准备好的图片,然后绘制选择框,当用户鼠标在棋盘上移动时,该选择框显示用户鼠标当前停留在哪个下棋点上。

      随后程序调用了 Canvas 的 coords() 方法,该方法负责重设选择框的坐标。这是 Tkinter 绘图的特别之处,绘制好的每一个图形项都不是固定的,程序后面完全可以修改它们。因此,程序将会控制选择框图片随着用户鼠标的移动而改变位置。

      程序根据用户鼠标单击来绘制黑色棋子(也就是下黑棋)和白色棋子(也就是下白棋)。在绘制黑色棋子和白色棋子的同时,也改变了底层代表棋盘状态的 board 列表的数据,这样即可记录下棋状态,从而让程序在后面可以根据 board[] 列表来判断胜负(本来这个功能在 Charlie 的程序中是有的,此处为了突出绘图的主题,作者删除了这部分)。另外,也可以加入人工智能,根据 board[] 列表来决定电脑的下棋点。

      运行该程序,可以看到如图 4 所示的效果:

Python tkinter Canvas画布完全攻略

 

 

图 4 五子棋

 

      在上面这个程序中,电脑下棋采用的方式是随机下棋,因此下得比较“凌乱”。如果要让电脑下棋更加智能,则可通过简单的人工智能来实现,本教程暂不涉及。 

Canvas操作图形项的标签

      在 Canvas 中通过 create_xxx 方法绘制图形项之后,这些图形项井不是完全静态的图形,每个图形项都是一个独立的对象,程序完全可以动态地修改、删除这些图形项。

      Canvas 以“堆叠”的形式来管理这些图形项,先绘制的图形项位于“堆叠”的下面,后绘制的图形项位于“堆叠”的上面。因此,如果两个图形项有重叠的部分,那么后绘制的图形项(位于上面)会遮挡先绘制的图形项。

      为了修改、删除这些图形项,程序需要先获得这些图形项的引用。获得这些图形项的引用有两种方式:

      通过图形项的 id,也就是 Canvas 执行 create_xxx() 方法的返回值。一般来说,create_xxx() 会依次返回 1、2、3 等整数作为图形项的 id。

      通过图形项的 tag(标签)。

      在 Canvas 中调用 create_xxx() 方法绘图时,还可传入一个 tags 选项,该选项可以为所绘制的图形项(比如矩形、椭圆、多边形等)添加一个或多个 tag(标签)。此外,Canvas 还允许调用方法为图形项添加 tag、删除 tag 等,这些 tag 也相当于该图形项的标识,程序完全可以根据 tag 来获取图形项。

      总结来说,Canvas 提供了如下方法来为图形项添加 tag:

      addtag_aboove(self, newtag, tagOrId):为 tagOrId 对应图形项的上一个图形项添加新 tag。

      addtag_all(self, newtag):为所有图形项添加新 tag。

      addtag_below(self, newtag, tagOrId):为 tagOrId 对应图形项的下一个图形项添加新 tag。

      addtag_closest(self, newtag, x, y):为和 x、y 点最接近的图形项添加新 tag。

      addtag_enclosed(self, newtag, x1, y1, x2, y2):为指定矩形区域内最上面的图形项添加新tag。其中 x1、y1 确定矩形区域的左上角坐标;x2、y2 确定矩形区域的右下角坐标。

      addtag_overlapping(self, newtag, x1, y1, x2, y2):为与指定矩形区域重叠的最上面的图形项添加tag。

      addtag_withtag(self, newtag, tagOrId):为 tagOrId 对应图形项添加新 tag。

      Canvas 提供了如下方法来删除图形项的tag:

      dtag(self, *args):删除指定图形项的tag。

      Canvas 提供了如下方法来获取图形项的所有tag:

      gettags(self, *args):获取指定图形项的所有tag。

      Canvas 提供了如下方法根据 tag 来获取其对应的所有图形项:

      find_withtag(self, tagOrId):获取tagOrId 对应的所有图形项。

      为了更好地理解上面方法的作用,下面用一个程序来进行演示:

from tkinter import *
# 创建窗口
root = Tk()
root.title('操作标签')
# 创建并添加Canvas
cv = Canvas(root, background='white', width=620, height=250)
cv.pack(fill=BOTH, expand=YES)
# 绘制一个矩形框
rt = cv.create_rectangle(40, 40, 300, 220,
outline='blue', width=2,
tag = ('t1', 't2', 't3', 'tag4')) # 为该图形项指定标签
# 访问图形项的id,也就是编号
print(rt) # 1
# 绘制一个椭圆
oval = cv.create_oval(350, 50, 580, 200,
fill='yellow', width=0,
tag = ('g1', 'g2', 'g3', 'tag4')) # 为该图形项指定标签
# 访问图形项的id,也就是编号
print(oval) # 2
# 根据指定tag该tag对应的所有图形项
print(cv.find_withtag('tag4')) # (1, 2)
# 获取指定图形项的所有tag
print(cv.gettags(rt)) # ('t1', 't2', 't3', 'tag4')
print(cv.gettags(2)) # ('g1', 'g2', 'g3', 'tag4')
cv.dtag(1, 't1') # 删除id为1的图形项上名为t1的tag
cv.dtag(oval, 'g1') # 删除id为oval的图形项上名为g1的tag
# 获取指定图形项的所有tag
print(cv.gettags(rt)) # ('tag4', 't2', 't3')
print(cv.gettags(2)) # ('tag4', 'g2', 'g3')
# 为所有图形项添加tag
cv.addtag_all('t5')
print(cv.gettags(1)) # ('tag4', 't2', 't3', 't5')
print(cv.gettags(oval)) # ('tag4', 'g2', 'g3', 't5')
# 为指定图形项添加tag
cv.addtag_withtag('t6', 'g2')
# 获取指定图形项的所有tag
print(cv.gettags(1)) # ('tag4', 't2', 't3', 't5')
print(cv.gettags(oval)) # ('tag4', 'g2', 'g3', 't5', 't6')
# 为指定图形项上面的图形项添加tag, t2上面的就是oval图形项
cv.addtag_above('t7', 't2')
print(cv.gettags(1)) # ('tag4', 't2', 't3', 't5')
print(cv.gettags(oval)) # ('tag4', 'g2', 'g3', 't5', 't6', 't7')
# 为指定图形项下面的图形项添加tag, g2下面的就是rt图形项
cv.addtag_below('t8', 'g2')
print(cv.gettags(1)) # ('tag4', 't2', 't3', 't5', 't8')
print(cv.gettags(oval)) # ('tag4', 'g2', 'g3', 't5', 't6', 't7')
# 为最接近指定点的图形项添加tag,最接近360、90的图形项是oval
cv.addtag_closest('t9', 360, 90)
print(cv.gettags(1)) # ('tag4', 't2', 't3', 't5', 't8')
print(cv.gettags(oval)) # ('tag4', 'g2', 'g3', 't5', 't6', 't7', 't9')
# 为位于指定区域内(几乎覆盖整个图形区)的最上面的图形项添加tag
cv.addtag_closest('t10', 30, 30, 600, 240)
print(cv.gettags(1)) # ('tag4', 't2', 't3', 't5', 't8')
print(cv.gettags(oval)) # ('tag4', 'g2', 'g3', 't5', 't6', 't7', 't9', 't10')
# 为与指定区域内重合的最上面的图形项添加tag
cv.addtag_closest('t11', 250, 30, 400, 240)
print(cv.gettags(1)) # ('tag4', 't2', 't3', 't5', 't8')
print(cv.gettags(oval)) # ('tag4', 'g2', 'g3', 't5', 't6', 't7', 't9', 't10', 't11')
root.mainloop()
上一篇:Docker安装ActiveMQ


下一篇:.NET多线程编程(3)——线程同步