五、Tkinter创建图像界面5

5.1 在Canvas中绘制图形

Tkinter 提供了Canvas组件来实现绘图,它可以实现的功能如下:

  1. 绘制图形,如弧线、直线、圆形、多边形、椭圆等几何图形。
  2. 展示图片(包括位图)
  3. 绘制图片、文字、UI组件(如Button)等
  4. 改变所绘制的“画布对象”(item)的属性,如坐标、外观等
    使用Canvas绘制图形的方法也很简单,只要创建并添加Canvas组件,然后调用Canvas的方法即可绘制图形,先来了解一下Canvas组件的属性,这些属性用来设置Canvas组件(此时还没有绘制具体图形)。

属性

方法

background(bg)

指定 Canvas 控件的背景颜色

borderwidth(bd)

指定 Canvas 控件的边框宽度

closeenough

1. 指定一个距离,当鼠标与画布对象的距离小于该值时,认为鼠标位于画布对象上,该选项是一个浮点类型的值

confine

指定 Canvas 控件是否允许滚动超出 scrollregion 选项设置的滚动范围,默认值为 True

selectbackground

指定当画布对象(即在 Canvas 画布上绘制的图形)被选中时的背景色,

selectborderwidth

指定当画布对象被选中时的边框宽度(选中边框)

selectforeground

指定当画布对象被选中时的前景色

state

设置 Canvas 的状态:“normal” 或 “disabled”,默认值是 “normal”,注意,该值不会影响画布对象的状态

takefocus

指定使用 Tab 键可以将焦点移动到输入框中,默认为开启,将该选项设置为 False 避免焦点在此输入框中

width

指定 Canvas 的宽度,单位为像素

xscrollcommand

与 scrollbar(滚动条)控件相关联(沿着 x 轴水平方向)

xscrollincrement

该选项指定 Canvas 水平滚动的“步长”,例如 ‘3c’ 表示 3 厘米,还可以选择的单位有 ‘i’(英寸),‘m’(毫米)和 ‘p’(DPI,大约是 ‘1i’ 等于 ‘72p’),默认为 0,表示可以水平滚动到任意位置

yscrollcommand

与 scrollbar 控件(滚动条)相关联(沿着 y 轴垂直方向)

yscrollincrement

该选项指定 Canvas 垂直滚动的“步长”,例如 ‘3c’ 表示 3 厘米,还可以选择的单位有 ‘i’(英寸),‘m’(毫米)和 ‘p’(DPI,大约是 ‘1i’ 等于 ‘72p’), 默认值是 0,表示可以垂直方向滚动到任意位置

接下来介绍Canvas组件的具体方法,来绘制具体图形:

方法

说明

create_line(x0, y0, x1, y1, … , xn, yn, options)

根据给定的坐标创建一条或者多条线段,参数 x0,y0,x1,y1,…,xn,yn 定义线条的坐标(至少指定2点坐标);参数 options 表示其他可选参数

create_oval(x0, y0, x1, y1, options)

绘制一个圆形或椭圆形;参数 x0 与 y0 定义绘图区域的左上角坐标;参数 x1 与 y1 定义绘图区域的右下角坐标;绘制的椭圆为参数指定矩形的内切圆,参数 options 表示其他可选参数

create_polygon(x0, y0, x1, y1, … , xn, yn, options)

绘制一个至少三个点的多边形;参数 x0、y0、x1、y1、…、xn、yn 定义多边形的坐标;参数 options 表示其他可选参数

create_rectangle(x0, y0, x1, y1, options)

绘制一个矩形;参数 x0 与 y0 定义矩形的左上角坐标;参数 x 1与 y1 定义矩形的右下角坐标;参数 options 表示其他可选参数

create_text(x0, y0, text, options)

绘制一个文字字符串。其中参数 x0 与 y0 定义文字字符串的左上角坐标,参数 text 定义文字字符串的文字;参数 options 表示其他可选参数

create_image(x, y, image)

