Skip to content

Latest commit

 

History

History
1043 lines (734 loc) · 40.4 KB

File metadata and controls

1043 lines (734 loc) · 40.4 KB

七、画布和图形

在本章中,我们将介绍以下配方:

  • 理解坐标系
  • 画线和箭头
  • 在画布上书写文本
  • 向画布添加形状
  • 按位置查找项目
  • 移动画布项目
  • 检测项目之间的冲突
  • 从画布中删除项目
  • 将事件绑定到画布项
  • 将画布渲染为 PostScript 文件

介绍

在第一章中,我们介绍了标准 Tkinter 小部件的几个配方。然而,我们跳过了Canvas小部件,因为它提供了大量的图形功能,它本身就应该有一章专门介绍它的常见用例。

画布是一个矩形区域,您不仅可以在其中显示文本和几何形状,如直线、矩形或椭圆形,还可以嵌套其他 Tkinter 小部件。这些对象称为画布项,每个对象都有一个唯一的标识符,允许我们在它们最初显示在画布上之前对其进行操作。

我们将用交互式示例介绍Canvas类的方法,这将帮助我们识别可以转换为我们想要构建的应用程序的频繁模式。

理解坐标系

要在画布上绘制图形项目,我们需要使用坐标系指定它们的位置。由于画布是二维空间,因此点将通过其在水平轴和垂直轴上的坐标来表示,通常分别标记为x和y。

通过一个简单的应用程序,我们可以很容易地说明如何相对于放置在画布区域左上角的坐标系原点定位这些点。

怎么做。。。

以下程序包含一个空画布和一个显示光标在画布上位置的标签;根据移动鼠标指针的方向,您可以移动光标以查看光标所在的位置,并提供有关 x 和 y 坐标增量或减量的清晰反馈:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Basic canvas")

        self.canvas = tk.Canvas(self, bg="white")
        self.label = tk.Label(self)
        self.canvas.bind("<Motion>", self.mouse_motion)

        self.canvas.pack()
        self.label.pack()

    def mouse_motion(self, event):
        x, y = event.x, event.y
        text = "Mouse position: ({}, {})".format(x, y)
        self.label.config(text=text)

if __name__ == "__main__":
    app = App()
    app.mainloop()

它是如何工作的。。。

Canvas实例与任何其他 Tkinter 小部件一样创建,即首先将父容器和其他配置选项作为关键字参数传递:

    def __init__(self):
        # ...
        self.canvas = tk.Canvas(self, bg="white")
        self.label = tk.Label(self)
        self.canvas.bind("<Motion>", self.mouse_motion)

下一个屏幕截图显示了由每个轴的垂直投影组成的点:

  • x 坐标对应于水平轴上的距离,并在从左向右移动光标时增加其值
  • y 坐标是垂直轴上的距离,当您从上到下移动光标时,y 坐标会增加其值

正如您在前面的屏幕截图中所注意到的,这些坐标直接映射到传递给处理程序的event实例的x和y属性:

    def mouse_motion(self, event):
        x, y = event.x, event.y
 text = "Mouse position: ({}, {})".format(x, y)
        self.label.config(text=text)

之所以会发生这种情况,是因为这些属性是针对事件绑定到的小部件计算的,在本例中为<Motion>序列。

还有更多。。。

画布曲面还可以显示坐标中具有负值的项目。根据项目大小,它们可以部分显示在画布的顶部或左侧边框上。

以类似的方式,如果项目放置在其任何坐标大于画布大小的点上,它可能会部分落在底部或右侧边界之外。

画线和箭头

使用画布可以执行的最基本的操作之一是从一个点到另一个点绘制线段。尽管可以使用其他方法直接绘制多边形,Canvas类的create_line方法有足够的选项来理解显示项目的基础知识。

准备

在此配方中,我们将构建一个应用程序,允许我们通过单击画布来绘制线条。通过首先单击将确定线起点的点,然后单击第二次以设置线终点,将显示每条线。

我们还可以指定一些外观选项,例如颜色和宽度:

怎么做。。。

我们的App类将负责创建一个空画布并处理鼠标点击事件。

行选项上的信息将从LineForm类中检索。将该组件划分为不同类的方法有助于我们抽象其实现细节,并关注如何使用Canvas小部件。

为了简洁起见,我们在下面的代码片段中省略了LineForm类的实现:

import tkinter as tk

