五、Tkinter创建图像界面5
5.1 在Canvas中绘制图形
Tkinter 提供了Canvas组件来实现绘图,它可以实现的功能如下:
- 绘制图形,如弧线、直线、圆形、多边形、椭圆等几何图形。
- 展示图片(包括位图)
- 绘制图片、文字、UI组件(如Button)等
- 改变所绘制的“画布对象”(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方法绘制的图形项并不是完全静止的图形,往往是很多个图形’堆叠‘在一起才构成了我们想要的界面,后绘制的图形会覆盖先绘制的,有时候我们想动态修改某一个图形项,就需要引用这个图形项,那么我们怎么才能选择到所需修改的图形项呢?实际上,获取这些图形项的引用方式有两种:
- 通过图形项的id,也就是Canvas执行creat方法的返回值,一般来说会依次返回1、2、3等整数id
- 通过图形项的tag(标签)
建立或修改tag标签有两种方式:
- 在调用creat方法时传入tag选项
- 调用方法为图像添加、删除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 绘制动画
实现动画效果的原理很简单,程序只需要增加一个定时器,周期性地改变界面上图形的颜色、大小、位置等选型,用户看上去就是所谓的’动画‘了。
下面以桌面弹球程序为例进行讲解,此程序涉及两个动画:
- 小球转动:小球转动是一个’组帧动画‘,程序会循环显示多张转动的小球图片,这样用户就会看到小球转动的效果。
- 小球移动:只要改变小球的坐标程序就可以控制小球移动
为了让用户控制挡板移动,程序还为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.