创建一个图片;参数 x 与 y 定义图片的左上角坐标;参数 image 定义图片的来源,必须是 tkinter 模块的 BitmapImage 类或 PhotoImage 类的实例变量。

create_bitmap(x, y, bitmap)

创建一个位图;参数 x 与 y 定义位图的左上角坐标;参数 bitmap 定义位图的来源,参数值可以是 gray12、gray25、gray50、gray75、hourglass、error、questhead、info、warning 或 question,或者也可以直接使用 XBM(X Bitmap)类型的文件,此时需要在 XBM 文件名称前添加一个 @ 符号,例如 bitmap=@hello.xbm

create_arc(coord, start, extent, fill)

绘制一个弧形;参数 coord 定义画弧形区块的左上角与右下角坐标;参数 start 定义画弧形区块的起始角度(逆时针方向);参数 extent 定义画弧形区块的结束角度(逆时针方向);参数 fill 定义填充弧形区块的颜色。

creat_window

绘制组件

注意:上述方法都会返回一个画布对象的唯一 ID。关于 options 参数,下面会通过一个示例对经常使用的参数做相关介绍。(但由于可选参数较多,并且每个方法中的参数作用大同小异,因此对它们不再逐一列举)

从上述表格不难看出,Canvas 控件采用了坐标系的方式来确定画布中的每一点。一般情况下,默认主窗口的左上角为坐标原点,这种坐标系被称作为“窗口坐标系”,但也会存在另外一种情况,即画布的大小可能大于主窗口,当发生这种情况的时,可以采用带滚动条的 Canvas 控件,此时会以画布的左上角为坐标原点,我们将这种坐标系称为“画布坐标系”。
需要注意的是,creat_arc绘制弧时,和creat_oval的用法类似,因为弧是椭圆的一部分,因此同样也是指定左上角和右下角两个点的坐标,默认总是绘制从3点(0)开始,逆时针旋转90°的那一段弧,程序也可通过start改变起始角度,通过extent改变转过的角度。

在绘制实例的时候,也可以指定一些参数,比如 fill、dash、arrow 等,大多数实例具有相同的参数,下表对 create_line() 函数的相关参数做了简单介绍:

属性

说明

activedash

当画布对象状态为 “active” 的时候,绘制虚线

activefill

当画布对象状态为 “active” 的时候,填充颜色

stipple

指定一个位图进行填充,默认值为空字符串,表示实心

activestipple

当画布对象状态为 “active” 的时候,指定填充的位图

activewidth

当画布对象状态为 “active” 的时候,指定边框的宽度

arrow

默认线段是不带箭头的"NONE",通过设置该选项添加箭头到线段中,“first” 表示添加箭头到线段开始的位置, “last” 表示添加箭头到线段结束的位置,“both” 表示两端均添加箭头

arrowshape

用一个三元组来指定箭头的形状,默认值是 (8, 10, 3),元组中的数值分别代表箭头中三条边的长度,即填充长度,箭头长度,箭头宽度

capstyle

指定线段两端的样式,默认值是 “butt”(线段的两段平切于起点和终点),“projecting”(线段的两段在起点和终点的位置将 width 选项设置的长度分别延长一半),“round”(线段的两段在起点和终点的位置将 width设置的长度分别延长一半,并以圆角进行绘制)

dash

绘制虚线,该选项值是一个整数元组,元组中的元素分别代表短线的长度和间隔,比如 (3, 5) 代表 3 个像素的短线和 5 个像素的间隔

dashoffset

指定虚线开始的偏移位置,比如 dash=(5, 1, 2, 1),dashoffset=3,则从 2 开始画虚线

disableddash

当画布对象状态为 “disabled” 的时候,绘制虚线

disabledfill

当画布对象状态为 “disabled” 的时候,填充颜色

disabledstipple

当画布对象状态为 “disabled” 的时候,指定填充的位图

disabledwidth

当画布对象状态为 “disabled” 的时候,指定边框的宽度