class LineForm(tk.LabelFrame):
    # ...

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Basic canvas")

        self.line_start = None
        self.form = LineForm(self)
        self.canvas = tk.Canvas(self, bg="white")
        self.canvas.bind("<Button-1>", self.draw)

        self.form.pack(side=tk.LEFT, padx=10, pady=10)
        self.canvas.pack(side=tk.LEFT)

    def draw(self, event):
        x, y = event.x, event.y
        if not self.line_start:
            self.line_start = (x, y)
        else:
            x_origin, y_origin = self.line_start
            self.line_start = None
            line = (x_origin, y_origin, x, y)
            arrow = self.form.get_arrow()
            color = self.form.get_color()
            width = self.form.get_width()
            self.canvas.create_line(*line, arrow=arrow,
                                    fill=color, width=width)

if __name__ == "__main__":
    app = App()
    app.mainloop()

您可以在chapter7_02.py文件中找到完整的代码示例。

它是如何工作的。。。

因为我们想要处理画布上的鼠标点击,所以我们将draw()方法绑定到这种类型的事件。我们还将定义line_start字段

要跟踪每条新线的起点,请执行以下操作:

    def __init__(self):
        # ...

        self.line_start = None
        self.form = LineForm(self)
        self.canvas = tk.Canvas(self, bg="white")
        self.canvas.bind("<Button-1>", self.draw)

draw()方法包含我们应用程序的主要逻辑。第一次点击每一条新行用于确定原点,不执行任何绘图操作。这些坐标从传递给处理程序的event对象中检索:

    def draw(self, event):
 x, y = event.x, event.y
 if not self.line_start:
 self.line_start = (x, y)
        else:
            # ...

如果line_start已经有一个值,我们检索原点并将其与当前事件的坐标一起传递以绘制线:

    def draw(self, event):
        x, y = event.x, event.y
        if not self.line_start:
            # ...
        else:
            x_origin, y_origin = self.line_start
 self.line_start = None
 line = (x_origin, y_origin, x, y)
 self.canvas.create_line(*line)
 text = "Line drawn from ({}, {}) to ({}, {})".format(*line)

canvas.create_line()方法有四个参数,前两个是线路起点的水平和垂直坐标,后两个是线路终点对应的坐标。

在画布上书写文本

如果我们想在画布上写一些文本,我们不需要使用额外的小部件,比如标签。Canvas类包含create_text方法来显示字符串,该字符串可以像任何其他类型的画布项目一样进行操作。

也可以使用我们可以指定的相同格式选项来为常规 Tkinter 小部件的文本添加样式,例如颜色、字体系列和大小。

准备

在本例中,我们将把条目小部件与文本画布项的内容连接起来。虽然输入将具有标准外观,但画布上的文本将具有自定义样式:

怎么做。。。

文本项最初将使用canvas.create_text()方法显示,一些附加选项使用 ConsoleAS 字体和蓝色。

文本项的动态行为将使用StringVar实现。通过跟踪此 Tkinter 变量,我们可以修改项目的内容:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Canvas text items")
        self.geometry("300x100")

        self.var = tk.StringVar()
        self.entry = tk.Entry(self, textvariable=self.var)
        self.canvas = tk.Canvas(self, bg="white")

        self.entry.pack(pady=5)
        self.canvas.pack()
        self.update()

        w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
        options = { "font": "courier", "fill": "blue",
                    "activefill": "red" }
        self.text_id = self.canvas.create_text((w/2, h/2), **options)
        self.var.trace("w", self.write_text)

    def write_text(self, *args):
        self.canvas.itemconfig(self.text_id, text=self.var.get())

if __name__ == "__main__":
    app = App()
    app.mainloop()

您可以通过在条目输入上键入一些任意文本并注意它如何自动更新画布上的文本来尝试此程序。

它是如何工作的。。。

首先,我们使用StringVar变量和画布小部件初始化Entry实例:

        self.var = tk.StringVar()
        self.entry = tk.Entry(self, textvariable=self.var)
        self.canvas = tk.Canvas(self, bg="white")

然后,我们通过调用 packgeometry 管理器的方法来放置小部件。请注意在根窗口上调用update()的重要性,因为我们希望强制 Tkinter 处理所有挂起的更改,在这种情况下,在__init__方法继续执行之前呈现小部件:

        self.entry.pack(pady=5)
        self.canvas.pack()
        self.update()

我们这样做是因为下一步将是计算画布尺寸,并且在几何体管理器显示小部件之前,它将不具有其宽度和高度的实际值。

在此之后,我们可以安全地检索画布维度。因为我们希望将文本项与画布的中心对齐,所以我们将宽度和高度的值除以一半。

这些坐标确定项目的位置,并与样式选项一起传递给create_text()方法。text关键字参数是此处使用的常用选项,但我们将忽略它,因为它将在StringVar更改其值时动态设置:

        w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
        options = { "font": "courier", "fill": "blue",
                    "activefill": "red" }
        self.text_id = self.canvas.create_text((w/2, h/2), **options)
        self.var.trace("w", self.write_text)

