← 学习库 Think Python (2e) · 中英对照 目录

Chapter 19  Case study: Tkinter 第 19 章 案例研究:Tkinter

本页译自 Think Python 2e(Allen B. Downey)· Chapter 19 Case study: Tkinter。代码块保留英文原文不翻译;正文段段对照,中文块可用右下角按钮隐藏。

19.1 GUI 19.1 图形用户界面

Most of the programs we have seen so far are text-based, but many programs use graphical user interfaces, also known as GUIs.

到目前为止我们看到的大多数程序都是基于文本的,但许多程序使用图形用户界面(graphical user interface,简称 GUI)。

Python provides several choices for writing GUI-based programs, including wxPython, Tkinter, and Qt. Each has pros and cons, which is why Python has not converged on a standard.

Python 提供了几种编写 GUI 程序的方案,包括 wxPython、Tkinter 和 Qt。它们各有优缺点,这也正是 Python 没有统一成一种标准的原因。

The one I will present in this chapter is Tkinter because I think it is the easiest to get started with. Most of the concepts in this chapter apply to the other GUI modules, too.

本章我要介绍的是 Tkinter,因为我觉得它最容易上手。本章中的大部分概念也适用于其他 GUI 模块。

There are several books and web pages about Tkinter. One of the best online resources is An Introduction to Tkinter by Fredrik Lundh.

关于 Tkinter 有不少书籍和网页。最好的在线资源之一是 Fredrik Lundh 写的《An Introduction to Tkinter》。

I have written a module called Gui.py that comes with Swampy. It provides a simplified interface to the functions and classes in Tkinter. The examples in this chapter are based on this module.

我写了一个随 Swampy 一起发布的模块 Gui.py。它对 Tkinter 中的函数和类做了简化封装。本章的例子都基于这个模块。

Here is a simple example that creates and displays a Gui:

下面是一个创建并显示一个 Gui 的简单例子:

To create a GUI, you have to import Gui from Swampy:

要创建 GUI,你得从 Swampy 导入 Gui:
Gui.py006

Or, depending on how you installed Swampy, like this:

或者,取决于你如何安装 Swampy,像这样:
Gui.py007 Gui.py008

When you run this code, a window should appear with an empty gray square and the title Gui. Gui.py00 runs the event loop, which waits for the user to do something and responds accordingly. It is an infinite loop; it runs until the user closes the window, or presses Control-C, or does something that causes the program to quit.

运行这段代码时,会弹出一个窗口,里面是一个空灰色方块,标题是 Gui。mainloop 运行事件循环,它等待用户有所动作并作出相应响应。这是一个无限循环;它会一直运行,直到用户关闭窗口、按下 Control-C,或做出某些导致程序退出的动作。

This Gui doesn’t do much because it doesn’t have any widgets. Widgets are the elements that make up a GUI; they include:

这个 Gui 没什么用,因为它没有任何控件。控件是构成 GUI 的元素;它们包括:
Button:
A widget, containing text or an image, that performs an action when pressed.
Canvas:
A region that can display lines, rectangles, circles and other shapes.
Entry:
A region where users can type text.
Scrollbar:
A widget that controls the visible part of another widget.
Frame:
A container, often invisible, that contains other widgets.
Button 按钮:
一种控件,包含文字或图像,按下时执行某个动作。
Canvas 画布:
一个可以显示线条、矩形、圆形及其他形状的区域。
Entry 输入框:
一个供用户输入文字的区域。
Scrollbar 滚动条:
一种控件,用来控制另一个控件中可见的部分。
Frame 框架:
一个容器,通常不可见,用来容纳其他控件。

The empty gray square you see when you create a Gui is a Frame. When you create a new widget, it is added to this Frame.

你创建 Gui 时看到的那个空灰色方块就是一个 Frame。当你新建一个控件时,它会被加入这个 Frame。

19.2 Buttons and callbacks 19.2 按钮与回调

The method bu creates a Button widget:

bu 方法创建一个 Button 控件:
Gui.py011

The return value from bu is a Button object. The button that appears in the Frame is a graphical representation of this object; you can control the button by invoking methods on it.

bu 的返回值是一个 Button 对象。Frame 中出现的按钮是这个对象的一种图形表示;你可以通过调用它的方法来控制按钮。

bu takes up to 32 parameters that control the appearance and function of the button. These parameters are called options. Instead of providing values for all 32 options, you can use keyword arguments, like Gui.py014, to specify only the options you need and use the default values for the rest.

bu 最多接受 32 个参数,用来控制按钮的外观和行为。这些参数称为选项。你不必为全部 32 个选项都提供值,而是可以使用关键字实参(如 text='Press me.')只指定需要的选项,其余采用默认值。

