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.
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.
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.
There are several books and web pages about Tkinter. One of the best online resources is An Introduction to Tkinter by Fredrik Lundh.
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.
Here is a simple example that creates and displays a Gui:
To create a GUI, you have to import Gui from Swampy:
Gui.py006
Or, depending on how you installed Swampy, like this:
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.
This Gui doesn’t do much because it doesn’t have any widgets. Widgets are the elements that make up a GUI; they include:
- 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.
19.2 Buttons and callbacks 19.2 按钮与回调
The method bu creates a Button widget:
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 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.
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.
The method bu creates a Label widget:
Gui.py016
By default, Tkinter stacks the widgets top-to-bottom and centers them. We’ll see how to override that behavior soon.
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:
Gui.py019
Now we can create a button with this function as its command:
Gui.py020
When you press this button, it should execute Gui.py021 and a new label should appear.
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.
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
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!”.
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.
The method bu creates a new Canvas:
Gui.py027
Gui00 and Gui.py are the dimensions of the canvas in pixels.
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:
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:
Gui.py034
Shapes on a Canvas are called items. For example, the Canvas method Gui.py draws (you guessed it) a 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.
The Gui0 option specifies that the circle should be filled in with red.
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:
Gui.py041
Gui00 is the thickness of the outline in pixels; Gui.py0 is the color.
Exercise 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):
Gui.py045
This way of specifying corners is called a bounding box because the two points bound the rectangle.
Gui0 takes a bounding box and draws an oval within the specified rectangle:
Gui.py047
Gui0 takes a sequence of coordinates and draws a line that connects the points. This example draws two legs of a triangle:
Gui.py049
Gui.py0 takes the same arguments, but it draws the last leg of the polygon (if necessary) and fills it in:
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.
bu creates a new 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):
Gui.py056
bu creates a Text widget:
Gui.py058
Gui00 and Gui.py are the dimensions of the widget in characters and lines.
Gui.py puts text into the Text widget:
Gui.py062
Gui is a special index that indicates the last character in the Text widget.
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.
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:
Gui.py068
The Gui.py method removes text from the widget; the following example deletes all but the first two characters:
Gui.py070
Exercise 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.
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).
Figure 19.1: TurtleWorld after running the snowflake code.
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.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.py076
Gui00 is the function that creates and arranges the widgets. Arranging widgets in a GUI is called 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.
Here is the code that creates the Canvas and the column Frame that hold the other widgets:
Gui.py079
The first widget in the column is a grid Frame, which contains four buttons arranged two-by-two:
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.
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.
The next widget in the column is a row Frame that contains a Button and an 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.
The option Gui0 “pads” this row in the y direction, adding 30 pixels of space above and below.
Gui.py ends this row of widgets, so subsequent widgets are packed in the column Frame. Gui.py keeps a stack of Frames:
- When you use
Gui,Guiorbuto create a Frame, it goes on top of the stack and becomes the current Frame. - When you use
Gui.py,Gui.pyorGui00 to close a Frame, it gets popped off the stack and the previous Frame on the stack becomes the current Frame.
- 当你用
Gui、Gui或bu创建 Frame 时,它会被压入栈顶并成为当前 Frame。 - 当你用
Gui.py、Gui.py或Gui00 关闭一个 Frame 时,它会从栈中弹出,栈中的前一个 Frame 成为当前 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.
Gui.py105
The last two widgets are a Text widget and a Button:
Gui.py106
Gui.py10 is similar to Gui.py10 except that it takes the code from the Text widget instead of from a file:
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”.
Fortunately, most of the concepts in this section apply to other GUI modules and other languages.
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.
Here is code that creates a color selection Menubutton (you can download it from Gui.py110 ):
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:
Gui.py114
The first argument of bu is the Menubutton these items are associated with.
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.
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.
Here is what Gui.py119 might look like:
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).
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.
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.
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 ):
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 .
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:
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.
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.
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):
Gui.py137
Gui.py138 gets the contents of the Entry and displays it as a Text item in the Canvas.
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.
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.
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:
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.
Gui0 computes how far the object has moved relative to the starting place, updates the stored coordinates, and then moves the item.
Gui.py146
This computation is done in pixel coordinates; there is no need to convert to Canvas coordinates.
Finally, Gui0 restores the original color of the item:
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:
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.
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.
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.
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?
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:
- What are the possible states? In the Circle example, we might consider two states: before and after the user creates the first circle.
- In each state, what events can occur? In the example, the user can press either of the buttons, or quit.
- For each state-event pair, what is the desired outcome? Since there are two states and two buttons, there are four state-event pairs to consider.
- What can cause a transition from one state to another? In this case, there is a transition when the user creates the first circle.
- 可能的状态有哪些?在圆这个例子中,我们可以认为有两种状态:用户创建第一个圆之前和之后。
- 在每种状态下,可能发生哪些事件?在例子中,用户可以按下两个按钮中的任意一个,或退出。
- 对每个状态-事件对,期望的结果是什么?因为有两种状态和两个按钮,要考虑四个状态-事件对。
- 什么会导致从一个状态转移到另一个状态?本例中,当用户创建第一个圆时发生转移。
You might also find it useful to define, and check, invariants that should hold regardless of the sequence of events.
This approach to GUI programming can help you write correct code without taking the time to test every possible sequence of user events!
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
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.
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:
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
- Download
Gui.py178 ,Gui.py179 andGui.py180 fromGui.py181 . RunGui.py182 . You might have to installGuiandGui.py1. They are probably in your software repository, but if not you can get them fromGui.py185 . - In
Gui.py186 change the name of the second PhotoImage fromGui.pytoGui00 and run the program again. You should see the second PhotoImage but not the first. The problem is that when you reassignGui00 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!”). - 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
Guistatement to catch the files PIL doesn’t recognize. When the user clicks on the image, the program should display the next one. - 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.
- 从
Gui.py192 下载Gui.py193 、Gui.py194 和Gui.py195。运行Gui.py196 。你可能需要安装Gui和Gui.py1。它们大概在你的软件仓库里,如果没有,可以从Gui.py199 获取。 - 在
Gui.py200 中,把第二个 PhotoImage 的名字从Gui.py改成Gui00,再运行一遍。你应该能看到第二个 PhotoImage 而看不到第一个。问题在于:当你重新赋值Gui00 时,它会覆盖对第一个 PhotoImage 的引用,于是第一个就消失了。如果你把一个 PhotoImage 赋给局部变量,也会发生同样的事;函数结束时它就消失了。要避免这个问题,你必须保存对每个想保留的 PhotoImage 的引用。你可以用全局变量,或把 PhotoImage 存进数据结构,或作为对象的属性。这种行为很让人头疼,所以我才提醒你(示例图片上写的「Danger!」也是这个原因)。 - 以这个例子为基础,写一个程序,接受一个目录名,遍历其中所有文件,显示任何被 PIL 识别为图像的文件。你可以用
Gui语句来捕获 PIL 无法识别的文件。当用户点击图像时,程序应显示下一张。 - PIL 提供了多种处理图像的方法。你可以在
Gui.py205 了解它们。作为挑战,挑选其中几种方法,并提供一个 GUI 来把它们应用到图像上。
Solution: Gui.py206 .
Gui.py207 。Exercise 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.
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.
Gui.py209 生成画布内容的 Postscript 描述。As a challenge, you could allow users to select and resize items on the Canvas.
Exercise 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.
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 ).
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.