create_text()返回的标识符存储在text_id字段中。它将在write_text()方法上用于引用该项,该项由var实例写操作的跟踪机制调用。

为了更新write_text()处理程序中的text选项,我们调用canvas.itemconfig()方法,将项目标识符作为第一个参数,然后调用配置选项。

在我们的例子中,我们使用初始化我们的App实例时存储的text_id字段,并通过get()方法使用StringVar的内容:

    def write_text(self, *args):
        self.canvas.itemconfig(self.text_id, text=self.var.get())

我们定义了write_text()方法,这样即使我们不需要参数,它也可以接收数量可变的参数,因为 Tkinter 变量的trace()方法将它们传递给回调函数。

还有更多。。。

canvas.create_text()方法有许多其他选项来定制创建的画布项。

将文本放置在其左上角

anchor选项允许我们控制相对于作为第一个参数传递给canvas.create_text()的位置放置项目的位置。默认情况下,此选项值为tk.CENTER,这意味着文本小部件位于这些坐标的中心。

如果要将文本放置在画布的左上角,可以通过传递(0, 0)位置并将anchor选项设置为tk.NW,将原点与文本放置的矩形区域的西北方向对齐:

        # ...
        options = { "font": "courier", "fill": "blue",
                    "activefill": "red", "anchor": tk.NW }
        self.text_id = self.canvas.create_text((0, 0), **options)

前面的代码片段将给出以下结果:

定线缠绕

默认情况下,文本项的内容将显示在一行中。width选项允许我们定义一个最大线宽,它将线包裹得比该宽度长:

        # ...
        options = { "font": "courier", "fill": "blue",
                    "activefill": "red", "width": 70 }
        self.text_id = self.canvas.create_text((w/2, h/2), **options)

现在,当我们在条目上写入Hello, world!时,文本中超出行宽的部分将显示在新的行中:

向画布添加形状

在本食谱中,我们将介绍三种标准画布项目:矩形、椭圆形和圆弧。它们都显示在边界框内,因此只需使用两个点来设置它们的位置:框的左上角和右下角。

准备

下面的应用程序允许用户在画布上自由绘制一些项目,方法是使用三个按钮选择其类型,每个按钮选择相应的形状。

首先在画布上单击以设置项目将包含在其中的框的左上角,然后单击以设置此框的左下角并使用一些预定义选项绘制项目,从而确定项目的位置:

怎么做。。。

我们的应用程序存储当前选择的项目类型,这是通过放置在画布下方框架上的三个按钮之一选择的。

在画布上使用主鼠标按钮单击会触发存储新项目第一个角点位置的处理程序,再次单击后,它将读取选定形状的值以有条件地绘制相应项目:

import tkinter as tk
from functools import partial

class App(tk.Tk):
    shapes = ("rectangle", "oval", "arc")
    def __init__(self):
        super().__init__()
        self.title("Drawing standard items")

        self.start = None
        self.shape = None
        self.canvas = tk.Canvas(self, bg="white")
        frame = tk.Frame(self)
        for shape in self.shapes:
            btn = tk.Button(frame, text=shape.capitalize())
            btn.config(command=partial(self.set_selection, btn, shape))
            btn.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)

        self.canvas.bind("<Button-1>", self.draw_item)
        self.canvas.pack()
        frame.pack(fill=tk.BOTH)

    def set_selection(self, widget, shape):
        for w in widget.master.winfo_children():
            w.config(relief=tk.RAISED)
        widget.config(relief=tk.SUNKEN)
        self.shape = shape

    def draw_item(self, event):
        x, y = event.x, event.y
        if not self.start:
            self.start = (x, y)
        else:
            x_origin, y_origin = self.start
            self.start = None
            bbox = (x_origin, y_origin, x, y)
            if self.shape == "rectangle":
                self.canvas.create_rectangle(*bbox, fill="blue",
                                             activefill="yellow")
            elif self.shape == "oval":
                self.canvas.create_oval(*bbox, fill="red",
                                        activefill="yellow")
            elif self.shape == "arc":
                self.canvas.create_arc(*bbox, fill="green",
                                       activefill="yellow")

if __name__ == "__main__":
    app = App()
    app.mainloop()

它是如何工作的。。。

为了通过点击画布动态选择项目类型,我们将通过迭代shapes元组为每个形状创建一个按钮。

我们使用functools模块中的partial函数定义每个回调命令。因此,我们可以将Button实例和循环的当前形状冻结为每个按钮回调的参数:

        for shape in self.shapes:
            btn = tk.Button(frame, text=shape.capitalize())
            btn.config(command=partial(self.set_selection, btn, shape))
            btn.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)