When you add a widget to the Frame, it gets “shrink-wrapped;” that is, the Frame shrinks to the size of the Button. If you add more widgets, the Frame grows to accommodate them.

当你往 Frame 里加入一个控件时,它会被「收缩包装」;也就是说,Frame 会收缩到和按钮一样大。如果你加入更多控件,Frame 会随之扩大以容纳它们。

The method bu creates a Label widget:

la 方法创建一个 Label 控件:
Gui.py016

By default, Tkinter stacks the widgets top-to-bottom and centers them. We’ll see how to override that behavior soon.

默认情况下,Tkinter 把控件从上到下堆叠并居中。我们很快会看到如何改变这种行为。

If you press the button, you will see that it doesn’t do much. That’s because you haven’t “wired it up;” that is, you haven’t told it what to do!

如果你按下按钮,会发现它没什么反应。那是因为你还没有「把它接上线」;也就是说,你还没告诉它该做什么!

The option that controls the behavior of a button is Gui.py0. The value of Gui.py0 is a function that gets executed when the button is pressed. For example, here is a function that creates a new Label:

控制按钮行为的选项是 command。command 的值是一个函数,在按钮被按下时执行。例如,下面是一个创建新 Label 的函数:
Gui.py019

Now we can create a button with this function as its command:

现在我们可以创建一个把这个函数作为 command 的按钮:
Gui.py020

When you press this button, it should execute Gui.py021 and a new label should appear.

当你按下这个按钮时,它应该执行 make_label,并出现一个新的标签。

The value of the Gui.py0 option is a function object, which is known as a callback because after you call bu to create the button, the flow of execution “calls back” when the user presses the button.

command 选项的值是一个函数对象,称为回调(callback),因为你在调用 bu 创建按钮之后,当用户按下按钮时,执行流会「回调」它。

This kind of flow is characteristic of event-driven programming. User actions, like button presses and key strokes, are called events. In event-driven programming, the flow of execution is determined by user actions rather than by the programmer.

这种流程是事件驱动编程的特征。用户的动作(如按键、敲击键盘)称为事件。在事件驱动编程中,执行流由用户动作决定,而不是由程序员决定。

The challenge of event-driven programming is to construct a set of widgets and callbacks that work correctly (or at least generate appropriate error messages) for any sequence of user actions.

事件驱动编程的挑战在于:要构造一组控件和回调,使它们对任何用户动作序列都能正确工作(或至少产生恰当的错误信息)。

Exercise 1

习题 1

Write a program that creates a GUI with a single button. When the button is pressed it should create a second button. When that button is pressed, it should create a label that says, “Nice job!”.

写一个程序,创建一个只含一个按钮的 GUI。按下这个按钮时,它应该创建第二个按钮。按下第二个按钮时,它应该创建一个写着「Nice job!」的标签。

What happens if you press the buttons more than once? Solution: Gui.py024

如果多次按下按钮会怎样?答案:Gui.py025

19.3 Canvas widgets 19.3 画布控件

One of the most versatile widgets is the Canvas, which creates a region for drawing lines, circles and other shapes. If you did Exercise 4 you are already familiar with canvases.

最通用的控件之一是 Canvas(画布),它创建一个用于绘制线条、圆形及其他形状的区域。如果你做过习题 4,应该已经熟悉画布了。

The method bu creates a new Canvas:

ca 方法创建一个新画布:
Gui.py027

Gui00 and Gui.py are the dimensions of the canvas in pixels.

width 和 height 是画布的尺寸,以像素为单位。

After you create a widget, you can still change the values of the options with the Gui.py method. For example, the bu option changes the background color:

创建控件之后,你仍可以用 config 方法改变选项的值。例如,bg 选项改变背景颜色:
Gui.py032

The value of bu is a string that names a color. The set of legal color names is different for different implementations of Python, but all implementations provide at least:

bg 的值是一个表示颜色名称的字符串。合法颜色名称的集合因 Python 的不同实现而异,但所有实现至少提供以下颜色:
Gui.py034

Shapes on a Canvas are called items. For example, the Canvas method Gui.py draws (you guessed it) a circle:

画布上的形状称为图元(item)。例如,画布的 circle 方法画(你猜到了)一个圆:
Gui.py036

The first argument is a coordinate pair that specifies the center of the circle; the second is the radius.

第一个实参是一个坐标对,指定圆心;第二个是半径。

Gui.py provides a standard Cartesian coordinate system with the origin at the center of the Canvas and the positive y axis pointing up. This is different from some other graphics systems where the origin is in the upper left corner, with the y axis pointing down.

