tkinter.Canvas 示例#

画布(canvas)小部件管理着 2D 图形对象(lines, circles, images, 其他更多小部件)组成的集合。Canvas 小部件是经典的 Tk 小部件,不是 Ttk 小部件。

创建方法是:

from tkinter import Canvas
canvas = Canvas(parent)

注意:在 Canvas 中的坐标系是以左上角作为原点 \((0,0)\),水平向右为 \(x\) 轴正方向,垂直向下为 \(y\) 轴正方向。

创建线段#

创建线段是以 \((x_0, y_0, x_1, y_1)\) 的形式传入 creata_line 函数的。其中 \((x_0, y_0)\)\((x_1, y_1)\) 分别为起点和终点。比如:

item_id = canvas.create_line(10, 10, 200, 50)

函数 creata_line 的返回值 item_id 是一个整数,被用来作为该对象的引用的唯一标识。

下面看一个例子:

class Segment(Canvas):
    def __init__(self, master=None, **kw):
        super().__init__(master=master, **kw)
        self.lastx, self.lasty = 0, 0
        self.bind("<Button-1>", self.xy) # 绑定鼠标左键
        self.bind("<B1-Motion>", self.add_line) # 拖动鼠标左键
        
    def xy(self, event):
        '''更新坐标'''
        self.lastx, self.lasty = event.x, event.y
        
    def add_line(self, event):
        '''画一条线段'''
        self.create_line(self.lastx, self.lasty, event.x, event.y, fill='red', width=3)
        self.xy(event)
    
    def layout(self):
        self.grid(column=0, row=0, sticky='nwes')

root = Tk()
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
segment = Segment(root)
segment.layout()
root.mainloop()

该例子实现了在画布上拖动鼠标左键来画线段的目标。其中的参数 width 表示线段的宽度,fill 表示使用的画笔的颜色。

可以做如下修改:

from tkinter import IntVar

class App(Tk):
    def __init__(self):
        super().__init__()
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.segment = Segment(self)
        self.id_var = IntVar()
        
    def modify(self):
        item_id = self.id_var.get()
        self.segment.itemconfigure(item_id, fill='blue', width=10)
        
    def layout(self):
        entry = ttk.Entry(textvariable=self.id_var)
        button = ttk.Button(text='modify', command=self.modify)
        self.segment.layout()
        entry.grid(column=0, row=1)
        button.grid(column=1, row=1)
        
app = App()
app.layout()
app.mainloop()

效果图见图1:

图1 Canvas 的一个例子

只要写入文本框数字,然后点击按钮 modify,便可 使用 Canvas.itemconfigure 函数修改 item_id 对应的点的配置。

绑定 item_id#

除了可以使用 bind 函数绑定事件之外,还可以使用 tag_bind 函数绑定 item_id 来触发事件。

下面的代码可以使用鼠标点击颜色选择块来切换画笔的颜色:

class Segment(Canvas):
    def __init__(self, master=None, **kw):
        super().__init__(master=master, **kw)
        self.lastx, self.lasty = 0, 0
        self.color = "black"
        self.bind("<Button-1>", self.xy)
        self.bind("<B1-Motion>", self.add_line)
        
    def set_color(self, new_color):
        self.color = new_color
    
    def xy(self, event):
        '''更新坐标'''
        self.lastx, self.lasty = event.x, event.y
        
    def add_line(self, event):
        self.create_line((self.lastx, self.lasty, event.x, event.y), fill=self.color)
        self.xy(event)

    def change(self):
        # 创建 3 个颜色选择块
        red_id = self.create_rectangle((10, 10, 30, 30), fill="red")
        blue_id = self.create_rectangle((10, 35, 30, 55), fill="blue")
        black_id = self.create_rectangle((10, 60, 30, 80), fill="black")
        # 绑定事件
        self.tag_bind(red_id, "<Button-1>", lambda x: self.set_color("red"))
        self.tag_bind(blue_id, "<Button-1>", lambda x: self.set_color("blue"))
        self.tag_bind(black_id, "<Button-1>", lambda x: self.set_color("black"))
    
root = Tk()
seg = Segment(root)
seg.grid()
seg.change()
root.mainloop()

效果图:

图2 可改变颜色的画布

标记:Tags#

使用配置选项 "tags" 可以为任何一个 item_id 代表的对象做标记,这里的标记可以是一个或者多个。

为什么要做标记呢?这是因为,做了标记的 item_id 将会更方便管理。比如,您可以指定所有被标记为 "line" 的 item_id 统一修改其画笔颜色。

