画布#

画布小部件管理一个二维图形对象的集合——包括线条、圆形、文本、图像、其它小部件等。Tk的画布是一个异常强大且灵活的小部件,确实是Tk的一大亮点。它适用于广泛的应用场景,包括绘图或制图、CAD工具、显示或监控模拟或实际设备,以及使用更简单的小部件构建更复杂的小部件。

注意:画布小部件属于经典Tk小部件,而非主题化Tk小部件。

canvas = Canvas(parent, width=500, height=400, background='gray75')

您通常会提供宽度和高度,单位可以是像素或任何其他标准距离单元。一如既往,您可以要求几何管理器将其扩展到填满窗口中的可用空间。您可能还会为画布提供一个默认背景颜色,正如您在上一章中学到的那样指定颜色。画布小部件还支持其他外观选项,如浮雕和边框宽度等我们之前使用过的功能。

画布小部件拥有大量功能,我们这里不会全部覆盖。相反,我们将从一个简单示例开始,即一个自由手绘工具,并逐步添加新功能,每个功能都展示了画布小部件的另一个特性。

创建项目#

当你创建新画布小部件时,它本质上是一个没有任何内容的大矩形,换句话说,就是一张空白的画布。要让它变得有用,你需要向其中添加项目。你可以添加的项目类型多种多样。在这里,我们将向画布中添加一个简单的线条项目。

为了创建一条线,你需要指定它的起始和终止坐标。坐标表示为像素数,从左上角水平向右和垂直向下计算,即(x, y)。左上角的像素点被称为原点,其坐标为(0, 0)。“x”值随着向右移动而增加,“y”值随着向下移动而增加。一条线由两个点描述,我们将其称为(x0, y0)和(x1, y1)。以下代码创建了从(10, 5)到(200, 50)的一条线:

canvas.create_line(10, 5, 200, 50)

create_line方法返回一个项目ID(一个整数),这个ID唯一地指向这个项目。稍后我们会看到如何使用它。通常,我们不需要以后引用该项目,可以忽略返回的ID。

简单的绘图板#

让我们开始我们的简单绘图板示例。现在,我们将实现在画布上用鼠标进行自由手绘。我们创建一个画布小部件,并附加事件绑定以捕获鼠标点击和拖动。当鼠标首次按下时,我们将记住那个位置作为我们下一条线的“起点”。当鼠标按钮按住并移动时,我们从这个“起点”位置到当前鼠标位置创建一条线项目。这个当前位置成为下一条线项目的“起点”。每次鼠标拖动都会创建一个新的线项目。

from tkinter import *
from tkinter import ttk

def savePosn(event):
    global lastx, lasty
    lastx, lasty = event.x, event.y

def addLine(event):
    canvas.create_line((lastx, lasty, event.x, event.y))
    savePosn(event)

root = Tk()
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)

canvas = Canvas(root)
canvas.grid(column=0, row=0, sticky=(N, W, E, S))
canvas.bind("<Button-1>", savePosn)
canvas.bind("<B1-Motion>", addLine)

root.mainloop()

在这个简单的例子中,我们使用全局变量来存储起始位置。实际上,我们会将所有内容封装在 Python 类中。这里展示了一种实现方式。请注意,这个例子创建了一个Canvas的子类,它在代码中像处理其他任何小部件一样被对待。我们同样可以使用一个独立的类,就像我们在“英尺到米”的例子中所做的那样。

from tkinter import *
from tkinter import ttk

class Sketchpad(Canvas):
    def __init__(self, parent, **kwargs):
        super().__init__(parent, **kwargs)
        self.bind("<Button-1>", self.save_posn)
        self.bind("<B1-Motion>", self.add_line)
        
    def save_posn(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))
        self.save_posn(event)

root = Tk()
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)

sketch = Sketchpad(root)
sketch.grid(column=0, row=0, sticky=(N, W, E, S))

root.mainloop()

尝试一下——在画布上拖动鼠标,创作你心中的杰作。

项目属性#