Gui.py 提供了一套标准笛卡尔坐标系,原点在画布中心,y 轴正方向朝上。这与其他一些图形系统不同,那些系统原点在左上角,y 轴朝下。

The Gui0 option specifies that the circle should be filled in with red.

fill 选项指定圆用红色填充。

The return value from Gui.py is an Item object that provides methods for modifying the item on the canvas. For example, you can use Gui.py to change any of the circle’s options:

circle 的返回值是一个 Item 对象,提供修改画布上图元的方法。例如,你可以用 config 改变圆的任意选项:
Gui.py041

Gui00 is the thickness of the outline in pixels; Gui.py0 is the color.

width 是轮廓的粗细(像素);outline 是颜色。

Exercise 2

习题 2

Write a program that creates a Canvas and a Button. When the user presses the Button, it should draw a circle on the canvas.

写一个程序,创建一个画布和一个按钮。当用户按下按钮时,它应该在画布上画一个圆。

19.4 Coordinate sequences 19.4 坐标序列

The Gui.py044 method takes a sequence of coordinates that specify opposite corners of the rectangle. This example draws a blue rectangle with the lower left corner at the origin and the upper right corner at (200,100):

rectangle 方法接受一组坐标序列,指定矩形对角的两个角。这个例子画了一个蓝色矩形,左下角在原点,右上角在 (200,100):
Gui.py045

This way of specifying corners is called a bounding box because the two points bound the rectangle.

这种指定角点的方式称为外接框(bounding box),因为这两个点「框住」了矩形。

Gui0 takes a bounding box and draws an oval within the specified rectangle:

oval 接受一个外接框,并在指定矩形内画一个椭圆:
Gui.py047

Gui0 takes a sequence of coordinates and draws a line that connects the points. This example draws two legs of a triangle:

line 接受一组坐标序列,画出连接各点的线。这个例子画出三角形的两条边:
Gui.py049

Gui.py0 takes the same arguments, but it draws the last leg of the polygon (if necessary) and fills it in:

polygon 接受同样的实参,但它会画出多边形的最后一条边(如有必要)并将其填充:
Gui.py051

19.5 More widgets 19.5 更多控件

Tkinter provides two widgets that let users type text: an Entry, which is a single line, and a Text widget, which has multiple lines.

Tkinter 提供两种供用户输入文字的控件:Entry(单行)和 Text 控件(多行)。

bu creates a new Entry:

en 创建一个新 Entry:
Gui.py053

The Gui0 option allows you to put text into the entry when it is created. The Gui method returns the contents of the Entry (which may have been changed by the user):

text 选项让你在创建时往输入框里放文字。get 方法返回 Entry 的内容(可能已被用户修改):
Gui.py056

bu creates a Text widget:

te 创建一个 Text 控件:
Gui.py058

Gui00 and Gui.py are the dimensions of the widget in characters and lines.

width 和 height 是控件的尺寸,以字符数和行数为单位。

Gui.py puts text into the Text widget:

insert 把文字放入 Text 控件:
Gui.py062

Gui is a special index that indicates the last character in the Text widget.

END 是一个特殊索引,表示 Text 控件中的最后一个字符。

You can also specify a character using a dotted index, like Gui, which has the line number before the dot and the column number after. The following example adds the letters Gui.py06 after the first character of the first line.

你也可以用点分索引指定一个字符,如 1.1,点前是行号,点后是列号。下面的例子在第一行第一个字符后加上字母 'nother':
Gui.py066

The Gui method reads the text in the widget; it takes a start and end index as arguments. The following example returns all the text in the widget, including the newline character:

get 方法读取控件中的文字;它接受起始和结束索引作为实参。下面的例子返回控件中的所有文字,包括换行符:
Gui.py068

The Gui.py method removes text from the widget; the following example deletes all but the first two characters:

delete 方法从控件中删除文字;下面的例子删除除前两个字符以外的所有内容:
Gui.py070

Exercise 3

习题 3

Modify your solution to Exercise 2 by adding an Entry widget and a second button. When the user presses the second button, it should read a color name from the Entry and use it to change the fill color of the circle. Use Gui.py to modify the existing circle; don’t create a new one.

修改习题 2 的答案,加入一个 Entry 控件和一个第二个按钮。当用户按下第二个按钮时,它应该从 Entry 读取一个颜色名,并用它改变圆的填充色。用 config 修改已有的圆;不要新建一个圆。

Your program should handle the case where the user tries to change the color of a circle that hasn’t been created, and the case where the color name is invalid.

你的程序应当处理这种情况:用户试图改变尚未创建的圆的颜色,以及颜色名无效的情况。