set_selection()回调用SUNKEN浮雕标记单击的按钮,并将选择存储在shape字段中。

其他小部件同级通过导航到当前小部件的master字段中的父级,然后使用winfo_children()方法检索所有子小部件,从而使用标准释放(RAISED)进行配置:

    def set_selection(self, widget, shape):
        for w in widget.master.winfo_children():
            w.config(relief=tk.RAISED)
        widget.config(relief=tk.SUNKEN)
        self.shape = shape

当画布再次被点击时,draw_item()处理程序存储每对事件的第一次点击的坐标,就像我们之前在绘制线条和箭头配方中所做的一样。

根据shape字段的值,调用以下方法之一以显示相应的项目类型:

  • canvas.create_rectangle(x0, y0, x1, y1, **options):绘制一个矩形,其左上角位于**(x0,y0),右下角位于(x1,y1)**:

  • canvas.create_oval(x0, y0, x1, y1, **options):在**(x0,y0)到(x1,y1)**之间绘制一个适合矩形的椭圆

*

  • canvas.create_arc(x0, y0, x1, y1, **options):从**(x0,y0)到(x1,y1)**绘制适合矩形的四分之一椭圆:

另见

  • 画线和箭头配方

按位置查找项目

Canvas类包括检索接近画布坐标的项目标识符的方法。

这是非常有用的,因为它避免了我们存储对画布项目的每个引用,然后计算它们的当前位置,以检测哪些引用位于特定区域内或最接近某个点。

准备

以下应用程序创建了一个具有四个矩形的画布,并更改了最靠近鼠标指针的画布的颜色:

怎么做。。。

为了找到离指针最近的项,我们将鼠标事件坐标传递给canvas.find_closest()方法,该方法检索离给定位置最近的项的标识符。

一旦画布中至少有一个项目,我们可以安全地假设此方法将始终返回有效的项目标识符:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Finding canvas items")

        self.current = None
        self.canvas = tk.Canvas(self, bg="white")
        self.canvas.bind("<Motion>", self.mouse_motion)
        self.canvas.pack()

        self.update()
        w = self.canvas.winfo_width()
        h = self.canvas.winfo_height()
        positions = [(60, 60), (w-60, 60), (60, h-60), (w-60, h-60)]
        for x, y in positions:
            self.canvas.create_rectangle(x-10, y-10, x+10, y+10,
                                         fill="blue")

    def mouse_motion(self, event):
        self.canvas.itemconfig(self.current, fill="blue")
        self.current = self.canvas.find_closest(event.x, event.y)
        self.canvas.itemconfig(self.current, fill="yellow")

if __name__ == "__main__":
    app = App()
    app.mainloop()

它是如何工作的。。。

在应用程序初始化期间,我们创建画布并定义current字段来存储对当前突出显示项的引用。我们还使用mouse_motion()方法处理画布上的"<Motion>"事件:

        self.current = None
        self.canvas = tk.Canvas(self, bg="white")
        self.canvas.bind("<Motion>", self.mouse_motion)
        self.canvas.pack()

然后,我们创建四个具有特定排列的项目,以便我们可以轻松地可视化最接近鼠标指针的项目:

        self.update()
        w = self.canvas.winfo_width()
        h = self.canvas.winfo_height()
        positions = [(60, 60), (w-60, 60), (60, h-60), (w-60, h-60)]
        for x, y in positions:
            self.canvas.create_rectangle(x-10, y-10, x+10, y+10,
                                         fill="blue")

mouse_motion()处理程序将当前项目的颜色设置回blue并保存新项目的项目标识符,该标识符更接近事件坐标。最后,该项目的fill颜色设置为yellow:

    def mouse_motion(self, event):
        self.canvas.itemconfig(self.current, fill="blue")
        self.current = self.canvas.find_closest(event.x, event.y)
        self.canvas.itemconfig(self.current, fill="yellow")

最初第一次调用mouse_motion()时没有错误,current字段仍然是None,因为它也是itemconfig()的有效输入参数;它只是不在画布上执行任何操作。

移动画布项目

放置后,画布项目可以移动到某个偏移量,而无需指定绝对坐标。

移动画布项目时,通常需要计算其当前位置,例如,确定它们是否放置在混凝土画布区域内,并限制它们的移动,使它们始终保持在该区域内。

怎么做。。。

我们的示例将包括一个带有矩形项的简单画布,可以使用箭头键水平和垂直移动该项。

