Python-GUI-编程秘籍-全-
Python GUI 编程秘籍(全)
原文:
zh.annas-archive.org/md5/de38d8b70825b858336fa5194110e245译者:飞龙
前言
在这本书中,我们将探索使用 Python 编程语言的图形用户界面(GUI)的美丽世界。
在这个过程中,我们将讨论网络、队列、OpenGL 图形库以及许多其他技术。
这是一本编程食谱。每一章都是独立的,解释了某个特定的编程解决方案。
我们将从非常简单的开始,然而在整本书中,我们将构建一个用 Python 3 编写的工作程序。
我们还将在整本书中应用一些设计模式和最佳实践。
本书假设读者具有一些使用 Python 编程语言的基本经验,但这并不是真正需要使用本书的前提条件。
如果您是任何编程语言的经验丰富的程序员,您将有一个愉快的时光,将您的技能扩展到使用 Python 编程 GUI!
你准备好了吗?
让我们开始我们的旅程吧...
本书涵盖的内容
第一章,“创建 GUI 表单并添加小部件”,解释了在 Python 中开发我们的第一个 GUI 的步骤。我们将从构建运行的 GUI 应用程序所需的最少代码开始。然后,每个示例都向 GUI 表单添加不同的小部件。
第二章,“布局管理”,探讨了如何安排小部件来创建我们的 Python GUI。网格布局管理器是内置在 tkinter 中的最重要的布局工具之一,我们将使用它。
第三章,“外观和感觉定制”,展示了如何创建一个良好的“外观和感觉”GUI 的几个示例。在实际层面上,我们将为我们在其中一个示例中创建的帮助 | 关于菜单项添加功能。
第四章,“数据和类”,讨论了保存我们的 GUI 显示的数据。我们将开始使用面向对象编程(OOP)来扩展 Python 的内置功能。
第五章,“Matplotlib 图表”,解释了如何创建美丽的图表来直观地表示数据。根据数据源的格式,我们可以在同一图表中绘制一个或多个数据列。
第六章,“线程和网络”,解释了如何使用线程、队列和网络连接扩展 Python GUI 的功能。这将向我们展示,我们的 GUI 并不局限于 PC 的本地范围。
第七章,“通过我们的 GUI 在 MySQL 数据库中存储数据”,向我们展示了如何连接到 MySQL 数据库服务器。本章的第一个示例将展示如何安装免费的 MySQL Server Community Edition,接下来的示例中,我们将创建数据库、表,然后将数据加载到这些表中,以及修改这些数据。我们还将从 MySQL 服务器中读取数据到我们的 GUI 中。
第八章,“国际化和测试”,展示了如何通过在不同语言中显示标签、按钮、选项卡和其他小部件上的文本来国际化我们的 GUI。我们将从简单开始,然后探讨如何在设计层面准备我们的 GUI 进行国际化。我们还将探讨使用 Python 内置的单元测试框架自动测试我们的 GUI 的几种方法。
第九章,“使用 wxPython 库扩展我们的 GUI”,介绍了另一个 Python GUI 工具包,它目前不随 Python 一起发布。它被称为 wxPython,我们将使用为 Python 3 设计的 Phoenix 版本的 wxPython。
第十章,使用 PyOpenGL 和 PyGLet 创建令人惊叹的 3D GUI,展示了如何通过赋予 GUI 真正的三维能力来改变我们的 GUI。我们将使用两个 Python 第三方包。PyOpenGL 是 OpenGL 标准的 Python 绑定,这是一个内置于所有主要操作系统中的图形库。这使得生成的小部件具有本地的外观和感觉。PyGLet 是我们将在本章中探索的一个这样的绑定。
第十一章,最佳实践,探讨了可以帮助我们以高效的方式构建 GUI 并使其易于维护和扩展的不同最佳实践。最佳实践适用于任何良好的代码,我们的 GUI 也不例外,设计和实施良好的软件实践。
你需要为这本书做些什么
本书所需的所有软件都可以在线获得,而且是免费的。这从 Python 3 本身开始,然后扩展到 Python 的附加模块。为了下载所需的任何软件,你需要一个可用的互联网连接。
这本书是为谁准备的
这本书是为希望创建图形用户界面(GUI)的程序员准备的。通过使用 Python 编程语言,我们可以创造出美丽、功能强大的 GUI,你可能会对我们能够实现什么感到惊讶。Python 是一种非常出色、直观的编程语言,而且非常容易学习。
我想邀请你现在就开始这段旅程。这将是非常有趣的!
约定
在这本书中,你会发现一些区分不同信息种类的文本样式。以下是一些这些样式的例子,以及它们的含义解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名和用户输入显示如下:“使用 Python,我们可以使用class关键字而不是def关键字创建我们自己的类。”
代码块设置如下:
import tkinter as tk # 1
win = tk.Tk() # 2
win.title("Python GUI") # 3
win.mainloop() # 4
任何命令行输入或输出都以以下方式编写:
**pip install numpy-1.9.2+mkl-cp34-none-win_amd64.whl**
新术语和重要单词以粗体显示。例如,屏幕上看到的单词,菜单或对话框中的单词会以这样的方式出现在文本中:“接下来,我们将为菜单项添加功能,例如,单击退出菜单项时关闭主窗口,并显示帮助|关于对话框。”
注意
警告或重要提示会出现在这样的框中。
提示
提示和技巧会出现在这样的地方。
第一章:创建 GUI 表单并添加小部件
在本章中,我们开始使用 Python 3 创建令人惊叹的 GUI:
-
创建我们的第一个 Python GUI
-
防止 GUI 大小调整
-
将标签添加到 GUI 表单
-
创建按钮并更改其文本属性
-
文本框小部件
-
将焦点设置为小部件并禁用小部件
-
组合框小部件
-
创建具有不同初始状态的复选按钮
-
使用单选按钮小部件
-
使用滚动文本小部件
-
在循环中添加多个小部件
介绍
在本章中,我们将在 Python 中开发我们的第一个 GUI。我们从构建运行的 GUI 应用程序所需的最少代码开始。然后,每个示例都向 GUI 表单添加不同的小部件。
在前两个示例中,我们展示了仅包含几行代码的完整代码。在接下来的示例中,我们只展示要添加到前面示例中的代码。
在本章结束时,我们将创建一个工作的 GUI 应用程序,其中包括各种状态的标签、按钮、文本框、组合框和复选按钮,以及可以更改 GUI 背景颜色的单选按钮。
创建我们的第一个 Python GUI
Python 是一种非常强大的编程语言。它附带了内置的 tkinter 模块。只需几行代码(确切地说是四行),我们就可以构建我们的第一个 Python GUI。
准备工作
要遵循此示例,需要一个可用的 Python 开发环境。Python 附带的 IDLE GUI 足以开始。IDLE 是使用 tkinter 构建的!
注意
本书中的所有示例都是在 Windows 7 64 位操作系统上使用 Python 3.4 开发的。它们尚未在任何其他配置上进行测试。由于 Python 是一种跨平台语言,预计每个示例的代码都可以在任何地方运行。
如果您使用的是 Mac,它确实内置了 Python,但可能缺少一些模块,例如我们将在本书中使用的 tkinter。
我们正在使用 Python 3,Python 的创建者有意选择不与 Python 2 向后兼容。
如果您使用的是 Mac 或 Python 2,您可能需要从www.python.org安装 Python 3,以便成功运行本书中的示例。
如何做...
以下是创建结果 GUI 所需的四行 Python 代码:
import tkinter as tk # 1
win = tk.Tk() # 2
win.title("Python GUI") # 3
win.mainloop() # 4
执行此代码并欣赏结果:

工作原理...
在第 1 行中,我们导入内置的tkinter模块,并将其别名为tk以简化我们的 Python 代码。在第 2 行中,我们通过调用其构造函数(括号附加到Tk将类转换为实例)创建Tk类的实例。我们使用别名tk,这样我们就不必使用更长的单词tkinter。我们将类实例分配给名为win(窗口的缩写)的变量。由于 Python 是一种动态类型的语言,我们在分配给它之前不必声明此变量,并且我们不必给它指定特定的类型。Python 从此语句的分配中推断出类型。Python 是一种强类型的语言,因此每个变量始终都有一个类型。我们只是不必像其他语言那样事先指定其类型。这使得 Python 成为一种非常强大和高效的编程语言。
注意
关于类和类型的一点说明:
在 Python 中,每个变量始终都有一个类型。我们不能创建一个没有分配类型的变量。然而,在 Python 中,我们不必事先声明类型,就像在 C 编程语言中一样。
Python 足够聪明,可以推断类型。在撰写本文时,C#也具有这种能力。
使用 Python,我们可以使用class关键字而不是def关键字来创建自己的类。
为了将类分配给变量,我们首先必须创建我们类的一个实例。我们创建实例并将此实例分配给我们的变量。
class AClass(object):
print('Hello from AClass')
classInstance = AClass()
现在变量classInstance的类型是AClass。
如果这听起来令人困惑,不要担心。我们将在接下来的章节中介绍面向对象编程。
在第 3 行,我们使用类的实例变量(win)通过title属性给我们的窗口设置了一个标题。在第 4 行,通过在类实例win上调用mainloop方法来启动窗口的事件循环。在我们的代码中到目前为止,我们创建了一个实例并设置了一个属性但是 GUI 直到我们启动主事件循环之前都不会显示。
注意
事件循环是使我们的 GUI 工作的机制。我们可以把它看作是一个无限循环,我们的 GUI 在其中等待事件发送给它。按钮点击在我们的 GUI 中创建一个事件,或者我们的 GUI 被调整大小也会创建一个事件。
我们可以提前编写所有的 GUI 代码,直到我们调用这个无限循环(win.mainloop()在上面显示的代码中)用户的屏幕上什么都不会显示。
当用户点击红色的X按钮或者我们编程结束 GUI 的小部件时,事件循环就会结束。当事件循环结束时,我们的 GUI 也会结束。
还有更多...
这个示例使用了最少量的 Python 代码来创建我们的第一个 GUI 程序。然而,在本书中,我们会在合适的时候使用 OOP。
阻止 GUI 的大小可调整
准备工作
这个示例扩展了之前的示例。因此,有必要自己输入第 1 个示例的代码到你自己的项目中,或者从www.packtpub.com/support下载代码。
如何做...
我们正在阻止 GUI 的大小可调整。
import tkinter as tk # 1 imports
win = tk.Tk() # 2 Create instance
win.title("Python GUI") # 3 Add a title
win.resizable(0, 0) # 4 Disable resizing the GUI
win.mainloop() # 5 Start GUI
运行这段代码会创建这个 GUI:

它是如何工作的...
第 4 行阻止 Python GUI 的大小可调整。
运行这段代码将会得到一个类似于我们在第 1 个示例中创建的 GUI。然而,用户不能再调整它的大小。同时,注意窗口工具栏中的最大化按钮是灰色的。
为什么这很重要?因为一旦我们向我们的表单添加小部件,调整大小可能会使我们的 GUI 看起来不如我们希望的那样好。我们将在下一个示例中向我们的 GUI 添加小部件。
Resizable()是Tk()类的一个方法,通过传入(0, 0),我们阻止了 GUI 的大小可调整。如果我们传入其他值,我们就会硬编码 GUI 的 x 和 y 的启动大小,但这不会使它不可调整大小。
我们还在我们的代码中添加了注释,为本书中包含的示例做准备。
注意
在 Visual Studio .NET 等可视化编程 IDE 中,C#程序员通常不会考虑阻止用户调整他们用这种语言开发的 GUI。这会导致 GUI 质量较差。添加这一行 Python 代码可以让我们的用户欣赏我们的 GUI。
向 GUI 表单添加标签
准备工作
我们正在扩展第一个示例。我们将保持 GUI 可调整大小,所以不要使用第二个示例中的代码(或者将第 4 行的win.resizable注释掉)。
如何做...
为了向我们的 GUI 添加一个Label小部件,我们从tkinter中导入了ttk模块。请注意这两个导入语句。
# imports # 1
import tkinter as tk # 2
from tkinter import ttk # 3
在示例 1 和 2 底部的win.mainloop()上面添加以下代码。
# Adding a Label # 4
ttk.Label(win, text="A Label").grid(column=0, row=0) # 5
运行这段代码会向我们的 GUI 添加一个标签:

它是如何工作的...
在上面的代码的第 3 行,我们从tkinter中导入了一个单独的模块。ttk模块有一些高级的小部件,可以让我们的 GUI 看起来很棒。在某种意义上,ttk是tkinter中的一个扩展。
我们仍然需要导入tkinter本身,但是我们必须指定我们现在也想要从tkinter中使用ttk。
注意
ttk代表"themed tk"。它改善了我们的 GUI 外观和感觉。
上面的第 5 行在调用mainloop之前向 GUI 添加了标签(这里没有显示以保持空间。请参见示例 1 或 2)。
我们将我们的窗口实例传递给ttk.Label构造函数,并设置文本属性。这将成为我们的Label将显示的文本。
我们还使用了网格布局管理器,我们将在第二章中更深入地探讨布局管理。
请注意我们的 GUI 突然变得比以前的食谱小得多。
它变得如此之小的原因是我们在表单中添加了一个小部件。没有小部件,tkinter使用默认大小。添加小部件会导致优化,通常意味着尽可能少地使用空间来显示小部件。
如果我们使标签的文本更长,GUI 将自动扩展。我们将在第二章中的后续食谱中介绍这种自动表单大小调整,布局管理。
还有更多...
尝试调整和最大化带有标签的 GUI,看看会发生什么。
创建按钮并更改它们的文本属性
准备就绪
这个食谱扩展了上一个食谱。您可以从 Packt Publishing 网站下载整个代码。
如何做...
我们正在添加一个按钮,当点击时执行一个动作。在这个食谱中,我们将更新上一个食谱中添加的标签,以及更新按钮的文本属性。
# Modify adding a Label # 1
aLabel = ttk.Label(win, text="A Label") # 2
aLabel.grid(column=0, row=0) # 3
# Button Click Event Callback Function # 4
def clickMe(): # 5
action.configure(text="** I have been Clicked! **")
aLabel.configure(foreground='red')
# Adding a Button # 6
action = ttk.Button(win, text="Click Me!", command=clickMe) # 7
action.grid(column=1, row=0) # 8
点击按钮之前:

点击按钮后,标签的颜色已经改变,按钮的文本也改变了。动作!

它是如何工作的...
在第 2 行,我们现在将标签分配给一个变量,在第 3 行,我们使用这个变量来定位表单中的标签。我们将需要这个变量来在clickMe()函数中更改它的属性。默认情况下,这是一个模块级变量,因此只要我们在调用它的函数上方声明变量,我们就可以在函数内部访问它。
第 5 行是一旦按钮被点击就被调用的事件处理程序。
在第 7 行,我们创建按钮并将命令绑定到clickMe()函数。
注意
GUI 是事件驱动的。点击按钮会创建一个事件。我们使用ttk.Button小部件的命令属性绑定事件发生时回调函数中的操作。请注意我们没有使用括号;只有名称clickMe。
我们还将标签的文本更改为包含red,就像印刷版中一样,否则可能不太明显。当您运行代码时,您会看到颜色确实改变了。
第 3 行和第 8 行都使用了网格布局管理器,这将在下一章中讨论。这样可以对齐标签和按钮。
还有更多...
我们将继续向我们的 GUI 中添加更多的小部件,并在本书的其他章节中利用许多内置属性。
文本框小部件
在tkinter中,典型的文本框小部件称为Entry。在这个食谱中,我们将向我们的 GUI 添加这样一个Entry。我们将通过描述Entry为用户做了什么来使我们的标签更有用。
准备就绪
这个食谱是基于创建按钮并更改它们的文本属性食谱的。
如何做...
# Modified Button Click Function # 1
def clickMe(): # 2
action.configure(text='Hello ' + name.get())
# Position Button in second row, second column (zero-based)
action.grid(column=1, row=1)
# Changing our Label # 3
ttk.Label(win, text="Enter a name:").grid(column=0, row=0) # 4
# Adding a Textbox Entry widget # 5
name = tk.StringVar() # 6
nameEntered = ttk.Entry(win, width=12, textvariable=name) # 7
nameEntered.grid(column=0, row=1) # 8
现在我们的 GUI 看起来是这样的:

输入一些文本并点击按钮后,GUI 发生了以下变化:

它是如何工作的...
在第 2 行,我们获取Entry小部件的值。我们还没有使用面向对象编程,那么我们怎么能访问甚至还没有声明的变量的值呢?
在 Python 过程式编码中,如果不使用面向对象编程类,我们必须在尝试使用该名称的语句上方物理放置一个名称。那么为什么这样会起作用呢(它确实起作用)?
答案是按钮单击事件是一个回调函数,当用户单击按钮时,此函数中引用的变量是已知且存在的。
生活很美好。
第 4 行给我们的标签一个更有意义的名称,因为现在它描述了它下面的文本框。我们将按钮移动到标签旁边,以视觉上将两者关联起来。我们仍然使用网格布局管理器,将在第二章中详细解释,布局管理。
第 6 行创建了一个变量name。这个变量绑定到Entry,在我们的“clickMe()”函数中,我们可以通过在这个变量上调用“get()”来检索Entry框的值。这非常有效。
现在我们看到,虽然按钮显示了我们输入的整个文本(以及更多),但文本框Entry小部件没有扩展。原因是我们在第 7 行中将其硬编码为宽度为 12。
注意
Python 是一种动态类型的语言,并且从赋值中推断类型。这意味着如果我们将一个字符串赋给变量“name”,那么该变量将是字符串类型,如果我们将一个整数赋给“name”,那么该变量的类型将是整数。
使用 tkinter,我们必须将变量name声明为类型“tk.StringVar()”才能成功使用它。原因是 Tkinter 不是 Python。我们可以从 Python 中使用它,但它不是相同的语言。
将焦点设置为小部件并禁用小部件
尽管我们的图形用户界面正在不断改进,但在 GUI 出现时让光标立即出现在Entry小部件中会更方便和有用。在这里,我们学习如何做到这一点。
准备工作
这个示例扩展了以前的示例。
如何做...
Python 真的很棒。当 GUI 出现时,我们只需调用先前创建的tkinter小部件实例上的“focus()”方法,就可以将焦点设置为特定控件。在我们当前的 GUI 示例中,我们将ttk.Entry类实例分配给了一个名为nameEntered的变量。现在我们可以给它焦点。
将以下代码放在启动主窗口事件循环的模块底部之上,就像以前的示例一样。如果出现错误,请确保将变量调用放在声明它们的代码下面。我们目前还没有使用面向对象编程,所以这仍然是必要的。以后,将不再需要这样做。
nameEntered.focus() # Place cursor into name Entry
在 Mac 上,您可能必须先将焦点设置为 GUI 窗口,然后才能将焦点设置为该窗口中的Entry小部件。
添加这一行 Python 代码将光标放入我们的文本Entry框中,使文本Entry框获得焦点。一旦 GUI 出现,我们就可以在不必先单击它的情况下在这个文本框中输入。

注意
请注意,光标现在默认驻留在文本Entry框内。
我们也可以禁用小部件。为此,我们在小部件上设置一个属性。通过添加这一行 Python 代码,我们可以使按钮变为禁用状态:
action.configure(state='disabled') # Disable the Button Widget
添加上述一行 Python 代码后,单击按钮不再产生任何动作!

它是如何工作的...
这段代码是不言自明的。我们将焦点设置为一个控件并禁用另一个小部件。在编程语言中良好的命名有助于消除冗长的解释。在本书的后面,将有一些关于如何在工作中编程或在家练习编程技能时进行高级提示。
还有更多...
是的。这只是第一章。还有更多内容。
组合框小部件
在这个示例中,我们将通过添加下拉组合框来改进我们的 GUI,这些下拉组合框可以具有初始默认值。虽然我们可以限制用户只能选择某些选项,但与此同时,我们也可以允许用户输入他们希望的任何内容。
准备工作
这个示例扩展了以前的示例。
如何做...
我们正在使用网格布局管理器在Entry小部件和Button之间插入另一列。以下是 Python 代码。
ttk.Label(win, text="Choose a number:").grid(column=1, row=0) # 1
number = tk.StringVar() # 2
numberChosen = ttk.Combobox(win, width=12, textvariable=number) #3
numberChosen['values'] = (1, 2, 4, 42, 100) # 4
numberChosen.grid(column=1, row=1) # 5
numberChosen.current(0) # 6
将此代码添加到以前的示例中后,将创建以下 GUI。请注意,在前面的代码的第 4 行中,我们将默认值的元组分配给组合框。然后这些值出现在下拉框中。如果需要,我们也可以在应用程序运行时更改它们(通过输入不同的值)。

它是如何工作的...
第 1 行添加了第二个标签以匹配新创建的组合框(在第 3 行创建)。第 2 行将框的值分配给特殊tkinter类型的变量(StringVar),就像我们在之前的示例中所做的那样。
第 5 行将两个新控件(标签和组合框)与我们之前的 GUI 布局对齐,第 6 行在 GUI 首次可见时分配要显示的默认值。这是numberChosen['values']元组的第一个值,字符串"1"。我们在第 4 行没有在整数元组周围放置引号,但它们被转换为字符串,因为在第 2 行,我们声明值为tk.StringVar类型。
屏幕截图显示用户所做的选择(42)。这个值被分配给number变量。
还有更多...
如果我们希望限制用户只能选择我们编程到Combobox中的值,我们可以通过将state 属性传递给构造函数来实现。修改前面代码中的第 3 行:
numberChosen = ttk.Combobox(win, width=12, textvariable=number, state='readonly')
现在用户不能再在Combobox中输入值。我们可以通过在我们的按钮单击事件回调函数中添加以下代码行来显示用户选择的值:
# Modified Button Click Callback Function
def clickMe():
action.configure(text='Hello ' + name.get()+ ' ' + numberChosen.get())
选择一个数字,输入一个名称,然后单击按钮,我们得到以下 GUI 结果,现在还显示了所选的数字:

创建具有不同初始状态的复选按钮
在这个示例中,我们将添加三个Checkbutton小部件,每个小部件都有不同的初始状态。
准备就绪
这个示例扩展了之前的示例。
如何做...
我们创建了三个Checkbutton小部件,它们的状态不同。第一个是禁用的,并且其中有一个复选标记。用户无法移除此复选标记,因为小部件被禁用。
第二个Checkbutton是启用的,并且默认情况下没有复选标记,但用户可以单击它以添加复选标记。
第三个Checkbutton既启用又默认选中。用户可以随意取消选中和重新选中小部件。
# Creating three checkbuttons # 1
chVarDis = tk.IntVar() # 2
check1 = tk.Checkbutton(win, text="Disabled", variable=chVarDis, state='disabled') # 3
check1.select() # 4
check1.grid(column=0, row=4, sticky=tk.W) # 5
chVarUn = tk.IntVar() # 6
check2 = tk.Checkbutton(win, text="UnChecked", variable=chVarUn)
check2.deselect() # 8
check2.grid(column=1, row=4, sticky=tk.W) # 9
chVarEn = tk.IntVar() # 10
check3 = tk.Checkbutton(win, text="Enabled", variable=chVarEn)
check3.select() # 12
check3.grid(column=2, row=4, sticky=tk.W) # 13
运行新代码将得到以下 GUI:

它是如何工作的...
在第 2、6 和 10 行,我们创建了三个IntVar类型的变量。在接下来的一行中,对于这些变量中的每一个,我们创建一个Checkbutton,传入这些变量。它们将保存Checkbutton的状态(未选中或选中)。默认情况下,它们是 0(未选中)或 1(选中),因此变量的类型是tkinter整数。
我们将这些Checkbutton小部件放在我们的主窗口中,因此传递给构造函数的第一个参数是小部件的父级;在我们的情况下是win。我们通过其text属性为每个Checkbutton提供不同的标签。
将网格的 sticky 属性设置为tk.W意味着小部件将对齐到网格的西侧。这与 Java 语法非常相似,意味着它将对齐到左侧。当我们调整 GUI 的大小时,小部件将保持在左侧,并不会向 GUI 的中心移动。
第 4 和 12 行通过调用这两个Checkbutton类实例的select()方法向Checkbutton小部件中放入复选标记。
我们继续使用网格布局管理器来排列我们的小部件,这将在第二章布局管理中详细解释。
使用单选按钮小部件
在这个示例中,我们将创建三个tkinter Radiobutton小部件。我们还将添加一些代码,根据选择的Radiobutton来更改主窗体的颜色。
准备就绪
这个示例扩展了之前的示例。
如何做...
我们将以下代码添加到之前的示例中:
# Radiobutton Globals # 1
COLOR1 = "Blue" # 2
COLOR2 = "Gold" # 3
COLOR3 = "Red" # 4
# Radiobutton Callback # 5
def radCall(): # 6
radSel=radVar.get()
if radSel == 1: win.configure(background=COLOR1)
elif radSel == 2: win.configure(background=COLOR2)
elif radSel == 3: win.configure(background=COLOR3)
# create three Radiobuttons # 7
radVar = tk.IntVar() # 8
rad1 = tk.Radiobutton(win, text=COLOR1, variable=radVar, value=1, command=radCall) # 9
rad1.grid(column=0, row=5, sticky=tk.W) # 10
rad2 = tk.Radiobutton(win, text=COLOR2, variable=radVar, value=2, command=radCall) # 11
rad2.grid(column=1, row=5, sticky=tk.W) # 12
rad3 = tk.Radiobutton(win, text=COLOR3, variable=radVar, value=3, command=radCall) # 13
rad3.grid(column=2, row=5, sticky=tk.W) # 14
运行此代码并选择名为Gold的Radiobutton将创建以下窗口:

它是如何工作的...
在 2-4 行中,我们创建了一些模块级全局变量,我们将在每个单选按钮的创建以及在创建改变主窗体背景颜色的回调函数(使用实例变量win)中使用这些变量。
我们使用全局变量使代码更容易更改。通过将颜色的名称分配给一个变量,并在多个地方使用这个变量,我们可以轻松地尝试不同的颜色。我们只需要更改一行代码,而不是全局搜索和替换硬编码的字符串(容易出错),其他所有东西都会工作。这被称为DRY 原则,代表不要重复自己。这是我们将在本书的后续食谱中使用的面向对象编程概念。
注意
我们分配给变量(COLOR1,COLOR2...)的颜色名称是tkinter关键字(从技术上讲,它们是符号名称)。如果我们使用不是tkinter颜色关键字的名称,那么代码将无法工作。
第 6 行是回调函数,根据用户的选择改变我们主窗体(win)的背景。
在第 8 行,我们创建了一个tk.IntVar变量。重要的是,我们只创建了一个变量供所有三个单选按钮使用。从上面的截图中可以看出,无论我们选择哪个Radiobutton,所有其他的都会自动为我们取消选择。
第 9 到 14 行创建了三个单选按钮,将它们分配给主窗体,并传入要在回调函数中使用的变量,以创建改变主窗口背景的操作。
注意
虽然这是第一个改变小部件颜色的食谱,但老实说,它看起来有点丑。本书中的大部分后续食谱都会解释如何使我们的 GUI 看起来真正令人惊叹。
还有更多...
这里是一小部分可用的符号颜色名称,您可以在官方 tcl 手册页面上查找:
www.tcl.tk/man/tcl8.5/TkCmd/colors.htm
| 名称 | 红 | 绿 | 蓝 |
|---|---|---|---|
| alice blue | 240 | 248 | 255 |
| AliceBlue | 240 | 248 | 255 |
| Blue | 0 | 0 | 255 |
| 金色 | 255 | 215 | 0 |
| 红色 | 255 | 0 | 0 |
一些名称创建相同的颜色,因此alice blue创建的颜色与AliceBlue相同。在这个食谱中,我们使用了符号名称Blue,Gold和Red。
使用滚动文本小部件
ScrolledText小部件比简单的Entry小部件大得多,跨越多行。它们就像记事本一样的小部件,自动换行,并在文本大于ScrolledText小部件的高度时自动启用垂直滚动条。
准备工作
这个食谱扩展了之前的食谱。您可以从 Packt Publishing 网站下载本书每一章的代码。
如何做...
通过添加以下代码行,我们创建了一个ScrolledText小部件:
# Add this import to the top of the Python Module # 1
from tkinter import scrolledtext # 2
# Using a scrolled Text control # 3
scrolW = 30 # 4
scrolH = 3 # 5
scr = scrolledtext.ScrolledText(win, width=scrolW, height=scrolH, wrap=tk.WORD) # 6
scr.grid(column=0, columnspan=3) # 7
我们实际上可以在我们的小部件中输入文字,如果我们输入足够多的单词,行将自动换行!

一旦我们输入的单词超过了小部件可以显示的高度,垂直滚动条就会启用。所有这些都是开箱即用的,我们不需要编写任何额外的代码来实现这一点。

它是如何工作的...
在第 2 行,我们导入包含ScrolledText小部件类的模块。将其添加到模块顶部,就在其他两个import语句的下面。
第 4 和 5 行定义了我们即将创建的ScrolledText小部件的宽度和高度。这些是硬编码的值,我们将它们传递给第 6 行中ScrolledText小部件的构造函数。
这些值是通过实验找到的魔术数字,可以很好地工作。您可以尝试将srcolW从 30 更改为 50,并观察效果!
在第 6 行,我们通过传入wrap=tk.WORD在小部件上设置了一个属性。
通过将wrap属性设置为tk.WORD,我们告诉ScrolledText小部件按单词换行,这样我们就不会在单词中间换行。默认选项是tk.CHAR,它会在单词中间换行。
第二个屏幕截图显示,垂直滚动条向下移动,因为我们正在阅读一个较长的文本,它不能完全适应我们创建的SrolledText控件的 x,y 维度。
将网格小部件的columnspan属性设置为3,使SrolledText小部件跨越所有三列。如果我们不设置这个属性,我们的SrolledText小部件将只驻留在第一列,这不是我们想要的。
在循环中添加多个小部件
到目前为止,我们已经通过基本上复制和粘贴相同的代码,然后修改变化(例如,列号)来创建了几个相同类型的小部件(例如Radiobutton)。在这个示例中,我们开始重构我们的代码,使其不那么冗余。
准备工作
我们正在重构上一个示例代码的一些部分,所以你需要将那个代码应用到这个示例中。
如何做到...
# First, we change our Radiobutton global variables into a list.
colors = ["Blue", "Gold", "Red"] # 1
# create three Radiobuttons using one variable
radVar = tk.IntVar()
Next we are selecting a non-existing index value for radVar.
radVar.set(99) # 2
Now we are creating all three Radiobutton widgets within one loop.
for col in range(3): # 3
curRad = 'rad' + str(col)
curRad = tk.Radiobutton(win, text=colors[col], variable=radVar, value=col, command=radCall)
curRad.grid(column=col, row=5, sticky=tk.W)
We have also changed the callback function to be zero-based, using the list instead of module-level global variables.
# Radiobutton callback function # 4
def radCall():
radSel=radVar.get()
if radSel == 0: win.configure(background=colors[0])
elif radSel == 1: win.configure(background=colors[1])
elif radSel == 2: win.configure(background=colors[2])
运行此代码将创建与以前相同的窗口,但我们的代码更清晰,更易于维护。这将有助于我们在下一个示例中扩展我们的 GUI。
它是如何工作的...
在第 1 行,我们将全局变量转换为列表。
在第 2 行,我们为名为radVar的tk.IntVar变量设置了默认值。这很重要,因为在上一个示例中,我们将Radiobutton小部件的值设置为 1,但在我们的新循环中,使用 Python 的基于零的索引更方便。如果我们没有将默认值设置为超出Radiobutton小部件范围的值,当 GUI 出现时,将选择一个单选按钮。虽然这本身可能并不那么糟糕,它不会触发回调,我们最终会选择一个不起作用的单选按钮(即更改主窗体的颜色)。
在第 3 行,我们用循环替换了之前硬编码创建Radiobutton小部件的三个部分,这样做是一样的。它只是更简洁(代码行数更少)和更易于维护。例如,如果我们想创建 100 个而不仅仅是 3 个Radiobutton小部件,我们只需要改变 Python 的 range 运算符中的数字。我们不必输入或复制粘贴 97 个重复代码段,只需一个数字。
第 4 行显示了修改后的回调,实际上它位于前面的行之上。我们将其放在下面是为了强调这个示例的更重要的部分。
还有更多...
这个示例结束了本书的第一章。接下来章节中的所有示例都将在我们迄今为止构建的 GUI 基础上进行扩展,大大增强它。
第二章:布局管理
在本章中,我们将使用 Python 3 来布局我们的 GUI:
-
在标签框架小部件内排列几个标签
-
使用填充在小部件周围添加空间
-
小部件如何动态扩展 GUI
-
通过在框架内嵌套框架来对齐 GUI 小部件
-
创建菜单栏
-
创建选项卡小部件
-
使用网格布局管理器
介绍
在这一章中,我们将探讨如何在小部件内部排列小部件,以创建我们的 Python GUI。学习 GUI 布局设计的基础知识将使我们能够创建外观出色的 GUI。有一些技术将帮助我们实现这种布局设计。
网格布局管理器是内置在 tkinter 中的最重要的布局工具之一,我们将使用它。
我们可以很容易地使用 tk 来创建菜单栏,选项卡控件(又名 Notebooks)以及许多其他小部件。
tk 中默认缺少的一个小部件是状态栏。
在本章中,我们将不费力地手工制作这个小部件,但这是可以做到的。
在标签框架小部件内排列几个标签
LabelFrame小部件允许我们以有组织的方式设计我们的 GUI。我们仍然使用网格布局管理器作为我们的主要布局设计工具,但通过使用LabelFrame小部件,我们可以更好地控制 GUI 设计。
准备工作
我们开始向我们的 GUI 添加越来越多的小部件,并且我们将在接下来的示例中使 GUI 完全功能。在这里,我们开始使用LabelFrame小部件。我们将重用上一章最后一个示例中的 GUI。
如何做...
在 Python 模块的底部朝向主事件循环上方添加以下代码:
# Create a container to hold labels
labelsFrame = ttk.LabelFrame(win, text=' Labels in a Frame ') # 1
labelsFrame.grid(column=0, row=7)
# Place labels into the container element # 2
ttk.Label(labelsFrame, text="Label1").grid(column=0, row=0)
ttk.Label(labelsFrame, text="Label2").grid(column=1, row=0)
ttk.Label(labelsFrame, text="Label3").grid(column=2, row=0)
# Place cursor into name Entry
nameEntered.focus()

注意
通过更改我们的代码,我们可以轻松地垂直对齐标签,如下所示。请注意,我们唯一需要更改的是列和行编号。
# Place labels into the container element – vertically # 3
ttk.Label(labelsFrame, text="Label1").grid(column=0, row=0)
ttk.Label(labelsFrame, text="Label2").grid(column=0, row=1)
ttk.Label(labelsFrame, text="Label3").grid(column=0, row=2)

它是如何工作的...
注释#1:在这里,我们将创建我们的第一个 ttk LabelFrame 小部件并为框架命名。父容器是win,即我们的主窗口。
在注释#2 之后的三行代码创建标签名称并将它们放置在 LabelFrame 中。我们使用重要的网格布局工具来排列 LabelFrame 内的标签。此布局管理器的列和行属性赋予我们控制 GUI 布局的能力。
注意
我们标签的父级是 LabelFrame,而不是主窗口的win实例变量。我们可以在这里看到布局层次的开始。
突出显示的注释#3 显示了通过列和行属性轻松更改布局的方法。请注意,我们如何将列更改为 0,并且如何通过按顺序编号行值来垂直叠加我们的标签。
注意
ttk 的名称代表“主题 tk”。tk-themed 小部件集是在 Tk 8.5 中引入的。
还有更多...
在本章的后面的一个示例中,我们将嵌套 LabelFrame(s)在 LabelFrame(s)中,以控制我们的 GUI 布局。
使用填充在小部件周围添加空间
我们的 GUI 正在很好地创建。接下来,我们将通过在它们周围添加一点空间来改善我们小部件的视觉效果,以便它们可以呼吸...
准备工作
尽管 tkinter 可能曾经以创建丑陋的 GUI 而闻名,但自 8.5 版本以来(随 Python 3.4.x 一起发布),这种情况发生了显著变化。您只需要知道如何使用可用的工具和技术。这就是我们接下来要做的。
如何做...
首先展示了围绕小部件添加间距的程序化方法,然后我们将使用循环以更好的方式实现相同的效果。
我们的 LabelFrame 看起来有点紧凑,因为它与主窗口向底部融合在一起。让我们现在来修复这个问题。
通过添加padx和pady修改以下代码行:
labelsFrame.grid(column=0, row=7, padx=20, pady=40)
现在我们的 LabelFrame 有了一些空间:

它是如何工作的...
在 tkinter 中,通过使用名为padx和pady的内置属性来水平和垂直地添加空间。这些属性可以用于在许多小部件周围添加空间,分别改善水平和垂直对齐。我们在 LabelFrame 的左右两侧硬编码了 20 像素的空间,并在框架的顶部和底部添加了 40 像素。现在我们的 LabelFrame 比以前更加突出。
注意
上面的屏幕截图只显示了相关的更改。
我们可以使用循环在 LabelFrame 内包含的标签周围添加空间:
for child in labelsFrame.winfo_children():
child.grid_configure(padx=8, pady=4)
现在 LabelFrame 小部件内的标签周围也有一些空间:

grid_configure()函数使我们能够在主循环显示 UI 元素之前修改它们。因此,我们可以在首次创建小部件时,而不是硬编码数值,可以在文件末尾的布局中工作,然后在创建 GUI 之前进行间距调整。这是一个不错的技巧。
winfo_children()函数返回属于labelsFrame变量的所有子项的列表。这使我们能够循环遍历它们并为每个标签分配填充。
注意
要注意的一件事是标签右侧的间距实际上并不明显。这是因为 LabelFrame 的标题比标签的名称长。我们可以通过使标签的名称更长来进行实验。
ttk.Label(labelsFrame, text="Label1 -- sooooo much loooonger...").grid(column=0, row=0)
现在我们的 GUI 看起来像下面这样。请注意,现在在长标签旁边的右侧添加了一些空间。最后一个点没有触及 LabelFrame,如果没有添加的空间,它就会触及。

我们还可以删除 LabelFrame 的名称,以查看padx对定位我们的标签的影响。

小部件如何动态扩展 GUI
您可能已经注意到在之前的屏幕截图中,并通过运行代码,小部件具有扩展自身以视觉显示其文本所需的能力。
注意
Java 引入了动态 GUI 布局管理的概念。相比之下,像 VS.NET 这样的可视化开发 IDE 以可视化方式布局 GUI,并且基本上是在硬编码 UI 元素的 x 和 y 坐标。
使用tkinter,这种动态能力既带来了优势,也带来了一点挑战,因为有时我们的 GUI 会在我们不希望它太动态时动态扩展!好吧,我们是动态的 Python 程序员,所以我们可以想出如何最好地利用这种奇妙的行为!
准备工作
在上一篇食谱的开头,我们添加了一个标签框小部件。这将一些控件移动到第 0 列的中心。我们可能不希望这种修改影响我们的 GUI 布局。接下来,我们将探讨一些修复这个问题的方法。
如何做...
让我们首先注意一下 GUI 布局中正在发生的微妙细节,以更好地理解它。
我们正在使用网格布局管理器小部件,并且它以从零开始的网格布局排列我们的小部件。
| 第 0 行;第 0 列 | 第 0 行;第 1 列 | 第 0 行;第 2 列 |
|---|---|---|
| 第 1 行;第 0 列 | 第 1 行;第 1 列 | 第 1 行;第 2 列 |
使用网格布局管理器时,任何给定列的宽度由该列中最长的名称或小部件确定。这会影响所有行。
通过添加 LabelFrame 小部件并给它一个比某些硬编码大小小部件(如左上角的标签和下面的文本输入)更长的标题,我们动态地将这些小部件移动到第 0 列的中心,并在这些小部件的左右两侧添加空间。
顺便说一句,因为我们为 Checkbutton 和 ScrolledText 小部件使用了 sticky 属性,它们仍然附着在框架的左侧。
让我们更详细地查看本章第一个示例的屏幕截图:

我们添加了以下代码来创建 LabelFrame,然后将标签放入此框架中:
# Create a container to hold labels
labelsFrame = ttk.LabelFrame(win, text=' Labels in a Frame ')
labelsFrame.grid(column=0, row=7)
由于 LabelFrame 的 text 属性(显示为 LabelFrame 的标题)比我们的Enter a name:标签和下面的文本框条目都长,这两个小部件会动态地居中于列 0 的新宽度。
列 0 中的 Checkbutton 和 Radiobutton 小部件没有居中,因为我们在创建这些小部件时使用了sticky=tk.W属性。
对于 ScrolledText 小部件,我们使用了sticky=tk.WE,这将小部件绑定到框架的西(即左)和东(即右)两侧。
让我们从 ScrolledText 小部件中删除 sticky 属性,并观察这个改变的影响。
scr = scrolledtext.ScrolledText(win, width=scrolW, height=scrolH, wrap=tk.WORD)
#### scr.grid(column=0, sticky='WE', columnspan=3)
scr.grid(column=0, columnspan=3)
现在我们的 GUI 在 ScrolledText 小部件的左侧和右侧都有新的空间。因为我们使用了columnspan=3属性,我们的 ScrolledText 小部件仍然跨越了所有三列。

如果我们移除columnspan=3,我们会得到以下 GUI,这不是我们想要的。现在我们的 ScrolledText 只占据列 0,并且由于其大小,它拉伸了布局。

将我们的布局恢复到添加 LabelFrame 之前的方法之一是调整网格列位置。将列值从 0 更改为 1。
labelsFrame.grid(column=1, row=7, padx=20, pady=40)
现在我们的 GUI 看起来像这样:

它是如何工作的...
因为我们仍在使用单独的小部件,所以我们的布局可能会混乱。通过将 LabelFrame 的列值从 0 移动到 1,我们能够将控件放回到它们原来的位置,也是我们喜欢它们的位置。至少最左边的标签、文本、复选框、滚动文本和单选按钮小部件现在位于我们打算的位置。第二个标签和文本Entry位于列 1,它们自己对齐到了Labels in a Frame小部件的长度中心,所以我们基本上将我们的对齐挑战移到了右边一列。这不太明显,因为Choose a number:标签的大小几乎与Labels in a Frame标题的大小相同,因此列宽已经接近 LabelFrame 生成的新宽度。
还有更多...
在下一个教程中,我们将嵌入框架以避免我们在本教程中刚刚经历的小部件意外错位。
通过嵌入框架来对齐 GUI 小部件
如果我们在框架中嵌入框架,我们将更好地控制 GUI 布局。这就是我们将在本教程中做的事情。
准备工作
Python 及其 GUI 模块的动态行为可能会对我们真正想要的 GUI 外观造成一些挑战。在这里,我们将嵌入框架以获得对布局的更多控制。这将在不同 UI 元素之间建立更强的层次结构,使视觉外观更容易实现。
我们将继续使用我们在上一个教程中创建的 GUI。
如何做...
在这里,我们将创建一个顶级框架,其中将包含其他框架和小部件。这将帮助我们将 GUI 布局调整到我们想要的样子。
为了做到这一点,我们将不得不将我们当前的控件嵌入到一个中央 ttk.LabelFrame 中。这个 ttk.LabelFrame 是主父窗口的子窗口,所有控件都是这个 ttk.LabelFrame 的子控件。
在我们的教程中到目前为止,我们已经直接将所有小部件分配给了我们的主 GUI 框架。现在我们将只将我们的 LabelFrame 分配给我们的主窗口,之后,我们将使这个 LabelFrame 成为所有小部件的父容器。
这在我们的 GUI 布局中创建了以下层次结构:

在这个图表中,win是指我们的主 GUI tkinter 窗口框架的变量;monty是指我们的 LabelFrame 的变量,并且是主窗口框架(win)的子窗口;aLabel和所有其他小部件现在都放置在 LabelFrame 容器(monty)中。
在我们的 Python 模块顶部添加以下代码(参见注释#1):
# Create instance
win = tk.Tk()
# Add a title
win.title("Python GUI")
# We are creating a container frame to hold all other widgets # 1
monty = ttk.LabelFrame(win, text=' Monty Python ')
monty.grid(column=0, row=0)
接下来,我们将修改所有以下控件,使用monty作为父控件,替换win。以下是如何做到这一点的示例:
# Modify adding a Label
aLabel = ttk.Label(monty, text="A Label")

请注意,现在所有的小部件都包含在Monty Python LabelFrame 中,它用几乎看不见的细线将它们全部包围起来。接下来,我们可以重置Labels in a Frame小部件到左侧,而不会弄乱我们的 GUI 布局:

哎呀-也许不是。虽然我们在另一个框架中的框架很好地对齐到了左侧,但它又把我们的顶部小部件推到了中间(默认)。
为了将它们对齐到左侧,我们必须使用sticky属性来强制我们的 GUI 布局。通过将其分配为"W"(西),我们可以控制小部件左对齐。
# Changing our Label
ttk.Label(monty, text="Enter a name:").grid(column=0, row=0, sticky='W')

它是如何工作的...
请注意我们对齐了标签,但没有对下面的文本框进行对齐。我们必须使用sticky属性来左对齐我们想要左对齐的所有控件。我们可以在一个循环中做到这一点,使用winfo_children()和grid_configure(sticky='W')属性,就像我们在本章的第 2 个配方中做的那样。
winfo_children()函数返回属于父控件的所有子控件的列表。这使我们能够循环遍历所有小部件并更改它们的属性。
注意
使用 tkinter 来强制左、右、上、下的命名与 Java 非常相似:west、east、north 和 south,缩写为:"W"等等。我们还可以使用以下语法:tk.W 而不是"W"。
在以前的配方中,我们将"W"和"E"组合在一起,使我们的 ScrolledText 小部件使用"WE"附加到其容器的左侧和右侧。我们可以添加更多的组合:"NSE"将使我们的小部件拉伸到顶部、底部和右侧。如果我们的表单中只有一个小部件,例如一个按钮,我们可以使用所有选项使其填满整个框架:"NSWE"。我们还可以使用元组语法:sticky=(tk.N, tk.S, tk.W, tk.E)。
让我们把非常长的标签改回来,并将条目对齐到第 0 列的左侧。
ttk.Label(monty, text="Enter a name:").grid(column=0, row=0, sticky='W')
name = tk.StringVar()
nameEntered = ttk.Entry(monty, width=12, textvariable=name)
nameEntered.grid(column=0, row=1, sticky=tk.W)

注意
为了分离我们的Labels in a Frame LabelFrame 对我们的 GUI 布局的影响,我们不能将这个 LabelFrame 放入与其他小部件相同的 LabelFrame 中。相反,我们直接将它分配给主 GUI 表单(win)。
我们将在以后的章节中做到这一点。
创建菜单栏
在这个配方中,我们将向我们的主窗口添加一个菜单栏,向菜单栏添加菜单,然后向菜单添加菜单项。
准备工作
我们将首先学习如何添加菜单栏、几个菜单和一些菜单项的技巧,以展示如何做到这一点的原则。单击菜单项将不会产生任何效果。接下来,我们将为菜单项添加功能,例如,单击Exit菜单项时关闭主窗口,并显示Help | About对话框。
我们将继续扩展我们在当前和上一章中创建的 GUI。
如何做到...
首先,我们必须从tkinter中导入Menu类。在 Python 模块的顶部添加以下代码,即导入语句所在的地方:
from tkinter import Menu
接下来,我们将创建菜单栏。在模块的底部添加以下代码,就在我们创建主事件循环的地方上面:
menuBar = Menu(win) # 1
win.config(menu=menuBar)
现在我们在菜单栏中添加一个菜单,并将一个菜单项分配给菜单。
fileMenu = Menu(menuBar) # 2
fileMenu.add_command(label="New")
menuBar.add_cascade(label="File", menu=fileMenu)
运行此代码将添加一个菜单栏,其中有一个菜单,其中有一个菜单项。

接下来,我们在我们添加到菜单栏的第一个菜单中添加第二个菜单项。
fileMenu.add_command(label="New")
fileMenu.add_command(label="Exit") # 3
menuBar.add_cascade(label="File", menu=fileMenu)

我们可以通过在现有的 MenuItems 之间添加以下代码(#4)来添加一个分隔线。
fileMenu.add_command(label="New")
fileMenu.add_separator() # 4
fileMenu.add_command(label="Exit")

通过将tearoff属性传递给菜单的构造函数,我们可以删除默认情况下出现在菜单中第一个 MenuItem 上方的第一条虚线。
# Add menu items
fileMenu = Menu(menuBar, tearoff=0) # 5

我们将添加第二个菜单,它将水平放置在第一个菜单的右侧。我们将给它一个菜单项,我们将其命名为关于,为了使其工作,我们必须将这第二个菜单添加到菜单栏。
文件和帮助 | 关于是非常常见的 Windows GUI 布局,我们都很熟悉,我们可以使用 Python 和 tkinter 创建相同的菜单。
菜单的创建顺序和命名可能一开始有点令人困惑,但一旦我们习惯了 tkinter 要求我们如何编码,这实际上变得有趣起来。
helpMenu = Menu(menuBar, tearoff=0) # 6
helpMenu.add_command(label="About")
menuBar.add_cascade(label="Help", menu=helpMenu)

此时,我们的 GUI 有一个菜单栏和两个包含一些菜单项的菜单。单击它们并没有太多作用,直到我们添加一些命令。这就是我们接下来要做的。在创建菜单栏之前添加以下代码:
def _quit(): # 7
win.quit()
win.destroy()
exit()
接下来,我们将文件 | 退出菜单项绑定到这个函数,方法是在菜单项中添加以下命令:
fileMenu.add_command(label="Exit", command=_quit) # 8
现在,当我们点击退出菜单项时,我们的应用程序确实会退出。
它是如何工作的...
在注释#1 中,我们调用了菜单的tkinter构造函数,并将菜单分配给我们的主 GUI 窗口。我们在实例变量中保存了一个名为menuBar的引用,并在下一行代码中,我们使用这个实例来配置我们的 GUI,以使用menuBar作为我们的菜单。
注释#2 显示了我们首先添加一个菜单项,然后创建一个菜单。这似乎有点不直观,但这就是 tkinter 的工作原理。add_cascade()方法将菜单项垂直布局在一起。
注释#3 显示了如何向菜单添加第二个菜单项。
在注释#4 中,我们在两个菜单项之间添加了一个分隔线。这通常用于将相关的菜单项分组并将它们与不太相关的项目分开(因此得名)。
注释#5 禁用了虚线以使我们的菜单看起来更好。
注意
在不禁用此默认功能的情况下,用户可以从主窗口“撕下”菜单。我发现这种功能价值不大。随意双击虚线(在禁用此功能之前)进行尝试。
如果您使用的是 Mac,这个功能可能没有启用,所以您根本不用担心。

注释#6 向您展示了如何向菜单栏添加第二个菜单。我们可以继续使用这种技术添加菜单。
注释#7 创建了一个函数来干净地退出我们的 GUI 应用程序。这是结束主事件循环的推荐 Pythonic 方式。
在#8 中,我们将在#7 中创建的函数绑定到菜单项,使用tkinter命令属性。每当我们想要我们的菜单项实际执行某些操作时,我们必须将它们中的每一个绑定到一个函数。
注意
我们使用了推荐的 Python 命名约定,通过在退出函数之前加上一个下划线,以表示这是一个私有函数,不应该由我们代码的客户端调用。
还有更多...
在下一章中,我们将添加帮助 | 关于功能,介绍消息框等等。
创建选项卡小部件
在这个配方中,我们将创建选项卡小部件,以进一步组织我们在 tkinter 中编写的扩展 GUI。
准备工作
为了改进我们的 Python GUI,我们将从头开始,使用最少量的代码。在接下来的配方中,我们将从以前的配方中添加小部件,并将它们放入这个新的选项卡布局中。
如何做...
创建一个新的 Python 模块,并将以下代码放入该模块:
import tkinter as tk # imports
from tkinter import ttk
win = tk.Tk() # Create instance
win.title("Python GUI") # Add a title
tabControl = ttk.Notebook(win) # Create Tab Control
tab1 = ttk.Frame(tabControl) # Create a tab
tabControl.add(tab1, text='Tab 1') # Add the tab
tabControl.pack(expand=1, fill="both") # Pack to make visible
win.mainloop() # Start GUI
这创建了以下 GUI:

尽管目前还不是非常令人印象深刻,但这个小部件为我们的 GUI 设计工具包增加了另一个非常强大的工具。它在上面的极简示例中有自己的限制(例如,我们无法重新定位 GUI,也不显示整个 GUI 标题)。
在以前的示例中,我们使用网格布局管理器来创建更简单的 GUI,我们可以使用更简单的布局管理器之一,“pack”是其中之一。
在上述代码中,我们将 tabControl ttk.Notebook“pack”到主 GUI 表单中,扩展选项卡控件以填充所有边缘。

我们可以向我们的控件添加第二个选项卡并在它们之间切换。
tab2 = ttk.Frame(tabControl) # Add a second tab
tabControl.add(tab2, text='Tab 2') # Make second tab visible
win.mainloop() # Start GUI
现在我们有两个标签。单击Tab 2以使其获得焦点。

我们真的很想看到我们的窗口标题。因此,为了做到这一点,我们必须向我们的选项卡中添加一个小部件。该小部件必须足够宽,以动态扩展我们的 GUI 以显示我们的窗口标题。我们正在将 Ole Monty 和他的孩子们重新添加。
monty = ttk.LabelFrame(tab1, text=' Monty Python ')
monty.grid(column=0, row=0, padx=8, pady=4)
ttk.Label(monty, text="Enter a name:").grid(column=0, row=0, sticky='W')
现在我们在Tab1中有我们的Monty Python。

我们可以继续将到目前为止创建的所有小部件放入我们新创建的选项卡控件中。

现在所有的小部件都驻留在Tab1中。让我们将一些移动到Tab2。首先,我们创建第二个 LabelFrame,作为我们将移动到Tab2的小部件的容器:
monty2 = ttk.LabelFrame(tab2, text=' The Snake ')
monty2.grid(column=0, row=0, padx=8, pady=4)
接下来,我们通过指定新的父容器monty2,将复选框和单选按钮移动到Tab2。以下是一个示例,我们将其应用于所有移动到Tab2的控件:
chVarDis = tk.IntVar()
check1 = tk.Checkbutton(monty2, text="Disabled", variable=chVarDis, state='disabled')
当我们运行代码时,我们的 GUI 现在看起来不同了。Tab1的小部件比以前少了,当它包含我们以前创建的所有小部件时。

现在我们可以单击Tab 2并查看我们移动的小部件。

单击移动的 Radiobutton(s)不再产生任何效果,因此我们将更改它们的操作以重命名文本属性,这是 LabelFrame 小部件的标题,以显示 Radiobuttons 的名称。当我们单击Gold Radiobutton 时,我们不再将框架的背景设置为金色,而是在这里替换 LabelFrame 文本标题。Python“ The Snake”现在变成“Gold”。
# Radiobutton callback function
def radCall():
radSel=radVar.get()
if radSel == 0: monty2.configure(text='Blue')
elif radSel == 1: monty2.configure(text='Gold')
elif radSel == 2: monty2.configure(text='Red')
现在,选择任何 RadioButton 小部件都会导致更改 LabelFrame 的名称。

它是如何工作的...
创建第二个选项卡后,我们将一些最初驻留在Tab1中的小部件移动到Tab2。添加选项卡是组织我们不断增加的 GUI 的另一种绝佳方式。这是处理 GUI 设计中复杂性的一种非常好的方式。我们可以将小部件分组放置在它们自然属于的组中,并通过使用选项卡使我们的用户摆脱混乱。
注意
在tkinter中,通过Notebook小部件创建选项卡是通过Notebook小部件完成的,这是允许我们添加选项卡控件的工具。 tkinter 笔记本小部件,就像许多其他小部件一样,具有我们可以使用和配置的附加属性。探索我们可以使用的 tkinter 小部件的其他功能的绝佳起点是官方网站:docs.python.org/3.1/library/tkinter.ttk.html#notebook
使用网格布局管理器
网格布局管理器是我们可以使用的最有用的布局工具之一。我们已经在许多示例中使用了它,因为它非常强大。
准备工作...
在这个示例中,我们将回顾一些网格布局管理器的技术。我们已经使用过它们,在这里我们将进一步探讨它们。
如何做...
在本章中,我们已经创建了行和列,这实际上是 GUI 设计的数据库方法(MS Excel 也是如此)。我们硬编码了前四行,但然后忘记了给下一行一个我们希望它驻留的位置的规范。
Tkinter 在我们不知不觉中为我们填充了这个。
以下是我们在代码中所做的:
check3.grid(column=2, row=4, sticky=tk.W, columnspan=3)
scr.grid(column=0, sticky='WE', columnspan=3) # 1
curRad.grid(column=col, row=6, sticky=tk.W, columnspan=3)
labelsFrame.grid(column=0, row=7)
Tkinter 自动添加了我们没有指定任何特定行的缺失行(在注释#1 中强调)。我们可能没有意识到这一点。
我们将复选框布置在第 4 行,然后“忘记”为我们的 ScrolledText 小部件指定行,我们通过 scr 变量引用它,然后我们添加了要布置在第 6 行的 Radiobutton 小部件。
这很好用,因为 tkinter 自动递增了我们的 ScrolledText 小部件的行位置,以使用下一个最高的行号,即第 5 行。
查看我们的代码,没有意识到我们“忘记”将我们的 ScrolledText 小部件明确定位到第 5 行,我们可能会认为那里什么都没有。
因此,我们可以尝试以下操作。
如果我们将变量curRad设置为使用第 5 行,我们可能会得到一个不愉快的惊喜:

它是如何工作的...
注意我们的 RadioButton(s)行突然出现在我们的 ScrolledText 小部件的中间!这绝对不是我们想要的 GUI 样式!
注意
如果我们忘记显式指定行号,默认情况下,tkinter将使用下一个可用的行。
我们还使用了columnspan属性来确保我们的小部件不会被限制在一列。以下是我们如何确保我们的 ScrolledText 小部件跨越 GUI 的所有列:
# Using a scrolled Text control
scrolW = 30; scrolH = 3
scr = ScrolledText(monty, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=0, sticky='WE', columnspan=3)
第三章:外观定制
在本章中,我们将使用 Python 3 自定义我们的 GUI:
-
创建消息框-信息、警告和错误
-
如何创建独立的消息框
-
如何创建 tkinter 窗体的标题
-
更改主根窗口的图标
-
使用旋转框控件
-
小部件的浮雕、凹陷和凸起外观
-
使用 Python 创建工具提示
-
如何使用画布小部件
介绍
在本章中,我们将通过更改一些属性来自定义 GUI 中的一些小部件。我们还介绍了一些 tkinter 提供给我们的新小部件。
使用 Python 创建工具提示示例将创建一个 ToolTip 面向对象的类,它将成为我们到目前为止一直在使用的单个 Python 模块的一部分。
创建消息框-信息、警告和错误
消息框是一个弹出窗口,向用户提供反馈。它可以是信息性的,暗示潜在问题,甚至是灾难性的错误。
使用 Python 创建消息框非常容易。
准备工作
我们将为上一个示例中创建的“帮助”|“关于”菜单项添加功能。在大多数应用程序中,单击“帮助”|“关于”菜单时向用户提供的典型反馈是信息性的。我们从这个信息开始,然后变化设计模式以显示警告和错误。
如何做...
将以下代码添加到导入语句所在的模块顶部:
from tkinter import messagebox as mBox
接下来,我们将创建一个回调函数来显示一个消息框。我们必须将回调的代码放在我们将回调附加到菜单项的代码上面,因为这仍然是过程性的而不是面向对象的代码。
将此代码添加到创建帮助菜单的行的上方:
# Display a Message Box
# Callback function
def _msgBox():
mBox.showinfo('Python Message Info Box', 'A Python GUI created using tkinter:\nThe year is 2015.')
# Add another Menu to the Menu Bar and an item
helpMenu = Menu(menuBar, tearoff=0)
helpMenu.add_command(label="About", command=_msgBox)
现在单击“帮助”|“关于”会导致以下弹出窗口出现:

让我们将这段代码转换为警告消息框弹出窗口。注释掉上一行并添加以下代码:
# Display a Message Box
def _msgBox():
# mBox.showinfo('Python Message Info Box', 'A Python GUI
# created using tkinter:\nThe year is 2015.')
mBox.showwarning('Python Message Warning Box', 'A Python GUI created using tkinter:\nWarning: There might be a bug in this code.')
运行上面的代码现在会导致以下略微修改的消息框出现:

显示错误消息框很简单,通常警告用户存在严重问题。如上所述,注释掉并添加此代码,如我们在这里所做的:
# Display a Message Box
def _msgBox():
# mBox.showinfo('Python Message Info Box', 'A Python GUI
# created using tkinter:\nThe year is 2015.')
# mBox.showwarning('Python Message Warning Box', 'A Python GUI
# created using tkinter:\nWarning: There might be a bug in
# this code.')
mBox.showerror('Python Message Error Box', 'A Python GUI created using tkinter:\nError: Houston ~ we DO have a serious PROBLEM!')

工作原理...
我们添加了另一个回调函数,并将其附加为处理单击事件的委托。现在,当我们单击“帮助”|“关于”菜单时,会发生一个动作。我们正在创建和显示最常见的弹出式消息框对话框。它们是模态的,因此用户在点击“确定”按钮之前无法使用 GUI。
在第一个示例中,我们显示了一个信息框,可以看到其左侧的图标。接下来,我们创建警告和错误消息框,它们会自动更改与弹出窗口关联的图标。我们只需指定要显示哪个 mBox。
有不同的消息框显示多个“确定”按钮,我们可以根据用户的选择来编程我们的响应。
以下是一个简单的例子,说明了这种技术:
# Display a Message Box
def _msgBox():
answer = mBox.askyesno("Python Message Dual Choice Box", "Are you sure you really wish to do this?")
print(answer)
运行此 GUI 代码会导致弹出一个用户响应可以用来分支的窗口,通过将其保存在answer变量中来驱动此事件驱动的 GUI 循环的答案。

在 Eclipse 中使用控制台输出显示,单击“是”按钮会导致将布尔值True分配给answer变量。

例如,我们可以使用以下代码:
If answer == True:
<do something>
如何创建独立的消息框
在这个示例中,我们将创建我们的 tkinter 消息框作为独立的顶层 GUI 窗口。
我们首先注意到,这样做会多出一个窗口,因此我们将探索隐藏此窗口的方法。
在上一个示例中,我们通过我们主 GUI 表单中的“帮助”|“关于”菜单调用了 tkinter 消息框。
那么为什么我们希望创建一个独立的消息框呢?
一个原因是我们可能会自定义我们的消息框,并在我们的 GUI 中重用它们。我们可以将它们从我们的主 GUI 代码中分离出来,而不是在我们设计的每个 Python GUI 中复制和粘贴相同的代码。这可以创建一个小的可重用组件,然后我们可以将其导入到不同的 Python GUI 中。
准备工作
我们已经在上一个食谱中创建了消息框的标题。我们不会重用上一个食谱中的代码,而是会用很少的 Python 代码构建一个新的 GUI。
如何做...
我们可以像这样创建一个简单的消息框:
from tkinter import messagebox as mBox
mBox.showinfo('A Python GUI created using tkinter:\nThe year is 2015')
这将导致这两个窗口:

这看起来不像我们想象的那样。现在我们有两个窗口,一个是不需要的,第二个是其文本显示为标题。
哎呀。
现在让我们来修复这个问题。我们可以通过添加一个单引号或双引号,后跟一个逗号来更改 Python 代码。
mBox.showinfo('', 'A Python GUI created using tkinter:\nThe year is 2015')

第一个参数是标题,第二个是弹出消息框中显示的文本。通过添加一对空的单引号或双引号,后跟一个逗号,我们可以将我们的文本从标题移到弹出消息框中。
我们仍然需要一个标题,而且我们肯定想摆脱这个不必要的第二个窗口。
注意
在像 C#这样的语言中,会出现第二个窗口的相同现象。基本上是一个 DOS 风格的调试窗口。许多程序员似乎不介意有这个额外的窗口漂浮。从 GUI 编程的角度来看,我个人觉得这很不雅。我们将在下一步中删除它。
第二个窗口是由 Windows 事件循环引起的。我们可以通过抑制它来摆脱它。
添加以下代码:
from tkinter import messagebox as mBox
from tkinter import Tk
root = Tk()
root.withdraw()
mBox.showinfo('', 'A Python GUI created using tkinter:\nThe year is 2015')
现在我们只有一个窗口。withdraw()函数移除了我们不希望漂浮的调试窗口。

为了添加标题,我们只需将一些字符串放入我们的空第一个参数中。
例如:
from tkinter import messagebox as mBox
from tkinter import Tk
root = Tk()
root.withdraw()
mBox.showinfo('This is a Title', 'A Python GUI created using tkinter:\nThe year is 2015')
现在我们的对话框有了标题:

它是如何工作的...
我们将更多参数传递给消息框的 tkinter 构造函数,以添加窗体的标题并在消息框中显示文本,而不是将其显示为标题。这是由于我们传递的参数的位置。如果我们省略空引号或双引号,那么消息框小部件将把第一个参数的位置作为标题,而不是要在消息框中显示的文本。通过传递一个空引号后跟一个逗号,我们改变了消息框显示我们传递给函数的文本的位置。
我们通过在我们的主根窗口上调用withdraw()方法来抑制 tkinter 消息框小部件自动创建的第二个弹出窗口。
如何创建 tkinter 窗体的标题
更改 tkinter 主根窗口的标题的原则与前一个食谱中讨论的原则相同。我们只需将一个字符串作为小部件的构造函数的第一个参数传递进去。
准备工作
与弹出对话框窗口不同,我们创建主根窗口并给它一个标题。
在这个食谱中显示的 GUI 是上一章的代码。它不是在本章中基于上一个食谱构建的。
如何做...
以下代码创建了主窗口并为其添加了标题。我们已经在以前的食谱中做过这个。在这里,我们只关注 GUI 的这个方面。
import tkinter as tk
win = tk.Tk() # Create instance
win.title("Python GUI") # Add a title

它是如何工作的...
通过使用内置的 tkinter title 属性,为主根窗口添加标题。在创建Tk()实例后,我们可以使用所有内置的 tkinter 属性来自定义我们的 GUI。
更改主根窗口的图标
自定义 GUI 的一种方法是给它一个与 tkinter 默认图标不同的图标。下面是我们如何做到这一点。
准备工作
我们正在改进上一个配方的 GUI。我们将使用一个随 Python 一起提供的图标,但您可以使用任何您认为有用的图标。确保您在代码中有图标所在的完整路径,否则可能会出错。
注意
虽然可能会有点混淆,上一章的这个配方指的是哪个配方,最好的方法就是只下载本书的代码,然后逐步执行代码以理解它。
如何做...
将以下代码放在主事件循环的上方某处。示例使用了我安装 Python 3.4 的路径。您可能需要调整它以匹配您的安装目录。
请注意 GUI 左上角的“feather”默认图标已更改。
# Change the main windows icon
win.iconbitmap(r'C:\Python34\DLLs\pyc.ico')

它是如何工作的...
这是另一个与 Python 3.x 一起提供的 tkinter 的属性。 iconbitmap是我们使用的属性,通过传递图标的绝对(硬编码)路径来改变主根窗口的图标。这将覆盖 tkinter 的默认图标,用我们选择的图标替换它。
注意
在上面的代码中,使用绝对路径的字符串中的“r”来转义反斜杠,因此我们可以使用“raw”字符串,而不是写C:\\,这让我们可以写更自然的单个反斜杠C:\。这是 Python 为我们创建的一个巧妙的技巧。
使用旋转框控件
在这个示例中,我们将使用Spinbox小部件,并且还将绑定键盘上的Enter键到我们的小部件之一。
准备工作
我们正在使用我们的分页 GUI,并将在ScrolledText控件上方添加一个Spinbox小部件。这只需要我们将ScrolledText行值增加一,并在Entry小部件上面的行中插入我们的新Spinbox控件。
如何做...
首先,我们添加了Spinbox控件。将以下代码放在ScrolledText小部件上方:
# Adding a Spinbox widget
spin = Spinbox(monty, from_=0, to=10)
spin.grid(column=0, row=2)
这将修改我们的 GUI,如下所示:

接下来,我们将减小Spinbox小部件的大小。
spin = Spinbox(monty, from_=0, to=10, width=5)

接下来,我们添加另一个属性来进一步自定义我们的小部件,bd是borderwidth属性的简写表示。
spin = Spinbox(monty, from_=0, to=10, width=5 , bd=8)

在这里,我们通过创建回调并将其链接到控件来为小部件添加功能。
这将把 Spinbox 的选择打印到ScrolledText中,也打印到标准输出。名为scr的变量是我们对ScrolledText小部件的引用。
# Spinbox callback
def _spin():
value = spin.get()
print(value)
scr.insert(tk.INSERT, value + '\n')
spin = Spinbox(monty, from_=0, to=10, width=5, bd=8, command=_spin)

除了使用范围,我们还可以指定一组值。
# Adding a Spinbox widget using a set of values
spin = Spinbox(monty, values=(1, 2, 4, 42, 100), width=5, bd=8, command=_spin)
spin.grid(column=0, row=2)
这将创建以下 GUI 输出:

它是如何工作的...
请注意,在第一个屏幕截图中,我们的新Spinbox控件默认为宽度 20,推出了此列中所有控件的列宽。这不是我们想要的。我们给小部件一个从 0 到 10 的范围,并且默认显示to=10值,这是最高值。如果我们尝试将from_/to范围从 10 到 0 反转,tkinter 不会喜欢。请自行尝试。
在第二个屏幕截图中,我们减小了Spinbox控件的宽度,这使其与列的中心对齐。
在第三个屏幕截图中,我们添加了 Spinbox 的borderwidth属性,这自动使整个Spinbox看起来不再是平的,而是三维的。
在第四个屏幕截图中,我们添加了一个回调函数,以显示在ScrolledText小部件中选择的数字,并将其打印到标准输出流中。我们添加了“\n”以打印在新行上。请注意默认值不会被打印。只有当我们单击控件时,回调函数才会被调用。通过单击默认为 10 的向上箭头,我们可以打印值“10”。
最后,我们将限制可用的值为硬编码集。这也可以从数据源(例如文本或 XML 文件)中读取。
小部件的 Relief、sunken 和 raised 外观
我们可以通过一个属性来控制Spinbox小部件的外观,使它们看起来是凸起的、凹陷的,或者是凸起的格式。
准备工作
我们将添加一个Spinbox控件来演示Spinbox控件的relief属性的可用外观。
如何做...
首先,让我们增加borderwidth以区分我们的第二个Spinbox和第一个Spinbox。
# Adding a second Spinbox widget
spin = Spinbox(monty, values=(0, 50, 100), width=5, bd=20, command=_spin)
spin.grid(column=1, row=2)

我们上面的两个 Spinbox 小部件具有相同的relief样式。唯一的区别是,我们右边的新小部件的边框宽度要大得多。
在我们的代码中,我们没有指定使用哪个relief属性,所以relief默认为tk.SUNKEN。
以下是可以设置的可用relief属性选项:
| tk.SUNKEN | tk.RAISED | tk.FLAT | tk.GROOVE | tk.RIDGE |
|---|
通过将不同的可用选项分配给relief属性,我们可以为这个小部件创建不同的外观。
将tk.RIDGE的relief属性分配给它,并将边框宽度减小到与我们的第一个Spinbox小部件相同的值,结果如下 GUI 所示:

它是如何工作的...
首先,我们创建了一个第二个Spinbox,对齐到第二列(索引==1)。它默认为SUNKEN,所以它看起来类似于我们的第一个Spinbox。我们通过增加第二个控件(右边的控件)的边框宽度来区分这两个小部件。
接下来,我们隐式地设置了Spinbox小部件的relief属性。我们使边框宽度与我们的第一个Spinbox相同,因为通过给它一个不同的relief,不需要改变任何其他属性,差异就变得可见了。
使用 Python 创建工具提示
这个示例将向我们展示如何创建工具提示。当用户将鼠标悬停在小部件上时,将以工具提示的形式提供额外的信息。
我们将把这些额外的信息编码到我们的 GUI 中。
准备工作
我们正在为我们的 GUI 添加更多有用的功能。令人惊讶的是,向我们的控件添加工具提示应该很简单,但实际上并不像我们希望的那样简单。
为了实现这种期望的功能,我们将把我们的工具提示代码放入自己的面向对象编程类中。
如何做...
在导入语句的下面添加这个类:
class ToolTip(object):
def __init__(self, widget):
self.widget = widget
self.tipwindow = None
self.id = None
self.x = self.y = 0
def showtip(self, text):
"Display text in tooltip window"
self.text = text
if self.tipwindow or not self.text:
return
x, y, _cx, cy = self.widget.bbox("insert")
x = x + self.widget.winfo_rootx() + 27
y = y + cy + self.widget.winfo_rooty() +27
self.tipwindow = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
label = tk.Label(tw, text=self.text, justify=tk.LEFT,
background="#ffffe0", relief=tk.SOLID, borderwidth=1,
font=("tahoma", "8", "normal"))
label.pack(ipadx=1)
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
#===========================================================
def createToolTip( widget, text):
toolTip = ToolTip(widget)
def enter(event):
toolTip.showtip(text)
def leave(event):
toolTip.hidetip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
在面向对象编程(OOP)方法中,我们在 Python 模块中创建一个新的类。Python 允许我们将多个类放入同一个 Python 模块中,并且还可以在同一个模块中“混合和匹配”类和常规函数。
上面的代码正在做这个。
ToolTip类是一个 Python 类,为了使用它,我们必须实例化它。
如果你不熟悉面向对象的编程,“实例化一个对象以创建类的实例”可能听起来相当无聊。
这个原则非常简单,非常类似于通过def语句创建一个 Python 函数,然后在代码中稍后调用这个函数。
以非常相似的方式,我们首先创建一个类的蓝图,并通过在类的名称后面添加括号将其分配给一个变量:
class AClass():
pass
instanceOfAClass = AClass()
print(instanceOfAClass)
上面的代码打印出一个内存地址,并且显示我们的变量现在引用了这个类实例。
面向对象编程的很酷的一点是,我们可以创建同一个类的许多实例。
在我们之前的代码中,我们声明了一个 Python 类,并明确地让它继承自所有 Python 类的基础对象。我们也可以将其省略,就像我们在AClass代码示例中所做的那样,因为它是所有 Python 类的默认值。
在ToolTip类中发生的所有必要的工具提示创建代码之后,我们接下来转到非面向对象的 Python 编程,通过在其下方创建一个函数。
我们定义了函数createToolTip(),它期望我们的 GUI 小部件之一作为参数传递进来,这样当我们将鼠标悬停在这个控件上时,我们就可以显示一个工具提示。
createToolTip()函数实际上为我们为每个调用它的小部件创建了ToolTip类的一个新实例。
我们可以为我们的 Spinbox 小部件添加一个工具提示,就像这样:
# Add a Tooltip
createToolTip(spin, 'This is a Spin control.')
以及我们所有其他 GUI 小部件的方式完全相同。我们只需传入我们希望显示一些额外信息的工具提示的小部件的父级。对于我们的 ScrolledText 小部件,我们使变量scr指向它,所以这就是我们传递给我们的 ToolTip 创建函数构造函数的内容。
# Using a scrolled Text control
scrolW = 30; scrolH = 3
scr = scrolledtext.ScrolledText(monty, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=0, row=3, sticky='WE', columnspan=3)
# Add a Tooltip to the ScrolledText widget
createToolTip(scr, 'This is a ScrolledText widget.')
它是如何工作的...
这是本书中面向对象编程的开始。这可能看起来有点高级,但不用担心,我们会解释一切,它确实有效!
嗯,实际上运行这段代码并没有起作用,也没有任何区别。
在创建微调器之后,添加以下代码:
# Add a Tooltip
createToolTip(spin, 'This is a Spin control.')
现在,当我们将鼠标悬停在微调小部件上时,我们会得到一个工具提示,为用户提供额外的信息。

我们调用创建工具提示的函数,然后将对小部件的引用和我们希望在悬停鼠标在小部件上时显示的文本传递进去。
本书中的其余示例将在合适的情况下使用面向对象编程。在这里,我们展示了可能的最简单的面向对象编程示例。作为默认,我们创建的每个 Python 类都继承自object基类。作为一个真正的务实的编程语言,Python 简化了类的创建过程。
我们可以写成这样:
class ToolTip(object):
pass
我们也可以通过省略默认的基类来简化它:
class ToolTip():
pass
在同样的模式中,我们可以继承和扩展任何 tkinter 类。
如何使用画布小部件
这个示例展示了如何通过使用 tkinter 画布小部件为我们的 GUI 添加戏剧性的颜色效果。
准备工作
通过为其添加更多的颜色,我们将改进我们先前的代码和 GUI 的外观。
如何做...
首先,我们将在我们的 GUI 中创建第三个选项卡,以便隔离我们的新代码。
以下是创建新的第三个选项卡的代码:
# Tab Control introduced here --------------------------------
tabControl = ttk.Notebook(win) # Create Tab Control
tab1 = ttk.Frame(tabControl) # Create a tab
tabControl.add(tab1, text='Tab 1') # Add the tab
tab2 = ttk.Frame(tabControl) # Add a second tab
tabControl.add(tab2, text='Tab 2') # Make second tab visible
tab3 = ttk.Frame(tabControl) # Add a third tab
tabControl.add(tab3, text='Tab 3') # Make second tab visible
tabControl.pack(expand=1, fill="both") # Pack to make visible
# ~ Tab Control introduced here -------------------------------
接下来,我们使用 tkinter 的另一个内置小部件,即画布。很多人喜欢这个小部件,因为它具有强大的功能。
# Tab Control 3 -------------------------------
tab3 = tk.Frame(tab3, bg='blue')
tab3.pack()
for orangeColor in range(2):
canvas = tk.Canvas(tab3, width=150, height=80, highlightthickness=0, bg='orange')
canvas.grid(row=orangeColor, column=orangeColor)
它是如何工作的...
以下屏幕截图显示了通过运行上述代码并单击新的Tab 3创建的结果。当你运行代码时,它真的是橙色和蓝色的。在这本无色的书中,这可能不太明显,但这些颜色是真实的;你可以相信我。
您可以通过在线搜索来查看绘图和绘制功能。我不会在这本书中深入探讨这个小部件(但它确实很酷)。

第四章:数据和类
在本章中,我们将使用 Python 3 来使用数据和 OOP 类:
-
如何使用 StringVar()
-
如何从小部件获取数据
-
使用模块级全局变量
-
如何在类中编码可以改进 GUI
-
编写回调函数
-
创建可重用的 GUI 组件
介绍
在本章中,我们将把 GUI 数据保存到 tkinter 变量中。
我们还将开始使用面向对象编程(OOP)来扩展现有的 tkinter 类,以扩展 tkinter 的内置功能。这将使我们创建可重用的 OOP 组件。
如何使用 StringVar()
在 tkinter 中有一些内置的编程类型,它们与我们习惯用 Python 编程的类型略有不同。StringVar()就是这些 tkinter 类型之一。
这个示例将向您展示如何使用 StringVar()类型。
准备工作
我们正在学习如何将 tkinter GUI 中的数据保存到变量中,以便我们可以使用这些数据。我们可以设置和获取它们的值,与 Java 的 getter/setter 方法非常相似。
这里是 tkinter 中可用的一些编码类型:
strVar = StringVar() |
# 保存一个字符串;默认值是一个空字符串"" |
|---|---|
intVar = IntVar() |
# 保存一个整数;默认值是 0 |
dbVar = DoubleVar() |
# 保存一个浮点数;默认值是 0.0 |
blVar = BooleanVar() |
# 保存一个布尔值,对于 false 返回 0,对于 true 返回 1 |
注意
不同的语言称带有小数点的数字为浮点数或双精度数。Tkinter 将 Python 中称为浮点数据类型的内容称为 DoubleVar。根据精度级别,浮点数和双精度数据可能不同。在这里,我们将 tkinter 的 DoubleVar 翻译成 Python 中的 Python 浮点类型。
如何做...
我们正在创建一个新的 Python 模块,下面的截图显示了代码和生成的输出:

首先,我们导入 tkinter 模块并将其别名为tk。
接下来,我们使用这个别名通过在Tk后面加括号来创建Tk类的一个实例,这样就调用了类的构造函数。这与调用函数的机制相同,只是这里我们创建了一个类的实例。
通常我们使用分配给变量win的实例来在代码中稍后启动主事件循环。但是在这里,我们不显示 GUI,而是演示如何使用 tkinter 的 StringVar 类型。
注意
我们仍然必须创建Tk()的一个实例。如果我们注释掉这一行,我们将从 tkinter 得到一个错误,因此这个调用是必要的。
然后我们创建一个 tkinter StringVar 类型的实例,并将其分配给我们的 PythonstrData变量。
之后,我们使用我们的变量调用 StringVar 的set()方法,并在设置为一个值后,然后获取该值并将其保存在一个名为varData的新变量中,然后打印出它的值。
在 Eclipse PyDev 控制台中,可以看到输出打印到控制台的底部,这是Hello StringVar。
接下来,我们将打印 tkinter 的 IntVar、DoubleVar 和 BooleanVar 类型的默认值。

它是如何工作的...
如前面的截图所示,默认值并没有像我们预期的那样被打印出来。
在线文献提到了默认值,但在调用它们的get方法之前,我们不会看到这些值。否则,我们只会得到一个自动递增的变量名(例如在前面的截图中可以看到的 PY_VAR3)。
将 tkinter 类型分配给 Python 变量并不会改变结果。我们仍然没有得到默认值。
在这里,我们专注于最简单的代码(创建 PY_VAR0):

该值是 PY_VAR0,而不是预期的 0,直到我们调用get方法。现在我们可以看到默认值。我们没有调用set,所以一旦我们在每种类型上调用get方法,就会看到自动分配给每种 tkinter 类型的默认值。

注意IntVar实例的默认值为 0 被打印到控制台,我们将其保存在intData变量中。我们还可以在屏幕截图的顶部看到 Eclipse PyDev 调试器窗口中的值。
如何从小部件中获取数据
当用户输入数据时,我们希望在我们的代码中对其进行处理。这个配方展示了如何在变量中捕获数据。在上一个配方中,我们创建了几个 tkinter 类变量。它们是独立的。现在我们正在将它们连接到我们的 GUI,使用我们从 GUI 中获取的数据并将其存储在 Python 变量中。
准备工作
我们将继续使用我们在上一章中构建的 Python GUI。
如何做...
我们正在将来自我们的 GUI 的值分配给一个 Python 变量。
在我们的模块底部,就在主事件循环之上,添加以下代码:
strData = spin.get()
print("Spinbox value: " + strData)
# Place cursor into name Entry
nameEntered.focus()
#======================
# Start GUI
#======================
win.mainloop()
运行代码会给我们以下结果:

我们正在检索Spinbox控件的当前值。
注意
我们将我们的代码放在 GUI 主事件循环之上,因此打印发生在 GUI 变得可见之前。如果我们想要在显示 GUI 并改变Spinbox控件的值之后打印出当前值,我们将不得不将代码放在回调函数中。
我们使用以下代码创建了我们的 Spinbox 小部件,将可用值硬编码到其中:
# Adding a Spinbox widget using a set of values
spin = Spinbox(monty, values=(1, 2, 4, 42, 100), width=5, bd=8, command=_spin)
spin.grid(column=0, row=2)
我们还可以将数据的硬编码从Spinbox类实例的创建中移出,并稍后设置它。
# Adding a Spinbox widget assigning values after creation
spin = Spinbox(monty, width=5, bd=8, command=_spin)
spin['values'] = (1, 2, 4, 42, 100)
spin.grid(column=0, row=2)
无论我们如何创建小部件并将数据插入其中,因为我们可以通过在小部件实例上使用get()方法来访问这些数据,所以我们可以访问这些数据。
它是如何工作的...
为了从使用 tkinter 编写的 GUI 中获取值,我们使用 tkinter 的get()方法来获取我们希望获取值的小部件的实例。
在上面的例子中,我们使用了 Spinbox 控件,但对于所有具有get()方法的小部件,原理是相同的。
一旦我们获得了数据,我们就处于一个纯粹的 Python 世界,而 tkinter 确实帮助我们构建了我们的 GUI。现在我们知道如何从我们的 GUI 中获取数据,我们可以使用这些数据。
使用模块级全局变量
封装是任何编程语言中的一个主要优势,它使我们能够使用 OOP 进行编程。Python 既是 OOP 又是过程化的。我们可以创建局部化到它们所在模块的全局变量。它们只对这个模块全局,这是一种封装的形式。为什么我们想要这样做?因为随着我们向我们的 GUI 添加越来越多的功能,我们希望避免命名冲突,这可能导致我们代码中的错误。
注意
我们不希望命名冲突在我们的代码中创建错误!命名空间是避免这些错误的一种方法,在 Python 中,我们可以通过使用 Python 模块(这些是非官方的命名空间)来实现这一点。
准备工作
我们可以在任何模块的顶部和函数之外声明模块级全局变量。
然后我们必须使用global Python 关键字来引用它们。如果我们在函数中忘记使用global,我们将意外创建新的局部变量。这将是一个错误,而且是我们真的不想做的事情。
注意
Python 是一种动态的、强类型的语言。我们只会在运行时注意到这样的错误(忘记使用全局关键字来限定变量的范围)。
如何做...
将第 15 行中显示的代码添加到我们在上一章和上一章中使用的 GUI 中,这将创建一个模块级的全局变量。我们使用了 C 风格的全大写约定,这并不真正“Pythonic”,但我认为这确实强调了我们在这个配方中要解决的原则。

运行代码会导致全局变量的打印。注意42被打印到 Eclipse 控制台。

它是如何工作的...
我们在我们的模块顶部定义一个全局变量,稍后,在我们的模块底部,我们打印出它的值。
那起作用。
在我们的模块底部添加这个函数:

在上面,我们正在使用模块级全局变量。很容易出错,因为global被遮蔽,如下面的屏幕截图所示:

请注意,即使我们使用相同的变量名,42也变成了777。
注意
Python 中没有编译器警告我们在本地函数中覆盖全局变量。这可能导致在运行时调试时出现困难。
使用全局限定符(第 234 行)打印出我们最初分配的值(42),如下面的屏幕截图所示:

但是,要小心。当我们取消本地全局时,我们打印出本地的值,而不是全局的值:

尽管我们使用了global限定符,但本地变量似乎会覆盖它。我们从 Eclipse PyDev 插件中得到了一个警告,即我们的GLOBAL_CONST = 777没有被使用,但运行代码仍然打印出 777,而不是预期的 42。
这可能不是我们期望的行为。使用global限定符,我们可能期望指向先前创建的全局变量。
相反,似乎 Python 在本地函数中创建了一个新的全局变量,并覆盖了我们之前创建的全局变量。
全局变量在编写小型应用程序时非常有用。它们可以帮助在同一 Python 模块中的方法和函数之间共享数据,并且有时 OOP 的开销是不合理的。
随着我们的程序变得越来越复杂,使用全局变量所获得的好处很快就会减少。
注意
最好避免使用全局变量,并通过在不同范围中使用相同的名称而意外地遮蔽变量。我们可以使用面向对象编程来代替使用全局变量。
我们在过程化代码中玩了全局变量,并学会了如何导致难以调试的错误。在下一章中,我们将转向面向对象编程,这可以消除这些类型的错误。
如何在类中编码可以改进 GUI
到目前为止,我们一直在以过程化的方式编码。这是来自 Python 的一种快速脚本化方法。一旦我们的代码变得越来越大,我们就需要进步到面向对象编程。
为什么?
因为,除了许多其他好处之外,面向对象编程允许我们通过使用方法来移动代码。一旦我们使用类,我们就不再需要在调用代码的代码上方物理放置代码。这使我们在组织代码方面具有很大的灵活性。
我们可以将相关代码写在其他代码旁边,不再担心代码不会运行,因为代码不在调用它的代码上方。
我们可以通过编写引用未在该模块中创建的方法的模块来将其推向一些相当花哨的极端。它们依赖于运行时状态在代码运行时创建了这些方法。
注意
如果我们调用的方法在那时还没有被创建,我们会得到一个运行时错误。
准备就绪
我们只需将整个过程化代码简单地转换为面向对象编程。我们只需将其转换为一个类,缩进所有现有代码,并在所有变量之前添加self。
这非常容易。
虽然起初可能感觉有点烦人,必须在所有东西之前加上self关键字,使我们的代码更冗长(嘿,我们浪费了这么多纸...);但最终,这将是值得的。
如何做...
一开始,一切都乱了,但我们很快就会解决这个明显的混乱。
请注意,在 Eclipse 中,PyDev 编辑器通过在代码编辑器的右侧部分将其标记为红色来提示编码问题。
也许我们毕竟不应该使用面向对象编程,但这就是我们所做的,而且理由非常充分。

我们只需使用self关键字在所有变量之前添加,并通过使用self将函数绑定到类中,这样官方和技术上将函数转换为方法。
注意
函数和方法之间有区别。Python 非常清楚地表明了这一点。方法绑定到一个类,而函数则没有。我们甚至可以在同一个 Python 模块中混合使用这两种方法。
让我们用self作为前缀来消除红色,这样我们就可以再次运行我们的代码。

一旦我们对所有在红色中突出显示的错误做了这些,我们就可以再次运行我们的 Python 代码。
clickMe函数现在绑定到类上,正式成为一个方法。
不幸的是,以过程式方式开始,然后将其转换为面向对象的方式并不像我上面说的那么简单。代码变得一团糟。这是以面向对象的方式开始编程的一个很好的理由。
注意
Python 擅长以简单的方式做事。简单的代码通常变得更加复杂(因为一开始很容易)。一旦变得过于复杂,将我们的过程式代码重构为真正的面向对象的代码变得越来越困难。
我们正在将我们的过程式代码转换为面向对象的代码。看看我们陷入的所有麻烦,仅仅将 200 多行的 Python 代码转换为面向对象的代码可能表明,我们可能最好从一开始就开始使用面向对象的方式编码。
实际上,我们确实破坏了一些之前工作正常的功能。现在无法使用 Tab 2 和点击单选按钮了。我们必须进行更多的重构。
过程式代码之所以容易,是因为它只是从上到下的编码。现在我们把我们的代码放入一个类中,我们必须把所有的回调函数移到方法中。这样做是可以的,但确实需要一些工作来转换我们的原始代码。
我们的过程式代码看起来像这样:
# Button Click Function
def clickMe():
action.configure(text='Hello ' + name.get())
# Changing our Label
ttk.Label(monty, text="Enter a name:").grid(column=0, row=0, sticky='W')
# Adding a Textbox Entry widget
name = tk.StringVar()
nameEntered = ttk.Entry(monty, width=12, textvariable=name)
nameEntered.grid(column=0, row=1, sticky='W')
# Adding a Button
action = ttk.Button(monty, text="Click Me!", command=clickMe)
action.grid(column=2, row=1)
The new OOP code looks like this:
class OOP():
def __init__(self):
# Create instance
self.win = tk.Tk()
# Add a title
self.win.title("Python GUI")
self.createWidgets()
# Button callback
def clickMe(self):
self.action.configure(text='Hello ' + self.name.get())
# … more callback methods
def createWidgets(self):
# Tab Control introduced here -----------------------
tabControl = ttk.Notebook(self.win) # Create Tab Control
tab1 = ttk.Frame(tabControl) # Create a tab
tabControl.add(tab1, text='Tab 1') # Add the tab
tab2 = ttk.Frame(tabControl) # Create second tab
tabControl.add(tab2, text='Tab 2') # Add second tab
tabControl.pack(expand=1, fill="both") # Pack make visible
#======================
# Start GUI
#======================
oop = OOP()
oop.win.mainloop()
我们将回调方法移到模块顶部,放在新的面向对象类内部。我们将所有的部件创建代码放入一个相当长的方法中,在类的初始化器中调用它。
从技术上讲,在低级代码的深处,Python 确实有一个构造函数,但 Python 让我们摆脱了对此的任何担忧。这已经为我们处理了。
相反,除了一个“真正的”构造函数之外,Python 还为我们提供了一个初始化器。
我们强烈建议使用这个初始化器。我们可以用它来向我们的类传递参数,初始化我们希望在类实例内部使用的变量。
注意
在 Python 中,同一个 Python 模块中可以存在多个类。
与 Java 不同,它有一个非常严格的命名约定(没有这个约定它就无法工作),Python 要灵活得多。
注意
我们可以在同一个 Python 模块中创建多个类。与 Java 不同,我们不依赖于必须与每个类名匹配的文件名。
Python 真的很棒!
一旦我们的 Python GUI 变得庞大,我们将把一些类拆分成它们自己的模块,但与 Java 不同,我们不必这样做。在这本书和项目中,我们将保持一些类在同一个模块中,同时,我们将把一些其他类拆分成它们自己的模块,将它们导入到可以被认为是一个 main()函数的地方(这不是 C,但我们可以像 C 一样思考,因为 Python 非常灵活)。
到目前为止,我们所做的是将ToolTip类添加到我们的 Python 模块中,并将我们的过程式 Python 代码重构为面向对象的 Python 代码。
在这里,在这个示例中,我们可以看到一个 Python 模块中可以存在多个类。
确实很酷!

ToolTip类和OOP类都驻留在同一个 Python 模块中。

它是如何工作的...
在这个示例中,我们将我们的过程式代码推进到面向对象编程(OOP)代码。
Python 使我们能够以实用的过程式风格编写代码,就像 C 编程语言一样。
与此同时,我们有选择以面向对象的方式编码,就像 Java、C#和 C++一样。
编写回调函数
起初,回调函数可能看起来有点令人生畏。您调用函数,传递一些参数,现在函数告诉您它真的很忙,会回电话给您!
你会想:“这个函数会永远回调我吗?”“我需要等多久?”
在 Python 中,即使回调函数也很容易,是的,它们通常会回调你。
它们只需要先完成它们分配的任务(嘿,是你编码它们的第一次...)。
让我们更多地了解一下当我们将回调编码到我们的 GUI 中时会发生什么。
我们的 GUI 是事件驱动的。在创建并显示在屏幕上之后,它通常会等待事件发生。它基本上在等待事件被发送到它。我们可以通过点击其动作按钮之一来向我们的 GUI 发送事件。
这创建了一个事件,并且在某种意义上,我们通过发送消息“调用”了我们的 GUI。
现在,我们发送消息到我们的 GUI 后应该发生什么?
点击按钮后发生的事情取决于我们是否创建了事件处理程序并将其与此按钮关联。如果我们没有创建事件处理程序,点击按钮将没有任何效果。
事件处理程序是一个回调函数(或方法,如果我们使用类)。
回调方法也是被动的,就像我们的 GUI 一样,等待被调用。
一旦我们的 GUI 被点击按钮,它将调用回调函数。
回调通常会进行一些处理,完成后将结果返回给我们的 GUI。
注意
在某种意义上,我们可以看到我们的回调函数在回调我们的 GUI。
准备就绪
Python 解释器会运行项目中的所有代码一次,找到任何语法错误并指出它们。如果语法不正确,您无法运行 Python 代码。这包括缩进(如果不导致语法错误,错误的缩进通常会导致错误)。
在下一轮解析中,解释器解释我们的代码并运行它。
在运行时,可以生成许多 GUI 事件,通常是回调函数为 GUI 小部件添加功能。
如何做...
这是 Spinbox 小部件的回调:

它是如何工作的...
我们在OOP类中创建了一个回调方法,当我们从 Spinbox 小部件中选择一个值时,它会被调用,因为我们通过command参数(command=self._spin)将该方法绑定到小部件。我们使用下划线前缀来暗示这个方法应该像一个私有的 Java 方法一样受到尊重。
Python 故意避免了私有、公共、友好等语言限制。
在 Python 中,我们使用命名约定。预期用双下划线包围关键字的前后缀应该限制在 Python 语言中,我们不应该在我们自己的 Python 代码中使用它们。
但是,我们可以使用下划线前缀来提供一个提示,表明这个名称应该被视为私有助手。
与此同时,如果我们希望使用本来是 Python 内置名称的名称,我们可以在后面加上一个下划线。例如,如果我们希望缩短列表的长度,我们可以这样做:
len_ = len(aList)
通常,下划线很难阅读,容易忽视,因此在实践中这可能不是最好的主意。
创建可重用的 GUI 组件
我们正在使用 Python 创建可重用的 GUI 组件。
在这个示例中,我们将简化操作,将我们的ToolTip类移动到其自己的模块中。接下来,我们将导入并在 GUI 的几个小部件上使用它来显示工具提示。
准备就绪
我们正在构建我们之前的代码。
如何做...
我们将首先将我们的ToolTip类拆分为一个单独的 Python 模块。我们将稍微增强它,以便传入控件小部件和我们希望在悬停鼠标在控件上时显示的工具提示文本。
我们创建了一个新的 Python 模块,并将ToolTip类代码放入其中,然后将此模块导入我们的主要模块。
然后,我们通过创建几个工具提示来重用导入的ToolTip类,当鼠标悬停在几个 GUI 小部件上时可以看到它们。
将我们通用的ToolTip类代码重构到自己的模块中有助于我们重用这些代码。我们使用 DRY 原则,将我们的通用代码放在一个地方,这样当我们修改代码时,导入它的所有模块将自动获得我们模块的最新版本,而不是复制/粘贴/修改。
注意
DRY 代表不要重复自己,我们将在以后的章节中再次讨论它。
我们可以通过将选项卡 3 的图像转换为可重用组件来做类似的事情。
为了保持本示例的代码简单,我们删除了选项卡 3,但您可以尝试使用上一章的代码进行实验。

# Add a Tooltip to the Spinbox
tt.createToolTip(self.spin, 'This is a Spin control.')
# Add Tooltips to more widgets
tt.createToolTip(nameEntered, 'This is an Entry control.')
tt.createToolTip(self.action, 'This is a Button control.')
tt.createToolTip(self.scr, 'This is a ScrolledText control.')
这也适用于第二个选项卡。

新的代码结构现在看起来像这样:

导入语句如下所示:

而在单独的模块中分解(重构)的代码如下所示:

工作原理...
在前面的屏幕截图中,我们可以看到显示了几条工具提示消息。主窗口的工具提示可能有点烦人,所以最好不要为主窗口显示工具提示,因为我们真的希望突出显示各个小部件的功能。主窗体有一个解释其目的的标题;不需要工具提示。
第五章:Matplotlib 图表
在这一章中,我们将使用 Python 3 和 Matplotlib 模块创建美丽的图表。
-
使用 Matplotlib 创建美丽的图表
-
Matplotlib - 使用 pip 下载模块
-
Matplotlib - 使用 whl 扩展名下载模块
-
创建我们的第一个图表
-
在图表上放置标签
-
如何给图表加上图例
-
调整图表的比例
-
动态调整图表的比例
介绍
在本章中,我们将创建美丽的图表,以直观地表示数据。根据数据源的格式,我们可以在同一图表中绘制一个或多个数据列。
我们将使用 Python Matplotlib 模块来创建我们的图表。
为了创建这些图形图表,我们需要下载额外的 Python 模块,有几种安装方法。
本章将解释如何下载 Matplotlib Python 模块,所有其他所需的 Python 模块,以及如何做到这一点的方法。
在安装所需的模块之后,我们将创建自己的 Python 图表。
使用 Matplotlib 创建美丽的图表
这个示例向我们介绍了 Matplotlib Python 模块,它使我们能够使用 Python 3 创建可视化图表。
以下 URL 是开始探索 Matplotlib 世界的好地方,并将教您如何创建本章中未提及的许多图表:
matplotlib.org/users/screenshots.html
准备工作
为了使用 Matplotlib Python 模块,我们首先必须安装该模块,以及诸如 numpy 等其他相关的 Python 模块。
如果您使用的 Python 版本低于 3.4.3,我建议您升级 Python 版本,因为在本章中我们将使用 Python pip 模块来安装所需的 Python 模块,而 pip 是在 3.4.3 及以上版本中安装的。
注意
可以使用较早版本的 Python 3 安装 pip,但这个过程并不是很直观,因此最好升级到 3.4.3 或更高版本。
如何做...
以下图片是使用 Python 和 Matplotlib 模块创建的令人难以置信的图表的示例。
我从matplotlib.org/网站复制了以下代码,它创建了这个令人难以置信的图表。该网站上有许多示例,我鼓励您尝试它们,直到找到您喜欢创建的图表类型。
以下是创建图表的代码,包括空格在内,不到 25 行的 Python 代码。
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure()
ax = fig.gca(projection='3d')
X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)
X, Y = np.meshgrid(X, Y)
R = np.sqrt(X**2 + Y**2)
Z = np.sin(R)
surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False)
ax.set_zlim(-1.01, 1.01)
ax.zaxis.set_major_locator(LinearLocator(10))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
fig.colorbar(surf, shrink=0.5, aspect=5)
plt.show()
运行代码会创建以下图片中显示的图表:

使用 Python 3.4 或更高版本与 Eclipse PyDev 插件运行代码可能会显示一些未解决的导入错误。这似乎是 PyDev 或 Java 中的一个错误。
如果您使用 Eclipse 进行开发,请忽略这些错误,因为代码将成功运行。
它是如何工作的...
为了创建如前面截图所示的美丽图表,我们需要下载其他几个 Python 模块。
以下示例将指导我们如何成功下载所有所需的模块,从而使我们能够创建自己的美丽图表。
Matplotlib - 使用 pip 下载模块
下载额外的 Python 模块的常规方法是使用 pip。pip 模块预装在最新版本的 Python(3.4 及以上)中。
注意
如果您使用的是较旧版本的 Python,可能需要自己下载 pip 和 setuptools。
除了使用 Python 安装程序外,还有其他几个预编译的 Windows 可执行文件,可以让我们轻松安装 Matplotlib 等 Python 模块。
这个示例将展示如何通过 Windows 可执行文件成功安装 Matplotlib,以及使用 pip 安装 Matplotlib 库所需的其他模块。
准备工作
我们所需要做的就是在我们的 PC 上安装一个 Python 3.4(或更高版本)的发行版,以便下载所需的 Python 模块来使用 Matplotlib 模块。
如何做...
我们可以通过官方 Matplotlib 网站上的 Windows 可执行文件来安装 Matplotlib。
确保安装与您正在使用的 Python 版本匹配的 Matplotlib 版本。例如,如果您在 64 位操作系统(如 Microsoft Windows 7)上安装了 Python 3.4,则下载并安装Matplotlib-1.4.3.win-amd64-py3.4.exe。
注意
可执行文件名称中的"amd64"表示您正在安装 64 位版本。如果您使用 32 位 x86 系统,则安装 amd64 将不起作用。如果您安装了 32 位版本的 Python 并下载了 64 位 Python 模块,则可能会出现类似的问题。

运行可执行文件将启动我们,并且看起来像这样:

我们可以通过查看我们的 Python 安装目录来验证我们是否成功安装了 Matplotlib。
安装成功后,Matplotlib 文件夹将添加到 site-packages。在 Windows 上使用默认安装,site-packages 文件夹的完整路径是:
C:\Python34\Lib\site-packages\matplotlib\

在官方 Matplotlib 网站上最简单的绘图示例需要使用 Python numpy 模块,所以让我们下载并安装这个模块。
注意
Numpy 是一个数学模块,它使 Matplotlib 图表的绘制成为可能,但远不止于 Matplotlib。如果您正在开发的软件需要大量的数学计算,您肯定会想要查看 numpy。
有一个优秀的网站,为我们提供了几乎所有 Python 模块的快速链接。它作为一个很好的时间节省者,指出了成功使用 Matplotlib 所需的其他 Python 模块,并给我们提供了下载这些模块的超链接,这使我们能够快速轻松地安装它们。
注意
这是链接:
www.lfd.uci.edu/~gohlke/pythonlibs/

注意安装程序包的文件扩展名都以 whl 结尾。为了使用它们,我们必须安装 Python wheel 模块,我们使用 pip 来做到这一点。
注意
Wheels 是 Python 分发的新标准,旨在取代 eggs。
您可以在以下网站找到更多详细信息:
最好以管理员身份运行 Windows 命令处理器,以避免潜在的安装错误。

它是如何工作的...
下载 Python 模块的常见方法是使用 pip,就像上面所示的那样。为了安装 Matplotlib 所需的所有模块,我们可以从主网站下载它们的下载格式已更改为使用 whl 格式。
下一个配方将解释如何使用 wheel 安装 Python 模块。
Matplotlib - 使用 whl 扩展名下载模块
我们将使用几个 Matplotlib 需要的额外 Python 模块,在这个配方中,我们将使用 Python 的新模块分发标准 wheel 来下载它们。
注意
您可以在以下网址找到新的 wheel 标准的 Python 增强提案(PEP):www.python.org/dev/peps/pep-0427/
准备工作
为了下载带有 whl 扩展名的 Python 模块,必须首先安装 Python wheel 模块,这在前面的配方中已经解释过了。
如何做...
让我们从网上下载numpy-1.9.2+mkl-cp34-none-win_amd64.whl。安装了 wheel 模块后,我们可以使用 pip 来安装带有 whl 文件扩展名的软件包。
注意
Pip 随 Python 3.4.3 及以上版本一起提供。如果您使用的是较旧版本的 Python,我建议安装 pip,因为它可以让安装所有其他额外的 Python 模块变得更加容易。
一个更好的建议可能是将您的 Python 版本升级到最新的稳定版本。当您阅读本书时,最有可能的是 Python 3.5.0 或更高版本。
Python 是免费软件。升级对我们来说是没有成本的。
浏览到要安装的软件包所在的文件夹,并使用以下命令进行安装:
**pip install numpy-1.9.2+mkl-cp34-none-win_amd64.whl**

现在我们可以使用官方网站上最简单的示例应用程序创建我们的第一个 Matplotlib 图表。之后,我们将创建自己的图表。

我们还没有准备好运行前面的代码,这表明我们需要下载更多的模块。虽然一开始需要下载更多的模块可能会有点烦人,但实际上这是一种代码重用的形式。
因此,让我们使用 pip 和 wheel 下载并安装 six 和所有其他所需的模块(如 dateutil、pyparsing 等),直到我们的代码能够工作并从只有几行 Python 代码中创建一个漂亮的图表。
我们可以从刚刚用来安装 numpy 的同一个网站下载所有所需的模块。这个网站甚至列出了我们正在安装的模块所依赖的所有其他模块,并提供了跳转到这个网站上的安装软件的超链接。
注意
如前所述,安装 Python 模块的 URL 是:www.lfd.uci.edu/~gohlke/pythonlibs/
它是如何工作的...
使我们能够从一个便利的地方下载许多 Python 模块的网站还提供其他 Python 模块。并非所有显示的依赖项都是必需的。这取决于您正在开发的内容。随着您使用 Matplotlib 库的旅程的推进,您可能需要下载和安装其他模块。

创建我们的第一个图表
现在我们已经安装了所有所需的 Python 模块,我们可以使用 Matplotlib 创建自己的图表。
我们可以只用几行 Python 代码创建图表。
准备工作
使用前一个示例中的代码,我们现在可以创建一个看起来类似于下一个示例的图表。
如何做...
使用官方网站上提供的最少量的代码,我们可以创建我们的第一个图表。嗯,几乎。网站上显示的示例代码在导入show方法并调用它之前是无法工作的。

我们可以简化代码,甚至通过使用官方 Matplotlib 网站提供的许多示例之一来改进它。

它是如何工作的...
Python Matplotlib 模块,结合诸如 numpy 之类的附加组件,创建了一个非常丰富的编程环境,使我们能够轻松进行数学计算并在可视化图表中绘制它们。
Python numpy 方法arange并不打算安排任何事情。它的意思是创建“一个范围”,在 Python 中用于内置的“range”运算符。linspace方法可能会造成类似的混淆。谁是“lin”,在什么“空间”?
事实证明,该名称意味着“线性间隔向量”。
pyglet 函数show显示我们创建的图形。在成功创建第一个图形后,调用show()会产生一些副作用,当您尝试绘制另一个图形时。
在图表上放置标签
到目前为止,我们已经使用了默认的 Matplotlib GUI。现在我们将使用 Matplotlib 创建一些 tkinter GUI。
这将需要更多的 Python 代码行和导入更多的库,但这是值得的,因为我们正在通过画布控制我们的绘画。
我们将标签放在水平轴和垂直轴上,也就是x和y。
我们将通过创建一个 Matplotlib 图形来实现这一点。
我们还将学习如何使用子图,这将使我们能够在同一个窗口中绘制多个图形。
准备工作
安装必要的 Python 模块并知道在哪里找到官方在线文档和教程后,我们现在可以继续创建 Matplotlib 图表。
如何做...
虽然plot是创建 Matplotlib 图表的最简单方法,但是结合Canvas使用Figure创建一个更定制的图表,看起来更好,还可以让我们向其添加按钮和其他小部件。
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
#--------------------------------------------------------------
fig = Figure(figsize=(12, 8), facecolor='white')
#--------------------------------------------------------------
# axis = fig.add_subplot(111) # 1 row, 1 column, only graph
axis = fig.add_subplot(211) # 2 rows, 1 column, Top graph
#--------------------------------------------------------------
xValues = [1,2,3,4]
yValues = [5,7,6,8]
axis.plot(xValues, yValues)
axis.set_xlabel('Horizontal Label')
axis.set_ylabel('Vertical Label')
# axis.grid() # default line style
axis.grid(linestyle='-') # solid grid lines
#--------------------------------------------------------------
def _destroyWindow():
root.quit()
root.destroy()
#--------------------------------------------------------------
root = tk.Tk()
root.withdraw()
root.protocol('WM_DELETE_WINDOW', _destroyWindow)
#--------------------------------------------------------------
canvas = FigureCanvasTkAgg(fig, master=root)
canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
#--------------------------------------------------------------
root.update()
root.deiconify()
root.mainloop()
运行上述代码会得到以下图表:

在导入语句之后的第一行代码中,我们创建了一个Figure对象的实例。接下来,我们通过调用add_subplot(211)向这个图添加子图。211 中的第一个数字告诉图要添加多少个图,第二个数字确定列数,第三个数字告诉图以什么顺序显示图。
我们还添加了一个网格并更改了其默认线型。
尽管我们在图表中只显示一个图,但通过选择 2 作为子图的数量,我们将图向上移动,这导致图表底部出现额外的空白。这第一个图现在只占据屏幕的 50%,这会影响在显示时此图的网格线有多大。
注意
通过取消注释axis =和axis.grid()的代码来尝试该代码,以查看不同的效果。
我们可以通过将它们分配到第二个位置使用add_subplot(212)来添加更多的子图。
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
#--------------------------------------------------------------
fig = Figure(figsize=(12, 8), facecolor='white')
#--------------------------------------------------------------
axis = fig.add_subplot(211) # 2 rows, 1 column, Top graph
#--------------------------------------------------------------
xValues = [1,2,3,4]
yValues = [5,7,6,8]
axis.plot(xValues, yValues)
axis.set_xlabel('Horizontal Label')
axis.set_ylabel('Vertical Label')
axis.grid(linestyle='-') # solid grid lines
#--------------------------------------------------------------
axis1 = fig.add_subplot(212) # 2 rows, 1 column, Bottom graph
#--------------------------------------------------------------
xValues1 = [1,2,3,4]
yValues1 = [7,5,8,6]
axis1.plot(xValues1, yValues1)
axis1.grid() # default line style
#--------------------------------------------------------------
def _destroyWindow():
root.quit()
root.destroy()
#--------------------------------------------------------------
root = tk.Tk()
root.withdraw()
root.protocol('WM_DELETE_WINDOW', _destroyWindow)
#--------------------------------------------------------------
canvas = FigureCanvasTkAgg(fig, master=root)
canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
#--------------------------------------------------------------
root.update()
root.deiconify()
root.mainloop()
现在运行略微修改的代码会将 axis1 添加到图表中。对于底部图的网格,我们将线型保留为默认值。

工作原理...
我们导入了必要的 Matplotlib 模块来创建一个图和一个画布,用于在其上绘制图表。我们为x和y轴给出了一些值,并设置了很多配置选项中的一些。
我们创建了自己的 tkinter 窗口来显示图表并自定义了绘图的位置。
正如我们在前几章中看到的,为了创建一个 tkinter GUI,我们首先必须导入 tkinter 模块,然后创建Tk类的实例。我们将这个类实例分配给一个我们命名为root的变量,这是在示例中经常使用的名称。
我们的 tkinter GUI 直到我们启动主事件循环才会变得可见,为此,我们使用root.mainloop()。
避免在这里使用 Matplotlib 默认 GUI 并改为使用 tkinter 创建自己的 GUI 的一个重要原因是,我们想要改善默认 Matplotlib GUI 的外观,而使用 tkinter 可以很容易地实现这一点。
如果我们使用 tkinter 构建 GUI,就不会再出现那些过时的按钮出现在 Matplotlib GUI 底部。
同时,Matplotlib GUI 具有我们的 tkinter GUI 没有的功能,即当我们在图表内移动鼠标时,我们实际上可以看到 Matplotlib GUI 中的 x 和 y 坐标。 x 和 y 坐标位置显示在右下角。
如何给图表添加图例
一旦我们开始绘制多条数据点的线,事情可能会变得有点不清楚。通过向我们的图表添加图例,我们可以知道哪些数据是什么,它们实际代表什么。
我们不必选择不同的颜色来表示不同的数据。Matplotlib 会自动为每条数据点的线分配不同的颜色。
我们所要做的就是创建图表并向其添加图例。
准备工作
在这个示例中,我们将增强上一个示例中的图表。我们只会绘制一个图表。
如何做...
首先,我们将在同一图表中绘制更多的数据线,然后我们将向图表添加图例。
我们通过修改上一个示例中的代码来实现这一点。
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
#--------------------------------------------------------------
fig = Figure(figsize=(12, 5), facecolor='white')
#--------------------------------------------------------------
axis = fig.add_subplot(111) # 1 row, 1 column
xValues = [1,2,3,4]
yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,8,6]
yValues2 = [6.5,7,8,7]
t0, = axis.plot(xValues, yValues0)
t1, = axis.plot(xValues, yValues1)
t2, = axis.plot(xValues, yValues2)
axis.set_ylabel('Vertical Label')
axis.set_xlabel('Horizontal Label')
axis.grid()
fig.legend((t0, t1, t2), ('First line', 'Second line', 'Third line'), 'upper right')
#--------------------------------------------------------------
def _destroyWindow():
root.quit()
root.destroy()
#--------------------------------------------------------------
root = tk.Tk()
root.withdraw()
root.protocol('WM_DELETE_WINDOW', _destroyWindow)
#--------------------------------------------------------------
canvas = FigureCanvasTkAgg(fig, master=root)
canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
#--------------------------------------------------------------
root.update()
root.deiconify()
root.mainloop()
运行修改后的代码会创建以下图表,图例位于右上角:

在这个示例中,我们只绘制了一个图表,我们通过更改fig.add_subplot(111)来实现这一点。我们还通过figsize属性略微修改了图表的大小。
接下来,我们创建了三个包含要绘制的值的 Python 列表。当我们绘制数据时,我们将图表的引用保存在本地变量中。
我们通过传入一个包含三个图表引用的元组,另一个包含随后在图例中显示的字符串的元组来创建图例,并在第三个参数中定位图例在图表中的位置。
Matplotlib 的默认设置为正在绘制的线条分配了一个颜色方案。
我们可以通过在绘制每个轴时设置属性来轻松地将这些默认颜色设置更改为我们喜欢的颜色。
我们通过使用颜色属性并为其分配一个可用的颜色值来实现这一点。
t0, = axis.plot(xValues, yValues0, color = 'purple')
t1, = axis.plot(xValues, yValues1, color = 'red')
t2, = axis.plot(xValues, yValues2, color = 'blue')
请注意,t0、t1 和 t2 的变量赋值后面的逗号不是错误,而是为了创建图例而需要的。
在每个变量后面的逗号将列表转换为元组。如果我们省略这一点,我们的图例将不会显示。
代码仍将运行,只是没有预期的图例。
注意
当我们在 t0 =赋值后移除逗号时,我们会得到一个错误,第一行不再出现在图中。图表和图例仍然会被创建,但图例中不再出现第一行。

它是如何工作的...
我们通过在同一图表中绘制三条数据线并为其添加图例来增强了我们的图表,以区分这三条线绘制的数据。
调整图表的比例
在以前的示例中,当我们创建我们的第一个图表并增强它们时,我们硬编码了这些值的视觉表示方式。
虽然这对我们使用的值很有帮助,但我们经常从非常大的数据库中绘制图表。
根据数据的范围,我们为垂直 y 维度的硬编码值可能并不总是最佳解决方案,这可能会使我们的图表中的线条难以看清。
准备工作
我们将改进我们在上一个示例中的代码。如果您没有输入所有以前示例中的代码,只需下载本章的代码,它将让您开始(然后您可以通过使用 Python 创建 GUI、图表等来玩得很开心)。
如何做...
将上一个示例中的yValues1代码行修改为使用 50 作为第三个值。
axis = fig.add_subplot(111) # 1 row, 1 column
xValues = [1,2,3,4]
yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,50,6] # one very high value
yValues2 = [6.5,7,8,7]
与上一个示例中创建图表的代码唯一的区别是一个数据值。
通过更改一个与所有其他值的平均范围不接近的值,数据的视觉表示已经发生了戏剧性的变化,我们失去了关于整体数据的许多细节,现在主要看到一个高峰。

到目前为止,我们的图表已根据它们所呈现的数据自动调整。
虽然这是 Matplotlib 的一个实用功能,但这并不总是我们想要的。我们可以通过限制垂直 y 维度来限制图表的比例。
yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,50,6] # one very high value (50)
yValues2 = [6.5,7,8,7]
axis.set_ylim(5, 8) # limit the vertical display
现在,axis.set_ylim(5, 8)这行代码限制了起始值为 5,垂直显示的结束值为 8。
现在,当我们创建图表时,高值峰值不再像以前那样有影响。

它是如何工作的...
我们增加了数据中的一个值,这产生了戏剧性的效果。通过设置图表的垂直和水平显示限制,我们可以看到我们最感兴趣的数据。
像刚才显示的那样的尖峰也可能非常有趣。这一切取决于我们要寻找什么。数据的视觉表示具有很大的价值。
注意
一图胜千言。
动态调整图表的比例
在上一个示例中,我们学习了如何限制我们图表的缩放。在这个示例中,我们将进一步通过在表示数据之前动态调整缩放来设置限制并分析我们的数据。
准备工作
我们将通过动态读取数据、对其进行平均并调整我们的图表来增强上一个示例中的代码。
虽然我们通常会从外部来源读取数据,在这个示例中,我们使用 Python 列表创建我们要绘制的数据,如下面的代码所示。
如何做...
我们通过将数据分配给 xvalues 和 yvalues 变量来在我们的 Python 模块中创建自己的数据。
在许多图表中,x 和 y 坐标系的起始点通常是(0, 0)。这通常是一个好主意,所以让我们相应地调整我们的图表坐标代码。
让我们修改代码以限制 x 和 y 两个维度:
xValues = [1,2,3,4]
yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,50,6] # one very high value (50)
yValues2 = [6.5,7,8,7]
axis.set_ylim(0, 8) # lower limit (0)
axis.set_xlim(0, 8) # use same limits for x
现在我们已经为 x 和 y 设置了相同的限制,我们的图表可能看起来更加平衡。当我们运行修改后的代码时,我们得到了以下结果:

也许从(0, 0)开始并不是一个好主意...
我们真正想做的是根据数据的范围动态调整我们的图表,同时限制过高或过低的值。
我们可以通过解析要在图表中表示的所有数据,同时设置一些明确的限制来实现这一点。
修改代码如下所示:
xValues = [1,2,3,4]
yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,50,6] # one very high value (50)
yValues2 = [6.5,7,8,7]
yAll = [yValues0, yValues1, yValues2] # list of lists
# flatten list of lists retrieving minimum value
minY = min([y for yValues in yAll for y in yValues])
yUpperLimit = 20
# flatten list of lists retrieving max value within defined limit
maxY = max([y for yValues in yAll for y in yValues if y < yUpperLimit])
# dynamic limits
axis.set_ylim(minY, maxY)
axis.set_xlim(min(xValues), max(xValues))
t0, = axis.plot(xValues, yValues0)
t1, = axis.plot(xValues, yValues1)
t2, = axis.plot(xValues, yValues2)
运行代码会得到以下图表。我们动态调整了它的 x 和 y 维度。请注意,现在 y 维度从 5.5 开始,而不是之前的 5.0。图表也不再从(0, 0)开始,这为我们提供了更有价值的关于我们的数据的信息。

我们正在为 y 维度数据创建一个列表的列表,然后使用一个列表推导包装成对 Python 的min()和max()函数的调用。
如果列表推导似乎有点高级,它们基本上是一个非常压缩的循环。
它们还被设计为比常规编程循环更快。
在创建上述图表的 Python 代码中,我们创建了三个包含要绘制的 y 维度数据的列表。然后我们创建了另一个包含这三个列表的列表,从而创建了一个列表的列表。
就像这样:
yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,50,6] # one very high value (50)
yValues2 = [6.5,7,8,7]
yAll = [yValues0, yValues1, yValues2] # list of lists
我们对获取所有 y 维度数据的最小值以及包含在这三个列表中的最大值感兴趣。
我们可以通过 Python 列表推导来实现这一点。
# flatten list of lists retrieving minimum value
minY = min([y for yValues in yAll for y in yValues])
在运行列表推导后,minY为 5.5。
上面的一行代码是列表推导,它遍历三个列表中包含的所有数据的所有值,并使用 Python 的min关键字找到最小值。
在同样的模式中,我们找到了我们希望绘制的数据中包含的最大值。这次,我们还在列表推导中设置了一个限制,忽略了所有超过我们指定限制的值,就像这样:
yUpperLimit = 20
# flatten list of lists retrieving max value within defined limit
maxY = max([y for yValues in yAll for y in yValues if y < yUpperLimit])
在使用我们选择的限制条件运行上述代码后,maxY的值为 8(而不是 50)。
我们根据预定义条件选择 20 作为图表中显示的最大值,对最大值应用了限制。
对于 x 维度,我们只需在 Matplotlib 方法中调用min()和max()来动态调整图表的限制。
工作原理...
在这个示例中,我们创建了几个 Matplotlib 图表,并调整了其中一些可用属性。我们还使用核心 Python 动态控制了图表的缩放。
第六章:线程和网络
在本章中,我们将使用 Python 3 创建线程、队列和 TCP/IP 套接字。
-
如何创建多个线程
-
启动一个线程
-
停止一个线程
-
如何使用队列
-
在不同模块之间传递队列
-
使用对话框小部件将文件复制到您的网络
-
使用 TCP/IP 通过网络进行通信
-
使用 URLOpen 从网站读取数据
介绍
在本章中,我们将使用线程、队列和网络连接扩展我们的 Python GUI 的功能。
注意
tkinter GUI 是单线程的。每个涉及休眠或等待时间的函数都必须在单独的线程中调用,否则 tkinter GUI 会冻结。
当我们在 Windows 任务管理器中运行我们的 Python GUI 时,我们可以看到一个新的python.exe进程已经启动。
当我们给我们的 Python GUI 一个.pyw扩展名时,然后创建的进程将是python.pyw,可以在任务管理器中看到。
当创建一个进程时,该进程会自动创建一个主线程来运行我们的应用程序。这被称为单线程应用程序。
对于我们的 Python GUI,单线程应用程序将导致我们的 GUI 在调用较长时间的任务时变得冻结,比如点击一个有几秒钟休眠的按钮。
为了保持我们的 GUI 响应,我们必须使用多线程,这就是我们将在本章中学习的内容。
我们还可以通过创建多个 Python GUI 的实例来创建多个进程,可以在任务管理器中看到。
进程在设计上是相互隔离的,彼此不共享公共数据。为了在不同进程之间进行通信,我们必须使用进程间通信(IPC),这是一种高级技术。
另一方面,线程确实共享公共数据、代码和文件,这使得在同一进程内的线程之间的通信比使用 IPC 更容易。
注意
关于线程的很好的解释可以在这里找到:www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html
在本章中,我们将学习如何保持我们的 Python GUI 响应,并且不会冻结。
如何创建多个线程
我们将使用 Python 创建多个线程。这是为了保持我们的 GUI 响应而必要的。
注意
线程就像编织由纱线制成的织物,没有什么可害怕的。
准备就绪
多个线程在同一计算机进程内存空间内运行。不需要进程间通信(IPC),这会使我们的代码变得复杂。在本节中,我们将通过使用线程来避免 IPC。
如何做...
首先,我们将增加我们的ScrolledText小部件的大小,使其更大。让我们将scrolW增加到 40,scrolH增加到 10。
# Using a scrolled Text control
scrolW = 40; scrolH = 10
self.scr = scrolledtext.ScrolledText(self.monty, width=scrolW, height=scrolH, wrap=tk.WORD)
self.scr.grid(column=0, row=3, sticky='WE', columnspan=3)
当我们现在运行结果的 GUI 时,Spinbox小部件相对于其上方的Entry小部件是居中对齐的,这看起来不好。我们将通过左对齐小部件来改变这一点。
在grid控件中添加sticky='W',以左对齐Spinbox小部件。
# Adding a Spinbox widget using a set of values
self.spin = Spinbox(self.monty, values=(1, 2, 4, 42, 100), width=5, bd=8, command=self._spin)
self.spin.grid(column=0, row=2, sticky='W')
GUI 可能看起来还不错,所以下一步,我们将增加Entry小部件的大小,以获得更平衡的 GUI 布局。
将宽度增加到 24,如下所示:
# Adding a Textbox Entry widget
self.name = tk.StringVar()
nameEntered = ttk.Entry(self.monty, width=24, textvariable=self.name)
nameEntered.grid(column=0, row=1, sticky='W')
让我们也稍微增加Combobox的宽度到 14。
ttk.Label(self.monty, text="Choose a number:").grid(column=1, row=0)
number = tk.StringVar()
numberChosen = ttk.Combobox(self.monty, width=14, textvariable=number)
numberChosen['values'] = (1, 2, 4, 42, 100)
numberChosen.grid(column=1, row=1)
numberChosen.current(0)
运行修改和改进的代码会导致一个更大的 GUI,我们将在本节和下一节中使用它。

为了在 Python 中创建和使用线程,我们必须从 threading 模块中导入Thread类。
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu
from tkinter import Spinbox
import B04829_Ch06_ToolTip as tt
from threading import Thread
GLOBAL_CONST = 42
让我们在OOP类中添加一个在线程中创建的方法。
class OOP():
def methodInAThread(self):
print('Hi, how are you?')
现在我们可以在代码中调用我们的线程方法,将实例保存在一个变量中。
#======================
# Start GUI
#======================
oop = OOP()
# Running methods in Threads
runT = Thread(target=oop.methodInAThread)
oop.win.mainloop())
现在我们有一个线程化的方法,但当我们运行代码时,控制台上什么都没有打印出来!
我们必须先启动Thread,然后它才能运行,下一节将向我们展示如何做到这一点。
然而,在 GUI 主事件循环之后设置断点证明我们确实创建了一个Thread对象,这可以在 Eclipse IDE 调试器中看到。

它是如何工作的...
在这个配方中,我们首先增加了 GUI 的大小,以便更好地看到打印到ScrolledText小部件中的结果,为了准备使用线程。
然后,我们从 Python 的threading模块中导入了Thread类。
之后,我们创建了一个在 GUI 内部从线程中调用的方法。
启动线程
这个配方将向我们展示如何启动一个线程。它还将演示为什么线程在长时间运行的任务期间保持我们的 GUI 响应是必要的。
准备工作
让我们首先看看当我们调用一个带有一些休眠的函数或方法时会发生什么,而不使用线程。
注意
我们在这里使用休眠来模拟一个现实世界的应用程序,该应用程序可能需要等待 Web 服务器或数据库响应,或者大文件传输或复杂计算完成其任务。
休眠是一个非常现实的占位符,并展示了涉及的原则。
在我们的按钮回调方法中添加一个循环和一些休眠时间会导致我们的 GUI 变得无响应,当我们尝试关闭 GUI 时,情况变得更糟。
# Button callback
def clickMe(self):
self.action.configure(text='Hello ' + self.name.get())
# Non-threaded code with sleep freezes the GUI
for idx in range(10):
sleep(5)
self.scr.insert(tk.INSERT, str(idx) + '\n')

如果我们等待足够长的时间,方法最终会完成,但在此期间,我们的 GUI 小部件都不会响应点击事件。我们通过使用线程来解决这个问题。
注意
在之前的配方中,我们创建了一个要在线程中运行的方法,但到目前为止,线程还没有运行!
与常规的 Python 函数和方法不同,我们必须start一个将在自己的线程中运行的方法!
这是我们接下来要做的事情。
如何做...
首先,让我们将线程的创建移到它自己的方法中,然后从按钮回调方法中调用这个方法。
# Running methods in Threads
def createThread(self):
runT = Thread(target=self.methodInAThread)
runT.start()
# Button callback
def clickMe(self):
self.action.configure(text='Hello ' + self.name.get())
self.createThread()
现在点击按钮会导致调用createThread方法,然后调用methodInAThread方法。
首先,我们创建一个线程并将其目标定位到一个方法。接下来,我们启动线程,该线程将在一个新线程中运行目标方法。
注意
GUI 本身运行在它自己的线程中,这是应用程序的主线程。

我们可以打印出线程的实例。
# Running methods in Threads
def createThread(self):
runT = Thread(target=self.methodInAThread)
runT.start()
print(runT)
现在点击按钮会创建以下输出:

当我们点击按钮多次时,我们可以看到每个线程都被分配了一个唯一的名称和 ID。

现在让我们将带有sleep的循环代码移到methodInAThread方法中,以验证线程确实解决了我们的问题。
def methodInAThread(self):
print('Hi, how are you?')
for idx in range(10):
sleep(5)
self.scr.insert(tk.INSERT, str(idx) + '\n')
当点击按钮时,数字被打印到ScrolledText小部件中,间隔五秒,我们可以在 GUI 的任何地方点击,切换标签等。我们的 GUI 再次变得响应,因为我们正在使用线程!

它是如何工作的...
在这个配方中,我们在它们自己的线程中调用了 GUI 类的方法,并学会了我们必须启动这些线程。否则,线程会被创建,但只是坐在那里等待我们运行它的目标方法。
我们注意到每个线程都被分配了一个唯一的名称和 ID。
我们通过在代码中插入sleep语句来模拟长时间运行的任务,这向我们表明线程确实可以解决我们的问题。
停止线程
我们必须启动一个线程来通过调用start()方法实际让它做一些事情,因此,直觉上,我们会期望有一个匹配的stop()方法,但实际上并没有这样的方法。在这个配方中,我们将学习如何将线程作为后台任务运行,这被称为守护线程。当关闭主线程,也就是我们的 GUI 时,所有守护线程也将自动停止。
准备工作
当我们在线程中调用方法时,我们也可以向方法传递参数和关键字参数。我们首先通过这种方式开始这个示例。
如何做到...
通过在线程构造函数中添加args=[8]并修改目标方法以期望参数,我们可以向线程方法传递参数。args的参数必须是一个序列,所以我们将我们的数字包装在 Python 列表中。
def methodInAThread(self, numOfLoops=10):
for idx in range(numOfLoops):
sleep(1)
self.scr.insert(tk.INSERT, str(idx) + '\n')
在下面的代码中,runT是一个局部变量,我们只能在创建runT的方法的范围内访问它。
# Running methods in Threads
def createThread(self):
runT = Thread(target=self.methodInAThread, args=[8])
runT.start()
通过将局部变量转换为成员变量,我们可以在另一个方法中调用isAlive来检查线程是否仍在运行。
# Running methods in Threads
def createThread(self):
self.runT = Thread(target=self.methodInAThread, args=[8])
self.runT.start()
print(self.runT)
print('createThread():', self.runT.isAlive())
在前面的代码中,我们将我们的局部变量runT提升为我们类的成员。这样做的效果是使我们能够从我们类的任何方法中评估self.runT变量。
这是通过以下方式实现的:
def methodInAThread(self, numOfLoops=10):
for idx in range(numOfLoops):
sleep(1)
self.scr.insert(tk.INSERT, str(idx) + '\n')
sleep(1)
print('methodInAThread():', self.runT.isAlive())
当我们单击按钮然后退出 GUI 时,我们可以看到createThread方法中的打印语句被打印出来,但我们看不到methodInAThread的第二个打印语句。
相反,我们会得到一个运行时错误。

线程预期完成其分配的任务,因此当我们在线程尚未完成时关闭 GUI 时,Python 告诉我们我们启动的线程不在主事件循环中。
我们可以通过将线程转换为守护程序来解决这个问题,然后它将作为后台任务执行。
这给我们的是,一旦我们关闭我们的 GUI,也就是我们的主线程启动其他线程,守护线程将干净地退出。
我们可以通过在启动线程之前调用setDaemon(True)方法来实现这一点。
# Running methods in Threads
def createThread(self):
runT = Thread(target=self.methodInAThread)
runT.setDaemon(True)
runT.start()
print(runT)
当我们现在单击按钮并在线程尚未完成其分配的任务时退出我们的 GUI 时,我们不再收到任何错误。

它是如何工作的...
虽然有一个启动线程运行的方法,但令人惊讶的是,实际上并没有一个等效的停止方法。
在这个示例中,我们正在一个线程中运行一个方法,该方法将数字打印到我们的ScrolledText小部件中。
当我们退出 GUI 时,我们不再对曾经向我们的小部件打印的线程感兴趣,因此,通过将线程转换为后台守护程序,我们可以干净地退出 GUI。
如何使用队列
Python 队列是一种实现先进先出范例的数据结构,基本上就像一个管道一样工作。你把东西塞进管道的一端,它就从管道的另一端掉出来。
这种队列填充和填充泥浆到物理管道的主要区别在于,在 Python 队列中,事情不会混在一起。你放一个单位进去,那个单位就会从另一边出来。接下来,你放另一个单位进去(比如,例如,一个类的实例),整个单位将作为一个完整的整体从另一端出来。
它以我们插入代码到队列的确切顺序从另一端出来。
注意
队列不是一个我们推送和弹出数据的堆栈。堆栈是一个后进先出(LIFO)的数据结构。
队列是容器,用于保存从潜在不同数据源输入队列的数据。我们可以有不同的客户端在有数据可用时向队列提供数据。无论哪个客户端准备好向我们的队列发送数据,我们都可以显示这些数据在小部件中或将其转发到其他模块。
在队列中使用多个线程完成分配的任务在接收处理的最终结果并显示它们时非常有用。数据被插入到队列的一端,然后以有序的方式从另一端出来,先进先出(FIFO)。
我们的 GUI 可能有五个不同的按钮小部件,每个按钮小部件都会启动我们想要在小部件中显示的不同任务(例如,一个 ScrolledText 小部件)。
这五个不同的任务完成所需的时间不同。
每当一个任务完成时,我们立即需要知道这一点,并在我们的 GUI 中显示这些信息。
通过创建一个共享的 Python 队列,并让五个任务将它们的结果写入这个队列,我们可以使用 FIFO 方法立即显示已完成的任务的结果。
准备工作
随着我们的 GUI 在功能和实用性上不断增加,它开始与网络、进程和网站进行通信,并最终必须等待数据可用于 GUI 表示。
在 Python 中创建队列解决了等待数据在我们的 GUI 中显示的问题。
如何做...
为了在 Python 中创建队列,我们必须从queue模块导入Queue类。在我们的 GUI 模块的顶部添加以下语句:
from threading import Thread
from time import sleep
from queue import Queue
这让我们开始了。
接下来,我们创建一个队列实例。
def useQueues(self):
guiQueue = Queue() # create queue instance
注意
在前面的代码中,我们创建了一个本地的“队列”实例,只能在这个方法中访问。如果我们希望从其他地方访问这个队列,我们必须使用self关键字将其转换为我们的类的成员,这将本地变量绑定到整个类,使其可以在类中的任何其他方法中使用。在 Python 中,我们经常在__init__(self)方法中创建类实例变量,但 Python 非常实用,使我们能够在代码中的任何地方创建这些成员变量。
现在我们有了一个队列的实例。我们可以通过打印它来证明它有效。

为了将数据放入队列,我们使用put命令。为了从队列中取出数据,我们使用get命令。
# Create Queue instance
def useQueues(self):
guiQueue = Queue()
print(guiQueue)
guiQueue.put('Message from a queue')
print(guiQueue.get())
运行修改后的代码会导致消息首先被放入“队列”,然后被从“队列”中取出,并打印到控制台。

我们可以将许多消息放入队列。
# Create Queue instance
def useQueues(self):
guiQueue = Queue()
print(guiQueue)
for idx in range(10):
guiQueue.put('Message from a queue: ' + str(idx))
print(guiQueue.get())
我们将 10 条消息放入了“队列”,但我们只取出了第一条。其他消息仍然在“队列”内,等待以 FIFO 方式取出。

为了取出放入“队列”的所有消息,我们可以创建一个无限循环。
# Create Queue instance
def useQueues(self):
guiQueue = Queue()
print(guiQueue)
for idx in range(10):
guiQueue.put('Message from a queue: ' + str(idx))
while True:
print(guiQueue.get())

虽然这段代码有效,但不幸的是它冻结了我们的 GUI。为了解决这个问题,我们必须在自己的线程中调用该方法,就像我们在之前的示例中所做的那样。
让我们在一个线程中运行我们的方法,并将其绑定到按钮事件:
# Running methods in Threads
def createThread(self, num):
self.runT = Thread(target=self.methodInAThread, args=[num])
self.runT.setDaemon(True)
self.runT.start()
print(self.runT)
print('createThread():', self.runT.isAlive())
# textBoxes are the Consumers of Queue data
writeT = Thread(target=self.useQueues, daemon=True)
writeT.start()
# Create Queue instance
def useQueues(self):
guiQueue = Queue()
print(guiQueue)
for idx in range(10):
guiQueue.put('Message from a queue: ' + str(idx))
while True:
print(guiQueue.get())
当我们现在点击“按钮”时,我们不再会得到一个多余的弹出窗口,代码也能正常工作。

它是如何工作的...
我们创建了一个“队列”,以 FIFO(先进先出)的方式将消息放入队列的一侧。我们从“队列”中取出消息,然后将其打印到控制台(stdout)。
我们意识到我们必须在自己的“线程”中调用该方法。
在不同模块之间传递队列
在这个示例中,我们将在不同的模块之间传递“队列”。随着我们的 GUI 代码变得越来越复杂,我们希望将 GUI 组件与业务逻辑分离,将它们分离到不同的模块中。
模块化使我们可以重用代码,并使代码更易读。
一旦要在我们的 GUI 中显示的数据来自不同的数据源,我们将面临延迟问题,这就是“队列”解决的问题。通过在不同的 Python 模块之间传递“队列”的实例,我们正在分离模块功能的不同关注点。
注意
GUI 代码理想情况下只关注创建和显示小部件。
业务逻辑模块的工作只是执行业务逻辑。
我们必须将这两个元素结合起来,理想情况下在不同模块之间尽可能少地使用关系,减少代码的相互依赖。
注意
避免不必要依赖的编码原则通常被称为“松耦合”。
为了理解松散耦合的重要性,我们可以在白板或纸上画一些框。一个框代表我们的 GUI 类和代码,而其他框代表业务逻辑、数据库等。
接下来,我们在框之间画线,绘制出这些框之间的相互依赖关系,这些框是我们的 Python 模块。
注意
我们在 Python 框之间的行数越少,我们的设计就越松散耦合。
准备工作
在上一个示例中,我们已经开始使用Queues。在这个示例中,我们将从我们的主 GUI 线程传递Queue的实例到其他 Python 模块,这将使我们能够从另一个模块向ScrolledText小部件写入内容,同时保持我们的 GUI 响应。
如何做...
首先,在我们的项目中创建一个新的 Python 模块。让我们称之为Queues.py。我们将在其中放置一个函数(暂时不需要 OOP),并将队列的一个实例传递给它。
我们还传递了创建 GUI 表单和小部件的类的自引用,这使我们能够从另一个 Python 模块中使用所有 GUI 方法。
我们在按钮回调中这样做。
注意
这就是面向对象编程的魔力。在类的中间,我们将自己传递给类内部调用的函数,使用self关键字。
现在代码看起来像这样。
import B04829_Queues as bq
class OOP():
# Button callback
def clickMe(self):
# Passing in the current class instance (self)
print(self)
bq.writeToScrol(self)
导入的模块包含我们正在调用的函数,
def writeToScrol(inst):
print('hi from Queue', inst)
inst.createThread(6)
我们已经在按钮回调中注释掉了对createThread的调用,因为我们现在是从我们的新模块中调用它。
# Threaded method does not freeze our GUI
# self.createThread()
通过从类实例向另一个模块中的函数传递自引用,我们现在可以从其他 Python 模块访问所有 GUI 元素。
运行代码会创建以下结果。

接下来,我们将创建Queue作为我们类的成员,并将对它的引用放在类的__init__方法中。
class OOP():
def __init__(self):
# Create a Queue
self.guiQueue = Queue()
现在我们可以通过简单地使用传入的类引用将消息放入队列中。
def writeToScrol(inst):
print('hi from Queue', inst)
for idx in range(10):
inst.guiQueue.put('Message from a queue: ' + str(idx))
inst.createThread(6)
我们 GUI 代码中的createThread方法现在只从队列中读取数据,这些数据是由我们新模块中的业务逻辑填充的,这样就将逻辑与我们的 GUI 模块分离开来了。
def useQueues(self):
# Now using a class member Queue
while True:
print(self.guiQueue.get())
运行我们修改后的代码会产生相同的结果。我们没有破坏任何东西(至少目前没有)!
它是如何工作的...
为了将 GUI 小部件与表达业务逻辑的功能分开,我们创建了一个类,将队列作为这个类的成员,并通过将类的实例传递到不同 Python 模块中的函数中,我们现在可以访问所有 GUI 小部件以及Queue。
这个示例是一个使用面向对象编程的合理情况的例子。
使用对话框小部件将文件复制到您的网络
这个示例向我们展示了如何将文件从本地硬盘复制到网络位置。
我们将使用 Python 的 tkinter 内置对话框之一,这使我们能够浏览我们的硬盘。然后我们可以选择要复制的文件。
这个示例还向我们展示了如何使Entry小部件只读,并将我们的Entry默认设置为指定位置,这样可以加快浏览我们的硬盘的速度。
准备工作
我们将扩展我们在之前示例中构建的 GUI 的Tab 2。
如何做...
将以下代码添加到我们的 GUI 中def createWidgets(self)方法中,放在我们创建 Tab Control 2 的底部。
新小部件框的父级是tab2,我们在createWidgets()方法的开头创建了它。只要您将下面显示的代码放在tab2的创建物理下方,它就会起作用。
###########################################################
def createWidgets(self):
tabControl = ttk.Notebook(self.win) # Create Tab
tab2 = ttk.Frame(tabControl) # Add a second tab
tabControl.add(tab2, text='Tab 2')
# Create Manage Files Frame
mngFilesFrame = ttk.LabelFrame(tab2, text=' Manage Files: ')
mngFilesFrame.grid(column=0, row=1, sticky='WE', padx=10, pady=5)
# Button Callback
def getFileName():
print('hello from getFileName')
# Add Widgets to Manage Files Frame
lb = ttk.Button(mngFilesFrame, text="Browse to File...", command=getFileName)
lb.grid(column=0, row=0, sticky=tk.W)
file = tk.StringVar()
self.entryLen = scrolW
self.fileEntry = ttk.Entry(mngFilesFrame, width=self.entryLen, textvariable=file)
self.fileEntry.grid(column=1, row=0, sticky=tk.W)
logDir = tk.StringVar()
self.netwEntry = ttk.Entry(mngFilesFrame, width=self.entryLen, textvariable=logDir)
self.netwEntry.grid(column=1, row=1, sticky=tk.W)
def copyFile():
import shutil
src = self.fileEntry.get()
file = src.split('/')[-1]
dst = self.netwEntry.get() + '\\'+ file
try:
shutil.copy(src, dst)
mBox.showinfo('Copy File to Network', 'Success: File copied.')
except FileNotFoundError as err:
mBox.showerror('Copy File to Network', '*** Failed to copy file! ***\n\n' + str(err))
except Exception as ex:
mBox.showerror('Copy File to Network', '*** Failed to copy file! ***\n\n' + str(ex))
cb = ttk.Button(mngFilesFrame, text="Copy File To : ", command=copyFile)
cb.grid(column=0, row=1, sticky=tk.E)
# Add some space around each label
for child in mngFilesFrame.winfo_children():
child.grid_configure(padx=6, pady=6)
这将在我们的 GUI 的Tab 2中添加两个按钮和两个输入。
我们还没有实现按钮回调函数的功能。
运行代码会创建以下 GUI:

点击浏览文件...按钮目前会在控制台上打印。

我们可以使用 tkinter 的内置文件对话框,所以让我们在我们的 Python GUI 模块的顶部添加以下import语句。
from tkinter import filedialog as fd
from os import path
现在我们可以在我们的代码中使用对话框。我们可以使用 Python 的 os 模块来查找 GUI 模块所在的完整路径,而不是硬编码路径。
def getFileName():
print('hello from getFileName')
fDir = path.dirname(__file__)
fName = fd.askopenfilename(parent=self.win, initialdir=fDir)
单击浏览按钮现在会打开askopenfilename对话框。

现在我们可以在这个目录中打开一个文件,或者浏览到另一个目录。在对话框中选择一个文件并单击打开按钮后,我们将保存文件的完整路径在fName本地变量中。
如果我们打开我们的 Python askopenfilename对话框小部件时,能够自动默认到一个目录,这将是很好的,这样我们就不必一直浏览到我们正在寻找的特定文件要打开的地方。
最好通过回到我们的 GUI Tab 1来演示如何做到这一点,这就是我们接下来要做的。
我们可以将默认值输入到 Entry 小部件中。回到我们的Tab 1,这非常容易。我们只需要在创建Entry小部件时添加以下两行代码即可。
# Adding a Textbox Entry widget
self.name = tk.StringVar()
nameEntered = ttk.Entry(self.monty, width=24, textvariable=self.name)
nameEntered.grid(column=0, row=1, sticky='W')
nameEntered.delete(0, tk.END)
nameEntered.insert(0, '< default name >')
当我们现在运行 GUI 时,nameEntered输入框有一个默认值。

我们可以使用以下 Python 语法获取我们正在使用的模块的完整路径,然后我们可以在其下创建一个新的子文件夹。我们可以将其作为模块级全局变量,或者我们可以在方法中创建子文件夹。
# Module level GLOBALS
GLOBAL_CONST = 42
fDir = path.dirname(__file__)
netDir = fDir + '\\Backup'
def __init__(self):
self.createWidgets()
self.defaultFileEntries()
def defaultFileEntries(self):
self.fileEntry.delete(0, tk.END)
self.fileEntry.insert(0, fDir)
if len(fDir) > self.entryLen:
self.fileEntry.config(width=len(fDir) + 3)
self.fileEntry.config(state='readonly')
self.netwEntry.delete(0, tk.END)
self.netwEntry.insert(0, netDir)
if len(netDir) > self.entryLen:
self.netwEntry.config(width=len(netDir) + 3)
我们为两个输入小部件设置默认值,并在设置它们后,将本地文件输入小部件设置为只读。
注意
这个顺序很重要。我们必须先填充输入框,然后再将其设置为只读。
在调用主事件循环之前,我们还选择Tab 2,不再将焦点设置到Tab 1的Entry中。在我们的 tkinter notebook上调用select是从零开始的,所以通过传入值 1,我们选择Tab 2...
# Place cursor into name Entry
# nameEntered.focus()
tabControl.select(1)

由于我们不都在同一个网络上,这个示例将使用本地硬盘作为网络的示例。
UNC 路径是通用命名约定,这意味着我们可以通过双反斜杠访问网络服务器,而不是在访问 Windows PC 上的本地硬盘时使用典型的C:\。
注意
你只需要使用 UNC,并用\\<server name> \<folder>\替换C:\。
这个例子可以用来将我们的代码备份到一个备份目录,如果不存在,我们可以使用os.makedirs来创建它。
# Module level GLOBALS
GLOBAL_CONST = 42
from os import makedirs
fDir = path.dirname(__file__)
netDir = fDir + '\\Backup'
if not path.exists(netDir):
makedirs(netDir, exist_ok = True)
在选择要复制到其他地方的文件后,我们导入 Python 的shutil模块。我们需要文件源的完整路径,一个网络或本地目录路径,然后我们使用shutil.copy将文件名附加到我们将要复制的路径上。
注意
Shutil 是 shell utility 的简写。
我们还可以通过消息框向用户提供反馈,指示复制是否成功或失败。为了做到这一点,导入messagebox并将其重命名为mBox。
在下面的代码中,我们将混合两种不同的方法来放置我们的导入语句。在 Python 中,我们有一些其他语言不提供的灵活性。
我们通常将所有的导入语句放在每个 Python 模块的顶部,这样可以清楚地看出我们正在导入哪些模块。
同时,现代编码方法是将变量的创建放在首次使用它们的函数或方法附近。
在下面的代码中,我们在 Python 模块的顶部导入了消息框,然后在一个函数中也导入了 shutil Python 模块。
为什么我们要这样做呢?
这样做会起作用吗?
答案是,是的,它确实有效,我们将这个导入语句放在一个函数中,因为这是我们的代码中唯一需要这个模块的地方。
如果我们从不调用这个方法,那么我们将永远不会导入这个方法所需的模块。
在某种意义上,您可以将这种技术视为惰性初始化设计模式。
如果我们不需要它,我们就不会在 Python 代码中导入它,直到我们真正需要它。
这里的想法是,我们的整个代码可能需要,比如说,二十个不同的模块。在运行时,真正需要哪些模块取决于用户的交互。如果我们从未调用copyFile()函数,那么就没有必要导入shutil。
一旦我们点击调用copyFile()函数的按钮,在这个函数中,我们就导入了所需的模块。
from tkinter import messagebox as mBox
def copyFile():
import shutil
src = self.fileEntry.get()
file = src.split('/')[-1]
dst = self.netwEntry.get() + '\\'+ file
try:
shutil.copy(src, dst)
mBox.showinfo('Copy File to Network', 'Success: File copied.')
except FileNotFoundError as err:
mBox.showerror('Copy File to Network', '*** Failed to copy file! ***\n\n' + str(err))
except Exception as ex:
mBox.showerror('Copy File to Network', '*** Failed to copy file! ***\n\n' + str(ex))
当我们现在运行我们的 GUI 并浏览到一个文件并点击复制时,文件将被复制到我们在Entry小部件中指定的位置。

如果文件不存在,或者我们忘记浏览文件并尝试复制整个父文件夹,代码也会让我们知道,因为我们使用了 Python 的内置异常处理能力。

它是如何工作的...
我们正在使用 Python shell 实用程序将文件从本地硬盘复制到网络。由于大多数人都没有连接到相同的局域网,我们通过将代码备份到不同的本地文件夹来模拟复制。
我们正在使用 tkinter 的对话框控件,并且通过默认目录路径,我们可以提高复制文件的效率。
使用 TCP/IP 通过网络进行通信
这个示例向您展示了如何使用套接字通过 TCP/IP 进行通信。为了实现这一点,我们需要 IP 地址和端口号。
为了保持简单并独立于不断变化的互联网 IP 地址,我们将创建自己的本地 TCP/IP 服务器,并作为客户端,学习如何连接到它并从 TCP/IP 连接中读取数据。
我们将通过使用我们在以前的示例中创建的队列,将这种网络功能集成到我们的 GUI 中。
准备工作
我们将创建一个新的 Python 模块,它将是 TCP 服务器。
如何做...
在 Python 中实现 TCP 服务器的一种方法是从socketserver模块继承。我们子类化BaseRequestHandler,然后覆盖继承的handle方法。在很少的 Python 代码行中,我们可以实现一个 TCP 服务器模块。
from socketserver import BaseRequestHandler, TCPServer
class RequestHandler(BaseRequestHandler):
# override base class handle method
def handle(self):
print('Server connected to: ', self.client_address)
while True:
rsp = self.request.recv(512)
if not rsp: break
self.request.send(b'Server received: ' + rsp)
def startServer():
serv = TCPServer(('', 24000), RequestHandler)
serv.serve_forever()
我们将我们的RequestHandler类传递给TCPServer初始化程序。空的单引号是传递本地主机的快捷方式,这是我们自己的 PC。这是 IP 地址 127.0.0.1 的 IP 地址。元组中的第二项是端口号。我们可以选择任何在本地 PC 上未使用的端口号。
我们只需要确保在 TCP 连接的客户端端口上使用相同的端口,否则我们将无法连接到服务器。当然,在客户端可以连接到服务器之前,我们必须先启动服务器。
我们将修改我们的Queues.py模块,使其成为 TCP 客户端。
from socket import socket, AF_INET, SOCK_STREAM
def writeToScrol(inst):
print('hi from Queue', inst)
sock = socket(AF_INET, SOCK_STREAM)
sock.connect(('localhost', 24000))
for idx in range(10):
sock.send(b'Message from a queue: ' + bytes(str(idx).encode()) )
recv = sock.recv(8192).decode()
inst.guiQueue.put(recv)
inst.createThread(6)
这是我们与 TCP 服务器通信所需的所有代码。在这个例子中,我们只是向服务器发送一些字节,服务器将它们发送回来,并在返回响应之前添加一些字符串。
注意
这显示了 TCP 通过网络进行通信的原理。
一旦我们知道如何通过 TCP/IP 连接到远程服务器,我们将使用由我们感兴趣的通信程序的协议设计的任何命令。第一步是在我们可以向驻留在服务器上的特定应用程序发送命令之前进行连接。
在writeToScrol函数中,我们将使用与以前相同的循环,但现在我们将把消息发送到 TCP 服务器。服务器修改接收到的消息,然后将其发送回给我们。接下来,我们将其放入 GUI 成员队列中,就像以前的示例一样,在其自己的Thread中运行。
注意
在 Python 3 中,我们必须以二进制格式通过套接字发送字符串。现在添加整数索引变得有点复杂,因为我们必须将其转换为字符串,对其进行编码,然后将编码后的字符串转换为字节!
sock.send(b'Message from a queue: ' + bytes(str(idx).encode()) )
注意字符串前面的b,然后,嗯,所有其他所需的转换...
我们在 OOP 类的初始化程序中启动 TCP 服务器的线程。
class OOP():
def __init__(self):
# Start TCP/IP server in its own thread
svrT = Thread(target=startServer, daemon=True)
svrT.start()
现在,在Tab 1上单击Click Me!按钮将在我们的ScrolledText小部件中创建以下输出,以及在控制台上,由于使用Threads,响应非常快。

它是如何工作的...
我们创建了一个 TCP 服务器来模拟连接到本地区域网络或互联网上的服务器。我们将我们的队列模块转换为 TCP 客户端。我们在它们自己的后台线程中运行队列和服务器,这样我们的 GUI 非常响应。
使用 URLOpen 从网站读取数据
这个示例展示了我们如何使用 Python 的内置模块轻松读取整个网页。我们将首先以原始格式显示网页数据,然后解码它,然后在我们的 GUI 中显示它。
准备工作
我们将从网页中读取数据,然后在我们的 GUI 的ScrolledText小部件中显示它。
如何做...
首先,我们创建一个新的 Python 模块并命名为URL.py。
然后,我们导入所需的功能来使用 Python 读取网页。
我们可以用很少的代码来做到这一点。
我们将我们的代码包装在一个类似于 Java 和 C#的try…except块中。这是 Python 支持的一种现代编码方法。
每当我们有可能不完整的代码时,我们可以尝试这段代码,如果成功,一切都很好。
如果try…except块中的代码块不起作用,Python 解释器将抛出几种可能的异常,然后我们可以捕获。一旦我们捕获了异常,我们就可以决定接下来要做什么。
Python 中有一系列的异常,我们还可以创建自己的类,继承并扩展 Python 异常类。
在下面显示的代码中,我们主要关注我们尝试打开的 URL 可能不可用,因此我们将我们的代码包装在try…except代码块中。
如果代码成功打开所请求的 URL,一切都很好。
如果失败,可能是因为我们的互联网连接断开了,我们就会进入代码的异常部分,并打印出发生异常的信息。
注意
您可以在docs.python.org/3.4/library/exceptions.html了解更多关于 Python 异常处理的信息。
from urllib.request import urlopen
link = 'http://python.org/'
try:
f = urlopen(link)
print(f)
html = f.read()
print(html)
htmldecoded = html.decode()
print(htmldecoded)
except Exception as ex:
print('*** Failed to get Html! ***\n\n' + str(ex))
通过在官方 Python 网站上调用urlopen,我们得到整个数据作为一个长字符串。
第一个打印语句将这个长字符串打印到控制台上。
然后我们对结果调用decode,这次我们得到了一千多行的网页数据,包括一些空白。
我们还打印调用urlopen的类型,它是一个http.client.HTTPResponse对象。实际上,我们首先打印出来。

这是我们刚刚读取的官方 Python 网页。如果您是 Web 开发人员,您可能对如何处理解析数据有一些好主意。

接下来,我们在我们的 GUI 中的ScrolledText小部件中显示这些数据。为了这样做,我们必须将我们的新模块连接到我们的 GUI,从网页中读取数据。
为了做到这一点,我们需要一个对我们 GUI 的引用,而一种方法是通过将我们的新模块绑定到Tab 1按钮回调。
我们可以将从 Python 网页解码的 HTML 数据返回给Button小部件,然后将其放在ScrolledText控件中。
因此,让我们将我们的代码转换为一个函数,并将数据返回给调用代码。
from urllib.request import urlopen
link = 'http://python.org/'
def getHtml():
try:
f = urlopen(link)
#print(f)
html = f.read()
#print(html)
htmldecoded = html.decode()
#print(htmldecoded)
except Exception as ex:
print('*** Failed to get Html! ***\n\n' + str(ex))
else:
return htmldecoded
现在,我们可以通过首先导入新模块,然后将数据插入到小部件中,在我们的button回调方法中写入数据到ScrolledText控件。在调用writeToScrol之后,我们还给它一些休眠时间。
import B04829_Ch06_URL as url
# Button callback
def clickMe(self):
bq.writeToScrol(self)
sleep(2)
htmlData = url.getHtml()
print(htmlData)
self.scr.insert(tk.INSERT, htmlData)
HTML 数据现在显示在我们的 GUI 小部件中。

它是如何工作的...
我们创建了一个新模块,将从网页获取数据的代码与我们的 GUI 代码分离。这总是一个好主意。我们读取网页数据,然后解码后返回给调用代码。然后我们使用按钮回调函数将返回的数据放入ScrolledText控件中。
本章向我们介绍了一些高级的 Python 编程概念,我们将它们结合起来,制作出一个功能性的 GUI 程序。
第七章:通过我们的 GUI 将数据存储在 MySQL 数据库中
在本章中,我们将通过连接到 MySQL 数据库来增强我们的 Python GUI。
-
从 Python 连接到 MySQL 数据库
-
配置 MySQL 连接
-
设计 Python GUI 数据库
-
使用 SQL INSERT 命令
-
使用 SQL UPDATE 命令
-
使用 SQL DELETE 命令
-
从我们的 MySQL 数据库中存储和检索数据
介绍
在我们可以连接到 MySQL 服务器之前,我们必须先访问 MySQL 服务器。本章的第一个步骤将向您展示如何安装免费的 MySQL 服务器社区版。
成功连接到我们的 MySQL 服务器运行实例后,我们将设计并创建一个数据库,该数据库将接受一本书的标题,这可能是我们自己的日记或者是我们在互联网上找到的引用。我们将需要书的页码,这可能为空白,然后我们将使用我们在 Python 3 中构建的 GUI 将我们喜欢的引用从一本书、日记、网站或朋友中插入到我们的 MySQL 数据库中。
我们将使用我们的 Python GUI 来插入、修改、删除和显示我们喜欢的引用,以发出这些 SQL 命令并显示数据。
注意
CRUD是您可能遇到的一个数据库术语,它缩写了四个基本的 SQL 命令,代表创建、读取、更新和删除。
从 Python 连接到 MySQL 数据库
在我们可以连接到 MySQL 数据库之前,我们必须先连接到 MySQL 服务器。
为了做到这一点,我们需要知道 MySQL 服务器的 IP 地址以及它所监听的端口。
我们还必须是一个注册用户,并且需要密码才能被 MySQL 服务器验证。
准备工作
您需要访问一个正在运行的 MySQL 服务器实例,并且您还需要具有管理员权限才能创建数据库和表。
在官方 MySQL 网站上有一个免费的 MySQL 社区版可用。您可以从以下网址在本地 PC 上下载并安装它:dev.mysql.com/downloads/
注意
在本章中,我们使用的是 MySQL 社区服务器(GPL)版本:5.6.26。
如何做...
为了连接到 MySQL,我们首先需要安装一个特殊的 Python 连接器驱动程序。这个驱动程序将使我们能够从 Python 与 MySQL 服务器通信。
该驱动程序可以在 MySQL 网站上免费获得,并附带一个非常好的在线教程。您可以从以下网址安装它:
dev.mysql.com/doc/connector-python/en/index.html
注意
确保选择与您安装的 Python 版本匹配的安装程序。在本章中,我们使用 Python 3.4 的安装程序。

在安装过程的最后,目前有一点小小的惊喜。当我们启动.msi安装程序时,我们会短暂地看到一个显示安装进度的 MessageBox,但然后它就消失了。我们没有收到安装是否成功的确认。
验证我们是否安装了正确的驱动程序,让 Python 能够与 MySQL 通信,一种方法是查看 Python site-packages 目录。
如果您的 site-packages 目录看起来类似于以下屏幕截图,并且您看到一些新文件的名称中带有mysql_connector_python,那么我们确实安装了一些东西...

上述提到的官方 MySQL 网站附带一个教程,网址如下:
dev.mysql.com/doc/connector-python/en/connector-python-tutorials.html
在线教程示例中关于验证安装 Connector/Python 驱动程序是否成功的部分有点误导,因为它试图连接到一个员工数据库,这个数据库在我的社区版中并没有自动创建。
验证我们的 Connector/Python 驱动程序是否真的安装了的方法是,只需连接到 MySQL 服务器而不指定特定的数据库,然后打印出连接对象。
注意
用你在 MySQL 安装中使用的真实凭据替换占位符括号名称<adminUser>和<adminPwd>。
如果您安装了 MySQL 社区版,您就是管理员,并且在 MySQL 安装过程中会选择用户名和密码。
import mysql.connector as mysql
conn = mysql.connect(user=<adminUser>, password=<adminPwd>,
host='127.0.0.1')
print(conn)
conn.close()
如果运行上述代码导致以下输出打印到控制台,则表示正常。

如果您无法连接到 MySQL 服务器,那么在安装过程中可能出了问题。如果是这种情况,请尝试卸载 MySQL,重新启动您的 PC,然后再次运行 MySQL 安装程序。仔细检查您下载的 MySQL 安装程序是否与您的 Python 版本匹配。如果您安装了多个版本的 Python,有时会导致混淆,因为您最后安装的版本会被添加到 Windows 路径环境变量中,并且一些安装程序只会使用在此位置找到的第一个 Python 版本。
当我安装了 Python 32 位版本并且我困惑为什么一些我下载的模块无法工作时,这种情况发生了。
安装程序下载了 32 位模块,这些模块与 64 位版本的 Python 不兼容。
它是如何工作的...
为了将我们的 GUI 连接到 MySQL 服务器,如果我们想创建自己的数据库,我们需要能够以管理员权限连接到服务器。
如果数据库已经存在,那么我们只需要连接、插入、更新和删除数据的授权权限。
在下一个教程中,我们将在 MySQL 服务器上创建一个新的数据库。
配置 MySQL 连接
在上一个教程中,我们使用了最短的方式通过将用于身份验证的凭据硬编码到connection方法中来连接到 MySQL 服务器。虽然这是早期开发的快速方法,但我们绝对不希望将我们的 MySQL 服务器凭据暴露给任何人,除非我们授予特定用户对数据库、表、视图和相关数据库命令的权限。
通过将凭据存储在配置文件中,通过 MySQL 服务器进行身份验证的一个更安全的方法是我们将在本教程中实现的。
我们将使用我们的配置文件连接到 MySQL 服务器,然后在 MySQL 服务器上创建我们自己的数据库。
注意
我们将在所有接下来的教程中使用这个数据库。
准备工作
需要具有管理员权限的运行中的 MySQL 服务器才能运行本教程中显示的代码。
注意
上一个教程展示了如何安装免费的 MySQL 服务器社区版。管理员权限将使您能够实现这个教程。
如何做...
首先,在MySQL.py代码的同一模块中创建一个字典。
# create dictionary to hold connection info
dbConfig = {
'user': <adminName>, # use your admin name
'password': <adminPwd>, # use your admin password
'host': '127.0.0.1', # IP address of localhost
}
接下来,在连接方法中,我们解压字典的值。而不是写成,
mysql.connect('user': <adminName>, 'password': <adminPwd>, 'host': '127.0.0.1')
我们使用(**dbConfig),这与上面的方法相同,但更简洁。
import mysql.connector as mysql
# unpack dictionary credentials
conn = mysql.connect(**dbConfig)
print(conn)
这将导致与 MySQL 服务器的相同成功连接,但不同之处在于连接方法不再暴露任何关键任务信息。
注意
数据库服务器对你的任务至关重要。一旦你丢失了宝贵的数据...并且找不到任何最近的备份时,你就会意识到这一点!

现在,在同一个 Python 模块中将相同的用户名、密码、数据库等放入字典中并不能消除任何人浏览代码时看到凭据的风险。
为了增加数据库安全性,我们首先将字典移到自己的 Python 模块中。让我们称这个新的 Python 模块为GuiDBConfig.py。
然后我们导入这个模块并解压凭据,就像之前做的那样。
import GuiDBConfig as guiConf
# unpack dictionary credentials
conn = mysql.connect(**guiConf.dbConfig)
print(conn)
注意
一旦我们将这个模块放在一个安全的地方,与其余代码分开,我们就为我们的 MySQL 数据实现了更高级别的安全性。
现在我们知道如何连接到 MySQL 并具有管理员权限,我们可以通过发出以下命令来创建我们自己的数据库:
GUIDB = 'GuiDB'
# unpack dictionary credentials
conn = mysql.connect(**guiConf.dbConfig)
cursor = conn.cursor()
try:
cursor.execute("CREATE DATABASE {} DEFAULT CHARACTER SET 'utf8'".format(GUIDB))
except mysql.Error as err:
print("Failed to create DB: {}".format(err))
conn.close()
为了执行对 MySQL 的命令,我们从连接对象创建一个游标对象。
游标通常是数据库表中特定行的位置,我们可以在表中向上或向下移动,但在这里我们使用它来创建数据库本身。
我们将 Python 代码包装到try...except块中,并使用 MySQL 的内置错误代码告诉我们是否出现了任何问题。
我们可以通过执行创建数据库的代码两次来验证此块是否有效。第一次,它将在 MySQL 中创建一个新数据库,第二次将打印出一个错误消息,说明此数据库已经存在。
我们可以通过使用完全相同的游标对象语法执行以下 MySQL 命令来验证哪些数据库存在。
我们不是发出CREATE DATABASE命令,而是创建一个游标并使用它来执行SHOW DATABASES命令,然后获取并打印到控制台输出的结果。
import mysql.connector as mysql
import GuiDBConfig as guiConf
# unpack dictionary credentials
conn = mysql.connect(**guiConf.dbConfig)
cursor = conn.cursor()
cursor.execute("SHOW DATABASES")
print(cursor.fetchall())
conn.close()
注意
我们通过在游标对象上调用fetchall方法来检索结果。
运行此代码会显示我们的 MySQL 服务器实例中当前存在哪些数据库。从输出中可以看到,MySQL 附带了几个内置数据库,例如information_schema等。我们已成功创建了自己的guidb数据库,如输出所示。所有其他数据库都是 MySQL 附带的。

请注意,尽管我们在创建时指定了数据库的混合大小写字母为 GuiDB,但SHOW DATABASES命令显示 MySQL 中所有现有数据库的小写形式,并将我们的数据库显示为guidb。
它是如何工作的...
为了将我们的 Python GUI 连接到 MySQL 数据库,我们首先必须知道如何连接到 MySQL 服务器。这需要建立一个连接,只有当我们能够提供所需的凭据时,MySQL 才会接受这个连接。
虽然将字符串放入一行 Python 代码很容易,但在处理数据库时,我们必须非常谨慎,因为今天的个人沙箱开发环境,明天很容易就可能变成全球网络上可以访问的环境。
您不希望危害数据库安全性,这个配方的第一部分展示了通过将 MySQL 服务器的连接凭据放入一个单独的文件,并将此文件放在外部世界无法访问的位置,来更安全地放置连接凭据的方法,我们的数据库系统将变得更加安全。
在真实的生产环境中,MySQL 服务器安装、连接凭据和 dbConfig 文件都将由 IT 系统管理员处理,他们将使您能够导入 dbConfig 文件以连接到 MySQL 服务器,而您不知道实际的凭据是什么。解压 dbConfig 不会像我们的代码那样暴露凭据。
第二部分在 MySQL 服务器实例中创建了我们自己的数据库,我们将在接下来的配方中扩展并使用这个数据库,将其与我们的 Python GUI 结合使用。
设计 Python GUI 数据库
在开始创建表并向其中插入数据之前,我们必须设计数据库。与更改本地 Python 变量名称不同,一旦创建并加载了数据的数据库模式就不那么容易更改。
在删除表之前,我们必须提取数据,然后DROP表,并以不同的名称重新创建它,最后重新导入原始数据。
你明白了...
设计我们的 GUI MySQL 数据库首先意味着考虑我们希望我们的 Python 应用程序如何使用它,然后选择与预期目的相匹配的表名。
准备工作
我们正在使用前一篇中创建的 MySQL 数据库。需要运行一个 MySQL 实例,前两篇文章介绍了如何安装 MySQL 和所有必要的附加驱动程序,以及如何创建本章中使用的数据库。
操作步骤…
首先,我们将在前几篇中创建的两个标签之间在我们的 Python GUI 中移动小部件,以便更好地组织我们的 Python GUI 以连接到 MySQL 数据库。
我们重命名了几个小部件,并将访问 MySQL 数据的代码分离到以前称为 Tab 1 的位置,我们将不相关的小部件移动到我们在早期配方中称为 Tab 2 的位置。
我们还调整了一些内部 Python 变量名,以便更好地理解我们的代码。
注意
代码可读性是一种编码美德,而不是浪费时间。
我们重构后的 Python GUI 现在看起来像下面的截图。我们将第一个标签重命名为 MySQL,并创建了两个 tkinter LabelFrame 小部件。我们将顶部的一个标记为 Python 数据库,它包含两个标签和六个 tkinter 输入小部件加上三个按钮,我们使用 tkinter 网格布局管理器将它们排列在四行三列中。
我们将书名和页数输入到输入小部件中,点击按钮将导致插入、检索或修改书籍引用。
底部的 LabelFrame 有一个图书引用的标签,这个框架中的 ScrolledText 小部件将显示我们的书籍和引用。

我们将创建两个 SQL 表来保存我们的数据。第一个将保存书名和书页的数据。然后我们将与第二个表连接,第二个表将保存书籍引用。
我们将通过主键到外键关系将这两个表连接在一起。
所以,现在让我们创建第一个数据库表。
在这之前,让我们先验证一下我们的数据库确实没有表。根据在线 MySQL 文档,查看数据库中存在的表的命令如下。
注意
13.7.5.38 SHOW TABLES 语法:
SHOW [FULL] TABLES [{FROM | IN} db_name]
[LIKE 'pattern' | WHERE expr]
需要注意的是,在上述语法中,方括号中的参数(如FULL)是可选的,而花括号中的参数(如FROM)是SHOW TABLES命令描述中所需的。在FROM和IN之间的管道符号表示 MySQL 语法要求其中一个。
# unpack dictionary credentials
conn = mysql.connect(**guiConf.dbConfig)
# create cursor
cursor = conn.cursor()
# execute command
cursor.execute("SHOW TABLES FROM guidb")
print(cursor.fetchall())
# close connection to MySQL
conn.close()
当我们在 Python 中执行 SQL 命令时,我们得到了预期的结果,即一个空列表,显示我们的数据库当前没有表。

我们还可以通过执行USE <DB>命令首先选择数据库。现在,我们不必将其传递给SHOW TABLES命令,因为我们已经选择了要交谈的数据库。
以下代码创建了与之前相同的真实结果:
cursor.execute("USE guidb")
cursor.execute("SHOW TABLES")
现在我们知道如何验证我们的数据库中是否有表,让我们创建一些表。创建了两个表之后,我们将使用与之前相同的命令验证它们是否真的进入了我们的数据库。
我们通过执行以下代码创建了第一个名为Books的表。
# connect by unpacking dictionary credentials
conn = mysql.connect(**guiConf.dbConfig)
# create cursor
cursor = conn.cursor()
# select DB
cursor.execute("USE guidb")
# create Table inside DB
cursor.execute("CREATE TABLE Books ( \
Book_ID INT NOT NULL AUTO_INCREMENT, \
Book_Title VARCHAR(25) NOT NULL, \
Book_Page INT NOT NULL, \
PRIMARY KEY (Book_ID) \
) ENGINE=InnoDB")
# close connection to MySQL
conn.close()
我们可以通过执行以下命令验证表是否在我们的数据库中创建了。

现在的结果不再是一个空列表,而是一个包含元组的列表,显示了我们刚刚创建的books表。
我们可以使用 MySQL 命令行客户端查看表中的列。为了做到这一点,我们必须以 root 用户身份登录。我们还必须在命令的末尾添加一个分号。
注意
在 Windows 上,您只需双击 MySQL 命令行客户端的快捷方式,这个快捷方式会在 MySQL 安装过程中自动安装。
如果您的桌面上没有快捷方式,您可以在典型默认安装的以下路径找到可执行文件:
C:\Program Files\MySQL\MySQL Server 5.6\bin\mysql.exe
如果没有运行 MySQL 客户端的快捷方式,您必须传递一些参数:
-
C:\Program Files\MySQL\MySQL Server 5.6\bin\mysql.exe -
--defaults-file=C:\ProgramData\MySQL\MySQL Server 5.6\my.ini -
-uroot -
-p
双击快捷方式,或使用完整路径到可执行文件的命令行并传递所需的参数,将打开 MySQL 命令行客户端,提示您输入 root 用户的密码。
如果您记得在安装过程中为 root 用户分配的密码,那么可以运行SHOW COLUMNS FROM books;命令,如下所示。这将显示我们的books表的列从我们的 guidb。
注意
在 MySQL 客户端执行命令时,语法不是 Pythonic 的。

接下来,我们将创建第二个表,用于存储书籍和期刊引用。我们将通过执行以下代码来创建它:
# select DB
cursor.execute("USE guidb")
# create second Table inside DB
cursor.execute("CREATE TABLE Quotations ( \
Quote_ID INT, \
Quotation VARCHAR(250), \
Books_Book_ID INT, \
FOREIGN KEY (Books_Book_ID) \
REFERENCES Books(Book_ID) \
ON DELETE CASCADE \
) ENGINE=InnoDB")
执行SHOW TABLES命令现在显示我们的数据库有两个表。

我们可以通过使用 Python 执行 SQL 命令来查看列。

使用 MySQL 客户端可能以更好的格式显示数据。我们还可以使用 Python 的漂亮打印(pprint)功能。

MySQL 客户端仍然以更清晰的格式显示我们的列,当您运行此客户端时可以看到。
工作原理
我们设计了 Python GUI 数据库,并重构了我们的 GUI,以准备使用我们的新数据库。然后我们创建了一个 MySQL 数据库,并在其中创建了两个表。
我们通过 Python 和随 MySQL 服务器一起提供的 MySQL 客户端验证了表是否成功进入我们的数据库。
在下一个步骤中,我们将向我们的表中插入数据。
使用 SQL INSERT 命令
本步骤介绍了整个 Python 代码,向您展示如何创建和删除 MySQL 数据库和表,以及如何显示我们的 MySQL 实例中现有数据库、表、列和数据。
在创建数据库和表之后,我们将向本步骤中创建的两个表中插入数据。
注意
我们正在使用主键到外键的关系来连接两个表的数据。
我们将在接下来的两个步骤中详细介绍这是如何工作的,我们将修改和删除我们的 MySQL 数据库中的数据。
准备工作
本步骤基于我们在上一个步骤中创建的 MySQL 数据库,并向您展示如何删除和重新创建 GuiDB。
注意
删除数据库当然会删除数据库中表中的所有数据,因此我们还将向您展示如何重新插入这些数据。
如何做…
我们的MySQL.py模块的整个代码都在本章的代码文件夹中,可以从 Packt Publishing 的网站上下载。它创建数据库,向其中添加表,然后将数据插入我们创建的两个表中。
在这里,我们将概述代码,而不显示所有实现细节,以节省空间,因为显示整个代码需要太多页面。
import mysql.connector as mysql
import GuiDBConfig as guiConf
class MySQL():
# class variable
GUIDB = 'GuiDB'
#------------------------------------------------------
def connect(self):
# connect by unpacking dictionary credentials
conn = mysql.connector.connect(**guiConf.dbConfig)
# create cursor
cursor = conn.cursor()
return conn, cursor
#------------------------------------------------------
def close(self, cursor, conn):
# close cursor
#------------------------------------------------------
def showDBs(self):
# connect to MySQL
#------------------------------------------------------
def createGuiDB(self):
# connect to MySQL
#------------------------------------------------------
def dropGuiDB(self):
# connect to MySQL
#------------------------------------------------------
def useGuiDB(self, cursor):
'''Expects open connection.'''
# select DB
#------------------------------------------------------
def createTables(self):
# connect to MySQL
# create Table inside DB
#------------------------------------------------------
def dropTables(self):
# connect to MySQL
#------------------------------------------------------
def showTables(self):
# connect to MySQL
#------------------------------------------------------
def insertBooks(self, title, page, bookQuote):
# connect to MySQL
# insert data
#------------------------------------------------------
def insertBooksExample(self):
# connect to MySQL
# insert hard-coded data
#------------------------------------------------------
def showBooks(self):
# connect to MySQL
#------------------------------------------------------
def showColumns(self):
# connect to MySQL
#------------------------------------------------------
def showData(self):
# connect to MySQL
#------------------------------------------------------
if __name__ == '__main__':
# Create class instance
mySQL = MySQL()
运行上述代码会在我们创建的数据库中创建以下表和数据。

工作原理
我们已经创建了一个 MySQL 数据库,建立了与之的连接,然后创建了两个表,用于存储喜爱的书籍或期刊引用的数据。
我们在两个表之间分配数据,因为引用往往相当大,而书名和书页码非常短。通过这样做,我们可以提高数据库的效率。
注意
在 SQL 数据库语言中,将数据分隔到单独的表中称为规范化。
使用 SQL UPDATE 命令
这个配方将使用前一个配方中的代码,对其进行更详细的解释,然后扩展代码以更新我们的数据。
为了更新我们之前插入到 MySQL 数据库表中的数据,我们使用 SQL UPDATE命令。
准备工作
这个配方是基于前一个配方的,所以请阅读和研究前一个配方,以便理解本配方中修改现有数据的编码。
如何做…
首先,我们将通过运行以下 Python 到 MySQL 命令来显示要修改的数据:
import mysql.connector as mysql
import GuiDBConfig as guiConf
class MySQL():
# class variable
GUIDB = 'GuiDB'
#------------------------------------------------------
def showData(self):
# connect to MySQL
conn, cursor = self.connect()
self.useGuiDB(cursor)
# execute command
cursor.execute("SELECT * FROM books")
print(cursor.fetchall())
cursor.execute("SELECT * FROM quotations")
print(cursor.fetchall())
# close cursor and connection
self.close(cursor, conn)
#==========================================================
if __name__ == '__main__':
# Create class instance
mySQL = MySQL()
mySQL.showData()
运行代码会产生以下结果:

也许我们不同意“四人帮”的观点,所以让我们修改他们著名的编程引语。
注意
四人帮是创作了世界著名书籍《设计模式》的四位作者,这本书对整个软件行业产生了深远影响,使我们认识到、思考并使用软件设计模式进行编码。
我们将通过更新我们最喜爱的引语数据库来实现这一点。
首先,我们通过搜索书名来检索主键值,然后将该值传递到我们对引语的搜索中。
#------------------------------------------------------
def updateGOF(self):
# connect to MySQL
conn, cursor = self.connect()
self.useGuiDB(cursor)
# execute command
cursor.execute("SELECT Book_ID FROM books WHERE Book_Title = 'Design Patterns'")
primKey = cursor.fetchall()[0][0]
print(primKey)
cursor.execute("SELECT * FROM quotations WHERE Books_Book_ID = (%s)", (primKey,))
print(cursor.fetchall())
# close cursor and connection
self.close(cursor, conn)
#==========================================================
if __name__ == '__main__':
# Create class instance
mySQL = MySQL()
mySQL.updateGOF()
这给我们带来了以下结果:

现在我们知道了引语的主键,我们可以通过执行以下命令来更新引语。
#------------------------------------------------------
def updateGOF(self):
# connect to MySQL
conn, cursor = self.connect()
self.useGuiDB(cursor)
# execute command
cursor.execute("SELECT Book_ID FROM books WHERE Book_Title = 'Design Patterns'")
primKey = cursor.fetchall()[0][0]
print(primKey)
cursor.execute("SELECT * FROM quotations WHERE Books_Book_ID = (%s)", (primKey,))
print(cursor.fetchall())
cursor.execute("UPDATE quotations SET Quotation = (%s) WHERE Books_Book_ID = (%s)", \
("Pythonic Duck Typing: If it walks like a duck and talks like a duck it probably is a duck...", primKey))
# commit transaction
conn.commit ()
cursor.execute("SELECT * FROM quotations WHERE Books_Book_ID = (%s)", (primKey,))
print(cursor.fetchall())
# close cursor and connection
self.close(cursor, conn)
#==========================================================
if __name__ == '__main__':
# Create class instance
mySQL = MySQL()
#------------------------
mySQL.updateGOF()
book, quote = mySQL.showData()
print(book, quote)
通过运行上述代码,我们使这个经典的编程更加 Pythonic。
如下截图所示,在运行上述代码之前,我们的Book_ID 1标题通过主外键关系与引语表的Books_Book_ID列相关联。
这是《设计模式》书中的原始引语。
然后,我们通过 SQL UPDATE命令更新了与该 ID 相关的引语。
ID 都没有改变,但现在与Book_ID 1相关联的引语已经改变,如下所示在第二个 MySQL 客户端窗口中。

工作原理…
在这个配方中,我们从数据库和之前配方中创建的数据库表中检索现有数据。我们向表中插入数据,并使用 SQL UPDATE命令更新我们的数据。
使用 SQL DELETE 命令
在这个配方中,我们将使用 SQL DELETE命令来删除我们在前面配方中创建的数据。
虽然删除数据乍一看似乎很简单,但一旦我们在生产中拥有一个相当大的数据库设计,事情可能就不那么容易了。
因为我们通过主外键关系设计了 GUI 数据库,当我们删除某些数据时,不会出现孤立记录,因为这种数据库设计会处理级联删除。
准备工作
这个配方使用了 MySQL 数据库、表以及本章前面配方中插入到这些表中的数据。为了展示如何创建孤立记录,我们将不得不改变其中一个数据库表的设计。
如何做…
我们通过只使用两个数据库表来保持我们的数据库设计简单。
虽然在删除数据时这样做是有效的,但总会有可能出现孤立记录。这意味着我们在一个表中删除数据,但在另一个 SQL 表中却没有删除相关数据。
如果我们创建quotations表时没有与books表建立外键关系,就可能出现孤立记录。
# create second Table inside DB --
# No FOREIGN KEY relation to Books Table
cursor.execute("CREATE TABLE Quotations ( \
Quote_ID INT AUTO_INCREMENT, \
Quotation VARCHAR(250), \
Books_Book_ID INT, \
PRIMARY KEY (Quote_ID) \
) ENGINE=InnoDB")
在向books和quotations表中插入数据后,如果我们执行与之前相同的delete语句,我们只会删除Book_ID 1的书籍,而与之相关的引语Books_Book_ID 1则会被留下。
这是一个孤立的记录。不再存在Book_ID为1的书籍记录。

这种情况可能会造成混乱,我们可以通过使用级联删除来避免这种情况。
我们在创建表时通过添加某些数据库约束来实现这一点。在之前的示例中,当我们创建包含引用的表时,我们使用外键约束创建了我们的“引用”表,明确引用了书籍表的主键,将两者联系起来。
# create second Table inside DB
cursor.execute("CREATE TABLE Quotations ( \
Quote_ID INT AUTO_INCREMENT, \
Quotation VARCHAR(250), \
Books_Book_ID INT, \
PRIMARY KEY (Quote_ID), \
FOREIGN KEY (Books_Book_ID) \
REFERENCES Books(Book_ID) \
ON DELETE CASCADE \
) ENGINE=InnoDB")
“外键”关系包括ON DELETE CASCADE属性,这基本上告诉我们的 MySQL 服务器,在删除与这些外键相关的记录时,删除这个表中的相关记录。
注意
在创建表时,如果不指定ON DELETE CASCADE属性,我们既不能删除也不能更新我们的数据,因为UPDATE是DELETE后跟INSERT。
由于这种设计,不会留下孤立的记录,这正是我们想要的。
注意
在 MySQL 中,我们必须指定ENGINE=InnoDB才能使用外键。
让我们显示我们数据库中的数据。
#==========================================================
if __name__ == '__main__':
# Create class instance
mySQL = MySQL()
mySQL.showData()
这显示了我们数据库表中的以下数据:

这显示了我们有两条通过主键到外键关系相关的记录。
当我们现在删除“书籍”表中的记录时,我们期望“引用”表中的相关记录也将通过级联删除被删除。
让我们尝试通过在 Python 中执行以下 SQL 命令来执行此操作:
import mysql.connector as mysql
import GuiDBConfig as guiConf
class MySQL():
#------------------------------------------------------
def deleteRecord(self):
# connect to MySQL
conn, cursor = self.connect()
self.useGuiDB(cursor)
# execute command
cursor.execute("SELECT Book_ID FROM books WHERE Book_Title = 'Design Patterns'")
primKey = cursor.fetchall()[0][0]
# print(primKey)
cursor.execute("DELETE FROM books WHERE Book_ID = (%s)", (primKey,))
# commit transaction
conn.commit ()
# close cursor and connection
self.close(cursor, conn)
#==========================================================
if __name__ == '__main__':
# Create class instance
mySQL = MySQL()
#------------------------
mySQL.deleteRecord()
mySQL.showData()
在执行前面的删除记录命令后,我们得到了以下新结果:

注意
著名的“设计模式”已经从我们喜爱的引用数据库中消失了…
工作原理…
通过通过主键到外键关系进行级联删除,通过设计我们的数据库,我们在这个示例中触发了级联删除。
这可以保持我们的数据完整和完整。
注意
在这个示例和示例代码中,我们有时引用相同的表名,有时以大写字母开头,有时全部使用小写字母。
这适用于 MySQL 的 Windows 默认安装,但在 Linux 上可能不起作用,除非我们更改设置。
这是官方 MySQL 文档的链接:dev.mysql.com/doc/refman/5.0/en/identifier-case-sensitivity.html
在下一个示例中,我们将使用我们的 Python GUI 中的MySQL.py模块的代码。
从我们的 MySQL 数据库中存储和检索数据
我们将使用我们的 Python GUI 将数据插入到我们的 MySQL 数据库表中。我们已经重构了之前示例中构建的 GUI,以便连接和使用数据库。
我们将使用两个文本框输入小部件,可以在其中输入书名或期刊标题和页码。我们还将使用一个 ScrolledText 小部件来输入我们喜爱的书籍引用,然后将其存储在我们的 MySQL 数据库中。
准备工作
这个示例将建立在我们之前创建的 MySQL 数据库和表的基础上。
操作方法…
我们将使用我们的 Python GUI 来插入、检索和修改我们喜爱的引用。我们已经重构了我们 GUI 中的 MySQL 选项卡,为此做好了准备。

为了让按钮起作用,我们将把它们连接到回调函数,就像我们在之前的示例中所做的那样。
我们将在按钮下方的 ScrolledText 小部件中显示数据。
为了做到这一点,我们将像之前一样导入MySQL.py模块。所有与我们的 MySQL 服务器实例和数据库通信的代码都驻留在这个模块中,这是一种封装代码的形式,符合面向对象编程的精神。
我们将“插入引用”按钮连接到以下回调函数。
# Adding a Button
self.action = ttk.Button(self.mySQL, text="Insert Quote", command=self.insertQuote)
self.action.grid(column=2, row=1)
# Button callback
def insertQuote(self):
title = self.bookTitle.get()
page = self.pageNumber.get()
quote = self.quote.get(1.0, tk.END)
print(title)
print(quote)
self.mySQL.insertBooks(title, page, quote)
当我们现在运行我们的代码时,我们可以从我们的 Python GUI 中将数据插入到我们的 MySQL 数据库中。

输入书名和书页以及书籍或电影中的引用后,通过单击“插入引用”按钮将数据插入到我们的数据库中。
我们当前的设计允许标题、页面和引语。我们还可以插入我们最喜欢的电影引语。虽然电影没有页面,但我们可以使用页面列来插入引语在电影中发生的大致时间。
接下来,我们可以通过发出与之前使用的相同命令来验证所有这些数据是否已经进入了我们的数据库表。

在插入数据之后,我们可以通过单击获取引语按钮来验证它是否已经进入了我们的两个 MySQL 表中,然后显示我们插入到两个 MySQL 数据库表中的数据,如上所示。
单击获取引语按钮会调用与按钮单击事件关联的回调方法。这给了我们在我们的 ScrolledText 小部件中显示的数据。
# Adding a Button
self.action1 = ttk.Button(self.mySQL, text="Get Quotes", command=self.getQuote)
self.action1.grid(column=2, row=2)
# Button callback
def getQuote(self):
allBooks = self.mySQL.showBooks()
print(allBooks)
self.quote.insert(tk.INSERT, allBooks)
我们使用self.mySQL类实例变量来调用showBooks()方法,这是我们导入的 MySQL 类的一部分。
from B04829_Ch07_MySQL import MySQL
class OOP():
def __init__(self):
# create MySQL instance
self.mySQL = MySQL()
class MySQL():
#------------------------------------------------------
def showBooks(self):
# connect to MySQL
conn, cursor = self.connect()
self.useGuiDB(cursor)
# print results
cursor.execute("SELECT * FROM Books")
allBooks = cursor.fetchall()
print(allBooks)
# close cursor and connection
self.close(cursor, conn)
return allBooks
它是如何工作的...
在这个示例中,我们导入了包含所有连接到我们的 MySQL 数据库并知道如何插入、更新、删除和显示数据的编码逻辑的 Python 模块。
我们现在已经将我们的 Python GUI 连接到了这个 SQL 逻辑。
第八章:准备工作
注意
-
在 Python 中国际化文本字符串的最简单方法是将它们移动到一个单独的 Python 模块中,然后通过向该模块传递参数来选择在我们的 GUI 中显示的语言。
-
在本章中,我们将通过在标签、按钮、选项卡和其他小部件上显示文本来国际化我们的 GUI,使用不同的语言。
-
在不同语言中显示小部件文本
-
为国际化准备 GUI
-
如何以敏捷方式设计 GUI
-
在本章中,我们将国际化和测试我们的 Python GUI,包括以下配方:
-
设置调试监视
-
配置不同的调试输出级别
-
如何使用 Eclipse PyDev IDE 编写单元测试
-
如何做...
-
我们需要测试 GUI 代码吗?
介绍
注意
第八章。国际化和测试
我们正在将 GUI 与其显示的语言分开,这是一个面向对象的设计原则。
让我们改变我们以前的一行代码:
注意
使用单元测试创建健壮的 GUI
我们将这个新的 Python 模块导入到我们的主要 Python GUI 代码中,然后使用它。
我们将从简单开始,然后探讨如何在设计级别准备我们的 GUI 进行国际化。
让我们创建一个新的 Python 模块,并将其命名为Resources.py。接下来,让我们将我们的 GUI 标题的英文字符串移到这个模块中,然后将此模块导入到我们的 GUI 代码中。
将字符串硬编码到代码中从来都不是一个好主意,所以我们可以改进我们的代码的第一步是将所有在我们的 GUI 中可见的字符串分离到它们自己的 Python 模块中。这是国际化我们的 GUI 可见方面的开始。
一次性更改整个 GUI 语言
本地化 GUI
它是如何工作的...
在这个配方中,我们将开始通过将 Windows 标题从英语更改为另一种语言来国际化我们的 GUI。
由于“GUI”在其他语言中是相同的,我们将首先扩展该名称,以便我们可以看到我们更改的视觉效果。
根据我们传递给 I18N 类的语言,我们的 GUI 将显示为该语言。
self.win.title("Python GUI")
虽然这种方法并不是高度推荐的,但根据在线搜索结果,根据您正在开发的应用程序的具体要求,这种方法可能仍然是最实用和最快速实现的。
self.win.title("Python Graphical User Interface")
上述代码更改导致我们的 GUI 程序的以下标题:
运行上述代码会给我们带来以下国际化的结果:
这有效。
我们还将测试我们的 GUI 代码并编写单元测试,并探索单元测试在我们的开发工作中可以提供的价值,这将使我们达到重构我们的代码的最佳实践。
在本章中,我们将使用英语和德语来举例说明国际化我们的 Python GUI 的原则。
我们的新 Python 模块,包含国际化的字符串,现在看起来像这样:
由于这些单词很长,它们已经被缩写为使用单词的第一个字符,后面跟着第一个和最后一个字符之间的总字符数,然后是单词的最后一个字符。
如何做...
注意
因此,国际化变成了 I18N,本地化变成了 L10N。
我们还将本地化 GUI,这与国际化略有不同。
Class I18N():
'''Internationalization'''
def __init__(self, language):
if language == 'en': self.resourceLanguageEnglish()
elif language == 'de': self.resourceLanguageGerman()
else: raise NotImplementedError('Unsupported language.')
def resourceLanguageEnglish(self):
self.title = "Python Graphical User Interface"
def resourceLanguageGerman(self):
self.title = 'Python Grafische Benutzeroberflaeche'
使用 Python 的 main 部分创建自测试代码
from B04829_Ch08_Resources import I18N
class OOP():
def __init__(self):
self.win = tk.Tk() # Create instance
self.i18n = I18N('de') # Select language
self.win.title(self.i18n.title) # Add a title
在不同语言中显示小部件文本
到:

我们将重用之前创建的 Python GUI。我们已经注释掉了一个创建 MySQL 选项卡的 Python 代码行,因为在本章中我们不与 MySQL 数据库交谈。
我们将 GUI 中的硬编码字符串分解为它们自己的单独模块。我们通过创建一个类来实现这一点,并在类的__init__()方法中,根据传入的语言参数选择我们的 GUI 将显示哪种语言。
当我们进行国际化时,我们将在一个步骤中进行这种积极的重构和语言翻译。
我们可以通过将国际化字符串分离到单独的文件中,可能是 XML 或其他格式,进一步模块化我们的代码。我们还可以从 MySQL 数据库中读取它们。
注意
这是一种“关注点分离”的编码方法,是面向对象编程的核心。
一次性更改整个 GUI 语言
在这个示例中,我们将通过将以前硬编码的英文字符串重构到一个单独的 Python 模块中,然后国际化这些字符串,一次性更改整个 GUI 显示名称。
这个示例表明,避免硬编码 GUI 显示的任何字符串,而是将 GUI 代码与 GUI 显示的文本分开,是一个很好的设计原则。
注意
以模块化的方式设计我们的 GUI 使得国际化变得更加容易。
准备工作
我们将继续使用上一个示例中开发的 GUI。在那个示例中,我们已经国际化了 GUI 的标题。
如何做...
为了国际化在我们的 GUI 小部件中显示的文本,我们必须将所有硬编码的字符串移到一个单独的 Python 模块中,这就是我们接下来要做的。
以前,我们的 GUI 显示的单词字符串分散在我们的 Python 代码中。
这是我们的 GUI 在没有 I18N 的情况下的样子。

每个小部件的每个字符串,包括我们的 GUI 的标题,选项卡控件名称等,都是硬编码的,并与创建 GUI 的代码混在一起。
注意
在 GUI 软件开发过程的设计阶段考虑如何最好地国际化我们的 GUI 是一个好主意。
以下是我们代码的摘录。
WIDGET_LABEL = ' Widgets Frame '
class OOP():
def __init__(self):
self.win = tk.Tk() # Create instance
self.win.title("Python GUI") # Add a title
# Radiobutton callback function
def radCall(self):
radSel=self.radVar.get()
if radSel == 0: self.monty2.configure(text='Blue')
elif radSel == 1: self.monty2.configure(text='Gold')
elif radSel == 2: self.monty2.configure(text='Red')
在这个示例中,我们正在国际化我们的 GUI 小部件中显示的所有字符串。我们不会国际化输入到我们的 GUI 中的文本,因为这取决于您 PC 上的本地设置。
以下是英文国际化字符串的代码:
classI18N():
'''Internationalization'''
def __init__(self, language):
if language == 'en': self.resourceLanguageEnglish()
elif language == 'de': self.resourceLanguageGerman()
else: raiseNotImplementedError('Unsupported language.')
def resourceLanguageEnglish(self):
self.title = "Python Graphical User Interface"
self.file = "File"
self.new = "New"
self.exit = "Exit"
self.help = "Help"
self.about = "About"
self.WIDGET_LABEL = ' Widgets Frame '
self.disabled = "Disabled"
self.unChecked = "UnChecked"
self.toggle = "Toggle"
# Radiobutton list
self.colors = ["Blue", "Gold", "Red"]
self.colorsIn = ["in Blue", "in Gold", "in Red"]
self.labelsFrame = ' Labels within a Frame '
self.chooseNumber = "Choose a number:"
self.label2 = "Label 2"
self.mgrFiles = ' Manage Files '
self.browseTo = "Browse to File..."
self.copyTo = "Copy File To : "
在我们的 Python GUI 模块中,所有以前硬编码的字符串现在都被我们新的 I18N 类的实例所取代,该类位于Resources.py模块中。
以下是我们重构后的GUI.py模块的示例:
from B04829_Ch08_Resources import I18N
class OOP():
def __init__(self):
self.win = tk.Tk() # Create instance
self.i18n = I18N('de') # Select language
self.win.title(self.i18n.title) # Add a title
# Radiobutton callback function
def radCall(self):
radSel = self.radVar.get()
if radSel == 0: self.widgetFrame.configure(text=self.i18n.WIDGET_LABEL + self.i18n.colorsIn[0])
elif radSel == 1: self.widgetFrame.configure(text=self.i18n.WIDGET_LABEL + self.i18n.colorsIn[1])
elif radSel == 2: self.widgetFrame.configure(text=self.i18n.WIDGET_LABEL + self.i18n.colorsIn[2])
请注意,以前所有的硬编码的英文字符串都已被我们新的 I18N 类的实例调用所取代。
一个例子是self.win.title(self.i18n.title)。
这给了我们国际化 GUI 的能力。我们只需要使用相同的变量名,并通过传递参数来选择我们希望显示的语言。
我们也可以在 GUI 的一部分中实时更改语言,或者我们可以读取本地 PC 设置,并根据这些设置决定我们的 GUI 文本应该显示哪种语言。
现在,我们可以通过简单地填写相应的单词来实现对德语的翻译。
class I18N():
'''Internationalization'''
def __init__(self, language):
if language == 'en': self.resourceLanguageEnglish()
elif language == 'de': self.resourceLanguageGerman()
else: raise NotImplementedError('Unsupported language.')
def resourceLanguageGerman(self):
self.file = "Datei"
self.new = "Neu"
self.exit = "Schliessen"
self.help = "Hilfe"
self.about = "Ueber"
self.WIDGET_LABEL = ' Widgets Rahmen '
self.disabled = "Deaktiviert"
self.unChecked = "NichtMarkiert"
self.toggle = "Markieren"
# Radiobutton list
self.colors = ["Blau", "Gold", "Rot"]
self.colorsIn = ["in Blau", "in Gold", "in Rot"]
self.labelsFrame = ' EtikettenimRahmen '
self.chooseNumber = "WaehleeineNummer:"
self.label2 = "Etikette 2"
self.mgrFiles = ' DateienOrganisieren '
self.browseTo = "WaehleeineDatei... "
self.copyTo = "KopiereDateizu : "
在我们的 GUI 代码中,我们现在可以通过一行 Python 代码更改整个 GUI 显示语言。
class OOP():
def __init__(self):
self.win = tk.Tk() # Create instance
self.i18n = I18N('de') # Pass in language
运行上述代码会创建以下国际化 GUI:

工作原理...
为了国际化我们的 GUI,我们将硬编码的字符串重构到一个单独的模块中,然后通过将字符串作为我们的 I18N 类的初始化器的参数来使用相同的类成员来国际化我们的 GUI,从而有效地控制我们的 GUI 显示的语言。
本地化 GUI
在国际化我们的 GUI 的第一步之后,下一步是本地化。我们为什么要这样做呢?
嗯,在美利坚合众国,我们都是牛仔,我们生活在不同的时区。
因此,虽然我们在美国“国际化”,但我们的马在不同的时区醒来(并且期望根据它们自己的内部马时区时间表被喂食)。
这就是本地化的作用。
准备工作
我们正在通过本地化扩展我们在上一个示例中开发的 GUI。
如何做...
我们首先通过 pip 安装 Python pytz 时区模块。我们在命令处理器提示中输入以下命令:
**pip install pytz**
注意
在本书中,我们使用的是 Python 3.4,其中内置了pip模块。如果您使用的是较旧版本的 Python,则可能需要先安装pip模块。
成功时,我们会得到以下结果。

注意
屏幕截图显示该命令下载了.whl格式。如果您还没有这样做,您可能需要先安装 Python 的wheel模块。
这将 Python 的pytz模块安装到site-packages文件夹中,现在我们可以从 Python GUI 代码中导入这个模块。
我们可以通过运行以下代码列出所有现有的时区,在我们的ScrolledText小部件中显示时区。首先,我们向 GUI 添加一个新的Button小部件。
import pytz
class OOP():
# TZ Button callback
def allTimeZones(self):
for tz in all_timezones:
self.scr.insert(tk.INSERT, tz + '\n')
def createWidgets(self):
# Adding a TZ Button
self.allTZs = ttk.Button(self.widgetFrame,
text=self.i18n.timeZones,
command=self.allTimeZones)
self.allTZs.grid(column=0, row=9, sticky='WE')
点击我们的新Button小部件会产生以下输出:

安装了 tzlocal Python 模块后,我们可以通过运行以下代码打印我们当前的区域设置:
# TZ Local Button callback
def localZone(self):
from tzlocal import get_localzone
self.scr.insert(tk.INSERT, get_localzone())
def createWidgets(self):
# Adding local TZ Button
self.localTZ = ttk.Button(self.widgetFrame,
text=self.i18n.localZone,
command=self.localZone
self.localTZ.grid(column=1, row=9, sticky='WE')
我们已经在Resources.py中国际化了我们两个新动作Button的字符串。
英文版本:
self.timeZones = "All Time Zones"
self.localZone = "Local Zone"
德语版本:
self.timeZones = "Alle Zeitzonen"
self.localZone = "Lokale Zone"
现在点击我们的新按钮会告诉我们我们在哪个时区(嘿,我们不知道这个,是吧…)。

我们现在可以将我们的本地时间转换为不同的时区。让我们以美国东部标准时间为例。
通过改进我们现有的代码,我们在未使用的标签 2 中显示我们当前的本地时间。
import pytz
from datetime import datetime
class OOP():
# Format local US time
def getDateTime(self):
fmtStrZone = ""%Y-%m-%d %H:%M:%S""
self.lbl2.set(datetime.now().strftime(fmtStrZone))
# Place labels into the container element
ttk.Label(labelsFrame, text=self.i18n.chooseNumber).grid(column=0, row=0)
self.lbl2 = tk.StringVar()
self.lbl2.set(self.i18n.label2)
ttk.Label(labelsFrame, textvariable=self.lbl2).grid(column=0, row=1)
# Adding getTimeTZ Button
self.dt = ttk.Button(self.widgetFrame, text=self.i18n.getTime, command=self.getDateTime)
self.dt.grid(column=2, row=9, sticky='WE')
当我们运行代码时,我们国际化的标签 2(在德语中显示为Etikette 2)将显示当前的本地时间。

我们现在可以通过首先将本地时间转换为协调世界时(UTC),然后应用从导入的pytz模块中的timezone函数来将本地时间更改为美国东部标准时间。
import pytz
class OOP():
# Format local US time with TimeZone info
def getDateTime(self):
fmtStrZone = "%Y-%m-%d %H:%M:%S %Z%z"
# Get Coordinated Universal Time
utc = datetime.now(timezone('UTC'))
print(utc.strftime(fmtStrZone))
# Convert UTC datetime object to Los Angeles TimeZone
la = utc.astimezone(timezone('America/Los_Angeles'))
print(la.strftime(fmtStrZone))
# Convert UTC datetime object to New York TimeZone
ny = utc.astimezone(timezone('America/New_York'))
print(ny.strftime(fmtStrZone))
# update GUI label with NY Time and Zone
self.lbl2.set(ny.strftime(fmtStrZone))
现在点击重命名为纽约的按钮会产生以下输出:

我们的标签 2 已更新为纽约的当前时间,并且我们正在使用美国日期格式字符串将洛杉矶和纽约的 UTC 时间及其相应的时区转换打印到 Eclipse 控制台。

注意
UTC 从不观察夏令时。在东部夏令时(EDT)期间,UTC 比本地时间提前四个小时,在标准时间(EST)期间,UTC 比本地时间提前五个小时。
工作原理
为了本地化日期和时间信息,我们首先需要将我们的本地时间转换为 UTC 时间。然后,我们应用时区信息,并使用pytz Python 时区模块中的astimezone函数将时间转换为世界上任何时区的时间!
在这个示例中,我们已经将美国西海岸的本地时间转换为 UTC,然后在我们的 GUI 的标签 2 中显示了美国东海岸的时间。
为国际化准备 GUI
在这个示例中,我们将通过意识到将英语翻译成外语并不像预期的那样容易,来为我们的 GUI 准备国际化。
我们还有一个问题要解决,那就是如何正确显示来自外语的非英语 Unicode 字符。
人们可能期望 Python 3 会自动处理德语ä、ö和ü的 Unicode 变音字符,但事实并非如此。
准备工作
我们将继续使用我们在最近章节中开发的 Python GUI。首先,我们将在GUI.py的初始化代码中将默认语言更改为德语。
我们通过取消注释self.i18n = I18N('de')这一行来实现这一点。
如何做...
当我们使用变音字符将单词Ueber更改为正确的德语Űber时,Eclipse PyDev 插件并不太开心。

我们收到了一个错误消息,有点令人困惑,因为当我们在 Eclipse PyDev 控制台中运行相同的代码行时,我们得到了预期的结果。

当我们询问 Python 的默认编码时,我们得到了预期的结果,即 UTF-8。

注意
当然,我们总是可以直接表示 Unicode。
使用 Windows 内置的字符映射,我们可以找到 umlaut 字符的 Unicode 表示,大写 U 带有 umlaut 的 Unicode 是 U+00DC。

虽然这种解决方法确实很丑陋,但它起到了作用。我们可以通过传递 Unicode 的\u00DC 来正确显示这个字符,而不是输入文字字符Ü。

我们也可以接受从 Cp1252 到 UTF-8 的默认编码更改,使用 PyDev 与 Eclipse,但我们可能并不总是会得到提示去这样做。
相反,我们可能会看到显示以下错误消息:

解决这个问题的方法是将 PyDev 项目的文本文件编码属性更改为 UTF-8。

更改 PyDev 默认编码后,我们现在可以显示那些德语 umlaut 字符。我们还更新了标题,使用了正确的德语ä字符。

工作原理...
国际化和处理外语 Unicode 字符通常并不像我们希望的那样直接。有时,我们不得不找到解决方法,并通过在 Python 中使用直接表示的方式来表示 Unicode 字符,可以解决问题。
在其他时候,我们只需找到我们开发环境的设置进行调整。
如何以敏捷方式设计 GUI
现代敏捷软件开发方法的设计和编码是由软件专业人员的经验教训总结而来的。这种方法适用于 GUI 和其他任何代码。敏捷软件开发的主要关键之一是持续应用的重构过程。
重构我们的代码可以通过首先使用函数实现一些简单功能来帮助我们进行软件开发工作的一个实际例子。
随着我们的代码复杂性的增加,我们可能希望将我们的函数重构为类的方法。这种方法可以让我们删除全局变量,并且更灵活地确定在类的哪个位置放置方法。
虽然我们的代码的功能没有改变,但结构已经改变了。
在这个过程中,我们编写、测试、重构,然后再次测试。我们会在短周期内进行这些操作,通常从需要一些功能的最小代码开始。
注意
测试驱动的软件开发是敏捷开发方法论的一种特定风格。
虽然我们的 GUI 运行得很好,但我们的主要GUI.py代码的复杂性不断增加,开始变得有点难以维护。
这意味着我们需要重构我们的代码。
准备工作
我们将重构之前章节中创建的 GUI。我们将使用 GUI 的英文版本。
如何做...
在之前的配方中,我们已经将 GUI 显示的所有名称都国际化了。这是重构我们的代码的一个很好的开始。
注意
重构是改进现有代码的结构、可读性和可维护性的过程。我们不会添加新功能。
在之前的章节和配方中,我们一直在以“自上而下”的瀑布式开发方法扩展我们的 GUI,向现有代码的顶部添加import,并向底部添加代码。
虽然在查看代码时这很有用,但现在看起来有点凌乱,我们可以改进这一点,以帮助我们未来的开发。
让我们首先清理我们的import语句部分,目前看起来是这样的:
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu
from tkinter import Spinbox
import B04829_Ch08_ToolTip as tt
from threading import Thread
from time import sleep
from queue import Queue
from tkinter import filedialog as fd
from os import path
from tkinter import messagebox as mBox
from B04829_Ch08_MySQL import MySQL
from B04829_Ch08_Resources import I18N
from datetime import datetime
from pytz import all_timezones, timezone
# Module level GLOBALS
GLOBAL_CONST = 42
通过简单地分组相关的导入,我们可以减少代码行数,从而提高导入的可读性,使其看起来不那么令人生畏。
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk, scrolledtext, Menu, Spinbox, filedialog as fd, messagebox as mBox
from queue import Queue
from os import path
import B04829_Ch08_ToolTip as tt
from B04829_Ch08_MySQL import MySQL
from B04829_Ch08_Resources import I18N
from B04829_Ch08_Callbacks_Refactored import Callbacks
from B04829_Ch08_Logger import Logger, LogLevel
# Module level GLOBALS
GLOBAL_CONST = 42
我们可以通过将回调方法分解成它们自己的模块来进一步重构我们的代码。这通过将不同的导入语句分离到它们所需的模块中来提高可读性。
让我们将我们的GUI.py重命名为GUI_Refactored.py,并创建一个新的模块,我们将其命名为Callbacks_Refactored.py。
这给了我们这种新的架构。
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk, scrolledtext, Menu, Spinbox, \
filedialog as fd, messagebox as mBox
from queue import Queue
from os import path
import B04829_Ch08_ToolTip as tt
from B04829_Ch08_MySQL import MySQL
from B04829_Ch08_Resources import I18N
from B04829_Ch08_Callbacks_Refactored import Callbacks
# Module level GLOBALS
GLOBAL_CONST = 42
class OOP():
def __init__(self):
# Callback methods now in different module
self.callBacks = Callbacks(self)
注意我们在调用Callbacks初始化程序时是如何传入我们自己的 GUI 类实例(self)的。
我们的新Callbacks类如下:
#======================
# imports
#======================
import tkinter as tk
from time import sleep
from threading import Thread
from pytz import all_timezones, timezone
from datetime import datetime
class Callbacks():
def __init__(self, oop):
self.oop = oop
def defaultFileEntries(self):
self.oop.fileEntry.delete(0, tk.END)
self.oop.fileEntry.insert(0, 'Z:\\') # bogus path
self.oop.fileEntry.config(state='readonly')
self.oop.netwEntry.delete(0, tk.END)
self.oop.netwEntry.insert(0, 'Z:\\Backup') # bogus path
# Combobox callback
def _combo(self, val=0):
value = self.oop.combo.get()
self.oop.scr.insert(tk.INSERT, value + '\n')
在我们新类的初始化程序中,传入的 GUI 实例被保存在名为self.oop的名称下,并在这个新的 Python 类模块中使用。
运行重构后的 GUI 代码仍然有效。我们只是增加了代码的可读性,并减少了代码的复杂性,为进一步的开发工作做准备。
它是如何工作的...
我们首先通过分组相关的导入语句来提高代码的可读性。接下来,我们将回调方法分解成它们自己的类和模块,以进一步减少代码的复杂性。
我们已经采用了相同的面向对象编程方法,通过将ToolTip类驻留在自己的模块中,并在先前的示例中国际化了所有 GUI 字符串。
在这个示例中,我们通过将我们自己的实例传递给 GUI 依赖的回调方法类,进一步进行了重构。
注意
现在我们更好地理解了模块化软件开发的价值,我们很可能会在未来的软件设计中采用这种方法。
我们需要测试 GUI 代码吗?
在编码阶段以及发布服务包或修复错误时,测试我们的软件是一项重要的活动。
有不同级别的测试。第一级是开发人员测试,通常从编译器或解释器不让我们运行有错误的代码开始,迫使我们在单个方法的级别上测试我们的代码的小部分。
这是第一层防御。
第二层防御性编码是当我们的源代码控制系统告诉我们有一些冲突需要解决,并且不让我们提交修改后的代码。
当我们在开发团队中进行专业工作时,这是非常有用且绝对必要的。源代码控制系统是我们的朋友,它指出了已经提交到特定分支或最新版本的更改,无论是我们自己提交的还是其他开发人员提交的,并告诉我们我们的本地代码版本已经过时,并且存在一些需要在提交代码到存储库之前解决的冲突。
这部分假设您使用源代码控制系统来管理和存储您的代码。示例包括 git、mercurial、svn 和其他几种。Git 是一个非常流行的源代码控制系统,对于单个用户是免费的。
第三级是 API 级别,我们通过只允许通过已发布的接口与我们的代码进行交互来封装对我们代码的潜在未来更改。
注意
请参考《面向接口编程,而不是实现》,设计模式,第 17 页。
另一种测试级别是集成测试,当我们最终构建的一半桥梁与其他开发团队创建的另一半桥梁相遇时,两者高度不一致(比如,一半比另一半高出两米或码...)。
然后,有最终用户测试。虽然我们构建了他们指定的内容,但实际上并不是他们想要的。
噢,好吧...我想所有前面的例子都是我们需要在设计和实施阶段都测试我们的代码的有效原因。
准备工作
我们将测试我们在最近的示例和章节中创建的 GUI。我们还将展示一些简单的例子,说明可能出现的问题以及为什么我们需要不断测试我们的代码和通过 API 调用的代码。
如何做...
虽然许多经验丰富的开发人员在调试时会在他们的代码中撒上printf()语句,但 21 世纪的许多开发人员习惯于现代 IDE 开发环境,这些环境可以有效地加快开发时间。
在本书中,我们使用 Eclipse IDE 的 PyDev Python 插件。
如果您刚开始使用像 Eclipse 这样的 IDE,并安装了 PyDev 插件,一开始可能会有点不知所措。Python 3 附带的 Python IDLE 工具也有一个更简单的调试器,您可能希望先探索一下。
每当我们的代码出现问题时,我们都必须进行调试。这样做的第一步是设置断点,然后逐行或逐个方法地执行我们的代码。
在我们的代码中进出是日常活动,直到代码顺利运行。
在 Python GUI 编程中,出错的第一件事可能是遗漏导入所需的模块或导入现有模块。
这里有一个简单的例子:

我们试图创建一个 tkinter 类的实例,但事情并不如预期那样运行。
好吧,我们只是忘记导入模块,我们可以通过在我们的类创建之前添加一行 Python 代码来修复这个问题,导入语句就在那里。
#======================
# imports
#======================
import tkinter as tk
这是一个例子,我们的开发环境为我们进行测试。我们只需要进行调试和修复代码。
另一个与开发人员测试更相关的例子是,当我们编写条件语句时,在常规开发过程中没有执行所有逻辑分支。
使用上一章的一个例子,假设我们点击获取报价按钮,这个操作成功了,但我们从未点击修改报价按钮。第一次按钮点击会创建期望的结果,但第二次会抛出异常(因为我们尚未实现此代码,可能已经完全忘记了)。

单击修改报价按钮会创建以下结果:

另一个潜在的错误区域是当函数或方法突然不再返回预期的结果。假设我们正在调用以下函数,它返回了预期的结果。

然后,有人犯了一个错误,我们不再得到以前的结果。

我们不是在进行乘法,而是以传入数字的幂进行计算,结果不再是以前的样子了。
注意
在软件测试中,这种错误被称为回归。
它是如何工作的...
在这个示例中,我们强调了在软件开发生命周期的几个阶段进行软件测试的重要性,通过展示代码可能出错并引入软件缺陷(也称为错误)的几个例子。
设置调试监视
在现代的集成开发环境(IDE)中,如 Eclipse 中的 PyDev 插件或其他 IDE(如 NetBeans),我们可以设置调试监视来监视我们的 GUI 在代码执行过程中的状态。
这与 Visual Studio 和更近期的 Visual Studio.NET 的 Microsoft IDE 非常相似。
注意
设置调试监视是帮助我们开发工作的一种非常方便的方式。
准备工作
在这个示例中,我们将重用之前开发的 Python GUI。我们正在逐步执行我们之前开发的代码并设置调试监视。
如何做...
注意
虽然这个示例适用于基于 Java 的 Eclipse IDE 中的 PyDev 插件,但其原则也适用于许多现代 IDE。
我们可能希望设置断点的第一个位置是在我们通过调用 tkinter 主事件循环使我们的 GUI 可见的地方。
PyDev/Eclipse 中左侧的绿色气球符号是一个断点。当我们以调试模式执行我们的代码时,一旦执行到达断点,代码的执行将停止。此时,我们可以看到当前作用域内的所有变量的值。我们还可以在调试器窗口中输入表达式,执行它们,显示结果。如果结果是我们想要的,我们可能决定使用我们刚学到的知识更改我们的代码。
我们通常通过单击 IDE 工具栏中的图标或使用键盘快捷键(例如按下F5进入代码,F6跳过,F7跳出当前方法)来逐步执行代码。

在我们放置断点并进入此代码时,出现了问题,因为我们最终进入了一些我们现在不希望调试的低级 tkinter 代码。我们通过单击 Step-Out 工具栏图标(该图标位于项目菜单下方的第三个黄色箭头)或按下F7(假设我们在 Eclipse 中使用 PyDev)来退出低级 tkinter 代码。
我们通过单击截图右侧的 bug 工具栏图标开始调试会话。如果我们在不调试的情况下执行,我们会单击绿色圆圈内部有白色三角形的图标,该图标位于 bug 图标右侧。

更好的做法是将断点放置在我们自己的代码附近,以便观察一些我们自己的 Python 变量的值。
在现代 GUI 的事件驱动世界中,我们必须将断点放置在在事件期间被调用的代码上,例如按钮单击。
目前,我们的一个主要功能位于按钮单击事件中。当我们单击标记为New York的按钮时,我们创建一个事件,然后在我们的 GUI 中发生某些事情。
让我们在New York按钮回调方法上放置一个断点,我们将其命名为getDateTime()。
当我们现在运行调试会话时,我们将在断点处停止,然后我们可以启用作用域内的变量观察。
在 Eclipse 中使用 PyDev,我们可以右键单击一个变量,然后从弹出菜单中选择观察命令。变量的名称、类型和当前值将显示在下一个截图中显示的表达式调试窗口中。我们也可以直接在表达式窗口中输入。
我们观察的变量不仅限于简单的数据类型。我们可以观察类实例、列表、字典等。
在观察这些更复杂的对象时,我们可以在表达式窗口中展开它们,并深入了解类实例、字典等所有值。
我们通过点击出现在每个变量名称列最左边的观察变量左侧的三角形来实现这一点。

虽然我们正在打印出不同时区位置的值,但从长远来看,设置调试观察更方便、更高效。我们不必用老式的 C 风格的printf()语句来使我们的代码混乱。
注意
如果您有兴趣学习如何为 Python 安装 Eclipse 和 PyDev 插件,有一个很好的教程可以帮助您开始安装所有必要的免费软件,然后通过创建一个简单的、可工作的 Python 程序来介绍您 PyDev 在 Eclipse 中的使用。www.vogella.com/tutorials/Python/article.html
工作原理...
我们在 21 世纪使用现代集成开发环境(IDE),这些 IDE 是免费提供的,可以帮助我们创建稳健的代码。
本文介绍了如何设置调试观察,这是每个开发人员技能中的基本工具。即使在不追踪错误时,逐步执行我们自己的代码可以确保我们理解我们的代码,并可能通过重构改进我们的代码。
以下是我读过的第一本编程书籍《Java 编程思想》中的一句话,作者是 Bruce Eckel。
| "抵制急躁的冲动,它只会减慢你的速度。" | ||
|---|---|---|
| --Bruce Eckel |
将近 20 年后,这些建议经受住了时间的考验。
注意
调试观察有助于我们创建可靠的代码,不是浪费时间。
配置不同的调试输出级别
在本示例中,我们将配置不同的调试级别,可以在运行时选择和更改。这使我们能够控制在调试代码时要深入到代码中的程度。
我们将创建两个新的 Python 类,并将它们放入同一个模块中。
我们将使用四种不同的日志级别,并将我们的调试输出写入我们将创建的日志文件中。如果日志文件夹不存在,我们也将自动创建它。
日志文件的名称是执行脚本的名称,即我们重构的GUI.py。我们还可以通过将完整路径传递给我们的记录器类的初始化程序来选择其他日志文件的名称。
准备工作
我们将继续使用上一个示例中的重构的GUI.py代码。
如何做...
首先,我们创建一个新的 Python 模块,将两个新的类放入其中。第一个类非常简单,定义了日志级别。这基本上是一个枚举。
class LogLevel:
'''Define logging levels.'''
OFF = 0
MINIMUM = 1
NORMAL = 2
DEBUG = 3
第二个类通过使用传入的文件名的完整路径创建一个日志文件,并将其放入logs文件夹中。在第一次运行时,logs文件夹可能不存在,因此代码会自动创建该文件夹。
class Logger:
''' Create a test log and write to it. '''
#-------------------------------------------------------
def __init__(self, fullTestName, loglevel=LogLevel.DEBUG):
testName = os.path.splitext(os.path.basename(fullTestName))[0]
logName = testName + '.log'
logsFolder = 'logs'
if not os.path.exists(logsFolder):
os.makedirs(logsFolder, exist_ok = True)
self.log = os.path.join(logsFolder, logName)
self.createLog()
self.loggingLevel = loglevel
self.startTime = time.perf_counter()
#------------------------------------------------------
def createLog(self):
with open(self.log, mode='w', encoding='utf-8') as logFile:
logFile.write(self.getDateTime() +
'\t\t*** Starting Test ***\n')
logFile.close()
为了写入我们的日志文件,我们使用writeToLog()方法。在方法内部,我们首先检查消息是否具有高于我们设置的期望日志输出的限制级别。如果消息级别较低,我们将丢弃它并立即从方法中返回。
如果消息具有我们想要显示的日志级别,那么我们检查它是否以换行符开头,如果是,我们通过使用 Python 的切片运算符(msg = msg[1:])来丢弃换行符。
然后,我们将一行写入我们的日志文件,其中包括当前日期时间戳、两个制表符、我们的消息,并以换行符结尾。
def writeToLog(self, msg='', loglevel=LogLevel.DEBUG):
# control how much gets logged
if loglevel > self.loggingLevel:
return
# open log file in append mode
with open(self.log, mode='a', encoding='utf-8') as logFile:
msg = str(msg)
if msg.startswith('\n'):
msg = msg[1:]
logFile.write(self.getDateTime() + '\t\t' + msg + '\n')
logFile.close()
现在我们可以导入我们的新 Python 模块,并在 GUI 代码的__init__部分中创建Logger类的实例。
from os import path
from B04829_Ch08_Logger import Logger
class OOP():
def __init__(self):
# create Logger instance
fullPath = path.realpath(__file__)
self.log = Logger(fullPath)
print(self.log)
我们通过path.realpath(__file__)获取正在运行的 GUI 脚本的完整路径,并将其传递给Logger类的初始化程序。如果logs文件夹不存在,我们的 Python 代码将自动创建它。
这会产生以下结果:

上述截图显示我们创建了一个新的Logger类的实例,下面的截图显示logs文件夹和日志都已创建。

当我们打开日志时,我们可以看到当前日期和时间以及默认字符串已被写入日志。

它是如何工作的...
在本示例中,我们创建了自己的日志记录类。虽然 Python 附带了一个日志记录模块,但很容易创建我们自己的日志记录类,这使我们对日志格式有绝对控制。当我们将自己的日志输出与我们在上一章中探索的 MS Excel 或 Matplotlib 结合使用时,这非常有用。
在下一个示例中,我们将使用 Python 内置的__main__功能来使用我们刚刚创建的四个不同的日志级别。
使用 Python 的 main 部分创建自测试代码
Python 具有一个非常好的功能,可以使每个模块进行自我测试。利用这个功能是确保我们的代码更改不会破坏现有代码的一个很好的方法,此外,__main__自测试部分还可以作为每个模块工作方式的文档。
注意
几个月或几年后,我们有时会忘记我们的代码在做什么,因此在代码本身中写下解释确实是一个很大的帮助。
在可能的情况下,为每个 Python 模块始终添加一个自测试部分是一个好主意。有时不可能,但在大多数模块中是可能的。
准备工作
我们将扩展上一个配方,因此,为了理解本配方中的代码在做什么,我们必须先阅读并理解上一个配方中的代码。
如何做...
首先,我们将通过向我们的Resources.py模块添加这个自测试部分来探索 Python__main__自测试部分的功能。每当我们运行一个具有此自测试部分位于模块底部的模块时,当模块单独执行时,此代码将运行。
当模块被导入并从其他模块中使用时,__main__自测试部分中的代码将不会被执行。
这是在随后的屏幕截图中显示的代码:
if __name__ == '__main__':
language = 'en'
inst = I18N(language)
print(inst.title)
language = 'de'
inst = I18N(language)
print(inst.title)
添加了自测试部分后,我们现在可以单独运行此模块,并且它会创建有用的输出,同时还会显示我们的代码按预期工作。

我们首先传入英语作为要在我们的 GUI 中显示的语言,然后传入德语作为我们的 GUI 将显示的语言。
我们打印出我们的 GUI 的标题,以显示我们的 Python 模块按我们的意图工作。
注意
下一步是使用我们在上一个配方中创建的日志功能。
我们首先通过向我们重构的GUI.py模块添加一个__main__自测试部分,然后验证我们创建了Logger类的实例。

接下来,我们使用所示的命令写入我们的日志文件。我们已经设计了我们的日志级别默认记录每条消息,这是 DEBUG 级别,因此我们不必更改任何内容。我们只需将要记录到writeToLog方法的消息传入。
if __name__ == '__main__':
#======================
# Start GUI
#======================
oop = OOP()
print(oop.log)
oop.log.writeToLog('Test message')
oop.win.mainloop()
这被写入我们的日志文件,如下面日志的屏幕截图所示:

现在我们可以通过向我们的日志语句添加日志级别并设置我们希望输出的级别来控制日志记录。让我们将这种能力添加到Callbacks.py模块中的getDateTime方法,这是New York按钮回调方法。
我们使用不同的调试级别将先前的print语句更改为log语句。
在GUI.py中,我们从我们的日志模块导入了两个新类。
from B04829_Ch08_Logger import Logger, LogLevel
接下来,我们创建这些类的本地实例。
# create Logger instance
fullPath = path.realpath(__file__)
self.log = Logger(fullPath)
# create Log Level instance
self.level = LogLevel()
由于我们将 GUI 类的一个实例传递给Callbacks.py初始化程序,因此我们可以根据我们创建的LogLevel类对日志级别进行约束。
# Format local US time with TimeZone info
def getDateTime(self):
fmtStrZone = "%Y-%m-%d %H:%M:%S %Z%z"
# Get Coordinated Universal Time
utc = datetime.now(timezone('UTC'))
self.oop.log.writeToLog(utc.strftime(fmtStrZone),
self.oop.level.MINIMUM)
# Convert UTC datetime object to Los Angeles TimeZone
la = utc.astimezone(timezone('America/Los_Angeles'))
self.oop.log.writeToLog(la.strftime(fmtStrZone),
self.oop.level.NORMAL)
# Convert UTC datetime object to New York TimeZone
ny = utc.astimezone(timezone('America/New_York'))
self.oop.log.writeToLog(ny.strftime(fmtStrZone),
self.oop.level.DEBUG)
# update GUI label with NY Time and Zone
self.oop.lbl2.set(ny.strftime(fmtStrZone))
当我们现在点击我们的纽约按钮时,根据所选的日志级别,我们会得到不同的输出写入我们的日志文件。默认的日志级别是DEBUG,这意味着一切都会被写入我们的日志。

当我们更改日志级别时,我们控制写入我们的日志的内容。我们通过调用Logger类的setLoggingLevel方法来实现这一点。
#----------------------------------------------------------------
def setLoggingLevel(self, level):
'''change logging level in the middle of a test.'''
self.loggingLevel = level
在我们的 GUI 的__main__部分中,我们将日志级别更改为MINIMUM,这将导致减少写入我们的日志文件的输出。
if __name__ == '__main__':
#======================
# Start GUI
#======================
oop = OOP()
oop.log.setLoggingLevel(oop.level.MINIMUM)
oop.log.writeToLog('Test message')
oop.win.mainloop()
现在,我们的日志文件不再显示Test Message,只显示符合设置的日志级别的消息。

工作原理...
在这个配方中,我们充分利用了 Python 内置的__main__自测试部分。我们引入了自己的日志文件,同时也介绍了如何创建不同的日志级别。
通过这样做,我们可以完全控制写入日志文件的内容。
使用单元测试创建健壮的 GUI
Python 自带了一个内置的单元测试框架,在这个示例中,我们将开始使用这个框架来测试我们的 Python GUI 代码。
在我们开始编写单元测试之前,我们想要设计我们的测试策略。我们可以很容易地将单元测试与它们测试的代码混合在一起,但更好的策略是将应用程序代码与单元测试代码分开。
注意
PyUnit 是根据所有其他 xUnit 测试框架的原则设计的。
准备工作
我们将测试本章前面创建的国际化 GUI。
如何做...
为了使用 Python 的内置单元测试框架,我们必须导入 Python 的unittest模块。让我们创建一个新模块,命名为UnitTests.py。
我们首先导入unittest模块,然后创建我们自己的类,在这个类中,我们继承并扩展unittest.TestCase类。
做到这一点的最简单的代码如下:
import unittest
class GuiUnitTests(unittest.TestCase):
pass
if __name__ == '__main__':
unittest.main()
这段代码还没有做太多事情,但当我们运行它时,我们没有得到任何错误,这是一个好迹象。

实际上,我们确实会在控制台上得到一个输出,说明我们成功地运行了零个测试...
嗯,这个输出有点误导,因为到目前为止我们只是创建了一个不包含任何实际测试方法的类。
我们添加了实际进行单元测试的测试方法,按照所有测试方法的默认命名以“test”开头。这是一个可以更改的选项,但坚持这种命名约定似乎更容易和更清晰。
让我们添加一个测试方法,测试我们的 GUI 的标题。这将验证通过传入预期的参数,我们得到了预期的结果。
import unittest
from B04829_Ch08_Resources import I18N
class GuiUnitTests(unittest.TestCase):
def test_TitleIsEnglish(self):
i18n = I18N('en')
self.assertEqual(i18n.title,
"Python Graphical User Interface")
我们从我们的Resources.py模块中导入我们的I18N类,将英语作为要在我们的 GUI 中显示的语言传入。由于这是我们的第一个单元测试,我们也打印出了标题的结果,只是为了确保我们知道我们得到了什么。接下来,我们使用unittest assertEqual方法来验证我们的标题是否正确。
运行这段代码会得到一个OK,这意味着单元测试通过了。

单元测试运行并成功,这由一个点和单词“OK”表示。如果它失败或出现错误,我们将不会得到点,而是得到“F”或“E”作为输出。
现在我们可以通过验证 GUI 的德语版本的标题来进行相同的自动化单元测试检查。
我们只需复制,粘贴和修改我们的代码。
import unittest
from B04829_Ch08_Resources import I18N
class GuiUnitTests(unittest.TestCase):
def test_TitleIsEnglish(self):
i18n = I18N('en')
self.assertEqual(i18n.title,
"Python Graphical User Interface")
def test_TitleIsGerman(self):
i18n = I18N('en')
self.assertEqual(i18n.title,
'Python Grafische Benutzeroberfl'
+ "\u00E4" + 'che')
现在我们正在测试我们国际化的 GUI 标题,使用两种语言,并在运行代码时得到以下结果:

我们运行了两个单元测试,但是,我们没有得到一个 OK,而是得到了一个失败。发生了什么?
我们的德语版本 GUI 的assertion失败了...
在调试我们的代码时,结果表明在复制,粘贴和修改我们的单元测试代码时,我们忘记了将德语作为语言传入。我们可以很容易地修复这个问题。
def test_TitleIsGerman(self):
# i18n = I18N('en') # <= Bug in Unit Test
i18n = I18N('de')
self.assertEqual(i18n.title,
'Python Grafische Benutzeroberfl'
+ "\u00E4" + 'che')
当我们重新运行我们的单元测试时,我们再次得到了所有测试都通过的预期结果。

注意
单元测试代码也是代码,也可能存在 bug。
虽然编写单元测试的目的是真正测试我们的应用程序代码,但我们必须确保我们的测试写得正确。来自测试驱动开发(TDD)方法论的一种方法可能会帮助我们。
注意
在 TDD 中,我们在实际编写应用程序代码之前先编写单元测试。现在,如果一个方法甚至不存在的测试通过了,那就有问题。下一步是创建不存在的方法,并确保它会失败。之后,我们可以编写最少量的代码来使单元测试通过。
工作原理...
在本篇中,我们已经开始测试我们的 Python GUI,编写 Python 单元测试。我们已经看到 Python 单元测试代码只是代码,可能包含需要纠正的错误。在下一篇中,我们将扩展本篇的代码,并使用随 Eclipse IDE 附带的 PyDev 插件的图形单元测试运行器。
如何使用 Eclipse PyDev IDE 编写单元测试
在上一篇中,我们开始使用 Python 的单元测试功能,而在本篇中,我们将进一步使用这一功能来确保我们的 GUI 代码的质量。
我们将对我们的 GUI 进行单元测试,以确保我们的 GUI 显示的国际化字符串符合预期。
在上一篇中,我们在单元测试代码中遇到了一些错误,但通常,我们的单元测试将发现由修改现有应用程序代码而引起的回归错误,而不是单元测试代码。一旦我们验证了我们的单元测试代码是正确的,通常不会再更改它。
注意
我们的单元测试也作为我们期望代码执行的文档。
默认情况下,Python 的单元测试是使用文本单元测试运行器执行的,我们可以在 Eclipse IDE 的 PyDev 插件中运行它。我们也可以从控制台窗口运行完全相同的单元测试。
除了本篇中的文本运行器,我们还将探讨 PyDev 的图形单元测试功能,该功能可以从 Eclipse IDE 内部使用。
准备就绪
我们正在扩展之前的配方,其中我们开始使用 Python 单元测试。
操作步骤...
Python 单元测试框架配备了所谓的装置。
请参考以下网址了解测试装置的描述:
这意味着我们可以创建setup()和teardown()单元测试方法,以便在执行任何单个测试之前调用setup()方法,并且在每个单元测试结束时调用teardown()方法。
注意
这种装置功能为我们提供了一个非常受控的环境,我们可以在其中运行我们的单元测试。这类似于使用前置条件和后置条件。
让我们设置我们的单元测试环境。我们将创建一个新的测试类,重点关注前面提到的代码正确性。
注意
unittest.main()运行任何以前缀"test"开头的方法,无论我们在给定的 Python 模块中创建了多少个类。
import unittest
from B04829_Ch08_Resources import I18N
from B04829_Ch08_GUI_Refactored import OOP as GUI
class GuiUnitTests(unittest.TestCase):
def test_TitleIsEnglish(self):
i18n = I18N('en')
self.assertEqual(i18n.title,
"Python Graphical User Interface")
def test_TitleIsGerman(self):
# i18n = I18N('en') # <= Bug in Unit Test
i18n = I18N('de')
self.assertEqual(i18n.title,
'Python Grafische Benutzeroberfl'
+ "\u00E4" + 'che')
class WidgetsTestsEnglish(unittest.TestCase):
def setUp(self):
self.gui = GUI('en')
def tearDown(self):
self.gui = None
def test_WidgetLabels(self):
self.assertEqual(self.gui.i18n.file, "File")
self.assertEqual(self.gui.i18n.mgrFiles, ' Manage Files ')
self.assertEqual(self.gui.i18n.browseTo,
"Browse to File...")
if __name__ == '__main__':
unittest.main()
这将产生以下输出:

前面的单元测试代码表明,我们可以创建几个单元测试类,并且可以通过调用unittest.main在同一个模块中运行它们。
它还显示setup()方法在单元测试报告的输出中不算作测试(测试数量为 3),同时,它也完成了其预期的工作,因为我们现在可以从单元测试方法内部访问我们的类实例变量self.gui。
我们有兴趣测试所有标签的正确性,特别是在我们更改代码时捕捉错误。
如果我们从应用程序代码复制并粘贴字符串到测试代码中,它将在单元测试框架按钮的点击下捕捉到任何意外更改。
我们还希望测试在任何语言中调用任何Radiobutton小部件都会导致labelframe小部件的text被更新。为了自动测试这一点,我们必须做两件事。
首先,我们必须检索labelframe text小部件的值,并将该值分配给一个名为labelFrameText的变量。我们必须使用以下语法,因为该小部件的属性是通过字典数据类型传递和检索的:
self.gui.widgetFrame['text']
现在我们可以验证默认文本,然后在以编程方式单击一个 Radiobutton 小部件后,验证国际化版本。
class WidgetsTestsGerman(unittest.TestCase):
def setUp(self):
self.gui = GUI('de')
def test_WidgetLabels(self):
self.assertEqual(self.gui.i18n.file, "Datei")
self.assertEqual(self.gui.i18n.mgrFiles,
' Dateien Organisieren ')
self.assertEqual(self.gui.i18n.browseTo,
"Waehle eine Datei... ")
def test_LabelFrameText(self):
labelFrameText = self.gui.widgetFrame['text']
self.assertEqual(labelFrameText, " Widgets Rahmen ")
self.gui.radVar.set(1)
self.gui.callBacks.radCall()
labelFrameText = self.gui.widgetFrame['text']
self.assertEqual(labelFrameText,
" Widgets Rahmen in Gold")
在验证默认的labelFrameText之后,我们以编程方式将单选按钮设置为索引 1,然后以编程方式调用单选按钮的回调方法。
self.gui.radVar.set(1)
self.gui.callBacks.radCall()
注意
这基本上与在 GUI 中单击单选按钮相同,但我们是通过代码在单元测试中进行按钮单击事件。
然后,我们验证labelframe小部件中的文本是否按预期更改了。
当我们在 Eclipse 中使用 Python PyDev 插件运行单元测试时,我们会得到以下输出写入 Eclipse 控制台。

从命令提示符运行时,一旦我们导航到当前代码所在的文件夹,我们会得到类似的输出。

使用 Eclipse,我们还可以选择运行我们的单元测试,不是作为简单的 Python 脚本,而是作为 Python 单元测试脚本,这样我们就可以得到一些丰富多彩的输出,而不是旧的 DOS 提示符的黑白世界。

单元测试结果栏是绿色的,这意味着我们所有的单元测试都通过了。前面的截图还显示,GUI 测试运行器比文本测试运行器慢得多:在 Eclipse 中为 1.01 秒,而文本测试运行器为 0.466 秒。
工作原理...
我们通过测试labels来扩展我们的单元测试代码,通过编程调用Radiobutton,然后在单元测试中验证labelframe小部件的相应text属性是否按预期更改。我们已经测试了两种不同的语言。
然后,我们开始使用内置的 Eclipse/PyDev 图形化单元测试运行器。
第九章:使用 wxPython 库扩展我们的 GUI
在本章中,我们将使用 wxPython 库增强我们的 Python GUI。
-
如何安装 wxPython 库
-
如何在 wxPython 中创建我们的 GUI
-
使用 wxPython 快速添加控件
-
尝试在主 tkinter 应用程序中嵌入主 wxPython 应用程序
-
尝试将我们的 tkinter GUI 代码嵌入到 wxPython 中
-
如何使用 Python 控制两个不同的 GUI 框架
-
如何在两个连接的 GUI 之间通信
介绍
在本章中,我们将介绍另一个 Python GUI 工具包,它目前不随 Python 一起发布。它被称为 wxPython。
这个库有两个版本。原始版本称为 Classic,而最新版本称为开发项目的代号 Phoenix。
在本书中,我们仅使用 Python 3 进行编程,因为新的 Phoenix 项目旨在支持 Python 3,这就是我们在本章中使用的 wxPython 版本。
首先,我们将创建一个简单的 wxPython GUI,然后我们将尝试将我们在本书中开发的基于 tkinter 的 GUI 与新的 wxPython 库连接起来。
注意
wxPython 是 Python 绑定到 wxWidgets 的库。
wxPython 中的 w 代表 Windows 操作系统,x 代表 Unix 操作系统,如 Linux 和 OS X。
如果同时使用这两个 GUI 工具包出现问题,我们将尝试使用 Python 解决任何问题,然后我们将使用 Python 内的进程间通信(IPC)来确保我们的 Python 代码按我们希望的方式工作。
如何安装 wxPython 库
wxPython 库不随 Python 一起发布,因此,为了使用它,我们首先必须安装它。
这个步骤将向我们展示在哪里以及如何找到正确的版本来安装,以匹配已安装的 Python 版本和正在运行的操作系统。
注意
wxPython 第三方库已经存在了 17 年多,这表明它是一个强大的库。
准备工作
为了在 Python 3 中使用 wxPython,我们必须安装 wxPython Phoenix 版本。
如何做...
在网上搜索 wxPython 时,我们可能会在www.wxpython.org找到官方网站。

如果我们点击 MS Windows 的下载链接,我们可以看到几个 Windows 安装程序,所有这些安装程序都仅适用于 Python 2.x。

使用 Python 3 和 wxPython,我们必须安装 wxPython/Phoenix 库。我们可以在快照构建链接中找到安装程序:
wxpython.org/Phoenix/snapshot-builds/
从这里,我们可以选择与我们的 Python 版本和操作系统版本匹配的 wxPython/Phoenix 版本。我正在使用运行在 64 位 Windows 7 操作系统上的 Python 3.4。

Python wheel(.whl)安装程序包有一个编号方案。
对我们来说,这个方案最重要的部分是我们正在安装的 wxPython/Phoenix 版本是为 Python 3.4(安装程序名称中的 cp34)和 Windows 64 位操作系统(安装程序名称中的 win_amd64)。

成功下载 wxPython/Phoenix 包后,我们现在可以转到该包所在的目录,并使用 pip 安装此包。

我们的 Pythonsite-packages文件夹中有一个名为wx的新文件夹。

注意
wx是 wxPython/Phoenix 库安装的文件夹名称。我们将在 Python 代码中导入此模块。
我们可以通过执行来自官方 wxPython/Phoenix 网站的简单演示脚本来验证我们的安装是否成功。官方网站的链接是wxpython.org/Phoenix/docs/html/。
import wx
app = wx.App()
frame = wx.Frame(None, -1, "Hello World")
frame.Show()
app.MainLoop()
运行上述 Python 3 脚本将使用 wxPython/Phoenix 创建以下 GUI。

工作原理...
在这个食谱中,我们成功安装了与 Python 3 兼容的正确版本的 wxPython 工具包。我们找到了这个 GUI 工具包的 Phoenix 项目,这是当前和活跃的开发线。Phoenix 将在未来取代 Classic wxPython 工具包,特别适用于与 Python 3 良好地配合使用。
成功安装了 wxPython/Phoenix 工具包后,我们只用了五行代码就创建了一个 GUI。
注意
我们之前使用 tkinter 实现了相同的结果。
如何在 wxPython 中创建我们的 GUI
在这个食谱中,我们将开始使用 wxPython GUI 工具包创建我们的 Python GUI。
我们将首先使用随 Python 一起提供的 tkinter 重新创建我们之前创建的几个小部件。
然后,我们将探索一些使用 tkinter 更难创建的 wxPython GUI 工具包提供的小部件。
准备工作
前面的食谱向您展示了如何安装与您的 Python 版本和操作系统匹配的正确版本的 wxPython。
如何做...
开始探索 wxPython GUI 工具包的一个好地方是访问以下网址:wxpython.org/Phoenix/docs/html/gallery.html
这个网页显示了许多 wxPython 小部件。点击任何一个小部件,我们会进入它们的文档,这是一个非常好的和有用的功能,可以快速了解 wxPython 控件。

以下屏幕截图显示了 wxPython 按钮小部件的文档。

我们可以非常快速地创建一个带有标题、菜单栏和状态栏的工作窗口。当鼠标悬停在菜单项上时,状态栏会显示菜单项的文本。这可以通过编写以下代码来实现:
# Import wxPython GUI toolkit
import wx
# Subclass wxPython frame
class GUI(wx.Frame):
def __init__(self, parent, title, size=(200,100)):
# Initialize super class
wx.Frame.__init__(self, parent, title=title, size=size)
# Change the frame background color
self.SetBackgroundColour('white')
# Create Status Bar
self.CreateStatusBar()
# Create the Menu
menu= wx.Menu()
# Add Menu Items to the Menu
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menu.AppendSeparator()
menu.Append(wx.ID_EXIT,"Exit"," Exit the GUI")
# Create the MenuBar
menuBar = wx.MenuBar()
# Give the MenuBar a Title
menuBar.Append(menu,"File")
# Connect the MenuBar to the frame
self.SetMenuBar(menuBar)
# Display the frame
self.Show()
# Create instance of wxPython application
app = wx.App()
# Call sub-classed wxPython GUI increasing default Window size
GUI(None, "Python GUI using wxPython", (300,150))
# Run the main GUI event loop
app.MainLoop()
这创建了以下使用 wxPython 库编写的 Python GUI。

在前面的代码中,我们继承自wx.Frame。在下面的代码中,我们继承自wx.Panel,并将wx.Frame传递给我们的类的__init__()方法。
注意
在 wxPython 中,顶级 GUI 窗口称为框架。没有框架就不能有 wxPython GUI,框架必须作为 wxPython 应用程序的一部分创建。
我们在代码底部同时创建应用程序和框架。
为了向我们的 GUI 添加小部件,我们必须将它们附加到一个面板上。面板的父级是框架(我们的顶级窗口),我们放置在面板中的小部件的父级是面板。
以下代码向一个面板小部件添加了一个多行文本框小部件。我们还向面板小部件添加了一个按钮小部件,当点击时,会向文本框打印一些文本。
以下是完整的代码:
import wx # Import wxPython GUI toolkit
class GUI(wx.Panel): # Subclass wxPython Panel
def __init__(self, parent):
# Initialize super class
wx.Panel.__init__(self, parent)
# Create Status Bar
parent.CreateStatusBar()
# Create the Menu
menu= wx.Menu()
# Add Menu Items to the Menu
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menu.AppendSeparator()
menu.Append(wx.ID_EXIT,"Exit"," Exit the GUI")
# Create the MenuBar
menuBar = wx.MenuBar()
# Give the Menu a Title
menuBar.Append(menu,"File")
# Connect the MenuBar to the frame
parent.SetMenuBar(menuBar)
# Create a Print Button
button = wx.Button(self, label="Print", pos=(0,60))
# Connect Button to Click Event method
self.Bind(wx.EVT_BUTTON, self.printButton, button)
# Create a Text Control widget
self.textBox = wx.TextCtrl(
self, size=(280,50), style=wx.TE_MULTILINE)
def printButton(self, event):
self.textBox.AppendText(
"The Print Button has been clicked!")
app = wx.App() # Create instance of wxPython application
frame = wx.Frame(None, title="Python GUI using wxPython", size=(300,180)) # Create frame
GUI(frame) # Pass frame into GUI
frame.Show() # Display the frame
app.MainLoop() # Run the main GUI event loop
运行前面的代码并点击我们的 wxPython 按钮小部件会产生以下 GUI 输出:

工作原理...
在这个食谱中,我们使用成熟的 wxPython GUI 工具包创建了自己的 GUI。只需几行 Python 代码,我们就能创建一个带有“最小化”、“最大化”和“退出”按钮的完全功能的 GUI。我们添加了一个菜单栏,一个多行文本控件和一个按钮。我们还创建了一个状态栏,当我们选择菜单项时会显示文本。我们将所有这些小部件放入了一个面板容器小部件中。
我们将按钮连接到文本控件以打印文本。
当鼠标悬停在菜单项上时,状态栏会显示一些文本。
使用 wxPython 快速添加控件
在这个食谱中,我们将重新创建我们在本书中早期使用 tkinter 创建的 GUI,但这次,我们将使用 wxPython 库。我们将看到使用 wxPython GUI 工具包创建我们自己的 Python GUI 是多么简单和快速。
我们不会重新创建我们在之前章节中创建的整个功能。例如,我们不会国际化我们的 wxPython GUI,也不会将其连接到 MySQL 数据库。我们将重新创建 GUI 的视觉方面并添加一些功能。
注意
比较不同的库可以让我们选择使用哪些工具包来开发我们自己的 Python GUI,并且我们可以在我们自己的 Python 代码中结合几个工具包。
准备工作
确保你已经安装了 wxPython 模块以便按照这个步骤进行。
如何做...
首先,我们像以前在 tkinter 中那样创建我们的 PythonOOP类,但这次我们继承并扩展了wx.Frame类。出于清晰的原因,我们不再将我们的类称为OOP,而是将其重命名为MainFrame。
注意
在 wxPython 中,主 GUI 窗口被称为 Frame。
我们还创建了一个回调方法,当我们单击“退出”菜单项时关闭 GUI,并将浅灰色的“元组”声明为我们 GUI 的背景颜色。
import wx
BACKGROUNDCOLOR = (240, 240, 240, 255)
class MainFrame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.createWidgets()
self.Show()
def exitGUI(self, event): # callback
self.Destroy()
def createWidgets(self):
self.CreateStatusBar() # wxPython built-in method
self.createMenu()
self.createNotebook()
接下来,我们通过创建 wxPythonNotebook类的实例并将其分配为我们自己的名为Widgets的自定义类的父类,向我们的 GUI 添加一个选项卡控件。
notebook类实例变量的父类是wx.Panel。
def createNotebook(self):
panel = wx.Panel(self)
notebook = wx.Notebook(panel)
widgets = Widgets(notebook) # Custom class explained below
notebook.AddPage(widgets, "Widgets")
notebook.SetBackgroundColour(BACKGROUNDCOLOR)
# layout
boxSizer = wx.BoxSizer()
boxSizer.Add(notebook, 1, wx.EXPAND)
panel.SetSizerAndFit(boxSizer)
注意
在 wxPython 中,选项卡小部件被命名为Notebook,就像在 tkinter 中一样。
每个Notebook小部件都需要一个父类,并且为了在 wxPython 中布局Notebook中的小部件,我们使用不同类型的 sizers。
注意
wxPython sizers 是类似于 tkinter 的网格布局管理器的布局管理器。
接下来,我们向我们的 Notebook 页面添加控件。我们通过创建一个从wx.Panel继承的单独类来实现这一点。
class Widgets(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.createWidgetsFrame()
self.addWidgets()
self.layoutWidgets()
我们通过将 GUI 代码模块化为小方法来遵循 Python OOP 编程最佳实践,这样可以使我们的代码易于管理和理解。
#------------------------------------------------------
def createWidgetsFrame(self):
self.panel = wx.Panel(self)
staticBox = wx.StaticBox( self.panel, -1, "Widgets Frame" )
self.statBoxSizerV = wx.StaticBoxSizer(staticBox,
wx.VERTICAL)
#-----------------------------------------------------
def layoutWidgets(self):
boxSizerV = wx.BoxSizer( wx.VERTICAL )
boxSizerV.Add( self.statBoxSizerV, 1, wx.ALL )
self.panel.SetSizer( boxSizerV )
boxSizerV.SetSizeHints( self.panel )
#------------------------------------------------------
def addWidgets(self):
self.addCheckBoxes()
self.addRadioButtons()
self.addStaticBoxWithLabels()
注意
在使用 wxPython StaticBox 小部件时,为了成功地对其进行布局,我们使用了StaticBoxSizer和常规的BoxSizer的组合。wxPython StaticBox 与 tkinter 的 LabelFrame 小部件非常相似。
在 tkinter 中,将一个StaticBox嵌入另一个StaticBox很简单,但在 wxPython 中使用起来有点不直观。使其工作的一种方法如下所示:
def addStaticBoxWithLabels(self):
boxSizerH = wx.BoxSizer(wx.HORIZONTAL)
staticBox = wx.StaticBox( self.panel, -1,
"Labels within a Frame" )
staticBoxSizerV = wx.StaticBoxSizer( staticBox, wx.VERTICAL )
boxSizerV = wx.BoxSizer( wx.VERTICAL )
staticText1 = wx.StaticText( self.panel, -1,
"Choose a number:" )
boxSizerV.Add( staticText1, 0, wx.ALL)
staticText2 = wx.StaticText( self.panel, -1,"Label 2")
boxSizerV.Add( staticText2, 0, wx.ALL )
#------------------------------------------------------
staticBoxSizerV.Add( boxSizerV, 0, wx.ALL )
boxSizerH.Add(staticBoxSizerV)
#------------------------------------------------------
boxSizerH.Add(wx.TextCtrl(self.panel))
# Add local boxSizer to main frame
self.statBoxSizerV.Add( boxSizerH, 1, wx.ALL )
首先,我们创建一个水平的BoxSizer。接下来,我们创建一个垂直的StaticBoxSizer,因为我们想在这个框架中以垂直布局排列两个标签。
为了将另一个小部件排列到嵌入的StaticBox的右侧,我们必须将嵌入的StaticBox及其子控件和下一个小部件都分配给水平的BoxSizer,然后将这个BoxSizer(现在包含了我们的嵌入的StaticBox和其他小部件)分配给主StaticBox。
这听起来令人困惑吗?
你只需要尝试使用这些 sizers 来感受如何使用它们。从这个步骤的代码开始,注释掉一些代码,或者修改一些 x 和 y 坐标来看看效果。
阅读官方的 wxPython 文档也是很有帮助的。
注意
重要的是要知道在代码中的哪里添加不同的 sizers 以实现我们希望的布局。
为了在第一个下面创建第二个StaticBox,我们创建单独的StaticBoxSizers并将它们分配给同一个面板。
class Widgets(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.panel = wx.Panel(self)
self.createWidgetsFrame()
self.createManageFilesFrame()
self.addWidgets()
self.addFileWidgets()
self.layoutWidgets()
#----------------------------------------------------------
def createWidgetsFrame(self):
staticBox = wx.StaticBox(
self.panel, -1, "Widgets Frame", size=(285, -1) )
self.statBoxSizerV = wx.StaticBoxSizer(
staticBox, wx.VERTICAL)
#----------------------------------------------------------
def createManageFilesFrame(self):
staticBox = wx.StaticBox(
self.panel, -1, "Manage Files", size=(285, -1) )
self.statBoxSizerMgrV = wx.StaticBoxSizer(
staticBox, wx.VERTICAL)
#----------------------------------------------------------
def layoutWidgets(self):
boxSizerV = wx.BoxSizer( wx.VERTICAL )
boxSizerV.Add( self.statBoxSizerV, 1, wx.ALL )
boxSizerV.Add( self.statBoxSizerMgrV, 1, wx.ALL )
self.panel.SetSizer( boxSizerV )
boxSizerV.SetSizeHints( self.panel )
#----------------------------------------------------------
def addFileWidgets(self):
boxSizerH = wx.BoxSizer(wx.HORIZONTAL)
boxSizerH.Add(wx.Button(
self.panel, label='Browse to File...'))
boxSizerH.Add(wx.TextCtrl(
self.panel, size=(174, -1), value= "Z:\\" ))
boxSizerH1 = wx.BoxSizer(wx.HORIZONTAL)
boxSizerH1.Add(wx.Button(
self.panel, label='Copy File To: '))
boxSizerH1.Add(wx.TextCtrl(
self.panel, size=(174, -1), value= "Z:\\Backup" ))
boxSizerV = wx.BoxSizer(wx.VERTICAL)
boxSizerV.Add(boxSizerH)
boxSizerV.Add(boxSizerH1)
self.statBoxSizerMgrV.Add( boxSizerV, 1, wx.ALL )
以下代码实例化了主事件循环,运行我们的 wxPython GUI 程序。
#======================
# Start GUI
#======================
app = wx.App()
MainFrame(None, title="Python GUI using wxPython", size=(350,450))
app.MainLoop()
我们的 wxPython GUI 的最终结果如下:

工作原理...
我们在几个类中设计和布局我们的 wxPython GUI。
在我们的 Python 模块的底部部分完成这些操作后,我们创建了一个 wxPython 应用程序的实例。接下来,我们实例化我们的 wxPython GUI 代码。
之后,我们调用主 GUI 事件循环,该循环执行在此应用程序进程中运行的所有 Python 代码。这将显示我们的 wxPython GUI。
注意
我们放置在创建应用程序和调用其主事件循环之间的任何代码都成为我们的 wxPython GUI。
可能需要一些时间来真正熟悉 wxPython 库及其 API,但一旦我们了解如何使用它,这个库就真的很有趣,是构建自己的 Python GUI 的强大工具。还有一个可与 wxPython 一起使用的可视化设计工具:www.cae.tntech.edu/help/programming/wxdesigner-getting-started/view
这个示例使用面向对象编程来学习如何使用 wxPython GUI 工具包。
尝试将主要的 wxPython 应用程序嵌入到主要的 tkinter 应用程序中
现在,我们已经使用 Python 内置的 tkinter 库以及 wxWidgets 库的 wxPython 包装器创建了相同的 GUI,我们确实需要结合使用这些技术创建的 GUI。
注意
wxPython 和 tkinter 库都有各自的优势。在诸如stackoverflow.com/的在线论坛上,我们经常看到诸如哪个更好?应该使用哪个 GUI 工具包?这表明我们必须做出“二选一”的决定。我们不必做出这样的决定。
这样做的主要挑战之一是每个 GUI 工具包都必须有自己的事件循环。
在这个示例中,我们将尝试通过从我们的 tkinter GUI 中调用它来嵌入一个简单的 wxPython GUI。
准备工作
我们将重用在第一章中构建的 tkinter GUI。
如何做...
我们从一个简单的 tkinter GUI 开始,看起来像这样:

接下来,我们将尝试调用在本章前一篇示例中创建的简单 wxPython GUI。
这是以简单的非面向对象编程方式完成此操作的整个代码:
#===========================================================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
win = tk.Tk()
win.title("Python GUI")
aLabel = ttk.Label(win, text="A Label")
aLabel.grid(column=0, row=0)
ttk.Label(win, text="Enter a name:").grid(column=0, row=0)
name = tk.StringVar()
nameEntered = ttk.Entry(win, width=12, textvariable=name)
nameEntered.grid(column=0, row=1)
ttk.Label(win, text="Choose a number:").grid(column=1, row=0)
number = tk.StringVar()
numberChosen = ttk.Combobox(win, width=12, textvariable=number)
numberChosen['values'] = (1, 2, 4, 42, 100)
numberChosen.grid(column=1, row=1)
numberChosen.current(0)
scrolW = 30
scrolH = 3
scr = scrolledtext.ScrolledText(win, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=0, sticky='WE', columnspan=3)
nameEntered.focus()
#===========================================================
def wxPythonApp():
import wx
app = wx.App()
frame = wx.Frame(None, -1, "wxPython GUI", size=(200,150))
frame.SetBackgroundColour('white')
frame.CreateStatusBar()
menu= wx.Menu()
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menuBar = wx.MenuBar()
menuBar.Append(menu,"File")
frame.SetMenuBar(menuBar)
frame.Show()
app.MainLoop()
action = ttk.Button(win, text="Call wxPython GUI", command= wxPythonApp )
action.grid(column=2, row=1)
#======================
# Start GUI
#======================
win.mainloop()
运行上述代码后,单击 tkinter Button控件后,从我们的 tkinter GUI 启动了一个 wxPython GUI。

它是如何工作的...
重要的是,我们将整个 wxPython 代码放入了自己的函数中,我们将其命名为def wxPythonApp()。
在按钮单击事件的回调函数中,我们只需调用此代码。
注意
需要注意的一点是,在继续使用 tkinter GUI 之前,我们必须关闭 wxPython GUI。
尝试将我们的 tkinter GUI 代码嵌入到 wxPython 中
在这个示例中,我们将与上一个示例相反,尝试从 wxPython GUI 中调用我们的 tkinter GUI 代码。
准备工作
我们将重用在本章前一篇示例中创建的一些 wxPython GUI 代码。
如何做...
我们将从一个简单的 wxPython GUI 开始,看起来像这样:

接下来,我们将尝试调用一个简单的 tkinter GUI。
这是以简单的非面向对象编程方式完成此操作的整个代码:
#=============================================================
def tkinterApp():
import tkinter as tk
from tkinter import ttk
win = tk.Tk()
win.title("Python GUI")
aLabel = ttk.Label(win, text="A Label")
aLabel.grid(column=0, row=0)
ttk.Label(win, text="Enter a name:").grid(column=0, row=0)
name = tk.StringVar()
nameEntered = ttk.Entry(win, width=12, textvariable=name)
nameEntered.grid(column=0, row=1)
nameEntered.focus()
def buttonCallback():
action.configure(text='Hello ' + name.get())
action = ttk.Button(win, text="Print", command=buttonCallback)
action.grid(column=2, row=1)
win.mainloop()
#=============================================================
import wx
app = wx.App()
frame = wx.Frame(None, -1, "wxPython GUI", size=(200,180))
frame.SetBackgroundColour('white')
frame.CreateStatusBar()
menu= wx.Menu()
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menuBar = wx.MenuBar()
menuBar.Append(menu,"File")
frame.SetMenuBar(menuBar)
textBox = wx.TextCtrl(frame, size=(180,50), style=wx.TE_MULTILINE)
def tkinterEmbed(event):
tkinterApp()
button = wx.Button(frame, label="Call tkinter GUI", pos=(0,60))
frame.Bind(wx.EVT_BUTTON, tkinterEmbed, button)
frame.Show()
#======================
# Start wxPython GUI
#======================
app.MainLoop()
运行上述代码后,单击 wxPython Button小部件后,从我们的 wxPython GUI 启动了一个 tkinter GUI。然后我们可以在 tkinter 文本框中输入文本。通过单击其按钮,按钮文本将更新为该名称。

在启动 tkinter 事件循环后,wxPython GUI 仍然可以响应,因为我们可以在 tkinter GUI 运行时输入TextCtrl小部件。
注意
在上一个示例中,我们在关闭 wxPython GUI 之前无法使用我们的 tkinter GUI。意识到这种差异可以帮助我们的设计决策,如果我们想要结合这两种 Python GUI 技术。
通过多次单击 wxPython GUI 按钮,我们还可以创建几个 tkinter GUI 实例。但是,只要有任何 tkinter GUI 仍在运行,我们就不能关闭 wxPython GUI。我们必须先关闭它们。

它是如何工作的...
在这个示例中,我们与上一个示例相反,首先使用 wxPython 创建 GUI,然后在其中使用 tkinter 创建了几个 GUI 实例。
当一个或多个 tkinter GUI 正在运行时,wxPython GUI 仍然保持响应。但是,单击 tkinter 按钮只会更新第一个实例中的按钮文本。
如何使用 Python 来控制两种不同的 GUI 框架
在这个配方中,我们将探讨如何从 Python 控制 tkinter 和 wxPython GUI 框架。在上一章中,我们已经使用 Python 的线程模块来保持我们的 GUI 响应,所以在这里我们将尝试使用相同的方法。
我们将看到事情并不总是按照直觉的方式工作。
然而,我们将改进我们的 tkinter GUI,使其在我们从中调用 wxPython GUI 的实例时不再无响应。
准备工作
这个配方将扩展本章的一个先前配方,我们试图将一个主要的 wxPython GUI 嵌入到我们的 tkinter GUI 中。
如何做...
当我们从 tkinter GUI 创建了一个 wxPython GUI 的实例时,我们就不能再使用 tkinter GUI 控件,直到关闭了 wxPython GUI 的一个实例。让我们现在改进一下。
我们的第一次尝试可能是在 tkinter 按钮回调函数中使用线程。
例如,我们的代码可能是这样的:
def wxPythonApp():
import wx
app = wx.App()
frame = wx.Frame(None, -1, "wxPython GUI", size=(200,150))
frame.SetBackgroundColour('white')
frame.CreateStatusBar()
menu= wx.Menu()
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menuBar = wx.MenuBar()
menuBar.Append(menu,"File")
frame.SetMenuBar(menuBar)
frame.Show()
app.MainLoop()
def tryRunInThread():
runT = Thread(target=wxPythonApp)
runT.setDaemon(True)
runT.start()
print(runT)
print('createThread():', runT.isAlive())
action = ttk.Button(win, text="Call wxPython GUI", command=tryRunInThread)
起初,这似乎是有效的,这是直观的,因为 tkinter 控件不再被禁用,我们可以通过单击按钮创建几个 wxPython GUI 的实例。我们还可以在其他 tkinter 小部件中输入和选择。

然而,一旦我们试图关闭 GUI,我们会从 wxWidgets 得到一个错误,我们的 Python 可执行文件会崩溃。

为了避免这种情况,我们可以改变代码,只让 wxPython 的app.MainLoop在一个线程中运行,而不是尝试在一个线程中运行整个 wxPython 应用程序。
def wxPythonApp():
import wx
app = wx.App()
frame = wx.Frame(None, -1, "wxPython GUI", size=(200,150))
frame.SetBackgroundColour('white')
frame.CreateStatusBar()
menu= wx.Menu()
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menuBar = wx.MenuBar()
menuBar.Append(menu,"File")
frame.SetMenuBar(menuBar)
frame.Show()
runT = Thread(target=app.MainLoop)
runT.setDaemon(True)
runT.start()
print(runT)
print('createThread():', runT.isAlive())
action = ttk.Button(win, text="Call wxPython GUI", command=wxPythonApp)
action.grid(column=2, row=1)
它是如何工作的...
我们最初尝试在一个线程中运行整个 wxPython GUI 应用程序,但这并不起作用,因为 wxPython 的主事件循环期望成为应用程序的主线程。
我们找到了一个解决方法,只在一个线程中运行 wxPython 的app.MainLoop,这样就可以欺骗它认为它是主线程。
这种方法的一个副作用是,我们不能再单独关闭所有的 wxPython GUI 实例。至少其中一个只有在我们关闭创建线程为守护进程的 wxPython GUI 时才关闭。
我不太确定为什么会这样。直觉上,人们可能期望能够关闭所有守护线程,而不必等待创建它们的主线程先关闭。
这可能与引用计数器没有被设置为零,而我们的主线程仍在运行有关。
在实际层面上,这是当前的工作方式。
如何在两个连接的 GUI 之间进行通信
在之前的配方中,我们找到了连接 wxPython GUI 和 tkinter GUI 的方法,相互调用彼此。
虽然两个 GUI 成功同时运行,但它们实际上并没有真正相互通信,因为它们只是互相启动。
在这个配方中,我们将探讨使这两个 GUI 相互通信的方法。
准备工作
阅读之前的一些配方可能是为这个配方做好准备的好方法。
在这个配方中,我们将使用与之前配方相比略有修改的 GUI 代码,但大部分基本的 GUI 构建代码是相同的。
如何做...
在之前的配方中,我们的主要挑战之一是如何将两个设计为应用程序的唯一 GUI 工具包的 GUI 技术结合起来。我们找到了各种简单的方法来将它们结合起来。
我们将再次从 tkinter GUI 的主事件循环中启动 wxPython GUI,并在 tkinter 进程中启动 wxPython GUI 的自己的线程。
为了做到这一点,我们将使用一个共享的全局多进程 Python 队列。
注意
虽然在这个配方中最好避免全局数据,但它们是一个实际的解决方案,Python 全局变量实际上只在它们被声明的模块中是全局的。
这是使两个 GUI 在一定程度上相互通信的 Python 代码。为了节省空间,这不是纯粹的面向对象编程代码。
我们也没有展示所有部件的创建代码。该代码与之前的示例中相同。
# Ch09_Communicate.py
import tkinter as tk
from tkinter import ttk
from threading import Thread
win = tk.Tk()
win.title("Python GUI")
from multiprocessing import Queue
sharedQueue = Queue()
dataInQueue = False
def putDataIntoQueue(data):
global dataInQueue
dataInQueue = True
sharedQueue.put(data)
def readDataFromQueue():
global dataInQueue
dataInQueue = False
return sharedQueue.get()
#===========================================================
import wx
class GUI(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
parent.CreateStatusBar()
button = wx.Button(self, label="Print", pos=(0,60))
self.Bind(wx.EVT_BUTTON, self.writeToSharedQueue, button)
#--------------------------------------------------------
def writeToSharedQueue(self, event):
self.textBox.AppendText(
"The Print Button has been clicked!\n")
putDataIntoQueue('Hi from wxPython via Shared Queue.\n')
if dataInQueue:
data = readDataFromQueue()
self.textBox.AppendText(data)
text.insert('0.0', data) # insert data into GUI
#============================================================
def wxPythonApp():
app = wx.App()
frame = wx.Frame(
None, title="Python GUI using wxPython", size=(300,180))
GUI(frame)
frame.Show()
runT = Thread(target=app.MainLoop)
runT.setDaemon(True)
runT.start()
print(runT)
print('createThread():', runT.isAlive())
#============================================================
action = ttk.Button(win, text="Call wxPython GUI", command=wxPythonApp)
action.grid(column=2, row=1)
#======================
# Start GUI
#======================
win.mainloop()
首先运行上述代码会创建程序的 tkinter 部分,当我们在这个 GUI 中点击按钮时,它会运行 wxPython GUI。与之前一样,两者同时运行,但这次,两个 GUI 之间有了额外的通信层级。

在上述截图的左侧显示了 tkinter GUI,通过点击Call wxPython GUI按钮,我们调用了一个 wxPython GUI 的实例。我们可以通过多次点击按钮来创建多个实例。
注意
所有创建的 GUI 都保持响应。它们不会崩溃或冻结。
在任何一个 wxPython GUI 实例上点击Print按钮会向其自己的TextCtrl部件写入一句话,然后也会向自己以及 tkinter GUI 写入另一行。您需要向上滚动以在 wxPython GUI 中看到第一句话。
注意
这种工作方式是通过使用模块级队列和 tkinter 的Text部件来实现的。
重要的一点是,我们创建一个线程来运行 wxPython 的app.MainLoop,就像我们在之前的示例中所做的那样。
def wxPythonApp():
app = wx.App()
frame = wx.Frame(
None, title="Python GUI using wxPython", size=(300,180))
GUI(frame)
frame.Show()
runT = Thread(target=app.MainLoop)
runT.setDaemon(True)
runT.start()
我们创建了一个从wx.Panel继承并命名为GUI的类。然后我们在上述代码中实例化了这个类。
我们在这个类中创建了一个按钮点击事件回调方法,然后调用了上面编写的过程代码。因此,该类可以访问这些函数并将数据写入共享队列。
#------------------------------------------------------
def writeToSharedQueue(self, event):
self.textBox.AppendText(
"The Print Button has been clicked!\n")
putDataIntoQueue('Hi from wxPython via Shared Queue.\n')
if dataInQueue:
data = readDataFromQueue()
self.textBox.AppendText(data)
text.insert('0.0', data) # insert data into tkinter
我们首先检查在上述方法中是否已将数据放入共享队列,如果是这样,我们就将公共数据打印到两个 GUI 中。
注意
putDataIntoQueue()将数据放入队列,readDataFromQueue()将其读取出来并保存在data变量中。
text.insert('0.0', data)是将这些数据从Print按钮的 wxPython 回调方法写入到 tkinter GUI 中的代码行。
以下是在代码中被调用并使其工作的过程函数(不是方法,因为它们没有绑定)。
from multiprocessing import Queue
sharedQueue = Queue()
dataInQueue = False
def putDataIntoQueue(data):
global dataInQueue
dataInQueue = True
sharedQueue.put(data)
def readDataFromQueue():
global dataInQueue
dataInQueue = False
return sharedQueue.get()
我们使用一个名为dataInQueue的简单布尔标志来通知数据何时可用于队列中。
工作原理
在这个示例中,我们成功地以类似的方式将我们之前独立的两个 GUI 结合在一起,但彼此之间没有交流。然而,在这个示例中,我们通过使一个 GUI 启动另一个 GUI,并通过一个简单的多进程 Python 队列机制,进一步连接它们,我们能够使它们相互通信,将数据从共享队列写入到两个 GUI 中。
有许多非常先进和复杂的技术可用于连接不同的进程、线程、池、锁、管道、TCP/IP 连接等。
在 Python 精神中,我们找到了一个对我们有效的简单解决方案。一旦我们的代码变得更加复杂,我们可能需要重构它,但这是一个很好的开始。
第十章:使用 PyOpenGL 和 PyGLet 创建令人惊叹的 3D GUI
在本章中,我们将创建令人惊叹的 Python GUI,显示真正的可以旋转的三维图像,这样我们可以从各个角度观察它们。
-
PyOpenGL 转换了我们的 GUI
-
我们的 3D GUI!
-
使用位图使我们的 GUI 更漂亮
-
PyGLet 比 PyOpenGL 更容易地转换了我们的 GUI
-
我们的 GUI 有惊人的颜色
-
使用 tkinter 创建幻灯片放映
介绍
在本章中,我们将通过赋予它真正的三维能力来转换我们的 GUI。我们将使用两个 Python 第三方包。PyOpenGL 是 OpenGL 标准的 Python 绑定,它是一个内置于所有主要操作系统中的图形库。这使得生成的小部件具有本地的外观和感觉。
Pyglet 是另一个 Python 绑定到 OpenGL 库,但它也可以创建 GUI 应用程序,这使得使用 Pyglet 编码比使用 PyOpenGL 更容易。
PyOpenGL 转换了我们的 GUI
在这个教程中,我们将成功创建一个导入 PyOpenGL 模块并实际工作的 Python GUI!
为了做到这一点,我们需要克服一些最初的挑战。
这个教程将展示一个已经被证明有效的方法。如果你自己尝试并卡住了,记住托马斯·爱迪生的著名话语。
注意
发明家托马斯·爱迪生在回答一位记者关于爱迪生的失败的问题时说:
"我并没有失败。我只是找到了一万种行不通的方法。"
首先,我们必须安装 PyOpenGL 扩展模块。
成功安装与我们的操作系统架构匹配的 PyOpenGL 模块后,我们将创建一些示例代码。
准备工作
我们将安装 PyOpenGL 包。在本书中,我们使用的是 Windows 7 64 位操作系统和 Python 3.4。接下来的下载截图是针对这个配置的。
我们还将使用 wxPython。如果你没有安装 wxPython,你可以阅读前一章关于如何安装 wxPython 以及如何使用这个 GUI 框架的一些教程。
注意
我们正在使用 wxPython Phoenix 版本,这是最新版本,旨在将原始的 Classic wxPython 版本替换掉。
如何做...
为了使用 PyOpenGL,我们必须首先安装它。以下 URL 是官方的 Python 包安装程序网站:
pypi.python.org/pypi/PyOpenGL/3.0.2#downloads

这似乎是正确的安装,但事实证明,它在 Windows 7 64 位操作系统和 Python 3.4.3 64 位上不起作用。
在前一章的教程中提到了一个更好的查找 Python 安装包的地方。你可能已经很熟悉了。我们下载与我们的操作系统和 Python 版本匹配的包。它使用新的.whl格式,所以我们首先要安装 Python 轮式包。
注意
如何安装 Python 轮式包的方法在之前的教程中有描述。
使用pip命令通过PyOpenGL-3.1.1a1-cp34-none-win_amd64.whl文件安装 PyOpenGL 既成功又安装了我们需要的所有 64 位模块。
用下载的轮式安装程序的完整路径替换<your full path>。
pip install <your full path> PyOpenGL-3.1.1a1-cp34-none-win_amd64.whl
当我们尝试导入一些 PyOpenGL 模块时,它可以工作,就像在这个代码示例中所看到的那样:
# Ch10_import_OpenGL.py
import wx
from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLUT import *
所有这些代码都在做的是导入几个 OpenGL Python 模块。它除此之外什么也不做,但是当我们运行我们的 Python 模块时,我们不会收到任何错误。
这证明我们已成功将 OpenGL 绑定到 Python 中。
现在我们的开发环境已经成功设置,我们可以使用 wxPython 来尝试它。
注意
许多在线示例都限制在使用 Python 2.x,以及使用 Classic 版本的 wxPython。我们使用的是 Python 3 和 Phoenix。
使用基于 wxPython 演示示例的代码创建了一个工作的 3D 立方体。相比之下,运行圆锥体示例没有成功,但这个示例让我们在正确的轨道上开始了。
这是 URL:
wiki.wxpython.org/GLCanvas%20update
以下是对代码的一些修改:
import wx
from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLUT import *
class MyCanvasBase(glcanvas.GLCanvas):
def __init__(self, parent):
glcanvas.GLCanvas.__init__(self, parent, -1)
# This context was missing from the code
self.context = glcanvas.GLContext(self) # <- added
def OnPaint(self, event):
dc = wx.PaintDC(self)
# We have to pass in a context ------
# self.SetCurrent() # commented out
self.SetCurrent(self.context) # <- changed
我们现在可以创建以下 GUI:

在 wxPython 的经典版本中,SetCurrent()不需要上下文。这是我们在网上搜索时可能会找到的一些代码。
def OnPaint(self, event):
dc = wx.PaintDC(self)
self.SetCurrent()
if not self.init:
self.InitGL()
self.init = True
self.OnDraw()
在使用 wxPython Phoenix 时,前面的代码不起作用。我们可以在网上查找 Phoenix 的正确语法。

它是如何工作的...
在这个配方中,我们首次使用了 PyOpenGL Python 绑定的 OpenGL。虽然 OpenGL 可以创建真正惊人的真 3D 图像,但我们在这个过程中遇到了一些挑战,然后找到了解决这些挑战的方法,使其工作起来。
注意
我们正在用 Python 编写 3D 图像!
我们的 GUI 是 3D 的!
在这个配方中,我们将使用 wxPython 创建自己的 GUI。我们正在重用一些来自 wxPython 演示示例的代码,我们已经将其减少到显示 3D OpenGL 所需的最少代码。
注意
OpenGL 是一个非常庞大的库。我们不会详细解释这个库。如果你想进一步学习 OpenGL,有很多书籍和在线文档可供参考。它有自己的着色语言。
准备工作
阅读前面的配方可能是准备这个配方的好方法。
如何做...
由于整个 Python 代码有点长,我们只会展示一小部分代码。
整个代码都可以在线获得,这个 Python 模块被称为:
# Ch10_wxPython_OpenGL_GUI
import wx
from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLUT import *
#---------------------------------------------------
class CanvasBase(glcanvas.GLCanvas):
def __init__(self, parent):
glcanvas.GLCanvas.__init__(self, parent, -1)
self.context = glcanvas.GLContext(self)
self.init = False
# Cube 3D start rotation
self.last_X = self.x = 30
self.last_Y = self.y = 30
self.Bind(wx.EVT_SIZE, self.sizeCallback)
self.Bind(wx.EVT_PAINT, self.paintCallback)
self.Bind(wx.EVT_LEFT_DOWN, self.mouseDownCallback)
self.Bind(wx.EVT_LEFT_UP, self.mouseUpCallback)
self.Bind(wx.EVT_MOTION, self.mouseMotionCallback)
def sizeCallback(self, event):
wx.CallAfter(self.setViewport)
event.Skip()
def setViewport(self):
self.size = self.GetClientSize()
self.SetCurrent(self.context)
glViewport(0, 0, self.size.width, self.size.height)
def paintCallback(self, event):
wx.PaintDC(self)
self.SetCurrent(self.context)
if not self.init:
self.initGL()
self.init = True
self.onDraw()
def mouseDownCallback(self, event):
self.CaptureMouse()
self.x, self.y = self.last_X, self.last_Y = event.GetPosition()
def mouseUpCallback(self, evt):
self.ReleaseMouse()
def mouseMotionCallback(self, evt):
if evt.Dragging() and evt.LeftIsDown():
self.last_X, self.last_Y = self.x, self.y
self.x, self.y = evt.GetPosition()
self.Refresh(False)
#-----------------------------------------------------
class CubeCanvas(CanvasBase):
def initGL(self):
# set viewing projection
glMatrixMode(GL_PROJECTION)
glFrustum(-0.5, 0.5, -0.5, 0.5, 1.0, 3.0)
# position viewer
glMatrixMode(GL_MODELVIEW)
glTranslatef(0.0, 0.0, -2.0)
# position object
glRotatef(self.y, 1.0, 0.0, 0.0)
glRotatef(self.x, 0.0, 1.0, 0.0)
glEnable(GL_DEPTH_TEST)
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
def onDraw(self):
# clear color and depth buffers
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# draw six faces of a cube
glBegin(GL_QUADS)
glNormal3f( 0.0, 0.0, 1.0)
glVertex3f( 0.5, 0.5, 0.5)
glVertex3f(-0.5, 0.5, 0.5)
glVertex3f(-0.5,-0.5, 0.5)
glVertex3f( 0.5,-0.5, 0.5)
glNormal3f( 0.0, 0.0,-1.0)
glVertex3f(-0.5,-0.5,-0.5)
#===========================================================
app = wx.App()
frame = wx.Frame(None, title="Python GUI using wxPython", size=(300,230))
GUI(frame)
frame.Show()
app.MainLoop()

前面的屏幕截图显示了我们的 wxPython GUI。当我们点击按钮小部件时,会出现以下第二个窗口。

注意
我们现在可以使用鼠标将立方体转动起来,看到它的所有六个面。

我们还可以最大化这个窗口,坐标会缩放,我们可以在这个更大的窗口中旋转这个立方体!

这个立方体也可以是一艘星际迷航太空飞船!
如果这是我们想要开发的内容,我们只需要成为这项技术的高级程序员。
注意
许多视频游戏正在使用 OpenGL 开发。
它是如何工作的...
我们首先创建了一个常规的 wxPython GUI,并在上面放置了一个按钮小部件。单击此按钮会调用导入的 OpenGL 3D 库。使用的代码是 wxPython 演示示例的一部分,我们稍微修改了它以使其与 Phoenix 一起工作。
注意
这个配方将我们自己的 GUI 与这个库粘合在一起。
OpenGL 是一个如此庞大和令人印象深刻的库。这个配方让我们体验了如何在 Python 中创建一个工作示例。
注意
通常,一个工作示例就足以让我们开始我们的旅程。
使用位图使我们的 GUI 漂亮
这个配方受到了一个 wxPython IDE 构建框架的启发,该框架在某个时候曾经起作用。
它不能在 Python 3 和 wxPython Phoenix 中工作,但这段代码非常酷。
我们将重用这个项目提供的大量代码中的一个位图图像。
在时间耗尽之前,你可以在 GitHub 上 fork Google 代码。

准备工作
在这个配方中,我们将继续使用 wxPython,因此阅读至少前一章的部分可能对准备这个配方有用。
如何做...
在反向工程 gui2py 代码并对此代码进行其他更改后,我们可能会实现以下窗口小部件,它显示了一个漂亮的平铺背景。

当然,我们在重构之前的网站代码时丢失了很多小部件,但它确实给了我们一个很酷的背景,点击“退出”按钮仍然有效。
下一步是弄清楚如何将代码的有趣部分集成到我们自己的 GUI 中。
我们通过将以下代码添加到上一个教程的 GUI 中来实现这一点。
#----------------------------------------------------------
class GUI(wx.Panel): # Subclass wxPython Panel
def __init__(self, parent):
wx.Panel.__init__(self, parent)
imageFile = 'Tile.bmp'
self.bmp = wx.Bitmap(imageFile)
# react to a resize event and redraw image
parent.Bind(wx.EVT_SIZE, self.canvasCallback)
def canvasCallback(self, event=None):
# create the device context
dc = wx.ClientDC(self)
brushBMP = wx.Brush(self.bmp)
dc.SetBrush(brushBMP)
width, height = self.GetClientSize()
dc.DrawRectangle(0, 0, width, height)
注意
我们必须绑定到父级,而不是 self,否则我们的位图将不会显示出来。
现在运行我们改进的代码会将位图平铺为 GUI 的背景。

点击按钮仍然会调用我们的 OpenGL 3D 绘图,所以我们没有失去任何功能。

它是如何工作的...
在这个教程中,我们通过使用位图作为背景来增强了我们的 GUI。我们平铺了位图图像,当我们调整 GUI 窗口的大小时,位图会自动调整以填充我们正在使用设备上绘制的画布的整个区域。
注意
上述 wxPython 代码可以加载不同的图像文件格式。
PyGLet 比 PyOpenGL 更容易地转换我们的 GUI
在这个教程中,我们将使用 PyGLet GUI 开发框架来创建我们的 GUI。
PyGLet 比 PyOpenGL 更容易使用,因为它自带了自己的 GUI 事件循环,所以我们不需要使用 tkinter 或 wxPython 来创建我们的 GUI。
如何做...
为了使用 Pyglet,我们首先必须安装这个第三方 Python 插件。
使用pip命令,我们可以轻松安装这个库,成功安装在我们的site-packages Python 文件夹中看起来像这样:

在线文档位于当前版本的这个网站:
pyglet.readthedocs.org/en/pyglet-1.2-maintenance/

使用 Pyglet 库的第一次体验可能是这样的:
import pyglet
window = pyglet.window.Window()
label = pyglet.text.Label('PyGLet GUI',
font_size=42,
x=window.width//2, y=window.height//2,
anchor_x='center', anchor_y='center')
@window.event
def on_draw():
window.clear()
label.draw()
pyglet.app.run()
上述代码来自官方网站 pyglet.org,并导致以下完全功能的 GUI:

它是如何工作的...
在这个教程中,我们使用了另一个包装了 OpenGL 库的第三方 Python 模块。
这个库自带了自己的事件循环处理能力,这使我们不必依赖另一个库来创建一个运行中的 Python GUI。
我们已经探索了官方网站,它向我们展示了如何安装和使用这个奇妙的 GUI 库。
我们 GUI 中惊人的颜色
在这个教程中,我们将扩展我们使用 Pyglet 编写的 GUI,将其转变为真正的 3D。
我们还将为其添加一些花哨的颜色。这个教程受到了OpenGL SuperBible图书系列中一些示例代码的启发。它创建了一个非常丰富多彩的立方体,我们可以使用键盘上、下、左、右按钮在三维空间中旋转它。
我们稍微改进了示例代码,使图像在按住一个键时转动,而不是必须按下并释放键。
准备工作
上一个教程解释了如何安装 PyGLet,并为您介绍了这个库。如果您还没有这样做,浏览一下那一章可能是个好主意。
注意
在在线文档中,PyGLet 通常以全小写拼写。虽然这可能是一种 Pythonic 的方式,但我们会将类的第一个字母大写,并且我们使用小写来开始每个变量、方法和函数名。
除非必要澄清代码,否则本书不使用下划线。
如何做...
以下代码创建了下面显示的 3D 彩色立方体。这次,我们将使用键盘箭头键来旋转图像,而不是鼠标。
import pyglet
from pyglet.gl import *
from pyglet.window import key
from OpenGL.GLUT import *
WINDOW = 400
INCREMENT = 5
class Window(pyglet.window.Window):
# Cube 3D start rotation
xRotation = yRotation = 30
def __init__(self, width, height, title=''):
super(Window, self).__init__(width, height, title)
glClearColor(0, 0, 0, 1)
glEnable(GL_DEPTH_TEST)
def on_draw(self):
# Clear the current GL Window
self.clear()
# Push Matrix onto stack
glPushMatrix()
glRotatef(self.xRotation, 1, 0, 0)
glRotatef(self.yRotation, 0, 1, 0)
# Draw the six sides of the cube
glBegin(GL_QUADS)
# White
glColor3ub(255, 255, 255)
glVertex3f(50,50,50)
# Yellow
glColor3ub(255, 255, 0)
glVertex3f(50,-50,50)
# Red
glColor3ub(255, 0, 0)
glVertex3f(-50,-50,50)
glVertex3f(-50,50,50)
# Blue
glColor3f(0, 0, 1)
glVertex3f(-50,50,-50)
# <… more color defines for cube faces>
glEnd()
# Pop Matrix off stack
glPopMatrix()
def on_resize(self, width, height):
# set the Viewport
glViewport(0, 0, width, height)
# using Projection mode
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
aspectRatio = width / height
gluPerspective(35, aspectRatio, 1, 1000)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
glTranslatef(0, 0, -400)
def on_text_motion(self, motion):
if motion == key.UP:
self.xRotation -= INCREMENT
elif motion == key.DOWN:
self.xRotation += INCREMENT
elif motion == key.LEFT:
self.yRotation -= INCREMENT
elif motion == key.RIGHT:
self.yRotation += INCREMENT
if __name__ == '__main__':
Window(WINDOW, WINDOW, 'Pyglet Colored Cube')
pyglet.app.run()

使用键盘箭头键,我们可以旋转 3D 立方体。

它是如何工作的...
在这个教程中,我们使用 pyglet 创建了一个丰富多彩的立方体,我们可以使用键盘箭头键在三维空间中旋转它。
我们已经为我们的立方体的六个面定义了几种颜色,并且我们已经使用 pyglet 创建了我们的主窗口框架。
这段代码与本章中以前的一个食谱类似,在那个食谱中,我们使用 wxPython 库创建了一个立方体。原因是在底层,wxPython 和 pyglet 都使用 OpenGL 库。
使用 tkinter 创建幻灯片
在这个食谱中,我们将使用纯 Python 创建一个漂亮的幻灯片 GUI。
我们将看到核心 Python 内置功能的限制,然后我们将探索另一个可用的第三方模块 Pillow,它扩展了 tkinter 在图像处理方面的内置功能。
虽然一开始 Pillow 这个名字听起来有点奇怪,但它实际上有很多历史背景。
注意
在本书中,我们只使用 Python 3.4 及以上版本。
我们不会回到 Python 2。
Guido 已经表达了他有意打破向后兼容性的决定,并决定 Python 3 是 Python 编程的未来。
对于 GUI 和图像,Python 2 的旧线有一个非常强大的模块,名为 PIL,代表 Python 图像库。这个库具有非常多的功能,在 Python 3 非常成功创建几年后,这些功能仍未被翻译成 Python 3。
许多开发人员仍然选择使用 Python 2 而不是未来版本,因为 Python 2 仍然有更多的可用库。
这有点令人伤感。
幸运的是,另一个图像处理库已经被创建出来,可以与 Python 3 一起使用,它的名字是 PIL 加一些东西。
注意
Pillow 与 Python 2 的 PIL 库不兼容。
准备就绪
在这个食谱的第一部分中,我们将使用纯 Python。为了改进代码,我们将使用 pip 功能安装另一个 Python 模块。因此,虽然您很可能熟悉 pip,但了解如何使用它可能会有用。
如何做...
首先,我们将使用纯 Python 创建一个工作的 GUI,在窗口框架内对幻灯片进行洗牌。
这是工作代码,接下来是运行此代码的一些截图的结果:
from tkinter import Tk, PhotoImage, Label
from itertools import cycle
from os import listdir
class SlideShow(Tk):
# inherit GUI framework extending tkinter
def __init__(self, msShowTimeBetweenSlides=1500):
# initialize tkinter super class
Tk.__init__(self)
# time each slide will be shown
self.showTime = msShowTimeBetweenSlides
# look for images in current working directory
listOfSlides = [slide for slide in listdir() if slide.endswith('gif')]
# cycle slides to show on the tkinter Label
self.iterableCycle = cycle((PhotoImage(file=slide), slide) for slide in listOfSlides)
# create tkinter Label widget which can display images
self.slidesLabel = Label(self)
# create the Frame widget
self.slidesLabel.pack()
def slidesCallback(self):
# get next slide from iterable cycle
currentInstance, nameOfSlide = next(self.iterableCycle)
# assign next slide to Label widget
self.slidesLabel.config(image=currentInstance)
# update Window title with current slide
self.title(nameOfSlide)
# recursively repeat the Show
self.after(self.showTime, self.slidesCallback)
#=================================
# Start GUI
#=================================
win = SlideShow()
win.after(0, win.slidesCallback())
win.mainloop()

这是幻灯片展示中的另一个时刻。

虽然幻灯片的滑动确实令人印象深刻,但纯 Python tkinter GUI 的内置功能不支持非常流行的.jpg格式,因此我们必须使用另一个 Python 库。
为了使用 Pillow,我们首先必须使用pip命令安装它。
成功的安装看起来像这样:

Pillow 支持.jpg格式,并且为了使用它,我们必须稍微改变我们的语法。
使用 Pillow 是一个高级主题,在本书的这一版本中不会涉及。
它是如何工作的...
Python 是一个非常棒的工具,在这个食谱中,我们已经探索了几种使用和扩展它的方法。
注意
当手指指向月亮时,它并不是月亮本身,只是一个指针。
第十一章:最佳实践
在本章中,我们将探讨与 Python GUI 相关的最佳实践。
-
避免意大利面代码
-
使用 init 连接模块
-
混合下降和 OOP 编码
-
使用代码命名约定
-
何时不使用 OOP
-
成功使用设计模式的方法
-
避免复杂性
介绍
在本章中,我们将探讨可以帮助我们以高效的方式构建 GUI 并使其易于维护和扩展的不同最佳实践。
这些最佳实践也将帮助我们调试 GUI,使其成为我们想要的样子。
避免意大利面代码
在这个配方中,我们将探讨创建意大利面代码的典型方式,然后我们将看到如何避免这样的代码的更好方式。
注意
意大利面代码是一种功能交织在一起的代码。
准备就绪
我们将使用内置的 Python 库 tkinter 来创建一个新的简单 GUI。
如何做...
在网上搜索并阅读文档后,我们可能会开始编写以下代码来创建我们的 GUI:
# Spaghetti Code #############################
def PRINTME(me):print(me)
import tkinter
x=y=z=1
PRINTME(z)
from tkinter import *
scrolW=30;scrolH=6
win=tkinter.Tk()
if x:chVarUn=tkinter.IntVar()
from tkinter import ttk
WE='WE'
import tkinter.scrolledtext
outputFrame=tkinter.ttk.LabelFrame(win,text=' Type into the scrolled text control: ')
scr=tkinter.scrolledtext.ScrolledText(outputFrame,width=scrolW,height=scrolH,wrap=tkinter.WORD)
e='E'
scr.grid(column=1,row=1,sticky=WE)
outputFrame.grid(column=0,row=2,sticky=e,padx=8)
lFrame=None
if y:chck2=tkinter.Checkbutton(lFrame,text="Enabled",variable=chVarUn)
wE='WE'
if y==x:PRINTME(x)
lFrame=tkinter.ttk.LabelFrame(win,text="Spaghetti")
chck2.grid(column=1,row=4,sticky=tkinter.W,columnspan=3)
PRINTME(z)
lFrame.grid(column=0,row=0,sticky=wE,padx=10,pady=10)
chck2.select()
try: win.mainloop()
except:PRINTME(x)
chck2.deselect()
if y==x:PRINTME(x)
# End Pasta #############################
运行上述代码会产生以下 GUI:

这并不是我们打算的 GUI。我们希望它看起来更像这样:

虽然意大利面代码创建了一个 GUI,但代码非常难以调试,因为代码中有很多混乱。
以下是产生所需 GUI 的代码:
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
#======================
# Create instance
#======================
win = tk.Tk()
#======================
# Add a title
#======================
win.title("Python GUI")
#=========================
# Disable resizing the GUI
#=========================
win.resizable(0,0)
#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)
#=============================================================
# Using a scrolled Text control
#=============================================================
outputFrame = ttk.LabelFrame(win, text=' Type into the scrolled text control: ')
outputFrame.grid(column=0, row=2, sticky='E', padx=8)
scrolW = 30
scrolH = 6
scr = scrolledtext.ScrolledText(outputFrame, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=1, row=0, sticky='WE')
#=============================================================
# Creating a checkbutton
#=============================================================
chVarUn = tk.IntVar()
check2 = tk.Checkbutton(lFrame, text="Enabled", variable=chVarUn)
check2.deselect()
check2.grid(column=1, row=4, sticky=tk.W, columnspan=3)
#======================
# Start GUI
#======================
win.mainloop()
它是如何工作的...
在这个配方中,我们将意大利面代码与良好的代码进行了比较。良好的代码比意大利面代码有很多优势。
它有清晰的注释部分。
意大利面代码:
def PRINTME(me):print(me)
import tkinter
x=y=z=1
PRINTME(z)
from tkinter import *
良好的代码:
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
它具有自然的流程,遵循小部件在 GUI 主窗体中的布局方式。
在意大利面代码中,底部的 LabelFrame 在顶部的 LabelFrame 之前创建,并且与导入语句和一些小部件创建混合在一起。
意大利面代码:
import tkinter.scrolledtext
outputFrame=tkinter.ttk.LabelFrame(win,text=' Type into the scrolled text control: ')
scr=tkinter.scrolledtext.ScrolledText(outputFrame,width=scrolW,height=scrolH,wrap=tkinter.WORD)
e='E'
scr.grid(column=1,row=1,sticky=WE)
outputFrame.grid(column=0,row=2,sticky=e,padx=8)
lFrame=None
if y:chck2=tkinter.Checkbutton(lFrame,text="Enabled",variable=chVarUn)
wE='WE'
if y==x:PRINTME(x)
lFrame=tkinter.ttk.LabelFrame(win,text="Spaghetti")
良好的代码:
#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)
#=============================================================
# Using a scrolled Text control
#=============================================================
outputFrame = ttk.LabelFrame(win, text=' Type into the scrolled text control: ')
outputFrame.grid(column=0, row=2, sticky='E', padx=8)
它不包含不必要的变量赋值,也没有print函数,当阅读代码时,它不会做人们期望的调试。
意大利面代码:
def PRINTME(me):print(me)
x=y=z=1
e='E'
WE='WE'
scr.grid(column=1,row=1,sticky=WE)
wE='WE'
if y==x:PRINTME(x)
lFrame.grid(column=0,row=0,sticky=wE,padx=10,pady=10)
PRINTME(z)
try: win.mainloop()
except:PRINTME(x)
chck2.deselect()
if y==x:PRINTME(x)
良好的代码:
没有上述任何一种。
import语句只导入所需的模块。它们不会在整个代码中混乱。没有重复的import语句。没有import *语句。
意大利面代码:
import tkinter 1
x=y=z=1
PRINTME(z)
from tkinter import *
scrolW=30;scrolH=6
win=tkinter.Tk()
if x:chVarUn=tkinter.IntVar()
from tkinter import ttk
WE='WE'
import tkinter.scrolledtext
良好的代码:
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
选择的变量名相当有意义。没有不必要使用数字1而不是True的if语句。
意大利面代码:
x=y=z=1
if x:chVarUn=tkinter.IntVar()
wE='WE'
良好的代码:
#=============================================================
# Using a scrolled Text control
#=============================================================
outputFrame = ttk.LabelFrame(win, text=' Type into the scrolled text control: ')
outputFrame.grid(column=0, row=2, sticky='E', padx=8)
scrolW = 30
scrolH = 6
scr = scrolledtext.ScrolledText(outputFrame, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=1, row=0, sticky='WE')
我们没有失去预期的窗口标题,我们的复选框最终出现在正确的位置。我们还使包围复选框的LabelFrame可见。
意大利面代码:
我们失去了窗口标题,也没有显示顶部的LabelFrame。复选框最终出现在错误的位置。
良好的代码:
#======================
# Create instance
#======================
win = tk.Tk()
#======================
# Add a title
#======================
win.title("Python GUI")
#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)
#=============================================================
# Creating a checkbutton
#=============================================================
chVarUn = tk.IntVar()
check2 = tk.Checkbutton(lFrame, text="Enabled", variable=chVarUn)
check2.deselect()
check2.grid(column=1, row=4, sticky=tk.W, columnspan=3)
#======================
# Start GUI
#======================
win.mainloop()
使用 init 连接模块
当我们使用 Eclipse IDE 的 PyDev 插件创建一个新的 Python 项目时,它会自动创建一个__init__.py模块。当不使用 Eclipse 时,我们也可以手动创建它。
注意
__init__.py模块通常是空的,然后大小为 0 千字节。
我们可以使用这个通常为空的模块来连接不同的 Python 模块,通过在其中输入代码。这个配方将展示如何做到这一点。
准备就绪
我们将创建一个类似于我们在上一个配方中创建的 GUI 的新 GUI。
如何做...
随着我们的项目变得越来越大,我们自然地将其拆分为几个 Python 模块。使用现代 IDE(如 Eclipse)时,惊人地复杂,找到位于不同子文件夹中的模块,无论是在需要导入它的代码的上方还是下方。
绕过这个限制的一个实际方法是使用__init__.py模块。
注意
在 Eclipse 中,我们可以将 Eclipse 内部项目环境设置为某些文件夹,我们的 Python 代码将找到它。但是在 Eclipse 之外,例如在命令窗口中运行时,Python 模块导入机制有时会不匹配,代码将无法运行。
这是一个空的__init__.py模块的截图,当在 Eclipse 代码编辑器中打开时,它的名称不是__init__,而是属于的 PyDev 包的名称。代码编辑器左侧的“1”是行号,而不是在这个模块中编写的任何代码。这个空的__init__.py模块中绝对没有代码。

这个文件是空的,但它确实存在。

当我们运行以下代码并点击clickMe Button时,我们会得到代码后面显示的结果。这是一个常规的 Python 模块,尚未使用__init__.py模块。
注意
__init__.py模块与 Python 类的__init__(self)方法不同。
# Ch11_GUI__init.py
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
#======================
# Create instance
#======================
win = tk.Tk()
#======================
# Add a title
#======================
win.title("Python GUI")
#=============================================================
# Adding a LabelFrame and a Button
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)
def clickMe():
from tkinter import messagebox
messagebox.showinfo('Message Box', 'Hi from same Level.')
button = ttk.Button(lFrame, text="Click Me ", command=clickMe)
button.grid(column=1, row=0, sticky=tk.S)
#======================
# Start GUI
#======================
win.mainloop()

在上面的代码中,我们创建了以下函数,它导入 Python 的消息框,然后使用它来显示消息框窗口:
def clickMe():
from tkinter import messagebox
messagebox.showinfo('Message Box', 'Hi from same Level.')
当我们将clickMe()消息框代码移动到嵌套的目录文件夹中,并尝试将其import到我们的 GUI 模块中时,我们遇到了一些问题。
我们在 Python 模块所在的位置下创建了三个子文件夹。然后,我们将clickMe()消息框代码放入一个新的 Python 模块中,我们将其命名为MessageBox.py。这个模块位于Folder3中,比我们的 Python 模块所在的位置低三级。
我们需要导入MessageBox.py模块,以便使用这个模块包含的clickMe()函数。
起初,它似乎可以工作,因为似乎我们可以导入新的嵌套模块,因为我们没有从 Eclipse IDE 中得到任何错误或警告。
我们使用 Python 的相对导入语法:
from .Folder1.Folder2.Folder3.MessageBox import clickme
这可以在以下截图中看到:

我们已经删除了本地的clickMe()函数,现在我们的回调应该使用导入的clickMe()函数,但它并没有按预期工作。我们运行代码时,没有得到预期的弹出窗口,而是得到了一个导入系统错误:

我们可以通过转到 PyDev 项目属性并将自己添加为外部库,在 Eclipse 中将包含新函数的子文件夹作为外部库。这似乎并不直观,但它确实有效。

现在,当我们注释掉文件夹结构,并直接从嵌套到三个级别的模块中导入函数时,代码会按预期工作。
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
# from .Folder1.Folder2.Folder3.MessageBox import clickMe
from MessageBox import clickMe
这个函数在消息框中显示不同的文本:

实现相同结果的更好方法是使用 Python 内置的__init__.py模块。
删除之前特定于 Eclipse 的外部库依赖后,我们现在可以直接使用这个模块。
注意
我们将代码放入这个模块中,如果我们将__init__.py模块导入到我们的程序中,它将在我们的所有其他代码之前运行,截至 Python 3.4.3。
忽略 PyDev 未解析的导入(带有红色圈和交叉)错误。这个import是必要的;它使我们的代码运行,并且整个 Python 导入机制工作。

将__init__.py模块导入到我们的程序后,我们可以使用它。检查它是否工作的第一个测试是在这个模块中编写一个打印语句。

通过添加以下代码,我们可以以编程方式找出我们的位置:

现在,我们可以通过向相同的__init__.py模块添加以下代码来从内部初始化我们的 Python 搜索路径:
print('hi from GUI init\n')
from sys import path
from pprint import pprint
#=======================================================
# Required setup for the PYTONPATH in order to find
# all package folders
#=======================================================
from site import addsitedir
from os import getcwd, chdir, pardir
for _ in range(10):
curFull = getcwd()
curDir = curFull.split('\\')[-1]
if 'B04829_Ch11_Code' == curDir:
addsitedir(curFull)
addsitedir(curFull + '\\Folder1\\Folder2\\Folder3\\')
break
chdir(pardir)
pprint(path)
当我们现在运行我们的 GUI 代码时,我们得到了相同预期的窗口,但我们已经移除了对 Eclipse PYTHONPATH变量的依赖。
现在我们可以成功地在 Eclipse PyDev 插件之外运行相同的代码。
注意
我们的代码变得更加 Pythonic。
它是如何工作的...
在这个示例中,我们发现了使用 PyDev 插件的局限性,这个插件是免费的,与出色的免费 Eclipse IDE 一起提供。
我们首先在 Eclipse IDE 中找到了一个解决方法,然后通过变得 Pythonic 而独立于这个 IDE。
注意
通常使用纯 Python 是最好的方法。
混合下降和面向对象编码
Python 是一种面向对象的编程语言,但并不总是使用 OOP 是有意义的。对于简单的脚本任务,传统的瀑布式编码风格仍然是合适的。
在这个示例中,我们将创建一个新的 GUI,将下降式编码风格与更现代的 OOP 编码风格混合在一起。
我们将创建一个 OOP 风格的类,当我们在 Python GUI 中使用瀑布样式创建小部件时,它将在鼠标悬停在小部件上时显示工具提示。
注意
下降和瀑布式编码风格是相同的。这意味着我们必须在调用下面的代码之前将代码物理放置在上面的代码之上。在这种范式中,当我们执行代码时,代码从程序的顶部字面上下降到程序的底部。
准备工作
在这个示例中,我们将使用 tkinter 创建一个 GUI,这类似于我们在本书第一章中创建的 GUI。
如何做...
在 Python 中,我们可以通过使用self关键字将函数绑定到类,将它们转换为方法。这是 Python 的一个真正美妙的能力,它允许我们创建可理解和可维护的大型系统。
有时,当我们只编写简短的脚本时,使用 OOP 并没有意义,因为我们发现自己不得不用self关键字大量添加变量,当代码不需要时,代码会变得不必要地庞大。
让我们首先使用 tkinter 创建一个 Python GUI,并以瀑布式编码风格编写它。
以下代码创建了 GUI:
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
#======================
# Create instance
#======================
win = tk.Tk()
#======================
# Add a title
#======================
win.title("Python GUI")
#=========================
# Disable resizing the GUI
#=========================
win.resizable(0,0)
#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)
#=============================================================
# Labels
#=============================================================
ttk.Label(lFrame, text="Enter a name:").grid(column=0, row=0)
ttk.Label(lFrame, text="Choose a number:").grid(column=1, row=0, sticky=tk.W)
#=============================================================
# Buttons click command
#=============================================================
def clickMe(name, number):
messagebox.showinfo('Information Message Box', 'Hello '+name+
', your number is: ' + number)
#=============================================================
# Creating several controls in a loop
#=============================================================
names = ['name0', 'name1', 'name2']
nameEntries = ['nameEntry0', 'nameEntry1', 'nameEntry2']
numbers = ['number0', 'number1', 'number2']
numberEntries = ['numberEntry0', 'numberEntry1', 'numberEntry2']
buttons = []
for idx in range(3):
names[idx] = tk.StringVar()
nameEntries[idx] = ttk.Entry(lFrame, width=12, textvariable=names[idx])
nameEntries[idx].grid(column=0, row=idx+1)
nameEntries[idx].delete(0, tk.END)
nameEntries[idx].insert(0, '<name>')
numbers[idx] = tk.StringVar()
numberEntries[idx] = ttk.Combobox(lFrame, width=14, textvariable=numbers[idx])
numberEntries[idx]['values'] = (1+idx, 2+idx, 4+idx, 42+idx, 100+idx)
numberEntries[idx].grid(column=1, row=idx+1)
numberEntries[idx].current(0)
button = ttk.Button(lFrame, text="Click Me "+str(idx+1), command=lambda idx=idx: clickMe(names[idx].get(), numbers[idx].get()))
button.grid(column=2, row=idx+1, sticky=tk.W)
buttons.append(button)
#======================
# Start GUI
#======================
win.mainloop()
当我们运行代码时,我们得到了 GUI,它看起来像这样:

我们可以通过添加工具提示来改进我们的 Python GUI。这样做的最佳方式是将创建工具提示功能的代码与我们的 GUI 隔离开来。
我们通过创建一个具有工具提示功能的单独类来实现这一点,然后在创建 GUI 的同一 Python 模块中创建这个类的实例。
使用 Python,我们不需要将我们的ToolTip类放入一个单独的模块中。我们可以将它放在过程化代码的上面,然后从这段代码下面调用它。
代码现在看起来像这样:
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
#-----------------------------------------------
class ToolTip(object):
def __init__(self, widget):
self.widget = widget
self.tipwindow = None
self.id = None
self.x = self.y = 0
#-----------------------------------------------
def createToolTip(widget, text):
toolTip = ToolTip(widget)
def enter(event): toolTip.showtip(text)
def leave(event): toolTip.hidetip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
#-----------------------------------------------
# further down the module we call the createToolTip function
#-----------------------------------------------
for idx in range(3):
names[idx] = tk.StringVar()
nameEntries[idx] = ttk.Entry(
lFrame, width=12, textvariable=names[idx])
nameEntries[idx].grid(column=0, row=idx+1)
nameEntries[idx].delete(0, tk.END)
nameEntries[idx].insert(0, '<name>')
numbers[idx] = tk.StringVar()
numberEntries[idx] = ttk.Combobox(
lFrame, width=14, textvariable=numbers[idx])
numberEntries[idx]['values'] = (
1+idx, 2+idx, 4+idx, 42+idx, 100+idx)
numberEntries[idx].grid(column=1, row=idx+1)
numberEntries[idx].current(0)
button = ttk.Button(
lFrame, text="Click Me "+str(idx+1), command=lambda idx=idx: clickMe(names[idx].get(), numbers[idx].get()))
button.grid(column=2, row=idx+1, sticky=tk.W)
buttons.append(button)
#-----------------------------------------------
# Add Tooltips to more widgets
createToolTip(nameEntries[idx], 'This is an Entry widget.')
createToolTip(
numberEntries[idx], 'This is a DropDown widget.')
createToolTip(buttons[idx], 'This is a Button widget.')
#-----------------------------------------------
运行代码会在我们悬停鼠标在小部件上时为它们创建工具提示。

它是如何工作的...
在这个示例中,我们以过程化的方式创建了一个 Python GUI,然后在模块的顶部添加了一个类。
我们可以很容易地在同一个 Python 模块中混合和匹配过程化和 OOP 编程。
使用代码命名约定
本书中以前的示例没有使用结构化的代码命名约定。这个示例将向您展示遵循代码命名方案的价值,因为它帮助我们找到我们想要扩展的代码,并提醒我们程序的设计。
准备工作
在这个示例中,我们将查看本书第一章中的 Python 模块名称,并将它们与更好的命名约定进行比较。
如何做...
在本书的第一章中,我们创建了我们的第一个 Python GUI。我们通过逐步增加不同的代码模块名称来改进我们的 GUI。
它看起来像这样:

虽然这是一种典型的编码方式,但它并没有提供太多的意义。当我们在开发过程中编写 Python 代码时,很容易递增数字。
稍后回到这段代码时,我们不太清楚哪个 Python 模块提供了哪些功能,有时,我们最后增加的模块不如之前的版本好。
注意
清晰的命名约定确实有所帮助。
我们可以将第一章中的模块名称与第八章中的模块名称进行比较,后者更有意义。

虽然不完美,但为不同的 Python 模块选择的名称表明了每个模块的责任。当我们想要添加更多单元测试时,清楚地知道它们位于哪个模块中。
以下是另一个示例,演示如何使用代码命名约定在 Python 中创建 GUI:

注意
将单词PRODUCT替换为您当前正在开发的产品。
整个应用程序都是一个 GUI。所有部分都是相互连接的。DEBUG.py模块仅用于调试我们的代码。调用 GUI 的主要函数在与所有其他模块相比时,其名称是颠倒的。它以Gui开头,并以.pyw扩展名结尾。
它是唯一具有这个扩展名的 Python 模块。
根据这个命名约定,如果您对 Python 足够熟悉,很明显,要运行这个 GUI,您需要双击Gui_PRODUCT.pyw模块。
所有其他 Python 模块都包含为 GUI 提供功能并执行底层业务逻辑以实现此 GUI 目的的功能。
工作原理...
Python 代码模块的命名约定对于保持高效并记住我们最初的设计非常有帮助。当我们需要调试和修复缺陷或添加新功能时,它们是首要资源。
注意
通过数字递增模块名称并不是非常有意义,最终会浪费开发时间。
另一方面,命名 Python 变量更像是自由形式。Python 推断类型,因此我们不必指定变量将是<list>类型(它可能不是,或者实际上,在代码的后面部分,它可能会变成不同的类型)。
为变量命名的一个好主意是使它们具有描述性,并且不要缩写得太多。
如果我们希望指出某个变量设计为<list>类型,则使用完整单词list比使用lst更直观。
这与使用number而不是num类似。
在为变量命名时,使用非常描述性的名称是一个好主意,但有时可能会太长。在苹果的 Objective-C 语言中,一些变量和函数名字非常极端:thisIsAMethodThatDoesThisAndThatAndAlsoThatIfYouPassInNIntegers:1:2:3
注意
在为变量、方法和函数命名时要遵循常识。
何时不使用面向对象编程
Python 内置了面向对象编程的能力,但与此同时,我们也可以编写不需要使用面向对象编程的脚本。
对于某些任务,面向对象编程是没有意义的。
这个示例将展示何时不使用面向对象编程。
准备工作
在这个示例中,我们将创建一个类似于之前示例的 Python GUI。我们将比较面向对象编程的代码和非面向对象的替代编程方式。
如何做...
让我们首先使用OOP方法创建一个新的 GUI。以下代码将创建下面代码中显示的 GUI:
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu
class OOP():
def __init__(self):
self.win = tk.Tk()
self.win.title("Python GUI")
self.createWidgets()
def createWidgets(self):
tabControl = ttk.Notebook(self.win)
tab1 = ttk.Frame(tabControl)
tabControl.add(tab1, text='Tab 1')
tabControl.pack(expand=1, fill="both")
self.monty = ttk.LabelFrame(tab1, text=' Monty Python ')
self.monty.grid(column=0, row=0, padx=8, pady=4)
ttk.Label(self.monty, text="Enter a name:").grid(
column=0, row=0, sticky='W')
self.name = tk.StringVar()
nameEntered = ttk.Entry(
self.monty, width=12, textvariable=self.name)
nameEntered.grid(column=0, row=1, sticky='W')
self.action = ttk.Button(self.monty, text="Click Me!")
self.action.grid(column=2, row=1)
ttk.Label(self.monty,
text="Choose a number:").grid(column=1, row=0)
number = tk.StringVar()
numberChosen = ttk.Combobox(self.monty,
width=12, textvariable=number)
numberChosen['values'] = (42)
numberChosen.grid(column=1, row=1)
numberChosen.current(0)
scrolW = 30; scrolH = 3
self.scr = scrolledtext.ScrolledText(
self.monty, width=scrolW, height=scrolH, wrap=tk.WORD)
self.scr.grid(column=0, row=3, sticky='WE', columnspan=3)
menuBar = Menu(tab1)
self.win.config(menu=menuBar)
fileMenu = Menu(menuBar, tearoff=0)
menuBar.add_cascade(label="File", menu=fileMenu)
helpMenu = Menu(menuBar, tearoff=0)
menuBar.add_cascade(label="Help", menu=helpMenu)
nameEntered.focus()
#==========================
oop = OOP()
oop.win.mainloop()

我们可以通过稍微重构我们的代码来实现相同的 GUI,而不使用面向对象的方法。首先,我们移除OOP类及其__init__方法。
接下来,我们将所有方法移到左侧,并移除self类引用,将它们转换为未绑定的函数。
我们还删除了先前代码中的任何其他self引用。然后,我们将createWidgets函数调用移动到函数声明点下方。我们将它放在mainloop调用的正上方。
最终,我们实现了相同的 GUI,但没有使用 OOP。
重构后的代码如下所示:
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu
def createWidgets():
tabControl = ttk.Notebook(win)
tab1 = ttk.Frame(tabControl)
tabControl.add(tab1, text='Tab 1')
tabControl.pack(expand=1, fill="both")
monty = ttk.LabelFrame(tab1, text=' Monty Python ')
monty.grid(column=0, row=0, padx=8, pady=4)
ttk.Label(monty, text="Enter a name:").grid(
column=0, row=0, sticky='W')
name = tk.StringVar()
nameEntered = ttk.Entry(monty, width=12, textvariable=name)
nameEntered.grid(column=0, row=1, sticky='W')
action = ttk.Button(monty, text="Click Me!")
action.grid(column=2, row=1)
ttk.Label(monty, text="Choose a number:").grid(
column=1, row=0)
number = tk.StringVar()
numberChosen = ttk.Combobox(
monty, width=12, textvariable=number)
numberChosen['values'] = (42)
numberChosen.grid(column=1, row=1)
numberChosen.current(0)
scrolW = 30; scrolH = 3
scr = scrolledtext.ScrolledText(
monty, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=0, row=3, sticky='WE', columnspan=3)
menuBar = Menu(tab1)
win.config(menu=menuBar)
fileMenu = Menu(menuBar, tearoff=0)
menuBar.add_cascade(label="File", menu=fileMenu)
helpMenu = Menu(menuBar, tearoff=0)
menuBar.add_cascade(label="Help", menu=helpMenu)
nameEntered.focus()
#======================
win = tk.Tk()
win.title("Python GUI")
createWidgets()
win.mainloop()
它是如何工作的...
Python 使我们能够在有意义的时候使用 OOP。其他语言如 Java 和 C#强制我们始终使用 OOP 方法进行编码。在这个示例中,我们探讨了一个不适合使用 OOP 的情况。
注意
如果代码库增长,OOP 方法将更具扩展性,但是如果确定只需要这个代码,那么就没有必要经过 OOP。
成功使用设计模式的方法
在这个示例中,我们将使用工厂设计模式为我们的 Python GUI 创建小部件。
在以前的示例中,我们要么手动创建小部件,要么在循环中动态创建小部件。
使用工厂设计模式,我们将使用工厂来创建我们的小部件。
准备工作
我们将创建一个 Python GUI,其中有三个按钮,每个按钮都有不同的样式。
如何做...
在我们的 Python GUI 模块顶部,在导入语句的下方,我们创建了几个类:
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu
class ButtonFactory():
def createButton(self, type_):
return buttonTypes[type_]()
class ButtonBase():
relief ='flat'
foreground ='white'
def getButtonConfig(self):
return self.relief, self.foreground
class ButtonRidge(ButtonBase):
relief ='ridge'
foreground ='red'
class ButtonSunken(ButtonBase):
relief ='sunken'
foreground ='blue'
class ButtonGroove(ButtonBase):
relief ='groove'
foreground ='green'
buttonTypes = [ButtonRidge, ButtonSunken, ButtonGroove]
class OOP():
def __init__(self):
self.win = tk.Tk()
self.win.title("Python GUI")
self.createWidgets()
我们创建一个基类,我们的不同按钮样式类都继承自该基类,并且每个类都覆盖了relief和foreground配置属性。所有子类都从这个基类继承getButtonConfig方法。该方法返回一个元组。
我们还创建了一个按钮工厂类和一个保存我们按钮子类名称的列表。我们将列表命名为buttonTypes,因为我们的工厂将创建不同类型的按钮。
在模块的下方,我们使用相同的buttonTypes列表创建按钮小部件。
def createButtons(self):
factory = ButtonFactory()
# Button 1
rel = factory.createButton(0).getButtonConfig()[0]
fg = factory.createButton(0).getButtonConfig()[1]
action = tk.Button(self.monty,
text="Button "+str(0+1), relief=rel, foreground=fg)
action.grid(column=0, row=1)
# Button 2
rel = factory.createButton(1).getButtonConfig()[0]
fg = factory.createButton(1).getButtonConfig()[1]
action = tk.Button(self.monty,
text="Button "+str(1+1), relief=rel, foreground=fg)
action.grid(column=1, row=1)
# Button 3
rel = factory.createButton(2).getButtonConfig()[0]
fg = factory.createButton(2).getButtonConfig()[1]
action = tk.Button(self.monty,
text="Button "+str(2+1), relief=rel, foreground=fg)
action.grid(column=2, row=1)
首先,我们创建一个按钮工厂的实例,然后我们使用我们的工厂来创建我们的按钮。
注意
buttonTypes列表中的项目是我们子类的名称。
我们调用createButton方法,然后立即调用基类的getButtonConfig方法,并使用点表示法检索配置属性。
当我们运行整个代码时,我们会得到以下 Python tkinter GUI:

我们可以看到,我们的 Python GUI 工厂确实创建了不同的按钮,每个按钮都有不同的样式。它们在文本颜色和 relief 属性上有所不同。
它是如何工作的...
在这个示例中,我们使用工厂设计模式创建了几个具有不同样式的小部件。我们可以轻松地使用这种设计模式来创建整个 GUI。
设计模式是我们软件开发工具箱中非常令人兴奋的工具。
避免复杂性
在这个示例中,我们将扩展我们的 Python GUI,并学习处理软件开发工作不断增加的复杂性的方法。
我们的同事和客户喜欢我们用 Python 创建的 GUI,并要求为我们的 GUI 添加越来越多的功能。
这增加了复杂性,很容易破坏我们最初的良好设计。
准备工作
我们将创建一个类似于之前示例中的新 Python GUI,并将以小部件的形式添加许多功能。
如何做...
我们将从一个具有两个选项卡并且看起来像这样的 Python GUI 开始:

我们收到的第一个新功能请求是为Tab 1添加功能,清除scrolledtext小部件。
足够简单。我们只需在Tab 1中添加另一个按钮。
# Adding another Button
self.action = ttk.Button(.
self.monty, text="Clear Text", command=self.clearScrol)
self.action.grid(column=2, row=2)
我们还必须创建回调方法以添加所需的功能,我们在类的顶部定义它,并在创建小部件的方法之外。
# Button callback
def clickMe(self):
self.action.configure(text='Hello ' + self.name.get())
# Button callback Clear Text
def clearScrol(self):
self.scr.delete('1.0', tk.END)
现在我们的 GUI 有一个新按钮,当我们点击它时,我们清除ScrolledText小部件的文本。

为了添加这个功能,我们不得不在同一个 Python 模块中的两个地方添加代码。
我们在createWidgets方法中插入了新按钮(未显示),然后我们创建了一个新的回调方法,当我们的新按钮被点击时调用。我们将这段代码放在第一个按钮的回调之下。
我们的下一个功能请求是添加更多功能。业务逻辑封装在另一个 Python 模块中。我们通过向Tab 1添加三个按钮来调用这个新功能。我们使用循环来实现这一点。
# Adding more Feature Buttons
for idx in range(3):
b = ttk.Button(
self.monty, text="Feature" + str(idx+1))
b.grid(column=idx, row=4)
我们的 GUI 现在看起来是这样的:

接下来,我们的客户要求更多功能,我们使用相同的方法。我们的 GUI 现在看起来是这样的:

注意
这并不太糟糕。当我们为另外 50 个新功能请求时,我们开始怀疑我们的方法是否仍然是最好的方法...
处理我们的 GUI 必须处理的不断增加的复杂性的一种方法是添加选项卡。通过添加更多的选项卡,并将相关功能放入自己的选项卡中,我们可以控制复杂性,并使我们的 GUI 更直观。
这是创建我们的新Tab 3的代码,下面是我们的新 Python GUI:
# Tab Control 3 -----------------------------------------
tab3 = ttk.Frame(tabControl) # Add a tab
tabControl.add(tab3, text='Tab 3') # Make tab visible
monty3 = ttk.LabelFrame(tab3, text=' New Features ')
monty3.grid(column=0, row=0, padx=8, pady=4)
# Adding more Feature Buttons
startRow = 4
for idx in range(24):
if idx < 2:
colIdx = idx
col = colIdx
else:
col += 1
if not idx % 3:
startRow += 1
col = 0
b = ttk.Button(monty3, text="Feature " + str(idx+1))
b.grid(column=col, row=startRow)
# Add some space around each label
for child in monty3.winfo_children():
child.grid_configure(padx=8)

它是如何工作的...
在这个示例中,我们向我们的 GUI 添加了几个新的小部件,以便为我们的 Python GUI 添加更多功能。我们看到,越来越多的新功能请求很容易使我们精美的 GUI 设计变得不太清楚如何使用 GUI。
注意
突然间,小部件占据了世界...
我们看到了如何通过将大功能分解为较小的部分,并将它们安排在功能相关的区域中,通过模块化我们的 GUI 来处理复杂性。
尽管复杂性有许多方面,但模块化和重构代码通常是处理软件代码复杂性的非常好的方法。
注意
在编程中,有时候我们会遇到障碍,陷入困境。我们不断地撞击这堵墙,但什么也没有发生。
有时候我们觉得想要放弃。
然而,奇迹确实会发生...
如果我们继续撞击这堵墙,在某个时刻,这堵墙将倒塌,道路将会开放。
在那个时候,我们可以在软件宇宙中留下积极的印记。


浙公网安备 33010602011771号