You can see my solution at Gui.py072.

你可以看我的答案:Gui.py073。

19.6 Packing widgets 19.6 打包控件

So far we have been stacking widgets in a single column, but in most GUIs the layout is more complicated. For example, Figure 19.1 shows a simplified version of TurtleWorld (see Chapter 4).

到目前为止我们一直把控件堆在单列中,但大多数 GUI 的布局更复杂。例如,图 19.1 展示了一个简化版的 TurtleWorld(见第 4 章)。

Figure 19.1: TurtleWorld after running the snowflake code.

图 19.1:运行完雪花代码后的 TurtleWorld。(原书插图未收录)

This section presents the code that creates this GUI, broken into a series of steps. You can download the complete example from Gui.py074.

本节给出创建这个 GUI 的代码,分成一系列步骤。你可以从 Gui.py075 下载完整示例。

At the top level, this GUI contains two widgets—a Canvas and a Frame—arranged in a row. So the first step is to create the row.

在顶层,这个 GUI 包含两个控件——一个 Canvas 和一个 Frame——排成一行。所以第一步是创建这一行。
Gui.py076

Gui00 is the function that creates and arranges the widgets. Arranging widgets in a GUI is called packing.

setup 是创建并排列控件的函数。在 GUI 中排列控件称为打包(packing)。

Gui creates a row Frame and makes it the “current Frame.” Until this Frame is closed or another Frame is created, all subsequent widgets are packed in a row.

row 创建一个行 Frame,并将其设为「当前 Frame」。在这个 Frame 关闭或创建另一个 Frame 之前,后续所有控件都打包进这一行。

Here is the code that creates the Canvas and the column Frame that hold the other widgets:

下面是创建画布和容纳其他控件的列 Frame 的代码:
Gui.py079

The first widget in the column is a grid Frame, which contains four buttons arranged two-by-two:

列中的第一个控件是一个网格 Frame,里面有两个两排列的四个按钮:
Gui.py080

bu creates the grid; the argument is the number of columns. Widgets in the grid are laid out left-to-right, top-to-bottom.

gr 创建网格;实参是列数。网格中的控件按从左到右、从上到下的顺序排列。

The first button uses Gui.py082 as a callback; the second uses Gui.py083. These are bound methods, which means they are associated with a particular object. When they are invoked, they are invoked on the object.

第一个按钮把 self.canvas.dump 作为回调;第二个用 self.quit。这些是绑定方法(bound method),也就是说它们和特定对象关联。调用它们时,是在该对象上调用的。

The next widget in the column is a row Frame that contains a Button and an Entry:

列中的下一个控件是一个行 Frame,里面包含一个按钮和一个 Entry:
Gui.py084

The first argument to Gui is a list of weights that determines how extra space is allocated between widgets. The list Gui00 means that all extra space is allocated to the second widget, which is the Entry. If you run this code and resize the window, you will see that the Entry grows and the Button doesn’t.

row 的第一个实参是一个权重列表,决定多余空间如何在控件间分配。列表 [0,1] 表示所有多余空间都分配给第二个控件,即 Entry。如果你运行这段代码并改变窗口大小,会看到 Entry 变大而按钮不变。

The option Gui0 “pads” this row in the y direction, adding 30 pixels of space above and below.

pady 选项在这个行的 y 方向「填充」空间,在上下各加 30 像素的空隙。

Gui.py ends this row of widgets, so subsequent widgets are packed in the column Frame. Gui.py keeps a stack of Frames:

endrow 结束这一行控件,所以后续控件打包进列 Frame。Gui.py 维护一个 Frame 栈:

The method Gui.py10 reads the contents of the Entry, uses it as a filename, reads the contents and passes it to Gui.py10. Gui.py104 is an Interpreter object that knows how to take a string and execute it as Python code.

run_file 方法读取 Entry 的内容,把它当作文件名,读取其内容并传给 run_code。self.inter 是一个 Interpreter 对象,知道如何把一个字符串当作 Python 代码执行。
Gui.py105

The last two widgets are a Text widget and a Button:

最后两个控件是一个 Text 控件和一个按钮:
Gui.py106

Gui.py10 is similar to Gui.py10 except that it takes the code from the Text widget instead of from a file:

run_text 与 run_file 类似,只是它从 Text 控件而不是从文件中获取代码:
Gui.py109

Unfortunately, the details of widget layout are different in other languages, and in different Python modules. Tkinter alone provides three different mechanisms for arranging widgets. These mechanisms are called geometry managers. The one I demonstrated in this section is the “grid” geometry manager; the others are called “pack” and “place”.