为防止此项目移出屏幕,我们将限制其在画布尺寸内的移动:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Moving canvas items")

        self.canvas = tk.Canvas(self, bg="white")
        self.canvas.pack()
        self.update()
        self.width = self.canvas.winfo_width()
        self.height = self.canvas.winfo_height()

        self.item = self.canvas.create_rectangle(30, 30, 60, 60,
                                                 fill="blue")
        self.pressed_keys = {}
        self.bind("<KeyPress>", self.key_press)
        self.bind("<KeyRelease>", self.key_release)
        self.process_movements()

    def key_press(self, event):
        self.pressed_keys[event.keysym] = True

    def key_release(self, event):
        self.pressed_keys.pop(event.keysym, None)

    def process_movements(self):
        off_x, off_y = 0, 0
        speed = 3
        if 'Right' in self.pressed_keys:
            off_x += speed
        if 'Left' in self.pressed_keys:
            off_x -= speed
        if 'Down' in self.pressed_keys:
            off_y += speed
        if 'Up' in self.pressed_keys:
            off_y -= speed

        x0, y0, x1, y1 = self.canvas.coords(self.item)
        pos_x = x0 + (x1 - x0) / 2 + off_x
        pos_y = y0 + (y1 - y0) / 2 + off_y
        if 0 <= pos_x <= self.width and 0 <= pos_y <= self.height:
            self.canvas.move(self.item, off_x, off_y)

        self.after(10, self.process_movements)

if __name__ == "__main__":
    app = App()
    app.mainloop()

它是如何工作的。。。

为了处理箭头键盘事件,我们将"<KeyPress>"和"<KeyRelease>"序列绑定到应用程序实例。当前按下的键符号存储在pressed_keys字典中:

    def __init__(self):
        # ...
        self.pressed_keys = {}
        self.bind("<KeyPress>", self.key_press)
        self.bind("<KeyRelease>", self.key_release)

    def key_press(self, event):
        self.pressed_keys[event.keysym] = True

    def key_release(self, event):
        self.pressed_keys.pop(event.keysym, None)

首选此方法,而不是单独绑定"<Up>"、"<Down>"、"<Right>"和"<Left>"键,因为这将仅在 Tkinter 处理输入键盘事件时调用每个处理程序,导致项目从一个位置“跳跃”到下一个位置,而不是在水平轴和垂直轴上平滑移动。

App实例初始化的最后一句话是对process_movements()的调用,它开始处理画布项的移动。

此方法计算项目应位移的每个轴上的偏移量。根据pressed_keys字典的内容,在坐标的每个分量上增加或减少speed值:

    def process_movements(self):
        off_x, off_y = 0, 0
        speed = 3
        if 'Right' in self.pressed_keys:
            off_x += speed
        if 'Left' in self.pressed_keys:
            off_x -= speed
        if 'Down' in self.pressed_keys:
            off_y += speed
        if 'Up' in self.pressed_keys:
            off_y -= speed

在此之后,我们通过调用canvas.coords()并将构成边界框的两个点解包为四个变量来检索当前项目位置。

通过将左上角的x和y分量添加到其宽度和高度的一半来计算项目的中心。此结果加上每个轴上的偏移量,对应于项目移动后的最终位置:

        x0, y0, x1, y1 = self.canvas.coords(self.item)
        pos_x = x0 + (x1 - x0) / 2 + off_x
        pos_y = y0 + (y1 - y0) / 2 + off_y

然后,我们检查最终项目位置是否在画布区域内。为此,我们利用 Python 对链接比较运算符的支持:

        if 0 <= pos_x <= self.width and 0 <= pos_y <= self.height:
            self.canvas.move(self.item, off_x, off_y)

最后,此方法通过调用self.after(10, self.process_movements)以 10 毫秒的延迟来调度自身。因此,我们实现了在 Tkinter 的主循环中有一个“自定义主循环”的效果。

还有更多。。。

您可能想知道为什么我们没有调用after_idle()而不是after()来调度process_movements()方法。

这看起来可能是一种有效的方法,因为除了重新绘制画布和处理键盘输入之外,没有其他事件需要处理,因此如果没有挂起的 GUI 事件,则无需在调用process_movements()之间添加延迟。

但是,使用after_idle会导致项目以取决于计算机速度的速度移动。这意味着一个快速的系统在同一时间间隔内调用process_movements()的次数比一个较慢的系统要多,并且这个差异在项目速度上是明显的。

通过引入最小固定延迟,我们使具有不同功能的机器有机会以类似的方式运行。

另见

  • 检测项目之间的冲突

检测项目之间的冲突

作为前面配方的延续,我们可以检测矩形项是否与另一个重叠。事实上,这是可以实现的,假设我们使用Canvas类中的find_overlapping()方法处理矩形框中包含的形状。

准备

此应用程序通过在画布上添加四个绿色矩形并突出显示被使用箭头键移动的蓝色矩形接触的矩形,扩展了前面的一个矩形:

