Python-GUI-编程秘籍第三版-全-
Python GUI 编程秘籍第三版(全)
原文:
zh.annas-archive.org/md5/cbfbc22790f6d6dfbd6c7f0c8c3d11c9译者:飞龙
前言
在本书的第三版中,我们将使用 Python 编程语言探索图形用户界面(GUIs)的美丽世界。我们将使用 Python 3 的最新版本。本版包含了第一版和第二版的所有食谱,除了过时的OpenGL库,毕竟它并不太符合 Python 的风格。我们在第三版中增加了一章全新的内容,并且大幅改变了第三版的风格,使其更接近食谱格式。通过这种方式,我们希望使食谱更容易应用于现实世界的编程场景,提供经过测试和有效的工作解决方案。
这是一本编程食谱书。每一章都是独立的,并解释了特定的编程解决方案。我们将从非常简单的内容开始,然而,在本书的整个过程中,我们将构建一个用 Python 3 编写的实际应用程序。每个食谱都将扩展这个应用程序的建设。在这个过程中,我们将讨论网络、队列、数据库、PyQt5 图形库以及许多其他技术。我们将应用设计模式并使用最佳实践。
本书假设你有一些使用 Python 编程语言的经验,但这并不是成功使用本书的必要条件。如果你有志于成为一名 Python 程序员,本书也可以作为 Python 编程语言的入门书籍。
如果你是在其他语言中经验丰富的开发者,你将会有乐趣通过添加使用 Python 编写 GUI 的能力来扩展你的专业工具箱。
本书面向的对象
本书是为希望创建 GUI 的程序员而写的。你可能会对我们的成就感到惊讶,我们可以通过使用 Python 编程语言创建美观、功能强大且强大的 GUI 来实现这一点。Python 是一种奇妙、直观的编程语言,非常容易学习。
我邀请你现在就开始这段旅程。这将非常有趣!
为了充分利用本书
为了最大限度地利用本书的内容,请记住以下要点:
-
本书中的所有食谱都是在 Windows 10 64 位操作系统上使用 Python 3.7 开发的。它们在其他配置上尚未经过测试。由于 Python 是一种跨平台语言,因此预计每个食谱中的代码可以在任何地方运行。
-
如果你使用的是 Mac,它确实内置了 Python,但可能缺少一些模块,例如我们将在这本书中使用的
tkinter。 -
我们使用 Python 3.7,Python 的创造者故意选择不使其与 Python 2 向后兼容。如果你使用 Mac 或 Python 2,你可能需要从www.python.org安装 Python 3.7,才能成功运行本书中的食谱。
-
如果您真的希望在 Python 2.7 上运行本书中的代码,您将需要进行一些调整。例如,Python 2.x 中的
tkinter有一个大写的T。Python 2.7 的 print 语句在 Python 3.7 中是一个函数,需要使用括号。 -
虽然 Python 2.x 分支的生命终结(EOL)已延长至 2020 年,但我强烈建议您开始使用 Python 3.7 及更高版本。
-
除非您真的需要,否则为什么要执着于过去?这里有一个链接到Python 增强提案(PEP)373,它提到了 Python 2 的 EOL:
www.python.org/dev/peps/pep-0373/。
下载示例代码文件
您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载”。
-
在搜索框中输入书名,并遵循屏幕上的说明。
下载文件后,请确保您使用最新版本解压缩或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Python-GUI-Programming-Cookbook-Third-Edition。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781838827540_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“以下是本章 Python 模块(以.py扩展名结尾)的概述”。
代码块应如下设置:
action = ttk.Button(win, text="Click Me!", command=click_me)
action.grid(column=2, row=1)
任何命令行输入或输出都应如下编写:
pip install pyqt5
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中如下所示。以下是一个示例:“点击文件菜单,然后点击新建”。
警告或重要注意事项如下所示。
技巧和窍门如下所示。
章节
在这本书中,您将找到几个频繁出现的标题(准备工作,如何操作...,它是如何工作的...,还有更多...,以及也查看)。
为了给出如何完成食谱的清晰说明,请按照以下方式使用这些部分:
准备工作
本节告诉您在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。
如何操作…
本节包含遵循食谱所需的步骤。
它是如何工作的...
本节通常包含对上一节发生情况的详细解释。
还有更多…
本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。
也查看
本节提供了指向其他有用信息的链接,以帮助您了解食谱。
联系我们
我们欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com给我们发邮件。
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并提供材料的链接。
如果您有兴趣成为作者: 如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为什么不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
关于 Packt 的更多信息,请访问 packt.com。
第一章:创建 GUI 表单并添加控件
在本章中,我们将使用 Python 开发我们的第一个 GUI。我们将从构建运行 GUI 应用程序所需的最少代码开始。然后,每个菜谱都会向 GUI 表单添加不同的控件。
我们将首先使用 tkinter 图形用户界面工具包。
tkinter 随 Python 一起提供。一旦安装了 Python 3.7 或更高版本,就无需安装它。tkinter 图形用户界面工具包使我们能够使用 Python 编写 GUI。
旧世界的 DOS 命令提示符已经过时。一些开发者仍然喜欢用它进行开发工作。你的程序最终用户期望一个更现代、更美观的 GUI。
在这本书中,你将学习如何使用 Python 编程语言开发 GUI。
通过从最少的代码开始,我们可以看到每个使用 tkinter 和 Python 编写的 GUI 都遵循的 模式。首先是 import 语句,然后是创建一个 tkinter 类。然后我们可以调用方法并更改属性。最后,我们总是调用窗口事件循环。现在我们可以运行代码了。
我们从最简单的代码开始,在每个后续菜谱中添加更多功能,引入不同的控件控制以及如何更改和检索属性。
在前两个菜谱中,我们将展示整个代码,它只包含几行代码。在随后的菜谱中,我们只展示要添加到先前菜谱中的代码,因为否则这本书会变得太长,反复看到相同的代码会相当无聊。
如果你没有时间自己输入代码,你可以从 github.com/PacktPublishing/Python-GUI-Programming-Cookbook-Third-Edition 下载整本书的代码。
每章开始时,我将展示属于每个章节的 Python 模块。然后我将引用属于展示、研究和运行的代码的不同模块。
到本章结束时,我们将创建一个包含标签、按钮、文本框、组合框、各种状态下的复选按钮和改变 GUI 背景颜色的单选按钮的工作 GUI 应用程序。
下面是本章中以 .py 扩展名结尾的 Python 模块概述:

在本章中,我们将从使用 Python 3.7 或更高版本开始创建令人惊叹的 GUI。我们将涵盖以下主题:
-
创建我们的第一个 Python GUI
-
防止 GUI 被调整大小
-
向 GUI 表单添加标签
-
创建按钮并更改它们的文本属性
-
创建文本框小部件
-
将焦点设置到控件上并禁用控件
-
创建组合框控件
-
创建具有不同初始状态的复选按钮
-
使用单选按钮控件
-
使用滚动文本控件
-
在循环中添加多个控件
创建我们的第一个 Python GUI
Python 是一种非常强大的编程语言。它自带内置的tkinter模块。仅用几行代码(确切地说,是四行)我们就可以构建我们的第一个 Python GUI。
tkinter是 Python 到tk的接口。tk是一个 GUI 工具包,与Tcl相关,Tcl是一种工具命令语言。您可以在docs.python.org/3/library/tk.html了解更多关于tk的信息。
另一个与tcl和tk相关的网站是www.tcl.tk/。
准备工作
要遵循这个食谱,一个有效的 Python 开发环境是先决条件。Python 附带的 IDLE GUI 足以开始。IDLE 是使用tkinter构建的!
如何做到这一点…
让我们看看如何创建我们的第一个 Python GUI:
-
创建一个新的 Python 模块并将其命名为
First_GUI.py。 -
在
First_GUI.py模块的顶部导入tkinter:
import tkinter as tk
- 创建
Tk类的实例:
win = tk.Tk()
- 使用实例变量设置标题:
win.title("Python GUI")
- 启动窗口的主事件循环:
win.mainloop()
以下截图显示了创建结果的 GUI 所需的First_GUI.py的四个代码行:

- 运行 GUI 模块。在执行前面的代码后,得到以下输出:

现在,让我们深入了解代码,以更好地理解它。
它是如何工作的…
在第 9 行,我们导入内置的tkinter模块并将其别名为tk以简化我们的 Python 代码。在第 12 行,我们通过调用其构造函数(Tk后附加的括号将类转换为实例)创建Tk类的实例。我们使用tk别名,这样我们就不必使用较长的单词tkinter。我们将类实例分配给一个名为win(代表窗口)的变量,这样我们就可以通过这个变量访问类属性。由于 Python 是一种动态类型语言,我们不必在分配之前声明这个变量,也不必给它指定一个特定的类型。Python 推断这个语句的赋值类型。Python 是一种强类型语言,所以每个变量始终有一个类型。我们只是不必像在其他语言中那样事先指定它的类型。这使得 Python 成为编程中非常强大和高效的编程语言。
关于类和类型的一点点说明:在 Python 中,每个变量始终有一个类型。我们无法创建一个没有类型的变量。然而,在 Python 中,我们不必事先声明类型,就像在 C 编程语言中那样。
Python 足够智能,可以推断类型。在撰写本书时,C#也有这种能力。
使用 Python,我们可以使用class关键字而不是def关键字来创建我们自己的类。
为了将类分配给一个变量,我们首先必须创建我们类的实例。我们创建实例并将此实例分配给我们的变量,例如:
class AClass(object): print('Hello from AClass') class_instance = AClass()
现在,class_instance变量是AClass类型。
如果这听起来很复杂,不要担心。我们将在接下来的章节中介绍面向对象编程(OOP)。
在第 15 行,我们使用类的实例变量(win)通过调用title()方法并传入一个字符串来给我们的窗口设置标题。
你可能需要放大正在运行的 GUI 才能看到整个标题。
在第 20 行,我们通过在类实例win上调用mainloop方法来开始窗口的事件循环。到目前为止,在我们的代码中,我们已经创建了一个实例并设置了一个属性(窗口标题),但 GUI 将不会显示,直到我们开始主事件循环。
事件循环是一种使我们的 GUI 工作的机制。我们可以将其视为一个无限循环,其中我们的 GUI 正在等待事件发送给它。按钮点击在我们的 GUI 中创建一个事件,或者我们的 GUI 被调整大小也会创建一个事件。
我们可以提前编写所有的 GUI 代码,直到我们调用这个无限循环(前述代码中的win.mainloop()),用户屏幕上都不会显示任何内容。当事件循环结束时,用户点击红色 X 按钮或我们编程来结束 GUI 的小部件时,事件循环结束。当事件循环结束时,我们的 GUI 也结束了。
这个菜谱使用了最少的 Python 代码来创建我们的第一个 GUI 程序。然而,在这本书中,当有需要时,我们会使用面向对象编程(OOP)。
我们已经成功地学习了如何创建我们的第一个 Python GUI。现在,让我们继续下一个菜谱。
防止 GUI 被调整大小
默认情况下,使用tkinter创建的 GUI 可以被调整大小。这并不总是理想的。我们放置到 GUI 表单中的小部件可能会以不适当的方式调整大小,因此在这个菜谱中,我们将学习如何防止我们的 GUI 被 GUI 应用程序的用户调整大小。
准备工作
这个菜谱扩展了之前的菜谱,创建我们的第一个 Python GUI,因此一个要求是你必须自己在一个项目中将第一个菜谱输入进去。或者,你可以从github.com/PacktPublishing/Python-GUI-Programming-Cookbook-Third-Edition/.
如何做到这一点…
这里是防止 GUI 被调整大小的步骤:
-
从上一个菜谱的模块开始,将其保存为
Gui_not_resizable.py。 -
使用
Tk实例变量win来调用resizable方法:
win.resizable(False, False)
这是防止 GUI 被调整大小的代码(GUI_not_resizable.py):

- 运行代码。运行代码创建了这个 GUI:

让我们深入幕后,更好地理解代码。
它是如何工作的…
第 18 行防止 Python GUI 被调整大小。
resizable() 方法属于 Tk() 类,通过传入 (False, False),我们防止 GUI 被调整大小。我们可以禁用 GUI 的 x 和 y 维度以防止调整大小,或者通过传入 True 或任何非零数字来启用一个或两个维度。(True, False) 将启用 x 维度但防止 y 维度被调整大小。
运行此代码将生成一个类似于我们在第一个食谱中创建的 GUI。然而,用户不能再调整它的大小。此外,请注意窗口工具栏中的最大化按钮已变灰。
为什么这很重要?因为一旦我们向表单添加小部件,调整 GUI 的大小可能会让它看起来不是我们想要的样子。我们将在下一个食谱中添加小部件到 GUI,从将标签添加到 GUI 表单开始。
我们还在代码中添加了注释,为这本书中包含的食谱做准备。
在 Visual Studio .NET 等可视化编程 IDE 中,C#程序员通常不会考虑防止用户调整他们用这种语言开发的 GUI 的大小。这会创建出低质量的 GUI。添加这一行 Python 代码可以让我们的用户欣赏我们的 GUI。
我们已经成功学习了如何防止 GUI 被调整大小。现在,让我们继续下一个食谱。
将标签添加到 GUI 表单
标签是一个非常简单的小部件,为我们的 GUI 增加了价值。它解释了其他小部件的目的,提供了额外的信息。这可以指导用户理解 Entry 小部件的含义,也可以解释小部件显示的数据,而无需用户输入数据。
准备工作
我们正在扩展第一个食谱,创建我们的第一个 Python GUI。我们将保持 GUI 可调整大小,因此不要使用第二个食谱中的代码(或注释掉 win.resizable 行)。
如何操作…
执行以下步骤以将标签添加到 GUI 中:
-
从
First_GUI.py模块开始,将其保存为GUI_add_label.py。 -
导入
ttk:
from tkinter import ttk
- 使用
ttk添加标签:
ttk.Label(win, text="A Label")
- 使用网格布局管理器定位标签:
.grid(column=0, row=0)
为了将 Label 小部件添加到我们的 GUI 中,我们将从 tkinter 中导入 ttk 模块。请注意第 9 行和第 10 行的两个 import 语句。
以下代码添加在 win.mainloop() 之上,位于第一个和第二个食谱的底部(GUI_add_label.py):

- 运行代码并观察标签是如何添加到我们的 GUI 中的:

让我们深入了解代码以更好地理解它。
它是如何工作的…
在前述代码的第 10 行,我们从一个单独的模块中导入 tkinter 包。ttk 模块包含一些高级小部件,如笔记本、进度条、标签和按钮,它们看起来不同。这些有助于使我们的 GUI 看起来更好。在某种程度上,ttk 是 tkinter 包中的一个扩展。
我们仍然需要导入tkinter包,但我们需要指定我们现在还想要使用tkinter包中的ttk。
ttk代表主题化的tk。它改善了我们的 GUI 的外观和感觉。您可以在docs.python.org/3/library/tkinter.ttk.html找到更多信息。
第 19 行在调用mainloop之前将标签添加到 GUI 中。
我们将窗口实例传递给ttk.Label构造函数并设置text属性。这将成为Label将显示的文本。我们还使用了网格布局管理器,我们将在第二章[7b1f337c-b9fe-4dc2-8c86-5827e7256831.xhtml]中更深入地探讨,布局管理。
观察到我们的 GUI 突然比之前的配方小得多。它变得如此之小的原因是我们在表单中添加了一个小部件。如果没有小部件,tkinter包将使用默认大小。添加小部件会导致优化,这通常意味着使用尽可能少的空间来显示小部件(s)。如果我们使标签的文本更长,GUI 将自动扩展。我们将在第二章[7b1f337c-b9fe-4dc2-8c86-5827e7256831.xhtml]的后续配方中介绍这种自动表单大小调整,布局管理。
尝试调整大小并最大化带有标签的此 GUI,看看会发生什么。我们已经成功地学习了如何向 GUI 表单添加标签。
现在,让我们继续下一个配方。
创建按钮并更改它们的文本属性
在这个配方中,我们将添加一个按钮小部件,并使用这个按钮来改变我们 GUI 中另一个小部件的属性。这使我们了解了 Python GUI 环境中的回调函数和事件处理。
准备工作
这个配方扩展了之前的配方,向 GUI 表单添加标签。您可以从github.com/PacktPublishing/Python-GUI-Programming-Cookbook-Third-Edition下载整个代码。
如何做到这一点...
在这个配方中,我们将更新我们在上一个配方中添加的标签以及按钮的text属性。添加一个在点击时执行操作的按钮的步骤如下:
-
从
GUI_add_label.py模块开始,并将其保存为GUI_create_button_change_property.py。 -
定义一个函数并命名为
click_me():
def click_me()
- 使用
ttk创建一个按钮并给它一个text属性:
action.configure(text="** I have been Clicked! **")
a_label.configure (foreground='red')
a_label.configure(text='A Red Label')
- 将函数绑定到按钮:
action = ttk.Button(win, text="Click Me!", command=click_me)
- 使用网格布局定位按钮:
action.grid(column=1, row=0)
之前的说明产生了以下代码(GUI_create_button_change_property.py):

- 运行代码并观察输出。
以下屏幕截图显示了点击按钮之前我们的 GUI 看起来是什么样子:

点击按钮后,标签的颜色发生了变化,按钮的文本也发生了变化,这可以从下面的屏幕截图中看到:

让我们深入幕后,更好地理解代码。
它是如何工作的…
在第 19 行,我们将标签分配给变量a_label,在第 20 行,我们使用这个变量在表单中定位标签。我们需要这个变量以便在click_me()函数中更改其属性。默认情况下,这是一个模块级变量,因此只要我们在调用它的函数上方声明变量,我们就可以在函数内部访问它。
第 23 行是当按钮被点击时调用的事件处理程序。
在第 29 行,我们创建了按钮并将命令绑定到click_me()函数。
GUI 是事件驱动的。点击按钮会创建一个事件。我们使用ttk.Button小部件的command属性将此事件发生的回调函数绑定。注意我们是如何不使用括号,只使用名称click_me的。
第 20 行和第 30 行都使用了网格布局管理器,这将在第二章的布局管理菜谱使用网格布局管理器中讨论。这使标签和按钮对齐。我们还更改了标签的文本,以包含单词red,使其更明显地表明颜色已更改。我们将继续在我们的 GUI 中添加更多和更多的小部件,并将在本书的其他菜谱中使用许多内置属性。
我们已经成功学习了如何创建按钮并更改它们的文本属性。现在,让我们继续到下一个菜谱。
创建文本框小部件
在tkinter中,典型的单行文本框小部件称为Entry。在这个菜谱中,我们将向我们的 GUI 添加这样的Entry小部件。我们将通过描述Entry小部件为用户做了什么来使我们的标签更有用。
准备工作
这个菜谱基于创建按钮并更改它们的文本属性菜谱,所以从存储库中下载它并开始工作。
如何做…
按照以下步骤创建文本框小部件:
-
从
GUI_create_button_change_property.py模块开始,并将其保存为GUI_textbox_widget.py。 -
使用
tkinter的tk别名创建一个StringVar变量:
name = tk.StringVar()
- 创建一个
ttk.Entry小部件并将其分配给另一个变量:
name_entered = ttk.Entry(win, width=12, textvariable=name)
- 使用这个变量定位
Entry小部件:
name_entered.grid(column=0, row=1)
上述指令生成了以下代码(GUI_textbox_widget.py):

- 运行代码并观察输出;我们的 GUI 看起来像这样:

- 输入一些文本并点击按钮;我们会看到 GUI 发生了变化,如下所示:

让我们深入幕后,更好地理解代码。
它是如何工作的…
在步骤 1中,我们创建一个新的 Python 模块,而在步骤 2中,我们添加了一个StringVar类型的tkinter,并将其保存在name变量中。当我们创建一个Entry小部件并将其分配给Entry小部件的textvariable属性时,我们使用这个变量。每次我们向Entry小部件中输入一些文本时,这些文本都会保存在name变量中。
在步骤 4中,我们定位了Entry小部件,前一个截图显示了整个代码。
在第 24 行,如图表所示,我们使用name.get()获取Entry小部件的值。
当我们创建我们的按钮时,我们将其引用保存在action变量中。我们使用action变量来调用按钮的configure方法,然后更新按钮的文本。
我们还没有使用 OOP,那么我们是如何访问一个甚至尚未声明的变量的值的呢?在不使用 OOP 类的情况下,在 Python 过程式编码中,我们必须在尝试使用该名称的语句上方实际放置一个名称。那么这是怎么工作的(它确实是这样工作的)?这个答案就是按钮点击事件是一个回调函数,并且当用户点击按钮时,这个函数中引用的变量是已知的并且确实存在。
第 27 行给我们的标签赋予了一个更有意义的名称;目前,它描述了其下方的文本框。我们将按钮向下移动到标签旁边,以便在视觉上关联这两个元素。我们仍在使用网格布局管理器,这将在第二章的布局管理中更详细地解释。
第 30 行创建了一个变量name。这个变量绑定到Entry小部件上,在我们的click_me()函数中,我们能够通过调用这个变量的get()来检索Entry小部件的值。这工作得很好。
现在我们注意到,虽然按钮显示了我们所输入的整个文本(以及更多),但文本框Entry小部件并没有扩展。这是因为我们在第 31 行将其硬编码为宽度为12。
Python 是一种动态类型语言,并从赋值中推断类型。这意味着如果我们将一个字符串分配给name变量,它将是string类型,如果我们将一个整数分配给name,它的类型将是整数。
使用tkinter,我们必须在成功使用之前将name变量声明为tk.StringVar()类型。原因是tkinter不是 Python。我们可以用 Python 使用它,但它不是同一种语言。更多信息请参见wiki.python.org/moin/TkInter。
我们已经成功学习了如何创建文本框小部件。现在,让我们继续学习下一个菜谱。
设置小部件的焦点和禁用小部件
当我们的 GUI 逐渐改进时,如果光标在 GUI 出现时立即出现在Entry小部件中,将会更加方便和有用。
在这个菜谱中,我们学习如何使光标出现在 Entry 框中,以便立即进行文本输入,而不是需要用户在输入到 entry 小部件之前先点击进入小部件以给它设置focus方法。
准备工作
这个菜谱扩展了之前的菜谱,创建文本框小部件。Python 确实很棒。我们只需要在 GUI 出现时将焦点设置到特定的控件上,只需在之前创建的tkinter小部件实例上调用focus()方法即可。在我们的当前 GUI 示例中,我们将ttk.Entry类实例分配给名为name_entered的变量。现在,我们可以给它设置焦点。
如何做到这一点…
将以下代码放置在模块底部之前的代码上方,该代码启动主窗口的事件循环,就像我们在之前的菜谱中所做的那样:
-
从
GUI_textbox_widget.py模块开始,并将其保存为GUI_set_focus.py。 -
使用我们分配给
ttk.Entry小部件实例的name_entered变量,并在这个变量上调用focus()方法:
name_entered.focus()
上述说明生成了以下代码(GUI_set_focus.py):

- 运行代码并观察输出。
如果你遇到一些错误,请确保你将变量调用放置在它们声明的代码下方。目前我们不是使用面向对象编程,所以这仍然是必要的。稍后,将不再需要这样做。
在 Mac 上,你可能需要首先将焦点设置到 GUI 窗口,然后才能设置此窗口中的 Entry 小部件的焦点。
添加 Python 代码的第 38 行将光标放置在我们的文本 Entry 小部件中,给文本 Entry 小部件设置焦点。一旦 GUI 出现,我们就可以直接输入到这个文本框中,而无需先点击它。结果 GUI 现在看起来像这样,光标位于 Entry 小部件内:

注意现在光标默认位于文本输入框内。
我们还可以禁用小部件。在这里,我们禁用按钮以展示原理。在更大的 GUI 应用程序中,禁用小部件的能力允许你在需要使某些内容只读时进行控制。最可能的是组合框小部件和 Entry 小部件,但因为我们还没有到达那些小部件,我们将使用我们的按钮。
要禁用小部件,我们将在小部件上设置一个属性。我们可以通过在创建按钮的 Python 代码的第 37 行下方添加以下代码来使按钮不可用:
-
使用
GUI_set_focus.py模块并将其保存为GUI_disable_button_widget.py。 -
使用
action按钮变量来调用configure方法并将state属性设置为disabled:
action.configure(state='disabled')
- 在
name_entered变量上调用focus()方法:
name_entered.focus()
上述说明生成了以下代码(GUI_disable_button_widget.py):

- 运行代码。在添加上述 Python 代码行之后,点击按钮将不再创建动作:

让我们深入了解代码,以更好地理解它。
它是如何工作的...
这段代码是自我解释的。在第 39 行,我们设置了一个控件的焦点,在第 37 行,我们禁用了另一个控件。在编程语言中使用良好的命名有助于消除冗长的解释。本书后面将有一些关于如何在工作中编程或在家中练习编程技能时如何做到这一点的先进技巧。
我们已经成功地学习了如何设置控件焦点并禁用控件。现在,让我们继续到下一个菜谱。
创建组合框小部件
在这个菜谱中,我们将通过添加具有初始默认值的下拉组合框来改进我们的 GUI。虽然我们可以限制用户只能选择某些选项,但我们也可以允许用户输入他们想要的任何内容。
准备工作
这个菜谱扩展了之前的菜谱,设置控件焦点和禁用控件。
如何做...
我们使用网格布局管理器在Entry控件和Button控件之间插入另一列。以下是 Python 代码:
-
从
GUI_set_focus.py模块开始,并将其保存为GUI_combobox_widget.py。 -
将按钮列更改为
2:
action = ttk.Button(win, text="Click Me!", command=click_me)
action.grid(column=2, row=1)
- 创建一个新的
ttk.Label小部件:
ttk.Label(win, text="Choose a number:").grid(cloumn=1, row=0)
- 创建一个新的
ttk.Combobox小部件:
number_chosen = ttk.Combobox(win, width=12, textvariable=number)
- 为
Combobox小部件分配值:
number_chosen['value'] = (1, 2, 4, 42, 100)
- 将
Combobox小部件放置在列 1:
number_chosen.grid(column=1, row=1)
number_chosen.current(0)
前面的步骤生成了以下代码(GUI_combobox_widget.py):

- 运行代码。
当将此代码添加到之前的菜谱中时,会创建以下 GUI。注意,在前面的代码的第 43 行中,我们给组合框分配了一个具有默认值的元组。这些值随后出现在下拉框中。我们也可以根据需要更改它们(在应用程序运行时输入不同的值):

让我们深入了解代码,以更好地理解它。
它是如何工作的...
第 40 行添加了一个第二个标签以匹配新创建的组合框(在第 42 行创建)。第 41 行将框的值分配给一个特殊的tkinter类型的StringVar变量,就像我们在之前的菜谱中所做的那样。
第 44 行将两个新的控件(标签和组合框)在我们的前一个 GUI 布局中对齐,第 45 行将默认值分配给当 GUI 首次可见时显示。这是number_chosen['values']元组的第一个值,字符串"1"。我们在第 43 行没有给整数的元组加上引号,但它们被转换成了字符串,因为在第 41 行中,我们声明了值应该是tk.StringVar类型。
前面的截图显示了用户所做的选择是42。此值被分配给number变量。
如果在组合框中选择100,则number变量的值变为100。
第 42 行通过textvariable属性将组合框中选择的值绑定到number变量。
还有更多...
如果我们想限制用户只能选择我们编程到 Combobox 小部件中的值,我们可以通过将 state 属性传递给构造函数来实现。将 第 42 行 修改如下:
-
从
GUI_combobox_widget.py模块开始,并将其保存为GUI_combobox_widget_readonly.py。 -
在创建
Combobox小部件时设置state属性:
number_chosen = ttk.Combobox(win, width=12, textvariable=number, state='readonly')
前面的步骤生成以下代码(GUI_combobox_widget_readonly.py):

- 运行代码。
现在,用户不能再向 Combobox 小部件中输入值。
我们可以通过在按钮点击事件回调函数中添加以下代码行来显示用户选择的价值:
-
从
GUI_combobox_widget_readonly.py模块开始,并将其保存为GUI_combobox_widget_readonly_plus_display_number.py。 -
通过在
name变量上使用get()方法扩展按钮点击事件处理程序,使用连接(+ ' ' +),并从number_chosen变量(也调用它的get()方法)获取数字:
def click_me():
action.configure(text='Hello ' + name.get() + ' ' +
number_chosen.get())
- 运行代码。
选择一个数字,输入一个名称,然后点击按钮,我们得到以下 GUI 结果,现在它还显示了输入名称旁边的所选数字(GUI_combobox_widget_readonly_plus_display_number.py):

我们已经成功学习了如何添加组合框小部件。现在,让我们继续下一个配方。
创建具有不同初始状态的复选框
在这个配方中,我们将添加三个复选框小部件,每个小部件都有一个不同的初始状态:
-
第一个是禁用的,里面有一个勾选标记。由于小部件是禁用的,用户不能移除这个勾选标记。
-
第二个复选框是启用的,默认情况下没有勾选标记,但用户可以点击它来添加勾选标记。
-
第三个复选框默认既启用又选中。用户可以随时取消选中并重新选中小部件。
准备工作
这个配方扩展了之前的配方,创建组合框小部件。
如何做到这一点…
下面是创建三个状态不同的复选框小部件的代码:
-
从
GUI_combobox_widget_readonly_plus_display_number.py模块开始,并将其保存为GUI_checkbutton_widget.py。 -
创建三个
tk.IntVar实例并将它们保存在局部变量中:
chVarDis = tk.IntVar()
chVarUn = tk.IntVar()
chVarEn = tk.IntVar()
- 为我们创建的每个
Combobox小部件设置text属性:
text="Disabled"
text="UnChecked"
text="Enabled"
- 将它们的
state设置为deselect/select:
check1.select()
check2.deselect()
check3.select()
- 使用
grid来布局:
check1.grid(column=0, row=4, sticky=tk.W)
check2.grid(column=1, row=4, sticky=tk.W)
check3.grid(column=2, row=4, sticky=tk.W)
前面的步骤最终生成以下代码(GUI_checkbutton_widget.py):

- 运行模块。运行新代码的结果如下 GUI:

让我们深入了解代码以更好地理解它。
它是如何工作的…
步骤 1 到 4 展示了详细信息和截图,步骤 5 显示了代码的重要方面。
在第 47 行、第 52 行和第 57 行,我们创建了三个IntVar类型的变量。在每个这些变量之后的行中,我们创建了一个Checkbutton小部件,传递这些变量。它们将保存Checkbutton小部件的状态(未选中或选中)。默认情况下,这将是0(未选中)或1(选中),因此变量的类型是tkinter整数。
我们将这些Checkbutton小部件放置在我们的主窗口中,因此构造函数传入的第一个参数是小部件的父级,在我们的情况下是win。我们通过其text属性给每个Checkbutton小部件一个不同的标签。
将网格的sticky属性设置为tk.W意味着小部件将被对齐到网格的西边。这非常类似于 Java 语法,意味着它将被对齐到左边。当我们调整我们的 GUI 大小时,小部件将保持在左侧,而不会移动到 GUI 的中心。
第 49 行和第 59 行通过在这两个Checkbutton类实例上调用select()方法,将勾选标记放入Checkbutton小部件中。
我们继续使用网格布局管理器来排列我们的小部件,这将在第二章,布局管理中更详细地解释。
我们已经成功地学习了如何创建具有不同初始状态的复选框。现在,让我们继续下一个菜谱。
使用单选按钮小部件
在这个菜谱中,我们将创建三个单选按钮小部件。我们还将添加一些代码,根据选中的哪个单选按钮改变主表单的颜色。
准备工作
这个菜谱扩展了之前的菜谱,创建具有不同初始状态的复选框。
如何做…
我们将以下代码添加到之前的菜谱中:
-
从
GUI_checkbutton_widget.py模块开始,将其保存为GUI_radiobutton_widget.py。 -
为颜色名称创建三个模块级别的全局变量:
COLOR1 = "Blue"
COLOR2 = "Gold"
COLOR3 = "Red"
- 为单选按钮创建一个回调函数:
if radSel == 1: win.configure(background=COLOR1)
elif radSel == 2: win.configure(background=COLOR2)
elif radSel == 3: win.configure(background=COLOR3)
- 创建三个
tk单选按钮:
rad1 = tk.Radiobutton(win, text=COLOR1, variable=radVar, value=1,
command=radCall)
rad2 = tk.Radiobutton(win, text=COLOR2, variable=radVar, value=2,
command=radCall)
rad3 = tk.Radiobutton(win, text=COLOR3, variable=radVar, value=3,
command=radCall)
- 使用网格布局来定位它们:
rad1.grid(column=0, row=5, sticky=tk.W, columnspan=3)
rad2.grid(column=1, row=5, sticky=tk.W, columnspan=3)
rad3.grid(column=2, row=5, sticky=tk.W, columnspan=3)
前面的步骤最终将生成以下代码(GUI_radiobutton_widget.py):

- 运行代码。运行此代码并选择名为 Gold 的单选按钮将创建以下窗口:

让我们深入了解代码以更好地理解。
它是如何工作的…
在第 75-77 行,我们创建了一些模块级别的全局变量,我们将在创建每个单选按钮以及创建改变主表单背景颜色的回调函数(使用win实例变量)时使用。
我们使用全局变量来简化代码的更改。通过将颜色名称分配给变量并在多个地方使用这个变量,我们可以轻松地尝试不同的颜色。我们不需要进行全局搜索和替换硬编码的字符串(这容易出错),只需更改一行代码,其他所有内容都会正常工作。这被称为DRY 原则,代表不要重复自己。这是我们在本书后面的菜谱中将要使用的一个面向对象的概念。
我们分配给变量的颜色名称(COLOR1、COLOR2、...)是tkinter关键字(技术上,它们是符号名称)。如果我们使用不是tkinter颜色关键字的名称,则代码将无法工作。
第 80 行是回调函数,根据用户的选中改变我们主表单(win)的背景。
在第 87 行,我们创建了一个tk.IntVar变量。重要的是,我们只创建了一个变量,供所有三个单选按钮使用。从截图可以看出,无论我们选择哪个单选按钮,其他所有单选按钮都会自动为我们取消选中。
第 89 行到第 96 行创建了三个单选按钮,并将它们分配给主表单,传递变量给回调函数,该函数用于改变我们主窗口的背景。
虽然这是第一个改变小部件颜色的菜谱,但坦白地说,它看起来有点丑。本书接下来的许多菜谱将解释如何使我们的 GUI 看起来真正令人惊叹。
更多内容…
这里是一个可用的符号颜色名称的小样本,您可以在官方 TCL 文档www.tcl.tk/man/tcl8.5/TkCmd/colors.htm中查找:
| 名称 | 红色 | 绿色 | 蓝色 |
|---|---|---|---|
alice blue |
240 | 248 | 255 |
AliceBlue |
240 | 248 | 255 |
蓝色 |
0 | 0 | 255 |
金色 |
255 | 215 | 0 |
红色 |
255 | 0 | 0 |
一些名称创建相同的颜色,所以alice blue与AliceBlue创建相同的颜色。在这个菜谱中,我们使用了符号名称Blue、Gold和Red。
我们已经成功学习了如何使用单选按钮小部件。现在,让我们继续学习下一个菜谱。
使用滚动文本小部件
ScrolledText小部件比简单的Entry小部件大得多,并且跨越多行。它们像记事本一样,自动换行,并在文本超过ScrolledText小部件高度时自动启用垂直滚动条。
准备中
这个菜谱扩展了之前的菜谱,使用单选按钮小部件。您可以从这个书的每个章节下载代码:github.com/PacktPublishing/Python-GUI-Programming-Cookbook-Third-Edition/。
如何做…
通过添加以下代码行,我们创建了一个ScrolledText小部件:
-
从
GUI_radiobutton_widget.py模块开始,并将其保存为GUI_scrolledtext_widget.py。 -
导入
scrolledtext:
from tkinter import scrolledtext
- 定义宽度和高度的变量:
scrol_w = 30
scrol_h = 3
- 创建一个
ScrolledText小部件:
scr = scrolledtext.ScrolledText(win, width=scrol_w, height=scrol_h, wrap=tk.WORD)
- 定位小部件:
scr.grid(column=0, columnspan=3)
前面的步骤最终将产生以下代码(GUI_scrolledtext_widget.py):

- 运行代码。我们实际上可以在小部件中输入文本,如果我们输入足够的单词,行将自动换行:

一旦我们输入的单词数量超过了小部件的高度,垂直滚动条就会启用。这一切都是默认的,我们不需要编写任何额外的代码来实现这一点:

让我们深入了解代码,以更好地理解它。
它是如何工作的…
在第 11 行,我们导入了包含ScrolledText小部件类的模块。将其添加到模块的顶部,位于其他两个import语句之下。
第 100 行和第 101 行定义了我们即将创建的ScrolledText小部件的宽度和高度。这些是通过实验找到的魔法数字,它们在ScrolledText小部件构造函数的第 102 行中被传递进去。
这些值是通过实验找到的,效果很好。你可以通过将scol_w从 30 改为 50 来实验,并观察效果!
在第 102 行,我们通过传递wrap=tk.WORD来设置小部件的一个属性。通过将wrap属性设置为tk.WORD,我们告诉ScrolledText小部件通过单词来断行,这样我们就不需要在单词中间换行。默认选项是tk.CHAR,它会将任何字符都换行,无论我们是否在单词中间。
第二张截图显示,垂直滚动条向下移动,因为我们正在读取一个较长的文本,它不完全适合我们创建的ScrolledText控制器的x, y维度。
将网格布局的columnspan属性设置为3以适用于ScrolledText小部件,这使得该小部件跨越所有三个列。如果我们不设置此属性,我们的ScrolledText小部件将只位于第一列,这并不是我们想要的。
我们已经成功学习了如何使用滚动文本小部件。现在,让我们继续下一个食谱。
在循环中添加多个小部件
到目前为止,我们已经通过基本上复制粘贴相同的代码并修改变体(例如,列数)来创建了多个相同类型的控件(例如,单选按钮)。在这个食谱中,我们开始重构代码,以使其更少冗余。
准备工作
我们正在重构之前食谱代码的一些部分,使用滚动文本小部件,因此你需要这段代码来完成这个食谱。
如何做到这一点…
这是我们的代码重构方式:
-
从
GUI_scrolledtext_widget.py模块开始,并将其保存为GUI_adding_widgets_in_loop.py。 -
删除全局名称变量,并创建一个 Python 列表代替:
colors = ["Blue", "Gold", "Red"]
- 使用单选按钮变量的
get()函数:
radSel=radVar.get()
- 使用
if ... elif结构创建逻辑:
if radSel == 0: win.configure(background=colors[0])
elif radSel == 1: win.configure(background=color[1])
elif radSel == 2: win.configure(background=color[2])
- 使用循环创建和定位单选按钮:
for col in range(3):
curRad = tk.Radiobutton(win, text=colors[col], cariable=radVar,
value, command=radCall)
curRad.brid(column=col, row=5, sticky=tk.W)
- 运行代码(
GUI_adding_widgets_in_loop.py):
运行此代码将创建与之前相同的窗口,但我们的代码更加整洁且易于维护。这将在我们接下来的菜谱中扩展 GUI 时帮助我们。
它是如何工作的……
在第 77 行,我们将全局变量转换成了一个列表。在第 89 行,我们为名为radVar的tk.IntVar变量设置了一个默认值。这很重要,因为在之前的菜谱中,我们为单选按钮小部件设置了从1开始的值,而在我们新的循环中,使用 Python 的基于零的索引要方便得多。如果我们没有将默认值设置在单选按钮小部件的范围之外,当 GUI 出现时,其中一个单选按钮会被选中。虽然这本身可能不是那么糟糕,但它不会触发回调函数,我们最终会选中一个不执行其工作(即改变主窗口颜色)的单选按钮。
在第 95 行,我们用循环替换了之前硬编码创建的单选按钮小部件的三个实例。这更加简洁(代码行数更少)且易于维护。例如,如果我们想创建 100 个而不是仅仅3个单选按钮小部件,我们只需更改 Python 范围运算符内的数字即可。我们不需要输入或复制粘贴 97 段重复的代码,只需一个数字。
第 82 行显示了修改后的回调函数。
这个菜谱标志着本书第一章节的结束。接下来所有章节的所有菜谱都将基于我们迄今为止构建的 GUI 进行构建,极大地增强了它。
第二章:布局管理
在本章中,我们将探讨如何在控件内排列控件以创建 Python GUI。了解 GUI 布局设计的基本原理将使我们能够创建外观出色的 GUI。有一些技术将帮助我们实现这种布局设计。
网格布局管理器是我们将要使用的重要布局工具之一,它内置在tkinter中。我们可以非常容易地创建菜单栏、标签控件(即 Notebooks)以及许多其他控件。
通过完成本章,你将学习如何排列你的控件,使你的 GUI 看起来真正出色!了解布局管理对于 GUI 设计至关重要,即使你使用其他编程语言——但 Python 确实很棒!
以下截图提供了本章将使用的 Python 模块的概述:

在本章中,我们将使用 Python 3.7 及以上版本来布局我们的 GUI。我们将提供以下菜谱:
-
在标签框架控件内排列多个标签
-
使用填充在控件周围添加空间
-
控件如何动态扩展 GUI
-
通过在框架内嵌入框架来对齐 GUI 控件
-
创建菜单栏
-
创建标签控件
-
使用网格布局管理
在标签框架控件内排列多个标签
LabelFrame控件允许我们以有组织的方式设计我们的 GUI。我们仍然使用网格布局管理器作为我们的主要布局设计工具,但通过使用LabelFrame控件,我们可以对我们的 GUI 设计有更多的控制。
准备工作
我们将首先向我们的 GUI 添加更多控件。我们将在后续的菜谱中使 GUI 完全功能化。在这里,我们将开始使用LabelFrame控件。我们将重用第一章中“向循环添加多个控件”菜谱的 GUI。
如何做到这一点...
-
从第一章的“创建 GUI 表单和添加控件”部分打开
GUI_adding_widgets_in_loop.py,并将其模块保存为GUI_LabelFrame_column_one.py。 -
创建一个
ttk.LabelFrame并将其放置在网格中:
buttons_frame = ttk.LabelFrame(win, text=' Labels in a Frame ')
buttons_frame.grid(column=0, row=7)
# button_frame.grid(column=1, row=7)
- 创建三个
ttk标签,设置它们的文本属性,并将它们放置在网格中:
ttk.Label(buttons_frame, text="Label1").grid(column=0, row=0, sticky=tk.W)
ttk.Label(buttons_frame, text="Label2").grid(column=1, row=0, sticky=tk.W)
ttk.Label(buttons_frame, text="Label3").grid(column=2, row=0, sticky=tk.W)
上述指令将生成GUI_LabelFrame_column_one.py文件中的以下代码:

- 运行代码。它将产生以下 GUI:

取消注释第 111 行并注意LabelFrame的不同对齐方式。
此外,我们可以通过更改代码轻松地垂直对齐标签。为此,请执行以下步骤:
-
打开
GUI_LabelFrame_column_one.py并将其模块保存为GUI_LabelFrame_column_one_vertical.py。 -
按照以下方式更改列和行值:
ttk.Label(button_frame, text="Label1").grid(column=0, row=0)
ttk.Label(button_frame, text="Label2").grid(column=0, row=1)
ttk.Label(button_frame, text="Label3").grid(column=0, row=2)
我们唯一需要更改的是列和行编号。
- 运行
GUI_LabelFrame_column_one_vertical.py文件。现在 GUI 标签框架将如下所示:

现在,让我们幕后了解代码,以便更好地理解。
它是如何工作的...
在第 109 行,我们创建了第一个 ttk LabelFrame 小部件,并将结果实例分配给 buttons_frame 变量。父容器是 win,即我们的主窗口。
在第 114 到 116 行,我们创建了标签并将它们放置到 LabelFrame 中。buttons_frame 是标签的父容器。我们使用重要的网格布局工具在 LabelFrame 内排列标签。这个布局管理器的列和行属性赋予我们控制 GUI 布局的能力。
我们标签的父容器是 LabelFrame 的 buttons_frame 实例变量,而不是主窗口的 win 实例变量。我们可以看到布局层次结构的开始。
我们可以看到,通过列和行属性,改变我们的布局是多么容易。注意我们如何将列设置为 0,以及我们如何通过按顺序编号行值来垂直堆叠我们的标签。
ttk 的名字代表 主题化的 tk。Tk 主题小部件集是在 Tk 8.5 中引入的。
我们已经成功地学习了如何在 LableFrame 小部件内排列几个标签。
参见...
在 通过在框架内嵌入框架对齐 GUI 小部件 菜谱中,我们将 LabelFrame 小部件嵌入到 LabelFrame 小部件中,嵌套它们以控制我们的 GUI 布局。
现在,让我们继续下一个菜谱。
使用填充在小部件周围添加空间
我们的 GUI 进行得很顺利。接下来,我们将通过在小部件周围添加一些空间来改善小部件的视觉外观,以便它们可以呼吸。
准备工作
虽然 tkinter 可能一直以创建不太美观的 GUI 而闻名,但自 8.5 版本以来,这一情况发生了戏剧性的变化。
为了更好地理解 Tk 的主要改进,以下是从官方网站摘录的一段话;您可以在以下链接中找到它:tkdocs.com/tutorial/onepage.html:
"本教程旨在帮助人们快速掌握使用 Tk 构建主流桌面图形用户界面,特别是 Tk 8.5 和 8.6。Tk 8.5 是一个具有里程碑意义的重大版本发布,与大多数人所知和认可的 Tk 旧版本有显著的不同。"
您只需知道如何使用可用的工具和技术。这就是我们接下来要做的。
tkinter 版本 8.6 与 Python 3.7 一起发布。为了使用它,除了 Python 之外,无需安装任何其他东西。
首先展示一种在小部件周围添加间距的简单方法,然后我们将使用循环以更好的方式实现相同的效果。
我们的 LabelFrame 在向底部融入主窗口时看起来有点紧凑。现在让我们修复这个问题。
如何做到这一点...
按照以下步骤在小部件周围添加填充:
-
打开
GUI_LabelFrame_column_one.py并将其保存为GUI_add_padding.py。 -
将
padx和pady添加到网格方法中:
buttons_frame.grid(column=0, row=7, padx=20, pady=40) # padx, pady
- 运行代码。现在我们的
LabelFrame周围有一些空间。这可以在下面的屏幕截图中看到:

我们可以使用循环来添加LabelFrame内包含的标签周围的空间。按照以下步骤操作:
-
打开
GUI_add_padding.py并将其保存为GUI_add_padding_loop.py。 -
在创建三个标签之后添加以下循环:
for child in buttons_frame.winfo_children():
child.grid_configure(padx=8, pady=4)
上述指令生成以下代码:

- 运行
GUI_add_padding_loop.py文件代码。现在LabelFrame小部件周围的标签也有了一些空间:

为了更好地看到这个效果,让我们做以下操作:
-
打开
GUI_add_padding_loop.py并将其保存为GUI_long_label.py。 -
改变
Label1的文本,如下所示:
ttk.Label(buttons_frame, text="Label1 -- sooooo much
loooonger...").grid(column=0, row=0)
- 运行代码。这将生成以下屏幕截图所示的内容,显示了我们的 GUI。注意现在在长标签旁边有空间,紧挨着点。最后一个点没有接触到
LabelFrame,如果没有添加空间,它本会这样接触:

我们还可以移除LabelFrame的名称来查看padx对我们标签位置的影响。让我们开始吧:
-
打开
GUI_add_padding_loop.py并将其保存为GUI_LabelFrame_no_name.py。 -
在创建按钮时,将文本属性设置为空字符串:
buttons_frame = ttk.LabelFrame(win, text='') # no LabelFrame name
- 运行代码。通过将
text属性设置为空字符串,我们移除了之前显示在LabelFrame上的名称。这可以在下面的屏幕截图中看到:

现在,让我们幕后了解代码。
它是如何工作的...
在tkinter中,通过使用内置的padx和pady属性来水平垂直添加空间。这些属性可以用来在许多小部件周围添加空间,分别改善水平和垂直对齐。我们将20像素的空间硬编码到LabelFrame的左右两侧,并在框架的上下两侧添加了40像素的空间。现在我们的LabelFrame比之前更加突出。
grid_configure()函数允许我们在主循环显示 UI 元素之前修改 UI 元素。因此,当我们首次创建小部件时,我们不必硬编码值,我们可以先处理布局,然后在文件末尾,在创建 GUI 之前安排间距。这是一个值得了解的技巧。
winfo_children()函数返回属于buttons_frame变量的所有子元素的列表。这允许我们遍历它们并将填充分配给每个标签。
注意到的一点是,标签右侧的间距实际上并不明显。这是因为LabelFrame的标题比标签的名称长。我们建议您通过使标签名称更长来实验一下。
我们已经成功学习了如何使用填充在控件周围添加空间。现在让我们继续下一个菜谱。
使用小部件动态扩展 GUI
你可能已经注意到,从之前的截图和运行前面的代码中,小部件可以扩展自己,以占据它们需要的空间来视觉上显示它们的文本。
Java 引入了动态 GUI 布局管理的概念。相比之下,像 VS.NET 这样的可视化开发 IDE 以可视化的方式布局 GUI,并且基本上硬编码了 UI 元素的 x 和 y 坐标。
使用tkinter,这种动态能力既带来了一些优势,也带来了一点挑战,因为有时我们希望 GUI 不这么动态扩展时,它却会动态扩展!嗯,我们是动态的 Python 程序员,所以我们可以找出如何充分利用这种出色的行为!
准备工作
在上一个菜谱的开始部分,使用填充在控件周围添加空间,我们添加了一个LabelFrame小部件。这将一些控件移动到了列0的中心。我们可能不希望我们的 GUI 布局有这种修改。我们将在这个菜谱中探讨一些解决方法。
首先,让我们仔细观察一下我们的 GUI 布局中正在进行的微妙细节,以便更好地理解它。
我们正在使用grid布局管理器小部件,它将我们的小部件放置在一个基于零的网格中。这非常类似于 Excel 电子表格或数据库表。
以下是一个具有两行和三列的网格布局管理器示例:
| 行 0;列 0 | 行 0;列 1 | 行 0;列 2 |
|---|---|---|
| 行 1;列 0 | 行 1;列 1 | 行 1;列 2 |
使用网格布局管理器,任何给定列的宽度由该列中最长名称或小部件决定。这反过来又影响所有行。
通过添加我们的LabelFrame小部件并给它一个比硬编码大小小部件更长的标题,我们动态地将这些小部件移动到列 0 的中心。这样做,我们在这些小部件的左右两侧添加了空间。
顺便说一下,因为我们使用了Checkbutton和ScrolledText小部件的粘性属性,所以它们仍然附着在框架的左侧。
让我们更详细地看看本章第一个菜谱在标签框架小部件内排列几个标签的截图。
由于LabelFrame的文本属性(作为LabelFrame的标题显示),比我们的输入一个名字:标签和下面的文本框输入都要长,因此这两个小部件在新的列 0 宽度内动态居中,如下面的截图所示:

注意,下面的标签和输入框不再位于左侧,而是已经移动到网格列的中心。
我们向 GUI_LabelFrame_no_name.py 添加了以下代码以创建一个 LabelFrame,然后在这个框架中放置标签以拉伸 Label 框架及其包含的小部件:
buttons_frame = ttk.LabelFrame(win, text='Labels in a Frame')
buttons_frame.grid(column=0, row=7)
在创建这些小部件时,我们使用了 sticky=tk.W 属性,因此列 0 中的 Checkbutton 和 Radiobutton 小部件没有居中。
对于 ScrolledText 小部件,我们也使用了 sticky=tk.WE,这将小部件绑定到框架的西边(左边)和东边(右边)。
sticky 属性在 tkinter 中可用,并在 grid 控制中定位小部件。
如何做到这一点...
执行以下步骤以完成此配方:
-
打开
GUI_arranging_labels.py并将其保存为GUI_remove_sticky.py。 -
从
ScrolledText小部件中移除sticky属性并观察这种变化带来的效果。
上述指令生成以下代码。注意原始的 src.grid(...) 现在已被注释掉,新的 src.grid(...) 不再具有 sticky 属性:

- 运行代码。现在我们的 GUI 在
ScrolledText小部件的左右两侧都出现了一个新的空间。因为我们使用了columnspan=3属性,所以ScrolledText小部件仍然跨越了所有三个列。这在下图中可以显示:

使用 columnspan 是为了以我们期望的方式排列我们的 GUI。
让我们看看不使用 columnspan 属性会如何通过以下修改破坏我们漂亮的 GUI 设计:
-
打开
GUI_remove_sticky.py并将其保存为GUI_remove_columnspan.py。 -
如果我们移除
columnspan=3,我们将得到下图中显示的 GUI,这不是我们想要的。现在ScrolledText只占用列 0,并且由于其大小,拉伸了布局。 -
运行
GUI_remove_columnspan.py文件并观察输出:

将布局恢复到添加 LabelFrame 之前的一种方法是通过调整网格列位置。让我们开始吧:
-
打开
GUI_remove_columnspan.py并将其保存为GUI_LabelFrame_column_one.py。 -
将列值从
0更改为1:
buttons_frame.frid(column=1, row=7) # now in col 1
- 运行代码。现在我们的 GUI 将如下所示:

让我们深入了解代码,以更好地理解。
它是如何工作的...
由于我们仍在使用单个小部件,我们的布局可能会变得混乱。通过将 LabelFrame 的列值从 0 更改为 1,我们能够将控件恢复到它们原来的位置,以及我们希望它们所在的位置。最左边的标签、文本、Checkbutton、ScrolledText 和 Radiobutton 小部件现在位于我们打算它们所在的位置。第二个标签和位于列 1 的 Entry 文本自动对齐到框架内 Labels in a Frame 小部件长度的中心,所以我们基本上将我们的对齐挑战向右移动了一列。现在它不太明显,因为 Choose a number: 标签的大小几乎与 Labels in a Frame 标题的大小相同,因此列的宽度已经接近由 LabelFrame 生成的新的宽度。
还有更多...
在下一个菜谱中,通过在框架内嵌入框架对齐 GUI 小部件,我们将嵌入框架以避免在本菜谱中刚刚经历的意外小部件错位。
我们已经成功学习了如何使用小部件动态扩展 GUI。现在让我们继续下一个菜谱。
通过在框架内嵌入框架来对齐 GUI 小部件
如果我们在框架内嵌入框架,我们将更好地控制我们的 GUI 布局。这正是本菜谱中我们将要做的。
准备工作
当我们想要使我们的 GUI 真的看起来像我们想要的样子时,Python 的动态行为及其 GUI 模块可能会带来挑战。在本菜谱中,我们将嵌入框架以获得更多对布局的控制。这将在不同 UI 元素之间建立更强的层次结构,使视觉外观更容易实现。
我们将继续使用在前一个菜谱中创建的 GUI,使用小部件动态扩展 GUI。
在这里,我们将创建一个顶层框架,它将包含其他框架和小部件。这将帮助我们获得我们想要的 GUI 布局。
要做到这一点,我们必须在名为 ttk.LabelFrame 的中央框架中嵌入我们的当前控件。这个框架,ttk.LabelFrame,是主父窗口的子框架,所有的控件都将成为这个 ttk.LabelFrame 的子框架。
到目前为止,我们已直接将所有小部件分配给我们的主 GUI 框架。现在,我们只将 LabelFrame 分配给我们的主窗口。之后,我们将使这个 LabelFrame 成为所有小部件的父容器。
这就在我们的 GUI 布局中创建了以下层次结构:

在前面的图中,win 是一个变量,它保存着我们主 GUI tkinter 窗口框架的引用,mighty 是一个变量,它保存着我们 LabelFrame 的引用,并且是主窗口框架(win)的子框架,而 Label 和所有其他小部件现在都放置在 LabelFrame 容器(mighty)中。
如何做到这一点...
执行以下步骤以完成本菜谱:
-
打开
GUI_LabelFrame_column_one.py并将其保存为GUI_embed_frames.py。 -
在我们的 Python 模块顶部添加以下代码:
mighty = ttk.LabelFrame(win, text=' Mighty Python ')
mighty.grid(column=0, row=0, padx=8, pady=4)
接下来,我们将修改以下控件以使用 mighty 作为父级,替换 win。
- 将标签的父级从
win更改为mighty:
a_label = ttk.Label(mighty, text="Enter a name:")
a_label.grid(column=0, row=0)
- 运行
GUI_embed_frames.py文件。这将在以下屏幕截图中显示 GUI:

注意现在所有小部件都包含在 Mighty Python 的 LabelFrame 中,它用几乎看不见的细线包围了它们。接下来,我们可以将 Frame 中的标签 小部件重置为左对齐,而不会破坏我们的 GUI 布局:
-
打开
GUI_embed_frames.py并将其保存为GUI_embed_frames_align.py。 -
将
column更改为0:
buttons_frame = ttk.LabelFrame(mighty, text=' Labels in a Frame ')
buttons_frame.grid(column=0, row=7)
- 运行
GUI_embed_frames_align.py文件。这将在以下屏幕截图中显示 GUI:

哎呀——可能不是这样。虽然我们的框架内的框架向左对齐得很好,但它将我们的顶部小部件推到了中心(默认设置)。
为了将它们左对齐,我们必须通过使用 sticky 属性强制我们的 GUI 布局。通过将其分配为 'W'(西),我们可以强制小部件左对齐。执行以下步骤:
-
打开
GUI_embed_frames_align.py并将其保存为GUI_embed_frames_align_west.py。 -
为标签添加
sticky属性:
a_label = ttk.Label(mighty, text="Enter a name:")
a_label.grid(column=0, row=0, sticky='W')
- 运行代码。这给我们以下 GUI:

让我们将 Entry 小部件在列 0 中左对齐:
-
打开
GUI_embed_frames_align_west.py并将其保存为GUI_embed_frames_align_entry_west.py。 -
使用
sticky属性将输入框左对齐:
name = tk.StringVar()
name_entered = ttk.Entry(mighty, width=12, textvariable=name)
name_entered.grid(column=0, row=1, sticky=tk.W) # align left/West
- 运行
GUI_embed_frames_align_entry_west.py文件。现在标签和输入框都向西方(左侧)对齐:

现在,让我们幕后了解代码以更好地理解。
它是如何工作的…
注意我们是如何对齐标签,但没有对下面的文本框进行对齐。我们必须对所有想要左对齐的控件使用 sticky 属性。我们可以通过使用 winfo_children() 和 grid_configure(sticky='W') 属性在循环中做到这一点,就像我们在本章的 使用填充在控件周围添加空间 菜单中做的那样。
winfo_children() 函数返回属于父级的所有子项的列表。这允许我们遍历所有小部件并更改它们的属性。
使用 tkinter 强制将命名设置为左、右、上或下,这与 Java 的 West、East、North 和 South 非常相似,它们被缩写为 'W'、'E' 等等。我们也可以使用 tk.W 而不是 'W'。这要求我们导入别名为 tk 的 tkinter 模块。
在之前的菜谱中,我们将 'W' 和 'E' 结合起来,使我们的 ScrolledText 小部件同时附着到其容器的左侧和右侧。将 'W' 和 'E' 结合的结果是 'WE'。我们还可以添加更多的组合:'NSE' 将使我们的小部件扩展到顶部、底部和右侧。如果我们表单中只有一个小部件,例如一个按钮,我们可以使用所有选项使其填充整个框架,即 'NSWE'。我们还可以使用元组语法:sticky=(tk.N, tk.S, tk.W, tk.E)。
为了消除我们框架中的 标签 LabelFrame 的长度对我们 GUI 布局其余部分的影响,我们不得将此 LabelFrame 放入与其他小部件相同的 LabelFrame 中。相反,我们需要将其直接分配给主 GUI 表单 (win)。
我们已经成功学习了如何通过嵌套框架来对齐 GUI 小部件。现在让我们继续进行下一个菜谱。
创建菜单栏
在本菜谱中,我们将向主窗口添加菜单栏,向菜单栏添加菜单,然后向菜单添加菜单项。
准备工作
我们将首先学习如何添加菜单栏、几个菜单和几个菜单项。一开始,点击菜单项不会有任何效果。我们将在稍后为菜单项添加功能,例如,当点击退出菜单项时关闭主窗口,并显示帮助 | 关于对话框。
我们将继续扩展我们在上一个菜谱中创建的 GUI,即 通过在框架内嵌套框架对齐 GUI 小部件。
如何做到这一点...
要创建菜单栏,请按照以下步骤操作:
-
打开
GUI_embed_frames_align_entry_west.py并将其保存为GUI_menubar_file.py。 -
从
tkinter导入Menu类:
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu
- 接下来,我们将创建菜单栏。在模块底部,在创建主事件循环之前添加以下代码:
# Creating a Menu Bar
menu_bar = Menu(win)
win.config(menu=menu_bar)
# Create menu and add menu items
file_menu = Menu(menu_bar) # create File menu
file_menu.add_command(label="New") # add File menu item
上述指令从 GUI_menubar_file.py 文件生成了以下代码:

在第 119 行,我们调用导入的 Menu 模块类的构造函数,并传入我们的主 GUI 实例 win。我们将 Menu 对象的实例保存在 menu_bar 变量中。在第 120 行,我们配置我们的 GUI 使用我们新创建的 Menu 作为 GUI 的菜单。
要使其工作,我们还需要将菜单添加到菜单栏,并给它一个标签。
- 菜单项已经被添加到菜单中,但我们仍然需要将菜单添加到菜单栏中:
menu_bar.add_cascade(label="File", menu=file_menu) # add File menu to menu bar and give it a label
- 运行上述代码添加了一个带有菜单项的菜单栏。这在上面的屏幕截图中有显示:

如果这个 tkinter 菜单栏语法看起来有点复杂,不要担心。这只是 tkinter 创建菜单栏的语法。它并不非常 Pythonic。
接下来,我们将向第一个菜单添加第二个菜单项,该菜单已添加到菜单栏中。可以通过以下步骤完成:
-
打开
GUI_menubar_file.py并将其保存为GUI_menubar_exit.py。 -
添加退出菜单项:
file_menu.add_command(label="Exit")
- 运行前面的代码会产生以下结果,即
GUI_menubar_exit.py:

我们可以通过在现有菜单项之间添加一行代码来在菜单项之间添加分隔线。这可以通过以下步骤完成:
-
打开
GUI_menubar_exit.py并将其保存为GUI_menubar_separator.py。 -
添加一个分隔线,如下所示:
file_menu.add_separator()
- 运行前面的代码。在以下屏幕截图中,我们可以看到在两个菜单项之间添加了一条分隔线:

通过将tearoff属性传递给菜单的构造函数,我们可以移除默认情况下出现在菜单第一个菜单项上方的第一条虚线。这可以通过以下步骤完成:
-
打开
GUI_menubar_separator.py并将其保存为GUI_menubar_tearoff.py。 -
将
tearoff属性设置为0:
file_menu = Menu(menu_bar, tearoff=0)
- 运行前面的代码。在以下屏幕截图中,虚线不再出现,我们的 GUI 看起来好多了:

接下来,我们将添加第二个菜单,名为Help,它将水平放置在第一个菜单的右侧。我们将给它一个名为About的菜单项,并将这个第二个菜单添加到菜单栏中。
文件和帮助 | 关于是我们都非常熟悉的常见 Windows GUI 布局,我们可以使用 Python 和tkinter创建相同的菜单:
-
打开
GUI_menubar_tearoff.py并将其保存为GUI_menubar_help.py。 -
添加一个带有菜单项的第二个菜单:
help_menu = Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label="Help", menu=help_menu)
help_menu.add_command(label="About")
前面的说明会产生以下代码,该代码可以在GUI_menubar_help.py文件中找到:

- 运行前面的代码。如图所示,我们有一个带有菜单栏的第二个菜单项:

到目前为止,我们的 GUI 有一个菜单栏和两个包含一些菜单项的菜单。点击它们并不会做太多,直到我们添加一些命令。这就是我们接下来要做的。在创建菜单栏的代码上方执行以下操作:
-
打开
GUI_menubar_help.py并将其保存为GUI_menubar_exit_quit.py。 -
创建一个
quit函数:
def _quit():
win.quit()
win.destroy()
exit()
- 接下来,我们将通过向菜单项添加以下命令将
File | Exit菜单项绑定到该函数:
file_menu.add_command(label="Exit", command=_quit)
前面的说明会产生以下代码,该代码可以在GUI_menubar_exit_quit.py文件中找到:

- 运行代码并点击退出菜单项。以下 GUI 显示了我们所运行代码的输出:

当我们点击退出菜单项时,我们的应用程序确实会退出。
现在,让我们深入了解代码,以更好地理解它。
它是如何工作的…
首先,我们调用Menu类的tkinter构造函数。然后,我们将新创建的菜单分配给我们的主 GUI 窗口。实际上,这变成了菜单栏。我们将其引用保存在名为menu_bar的实例变量中。
接下来,我们创建一个菜单并添加两个菜单项到菜单中。add_cascade()方法将菜单项一个接一个地排列,形成一个垂直布局。
然后,我们在两个菜单项之间添加一条分隔线。这通常用于将相关的菜单项分组(因此得名)。
最后,我们禁用tearoff虚线,使我们的菜单看起来更好。
在不禁用此默认功能的情况下,用户可以撕下菜单从主窗口。我发现这个功能价值不大。您可以自由地通过双击虚线(在禁用此功能之前)来尝试它。如果您使用的是 Mac,这个功能可能没有被启用;如果是这样,您不必担心。
我们接着在菜单栏中添加第二个菜单。我们可以继续使用这种技术添加菜单。
接下来,我们创建一个函数来干净地退出我们的 GUI 应用程序。如何退出正在运行的 Python 应用程序是推荐的方式来结束主事件循环。
我们将我们创建的函数绑定到菜单项上,这是将函数绑定到菜单项的标准方式,使用tkinter的command属性。每次我们想让我们的菜单项真正做些什么时,我们必须将它们中的每一个绑定到一个函数上。
我们遵循推荐的 Python 命名约定,在退出函数前加一个单下划线,这表示这是一个私有函数,不能被我们的代码客户端调用。
更多内容...
我们将在第三章中添加帮助 | 关于功能,外观定制,它介绍了消息框以及更多内容。
我们已经成功地学习了如何创建菜单栏。现在让我们继续到下一个菜谱。
创建标签式小部件
在这个菜谱中,我们将创建标签式小部件来进一步组织我们用tkinter编写的扩展 GUI。
准备工作
为了改进我们的 Python GUI 使用标签,我们将从最基础开始,尽可能少地使用代码。在这个菜谱中,我们将创建一个简单的 GUI,然后从之前的菜谱中添加小部件,将它们放置在这个新的标签布局中。
如何做到这一点...
按照以下步骤创建标签控件,在tkinter中称为Notebook:
-
创建一个新的 Python 模块,并将其命名为
GUI_tabbed.py。 -
在模块顶部,导入
tkinter:
import tkinter as tk
from tkinter import ttk
- 创建
Tk类的实例:
win = tk.Tk()
- 通过
title属性添加标题:
win.title ("Python GUI")
- 使用
ttk的Notebook创建tabControl:
tabControl = ttk.Notebook(win)
- 将标签添加到
tabControl:
tabControl.add(tab1, text-'Tab 1')
- 使用
pack使控件在 GUI 中可见:
tabControl.pack(expand=1, fill="both")
上述指令生成以下代码,可以在GUI_tabbed.py文件中找到:

- 运行上述代码。以下截图显示了运行代码后的 GUI:

此小部件为我们 GUI 设计工具包添加了另一个非常强大的工具。它带有自己的限制,所有这些限制都可以在本菜谱中看到(例如,我们无法重新定位 GUI,它也不显示整个 GUI 标题)。
虽然我们在前面的菜谱中使用了网格布局管理器来简化 GUI,但我们可以使用一个更简单的布局管理器:pack 就是其中之一。
在前面的代码中,我们将 tabControl 和 ttk.Notebook 小部件打包到主 GUI 表单中,将笔记本标签式控制扩展到填充所有侧面。我们可以通过以下步骤向我们的控制中添加第二个标签页并在这两者之间点击:
-
打开
GUI_tabbed.py并将其保存为GUI_tabbed_two.py。 -
添加第二个标签:
tab2 = ttk.Frame(tabControl) # Add a second tab
tabControl.add(tab2, text='Tab 2') # Add second tab
- 运行前面的代码。在下面的屏幕截图中,我们有两个标签页。点击标签 2 以使其获得焦点:

我们非常希望看到窗口的标题;为此,我们必须向我们的标签之一添加一个小部件。这个小部件必须足够宽,以便动态扩展我们的 GUI 以显示窗口标题。按照以下步骤操作:
-
打开
GUI_tabbed_two.py并将其保存为GUI_tabbed_two_mighty.py。 -
添加一个
LabelFrame和一个Label:
# LabelFrame using tab1 as the parent
mighty = ttk.LabelFrame(tab1, text=' Mighty Python ')
mighty.grid(column=0, row=0, padx=8, pady=4)
# Label using mighty as the parent
a_label = ttk.Label(mighty, text="Enter a name:")
a_label.grid(column=0, row=0, sticky='W')
- 运行前面的代码。如以下屏幕截图所示,我们在标签 1内看到 Mighty Python。这扩展了我们的 GUI,但添加的小部件不够大,无法使 GUI 标题可见:

在添加第二个标签以及它们周围的一些间距后,我们将布局拉伸足够长,以便再次看到我们的 GUI 标题:
-
打开
GUI_tabbed_two_mighty.py并将其保存为GUI_tabbed_two_mighty_labels.py。 -
通过循环添加第二个标签和一些间距:
# Add another label
ttk.Label(mighty, text="Choose a number:").grid(column=1, row=0)
# Add some space around each label
for child in mighty.winfo_children():
child.grid_configure(padx=8)
- 运行前面的代码。下面的屏幕截图显示了运行此代码的输出,该输出也可以在
GUI_tabbed_two_mighty_labels.py文件中找到:

我们可以将迄今为止创建的所有小部件都放置到我们新创建的标签页控制中。
您可以从 github.com/PacktPublishing/Python-GUI-Programming-Cookbook-Third-Edition 下载代码。尝试自己创建标签式 GUI。我们在前面的菜谱中创建并排列了所有小部件,但尚未将它们放置在两个不同的标签页上。
查看以下 GUI_tabbed_all_widgets.py 文件:

如您所见,所有小部件都位于标签 1内。让我们将其中一些移动到标签 2:
- 创建第二个
LabelFrame,它将成为我们将要重新定位到标签 2的小部件的容器:
mighty2 = ttk.LabelFrame(tab2, text=' The Snake ')
mighty2.grid(column=0, row=0, padx=8, pady=4)
- 接下来,我们将
Check和Radio按钮移动到标签 2,通过指定新的父容器,即我们命名为mighty2的新变量。以下是我们将应用于所有移动到标签 2 的控制的一个示例:
chVarDis = tk.IntVar()
check 1 = tk.Checkbutton(mighty2, text="Disabled", variable=chVarDis,
state='disabled')
- 运行
GUI_tabbed_all_widgets_both_tabs.py文件。下面的截图显示了运行前面代码后的输出:

我们现在可以点击标签 2 并看到我们重新定位的小部件:

在运行前面的代码后,我们的 GUI 看起来不同。标签 1 比之前包含所有之前创建的小部件时少。
点击重新定位的Radiobutton不再有任何效果,因此我们将它们的动作更改为重命名文本属性,从LabelFrame小部件的标题更改为Radiobuttons显示的名称。当我们点击金色Radiobutton时,我们不再将框架的背景设置为金色。相反,我们替换LabelFrame的文本标题。Python 的蛇现在变为金色:
def radCall():
radSel=radVar.get()
if radSel == 0: mighty2.configure(text ='Blue')
if radSel == 1: mighty2.configure(text ='Gold')
if radSel == 0: mighty2.configure(text ='Red')
-
现在选择任何
RadioButton小部件都会更改LabelFrame的名称。 -
运行
GUI_tabbed_all_widgets_both_tabs_radio.py文件。下面的截图显示了运行此文件中代码的输出:

注意标签框架现在被命名为蓝色。点击金色单选按钮会将标题更改为金色,如下面的截图所示:

现在,让我们深入了解代码以更好地理解它。
它是如何工作的…
在执行创建标签 1 的代码时,它被创建,但没有包含任何信息。然后我们创建了一个第二个标签,标签 2。在创建第二个标签后,我们将原本位于标签 1中的某些小部件移动到了标签 2。添加标签是组织我们不断增加的 GUI 的另一种绝佳方式。这是一种处理 GUI 设计复杂性的好方法。我们可以将小部件分组排列,使它们自然地属于一组,并通过使用标签来让用户摆脱杂乱。
在tkinter中,创建标签是通过Notebook小部件完成的,这是允许我们添加标签控制的工具。与许多其他小部件一样,tkinter notebook小部件附带了一些额外的属性,我们可以使用和配置。探索我们可用的tkinter小部件的额外功能的一个绝佳起点是官方网站:docs.python.org/3.1/library/tkinter.ttk.html#notebook。
我们已经成功学习了如何创建标签小部件。现在让我们继续下一个菜谱。
使用网格布局管理器
网格布局管理器是我们可用的最有用的布局工具之一。虽然pack等布局工具简单易用,但grid为我们提供了对布局的更多控制——尤其是在我们将grid与嵌入式frames结合使用时。
我们已经在许多菜谱中使用了它,例如,因为它真的很强大。
准备工作…
在这个菜谱中,我们将回顾一些网格布局管理器技术。我们已经在使用它们,但我们将在这里更详细地探讨它们。
如何做到这一点…
在本章中,我们创建了行和列,这是 GUI 设计的数据库方法(MS Excel 也这样做)。我们硬编码了第一行。然而,如果我们忘记指定下一行应该在哪里,tkinter会自动填充,甚至我们都没有注意到。
为了观察这一点,让我们从我们之前工作过的菜谱中取代码:
-
打开
GUI_tabbed_all_widgets_both_tabs_radio.py。 -
将
scr.grid行注释掉,如下所示:

tkinter会自动将缺失的行添加到我们没有指定任何特定行的位置。
- 运行代码并注意我们的单选按钮突然出现在文本小部件的中间!

现在,让我们幕后了解代码,以便更好地理解。
它是如何工作的…
我们在行 1 上布局了Entry小部件。然而,我们忘记指定我们的ScrolledText小部件所在的行,我们通过scr变量引用它。然后,我们添加了想要布局在行3的Radiobutton小部件。
这很好,因为tkinter自动增加了我们的ScrolledText小部件的行位置,使其使用下一个最高的行号,即行2。
看着我们的代码,没有意识到我们忘记明确地将我们的ScrolledText小部件定位到行2,我们可能会认为那里没有任何内容。
由于这个原因,我们可能会尝试以下方法。如果我们将curRad变量设置为使用行2,我们可能会在菜谱的如何做到这一点…部分的最终截图中获得一个不愉快的惊喜。
现在,让我们幕后了解代码,以便更好地理解。
注意我们的RadioButton(s)行突然出现在我们的ScrolledText小部件的中间!这绝对不是我们想要我们的 GUI 看起来像的!
如果我们忘记明确指定行号,默认情况下,tkinter将使用下一个可用的行。
我们还使用了columnspan属性来确保我们的小部件不会仅限于一个列,如下面的截图所示:

上述截图展示了我们如何确保我们的ScrolledText小部件横跨 GUI 中的所有列。
第三章:界面外观定制
在本章中,我们将通过更改一些属性来定制我们的 GUI 中的某些小部件。我们还将介绍一些tkinter提供的新的小部件。
在使用 Python 创建工具提示菜谱中,我们将创建一个ToolTip面向对象风格的类,它将是我们至今为止使用的单个 Python 模块的一部分。
你将学习如何创建不同的消息框,更改 GUI 窗口标题,以及更多。我们将使用旋转框控件来学习如何应用不同的样式。
界面外观定制是 GUI 设计的重要组成部分,因为它使我们的 GUI 看起来更专业。
这是本章 Python 模块的概述:

在本章中,我们将使用 Python 3.7 及以上版本来定制我们的 GUI。我们将涵盖以下菜谱:
-
创建消息框 – 信息、警告和错误
-
如何创建独立的消息框
-
如何创建 tkinter 窗口的标题
-
更改主根窗口的图标
-
使用旋转框控件
-
应用浮雕效果 – 小部件的凹凸外观
-
使用 Python 创建工具提示
-
将进度条添加到 GUI 中
-
如何使用画布控件
创建消息框 – 信息、警告和错误
消息框是一个弹出窗口,向用户提供反馈。它可以提供信息,暗示潜在问题,以及灾难性的错误。
使用 Python 创建消息框非常简单。
准备工作
我们将在第二章,布局管理中创建的创建标签控件菜单位置添加功能。
代码来自GUI_tabbed_all_widgets_both_tabs.py。在大多数应用程序中,当点击帮助 | 关于菜单时,用户通常会收到信息反馈。我们将从这个信息开始,然后改变设计模式以显示警告和错误。
如何操作…
以下是创建 Python 中消息框的步骤:
-
从第二章,布局管理中打开
GUI_tabbed_all_widgets_both_tabs.py,并将其模块保存为GUI_message_box.py。 -
将以下代码行添加到模块顶部的导入语句部分:
from tkinter import messagebox as msg
- 接下来,创建一个回调函数,该函数将显示一个消息框。我们必须将回调函数的代码放在将回调附加到菜单项的代码之上,因为这部分仍然是过程式代码,而不是面向对象的代码。
在创建帮助菜单的代码上方添加以下代码:
def _msgBox():
msg.showinfo('Python Message Info Box', 'A Python GUI created
using tkinter:\nThe year is 2019.')
上述指令生成以下代码,GUI_message_box.py:

- 运行代码。现在点击帮助 | 关于将导致以下弹出窗口出现:

让我们将此代码转换为警告消息框弹出窗口:
-
打开
GUI_message_box.py并将模块保存为GUI_message_box_warning.py。 -
注释掉
msg.showinfo行。 -
将信息框代码替换为警告框代码:
msg.showwarning('Python Message Warning Box', 'A Python GUI created using tkinter:' '\nWarning: There might be a bug in this code.')
上述指令产生以下代码,GUI_message_box_warning.py:

- 运行上述代码现在将产生以下略微修改的消息框:

显示错误消息框很简单,通常警告用户存在严重问题。正如我们在前面的代码片段中所做的那样,注释掉上一行并添加以下代码,就像我们在这里所做的那样:
-
打开
GUI_message_box_warning.py并将模块保存为GUI_message_box_error.py。 -
将警告框代码替换为错误框代码:
msg.showerror('Python Message Error Box', 'A Python GUI created using tkinter:'
'\nError: Houston ~ we DO have a serious PROBLEM!')
上述指令产生以下代码:

- 运行
GUI_message_box_error.py文件。错误消息看起来像这样:

有不同的消息框显示多个“确定”按钮,我们可以根据用户的选项编程我们的响应。
以下是一个简单示例,说明了这项技术:
-
打开
GUI_message_box_error.py并将模块保存为GUI_message_box_yes_no_cancel.py。 -
将错误框替换为
yes_no_cancel框:
answer = msg.askyesnocancel("Python Message Multi Choice Box", "Are you sure you really wish to do this?")
上述指令产生以下代码:

- 运行
GUI_message_box_yes_no_cancel.py文件。运行此 GUI 代码将弹出一个弹出窗口,用户响应可以用于根据事件驱动的 GUI 循环的答案进行分支,通过将其保存在answer变量中:

使用 Eclipse 显示的控制台输出表明,点击“是”按钮将布尔值 True 分配给 answer 变量:

例如,我们可以使用以下代码:
If answer == True:
<do something>
点击“否”返回 False,点击“取消”返回 None。
现在,让我们深入了解代码,以更好地理解它。
它是如何工作的…
我们为所有的 GUI_message_box Python 模块添加了另一个回调函数,def _msgBox(),并将其附加到帮助菜单的 command 属性以处理点击事件。现在,当我们点击帮助 | 关于菜单时,将发生一个动作。我们正在创建并显示最常见的弹出消息框对话框。它们是模态的,因此用户在点击“确定”按钮之前不能使用 GUI。
在第一个示例中,我们显示了一个信息框,如左边的图标所示。接下来,我们创建警告和错误消息框,它们会自动更改与弹出窗口关联的图标。我们只需指定我们想要显示的消息框即可。
askyesnocancel 消息框根据用户点击的按钮返回不同的值。我们可以将答案捕获到变量中,并根据所选答案编写不同的代码。
我们已经成功学习了如何创建消息框。现在,让我们继续学习下一个技巧。
如何创建独立的消息框
在这个技巧中,我们将创建我们的tkinter消息框作为独立的顶级 GUI 窗口。
你首先会注意到,这样做会导致一个额外的窗口,所以我们将探讨隐藏这个窗口的方法。
在之前的技巧中,我们通过主 GUI 表中的帮助 | 关于菜单调用了tkinter消息框。
那么,我们为什么想要创建一个独立的消息框呢?
一个原因是我们可能需要自定义我们的消息框并在多个 GUI 中重用它们。我们不需要在设计的每个 Python GUI 中复制和粘贴相同的代码,我们可以将其从主 GUI 代码中提取出来。这创建了一个小的可重用组件,然后我们可以将其导入到不同的 Python GUI 中。
准备工作
在之前的技巧中,我们已经创建了消息框的标题,创建消息框 - 信息、警告和错误。我们不会重用之前的代码,而是使用非常少的 Python 代码构建一个新的 GUI。
如何做到这一点...
我们可以创建一个简单的消息框如下:
-
创建一个新的模块并将其保存为
GUI_independent_msg.py。 -
添加以下两行代码,这就是所有需要做的:
from tkinter import messagebox as msg
msg.showinfo('Python GUI created using tkinter:\nThe year is 2019')
- 运行
GUI_independent_msg.py文件。这将导致以下两个窗口:

这看起来并不像我们想象中的样子。现在,我们有两个窗口,一个是我们不想要的,另一个窗口显示其文本作为标题。
哎呀!
让我们现在解决这个问题。我们可以通过添加一个单引号或双引号后跟一个逗号来更改 Python 代码:
-
打开
GUI_independent_msg.py并将模块保存为GUI_independent_msg_info.py。 -
创建一个空标题:
from tkinter import messagebox as msg
msg.showinfo('', 'Python GUI created using tkinter:\nThe year is 2019')
- 运行
GUI_independent_msg_info.py文件。现在,我们没有标题,但我们的文本最终出现在弹出窗口中,正如我们预期的:

第一个参数是标题,第二个参数是在弹出消息框中显示的文本。通过添加一个空的单引号或双引号后跟一个逗号,我们可以将我们的文本从标题移动到弹出消息框中。
我们仍然需要一个标题,我们肯定希望去掉这个不必要的第二个窗口。第二个窗口是由 Windows 事件循环引起的。我们可以通过抑制它来去掉它。
添加以下代码:
-
打开
GUI_independent_msg_info.py并将模块保存为GUI_independent_msg_one_window.py。 -
导入
Tk创建Tk类的一个实例,并调用withdraw方法:
from tkinter import Tk
root = Tk()
root.withdraw()
现在,我们只有一个窗口。withdraw()方法移除了我们不感兴趣的浮动调试窗口。
- 运行代码。这将导致以下窗口:

为了添加一个标题,我们只需要将字符串放入我们的第一个空参数中。
例如,考虑以下代码片段:
-
打开
GUI_independent_msg_one_window.py并将模块保存为GUI_independent_msg_one_window_title.py。 -
通过在第一个参数位置添加一些文字来给它一个标题:
msg.showinfo('This is a Title', 'Python GUI created using tkinter:\nThe year is 2019')
上述指令产生以下代码:

- 运行
GUI_independent_msg_one_window_title.py文件。现在,我们的对话框有一个标题,如下面的截图所示:

现在,让我们深入了解代码,以更好地理解它。
它是如何工作的…
我们向消息框的 tkinter 构造函数传递更多的参数,以添加窗口表单的标题并在消息框中显示文本,而不是将其显示为标题。这是由于我们传递参数的位置。如果我们省略一个空引号或双引号,那么消息框小部件将参数的第一个位置作为标题,而不是消息框内要显示的文本。通过传递一个空引号后跟一个逗号,我们改变了消息框显示我们传递给函数的文本的位置。
我们通过在主根窗口上调用 withdraw() 方法来抑制由 tkinter 消息框小部件自动创建的第二个弹出窗口。
通过在之前为空的字符串中添加一些文字,我们给消息框添加了一个标题。这表明不同的消息框,除了它们显示的主要消息外,还有它们自己的自定义标题。这可以用来将几个不同的消息框关联到相同的功能。
我们已经成功地学习了如何创建独立的消息框。现在,让我们继续下一个菜谱。
如何创建 tkinter 窗口的标题
改变 tkinter 主根窗口标题的原则与我们之前讨论的相同:如何创建独立的消息框。我们只需将一个字符串作为第一个参数传递给小部件的构造函数。
准备工作
我们不是创建一个弹出对话框,而是创建主根窗口并为其添加标题。
如何做到这一点…
以下代码创建主窗口并为其添加标题。我们已经在之前的菜谱中这样做过;例如,在 创建标签小部件 菜谱中,在 第二章,布局管理。这里,我们只关注我们 GUI 的这个方面:
-
打开
GUI_tabbed_all_widgets_both_tabs.py并将模块保存为GUI_title.py。 -
给主窗口添加标题:
import tkinter as tk
win = tk.Tk() # Create instance
win.title("Python GUI") # Add a title
- 运行
GUI_title.py文件。这将产生以下两个标签:

现在,让我们深入了解代码,以更好地理解它。
它是如何工作的…
通过使用 tkinter 内置的 title 属性,我们为主根窗口添加了一个标题。在创建一个 Tk() 实例之后,我们可以使用所有内置的 tkinter 属性来自定义我们的 GUI。
我们已经成功学习了如何为tkinter窗口表单创建标题。现在,让我们继续下一个菜谱。
更改主根窗口的图标
定制我们的 GUI 的一种方法是为它提供一个与tkinter默认图标不同的图标。以下是我们的操作方法。
准备工作
我们正在改进我们的 GUI,这是从第二章的创建标签式小部件菜谱中,布局管理。我们将使用 Python 附带的图标,但你也可以使用任何你认为有用的图标。确保你有图标所在位置的完整路径,否则可能会出错。
如何操作...
对于这个例子,我已经将图标从安装 Python 3.7 的地方复制到代码所在的同一文件夹中。以下截图显示了我们将要使用的图标:

为了使用此图标或其他图标文件,请执行以下步骤:
-
打开
GUI_title.py并将模块保存为GUI_icon.py。 -
将以下代码放置在主事件循环之上:
# Change the main windows icon
win.iconbitmap('pyc.ico')
- 运行
GUI_icon.py文件。观察 GUI 左上角的默认羽毛图标是如何改变的:

现在,让我们深入了解代码,以更好地理解它。
它是如何工作的...
这是tkinter附带的一个属性,tkinter与 Python 3.7 及以上版本一起提供。我们使用iconbitmap属性通过传递到图标的相对路径来更改我们的主根窗口的图标,这会覆盖tkinter的默认图标,用我们选择的图标替换它。
如果图标位于与 Python 模块相同的文件夹中,我们可以简单地通过图标名称来引用它,而无需传递图标位置的完整路径。
我们已经成功学习了如何更改主根窗口的图标。现在,让我们继续下一个菜谱。
使用旋钮控制
在这个菜谱中,我们将使用一个Spinbox小部件,并且我们还将把键盘上的Enter键绑定到我们的某个小部件上。Spinbox小部件是一个单行小部件,类似于Entry小部件,它还具有限制它将显示的值的额外功能。它还有一些小的上下箭头,可以滚动上下值。
准备工作
我们将使用我们的标签式 GUI,来自如何创建 tkinter 窗口标题菜谱,并在ScrolledText控件上方添加一个Spinbox小部件。这只需要我们将ScrolledText的行值增加一行,并在Entry小部件上方插入我们的新Spinbox控件。
如何操作...
首先,我们通过以下步骤添加Spinbox控件:
-
打开
GUI_title.py并将模块保存为GUI_spinbox.py。 -
将以下代码放置在
ScrolledText小部件之上:
# Adding a Spinbox widget
spin = Spinbox(mighty, from_=0, to=10)
spin.grid(column=0, row=2)
- 运行代码。这将按以下方式修改我们的 GUI:

接下来,我们将减小Spinbox小部件的大小:
-
打开
GUI_spinbox.py并将模块保存为GUI_spinbox_small.py。 -
在创建
Spinbox小部件时添加一个width属性:
spin = Spinbox(mighty, from_=0, to=10, width=5)
- 运行前面的代码会产生以下 GUI:

接下来,我们添加另一个属性来进一步自定义我们的小部件;bd是borderwidth属性的缩写,它改变了围绕滚动框的边框宽度:
-
打开
GUI_spinbox_small.py并将模块保存为GUI_spinbox_small_bd.py。 -
添加一个
bd属性,将其大小设置为8:
spin = Spinbox(mighty, from_=0, to=10, width=5 , bd=8)
- 运行前面的代码会产生以下 GUI:

接下来,我们通过创建一个回调并将其链接到控制条来为小部件添加功能。
以下步骤展示了如何将Spinbox小部件的选择打印到ScrolledText以及到stdout。名为scrol的变量是我们对ScrolledText小部件的引用:
-
打开
GUI_spinbox_small_bd.py并将模块保存为GUI_spinbox_small_bd_scrol.py。 -
在创建
Spinbox小部件的上方编写一个回调函数,并将其分配给Spinbox小部件的command属性:
# Spinbox callback
def _spin():
value = spin.get()
print(value)
scrol.insert(tk.INSERT, value + '\n') # <-- add a newline
spin = Spinbox(mighty, from_=0, to=10, width=5, bd=8,
command=_spin) # <-- command=_spin
- 点击
Spinbox箭头时,运行GUI_spinbox_small_bd_scrol.py文件会产生以下 GUI:

我们可以使用一组值而不是使用范围,通过执行以下指令:
-
打开
GUI_spinbox_small_bd_scrol.py并将模块保存为GUI_spinbox_small_bd_scrol_values.py。 -
在创建
Spinbox小部件时添加values属性,替换from_=0, to=10,并分配一个数字元组:
# Adding a Spinbox widget using a set of values
spin = Spinbox(mighty, values=(1, 2, 4, 42, 100), width=5, bd=8,
command=_spin)
spin.grid(column=0, row=2)
- 运行代码。这将创建以下 GUI 输出:

现在,让我们深入了解代码,以更好地理解它。
它是如何工作的…
注意,在第一个 Python 模块GUI_spinbox.py中,我们的新Spinbox控制条的默认宽度为20,这推高了该列中所有控制条的列宽。这不是我们想要的。我们给小部件指定了一个从0到10的范围。
在第二个 Python 模块GUI_spinbox_small.py中,我们减小了Spinbox控制条的宽度,使其与列中心对齐。
在第三个 Python 模块GUI_spinbox_small_bd.py中,我们添加了Spinbox的borderwidth属性,这使得整个Spinbox看起来不再扁平,而是三维的。
在第四个 Python 模块GUI_spinbox_small_bd_scrol.py中,我们添加了一个回调函数,用于在ScrolledText小部件中显示所选的数字,并将其打印到标准输出流。我们在回调函数def _spin()中添加了\n以在新的行中插入值。
注意默认值并没有被打印出来。只有当我们点击控制条时,回调函数才会被调用。通过点击带有默认值0的下箭头,我们可以打印出0值。
最后,在GUI_spinbox_small_bd_scrol_values.py中,我们将可用的值限制为硬编码的集合。这也可以以数据源的形式读取(例如,文本或 XML 文件)。
我们已经成功地学习了如何使用微调框控制。现在,让我们继续学习下一个菜谱。
应用 relief – 小部件的凹凸外观
我们可以通过使用一个属性来控制Spinbox小部件的外观,使其以不同的格式出现,例如凹或凸。这个属性是relief属性。
准备中
我们将添加一个额外的Spinbox控制,以演示小部件的可用外观,使用Spinbox控制的relief属性。
如何做到这一点…
当我们创建第二个Spinbox时,让我们也增加borderwidth以区分第二个Spinbox和第一个Spinbox:
-
打开
GUI_spinbox_small_bd_scrol_values.py并将模块保存为GUI_spinbox_two_sunken.py。 -
在第一个
Spinbox下方添加一个第二个Spinbox并设置bd=20:
# Adding a second Spinbox widget
spin2 = Spinbox(mighty, values=(0, 50, 100), width=5, bd=20,
command=_spin2) # <-- new function
spin2.grid(column=1, row=2)
- 我们还将为
command属性创建一个新的回调函数_spin2。将此函数放在刚刚显示的代码上方,即创建第二个Spinbox的地方:
# Spinbox2 callback function
def _spin2():
value = spin2.get()
print(value)
scrol.insert(tk.INSERT, value + '\n')
# <-- write to same ScrolledText
- 运行代码。这将创建以下 GUI 输出:

我们的两个微调框看起来不同,但这只是因为我们指定的borderwidth(bd)不同。两个小部件看起来都是三维的,这在第二个我们添加的Spinbox中更为明显。
尽管我们在创建微调框时没有指定relief属性,但它们实际上都有relief样式。
如果未指定,relief样式默认为SUNKEN。
这里是可以设置的relief属性选项:
-
tk.SUNKEN -
tk.RAISED -
tk.FLAT -
tk.GROOVE -
tk.RIDGE
我们将tkinter导入为tk。这就是为什么我们可以调用relief属性为tk.SUNKEN,等等。
通过将不同的可用选项分配给relief属性,我们可以为这个小部件创建不同的外观。
将tk.RIDGE relief 和边框宽度减少到与我们的第一个Spinbox小部件相同的值,结果如下 GUI:
-
打开
GUI_spinbox_two_sunken.py并将模块保存为GUI_spinbox_two_ridge.py。 -
将
relief设置为tk.RIDGE:
spin2 = Spinbox(mighty, values=(0, 50, 100), width=5, bd=9, command=_spin2, relief=tk.RIDGE)
- 运行代码。运行代码后,可以得到以下 GUI 界面:

注意右侧第二个Spinbox小部件外观的差异。
现在,让我们深入了解代码,以更好地理解它。
它是如何工作的…
首先,我们在第二列(index == 1)中创建了一个第二个Spinbox,它默认为SUNKEN,因此看起来与我们的第一个Spinbox相似。我们通过增加第二个控制(右侧的控制)的边框宽度来区分这两个小部件。
接下来,我们明确设置Spinbox小部件的relief属性。我们将borderwidth设置为与我们的第一个Spinbox相同,因为通过给它不同的relief,差异在没有改变任何其他属性的情况下变得明显。
下面是一个不同relief选项的示例,GUI_spinbox_two_ridge.py:

下面是一个截图,展示了这些relief属性创建的效果:

我们已经成功地学习了如何使用和应用relief、凹陷和凸起外观到小部件上。现在,让我们继续到下一个示例。
使用 Python 创建工具提示
这个示例将向您展示如何创建工具提示。当用户将鼠标悬停在控件上时,将以工具提示的形式提供额外的信息。
我们将把这个附加信息编码到我们的 GUI 中。
准备工作
我们将向我们的 GUI 添加更多有用的功能。令人惊讶的是,给我们的控件添加工具提示应该是简单的,但实际上并不像我们希望的那样简单。
为了实现这个期望的功能,我们将我们的工具提示代码放置在其自己的面向对象类中。
如何做到这一点…
创建工具提示的步骤如下:
-
打开
GUI_spinbox_small_bd_scrol_values.py并将模块保存为GUI_tooltip.py。 -
在
import语句下面添加以下类:
class ToolTip(object):
def __init__(self, widget, tip_text=None):
self.widget = widget
self.tip_text = tip_text
widget.bind('<Enter>', self.mouse_enter)
widget.bind('<Leave>', self.mouse_leave)
- 在
__init__下面添加两个新方法:
def mouse_enter(self, _event):
self.show_tooltip()
def mouse_leave(self, _event):
self.hide_tooltip()
- 在这两个方法下面添加另一个方法,并命名为
show_tooltip:
def show_tooltip(self):
if self.tip_window:
x_left = self.widget.winfo_rootx()
y_top = self.widget.winfo_rooty() - 18
self.tip_window = tk.Toplevel(self.widget)
self.tip_window.overrideredirect(True)
self.tip_window.geometry("+%d+%d" % (x_left, y_top))
label = tk.Label(self.tip_window, text=self.tip_text,
justify=tk.LEFT, background="#ffffe0", relief=tk.SOLID,
borderwidth=1, font=("tahoma", "8", "normal"))
label.pack(ipadx=1)
- 在
show_tooltip方法下面添加另一个方法,并命名为hide_tooltip:
def hide_tooltip(self):
if self.tip_window:
self.tip_window.destroy()
- 在类下面和创建
Spinbox小部件的代码下面创建ToolTip类的一个实例,传递Spinbox变量spin:
# Adding a Spinbox widget
spin = Spinbox(mighty, values=(1, 2, 4, 42, 100), width=5, bd=9, command=_spin) spin.grid(column=0, row=2)
# Add a Tooltip to the Spinbox
ToolTip(spin, 'This is a Spin control') # <-- add this code
- 对
Spinbox小部件下面的ScrolledText小部件执行相同的步骤:
scrol = scrolledtext.ScrolledText(mighty, width=scrol_w, height=scrol_h, wrap=tk.WORD)
scrol.grid(column=0, row=3, sticky='WE', columnspan=3)
# Add a Tooltip to the ScrolledText widget
ToolTip(scrol, 'This is a ScrolledText widget') # <-- add this code
- 运行代码并将鼠标悬停在
ScrolledText小部件上:

现在,让我们深入了解代码,以更好地理解它。
它是如何工作的…
这本书中我们将要介绍的面向对象编程(OOP)的开始。这可能会显得有些高级,但请不要担心;我们会解释一切,并且它确实可行。
我们首先创建了一个新类,并将其命名为ToolTip。在初始化方法__init__中,我们期望传递widget和tip_text。我们使用self关键字将这些保存为实例变量。
接下来,我们将Enter和Leave鼠标事件绑定到我们刚刚在初始化器下面创建的新方法。当我们将鼠标悬停在为我们创建了工具提示的控件上时,这些方法会自动调用。这两个方法调用我们下面创建的两个方法。
show_tooltip方法检查在创建ToolTip类实例时是否传递了文本,如果是,我们就使用winfo_rootx和winfo_rooty获取小部件的左上角坐标。这些是我们可以使用的tkinter内置方法。
对于 y_top 变量,我们 减去 18,这会定位小部件。这看起来可能有些反直觉,但 tkinter 坐标系统从屏幕左上角的 0,0 开始,所以从 y 坐标中减去实际上是将它向上移动。
然后,我们为工具提示创建一个 tkinter 的 TopLevel 窗口。设置 overrideredirect(True) 会移除围绕工具提示的标题栏,而我们不想那样做。
我们使用 geometry 来定位我们的 tooltip,然后创建一个 Label 小部件。我们使 tooltip 成为标签的父级。然后我们使用 tooltip 的 text 属性来显示在标签内部。
然后,我们 pack Label 小部件,使其可见。
在 hide_tooltip 方法中,我们检查是否已创建工具提示,如果是,则调用其上的 destroy 方法。否则,每次我们将鼠标悬停在某个小部件上然后移开鼠标时,工具提示都不会消失。
在我们的 ToolTip 类代码就绪后,我们现在可以为我们的小部件创建工具提示。我们通过创建 ToolTip 类的实例来实现,传入我们的小部件变量和希望显示的文本。
我们对 ScolledText 和 Spinbox 小部件也这样做。
我们已经成功学习了如何使用 Python 创建工具提示。现在,让我们继续下一个配方。
将进度条添加到 GUI 中
在这个配方中,我们将向我们的 GUI 添加一个 Progressbar。添加 ttk.Progressbar 非常简单,我们将演示如何启动和停止 Progressbar。这个配方还将向您展示如何延迟停止 Progressbar,以及如何在循环中运行它。
Progressbar 通常用于显示长时间运行进程的当前状态。
准备工作
我们将在之前的一个配方中开发的 GUI 的 Tab 2 中添加 Progressbar:使用旋钮控制。
如何做到这一点...
创建 Progressbar 和一些新的 Buttons 以启动和停止 Progressbar 的步骤如下:
-
打开
GUI_spinbox_small_bd_scrol_values.py并将模块保存为GUI_progressbar.py。 -
在模块顶部添加
sleep到导入中:
from time import sleep # careful - this can freeze the GU
- 在创建三个
Radiobutton小部件的代码下方添加Progressbar:
# Now we are creating all three Radiobutton widgets within one loop
for col in range(3):
curRad = tk.Radiobutton(mighty2, text=colors[col],
variable=radVar, value=col, command=radCall)
curRad.grid(column=col, row=1, sticky=tk.W) # row=6
# Add a Progressbar to Tab 2 # <--- add this code here
progress_bar = ttk.Progressbar(tab2, orient='horizontal', length=286, mode='determinate')
progress_bar.grid(column=0, row=3, pady=2)
- 接下来,我们编写一个回调函数来更新
Progressbar:
# update progressbar in callback loop
def run_progressbar():
progress_bar["maximum"] = 100
for i in range(101):
sleep(0.05)
progress_bar["value"] = i # increment progressbar
progress_bar.update() # have to call update() in loop
progress_bar["value"] = 0 # reset/clear progressbar
- 然后,我们在前面代码的下方编写以下三个函数:
def start_progressbar():
progress_bar.start()
def stop_progressbar():
progress_bar.stop()
def progressbar_stop_after(wait_ms=1000):
win.after(wait_ms, progress_bar.stop)
- 我们将重用
buttons_frame和LabelFrame,但用新代码替换标签。更改以下代码:
# PREVIOUS CODE -- REPLACE WITH BELOW CODE
# Create a container to hold labels
buttons_frame = ttk.LabelFrame(mighty2, text=' Labels in a Frame ')
buttons_frame.grid(column=0, row=7)
# NEW CODE
# Create a container to hold buttons
buttons_frame = ttk.LabelFrame(mighty2, text=' ProgressBar ')
buttons_frame.grid(column=0, row=2, sticky='W', columnspan=2)
- 删除位于
buttons_frame中的先前标签:
# DELETE THE LABELS BELOW
# Place labels into the container element
ttk.Label(buttons_frame, text="Label1").grid(column=0, row=0, sticky=tk.W)
ttk.Label(buttons_frame, text="Label2").grid(column=1, row=0, sticky=tk.W)
ttk.Label(buttons_frame, text="Label3").grid(column=2, row=0, sticky=tk.W)
- 创建四个新的按钮。
buttons_frame是它们的父级:
# Add Buttons for Progressbar commands
ttk.Button(buttons_frame, text=" Run Progressbar ",
command=run_progressbar).grid(column=0, row=0, sticky='W')
ttk.Button(buttons_frame, text=" Start Progressbar ",
command=start_progressbar).grid(column=0, row=1, sticky='W')
ttk.Button(buttons_frame, text=" Stop immediately ",
command=stop_progressbar).grid(column=0, row=2, sticky='W')
ttk.Button(buttons_frame, text=" Stop after second ",
command=progressbar_stop_after).grid(column=0, row=3, sticky='W')
- 在循环中为
buttons_frame的子项添加额外的填充:
for child in buttons_frame.winfo_children():
child.grid_configure(padx=2, pady=2)
- 为 Tab2 的所有子项添加额外的填充:
for child in mighty2.winfo_children():
child.grid_configure(padx=8, pady=2)
- 运行代码。点击运行进度条按钮后,得到以下 GUI:

现在,让我们深入了解代码以更好地理解它。
它是如何工作的...
首先,我们导入了 sleep,否则 Progressbar 会太快而看不见。但是,使用 sleep 时要小心,因为它可能会冻结 GUI。我们在这里使用它来模拟一个长时间运行的过程,这通常是 Progressbar 被使用的地方。
然后,我们创建一个 ttk.Progressbar 小部件并将其分配给 Tab2。
我们创建了自己的回调函数 run_progressbar,在其中我们从 0 开始,使用 sleep 循环,一旦我们达到设置的 100 最大值,并且一旦 Progressbar 达到末端,我们将其重置为 0,这样 Progressbar 就会再次显示为空。
我们创建另一个函数 start_progressbar,在其中我们使用 ttk.Progressbar 内置的 start 方法。如果我们不在 Progressbar 运行时调用 stop 方法,一旦它到达末端,它将从头开始再次运行,形成一个无限循环,直到调用 stop。
stop_progressbar 函数会立即停止 Progressbar。
progressbar_stop_after 函数延迟停止一段时间。我们将其默认设置为 1000 毫秒,即 1 秒,但可以向这个函数传递不同的值。
我们通过在主 GUI 窗口的引用上调用 after 函数来实现这个延迟,我们将它命名为 win。
这四个函数展示了两种启动和停止 Progressbar 的方法。
在 start_progressbar 函数上调用 Stop 函数并不会停止它;它将完成循环。
我们创建了四个新的按钮,并将我们的函数分配给它们的 command 属性。现在点击按钮会调用这些函数。
我们已经成功学习了如何创建 Progressbar 并启动和停止它。现在,让我们继续到下一个菜谱。
如何使用画布小部件
这个菜谱展示了如何通过使用 tkinter 画布小部件来添加戏剧性的颜色效果到我们的 GUI。
准备工作
我们将改进之前从 GUI_tooltip.py 的代码,并且通过添加更多颜色来改善我们的 GUI 的外观。
如何做到这一点…
首先,我们将在我们的 GUI 中创建第三个标签,以便隔离我们的新代码。
这是创建新第三个标签的代码:
-
打开
GUI_tooltip.py并将模块保存为GUI_canvas.py。 -
创建第三个标签控制:
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)
tabControl.add(tab2, text='Tab 2') # Add a second tab
tab3 = ttk.Frame(tabControl)
tabControl.add(tab3, text='Tab 3') # Add a third tab
tabControl.pack(expand=1, fill="both") # Pack to make tabs visible
- 接下来,我们使用
tkinter的另一个内置小部件,称为Canvas。很多人喜欢这个小部件,因为它具有强大的功能:
# Tab Control 3 -------------------------------
tab3_frame = tk.Frame(tab3, bg='blue')
tab3_frame.pack()
for orange_color in range(2):
canvas = tk.Canvas(tab3_frame, width=150, height=80,
highlightthickness=0, bg='orange')
canvas.grid(row=orange_color, column=orange_color)
- 运行
GUI_canvas.py文件。运行代码后获得以下 GUI:

现在,让我们幕后了解代码,以便更好地理解。
它是如何工作的…
在我们创建了新标签后,我们在其中放置了一个常规的 tk.Frame 并将其背景颜色设置为蓝色。在循环中,我们创建了两个 tk.Canvas 小部件,将它们的颜色设置为橙色,并将它们分配到网格坐标 0,0 和 1,1。这也使得 tk.Frame 的蓝色背景颜色在两个其他网格位置可见。
前面的截图显示了运行前面代码并点击新标签页 3 所创建的结果。当你运行代码时,它确实是橙色和蓝色的。在非彩色印刷的书中,这可能不会在视觉上很明显,但这些颜色是真实的;你可以相信我。
你可以通过在线搜索来查看图形和绘图功能。在这本书中,我不会深入探讨小部件(但它非常酷)。
第四章:数据和类
在本章中,我们将把我们的 GUI 数据保存到tkinter变量中。我们还将开始使用面向对象编程(OOP),在 Python 中编写我们自己的类。这将引导我们创建可重用的 OOP 组件。到本章结束时,你将知道如何将 GUI 中的数据保存到本地的tkinter变量中。你还将学习如何显示工具提示,这会给用户额外的信息。了解如何做这一点可以使我们的 GUI 更功能化,更容易使用。
这里是本章 Python 模块的概述:

在本章中,我们将使用 Python 3.7 及以上版本的 Python 数据和 OOP 类。我们将涵盖以下食谱:
-
如何使用
StringVar() -
如何从一个小部件获取数据
-
使用模块级全局变量
-
如何在类中编码可以提高 GUI
-
编写回调函数
-
创建可重用的 GUI 组件
如何使用 StringVar()
tkinter中有一些内置的编程类型,与我们习惯编程的 Python 类型略有不同。StringVar()就是这样的tkinter类型。这个食谱将向你展示如何使用StringVar()类型。
准备工作
在这个食谱中,你将学习如何将tkinter GUI 中的数据保存到变量中,这样我们就可以使用这些数据。我们可以设置和获取它们的值,这与你使用 Java 的getter/setter方法非常相似。
这里是tkinter中的一些代码类型:
strVar = StringVar() |
保存一个字符串;默认值是一个空字符串("") |
|---|---|
intVar = IntVar() |
保存一个整数;默认值是0 |
dbVar = DoubleVar() |
保存一个float;默认值是0.0 |
blVar = BooleanVar() |
保存一个布尔值,对于False返回0,对于True返回1 |
不同的语言用float或double来表示小数点后的数字。tkinter将它们称为DoubleVar,在 Python 中这被称为float数据类型。根据精度的不同,float和double数据可能不同。在这里,我们将tkinter的DoubleVar转换为 Python 的float类型。
当我们添加一个带有 Python float的DoubleVar并查看生成的类型时,这一点变得更加清晰,它是一个 Python float,而不再是DoubleVar。
如何做呢...
我们将创建一个tkinter的DoubleVar变量,并使用+运算符向其中添加一个float数字字面量。之后,我们将查看生成的 Python 类型。
查看不同tkinter数据类型的步骤如下:
-
创建一个新的 Python 模块,并将其命名为
GUI_PyDoubleVar_to_Float_Get.py。 -
在
GUI_PyDoubleVar_to_Float_Get.py模块的顶部导入tkinter:
import tkinter as tk
- 创建
tkinter类的实例:
win = tk.Tk()
- 创建一个
DoubleVar并给它赋值:
doubleData = tk.DoubleVar()
print(doubleData.get())
doubleData.set(2.4)
print(type(doubleData))
add_doubles = 1.222222222222222222222222 + doubleData.get()
print(add_doubles)
print(type(add_doubles))
- 以下截图显示了最终的
GUI_PyDoubleVar_to_Float_Get.py代码和运行代码后的输出:

我们可以用tkinter对字符串做同样的事情。
我们将按照以下方式创建一个新的 Python 模块:
-
创建一个新的 Python 模块并将其命名为
GUI_StringVar.py。 -
在
GUI_StringVar.py模块的顶部,导入tkinter:
import tkinter as tk
- 创建
tkinter类的实例:
win = tk.Tk()
- 将
tkinter的StringVar分配给strData变量:
strData = tk.StringVar()
- 设置
strData变量:
strData.set('Hello StringVar')
- 获取
strData变量的值并将其保存到varData:
varData = strData.get()
- 打印出
strData的当前值:
print(varData)
- 以下截图显示了最终的
GUI_StringVar.py代码以及运行代码后的输出:

接下来,我们将打印 tkinter 的 IntVar、DoubleVar 和 BooleanVar 类型默认值:
-
打开
GUI_StringVar.py并将模块保存为GUI_PyVar_defaults.py。 -
在此模块的底部添加以下代码行:
print(tk.IntVar())
print(tk.DoubleVar())
print(tk.BooleanVar())
- 以下截图显示了最终的
GUI_PyVar_defaults.py代码以及运行GUI_PyVar_defaults.py代码文件后的输出:

打印默认 tkinter 变量值的步骤如下:
-
创建一个新的 Python 模块并将其命名为
GUI_PyVar_Get.py。 -
将以下代码输入到模块中:
import tkinter as tk
# Create instance of tkinter
win = tk.Tk()
# Print out the default tkinter variable values
intData = tk.IntVar()
print(intData)
print(intData.get())
# Set a breakpoint here to see the values in the debugger
print()
- 运行代码,可选地在 IDE 中的最终
print()语句处设置断点:

让我们深入了解代码以更好地理解它。
它是如何工作的…
在 Eclipse PyDev 控制台中,在 步骤 8 的 GUI_StringVar.py 的截图底部,我们可以看到打印到控制台的信息,即 Hello StringVar。这表明我们必须调用 get() 方法来获取数据。
如 步骤 3 中的 GUI_PyVar_defaults.py 的截图所示,默认值没有打印出来,正如我们预期的那样,因为我们没有调用 get()。
在线文献提到了默认值,但除非我们调用它们的 get 方法,否则我们看不到这些值。否则,我们只得到一个自动递增的变量名(例如,PY_VAR3,如前述 GUI_PyVar_defaults.py 的截图所示)。
将 tkinter 类型分配给 Python 变量不会改变结果。我们仍然得不到默认值,直到我们在这个变量上调用 get()。
值是 PY_VAR0,而不是预期的 0,直到我们调用 get 方法。现在我们可以看到默认值。我们没有调用 set,所以我们看到一旦我们调用每个类型的 get 方法,每个 tkinter 类型都会自动分配默认值。
注意默认值 0 被打印到控制台,这是我们在 intData 变量中保存的 IntVar 实例。我们还可以在截图顶部的 Eclipse PyDev 调试器窗口中看到这些值。
首先,我们导入tkinter模块并将其别名为tk。接下来,我们使用这个别名通过在Tk后添加括号来创建Tk类的实例,这调用类的构造函数。这与调用函数的机制相同;只是在这里,我们创建了一个类的实例。
通常,我们使用分配给win变量的这个实例在代码的稍后部分启动主事件循环,但在这里,我们不是显示 GUI;而是在演示如何使用tkinter的StringVar类型。
我们仍然需要创建一个Tk()的实例。如果我们取消注释这一行,我们将从tkinter得到一个错误,所以这个调用是必要的。
然后,我们创建一个StringVar类型的实例并将其分配给我们的 Python 变量strData。之后,我们使用我们的变量来调用StringVar上的set()方法,在设置值之后,我们获取该值,将其保存在名为varData的新变量中,然后打印其值。我们已经成功学习了如何使用StringVar()。现在让我们继续下一个菜谱。
如何从小部件获取数据
当用户输入数据时,我们想在代码中对其进行处理。这个菜谱展示了如何在变量中捕获数据。在前一个菜谱中,我们创建了几个tkinter类变量。它们是独立的。现在,我们将它们连接到我们的 GUI,使用从 GUI 获取的数据,并将它们存储在 Python 变量中。
准备工作
我们将继续使用我们在第三章,“外观和感觉定制”中构建的 Python GUI。我们将重用并增强该章节中的GUI_progressbar.py代码。
如何操作...
我们将 GUI 中的一个值分配给 Python 变量:
-
打开第三章,“外观和感觉定制”中的
GUI_progressbar.py,并将其模块保存为GUI_data_from_widget.py。 -
在模块的底部添加以下代码。在主事件循环上方添加
strData:
strData = spin.get()
print("Spinbox value: " + strData)
- 添加代码将光标置于名称输入框中:
name_entered.focus()
- 启动 GUI:
win.mainloop()
- 运行代码给出以下结果:

我们将代码放置在 GUI 主事件循环上方,所以打印发生在 GUI 变得可见之前。如果我们想在显示 GUI 并更改Spinbox控制器的值之后打印当前值,我们必须将代码放入回调函数中。
我们将检索Spinbox控制器的当前值:
- 我们使用以下代码创建
Spinbox小部件,并将可用的值硬编码到其中:
# Adding a Spinbox widget using a set of values
spin = Spinbox(mighty, 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(mighty, width=5, bd=8, command=_spin)
spin['values'] = (1, 2, 4, 42, 100)
spin.grid(column=0, row=2)
我们如何创建小部件并将数据插入其中并不重要,因为我们可以通过在部件的实例上使用get()方法来访问这些数据。
让我们深入了解代码。
它是如何工作的...
为了从使用tkinter编写的 GUI 中获取值,我们使用我们希望获取值的实例小部件的get()方法。
在前面的例子中,我们使用了Spinbox控件,但对于所有具有get()方法的小部件,原理是相同的。
一旦我们获取了数据,我们就处于纯 Python 的世界,tkinter在构建我们的 GUI 方面做得很好。现在我们知道如何从我们的 GUI 中获取数据,我们可以使用这些数据。
我们已经成功学习了如何从小部件获取数据。现在让我们继续下一个菜谱。
使用模块级全局变量
封装是任何编程语言的主要优势之一,它使我们能够使用面向对象编程(OOP)进行编程。Python 既面向对象友好,也支持过程式编程。我们可以创建局部于它们所在模块的global变量。它们只对这个模块是全局的,这是封装的一种形式。我们为什么想要这样做呢?因为随着我们向 GUI 添加越来越多的功能,我们想要避免可能导致代码中错误的命名冲突。
我们不希望命名冲突在代码中产生错误!命名空间是避免这些错误的一种方法,在 Python 中,我们可以通过使用 Python 模块(这些是非官方的命名空间)来实现。
准备工作
我们可以在任何模块的任何函数上方和外部声明模块级全局变量。
我们随后必须使用 Python 的global关键字来引用它们。如果我们忘记在函数中使用global,我们将会意外地创建新的局部变量。这将是一个错误,是我们真的不想做的事情。
Python 是一种动态的强类型语言。我们将在运行时注意到像这样的错误(忘记使用global关键字来限定变量的作用域)。
如何做到这一点...
将以下代码添加到之前菜谱中使用的 GUI,即如何从小部件获取数据,创建一个模块级全局变量。我们使用全部大写约定来表示常量:
你可以在www.python.org/dev/peps/pep-0008/#constants上的PEP 8 -- Python 代码风格指南中找到更多信息。
-
打开
GUI_data_from_widget.py并将其模块保存为GUI_const_42_print.py。 -
在模块顶部添加常量变量,并在底部添加
print语句:
GLOBAL_CONST = 42
# ...
print(GLOBAL_CONST)
- 运行代码会导致打印出
global。注意 42 被打印到 Eclipse 控制台(GUI_const_42_print.py):

将usingGlobal函数添加到模块底部:
-
打开
GUI_const_42_print.py并将其模块保存为GUI_const_42_print_func.py。 -
添加函数然后调用它:
def usingGlobal():
print(GLOBAL_CONST)
# call the function
usingGlobal()
- 以下截图显示了最终的
GUI_const_42_print_func.py代码和运行代码后的输出:

在前面的代码片段中,我们使用了模块级的global。很容易通过遮蔽全局变量来犯错误,如下面的代码所示:
-
打开
GUI_const_42_print_func.py并将模块保存为GUI_const_42_777.py。 -
在函数内添加常量的声明:
def usingGlobal():
GLOBAL_CONST = 777
print(GLOBAL_CONST)
- 以下截图显示了最终的
GUI_const_42_777.py代码和运行代码后的输出:

注意 42 变成了 777,尽管我们使用的是相同的变量名。
Python 中没有编译器会警告我们在局部函数中覆盖全局变量。这可能导致运行时调试困难。
如果我们尝试在不使用全局关键字的情况下打印出全局变量的值,我们会得到一个错误:
-
打开
GUI_const_42_777.py并将模块保存为GUI_const_42_777_global_print_error.py。 -
注释掉
全局并尝试打印:
def usingGlobal():
# global GLOBAL_CONST
print(GLOBAL_CONST)
GLOBAL_CONST = 777
print(GLOBAL_CONST)
- 运行代码并观察输出:

当我们使用全局关键字限定我们的局部变量时,我们可以打印出全局变量的值并局部覆盖此值:
-
打开
GUI_const_42_777_global.py。 -
添加以下代码:
def usingGlobal():
global GLOBAL_CONST
print(GLOVAL_CONST)
GLOBAL_CONST = 777
print(GLOBAL_CONST)
- 运行代码并观察输出:

我们可能认为全局变量的值仅限于我们的函数。
-
打开
GUI_const_42_777_global.py并将其保存为GUI_const_42_777_global_shadowing.py。 -
在函数下方添加
print('GLOBAL_CONST:', GLOBAL_CONS``T)。 -
运行代码并观察输出:

让我们深入了解代码,以更好地理解它。
它是如何工作的…
我们在模块的顶部定义了一个全局变量,并在模块的底部打印出它的值。
这有效。然后我们定义一个函数,并使用全局关键字在函数内打印出全局变量的值。如果我们忘记使用全局关键字,我们正在创建一个新的、局部的变量。当我们更改函数内全局变量的值时,这实际上会改变全局变量。正如我们所看到的,即使在我们的函数之外,全局值也发生了变化。
在编写小型应用程序时,全局变量非常有用。它们可以帮助我们在同一 Python 模块内的方法和函数之间共享数据,有时,面向对象的额外开销是不必要的。
随着我们的程序变得越来越复杂,使用全局变量的好处可能会迅速减少。
最好避免使用全局变量,并通过在不同作用域中使用相同的名称意外地覆盖变量。我们可以使用面向对象编程而不是使用全局变量。
我们在过程式代码中玩弄过全局变量,并了解到它可能导致难以调试的错误。在下一个菜谱中,我们将转向面向对象编程,这可以消除此类错误。
如何通过类编程改进 GUI
到目前为止,我们一直在以过程式风格编写代码。这是我们可以用 Python 做的快速脚本方法。当我们的代码变得越来越大时,我们需要转向面向对象编程。
为什么?
因为,在许多其他好处中,面向对象编程(OOP)允许我们通过使用方法来移动代码。一旦我们使用了类,我们就不再需要物理地将代码放在调用它的代码上方。这给了我们在组织代码方面很大的灵活性。我们可以将相关代码写在与其他代码相邻的位置,并且不再需要担心代码无法运行,因为代码没有放在调用它的代码上方。我们可以通过编写引用该模块中未创建的方法的模块来达到一些相当高级的效果。它们依赖于在代码运行时已经创建了这些方法。
如果我们调用的方法那时还没有被创建,我们会得到一个运行时错误。
准备工作
我们将非常简单地把我们整个过程性代码转换为面向对象编程。我们只需将其转换为一个类,缩进所有现有代码,并将 self 前缀添加到所有变量上。
这非常简单。
虽然一开始可能觉得需要在每件事前都加上 self 关键字有点烦人,这使得我们的代码更加冗长(嘿,我们浪费了这么多纸张…
),但最终这是值得的。
如何做到这一点…
注意,在 Eclipse IDE 中,PyDev 编辑器通过在代码编辑器的右侧部分用红色突出显示来提示编码问题。
-
打开
GUI_const_42_777_global.py并将模块保存为GUI_OOP_classes.py。 -
高亮显示导入下面的整个代码,并缩进四个空格。
-
在缩进的代码上方添加
class OOP():。 -
看看右侧代码编辑器中所有的红色错误:

我们必须将所有变量都加上 self 关键字,并且通过使用 self 将函数绑定到类上,这在官方和技术上正式地将函数转换为方法。
让我们用 self 前缀来修复所有的红色错误,这样我们就可以再次运行我们的代码:
-
打开
GUI_OOP_classes.py并将模块保存为GUI_OOP_2_classes.py。 -
在需要的地方添加
self关键字,例如,click_me(self)。 -
运行代码并观察它:

一旦我们为所有高亮的红色错误做了这件事,我们就可以再次运行我们的 Python 代码。click_me 函数现在绑定到了类上,并正式成为了一个方法。我们不再得到任何阻止代码运行的错误。
现在,让我们将来自 第三章,外观和感觉定制 的 ToolTip 类添加到这个 Python 模块中:
-
打开
GUI_OOP_2_classes.py。 -
将
GUI_tooltip.py中的ToolTip类添加到以下模块的import语句的顶部:
class ToolTip(object):
def __init__(self, widget, tip_text=None):
self.widget = widget
...
class OOP():
def __init__(self):
self.win = tk.Tk()
ToolTip(self.win, 'Hello GUI')
# <-- use the ToolTip class here
...
让我们深入幕后,更好地理解代码。
它是如何工作的…
我们正在将过程代码转换为面向对象的代码。首先,我们缩进整个代码,并定义代码是类的一部分,我们将其命名为OOP。为了使这可行,我们必须使用self关键字来表示变量和方法。以下是我们的旧代码与使用类的新 OOP 代码的简要比较:
########################################
# Our procedural code looked like this:
########################################
# Button Click Function
def click_me():
action.configure(text='Hello ' + name.get() + ' ' +
number_chosen.get())
# Adding a Textbox Entry widget
name = tk.StringVar()
name_entered = ttk.Entry(mighty, width=12, textvariable=name)
name_entered.grid(column=0, row=1, sticky='W')
# Adding a Button
action = ttk.Button(mighty, text="Click Me!", command=click_me)
action.grid(column=2, row=1)
ttk.Label(mighty, text="Choose a number:").grid(column=1, row=0)
number = tk.StringVar()
number_chosen = ttk.Combobox(mighty, width=12,
textvariable=number, state='readonly')
number_chosen['values'] = (1, 2, 4, 42, 100)
number_chosen.grid(column=1, row=1)
number_chosen.current(0)
# ...
********************************************
The new OOP code looks like this:
********************************************
class OOP():
def __init__(self): # Initializer method
# Create instance
self.win = tk.Tk() # notice the self keyword
ToolTip(self.win, 'Hello GUI')
# Add a title
self.win.title("Python GUI")
self.create_widgets()
# Button callback
def click_me(self):
self.action.configure(text='Hello ' + self.name.get() + ' '
+self.number_chosen.get())
# ... more callback methods
def create_widgets(self):
# Create Tab Control
tabControl = ttk.Notebook(self.win)
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
# Pack to make visible
tabControl.pack(expand=1, fill="both")
# Adding a Textbox Entry widget - using self
self.name = tk.StringVar()
name_entered = ttk.Entry(mighty, width=12,
textvariable=self.name)
name_entered.grid(column=0, row=1, sticky='W')
# Adding a Button - using self
self.action = ttk.Button(mighty, text="Click Me!",
command=self.click_me)
self.action.grid(column=2, row=1)
# ...
#======================
# Start GUI
#======================
oop = OOP() # create an instance of the class
# use instance variable to call mainloop via oop.win
oop.win.mainloop()
我们将回调方法移动到模块顶部,在新的OOP类内部。我们将所有小部件创建代码移动到一个相当长的create_widgets方法中,我们在类的初始化器中调用这个方法。技术上,在底层代码的深处,Python 确实有一个构造函数,但 Python 让我们免除了对此类问题的担忧。它由我们负责。除了真正的构造函数之外,Python 还为我们提供了一个初始化器,__init__(self)。我们强烈建议使用这个初始化器。我们可以用它向我们的类传递参数,初始化我们希望在类实例内部使用的变量。
最后,我们在模块顶部添加了ToolTip类,紧位于import语句之下。
在 Python 中,同一个 Python 模块中可以存在多个类,并且模块名称不必与类名称相同。
在这个菜谱中,我们可以看到,同一个 Python 模块中可以存在多个类。
确实很酷!以下是两个同一模块中驻留的两个类的屏幕截图:

ToolTip类和OOP类都位于同一个 Python 模块GUI_OOP_2_classes.py中:

在这个菜谱中,我们将我们的过程代码提升到了面向对象的代码。Python 使我们能够以实用和过程式的方式编写代码,就像 C 编程语言风格一样。同时,我们有选择以面向对象的方式编码的选项,就像 Java、C#和 C++风格一样。
我们已经成功地学习了如何在类中编码可以改进 GUI。现在让我们继续下一个菜谱。
编写回调函数
最初,回调函数可能看起来有点令人畏惧。你调用函数,传递一些参数,然后函数告诉你它真的很忙,它将回叫你!
你可能会想:这个函数会回叫我吗?我需要等多久?在 Python 中,即使是回调函数也很容易,是的,它们通常会回叫你。它们只是必须首先完成分配的任务(“嘿,是你最初编写它们的…”)。
让我们更深入地了解一下,当我们把回调函数编码到我们的 GUI 中时会发生什么。我们的 GUI 是事件驱动的。创建并显示在屏幕上后,它通常就坐在那里等待事件发生。它基本上是在等待一个事件发送给它。我们可以通过点击其按钮之一来向我们的 GUI 发送一个事件。这创建了一个事件,从某种意义上说,我们通过发送消息来调用我们的 GUI。
现在,在我们向我们的 GUI 发送消息后,会发生什么?点击按钮后会发生什么取决于我们是否创建了一个事件处理程序并将其与该按钮关联。如果我们没有创建事件处理程序,点击按钮将没有任何效果。事件处理程序是一个回调函数(或方法,如果我们使用类)。回调方法也像我们的 GUI 一样被动地等待被调用。一旦我们的 GUI 的按钮被点击,它将调用回调。
回调通常执行一些处理,完成后将结果返回到我们的 GUI。
在某种程度上,我们可以看到我们的回调函数正在调用我们的 GUI。
准备工作
Python 解释器会遍历模块中的所有代码一次,寻找任何语法错误并指出。如果你的语法不正确,你无法运行你的 Python 代码。这包括缩进(如果不导致语法错误,不正确的缩进通常会导致错误)。
在下一个解析循环中,解释器解析我们的代码并运行它。
在运行时,可以生成许多 GUI 事件,通常回调函数会给 GUI 小部件添加功能。
如何做到这一点……
这里是 Spinbox 小部件的回调:
-
打开
GUI_OOP_2_classes.py文件。 -
观察代码中的
_spin(self)方法:

让我们深入了解代码,以更好地理解它。
它是如何工作的……
我们在 OOP 类中创建一个回调方法,当我们在 Spinbox 小部件中选择一个值时会被调用,因为我们通过 command 参数(command=self._spin)将方法绑定到小部件。我们使用前导下划线来暗示这个方法意味着要像私有 Java 方法一样被尊重。
Python 故意避免了语言限制,例如私有、公有、友元等。在 Python 中,我们使用命名约定。围绕关键字的前后双下划线通常被期望是 Python 语言的限制,并且我们期望不在自己的 Python 代码中使用它们。
然而,我们可以使用前导下划线前缀与变量名或函数来提供提示,表明这个名称意味着要像私有辅助程序一样被尊重。
同时,如果我们希望使用否则将是内置 Python 名称的名称,我们可以后缀一个单下划线。例如,如果我们想缩短列表的长度,我们可以这样做:
len_ = len(aList)
通常,下划线难以阅读且容易忽略,所以在实践中这可能不是最好的主意。
我们已经成功学习了如何编写回调函数。现在让我们继续下一个配方。
创建可重用的 GUI 组件
我们将使用 Python 创建可重用的 GUI 组件。在这个配方中,我们将通过将我们的 ToolTip 类移动到其自己的模块中来保持简单。然后,我们将导入并使用它来显示 GUI 中几个小部件的工具提示。
准备工作
我们正在从第三章,外观和感觉定制:GUI_tooltip.py构建我们的代码。我们将首先将我们的ToolTip类分离到一个单独的 Python 模块中。
如何做到这一点…
我们将创建一个新的 Python 模块,并将ToolTip类的代码放入其中,然后将其导入到我们的主模块中:
-
打开
GUI_OOP_2_classes.py并将模块保存为GUI_OOP_class_imported_tooltip.py。 -
将
GUI_tooltip.py中的ToolTip代码分离到一个新的 Python 模块中,并命名为ToolTip.py。 -
将
ToolTip类导入到GUI_OOP_class_imported_tooltip.py:
from Ch04_Code.ToolTip import ToolTip
- 将以下代码添加到
GUI_OOP_class_imported_tooltip.py:
ToolTip(self.win, 'Hello GUI')
# Add a ToolTip to the Spinbox
ToolTip(self.spin, 'This is a Spinbox control')
# Add tooltips to more widgets
ToolTip(self.name_entered, 'This is an Entry control')
ToolTip(self.action, 'This is a Button control')
ToolTip(self.scrol, 'This is a ScrolledText control')
# Tab 2
ToolTip(curRad, 'This is a Radiobutton control')
- 运行代码并将鼠标悬停在不同的小部件上:

这也适用于第二个标签页:

让我们深入幕后,更好地理解代码。
它是如何工作的…
首先,我们创建了一个新的 Python 模块,并将ToolTip类放入这个新模块中。然后,我们将这个ToolTip类导入到另一个 Python 模块中。之后,我们使用这个类创建了几个工具提示。
在前面的截图中,我们可以看到几个ToolTip消息正在显示。对于主窗口的工具提示可能会显得有点烦人,所以最好不为主窗口显示ToolTip,因为我们真的希望突出各个小部件的功能。主窗口表单有一个标题来解释其目的;不需要ToolTip。
将我们共同的ToolTip类代码重构到它自己的模块中,有助于我们从其他模块重用这段代码。而不是复制/粘贴/修改,我们使用DRY原则,并且我们的共同代码只位于一个地方,所以当我们修改代码时,所有导入它的模块都将自动获取我们模块的最新版本。
DRY代表不要重复自己,我们将在后面的章节中再次讨论它。我们可以通过将我们的 Tab 3 图像转换成一个可重用组件来做类似的事情。为了使这个菜谱的代码简单,我们移除了 Tab 3,但你可以在前一章的代码上进行实验。
第五章:Matplotlib 图表
在本章中,我们将创建美观的图表,这些图表可以直观地表示数据。根据数据源的格式,我们可以在同一图表中绘制一个或多个数据列。
我们将使用 Python 的Matplotlib模块来创建我们的图表。
在我工作的一家公司,我们有一个现有的用于收集分析数据的程序。这是一个手动过程,需要将数据加载到 Excel 中,然后在 Excel 中生成图表。
我使用 Python 和Matplotlib自动化了整个流程。只需点击一下鼠标,数据就会被备份到网络驱动器上,再点击一下,图表就会自动创建。
为了创建这些图形图表,我们需要下载额外的 Python 模块,并且有几种安装方法。
本章将解释如何下载MatplotlibPython 模块以及所有其他必需的 Python 模块和下载方法。在我们安装所需的模块后,我们将创建自己的 Python 图表。
可视化表示数据使我们的 GUI 非常实用且外观出色,并极大地提高了你的编码技能。对于你的管理团队来说,用可视化方式表示数据也非常有用。
下面是本章中 Python 模块的概述:

在本章中,我们将使用 Python 3.7 及以上版本和Matplotlib模块创建美观的图表。
以下 URL,matplotlib.org/users/screenshots.html,是一个开始探索Matplotlib世界的绝佳地方,它教我们如何创建本章未展示的许多图表。
我们将介绍以下食谱:
-
使用 pip 和
.whl扩展安装 Matplotlib -
创建我们的第一个图表
-
在图表上放置标签
-
如何给图表添加图例
-
调整图表的比例
-
动态调整图表的比例
使用 pip 和.whl扩展安装 Matplotlib
下载额外的 Python 模块的常用方法是使用pip。pip模块是预安装在 Python 最新版本(3.7 及以上)中的。
如果你使用的是较旧的 Python 版本,你可能需要自己下载pip和setuptools。
这个食谱将展示如何成功使用pip安装Matplotlib。我们将使用.whl扩展名进行此安装,因此这个食谱还将向你展示如何安装wheel模块。
准备工作
首先,让我们找出你是否已经安装了wheel模块。wheel模块是下载和安装具有.whl扩展名的 Python 包所必需的。
我们可以使用pip找出我们目前安装了哪些模块。
从 Windows 命令提示符中运行pip list命令:

如果你运行此命令时遇到错误,你可能需要检查 Python 是否在你的环境路径上。如果目前不在,可以通过点击 Edit... 按钮将其添加到系统变量 | 路径(左下角)。然后,点击新建按钮(右上角)并输入你的 Python 安装路径。此外,添加 C:\Python37\Scripts 目录,因为 pip.exe 文件就位于那里:

如果你安装了多个版本的 Python,将 Python 3.7 移到列表的顶部是个好主意。当我们输入 pip install <module> 时,可能会使用在 系统变量 | 路径 中找到的第一个版本,如果 Python 3.7 之上的版本较旧,你可能会遇到一些意外的错误。
让我们运行 pip install wheel 并然后使用 pip list 验证它是否已成功安装:

如果运行 pip list 没有显示 wheel,尝试在命令提示符中简单地输入 wheel。这假设你已经正确设置了你的 Python 路径:

如果你真的非常习惯使用 Python 2.7 并坚持在 Python 2.7 中运行代码,你可以尝试这个技巧。在一切与 Python 3.7 一起正常工作后,你可以将 3.7 的 python.exe 重命名为 python3.exe,然后通过在命令窗口中输入 python.exe 或 python3.exe 来运行不同的 Python 可执行文件,享受使用 2.7 和 3.7 的乐趣。这是一个技巧。
如果你真的希望继续这条路,那就由你自己来,但它是可行的。
如何操作...
在安装了 wheel 模块之后,我们现在可以继续从 www.lfd.uci.edu/~gohlke/pythonlibs/ 下载并安装 Matplotlib。
- 将匹配的
Matplotlib轮子下载到你的硬盘上:

- 打开命令提示符并运行
pip install <matplotlib wheel>,如下所示:

- 如果你遇到前面的错误,请下载 Microsoft Visual C++ Build Tools 并从
visualstudio.microsoft.com/visual-cpp-build-tools/安装它们:

开始安装 Microsoft Visual C++ Build Tools 的界面如下所示:

- 如果遇到前面的错误,请重新运行
Matplotlib安装,使用pip install:

- 通过查看
site-packages文件夹来验证安装是否成功:

让我们现在深入了解安装过程。
它是如何工作的...
下载 wheel 安装程序后,我们现在可以使用 pip 安装 Matplotlib 轮子。
在步骤 1中,确保你下载并安装与你的 Python 版本匹配的Matplotlib版本。例如,如果你在 64 位操作系统(如 Microsoft Windows 10)上安装了 Python 3.7,请下载并安装matplotlib-3.1.0-cp37-cp37m-win_amd64.whl。
可执行文件名中间的amd64表示你正在安装 64 位版本。如果你使用的是 32 位 x86 系统,那么安装amd64将不会工作。如果你已经安装了 32 位版本的 Python 并下载了 64 位 Python 模块,也可能出现类似的问题。
根据你已经在系统上安装的内容,运行pip install matplotlib-3.1.0-cp37-cp37m-win_amd64.whl命令可能一开始运行正常,但可能无法完成。参考步骤 2中的前一个截图,了解安装过程中可能发生的情况。安装遇到了错误。解决这个问题的方法是下载并安装Microsoft Visual C++构建工具,我们在步骤 3中从错误消息中提到的网站进行安装(visualstudio.microsoft.com/visual-cpp-build-tools/)。
如果你安装 Microsoft Visual C++构建工具时遇到任何问题,这里有一个来自 Stack Overflow 的有用答案:stackoverflow.com/a/54136652。还有一条链接到 MS:devblogs.microsoft.com/cppblog/announcing-visual-c-build-tools-2015-standalone-c-tools-for-build-environments/。
在我们成功安装构建工具后,现在我们可以重新运行我们的Matplotlib安装,完成步骤 4。只需输入我们在步骤 2中之前使用的相同的pip install命令。
我们可以通过查看我们的 Python 安装目录来验证是否已成功安装Matplotlib,这是我们在步骤 5中做的。安装成功后,Matplotlib文件夹会被添加到site-packages中。根据我们安装 Python 的位置,Windows 上site-packages文件夹的完整路径可能是..\Python37\Lib\site-packages。
如果你看到matplotlib文件夹被添加到你的 Python 安装目录中的site-packages文件夹,那么你已经成功安装了Matplotlib。
使用pip安装 Python 模块通常非常简单,尽管你可能会遇到一些意想不到的问题。遵循前面的步骤,你的安装将会成功。
让我们继续到下一个示例。
创建我们的第一个图表
现在我们已经安装了所有必需的 Python 模块,我们可以使用Matplotlib创建自己的图表。
我们可以用几行 Python 代码创建图表。
准备工作
如前一个示例所示,成功安装Matplotlib是这个步骤的要求。
如何操作…
使用最少的代码,我们可以创建我们的第一个Matplotlib图表。
对于第一个图表,步骤如下:
-
创建一个新的 Python 模块并将其保存为
Matplotlib_our_first_chart.py。 -
将以下代码输入到模块中:
import matplotlib.pyplot as plt
from pylab import show
x_values = [1,2,3,4]
y_values = [5,7,6,8]
plt.plot(x_values, y_values)
show()
- 运行代码以查看以下图表:

让我们现在深入了解代码。
它是如何工作的…
首先,我们导入matplotlib.pyplot并将其别名为plt。然后我们创建两个列表来存储我们的x和y值。然后我们将这两个列表传递给plt或plot函数。
我们还从pylab导入show并调用它以显示我们的图表。
注意,这会自动为我们创建一个 GUI,甚至包括一些按钮。
在左下角的按钮上试一试,因为它们完全可用。
注意,x轴和y轴会自动缩放以显示我们的x和y值的范围。
更多内容…
Python 的Matplotlib模块,结合numpy等附加组件,创建了一个非常丰富的编程环境,使我们能够轻松执行数学计算并在视觉图表中绘制它们。
现在,让我们继续下一个菜谱。
在图表上放置标签
到目前为止,我们已经使用了默认的Matplotlib GUI。现在,我们将创建一些tkinter GUI,我们将使用Matplotlib。
这将需要更多行 Python 代码和一些库的导入,但这值得努力,因为我们正在通过画布来控制我们的画作。
我们将在水平和垂直轴(即x和y)上放置标签。我们将通过创建一个我们将绘制到其中的Matplotlib图来做到这一点。
你还将学习如何使用子图,这将使你能够在同一个 GUI 窗口中绘制多个图表。
准备工作
在安装了必要的 Python 模块并知道如何找到官方在线文档和教程后,我们现在可以继续创建Matplotlib图表。
如何做到这一点…
虽然plot是创建Matplotlib图表的最简单方法,但使用Figure与Canvas结合创建的图表更加定制化,看起来更好,还使我们能够向其添加按钮和其他小部件:
-
创建一个新的 Python 模块并将其保存为
Matplotlib_labels.py。 -
将以下代码输入到模块中:
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 #<-- uncomment
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.protocol('WM_DELETE_WINDOW', _destroyWindow)
#--------------------------------------------------------------
- 现在,在上一段代码下方添加以下代码:
canvas = FigureCanvasTkAgg(fig, master=root)
canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
#--------------------------------------------------------------
root.mainloop()
- 运行前面的代码将生成以下图表:

现在,让我们处理一个新的模块:
-
创建一个新的模块并将其保存为
Matplotlib_labels_four.py。 -
将以下新代码输入到模块中:
# imports and figure are the same as in the previous code
#--------------------------------------------------------------
axis1 = fig.add_subplot(221)
axis2 = fig.add_subplot(222, sharex=axis1, sharey=axis1)
axis3 = fig.add_subplot(223, sharex=axis1, sharey=axis1)
axis4 = fig.add_subplot(224, sharex=axis1, sharey=axis1)
#--------------------------------------------------------------
axis1.plot(xValues, yValues)
axis1.set_xlabel('Horizontal Label 1')
axis1.set_ylabel('Vertical Label 1')
axis1.grid(linestyle='-') # solid grid lines
#--------------------------------------------------------------
axis2.plot(xValues, yValues)
axis2.set_xlabel('Horizontal Label 2')
axis2.set_ylabel('Vertical Label 2')
axis2.grid(linestyle='-') # solid grid lines
#--------------------------------------------------------------
axis3.plot(xValues, yValues)
axis3.set_xlabel('Horizontal Label3')
axis3.set_ylabel('Vertical Label 3')
axis3.grid(linestyle='-') # solid grid lines
#--------------------------------------------------------------
axis4.plot(xValues, yValues)
axis4.set_xlabel('Horizontal Label 4')
axis4.set_ylabel('Vertical Label 4')
axis4.grid(linestyle='-') # solid grid lines
#--------------------------------------------------------------
# root and canvas are the same as in the previous code
- 运行代码将创建以下图表:

我们可以通过使用add_subplot(212)将子图分配到第二个位置来添加更多子图:
-
创建一个新的模块并将其保存为
Matplotlib_labels_two_charts.py。 -
将以下代码输入到模块中:
# imports and figure are the same as in the previous code
#--------------------------------------------------------------
#--------------------------------------------------------------
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
#--------------------------------------------------------------
#--------------------------------------------------------------
# root and canvas are the same as in the previous code
- 运行代码以查看以下图表:

现在,让我们幕后了解代码以更好地理解它。
它是如何工作的...
在Matplotlib_labels.py的第一行代码中,在步骤 2之后,我们创建了一个Figure对象的实例。
这里是官方文档的链接:matplotlib.org/3.1.1/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure.add_subplot。
接下来,我们通过调用add_subplot(211)向这个图表添加子图。
211中的第一个数字告诉图表添加多少个绘图,第二个数字确定列数,第三个告诉图表显示绘图的顺序。
在步骤 3中,我们创建值,绘制它们,并且我们还添加了一个网格并更改了其默认的线条样式。
尽管我们在图表中只显示一个绘图,但通过将子图数量选择为2,我们将绘图向上移动,这导致图表底部出现额外的空白空间。这个第一个绘图现在只占用屏幕的 50%,这影响了显示时该绘图网格线的大小。
通过取消注释axis =和axis.grid()的代码来实验代码,以查看不同的效果。你还得在每个代码下方取消注释原始行。
在步骤 4中,我们创建一个回调函数,当点击红色 X 按钮时,正确退出tkinter GUI。我们创建一个tkinter实例并将回调分配给root变量。
在步骤 5中,我们创建了一个画布并使用pack几何管理器,然后我们开始主窗口 GUI 事件循环。
在 S步骤 6中运行整个代码然后创建图表。
我们可以在同一个画布上放置多个图表。在Matplotlib_labels_four.py中,大部分代码与Matplotlib_labels.py相同。我们正在创建四个坐标轴并将它们定位在两行中。
重要的是要注意,我们创建了一个坐标轴,然后将其用作图表内其他图形的共享x和y坐标轴。这样,我们可以实现类似数据库的图表布局。
在Matplotlib_labels_two_charts.py中,现在运行代码将axis1添加到图表中。对于底部图表的网格,我们保留了默认的线条样式。与之前的图表相比,主要的不同之处在于我们使用add_subplot(212)将第二个图表分配到第二个位置。
这意味着:2 行,1 列,此图表的位置 2,这意味着它位于第二行,因为只有一列。
现在,让我们继续到下一个食谱。
如何给图表添加图例
一旦我们开始绘制多于一条的数据点线,事情可能会变得有点不清楚。通过在我们的图表中添加图例,我们可以识别数据,并了解它的实际含义。
我们不必选择不同的颜色来表示不同的数据。Matplotlib自动为每个数据点分配不同的颜色。
我们要做的只是创建图表并为其添加图例。
准备工作
在这个菜谱中,我们将增强前一个菜谱在图表上放置标签中的图表。我们将只绘制一个图表。
如何做到这一点…
首先,我们将在同一张图表上绘制更多数据行,然后我们将在图表上添加一个图例。
-
创建一个新的模块并将其保存为
Matplotlib_chart_with_legend.py。 -
将以下代码输入到模块中:
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.protocol('WM_DELETE_WINDOW', _destroyWindow)
#--------------------------------------------------------------
canvas = FigureCanvasTkAgg(fig, master=root)
canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
#--------------------------------------------------------------
root.mainloop()
- 运行代码创建以下图表,该图表在右上角有一个图例:

接下来,我们更改图例中线条的默认颜色。
-
打开
Matplotlib_chart_with_legend.py并将其保存为Matplotlib_chart_with_legend_colors.py。 -
将以下颜色添加到每个绘图:
t0, = axis.plot(xValues, yValues0, color = 'purple')
t1, = axis.plot(xValues, yValues1, color = 'red')
t2, = axis.plot(xValues, yValues2, color = 'blue')
- 运行修改后的代码并观察不同的颜色:

现在,让我们更仔细地看看将绘图分配给变量时的正确语法。
-
打开
Matplotlib_chart_with_legend.py并将其保存为Matplotlib_chart_with_legend_missing_comma.py。 -
删除
t0后面的逗号。 -
运行代码。
-
注意到
第一行不再出现在右上角的图例中:

让我们现在幕后了解代码。
它是如何工作的…
在Matplotlib_chart_with_legend.py中,我们在这个菜谱中只绘制一个图表。
对于步骤 2,参考前一个菜谱的解释,在图表上放置标签,因为代码是相同的,除了我们通过figsize属性稍微修改了图形的大小。
在步骤 3中,我们将fig.add_subplot(111)更改为使用111。接下来,我们创建三个包含要绘制值的 Python 列表。当我们绘制数据时,我们将绘图引用保存在局部变量中。
我们通过传递一个包含三个绘图引用的元组来创建图例,然后传递另一个包含随后在图例中显示的字符串的元组,在第三个参数中,我们将图例定位在图表内。
对于步骤 4,参考前一个菜谱的解释,在图表上放置标签,因为代码是相同的*。
您可以在以下链接找到tkinter协议的官方文档:www.tcl.tk/man/tcl8.4/TkCmd/wm.htm#M39。
在步骤 5中,运行代码时,我们可以看到我们的图表现在为每条数据行都有一个图例。
Matplotlib的默认设置将颜色方案分配给正在绘制的线条。在Matplotlib_chart_with_legend_colors.py中,我们可以通过在绘制每个坐标轴时设置一个属性,轻松地将这种默认的颜色设置更改为我们喜欢的颜色。
我们在 步骤 2 中通过使用 color 属性并分配一个可用的颜色值来完成此操作。现在在 步骤 3 中运行代码显示的颜色与默认颜色不同。
在 Matplotlib_chart_with_legend_missing_comma.py 中,我们故意删除了 t0 后面的逗号,以查看这会产生什么效果。
注意,t0、t1 和 t2 变量赋值后的逗号不是一个错误。它是创建图例所必需的。
每个变量的逗号解包列表值到变量中。这个值是 Matplotlib 的 Line2D 对象。如果我们省略逗号,我们的图例将不会显示,因为 Line2D 对象嵌入在一个列表中,我们必须将其从列表中解包出来。
当我们删除 t0 赋值后的逗号时,我们会得到一个错误,并且第一行不再出现在图中。图表和图例仍然被创建,但没有第一行出现在图例中。
让我们继续到下一个配方。
缩放图表
在之前的配方中,当我们创建第一个图表并增强它们时,我们硬编码了如何视觉表示这些值的缩放。
当前的值对我们使用的值来说效果很好,但我们可能需要从大型数据库中绘制图表。
根据数据范围,我们硬编码的垂直 y 维度的值可能并不总是最佳解决方案,并且可能使我们的图表中的线条难以看到。
准备工作
我们将从之前的配方,如何给图表添加图例,改进我们的代码。如果您没有输入之前配方中的所有代码,只需从 Packt 网站下载本章的代码,它将帮助您开始(然后您可以使用 Python 创建 GUI、图表等,享受很多乐趣)。
如何操作...
我们将修改之前配方中的 yValues1 代码行,使用 50 作为第三个值:
-
打开
Matplotlib_chart_with_legend.py并将其保存为Matplotlib_labels_two_charts_not_scaled.py。 -
将
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 (50)
yValues2 = [6.5,7,8,7]
- 运行代码以查看以下图表:

-
打开
Matplotlib_labels_two_charts_not_scaled.py并将其保存为Matplotlib_labels_two_charts_scaled.py。 -
在值代码下添加
axis.set_ylim(5, 8):
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
- 运行代码后,出现以下图表:

让我们现在幕后了解代码以更好地理解它。
它是如何工作的...
在 Matplotlib_labels_two_charts_not_scaled.py 中,与之前配方中创建图表的代码的唯一区别是一个数据值。
通过更改一个与所有其他绘制线条的平均值范围都不接近的值,数据的视觉表示发生了显著变化。我们失去了关于整体数据的大量细节,现在我们主要看到一个高峰。
到目前为止,我们的图表已经根据它们所视觉表示的数据进行了调整。
虽然这是Matplotlib的一个实用功能,但这并不总是我们想要的。我们可以通过限制垂直的y维度来限制表示的图表的刻度。
在Matplotlib_labels_two_charts_scaled.py中,axis.set_ylim(5, 8)这一行代码现在将起始值限制为5,并将垂直显示的结束值限制为8。
现在,当我们创建我们的图表时,高值峰值不再像以前那样有影响。
我们在数据中增加了一个值,这导致了戏剧性的效果。通过设置图表的垂直和水平显示的限制,我们可以看到我们最感兴趣的数据。
就像刚刚展示的尖峰一样,这些尖峰也可能非常有兴趣。这完全取决于我们在寻找什么。数据的视觉表示非常有价值。
一图胜千言。
现在,让我们继续下一个菜谱。
动态调整图表的刻度
在上一个菜谱中,我们学习了如何限制图表的缩放。在这个菜谱中,我们将更进一步,通过设置限制并在表示之前分析我们的数据来动态调整缩放。
准备工作
我们将通过动态读取我们正在绘制的图形数据、计算平均值然后调整我们的图表来增强上一个菜谱中缩放图表的代码。
虽然我们通常会从外部源读取数据,但在本菜谱中,我们将使用 Python 列表创建我们正在绘制的图形数据,如以下代码部分所示。
如何做到这一点…
我们在我们的 Python 模块中通过将包含数据的列表分配给xValues和yValues变量来创建自己的数据。现在让我们修改代码,以设置x和y维度的限制。
-
打开
Matplotlib_labels_two_charts_scaled.py并将其保存为Matplotlib_labels_two_charts_scaled_dynamic_spike.py。 -
按照以下方式添加/调整
set_ylim和set_xlim代码:
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
- 当我们运行修改后的代码时,我们得到以下结果:

按照以下方式修改代码:
-
打开
Matplotlib_labels_two_charts_scaled_dynamic_spike.py并将其保存为Matplotlib_labels_two_charts_scaled_dynamic.py。 -
从
yAll开始插入以下新代码:
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坐标系的起始点在(0, 0)。这通常是一个好主意,因此我们相应地调整了我们的图表坐标代码。在Matplotlib_labels_two_charts_scaled_dynamic_spike.py中,我们为x和y设置了相同的限制,希望我们的图表可能看起来更平衡。查看结果,这并不是事实。
可能从(0, 0)开始并不是一个很好的主意。
我们真正想要做的是根据数据的范围动态调整我们的图表,同时限制过高或过低的值。
我们可以通过解析图表中要表示的所有数据,同时设置一些明确的限制来实现这一点。在 Matplotlib_labels_two_charts_scaled_dynamic.py 中,我们动态调整了其 x 和 y 维度。注意 y- 维度从 5.5 开始。图表也不再从 (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 GUI 的功能。
tkinter GUI 是一个单线程应用程序。涉及睡眠或等待时间的每个函数都必须在单独的线程中调用;否则,tkinter GUI 会冻结。
当我们运行我们的 Python GUI 时,在 Windows 任务管理器中,我们可以看到已启动一个新的python.exe进程。当我们给我们的 Python GUI 添加.pyw扩展名时,创建的进程将是python.pyw,这也可以在任务管理器中看到。
当创建进程时,进程会自动创建一个主线程来运行我们的应用程序。这被称为单线程应用程序。
单线程进程包含指令的单个序列执行。换句话说,一次只处理一个命令。
对于我们的 Python GUI,单线程应用程序会导致我们在调用长时间运行的任务(如点击具有几秒睡眠时间的按钮)时,GUI 立即冻结。为了保持我们的 GUI 响应,我们必须使用多线程,这正是本章我们将要学习的内容。
我们的 GUI 在单个线程中运行。了解如何使用多个线程是 GUI 开发中的一个重要概念。
我们也可以通过创建多个 Python GUI 实例来创建多个进程,如任务管理器所示,我们可以看到同时运行着几个python.exe进程。
设计上,进程之间是隔离的,并且不共享公共数据。为了在独立进程之间进行通信,我们必须使用进程间通信(IPC),这是一种高级技术。另一方面,线程之间确实共享公共数据、代码和文件,这使得在同一进程内使用线程进行通信比使用 IPC 要容易得多。关于线程的精彩解释可以在www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html找到。
在本章中,我们将学习如何保持我们的 Python GUI 响应,并防止它冻结。在创建工作 GUI 时,拥有这种知识是必不可少的,并且知道如何创建线程和使用队列可以提高您的编程技能。
我们还将使用 TCP/IP 将我们的 GUI 连接到网络。除此之外,我们还将读取 URL 网页,这也是互联网上的一个网络组件。
这是本章 Python 模块的概述:

我们将使用 Python 3.7 或更高版本创建线程、队列和 TCP/IP 套接字。
总结来说,我们将涵盖以下内容:
-
如何创建多个线程
-
启动线程
-
停止线程
-
如何使用队列
-
在不同模块之间传递队列
-
使用对话框小部件将文件复制到您的网络中
-
使用 TCP/IP 通过网络进行通信
-
使用
urlopen从网站读取数据
如何创建多个线程
多个线程是必要的,以便保持我们的 GUI 响应。如果没有使用多个线程运行我们的 GUI 程序,我们的应用程序可能会冻结并可能崩溃。
准备工作
多个线程在同一个计算机进程的内存空间中运行。不需要 IPC,这会复杂化我们的代码。在这个菜谱中,我们将通过使用线程来避免 IPC。
如何做到…
首先,我们将增加我们的ScrolledText小部件的大小,使其更大。让我们将scrol_w增加到40,将scrol_h增加到10。
我们将首先使用第五章的最新代码,Matplotlib 图表:
-
打开
Ch04_Code.GUI_OOP_class_imported_tooltip.py并将其保存为GUI_multiple_threads.py。 -
进行以下代码所示更改:
# Using a scrolled Text control
scrol_w = 40; scrol_h = 10 # increase sizes
self.scrol = scrolledtext.ScrolledText(mighty, width=scrol_w,
height=scrol_h, wrap=tk.WORD)
self.scrol.grid(column=0, row=3, sticky='WE', columnspan=3)
- 将
self.spin.grid修改为使用sticky:
# Adding a Spinbox widget
self.spin = Spinbox(mighty, values=(1, 2, 4, 42, 100), width=5,
bd=9, command=self._spin)
self.spin.grid(column=0, row=2, sticky='W') # align left, use sticky
- 增加小部件
Entry的width大小:
# Adding a Textbox Entry widget
self.name = tk.StringVar()
self.name_entered = ttk.Entry(mighty, width=24,
# increase width
textvariable=self.name)
self.name_entered.grid(column=0, row=1, sticky='W')
- 将
Combobox小部件的width大小增加到14:
ttk.Label(mighty, text="Choose a number:").grid(column=1, row=0)
number = tk.StringVar()
self.number_chosen = ttk.Combobox(mighty, width=14,
# increase width
textvariable=number, state='readonly')
self.number_chosen['values'] = (1, 2, 4, 42, 100)
self.number_chosen.grid(column=1, row=1)
self.number_chosen.current(0)
- 运行代码并观察输出:

- 从 Python 的内置
threading模块导入Thread:
#======================
# imports
#======================
import tkinter as tk
...
from threading import Thread
- 添加
method_in_a_thread方法:
class OOP():
def method_in_a_thread(self):
print('Hi, how are you?')
- 创建线程如下:
#======================
# Start GUI
#======================
oop = OOP()
# Running methods in Threads
run_thread = Thread(target=oop.method_in_a_thread) # create Thread
oop.win.mainloop()
- 为
run_thread变量设置断点或使用print语句:

让我们深入了解代码,以更好地理解它。
它是如何工作的…
在我们对GUI_multiple_threads.py的第 2 步所做的第一次更改之后,当我们运行生成的 GUI 时,位于其上方的Spinbox小部件相对于Entry小部件是居中对齐的,这看起来不太好。我们将通过左对齐小部件来改变这一点。我们向grid控件添加sticky='W'以左对齐Spinbox小部件。
GUI 仍然可以更好,所以接下来,我们将Entry小部件的大小增加到获得更平衡的 GUI 布局。之后,我们也增加了Combobox小部件。运行修改和改进后的代码将产生一个更大的 GUI,我们将使用这个 GUI 来完成这个菜谱以及接下来的菜谱。
为了在 Python 中创建和使用线程,我们必须从threading模块导入Thread类。在添加method_in_a_thread方法后,我们现在可以在代码中调用我们的线程方法,并将实例保存在名为run_thread的变量中。
现在我们有一个线程化的方法,但当我们运行代码时,控制台没有任何打印输出!
我们必须首先启动线程,然后它才能运行,下一个菜谱将展示如何做到这一点。
然而,在 GUI 主事件循环之后设置断点证明我们确实创建了一个线程对象,如 Eclipse IDE 调试器中所示。
在这个菜谱中,我们首先通过增加 GUI 大小来准备我们的 GUI 使用线程,这样我们就可以更好地看到打印到ScrolledText小部件的结果。然后,我们从 Python 的threading模块导入了Thread类。接下来,我们创建了一个在 GUI 内部调用的方法。
让我们继续下一个菜谱。
启动线程
这个配方将向我们展示如何启动一个线程。它还将演示为什么在长时间运行的任务期间,线程对于保持我们的 GUI 响应是必要的。
准备工作
让我们先看看当我们调用与 sleep 相关的函数或 GUI 方法而没有使用线程时会发生什么。
我们在这里使用 sleep 来模拟可能需要等待网络服务器或数据库响应、大文件传输或复杂计算完成任务的现实世界应用程序。sleep 是一个非常现实的占位符,展示了涉及的原则。
如何操作…
在我们的按钮回调方法中添加一个循环和一些 sleep 时间会导致我们的 GUI 变得无响应,当我们尝试关闭 GUI 时,情况变得更糟。
-
打开
GUI_multiple_threads.py并将其保存为GUI_multiple_threads_sleep_freeze.py。 -
对代码进行以下更改:
# Button callback
def click_me(self):
self.action.configure(text='Hello ' + self.name.get() + ' '
+ self.number_chosen.get())
# Non-threaded code with sleep freezes the GUI
for idx in range(10):
sleep(5)
self.scrol.insert(tk.INSERT, str(idx) + 'n')
- 运行前面的代码会产生以下截图:

-
让我们将线程的创建移动到它自己的方法中,然后从按钮回调方法中调用此方法:
-
打开
GUI_multiple_threads_sleep_freeze.py并将其保存为GUI_multiple_threads_starting_a_thread.py。 -
添加以下代码:
-
# Running methods in Threads
def create_thread(self):
self.run_thread = Thread(target=self.method_in_a_thread)
self.run_thread.start() # start the thread
# Button callback
def click_me(self):
self.action.configure(text='Hello ' + self.name.get())
self.create_thread()
- 运行代码并观察输出。现在运行代码不再使我们的 GUI 冻结:

我们可以通过以下步骤打印线程的实例:
-
-
打开
GUI_multiple_threads_starting_a_thread.py。 -
在代码中添加一个
print语句:
-
# Running methods in Threads
def create_thread(self):
self.run_thread =
Thread(target=self.method_in_a_thread)
self.run_thread.start() # start the thread
print(self.run_thread)
- 点击按钮现在会创建以下输出:

- 在多次点击按钮后,你会得到以下输出:

- 将带有
sleep的代码移动到method_in_a_thread方法中的循环:
def method_in_a_thread(self):
print('Hi, how are you?')
for idx in range(10):
sleep(5)
self.scrol.insert(tk.INSERT, str(idx) + 'n')
- 点击按钮,切换标签页,然后点击其他小部件:

让我们深入了解代码,更好地理解它。
它是如何工作的…
在 GUI_multiple_threads_sleep_freeze.py 中,我们添加了一个 sleep 语句,并注意到我们的 GUI 变得无响应。
如果等待足够长的时间,方法最终会完成,但在这段时间内,我们的任何 GUI 小部件都不会对点击事件做出响应。我们通过使用线程来解决这个问题。
与常规 Python 函数和方法不同,我们必须 start 一个将在其自己的线程中运行的方法!这就是我们在 GUI_multiple_threads_starting_a_thread.py 中接下来所做的事情。点击按钮现在会导致调用 create_thread 方法,然后它反过来调用 method_in_a_thread 方法。
首先,我们创建一个线程并将其指向一个方法。然后,我们启动一个新线程来运行目标方法。现在运行代码不再使我们的 GUI 冻结。
GUI 本身在其自己的线程中运行,这是应用程序的主线程。
当我们多次点击按钮时,我们可以看到每个线程都会被分配一个唯一的名称和 ID。在将带有sleep的代码移动到method_in_a_thread方法的循环中之后,我们能够验证线程确实解决了我们的问题。
当点击按钮,同时数字以五秒的延迟打印到ScrolledText小部件时,我们可以在我们的 GUI 的任何地方点击,切换标签页等等。由于我们使用了线程,我们的 GUI 再次变得响应。
在这个菜谱中,我们在自己的线程中调用了我们的 GUI 类的函数,并了解到我们必须启动线程。否则,线程会被创建,但只是在那里等待我们运行它的目标方法。我们还注意到每个线程都会被分配一个唯一的名称和 ID。最后,我们通过在代码中插入sleep语句来模拟长时间运行的任务,这表明线程确实可以解决问题。
让我们继续下一个菜谱。
停止一个线程
我们必须通过调用start()方法来启动线程,以便真正让它做些事情,直观上,我们期望有一个匹配的stop()方法,但并没有这样的方法。在这个菜谱中,我们将学习如何将线程作为一个后台任务运行,这被称为守护进程。当关闭主线程,即我们的 GUI 时,所有守护进程也会自动停止。
准备工作
当我们在线程中调用方法时,我们也可以向方法传递参数和关键字参数。我们通过这样做来开始这个菜谱。我们将从上一个菜谱中的代码开始。
如何做到这一点...
通过在线程构造函数中添加args=[8]并将目标方法修改为期望参数,我们可以向线程方法传递参数。args参数必须是一个序列,因此我们将我们的数字包裹在一个 Python 列表中。让我们通过这个过程来了解一下:
-
打开
GUI_multiple_threads_starting_a_thread.py并将其保存为GUI_multiple_threads_stopping_a_thread.py。 -
将
run_thread改为self.run_thread并将arg=[8]改为arg=[8]:
# Running methods in Threads
def create_thread(self):
self.run_thread = Thread(target=self.method_in_a_thread,
args=[8])
self.run_thread.start()
print(self.run_thread)
print('createThread():', self.run_thread.isAlive())
- 将
num_of_loops作为新参数添加到method_in_a_thread中:
def method_in_a_thread(self, num_of_loops=10):
for idx in range(num_of_loops):
sleep(1)
self.scrol.insert(tk.INSERT, str(idx) + 'n')
sleep(1)
print('method_in_a_thread():', self.run_thread.isAlive())
- 运行代码,点击按钮,然后关闭 GUI:

- 在代码中添加
self.run_thread.setDaemon(True):
# Running methods in Threads
def create_thread(self):
self.run_thread = Thread(target=self.method_in_a_thread,
args=[8])
self.run_thread.setDaemon(True) # <=== add this line
self.run_thread.start()
print(self.run_thread)
- 运行修改后的代码,点击按钮,然后关闭 GUI:

现在我们来看看这个菜谱是如何工作的!
它是如何工作的...
在以下代码中,run_thread是一个局部变量,我们只在我们创建run_thread的方法的作用域内访问它:
# Running methods in Threads
def create_thread(self):
run_thread = Thread(target=self.method_in_a_thread, args=[8])
run_thread.start()
通过将局部变量转换为类实例属性,我们可以通过在另一个方法中调用它的isAlive来检查线程是否仍在运行。在GUI_multiple_threads_stopping_a_thread.py中,我们将我们的局部run_thread变量提升为类的实例属性。这使得我们可以从我们类中的任何方法访问self.run_thread变量。
当我们点击按钮然后在线程完成之前退出 GUI 时,我们会得到一个运行时错误。
预期线程完成其分配的任务,因此当我们在线程完成之前关闭 GUI 时,根据错误信息,Python 告诉我们我们启动的线程不在主事件循环中。我们可以通过将线程转换为守护线程来解决这个问题,这样它就会作为一个后台任务执行。这给我们带来的好处是,当我们关闭我们的 GUI,即我们的主要线程,它启动其他线程时,守护线程会干净地退出。我们通过在启动线程之前调用线程的setDaemon(True)方法来实现这一点。
当我们现在点击按钮并在线程完成其分配的任务之前退出我们的 GUI 时,我们不再收到任何错误。虽然有一个start方法可以让线程运行,但令人惊讶的是,并没有一个等效的停止方法。
在这个菜谱中,我们在一个线程中运行一个方法,该方法将数字打印到我们的ScrolledText小部件中。当我们退出 GUI 时,我们不再对曾经打印到我们小部件的线程感兴趣,因此通过将线程转换为守护线程,我们可以干净地退出 GUI。
让我们继续下一个菜谱。
如何使用队列
Python 队列是一个实现先进先出(FIFO)范式的数据结构,基本上就像一个管道。你在一侧把东西铲进管道,它就会从管道的另一侧掉出来。
与将泥铲入物理管道相比,这个队列铲泥的主要区别在于,在 Python 队列中,事物不会混淆。你放一个单位进去,那个单位就会从另一侧出来。接下来,你放另一个单位进去(例如,一个类的实例),这个整个单位就会作为一个整体从另一端出来。它以我们插入队列代码的确切顺序从另一端出来。
队列不是一个我们推和弹出数据的栈。栈是一个后进先出(LIFO)的数据结构。
队列是容器,用于存储从可能不同的数据源输入队列中的数据。当这些客户端有数据可用时,我们可以让不同的客户端向队列提供数据。哪个客户端准备好向我们的队列发送数据就发送,然后我们可以在小部件中显示这些数据或将它们发送到其他模块。
使用多个线程在队列中完成分配的任务,在接收处理结果的最终结果并显示它们时非常有用。数据被插入到队列的一端,然后以有序的方式从另一端出来,FIFO。
我们的 GUI 可能有五个不同的按钮小部件,这样每个都可以启动一个不同的任务,我们希望在 GUI 中的小部件中显示这些任务(例如,一个ScrolledText小部件)。这五个不同的任务完成所需的时间各不相同。
每当一项任务完成时,我们立即需要知道这一点,并在 GUI 中显示此信息。通过创建共享的 Python 队列并让五个任务将它们的结果写入此队列,我们可以使用 FIFO 方法立即显示已完成任务的任何任务的结果。
准备工作
随着我们的 GUI 在功能性和实用性上的不断增长,它开始与网络、进程和网站进行通信,最终将不得不等待数据可用以便 GUI 显示。
在 Python 中创建队列解决了在 GUI 内部等待数据显示的问题。
如何做到这一点...
为了在 Python 中创建队列,我们必须从queue模块导入Queue类。在 GUI 模块的顶部添加以下语句:
-
打开
GUI_multiple_threads_starting_a_thread.py并将其保存为GUI_queues.py。 -
对代码进行以下更改:
from threading import Thread
from queue import Queue
- 添加以下方法:
def use_queues(self):
gui_queue = Queue() # create queue instance
print(gui_queue) # print instance
- 修改
click_me方法:
# Button callback
def click_me(self):
self.action.configure(text='Hello ' + self.name.get())
self.create_thread()
self.use_queues()
- 运行前面的代码并观察输出,如图所示:

- 修改
use_queues以使用put和get:
# Create Queue instance
def use_queues(self):
gui_queue = Queue()
print(gui_queue)
gui_queue.put('Message from a queue')
print(gui_queue.get())
- 运行前面的代码并观察输出,如图所示:

- 编写一个循环,将许多消息放入
Queue:
# Create Queue instance
def use_queues(self):
gui_queue = Queue()
print(gui_queue)
for idx in range(10):
gui_queue.put('Message from a queue: ' + str(idx))
print(gui_queue.get())
- 运行前面的代码:

- 添加一个
while循环:
# Create Queue instance
def use_queues(self):
gui_queue = Queue()
print(gui_queue)
for idx in range(10):
gui_queue.put('Message from a queue: ' + str(idx))
while True:
print(gui_queue.get())
- 运行前面的代码以查看以下结果:

现在,让我们考虑无限循环的场景:
-
打开
GUI_queues.py并将其保存为GUI_queues_put_get_loop_endless_threaded.py。 -
进行以下更改以将
self.run_thread作为后台守护线程启动:
# Running methods in Threads
def create_thread(self):
self.run_thread = Thread(target=self.method_in_a_thread,
args=[8])
self.run_thread.setDaemon(True)
self.run_thread.start()
# start queue in its own thread
write_thread = Thread(target=self.use_queues, daemon=True)
write_thread.start()
- 在
click_me方法中,我们注释掉self.use_queues()并现在调用self.create_thread():
# Button callback
def click_me(self):
self.action.configure(text='Hello ' + self.name.get())
self.create_thread()
# now started as a thread in create_thread()
# self.use_queues()
- 运行代码以查看以下结果:

让我们深入了解代码以更好地理解它。
它是如何工作的...
在GUI_queues.py中,我们首先添加import语句,然后创建一个新的方法来创建Queue。我们在按钮点击事件中调用该方法。
在代码中,我们创建了一个局部Queue实例,该实例仅在此方法内部可访问。如果我们希望从其他地方访问此队列,我们必须使用self关键字将其转换为类的实例属性,这会将局部变量绑定到整个类,使其可在类中的任何其他方法中访问。在 Python 中,我们通常在__init__(self)方法中创建类实例变量,但 Python 非常实用,允许我们在代码的任何位置创建这些属性。
现在我们有一个队列的实例。我们可以通过打印它来看到这是如何工作的。
为了将数据放入队列,我们使用put命令。为了从队列中获取数据,我们使用get命令。
运行代码的结果是消息首先被放入Queue,然后从Queue中取出,然后被打印到控制台。我们已将 10 条消息放入Queue,但我们只取出第一条。其他消息仍然在Queue中,等待以 FIFO 的方式取出。为了取出所有已放入Queue的消息,我们可以创建一个无限循环。
尽管这段代码可以工作,但不幸的是,它使我们的 GUI 冻结。为了解决这个问题,我们必须在它的自己的线程中调用该方法,就像我们在前面的食谱中所做的那样。
我们在GUI_queues_put_get_loop_endless_threaded.py中这样做。
当我们现在点击按钮时,GUI 不再冻结,代码可以正常工作。我们创建了Queue,并以 FIFO 的方式将消息放入Queue的一侧。我们从Queue中取出消息,然后将其打印到控制台(stdout)。我们意识到我们必须在它的自己的线程中调用该方法,否则我们的 GUI 可能会冻结。
让我们继续下一个食谱。
在不同模块之间传递队列
在这个食谱中,我们将传递不同模块之间的队列。随着我们的 GUI 代码复杂性的增加,我们希望将 GUI 组件从业务逻辑中分离出来,将它们分离到不同的模块中。模块化使我们能够重用代码,并使代码更具可读性。
当我们在我们的 GUI 中显示的数据来自不同的数据源时,我们将面临延迟问题,这正是队列解决的问题。通过在不同 Python 模块之间传递Queue的实例,我们正在分离模块功能的不同关注点。
理想的 GUI 代码只应关注创建和显示小部件以及数据。
业务逻辑模块的工作只是执行业务逻辑并向 GUI 提供结果数据。
我们必须结合这两个元素,理想情况下使用尽可能少的模块间关系,减少代码依赖性。
避免不必要的依赖的编码原则通常被称为松散耦合。这是一个非常重要的原则,我强烈建议你深入研究它,理解它,并将其应用到自己的编码项目中。
为了理解松散耦合的重要性,我们可以在白板或一张纸上画一些盒子。一个盒子代表我们的 GUI 类和代码,而其他盒子代表业务逻辑、数据库等。
接下来,我们在盒子之间画线,绘制出这些盒子(即我们的 Python 模块)之间的相互依赖关系,如图所示:

虽然这三个通过三条线连接的盒子看起来可能有点简单,但这正是你会在软件团队会议的白板上画出的内容。我已经省略了任何标签,但一个盒子可以标记为UI,另一个数据库,第三个业务处理逻辑。
我们 Python 盒子之间的线条越少,我们的设计就越是松散耦合。
准备工作
在之前的配方中,如何使用队列,我们开始使用队列。在这个配方中,我们将从我们的主 GUI 线程传递Queue实例到其他 Python 模块,这将使我们能够从另一个模块写入ScrolledText小部件,同时保持我们的 GUI 响应。
如何做到这一点…
-
首先,我们在项目中创建一个新的 Python 模块。让我们称它为
Queues.py。我们将一个函数放入其中(目前不需要面向对象)。依次,我们可以这样表述:-
创建一个新的 Python 模块,并命名为
Queues.py。 -
将以下代码写入此模块以将消息放入实例队列:
-
def write_to_scrol(inst):
print('hi from Queue', inst)
for idx in range(10):
inst.gui_queue.put('Message from a queue: ' +
str(idx))
inst.create_thread(6)
-
下一步将展示我们如何导入这个新创建的模块:
-
打开
GUI_queues_put_get_loop_endless_threaded.py并将其保存为GUI_passing_queues_member.py。 -
对导入的模块进行以下更改以调用该函数:
-
import Ch06_Code.Queues as bq # bq; background queue
class OOP():
# Button callback
def click_me(self):
# Passing in the current class instance (self)
print(self)
bq.write_to_scrol(self)
- 在
GUI_passing_queues_member.py中创建一个Queue的实例:
class OOP():
def __init__(self):
# Create a Queue
self.gui_queue = Queue()
- 修改
use_queues方法:
def use_queues(self):
# Now using a class instance member Queue
while True:
print(self.gui_queue.get())
- 运行代码会产生以下结果:

让我们深入了解代码以更好地理解它。
它是如何工作的…
首先,我们创建一个新的 Python 模块,名为Queues.py。其中write_to_scrol函数接受一个类的实例。我们使用这个实例来访问类的方法和属性。
在这里,我们依赖于我们的类实例具有我们函数中访问的两个方法。
在GUI_passing_queues_member.py中,我们首先导入Queues模块,将其别名设置为bq,然后我们使用它来调用位于Queues模块中的函数。
将模块别名设置为bq可能不是最好的名字。我的意思是想表示后台队列,因为它在后台以守护线程的形式运行。由于我在本书的前两版中已经使用了它,出于一致性的原因,我在这一版中不更改别名。
在click_me按钮回调方法中,我们将self传递给这个函数。这使得我们能够从另一个 Python 模块中使用所有的 GUI 方法。
导入的模块包含我们正在调用的write_to_scrol函数:
def write_to_scrol(inst):
print('hi from Queue', inst)
inst.create_thread(6)
通过传递类实例的自我引用到另一个模块中类调用的函数,我们现在可以从其他 Python 模块访问所有我们的 GUI 元素。
gui_queue是一个实例属性,create_thread是一个方法,两者都在GUI_passing_queues_member.py中定义,我们通过Queues模块中传递的 self 引用来访问它们。
我们将Queue创建为类的实例属性,将其引用放置在GUI_passing_queues_member.py类的__init__方法中。
现在,我们可以通过简单地使用传递给我们的 GUI 的类引用,从我们的新模块中向队列中放入消息。注意Queues.py代码中的inst.gui_queue.put:
def write_to_scrol(inst):
print('hi from Queue', inst)
for idx in range(10):
inst.gui_queue.put('Message from a queue: ' + str(idx))
inst.create_thread(6)
在我们修改了 use_queues 方法之后,我们的图形用户界面(GUI)代码中的 create_thread 方法只从 Queue 中读取,这个 Queue 被我们新模块中的业务逻辑填充,这个新模块将逻辑从我们的图形用户界面(GUI)模块中分离出来。
为了将图形用户界面(GUI)小部件与表达业务逻辑的功能分离,我们创建了一个类,将队列作为该类的实例属性,并通过将类的实例传递到不同 Python 模块中的函数,我们现在可以访问所有图形用户界面(GUI)小部件以及队列。
这就是面向对象编程(OOP)的魔力。在一个类的中间,我们使用 self 关键字将自身传递给类内部调用的函数。
这个菜谱是一个例子,说明了在面向对象编程(OOP)中编程是有意义的。
让我们继续下一个菜谱。
使用对话框小部件将文件复制到您的网络
这个菜谱展示了如何从您的本地硬盘复制文件到网络位置。我们将通过使用 Python 的 tkinter 内置对话框来完成此操作,它使我们能够浏览硬盘。然后我们可以选择要复制的文件。
这个菜谱还展示了如何使 Entry 小部件只读,并将 Entry 默认设置为指定位置,这可以加快浏览硬盘的速度。
准备工作
我们将扩展前一个菜谱中构建的图形用户界面(GUI)的第二个标签页,在不同模块间传递队列。
如何操作…
在 create_widgets() 方法中接近底部的地方,我们将添加以下代码到图形用户界面(GUI)中,在那里我们创建了标签控制 2。新的小部件框架的父元素是 tab2,我们在 create_widgets() 方法的开头创建了它。只要您将以下代码物理地放置在 tab2 创建的下方,它就会工作:
-
打开
GUI_passing_queues_member.py并将其保存为GUI_copy_files.py。 -
进行以下更改:
###########################################################
def create_widgets(self):
# Create Tab Control
tabControl = ttk.Notebook(self.win)
# Add a second tab
tab2 = ttk.Frame(tabControl)
# Make second tab visible
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 = scrol_w
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)
msg.showinfo('Copy File to Network', 'Succes:
File copied.')
except FileNotFoundError as err:
msg.showerror('Copy File to Network', '*** Failed to copy
file! ***\n\n' + str(err))
except Exception as ex:
msg.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):

- 点击浏览到文件…按钮:

-
-
打开
GUI_copy_files.py。 -
添加以下两个
import语句:
-
from tkinter import filedialog as fd
from os import path
- 创建以下函数:
def getFileName():
print('hello from getFileName')
fDir = path.dirname(__file__)
fName = fd.askopenfilename(parent=self.win, initialdir=fDir)
- 运行代码并点击浏览到按钮:

- 在创建
Entry小部件时添加以下两行代码:
# Adding a Textbox Entry widget
self.name = tk.StringVar()
self.name_entered = ttk.Entry(mighty, width=24, textvariable=self.name)
self.name_entered.grid(column=0, row=1, sticky='W')
self.name_entered.delete(0, tk.END)
self.name_entered.insert(0, '< default name >')
- 运行代码并查看以下结果:

- 现在,打开
GUI_copy_files.py并添加以下代码:
# 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)
- 运行
GUI_copy_files.py会导致以下截图:

- 打开
GUI_copy_files.py并添加以下代码:
# 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)
一旦我们点击调用 copyFile() 函数的按钮,我们就导入所需的模块。
- 打开
GUI_copy_files.py并添加以下代码:
from tkinter import messagebox as msg
def copyFile():
import shutil #import module within function
src = self.fileEntry.get()
file = src.split('/')[-1]
dst = self.netwEntry.get() + ''+ file
try:
shutil.copy(src, dst)
msg.showinfo('Copy File to Network', 'Succes: File
copied.')
except FileNotFoundError as err:
msg.showerror('Copy File to Network',
'*** Failed to copy file! ***\n\n' + str(err))
except Exception as ex:
msg.showerror('Copy File to Network',
'*** Failed to copy file! ***\n\n' + str(ex))
- 运行代码,浏览到文件,并点击复制按钮:

- 运行代码,但不要浏览并点击复制按钮:

-
打开
GUI_copy_files.py并将其保存为GUI_copy_files_limit.py。 -
添加以下代码:

- 运行前面的代码以观察输出,如下面的截图所示:

让我们深入了解代码以更好地理解它。
它是如何工作的...
在GUI_copy_files.py中,我们在 GUI 的标签 2 中添加了两个按钮和两个输入框。我们还没有实现按钮回调函数的功能。
点击“浏览文件...”按钮当前会在控制台打印hello from getFileName。在添加import语句后,我们可以使用tkinter内置的文件对话框。
我们现在可以在代码中使用对话框。而不是硬编码路径,我们可以使用 Python 的os模块来找到我们的 GUI 模块所在的全路径。点击“浏览文件...”按钮现在会打开askopenfilename对话框。我们现在可以在这个目录中打开文件或浏览到不同的目录。在对话框中选择文件并点击“打开”按钮后,我们将文件的全路径保存到fName局部变量中。
如果我们在打开 Python 的askopenfilename对话框小部件时,它自动默认到一个目录,这样我们就不必浏览到我们要打开特定文件的地方。最好的做法是通过回到我们的 GUI 标签 1 来演示如何做这件事,这就是我们接下来要做的。
我们可以将值默认到Entry小部件中。回到我们的标签 1,这非常简单。当我们现在运行 GUI 时,name_entered输入框有一个默认值。我们可以获取我们正在使用的模块的全路径,然后我们可以在它下面创建一个新的子文件夹。我们可以将这作为一个模块级别的全局变量,或者我们可以在一个方法中创建子文件夹。
我们为两个Entry小部件设置了默认值,并在设置后,使本地文件Entry小部件为只读。
这个顺序很重要。我们必须首先填充输入框,然后再将其设置为只读。
在调用主事件循环之前,我们也在选择标签 2,并且不再将焦点设置到标签 1 的Entry中。在tkinter的notebook上调用select是零基索引的,所以通过传入值1,我们选择了标签 2:
# Place cursor into name Entry
# name_entered.focus() # commented out
tabControl.select(1) # displayTab 2 at GUI startup
由于我们不在同一个网络上,这个菜谱使用本地硬盘代替网络。
UNC路径是通用命名约定,这意味着通过使用双反斜杠而不是典型的C:\,我们可以访问网络上的服务器。
您只需使用 UNC 路径,并将C:\替换为\\<servername>\<folder>。
这个例子可以用来将我们的代码备份到备份目录,如果它不存在,我们可以使用os.makedirs来创建它。在选择了要复制到其他地方的文件后,我们导入 Python 的shutil模块。我们需要复制文件的源完整路径以及网络或本地目录路径,然后使用shutil.copy将文件名追加到我们将要复制的路径中。
shutil 是 shell 工具的缩写表示。
我们还通过消息框向用户反馈,以指示复制是否成功。为了做到这一点,我们导入messagebox并将其别名设置为msg。
在接下来的代码中,我们混合了两种不同的放置import语句的方法。在 Python 中,我们有其他语言所不具备的灵活性。我们通常将所有的import语句放置在每个 Python 模块的顶部,以便清楚地知道我们正在导入哪些模块。同时,现代编码方法是将变量的创建放置在它们首次被使用的函数或方法附近。
在代码中,我们在 Python 模块的顶部导入消息框,但随后也在一个函数中导入了shutil Python 模块。我们为什么希望这样做?这样做甚至可行吗?答案是肯定的,它是可行的,我们将这个import语句放入函数中,因为这是我们代码中唯一真正需要此模块的地方。
如果我们从未调用这个方法,那么我们永远不会导入这个方法所需的模块。从某种意义上说,你可以将这种技术视为懒加载初始化设计模式。如果我们不需要它,我们不会在 Python 代码中真正需要它时才导入它。这里的想法是,我们的整个代码可能需要,比如说,20 个不同的模块。在运行时,哪些模块真正需要取决于用户交互。如果我们从未调用copyFile()函数,那么就没有必要导入shutil。
当我们现在运行 GUI,浏览到文件,并点击复制时,文件将被复制到我们在 Entry 小部件中指定的位置。
如果文件不存在或者我们忘记浏览到文件而试图复制整个父文件夹,代码也会告诉我们这一点,因为我们正在使用 Python 内置的异常处理功能。
我们新的Entry小部件确实扩展了 GUI 的宽度。虽然有时候看到整个路径是件好事,但同时它也会推其他小部件,使我们的 GUI 看起来不那么好。我们可以通过限制Entry小部件的宽度参数来解决这个问题。我们在GUI_copy_files_limit.py中这样做。这导致 GUI 大小有限。我们可以按右箭头键在启用的Entry小部件中到达该小部件的末尾。
我们正在使用 Python shell 工具将文件从本地硬盘复制到网络上。由于我们大多数人没有连接到同一个局域网,我们通过将代码备份到不同的本地文件夹来模拟复制。
我们正在使用tkinter对话框控件之一,并通过默认目录路径,我们可以提高复制文件的效率。
让我们继续下一个菜谱。
使用 TCP/IP 通过网络进行通信
本菜谱展示了如何使用sockets通过TCP/IP进行通信。为了实现这一点,我们需要一个IP 地址和一个端口号。
为了使事情简单,并且独立于不断变化的互联网 IP 地址,我们将创建自己的本地 TCP/IP 服务器和客户端,我们将学习如何通过 TCP/IP 连接将客户端连接到服务器并读取数据。
我们将通过使用之前菜谱中创建的队列将这种网络功能集成到我们的 GUI 中。
TCP/IP代表传输控制协议/互联网协议,是一组网络协议,允许两台或多台计算机进行通信。
准备工作
我们将创建一个新的 Python 模块,它将成为 TCP 服务器。
如何实现…
在 Python 中实现 TCP 服务器的一种方法是从socketserver模块继承。我们子类化BaseRequestHandler然后覆盖继承的handle方法。在非常少的 Python 代码中,我们可以实现一个 TCP 服务器:
-
创建一个新的 Python 模块并将其保存为
TCP_Server.py。 -
添加以下代码以创建 TCP 服务器和
start函数:
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 start_server():
server = TCPServer(('', 24000), RequestHandler)
server.serve_forever()
- 打开
Queues.py并添加以下代码以创建套接字并使用它:
# using TCP/IP
from socket import socket, AF_INET, SOCK_STREAM
def write_to_scrol_TCP(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.gui_queue.put(recv)
inst.create_thread(6)
-
打开
GUI_copy_files_limit.py并将其保存为GUI_TCP_IP.py。 -
添加以下代码以在单独的线程中启动 TCP 服务器:
class OOP():
def __init__(self):
# Start TCP/IP server in its own thread
svrT = Thread(target=start_server, daemon=True)
svrT.start()
- 运行代码并点击 Tab 1 上的“点击我!”按钮:

让我们深入幕后更好地理解代码。
它是如何工作的…
在TCP_Server.py中,我们将RequestHandler类传递给TCPServer初始化器。空的单引号是 localhost 的快捷方式,即我们的个人电脑。这是127.0.0.1的 IP 地址。元组中的第二个项目是端口号。我们可以选择任何在我们本地电脑上未使用的端口号。
我们必须确保我们在 TCP 连接的客户端使用相同的端口;否则,我们就无法连接到服务器。
当然,在客户端能够连接之前,我们必须首先启动服务器。我们将修改Queues.py模块以成为 TCP 客户端。当我们现在点击“点击我!”按钮时,我们正在调用bq.write_to_scrol_TCP(self),然后创建套接字和连接。
这就是我们与 TCP 服务器通信所需的所有代码。在这个例子中,我们只是向服务器发送一些字节,服务器将它们发送回来,并在返回响应之前添加一些字符串。
这展示了 TCP 通过网络通信的工作原理。
一旦我们知道了如何通过 TCP/IP 连接到远程服务器,我们将使用我们感兴趣与之通信的程序协议设计的任何命令。在我们可以向服务器上驻留的特定应用程序发送命令之前,第一步是连接。
在write_to_scrol_TCP函数中,我们使用与之前相同的循环,但现在我们将消息发送到 TCP 服务器。服务器修改接收到的消息,然后将其发送回我们。接下来,我们将它放入 GUI 类实例队列中,正如之前的菜谱中所述,它在自己的线程中运行:
sock.send(b'Message from a queue: ' + bytes(str(idx).encode()) )
注意字符串前面的b字符,然后进行所需的其余转换。
我们在 OOP 类的初始化器中在自己的线程中启动 TCP 服务器。
在 Python 3 中,我们以二进制格式通过套接字发送字符串。现在添加整数索引变得有点复杂,因为我们不得不将其转换为字符串,对其进行编码,然后将编码后的字符串转换为字节!
在 Tab 1 上点击“点击我!”按钮现在会在我们的ScrolledText小部件以及控制台上创建输出,由于使用了线程,响应速度非常快。我们创建了一个 TCP 服务器来模拟连接到我们局域网或互联网上的服务器。我们将我们的队列模块变成了 TCP 客户端。我们在各自的背景线程中运行队列和服务器,这使得我们的 GUI 非常响应。
让我们继续下一个菜谱。
使用urlopen从网站读取数据
这个菜谱展示了我们如何通过使用 Python 的一些内置模块轻松地读取整个网页。我们首先以原始格式显示网页数据,然后对其进行解码,然后我们在我们的 GUI 中显示它。
准备工作
我们将从网页中读取数据,然后在我们的 GUI 的ScrolledText小部件中显示它。
如何做到这一点...
首先,我们创建一个新的 Python 模块,并将其命名为URL.py。然后我们导入使用 Python 读取网页所需的功能。我们可以在非常少的代码行中做到这一点:
-
创建一个新的模块并命名为
URL.py。 -
添加以下代码以打开和读取 URL:
from urllib.request import urlopen
link = 'http://python.org/'
try:
http_rsp = urlopen(link)
print(http_rsp)
html = http_rsp.read()
print(html)
html_decoded = html.decode()
print(html_decoded)
except Exception as ex:
print('*** Failed to get Html! ***\n\n' + str(ex))
else:
return html_decoded
- 运行前面的代码并观察以下输出:

- 将结果与刚刚读取的官方 Python 网页进行比较:

让我们考虑下一个场景:
-
打开
URL.py。 -
将代码放入一个函数中:
from urllib.request import urlopen
link = 'http://python.org/'
def get_html():
try:
http_rsp = urlopen(link)
print(http_rsp)
html = http_rsp.read()
print(html)
html_decoded = html.decode()
print(html_decoded)
except Exception as ex:
print('*** Failed to get Html! ***\n\n' + str(ex))
else:
return html_decoded
-
从上一个菜谱中的
GUI_TCP_IP.py打开并保存为GUI_URL.py。 -
导入
URL模块并修改click_me方法:
import Ch06_Code.URL as url
# Button callback
def click_me(self):
self.action.configure(text='Hello ' + self.name.get())
bq.write_to_scrol(self)
sleep(2)
html_data = url.get_html()
print(html_data)
self.scrol.insert(tk.INSERT, html_data)
- 运行代码,输出如下:

下一个部分将详细讨论这个过程。
它是如何工作的...
我们将URL.py代码包裹在一个类似于 Java 和 C#的try...except块中。这是一种现代的编程方法,Python 支持。每当我们的代码可能无法完成时,我们可以尝试这段代码,如果它有效,那就没问题。如果try...except块中的代码无法工作,Python 解释器将抛出几种可能的异常之一,然后我们可以捕获它。一旦我们捕获了异常,我们就可以决定下一步做什么。
Python 中存在异常的层次结构,我们也可以创建自己的类,这些类从 Python 异常类继承并扩展。在下面的代码中,我们主要关注的是我们试图打开的 URL 可能不可用,因此我们将代码包裹在一个try...except代码块中。如果代码成功打开请求的 URL,那就没问题。如果失败了,可能是因为我们的互联网连接断开,我们将进入代码的异常部分并打印出异常已发生。
你可以在docs.python.org/3.7/library/exceptions.html了解更多关于 Python 异常处理的信息。
通过在官方 Python 网站上调用urlopen,我们得到整个数据作为一个长字符串。第一个print语句将这个长字符串打印到控制台。然后我们对结果调用decode,这次我们得到了超过 1,000 行的网页数据,包括一些空白。我们还打印出type,调用urlopen的是http.client.HTTPResponse对象。实际上,我们首先打印它。
接下来,我们在 GUI 中的ScrolledText小部件中显示这些数据。为了做到这一点,我们必须将我们的新模块(从网页读取数据)连接到我们的 GUI。为了实现这一点,我们需要一个对我们 GUI 的引用,一种方法是将我们的新模块绑定到 Tab 1 按钮回调。我们可以从 Python 网页返回解码后的 HTML 数据到Button小部件,然后将其放置到ScrolledText控制中。
我们将URL.py代码转换为一个函数,并将数据返回给调用代码。现在,我们可以通过首先导入新模块,然后将数据插入到小部件中,将我们的按钮回调方法中的数据写入ScrolledText控制。我们还在调用write_to_scrol后给它一些休眠时间。
在GUI_URL.py中,HTML 数据现在显示在我们的 GUI 小部件中。
第七章:通过我们的 GUI 将数据存储在我们的 MySQL 数据库中
在本章中,我们将学习如何安装和使用 MySQL 数据库,并将其连接到我们的 GUI。
MySQL 是一个完整的 结构化查询语言(SQL)数据库服务器,并自带一个非常好的 GUI,以便我们可以查看和使用数据。我们将创建一个数据库,将数据插入到我们的数据库中,然后看看我们如何修改、读取和删除数据。
对于用 Python 编写的软件程序,数据存储在 SQL 数据库中是必不可少的。我们目前的所有数据都仅存在于内存中,我们希望使其持久化,这样我们关闭正在运行的 Python 程序时就不会丢失数据。
在这里,您将学习如何通过将 SQL 添加到您的编程工具箱中来提高您的编程技能。
本章的第一个菜谱将向您展示如何安装免费的 MySQL Community Edition。
在成功连接到我们的 MySQL 服务器的一个运行实例之后,我们将设计和创建一个数据库,该数据库将接受一个书名,这可能是我们的个人日记或我们在互联网上找到的引言。我们将需要一个页码,对于书籍来说,这可能为空白(在 SQL 术语中为 NULL),然后我们将使用我们的 GUI(使用 Python 3.7 或更高版本构建)将我们从书籍、期刊、网站或朋友那里喜欢的引言插入到我们的 MySQL 数据库中。
我们将通过我们的 Python GUI 发出这些 SQL 命令并显示数据,来插入、修改、删除和显示我们最喜欢的引言。
CRUD 是您可能之前遇到过的数据库术语,它是四个基本 SQL 命令的缩写,即 创建、读取、更新和删除。
这里是本章 Python 模块的概述:

在本章中,我们将通过将 GUI 连接到 MySQL 数据库来增强我们的 Python GUI。我们将涵盖以下菜谱:
-
从 Python 安装并连接到 MySQL 服务器
-
配置 MySQL 数据库连接
-
设计 Python GUI 数据库
-
使用 SQL INSERT 命令
-
使用 SQL UPDATE 命令
-
使用 SQL DELETE 命令
-
从我们的 MySQL 数据库存储和检索数据
-
使用 MySQL Workbench
从 Python 安装并连接到 MySQL 服务器
在我们能够连接到 MySQL 数据库之前,我们必须连接到 MySQL 服务器。为了做到这一点,我们需要知道 MySQL 服务器的 IP 地址以及它监听的端口。
我们还必须是一个注册用户,并有一个密码,以便能够被 MySQL 服务器认证。
准备工作
您需要能够访问一个正在运行的 MySQL 服务器实例,并且拥有创建数据库和表的管理员权限。
如何做到这一点…
让我们看看如何从 Python 安装和连接到 MySQL 服务器:
- 下载 MySQL 安装程序。
官方的 MySQL 网站上有一个免费的 MySQL 社区版。你可以从 dev.mysql.com/downloads/windows/installer/ 下载并安装到你的本地 PC 上。
- 运行安装程序:

- 为
root用户选择一个密码,并且可选地添加更多用户:

- 验证你是否拥有
\Python37\Lib\site-packages\mysql\connector文件夹:

- 打开
mysqlsh.exe可执行文件,双击它以运行:

-
在提示符中输入
\sql以进入SQL模式。 -
在
MySql>提示符中,输入SHOW DATABASES。然后,按 Enter:

- 创建一个新的 Python 模块并将其保存为
MySQL_connect.py:
import mysql
conn = mysql.connector.connect(user=<adminUser>, password=<adminPwd>, host='127.0.0.1')
print(conn)
conn.close()
- 如果运行前面的代码产生以下输出,那么我们就已成功连接:

让我们深入了解代码,以便更好地理解它。
它是如何工作的…
首先,我们下载并安装了与我们的操作系统匹配的 MySQL 版本。
在安装过程中,你将为 root 用户选择一个密码,你也可以添加更多用户。我建议你添加自己作为 DB Admin 并选择一个密码。
在本章中,我们使用的是最新的 MySQL 社区服务器版本,即 8.0.16。
SQL 代表 结构化查询语言,有时发音为 sequel。它使用 集合 数学方法,该方法基于数学和 集合理论。你可以在 en.wikipedia.org/wiki/Set_theory 上了解更多信息。
为了连接到 MySQL,我们可能需要安装一个特殊的 Python 连接器驱动程序。这个驱动程序将允许我们从 Python 与 MySQL 服务器通信。MySQL 网站上有一个免费可用的驱动程序 (dev.mysql.com/doc/connector-python/en/index.html),并且它附带了一个非常好的在线教程。
当我安装最新版本的 MySQL 的新安装时,Python 连接器会自动安装。因此,你可能根本不需要安装它。不过,了解这一点是好的,以防你遇到任何问题并需要自己安装它。
验证我们已经安装了正确的驱动程序,并且它能让 Python 与 MySQL 通信的一种方法是查看 Python 的 site-packages 目录。如果你的 site-packages 目录中有一个新的MySQL文件夹,其中包含一个 connector 子文件夹,则安装成功。我们在 步骤 4 中做了这件事。
在 步骤 5 中,我们通过使用 MySQL Shell. 验证了我们的 MySQL 服务器安装实际上是否工作。
你的路径可能不同,尤其是如果你使用的是 macOS 或 Linux:<path to>\Program Files\MySQL\MySQL Shell 8.0\bin。
然后,我们验证了我们可以使用 Python 3.7 实现相同的结果。
将占位符括号中的名称,即<adminUser>和<adminPwd>,替换为您在 MySQL 安装中使用的真实凭据。
我们必须能够连接到 MySQL 服务器。默认情况下,我们处于 JavaScript JS 模式。我们可以通过在提示符中键入 \sql 来切换到 SQL 模式。现在,我们可以使用 SQL 命令。我们在步骤 6和步骤 7中这样做过。
如果您无法通过命令行或 Python 的 mysqlclient 连接到 MySQL 服务器,那么在安装过程中可能出了些问题。如果是这种情况,请尝试卸载 MySQL,重新启动您的 PC,然后再次运行安装程序。
为了将我们的 GUI 连接到 MySQL 服务器,我们需要能够以管理员权限连接到服务器。如果我们想创建自己的数据库,我们也需要这样做。如果数据库已经存在,那么我们只需要连接、插入、更新和删除数据的授权权限。我们将在下一个配方中在 MySQL 服务器上创建一个新的数据库。
配置 MySQL 数据库连接
在上一个配方中,我们使用了连接到 MySQL 服务器的最短方式,即通过在 connect 方法中硬编码认证所需的凭据。虽然这是一种早期开发中快速的方法,但我们绝对不希望将我们的 MySQL 服务器凭据暴露给任何人。相反,我们希望授权特定用户,以便他们可以访问数据库、表、视图和相关数据库命令。
通过将凭据存储在配置文件中来由 MySQL 服务器进行认证是一种更安全的方法,这正是我们在本配方中要做的。我们将使用我们的配置文件来连接到 MySQL 服务器,然后在 MySQL 服务器上创建自己的数据库。
我们将在本章的所有配方中使用此数据库。
准备工作
运行本配方中所示代码需要具有管理员权限的运行 MySQL 服务器访问权限。
前一个配方展示了如何安装 MySQL 服务器的免费 社区版。管理员权限将允许您实施此配方。
如何做…
让我们看看如何执行此配方:
-
首先,我们将在与
MySQL_connect.py代码相同的模块中创建一个字典。然后,我们将按顺序执行以下操作:-
打开
MySQL_connect.py并将其保存为MySQL_connect_with_dict.py。 -
向模块中添加以下代码:
-
# create dictionary to hold connection info
dbConfig = {
'user': <adminName>, # use your admin name
'password': <adminPwd>, # use your real password
'host': '127.0.0.1', # IP address of localhost
}
- 在
dbConfig下方写下以下代码:
import mysql.connector
# unpack dictionary credentials
conn = mysql.connector.connect(**dbConfig)
print(conn)
-
运行代码以确保其正常工作。
-
创建一个新的模块,
GuiDBConfig.py,并将以下代码放入其中:
# create dictionary to hold connection info
dbConfig = {
'user': <adminUser>, # your user name
'password': <adminPwd>, # your password
'host': '127.0.0.1', # IP address
}
-
现在,打开
MySQL_connect_with_dict.py并将其保存为MySQL_connect_import_dict.py。 -
导入
GuiDBConfig并解包字典,如下所示:
import GuiDBConfig as guiConf
# unpack dictionary credentials
conn = mysql.connector.connect(**guiConf.dbConfig)
print(conn)
- 创建一个新的 Python 模块并将其保存为
MySQL_create_DB.py。接下来,添加以下代码:
import mysql.connector
import Ch07_Code.GuiDBConfig as guiConf
GUIDB = 'GuiDB'
# unpack dictionary credentials
conn = mysql.connector.connect(**guiConf.dbConfig)
cursor = conn.cursor()
try:
cursor.execute("CREATE DATABASE {}
DEFAULT CHARACTER SET 'utf8'".format(GUIDB))
except mysql.connector.Error as err:
print("Failed to create DB: {}".format(err))
conn.close()
- 执行
MySQL_create_DB.py两次:

- 创建一个新的 Python 模块并将其保存为
MySQL_show_DBs.py。然后,添加以下代码:
import mysql.connector
import GuiDBConfig as guiConf
# unpack dictionary credentials
conn = mysql.connector.connect(**guiConf.dbConfig)
cursor = conn.cursor()
cursor.execute("SHOW DATABASES")
print(cursor.fetchall())
conn.close()
- 运行前面的代码会给我们以下输出:

让我们深入了解代码以更好地理解它。
它是如何工作的...
首先,我们创建了一个字典,并将连接凭据保存在 Python 字典中。
接下来,在connect方法中,我们解包了字典值。看看以下代码:
mysql.connector.connect('user': <adminName>, 'password': <adminPwd>, 'host': '127.0.0.1')
我们不是使用此代码,而是使用(**dbConfig),它达到相同的效果但更简洁。
这导致与 MySQL 服务器成功建立相同的连接,但不同之处在于连接方法不再暴露任何关键任务信息。
数据库服务器对你的任务至关重要。一旦你失去了宝贵的资料,而且找不到任何最近的备份,你就会意识到这一点!
请注意,将相同的用户名、密码、数据库等放入同一 Python 模块的字典中并不能消除凭据被任何查看代码的人看到的危险。
为了提高数据库安全性,我们必须将字典移动到它自己的 Python 模块中。我们称新的 Python 模块为GuiDBConfig.py。
然后,我们导入了此模块,并解包了凭据,就像我们之前做的那样。
一旦我们将此模块放置在安全的地方,与代码的其他部分分离,我们就为我们的 MySQL 数据实现了更高的安全级别。
现在我们知道了如何连接到 MySQL 并拥有管理员权限,我们可以通过发出 SQL 命令来创建自己的数据库。
为了执行对 MySQL 的命令,我们从一个连接对象中创建了一个游标对象。
光标通常是指向数据库表中特定行的指针,我们可以将其在表中上下移动,但在这里,我们用它来创建数据库本身。我们将 Python 代码封装在try...except块中,并使用 MySQL 的内置错误代码来告诉我们是否发生了错误。
我们可以通过执行两次数据库创建代码来验证此块是否工作。第一次,它将在 MySQL 中创建一个新的数据库,第二次,它将打印出一个错误消息,指出该数据库已存在。
我们可以通过使用相同的游标对象语法执行SHOW DATABASES命令来验证哪些数据库存在。我们不是发出CREATE DATABASE命令,而是创建一个游标并使用它来执行SHOW DATABASES命令,然后将结果检索并打印到控制台输出。
我们通过在游标对象上调用fetchall方法来检索结果。
运行MySQL_show_DBs.py代码显示我们 MySQL 服务器实例中当前存在的数据库。正如我们从输出中看到的那样,MySQL 自带了几个内置数据库,例如information_schema。我们成功创建了我们的guidb数据库,如输出所示。所有其他展示的数据库都是 MySQL 自带。
注意,尽管我们在创建时将其指定为混合大小写的 GuiDB,但 SHOW DATABASES 命令显示 MySQL 中所有现有数据库均为小写,并显示我们的数据库为 guidb。
物理 MySQL 文件根据 my.ini 文件存储在硬盘上,在 Windows 10 安装中,可能位于 C:\ProgramData\MySQL\MySQL Server 8.0。在此 .ini 文件中,您可以找到以下指向 Data 文件夹的配置路径:
# 数据库根路径
datadir=C:/ProgramData/MySQL/MySQL Server 8.0/Data
让我们继续到下一个菜谱。
设计 Python GUI 数据库
在我们开始创建表并将数据插入其中之前,我们必须设计数据库。与更改本地 Python 变量名不同,一旦创建并加载了数据,更改数据库 schema 就不是那么容易了。
我们将不得不 DROP 表,这意味着我们会丢失表中所有的数据。因此,在删除表之前,我们必须提取数据,将数据保存到临时表或其他数据格式中,然后 DROP 表,重新创建它,并最终重新导入原始数据。
希望您已经明白了这可能会多么繁琐。
设计我们的 GUI MySQL 数据库意味着我们需要考虑我们的 Python 应用程序将如何使用它,然后为我们的表选择符合预期目的的名称。
准备工作
我们将使用在前一个菜谱中创建的 MySQL 数据库,配置 MySQL 数据库连接。需要一个正在运行的 MySQL 实例,前两个菜谱展示了如何安装 MySQL、所有必要的附加驱动程序以及如何创建本章中使用的数据库。
如何操作…
在这个菜谱中,我们从上一章的 GUI_TCP_IP.py 文件开始。我们将把我们的 Python GUI 中的小部件从上一个菜谱中创建的两个标签页之间移动,以便组织我们的 Python GUI,使其能够连接到 MySQL 数据库。让我们看看如何完成这个菜谱:
-
打开
GUI_TCP_IP.py并将其保存为GUI_MySQL.py。 -
从 Packt 网站下载完整代码。
-
使用 WinMerge 等工具比较两个版本的 GUI:

- 运行位于
GUI_MySQL.py中的代码。您将观察到以下输出:

-
-
现在,打开
MySQL_create_DB.py并将其保存为MySQL_show_DB.py。 -
将
try...catch块替换为以下代码:
-
# 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()
- 运行代码并观察输出:

-
-
创建一个类似于
GUI_MySQL_class.py的模块。 -
添加并运行以下代码:
-
# 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()
- 运行以下代码,该代码位于
GUI_MySQL_class.py中:

- 打开命令提示符并导航到
mysql.exe:

- 运行
mysql.exe:

- 输入
SHOW COLUMNS FROM books;命令:

- 通过运行以下代码创建第二个表:
# 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命令:

- 执行
SHOW COLUMNS命令:

- 再次执行
SHOW COLUMNS命令:

让我们深入了解代码,以更好地理解它。
它是如何工作的...
我们从上一章的GUI_TCP_IP.py文件开始,重新组织了小部件。
我们重命名了几个小部件,并将访问 MySQL 数据的代码分离到之前命名为Tab 1的部分,并将无关的小部件移动到之前命名为Tab 2的部分。我们还调整了一些内部 Python 变量名,以便我们更好地理解我们的代码。
代码可读性是一种编程美德,而不是浪费时间。
重构的模块接近 400 行 Python 代码,在这里展示整个代码会占用太多页面。在 Windows 上,我们可以使用一个名为WinMerge的工具来比较不同的 Python 代码模块。我确信 macOS 和 Linux 也有类似的工具。
WinMerge 是一个在 Windows 上比较不同 Python(和其他)代码模块的出色工具。我们可以用它来查看代码模块之间的差异。您可以从sourceforge.net/projects/winmerge免费下载它。
我们重构的 Python GUI 现在看起来如下:

我们将第一个标签重命名为 MySQL,并创建了两个LabelFrame小部件。我们称顶部的为Python Database,它包含两个标签和六个tkinter Entry 小部件,以及三个按钮,我们使用tkinter网格布局管理器将它们排列成四行三列。我们将输入书名和页数到 Entry 小部件中。点击按钮将导致插入、检索或修改书籍引用。底部的LabelFrame小部件的标签为“Book Quotation”,而这个框架中的ScrolledText小部件将显示我们的书籍和引用。
然后,我们创建了两个 SQL 表来存储我们的数据。第一个将存储书名和书页数据,然后将与第二个表连接,该表将存储书籍引用。我们将通过主键到外键关系将两个表链接在一起。
现在,让我们创建第一个数据库表。在我们这样做之前,让我们验证一下我们的数据库确实没有表。根据在线 MySQL 文档,查看数据库中存在的表的命令如下:
13.7.6.37 SHOW TABLES Syntax
SHOW [FULL] TABLES [{FROM | IN} db_name]
[LIKE 'pattern' | WHERE expr]
重要的是要注意,在前面的语法中,方括号中的参数,如FULL,是可选的,而花括号中的参数,如FROM,对于SHOW TABLES命令是必需的。FROM和IN之间的管道符号表示 MySQL 语法要求使用其中一个或另一个。
当我们在MySQL_show_DB.py中执行 SQL 命令时,我们得到预期的结果,即一个空元组,显示我们的数据库目前没有表。
我们也可以通过执行USE <DB>命令来选择数据库。通过这样做,我们不需要将它传递给SHOW TABLES命令,因为我们已经选择了我们想要与之通信的数据库。
所有的 SQL 代码都位于GUI_MySQL_class.py中,我们将它导入到GUI_MySQL.py中。
既然我们已经知道如何验证我们的数据库中没有表,我们就创建一些。在创建两个表之后,我们使用之前的相同命令来验证它们确实已经进入我们的数据库。
通过这样做,我们创建了第一个表,命名为Books。
我们可以通过执行cursor.execute("SHOW TABLES FROM guidb")命令来验证表是否已经创建在我们的数据库中。
结果不再是空元组,而是一个包含元组的元组,显示了刚刚创建的books表。
我们可以使用 MySQL 命令行客户端查看我们表中的列。为了做到这一点,我们必须以root用户登录。我们还需要在命令的末尾添加一个分号。
在 Windows 上,你只需双击 MySQL 命令行客户端快捷方式,该快捷方式在 MySQL 安装期间自动安装。
如果你桌面上没有快捷方式,你可以找到典型默认安装的可执行文件在以下路径:
C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe
没有运行 MySQL 客户端的快捷方式,你必须传递给它一些参数:
-
C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe -
-u root -
-p
如果双击创建错误,请确保你使用-u和-p选项。
要启动 MySQL 命令行客户端,无论是通过双击快捷方式还是使用带有完整路径的可执行文件的命令行并传递所需参数,都会提示你输入 root 用户的密码。
如果你记得你在安装期间为 root 用户分配的密码,那么你可以运行SHOW COLUMNS FROM books;命令。这将显示我们guidb数据库中books表的列。
在 MySQL 客户端中执行命令时,语法不是 Python 式的,因为它需要一个尾随的分号来完成语句。
接下来,我们创建了第二个表,它将存储书籍和期刊引用。我们通过编写与创建第一个表时类似的代码来创建它。通过运行相同的 SQL 命令,我们验证了我们现在有两个表。
我们可以通过使用 Python 执行 SQL 命令来查看列:
cursor.execute("SHOW COLUMNS FROM quotations")
使用 MySQL 客户端可能比命令提示符更好地显示数据格式。我们还可以使用 Python 的 pretty print (pprint)功能来完成此操作。
MySQL 客户端仍然以更清晰的格式显示我们的列,您可以在运行此客户端时看到这一点。
我们设计了我们的 Python GUI 数据库,并重构了我们的 GUI,为使用新的数据库做准备。然后,我们创建了一个 MySQL 数据库,并在其中创建了两个表。
我们通过使用 Python 和 MySQL 服务器附带 MySQL 客户端,验证了表已成功添加到我们的数据库中。
在下一个菜谱中,我们将向我们的表中插入数据。
使用 SQL INSERT 命令
这个菜谱展示了整个 Python 代码,展示了如何创建和删除 MySQL 数据库和表,以及如何显示 MySQL 实例的现有数据库、表、列和数据。
在创建数据库和表之后,我们将向本章中创建的两个表中插入数据。
我们使用主键到外键的关系来连接两个表的数据。
我们将在接下来的两个菜谱中详细介绍这是如何工作的,我们将修改和删除 MySQL 数据库中的数据。
准备工作
这个菜谱基于我们在上一个菜谱中创建的 MySQL 数据库,设计 Python GUI 数据库,并展示了如何删除和重新创建 GuiDB。
删除数据库当然会删除数据库表中所有的数据,因此我们将向您展示如何重新插入这些数据。
如何做...
GUI_MySQL_class.py模块中的所有代码都位于本章的代码文件夹中,您可以从github.com/PacktPublishing/Python-GUI-Programming-Cookbook-Third-Edition下载。让我们按顺序进行这些步骤:
-
下载本章的代码。
-
打开
GUI_MySQL_class.py并查看类方法:
import mysql.connector
import Ch07_Code.GuiDBConfig as guiConf
class MySQL():
# class variable
GUIDB = 'GuiDB'
#------------------------------------------------------
def connect(self):
# connect by unpacking dictionary credentials
# create 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()
-
运行前面的代码(包括代码的完整实现)将在我们创建的数据库中创建以下表和数据。
-
打开命令提示符并执行两个
SELECT *语句:

让我们深入了解代码,以更好地理解它。
工作原理...
GUI_MySQL_class.py代码创建数据库,向其中添加表,然后向两个我们创建的表中插入数据。
在这里,我们概述了代码,而没有展示所有实现细节,以节省空间,因为展示整个代码需要太多页面。
我们创建了一个 MySQL 数据库,连接到它,然后创建了两个表,用于存储喜欢的书籍或期刊引文的数据。
我们在两个表中分配了数据,因为引文往往相当长,而书名和书页码非常短。通过这样做,我们可以提高数据库的效率。
在 SQL 数据库语言中,将数据分离到单独的表中称为规范化。在使用 SQL 数据库时,你需要做的最重要的事情之一是将数据分离到相关的表中,也称为关系。
让我们继续到下一个菜谱。
使用 SQL 的UPDATE命令
这个菜谱将使用上一个菜谱中的代码,即使用 SQL 的INSERT命令,更详细地解释它,然后扩展代码以更新数据。
为了更新我们之前插入到 MySQL 数据库表中的数据,我们需要使用 SQL 的UPDATE命令。
准备工作
这个菜谱建立在之前的菜谱使用 SQL 的INSERT命令的基础上,所以请阅读并学习之前的菜谱,以便跟随这个菜谱中的代码,我们将修改现有数据。
如何做到这一点...
让我们看看我们如何使用SQL UPDATE命令:
-
首先,我们将通过运行以下 Python 到 MySQL 命令来显示要修改的数据。然后,我们按顺序执行以下步骤:
-
打开
GUI_MySQL_class.py。 -
看看
showData方法:
-
import mysql.connector
import Ch07_Code.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()
- 运行前面的代码给出了以下输出:

- 看看
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("Primary key=" + str(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__':
mySQL = MySQL() # Create class instance
mySQL.updateGOF()
- 运行位于
GUI_MySQL_class.py中的方法:

- 添加以下代码并运行:
#------------------------------------------------------
def showDataWithReturn(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.showDataWithReturn()
print(book, quote)
- 打开 MySQL 客户端窗口并运行
SELECT *语句:

让我们深入了解代码以更好地理解它。
它是如何工作的...
首先,我们打开了GUI_MySQL_class.py文件或者在我们自己的模块中输入显示的代码并运行它。
我们可能不同意四人帮的观点,所以让我们改变他们著名的编程名言。
四人帮是四位创建了世界著名的《设计模式》这本书的作者,这本书强烈影响了我们整个软件行业,使我们认识到、思考和用软件设计模式进行编码。
我们通过更新我们最喜欢的引言数据库来做到这一点。首先,我们通过搜索书名来检索主键值。然后,我们将该值传递到我们的引言搜索中。
现在我们知道了引言的主键,我们可以通过执行 SQL 的UPDATE命令来更新引言。
在运行代码之前,我们的Book_ID = 1的标题通过一个主键与引言表中的Books_Book_ID列的外键关系相关联。这是来自设计模式书的原始引言。
在步骤 5中,我们通过 SQL 的UPDATE命令更新了与该 ID 相关的引言。
没有 ID 发生变化,但现在与Book_ID = 1关联的引言已经改变,如第二个 MySQL 客户端窗口所示。
在这个菜谱中,我们从数据库中检索了我们之前菜谱中创建的数据库表中的现有数据。我们使用 SQL 的UPDATE命令向表中插入数据并更新我们的数据。
让我们继续到下一个菜谱。
使用 SQL DELETE 命令
在这个菜谱中,我们将使用 SQL DELETE命令来删除之前在使用 SQL UPDATE 命令中创建的数据。
打开GUI_MySQL_class.py并查看def createTables(self): ...:
它是如何工作的…
虽然删除数据一开始可能看起来很简单,但一旦我们在生产环境中有一个相当大的数据库设计,事情可能就不会那么简单了。
这个菜谱使用了 MySQL 数据库、表以及之前菜谱中插入到这些表中的数据,使用 SQL UPDATE 命令。为了演示如何创建孤立记录,我们不得不更改我们数据库中的一个表的设计。
故意将设计改为糟糕的设计只是为了演示目的,并不是设计数据库的推荐方式。
如何操作…
在将数据插入到books和quotations表之后,如果我们执行一个DELETE语句,我们只删除了Book_ID = 1的书籍,而与之相关的Books_Book_ID = 1的引文却被留下了。
- 让我们深入了解代码以更好地理解它。
# 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")
- 执行以下 SQL 命令:
cursor.execute("DELETE FROM books WHERE Book_ID = 1")
- 执行以下两个
SELECT *命令:

- 因为我们通过将两个表通过主键到外键关系关联起来设计了我们的 GUI 数据库,当我们删除某些数据时,我们不会得到孤立记录,因为这种数据库设计会处理级联删除。
# 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")
#==========================================================
if __name__ == '__main__':
# Create class instance
mySQL = MySQL()
mySQL.showData()
- 执行
showData()方法:

- 执行
deleteRecord()方法,然后执行showData()方法:
import mysql.connector
import Ch07_Code.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()
- 上述代码将产生以下输出:

我们通过仅使用两个数据库表来保持我们的数据库设计简单。
如果我们不将quotations表与books表建立外键关系,我们可能会得到孤立记录。请看以下步骤:
准备工作
当我们删除数据时,虽然这看起来很简单,但如果我们有一个相当大的数据库设计在生产环境中,事情可能不会那么简单。
打开GUI_MySQL_class.py并查看def createTablesNoFK(self): ...:
这是一个 孤立的记录。一本Book_ID为1的书籍记录已不再存在。
这种情况可能会导致 数据损坏,我们可以通过使用 级联 删除来避免这种情况。
我们在创建表时通过添加某些数据库 约束 来防止这种情况。当我们创建之前菜谱中保存引文的quotations表时,我们创建了一个带有 外键 约束 的quotations表,该约束明确引用了书籍表的 主键,将两者联系起来。
FOREIGN KEY关系包括ON DELETE CASCADE属性,这基本上告诉我们的 MySQL 服务器,当与这个外键相关的记录被删除时,要删除此表中的相关记录。
由于这种设计,不会留下任何孤立的记录,这正是我们想要的。
在 MySQL 中,我们必须在相关的两个表中指定ENGINE=InnoDB,才能使用主键与外键关系。
showData()方法显示我们有两条记录,它们通过主键与外键关系相关联。
当我们现在在books表中删除一条记录时,我们期望quotations表中的相关记录也会通过级联删除被删除。
执行删除和显示记录的命令后,我们得到了新的结果。
我们最喜欢的引言数据库中的著名设计模式已经消失了。这只是一个玩笑——我个人非常重视著名的设计模式。然而,Python 的鸭子类型确实是一个非常酷的特性!
通过通过设计我们的数据库,通过主键到外键关系以及级联删除,我们在这个菜谱中触发了级联删除。
这保持了我们的数据健全和完整。
在下一个菜谱中,我们将使用我们的 Python GUI 中的GUI_MySQL_class.py模块的代码。
从我们的 MySQL 数据库存储和检索数据
我们将使用我们的 Python GUI 将数据插入到我们的 MySQL 数据库表中。我们已经重构了在之前的菜谱中构建的 GUI,为连接和使用数据库做准备。
我们将使用两个文本框输入控件,我们可以在这里输入书籍或期刊的标题和页码。我们还将使用一个ScrolledText控件来输入我们喜欢的书籍引言,然后将其存储在我们的 MySQL 数据库中。
准备工作
这个菜谱将基于我们在本章前面的菜谱中创建的 MySQL 数据库和表。
如何做…
我们将使用我们的 Python GUI 插入、检索和修改我们最喜欢的引言。我们重构了 GUI 的 MySQL 标签,为连接和使用数据库做准备。让我们看看我们如何处理这个问题:
-
打开
GUI_MySQL.py。 -
运行此文件中的代码显示我们的 GUI:

-
打开
GUI_MySQL.py。 -
注意这里显示的
insertQuote()方法:
# 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)
- 运行
GUI_MySQL.py,输入一个引言,然后点击插入引言按钮:

- 点击获取引言:

- 打开
GUI_MySQL.py并查看getQuote方法和按钮:
# 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)
- 打开
GUI_MySQL.py并查看self.mySQL和showBooks():
from Ch07_Code.GUI_MySQL_class 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
让我们回顾一下这个菜谱是如何工作的。
它是如何工作的…
为了让GUI_MySQL.py中的按钮执行某些操作,我们将它们连接到回调函数,就像我们在本书中多次做的那样。我们在按钮下方显示ScrolledText控件中的数据。
为了做到这一点,我们导入了 GUI_MySQL_class.py 模块。与我们的 MySQL 服务器实例和数据库通信的所有代码都驻留在该模块中,这是一种在面向对象编程(OOP)精神中封装代码的形式。
我们将“插入引文”按钮连接到 insertQuote() 方法回调。
当我们运行我们的代码时,我们可以将我们的 Python GUI 中的数据插入到我们的 MySQL 数据库中。
在输入书名、书页以及书中的引文后,我们通过点击“插入引文”按钮将数据插入到我们的数据库中。
我们当前的设计允许标题、页码和引文。我们还可以插入我们喜欢的电影引文。虽然电影没有页码,但我们可以使用页码列来插入引文在电影中发生的大致时间。
在插入数据后,我们通过点击“获取引文”按钮来验证它是否已进入我们的两个 MySQL 表,然后显示了我们在两个 MySQL 数据库表中插入的数据,如步骤 6中的截图所示。
点击“获取引文”按钮将调用我们与按钮点击事件关联的回调方法。这为我们提供了我们在 ScrolledText 小部件中显示的数据。
我们使用 self.mySQL 类实例属性来调用 showBooks() 方法,这是我们在导入的 MySQL 类中的一部分。
在这个食谱中,我们导入了我们编写的 Python 模块,其中包含我们连接到 MySQL 数据库所需的全部编码逻辑。它也知道如何插入、更新和删除数据。
现在,我们已经将我们的 Python GUI 连接到这个 SQL 逻辑。
让我们继续下一个食谱。
使用 MySQL Workbench
MySQL 提供了一个非常棒的免费 GUI,我们可以下载。它被称为MySQL Workbench。
在这个食谱中,我们将成功安装 Workbench,然后使用它来运行针对我们在之前的食谱中创建的 GuiDB 的 SQL 查询。
准备工作
为了使用这个食谱,你需要我们在之前的食谱中开发的 MySQL 数据库。你还需要一个正在运行的 MySQL 服务器。
如何操作…
我们可以从官方 MySQL 网站下载 MySQL Workbench:dev.mysql.com/downloads/workbench/.
让我们看看我们如何执行这个食谱:
-
下载 MySQL Workbench 安装程序。
-
点击“下载”按钮:

- 运行安装:

- 点击“下一步 >”直到安装完成:

- 打开 MySQL Workbench:

- 选择我们的
guidb:

- 编写并执行一些 SQL 命令:

让我们深入了解代码以更好地理解它。
它是如何工作的…
当您安装 MySQL 时,如果您已经在您的 PC 上安装了所需的组件,您可能已经安装了 MySQL Workbench。如果您没有安装 Workbench,步骤 1 到 3 将向您展示如何安装 MySQL Workbench。
MySQL Workbench 本身就是一个图形用户界面,与我们在前面的菜谱中开发的非常相似。它确实包含了一些特定于与 MySQL 一起工作的附加功能。安装窗口中的 8.0 CE 是版本 8.0 社区版 的缩写。
当您启动 MySQL Workbench 时,它将提示您连接。使用您为其创建的 root 用户和密码。MySQL Workbench 足够智能,能够识别您正在运行 MySQL 服务器以及它监听的端口。
一旦成功登录到您的 MySQL 服务器实例,您可以选择我们创建的 guidb。
我们可以在 SCHEMAS 标签下找到我们的 guidb。
在某些文献和产品中,数据库通常被称为 SCHEMAS。图示指的是数据库的结构和布局。我个人来自 Microsoft SQL Server,习惯于简单地称它们为 数据库。
我们可以在查询编辑器中输入 SQL 命令,并通过点击闪电图标来执行我们的命令。它位于右上角,如以下截图所示:

我们可以在结果网格中看到结果。我们可以点击不同的选项卡来查看不同的结果。
现在,我们可以通过 MySQL Workbench 图形用户界面连接到我们的 MySQL 数据库。我们可以执行之前发出的相同 SQL 命令,并获得与我们在 Python 图形用户界面中执行它们时相同的结果。
还有更多...
通过在本章和前几章的菜谱中获得的全部知识,我们现在已经准备好创建自己的 Python 编写的 GUI,所有这些 GUI 都可以连接并与 MySQL 数据库进行通信。
第八章:国际化和测试
在本章中,我们将通过在标签、按钮、标签页和其他小部件上显示不同语言的文本来国际化我们的 GUI。我们将从简单开始,然后探讨我们如何在设计层面为国际化准备我们的 GUI。
我们还将本地化GUI,这与国际化略有不同。
由于这些单词很长,它们已经被缩写为使用单词的第一个字母,然后是第一个和最后一个字母之间的总字符数,最后是单词的最后一个字母。因此,国际化变为I18N,本地化变为L10N。
我们还将测试我们的 GUI 代码,编写单元测试,并探索单元测试在我们开发努力中的价值,这将引导我们到重构代码的最佳实践。
没有额外的 Python 包需要安装。我们用 Python 编写自己的代码,单元测试框架是 Python 自带的,所以我们可以简单地导入它。
了解如何国际化并测试我们的代码是每个程序员都需要掌握的基本技能。
在本章中,你将获得测试、重构和国际化等宝贵技能。
下面是本章 Python 模块的概述:

我们将国际化并测试我们的 Python GUI,涵盖以下菜谱:
-
在不同语言中显示小部件文本
-
一次性更改整个 GUI 语言
-
本地化 GUI
-
为国际化准备 GUI
-
如何以敏捷的方式设计 GUI
-
我们是否需要测试 GUI 代码?
-
设置调试监视器
-
配置不同的调试输出级别
-
使用 Python 的
__main__部分创建自测试代码 -
使用单元测试创建健壮的 GUI
-
如何使用 Eclipse PyDev 集成开发环境(IDE)编写单元测试
在不同语言中显示小部件文本
在 Python 中将文本字符串国际化最简单的方法是将它们移动到单独的 Python 模块中,然后通过传递一个参数给这个模块来选择 GUI 中要显示的语言。
根据在线搜索结果,这种方法并不特别推荐,但根据你正在开发的应用程序的具体要求,它可能仍然是实现起来最实用和最快的方法。
准备工作
我们将重用我们在第七章,“通过我们的 GUI 将数据存储到我们的 MySQL 数据库中”中创建的 Python GUI。我们将注释掉一行创建 MySQL 标签的 Python 代码,因为我们在这个章节中不与 MySQL 数据库交互。
如何操作...
在这个菜谱中,我们将通过更改窗口标题为其他语言来开始我们 GUI 的 I18N。
由于 GUI 在其他语言中名称相同,我们将首先扩展名称,以便我们可以看到我们更改的视觉效果。现在让我们详细查看步骤:
- 打开上一章(第七章,通过我们的 GUI 在我们的 MySQL 数据库中存储数据)中的
GUI_MySQL.py并将其保存为GUI.py。
以下是我们之前的代码行:
self.win.title("Python GUI")
- 让我们将此更改为以下代码。同时,注释掉创建 MySQL 标签的代码:
self.win.title("Python Graphical User Interface") # new window title
# self.mySQL = MySQL() # comment this line out
- 前面的代码更改导致我们的 GUI 程序的以下标题:

请注意,在本章中,我们将使用英语和德语来举例说明国际化我们的 Python GUI 的原则。
-
让我们创建一个新的 Python 模块,并将其命名为
LanguageResources.py。接下来,我们将我们的 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'
- 导入
I18N类并将语言更改为'de':
from Ch08_Code.LanguageResources import I18N
class OOP():
def __init__(self, language='en'):
self.win = tk.Tk() # Create instance
self.i18n = I18N('de') # Select different language
self.win.title(self.i18n.title) # Add a title using self.i18n
- 运行前面的代码,我们现在得到以下国际化结果:

现在,让我们幕后了解代码,以便更好地理解。
它是如何工作的…
从第 4 步开始,我们将 GUI 中作为其一部分的硬编码字符串分离到它们自己的单独模块 LanguageResources.py 中。在类的 __init__() 方法中,我们根据传入的语言参数选择 GUI 将显示哪种语言。然后我们将 LanguageResources.py 模块导入到我们的面向对象类模块中。
我们将默认语言设置为 'en',这意味着英语。
在 GUI.py 中,我们正在创建 I18N 类的一个实例。这个类位于 LanguageResources.py 中,因此我们类的名称更短,与 Python 模块的名称不同。我们将选定的语言保存在类的实例属性 self.i18n 中,并使用它来显示标题。我们正在将 GUI 与其显示的语言分离,这是一个面向对象的设计原则。
我们可以通过将国际化字符串分离到单独的文件中进一步模块化我们的代码,这些文件可能是 XML 或其他格式。我们也可以从 MySQL 数据库中读取它们。
这是一个 关注点分离(SoC)的编码方法,这是面向对象编程的核心。
一次性更改整个 GUI 语言
在这个示例中,我们将通过重构所有之前硬编码的英文字符串到一个单独的 Python 模块中,然后国际化这些字符串,一次性更改所有 GUI 显示名称。
这个示例表明,避免硬编码任何字符串,这些字符串是我们 GUI 显示的,将 GUI 代码与 GUI 显示的文本分离,是一个好的设计原则。
以模块化方式设计我们的 GUI 使得国际化它变得容易得多。
准备工作
我们将继续使用之前菜谱中的 GUI,即GUI.py。在那个菜谱中,我们已经国际化了 GUI 的标题。我们还将增强之前菜谱中的LanguageResources.py模块,通过添加更多的国际化字符串。
如何做到这一点...
为了国际化所有 GUI 小部件中显示的文本,我们必须将所有硬编码的字符串移动到一个单独的 Python 模块中,这就是我们接下来要做的:
-
打开
LanguageResources.py模块。 -
为英文国际化字符串添加以下代码:
class I18N():
'''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.py模块中,将所有硬编码的字符串替换为我们的新I18N类的一个实例,例如,self.i18n.colorsIn:
from Ch08_Code.LanguageResources import I18N
class OOP():
def __init__(self, language='en'):
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])
我们现在可以通过简单地填写相应的单词来变量名来实现对德语的翻译。
- 将以下代码添加到
LanguageResources.py:
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 = "Nicht Markiert"
self.toggle = "Markieren"
# Radiobutton list
self.colors = ["Blau", "Gold", "Rot"]
self.colorsIn = ["in Blau", "in Gold", "in Rot"]
self.labelsFrame = ' Etiketten im Rahmen '
self.chooseNumber = "Waehle eine Nummer:"
self.label2 = "Etikette 2"
self.mgrFiles = ' Dateien Organisieren '
self.browseTo = "Waehle eine Datei... "
self.copyTo = "Kopiere Datei zu : "
- 在我们的 GUI 代码中,我们现在可以用一行 Python 代码更改整个 GUI 显示的语言:
classOOP():
def __init__(self, language='en'):
self.win = tk.Tk() # Create instance
self.i18n = I18N('de') # Pass in language
- 运行前面的代码创建以下国际化的 GUI:

现在我们来看看这个菜谱是如何工作的!
它是如何工作的...
为了国际化我们的图形用户界面(GUI),我们将硬编码的字符串重构到一个单独的模块中,然后通过将字符串传递给我们的I18N类的初始化器,使用相同的类实例属性来国际化我们的 GUI,从而有效地控制 GUI 显示的语言。
注意,之前所有硬编码的英文字符串都已被替换为对新的I18N类实例的调用。一个例子是self.win.title(self.i18n.title)。
这为我们提供了国际化 GUI 的能力。我们只需使用相同的变量名,并通过传递参数来组合它们,以选择我们希望显示的语言。
我们可以在 GUI 中动态更改语言,或者我们可以读取本地 PC 设置,并根据这些设置决定我们的 GUI 文本应该显示哪种语言。如何读取本地设置的示例将在下一个菜谱中介绍,本地化 GUI。
之前,每个小部件的每一行字符串,包括我们 GUI 的标题、标签控件名称等,都是硬编码的,并且与创建 GUI 的代码混合在一起。
在我们的 GUI 软件开发过程的设计阶段考虑如何最好地国际化我们的 GUI 是一个好主意。
在这个菜谱中,我们国际化了 GUI 小部件中显示的所有字符串。我们不会国际化输入到 GUI 中的文本,因为这取决于您 PC 上的本地设置。
本地化 GUI
在我们的 GUI 国际化第一步之后,下一步是本地化它。我们为什么要这样做呢?
嗯,在这里的美国,我们都是牛仔,我们生活在不同的时区。
当我们国际化到美国时,我们的马匹在不同的时区醒来(并且确实期望根据自己的内在马时区时间表被喂食)。
这就是本地化的作用。
准备工作
我们通过本地化扩展了我们之前开发的 GUI。
如何操作...
让我们看看如何执行这个食谱:
-
我们首先使用
pip安装 Python 的pytz时区模块。 -
接下来,打开命令提示符并输入以下命令:
pip install pytz
- 安装成功后,我们得到以下结果:

- 接下来,我们在
GUI.py中添加一个新的Button小部件。我们可以通过运行以下代码列出所有现有时区,该代码将时区添加到我们的ScrolledText小部件中,如下所示:
import pytz
class OOP():
# TZ Button callback
def allTimeZones(self):
for tz in pytz.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小部件,结果如下输出:

- 使用
pip安装tzlocalPython 模块,然后我们可以通过添加localZone方法并将其连接到新的Button命令来显示我们的当前区域设置:
# 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')
我们在LanguageResources.py中国际化了我们两个新按钮的字符串。
英文版本如下:
self.timeZones = "All Time Zones"
self.localZone = "Local Zone"
德语版本如下:
self.timeZones = "Alle Zeitzonen"
self.localZone = "Lokale Zone"
点击我们新的按钮现在告诉我们我们所在的时区(嘿,我们不知道这个,对吧…):

- 我们现在可以通过首先将其转换为协调世界时(UTC)然后应用导入的
pytz模块中的时区函数来将本地时间更改为美国东部标准时间(US EST)。接下来,将以下代码添加到GUI.py中:
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)) # <-- set Label 2
- 点击现在重命名为纽约的按钮,结果在 GUI 左上角的标签 2 中如下输出:

- 注意控制台中的以下输出:

让我们在下一节学习这个食谱。
它是如何工作的…
首先,我们使用pip安装了 Python 的pytz模块。
在这本书中,我们使用的是内置pip模块的 Python 3.7。如果您使用的是较旧版本的 Python,那么您可能首先需要安装pip模块。
第 2 步的截图显示pip命令下载了.whl格式。如果您尚未这样做,您可能还需要安装 Python 的 wheel 模块。
这将 Python 的pytz模块安装到site-packages文件夹中,因此现在我们可以从我们的 Python GUI 代码中导入此模块。
为了本地化日期和时间信息,我们首先需要将本地时间转换为 UTC 时间。然后应用时区信息,并使用从导入的pytz Python 时区模块中的astimezone函数将时间转换为全球任何时区!
我们使用pip安装了 Python 的tzlocal模块,现在我们可以将本地时间转换为不同的时区。我们以美国东部标准时间(US EST)为例。
在第 8 步中,我们将美国西海岸的本地时间转换为 UTC,然后在 GUI 的标签 2(self.lbl2)中显示美国东部时间。
同时,我们正在将洛杉矶和纽约的 UTC 时间及其相应的时间区域转换相对于 UTC 时间打印到控制台,使用的是美国日期格式字符串。
UTC 从不观察夏令时(DST)。在东部夏令时(EDT)期间,UTC 比本地时间快四小时,而在标准时间(EST)期间,UTC 比本地时间快五小时。
准备 GUI 进行国际化
在这个配方中,我们将通过实现以下事实来为我们的 GUI 进行国际化准备:当将英语翻译成外语时,并非所有事情都像预期的那样简单。
我们仍然有一个问题要解决,那就是如何正确显示来自外语的非英语 Unicode 字符。
你可能会期望显示德语的ä、ö和ü Unicode 重音字符会由 Python 3.7 自动处理,但这并不是事实。
准备就绪
我们将继续使用我们在最近章节中开发的 Python GUI。首先,我们将更改 GUI.py 初始化代码中的默认语言为德语。
这个配方可能特定于 Eclipse PyDev IDE,但将其视为一个例子是很好的。
如何做到这一点...
在深入配方之前,我们应该知道,当我们使用重音符号将单词 "Ueber" 更改为正确的德语单词 "Űber" 时,Eclipse PyDev 插件并不太高兴。
让我们现在按顺序查看这个配方的步骤:
-
打开
GUI.py。 -
取消注释行
self.i18n = I18N('de')以使用德语。 -
运行
GUI.py:

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

- 当我们请求 Python 默认编码时,我们得到预期的结果,即 utf-8:

- 使用 Windows 内置字符映射表,我们可以找到重音字符的 Unicode 表示,对于大写 U 带有重音的是 U+00DC:

- 虽然这个解决方案确实很丑陋,但它确实有效。我们不必输入字面字符 Ü,而是可以传递 U+00DC 的 Unicode 来正确地在我们的 GUI 中显示这个字符:

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

- 解决这个问题的方法是更改 PyDev 项目的文本文件编码设置到 UTF-8:

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

让我们现在幕后了解代码。
它是如何工作的…
国际化和处理外文 Unicode 字符通常不像我们希望的那样简单直接。
有时候,我们需要找到解决方案,通过 Python 使用\u前缀直接表示 Unicode 字符可以解决这个问题。
Windows 内置字符映射表显示“U+00DC”,我们将其转换为 Python 中的"\u00DC"。
我们总是可以求助于 Unicode 的直接表示。
在其他时候,我们只需要找到我们的开发环境的设置来调整。我们看到了如何使用 Eclipse IDE 来完成这个任务的例子。
如何以敏捷的方式设计 GUI
现代敏捷软件开发的设计和编码方法源于软件专业人士的经验教训。这种方法适用于 GUI,也适用于任何其他代码。敏捷软件开发的一个主要关键是持续进行的重构过程。
代码重构如何帮助我们进行软件开发工作的一个实际例子是首先通过函数实现一些简单的功能。
随着我们的代码复杂性增加,我们可能希望将我们的函数重构为类的成员方法。这种方法将使我们能够删除全局变量,并且可以在类内部更灵活地放置方法。
虽然我们的代码的功能没有改变,但其结构已经改变了。
在这个过程中,我们编写代码、测试、重构,然后再测试。我们在短周期内这样做,通常从实现某些功能所需的最小代码开始。
测试驱动软件开发是敏捷开发方法中的一种特定风格。
虽然我们的 GUI 运行得很好,但我们的主要GUI.py代码的复杂性一直在增加,开始变得有点难以对代码有一个全面的了解。这意味着我们需要重构我们的代码。
准备工作
我们将重构在前几章中创建的 GUI。我们将使用 GUI 的英文版本。
如何操作…
在上一个菜谱中,当我们国际化 GUI 时,我们已经将 GUI 显示的所有名称都提取出来了。这是重构我们代码的一个很好的开始:
-
让我们将
GUI.py文件重命名为GUI_Refactored.py。 -
按照以下方式分组
import语句:
#======================
# 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
from Ch08_Code.ToolTip import ToolTip
from Ch08_Code.LanguageResources import I18N
from Ch08_Code.Logger import Logger, LogLevel
# Module level GLOBALS
GLOBAL_CONST = 42
我们可以通过将回调方法分离到它们自己的模块中来进一步重构我们的代码。
-
创建一个新的模块,
Callbacks_Refactored.py。 -
在
GUI_Refactored.py中导入Callbacks类。 -
在
Callbacks_Refactored.py中添加self.callBacks = Callbacks(self):
#======================
# 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 Ch08_Code.ToolTip as tt
from Ch08_Code.LanguageResources import I18N
from Ch08_Code.Logger import Logger, LogLevel
from Ch08_Code.Callbacks_Refactored import Callbacks # <-- import the class
# Module level GLOBALS
GLOBAL_CONST = 42
class OOP():
def __init__(self):
# Callback methods now in different module
self.callBacks = Callbacks(self) # <-- pass in 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')
...
让我们现在看看这个菜谱是如何工作的!
如何工作…
我们首先通过分组相关的import语句来提高了代码的可读性。
通过简单地分组相关的导入,我们可以减少代码行数,这提高了我们导入的可读性,使它们看起来不那么令人压倒。
我们接下来将回调方法分离到它们自己的类和模块中,即Callbacks_Refactored.py,以进一步降低代码的复杂性。
在我们新类初始化器中,传入的 GUI 实例被保存为self.oop名称,并在整个新的 Python 类模块中使用。
运行重构后的 GUI 代码仍然像以前一样工作。我们只是提高了其可读性,并降低了代码的复杂性,为后续的开发工作做准备。
我们已经通过将ToolTip类放在自己的模块中,并在之前的菜谱中国际化所有 GUI 字符串,采用了相同的面向对象(OOP)方法。在这个菜谱中,我们通过将我们的实例传递给我们的 GUI 所依赖的回调方法类,进一步进行了重构。这使得我们能够使用所有的 GUI 小部件。
现在我们更好地理解了模块化软件开发方法的价值,我们很可能会在未来的软件设计中从这种方法开始。
重构是改进现有代码结构、可读性和可维护性的过程。我们并没有添加新的功能。
我们需要测试 GUI 代码吗?
在编码阶段以及发布服务包或错误修复时,测试我们的软件是一项重要的活动。
存在不同的测试级别。第一级是开发者测试,通常从编译器或解释器不允许我们运行有错误的代码开始,迫使我们测试代码的各个方法级别的小部分。
这是第一层防御。
防御性编码的第二层是当我们的源代码控制系统告诉我们有一些冲突需要解决,并且不允许我们检查修改后的代码。
当我们在一个由开发者组成的团队中专业工作时,这非常有用且绝对必要。源代码控制系统是我们的朋友,它会指出已经提交到特定分支或树顶部的更改,无论是我们自己还是我们的其他开发者,并告诉我们我们的本地代码版本已经过时,并且在我们将代码提交到仓库之前需要解决一些冲突。
这部分假设你使用源代码控制系统来管理和存储你的代码。例如,Git、Mercurial、SVN 以及几个其他系统。Git 是一个非常流行的源代码控制系统。
第三个级别是 API 级别,我们通过只允许通过发布的接口与我们的代码进行交互,来封装我们代码的潜在未来更改。
另一个测试级别是集成测试,当我们所建造的桥梁的一半遇到其他开发团队创建的另一半,并且两者不在同一高度(比如说,一半最终比另一半高出两米或码…)。
然后,是最终用户测试。虽然我们构建了他们指定的内容,但这并不是他们真正想要的。
所有的前述例子都是我们需要在设计阶段和实现阶段测试代码的有效理由。
准备工作
我们将测试最近配方和章节中创建的 GUI。我们还将展示一些简单的例子,说明可能会出错的原因,以及为什么我们需要持续测试我们的代码和通过 API 调用的代码。
如何做到这一点...
让我们详细检查这个配方。
在 Python GUI 编程中,可能出错的第一件事是遗漏导入所需的模块。
这里有一个简单的例子:
-
打开
GUI.py并取消注释import语句,# import tkinter as tk。 -
运行代码并观察以下输出:

- 在顶部添加
import语句以解决此错误,如下所示:
#======================
# imports
#======================
import tkinter as tk
使用来自 第七章,通过我们的 GUI 将数据存储在我们的 MySQL 数据库中 的示例,假设我们点击获取报价按钮,这成功了,但我们从未点击过修改报价按钮。
- 从 第七章,通过我们的 GUI 将数据存储在我们的 MySQL 数据库中 的
GUI_MySQL.py打开,并点击获取报价按钮:

- 接下来,点击修改报价按钮,这将产生以下结果:

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

- 然后,有人犯了一个错误,我们不再得到之前的结果。
将 (num * num) 改为 (num ** num) 并运行代码:

现在,让我们深入幕后,更好地理解代码。
它是如何工作的...
首先,在 步骤 1 和 步骤 2 中,我们正在尝试创建 tkinter 类的实例,但事情并没有按预期进行。
嗯,我们只是忘记导入模块并将其别名为 tk,我们可以在类创建上方添加一行 Python 代码来修复这个问题,其中 import 语句位于那里。
这是一个例子,我们的开发环境为我们进行测试。我们只需要进行调试和代码修复。
另一个与开发者测试更相关的例子是,当我们编写条件语句时,在常规开发过程中,我们没有测试所有逻辑分支。
为了演示这一点,在 步骤 4 和 步骤 5 中,我们使用来自 第七章,通过我们的 GUI 将数据存储在我们的 MySQL 数据库中 的示例。我们点击获取报价按钮,这成功了,但我们从未点击过修改报价按钮。第一个按钮点击创建了所需的结果,但第二个抛出了异常(因为我们还没有实现这段代码,可能完全忘记了它)。
在下一个示例中,在步骤 6和步骤 7中,我们不是进行乘法运算,而是通过传入数字的幂进行指数级提升,结果不再是之前的样子。
在软件测试中,这种类型的错误被称为回归。
无论我们的代码中发生什么问题,我们都需要调试它。进行此操作的第一步是设置断点,然后逐行或逐方法地逐步通过我们的代码。
逐步进入和退出我们的代码是我们日常活动的一部分,直到代码运行顺畅。
在这个菜谱中,我们通过展示代码可能出错并引入软件缺陷(也称为错误)的几个示例,强调了在软件开发生命周期的几个阶段进行软件测试的重要性。
设置调试监视器
在现代 IDE 中,例如 Eclipse 中的 PyDev 插件或其他 IDE,如 NetBeans,我们可以在代码执行期间设置调试监视器来监控我们的 GUI 状态。
这与微软的 Visual Studio 和更近版本的 Visual Studio .NET 的 IDE 非常相似。
设置调试监视器是一种非常方便的方式来帮助我们开发工作。
准备工作
在这个菜谱中,我们将重用我们在早期菜谱中开发的 Python GUI。我们将逐步通过我们之前开发的代码,并设置调试监视器。
如何做…
尽管这个菜谱适用于基于 Java 的 Eclipse IDE 中的 PyDev 插件,但其原则也适用于许多现代 IDE。
让我们现在看看我们如何可以按顺序进行这个菜谱:
- 打开
GUI.py并在带有mainloop的行上放置一个断点:

- 按以下步骤开始调试会话:

让我们在名为 getDateTime 的纽约按钮回调方法上放置一个断点。
-
打开
Callbacks_Refactored.py并在getDateTime方法上放置一个断点。 -
逐步通过代码:

让我们现在幕后了解代码,以便更好地理解它。
它是如何工作的…
我们可能希望放置断点的第一个位置是在我们通过调用 tkinter 主事件循环使我们的 GUI 可见的地方。我们在步骤 1中这样做。
左侧的绿色气球符号是 PyDev/Eclipse 中的断点。当我们以调试模式执行代码时,一旦执行达到断点,代码的执行将被中断。此时,我们可以看到当前作用域内所有变量的值。我们还可以在调试器窗口中输入表达式,这些表达式将被执行,并显示结果。如果结果是我们要的,我们可能会决定使用我们刚刚学到的东西来更改我们的代码。
我们通常通过点击 IDE 工具栏中的一个图标或使用键盘快捷键(例如按 F5 进入代码,F6 跳过,F7 从当前方法中退出)来逐步通过代码。
将断点放置在之前的位置,然后逐步进入这段代码,结果是我们最终进入了一些我们目前并不想调试的低级tkinter代码。我们可以通过点击步骤退出工具栏图标(位于项目菜单下方右侧的第三个黄色箭头)或按F7(假设我们在使用 Eclipse 中的 PyDev)来退出这种低级tkinter代码。
一个更好的主意是将断点放置得更靠近我们自己的代码,以便观察我们自己的 Python 变量的值。在现代 GUI 的事件驱动世界中,我们必须将断点放置在事件期间被调用的代码中,例如,按钮点击。我们在步骤 3和步骤 4中这样做。
目前,我们的一项主要功能位于一个按钮点击事件中。当我们点击标记为纽约的按钮时,我们创建了一个事件,然后导致我们的 GUI 中发生某些事情。
如果你对学习如何安装带有 PyDev 插件的 Eclipse 以用于 Python 感兴趣,有一个很好的教程,它将帮助你安装所有必要的免费软件,然后通过创建一个简单、有效的 Python 程序来介绍 Eclipse 中的 PyDev:www.vogella.com/tutorials/Python/article.html。
我们在 21 世纪使用现代 IDE,这些 IDE 是免费提供的,帮助我们创建坚实的代码。
这个菜谱展示了如何设置调试监视器,这是每个开发者技能集中的基本工具。即使不是在寻找错误,逐步执行我们自己的代码也能确保我们理解我们的代码,并且这可以通过重构来改进我们的代码。
调试监视器帮助我们创建坚实的代码,这并不是浪费时间。
配置不同的调试输出级别
在这个菜谱中,我们将配置不同的调试级别,我们可以在运行时选择和更改它们。这允许我们在调试代码时控制我们想要深入挖掘代码的程度。
我们将创建两个新的 Python 类,并将它们都放在同一个模块中。
我们将使用四个不同的日志级别,并将我们的调试输出写入我们将创建的日志文件。如果日志文件夹不存在,我们将自动创建它。
日志文件的名称是执行脚本的名称,即我们重构后的GUI_Refactored.py。我们也可以通过传递Logger类初始化器的完整路径来为我们的日志文件选择其他名称。
准备工作
我们将继续使用之前菜谱中重构的GUI_Refactored.py代码。
如何操作...
让我们看看我们将如何进行这个菜谱:
-
首先,我们创建一个新的 Python 模块,并将两个新的类放入其中。第一个类非常简单,定义了日志级别。这基本上是一个枚举。
-
创建一个新的模块,并将其命名为
Logger.py。 -
添加如下代码:
class LogLevel:
'''Define logging levels.'''
OFF = 0
MINIMUM = 1
NORMAL = 2
DEBUG = 3
- 将第二个类添加到同一个模块中,
Logger.py:
import os, time
from datetime import datetime
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方法:
#------------------------------------------------------
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()
- 打开
GUI_Refactored.py并添加以下代码:
from os import path
from Ch08_Code.Logger import Logger
class OOP():
def __init__(self):
# create Logger instance
fullPath = path.realpath(__file__)
self.log = Logger(fullPath)
print(self.log)
- 运行代码并观察输出:

上一张截图显示我们创建了一个新的 Logger 类的实例,下一张截图显示日志文件夹以及日志都已创建:

- 最后,打开日志文件:

让我们现在幕后了解代码,以便更好地理解。
它是如何工作的...
我们首先创建了一个新的模块,并使用一个简单的类作为枚举。
第二个类通过使用传递的文件名完整路径创建日志文件,并将其放置在日志文件夹中。在第一次运行时,日志文件夹可能不存在,因此代码会自动创建文件夹。
为了将内容写入我们的日志文件,我们使用 writeToLog 方法。在方法内部,我们首先检查消息的日志级别是否高于我们为期望的日志输出设置的极限。如果消息的级别较低,我们将其丢弃并立即从方法返回。
如果消息具有我们想要显示的日志级别,我们接下来检查它是否以换行符开头,如果是,我们通过使用 Python 的切片操作符(msg = msg[1:])从索引 1 开始切片来丢弃换行符。
然后我们向日志文件写入一行,包括当前日期时间戳、两个制表符、我们的消息,并以换行符结束。
我们现在可以导入我们的新 Python 模块,并在我们的 GUI 代码的 __init__ 部分创建 Logger 类的一个实例。
我们通过 path.realpath(__file__) 获取我们正在运行的 GUI 脚本的完整路径,并将其传递给 Logger 类的初始化器。如果日志文件夹不存在,我们的 Python 代码将自动创建它。
我们接下来验证日志和文件夹是否已创建。
在这个菜谱中,我们创建了自己的日志类。虽然 Python 附带了日志模块,但创建自己的日志模块非常简单,这使我们能够完全控制我们的日志格式。当我们将自己的日志输出与 MS Excel 或 Matplotlib 结合使用时,这非常有用,这在上一章的菜谱中已经探讨过。
在下一个菜谱中,我们将使用 Python 的内置 __main__ 功能来使用我们刚刚创建的四个不同的日志级别。
使用 Python 的 main 部分创建自我测试代码
Python 具有一个非常棒的功能,它使每个模块都能够自我测试。利用这个功能是确保我们的代码更改不会破坏现有代码的绝佳方式,此外,__main__ 自我测试部分还可以作为每个模块如何工作的文档。
几个月或几年后,我们有时会忘记我们的代码在做什么,所以代码本身有解释确实非常有好处。
总是在可能的情况下给每个 Python 模块添加一个自我测试部分是一个好主意。有时可能做不到,但在大多数模块中,这样做是可能的。
准备工作
我们将扩展之前的菜谱,因此为了理解这个菜谱中的代码在做什么,我们首先需要阅读和理解之前菜谱中的代码。
如何做到这一点...
让我们详细看看这个菜谱的步骤:
-
首先,我们将通过将这个自我测试部分添加到我们的
LanguageResources.py模块来探索 Python__main__自我测试部分的力量。 -
接下来,将以下代码添加到
LanguageResources.py:
if __name__ == '__main__':
language = 'en'
inst = I18N(language)
print(inst.title)
language = 'de'
inst = I18N(language)
print(inst.title)
- 运行代码并观察输出:

- 在
GUI_Refactored.py模块中添加一个__main__自我测试部分并运行代码以查看以下输出:

- 接下来,在
GUI_Refactored.py模块中,添加oop.log.writeToLog('Test message'):
if __name__ == '__main__':
#======================
# Start GUI
#======================
oop = OOP()
print(oop.log)
oop.log.writeToLog('Test message')
oop.win.mainloop()
这将被写入我们的日志文件,如下面的日志截图所示:

- 在
GUI_Refactored.py中,从我们的Logger模块导入这两个新类:
from Ch08_Code.Logger import Logger, LogLevel
- 接下来,创建这些类的本地实例:
# create Logger instance
fullPath = path.realpath(__file__)
self.log = Logger(fullPath)
# create Log Level instance
self.level = LogLevel()
- 通过
self.oop.level使用不同的日志级别:
# 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))
- 运行代码并打开日志:

注意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()
- 打开日志文件:

现在,让我们深入幕后更好地理解代码。
它是如何工作的...
我们首先在LanguageResources.py中添加一个__main__自我测试部分。
每当我们运行一个包含位于模块底部的这个自我测试部分的模块时,当模块独立执行时,这段代码将会运行。
当模块被其他模块导入和使用时,__main__自我测试部分的代码将不会执行。
我们首先传递英语作为 GUI 中要显示的语言,然后传递德语作为 GUI 将显示的语言。
我们打印出 GUI 的标题,以表明我们的 Python 模块按预期工作。
下一步是使用我们在之前的菜谱中创建的日志功能。
我们在GUI_Refactored.py中添加一个__main__自我测试部分,然后验证我们是否创建了一个Logger类的实例。
接下来,我们通过使用显示的命令将信息写入我们的日志文件。我们已经设计好我们的日志级别默认为记录每条消息,即DEBUG级别,因此我们不需要做任何改变。我们只需将需要记录的消息传递给writeToLog方法。
现在,我们可以通过添加日志级别到我们的日志语句并设置我们希望输出的级别来控制日志。我们在Callbacks_Refactored.py模块的 New York 按钮回调方法中添加了这个功能,即getDateTime方法。
我们将之前的打印语句更改为使用不同调试级别的日志语句。
由于我们将 GUI 类的实例传递给Callbacks_Refactored.py初始化器,我们可以使用我们创建的LogLevel类的日志级别约束。
当我们现在点击纽约按钮时,根据选择的日志级别,我们将不同的输出写入日志文件。默认的日志级别是DEBUG,这意味着所有内容都会写入日志。
当我们更改日志级别时,我们控制写入日志的内容。我们通过调用Logger类的setLoggingLevel方法来完成此操作。
将级别设置为MINIMUM会导致写入日志文件的输出减少。
现在,我们的日志文件不再显示测试消息,只显示符合设置日志级别的消息。
在这个菜谱中,我们很好地使用了 Python 的内置__main__自我测试部分。我们引入了自己的日志文件,同时学习了如何创建不同的日志级别。通过这样做,我们完全控制了写入日志文件的内容。
使用单元测试创建健壮的 GUI
Python 自带了一个单元测试框架,在这个菜谱中,我们将开始使用这个框架来测试我们的 Python GUI 代码。
在我们开始编写单元测试之前,我们想要设计我们的测试策略。我们可以很容易地将单元测试与它们正在测试的代码混合,但更好的策略是将应用程序代码与单元测试代码分开。
PyUnit是根据所有其他 xUnit 测试框架的原则设计的。
准备中
我们将测试本章中较早创建的国际化 GUI。
如何做到…
为了使用 Python 的内置单元测试框架,我们导入 Python 的unittest模块。现在让我们看看下一步:
-
创建一个新的模块,并将其命名为
UnitTestsMinimum.py。 -
添加以下代码:
import unittest
class GuiUnitTests(unittest.TestCase):
pass
if __name__ == '__main__':
unittest.main()
- 运行
UnitTestsMinimum.py并观察以下输出:

- 创建一个新的模块,命名为
UnitTests_One.py,然后添加以下代码:
import unittest
from Ch08_Code.LanguageResources import I18N
class GuiUnitTests(unittest.TestCase):
def test_TitleIsEnglish(self):
i18n = I18N('en')
self.assertEqual(i18n.title,
"Python Graphical User Interface")
- 运行
UnitTests_One.py:

- 将模块保存为
UnitTestsFail.py,然后复制、粘贴并修改代码:
import unittest
from Ch08_Code.LanguageResources 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')
- 运行
UnitTestsFail.py:

- 通过传递
'de'到I18N来纠正这个错误:
def test_TitleIsGerman(self):
# i18n = I18N('en') # <= Bug in Unit Test
i18n = I18N('de')
self.assertEqual(i18n.title, 'Python Grafische Benutzeroberfl'
+ "u00E4" + 'che')
- 重新运行
UnitTestsFail.py,修正错误并观察输出:

让我们现在幕后了解代码,以便更好地理解。
它是如何工作的…
我们首先导入unittest模块,然后创建我们自己的类,并在该类中继承并扩展unittest.TestCase类。我们使用最少的代码来开始。目前的代码还没有做很多事情,但是当我们运行它时,我们没有得到任何错误,这是一个好兆头。
我们实际上确实在控制台上写入了输出,表明我们成功运行了零个测试。
那个输出有点误导,因为我们到目前为止只是创建了一个不包含实际测试方法的类。
我们添加了测试方法,通过遵循所有测试方法以单词test开头的默认命名来执行实际的单元测试。这是一个可以更改的选项,但遵循这个命名约定要容易和清晰得多。
我们随后添加了一个测试方法来测试我们的 GUI 标题。这将验证通过传递预期的参数,我们是否得到了预期的结果。
我们从LanguageResources.py模块导入我们的I18N类,将英语作为在 GUI 中显示的语言。由于这是我们第一个单元测试,我们将打印出标题结果,以确保我们知道我们得到了什么。接下来,我们使用unittest assertEqual方法来验证我们的标题是否正确。
运行这段代码给我们一个 OK,这意味着单元测试通过了。
单元测试运行并成功,这由一个点和单词 OK 表示。如果它失败了或者出现了错误,我们不会得到点,而是得到输出中的F或E。
然后,我们通过验证 GUI 德语版本的标题来进行相同的自动化单元测试检查。我们在两种语言中测试了我们的国际化 GUI 标题。
我们运行了两个单元测试,但得到的不是 OK,而是失败。发生了什么?
我们的断言对于 GUI 的德语版本失败了。
在调试我们的代码时,我们发现,在我们的单元测试代码的复制、粘贴和修改方法中,我们忘记传递德语作为语言。
在纠正了失败之后,我们重新运行了我们的单元测试,并且所有测试都通过了预期的结果。
单元测试代码也是代码,也可能有错误。
虽然编写单元测试的目的是真正测试我们的应用程序代码,但我们必须确保我们的测试被正确编写。测试驱动开发(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 方法。
这种测试用例功能为我们提供了一个非常受控的环境,我们可以在这个环境中运行我们的单元测试。
如何操作...
现在,让我们看看如何执行这个菜谱:
-
首先,让我们设置我们的单元测试环境。我们将创建一个新的测试类,专注于上述代码的正确性。
-
创建一个新的模块,
UnitTestsEnglish.py。 -
添加以下代码:
import unittest
from Ch08_Code.LanguageResources import I18N
from Ch08_Code.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()
- 运行代码并观察输出:

-
打开
UnitTestsEnglish.py并将其保存为UnitTests.py。 -
将以下代码添加到模块中:
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")
...
- 运行
UnitTests.py:

- 从命令提示符运行代码并观察以下输出:

- 使用 Eclipse,我们还可以选择将我们的单元测试作为 Python 单元测试脚本运行,而不是简单的 Python 脚本,这会给我们一些彩色的输出:

让我们在下一节中了解这些步骤。
它是如何工作的...
首先,我们创建了三个测试方法。
unittest.main() 运行任何以 test 前缀开始的任何方法,无论我们在给定的 Python 模块中创建了多少个类。
单元测试代码显示我们可以创建几个单元测试类,并且它们都可以通过调用 unittest.main() 在同一个模块中运行。
它还显示,在单元测试报告的输出中,setup 方法不被算作一个测试(测试数量为三个),同时,它也完成了预期的任务,因为我们现在可以从单元测试方法中访问我们的 self.gui 类实例变量。
我们对测试所有标签的正确性感兴趣,尤其是在我们更改代码时捕捉到错误。
如果我们将应用程序代码中的字符串复制粘贴到测试代码中,它将通过点击单元测试框架按钮捕捉到任何意外的更改。
我们还希望测试调用任何语言中的任何单选按钮小部件都会导致 LabelFrame 小部件文本更新的事实。为了自动测试这一点,我们必须做两件事。
首先,我们必须检索 LabelFrame 小部件的值,并将其分配给一个我们命名为 labelFrameText 的变量。我们必须使用以下语法,因为此小部件的属性是通过字典数据类型传递和检索的:
self.gui.widgetFrame['text']
我们现在可以验证默认文本,然后点击其中一个单选按钮小部件后验证国际化版本。
在验证默认 labelFrameText 后,我们程序化地将单选按钮设置为 索引 1,然后调用单选按钮的回调方法:
self.gui.radVar.set(1)
self.gui.callBacks.radCall()
这基本上与在 GUI 中点击单选按钮相同,但我们通过代码在单元测试中执行此按钮点击事件。
然后,我们验证我们的 LabelFrame 小部件中的文本是否按预期更改。
如果你遇到 ModuleNotFoundError,只需将你的 Python 代码所在的目录添加到 Windows 的 PYTHONPATH 环境变量中,如下面的截图所示:
如下所示,遇到错误:

如果遇到错误,解决方案如下所示:

例如,C:\Eclipse_Oxygen_workspace_Packt_3rd_GUI_BOOK\3rd Edition Python GUI Programming Cookbook:

这将识别 Ch08_Code 文件夹为 Python 包,代码将运行。
当我们从 Eclipse 的图形运行器中运行单元测试时,结果栏是绿色的,这意味着我们所有的单元测试都通过了。
我们通过测试标签、程序化调用 Radiobutton 并在单元测试中验证 LabelFrame 小部件的相应文本属性是否按预期更改来扩展我们的单元测试代码。我们测试了两种不同的语言。然后,我们继续使用内置的 Eclipse/PyDev 图形单元测试运行器。
第九章:使用 wxPython 库扩展我们的 GUI
在本章中,我们将介绍另一个不与 Python 一起分发的 Python GUI 工具包。它被称为 wxPython。这个库有两个版本。原始版本被称为 Classic,而最新版本被称为其开发项目的代码名,即 Phoenix。
较旧的 Classic 版本不与 Python 3.x 兼容,我们将不会进一步探讨这个版本,而是专注于 Phoenix 软件版本。
在本书中,我们仅使用 Python 3.7 及更高版本进行编程,因为新的 Phoenix 项目也旨在支持 Python 3.7 及更高版本,因此这是我们本章将使用的 wxPython 版本。
首先,我们将安装框架。然后,我们将创建一个简单的 wxPython GUI,之后,我们将尝试将我们在本书中开发的基于 tkinter 的 GUI 与新的 wxPython 库连接起来。
wxPython 是 wxWidgets 的 Python 绑定。wxPython 中的 w 代表 Windows 操作系统,而 x 代表基于 Unix 的操作系统,例如 Linux 和苹果的 macOS。
虽然 tkinter 与 Python 一起分发,但拥有使用其他与 Python 兼容的 GUI 框架的经验是有价值的。这将提高你的 Python GUI 编程技能,并且你可以选择在你的项目中使用哪个框架。
这是本章 Python 模块的概述:

在本章中,我们将通过使用 wxPython 库来增强我们的 Python GUI。我们将涵盖以下配方:
-
安装 wxPython 库
-
在 wxPython 中创建我们的 GUI
-
使用 wxPython 快速添加控件
-
尝试将主 wxPython 应用嵌入到主 tkinter 应用中
-
尝试将我们的 tkinter GUI 代码嵌入到 wxPython 中
-
使用 Python 控制两个不同的 GUI 框架
-
两个连接的 GUI 之间的通信
安装 wxPython 库
wxPython 库不是与 Python 一起分发的,因此为了使用它,我们首先必须安装它。这个配方将向我们展示在哪里以及如何找到正确的版本来安装,以便与已安装的 Python 版本和正在运行的操作系统相匹配。
wxPython 第三方库已经存在了 18 年多,这表明它是一个健壮的库。
准备工作
为了使用 wxPython 与 Python 3.7 及更高版本,我们必须安装 wxPython Phoenix 版本。这是下载页面的链接:wxpython.org/pages/downloads/。我们将使用此链接下载和安装 wxPython GUI 框架。
这是到 PyPI 的链接,其中包含有关如何使用 wxPython 的良好信息:pypi.org/project/wxPython/。
如何做到这一点...
几年前,找到适合 Python 3 的正确 wxPython 版本有点棘手,但现在我们可以简单地使用 pip。让我们详细看看:
-
打开命令提示符或 PowerShell 窗口。
-
输入
pip install wxPython。 -
结果应该看起来像这样:

- 确认在你的 Python
site-packages文件夹中有一个名为wx的新文件夹:

-
创建一个新的模块,并将其命名为
Hello_wxPython.py。 -
添加以下代码:
import wx
app = wx.App()
frame = wx.Frame(None, -1, "Hello World")
frame.Show()
app.MainLoop()
- 运行前面的 Python 3.7 脚本将使用 wxPython/Phoenix 创建以下 GUI:

现在,让我们深入了解代码,更好地理解它。
它是如何工作的…
首先,我们使用 pip 安装 wxPython 框架。然后,我们验证我们是否有新的 wx 文件夹。
wx 是 wxPython Phoenix 库安装到的文件夹的名称。我们将使用 wx 这个名称将这个模块导入到我们的 Python 代码中。
我们可以通过执行来自官方 wxPython/Phoenix 网站的简单演示脚本来验证我们的安装是否成功。官方网站的链接是 wxpython.org/pages/overview/#hello-world。
在这个菜谱中,我们成功安装了与 Python 3.7 兼容的正确版本的 wxPython 工具包。我们找到了这个 GUI 工具包的 Phoenix 项目,这是当前和活跃的开发线。Phoenix 将最终取代经典的 wxPython 工具包,并且专门设计来与 Python 3.7 一起很好地工作。
在成功安装 wxPython/Phoenix 工具包后,我们只用五行代码就创建了 GUI。
我们之前通过使用 tkinter 实现了相同的结果。
在 wxPython 中创建我们的 GUI
在这个菜谱中,我们将开始使用 wxPython GUI 工具包创建我们的 Python GUI。我们首先将重新创建我们之前使用 tkinter 创建的一些小部件,tkinter 是 Python 的一部分。然后,我们将探索 wxPython GUI 工具包提供的一些小部件,这些小部件使用 tkinter 创建起来并不那么容易。
准备工作
之前的菜谱向你展示了如何安装与你的 Python 版本和运行操作系统相匹配的正确版本的 wxPython。探索 wxPython GUI 工具包的一个好地方是访问以下网址:wxpython.org/Phoenix/docs/html/gallery.html。
这个网页显示了多个 wxPython 小部件,通过点击任何一个,我们都会被带到它们的文档,这对于想要快速了解 wxPython 控件的人来说是一个非常有用的功能:

现在,让我们使用 wxPython 库。
如何做到这一点…
我们可以非常快速地创建一个带有标题、菜单栏和状态栏的工作窗口。这个状态栏在鼠标悬停在小部件上时显示菜单项的文本。接下来,执行以下步骤:
-
创建一个新的 Python 模块,并将其命名为
wxPython_frame_GUI.py。 -
添加以下代码:
# 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 Menu 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()
- 这将创建以下 GUI,它使用 wxPython 库用 Python 编写:

-
创建一个新的模块并将其命名为
wxPython_panel_GUI.py。 -
添加以下代码:
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)
# callback event handler
def printButton(self, event):
self.textBox.AppendText(
"The Print Button has been clicked!")
app = wx.App() # Create instance of wxPython application
# Create frame
frame = wx.Frame(None, title="Python GUI using wxPython", size=(300,180))
GUI(frame) # Pass frame into GUI
frame.Show() # Display the frame
app.MainLoop() # Run the main GUI event loop
- 运行前面的代码并点击我们的 wxPython 按钮小部件,将产生以下 GUI 输出:

现在,让我们幕后了解代码,以便更好地理解它。
它是如何工作的…
在 wxPython_frame_GUI.py 代码中,我们继承自 wx.Frame。在下一代码中,我们继承自 wx.Panel 并将 wx.Frame 传递到我们类的 __init__() 方法中。
在 wxPython 中,顶级 GUI 窗口被称为框架。没有框架就不能有 wxPython GUI,框架必须作为 wxPython 应用程序的一部分来创建。我们在代码的底部创建了应用程序和框架。
为了将小部件添加到我们的 GUI 中,我们必须将它们附加到一个面板上。面板的父级是框架(我们的顶级窗口),而我们放入面板中的小部件的父级是面板。在 wxPython_panel_GUI.py 代码中,parent 是我们传递给 GUI 初始化器的 wx.Frame。我们还向面板小部件添加了一个按钮小部件,当点击时,会在 textbox 中打印一些文本。
在这个菜谱中,我们使用成熟的 wxPython GUI 工具包创建了我们的 GUI。仅用几行 Python 代码,我们就能够创建一个带有最小化、最大化和退出按钮的完整功能的 GUI。我们添加了一个菜单栏、一个多行文本控件和一个按钮。我们还创建了一个状态栏,当选择菜单项时,会在状态栏中显示文本。我们将所有这些小部件放入一个面板容器小部件中。我们将按钮连接到打印到文本控件。当悬停在菜单项上时,状态栏中会显示一些文本。
使用 wxPython 快速添加控件
在这个菜谱中,我们将重新创建本书早期使用 tkinter 创建的 GUI,但这次我们将使用 wxPython 库。我们将看到使用 wxPython GUI 工具包创建自己的 Python GUI 是多么容易和快捷。
我们不会重新创建之前章节中创建的全部功能。例如,我们不会国际化我们的 wxPython GUI,也不会将其连接到 MySQL 数据库。我们将重新创建 GUI 的视觉方面并添加一些功能。
比较不同的库为我们提供了选择用于我们自己的 Python GUI 开发的工具包的机会,我们可以在自己的 Python 代码中组合这些工具包中的几个。
准备工作
确保您已安装 wxPython 模块,以便遵循此菜谱。
如何做到这一点…
让我们看看如何执行这个菜谱:
-
首先,我们创建我们的 Python
OOP类,就像之前使用tkinter一样,但这次我们继承并扩展了wx.Frame类。我们将类命名为MainFrame。 -
创建一个新的 Python 模块,并将其命名为
GUI_wxPython.py。 -
添加以下代码:
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()
- 添加以下代码以创建一个
notebook小部件:
#----------------------------------------------------------
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)
- 添加一个新的类并命名为
Widgets:
class Widgets(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.createWidgetsFrame()
self.addWidgets()
self.layoutWidgets()
- 添加以下方法:
#------------------------------------------------------
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()
- 添加
addStaticBoxWithLabels方法:
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)
- 添加以下方法并在
__init__中调用它们:
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 )
- 在模块的底部,添加调用
MainLoop的代码:
#======================
# Start GUI
#======================
app = wx.App()
MainFrame(None, , size=(350,450))
app.MainLoop()
- 运行
GUI_wxPython.py。我们的 wxPython GUI 的最终结果如下:

现在,让我们深入了解代码,以更好地理解它。
它是如何工作的……
首先,我们创建一个新的 Python 模块。为了清晰起见,我们不再将我们的类命名为 OOP,而是将其重命名为 MainFrame。
在 wxPython 中,主 GUI 窗口被称为框架。
我们还创建了一个回调方法,当点击 Exit 菜单项时关闭 GUI,并声明一个浅灰色元组作为我们的 GUI 的背景颜色。然后,我们通过创建 wxPython 的 Notebook 类的实例并将其分配给我们的自定义类 Widgets 作为其父类,向我们的 GUI 添加一个选项卡控件。Notebook 类实例变量以 wx.Panel 作为其父类。
在 wxPython 中,选项卡控件被命名为 Notebook,就像在 tkinter 中一样。
每个 Notebook 小部件都需要一个父类,为了在 wxPython 的 Notebook 中布局小部件,我们使用不同类型的 sizers。
wxPython 的 sizers 是布局管理器,类似于 tkinter 的网格布局管理器。
接下来,我们向 Notebook 页面添加控件,我们通过创建一个继承自 wx.Panel 的单独类 Widgets 来完成这项工作。我们通过将代码分解成小方法来模块化我们的 GUI 代码,遵循 Python OOP 编程的最佳实践,这使我们的代码易于管理和理解。
当使用 wxPython 的 StaticBox 小部件时,为了成功布局它们,我们使用 StaticBoxSizer 和常规 BoxSizer 的组合。wxPython 的 StaticBox 与 tkinter 的 LabelFrame 小部件非常相似。
在 tkinter 中将 StaticBox 嵌套在另一个 StaticBox 内是直接的,但使用 wxPython 则稍微不那么直观。使它工作的一种方法在 addStaticBoxWithLabels 方法中显示。
在此之后,我们创建一个水平的 BoxSizer。接下来,我们创建一个垂直的 StaticBoxSizer,因为我们想在这个框架中将两个标签垂直排列。为了将另一个小部件安排在嵌入的 StaticBox 的右侧,我们必须将嵌入的 StaticBox 及其子控件和下一个小部件都分配给水平 BoxSizer。接下来,我们需要将这个 BoxSizer 分配给主 StaticBox。
这听起来是不是很困惑?
只需实验这些 sizers,以了解如何使用它们。从本食谱的代码开始,注释掉一些代码或修改一些 x 和 y 坐标以查看效果。阅读官方 wxPython 文档以了解更多信息也是好的。
重要的是要知道在代码中添加不同的 sizers 的位置,以便实现我们想要的布局。
为了在第一个StaticBox下方创建第二个StaticBox,我们创建了单独的StaticBoxSizer并将它们分配给同一个面板。我们在几个类中设计和布局我们的 wxPython GUI。一旦完成,在 Python 模块的底部部分,我们创建 wxPython 应用程序的一个实例。接下来,我们实例化我们的 wxPython GUI 代码。
之后,我们调用主 GUI 事件循环,它执行此应用程序进程中运行的全部 Python 代码。这显示了我们的 wxPython GUI。
这个配方使用了面向对象编程(OOP)来展示如何使用 wxPython GUI 工具包。
尝试在主 tkinter 应用程序中嵌入主 wxPython 应用程序
现在我们已经使用 Python 内置的tkinter库以及 wxWidgets 库的 wxPython 包装器创建了相同的 GUI,我们真的希望结合使用这些技术创建的 GUI。
wxPython 和tkinter库都有自己的优点。在在线论坛,如stackoverflow.com/,我们经常看到诸如哪个更好、我应该使用哪个 GUI 工具包等问题。这表明我们必须做出非此即彼的决定。实际上,我们不必做出这样的决定。
在这样做的主要挑战之一是,每个 GUI 工具包都必须有自己的事件循环。在这个配方中,我们将尝试通过从我们的tkinter GUI 中调用它来嵌入一个简单的 wxPython GUI。
准备工作
我们将重用我们在第一章“创建 GUI 表单和添加小部件”中“组合框小部件”配方中构建的tkinter GUI。
如何操作...
我们将从简单的tkinter GUI 开始:
-
创建一个新的模块,并将其命名为
Embed_wxPython.py。 -
添加以下代码:
#===========================================================
import tkinter as tk
from tkinter import ttk, 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()
action = ttk.Button(win, text="Call wxPython GUI")
action.grid(column=2, row=1)
#======================
# Start GUI
#======================
win.mainloop()
- 运行代码并观察以下输出:

- 创建一个新的函数
wxPythonApp,并将其放置在主循环之上:
#===========================================================
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()
#=============== Bottom of module =================
action = ttk.Button(win, text="Call wxPython GUI", command=wxPythonApp)
action.grid(column=2, row=1)
#======================
# Start GUI
#======================
win.mainloop()
- 运行前面的代码会在点击
tkinter按钮控件后从我们的tkinterGUI 启动 wxPython GUI:

现在,让我们深入了解代码以更好地理解它。
它是如何工作的...
首先,我们创建一个简单的tkinter GUI 并单独运行它。然后,我们尝试调用一个简单的 wxPython GUI,这是我们在这个章节之前的一个配方中创建的。
我们创建一个新的函数,wxPythonApp,其中包含 wxPython 代码,并将其放置在tkinter按钮之上。之后,我们将按钮的command属性设置为这个函数:
action = ttk.Button(win, text="Call wxPython GUI", command=wxPythonApp) # <====
重要的是,我们将整个 wxPython 代码放入一个名为def wxPythonApp()的单独函数中。在按钮点击事件的回调函数中,我们简单地调用这段代码。
需要注意的一件事是,我们必须在继续使用tkinter GUI 之前关闭 wxPython GUI。
尝试将我们的 tkinter GUI 代码嵌入到 wxPython 中
在这个配方中,我们将与之前的配方相反,尝试在 wxPython GUI 中调用我们的tkinter GUI 代码。
准备工作
我们将重用本章之前菜谱中创建的一些 wxPython GUI 代码。
如何做到这一点...
我们将从简单的 wxPython GUI 开始:
-
创建一个新的模块并将其命名为
Embed_tkinter.py。 -
添加以下代码:
#=============================================================
import wx
app = wx.App()
frame = wx.Frame(None, -1, "wxPython GUI", size=(270,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=(250,50), style=wx.TE_MULTILINE)
def tkinterEmbed(event):
tkinterApp() # <==== we create this function next
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()
- 运行代码并观察以下输出:

- 将以下代码添加到模块的顶部:
#=============================================================
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()
#=============== Bottom of module =================
import wx
# ...
-
运行代码并点击调用 tkinter GUI 按钮。
-
在
tkinterGUI 中输入一些文本并点击打印按钮。 -
在 wxPython 的
TextCtrl小部件中输入:

-
运行代码并多次点击调用 tkinter GUI 按钮。
-
在
tkinterGUI 中输入并点击打印按钮:

现在,让我们深入了解幕后,更好地理解代码。
它是如何工作的...
在这个菜谱中,我们与之前的菜谱相反,首先使用 wxPython 创建了一个 GUI,然后从其中创建了几种使用 tkinter 构建的 GUI 实例。
运行 Embed_tkinter.py 代码在点击 wxPython 按钮小部件后从我们的 wxPython GUI 启动一个 tkinter GUI。然后我们可以输入文本到 tkinter 文本框中,并通过点击其按钮,按钮文本会更新为名称。
在启动 tkinter 事件循环后,wxPython GUI 仍然可以响应,因为我们可以在 tkinter GUI 运行时仍然向 TextCtrl 小部件中输入。
在之前的菜谱中,我们无法使用我们的 tkinter GUI,直到我们关闭了 wxPython GUI。了解这种差异可以帮助我们做出设计决策,如果我们想结合这两种 Python GUI 技术。
我们还可以通过多次点击 wxPython GUI 按钮来创建几个 tkinter GUI 实例。然而,我们无法在 tkinter GUI 运行时关闭 wxPython GUI。我们必须先关闭它们。
当一个或多个 tkinter GUI 运行时,wxPython GUI 仍然可以响应。然而,点击 tkinter 按钮只在其第一次更新按钮文本。
使用 Python 控制两个不同的 GUI 框架
在这个菜谱中,我们将探讨从 Python 控制 tkinter 和 wxPython GUI 框架的方法。我们已经在第六章 [802f3638-4c00-4d83-8f04-3acdb39b53ec.xhtml],线程和网络 中使用了 Python 线程模块来保持我们的 GUI 响应,所以在这里我们将尝试使用相同的方法。
我们将看到事情并不总是以直观的方式工作。然而,我们将改进我们的 tkinter GUI,使其在从内部调用 wxPython GUI 实例时不再无响应。
准备工作
这个菜谱将扩展本章之前的菜谱,尝试在主 tkinter 应用程序中嵌入主 wxPython 应用程序,其中我们成功地将主 wxPython GUI 嵌入到我们的 tkinter GUI 中。
如何做到这一点...
当我们从 tkinter GUI 创建 wxPython GUI 实例时,我们不能再使用 tkinter GUI 控件,直到我们关闭一个 wxPython GUI 实例。现在让我们改进这一点:
-
创建一个新的模块,并将其命名为
Control_Frameworks_NOT_working.py。 -
编写以下代码:
#==================================================================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from threading import Thread
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()
#==================================================================
# NOT working - CRASHES Python -----------------------------------
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) # <==== calling wxPythonApp in thread
runT.setDaemon(True)
runT.start()
print(runT)
print('createThread():', runT.isAlive())
action = ttk.Button(win, text="Call wxPython GUI", command=tryRunInThread)
action.grid(column=2, row=1)
#-----------------------------------------------------------------
#======================
# Start GUI
#======================
win.mainloop()
- 运行代码。打开一些 wxPython GUI 并在
tkinterGUI 中输入:

- 关闭 GUI:

为了避免如前一张截图所示,Python.exe 可执行进程崩溃的情况,我们不是在单独的线程中运行整个 wxPython 应用程序,而是可以将代码修改为只让 wxPython 的 app.MainLoop 在一个线程中运行。
-
创建一个新的模块,并将其命名为
Control_Frameworks.py。 -
编写以下代码并运行:
#==================================================================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from threading import Thread
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()
#==================================================================
## working
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) # <==== Thread for MainLoop only
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()
让我们在下一节中详细理解这些步骤。
它是如何工作的…
我们首先尝试在一个线程中运行整个 wxPython GUI 应用程序,即 Control_Frameworks_NOT_working.py,但这没有工作,因为 wxPython 的主事件循环期望它是应用程序的主线程。
首先,Control_Frameworks_NOT_working.py 似乎可以正常工作,这是直观的,因为 tkinter 控件不再被禁用,我们可以通过点击按钮创建几个 wxPython GUI 实例。我们还可以在 wxPython GUI 中输入并选择其他的 tkinter 小部件。然而,当我们尝试关闭 GUI 时,我们会从 wxWidgets 接收错误,并且我们的 Python 可执行文件会崩溃。
我们通过只在一个线程中运行 wxPython 的 app.MainLoop 并使其相信它是主线程来解决这个问题,这个线程是一个技巧。这种方法的一个副作用是我们不能再单独关闭所有的 wxPython GUI 实例。至少有一个实例只有在关闭创建线程作为守护进程的 wxPython GUI 时才会关闭。你可以通过点击一次或多次调用 wxPython GUI 按钮来测试这一点,然后尝试关闭所有创建的 wxPython 窗口表单。我们无法关闭最后一个,除非我们关闭调用的 tkinter GUI!
我并不完全清楚这是为什么。直观上,我们可能期望能够关闭所有守护线程,而无需等待创建它们的父线程首先关闭。这可能与我们的主线程仍在运行时引用计数器没有被设置为零有关。在实用层面上,这就是它目前的工作方式。
两个连接的 GUI 之间的通信
在这个食谱中,我们将探讨使两个 GUI 互相通信的方法。
准备工作
阅读前面的食谱可能是为这个食谱做准备的好方法。
在这个食谱中,我们将使用与上一个食谱略有修改的 GUI 代码,但大部分基本的 GUI 构建代码是相同的。
如何做到这一点…
我们将编写 Python 代码,使两个 GUI 以一定程度的相互通信:
-
创建一个新的模块,并将其命名为
Communicate.py。 -
添加以下代码:
#==================================================================
import tkinter as tk
from tkinter import ttk
from threading import Thread
win = tk.Tk()
win.title("Python GUI")
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()
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)
text = tk.Text(win, height=10, width=40, borderwidth=2, wrap='word')
text.grid(column=0, sticky='WE', columnspan=3)
#==================================================================
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()
menu= wx.Menu()
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menuBar = wx.MenuBar()
menuBar.Append(menu, "File")
parent.SetMenuBar(menuBar)
button = wx.Button(self, label="Print", pos=(0,60))
self.Bind(wx.EVT_BUTTON, self.writeToSharedQueue, button)
self.textBox = wx.TextCtrl(self, size=(280,50), style=wx.TE_MULTILINE)
#-----------------------------------------------------------------
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
#==================================================================
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()
-
运行代码,然后执行下一步。
-
点击两个按钮并在控件中输入:

现在,让我们深入了解幕后,更好地理解代码。
它是如何工作的...
首先运行Communicate.py代码创建程序的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)是将此数据写入tkinter GUI 的 Print 按钮的 wxPython 回调方法的行。
代码中调用了以下函数,使其工作:
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 精神下,我们找到了一个简单的解决方案,对我们来说很适用。一旦我们的代码变得更加复杂,我们可能需要重构它,但这是一个好的开始。
第十章:使用 PyQt5 构建 GUI
在本章中,我们将介绍另一个 Python GUI 工具包,名为 PyQt5,它确实非常出色。PyQt5 具有与tkinter相似的功能,但它附带了一个非常棒的视觉设计工具,允许我们将小部件拖放到表单上。我们还将使用另一个工具,将设计器的.ui代码转换为 Python 代码。
在设计器中视觉设计我们的 GUI 并将其代码转换为 Python 代码后,我们将继续使用纯 Python 为我们的小部件添加功能。首先,我们将安装 PyQt5 和设计器,然后在不使用设计器的情况下编写一个简单的 PyQt5 GUI。之后,我们将视觉设计我们的 GUI。
了解如何使用 PyQt5、视觉设计工具以及如何将.ui转换为.py代码将为您的 Python GUI 开发工具箱增添强大的技能。从这,您将学习如何创建强大而复杂的 GUI,以及如何使用模块化软件开发方法来视觉设计 UI,然后从设计中解耦功能。
这也给你提供了一个机会,比较我们在整本书中向您展示的不同 GUI 框架,这最终将引导你选择一个进行更深入的探索。
我已经创建了两个 Packt 视频课程,专注于使用tkinter和PyQt5进行 Python GUI 编程。您可以在 Packt 网站上找到它们。我还会在本章末尾提供它们的链接。
以下截图提供了您在本章中需要的 Python 模块的概述:

我们将介绍以下菜谱:
-
安装 PyQt5
-
安装 PyQt5 设计器工具
-
编写我们的第一个 PyQt5 GUI
-
更改 GUI 的标题
-
使用面向对象编程重构我们的代码
-
从 QMainWindow 继承
-
添加状态栏小部件
-
添加菜单栏小部件
-
启动 PyQt5 设计器工具
-
在 PyQt5 设计器中预览表单
-
保存 PyQt5 设计器表单
-
将设计器的
.ui代码转换为.py代码 -
理解转换后的设计器代码
-
构建模块化 GUI 设计
-
在我们的菜单栏中添加另一个菜单项
-
将功能连接到退出菜单项
-
通过设计器添加标签小部件
-
在设计器中使用布局
-
在设计器中添加按钮和标签
安装 PyQt5
在这个菜谱中,我们将安装 PyQt5 GUI 框架。我们将使用 Python 的pip工具下载 PyQt5 的 wheel 格式安装程序。
您可以在以下链接找到官方文档:www.riverbankcomputing.com/static/Docs/PyQt5/installation.html。
准备工作
您需要在您的计算机上安装 Python 的pip工具。您可能已经有了它。
如何做到这一点...
让我们看看如何使用 Python 的pip工具安装 PyQt5:
-
打开 Windows PowerShell 窗口或命令提示符。
-
输入
pip install pyqt5命令。 -
按下Enter键。
-
通过运行
pip list来验证安装。
它是如何工作的...
在步骤 1中,我们打开一个 PowerShell 窗口,在步骤 2中,我们使用 Python 的pip工具。在步骤 3中按下Enter键运行命令后,安装将开始并运行至完成。您将看到类似于以下输出的内容:

在步骤 4中,我们再次使用pip来验证我们是否成功安装了 PyQt5。输出将类似于以下截图:

您可能会在您的计算机上看到安装了更多的软件包。重要的是要检查 PyQt5 软件包是否列在列表中。安装的版本号列在软件包名称的右侧。
安装 PyQt5 Designer 工具
在这个菜谱中,我们将安装 PyQt5 Designer 工具。我们将通过使用 Python 的pip工具来完成此操作。步骤与之前安装 PyQt5 的菜谱非常相似。
准备工作
您需要在您的计算机上安装 Python 的pip工具。
如何操作...
让我们看看如何使用 Python 的pip工具安装 PyQt5 Designer。请注意,该软件包不仅包括 Designer 工具:
-
打开一个 Windows PowerShell 窗口或命令提示符。
-
输入以下命令:
pip install pyqt5-tools
-
按下Enter键。
-
通过运行以下命令来验证安装:
pip list
- 在您的硬盘上找到
Designer.exe文件。
它是如何工作的...
在步骤 1中,我们打开一个 PowerShell 窗口,在步骤 2中,我们使用 Python 的pip工具。在步骤 3中按下Enter键运行命令后,安装将开始并运行至完成。您将看到类似于以下输出的内容:

请注意,在前面的截图中,安装遇到了错误。我不知道为什么,但有时安装会遇到错误。我简单地重新运行了安装,这次它没有遇到任何错误。包括 Designer 在内的必要工具都成功安装了。
步骤 4与之前的菜谱中的步骤完全相同,输出也是完全相同的。请参考安装 PyQt5菜谱的输出截图以获取更多信息。
在步骤 5中,我们想要找到Designer.exe文件,这是我们将在后续菜谱中使用的 Visual Designer 工具。找到它后,您可能想要在桌面上为其创建一个快捷方式。
这是Designer.exe在我的计算机上安装位置的截图:

您的位置可能不同,但这可以给您一个寻找工具的大致方向。
编写我们的第一个 PyQt5 GUI
在这个菜谱中,我们将编写我们的第一个 PyQt5 GUI。我们将直接使用 PyQt5 而不使用 Designer。
准备工作
您需要安装 PyQt5。请参阅安装 PyQt5菜谱,了解如何安装 PyQt5。使用您喜欢的 Python 编辑器编写代码。如果您不熟悉 Eclipse、PyCharm 等现代 IDE,可以使用随 Python 一起提供的 IDLE 编辑器。
如何操作...
让我们看看我们如何使用 PyQt5 构建我们的第一个 GUI:
-
打开您的 Python 编辑器。
-
创建一个新的 Python 模块并将其保存为
First_GUI_PyQt5.py。 -
首先输入以下导入语句:
import sys
from PyQt5.QtWidgets import QApplication, QWidget
- 在导入语句下方添加以下四行代码:
app = QApplication(sys.argv)
gui = QWidget()
gui.show()
sys.exit(app.exec_())
- 运行前面的代码。最大化、最小化和调整结果 GUI 的大小。点击右上角的×符号来关闭应用程序:

现在,让我们幕后看看它是如何工作的。
工作原理...
在步骤 1和步骤 2中,我们创建了一个新的 Python 模块。在步骤 3中,我们编写了一些导入语句。
我们导入sys以便我们可以将命令行参数传递到我们的 GUI 中。
从 PyQt5 包中导入QApplication和QWidget类,这两个类都位于QtWidgets模块中。
我们创建了一个QApplication类的实例,传递sys.argv以便我们可以传递额外的命令行参数。我们将此实例保存在app变量中。这将创建我们的应用程序。
然后,我们创建一个QWidget类的实例,它成为我们的 GUI。我们将此实例保存在名为gui的局部变量中。
接下来,我们在gui类实例上调用show方法,使 GUI 可见。
之后,我们在我们的应用程序类实例上调用exec_方法,这会执行我们的应用程序。我们将调用包装在sys.exit中,以便捕获可能发生的任何异常。如果发生异常,这将确保我们的 Python 应用程序干净地退出,而不会崩溃。
更改 GUI 的标题
在这个菜谱中,我们将更改上一菜谱中创建的 GUI 的标题。
准备工作
我们将使用上一菜谱中的代码,所以您可以选择将其输入到自己的模块中,或者从 Packt 网站下载这本书的代码。
如何操作...
我们将通过更改此 GUI 的标题来增强上一菜谱中的 GUI。让我们开始吧:
-
打开
First_GUI_PyQt5.py并将其保存为GUI_PyQt5_title.py。 -
在现有代码的中间添加以下代码行:
gui.setWindowTitle('PyQt5 GUI')
- 运行代码并注意新的标题:

现在,让我们幕后看看它是如何工作的。
工作原理...
在步骤 1中,我们通过保存为新名称来重用上一菜谱中的代码。
在步骤 2中,我们在gui实例上调用setWindowTitle方法,将其作为字符串传递。当我们运行应用程序时,这个字符串成为我们的新标题。
完整的代码现在看起来是这样的:
import sys
from PyQt5.QtWidgets import QApplication, QWidget
app = QApplication(sys.argv)
gui = QWidget()
gui.setWindowTitle('PyQt5 GUI') # <-- method call in the middle
gui.show()
sys.exit(app.exec_())
在步骤 3中,我们运行代码并看到窗口标题现在显示为 PyQt5 GUI 而不是 python。
更多...
在前面的代码中需要注意的一个非常重要的事情是我们调用 setWindowTitle 的位置,因为这显示了每个 PyQt5 应用程序遵循的典型代码结构。
在导入语句之后,在顶部创建一个 PyQt5 应用程序。在底部执行应用程序。我们添加到 GUI 的所有功能都位于顶部和底部代码之间。
将我们的代码重构为面向对象编程
在这个菜谱中,我们将使用类将我们的代码重构为 面向对象编程 (OOP)。这是为 PyQt5 设计器代码和本章后面将要构建的菜谱做准备。在这个菜谱中,GUI 的最终输出看起来相同,但代码将不同。
我们将构建一个继承自 QWidget 的类。
准备工作
我们将重构之前菜谱中的代码,所以请确保你理解那段代码。
如何做到这一点...
我们将把之前的、过程化的代码转换为面向对象的代码。以下是我们的做法:
-
创建一个新的模块,并将其命名为
GUI_PyQt5_refactored_OOP.py。 -
首先编写相同的导入语句:
import sys
from PyQt5.QtWidgets import QApplication, QWidget
- 创建一个继承自
QWidget的类:
class GUI(QWidget):
def __init__(self):
super().__init__()
# initialize super class, which creates the Window
self.initUI()
def initUI(self):
self.setWindowTitle('PyQt5 GUI')
- 在前面的代码下添加一个 Python 自测部分:
if __name__ == '__main__':
app = QApplication(sys.argv)
gui = GUI()
gui.show()
sys.exit(app.exec_())
- 运行应用程序。生成的 GUI 将与之前菜谱中的相同。
它是如何工作的...
在 步骤 1 中,我们创建了一个新的模块,而在 步骤 2 中,我们在本章之前的菜谱中添加了相同的导入语句。
在 步骤 3 中,我们创建了一个新的类,它继承自 QWidget。我们调用 super 来初始化父类,这反过来又创建了我们的 GUI。
然后,我们创建并调用一个设置窗口标题的类方法。
在 步骤 4 中,我们使用 Python 的自测功能来创建 PyQt5 应用程序和 GUI,然后执行代码。
运行此代码将创建与之前菜谱中相同的 GUI,但我们的代码现在正在使用面向对象编程。
从 QMainWindow 继承
现在我们已经看到了如何从 PyQt5 类中继承,在这个菜谱中,我们将从 QMainWindow 继承。与从 QWidgets 继承相比,这为我们设计 GUI 提供了更多选项。除了设置 GUI 窗口标题外,我们还将给它一个特定的尺寸。
准备工作
阅读之前的菜谱,以便理解我们在这里编写的代码。
如何做到这一点...
我们将继承自 QMainWindow 并指定 GUI 的大小。让我们开始吧:
-
创建一个新的模块,并将其命名为
GUI_PyQt5_QMainWindow.py。 -
编写以下导入语句:
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
- 创建以下类:
class GUI(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('PyQt5 GUI')
self.resize(400, 300)
-
添加之前菜谱中显示的相同的
'__main__'代码。 -
运行代码。生成的 GUI 将看起来与前面两个菜谱中的相同,但会更小。
它是如何工作的...
在 步骤 1 中,我们创建了一个新的模块,而在 步骤 2 中,我们编写了导入语句。然而,这一次,我们不是导入 QWidgets,而是导入 QMainWindow。在 步骤 3 中,我们创建了一个新的类,它继承自 QMainWindow。和之前一样,我们在调用的方法中设置了标题。然而,除了设置标题之外,我们还为我们的 GUI 指定了一个特定的尺寸。我们通过调用 resize 方法,并传入宽度和高度来实现这一点。
步骤 4 和 步骤 5 与前面的配方中相同,但现在的 GUI 大小是我们在 resize 方法中指定的。
添加状态栏小部件
在这个配方中,我们将开始向之前创建的 GUI 添加小部件。我们将从添加状态栏开始。这是一个 PyQt5 内置的小部件,所以我们只需要使用它。
准备工作
我们将扩展前面配方中的 GUI,因此请阅读前面的配方,以便理解我们在这里编写的代码。
如何实现...
让我们开始吧:
-
创建一个新的模块,并将其命名为
GUI_PyQt5_statusbar.py。 -
写出与前面配方中完全相同的代码,该代码可以在
GUI_PyQt5_QMainWindow.py中找到。 -
在类中创建一个新的方法,命名为
add_widgets并调用它,如下面的代码块所示:
Def initUI(self):
self.setWindowTitle('PyQt5 GUI')
self.resize(400, 300)
self.add_widgets() # <== call new method here
def add_widgets(self):
self.statusBar().showMessage('Text in statusbar')
- 运行前面的代码,并注意 GUI 底部的新的状态栏:

现在,让我们深入了解这是如何工作的。
它是如何工作的...
在 步骤 1 中,我们创建了一个新的模块,而在 步骤 2 中,我们重用了前面配方中的代码。在 步骤 3 中,我们创建了一个新的方法 add_widget,在其中我们创建了 PyQt5 内置的状态栏。我们使用 self 来访问这个小部件,因为 statusBar 小部件是 QMainWindow 的一部分。这也是我们为什么选择从 QMainWindow 继承而不是从 QWidgets 继承来构建我们的 GUI 的原因之一。
在创建状态栏后,我们立即调用其上的 showMessage 方法。我们本可以将这分成两个步骤来做,即创建状态栏并将这个类的实例保存在一个局部变量中,然后使用这个变量来调用 showMessage。在这里,我们将代码简化为了一行。
添加菜单栏小部件
在这个配方中,我们将向前面配方中创建的 GUI 添加一个菜单栏。我们之前在 tkinter 中这样做过,但在这个配方中,我们将看到如何使用 PyQt5 创建菜单栏要简单得多,也更直观。
我们还将开始创建 PyQt5 动作,这些动作将为 GUI 添加功能。
准备工作
我们将扩展前面配方中的 GUI,其中我们添加了一个状态栏。为了理解我们在这里编写的代码,请阅读前面的配方。
如何实现...
我们将扩展前面配方中的内容,其中我们添加了第一个小部件。让我们看看我们如何做到这一点:
-
创建一个新的模块,并将其命名为
GUI_PyQt5_menubar.py。 -
复制之前菜谱中的代码,该代码位于
GUI_PyQt5_statusbar.py文件中。 -
在
add_widgets方法中,添加以下代码:
def add_widgets(self):
self.statusBar().showMessage('Text in statusbar')
menubar = self.menuBar()
file_menu = menubar.addMenu('File')
new_action = QAction('New', self)
file_menu.addAction(new_action)
new_action.setStatusTip('New File')
- 运行前面的代码。您将看到一个带有菜单项的新菜单栏。点击文件菜单,然后点击新建。查看状态栏中的文本:

让我们深入了解它是如何工作的。
它是如何工作的...
在 步骤 1 中,我们创建了一个新模块,而在 步骤 2 中,我们重用了之前菜谱中的代码。在 步骤 3 中,我们在 add_widgets 方法中添加了新代码。再次,我们使用 self 来访问内置在 QMainWindow 中的 menuBar 类。在创建菜单栏的实例后,我们使用 addMenu 方法创建一个菜单。我们使用 QAction 类创建一个菜单项,然后我们使用 addAction 方法将此菜单项添加到菜单中。
我们使用 new_action 变量来调用 setStatusTip。现在,当我们点击文件 | 新建时,我们可以在状态栏中看到显示的文本,如图 步骤 4 所示。
启动 PyQt5 设计器工具
在这个菜谱中,我们将开始使用 PyQt5 设计器工具。我们将通过视觉设计我们的 GUI,并将小部件拖放到主窗口表单中。这个表单可以是 QWidgets 表单或 QMainWindow 表单。
准备工作
您需要在您的计算机上安装 PyQt5 和 Qt 设计器工具。请阅读 安装 PyQt5 和 安装 PyQt5 设计器工具 菜谱,以了解如何进行此操作。
如何做...
您需要运行 Designer.exe 文件。其位置可以在 安装 PyQt5 设计器工具 菜谱中找到。
让我们开始吧:
-
定位
Designer.exe并双击它以运行。 -
设计器 GUI 将以以下方式打开:

-
在“新建表单 - Qt 设计器”对话框中,如图所示,将左上角默认值更改为主窗口。
-
点击对话框中的创建按钮。
-
您应该看到 Qt 设计器变为以下视图:

让我们深入了解我们所看到的内容。
它是如何工作的...
在 步骤 1 中,我们通过双击可执行文件来启动 Qt 设计器。在 步骤 2 中,我们可以看到,默认情况下,我们被提供了一个对话框表单,允许我们创建新的 UI 或打开现有的 UI。
在对话框后面的表单是深灰色,这意味着它是空的。这实际上是我们在其中设计 GUI 的区域。
在左侧,我们可以看到 Widget Box 区域。这个区域包含所有 Designer 可以访问的 PyQt5 小部件。我们将从 Widget Box 中拖放小部件到 UI 表单中。
在设计器的右侧,我们有两个窗口:对象检查器和属性编辑器。目前它们都是空的。
在步骤 3中,我们将默认设置更改为主窗口,因为我们想创建一个QMainWindow应用程序。在之前的菜谱中,我们是手动完成这个操作的,但在这里我们使用设计器来为我们完成这个操作。
在步骤 4中,我们点击创建按钮,这将关闭对话框并在设计器的中心区域创建一个新的主窗口表单。同时,右侧的两个窗口也不再为空。
在步骤 5中,我们注意到主窗口拥有的类和属性。在对象检查器中,我们可以看到四个类:QMainWindow、QWidget、QMenuBar 和 QStatusBar。在之前的菜谱中,我们手动添加了菜单栏和状态栏。在创建新的QMainWindow时使用设计器工具,我们可以看到设计器已经为我们自动添加了这项功能。
在属性编辑器中,我们可以看到中央小部件对象的几何形状属性。这是一个 QWidget,是整个主窗口的中心部分。菜单栏和状态栏分别位于中央表单的上方和下方。几何形状属性的默认值为 800 x 600,这将成为我们以这种方式运行代码时的 GUI 的最终大小。
我们可以使用这个属性来更改 UI 的大小。或者,我们可以将 UI 表单拖到设计器的中心来更改其大小。这将更新这个属性,使其两种方式都有效。
在设计器周围看看,以了解它是如何工作的以及它提供了哪些信息。
在 PyQt5 设计器中预览表单
在这个菜谱中,我们将学习如何预览我们使用设计器创建的表单。这是设计器为我们提供的一个非常有用的功能,因为我们可以在设计过程中进行更改、撤销更改、预览更改,等等,直到我们对设计满意为止。到那时,我们可以保存设计。
准备工作
您需要在您的计算机上安装 PyQt5 和 Qt Designer 工具。
如何操作...
按照之前的菜谱中解释的,运行 Designer.exe。我们将更改主窗口的大小,然后预览它。按照以下步骤学习如何预览表单:
-
从上一个菜谱中执行步骤 1到步骤 5。
-
在属性编辑器中,将几何形状属性更改为
400x300,如图所示:

-
在设计器菜单中,点击表单 | 预览... 或按 Ctrl + R。
-
您应该看到以下预览:

让我们深入了解代码,以更好地理解它。
它是如何工作的...
在步骤 1中,我们正在执行与之前菜谱中相同的步骤。这使我们回到了相同的状态,因为我们一旦关闭设计器工具,如果没有保存我们的 UI,我们的 UI 将会丢失。我们到目前为止还没有保存。
在 步骤 2 中,我们正在使用 Designer 右侧的属性编辑器更改 UI 的大小。确保在此编辑器中选中 QWidget 而不是 QMainWindow。如果你的编辑器看起来像下面的截图,只需点击它左侧的箭头来展开 QWidget 属性:

在 步骤 3 中,我们正在预览当前的 UI 设计。有两种方法可以做到这一点:点击菜单项并按快捷键。
步骤 4 显示了生成的 UI。注意窗口标题栏中的“预览”一词。
保存 PyQt5 Designer 表单
在本食谱中,我们将添加之前创建的相同菜单和菜单项。在预览后,我们将保存我们的 UI。
准备工作
您需要在您的计算机上安装 PyQt5 和 Qt Designer 工具。
如何操作...
运行 Designer.exe,如前一个食谱中所述。为了创建菜单和菜单项,我们可以在 Designer 的主窗口中直接输入。接下来,执行以下步骤:
-
执行前一个食谱中的 步骤 1 和 步骤 2。
-
在 Designer 中,在 MainWindow - untitled* 中,将
File输入到“在此处输入”菜单中,如下面的截图所示:

- 点击文件,输入
New并按 Enter 键创建菜单项:

- 按 Ctrl + R 预览 UI:

- 关闭预览,并在 Designer 中将设计保存为
Designer_First_UI.ui,如下面的截图所示:

让我们深入了解,以便更好地理解这些步骤。
它是如何工作的...
在 步骤 1 中,我们正在执行与前一个食谱中相同的步骤。在 步骤 2 中,我们通过在 Designer 提供的菜单栏中直接输入来创建文件菜单。在 步骤 3 中,我们在新菜单中添加一个菜单项,同样也是通过在“在此处输入”下方输入。
在 步骤 4 中,我们预览我们的 UI 设计,而在 步骤 5 中,我们实际上第一次保存我们的设计。
注意我们设计的 UI 的扩展名是 .ui。
将 Designer .ui 代码转换为 .py 代码
在本食谱中,我们将查看我们在使用 Qt Designer 工具保存设计时保存的 .ui 代码。之后,我们将使用在安装 PyQt5 工具期间安装的一个实用工具,该工具将 ui 代码转换为 Python py 代码。
我们将特别使用 pyuic5 工具。你可以这样想:
通过使用 PyQt 版本 5 将 Designer ui 代码转换为 Python py 代码。
如果您正在尝试找到pyuic5.exe的位置,它实际上被安装到 Python 的scripts子文件夹中。在我的安装中,这是C:\Python37\Scripts\pyuic5.exe。确保您的PATH设置为Scripts文件夹,以便成功运行它。
让我们做好准备。
准备工作
您需要在您的计算机上安装 PyQt5 工具。
如何做...
首先,我们将打开在上一个菜谱中保存我们的 UI 时在 Designer 中生成的.ui代码。现在,按照以下步骤操作:
-
从上一个菜谱中在文字编辑器(如 Notepad++)中打开
Designer_First_UI.ui。 -
查看
.ui代码:

-
导航到您在硬盘上保存
Designer_First_UI.ui的位置,并打开一个 Windows PowerShell 或命令提示符窗口。 -
输入
pyuic5 -x -o Designer_First_UI.py Designer_First_UI.ui命令,并按以下截图所示按Enter键:

- 在 PowerShell 或命令提示符窗口中运行
ls命令以查看新生成的.py文件。或者,使用 Windows 文件资源管理器查看新文件:

让我们深入了解这些转换步骤以更好地理解它们。
它是如何工作的...
在步骤 1和步骤 2中,我们正在打开 Designer 中保存的.ui文件。我们使用 Notepad++或任何其他文字编辑器进行此操作。生成的.ui输出是清晰的 XML。
这绝对不是 Python 代码。我们必须将 XML 转换为 Python 代码,我们在步骤 3和步骤 4中这样做。
pyuic5.exe后面的-x参数使生成的 Python 模块可执行,而-o指定输出文件的名称。我们选择了与.ui文件相同的名称,但扩展名为.py。我们可以选择任何我们想要的名称,只要它有一个.py扩展名。pyuic5实用程序还具有将多个.ui文件转换为单个.py文件的能力,因此能够选择名称非常有用。
当运行pyuic5.exe时,如果转换成功,我们不会得到任何输出。如果我们没有收到任何错误,这意味着转换成功。
在步骤 5中,我们验证我们有了新的输出文件,即Designer_First_UI.py。
理解转换后的 Designer 代码
在上一个菜谱中,我们使用pyuic5转换工具将 Designer UI 代码转换为 Python 代码。在这个菜谱中,我们将查看生成的代码。我们用 Designer 创建的每个 GUI 都需要转换,我们做出的任何更改都将覆盖所有之前的代码。这将使我们能够理解如何通过 Python 的模块化方法将 UI 代码与我们将添加到 UI 的功能解耦。
准备工作
您需要拥有从上一个菜谱中转换的代码。如果您没有遵循本章前面的菜谱,只需从本书的 Packt 网站下载必要的代码。该网站提供了本书的所有代码,您只需点击一个按钮即可通过 GitHub 下载所有代码。
如何做到...
我们需要打开从.ui代码转换而来的.py代码以了解其结构。现在我们已经这样做,我们可以遵循以下步骤:
-
从上一个菜谱中打开
Designer_First_UI.py。 -
注意自动生成模块的顶部部分:

- 看看上一节下面的导入语句:

- 看看创建的类及其第一个方法:

- 看看类中第一个方法下面的第二个方法:

- 最后,看看代码底部的
"__main__"部分:

- 运行此代码。结果应该是一个运行的 Python GUI:

让我们深入了解代码背后的情况。
它是如何工作的...
在 步骤 1 中,我们将转换后的 UI 作为 Python 模块打开。在 步骤 2 中,我们可以看到一个重要的警告。
我强烈建议您认真对待这个警告。如果您在此模块中添加代码,并在以后通过pyuic5.exe重新生成代码,您所做的所有更改确实都会丢失!
步骤 3 显示了三个导入语句。这些语句始终被导入,尽管QtGui不是必需的,正如我在 Eclipse/PyDev 编辑器中的黄色警告和下划线所示。
步骤 4 显示了始终创建的类。它紧随setupUi方法之后。__init__方法之间没有。此方法中的代码对我们非常重要,因为我们可以通过生成的名称访问类属性。
在 步骤 5 中,我们注意到retranslateUi方法。此方法也是自动生成的。通过仔细观察,我们可以找到在 UI 设计阶段添加的菜单和菜单项的名称。
步骤 6 显示了代码底部的 "__main__" 部分。关于这一点,重要的是要知道,此部分仅在指定pyuic5转换期间的-x选项时创建。如果我们省略此选项,我们将看不到此部分。
在 步骤 7 中,我们运行我们的 GUI。注意我们不再 预览 UI。现在这是真正的纯 Python 代码。
构建模块化 GUI 设计
正如我们在上一个菜谱中看到的,我们使用设计师设计的 UI 的所有自动生成的代码,一旦我们重新运行pyuic5工具,就会被覆盖。这是好事,因为它鼓励我们以模块化的方式设计 Python 模块(因此得名模块)。
在这个菜谱中,我们将从新的 Python 模块中导入生成的 UI 并在其中添加功能。每次我们重新运行 pyuic5 工具时,我们的代码都不会意外地被覆盖,因为我们正在将逻辑与 UI 分离。
关注点分离(SoC)是一个软件术语,指的是良好、模块化设计的好处。
因此,让我们编写一些代码!
准备工作
你需要从之前的菜谱中获取转换后的代码,该代码可以在 Designer_First_UI.py 中找到。
如何操作...
我们将创建一个新的模块,在其中我们将向我们的 UI 代码添加功能。我们将导入我们在 Qt 设计器中创建并转换为 Python 代码的 UI。让我们开始吧:
-
创建一个新的 Python 模块,并将其命名为
Designer_GUI_modular.py。 -
在这个模块中,编写以下代码行:
from Ch10_Code.Designer_First_UI import Ui_MainWindow
注意,你可能需要调整 Ch10_Code 前缀以匹配你的位置。
-
运行前面的代码。你不应该遇到任何错误。
-
接下来,将
Designer_First_UI.py中的"__main__"部分复制到这个新模块中。 -
你还需要导入
QtWidgets来使这生效:
from PyQt5 import QtWidgets
from Ch10_Code.Designer_First_UI import Ui_MainWindow
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
- 现在,运行前面的代码。你应该能看到我们设计和之前独立运行的 GUI:

让我们深入了解代码背后的原理。
工作原理...
在 步骤 1 中,我们创建一个新的 Python 模块。在 步骤 2 中,我们只是将 UI 导入到 Python 生成的代码中。
这非常重要,因为它展示了 SoC 的原则!
在 步骤 3 中,我们运行这一行代码。将不会显示任何 GUI,但这里重要的是我们不会遇到任何错误。如果我们遇到一些错误,通常意味着我们的导入语句失败了,因为我们的模块无法定位我们试图导入的模块。
步骤 4 复制我们转换的 .py 文件中的 "__main__" 部分。当模块自行运行时,当我们导入它时,我们还需要导入 QtWidgets,因为当我们导入模块时,那些模块的导入语句不会自动导入。我们在 步骤 5 中这样做。在 步骤 6 中,我们的 GUI 正在运行,但这次是通过模块化方法。
向菜单栏添加另一个菜单项
在这个菜谱中,我们将向我们的 GUI 添加第二个菜单项。我们将使用设计器然后重新生成 UI 代码。之后,我们将从我们的模块化 Python 模块中附加功能到菜单项。设计器具有某些功能,因此它可以添加此功能,但在这里,我们只是将 UI 代码与 GUI 的功能分开。
准备工作
你需要从之前的菜谱中获取 UI 代码。所有其他菜谱的先决条件也适用于这个菜谱。
如何操作...
我们将通过添加第二个菜单项来增强之前菜谱中的 UI 设计。之后,我们将像之前一样将 UI 代码转换为 Python 代码。让我们开始吧:
-
在 Qt 设计器中打开
Designer_First_UI.ui。 -
在文件 | 新菜单项下方创建另一个菜单项并将其命名为
Exit:

将这个新的菜单项输入到“在此处输入”区域。它看起来会是这样:

- 按下 Enter 键并保存
.ui文件。接下来,预览 UI:

- 运行
pyuic5.exe工具将.ui文件转换为.py文件。让我们将其保存为新的名称,以区分我们的原始模块。
在 PowerShell 或命令提示符窗口中,输入 pyuic5.exe -x -o Designer_First_UI_Exit.py Designer_First_UI.ui 并然后按 Enter 键:

你现在应该有一个名为 Designer_First_UI_Exit.py 的新 Python 模块。
- 创建一个新的 Python 模块并将其命名为
Designer_GUI_modular_exit.py。将新转换的文件导入其中。以下是代码的示例:
from PyQt5 import QtWidgets
from Ch10_Code.Designer_First_UI_Exit import Ui_MainWindow
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
- 运行 GUI 并点击文件菜单以查看新的退出菜单项。结果将与我们在设计器中转换之前预览运行 GUI 的 步骤 3 中得到的结果相同:

现在,让我们幕后了解一下发生了什么。
它是如何工作的...
在 步骤 1 中,我们打开我们之前创建的 .ui 设计文件。我们并没有将其保存为不同的名称,所以我们基本上是在现有的设计中添加了一个新的菜单项。
在 步骤 2 中,我们使用设计器工具添加一个新菜单项并将其命名为 Exit。
在 步骤 3 中,我们按 Enter 键,坦白说,这听起来非常简单,但如果我们不这样做,我们的新项将不会被保存。我们还将 .ui 文件以相同的名称保存,覆盖我们之前的版本。这是可以的,因为我们只是在 UI 中添加了一些小的功能。
在 步骤 4 中,我们运行 pyuic5.exe 工具将 .ui 文件的 XML 转换为 Python 代码。然而,这一次,我们给生成的 .py 输出文件一个与 .ui 文件不同的名称。我们这样做是为了不覆盖我们之前的 Python 模块。
在 步骤 5 中,我们创建一个新的 Python 模块并将转换后的 .ui 导入其中。我们之前已经这样做过了。最后,在 步骤 6 中,我们运行纯 Python 代码,这样我们就可以看到我们新的菜单项。
还有更多...
在下一个菜谱中,我们将向我们的新退出菜单项添加功能,以便当我们点击它时,我们的 GUI 确实会退出,应用程序也会结束。
将功能连接到退出菜单项
在这个菜谱中,我们将向我们在上一个菜谱中创建的退出菜单项添加功能。到目前为止,我们有两个菜单项,但它们不是交互式的。
在这里,我们将学习如何通过使用我们的模块化编码方法在 UI 之外添加功能。我们还将通过将 "__main__" 自测试部分转换为其自己的类来改进我们的代码。
准备中
您需要将前一个菜谱中的.ui代码准备好。所有其他先决条件也适用于此菜谱。
如何做到这一点...
我们将通过 SoC 的模块化方法为我们的 GUI 添加功能。为了使我们的代码更健壮,我们将创建一个新的类。让我们开始吧:
-
创建一个新的 Python 模块,并将其命名为
Designer_GUI_modular_exit_class.py。 -
将以下导入语句输入到模块中:
import sys
from PyQt5 import QtWidgets
from Ch10_Code.Designer_First_UI_Exit import Ui_MainWindow
- 创建一个带有
__init__方法的新的 Python 类:
class ExitDesignerGUI():
def __init__(self):
app = QtWidgets.QApplication(sys.argv)
self.MainWindow = QtWidgets.QMainWindow()
self.ui = Ui_MainWindow()
self.ui.setupUi(self.MainWindow)
self.update_widgets()
self.widget_actions()
self.MainWindow.show()
sys.exit(app.exec_())
- 创建一个更新状态栏并连接动作到菜单项的方法:
def widget_actions(self):
self.ui.actionExit.setStatusTip(
'Click to exit the application')
self.ui.actionExit.triggered.connect(self.close_GUI)
- 编写一个关闭 GUI 的回调方法:
def close_GUI(self):
self.MainWindow.close()
- 编写一个更新 GUI 标题的类方法:
def update_widgets(self):
self.MainWindow.setWindowTitle('PyQt5 GUI')
- 在
"__main__"部分创建类的实例:
if __name__ == "__main__":
ExitDesignerGUI()
现在,让我们看看这是如何工作的。
它是如何工作的...
在步骤 1中,我们创建了一个新的 Python 模块,而在步骤 2中,我们在模块顶部编写了所需的导入语句。
在模块顶部编写所有导入语句是 Python 的最佳实践,并且强烈推荐。
在步骤 3中,我们创建了自己的 Python 类,并从典型的初始化器开始。在初始化器方法的末尾,我们调用在初始化器下面创建的方法。因为我们这样做,所以我们只需要实例化类,方法就会运行,而无需在类实例(对象)创建后调用类的任何特定方法。
在步骤 4中,我们通过self.ui.actionExit访问我们的菜单项名称。我们可以使用self.ui,因为在"__init__"方法中,我们创建了一个MainWindow的实例并将其保存为这样的实例。这是代码行:
self.ui = Ui_MainWindow()
为什么我们使用actionExit?我们必须查看自动生成的代码以找到这个对象名。
我们在设计师中设计了我们的 UI,设计师为我们选择了一个名字。如果我们愿意,我们可以更改那个对象名,但这不是必要的。我们只需要找到我们正在寻找的名字。所以,让我们看看Designer_First_UI_Exit.py:

我们可以在该文件中找到"actionExit"名称。
我们也可以在 Qt 设计师的对象检查器中查找它:

无论哪种方式都行。
在步骤 4中,我们正在实现所需的功能,即通过退出菜单项关闭我们的 GUI。
我们在以下代码行中这样做,它调用了我们在步骤 5中创建的方法:
self.ui.actionExit.triggered.connect(self.close_GUI)
需要注意的非常重要的一点是,我们通过调用triggered.connect(**<method name>**)来实现这个功能。这连接了动作到菜单项被触发的事件。
这是 PyQt5 的语法和语义。
在步骤 5中,为了关闭 GUI,我们在MainWindow上调用内置的close方法。我们在"__init__"方法中保存了对MainWindow的引用,这样我们就可以在这个方法中,在同一个 Python 类中引用它。
在第 6 步中,我们使用相同的self.MainWindow引用来给我们的 GUI 窗口设置标题。
在第 7 步中,我们使用 Python 的自测试"__main__"部分来创建一个类实例。这就是我们运行我们的 GUI 所需要做的所有事情。
现在,当我们点击退出菜单项时,我们的 GUI 将退出。
通过设计师添加标签控件
在这个菜谱中,我们将使用设计师工具将标签控件添加到我们的 UI 中。然后,我们将.ui代码转换为 Python 代码。这将为我们添加更多小部件和功能到我们的 GUI 做好准备。
准备工作
你需要从之前的菜谱中获取.ui代码。所有其他先决条件也适用于此菜谱。
如何做到这一点...
我们将通过使用设计师将标签控件添加到我们的 UI 设计中。这就像拖放一样简单。让我们开始吧:
-
在 Qt Designer 中打开
Designer_First_UI.ui并将其保存为Designer_Second_UI.ui。 -
从设计师的左侧拖动一个标签控件到主表单中:

-
调整标签控件的大小,使其填充
MainWindow的大部分区域,如前一张截图所示。 -
保存
.ui设计并预览它:

-
点击标签 1 和标签 2。
-
点击退出菜单项。
-
使用
pyuic5.exe工具将.ui代码转换为 Python 代码。 -
运行转换后的 Python UI 代码。
它是如何工作的...
在第 1 步中,我们正在打开 Qt Designer 中的现有.ui文件,但这次我们以不同的文件名保存它。将我们的 UI 设计的不同版本保存在不同的名称下通常是一个好主意,以防我们的 UI 设计出了问题可以回到之前的版本。
我们漂亮的 UI 设计意外地被搞乱是很常见的,所以请确保你备份了你的.ui文件。你也可以使用像 GitHub 这样的版本控制系统来备份你的代码。
在第 2 步中,我们正在使用设计师的出色拖放功能将小部件(在视觉上和物理上)移动到画布表单上。我们可以简单地使用调整大小手柄以任何我们想要的方式调整小部件。我们在第 3 步中这样做。
看看设计师右侧的对象检查器,注意新的标签控件,以及两个标签、对象名称和它们所属的 PyQt5 类。
你也可能注意到MainWindow标题右侧有一个星号(*)。这意味着我们还没有保存设计。请注意这一点,因为如果你在没有保存的情况下关闭 UI 设计,你将丢失你的美丽设计。
在第 4 步中,我们保存我们的 UI 设计并预览它。
在第 5 步中,我们可以在两个新标签之间点击。注意每个标签的默认颜色是白色,在预览模式下的颜色与设计模式下的颜色不同。
第 6 步将不会关闭 UI,因为关闭 UI 的功能与 UI 解耦。我们编写的代码使得我们可以在不同的 Python 模块中关闭 GUI。
第 7 步主要留给你作为练习。
习惯在设计师中做小改动,转换代码,导入,运行,然后在设计师中做更多改动,如此循环。当您使用设计师工具时,习惯这种 GUI 开发的节奏。
在 步骤 8 中,我们运行代码。使用 -x 选项创建自测试的 "__main__" 部分,因此您应该能够在不导入的情况下运行转换后的 Python 代码。
在设计师中使用布局
在本食谱中,我们将探讨使用 PyQt5 布局的非常重要的概念,我们将使用设计师工具来实现这一点。在 tkinter 中,我们探讨了标签框架。在 PyQt5 中,水平和垂直布局是我们设计 UI 的主要方式。
这可能有点棘手,所以,如我之前提到的,请确保您经常备份您的 UI 设计,这样当事情变得糟糕时,您有一个可以返回的基础。
在下面的食谱中,我们将将这些小部件放置到这些布局中。
准备工作
您需要从上一个食谱中获取 .ui 代码。所有其他先决条件也适用于本食谱。
如何做到这一点...
我们将在主窗口中添加两个水平布局,我们将使用 Qt 设计师中的拖放来完成。让我们开始吧:
-
在设计师中打开
Designer_Second_UI.ui并将其保存为Designer_Second_UI_layout.ui。 -
将一个水平布局拖动到表单底部并调整其大小。
-
将第二个水平布局拖动到第一个布局上方:

现在,您的 MainWindow 将看起来像前面的截图。
- 保存设计,将其转换为 Python 代码,然后运行以确保没有错误。代码应该能运行,但请注意,您在最终的 GUI 中不会看到任何差异。
它是如何工作的...
在 步骤 1 中,我们正在以不同的名称保存我们的 UI 设计,实际上是在做一个备份。
在 步骤 2 和 步骤 3 中,我们是在视觉上将水平布局拖动到主表单上。您可以在前面的截图中看到它们,但在预览模式下您不会注意到任何差异。
一个非常重要的事情是要注意设计师右上角的对象检查器中的名称和类,以及设计师右下角的属性编辑器中自动为我们提供的属性。
此外,请注意我们是如何将这些小部件放置在标签 1 中的。标签 2 仍然是空的。
使用这些 PyQt5 布局真正酷的地方在于,一旦我们将小部件放置到这些布局中,我们可以通过移动布局来移动整个小部件组。我们甚至可以隐藏它们,使它们变得不可见!
在 步骤 4 中,我们确保我们知道在设计师中做更改并将其转换为 Python 代码的过程。如果运行代码不成功,可能出了点问题。做好小改动然后测试您的代码是个好习惯。
在设计师中添加按钮和标签
在本食谱中,我们将添加一个按钮和一个标签,我们将把它们放在我们在前一个食谱中添加到 UI 设计的布局中。
准备工作
你需要从上一个食谱中获取.ui代码。所有其他先决条件也适用于本食谱。
如何做到这一点...
我们将在我们的 UI 设计中添加一个按钮和一个标签。我们还将使用设计师将它们连接起来,在设计师中直接创建一些可用功能。让我们看看步骤:
-
在设计师中打开
Designer_Second_UI_layout.ui并将其保存为Designer_Second_UI_layout_button.ui。 -
从左侧拖动一个
PushButton到下方的水平布局中:

注意按钮如何自动调整自身以适应水平布局的左右边缘。
- 保存并预览 UI。在 UI 预览运行期间点击按钮:

当点击它时,按钮看起来被按下,并带有我们可以更改的默认名称。
- 从左侧拖动一个标签小部件并将其放置到顶部的水平布局中:

- 保存
.ui设计并预览它:

- 接下来,我们将
PushButton连接到设计师内部的标签。
按下F4键进入信号/槽编辑模式或使用设计师中的编辑菜单。从按钮拖动一个信号/槽连接到标签。现在设计师将看起来像这样:

- 在配置连接弹出对话框中,点击左上角的
clicked()。这将启用对话框的右侧。选择clear()方法:

- 点击确定并保存
.ui文件。现在你将看到按钮和标签之间的连接:

-
预览
.ui代码并点击按钮。你会注意到标签文本被清除了。 -
使用
pyuic5.exe将.ui代码转换为 Python 代码,就像我们之前做的那样。
让我们看看转换后的代码,以了解按钮连接到清除标签功能的工作原理。
它是如何工作的...
在步骤 1中,我们正在以新名称保存 UI 设计,而在步骤 2中,我们正在将PushButton拖放到MainWindow表单上。这个按钮本身并不做任何事情,但在设计师中还有一些方法可以添加功能。
在步骤 3中,我们正在预览 UI。请注意,按钮自动拉伸以填充整个水平布局框。垂直方向上,它的高度仅足以显示其默认文本。这些是我们可以在属性编辑器中更改的属性。
在 第 4 步 中,我们将文本标签小部件可视地拖放到 MainWindow 中。这个小部件不会附着到水平布局的左右两侧,因为它自带一个预定义的宽度。
它会自动附着到左侧。在属性编辑器中,如果我们愿意,我们可以更改它。
第 5 步 给我们提供了一个从设计器内部运行的用户界面预览。
第 6 步 带我们进入信号/槽编辑器。信号和槽是 PyQt5 的特性。以下截图显示了如何启用此编辑器:

您可以在视图菜单下找到编辑器。这将在属性编辑器右侧打开一个新窗口:

您可以看到,pushButton 对象是发送者,信号是 clicked(),接收者是 label,槽是 clear()。
第 7 步 展示了当我们连接 PushButton 和 Label 小部件之间的信号和槽时,可用的功能。clear() 方法是内置的,因此我们可以简单地选择它,以便在按下按钮时清除标签。
在 第 8 步 中,我们可以看到在点击确定连接两个小部件后,编辑器发生了变化。
由于功能是在设计器内添加的,当我们预览我们的 UI 时,它实际上是可以工作的。点击按钮确实会清除标签,如 第 9 步 所示。
当我们将 .ui 代码转换为 Python 代码时,我们得到以下输出:

注意 pushButton 有与 clicked.connect(<method>) 相同的语法,这是我们在外部设计器中启用退出菜单项时所拥有的。
更多...
PyQt5 GUI 框架是一个非常令人兴奋的工具,可以与之一起工作。我特别喜欢 Qt 设计器工具。
与 Packt 合作,我创建了几部 使用 PyQt5 的 Python GUI 编程食谱 视频课程。
其中一门课程特别关注 PyQt5 GUI 开发。它大约有四个小时长。
这是我课程的截图,可以在www.packtpub.com/application-development/python-gui-programming-recipes-using-pyqt5-video找到:

如果这对你来说太长了(我在这门课程中涵盖了大量的 PyQt5 内容),还有一个更短的课程,专注于 tkinter 和 PyQt5,可以在www.packtpub.com/application-development/hands-python-3x-gui-programming-video找到:

我希望
你在软件开发的努力中 一切顺利
。Python 是一种
很棒 编程语言。
第十一章:最佳实践
在本章中,我们将探讨不同的 最佳实践,这些实践可以帮助我们高效地构建 GUI,并使其既 可维护 又 可扩展。
这些最佳实践还将帮助你调试 GUI,使其达到你想要的样子。
这里是本章 Python 模块的概述:

了解如何使用最佳实践将大大提高你的 Python 编程技能。
本章将要讨论的食谱如下:
-
-
避免使用 spaghetti 代码
-
使用
__init__连接模块 -
混合 fall-down 和 OOP 编码
-
使用代码命名约定
-
当不使用 OOP 时
-
如何成功使用设计模式
-
避免复杂性
-
使用多个笔记本进行 GUI 设计
-
避免使用 spaghetti 代码
在这个菜谱中,我们将探讨创建 spaghetti 代码的典型方法,然后我们将看到避免此类代码的更好方法。
spaghetti 代码是功能很多且相互交织的代码。
准备工作
我们将创建一个新的、简单的 GUI,使用 Python 内置的 tkinter 库编写。
如何操作...
在网上搜索并阅读文档后,我们可能会先编写以下代码来创建我们的 GUI:
-
创建一个新的模块:
GUI_Spaghetti.py。 -
添加以下代码:
# 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_NOT_Spaghetti.py,并添加以下代码:
#======================
# 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()
- 运行代码并观察以下输出:

让我们深入了解代码以更好地理解它。
它是如何工作的...
当 spaghetti 代码创建了一个 GUI 时,它非常难以阅读,因为代码中存在太多的混乱。良好的代码与 spaghetti 代码相比有很多优势。
让我们先看看 spaghetti 代码的一个例子:
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
良好的代码有一个清晰的注释部分。我们可以轻松地找到导入语句:
#-----------------------------------
考虑以下 spaghetti 代码:
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)
如前所述的代码块所示,良好的代码有一个自然的流程,遵循小部件在主 GUI 表单中的布局方式。
在 spaghetti 代码中,底部的 LabelFrame 在顶部的 LabelFrame 之前创建,并且与一个 import 语句和一些小部件创建混合在一起:
#-----------------------------------
以下是一个 spaghetti 代码的示例,展示了这一特性:
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)
良好的代码不包含不必要的变量赋值,也没有一个 PRINTME 函数,它不会进行我们阅读代码时可能期望的调试:
#-----------------------------------
以下代码块列举了这一方面。
这里是 spaghetti 代码:
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
这里是良好的代码:
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
良好的代码不应包含任何提到的意大利面代码实例。
import语句仅导入所需的模块,并且它们不会在代码中杂乱无章。也没有重复的import语句。没有import *语句:
#-----------------------------------
以下代码块列举了这一方面。
这就是意大利面代码:
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')
良好的代码,如前例所示,与意大利面代码相比,具有相当有意义的变量名。没有使用数字1而不是True的不必要的if语句。它还有良好的缩进,使代码更容易阅读。
在GUI_NOT_Spaghetti.py中,我们没有丢失预期的窗口标题,并且我们的复选框最终位于正确的位置。我们还使包围复选框的LabelFrame可见。
在GUI_Spaghetti.py中,我们既丢失了窗口标题,也没有显示顶部的LabelFrame。复选框最终位于错误的位置。
使用 init 连接模块
当我们使用 Eclipse IDE 的 PyDev 插件创建新的 Python 包时,它会自动创建一个__init__.py模块。如果我们不使用 Eclipse,我们也可以手动创建它。
__init__.py模块通常是空的,然后其大小为 0 KB。
我们可以使用这个通常为空的模块,通过在其中输入代码来连接不同的 Python 模块。这个菜谱将展示如何做到这一点。
准备工作
我们将创建一个新的 GUI,类似于我们在之前的菜谱中创建的,避免意大利面代码。
如何做…
随着我们的项目越来越大,我们自然会将其分解成几个 Python 模块。有时,找到位于不同子目录中的模块可能会很复杂,无论是位于需要导入它的代码之上还是之下。
让我们按顺序看看这个菜谱:
-
创建一个空文件并将其保存为
__init__.py。 -
看看它的大小:

- 创建一个新的模块,
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()
- 运行代码并点击
Click Me按钮:

-
在您运行 Python 模块的目录下创建三个子目录。
-
将它们命名为
Folder1、Folder2和Folder3:

-
在
Folder3中创建一个新的模块:MessageBox.py。 -
添加以下代码:
from tkinter import messagebox
def clickMe():
messagebox.showinfo('Imported Message Box', 'Hi from Level 3')
-
打开
GUI__init.py并将其保存为GUI__init_import_folder.py。 -
添加以下导入:
from Ch11_Code.Folder1.Folder2.Folder3.MessageBox import clickMe
- 注释掉或删除
clickMe函数:
# def clickMe(): # commented out
# from tkinter import messagebox
# messagebox.showinfo('Message Box', 'Hi from same Level.')
- 在您的开发环境中运行代码并观察输出:

- 打开命令提示符并尝试运行它。如果运行代码失败,您可以看到以下输出:

-
打开
__init__.py。 -
将以下代码添加到
__init__.py模块中:
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
while True:
curFull = getcwd()
curDir = curFull.split('\\')[-1]
if 'Ch11_Code' == curDir:
addsitedir(curFull)
addsitedir(curFull + 'Folder1\Folder2\Folder3')
break
chdir(pardir)
pprint(path)
-
打开
GUI__init_import_folder.py并将其保存为GUI__init_import_folder_directly.py。 -
添加以下两个导入语句并注释掉之前的导入:
# from Ch11_Code.Folder1.Folder2.Folder3.MessageBox import clickMe # comment out
import __init__
from MessageBox import clickMe
- 在命令提示符中运行代码:

让我们深入了解代码,以便更好地理解它。
它是如何工作的…
当我们创建一个 __init__.py 模块时,它通常是空的,文件大小为 0 KB。
__init__.py 模块与 Python 类的 __init__(self): 方法不同。
在 GUI__init.py 中,我们创建了以下函数,该函数导入 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() 函数。
我们可以使用 Python 的相对导入语法:
from Ch11_Code.Folder1.Folder2.Folder3.MessageBox import clickMe
在前面的代码中,路径是硬编码的。如果我们删除 Folder2,它将不再工作。
在 GUI__init_import_folder.py 中,我们删除了本地的 clickMe() 函数,现在我们的回调函数预期将使用导入的 clickMe() 函数。这适用于在 Eclipse 和其他将 PYTHONPATH 设置为开发代码的项目路径的 IDE 中。
它可能或可能不会在命令提示符中工作,这取决于你是否已将 PYTHONPATH 设置为 Ch11_Code\Folder1\Folder2\Folder3 文件夹的根目录。
为了解决这个错误,我们可以在 __init__.py 模块中初始化我们的 Python 搜索路径。这通常可以解决相对导入错误。
在 GUI__init_import_folder_directly.py 模块中,我们不再需要指定完整的文件夹路径。我们可以直接导入模块及其函数。
我们必须显式导入 __init__ 才能使此代码工作。
这个配方展示了在遇到这种挑战时的几个故障排除方法。
混合逐级下降和面向对象编程(OOP)编码
Python 是一种面向对象编程语言,但并不总是使用面向对象编程有意义。对于简单的脚本任务,传统的瀑布式编码风格仍然适用。
在这个配方中,我们将创建一个新的 GUI,它混合了逐级下降编码风格和更现代的面向对象编程风格。
我们将创建一个面向对象风格的类,当我们将鼠标悬停在 Python GUI 中的小部件上时,它将显示一个工具提示,我们将使用瀑布风格创建它。
逐级下降和瀑布式编码风格是相同的。这意味着在我们从下面的代码中调用它之前,我们必须在物理上将代码放置在代码之上。在这个范例中,当我们执行代码时,代码实际上是从程序顶部逐级下降到程序底部的。
准备工作
在这个菜谱中,我们将使用 tkinter 创建一个 GUI,这与本书第一章中创建的 GUI 类似。
如何做到这一点...
让我们看看如何执行这个菜谱:
-
我们将首先以过程式的方式使用
tkinter创建一个 GUI,然后我们将向其中添加一个类来显示 GUI 小部件的提示信息。 -
接下来,我们将创建一个新的模块:
GUI_FallDown.py。 -
添加以下代码:
#======================
# 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_FallDown_Tooltip.py。 -
使用
GUI_FallDown.py中的代码,然后在顶部添加以下代码:
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
#===================================================================
# Add this code at the top
class ToolTip(object):
def __init__(self, widget, tip_text=None):
self.widget = widget
self.tip_text = tip_text
widget.bind('<Enter>', self.mouse_enter)
widget.bind('<Leave>', self.mouse_leave)
def mouse_enter(self, _event):
self.show_tooltip()
def mouse_leave(self, _event):
self.hide_tooltip()
def show_tooltip(self):
if self.tip_text:
x_left = self.widget.winfo_rootx()
# get widget top-left coordinates
y_top = self.widget.winfo_rooty() - 18
# place tooltip above widget
self.tip_window = tk.Toplevel(self.widget)
self.tip_window.overrideredirect(True)
self.tip_window.geometry("+%d+%d" % (x_left, y_top))
label = tk.Label(self.tip_window, text=self.tip_text,
justify=tk.LEFT, background="#ffffe0",
relief=tk.SOLID, borderwidth=1,
font=("tahoma", "8", "normal"))
label.pack(ipadx=1)
def hide_tooltip(self):
if self.tip_window:
self.tip_window.destroy()
#====================================
# ...
# Add this code at the bottom
# Add Tooltips to widgets
ToolTip(nameEntries[idx], 'This is an Entry widget.')
ToolTip(numberEntries[idx], 'This is a DropDown widget.')
ToolTip(buttons[idx], 'This is a Button widget.')
#======================
# Start GUI
#======================
win.mainloop()
- 运行代码并将鼠标悬停在几个小部件上:

让我们深入了解代码以更好地理解它。
它是如何工作的...
首先,我们在 GUI_FallDown.py 中使用 tkinter 创建一个 Python GUI,并以瀑布式编写代码。
我们可以通过添加提示信息来改进我们的 Python GUI。最好的方法是隔离创建提示功能代码与我们的 GUI。
我们通过创建一个具有提示功能的不同类来实现这一点,然后在创建 GUI 的同一个 Python 模块中创建这个类的实例。
使用 Python,我们不需要将我们的 ToolTip 类放入一个单独的模块中。我们可以在过程式代码的上方放置它,然后从类代码下方调用它。
在 GUI_FallDown_Tooltip.py 中,代码几乎与 GUI_FallDown.py 相同,但现在我们有了提示信息。
我们可以非常容易地在同一个 Python 模块中混合使用过程式和面向对象编程。
使用代码命名约定
这个菜谱将展示遵循代码命名方案的价值:它帮助我们找到想要扩展的代码,并提醒我们程序的设计。
准备工作
在这个菜谱中,我们将查看 Python 模块名称,并探讨良好的命名约定。
如何做到这一点...
我们将创建具有不同 Python 模块名称的示例项目以比较命名:
-
在
Folder1下创建一个新的ModuleNames文件夹。 -
添加以下 Python 模块:1、11、2 和 3:

-
接下来,在
Folder1下创建一个新的ModuleNames_文件夹。 -
添加以下 Python 模块:1、11、2 和 3:

-
接下来,在
Folder1下创建一个新的ModuleNames_Desc文件夹。 -
添加以下 Python 模块:
Logger、UnitTests、GUI和debug:

- 将此命名约定作为一个例子:

让我们深入了解代码以更好地理解它。
它是如何工作的...
在 步骤 1 中,我们创建一个名为 ModuleNames 的包子文件夹。
在 步骤 2 中,我们将 Python 模块添加到其中。
在 步骤 3 中,我们创建另一个包文件夹,并在名称后添加一个尾随下划线:ModuleNames_。
在步骤 4中,我们添加了与步骤 2中相同的名称的新 Python 模块。
在步骤 5中,我们创建了一个具有更多描述性名称的另一个包文件夹,名为ModuleNames_Desc。
在步骤 6中,我们添加了 Python 模块,但这次使用了更具描述性的名称,这些名称解释了每个 Python 模块的目的。
最后,在步骤 7中,我们展示了如何完整地展示这一点。
还有更多...
通常,开始编码的一种典型方式是通过递增数字,正如在ModuleNames中可以看到的那样。
后来,当我们回到这段代码时,我们并不清楚哪个 Python 模块提供了哪种功能,有时,我们最后递增的模块并不如早期版本好。
清晰的命名约定确实有帮助。
稍微改进一下,添加下划线可以使模块名称更易读,例如ModuleNames_。
更好的方法是添加一些关于模块做什么的描述,就像在ModuleNames_Desc中看到的那样。
虽然不完美,但为不同的 Python 模块选择的名称表明了每个模块的责任。当我们想要添加更多单元测试时,它们位于哪个模块中是清晰的。
在最后一个例子中,我们使用占位符PRODUCT来表示一个真实名称。
将单词PRODUCT替换为你目前正在工作的产品名称。
整个应用程序是一个 GUI。所有部分都是连接的。DEBUG.py模块仅用于调试代码。与所有其他模块相比,调用 GUI 的主要模块的名称是反转的。它以Gui开头,并具有.pyw扩展名。
这是唯一具有这种扩展名的 Python 模块。
从这种命名约定来看,如果你对 Python 足够熟悉,那么很明显,要运行这个 GUI,你可以双击Gui_PRODUCT.pyw模块。
所有其他 Python 模块都包含用于 GUI 的功能,并且执行底层业务逻辑以满足 GUI 所解决的问题。
Python 代码模块的命名约定对我们保持高效和帮助我们记住原始设计非常有帮助。当我们需要调试和修复缺陷或添加新功能时,它们是我们首先查看的资源。
通过数字递增模块名称并不很有意义,并且最终会浪费开发时间。
另一方面,Python 变量的命名更自由。Python 推断类型,因此我们不必指定变量将是list类型(它可能不是,或者稍后代码中,它可能成为不同的类型)。
变量命名的良好建议是使它们具有描述性,并且最好不要过度缩写。
如果我们想要指出某个变量被设计为list类型,那么使用完整的单词list_of_things而不是lst_of_things会更加直观。同样,使用number而不是num。
虽然给变量起非常描述性的名字是个好主意,但有时名字可能会太长。在苹果的 Objective-C 语言中,一些变量和函数名非常极端:thisIsAMethodThatDoesThisAndThatAndAlsoThatIfYouPassInNIntegers:1:2:3。
在命名变量、方法和函数时使用常识。
现在,让我们继续下一个示例。
何时不使用 OOP
Python 自带了面向对象编程(OOP)的能力,但与此同时,我们也可以编写不需要使用 OOP 的脚本。对于某些任务,使用 OOP 并不合适。
这个示例将向我们展示何时不使用 OOP。
准备工作
在这个示例中,我们将创建一个类似于之前示例的 Python GUI。我们将比较 OOP 代码和非 OOP 编程 GUI 的替代方法。结果输出将相同,但两个版本的代码略有不同。
如何做到这一点…
让我们看看如何执行这个示例:
-
首先,让我们使用面向对象的方法创建一个新的 GUI。以下步骤中显示的代码将创建显示的 GUI。
-
创建一个新的模块:
GUI_OOP.py。 -
添加以下代码:
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=' Mighty 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_NOT_OOP.py,并添加以下代码:
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=' Mighty 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()
- 现在,创建整个 GUI,调用创建小部件的函数:
#======================
win = tk.Tk()
win.title("Python GUI")
createWidgets()
win.mainloop()
- 运行代码。生成的 GUI 将与之前显示的
GUI_OOP.py中的 GUI 相同。
它是如何工作的…
首先,我们以 OOP 风格创建一个 Python tkinter GUI,GUI_OOP.py。然后,我们以过程式风格创建相同的 GUI,GUI_NOT_OOP.py。
我们可以通过稍微重构我们的代码来实现不使用 OOP 方法达到相同的 GUI。首先,我们移除OOP类及其__init__方法。接下来,我们将所有方法移动到左边,并移除self类引用,使它们变成未绑定函数。我们还移除了之前代码中所有的self引用。然后,我们将createWidgets函数调用移动到函数声明点下方。我们将其放置在mainloop调用之上。
最后,我们达到了相同的 GUI,但没有使用 OOP。
Python 使我们能够在合适的时候使用 OOP。其他语言,如 Java 和 C#,强制我们始终使用 OOP 方法进行编码。在这个示例中,我们探讨了不使用 OOP 的情况。
如果代码库增长,OOP 方法将更具可扩展性,但如果确定只需要这段代码,那么就没有必要通过 OOP。
现在,让我们继续下一个示例。
如何成功使用设计模式
在这个配方中,我们将通过使用工厂设计模式来创建我们的 Python GUI 的小部件。在之前的配方中,我们手动逐个创建小部件或动态地在循环中创建。使用工厂设计模式,我们将使用工厂来创建我们的小部件。
准备工作
我们将创建一个具有三个按钮的 Python GUI,每个按钮都有不同的样式。
如何操作...
我们将创建一个具有不同按钮样式的 Python GUI,我们将使用工厂设计模式来创建这些不同的样式:
-
创建一个新的模块:
GUI_DesignPattern.py。 -
添加以下代码:
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()
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)
scr = scrolledtext.ScrolledText(self.monty, width=30,
height=3, wrap=tk.WORD)
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)
self.createButtons()
def createButtons(self):
factory = ButtonFactory() # <-- create the factory
# 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)
#==========================
oop = OOP()
oop.win.mainloop()
- 运行代码并观察输出:

让我们深入了解代码以更好地理解它。
它是如何工作的...
我们创建了一个基类,我们的不同按钮样式类都从它继承,并且每个类都覆盖了relief和foreground配置属性。所有子类都从该基类继承getButtonConfig方法。此方法返回一个元组。
我们还创建了一个按钮工厂类和一个包含我们的按钮子类名称的列表。我们把这个列表命名为buttonTypes,因为我们的工厂将创建不同类型的按钮。
在模块的更下方,我们使用相同的buttonTypes列表创建了按钮小部件。我们创建了一个按钮工厂的实例,然后我们使用我们的工厂来创建我们的按钮。
buttonTypes列表中的项是我们子类的名称。
我们调用createButton方法,然后立即调用基类的getButtonConfig方法,并使用点符号获取配置属性。
我们可以看到,我们的 Python GUI 工厂确实创建了不同的按钮,每个按钮都有不同的样式。它们在文本颜色和relief属性上有所不同。
设计模式是我们软件开发工具箱中一个非常激动人心的工具。
避免复杂性
在这个配方中,我们将扩展我们的 Python GUI,并学习处理软件开发努力不断增加的复杂性。
我们的同事和客户都喜欢我们在 Python 中创建的 GUI,并要求添加更多功能到我们的 GUI 中。
这增加了复杂性,并可能轻易破坏我们的原始良好设计。
准备工作
我们将创建一个新的 Python GUI,类似于之前的配方,并将添加许多以小部件形式的功能。
如何操作...
让我们看看如何执行配方:
-
我们将从一个具有两个标签页的 Python GUI 开始,然后我们将添加更多的小部件到它。
-
创建一个新的模块:
GUI_Complexity_start.py。 -
添加以下代码:
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu
from tkinter import Spinbox
from Ch11_Code.ToolTip import ToolTip
- 创建一个全局变量和一个类:
GLOBAL_CONST = 42
#===================================================================
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())
# Button callback Clear Text
def clearScrol(self):
self.scr.delete('1.0', tk.END)
# Spinbox callback
def _spin(self):
value = self.spin.get()
print(value)
self.scr.insert(tk.INSERT, value + '\n')
# Checkbox callback
def checkCallback(self, *ignoredArgs):
# only enable one checkbutton
if self.chVarUn.get():
self.check3.configure(state='disabled')
else: self.check3.configure(state='normal')
if self.chVarEn.get():
self.check2.configure(state='disabled')
else: self.check2.configure(state='normal')
# 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')
# Exit GUI cleanly
def _quit(self):
self.win.quit()
self.win.destroy()
exit()
def usingGlobal(self):
GLOBAL_CONST = 777
print(GLOBAL_CONST)
- 添加一个创建小部件的方法:
#######################################################################
def createWidgets(self):
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) # Add a second tab
tabControl.add(tab2, text='Tab 2') # Make second tab visible
tabControl.pack(expand=1, fill="both") # Pack to make visible
self.monty = ttk.LabelFrame(tab1, text=' Mighty 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!",
command=self.clickMe)
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'] = (1, 2, 4, 42, 100)
numberChosen.grid(column=1, row=1)
numberChosen.current(0)
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)
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)
self.monty2 = ttk.LabelFrame(tab2, text=' Holy Grail ')
self.monty2.grid(column=0, row=0, padx=8, pady=4)
chVarDis = tk.IntVar()
check1 = tk.Checkbutton(self.monty2, text="Disabled",
variable=chVarDis, state='disabled')
check1.select()
check1.grid(column=0, row=0, sticky=tk.W)
self.chVarUn = tk.IntVar()
self.check2 = tk.Checkbutton(self.monty2, text="UnChecked",
variable=self.chVarUn)
self.check2.deselect()
self.check2.grid(column=1, row=0, sticky=tk.W )
self.chVarEn = tk.IntVar()
self.check3 = tk.Checkbutton(self.monty2, text="Toggle",
variable=self.chVarEn)
self.check3.deselect()
self.check3.grid(column=2, row=0, sticky=tk.W)
self.chVarUn.trace('w', lambda unused0, unused1, unused2 :
self.checkCallback())
self.chVarEn.trace('w', lambda unused0, unused1, unused2 :
self.checkCallback())
colors = ["Blue", "Gold", "Red"]
self.radVar = tk.IntVar()
self.radVar.set(99)
for col in range(3):
curRad = 'rad' + str(col)
curRad = tk.Radiobutton(self.monty2, text=colors[col],
variable=self.radVar, value=col, command=self.radCall)
curRad.grid(column=col, row=6, sticky=tk.W, columnspan=3)
ToolTip(curRad, 'This is a Radiobutton control.')
labelsFrame = ttk.LabelFrame(self.monty2,
text=' Labels in a Frame ')
labelsFrame.grid(column=0, row=7)
ttk.Label(labelsFrame, text="Label1").grid(column=0, row=0)
ttk.Label(labelsFrame, text="Label2").grid(column=0, row=1)
for child in labelsFrame.winfo_children():
child.grid_configure(padx=8)
menuBar = Menu(tab1)
self.win.config(menu=menuBar)
fileMenu = Menu(menuBar, tearoff=0)
fileMenu.add_command(label="New")
fileMenu.add_separator()
fileMenu.add_command(label="Exit", command=self._quit)
menuBar.add_cascade(label="File", menu=fileMenu)
helpMenu = Menu(menuBar, tearoff=0)
helpMenu.add_command(label="About")
menuBar.add_cascade(label="Help", menu=helpMenu)
self.win.iconbitmap('pyc.ico')
strData = tk.StringVar()
strData.set('Hello StringVar')
intData = tk.IntVar()
strData = tk.StringVar()
strData = self.spin.get()
self.usingGlobal()
nameEntered.focus()
ToolTip(self.spin, 'This is a Spin control.')
ToolTip(nameEntered, 'This is an Entry control.')
ToolTip(self.action, 'This is a Button control.')
ToolTip(self.scr, 'This is a ScrolledText control.')
#======================
# Start GUI
#======================
oop = OOP()
oop.win.mainloop()
- 运行代码并点击两个标签页:

-
打开
GUI_Complexity_start.py并将其保存为GUI_Complexity_start_add_button.py。 -
将以下代码添加到
createWidgets方法中:
# Adding another Button
self.action = ttk.Button(self.monty, text="Clear Text", command=self.clearScrol)
self.action.grid(column=2, row=2)
- 在
__init__(self)下方添加以下代码:
# 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_Complexity_start_add_button.py并将其保存为GUI_Complexity_start_add_three_more_buttons.py。 -
将以下代码添加到
createWidgets方法中:
# 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_Complexity_start_add_three_more_buttons.py并将其保存为GUI_Complexity_start_add_three_more_buttons_add_more.py。 -
将以下代码添加到
createWidgets方法中:
# Adding more Feature Buttons
startRow = 4
for idx in range(12):
if idx < 2: col = idx
else: col += 1
if not idx % 3:
startRow += 1
col = 0
b = ttk.Button(self.monty, text="Feature " + str(idx+1))
b.grid(column=col, row=startRow)
- 运行代码并观察以下输出:

-
打开
GUI_Complexity_start_add_three_more_buttons_add_more.py并将其保存为GUI_Complexity_end_tab3.py。 -
将以下代码添加到
createWidgets方法中:
# 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: col = idx
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)
- 运行代码并点击标签 3:

让我们深入了解代码,以便更好地理解。
它是如何工作的…
我们从一个使用 tkinter、GUI_Complexity_start.py 构建的 GUI 开始,它有两个标签页上的控件。我们在这整本书中创建了许多类似的 GUI。
我们收到的第一个新功能请求是在标签 1 中添加清除 scrolledtext 小部件的功能。
足够简单。我们只需在标签 1 中添加另一个按钮。
我们还必须在 GUI_Complexity_start_add_button.py 中创建回调方法以添加所需的功能,我们定义这个功能在类的顶部和创建小部件的方法之外。现在,我们的 GUI 有一个新的按钮,当我们点击它时,它会清除 ScrolledText 小部件中的文本。为了添加此功能,我们不得不在同一个 Python 模块中的两个地方添加代码。
我们在 createWidgets 方法中插入了新的按钮,然后创建了一个新的回调方法,当我们的新按钮被点击时,它会调用这个方法。我们将此代码放置在第一个按钮的回调代码下方。
我们下一个功能请求是添加更多功能。业务逻辑封装在另一个 Python 模块中。我们在 GUI_Complexity_start_add_three_more_buttons.py 中的标签 1 中添加了三个更多按钮来调用这个新功能。我们使用循环来完成这个操作。
接下来,我们的客户要求更多功能,我们在 GUI_Complexity_start_add_three_more_buttons_add_more.py 中使用了相同的方法。
这并不太糟糕。当我们收到新的 50 个新功能请求时,我们开始怀疑我们的方法是否仍然是最佳选择。
管理我们 GUI 处理的日益增加的复杂性的方法之一是添加标签页。通过添加更多标签页并将相关功能放入它们自己的标签页中,我们控制了复杂性,并使我们的 GUI 更加直观。我们在 GUI_Complexity_end_tab3.py 中这样做,它创建了我们的新标签 3。
我们看到如何通过将 GUI 模块化,将大特性分解成小块,并使用标签在功能相关区域中排列,来处理复杂性。
虽然复杂性有许多方面,但模块化和重构代码通常是处理软件代码复杂性的非常有效的方法。
使用多个笔记本进行 GUI 设计
在这个食谱中,我们将使用多个笔记本创建我们的 GUI。令人惊讶的是,tkinter没有自带这种功能,但我们可以自己设计这样的小部件。
使用多个笔记本将进一步减少前一个食谱中讨论的复杂性。
准备中
我们将创建一个新的 Python GUI,类似于前一个食谱中的 GUI。然而,这次我们将使用两个笔记本来设计我们的 GUI。为了专注于这个特性,我们将使用函数而不是类方法。阅读前一个食谱将是对这个食谱的良好介绍。
如何操作...
让我们看看如何执行这个食谱:
-
要在同一个 GUI 中使用多个笔记本,我们首先创建两个框架。第一个框架将包含笔记本及其标签页,而第二个框架将作为每个标签页设计的控件显示区域。
-
创建一个新的模块:
GUI_Complexity_end_tab3_multiple_notebooks.py。 -
添加以下代码:
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu
from tkinter import Spinbox
from tkinter.messagebox import showinfo
- 创建回调函数:
def clickMe(button, name, number):
button.configure(text='Hello {} {}'.format(name.get(),
number.get()))
def clearScrol(scr):
scr.delete('1.0', tk.END)
def _spin(spin, scr):
value = spin.get()
print(value)
scr.insert(tk.INSERT, value + '\n')
def checkCallback(*ignoredArgs):
pass
#------------------------------------------
def create_display_area():
# add empty label for spacing
display_area_label = tk.Label(display_area, text="", height=2)
display_area_label.grid(column=0, row=0)
#------------------------------------------
def clear_display_area():
# remove previous widget(s) from display_area:
for widget in display_area.grid_slaves():
if int(widget.grid_info()["row"]) == 0:
widget.grid_forget()
#------------------------------------------
def _quit():
win.quit()
win.destroy()
exit()
- 创建菜单栏:
def create_menu():
menuBar = Menu(win_frame_multi_row_tabs)
win.config(menu=menuBar)
fileMenu = Menu(menuBar, tearoff=0)
fileMenu.add_command(label="New")
fileMenu.add_separator()
fileMenu.add_command(label="Exit", command=_quit)
menuBar.add_cascade(label="File", menu=fileMenu)
helpMenu = Menu(menuBar, tearoff=0)
helpMenu.add_command(label="About")
menuBar.add_cascade(label="Help", menu=helpMenu)
- 创建标签显示区域 1:
def display_tab1():
monty = ttk.LabelFrame(display_area, text=' Mighty 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')
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'] = (1, 2, 4, 42, 100)
numberChosen.grid(column=1, row=1)
numberChosen.current(0)
action = ttk.Button(monty, text="Click Me!",
command= lambda: clickMe(action, name, number))
action.grid(column=2, row=1)
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)
spin = Spinbox(monty, values=(1, 2, 4, 42, 100), width=5, bd=8,
command= lambda: _spin(spin, scr))
spin.grid(column=0, row=2, sticky='W')
clear = ttk.Button(monty, text="Clear Text", command= lambda:
clearScrol(scr))
clear.grid(column=2, row=2)
startRow = 4
for idx in range(12):
if idx < 2:col = idx
else: col += 1
if not idx % 3:
startRow += 1
col = 0
b = ttk.Button(monty, text="Feature " + str(idx+1))
b.grid(column=col, row=startRow)
- 创建标签显示区域 2:
def display_tab2():
monty2 = ttk.LabelFrame(display_area, text=' Holy Grail ')
monty2.grid(column=0, row=0, padx=8, pady=4)
chVarDis = tk.IntVar()
check1 = tk.Checkbutton(monty2, text="Disabled",
variable=chVarDis, state='disabled')
check1.select()
check1.grid(column=0, row=0, sticky=tk.W)
chVarUn = tk.IntVar()
check2 = tk.Checkbutton(monty2, text="UnChecked",
variable=chVarUn)
check2.deselect()
check2.grid(column=1, row=0, sticky=tk.W )
chVarEn = tk.IntVar()
check3 = tk.Checkbutton(monty2, text="Toggle",
variable=chVarEn)
check3.deselect()
check3.grid(column=2, row=0, sticky=tk.W)
labelsFrame = ttk.LabelFrame(monty2,
text=' Labels in a Frame ')
labelsFrame.grid(column=0, row=7)
ttk.Label(labelsFrame, text="Label1").grid(column=0, row=0)
ttk.Label(labelsFrame, text="Label2").grid(column=0, row=1)
for child in labelsFrame.winfo_children():
child.grid_configure(padx=8)
- 创建标签显示区域 3:
def display_tab3():
monty3 = ttk.LabelFrame(display_area, text=' New Features ')
monty3.grid(column=0, row=0, padx=8, pady=4)
startRow = 4
for idx in range(24):
if idx < 2: col = idx
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)
for child in monty3.winfo_children():
child.grid_configure(padx=8)
- 编写代码以显示所有其他标签的按钮:
def display_button(active_notebook, tab_no):
btn = ttk.Button(display_area, text=active_notebook +' - Tab '+
tab_no, \ command= lambda: showinfo("Tab Display",
"Tab: " + tab_no) )
btn.grid(column=0, row=0, padx=8, pady=8)
- 创建笔记本回调函数:
def notebook_callback(event):
clear_display_area()
current_notebook = str(event.widget)
tab_no = str(event.widget.index("current") + 1)
if current_notebook.endswith('notebook'):
active_notebook = 'Notebook 1'
elif current_notebook.endswith('notebook2'):
active_notebook = 'Notebook 2'
else:
active_notebook = ''
if active_notebook is 'Notebook 1':
if tab_no == '1': display_tab1()
elif tab_no == '2': display_tab2()
elif tab_no == '3': display_tab3()
else: display_button(active_notebook, tab_no)
else:
display_button(active_notebook, tab_no)
- 使用多个笔记本创建 GUI:
win = tk.Tk() # Create instance
win.title("Python GUI") # Add title
#------------------------------------------
win_frame_multi_row_tabs = ttk.Frame(win)
win_frame_multi_row_tabs.grid(column=0, row=0, sticky='W')
display_area = ttk.Labelframe(win, text=' Tab Display Area ')
display_area.grid(column=0, row=1, sticky='WE')
note1 = ttk.Notebook(win_frame_multi_row_tabs)
note1.grid(column=0, row=0)
note2 = ttk.Notebook(win_frame_multi_row_tabs)
note2.grid(column=0, row=1)
# create and add tabs to Notebooks
for tab_no in range(5):
tab1 = ttk.Frame(note1, width=0, height=0)
# Create a tab for notebook 1
tab2 = ttk.Frame(note2, width=0, height=0)
# Create a tab for notebook 2
note1.add(tab1, text=' Tab {} '.format(tab_no + 1))
# Add tab notebook 1
note2.add(tab2, text=' Tab {} '.format(tab_no + 1))
# Add tab notebook 2
# bind click-events to Notebooks
note1.bind("<ButtonRelease-1>", notebook_callback)
note2.bind("<ButtonRelease-1>", notebook_callback)
create_display_area()
create_menu()
display_tab1()
#-------------
win.mainloop()
#-------------
- 运行代码,点击第 1 个标签页,并观察以下输出:

- 点击第 2 个标签页。您将看到以下输出:

- 点击第 3 个标签页。您将看到以下输出:

- 在第二行的第 4 个标签页上点击,并观察以下输出:

- 在第一行的第 5 个标签页上点击,然后点击标签显示区域中的按钮,您将看到以下输出:

让我们幕后了解代码,以便更好地理解。
它是如何工作的...
在GUI_Complexity_end_tab3_multiple_notebooks.py中,我们使用网格布局管理器来安排我们创建的两个框架,将一个放置在另一个之上。然后,我们创建两个笔记本并将它们安排在第一个框架内:

接下来,我们使用循环创建五个标签页并将它们添加到每个笔记本中:

我们创建了一个回调函数并将两个笔记本的点击事件绑定到这个回调函数。现在,当用户点击属于这两个笔记本的任何标签时,这个回调函数将被调用:

在回调函数中,我们添加逻辑来决定点击标签后显示哪些控件:

我们添加了一个创建显示区域的函数和另一个清除区域的函数:

注意notebook_callback()函数是如何调用clear_display_area()函数的。
clear_display_area()函数知道小部件在标签中创建的行和列,通过找到行 0,我们可以使用grid_forget()来清除显示。
对于第一个笔记本的 1 到 3 标签,我们创建新的框架来容纳更多的小部件。点击这三个标签中的任何一个,然后结果会是一个与我们在上一个菜谱中创建的 GUI 非常相似的 GUI。
这前三个标签在回调函数中被调用为display_tab1()、display_tab2()和display_tab3(),当点击这些标签时。
这里是点击第一个笔记本的标签 3 时运行的代码:

点击第一个笔记本的前三个标签之外的任何标签都会调用相同的函数,即display_button(),这会导致显示一个按钮,其文本属性被设置为显示笔记本和标签编号:

点击这些按钮中的任何一个都会弹出一个消息框。
在代码的末尾,我们调用了display_tab1()函数。当 GUI 首次启动时,这个标签的小部件会显示在显示区域中:

运行这个菜谱的GUI_Complexity_end_tab3_multiple_notebooks.py代码会创建以下 GUI:

点击第一个笔记本的标签 2 会清除标签显示区域,然后显示display_tab2()函数创建的小部件:

注意标签显示区域是如何自动调整到创建的小部件的大小。
点击标签 3 会导致以下 GUI 显示:

点击第一个或第二个笔记本中的任何其他标签都会在标签显示区域中显示一个按钮:

点击这些按钮中的任何一个都会弹出一个消息框:

创建笔记本没有限制。我们可以创建我们设计所需的任意数量的笔记本。


浙公网安备 33010602011771号