遗憾的是,控件布局的细节在其他语言和其他 Python 模块中各不相同。仅 Tkinter 就提供三种排列控件的机制。这些机制称为布局管理器(geometry manager)。本节演示的是「grid」布局管理器;另外两个叫「pack」和「place」。

Fortunately, most of the concepts in this section apply to other GUI modules and other languages.

幸运的是,本节中的大部分概念也适用于其他 GUI 模块和其他语言。

19.7 Menus and Callables 19.7 菜单与可调用对象

A Menubutton is a widget that looks like a button, but when pressed it pops up a menu. After the user selects an item, the menu disappears.

Menubutton 是一种看起来像按钮的控件,但按下时会弹出一个菜单。用户选择一项后,菜单消失。

Here is code that creates a color selection Menubutton (you can download it from Gui.py110):

下面是创建颜色选择 Menubutton 的代码(你可以从 Gui.py111 下载):
Gui.py112

bu creates the Menubutton. Initially, the text on the button is the name of the default color. The following loop creates one menu item for each color:

mb 创建 Menubutton。最初,按钮上的文字是默认颜色的名称。下面的循环为每种颜色创建一个菜单项:
Gui.py114

The first argument of bu is the Menubutton these items are associated with.

mi 的第一个实参是这些菜单项关联的 Menubutton。

The Gui.py1 option is a Callable object, which is something new. So far we have seen functions and bound methods used as callbacks, which works fine if you don’t have to pass any arguments to the function. Otherwise you have to construct a Callable object that contains a function, like Gui.py117, and its arguments, like Gui00.

command 选项是一个 Callable 对象,这是新东西。到目前为止我们见过的回调是函数和绑定方法,只要不需要给函数传任何实参,这没问题。否则你就得构造一个 Callable 对象,它包含一个函数(如 set_color)和它的实参(如 color)。

The Callable object stores a reference to the function and the arguments as attributes. Later, when the user clicks on a menu item, the callback calls the function and passes the stored arguments.

Callable 对象把对函数和实参的引用作为属性保存起来。之后,当用户点击菜单项时,回调会调用该函数并传入保存的实参。

Here is what Gui.py119 might look like:

set_color 大概长这样:
Gui.py120

When the user selects a menu item and Gui.py121 is called, it configures the Menubutton to display the newly-selected color. It also print the color; if you try this example, you can confirm that Gui.py122 is called when you select an item (and not called when you create the Callable object).

当用户选择一个菜单项并调用 set_color 时,它配置 Menubutton 以显示新选中的颜色。它还会打印颜色;如果你试这个例子,可以确认 set_color 是在你选择某项时被调用的(而不是在创建 Callable 对象时被调用)。

19.8 Binding 19.8 绑定

A binding is an association between a widget, an event and a callback: when an event (like a button press) happens on a widget, the callback is invoked.

绑定(binding)是控件、事件和回调之间的关联:当控件上发生某个事件(如按下按钮)时,就会调用回调。

Many widgets have default bindings. For example, when you press a button, the default binding changes the relief of the button to make it look depressed. When you release the button, the binding restores the appearance of the button and invokes the callback specified with the Gui.py1 option.

许多控件有默认绑定。例如,当你按下按钮时,默认绑定会改变按钮的 relief(浮雕效果)让它看起来被按下。当你松开按钮时,绑定恢复按钮外观,并调用 command 选项指定的回调。

You can use the Gui0 method to override these default bindings or to add new ones. For example, this code creates a binding for a canvas (you can download the code in this section from Gui.py125):

你可以用 bind 方法覆盖这些默认绑定,或添加新的绑定。例如,这段代码为一个画布创建绑定(你可以从 Gui.py126 下载本节代码):
Gui.py127

The first argument is an event string; this event is triggered when the user presses the left mouse button. Other mouse events include Gui.py128, Gui.py129 and Gui.py130.

第一个实参是一个事件字符串;当用户按下鼠标左键时触发这个事件。其他鼠标事件包括 ButtonMotion、ButtonRelease 和 Double-Button。

The second argument is an event handler. An event handler is a function or bound method, like a callback, but an important difference is that an event handler takes an Event object as a parameter. Here is an example:

第二个实参是一个事件处理器(event handler)。事件处理器是一个函数或绑定方法,像回调一样,但一个重要区别是事件处理器接受一个 Event 对象作为实参。例如:
Gui.py131

The Event object contains information about the type of event and details like the coordinates of the mouse pointer. In this example the information we need is the location of the mouse click. These values are in “pixel coordinates,” which are defined by the underlying graphical system. The method Gui.py132 translates them to “Canvas coordinates,” which are compatible with Canvas methods like Gui.py.