fill

指定填充的颜色,空字符串表示透明

joinstyle

指定当绘制两个相邻线段之间时接口的样式,默认为 “round”(以连接点为圆心,1/2 width 选项设置的长度为半径来绘制圆角),“bevel”(在连接点处将两线段的夹角做平切操作),“miter”(沿着两线段的夹角延伸至一个点),

offset

指定当点画模式时填充位图的偏移

smooth

默认值为 False,若设置为 True,表示将以曲线的样式代替所绘线段

splinesteps

当绘制曲线的时,该选项指定由多少条折线来构成曲线,默认值是 12,这里需要注意,只有当 smooth 选项为 True 时该选项才会生效。

state

指定该画布对象的状态,默认值为 “normal”,参数值有 “normal”,“disabled”(不可用)和 “hidden”(隐藏)三种状态。

tags

为创建的画布对象添加标签

width

指定边框的宽度

对于扇形、矩形、三角形、圆形等,这些封闭式图形,它们由轮廓线和填充颜色两部分组成。在绘制这些图形时相关函数的可选参数与上述表格也存在略微差异,下面以绘制扇形的 create_arc() 函数为例,简单的介绍额外增加的一些参数:

属性

方法

activeoutline

当画布对象状态为 “active” 的时候,绘制轮廓线

activeoutlinestipple

当画布对象状态为 “active” 的时候,指定填充轮廓的位图

disabledoutline

当画布对象状态为 “disabled” 的时候,绘制轮廓线

disabledoutlinestipple

当画布对象状态为 “disabled” 的时候,指定填充轮廓的位图

start

指定起始位置的偏移角度

extent

指定跨度(从 start 选项指定的位置开始到结束位置的角度)默认值是 90.0

outline

指定轮廓的颜色

outlineoffset

指定当点画模式绘制轮廓时位图的偏移

outlinestipple

当 outline 选项被设置时,该选项用于指定一个位图来填充边框,默认值是空字符串,表示黑色

style

默认创建的是扇形,指定该方法创建的是扇形(“pieslice”)、弓形(“chord”)还是弧形(“arc”)

anchor

指定绘制文字、GUI组件的位置。仅对creat_text()、creat_windows()有效

justify

指定文字的对齐方式,该选项支持CENTER\LEFT\RIGHT常量值,仅对creat_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.creat_text((130+i*140,20),test=st,font=columnFont,fill='gray',anchor=W,justify=LEFT)
#1.绘制文字(矩形)
cv.creat_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.creat_rectangle(130+i*140,50,240+i*140,120,width=op[0],fill=op[1],outline=op[2],stipple=op[3])
#
#2.绘制文字(椭圆)
cv.creat_text(10,160,text='绘制椭圆',font=titleFont,fill='magenta',anchor=W,justify=LEFT)
#采用循环绘制5个椭圆
for i ,op in enumerate(options):
    cv.creat_oval(130+i*140,150,240+i*140,220,width=op[0],fill=op[1],outline=op[2],stipple=op[3])
#
#3.绘制文字(多边形)
cv.creat_text(10,260,text='绘制多边形',font=titleFont,fill='magenta',anchor=W,justify=LEFT)
#采用循环绘制5个多边形
for i ,op in enumerate(options):
    cv.creat_polygon(130+i*140,320,240+i*140,250,width=op[0],fill=op[1],outline=op[2],stipple=op[3])
#
#4.绘制文字(扇形)
cv.creat_text(10,360,text='绘制扇形',font=titleFont,fill='magenta',anchor=W,justify=LEFT)
#采用循环绘制5个扇形
for i ,op in enumerate(options):
    cv.creat_arc(130+i*140,350,240+i*140,420,width=op[0],fill=op[1],outline=op[2],stipple=op[3])