除了使用配置选项 "tags" 创建标记之外,您可以在 item_id 创建之后使用 add_tag 函数创建或者添加新的标记。移除标记可以使用方法 dtag。您也可以使用 gettags(item_id) 方式获取 item_id 的全部标记列表。可以使用 find_withtag 函数获取指定的标记的全部 item_id 列表。

直接看一个例子:

class Segment(Canvas):
    def __init__(self, master=None, **kw):
        super().__init__(master=master, **kw)
        self.lastx, self.lasty = 0, 0
        self.color = "黑色"
        self.bind("<Button-1>", self.xy)
        self.bind("<B1-Motion>", self.add_line)
        # 设定调色板的线宽
        self.itemconfigure('调色板', width=5) 
        # 释放鼠标触发事件
        self.bind('<B1-ButtonRelease>', self.done_stroke)
    
    @property
    def color_map(self):
        return {
            '红色': 'red',
            '蓝色': 'blue',
            '黑色': 'black'
        }
        
    def set_color(self, new_color):
        self.color = new_color
        self.dtag('all', '被选中的调色板')
        self.itemconfigure('调色板', outline='white')
        self.addtag('被选中的调色板', 'withtag', f"{self.color}调色板")
        self.itemconfigure('被选中的调色板', outline='#999999')
    
    def xy(self, event):
        '''更新坐标'''
        self.lastx, self.lasty = event.x, event.y
        
    def add_line(self, event):
        loc = (self.lastx, self.lasty, event.x, event.y)
        color = self.color_map[self.color]
        self.create_line(loc, fill=color, width=5, tags='当前的线段')
        self.xy(event)
        
    def palette(self, loc, color):
        kw = {
            'fill': color,
            'tags': ('调色板', f'{color}调色板')
        }
        return self.create_rectangle(loc, **kw)

    def change(self):
        # 创建 3 个颜色选择块
        red_id = self.palette((10, 10, 30, 30), "red")
        blue_id = self.palette((10, 35, 30, 55), "blue")
        black_id = self.palette((10, 60, 30, 80), "black")
        # 添加标记
        self.addtag('被选中的调色板', 'withtag', black_id)
        # 绑定事件
        self.tag_bind(red_id, "<Button-1>", lambda x: self.set_color("红色"))
        self.tag_bind(blue_id, "<Button-1>", lambda x: self.set_color("蓝色"))
        self.tag_bind(black_id, "<Button-1>", lambda x: self.set_color("黑色"))
        
    def done_stroke(self, event):
        self.itemconfigure('当前的线段', width=1)
    
root = Tk()
seg = Segment(root)
seg.grid()
seg.change()
root.mainloop()

显示的效果图:

图3 可改变线条的画布

该例子展示了一种使用 tags 修改 item 的手段,达到的效果是:在鼠标释放之前线条比较粗,释放之后线条会变细。

修改 item#

我们可以使用 delete 方法删除 item,使用 coords 方法改变 item 的尺寸和位置(允许变换坐标系)。可以使用 move 方法移动 item。还有 "raise""lower" 方法可以改变不同画布的排列布局。比如:

改变 item 属性#

from tkinter import *

root=Tk()

cv=Canvas(root,bg='white')
rt1=cv.create_rectangle(10,10,110,110,tags=('r1','r2','r3'))
rt2=cv.create_rectangle(20,20,80,80,tags=('s1','s2','s3'))
rt3=cv.create_rectangle(30,30,70,70,tags=('y1','y2','y3'))

cv.tag_lower(rt3)
cv.tag_raise(rt1)
cv.itemconfig(cv.find_above(rt2),outline='red')
cv.itemconfig(cv.find_below(rt2),outline='green')
cv.pack()

root.mainloop()

删除 item#

root=Tk()

cv=Canvas(root,bg='white')
rt1=cv.create_rectangle(10,10,110,110,tags=('r1','r2','r3'))
rt2=cv.create_rectangle(20,20,80,80,tags=('s1','s2','s3'))
rt3=cv.create_rectangle(30,30,70,70,tags=('s1','y2','y3'))

cv.delete(rt1)
cv.delete('s1')
cv.pack()

root.mainloop()

缩放 item#

root=Tk()
cv=Canvas(root,bg='white')
rt1=cv.create_rectangle(10,10,110,110,tags=('r1','r2','r3'))
cv.scale(rt1,0,0,1,2)
cv.pack()

root.mainloop()

滚动鼠标#

在许多应用程序中,您希望画布大于屏幕上显示的内容。 您可以通过 “xview” 和 “yview” 方法以通常的方式将水平和垂直滚动条附加到画布上。

至于画布的大小,您既可以指定希望在屏幕上显示的大小,也可以指定需要滚动才能看到的画布的完整大小。 画布小部件的 “width” 和 “height” 配置选项将从几何管理器请求给定的空间量。“scrollregion” 配置选项(例如 “0 0 1000 1000”)告诉 Tk 画布表面有多大。