Event 对象包含事件类型的信息以及诸如鼠标指针坐标等细节。本例中我们需要的信息是鼠标点击的位置。这些值是「像素坐标」,由底层图形系统定义。canvas_coords 方法把它们转换为「Canvas 坐标」,后者与 circle 等画布方法兼容。

For Entry widgets, it is common to bind the Gui.py134 event, which is triggered when the user presses the Return or Enter key. For example, the following code creates a Button and an Entry.

对于 Entry 控件,常会绑定 <Return> 事件,即在用户按下 Return 或 Enter 键时触发。例如,下面的代码创建一个按钮和一个 Entry。
Gui.py135

Gui.py136 is called when the Button is pressed or when the user hits Return while typing in the Entry. To make this work, we need a function that can be called as a command (with no arguments) or as an event handler (with an Event as an argument):

当按钮被按下,或用户在 Entry 中输入时按下 Return,都会调用 make_text。要让这成立,我们需要一个既能被当作命令调用(无实参)、又能被当作事件处理器调用的函数(以 Event 为实参):
Gui.py137

Gui.py138 gets the contents of the Entry and displays it as a Text item in the Canvas.

make_text 获取 Entry 的内容,并在画布上以 Text 图元的形式显示出来。

It is also possible to create bindings for Canvas items. The following is a class definition for Gui.py139, which is a child class of Gui0 that provides bindings that implement drag-and-drop capability.

也可以为画布图元创建绑定。下面是一个 Draggable 的类定义,它是 Item 的子类,提供实现拖放能力的绑定。
Gui.py141

The init method takes an Item as a parameter. It copies the attributes of the Item and then creates bindings for three events: a button press, button motion, and button release.

__init__ 方法接受一个 Item 作为实参。它复制 Item 的属性,然后为三个事件创建绑定:按下按钮、按钮移动、按钮松开。

The event handler Gui.py stores the coordinates of the current event and the original color of the item, then changes the color to yellow:

事件处理器 select 保存当前事件的坐标和图元的原始颜色,然后把颜色改成黄色:
Gui.py143

Gui0 stands for “get configuration;” it takes the name of an option as a string and returns the current value of that option.

cget 代表「get configuration(获取配置)」;它接受一个选项名(字符串)并返回该选项的当前值。

Gui0 computes how far the object has moved relative to the starting place, updates the stored coordinates, and then moves the item.

drag 计算对象相对于起始位置移动了多远,更新保存的坐标,然后移动图元。
Gui.py146

This computation is done in pixel coordinates; there is no need to convert to Canvas coordinates.

这个计算是在像素坐标中完成的;无需转换为 Canvas 坐标。

Finally, Gui0 restores the original color of the item:

最后,drop 恢复图元的原始颜色:
Gui.py148

You can use the Gui.py149 class to add drag-and-drop capability to an existing item. For example, here is a modified version of Gui.py150 that uses Gui.py to create an Item and Gui.py152 to make it draggable:

你可以用 Draggable 类给已有的图元加上拖放能力。例如,下面是 make_circle 的改版,它用 circle 创建一个 Item,再用 Draggable 让它可拖动:
Gui.py153

This example demonstrates one of the benefits of inheritance: you can modify the capabilities of a parent class without modifying its definition. This is particularly useful if you want to change behavior defined in a module you did not write.

这个例子展示了继承的一个好处:你可以修改父类的功能而不必改动它的定义。如果你想改变一个非你编写的模块中定义的行为,这尤其有用。

19.9 Debugging 19.9 调试

One of the challenges of GUI programming is keeping track of which things happen while the GUI is being built and which things happen later in response to user events.

GUI 编程的挑战之一是:要分清哪些事情发生在 GUI 构建期间,哪些事情后来才在响应用户事件时发生。

For example, when you are setting up a callback, it is a common error to call the function rather than passing a reference to it:

例如,设置回调时,一个常见错误是调用函数本身,而不是把对它的引用传进去:
Gui.py154

If you run this code, you will see that it calls Gui.py155 immediately, and then creates the button. When you press the button, it does nothing because the return value from Gui.py156 is Gui0. Usually you do not want to invoke a callback while you are setting up the GUI; it should only be invoked later in response to a user event.

如果你运行这段代码,会看到它立刻调用 the_callback,然后才创建按钮。当你按下按钮时它什么也不做,因为 the_callback 的返回值是 None。通常你不想在设置 GUI 时就调用回调;它应该只在此后响应用户事件时才被调用。

Another challenge of GUI programming is that you don’t have control of the flow of execution. Which parts of the program execute and their order are determined by user actions. That means that you have to design your program to work correctly for any possible sequence of events.