怎么做。。。

由于此脚本与前一个脚本有许多相似之处,我们标记了创建四个矩形并调用canvas.find_overlapping()方法的代码部分:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Detecting collisions between items")

        self.canvas = tk.Canvas(self, bg="white")
        self.canvas.pack()
        self.update()
 self.width = w = self.canvas.winfo_width()
 self.height = h = self.canvas.winfo_height()

 pos = (w/2 - 15, h/2 - 15, w/2 + 15, h/2 + 15)
 self.item = self.canvas.create_rectangle(*pos, fill="blue") 
 positions = [(60, 60), (w-60, 60), (60, h-60), (w-60, h-60)]
 for x, y in positions:
 self.canvas.create_rectangle(x-10, y-10, x+10, y+10,
 fill="green")

        self.pressed_keys = {}
        self.bind("<KeyPress>", self.key_press)
        self.bind("<KeyRelease>", self.key_release)
        self.process_movements()

    def key_press(self, event):
        self.pressed_keys[event.keysym] = True

    def key_release(self, event):
        self.pressed_keys.pop(event.keysym, None)

    def process_movements(self):
 all_items = self.canvas.find_all()
 for item in filter(lambda i: i is not self.item, all_items):
 self.canvas.itemconfig(item, fill="green")

 x0, y0, x1, y1 = self.canvas.coords(self.item)
 items = self.canvas.find_overlapping(x0, y0, x1, y1)
 for item in filter(lambda i: i is not self.item, items):
 self.canvas.itemconfig(item, fill="yellow")

        off_x, off_y = 0, 0
        speed = 3
        if 'Right' in self.pressed_keys:
            off_x += speed
        if 'Left' in self.pressed_keys:
            off_x -= speed
        if 'Down' in self.pressed_keys:
            off_y += speed
        if 'Up' in self.pressed_keys:
            off_y -= speed

        pos_x = x0 + (x1 - x0) / 2 + off_x
        pos_y = y0 + (y1 - y0) / 2 + off_y
        if 0 <= pos_x <= self.width and 0 <= pos_y <= self.height:
            self.canvas.move(self.item, off_x, off_y)

        self.after(10, self.process_movements)

if __name__ == "__main__":
    app = App()
    app.mainloop()

它是如何工作的。。。

对__init__方法的修改类似于按位置配方查找项目中的修改,因此如果您有任何疑问,可以查看该方法,并直接跳到process_movements()方法中的更改。

在计算任何重叠之前,所有画布项目的填充颜色(用户可以控制的除外)都将更改为绿色。通过canvas.find_all()方法检索这些项目的标识符:

    def process_movements(self):
 all_items = self.canvas.find_all()
 for item in filter(lambda i: i != self.item, all_items):
 self.canvas.itemconfig(item, fill="green")

现在项目颜色已重置,我们调用canvas.find_overlapping()获取当前与移动项目碰撞的所有项目。同样,由用户控制的项目从循环中排除,其余重叠项目(如果有)的颜色更改为黄色:

    def process_movements(self):
        # ...

 x0, y0, x1, y1 = self.canvas.coords(self.item)
 items = self.canvas.find_overlapping(x0, y0, x1, y1)
 for item in filter(lambda i: i != self.item, items):
 self.canvas.itemconfig(item, fill="yellow")

该方法通过按计算出的偏移量移动蓝色矩形,并再次调度process_movements()本身来继续执行。

还有更多。。。

如果要检测移动项目何时与另一个项目完全重叠,而不是部分重叠,请使用相同的参数替换对canvas.find_overlapping()到canvas.find_enclosed()的调用。

从画布中删除项目

除了在画布上添加和修改项目外,还可以通过Canvas类的delete()方法删除它们。尽管此方法的使用非常简单,但我们将在下一个示例中看到一些有用的模式。

请记住,画布上显示的项目越多,Tkinter 重新绘制小部件所需的时间就越长。因此,如果删除不必要的项可能导致性能问题,则可以很方便地删除这些项。

准备

对于这个配方,我们将构建一个在画布上随机显示几个圆圈的应用程序。单击每个圆后,它会自动删除,窗口包含一个按钮以清除所有项目,另一个按钮以重新开始:

怎么做。。。

为了不规则地将项目放置在画布上,我们将使用random模块中的randint函数生成坐标。项目颜色也将通过调用带有预定义颜色列表的choice随机选择。

一旦生成,可以在单击时触发on_click处理程序或按下Clear items按钮删除项目,该按钮执行clear_all回调。这些方法使用适当的参数在内部调用canvas.delete():

import random
import tkinter as tk