在创建项目时,您还可以指定一个或多个项目属性,这些属性会影响其外观。例如,我们可以指定线条应该是红色且宽度为三像素。

canvas.create_line(10, 10, 200, 50, fill='red', width=3)

确切的属性集合将根据项目的类型而有所不同。一些常用的属性包括:

  • fill: 绘制对象的颜色。

  • width: 项目的线宽(或其轮廓)。

  • outline: 对于像矩形这样的填充形状,绘制项目轮廓的颜色。

  • dash: 绘制虚线而不是实线,例如,2 4 6 4交替短(2像素)和长(6像素)的虚线,每4像素间隔一次。

  • stipple: 使用图案而不是纯色填充,通常是gray75、gray50、gray25或gray12;目前macOS不支持stippling。

  • state: 分配 normal(默认)、disabled(忽略项目事件绑定)或 hidden(从显示中移除)的状态。

  • disabledfill, disabledwidth, …: 如果项目 state 设置为 disabled,该项目将使用这些通常属性的变体进行显示。

  • activefill, activewidth, …: 当鼠标指针悬停在项目上时,它将使用这些通常属性的变体进行显示。

如果您有会改变状态的画布项目通过同时创建常规和 `disabled*` 属性变体可以简化您的代码您只需更改项目的 `state`,而不是编写代码来更改多个显示属性这同样适用于 `active*` 属性变体这两种方法都鼓励一种更声明式的风格可以减少大量样板代码

就像Tk小部件一样,您可以在创建后更改画布项目的属性。

id = canvas.create_line(0, 0, 10, 10, fill='red')
...
canvas.itemconfigure(id, fill='blue', width=2)

项目类型#

画布小部件支持多种项目类型。

线段#

我们的绘图板创建了简单的线段项目,每一条都是带有起点和终点的单一段。线段也可以由多个段组成。

canvas.create_line(10, 10, 200, 50, 90, 150, 50, 80)

线条具有多个有趣的附加属性,允许绘制曲线、箭头等。

  • arrow: 在起点(first)、终点(last)或两端(both)放置一个箭头头;默认为none

  • arrowshape: 允许改变任何箭头头的外观

  • capstyle: 对于没有箭头头的宽线条,这控制线条末端的绘制方式;选项包括 butt(默认)、projectinground

  • joinstyle: 对于具有多个段的宽线条,这控制每个顶点的绘制;选项包括 round(默认)、bevelmiter

  • smooth: 如果指定为true(或bezier),则在多个段之间绘制平滑曲线(通过二次样条线);raw 指定不同类型的曲线(三次样条线)

  • splinesteps: 控制曲线线的平滑度,即设置了 smooth 选项的那些

矩形#

矩形是通过指定对角顶点的坐标来定义的,例如左上角和右下角。它们可以用一种颜色填充(通过fill参数),并且轮廓可以设置为不同的颜色。

canvas.create_rectangle(10, 10, 200, 50, fill='red', outline='blue')

椭圆#

椭圆项的工作方式与矩形完全相同。

canvas.create_oval(10, 10, 200, 150, fill='red', outline='blue')

多边形#

多边形项允许您根据一系列点定义任意形状。坐标的给出方式与多点线相同。Tk确保多边形是“封闭”的,如有必要,将最后一个点连接到第一个点。像椭圆和矩形一样,它们可以有单独的填充和轮廓颜色。他们还支持线条项的joinstyle、smooth和splinesteps属性。

canvas.create_polygon(10, 10, 200, 50, 90, 150, 50, 80, 120, 55, fill='red', outline='blue')

弧线#

弧线项绘制椭圆的一部分;想象一下饼图的一片。其显示由三个属性控制:

  • start:弧线从椭圆上的哪个位置开始,以度为单位(0是3点钟位置)

  • extent:弧线应该“宽”多少度,从起始点逆时针为正,顺时针为负

  • style:pieslice(默认),arc(仅绘制外周),或chord(绘制连接弧的起点和终点的线与外周之间的区域)。

canvas.create_arc(10, 10, 200, 150, fill='yellow', outline='black', start=45, extent=135, width=5)