GUI 编程的另一个挑战是你无法控制执行流。程序的哪些部分执行、以什么顺序执行,都由用户动作决定。这意味着你必须把程序设计成能对任意可能的事件序列都正确工作。

For example, the GUI in Exercise 3 has two widgets: one creates a Circle item and the other changes the color of the Circle. If the user creates the circle and then changes its color, there’s no problem. But what if the user changes the color of a circle that doesn’t exist yet? Or creates more than one circle?

例如,习题 3 中的 GUI 有两个控件:一个创建 Circle 图元,另一个改变 Circle 的颜色。如果用户先创建圆再改变颜色,没有问题。但如果用户改变一个尚不存在的圆的颜色呢?或者创建了不止一个圆呢?

As the number of widgets grows, it is increasingly difficult to imagine all possible sequences of events. One way to manage this complexity is to encapsulate the state of the system in an object and then consider:

随着控件数量增加,想象所有可能的事件序列越来越困难。管理这种复杂性的一种方法是把系统状态封装进一个对象,然后考虑:

You might also find it useful to define, and check, invariants that should hold regardless of the sequence of events.

你或许还会发现,定义并检查那些无论事件序列如何都应成立的不变量(invariant)很有用。

This approach to GUI programming can help you write correct code without taking the time to test every possible sequence of user events!

这种 GUI 编程方法能帮你写出正确的代码,而不必花时间去测试每一种可能的用户事件序列!

19.10 Glossary 19.10 术语表

GUI:
A graphical user interface.
widget:
One of the elements that makes up a GUI, including buttons, menus, text entry fields, etc.
option:
A value that controls the appearance or function of a widget.
keyword argument:
An argument that indicates the parameter name as part of the function call.
callback:
A function associated with a widget that is called when the user performs an action.
bound method:
A method associated with a particular instance.
event-driven programming:
A style of programming in which the flow of execution is determined by user actions.
event:
A user action, like a mouse click or key press, that causes a GUI to respond.
event loop:
An infinite loop that waits for user actions and responds.
item:
A graphical element on a Canvas widget.
bounding box:
A rectangle that encloses a set of items, usually specified by two opposing corners.
pack:
To arrange and display the elements of a GUI.
geometry manager:
A system for packing widgets.
binding:
An association between a widget, an event, and an event handler. The event handler is called when the event occurs in the widget.
GUI 图形用户界面:
一种图形用户界面。
widget 控件:
构成 GUI 的元素之一,包括按钮、菜单、文本输入框等。
option 选项:
控制控件外观或功能的值。
keyword argument 关键字实参:
在函数调用中同时指明参数名的实参。
callback 回调:
与控件相关联的函数,在用户执行动作时被调用。
bound method 绑定方法:
与特定实例相关联的方法。
event-driven programming 事件驱动编程:
一种编程风格,其执行流由用户动作决定。
event 事件:
用户的动作,如鼠标点击或按键,会引发 GUI 作出响应。
event loop 事件循环:
等待并响应用户动作的无限循环。
item 图元:
画布控件上的一个图形元素。
bounding box 外接框:
一个包围一组图元的矩形,通常由两个对角的角点指定。
pack 打包:
排列并显示 GUI 中的元素。
geometry manager 布局管理器:
用于打包控件的系统。
binding 绑定:
控件、事件和事件处理器之间的关联。当事件在控件中发生时,会调用该事件处理器。

19.11 Exercises 19.11 习题

Exercise 4

习题 4

For this exercise, you will write an image viewer. Here is a simple example:

本练习要写一个图像查看器。下面是一个简单例子:
Gui.py158

Gui.py159 reads a file and returns a Gui.py160 object that Tkinter can display. Gui.py161 puts the image on the canvas, centered on the given coordinates. You can also put images on labels, buttons, and some other widgets:

Gui.py162 读取一个文件并返回一个 Tkinter 能显示的 Gui.py163 对象。Gui.py164 把图像放在画布上,以给定坐标为中心。你也可以把图像放在标签、按钮及其他一些控件上:
Gui.py165

PhotoImage can only handle a few image formats, like GIF and PPM, but we can use the Python Imaging Library (PIL) to read other files.

PhotoImage 只能处理少数几种图像格式,如 GIF 和 PPM,但我们可以用 Python Imaging Library(PIL)来读取其他文件。

The name of the PIL module is Gui00, but Tkinter defines an object with the same name. To avoid the conflict, you can use Gui.py167 like this:

PIL 模块的名字是 Gui00,但 Tkinter 也定义了一个同名对象。为避免冲突,你可以像这样使用 Gui.py169:
Gui.py170