针对鼠标滚动,“canvasx” 和 “canvasy” 方法会将屏幕上的位置(正在报告的绑定)转换为画布上的实际点。如果将它们直接添加到事件绑定中(而不是从事件绑定中调用),请注意引用和替换,以确保在事件触发时完成转换。

下面的例子很好的说明了这种机制:

class SegmentScroll(Segment):
    def __init__(self, master=None, **kw):
        super().__init__(master=master, **kw)
        self.master = master
        
    def scroll(self):
        self._h = ttk.Scrollbar(orient='horizontal')
        self._v = ttk.Scrollbar(orient='vertical')
        # 告诉 Tk 画布表面有多大
        self['scrollregion'] = (0, 0, 1000, 1000)
        self.itemconfig('scrollregion', )
        self.configure(yscrollcommand=self._v.set, xscrollcommand=self._h.set)
        self._h['command'] = self.xview
        self._v['command'] = self.yview
        
    def custom(self):
        ttk.Sizegrip(root).grid(column=1, row=1, sticky=(S,E))
        self.grid(column=0, row=0, sticky=(N,W,E,S))
        self._h.grid(column=0, row=1, sticky=(W,E))
        self._v.grid(column=1, row=0, sticky=(N,S))
        self.master.grid_columnconfigure(0, weight=1)
        self.master.grid_rowconfigure(0, weight=1)
        
    def xy(self, event):
        self.lastx, self.lasty = self.canvasx(event.x), self.canvasy(event.y)
        
    def add_line(self, event):
        x, y = self.canvasx(event.x), self.canvasy(event.y)
        color = self.color_map[self.color]
        self.create_line((self.lastx, self.lasty, x, y), fill=color, width=5, tags='当前的线段')
        self.xy(event)
        
root = Tk()
seg = SegmentScroll(root)
seg.change()
seg.scroll()
seg.custom()
root.mainloop()

显示效果:

图4 带滚动条和缩放的画布

更改线段的样式#

我们也可以设定参数 arrowarrowshape 来改变线段的样式:

root = Tk()

cv = Meta(root, bg='white')
d = [(0, 'none'), (1, 'first'), (2, 'last'), (3, 'both')]
for i in d:
    cv.create_line((10, 10+i[0]*20, 110, 110+i[0]*20),
                   arrow=i[1], arrowshape='40 30 10')

cv.grid()
root.mainloop()

显示效果图:

图5 画出不同样式的线段

还有:

root = Tk()

cv = Canvas(root, bg='white')
d = [(0, 'none', 'bevel'), (1, 'first', 'miter'),
     (2, 'last', 'round'), (3, 'both', 'round')]
for i in d:
    cv.create_line((10, 10+i[0]*20, 110, 110+i[0]*20),
                   arrow=i[1], arrowshape='8 10 3', joinstyle=i[2])

cv.grid()
root.mainloop()

效果图:

图6 直线的 joinstyle 参数设定

画布的其他设定#

Canvas 除了支持 “line”, “rectangle”,还支持 “oval”, “arc”, “polygon”, “bitmap” (位图,可用于分割物体), “image”,“text”,甚至还支持 “window” 的嵌入。

绘制位图#

root = Tk()
self = Canvas(root, background='white')
bitmap = ('error', 'info', 'question', 'hourglass',
          "warning", "gray12",
         "gray25", "gray50", "gray75", "questhead")

for k, name in enumerate(bitmap):
    location = [20*(k+1)]*2
    self.create_bitmap(location, bitmap=name)
    
self.grid()
root.mainloop()

图7 位图的绘制

绘制多边形#

root = Tk()
self = Canvas(root)
# 点的坐标
points = (10, 10), (10, 200), (90, 200), (200, 160)
self.create_polygon(points, fill='red')
    
self.grid()
root.mainloop()

图8 绘制多边形

绘制文本#

root = Tk()
self = Canvas(root)
location = 50, 50
text = self.create_text(location, text='一个文本:永不言败!', anchor='sw', fill='blue', font='italic 15')
# 选中文本
self.select_from(text, 5)
self.select_to(text, 8)
self.grid()
root.mainloop()

图9 绘制文本

创建组件#

from tkinter import Canvas, ttk, Tk

root = Tk()
self = Canvas(root)
def print_text():
    print("你好")
    
bt = ttk.Button(self, text='点我', command=print_text)
self.create_window((10, 10), window=bt, anchor='w')
self.create_line(30, 30, 50, 90)
self.grid()
root.mainloop()

图10 创建组件