图像项#

图像项能够展示任意图片。默认情况下,图像会以你指定的坐标为中心进行定位,但可以通过锚点选项来改变这一行为,例如,nw表示将图像的左上角放置在指定的坐标位置。

myimg = PhotoImage(file='pretty.png')
canvas.create_image(10, 10, image=myimg, anchor='nw')

此外,对于仅有两种颜色的图像,还有一种位图项类型,可以通过前景色和背景色的设置来更改颜色。不过,这种类型如今已经不太常用了。

文本项#

文本项能够显示一段文本。文本的定位方式与图像项相同。通过text属性指定要显示的文本。文本项中的所有文字都将具有相同的颜色(由fill属性指定)和相同的字体(由font属性指定)。

如果嵌入 在文本中,文本项可以显示多行文本。或者,你也可以通过提供一个width属性来让文本自动换行,该属性代表每行的最大宽度。多行文本的对齐方式可以通过justify属性来设置,可以是左对齐(默认)、右对齐或居中对齐。

canvas.create_text(100, 100, text='A wonderful story', anchor='nw', font='TkMenuFont', fill='red')

小部件#

在画布小部件中最酷的功能之一就是可以嵌入其他小部件。这些小部件可以是简单的按钮、输入框(想象成内嵌的文字项编辑)、列表框、甚至是包含复杂小部件集合的框架…任何东西都可以!还记得我们之前提到的画布小部件可以作为几何管理器吗?这就是我们的意思。

显示其他小部件的画布项被称为窗口项(Tk长期使用的术语来指代小部件)。它们的定位方式与文本和图像项相似。你可以给它们显式地指定宽度和高度属性;默认情况下,它们的大小是小部件的首选大小。最后,重要的是,你放在画布上的小部件(通过window属性)必须是画布的子小部件。

b = ttk.Button(canvas, text='Implode!')
canvas.create_window(10, 10, anchor='nw', window=b)

修改项目#

我们已经看到了如何修改一个项目的配置选项——它的颜色、宽度等。你还可以做更多与项目相关的操作。

要删除项目,请使用delete方法。

若要更改项目的大小和位置,可以使用coords方法。你需要为该项目提供新的坐标,指定方式与你首次创建它时相同。如果调用此方法时没有提供新的坐标集,它将返回项目的当前坐标。你可以使用move方法从其当前位置水平或垂直偏移一个或多个项目。

所有项目都是按照所谓的堆叠顺序从上到下排列的。如果堆叠顺序中较后的项目覆盖了下面的项目,那么第一个项目将绘制在第二个项目的上面。raise(在Tkinter中称为lift)和lower方法允许你调整项目在堆叠顺序中的位置。

参考手册中有更多详细的操作,用于修改项目和检索关于它们的信息。

事件绑定#

我们已经看到,画布小部件作为一个整体,就像任何其他Tk小部件一样,可以使用bind命令来捕获事件。

您还可以将绑定附加到画布中的单个项目(或者像我们在下一节中使用标签看到的,将它们分组)。所以,如果你想要知道某个特定的项目是否被点击了,你不需要监视整个画布的鼠标点击事件,然后判断那个点击是否发生在你的项目上。Tk会为你处理所有这些。

为了捕获这些事件,你使用内置在画布中的bind命令。它的工作方式与常规的bind命令完全相同,接受一个事件模式和一个回调函数。唯一的区别是你需要指定这个绑定适用的画布项目。

canvas.tag_bind(id, '<1>', ...)

备注

注意特定项目的tag_bind方法和小部件级别的bind方法之间的区别。

让我们在我们的草图示例中添加一些代码,以允许更改绘图颜色。我们首先创建几个不同颜色的矩形项目。然后我们将为每一个附加一个绑定。当它们被点击时,它们会将一个全局变量设置为新的绘图颜色。我们的鼠标移动绑定将在创建线段时查看该变量。

color = "black"
def setColor(newcolor):
    global color
    color = newcolor