#
#5.绘制文字(弓形)
cv.creat_text(10,460,text='绘制弓形',font=titleFont,fill='magenta',anchor=W,justify=LEFT)
#采用循环绘制5个弓形
for i ,op in enumerate(options):
    cv.creat_arc(130+i*140,350,450+i*140,520,width=op[0],fill=op[1],outline=op[2],stipple=op[3],start=30,extent=60,style=CHORD)
#
#6.绘制文字(弧)
cv.creat_text(10,560,text='绘制弧形',font=titleFont,fill='magenta',anchor=W,justify=LEFT)
#采用循环绘制5个弧形
for i ,op in enumerate(options):
    cv.creat_arc(130+i*140,350,450+i*140,520,width=op[0],fill=op[1],outline=op[2],stipple=op[3],start=30,extent=60,style=ARC)
#
#7.绘制文字(直线)
cv.creat_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.creat_line(130+i*140,350,650+i*140,720,width=op[0],fill=op[1],stipple=op[2],arrow=op[3],arrowshape=op[4])
#
#8.绘制文字(位图)
cv.creat_text(10,760,text='绘制位图\n图片、组件',font=titleFont,fill='magenta',anchor=W,justify=LEFT)
#定义包括creat_bitmap、creat_image、creat_window三个方法的数组
funcs = [Cnavas.creat_bitmap、Canvas.creat_image、Canvas.creat_window]
#为上面三个方法定义选项
items = [{'bitmap':'questhead'},{'image':PhotoImage(file='image/logo.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,**item[i])
root.mainloop()

5.2 图形项标签

在Canvas中通过creat方法绘制的图形项并不是完全静止的图形,往往是很多个图形’堆叠‘在一起才构成了我们想要的界面,后绘制的图形会覆盖先绘制的,有时候我们想动态修改某一个图形项,就需要引用这个图形项,那么我们怎么才能选择到所需修改的图形项呢?实际上,获取这些图形项的引用方式有两种:

  1. 通过图形项的id,也就是Canvas执行creat方法的返回值,一般来说会依次返回1、2、3等整数id
  2. 通过图形项的tag(标签)

建立或修改tag标签有两种方式:

  1. 在调用creat方法时传入tag选项
  2. 调用方法为图像添加、删除tag
    下表总结了tag相关的方法:

方法

描述

addtag_above(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

addtag_overlapping(self,newtag,x1,y1,x2,y2)

为与指定矩形区域重叠的最上面的图形项添加tag

addtag_withtag(self,newtag,tagOrId)

为tagOrId对应的图形项添加新tag

addtag_dtag(self,*args)

删除指定图形项的tag

addtag_gettags(self,*args)

获取指定图形项的所有tag

addtag_withtag(self,tagOrId)

为tagOrId对应的所有图形项

示例程序:

from thinter import *
root = Tk()
root.title('操作标签')
cv.Canvas(root,background='white',width=620,height=250)
cv.pack(fill=BOTH,wxpand=YES)
#绘制一个矩形
rt = cv.creat_rectangle(40,40,300,220,outline='blue',width=2,tag=('t1','t2','t3','tag4'))
#访问图形项的id,也就是编号
print(rt)    #1
#绘制一个椭圆
oval = cv.creat_oval(350,50,580,200,fill='yellow',,width=0,tag=('g1','g2','g3','tag4'))
print(oval)   #2

#根据指定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')
#删除tag
cv.dtag(1,'t1')
cv.dtag(2,'g1')
print(cv.gettags(rt)) #('t2','t3','tag4')
print(cv.gettags(2)) #('g2','g3','tag4')
#为所有图形添加tag
cv.addtag_all('t5')
#为指定图形添加tag
cv.addtag_withtag('t6','g2')
print(cv.gettags(rt)) #('t2','t3','tag4','t5')
print(cv.gettags(2)) #('g2','g3','tag4','t5','t6')

其他的方法类似,此处不多举例。

5.3 操作图形项

在Canvas中获取图形项之后,接下来可通过Canvas提供的大量方法来操作图形项。
下表列出了常用的方法:

方法

描述

find_above(self,tagOrId)

返回tagOrId对应图形相关的上一个图形项

find_all(self)

返回全部图形项

find_below(self,tagOrId)

返回tagOrId对应图形项的下一个图形

find_colsest(self,x,y)

返回和x,y点最接近的图形项

find_enclosed(self,x1,y1,x2,y2)

返回位于指定矩形区域内最上面的图形项

find_overlapping(self,x1,y1.x2,y2)

返回与指定矩形区域重叠的最上面的图形项

find_withtag(self,tagOrId)

返回tagOrId对应的全部图形项

tag_lower(self,*args(,lower))

将args的第一个参数对应的图形移动到最底层,也可额外指定lower参数,移动到lower的下面

tag_raise(self,*args(,lift))

将args的第一个参数对应的图形移动到最顶层,也可额外指定lift参数,移动到lift的下面

itemcget(self,tagOrId,option)

获取tagOrId的option选项值

itemconfig(self,tagOrId,cnf=None,**kw)

为tagOrId配置选项

itemconfigure

同上一方法

coords(self,*args)

重置图形项大小和位置

move(self,*args)

移动图形项

scale(self,*args)

缩放args传入4个值,前两个指定缩放中心,后两个指定缩放比

delete(self,*args)

删除指定id或tag对应的全部图形项

dchars(self,*args)

删除文字图形项中间的部分文字

5.4 图形项事件绑定

Canvas提供了一个tag_bind()方法,该方法用于为指定图形项绑定事件出苦力函数或方法。
下面通过一个稍微复杂一点的示例讲解:

from thinter import *

from thinter import ttk
from tkinter import colorchooser 
import threading

class App:
    def __init__(self,master)
        self.master = master
        #保存设置初始的边框宽度
        self.width = IntVar()
        self.width.set(1)
        #保存设置初始的边框颜色
        self.outline = 'black'
        #保存设置的初始填充颜色
        self.fill = None
        # 记录拖动时前一个点的x\y坐标
        self.prevx = self.prevy = -10
        # 记录拖动开始的第一个点的x、y坐标
        self.firstx = self.firsty = -10
        # 记录拖动右键来移动图形时,前一个点的x、y坐标
        self.mv_prevx = self.mv_prevy = -10
        #用item_type记录要绘制哪种图形
        self.item_type = 0
        self.points = []
        self.init_widgets()
        self.temp_item = None
        self.temp_items = []
        # 初始化所选择的图形项
        self.choose_item = None
    #创建界面组件
    def init_widgets(self):
        self.cv = Canvas(self.master,background='white')
        self.cv.pack(fill=BOTH,expand=Ture)
        #为鼠标左键拖动事件,鼠标左键释放事件绑定处理函数
        self.cv.bind('<B1-Motion>',self.drag_handler)
        self.cv.bind('<ButtonRelease-1>',self.release_handler)
        #为鼠标左键双击事件绑定事件处理函数
        self.cv.bind('<Double-1>',self.double_handler)
        f = ttk.Frame(self.master)
        f.pack(fill=X)
        self.bns = []
        #采用循环创建多个按钮,用于绘制不同的图形
        for i ,lb in enumerate(('直线','矩形','椭圆','多边形','铅笔')):
            bn = Button(f,text=lb,command=lambda i=i:self.choose_type(i))
            bn.pack(side=LEFT,ipadx=8,ipady=5,padx=5)
            self.bns.append(bn)
        #默认选择直线,将按钮设置成下沉显示
        self.bns[self.item_type]['relief'] = SUNKEN
        ttk.Button(f,text='边框颜色',command=self.choose_fill).pack(side=LEFT,ipadx=8,ipady=5,padx=5)
        om = ttk.OptionMenu(f,self.width,#绑定变量
        '1',#设置初始选择值
        '0',#以下多个值用于设置菜单项
        '1','2','3','4','5','6','7','8',command=None)
        om.pack(side=LEFT,ipadx=8,ipady=5,padx=5)
    def choose_type(self,i):
        #将所有的按钮恢复为默认状态,按钮显示突出
        for b in self.bns:b['relief'] = RAISED
        #将当前按钮设置为选择样式
        self.bns[i]['relief'] = SUNKEN
        #设置要绘制的图形
        self.item_type = i
    #处理选择边框颜色的方法
    def choose_outline(self):
        #弹出颜色选择对话框
        select_color = colorchooser.askcolor(parent=self.master,title="请选择边框颜色",color=self.outline)
        if select_color is not None:
            self.outline = select_color[1]
    # 处理选择填充颜色方法
    def choose_fill(self):
        #弹出颜色选择对话框
        select_color = colorchooser.askcolor(parent=self.master,title="请选择填充颜色",color=self.fill)
        if select_color is not None:
            self.fill = select_color[1]
        else:
            self.fill = None
    # 左键拖动事件
    def drag_handler(self,event):
        #如果绘制直线
        if self.item_type == 0:
            #如果第一个点不存在(self.firstx和self.firsty都小于0)
            if self.firstx < -1 and self.firsty < -1:
                self.firstx,self.firsty = event.x,event.y
            #删除上一次绘制的虚线图形
            if self.temp_item is not None:
                self.cv.delete(self.temp_item)
            #重新绘制虚线
            self.temp_item = self.cv.creat_line(self.firstx,self.firsty,event.x,event.y,dash=2)
        #如果绘制矩形或椭圆形
        if self.iten_type ==1 or self.item_type ==2:
            # 如果第一个点不存在
            if self.firstx < -1 and self.firsty <-1:
                self.firstx,self.firsty = event.x,event.y
            if self.temp_item is not None:
                self.cv.delete(self.temp_item)
            leftx,lefty = min(self.firstx,eventx),min(self.firsty,event.y)
            rightx,righty = max(self.firstx,event.x),max(self.firsty,event.y)
            #重新绘制虚线选择框
            self.temp_item = self.cv.creat_rectangle(leftx,lefty,rightx,righty,dash=2)
        if self.item_type ==3:
            self.draw_polygon = True
            # 如果第一个点不存在
            if self.firstx < -1 and self.firsty < -1:
                self.firstx,self.firsty = event.x,event.y
            #删除上一次绘制的虚线图形
            if self.temp_item is not None :
                self.cv.delete(self.temp_item)
            #重新绘制虚线
            self.temp_item = self.cv.create_line(self.firstx,self.firsty,event.x,event.y,dash=2)
        if self.item_type ==4:
            #如果前一个点存在
            if self.prevx >0 and self.pervy > 0:
                self.cv.creat_line(self.prevx,self.prevy,event.x,event.y,fill=self.outline,width=self.width.get())
            self.prevx,self.prevy = event.x,event.y
    def item_bind(self,t):
        #为鼠标右键拖动事件绑定事件处理函数
        self.cv.tag_bind(t,'<B3-Motion>',self.move)
        #为鼠标右键释放绑定事件处理函数
        self.cv.tag_bind(t,'<ButtonRelease-3>',self.move_end)
    def release_handler(self,event):
        #删除临时绘制的虚线图形项
        if self.temp_item is nor None:
            #如果不是绘制多边形
            if self.item_type != 3:
                self.cv.delete(self.temp_item)
            #如果绘制多边形,将之前绘制的虚线先保存下来,一边后面删除他们
            else:
                self.temp_items.append(self.temp_item)
        self.temp_item = None
        # 如果绘制直线
        if self.item_type == 0:
            #如果第一个点存在
            if self.firstx > 0 and self.firsty > 0:
                #绘制实际的直线
                t = self.cv.create_line(self.firstx,self.firsty,event.x,event.y,fill=self.outline,width=self.width.get())
                #为鼠标左键单击事件绑定事件处理函数,用于选择被单击的图形项
                self.cv.tag_bind(t,'<Button-1>',lambda event=event,t=t:self.choose_item_handler(event,t))
                self.item_bind(t)
        # 如果绘制矩形或椭圆
        if self.item_type == 1 or self.item_type == 2:
            #如果第一个点存在
            if self.firstx > 0 and self.firsty > 0:
                leftx,lefty = min(self.firstx,event.x),min(self.firsty,event.y)
                rightx,righty = max(self.firstx,event.x),max(self.firsty,event.y)
                if self.item_type == 1:
                    # 绘制实际的矩形
                    t = self.cv.creat_rtctangle(leftx,lefty,rightx,tighty,outline=self.outline,fill=self.fill,width=self.width.get())
                if self.item_type == 2:
                    #绘制实际的椭圆
                    t = self.cv.creat_oval(leftx,lefty,rightx,righty,outline=self.outline,fill=self.fill,width=self.width.get())
                #为鼠标左键单击事件绑定事件处理函数,用于选择被单击的图形项
                self.cv.tag_bind(t,'<Button-1>',lambda event=event,t=t,self.choose_item_handler(event,t))
                self.item_bind(t)
        if self.item_type != 3:
            self.prevx = self.prevy =-10
            self.firstx = self.firsty = -10
        #如果横着绘制多边形
        elif(self.draw_polygon):
            # 将第一个点添加到列表中
            self.points.append((self.firstx,self.firsty))
            self.firstx,self.firsty = event.x,event.y
    def double_handler(self,event(:
        # 只处理绘制多边形的情形
        if self.item_type == 3:
            t = self.cv.creat_polygon(*self.points,outline=self.outline,fill="" if self.fill is None else self.fill,width=self.width.get())
            #为鼠标左键单击事件绑定事件处理函数,用于选择被单击的图形项
            self.cv.tag_bind(t,'<Button-1>',lambda event=event,t=t:self.choose_item_handler(event,t))
            self.item_bind(t)
            # 清空所有板寸的点数据
            self.points.clear()
            #将self.firstx=self.firsty设置-10,停止绘制
            self.firstx = self.firsty = -10
            #删除所有临时的虚线框
            for it in self.temp_items:self.cv.delete(it)
            self.temp_item.clear()
            self.draw_polygon = False
    #根据传入的参数t来选择对应的图形项
    def choose_item_handler(self,event,t):
        #使用self.choose_item保存当前选择项
        self.choost_item = t
    #定义移动图形项的方法
    def move(self,event):
        # 只有当被选择的图形项不为空是,才可以执行移动
        if self.choose_item is not None:
            #如果前一个带能存在
            if self.mv_prevx > 0 and self.prevy > 0:
                # 移动所选择的图形项
                self.cv.move(self.choose_item,event.x-self.mv_prevx,event.y - self,mv_prevy)
            self.mv_prevx,self.prevy = event.x,event.y
    #结束移动的方法
    def move_end(self,event):
        self.mv_prevx = self.mv_prevy = -10
    def delete_item(self,event):
        #如果被选择的图形项不空,则删除被选择的图形项
        if self.choose_item is not None:
            self.cv.delete(self.choose_item)
root = Tk()
root.title("绘图工具")
root.geometry('800*680')
app = App(root)
root.bind('<Delete>',app.delete_item)
root.mainloop()

读者可以自己试验程序的效果,但是此程序依然不是很完善,有兴趣的可以自己改进。

5.5 绘制动画

实现动画效果的原理很简单,程序只需要增加一个定时器,周期性地改变界面上图形的颜色、大小、位置等选型,用户看上去就是所谓的’动画‘了。
下面以桌面弹球程序为例进行讲解,此程序涉及两个动画:

  1. 小球转动:小球转动是一个’组帧动画‘,程序会循环显示多张转动的小球图片,这样用户就会看到小球转动的效果。
  2. 小球移动:只要改变小球的坐标程序就可以控制小球移动
    为了让用户控制挡板移动,程序还为Canvas的向左、向右箭头绑定了时间处理函数。
from thinter import *
import threading
import random

GAME_WIDTH = 500
GAME_HIGHT = 680
BOARD_X = 230 #挡球板横坐标
BOARD_Y = 600 #挡球板纵坐标
BOARD_WIDTH = 80
BALL_RADIUS = 9
class App:
    def __init__(self,master):
        self.master = master
        #记录小球动画的第几帧
        self.ball_index = 0
        #记录游戏是否失败旗标
        self.is_lose = False
        #初始化记录小球位置的变量
        self.curx = 260
        self.cury = 30
        self.boardx = BOARD_X
        self.vx = random.randint(3,6)#x方向的速度
        self.vy = random.randint(5,10) #y方向的速度
        #通过定时器指定0.1s之后执行moveball函数
        self.t = threading.Timer(0.1,self.moveball)
        self.t.start()
    #创建界面组件
    #创建界面组件
    def init_widgets(self):
        self.cv = Canvas(self.master,background='white',width=GAME_WIDTH,heifht=GAME_HEIGHT)
        self.cv.pack()
        #让画布得到焦点,从而可以响应按键事件
        self.cv.focus_set()
        self.cv.bms = []
        #初始化小球的动画帧
        for i in range(8):
            self.cv.bms.append(PhotoImage(file='images\ball_'+str(i+1)+'.gif'))
        #绘制小球
        self.ball = self.cv.create_image(self.curx,self.cury,image=self.cv.bms[self.ball_index])
        self.board = self.cv.create_rectangle(BOARD_X,BOARD_Y,BOARD_X+BOARD_WIDTH,BOARD_Y+20,width=0,fill='lightblue')
        #为向左箭头绑定事件处理函数,挡板左移
        self.cv.bind('<Left>',self.move_left)
        #为向右箭头绑定事件处理函数,挡板右移
        self.cv.bind('<Right>',self.move_right)
    def move_left(self,event):
        if self.boardx <=0:
            return
        self.boardx -= 5
        self.cv.coords(self.board,self.boardx,BOARD_Y,self.boardx + BOARD_Y+20)
    def move_right(self,event):
        if self.boardx +BOARD_WIDTH >=GAME_WIDTH:
            return
        self.boardx +=5
        self.cv.coords(self.board,self.boardx,BOARD_Y,self.boardx+BOARD_WIDTH,BOARD_Y+20)
    def moveball(self):
        self.curx += self.vx
        self.cury += self.vy
        #小球到了右边墙壁,转向
        if self.curx + BALL_RADIUS >= GAME_WIDTH:
            self.vx = -self.vx
        # 小球到了左边墙壁,转向
        if self.curx - BALL_RADIUS <= 0:
            self.vx = -self.vx
        # 小球到了上边墙壁,转向
        if self.cury - BALL_RADIUS <=0:
            self.vy = -self.vy
        # 小球到了挡板出
        if self.cury +BALL_RADIUS >= BOARD_Y:
            # 如果在挡板范围内
            if self.boardx <= self.curx <= (self.boardx + BOARD_WIDTH):
                self.vy = -self.vy
            else:
                messagebox.showinfo(title='失败',message='您已经输了')
                self.is_lose = Ture
        self.cv.coords(self.ball,self.curx,self.cury)
        self.ball_index += 1
        self.cv.itemconfig(self.ball,image=self.cv.bms[self.ball_index % 8])
        #如果游戏还未失败,让定时器继续执行
        if not self.is_lose:
            #通过定时器指定0.1s之后执行moveball函数
            self.t = threading.Timer(0.1,self.moveball)
            self.t.start()
   root = Tk()
   root.title("弹球游戏")
   root.geometry('%dx%d'%(GAME_WIDTH,GAME_HEIGHT))
   #禁止改变窗口大小
   root.resizable(width=False,height=False)
   App(root)
   root.mainloop()

如果对开发游戏感兴趣,建议使用专业的游戏工具库,比如Pygame.