class App(tk.Tk):
    colors = ("red", "yellow", "green", "blue", "orange")

    def __init__(self):
        super().__init__()
        self.title("Removing canvas items")

        self.canvas = tk.Canvas(self, bg="white")
        frame = tk.Frame(self)
        generate_btn = tk.Button(frame, text="Generate items",
                                 command=self.generate_items)
        clear_btn = tk.Button(frame, text="Clear items",
                              command=self.clear_items)

        self.canvas.pack()
        frame.pack(fill=tk.BOTH)
        generate_btn.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
        clear_btn.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)

        self.update()
        self.width = self.canvas.winfo_width()
        self.height = self.canvas.winfo_height()

        self.canvas.bind("<Button-1>", self.on_click)
        self.generate_items()

    def on_click(self, event):
        item = self.canvas.find_withtag(tk.CURRENT)
        self.canvas.delete(item)

    def generate_items(self):
        self.clear_items()
        for _ in range(10):
            x = random.randint(0, self.width)
            y = random.randint(0, self.height)
            color = random.choice(self.colors)
            self.canvas.create_oval(x, y, x + 20, y + 20, fill=color)

    def clear_items(self):
        self.canvas.delete(tk.ALL)

if __name__ == "__main__":
    app = App()
    app.mainloop()

它是如何工作的。。。

canvas.delete()方法接受一个参数(可以是项目标识符或标记),并删除匹配的项目,因为同一个标记可以多次使用。

在on_click()处理程序中,我们可以看到一个如何通过标识符删除项目的示例:

    def on_click(self, event):
        item = self.canvas.find_withtag(tk.CURRENT)
        self.canvas.delete(item)

请注意,如果我们点击一个空点,canvas.find_withtag(tk.CURRENT)将返回None,但在传递到canvas.delete()时不会引发任何错误。之所以会出现这种情况,是因为None参数将不匹配任何项目标识符或标记,因此,即使它不会执行任何操作,它也是一个有效值。

在clear_items()回调中,我们可以找到另一个删除项目的示例。在这里,我们没有传递项目标识符,而是使用ALL标记匹配所有项目并将其从画布中移除:

    def clear_items(self):
        self.canvas.delete(tk.ALL)

正如您可能已经注意到的,ALL标记是开箱即用的,不需要添加到每个画布项目中。

将事件绑定到画布项

到目前为止,我们已经看到了如何将事件绑定到小部件;但是,对于画布项目也可以这样做。这有助于我们编写更具体、更简单的事件处理程序,而不是在Canvas实例上绑定我们想要处理的所有类型的事件,然后根据受影响的项目确定必须应用哪些操作。

准备

下面的应用程序演示如何在画布项目上实现拖放功能。这是一个常用功能,用于说明此方法如何简化我们的程序。

怎么做。。。

我们将创建两个可以用鼠标拖放的项目——一个矩形和一个椭圆形。不同的形状有助于我们注意单击事件如何正确应用于相应的项目,即使这些项目彼此重叠放置:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Drag and drop")

        self.dnd_item = None
        self.canvas = tk.Canvas(self, bg="white")
        self.canvas.pack()

        self.canvas.create_rectangle(30, 30, 60, 60, fill="green",
                                     tags="draggable")
        self.canvas.create_oval(120, 120, 150, 150, fill="red",
                                tags="draggable")
        self.canvas.tag_bind("draggable", "<ButtonPress-1>",
                             self.button_press)
        self.canvas.tag_bind("draggable", "<Button1-Motion>",
                             self.button_motion)

    def button_press(self, event):
        item = self.canvas.find_withtag(tk.CURRENT)
        self.dnd_item = (item, event.x, event.y)

    def button_motion(self, event):
        x, y = event.x, event.y
        item, x0, y0 = self.dnd_item
        self.canvas.move(item, x - x0, y - y0)
        self.dnd_item = (item, x, y)

if __name__ == "__main__":
    app = App()
    app.mainloop()

它是如何工作的。。。

为了将事件绑定到项,我们使用Canvas类中的tag_bind()方法。这会将事件绑定添加到与项说明符匹配的所有项中,在我们的示例中,"draggable"标记。

甚至该方法也被命名为tag_bind();传递项目标识符而不是标记也是有效的:

        self.canvas.tag_bind("draggable", "<ButtonPress-1>",
                             self.button_press)
        self.canvas.tag_bind("draggable", "<Button1-Motion>",
                             self.button_motion)

请记住,这只会影响现有的标记项,因此如果我们稍后使用"draggable"标记添加新项,它们将不会附加这些绑定。

button_press()方法是单击项目时调用的处理程序。通常,检索单击项的常见模式是调用canvas.find_withtag(tk.CURRENT)。