def addLine(event):
    global lastx, lasty
    canvas.create_line((lastx, lasty, event.x, event.y), fill=color)
    lastx, lasty = event.x, event.y

id = canvas.create_rectangle((10, 10, 30, 30), fill="red")
canvas.tag_bind(id, "<Button-1>", lambda x: setColor("red"))
id = canvas.create_rectangle((10, 35, 30, 55), fill="blue")
canvas.tag_bind(id, "<Button-1>", lambda x: setColor("blue"))
id = canvas.create_rectangle((10, 60, 30, 80), fill="black")
canvas.tag_bind(id, "<Button-1>", lambda x: setColor("black"))

标记#

我们已经看到,每个画布项都可以通过一个唯一的ID号来引用。还有一种方便且强大的方式,可以使用标签来引用画布上的物品。

标签只是你创作的一种标识符,对你的程序来说有特定意义的东西。你可以将标签附加到画布项上;每个项目可以有任意数量的标签。与物品ID号不同,后者对于每个物品是唯一的,许多物品可以共享同一个标签。

你能用标签做什么呢?我们看到你可以使用物品ID来修改画布项(我们很快就会看到,你还可以做其他事情,比如移动它们、删除它们等)。任何时候你可以使用物品ID的地方,你都可以使用标签。例如,你可以改变所有具有特定标签的物品的颜色。