The first line imports Gui00 and gives it the local name Gui. The second line imports Gui.py1, which can translate a PIL image into a Tkinter PhotoImage. Here’s an example:

第一行导入 Gui00 并给它取本地名 Gui。第二行导入 Gui.py1,它能把一个 PIL 图像转换成 Tkinter 的 PhotoImage。例如:
Gui.py177
  1. Download Gui.py178, Gui.py179 and Gui.py180 from Gui.py181. Run Gui.py182. You might have to install Gui and Gui.py1. They are probably in your software repository, but if not you can get them from Gui.py185.
  2. In Gui.py186 change the name of the second PhotoImage from Gui.py to Gui00 and run the program again. You should see the second PhotoImage but not the first. The problem is that when you reassign Gui00 it overwrites the reference to the first PhotoImage, which then disappears. The same thing happens if you assign a PhotoImage to a local variable; it disappears when the function ends. To avoid this problem, you have to store a reference to each PhotoImage you want to keep. You can use a global variable, or store PhotoImages in a data structure or as an attribute of an object. This behavior can be frustrating, which is why I am warning you (and why the example image says “Danger!”).
  3. Starting with this example, write a program that takes the name of a directory and loops through all the files, displaying any files that PIL recognizes as images. You can use a Gui statement to catch the files PIL doesn’t recognize. When the user clicks on the image, the program should display the next one.
  4. PIL provides a variety of methods for manipulating images. You can read about them at Gui.py191. As a challenge, choose a few of these methods and provide a GUI for applying them to images.
  1. Gui.py192 下载 Gui.py193、Gui.py194 和 Gui.py195。运行 Gui.py196。你可能需要安装 GuiGui.py1。它们大概在你的软件仓库里,如果没有,可以从 Gui.py199 获取。
  2. Gui.py200 中,把第二个 PhotoImage 的名字从 Gui.py 改成 Gui00,再运行一遍。你应该能看到第二个 PhotoImage 而看不到第一个。问题在于:当你重新赋值 Gui00 时,它会覆盖对第一个 PhotoImage 的引用,于是第一个就消失了。如果你把一个 PhotoImage 赋给局部变量,也会发生同样的事;函数结束时它就消失了。要避免这个问题,你必须保存对每个想保留的 PhotoImage 的引用。你可以用全局变量,或把 PhotoImage 存进数据结构,或作为对象的属性。这种行为很让人头疼,所以我才提醒你(示例图片上写的「Danger!」也是这个原因)。
  3. 以这个例子为基础,写一个程序,接受一个目录名,遍历其中所有文件,显示任何被 PIL 识别为图像的文件。你可以用 Gui 语句来捕获 PIL 无法识别的文件。当用户点击图像时,程序应显示下一张。
  4. PIL 提供了多种处理图像的方法。你可以在 Gui.py205 了解它们。作为挑战,挑选其中几种方法,并提供一个 GUI 来把它们应用到图像上。

Solution: Gui.py206.

答案:Gui.py207。

Exercise 5

习题 5

A vector graphics editor is a program that allows users to draw and edit shapes on the screen and generate output files in vector graphics formats like Postscript and SVG.

矢量图形编辑器是一种允许用户屏幕上绘制并编辑形状、并生成 Postscript、SVG 等矢量图形格式输出文件的程序。

Write a simple vector graphics editor using Tkinter. At a minimum, it should allow users to draw lines, circles and rectangles, and it should use Gui.py208 to generate a Postscript description of the contents of the Canvas.

写一个用 Tkinter 实现的简单矢量图形编辑器。至少它应允许用户画线条、圆和矩形,并应使用 Gui.py209 生成画布内容的 Postscript 描述。

As a challenge, you could allow users to select and resize items on the Canvas.

作为挑战,你可以允许用户选择并调整画布上图元的大小。

Exercise 6

习题 6

Use Tkinter to write a basic web browser. It should have a Text widget where the user can enter a URL and a Canvas to display the contents of the page.

用 Tkinter 写一个基本的网页浏览器。它应有一个 Text 控件供用户输入 URL,并有一个画布来显示页面内容。

You can use the Gui.py module to download files (see Exercise 6) and the Gui.py211 module to parse the HTML tags (see Gui.py212).

你可以用 urllib 模块下载文件(见习题 6),用 HTMLParser 模块解析 HTML 标签(见 Gui.py213)。

At a minimum your browser should handle plain text and hyperlinks. As a challenge you could handle background colors, text formatting tags and images.

至少你的浏览器应处理纯文本和超链接。作为挑战,你可以处理背景颜色、文字格式标签和图像。