该项目标识符和click事件的x和y坐标存储在dnd_item字段中。这些值稍后将用于与鼠标运动同步移动项目:

    def button_press(self, event):
        item = self.canvas.find_withtag(tk.CURRENT)
        self.dnd_item = (item, event.x, event.y)

按住主按钮时,button_motion()方法处理鼠标运动事件。

为了设置项目应该移动的距离,我们计算当前事件位置与先前存储的坐标之间的差值。这些值被传递到canvas.move()方法,并再次保存在dnd_item字段中:

    def button_motion(self, event):
        x, y = event.x, event.y
        item, x0, y0 = self.dnd_item
        self.canvas.move(item, x - x0, y - y0)
        self.dnd_item = (item, x, y)

此拖放功能的一些变体还实现了<ButtonRelease-1>序列的处理程序,该处理程序将取消当前拖动的项。

但是,这不是必需的,因为一旦发生此类事件,<Button1-Motion>绑定将不会被触发,直到再次单击某个项目。这也避免了我们在button_motion()处理程序的开头检查dnd_item是否为None。

还有更多。。。

可以通过添加一些基本验证来改进此示例,例如验证用户不能将项目放置在画布可见区域之外。

为了实现这一点,您可以使用我们在前面的配方中介绍的模式来计算画布的宽度和高度,并通过链接比较运算符来验证项目的最终位置是否在有效范围内。您可以使用以下代码段中所示的结构作为模板:

final_x, final_y = pos_x + off_x, pos_y + off_y
if 0 <= final_x <= canvas_width and 0 <= final_y <= canvas_height:
     canvas.move(item, off_x, off_y)

另见

  • 移动画布项目配方

将画布渲染为 PostScript 文件

Canvas类本机支持通过postscript()方法使用 PostScript 语言保存其内容。它存储画布项目的图形表示,例如直线、矩形、多边形、椭圆和圆弧,但是,对于嵌入式小部件和图像,它不这样做。

我们将修改以前动态生成此类简单项的配方,以添加将画布表示保存到 PostScript 文件中的功能。

怎么做。。。

我们将从绘制线条和箭头配方中获取代码示例,添加一个按钮,将画布内容打印到 PostScript 文件中:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Basic canvas")

        self.line_start = None
        self.form = LineForm(self)
        self.render_btn = tk.Button(self, text="Render canvas",
                                    command=self.render_canvas)
        self.canvas = tk.Canvas(self, bg="white")
        self.canvas.bind("<Button-1>", self.draw)

        self.form.grid(row=0, column=0, padx=10, pady=10)
        self.render_btn.grid(row=1, column=0)
        self.canvas.grid(row=0, column=1, rowspan=2)

    def draw(self, event):
        # ...

    def render_canvas(self):
        self.canvas.postscript(file="output.ps", colormode="color")

它是如何工作的。。。

原始脚本的主要新增内容是带有render_canvas()回调的Render canvas按钮。

它使用file和colormode参数对canvas实例调用postscript()方法。这些选项指定写入 PostScript 和输出颜色信息的目标文件的路径,对于全色输出,可以是"color","gray"转换为灰度等价物,以及"mono"将所有颜色转换为黑色或白色:

    def render_canvas(self):
        self.canvas.postscript(file="output.ps", colormode="color")

您可以在上的 Tk/Tcl 文档中检查可以传递给postscript()方法的所有有效选项 https://www.tcl.tk/man/tcl8.6/TkCmd/canvas.htm#M61 。请记住,PostScript 是一种主要用于打印的语言,因此大多数选项都涉及页面设置。

还有更多。。。

由于 PostScript 文件不像其他文件格式那样受欢迎,因此您可能希望将生成的文件从 PostScript 转换为更熟悉的格式,如 PDF。

为此,您需要一个第三方软件,如Ghostscript,它是根据 GNU 的Affero 通用公共许可证(AGPL发布的。可以从程序中调用 Ghostscript 的解释器和渲染器实用程序,以自动将 PostScript 结果转换为 PDF。

从下载并安装最新版本的软件 https://www.ghostscript.com/download/gsdnld.html 并将安装的bin和lib文件夹添加到您的操作系统路径中。

然后,修改您的 Tkinter 应用程序,将ps2pdf程序作为子进程调用,并在output.ps文件执行完毕后将其删除,如下所示:

import os
import subprocess
import tkinter as tk

class App(tk.Tk):
    # ...

    def render_canvas(self):
        output_filename = "output.ps"
        self.canvas.postscript(file=output_filename, colormode="color")
 process = subprocess.run(["ps2pdf", output_filename, "output.pdf"],
 shell=True)
 os.remove(output_filename)
```*