标签是一种很好的方式,用来在你的画布中识别一组物品(如一条绘制线中的物品、调色板中的物品等)。你可以使用标签将画布项与你应用程序中的特定对象相关联(例如,将所有属于机器人ID #X37部分的画布项标记为“robotX37”)。有了标签,你就不必跟踪画布项的ID以便于以后引用组内的物品;标签让Tk为你做这件事。

你可以在创建物品时通过tags项目配置选项来分配标签。你可以稍后使用addtag方法添加标签或使用dtags方法移除它们。你可以使用gettags方法获取一个项目的标签列表,或者使用find命令返回拥有给定标签的项目ID号列表。

例如:

>>> c = Canvas(root)
>>> c.create_line(10, 10, 20, 20, tags=('firstline', 'drawing'))
1
>>> c.create_rectangle(30, 30, 40, 40, tags=('drawing'))
2
>>> c.addtag('rectangle', 'withtag', 2)
>>> c.addtag('polygon', 'withtag', 'rectangle')
>>> c.gettags(2)
('drawing', 'rectangle', 'polygon')
>>> c.dtag(2, 'polygon')
>>> c.gettags(2)
('drawing', 'rectangle')	
>>> c.find_withtag('drawing')
(1, 2)

正如您所看到的,像withtag这样的方法可以接受单个项目或者一个标签;在后一种情况下,它们将应用于所有具有该标签的项目(可能一个也没有)。addtag和find方法还有许多其他选项,允许您指定接近某一点的项目、重叠特定区域等。

让我们首先使用标签来为调色板中当前选定的项添加边框。

def setColor(newcolor):
    global color
    color = newcolor
    canvas.dtag('all', 'paletteSelected')
    canvas.itemconfigure('palette', outline='white')
    canvas.addtag('paletteSelected', 'withtag', 'palette%s' % color)
    canvas.itemconfigure('paletteSelected', outline='#999999')

id = canvas.create_rectangle((10, 10, 30, 30), fill="red", tags=('palette', 'palettered'))
id = canvas.create_rectangle((10, 35, 30, 55), fill="blue", tags=('palette', 'paletteblue'))
id = canvas.create_rectangle((10, 60, 30, 80), fill="black", tags=('palette', 'paletteblack', 'paletteSelected'))

setColor('black')
canvas.itemconfigure('palette', width=5)

让我们也利用标签来使当前正在绘制的笔触更加突出。当鼠标按钮被释放时,我们将线条恢复到正常状态。

def addLine(event):
    global lastx, lasty
    canvas.create_line((lastx, lasty, event.x, event.y), fill=color, width=5, tags='currentline')
    lastx, lasty = event.x, event.y

def doneStroke(event):
    canvas.itemconfigure('currentline', width=1)        

canvas.bind("<B1-ButtonRelease>", doneStroke)

滚动画布#

在许多应用场景中,您可能希望画布的尺寸大于屏幕上显示的部分。您可以通过xview和yview方法以常规方式为画布添加水平和垂直滚动条。

您可以指定画布在屏幕上的显示大小以及其完整尺寸(需要滚动查看)。width和height配置选项控制画布小部件向几何管理器请求的空间量。scrollregion配置选项通过指定其左、上、右、下坐标来告诉Tk画布表面的尺寸,例如0 0 1000 1000。

考虑到您已经了解的知识,您应该能够修改草图板程序以增加滚动功能。不妨试一试。

完成之后,将画布向下滚动一点,然后尝试绘图。您会看到绘制的线条出现在鼠标指向位置的上方!感到惊讶吗?

发生这种情况的原因是全局绑定命令不知道画布已滚动(它不了解任何特定小部件的细节)。因此,如果您将画布向下滚动了50像素,并在左上角点击,bind将报告您点击的位置为(0,0)。但我们知道由于滚动的原因,那个位置实际上应该是(0,50)。

canvasx和canvasy方法将屏幕上的位置(bind报告的位置)转换为画布上的实际点(考虑了滚动)。

如果您直接在事件绑定脚本中添加canvasx和canvasy方法请小心处理您需要注意引号和替换以确保在事件发生时进行转换一如既往最好将该代码放在与事件绑定本身分开的程序中

那么,这就是我们的完整示例。我们可能不希望调色板随着画布的滚动而滚动消失,但我们将把这个问题留到另一天解决。

from tkinter import *
from tkinter import ttk
root = Tk()

h = ttk.Scrollbar(root, orient=HORIZONTAL)
v = ttk.Scrollbar(root, orient=VERTICAL)
canvas = Canvas(root, scrollregion=(0, 0, 1000, 1000), yscrollcommand=v.set, xscrollcommand=h.set)
h['command'] = canvas.xview
v['command'] = canvas.yview

canvas.grid(column=0, row=0, sticky=(N,W,E,S))
h.grid(column=0, row=1, sticky=(W,E))
v.grid(column=1, row=0, sticky=(N,S))
root.grid_columnconfigure(0, weight=1)
root.grid_rowconfigure(0, weight=1)

lastx, lasty = 0, 0

def xy(event):
    global lastx, lasty
    lastx, lasty = canvas.canvasx(event.x), canvas.canvasy(event.y)

def setColor(newcolor):
    global color
    color = newcolor
    canvas.dtag('all', 'paletteSelected')
    canvas.itemconfigure('palette', outline='white')
    canvas.addtag('paletteSelected', 'withtag', 'palette%s' % color)
    canvas.itemconfigure('paletteSelected', outline='#999999')

def addLine(event):
    global lastx, lasty
    x, y = canvas.canvasx(event.x), canvas.canvasy(event.y)
    canvas.create_line((lastx, lasty, x, y), fill=color, width=5, tags='currentline')
    lastx, lasty = x, y

def doneStroke(event):
    canvas.itemconfigure('currentline', width=1)        
        
canvas.bind("<Button-1>", xy)
canvas.bind("<B1-Motion>", addLine)
canvas.bind("<B1-ButtonRelease>", doneStroke)

id = canvas.create_rectangle((10, 10, 30, 30), fill="red", tags=('palette', 'palettered'))
canvas.tag_bind(id, "<Button-1>", lambda x: setColor("red"))
id = canvas.create_rectangle((10, 35, 30, 55), fill="blue", tags=('palette', 'paletteblue'))
canvas.tag_bind(id, "<Button-1>", lambda x: setColor("blue"))
id = canvas.create_rectangle((10, 60, 30, 80), fill="black", tags=('palette', 'paletteblack', 'paletteSelected'))
canvas.tag_bind(id, "<Button-1>", lambda x: setColor("black"))

setColor('black')
canvas.itemconfigure('palette', width=5)
root.mainloop()