QT5-Python-GUI-编程秘籍-全-

QT5 Python GUI 编程秘籍(全)

原文:zh.annas-archive.org/md5/261ce8c146dbb5cb015ee100187951c5

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在这本书中,我们将使用 Python 编程语言探索图形用户界面(GUIs)的美丽世界。

在这个过程中,我们将与网络、队列、OpenGL 图形库以及许多其他技术进行交流。

这是一本编程食谱书。每一章都是独立的,并解释了某种编程解决方案。

我们将从非常简单的地方开始,然而在这本书的整个过程中,我们将构建一个用 Python 3 编写的可运行程序。

我们也将在这本书中应用一些设计模式并使用最佳实践。

本书假设读者具备一些使用 Python 编程语言的基本经验,但实际上使用本书并不需要这种经验。

如果你是一位任何编程语言的资深程序员,你将会有趣地扩展你的技能,使用 Python 来编程 GUI!

你准备好了吗?

让我们开始我们的旅程……

本书涵盖的内容

第一章, 创建 GUI 表单和添加小部件,解释了开发我们第一个 Python GUI 的步骤。我们将从构建一个运行中的 GUI 应用程序所需的最少代码开始。然后,每个菜谱都会向 GUI 表单添加不同的控件。

第二章,布局管理,探讨了如何排列小部件以创建我们的 Python GUI。网格布局管理器是 tkinter 内置的最重要的布局工具之一,我们将使用它。

第三章, 外观和感觉定制,展示了如何创建良好的“外观和感觉”GUI 的几个示例。在实用层面上,我们将为我们在一道菜谱中创建的帮助 | 关于菜单项添加功能。

第四章, 数据和类,讨论了保存我们的 GUI 显示的数据。我们将开始使用面向对象编程(OOP)来扩展 Python 的内置功能。

第五章, Matplotlib 图表,解释了如何创建美观的图表来直观地表示数据。根据数据源格式,我们可以在同一图表中绘制一个或多个数据列。

第六章,线程和网络,解释了如何使用线程、队列和网络连接来扩展我们 Python GUI 的功能。这将表明我们的 GUI 并不局限于我们 PC 的本地范围。

第七章, 通过我们的 GUI 在我们的 MySQL 数据库中存储数据,展示了如何连接到 MySQL 数据库服务器。本章的第一个菜谱将展示如何安装免费的 MySQL Server Community Edition,在接下来的菜谱中,我们将创建数据库、表,并将数据加载到这些表中,同时修改这些数据。我们还将从 MySQL 服务器读取数据回我们的 GUI。

第八章, 《国际化与测试》展示了如何通过在标签、按钮、选项卡和其他小部件上显示不同语言文本来国际化我们的图形用户界面。我们将从简单开始,然后探讨如何在设计层面为我们的 GUI 准备国际化。我们还将探讨几种使用 Python 内置的单元测试框架自动测试我们的 GUI 的方法。

第九章, 《使用 wxPython 库扩展我们的 GUI》介绍了另一个目前不随 Python 一起提供的 Python GUI 工具包。它被称为 wxPython,我们将使用与 Python 3 兼容性良好的 Phoenix 版本的 wxPython。

第十章,使用 PyOpenGL 和 PyGLet 创建惊人的 3D GUIs,展示了如何通过赋予它真正的三维能力来转换我们的 GUI。我们将使用两个 Python 第三方包。PyOpenGL 是 OpenGL 标准的 Python 绑定,这是一个所有主要操作系统都内置的图形库。这使得生成的控件具有原生外观和感觉。PyGLet 就是这样一种绑定,我们将在本章中对其进行探讨。

第十一章,最佳实践,探讨了不同的最佳实践,这些实践可以帮助我们以高效的方式构建我们的 GUI,并保持其可维护性和可扩展性。最佳实践适用于任何优秀的代码,我们的 GUI 在设计和实施良好的软件实践方面也不例外。

你需要这本书的内容

本书所需的所有软件均可在网上免费获取。这包括 Python 3 本身,以及 Python 的附加模块。为了下载任何所需的软件,您需要一个有效的互联网连接。

这本书面向的对象

这本书是为希望创建图形用户界面(GUI)的程序员所写。您可能会对我们通过使用 Python 编程语言创建美观、实用且强大的 GUI 所能实现的事情感到惊讶。Python 是一种奇妙、直观的编程语言,而且非常容易学习。

我喜欢邀请你们现在就开始这段旅程。这将非常有趣!

习惯用法

在这本书中,您将发现多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名和用户输入如下所示:"使用 Python,我们可以使用class关键字而不是def关键字来创建自己的类。"

代码块设置如下:

import tkinter as tk     # 1
win = tk.Tk()            # 2
win.title("Python GUI")  # 3
win.mainloop()           # 4

任何命令行输入或输出都应按照以下格式编写:

pip install numpy-1.9.2+mkl-cp34-none-win_amd64.whl

新术语重要词汇将以粗体显示。你在屏幕上看到的词汇,例如在菜单或对话框中,将以如下方式显示:“接下来,我们将为菜单项添加功能,例如,点击退出菜单项时关闭主窗口,并显示帮助 | 关于对话框。”

注意事项

警告或重要提示会出现在这样的方框中。

小贴士

小贴士和技巧看起来是这样的。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中获得最大收益的书籍。

要向我们发送一般性反馈,请直接发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果你在某个领域有专业知识,并且对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,链接为www.packtpub.com/authors

客户支持

现在你已经是 Packt 图书的骄傲拥有者了,我们有许多事情可以帮助你从你的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

错误清单

尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下现有的任何勘误列表中。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support并在搜索字段中输入书籍名称。所需信息将显示在勘误部分下。

海盗行为

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过链接发送至 <copyright@packtpub.com> 与我们联系,以提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者以及为我们提供有价值内容的能力方面所提供的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。

第一章:创建 GUI 表单并添加小部件

在本章中,我们开始使用 Python 3 创建令人惊叹的图形用户界面:

  • 创建我们的第一个 Python 图形用户界面

  • 防止 GUI 被调整大小

  • 为 GUI 表单添加标签

  • 创建按钮并更改它们的文本属性

  • 文本框小部件

  • 将焦点设置到小部件并禁用小部件

  • 组合框小部件

  • 创建具有不同初始状态的复选框

  • 使用单选按钮小部件

  • 使用滚动文本小部件

  • 在循环中添加几个小部件

简介

在本章中,我们将开发我们的第一个 Python 图形用户界面(GUI)。我们从构建一个运行中的 GUI 应用程序所需的最少代码开始。然后,每个菜谱都会向 GUI 表单添加不同的控件。

在前两个菜谱中,我们展示了整个代码,它只包含几行代码。在接下来的菜谱中,我们只展示需要添加到前一个菜谱中的代码。

到本章结束时,我们将已经创建了一个包含各种状态的标签、按钮、文本框、组合框和复选按钮的工作 GUI 应用程序,以及可以改变 GUI 背景颜色的单选按钮。

创建我们的第一个 Python 图形用户界面

Python 是一种非常强大的编程语言。它自带内置的 tkinter 模块。只需几行代码(确切地说,是四行)我们就能构建我们的第一个 Python 图形用户界面。

准备就绪

要遵循这个食谱,一个有效的 Python 开发环境是先决条件。Python 附带的 IDLE 图形用户界面足以开始。IDLE 是使用 tkinter 构建的!

注意事项

本书中的所有食谱都是在 Windows 7 64 位操作系统上使用 Python 3.4 开发的。它们在其他配置上尚未经过测试。由于 Python 是一种跨平台语言,因此预计每个食谱中的代码可以在任何地方运行。

如果你使用的是 Mac,它确实内置了 Python,但可能缺少一些模块,例如 tkinter,我们将在整本书中使用这个模块。

我们正在使用 Python 3,Python 的创造者有意选择不使其与 Python 2 兼容。

如果你正在使用 Mac 或 Python 2,你可能需要从www.python.org安装 Python 3,以便成功运行本书中的食谱。

如何做到这一点...

这里是创建结果 GUI 所需的四行 Python 代码:

import tkinter as tk     # 1
win = tk.Tk()            # 2
win.title("Python GUI")  # 3
win.mainloop()           # 4

执行此代码并欣赏结果:

如何做...

它是如何工作的...

在第 1 行,我们导入内置的tkinter模块并将其别名为tk以简化我们的 Python 代码。在第 2 行,我们通过调用其构造函数(Tk后附加的括号将类转换为实例)创建了一个Tk类的实例。我们使用别名tk,这样我们就不必使用较长的单词tkinter。我们将类实例分配给一个名为win的变量(代表窗口)。由于 Python 是一种动态类型语言,我们不需要在分配之前声明这个变量,也不需要给它指定一个特定的类型。Python 从该语句的赋值中推断类型。Python 是一种强类型语言,所以每个变量始终都有一个类型。我们只是不必像在其他语言中那样事先指定它的类型。这使得 Python 成为一种非常强大且高效的编程语言。

注意

关于类和类型的简要说明:

在 Python 中,每个变量始终都有一个类型。我们不能创建一个没有指定类型的变量。然而,在 Python 中,我们不需要事先声明类型,就像在 C 编程语言中那样。

Python 足够智能,可以推断类型。在撰写本文时,C#也具备这种能力。

使用 Python,我们可以通过 class 关键字而不是 def 关键字来创建自己的类。

为了将类分配给一个变量,我们首先需要创建我们类的实例。我们创建这个实例并将这个实例分配给我们的变量。

class AClass(object):
    print('Hello from AClass')

classInstance = AClass()

现在变量 classInstance 的类型是 AClass

如果这听起来很困惑,请不要担心。我们将在接下来的章节中介绍面向对象编程(OOP)。

在第 3 行,我们使用类的实例变量(win)通过title属性给我们的窗口设置标题。在第 4 行,我们通过在类实例win上调用mainloop方法来启动窗口的事件循环。到目前为止,我们的代码创建了一个实例并设置了一个属性,但是 GUI 将不会显示,直到我们启动主事件循环

注意事项

事件循环是使我们的图形用户界面(GUI)工作的机制。我们可以将其想象为一个无限循环,其中我们的 GUI 正在等待事件被发送给它。按钮点击会在我们的 GUI 内创建一个事件,或者我们的 GUI 被调整大小也会创建一个事件。

我们可以预先编写所有的 GUI 代码,直到我们调用这个无限循环(如上面代码所示win.mainloop()),用户屏幕上不会显示任何内容。

事件循环在用户点击红色X按钮或我们编程来结束我们的 GUI 的小部件时结束。当事件循环结束时,我们的 GUI 也随之结束。

还有更多...

这个菜谱使用了最少的 Python 代码来创建我们的第一个 GUI 程序。然而,在这本书中,当有需要时,我们将使用面向对象编程(OOP)。

防止 GUI 被调整大小

准备工作

这个菜谱扩展了之前的版本。因此,有必要你自己将菜谱 1 输入到自己的项目中,或者从www.packtpub.com/support下载代码。

如何做到这一点...

我们正在阻止 GUI 被调整大小。

import tkinter as tk        # 1 imports

win = tk.Tk()               # 2 Create instance
win.title("Python GUI")     # 3 Add a title       

win.resizable(0, 0)         # 4 Disable resizing the GUI

win.mainloop()              # 5 Start GUI

运行代码将创建此图形用户界面:

如何做...

它是如何工作的...

第 4 行防止 Python GUI 被调整大小。

运行此代码将生成一个类似于我们在菜谱 1 中创建的 GUI。然而,用户不能再调整其大小。此外,请注意窗口工具栏中的最大化按钮已变为灰色。

为什么这很重要?因为,一旦我们在表单中添加小部件,调整大小可能会让我们的 GUI 看起来不如我们期望的好。我们将在下一章节中学习如何在 GUI 中添加小部件。

Resizable()Tk() 类的一个方法,通过传入 (0, 0),我们防止了 GUI 的调整大小。如果我们传入其他值,我们将硬编码 GUI 的 x 和 y 启动大小,但这不会使其不可调整大小

我们还在代码中添加了注释,为这本书中包含的食谱做准备。

注意事项

在诸如 Visual Studio .NET 这样的可视化编程 IDE 中,C#程序员通常不会考虑阻止用户调整他们用这种语言开发的 GUI 的大小。这会导致 GUI 质量较差。添加这一行 Python 代码可以让我们的用户欣赏我们的 GUI。

在 GUI 表单中添加标签

准备工作

我们正在扩展第一个菜谱。我们将保持 GUI 可调整大小,因此不要使用第二个菜谱中的代码(或者注释掉第 4 行的win.resizable行)。

如何做到这一点...

为了将一个标签小部件添加到我们的图形用户界面中,我们正在从tkinter导入ttk模块。请注意这两个导入语句。

# imports                  # 1
import tkinter as tk       # 2
from tkinter import ttk    # 3

在食谱 1 和 2 的底部找到的win.mainloop()上方添加以下代码。

# Adding a Label           # 4
ttk.Label(win, text="A Label").grid(column=0, row=0) # 5

运行代码为我们的 GUI 添加一个标签:

如何做...

它是如何工作的...

在上述代码的第 3 行,我们正在从tkinter导入一个独立的模块。ttk模块包含一些高级控件,可以让我们的 GUI 看起来很棒。从某种意义上说,ttktkinter的一个扩展。

我们仍然需要导入tkinter本身,但我们必须指定我们现在还想使用来自tkinterttk

注意事项

ttk代表“主题 tk”。它改善了我们的 GUI 外观和感觉。

上面的第 5 行在调用mainloop之前(此处未显示以节省空间。请参阅菜谱 1 或 2)将标签添加到 GUI 中。

我们将窗口实例传递给ttk.Label构造函数并设置文本属性。这将成为Label将显示的文本。

我们还在使用网格布局管理器,我们将在第二章布局管理中对其进行更深入的探讨。

注意我们的图形用户界面(GUI)突然比之前的菜谱小了很多。

它变得如此之小的原因是我们向我们的表单添加了一个小部件。没有小部件,tkinter会使用默认大小。添加小部件会导致优化,这通常意味着使用尽可能少的空间来显示小部件(们)。

如果我们将标签的文本变长,GUI 将会自动扩展。我们将在第二章的后续菜谱中介绍这种自动调整表单大小的功能,布局管理

还有更多...

尝试调整并最大化这个带有标签的 GUI,看看会发生什么。

创建按钮并更改它们的文本属性

准备就绪

这个食谱扩展了之前的版本。您可以从 Packt Publishing 网站下载完整的代码。

如何做到这一点...

我们添加了一个按钮,点击后会执行一个动作。在这个菜谱中,我们将更新之前菜谱中添加的标签,以及更新按钮的文本属性。

# Modify adding a Label                                      # 1
aLabel = ttk.Label(win, text="A Label")                      # 2
aLabel.grid(column=0, row=0)                                 # 3

# Button Click Event Callback Function                       # 4
def clickMe():                                               # 5
    action.configure(text="** I have been Clicked! **")
    aLabel.configure(foreground='red')

# Adding a Button                                            # 6
action = ttk.Button(win, text="Click Me!", command=clickMe)  # 7
action.grid(column=1, row=0)                                 # 8

在点击按钮之前:

如何做...

点击按钮后,标签的颜色已经改变,按钮上的文字也相应地发生了变化。行动!

如何做...

它是如何工作的...

在第 2 行,我们现在将标签分配给一个变量,在第 3 行我们使用这个变量在表单中定位标签。我们需要这个变量在clickMe()函数中更改其属性。默认情况下,这是一个模块级变量,因此只要我们在调用它的函数上方声明变量,我们就可以在函数内部访问它。

第五行是当按钮被点击时被调用的事件处理器。

在第 7 行,我们创建了按钮并将命令绑定到clickMe()函数。

注意事项

图形用户界面是事件驱动的。点击按钮会创建一个事件。我们使用ttk.Button小部件的命令属性,在回调函数中绑定此事件发生时会发生什么。注意我们并没有使用括号;只有名称clickMe

我们还将标签的文本改为包含红色,就像在印刷书中一样,否则可能不明显。当你运行代码时,你可以看到颜色确实发生了变化。

第三行和第八行都使用了网格布局管理器,这将在下一章中进行讨论。这会使标签和按钮对齐。

还有更多...

我们将继续向我们的 GUI 添加越来越多的组件,并且我们会利用书中其他菜谱中的许多内置属性。

文本框小部件

tkinter 中,典型的文本框小部件被称为 Entry。在本教程中,我们将向我们的图形用户界面添加这样一个 Entry。我们将通过描述 Entry 为用户做了什么来使我们的标签更加有用。

准备就绪

此配方基于 创建按钮并更改其文本属性 配方。

如何操作...

# Modified Button Click Function   # 1
def clickMe():                     # 2
    action.configure(text='Hello ' + name.get())

# Position Button in second row, second column (zero-based)
action.grid(column=1, row=1)

# Changing our Label               # 3
ttk.Label(win, text="Enter a name:").grid(column=0, row=0) # 4

# Adding a Textbox Entry widget    # 5
name = tk.StringVar()              # 6
nameEntered = ttk.Entry(win, width=12, textvariable=name) # 7
nameEntered.grid(column=0, row=1)  # 8

现在我们的图形用户界面看起来是这样的:

如何做...

在输入一些文本并点击按钮后,GUI(图形用户界面)将发生以下变化:

如何做...

它是如何工作的...

在第 2 行,我们正在获取Entry小部件的值。我们还没有使用面向对象编程(OOP),那么我们是如何访问一个甚至还未声明的变量的值的呢?

在 Python 的过程式编程中,如果不使用面向对象(OOP)类,我们必须在尝试使用该名称的语句上方实际放置一个名称。那么这是怎么做到的(它确实做到了)?

答案是按钮点击事件是一个回调函数,当用户点击按钮时,这个函数中引用的变量是已知的并且确实存在。

生活很美好。

第 4 行给我们的标签赋予了一个更有意义的名称,因为它现在描述了其下方的文本框。我们将按钮向下移动到标签旁边,以便在视觉上关联这两个元素。我们仍然在使用网格布局管理器,这将在第二章布局管理中更详细地解释。

第 6 行创建了一个变量name。这个变量绑定到Entry上,在我们的clickMe()函数中,我们能够通过在这个变量上调用get()方法来检索Entry框的值。这就像魔法一样有效。

现在我们看到,虽然按钮显示了我们所输入的完整文本(甚至更多),但Entry文本框并没有扩展。这是因为我们在第 7 行将其硬编码为 12 英寸的宽度。

注意事项

Python 是一种动态类型语言,它从赋值中推断类型。这意味着如果我们将字符串赋值给变量 name,该变量将是字符串类型,如果我们将整数赋值给 name,这个变量的类型将是整数。

使用 tkinter 时,我们必须在成功使用之前将变量name声明为类型tk.StringVar()。原因是这样的,Tkinter 不是 Python。我们可以从 Python 中使用它,但它并不是同一种语言。

将焦点设置到小部件并禁用小部件

当我们的 GUI 界面正在得到很好的改进时,如果光标能在 GUI 出现时立即出现在Entry小部件中,将会更加方便和实用。在这里,我们将学习如何实现这一点。

准备就绪

这个食谱扩展了之前的食谱。

如何做到这一点...

Python 真的是非常出色。当 GUI 出现时,我们只需在之前创建的 tkinter 小部件实例上调用 focus() 方法,就可以将焦点设置到特定的控件上。在我们的当前 GUI 示例中,我们将 ttk.Entry 类实例分配给了一个名为 nameEntered 的变量。现在我们可以给它设置焦点。

将以下代码放置在启动主窗口事件循环的模块底部上方,就像在之前的食谱中一样。如果你遇到一些错误,请确保你将变量调用放置在代码下方,它们被声明的位置。因为我们目前还没有使用面向对象编程(OOP),所以这仍然是必要的。稍后,将不再需要这样做。

nameEntered.focus()            # Place cursor into name Entry

在 Mac 上,你可能需要首先将焦点设置到 GUI 窗口,然后才能将焦点设置到该窗口中的Entry小部件。

添加这一行 Python 代码将光标置于我们的文本Entry框中,使文本Entry框获得焦点。一旦 GUI 出现,我们就可以直接在文本框中输入,而无需先点击它。

如何做...

注意事项

注意现在光标默认位于文本Entry框内。

我们还可以禁用小部件。为此,我们可以在小部件上设置一个属性。我们可以通过添加以下这一行 Python 代码来使按钮不可用:

action.configure(state='disabled')    # Disable the Button Widget

在添加上述 Python 代码行之后,点击按钮不再产生任何动作!

如何做...

它是如何工作的...

这段代码是自我解释的。我们将焦点设置到一个控件上,并禁用另一个小部件。在编程语言中良好的命名有助于消除冗长的解释。本书后面将会有一些关于如何在工作中编程或在家中练习编程技能时如何做到这一点的进阶技巧。

还有更多...

是的。这仅仅是第一章。还有更多内容即将到来。

组合框小部件

在这个菜谱中,我们将通过添加具有初始默认值的下拉组合框来改进我们的 GUI。虽然我们可以限制用户只能选择某些选项,但同时,我们也可以允许用户输入他们想要的任何内容。

准备就绪

这个菜谱扩展了之前的菜谱。

如何做到这一点...

我们正在使用网格布局管理器在Entry小部件和Button之间插入另一列。以下是 Python 代码。

ttk.Label(win, text="Choose a number:").grid(column=1, row=0)  # 1
number = tk.StringVar()                         # 2
numberChosen = ttk.Combobox(win, width=12, textvariable=number) #3
numberChosen['values'] = (1, 2, 4, 42, 100)     # 4
numberChosen.grid(column=1, row=1)              # 5
numberChosen.current(0)                         # 6

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

如何做...

它是如何工作的...

第一行添加第二个标签以匹配新创建的组合框(在第三行创建)。第二行将框的值赋给一个特殊的 tkinter 类型变量(StringVar),正如我们在之前的菜谱中所做的那样。

第 5 行将两个新的控件(标签和组合框)在我们的前一个 GUI 布局中进行对齐,第 6 行分配了一个默认值,当 GUI 首次可见时将显示此值。这是numberChosen['values']元组的第一个值,字符串"1"。我们在第 4 行没有给整数的元组加上引号,但它们因为,在第 2 行中,我们声明了值的数据类型为tk.StringVar,而被转换成了字符串。

截图显示了用户所做的选择(42)。此值被分配给number变量。

还有更多...

如果我们想要限制用户只能选择我们编程到Combobox中的值,我们可以通过将状态属性传递给构造函数来实现。将前一段代码的第 3 行修改为:

numberChosen = ttk.Combobox(win, width=12, textvariable=number, state='readonly')

现在用户不能再将值输入到Combobox中。我们可以在我们的按钮点击事件回调函数中添加以下代码行来显示用户选择的价值:

# Modified Button Click Callback Function
def clickMe():
    action.configure(text='Hello ' + name.get()+ ' ' + numberChosen.get())

在选择一个数字、输入一个名称然后点击按钮后,我们得到以下 GUI 结果,现在它也显示了所选的数字:

还有更多...

创建具有不同初始状态的复选框

在这个示例中,我们将添加三个Checkbutton小部件,每个小部件的初始状态都不同。

准备就绪

这个菜谱扩展了之前的菜谱。

如何做到这一点...

我们创建了三个状态不同的Checkbutton小部件。第一个是禁用的,并且里面有一个勾选标记。由于小部件处于禁用状态,用户无法移除这个勾选标记。

第二个 Checkbutton 被启用,并且默认情况下其中没有勾选标记,但用户可以点击它来添加勾选标记。

第三个Checkbutton默认既启用又选中。用户可以根据自己的喜好频繁地取消选中并重新选中该控件。

# Creating three checkbuttons    # 1
chVarDis = tk.IntVar()           # 2
check1 = tk.Checkbutton(win, text="Disabled", variable=chVarDis, state='disabled')                     # 3
check1.select()                  # 4
check1.grid(column=0, row=4, sticky=tk.W) # 5

chVarUn = tk.IntVar()            # 6
check2 = tk.Checkbutton(win, text="UnChecked", variable=chVarUn)
check2.deselect()                # 8
check2.grid(column=1, row=4, sticky=tk.W) # 9                  

chVarEn = tk.IntVar()            # 10
check3 = tk.Checkbutton(win, text="Enabled", variable=chVarEn)
check3.select()                  # 12
check3.grid(column=2, row=4, sticky=tk.W) # 13

运行新代码将产生以下图形用户界面:

如何做...

它是如何工作的...

在第 2 行、第 6 行和第 10 行,我们创建了三个类型为IntVar的变量。在下一行,对于这些变量中的每一个,我们创建了一个Checkbutton,并将这些变量传递进去。它们将保存Checkbutton的状态(未选中或选中)。默认情况下,这要么是 0(未选中),要么是 1(选中),因此变量的类型是tkinter整数。

我们将这些Checkbutton小部件放置在我们的主窗口中,因此构造函数传入的第一个参数是部件的父级;在我们的例子中是win。我们通过每个Checkbuttontext属性为其分配不同的标签。

将网格的粘性属性设置为 tk.W 意味着小部件将被对齐到网格的西部。这与 Java 语法非常相似,意味着它将被对齐到左侧。当我们调整我们的 GUI 大小时,小部件将保持在左侧,而不会移动到 GUI 的中心。

第四行和第十二行通过在这两个Checkbutton类实例上调用select()方法,在Checkbutton小部件中放置一个勾选标记。

我们继续使用网格布局管理器来排列我们的小部件,这将在第二章布局管理中更详细地解释。

使用单选按钮小部件

在这个菜谱中,我们将创建三个 tkinter Radiobutton 小部件。我们还将添加一些代码,根据选中的哪个 Radiobutton 来改变主表单的颜色。

准备就绪

这个菜谱扩展了之前的菜谱。

如何做到这一点...

我们正在将以下代码添加到之前的食谱中:

# Radiobutton Globals   # 1
COLOR1 = "Blue"         # 2
COLOR2 = "Gold"         # 3
COLOR3 = "Red"          # 4

# Radiobutton Callback  # 5
def radCall():          # 6
   radSel=radVar.get()
   if   radSel == 1: win.configure(background=COLOR1)
   elif radSel == 2: win.configure(background=COLOR2)
   elif radSel == 3: win.configure(background=COLOR3)

# create three Radiobuttons   # 7
radVar = tk.IntVar()          # 8
rad1 = tk.Radiobutton(win, text=COLOR1, variable=radVar, value=1,               command=radCall)              # 9
rad1.grid(column=0, row=5, sticky=tk.W)  # 10

rad2 = tk.Radiobutton(win, text=COLOR2, variable=radVar, value=2, command=radCall)                             # 11
rad2.grid(column=1, row=5, sticky=tk.W)  # 12

rad3 = tk.Radiobutton(win, text=COLOR3, variable=radVar, value=3, command=radCall)                             # 13
rad3.grid(column=2, row=5, sticky=tk.W)  # 14

运行此代码并选择名为黄金单选按钮将创建以下窗口:

如何做...

它是如何工作的...

在第 2-4 行中,我们创建了一些模块级别的全局变量,这些变量将在创建每个单选按钮以及创建更改主表单背景颜色的动作的回调函数(使用实例变量win)中使用。

我们使用全局变量来简化代码的修改。通过将颜色的名称分配给一个变量并在多个地方使用这个变量,我们可以轻松地尝试不同的颜色。而不是进行全局的硬编码字符串搜索和替换(这容易出错),我们只需更改一行代码,其他所有内容都会正常工作。这被称为DRY 原则,即不要重复自己。这是我们将在本书后面的食谱中使用的面向对象编程(OOP)概念。

注意事项

我们分配给变量(COLOR1, COLOR2 ...)的颜色名称是 tkinter 关键字(技术上,它们是 符号名称)。如果我们使用不是 tkinter 颜色关键字的名称,那么代码将无法工作。

第 6 行是回调函数,根据用户的选项改变我们的主表单(win)的背景。

在第 8 行,我们创建了一个tk.IntVar变量。这个变量的重要之处在于我们只创建了一个变量,将被三个单选按钮共同使用。从上面的截图可以看出,无论我们选择哪个Radiobutton,其他所有选项都会自动为我们取消选中。

第 9 至 14 行创建了三个单选按钮,将它们分配给主表单,并将用于回调函数中创建更改主窗口背景动作的变量传入。

注意事项

虽然这是第一个改变小部件颜色的配方,但坦白说,它看起来有点丑陋。本书中接下来的大部分配方都解释了如何让我们的图形用户界面真正令人惊叹。

还有更多...

这里是您可以在官方 tcl 手册页面查找的可用符号颜色名称的小样本:

www.tcl.tk/man/tcl8.5/TkCmd/colors.htm

姓名 红色 绿色 蓝色
爱丽丝蓝 240 248 255
雅典蓝 240 248 255
蓝色 0 0 255
金色 255 215 0
红色 255 0 0

一些名称创建相同的颜色,所以 alice blueAliceBlue 创建相同的颜色。在这个配方中我们使用了符号名称 BlueGoldRed

使用滚动文本小部件

ScrolledText小部件比简单的Entry小部件大得多,并且跨越多行。它们类似于记事本这样的小部件,会自动换行,当文本超过ScrolledText小部件的高度时,会自动启用垂直滚动条。

准备就绪

这个菜谱扩展了之前的菜谱。您可以从 Packt Publishing 网站下载本书每一章的代码。

如何做到这一点...

通过添加以下代码行,我们创建一个ScrolledText小部件:

# Add this import to the top of the Python Module    # 1
from tkinter import scrolledtext      # 2

# Using a scrolled Text control       # 3
scrolW  = 30                          # 4
scrolH  =  3                          # 5
scr = scrolledtext.ScrolledText(win, width=scrolW, height=scrolH, wrap=tk.WORD)                         # 6
scr.grid(column=0, columnspan=3)      # 7

我们实际上可以在我们的小部件中输入文字,如果我们输入足够的单词,行就会自动换行!

如何做...

一旦我们输入的单词数量超过了小部件能够显示的高度,垂直滚动条就会被启用。这一切都是现成的,无需我们再编写任何代码来实现这一功能。

如何做...

它是如何工作的...

在第 2 行,我们正在导入包含ScrolledText小部件类的模块。将其添加到模块顶部,位于其他两个import语句之下。

第 4 行和第 5 行定义了我们即将创建的ScrolledText小部件的宽度和高度。这些是硬编码的值,我们在第 6 行的ScrolledText小部件构造函数中传递了这些值。

这些值是经过实验发现的魔法数字,效果良好。你可以通过将srcolW从 30 改为 50 进行实验,观察其效果!

在第 6 行,我们通过传递wrap=tk.WORD来在部件上设置一个属性。

通过将wrap属性设置为tk.WORD,我们告诉ScrolledText小部件按单词断行,这样我们就不需要在单词内部换行。默认选项是tk.CHAR,它会无论我们是否在单词中间都进行换行。

第二张截图显示,垂直滚动条向下移动了,因为我们正在阅读一段较长的文本,而这段文本并没有完全适合我们创建的SrolledText控件在 x, y 维度内。

将网格小部件的columnspan属性设置为3,以便SrolledText小部件跨越所有三列。如果我们没有设置此属性,我们的SrolledText小部件将仅位于第一列,这并不是我们想要的。

在循环中添加几个小部件

到目前为止,我们通过基本复制粘贴相同的代码并修改变体(例如,列数)的方式创建了几个相同类型的组件(例如,Radiobutton)。在这个菜谱中,我们开始重构我们的代码,使其更加简洁。

准备就绪

我们正在重构之前食谱代码的一些部分,因此你需要这段代码来应用到这个食谱中。

如何做到这一点...

# First, we change our Radiobutton global variables into a list.
colors = ["Blue", "Gold", "Red"]              # 1

# create three Radiobuttons using one variable
radVar = tk.IntVar()

Next we are selecting a non-existing index value for radVar.
radVar.set(99)                                # 2

Now we are creating all three Radiobutton widgets within one loop.

for col in range(3):                          # 3
    curRad = 'rad' + str(col)  
    curRad = tk.Radiobutton(win, text=colors[col], variable=radVar,     value=col, command=radCall)
    curRad.grid(column=col, row=5, sticky=tk.W)

We have also changed the callback function to be zero-based, using the list instead of module-level global variables. 

# Radiobutton callback function                # 4
def radCall():
   radSel=radVar.get()
   if   radSel == 0: win.configure(background=colors[0])
   elif radSel == 1: win.configure(background=colors[1])
   elif radSel == 2: win.configure(background=colors[2])

运行此代码将创建与之前相同的窗口,但我们的代码更加简洁且易于维护。这将在我们后续的 GUI 扩展中有所帮助。

它是如何工作的...

在第一行,我们将全局变量转换成了一个列表。

在第 2 行,我们为名为radVartk.IntVar变量设置了一个默认值。这很重要,因为在之前的菜谱中,我们为Radiobutton小部件设置了从 1 开始的值,而在我们新的循环中,使用 Python 的基于 0 的索引方式要方便得多。如果我们没有将默认值设置为我们Radiobutton小部件的范围之外的值,当 GUI 出现时,其中一个单选按钮会被选中。虽然这本身可能不是那么糟糕,但它不会触发回调函数,我们最终会选中一个不执行其工作(即改变主窗口表单的颜色)的单选按钮。

在第 3 行,我们将之前硬编码的三个 Radiobutton 小部件的创建替换为一个循环,它执行相同的操作。这仅仅更加简洁(代码行数更少)且易于维护。例如,如果我们想要创建 100 个而不是仅仅 3 个 Radiobutton 小部件,我们只需更改 Python 范围运算符内的数字即可。我们不需要输入或复制粘贴 97 段重复的代码,只需一个数字。

第 4 行显示了修改后的回调函数,它在物理上位于前面的行之上。我们将它放置在下方,以强调这个菜谱中更重要的部分。

还有更多...

本食谱总结了本书的第一章。所有接下来的章节中的食谱都将基于我们迄今为止构建的 GUI 进行构建,极大地增强它。

第二章:布局管理

在本章中,我们将使用 Python 3 来搭建我们的图形用户界面:

  • 在标签框架小部件中排列多个标签

  • 使用填充来为小部件周围添加空间

  • 小部件如何动态扩展 GUI

  • 通过在框架内嵌套框架来对齐 GUI 小部件

  • 创建菜单栏

  • 创建标签式小部件

  • 使用网格布局管理器

简介

在本章中,我们将探讨如何在窗口小部件内部排列小部件以创建我们的 Python 图形用户界面。掌握 GUI 布局设计的根本原理将使我们能够创建外观出色的 GUI。有一些技术将帮助我们实现这种布局设计。

网格布局管理器是 tkinter 内置的最重要的布局工具之一,我们将要使用它。

我们可以非常容易地使用 tk 创建菜单栏、标签控制(即笔记本)以及许多其他小部件。

tk 中缺少一个开箱即用的状态栏组件。

在本章中,我们不必费心手工制作这个小部件,但它是可以完成的。

在标签框架小部件中排列多个标签

LabelFrame小部件允许我们以有组织的方式设计我们的 GUI。我们仍然使用网格布局管理器作为我们的主要布局设计工具,但通过使用LabelFrame小部件,我们对 GUI 设计有了更多的控制。

准备就绪

我们开始在我们的 GUI 中添加越来越多的控件,并在接下来的菜谱中使 GUI 完全功能化。在这里,我们开始使用LabelFrame控件。我们将重用上一章最后一个菜谱中的 GUI。

如何做到这一点...

在 Python 模块的底部主事件循环上方添加以下代码:

# Create a container to hold labels
labelsFrame = ttk.LabelFrame(win, text=' Labels in a Frame ') # 1
labelsFrame.grid(column=0, row=7)

# Place labels into the container element # 2
ttk.Label(labelsFrame, text="Label1").grid(column=0, row=0)
ttk.Label(labelsFrame, text="Label2").grid(column=1, row=0)
ttk.Label(labelsFrame, text="Label3").grid(column=2, row=0)

# Place cursor into name Entry
nameEntered.focus()

如何做...

注意

我们可以通过修改代码轻松地垂直对齐标签,如下所示。请注意,我们唯一需要做的更改是在列和行编号上。

# Place labels into the container element – vertically # 3
ttk.Label(labelsFrame, text="Label1").grid(column=0, row=0)
ttk.Label(labelsFrame, text="Label2").grid(column=0, row=1)
ttk.Label(labelsFrame, text="Label3").grid(column=0, row=2)

如何做...

它是如何工作的...

评论 # 1:在这里,我们将创建我们的第一个 ttk LabelFrame 小部件,并为框架命名。父容器是 win,即我们的主窗口。

在注释# 2 之后的三个行创建标签名称并将它们放置在 LabelFrame 中。我们正在使用重要的网格布局工具来排列 LabelFrame 内的标签。这个布局管理器的列和行属性赋予我们控制 GUI 布局的能力。

注意事项

我们标签的父容器是 LabelFrame,而不是主窗口的win实例变量。在这里我们可以看到布局层次结构的开始。

突出的注释 # 3 展示了如何通过列和行属性轻松更改我们的布局。注意我们如何将列更改为 0,以及我们如何通过按顺序编号行值来垂直堆叠我们的标签。

注意事项

ttk 的名字代表 "主题化的 tk"。Tk 8.5 中引入了 tk 主题化的控件集。

还有更多...

在本章后面的食谱中,我们将嵌入 LabelFrame(s)到 LabelFrame(s)中,通过嵌套它们来控制我们的 GUI 布局。

使用填充来为小部件周围添加空间

我们的图形用户界面正在顺利创建。接下来,我们将通过在它们周围添加一些空间来改善我们小部件的视觉外观,这样它们就可以呼吸了...

准备就绪

虽然 tkinter 可能曾因创建丑陋的 GUI 而声名狼藉,但自从 8.5 版本(随 Python 3.4.x 一起发布)以来,这一情况已经发生了显著变化。你只需知道如何使用可用的工具和技术。这正是我们接下来要做的。

如何做到这一点...

首先展示的是围绕小部件添加间距的流程方式,然后我们将使用循环以更优的方式实现相同的效果。

我们的 LabelFrame 在底部与主窗口融合时看起来有点紧凑。现在我们来修复这个问题。

修改以下代码行,通过添加padxpady

labelsFrame.grid(column=0, row=7, padx=20, pady=40)

现在 我们的 LabelFrame 获得了一些呼吸空间:

如何做...

它是如何工作的...

在 tkinter 中,通过使用名为padxpady的内置属性来添加水平和垂直空间。这些属性可以用来在许多小部件周围添加空间,分别改善水平和垂直对齐。我们硬编码了 20 像素的空间到 LabelFrame 的左右两侧,并在框架的顶部和底部添加了 40 像素的空间。现在我们的 LabelFrame 比之前更加突出。

注意

上面的截图仅显示了相关更改。

我们可以使用循环来在包含在 LabelFrame 中的标签周围添加空格:

for child in labelsFrame.winfo_children(): 
    child.grid_configure(padx=8, pady=4)

现在 LabelFrame 小部件内的标签周围也有一些空间:

如何工作...

grid_configure() 函数使我们能够在主循环显示之前修改 UI 元素。因此,当我们首次创建小部件时,我们不必硬编码值,而是可以先工作在布局上,然后在文件末尾创建 GUI 之前调整间距。这是一个值得了解的技巧。

winfo_children() 函数返回属于 labelsFrame 变量的所有子元素的列表。这使得我们可以遍历它们,并为每个标签分配填充。

注意事项

注意事项之一是标签右侧的间距实际上并不明显。这是因为 LabelFrame 的标题长度超过了标签的名称。我们可以通过使标签的名称更长来实验这个问题。

ttk.Label(labelsFrame, text="Label1 -- sooooo much loooonger...").grid(column=0, row=0)

现在我们的 GUI 看起来如下。注意现在在点旁边的长标签右侧增加了一些空间。最后一个点没有接触到 LabelFrame,如果没有增加空间,它本会触碰到。

如何工作...

我们还可以移除 LabelFrame 的名称来观察padx对定位我们的标签有何影响。

如何工作...

小部件如何动态扩展 GUI

你可能已经注意到了在之前的截图和运行代码的过程中,小部件具有扩展自身以适应所需空间来视觉显示文本的能力。

注意事项

Java 引入了动态 GUI 布局管理的概念。相比之下,像 VS.NET 这样的可视化开发 IDE 以可视化的方式布局 GUI,基本上是硬编码 UI 元素的 x 和 y 坐标。

使用 tkinter,这种动态能力既带来优势也带来一点挑战,因为有时候我们更希望我们的 GUI 不要那么动态地扩展!嗯,我们是动态的 Python 程序员,所以我们可以找出如何充分利用这种出色的行为!

准备就绪

在上一道菜谱的开始部分,我们添加了一个标签框架小部件。这使我们的部分控件移动到了第 0 列的中心。我们可能不希望对 GUI 布局进行这种修改。接下来,我们将探讨一些修复此问题的方法。

如何做到这一点...

让我们先意识到在我们 GUI 布局中正在发生的微妙细节,以便更好地理解它。

我们正在使用网格布局管理器小部件,并且它以零为基础的网格排列我们的小部件。

行 0; 列 0 行 0; 列 1 行 0; 列 2
第 1 行;第 0 列 第 1 行;第 1 列 第 1 行;第 2 列

使用网格布局管理器时,所发生的情况是任何给定列的宽度由该列中最长名称或小部件决定。这会影响所有行。

通过添加我们的 LabelFrame 小部件,并给它一个比像左上角的标签和其下方的文本输入框这样的硬编码尺寸小部件更长的标题,我们动态地将这些小部件移动到第 0 列的中心,为这些小部件的左右两侧添加空间。

偶然的是,因为我们使用了粘性属性来设置 Checkbutton 和 ScrolledText 小部件,所以它们仍然附着在框架的左侧。

让我们更详细地查看本章第一道菜谱的截图:

如何做...

我们添加了以下代码来创建 LabelFrame,然后在这个框架中放置了标签:

# Create a container to hold labels
labelsFrame = ttk.LabelFrame(win, text=' Labels in a Frame ')
labelsFrame.grid(column=0, row=7)

由于 LabelFrame 的文本属性,即显示为 LabelFrame 标题的文本,比我们的输入一个名称:标签和其下方的文本框输入都要长,因此这两个小部件会根据列 0 的新宽度动态居中。

列 0 中的 Checkbutton 和 Radiobutton 小部件没有居中,因为我们创建这些小部件时使用了sticky=tk.W属性。

对于 ScrolledText 小部件,我们使用了sticky=tk.WE,,这会将小部件绑定到框架的西边(即左边)和东边(即右边)。

让我们从 ScrolledText 小部件中移除粘性属性,并观察这种变化带来的效果。

scr = scrolledtext.ScrolledText(win, width=scrolW, height=scrolH, wrap=tk.WORD)
#### scr.grid(column=0, sticky='WE', columnspan=3)
scr.grid(column=0, columnspan=3)

现在我们的 GUI 在 ScrolledText 小部件的左右两侧都增加了新的空间。因为我们使用了columnspan=3属性,所以我们的 ScrolledText 小部件仍然跨越了所有三个列。

如何做...

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

如何做...

要将布局恢复到添加 LabelFrame 之前的状态,一种方法是通过调整网格列位置。将列值从 0 更改为 1。

labelsFrame.grid(column=1, row=7, padx=20, pady=40)

现在我们的图形用户界面看起来是这样的:

如何做...

它是如何工作的...

由于我们仍在使用单个小部件,我们的布局可能会变得混乱。通过将 LabelFrame 的列值从 0 更改为 1,我们能够将控件恢复到它们原本的位置,以及我们希望它们所在的位置。至少最左边的标签、文本、复选框、滚动文本和单选按钮小部件现在都位于我们期望它们所在的位置。位于第 1 列的第二标签和文本Entry已经自动对齐到框架中的标签小部件长度的中心,因此我们基本上将我们的对齐挑战向右移动了一列。这并不那么明显,因为选择一个数字:标签的大小几乎与框架中的标签标题的大小相同,因此列宽已经接近由 LabelFrame 生成的新宽度。

还有更多...

在下一个菜谱中,我们将嵌套框架以避免在本菜谱中刚刚经历的部件意外错位问题。

通过在框架内嵌套框架来对齐 GUI 小部件

如果我们在框架中嵌套框架,我们将对我们的 GUI 布局有更好的控制。这正是本食谱中我们将要做的。

准备就绪

Python 及其 GUI 模块的动态行为可能会给真正实现我们想要的 GUI 外观带来一定的挑战。在这里,我们将嵌套框架以获得更多对布局的控制。这将增强不同 UI 元素之间的层次结构,使得视觉外观更容易实现。

我们将继续使用在前一个菜谱中创建的图形用户界面。

如何做到这一点...

在这里,我们将创建一个顶层框架,它将包含其他框架和小部件。这将帮助我们获得我们想要的 GUI 布局。

为了做到这一点,我们不得不将当前的控件嵌入到一个中央的 ttk.LabelFrame 中。这个 ttk.LabelFrame 是主父窗口的一个子窗口,所有的控件都将成为这个 ttk.LabelFrame 的子控件。

到目前为止,在我们的配方中,我们已经直接将所有小部件分配给了我们的主 GUI 框架。现在,我们只将我们的 LabelFrame 分配给主窗口,之后,我们将这个 LabelFrame 作为所有小部件的父容器。

这在我们 GUI 布局中创建以下层次结构:

如何做...

在此图中,win 是一个变量,它引用我们的主 GUI tkinter 窗口框架;monty 是一个变量,它引用我们的 LabelFrame,并且是主窗口框架(win)的子框架;而 aLabel 和所有其他小部件现在都被放置到 LabelFrame 容器(monty)中。

在我们的 Python 模块顶部添加以下代码(参见注释 #1):

# Create instance
win = tk.Tk()

# Add a title       
win.title("Python GUI")    

# We are creating a container frame to hold all other widgets # 1
monty = ttk.LabelFrame(win, text=' Monty Python ')
monty.grid(column=0, row=0)

接下来,我们将修改所有以下控件以使用monty作为父控件,替换win。以下是一个如何操作的示例:

# Modify adding a Label
aLabel = ttk.Label(monty, text="A Label")

如何做...

注意所有的小部件现在都被包含在蒙提·派森标签框架中,它用几乎看不见的细线包围了它们。接下来,我们可以将左边的框架中的标签小部件重置,而不会弄乱我们的 GUI 布局:

如何做...

哎呀——也许不是这样。虽然我们嵌套的框架在左侧对齐得很好,但它又把我们的顶部小部件推到了中间(默认行为)。

为了将它们左对齐,我们必须通过使用sticky属性强制我们的 GUI 布局。通过将其赋值为"W"(西),我们可以控制小部件实现左对齐。

# Changing our Label
ttk.Label(monty, text="Enter a name:").grid(column=0, row=0, sticky='W')

如何做...

它是如何工作的...

注意我们是如何对齐标签,但下面的文本框并没有对齐。我们必须为所有想要左对齐的控件使用sticky属性。我们可以通过循环使用winfo_children()grid_configure(sticky='W')属性来实现,就像我们在本章第 2 个菜谱中做的那样。

winfo_children() 函数返回属于父元素的所有子元素的列表。这使得我们能够遍历所有小部件并更改它们的属性。

注意

使用 tkinter 强制左、右、上、下命名与 Java 非常相似:west(西)、east(东)、north(北)和 south(南),缩写为:"W"等等。我们还可以使用以下语法:tk.W 代替"W"。

在之前的配方中,我们结合了“W”和“E”来使我们的 ScrolledText 小部件通过“WE”同时附着到其容器的左右两侧。我们可以添加更多的组合:“NSE”将我们的小部件拉伸到顶部、底部和右侧。如果我们表单中只有一个小部件,例如一个按钮,我们可以通过使用所有选项来使其填充整个框架:“NSWE”。我们还可以使用元组语法:sticky=(tk.N, tk.S, tk.W, tk.E)

让我们将非常长的标签改回来,并将列 0 中的条目左对齐。

ttk.Label(monty, text="Enter a name:").grid(column=0, row=0, sticky='W')

name = tk.StringVar()
nameEntered = ttk.Entry(monty, width=12, textvariable=name)
nameEntered.grid(column=0, row=1, sticky=tk.W)

如何工作...

注意

为了分离我们Frame 中的标签LabelFrame 的长度对我们 GUI 布局其余部分的影响,我们不应将此 LabelFrame 放置在与其他小部件相同的 LabelFrame 中。相反,我们直接将其分配给主 GUI 表单(win)。

我们将在后面的章节中这样做。

创建菜单栏

在这个菜谱中,我们将为主窗口添加一个菜单栏,然后将菜单添加到菜单栏中,最后将菜单项添加到菜单里。

准备就绪

我们将首先学习如何添加菜单栏、几个菜单和几个菜单项的技术,以展示如何实现这一原理。点击菜单项将没有任何效果。接下来,我们将为菜单项添加功能,例如,当点击退出菜单项时关闭主窗口,并显示帮助 | 关于对话框。

我们正在继续扩展当前和上一章中创建的图形用户界面。

如何做到这一点...

首先,我们需要从 tkinter 模块中导入 Menu 类。将以下代码行添加到 Python 模块的顶部,即导入语句所在的位置:

from tkinter import Menu

接下来,我们将创建菜单栏。将以下代码添加到模块的底部,正好在我们创建主事件循环的上方:

menuBar = Menu(win)                      # 1
win.config(menu=menuBar)

现在我们给这个栏添加一个菜单,并将一个菜单项分配给该菜单。

fileMenu = Menu(menuBar)                 # 2
fileMenu.add_command(label="New")
menuBar.add_cascade(label="File", menu=fileMenu)

运行此代码将添加一个菜单栏,其中包含一个菜单,该菜单有一个菜单项。

如何做...

接下来,我们在之前添加到菜单栏的第一个菜单中添加第二个菜单项。

fileMenu.add_command(label="New")
fileMenu.add_command(label="Exit")        # 3
menuBar.add_cascade(label="File", menu=fileMenu)

如何做...

我们可以通过在现有菜单项之间添加以下代码行(# 4)来在菜单项之间添加分隔线。

fileMenu.add_command(label="New")
fileMenu.add_separator()               # 4
fileMenu.add_command(label="Exit")

如何做...

通过将属性 tearoff 传递给菜单的构造函数,我们可以移除默认情况下出现在第一个 MenuItem 上方的第一条虚线。

# Add menu items
fileMenu = Menu(menuBar, tearoff=0)      # 5

如何做...

我们将添加第二个菜单,它将水平放置在第一个菜单的右侧。我们将给它一个 MenuItem,我们将其命名为关于,为了使这个功能正常工作,我们必须将这个第二个菜单添加到 MenuBar 中。

文件帮助|关于是我们都非常熟悉的常见 Windows GUI 布局,我们可以使用 Python 和 tkinter 创建相同的菜单。

菜单、菜单项和菜单栏的创建顺序和命名可能一开始会有些令人困惑,但一旦我们习惯了 tkinter 要求我们这样编写代码的方式,这实际上变得很有趣。

helpMenu = Menu(menuBar, tearoff=0)            # 6
helpMenu.add_command(label="About")
menuBar.add_cascade(label="Help", menu=helpMenu)

如何做...

在这个阶段,我们的 GUI 有一个菜单栏和两个包含一些菜单项的菜单。点击它们并不会做太多,直到我们添加一些命令。这正是我们接下来要做的。在创建菜单栏的上方添加以下代码:

def _quit():         # 7
    win.quit()
    win.destroy()
    exit()

接下来,我们将文件 | 退出菜单项绑定到该函数,通过在 MenuItem 中添加以下命令:

fileMenu.add_command(label="Exit", command=_quit)    # 8

现在,当我们点击退出菜单项时,我们的应用程序确实会退出。

它是如何工作的...

在评论 # 1 中,我们调用菜单的 tkinter 构造函数并将菜单分配给我们的主 GUI 窗口。我们在实例变量 menuBar 中保存了一个引用,在下一行代码中,我们使用这个实例来配置我们的 GUI 以使用 menuBar 作为我们的菜单。

评论 #2 展示了我们首先添加一个 MenuItem,然后创建菜单的过程。这似乎不太直观,但这就是 tkinter 的工作方式。add_cascade() 方法将 MenuItems 从上到下垂直排列。

评论 # 3 展示了如何向菜单中添加第二个 MenuItem。

在评论 # 4 中,我们在两个菜单项之间添加了一条分隔线。这通常用于将相关的菜单项分组,并将它们与不太相关的项分开(因此得名)。

评论 # 5 禁用了撕裂虚线,使我们的菜单看起来好得多。

注意事项

在不禁用此默认功能的情况下,用户可以从主窗口“撕下”菜单。我发现这个功能价值不大。您可以通过双击虚线(在禁用此功能之前)随意尝试操作。

如果你使用的是 Mac,这个功能可能没有被启用,所以你根本不必担心它。

如何工作...

评论 #6 展示了如何向菜单栏添加第二个菜单。我们可以通过使用这种技术继续添加菜单。

评论 #7 创建了一个函数来干净地退出我们的 GUI 应用程序。这是结束主事件循环的推荐 Python 方式。

在第八部分中,我们使用tkinter命令属性将第七部分中创建的函数绑定到 MenuItem 上。每当我们要让我们的菜单项真正执行某些操作时,我们必须将每个菜单项绑定到一个函数上。

注意事项

我们遵循推荐的 Python 命名规范,在退出函数前加上一个单下划线,以表明这是一个私有函数,不应被我们的代码客户端调用。

还有更多……

我们将在下一章中添加帮助 | 关于功能,该功能介绍了消息框以及更多内容。

创建标签式小部件

在这个菜谱中,我们将创建标签式小部件来进一步组织我们用 tkinter 编写的不断扩展的 GUI。

准备就绪

为了提升我们的 Python GUI 使用标签页的功能,我们将从最基础开始,使用最少的代码来实现。在接下来的菜谱中,我们将从之前的菜谱中添加小部件,并将它们放置到这个新的标签页布局中。

如何做到这一点...

创建一个新的 Python 模块,并将以下代码放入此模块中:

import tkinter as tk                    # imports
from tkinter import ttk
win = tk.Tk()                           # Create instance      
win.title("Python GUI")                 # Add a title 
tabControl = ttk.Notebook(win)          # Create Tab Control
tab1 = ttk.Frame(tabControl)            # Create a tab 
tabControl.add(tab1, text='Tab 1')      # Add the tab
tabControl.pack(expand=1, fill="both")  # Pack to make visible
win.mainloop()                          # Start GUI

这将创建以下图形用户界面:

如何做...

尽管目前并不令人特别印象深刻,这个小部件还是为我们 GUI 设计工具箱增添了一个非常强大的工具。在上面的简约示例中,它有其自身的局限性(例如,我们无法重新定位 GUI,并且它也没有显示整个 GUI 标题)。

在之前的菜谱中,我们使用了网格布局管理器来处理更简单的 GUI,而现在我们可以使用更简单的布局管理器,“pack”就是其中之一。

在前面的代码中,我们将 tabControl ttk.Notebook“打包”到主 GUI 表单中,扩展笔记本标签控制以填充所有侧面。

如何做...

我们可以在我们的控制面板中添加第二个标签页,并在它们之间进行点击。

tab2 = ttk.Frame(tabControl)            # Add a second tab
tabControl.add(tab2, text='Tab 2')      # Make second tab visible
win.mainloop()                          # Start GUI

现在我们有两个标签页。点击标签页 2以使其获得焦点。

如何做...

我们真的很想看到我们的窗口标题。所以,为了做到这一点,我们必须在我们的标签页之一中添加一个小部件。这个小部件必须足够宽,以便动态扩展我们的 GUI 来显示我们的窗口标题。我们将重新添加 Ole Monty,以及他的孩子们。

monty = ttk.LabelFrame(tab1, text=' Monty Python ')
monty.grid(column=0, row=0, padx=8, pady=4)
ttk.Label(monty, text="Enter a name:").grid(column=0, row=0, sticky='W')

现在我们把我们的蒙提·派森放进了Tab1

如何做...

我们可以将迄今为止创建的所有小部件放入我们新创建的标签控制中。

如何做...

现在所有的小部件都位于Tab1内。让我们将一些移动到Tab2。首先,我们创建第二个 LabelFrame 作为将要移动到Tab2的小部件的容器:

monty2 = ttk.LabelFrame(tab2, text=' The Snake ')
monty2.grid(column=0, row=0, padx=8, pady=4)

接下来,我们将复选框和单选按钮移动到Tab2,通过指定新的父容器,这是一个我们命名为monty2的新变量。以下是一个示例,我们将它应用于所有移动到Tab2的控件:

chVarDis = tk.IntVar()
check1 = tk.Checkbutton(monty2, text="Disabled", variable=chVarDis, state='disabled')

当我们运行代码时,我们的 GUI 现在看起来不同了。Tab1中的小部件比之前少,因为它包含了我们之前创建的所有小部件。

如何做...

我们现在可以点击Tab 2并查看我们重新定位的控件。

如何做...

点击重新定位的单选按钮(复选)不再有任何效果,因此我们将更改它们的操作,将文本属性重命名为单选按钮显示的名称,即 LabelFrame 小部件的标题。当我们点击黄金单选按钮时,我们不再将框架的背景设置为金色,而是在这里替换 LabelFrame 文本标题。Python "蛇"现在变为"黄金"。

# Radiobutton callback function
def radCall():
    radSel=radVar.get()
    if   radSel == 0: monty2.configure(text='Blue')
    elif radSel == 1: monty2.configure(text='Gold')
    elif radSel == 2: monty2.configure(text='Red')

现在,选择任何一个 RadioButton 小部件都会导致 LabelFrame 的名称发生变化。

如何做...

它是如何工作的...

在创建第二个标签页之后,我们将原本位于Tab1的一些小部件移动到了Tab2。添加标签页是组织我们不断增长的 GUI 的另一种极好方式。这是处理我们 GUI 设计中复杂性的一个非常不错的方法。我们可以将小部件按照它们自然归属的组进行排列,并通过使用标签页来让用户摆脱杂乱。

注意事项

tkinter 中,创建标签页是通过 Notebook 小部件来完成的,这是允许我们添加标签控制的工具。与许多其他小部件一样,tkinter 的笔记本小部件也附带了一些我们可以使用和配置的额外属性。探索我们可用的 tkinter 小部件的额外功能的一个绝佳起点是官方网站:docs.python.org/3.1/library/tkinter.ttk.html#notebook

使用网格布局管理器

网格布局管理器是我们可用的最有用的布局工具之一。我们已经在许多菜谱中使用了它,因为它实在是非常强大。

准备中…

在这个菜谱中,我们将回顾网格布局管理器的一些技术。我们已经使用过它们了,在这里我们将进一步探索它们。

如何做到这一点...

在本章中,我们创建了行和列,这实际上是一种数据库方法来设计 GUI(MS Excel 也是这样做)。我们硬编码了前四行,但后来我们忘记为下一行指定它希望驻留的位置。

Tkinter 在我们甚至没有注意到的情况下就已经帮我们填充了这个内容。

这里是我们代码中执行的操作:

check3.grid(column=2, row=4, sticky=tk.W, columnspan=3)
scr.grid(column=0, sticky='WE', columnspan=3)              # 1
curRad.grid(column=col, row=6, sticky=tk.W, columnspan=3)
labelsFrame.grid(column=0, row=7)

Tkinter 会自动添加我们未指定任何特定行的缺失行(在注释# 1 中已强调)。我们可能没有意识到这一点。

我们在第四行布局了复选框,然后我们“忘记”指定我们的 ScrolledText 小部件所在的行,我们通过 scr 变量引用这个小部件,接着我们添加了单选按钮小部件,以便在第六行布局。

这工作得很好,因为 tkinter 自动增加了我们的 ScrolledText 小部件的行位置,以便使用下一个最高的行号,即第 5 行。

看着我们的代码,没有意识到我们“忘记”将 ScrolledText 小部件显式地定位到第 5 行,我们可能会认为那里没有任何内容。

因此,我们可能会尝试以下方法。

如果我们将变量 curRad 设置为使用第 5 行,我们可能会得到一个不愉快的惊喜:

如何做...

它是如何工作的...

注意我们的 RadioButton(s)行突然出现在 ScrolledText 小部件的中间!这绝对不是我们想要我们的 GUI 看起来像的样子!

注意事项

如果我们忘记明确指定行号,默认情况下,tkinter将使用下一个可用的行。

我们还使用了columnspan属性来确保我们的小部件不会仅限于一个列。以下是确保我们的 ScrolledText 小部件跨越我们 GUI 所有列的方法:

# Using a scrolled Text control    
scrolW = 30; scrolH = 3
scr = ScrolledText(monty, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=0, sticky='WE', columnspan=3)

第三章. 外观和感觉自定义

在本章中,我们将使用 Python 3 来定制我们的图形用户界面:

  • 创建消息框 – 信息、警告和错误

  • 如何创建独立的消息框

  • 如何创建 tkinter 窗口的标题

  • 更改主根窗口的图标

  • 使用旋转框控件

  • 小部件的缓解、凹陷和凸起外观

  • 使用 Python 创建工具提示

  • 如何使用画布小部件

简介

在本章中,我们将通过更改一些属性来定制我们 GUI 中的某些小部件。同时,我们还将介绍一些 tkinter 为我们提供的新小部件。

使用 Python 创建工具提示 的配方将创建一个 OOP 风格的工具提示类,这将是我们迄今为止一直使用的单个 Python 模块的一部分。

创建消息框 – 信息、警告和错误

消息框是一种弹出窗口,用于向用户反馈信息。它可以提供信息性内容,暗示潜在问题,甚至可能显示灾难性错误。

使用 Python 创建消息框非常简单。

准备就绪

我们将在前一个菜谱中创建的帮助 | 关于菜单项中添加功能。在大多数应用程序中,当点击帮助 | 关于菜单时,用户通常会收到信息反馈。我们从这个信息开始,然后改变设计模式以显示警告和错误。

如何做到这一点...

将以下代码行添加到包含导入语句的模块顶部:

from tkinter import messagebox as mBox

接下来,我们将创建一个回调函数来显示一个消息框。我们必须将回调函数的代码放在我们将回调附加到菜单项的代码之上,因为这部分代码仍然是过程式代码,而不是面向对象的代码。

在创建帮助菜单的行上方添加此代码:

# Display a Message Box
# Callback function
def _msgBox():
    mBox.showinfo('Python Message Info Box', 'A Python GUI created using tkinter:\nThe year is 2015.')   

# Add another Menu to the Menu Bar and an item
helpMenu = Menu(menuBar, tearoff=0)
helpMenu.add_command(label="About", command=_msgBox)

现在点击帮助 | 关于将显示以下弹出窗口:

如何做...

让我们将这段代码转换为一个警告信息弹窗。注释掉上一行,并添加以下代码:

# Display a Message Box
def _msgBox():
#    mBox.showinfo('Python Message Info Box', 'A Python GUI 
#      created using tkinter:\nThe year is 2015.')
    mBox.showwarning('Python Message Warning Box', 'A Python GUI created using tkinter:\nWarning: There might be a bug in this code.')

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

如何做...

显示错误信息框很简单,通常它会警告用户存在一个严重问题。正如上面所说,下面也是如此。注释掉并添加以下代码,就像我们在这里所做的那样:

# Display a Message Box
def _msgBox():
#    mBox.showinfo('Python Message Info Box', 'A Python GUI 
#      created using tkinter:\nThe year is 2015.')
#    mBox.showwarning('Python Message Warning Box', 'A Python GUI 
#      created using tkinter:\nWarning: There might be a bug in 
#      this code.')
    mBox.showerror('Python Message Error Box', 'A Python GUI created using tkinter:\nError: Houston ~ we DO have a serious PROBLEM!')

如何做...

它是如何工作的...

我们添加了另一个回调函数,并将其作为代理附加以处理点击事件。现在,当我们点击帮助 | 关于菜单时,将执行一个操作。我们正在创建并显示最常见的弹出消息框对话框。它们是模态的,因此用户在点击确定按钮之前无法使用 GUI。

在第一个例子中,我们展示了一个信息框,如它左侧的图标所示。接下来,我们创建了警告和错误信息框,这些信息框会自动更改与弹出窗口关联的图标。我们只需指定要显示哪个 mBox 即可。

有不同的消息框显示多个确定按钮,并且我们可以根据用户的选取编程我们的响应。

以下是一个简单示例,说明了这项技术:

# Display a Message Box
def _msgBox():
    answer = mBox.askyesno("Python Message Dual Choice Box", "Are you sure you really wish to do this?")
    print(answer)

运行此 GUI 代码将弹出一个窗口,用户对该窗口的响应可以用来根据此事件驱动 GUI 循环的答案进行分支,并将其保存在answer变量中。

如何工作...

使用 Eclipse 的控制台输出显示,点击 按钮会将布尔值 True 赋值给 answer 变量。

如何工作...

例如,我们可以使用以下代码:

If answer == True:
    <do something>

如何创建独立的消息框

在这个菜谱中,我们将创建我们的 tkinter 消息框作为独立的顶层 GUI 窗口。

我们首先会注意到,这样做会导致多出一个窗口,因此我们将探讨隐藏这个窗口的方法。

在上一个菜谱中,我们通过主 GUI 表单的帮助 | 关于菜单调用了 tkinter 消息框。

那么我们为什么想要创建一个独立的消息框呢?

一个原因是,我们可能会自定义我们的消息框并在多个 GUI 中重复使用它们。我们不必在设计的每个 Python GUI 中复制和粘贴相同的代码,而是可以将这部分代码从主 GUI 代码中提取出来。这样就可以创建一个小的可重复使用的组件,然后我们可以将其导入到不同的 Python GUI 中。

准备就绪

我们已经在之前的菜谱中创建了消息框的标题。我们不会重用之前的代码,而是将通过非常少的 Python 代码行来构建一个新的 GUI。

如何做到这一点...

我们可以创建一个简单的消息框,如下所示:

from tkinter import messagebox as mBox
mBox.showinfo('A Python GUI created using tkinter:\nThe year is 2015')

这将导致出现这两个窗口:

如何做...

这看起来并不像我们想象中的样子。现在我们有两个窗口,一个是未期望的,另一个的文本显示为其标题。

哎呀。

让我们立即解决这个问题。我们可以通过添加一个单引号或双引号,然后跟一个逗号来修改 Python 代码。

mBox.showinfo('', 'A Python GUI created using tkinter:\nThe year is 2015')

如何做...

第一个参数是标题,第二个参数是在弹出消息框中显示的文本。通过添加一个空的单引号或双引号对,后面跟着一个逗号,我们可以将我们的文本从标题移动到弹出消息框中。

我们仍然需要一个标题,我们肯定想摆脱这个不必要的第二个窗口。

注意事项

在像 C#这样的语言中,也会出现第二个窗口的现象。这基本上是一个 DOS 风格的调试窗口。许多程序员似乎并不介意有这样一个额外的窗口漂浮在周围。我个人从 GUI 编程的角度来看,觉得这很令人不快。我们将在下一版本中移除它。

第二个窗口是由 Windows 事件循环引起的。我们可以通过抑制它来消除它。

添加以下代码:

from tkinter import messagebox as mBox
from tkinter import Tk
root = Tk()
root.withdraw()
mBox.showinfo('', 'A Python GUI created using tkinter:\nThe year is 2015')

现在我们只有一个窗口。withdraw() 函数移除了我们不希望漂浮在周围的调试窗口。

如何做...

为了添加标题,我们只需在我们的空第一个参数中放入一些字符串即可。

例如:

from tkinter import messagebox as mBox
from tkinter import Tk
root = Tk()
root.withdraw()
mBox.showinfo('This is a Title', 'A Python GUI created using tkinter:\nThe year is 2015')

现在我们的对话有一个标题:

如何做...

它是如何工作的...

我们向消息框的 tkinter 构造函数传递了更多的参数,以在窗口表单中添加标题并显示消息框中的文本,而不是将其显示为标题。这是由于我们传递参数的位置造成的。如果我们省略一个空引号或双引号,那么消息框小部件将参数的第一个位置作为标题,而不是消息框内要显示的文本。通过传递一个空引号后跟一个逗号,我们改变了消息框显示我们传递给函数的文本的位置。

我们通过在主根窗口上调用withdraw()方法来抑制由 tkinter 消息框小部件自动创建的第二个弹出窗口。

如何创建 tkinter 窗口的标题

改变 tkinter 主根窗口标题的原则与之前讨论的食谱相同。我们只需将一个字符串作为第一个参数传递给小部件的构造函数。

准备就绪

不同于弹出对话框,我们创建主根窗口并给它一个标题。

本食谱中显示的 GUI 是上一章的代码。它并不基于本章之前的食谱构建。

如何做到这一点...

以下代码创建主窗口并为其添加标题。我们已经在之前的菜谱中做过这个操作。在这里,我们只关注我们 GUI 的这个方面。

import tkinter as tk
win = tk.Tk()               # Create instance
win.title("Python GUI")     # Add a title

如何做...

它是如何工作的...

这通过使用内置的 tkinter title 属性为主要的根窗口提供了一个标题。在我们创建一个 Tk() 实例之后,我们可以使用所有内置的 tkinter 属性来自定义我们的 GUI。

更改主根窗口的图标

修改我们的 GUI 的一种方法是为它提供一个不同于 tkinter 默认图标的不同图标。以下是我们的操作步骤。

准备就绪

我们正在改进之前的 GUI。我们将使用随 Python 一起提供的图标,但你也可以使用任何你认为有用的图标。确保你有图标所在位置的完整路径,否则可能会出现错误。

注意

虽然可能有点令人困惑,不知道这个菜谱指的是上一章的哪个菜谱,但最好的方法就是直接下载这本书的代码,然后逐步阅读代码来理解它。

如何做到这一点...

将以下代码放置在主事件循环上方某处。示例使用的是我安装 Python 3.4 的路径。你可能需要调整它以匹配你的安装目录。

注意到 GUI 左上角的默认“羽毛”图标已发生变化。

# Change the main windows icon
win.iconbitmap(r'C:\Python34\DLLs\pyc.ico')

如何做...

它是如何工作的...

这是 tkinter 自带的一个属性,tkinter 是 Python 3.x 的一部分。iconbitmap是我们要使用的属性,通过传入一个图标文件的绝对(硬编码)路径来改变我们主根窗口的图标。这会覆盖 tkinter 的默认图标,用我们选择的图标替换它。

注意事项

在上述代码的绝对路径字符串中使用 "r" 可以转义反斜杠,因此我们不必写 C:\\,而是可以使用“原始”字符串,这样我们就可以写出更自然的单个反斜杠 C:\。这是 Python 为我们创造的一个巧妙技巧。

使用旋转框控件

在这个菜谱中,我们将使用一个Spinbox小部件,并且还将把键盘上的Enter键绑定到我们的小部件之一。

准备就绪

我们正在使用我们的标签式 GUI,并将一个Spinbox小部件添加到ScrolledText控件上方。这仅仅需要我们将ScrolledText的行值增加一个,并在Entry小部件的上方插入我们的新Spinbox控件。

如何做到这一点...

首先,我们添加Spinbox控件。将以下代码放置在ScrolledText小部件之上:

# Adding a Spinbox widget
spin = Spinbox(monty, from_=0, to=10)
spin.grid(column=0, row=2)

这将修改我们的图形用户界面,如下所示:

如何做...

接下来,我们将减小Spinbox小部件的大小。

spin = Spinbox(monty, from_=0, to=10, width=5)

如何做...

接下来,我们添加另一个属性以进一步自定义我们的小部件,bdborderwidth 属性的简写表示。

spin = Spinbox(monty, from_=0, to=10, width=5 , bd=8)

如何做...

在这里,我们通过创建一个回调并将其链接到控件来为小部件添加功能。

这将把 Spinbox 的选择打印到 ScrolledText 中,以及输出到 stdout。名为 scr 的变量是我们对 ScrolledText 小部件的引用。

# Spinbox callback 
def _spin():
    value = spin.get()
    print(value)
    scr.insert(tk.INSERT, value + '\n')

spin = Spinbox(monty, from_=0, to=10, width=5, bd=8, command=_spin)

如何做...

我们也可以指定一组值,而不是使用一个范围。

# Adding a Spinbox widget using a set of values
spin = Spinbox(monty, values=(1, 2, 4, 42, 100), width=5, bd=8, command=_spin) 
spin.grid(column=0, row=2)

这将创建以下 GUI 输出:

如何做...

它是如何工作的...

注意,在第一张截图里,我们新的Spinbox控件默认宽度为 20,这导致该列中所有控件的列宽都被推宽了。这并不是我们想要的效果。我们给小部件设置了从 0 到 10 的范围,但它默认显示to=10的值,这是最大值。如果我们尝试将from_/to范围从 10 反转到 0,tkinter 并不喜欢这样做。请亲自尝试一下。

在第二张截图,我们减小了Spinbox控制条的宽度,使其与列的中心对齐。

在第三张截图,我们添加了 Spinbox 的 borderwidth 属性,这使得整个 Spinbox 自动呈现出不再扁平而是三维的效果。

在第四张截图,我们添加了一个回调函数来显示在ScrolledText小部件中选择的数字,并将其打印到标准输出流。我们添加了"\n"来换行打印。注意默认值并没有被打印出来。只有当我们点击控件时,回调函数才会被调用。通过点击带有默认值 10 的上箭头,我们可以打印出"10"这个值。

最后,我们将可用的值限制在一个硬编码的集合中。这些值也可以从数据源(例如,一个文本或 XML 文件)中读取。

小部件的缓解、凹陷和凸起外观

我们可以通过一个属性来控制我们的Spinbox小部件的外观,使其看起来是凸起的、凹进的,或者以提升格式显示。

准备就绪

我们将添加一个额外的Spinbox控件来演示使用Spinbox控件的浮雕属性所能提供的控件外观。

如何做到这一点...

首先,让我们将borderwidth增加,以便区分我们的第二个Spinbox和第一个Spinbox

# Adding a second Spinbox widget 
spin = Spinbox(monty, values=(0, 50, 100), width=5, bd=20, command=_spin) 
spin.grid(column=1, row=2)

如何做...

我们上面的两个 Spinbox 小部件具有相同的浮雕样式。唯一的区别是,位于第一个 Spinbox 右侧的新部件具有更大的边框宽度。

在我们的代码中,我们没有指定使用哪种缓解属性,因此缓解默认为 tk.SUNKEN。

这里是可以设置的可用缓解属性选项:

通过将不同的可用选项分配给relief属性,我们可以为这个小部件创建不同的外观。

将 tk.RIDGE 风格应用于边框并使边框宽度与我们的第一个 Spinbox 小部件相同,结果得到以下 GUI:

如何做...

它是如何工作的...

首先,我们在第二列(索引 == 1)创建了一个第二个 Spinbox。它默认为 SUNKEN,因此看起来与我们的第一个 Spinbox 类似。我们通过增加第二个控件(右侧的控件)的边框宽度来区分这两个小部件。

接下来,我们隐式地设置了 Spinbox 小部件的凹凸属性。我们将边框宽度设置为与我们的第一个 Spinbox 相同,因为通过给它不同的凹凸效果,差异在没有改变其他任何属性的情况下就变得明显了。

使用 Python 创建工具提示

这个菜谱将向我们展示如何创建工具提示。当用户将鼠标悬停在控件上时,将以工具提示的形式提供额外的信息。

我们将把这个额外信息编码到我们的图形用户界面中。

准备就绪

我们正在为我们的图形用户界面添加更多有用的功能。令人惊讶的是,给我们的控件添加工具提示应该很简单,但实际上并没有我们希望的那样简单。

为了实现这个期望的功能,我们将把我们的工具提示代码放入它自己的面向对象(OOP)类中。

如何做到这一点...

在导入语句下方添加此类:

class ToolTip(object):
    def __init__(self, widget):
        self.widget = widget
        self.tipwindow = None
        self.id = None
        self.x = self.y = 0

    def showtip(self, text):
        "Display text in tooltip window"
        self.text = text
        if self.tipwindow or not self.text:
            return
        x, y, _cx, cy = self.widget.bbox("insert")
        x = x + self.widget.winfo_rootx() + 27
        y = y + cy + self.widget.winfo_rooty() +27
        self.tipwindow = tw = tk.Toplevel(self.widget)
        tw.wm_overrideredirect(1)
        tw.wm_geometry("+%d+%d" % (x, y))

        label = tk.Label(tw, text=self.text, justify=tk.LEFT,
   background="#ffffe0", relief=tk.SOLID, borderwidth=1,
                      font=("tahoma", "8", "normal"))

        label.pack(ipadx=1)

    def hidetip(self):
        tw = self.tipwindow
        self.tipwindow = None
        if tw:
            tw.destroy()

#===========================================================
def createToolTip( widget, text):
    toolTip = ToolTip(widget)
    def enter(event):
        toolTip.showtip(text)
    def leave(event):
        toolTip.hidetip()
    widget.bind('<Enter>', enter)
    widget.bind('<Leave>', leave)

在面向对象编程(OOP)方法中,我们在 Python 模块中创建一个新的类。Python 允许我们在同一个 Python 模块中放置多个类,并且它还使我们能够在同一个模块中“混合匹配”类和常规函数。

上面的代码正是这样做的。

ToolTip类是一个 Python 类,为了使用它,我们必须实例化它。

如果你对面向对象编程(OOP)不熟悉,"实例化一个对象以创建类的实例"可能听起来相当无聊。

原理非常简单,非常类似于通过def语句创建一个 Python 函数,然后在代码的后续部分实际调用这个函数。

以非常相似的方式,我们首先创建一个类的蓝图,并通过在类名后添加括号将其简单地分配给一个变量,如下所示:

class AClass():
    pass
instanceOfAClass = AClass()
print(instanceOfAClass)

上述代码打印出一个内存地址,同时也显示我们的变量现在对这个类实例有一个引用。

面向对象编程(OOP)的酷之处在于我们可以创建同一类的多个实例。

在我们之前的代码中,我们声明了一个 Python 类,并明确使其继承自所有 Python 类的基础对象。我们也可以像在AClass代码示例中所做的那样省略它,因为这是所有 Python 类的默认设置。

ToolTip类中完成所有必要的提示信息创建代码之后,我们接下来通过创建一个位于其下方的函数来转向非面向对象的 Python 编程。

我们定义了函数 createToolTip(),它期望传入我们 GUI 小部件中的一个作为参数,这样我们就可以在鼠标悬停在此控件上时显示工具提示。

createToolTip() 函数实际上为每个我们调用它的小部件创建我们 ToolTip 类的新实例。

我们可以为我们的 Spinbox 小部件添加一个工具提示,如下所示:

# Add a Tooltip
createToolTip(spin, 'This is a Spin control.')

以及以完全相同的方式处理我们所有的其他 GUI 小部件。我们只需传入我们希望显示额外信息的工具提示的父小部件。对于我们的 ScrolledText 小部件,我们让变量scr指向它,因此这就是我们传递给我们的工具提示创建函数构造函数的内容。

# Using a scrolled Text control    
scrolW  = 30; scrolH  =  3
scr = scrolledtext.ScrolledText(monty, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=0, row=3, sticky='WE', columnspan=3)

# Add a Tooltip to the ScrolledText widget
createToolTip(scr, 'This is a ScrolledText widget.')

它是如何工作的...

这本书中面向对象编程(OOP)的开始。这可能会显得有些高级,但请不要担心,我们会解释一切,实际上它确实可行!

好吧,运行这段代码实际上目前还没有起作用或者产生任何变化。

在创建旋转器的下方添加以下代码:

# Add a Tooltip
createToolTip(spin, 'This is a Spin control.')

现在,当我们将鼠标悬停在旋转控件上时,我们会看到一个工具提示,为用户提供额外的信息。

如何工作...

我们正在调用创建工具提示的函数,然后我们传递一个对小部件的引用以及当我们将鼠标悬停在工具上时要显示的文本。

本书余下的食谱将在适用时使用面向对象编程(OOP)。在此,我们展示了最简单的面向对象编程示例。默认情况下,我们创建的每个 Python 类都继承自object基类。Python,作为它真正所是的实用编程语言,简化了类的创建过程。

我们可以写出这个语法:

class ToolTip(object):
    pass

我们也可以通过省略默认基类来简化它:

class ToolTip():
    pass

在这个相同的模式中,我们可以继承和扩展任何 tkinter 类。

如何使用画布小部件

本食谱展示了如何通过使用 tkinter 画布小部件来为我们的 GUI 添加戏剧性的色彩效果。

准备就绪

我们将通过添加更多颜色来改进我们之前的代码和 GUI 的外观。

如何做到这一点...

首先,我们将在我们的 GUI 中创建一个第三个标签页,以便隔离我们的新代码。

这里是创建新第三个标签的代码:

# Tab Control introduced here --------------------------------
tabControl = ttk.Notebook(win)          # Create Tab Control

tab1 = ttk.Frame(tabControl)            # Create a tab 
tabControl.add(tab1, text='Tab 1')      # Add the tab

tab2 = ttk.Frame(tabControl)            # Add a second tab
tabControl.add(tab2, text='Tab 2')      # Make second tab visible

tab3 = ttk.Frame(tabControl)            # Add a third tab
tabControl.add(tab3, text='Tab 3')      # Make second tab visible

tabControl.pack(expand=1, fill="both")  # Pack to make visible
# ~ Tab Control introduced here -------------------------------

接下来,我们使用另一个 tkinter 内置小部件,即画布。很多人喜欢这个小部件,因为它具有强大的功能。

# Tab Control 3 -------------------------------
tab3 = tk.Frame(tab3, bg='blue')
tab3.pack()
for orangeColor in range(2):
    canvas = tk.Canvas(tab3, width=150, height=80, highlightthickness=0, bg='orange')
    canvas.grid(row=orangeColor, column=orangeColor)

它是如何工作的...

以下截图显示了运行前面代码并点击新Tab 3所创建的结果。当你运行代码时,它确实是橙色和蓝色的。在这本非彩色的书中,这可能在视觉上不那么明显,但那些颜色是真实的;你可以相信我。

您可以通过在线搜索来查看图形和绘图功能。在这本书中,我不会深入探讨这个小部件(但它非常酷)。

如何工作...

第四章:数据和类

在本章中,我们将使用 Python 3 中的数据和面向对象类:

  • 如何使用 StringVar()

  • 如何从小部件获取数据

  • 使用模块级全局变量

  • 如何在课堂上编码来提升图形用户界面(GUI)

  • 编写回调函数

  • 创建可重用 GUI 组件

简介

在本章中,我们将把我们的 GUI 数据保存到 tkinter 变量中。

我们还将开始使用面向对象编程OOP)来扩展现有的 tkinter 类,以便扩展 tkinter 的内置功能。这将引导我们创建可重用的 OOP 组件。

如何使用 StringVar()

tkinter 中内置了一些编程类型,这些类型与我们习惯的 Python 类型略有不同。StringVar() 就是这些 tkinter 类型之一。

本食谱将向您展示如何使用 StringVar() 类型。

准备就绪

我们正在学习如何将 tkinter GUI 中的数据保存到变量中,以便我们可以使用这些数据。我们可以设置和获取它们的值,这与 Java 的 getter/setter 方法非常相似。

这里是 tkinter 中可用的一些编码类型:

strVar = StringVar() # 存储一个字符串;默认值是一个空字符串 ""
intVar = IntVar() # 存储一个整数;默认值是 0
dbVar = DoubleVar() # 存储一个浮点数;默认值是 0.0
blVar = BooleanVar() # 存储布尔值,它返回 0 表示假,1 表示真

注意事项

不同的语言用小数点、浮点数或双精度数来表示数字。Tkinter 将 Python 中的浮点数据类型称为 DoubleVar。根据精度的不同,浮点数和双精度数可能会有所不同。在这里,我们将 tkinter 的 DoubleVar 转换为 Python 转换为 Python 浮点类型的结果。

如何做到这一点...

我们正在创建一个新的 Python 模块,以下截图展示了代码和生成的输出:

如何做...

首先,我们导入 tkinter 模块并将其别名设置为tk

接下来,我们通过在Tk后添加括号来使用这个别名创建Tk类的一个实例,这会调用类的构造函数。这与调用函数的机制相同,只是在这里我们是在创建一个类的实例。

通常我们使用这个分配给变量win的实例来在代码的后面启动主事件循环。但在这里,我们不是显示一个 GUI,而是在演示如何使用 tkinter 的 StringVar 类型。

注意事项

我们仍然需要创建一个Tk()实例。如果我们取消注释这一行,我们将从 tkinter 获得错误,因此这个调用是必要的。

然后我们创建一个 tkinter StringVar 类型的实例,并将其分配给我们的 Python strData 变量。

之后,我们使用我们的变量来调用 StringVar 的set()方法,在将其设置为某个值之后,我们获取这个值并将其保存在一个名为varData的新变量中,然后打印出它的值。

在 Eclipse PyDev 控制台中,截图的底部我们可以看到打印到控制台的信息,即 Hello StringVar

接下来,我们将打印 tkinter 的 IntVar、DoubleVar 和 BooleanVar 类型的默认值。

如何做...

它是如何工作的...

如前一张截图所示,默认值并没有像我们预期的那样被打印出来。

在在线文献中提到了默认值,但我们在调用它们的get方法之前不会看到这些值。否则,我们只会得到一个自动递增的变量名(例如 PY_VAR3,如前一张截图所示)。

将 tkinter 类型分配给 Python 变量并不会改变结果。我们仍然没有得到默认值。

在这里,我们专注于最简单的代码(它创建 PY_VAR0):

如何工作...

值为 PY_VAR0,而不是预期的 0,直到我们调用 get 方法。现在我们可以看到默认值。我们没有调用 set,因此当我们对每个类型调用 get 方法时,我们看到默认值被自动分配给每个 tkinter 类型。

如何工作...

注意默认值 0 是如何打印到控制台上的,这是我们保存在intData变量中的IntVar实例。我们还可以在截图顶部的 Eclipse PyDev 调试器窗口中看到这些值。

如何从小部件获取数据

当用户输入数据时,我们希望在代码中对它进行处理。本菜谱展示了如何将数据捕获到变量中。在前一个菜谱中,我们创建了几个 tkinter 类变量。它们是独立的。现在,我们正在将它们连接到我们的 GUI,使用从 GUI 获取的数据并将其存储在 Python 变量中。

准备就绪

我们正在继续使用我们在上一章中构建的 Python 图形用户界面。

如何做到这一点...

我们将 GUI 中的值赋给一个 Python 变量。

在我们模块的底部添加以下代码,位于主事件循环之上:

strData = spin.get()
print("Spinbox value: " + strData)

# Place cursor into name Entry
nameEntered.focus()      
#======================
# Start GUI
#======================
win.mainloop()

运行代码得到以下结果:

如何做...

我们正在检索Spinbox控件当前值。

注意事项

我们将代码放置在 GUI 主事件循环之上,因此打印操作发生在 GUI 可见之前。如果我们想在显示 GUI 并更改Spinbox控件值之后打印当前值,我们就必须将代码放在回调函数中。

我们使用以下代码创建了我们的 Spinbox 小部件,并将可用的值硬编码到其中:

# Adding a Spinbox widget using a set of values
spin = Spinbox(monty, values=(1, 2, 4, 42, 100), width=5, bd=8, command=_spin) 
spin.grid(column=0, row=2)

我们也可以将数据的硬编码从Spinbox类实例的创建中移除,并在之后设置它。

# Adding a Spinbox widget assigning values after creation
spin = Spinbox(monty, width=5, bd=8, command=_spin) 
spin['values'] = (1, 2, 4, 42, 100)
spin.grid(column=0, row=2)

我们创建小部件并插入数据的方式并不重要,因为我们可以通过在部件实例上使用get()方法来访问这些数据。

它是如何工作的...

为了从使用 tkinter 编写的 GUI 中获取值,我们使用 tkinter 的 get() 方法在我们要获取值的控件实例上。

在上述示例中我们使用了 Spinbox 控件,但对于所有具有 get() 方法的控件,其原理都是相同的。

一旦我们获取了数据,我们就进入了纯 Python 的世界,tkinter 确实为我们构建 GUI 提供了帮助。现在我们知道了如何从 GUI 中获取数据,我们可以使用这些数据。

使用模块级全局变量

封装是任何编程语言的主要优势之一,它使我们能够使用面向对象编程(OOP)进行编程。Python 既是面向对象又是过程式的。我们可以创建全局变量,这些变量仅限于它们所在的模块。它们仅对这个模块是全局的,这是封装的一种形式。我们为什么想要这样做呢?因为,随着我们向我们的图形用户界面(GUI)添加越来越多的功能,我们希望避免命名冲突,这可能导致代码中的错误。

注意事项

我们不希望命名冲突在我们的代码中产生错误!命名空间是避免这些错误的一种方法,在 Python 中,我们可以通过使用 Python 模块(这些是非官方的命名空间)来实现这一点。

准备就绪

我们可以在任何模块中声明模块级别的全局变量,只需在函数之上和之外即可。

我们随后必须使用 Python 的 global 关键字来引用它们。如果我们忘记在函数中使用 global,我们将会意外地创建新的局部变量。这将是一个错误,是我们真的不希望做的事情。

注意事项

Python 是一种动态、强类型语言。我们只有在运行时才会注意到这类错误(忘记使用全局关键字来作用域变量)。

如何做到这一点...

将第 15 行显示的代码添加到我们在上一道菜谱和上一章中使用的 GUI 中,这将创建一个模块级别的全局变量。我们使用 C 风格的全部大写约定,这并不真正是“Pythonic”,但我认为这确实强调了我们在本菜谱中要处理的原则。

如何做...

运行代码会在全局变量上打印输出。注意42被打印到了 Eclipse 控制台。

如何做...

它是如何工作的...

我们在模块顶部定义一个全局变量,随后在模块底部,我们打印出它的值。

那是有效的。

将此函数添加到我们模块的底部:

如何工作...

如上所述,我们正在使用模块级别的全局变量。由于在下面的截图中所展示的,通过覆盖global变量很容易犯错:

如何工作...

注意如何42变成了777,尽管我们使用了相同的变量名。

注意事项

Python 中没有编译器会警告我们在局部函数中覆盖全局变量。这可能导致在运行时调试出现困难。

使用全局限定符(第 234 行)将打印出我们最初分配给它的值(42),如下面的截图所示:

如何工作...

但是,请注意。当我们取消对局部全局变量的注释时,我们打印出的是局部变量的值,而不是全局变量的值:

如何工作...

尽管我们使用了global限定符,但局部变量似乎覆盖了它。我们从 Eclipse PyDev 插件那里收到了警告,指出我们的GLOBAL_CONST = 777没有被使用,然而运行代码仍然打印出 777 而不是预期的 42。

这可能不是我们预期的行为。使用global限定符,我们可能期望我们正在指向之前创建的全局变量。

相反,似乎 Python 在局部函数中创建了一个新的全局变量,并覆盖了我们之前创建的那个。

全局变量在编写小型应用程序时非常有用。它们可以帮助在同一个 Python 模块内的方法和函数之间共享数据,有时面向对象的额外开销也是不必要的。

随着我们的程序复杂性增加,从使用全局变量中获得的益处可能会迅速减少。

注意事项

最好避免使用全局变量,并通过在不同作用域中使用相同的名称意外地覆盖变量。我们可以使用面向对象编程(OOP)来代替使用全局变量。

我们在过程式代码中玩弄全局变量,并了解到这可能导致难以调试的错误。在下一章中,我们将转向面向对象编程(OOP),这可以消除这类错误。

如何在课堂上编码来提升图形用户界面(GUI)

到目前为止,我们一直在使用过程式编程风格。这是一种快速的 Python 脚本方法。一旦我们的代码变得越来越大,我们就需要进阶到面向对象编程(OOP)的编程方式。

为什么?

因为,在众多其他好处中,面向对象编程(OOP)允许我们通过使用方法来移动代码。一旦我们使用了类,就不再需要物理地将代码放置在调用它的代码之上。这使我们能够极大地提高代码组织的灵活性。

我们可以将相关代码写在其他代码旁边,再也不必担心代码无法运行,因为代码不再位于调用它的代码之上。

我们可以通过编写模块来达到一些相当复杂的极端情况,这些模块引用的方法不是在该模块内创建的。它们依赖于在代码运行期间,运行时状态已经创建了这些方法。

注意事项

如果我们称之为的方法在那个时间点尚未被创建,我们将得到一个运行时错误。

准备就绪

我们将非常简单地把我们所有的过程性代码转换为面向对象编程(OOP)。我们只需将其转换为一个类,缩进所有现有代码,并将self前缀添加到所有变量之前。

这非常简单。

虽然一开始可能觉得在每样东西前都加上self关键字有点烦人,使得我们的代码更加冗长(嘿,我们浪费了这么多纸张……);但最终,这将是值得的。

如何做到这一点...

在一开始,一切似乎都陷入了混乱,但我们很快就会解决这个问题表面的混乱。

注意,在 Eclipse 中,PyDev 编辑器通过在代码编辑器的右侧部分用红色突出显示来提示编码问题。

也许我们最终不应该使用面向对象编程(OOP)来编码,但这是我们正在做的事情,而且有非常好的理由。

如何做...

我们只需在所有变量前加上self关键字,并且通过使用self将函数绑定到类上,这样在官方和技术上正式地将函数转换为方法。

注意

函数和方法之间有区别。Python 将这一点阐述得非常清晰。方法绑定到类上,而函数则不是。我们甚至可以在同一个 Python 模块中混合使用这两种。

让我们给所有内容都加上self前缀,以消除红色提示,这样我们就可以再次运行我们的代码了。

如何做...

一旦我们处理完所有用红色突出显示的错误,我们就可以再次运行我们的 Python 代码。

clickMe 函数现在已绑定到类中,并正式成为了一个方法。

很不幸,从过程式编程开始然后转换为面向对象编程并不像我上面所说的那么简单。代码变得一团糟。这正是开始用 Python 使用面向对象范式编程的一个很好的理由。

注意事项

Python 擅长以简单的方式完成任务。简单的代码往往变得更为复杂(因为一开始它就是简单的)。一旦我们变得过于复杂,将我们的过程性代码重构为真正意义上的面向对象代码就会随着每一行代码的增加而变得更加困难。

我们正在将我们的过程式代码转换为面向对象的代码。看看我们给自己带来的所有麻烦,仅仅将 200 多行 Python 代码转换为面向对象(OOP)可能意味着我们最好从一开始就使用面向对象编程。

我们实际上破坏了一些之前正常工作的功能。使用 Tab 2 并点击单选按钮不再起作用。我们不得不进行更多的重构。

程序代码在某种程度上是简单的,因为它只是从上到下的编码。现在我们将代码放入了一个类中,我们必须将所有回调函数移动到方法中。这可行,但确实需要一些工作来转换我们的原始代码。

我们的程序代码看起来是这样的:

# Button Click Function
def clickMe():
    action.configure(text='Hello ' + name.get())

# Changing our Label
ttk.Label(monty, text="Enter a name:").grid(column=0, row=0, sticky='W')

# Adding a Textbox Entry widget
name = tk.StringVar()
nameEntered = ttk.Entry(monty, width=12, textvariable=name)
nameEntered.grid(column=0, row=1, sticky='W')

# Adding a Button
action = ttk.Button(monty, text="Click Me!", command=clickMe)
action.grid(column=2, row=1)

The new OOP code looks like this:
class OOP():
    def __init__(self): 
        # Create instance
        self.win = tk.Tk()   

        # Add a title       
        self.win.title("Python GUI")      
        self.createWidgets()

    # Button callback
    def clickMe(self):
        self.action.configure(text='Hello ' + self.name.get())

    # … more callback methods 

    def createWidgets(self):    
        # Tab Control introduced here -----------------------
        tabControl = ttk.Notebook(self.win)     # Create Tab Control

        tab1 = ttk.Frame(tabControl)            # Create a tab 
        tabControl.add(tab1, text='Tab 1')      # Add the tab

        tab2 = ttk.Frame(tabControl)            # Create second tab
        tabControl.add(tab2, text='Tab 2')      # Add second tab 

        tabControl.pack(expand=1, fill="both")  # Pack make visible
#======================
# Start GUI
#======================
oop = OOP()
oop.win.mainloop()

我们将回调方法移动到了模块的顶部,在新的面向对象类内部。我们将所有的小部件创建代码合并到一个相对较长的函数中,我们称这个函数为类的初始化器中调用的函数。

技术上讲,在底层代码的深处,Python 确实有一个构造函数,然而 Python 让我们免于对此有任何担忧。它已经由系统为我们处理好了。

相反,除了一个“真实”的构造函数外,Python 还为我们提供了一个初始化器。

我们强烈建议使用这个初始化器。我们可以用它向我们的类传递参数,初始化我们希望在类实例内部使用的变量。

注意事项

在 Python 中,同一个 Python 模块中可以存在多个类。

与 Java 不同,Java 有一个非常严格的命名规范(没有这个规范就无法工作),而 Python 则要灵活得多。

注意事项

我们可以在同一个 Python 模块中创建多个类。与 Java 不同,我们不需要依赖一个必须与每个类名匹配的文件名。

Python 真的太棒了!

当我们的 Python 图形用户界面变得庞大时,我们将把一些类拆分到它们自己的模块中,但与 Java 不同,我们不必这样做。在这本书和项目中,我们将保持一些类在同一个模块中,同时,我们还将把一些其他类拆分到它们自己的模块中,并将它们导入可以被认为是 main() 函数的部分(这虽然不是 C 语言,但我们可以以 C 语言的方式去思考,因为 Python 非常灵活)。

我们目前所取得的成就是将ToolTip类添加到我们的 Python 模块中,并将我们的过程式 Python 代码重构为面向对象(OOP)的 Python 代码。

在这个菜谱中,我们可以看到多个类可以存在于同一个 Python 模块中。

确实很酷!

如何做...

ToolTip 类和 OOP 类都位于同一个 Python 模块中。

如何做...

它是如何工作的...

在这个菜谱中,我们将我们的过程式代码提升到了面向对象编程(OOP)代码。

Python 使我们能够以类似于 C 编程语言的实际、过程式风格编写代码。

同时,我们也有选择以面向对象(OOP)风格进行编码的选项,例如 Java、C# 和 C++。

编写回调函数

最初,回调函数可能会显得有些令人畏惧。你调用函数,传递给它一些参数,然后这个函数告诉你它真的很忙,它将会回过头来调用你!

你会想:“这个功能再联系我吗?”“我需要多久?”

在 Python 中,即使是回调函数也容易使用,是的,它们通常确实会回调你。

他们只需首先完成分配给他们的任务(嘿,毕竟是你最初为他们编写代码的……)。

让我们更深入地了解一下,当我们把回调函数编码到我们的图形用户界面(GUI)中时会发生什么。

我们的图形用户界面是事件驱动的。创建并显示在屏幕上后,它通常就坐在那里等待事件发生。它基本上是在等待事件被发送给它。我们可以通过点击其操作按钮之一来向我们的 GUI 发送事件。

这创建了一个事件,从某种意义上说,我们通过发送消息“调用”了我们的 GUI。

现在,在我们向 GUI 发送消息后,应该发生什么情况?

点击按钮后会发生什么取决于我们是否创建了一个事件处理器并将其与该按钮关联。如果我们没有创建事件处理器,点击按钮将没有任何效果。

事件处理器是一个回调函数(或者,如果我们使用类,是方法)。

回调方法也像我们的 GUI 一样,在那里被动地等待被调用。

一旦我们的 GUI 按钮被点击,它将调用回调函数。

回调函数通常会进行一些处理,完成后将结果返回到我们的图形用户界面(GUI)。

注意事项

在某种意义上,我们可以看到我们的回调函数正在回调到我们的 GUI。

准备就绪

Python 解释器会遍历项目中的所有代码一次,查找任何语法错误并指出它们。如果你没有正确使用语法,就无法运行你的 Python 代码。这包括缩进(即使不会导致语法错误,错误的缩进通常也会导致错误)。

在下一轮解析中,解释器将解析我们的代码并运行它。

在运行时,可以生成许多 GUI 事件,通常是通过回调函数来为 GUI 小部件添加功能。

如何做到这一点...

这里是 Spinbox 小部件的回调函数:

如何做...

它是如何工作的...

我们在OOP类中创建了一个回调方法,当我们在 Spinbox 小部件中选择一个值时会被调用,因为我们通过command参数(command=self._spin)将方法绑定到小部件。我们使用前导下划线来暗示这个方法应该像私有 Java 方法一样受到尊重。

Python 故意避免了诸如私有、公有、友元等语言限制。

在 Python 中,我们使用命名约定。期望关键字周围的前导和尾随双下划线仅限于 Python 语言,我们不应在我们的 Python 代码中使用它们。

然而,我们可以使用一个前置下划线前缀来命名变量或函数,以提供提示,表明这个名称应该被视为一个私有辅助者。

同时,如果我们希望使用否则会是 Python 内置名称的变量,我们可以给其后缀一个单下划线。例如,如果我们想缩写列表的长度,我们可以这样做:

len_ = len(aList)

通常,下划线难以阅读且容易忽略,因此在实践中这可能不是最好的主意。

创建可重用的 GUI 组件

我们正在使用 Python 创建可重用的 GUI 组件。

在这个示例中,我们将通过将我们的 ToolTip 类移动到它自己的模块中来保持简单。接下来,我们将导入并使用它来在我们的 GUI 的几个小部件上显示工具提示。

准备工作

我们正在基于之前的代码进行构建。

如何做到这一点...

我们将首先将我们的ToolTip类拆分到一个独立的 Python 模块中。我们将对其进行轻微增强,以便在鼠标悬停在控件上时,能够传入我们希望显示的控件小部件和提示文本。

我们创建一个新的 Python 模块,并将ToolTip类的代码放入其中,然后将其导入到我们的主模块中。

我们随后通过创建多个工具提示来重用导入的ToolTip类,这些工具提示会在鼠标悬停在我们的 GUI 小部件上时显示。

将我们常用的 ToolTip 类代码重构到一个独立的模块中,有助于我们从其他模块中复用这段代码。我们不再使用复制/粘贴/修改的方式,而是遵循 DRY 原则,将公共代码放置在唯一的位置,因此当我们修改代码时,所有导入它的模块将自动获取模块的最新版本。

注意事项

DRY 代表“不要重复自己”,我们将在后面的章节中再次探讨它。

我们可以通过将 Tab3 图像转换成一个可重复使用的组件来做类似的事情。

为了使这个菜谱的代码简单,我们移除了 Tab 3,但你可以在前一章的代码上进行实验。

如何做...


# Add a Tooltip to the Spinbox
tt.createToolTip(self.spin, 'This is a Spin control.')

# Add Tooltips to more widgets
tt.createToolTip(nameEntered, 'This is an Entry control.')
tt.createToolTip(self.action, 'This is a Button control.')
tt.createToolTip(self.scr, 'This is a ScrolledText control.')

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

如何做...

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

如何做...

导入语句看起来是这样的:

如何做...

并且拆分出来(即重构)的代码在单独的模块中看起来是这样的:

如何做...

它是如何工作的...

在前面的截图里,我们可以看到几个提示信息正在显示。对于主窗口的提示信息可能显得有些烦人,因此最好不为主窗口显示提示信息,因为我们真的希望突出各个小部件的功能。主窗口的表单有一个标题来解释其用途;无需提示信息。

第五章. Matplotlib 图表

在本章中,我们将使用 Python 3 和 Matplotlib 模块创建漂亮的图表。

  • 使用 Matplotlib 创建漂亮的图表

  • Matplotlib – 使用 pip 下载模块

  • Matplotlib – 使用 whl 扩展下载模块

  • 创建我们的第一个图表

  • 在图表上添加标签

  • 如何给图表添加图例

  • 比例图

  • 动态调整图表的缩放比例

简介

在本章中,我们将创建美观的图表,以直观地展示数据。根据数据源格式,我们可以在同一图表中绘制一个或多个数据列。

我们将使用 Python Matplotlib 模块来创建我们的图表。

为了创建这些图形图表,我们需要下载额外的 Python 模块,并且有几种安装它们的方法。

本章将解释如何下载 Matplotlib Python 模块、所有其他必需的 Python 模块以及完成这一任务的方法。

在安装了所需的模块之后,我们将创建我们自己的 Python 图表。

使用 Matplotlib 创建漂亮的图表

这份食谱向我们介绍了 Matplotlib Python 模块,它使我们能够使用 Python 3 创建可视化图表。

以下网址是一个开始探索 Matplotlib 世界的好地方,它将教会你如何创建本章未展示的许多图表:

matplotlib 用户截图

准备就绪

为了使用 Matplotlib Python 模块,我们首先必须安装此模块,以及几个其他相关的 Python 模块,例如 numpy。

如果你正在使用低于 3.4.3 版本的 Python,我建议你升级你的 Python 版本,因为在本章中我们将使用 Python pip 模块来安装所需的 Python 模块,而 pip 是从 3.4.3 版本开始安装的。

注意事项

使用早期版本的 Python 3 可以安装 pip,但这个过程并不直观,因此肯定最好是升级到 3.4.3 或更高版本。

如何做到这一点...

以下图片展示了使用 Python 和 Matplotlib 模块可以创建多么令人惊叹的图表。

我已从matplotlib.org/网站复制了以下代码,它创建了这张令人难以置信的图表。该网站上有很多示例,我鼓励您尝试它们,直到找到您喜欢创建的图表类型。

这里是创建图表的代码,少于 25 行 Python 代码,包括空白字符。

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()
ax = fig.gca(projection='3d')
X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)
X, Y = np.meshgrid(X, Y)
R = np.sqrt(X**2 + Y**2)
Z = np.sin(R)
surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False)

ax.set_zlim(-1.01, 1.01)

ax.zaxis.set_major_locator(LinearLocator(10))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))

fig.colorbar(surf, shrink=0.5, aspect=5)

plt.show()

运行代码将生成如下所示的图表:

如何做...

使用 Python 3.4 或更高版本以及 Eclipse PyDev 插件运行代码可能会显示一些未解决的导入错误。这看起来像是 PyDev 或 Java 中的一个错误。

如果你在使用 Eclipse 进行开发时遇到错误,请忽略它们,因为代码将能够成功运行。

它是如何工作的...

为了创建如图所示的前一个屏幕截图中的美丽图表,我们需要下载几个其他的 Python 模块。

以下食谱将指导我们如何成功下载所有必需的模块,这使我们能够创建我们自己的美丽图表。

Matplotlib – 使用 pip 下载模块

下载额外的 Python 模块的常用方法是使用 pip。pip 模块随 Python 最新版(3.4 及以上版本)预装。

注意事项

如果你正在使用较旧的 Python 版本,你可能需要自己下载 pip 和 setuptools。

除了使用 Python 安装程序外,还有几个预先编译的 Windows 可执行文件,这使得我们能够轻松安装 Python 模块,例如 Matplotlib。

本食谱将展示如何通过 Windows 可执行文件成功安装 Matplotlib,以及如何使用 pip 来安装 Matplotlib 库所需的附加模块。

准备就绪

我们要下载使用 Matplotlib 模块所需的 Python 模块,只需在我们的电脑上安装 Python 3.4(或更高版本)即可。

如何做到这一点...

我们可以通过从官方 Matplotlib 网站下载的 Windows 可执行文件来安装 Matplotlib。

确保您安装的 Matplotlib 版本与您正在使用的 Python 版本相匹配。例如,如果您在 64 位操作系统(如 Microsoft Windows 7)上安装了 Python 3.4,请下载并安装 Matplotlib-1.4.3.win-amd64-py3.4.exe

注意事项

可执行文件名中间的 "amd64" 表示您正在安装 64 位版本。如果您使用的是 32 位 x86 系统,那么安装 amd64 将不会生效。如果您已经安装了 32 位的 Python 版本并下载了 64 位的 Python 模块,也可能出现类似的问题。

如何做...

运行可执行文件将使我们开始,看起来是这样的:

如何做...

我们可以通过查看我们的 Python 安装目录来验证我们已经成功安装了 Matplotlib。

安装成功后,Matplotlib 文件夹将被添加到 site-packages 中。使用默认安装,Windows 上 site-packages 文件夹的完整路径为:

C:\Python34\Lib\site-packages\matplotlib\

如何做...

在官方 Matplotlib 网站上的最简单绘图示例需要使用 Python numpy 模块,所以让我们接下来下载并安装这个模块。

注意事项

Numpy 是一个数学模块,它不仅能够实现 Matplotlib 图表的绘图,而且功能远超 Matplotlib。如果你正在开发的软件需要大量的数学计算,你绝对应该检查一下 numpy。

有一个极好的网站为我们提供了快速链接,几乎涵盖了所有现有的 Python 模块。它通过指出哪些其他 Python 模块对于成功使用 Matplotlib 是必要的,并提供了下载这些模块的超链接,这使得我们能够快速且轻松地安装它们。

注意

这里是链接:

www.lfd.uci.edu/~gohlke/pythonlibs/

如何做...

注意到安装包的文件扩展名都以 whl 结尾。为了使用它们,我们必须安装 Python wheel 模块,并且我们使用 pip 来完成这一操作。

注意事项

轮子(Wheels)是 Python 分发的最新标准,旨在取代 eggs。

你可以在以下网站上找到更多详细信息:

pythonwheels.com/

最好以管理员身份运行 Windows 命令处理器,以避免潜在的安装错误。

如何做...

它是如何工作的...

下载 Python 模块的常见方法是使用 pip,如上所示。为了安装 Matplotlib 所需的所有模块,我们可以下载它们的主网站上的下载格式已更改为使用 whl 格式。

下一个菜谱将解释如何使用 wheel 安装 Python 模块。

Matplotlib – 使用 whl 扩展下载模块

我们将使用 Matplotlib 所需的几个额外的 Python 模块,在这个菜谱中,我们将使用 Python 的新模块分发标准,称为 wheel,来下载它们。

注意事项

您可以在以下网址找到关于新轮标准(wheel standard)的 Python 增强提案(PEP):www.python.org/dev/peps/pep-0427/

准备就绪

为了下载带有 whl 扩展名的 Python 模块,必须首先安装 Python wheel 模块,这在之前的菜谱中已有解释。

如何做到这一点...

让我们从网络上下载 numpy-1.9.2+mkl-cp34-none-win_amd64.whl。安装完轮子模块后,我们可以使用 pip 来安装带有 whl 文件扩展名的包。

注意事项

Pip 与 Python 3.4.3 及以上版本兼容。如果你正在使用较旧的 Python 版本,我建议安装 pip,因为它使得安装所有其他额外的 Python 模块变得容易得多。

一个更好的建议可能是将您的 Python 版本升级到最新的稳定版本。到您阅读这本书的时候,那很可能将是 Python 3.5.0 或更高版本。

Python 是免费软件。升级它对我们来说不花费任何费用。

浏览到要安装的软件包所在的文件夹,并使用以下命令进行安装:

pip install numpy-1.9.2+mkl-cp34-none-win_amd64.whl

如何做...

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

如何做...

我们还没有准备好运行前面的代码这一事实表明我们需要下载更多的模块。虽然一开始可能觉得下载更多模块有点烦人,但实际上这是一种代码复用的形式。

因此,让我们使用 pip 和 wheel 下载并安装所需的六个以及其他所有模块(如 dateutil、pyparsing 等),直到我们的代码运行正常并仅从几行 Python 代码中创建出一张漂亮的图表。

我们可以从刚刚用来安装 numpy 的同一网站下载所有所需的模块。这个网站甚至列出了我们正在安装的模块所依赖的所有其他模块,并且提供了超链接,可以直接跳转到位于该网站内的安装软件。

注意事项

如前所述,安装 Python 模块的网址是:www.lfd.uci.edu/~gohlke/pythonlibs/

它是如何工作的...

该网站使我们能够从一个便捷的地方下载许多 Python 模块,同时也提供了其他 Python 模块。并非所有显示的依赖项都是必需的。这取决于你正在开发什么。随着你使用 Matplotlib 库的旅程不断深入,你可能需要下载并安装额外的模块。

如何工作...

创建我们的第一个图表

现在我们已经安装了所有必需的 Python 模块,我们可以使用 Matplotlib 创建自己的图表。

我们可以用几行 Python 代码就创建图表。

准备就绪

使用上一道菜谱中的代码,我们现在可以创建一个看起来与下面展示的图表相似的图表。

如何做到这一点...

使用官方网站上展示的最少代码量,我们可以创建我们的第一个图表。嗯,几乎是这样。网站上展示的示例代码在导入show方法并调用它之前是不工作的。

如何做...

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

如何做...

它是如何工作的...

Python 的 Matplotlib 模块,结合如 numpy 等插件,创建了一个非常丰富的编程环境,使我们能够非常容易地进行数学计算并在可视化图表中绘制它们。

Python numpy 方法 arange 并非用于排列任何东西。它的意思是创建“一个范围”,这在 Python 中用于内置的 “range” 操作符。linspace 方法也可能造成类似的困惑。这里的 “lin” 是谁,又在什么 “空间” 中?

事实上,这个名字的意思是“线性间隔向量”。

Pyglet 函数 show 显示我们创建的图表。在成功创建第一个图表之后,尝试绘制另一个图表时调用 show() 会有一些副作用。

在图表上放置标签

到目前为止,我们使用了默认的 Matplotlib GUI。现在我们将使用 Matplotlib 创建一些 tkinter GUI。

这将需要几行额外的 Python 代码和导入一些更多的库,但这非常值得努力,因为我们正在通过画布来掌控我们的画作。

我们将标签定位到水平轴以及垂直轴上,也就是所说的x轴和y轴。

我们将通过创建一个 Matplotlib 图形来实现这一点,然后我们在其上绘制。

我们还将学习如何使用子图,这将使我们能够在同一个窗口中绘制多个图表。

准备就绪

在安装了必要的 Python 模块,并知道如何找到官方在线文档和教程后,我们现在可以继续创建 Matplotlib 图表了。

如何做到这一点...

虽然plot是创建 Matplotlib 图表最简单的方式,但结合使用FigureCanvas可以创建一个更加定制化的图表,看起来更加美观,同时也使我们能够向其中添加按钮和其他小部件。

from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
#--------------------------------------------------------------
fig = Figure(figsize=(12, 8), facecolor='white')
#--------------------------------------------------------------
# axis = fig.add_subplot(111)   # 1 row,  1 column, only graph
axis = fig.add_subplot(211)     # 2 rows, 1 column, Top graph
#--------------------------------------------------------------
xValues = [1,2,3,4]
yValues = [5,7,6,8]
axis.plot(xValues, yValues)

axis.set_xlabel('Horizontal Label')
axis.set_ylabel('Vertical Label')

# axis.grid()                   # default line style 
axis.grid(linestyle='-')        # solid grid lines
#--------------------------------------------------------------
def _destroyWindow():
    root.quit()
    root.destroy() 
#--------------------------------------------------------------
root = tk.Tk() 
root.withdraw()
root.protocol('WM_DELETE_WINDOW', _destroyWindow)   
#--------------------------------------------------------------
canvas = FigureCanvasTkAgg(fig, master=root)
canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
#--------------------------------------------------------------
root.update()
root.deiconify()
root.mainloop()

运行前面的代码将生成以下图表:

如何做...

在导入语句之后的代码第一行,我们创建了一个Figure对象的实例。接下来,通过调用add_subplot(211),我们将子图添加到这个图中。211 中的第一个数字告诉图需要添加多少个绘图,第二个数字确定列数,第三个数字告诉图以何种顺序显示绘图。

我们还添加了一个网格并更改了其默认的线条样式。

即使我们在图表中只显示一个图表,通过选择 2 作为子图的数量,我们实际上是在将图表向上移动,这导致图表底部出现额外的空白。现在这个图表只占据了屏幕的 50%,这影响了显示时该图表网格线的宽度。

注意事项

通过取消注释axis =axis.grid()的代码来实验,以查看不同的效果。

我们可以通过将它们分配到第二个位置来添加更多子图,使用add_subplot(212)

from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
#--------------------------------------------------------------
fig = Figure(figsize=(12, 8), facecolor='white')
#--------------------------------------------------------------
axis = fig.add_subplot(211)     # 2 rows, 1 column, Top graph
#--------------------------------------------------------------
xValues = [1,2,3,4]
yValues = [5,7,6,8]
axis.plot(xValues, yValues)

axis.set_xlabel('Horizontal Label')
axis.set_ylabel('Vertical Label')

axis.grid(linestyle='-')        # solid grid lines
#--------------------------------------------------------------
axis1 = fig.add_subplot(212)    # 2 rows, 1 column, Bottom graph
#--------------------------------------------------------------
xValues1 = [1,2,3,4]
yValues1 = [7,5,8,6]
axis1.plot(xValues1, yValues1)
axis1.grid()                    # default line style 
#--------------------------------------------------------------
def _destroyWindow():
    root.quit()
    root.destroy() 
#--------------------------------------------------------------
root = tk.Tk() 
root.withdraw()
root.protocol('WM_DELETE_WINDOW', _destroyWindow)   
#--------------------------------------------------------------
canvas = FigureCanvasTkAgg(fig, master=root)
canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
#--------------------------------------------------------------
root.update()
root.deiconify()
root.mainloop()

现在运行略微修改后的代码,将轴 1 添加到图表中。对于底部图表的网格,我们保留了默认的线条样式。

如何做...

它是如何工作的...

我们导入了必要的 Matplotlib 模块来创建一个图表,并在其上绘制图表。我们为xy轴赋予了一些值,并设置了一些众多配置选项中的几个。

我们创建了自己的 tkinter 窗口来显示图表,并自定义了图表的位置。

如我们在前几章所见,为了创建一个 tkinter 图形用户界面,我们首先必须导入 tkinter 模块,然后创建Tk类的实例。我们将这个类的实例分配给一个我们命名为root的变量,这个名称在示例中经常被使用。

我们的 tkinter 图形用户界面(GUI)将不会变得可见,直到我们启动主事件循环,为此,我们使用root.mainloop()

避免在这里使用 Matplotlib 默认的 GUI,而是创建我们自己的 GUI 使用 tkinter 的一个重要原因是,我们希望改善默认 Matplotlib GUI 不太美观的外观,而我们可以非常容易地使用 tkinter 做到这一点。

如果我们使用 tkinter 构建 GUI,那么 Matplotlib GUI 底部将不再出现那些看起来过时的按钮。

同时,Matplotlib 的 GUI 有一个我们的 tkinter GUI 没有的功能,那就是当我们把鼠标在图表内移动时,我们实际上可以看到 Matplotlib GUI 中的 x 和 y 坐标。x 和 y 坐标的位置显示在右下角。

如何给图表添加图例

一旦我们开始绘制多于一条的数据点线,事情可能会变得有些不清楚。通过在我们的图表中添加图例,我们可以知道哪些数据代表什么,以及它实际上意味着什么。

我们不必选择不同的颜色来表示不同的数据。Matplotlib 会自动为每条数据点线分配不同的颜色。

我们只需要创建图表并为其添加图例。

准备就绪

在这个菜谱中,我们将增强之前菜谱中的图表。我们只会绘制一个图表。

如何做到这一点...

首先,我们将在同一图表中绘制更多数据行,然后我们将为图表添加图例。

我们通过修改前一个菜谱中的代码来完成这项工作。

from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
#--------------------------------------------------------------
fig = Figure(figsize=(12, 5), facecolor='white')
#--------------------------------------------------------------
axis  = fig.add_subplot(111)                  # 1 row, 1 column

xValues  = [1,2,3,4]

yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,8,6]
yValues2 = [6.5,7,8,7]

t0, = axis.plot(xValues, yValues0)
t1, = axis.plot(xValues, yValues1)
t2, = axis.plot(xValues, yValues2)

axis.set_ylabel('Vertical Label')
axis.set_xlabel('Horizontal Label')

axis.grid()

fig.legend((t0, t1, t2), ('First line', 'Second line', 'Third line'), 'upper right')

#--------------------------------------------------------------
def _destroyWindow():
    root.quit()
    root.destroy() 
#--------------------------------------------------------------
root = tk.Tk() 
root.withdraw()
root.protocol('WM_DELETE_WINDOW', _destroyWindow)
#--------------------------------------------------------------
canvas = FigureCanvasTkAgg(fig, master=root)
canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
#--------------------------------------------------------------
root.update()
root.deiconify()
root.mainloop()

运行修改后的代码会生成以下图表,其中图例位于右上角:

如何做...

在这个菜谱中,我们只绘制一个图表,这是通过更改fig.add_subplot(111)来实现的。我们还通过figsize属性稍微修改了图表的大小。

接下来,我们创建三个包含要绘制值的 Python 列表。当我们绘制数据时,我们将绘图引用保存在局部变量中。

我们通过传入一个包含三个图表引用的元组、另一个包含随后在图例中显示的字符串的元组,以及在第三个参数中定位图例在图表中的位置,来创建图例。

Matplotlib 的默认设置将颜色方案分配给正在绘制的线条。

我们可以通过在绘制每个轴时设置一个属性,轻松地将这个默认的颜色设置更改为我们喜欢的颜色。

我们通过使用颜色属性并为其分配一个可用的颜色值来实现这一点。

t0, = axis.plot(xValues, yValues0, color = 'purple')
t1, = axis.plot(xValues, yValues1, color = 'red')
t2, = axis.plot(xValues, yValues2, color = 'blue')

注意到在 t0、t1 和 t2 的变量赋值之后的逗号不是一个错误,而是为了创建图例所必需的。

每个变量后面的逗号将列表转换为元组。如果我们省略这一点,我们的图例将不会显示。

代码仍然会运行,只是没有预期的图例。

注意事项

当我们在 t0 =赋值之后删除逗号时,会出现错误,并且第一行不再出现在图中。图表和图例仍然会被创建,但图例中不会出现第一行。

如何做...

它是如何工作的...

我们通过在同一图表中绘制三条数据线并添加图例来增强我们的图表,以便区分这三条线所表示的数据。

比例图

在之前的菜谱中,当我们创建第一个图表并增强它们时,我们硬编码了如何视觉表示这些值的缩放。

虽然这对于我们使用的数值来说效果很好,但我们经常从非常大的数据库中绘制图表。

根据数据范围的不同,我们硬编码的垂直 y 维度值可能并不总是最佳解决方案,这可能会导致我们在图表中难以看到线条。

准备就绪

我们将从之前的食谱中改进我们的代码。如果你还没有输入所有之前的食谱中的代码,只需下载本章的代码即可开始(然后你可以有很多乐趣,使用 Python 创建 GUI、图表等等)。

如何做到这一点...

将前一个菜谱中的yValues1代码行修改为使用 50 作为第三个值。

axis  = fig.add_subplot(111)        # 1 row, 1 column

xValues  = [1,2,3,4]

yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,50,6]           # one very high value
yValues2 = [6.5,7,8,7]

与上一道菜谱中创建图表的代码相比,唯一的区别是一个数据值。

通过改变一个与其他所有绘制线条的平均值范围都不接近的值,数据的视觉表示发生了显著变化,我们失去了关于整体数据的大量细节,现在主要看到一个高峰。

如何做...

到目前为止,我们的图表已经根据它们所视觉表示的数据进行了调整。

虽然这是 Matplotlib 的一个实用功能,但这并不总是我们想要的。我们可以通过限制垂直的 y 轴维度来限制所表示图表的刻度。

yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,50,6]           # one very high value (50)
yValues2 = [6.5,7,8,7]

axis.set_ylim(5, 8)                 # limit the vertical display

axis.set_ylim(5, 8) 这行代码现在将垂直显示的开始值限制为 5,结束值限制为 8。

现在,当我们创建图表时,高值峰值不再像以前那样有影响。

如何做...

它是如何工作的...

我们在数据中增加了一个值,这导致了显著的效果。通过设置图表的垂直和水平显示限制,我们可以看到我们最感兴趣的数据。

就像刚才展示的那些尖峰,也可能非常有意思。这完全取决于我们在寻找什么。数据的视觉表示非常有价值。

注意

一图胜千言。

动态调整图表的缩放比例

在上一个菜谱中,我们学习了如何限制图表的缩放。在这个菜谱中,我们将更进一步,通过设置限制并分析我们的数据,在表示之前动态调整缩放。

准备就绪

我们将通过读取我们动态绘制的数据进行增强,计算平均值,然后调整我们的图表。

虽然我们通常从外部源读取数据,但在本菜谱中,我们使用 Python 列表创建我们正在绘制的图形数据,如下面的代码所示。

如何做到这一点...

我们通过将包含数据的列表分配给 xvalues 和 yvalues 变量,在我们的 Python 模块中创建自己的数据。

在许多图表中,x 轴和 y 轴坐标系统的起始点通常在(0, 0)。这通常是一个好主意,因此让我们相应地调整我们的图表坐标代码。

让我们修改代码以设置 x 和 y 维度的限制:

xValues  = [1,2,3,4]

yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,50,6]           # one very high value (50)
yValues2 = [6.5,7,8,7]              

axis.set_ylim(0, 8)                 # lower limit (0)
axis.set_xlim(0, 8)                 # use same limits for x

现在我们为 x 和 y 设置了相同的限制,我们的图表可能看起来更平衡。当我们运行修改后的代码时,我们得到以下结果:

如何做...

也许一开始从 (0, 0) 开始并不是一个很好的主意……

我们真正想要做的是根据数据的范围动态调整我们的图表,同时限制过高或过低的值。

我们可以通过解析图表中要表示的所有数据,同时设置一些明确的限制来实现这一点。

修改代码,如下所示:

xValues  = [1,2,3,4]

yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,50,6]              # one very high value (50)
yValues2 = [6.5,7,8,7]              
yAll = [yValues0, yValues1, yValues2]  # list of lists

# flatten list of lists retrieving minimum value
minY = min([y for yValues in yAll for y in yValues])

yUpperLimit = 20
# flatten list of lists retrieving max value within defined limit
maxY = max([y for yValues in yAll for y in yValues if y < yUpperLimit])

# dynamic limits
axis.set_ylim(minY, maxY)                 
axis.set_xlim(min(xValues), max(xValues))                

t0, = axis.plot(xValues, yValues0)
t1, = axis.plot(xValues, yValues1)
t2, = axis.plot(xValues, yValues2)

运行代码后得到以下图表。我们对其 x 轴和 y 轴的尺寸进行了动态调整。注意现在 y 轴的起始值是 5.5 而不是之前的 5.0。此外,图表也不再从(0, 0)开始,这为我们提供了更多关于数据的有价值信息。

如何做...

我们正在创建一个用于 y 维度数据的列表列表,然后使用列表推导式将其包裹在调用 Python 的min()max()函数中。

如果列表推导看起来有些高级,它们本质上是一个非常紧凑的循环。

它们也被设计成比常规编程循环更快。

在创建前面图表的 Python 代码中,我们创建了三个列表来存储要绘制的 y 维数据。然后我们创建了另一个列表来存储这三大列表,从而创建了一个列表的列表。

就像这样:

yValues0 = [6,7.5,8,7.5]
yValues1 = [5.5,6.5,50,6]              # one very high value (50)
yValues2 = [6.5,7,8,7]              
yAll = [yValues0, yValues1, yValues2]  # list of lists

我们对获取所有 y 维数据的极小值以及这三个列表中包含的最大值都感兴趣。

我们可以通过 Python 列表推导来完成这个操作。

# flatten list of lists retrieving minimum value
minY = min([y for yValues in yAll for y in yValues])

在运行列表推导后,minY的值是:5.5。

上面的单行代码是列表推导式,它遍历了包含在三个列表中的所有值,并使用 Python 的min关键字找到最小值。

在完全相同的模式中,我们找到了我们希望绘制的数据中的最大值。这次,我们在列表推导式中也设置了一个限制,忽略所有高于我们指定限制的值,如下所示:

yUpperLimit = 20
# flatten list of lists retrieving max value within defined limit
maxY = max([y for yValues in yAll for y in yValues if y < yUpperLimit])

在运行前面代码并应用我们选择的限制条件后,maxY的值为 8(而不是 50)。

我们根据预定义的条件对最大值进行了限制,选择 20 作为图表中显示的最大值。

对于 x 轴维度,我们只需在 Matplotlib 方法中调用min()max()函数来动态调整图表的界限。

它是如何工作的...

在这个菜谱中,我们创建了几个 Matplotlib 图表并调整了众多可用属性中的某些。我们还使用核心 Python 动态控制图表的缩放。

第六章:线程和网络

在本章中,我们将使用 Python 3 创建线程、队列和 TCP/IP 套接字。

  • 如何创建多个线程

  • 开始一个线程

  • 停止一个线程

  • 如何使用队列

  • 在不同模块之间传递队列

  • 使用对话框小部件将文件复制到您的网络

  • 使用 TCP/IP 通过网络进行通信

  • 使用 URLOpen 从网站读取数据

简介

在本章中,我们将通过使用线程、队列和网络连接来扩展我们 Python GUI 的功能。

注意事项

Tkinter 图形用户界面是单线程的。涉及睡眠或等待时间的任何函数都必须在单独的线程中调用,否则 Tkinter GUI 会冻结。

当我们在 Windows 任务管理器中运行我们的 Python GUI 时,我们可以看到一个新的 python.exe 进程已经被启动。

当我们给我们的 Python GUI 添加 .pyw 扩展名时,创建的进程将是 python.pyw,如任务管理器中所示。

当创建一个进程时,进程会自动创建一个主线程来运行我们的应用程序。这被称为单线程应用程序。

对于我们的 Python GUI,如果是一个单线程应用程序,那么一旦我们调用一个运行时间较长的任务,比如点击一个有几秒休眠的按钮,我们的 GUI 就会立即冻结。

为了保持我们的图形用户界面(GUI)响应,我们必须使用多线程,这正是本章将要探讨的内容。

我们也可以通过创建多个 Python GUI 实例来创建多个进程,这在任务管理器中可以看得到。

设计上,进程彼此隔离且不共享公共数据。为了在分离的进程之间进行通信,我们不得不使用进程间通信IPC),这是一种高级技术。

另一方面,线程确实共享相同的数据、代码和文件,这使得在同一进程内线程之间的通信比使用进程间通信(IPC)要容易得多。

注意事项

一份关于线程的精彩解释可以在以下链接找到:www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html

在本章中,我们将学习如何保持我们的 Python 图形用户界面(GUI)响应,并防止其冻结。

如何创建多个线程

我们将使用 Python 创建多个线程。这是为了保持我们的 GUI 响应性所必需的。

注意事项

线程就像是用线织成的布料,没有什么可怕的。

准备就绪

多个线程在同一个计算机进程的内存空间中运行。无需进行进程间通信(简称 IPC),这会使得我们的代码变得复杂。在本食谱中,我们将通过使用线程来避免 IPC。

如何做到这一点...

首先,我们将增加我们的ScrolledText小部件的大小,使其更大。让我们将scrolW增加到 40,将scrolH增加到 10。

# Using a scrolled Text control
scrolW  = 40; scrolH  =  10
self.scr = scrolledtext.ScrolledText(self.monty, width=scrolW, height=scrolH, wrap=tk.WORD)
self.scr.grid(column=0, row=3, sticky='WE', columnspan=3)

当我们现在运行生成的 GUI 时,相对于其上方的Entry小部件,Spinbox小部件是居中对齐的,这看起来不太好。我们将通过左对齐小部件来改变这一点。

sticky='W' 添加到 grid 控制中,以左对齐 Spinbox 小部件。

# Adding a Spinbox widget using a set of values
self.spin = Spinbox(self.monty, values=(1, 2, 4, 42, 100), width=5, bd=8, command=self._spin) 
self.spin.grid(column=0, row=2, sticky='W')

界面看起来还可以更好,所以接下来,我们将增加Entry小部件的大小,以获得更平衡的 GUI 布局。

将宽度增加到 24,如下所示:

# Adding a Textbox Entry widget
self.name = tk.StringVar()
nameEntered = ttk.Entry(self.monty, width=24, textvariable=self.name)
nameEntered.grid(column=0, row=1, sticky='W')

让我们也将Combobox的宽度略微增加到 14。

ttk.Label(self.monty, text="Choose a number:").grid(column=1, row=0)
number = tk.StringVar()
numberChosen = ttk.Combobox(self.monty, width=14, textvariable=number)
numberChosen['values'] = (1, 2, 4, 42, 100)
numberChosen.grid(column=1, row=1)
numberChosen.current(0)

运行修改和改进后的代码将产生一个更大的 GUI,我们将使用这个 GUI 以及接下来的食谱。

如何做...

为了在 Python 中创建和使用线程,我们必须从 threading 模块导入Thread类。

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu  
from tkinter import Spinbox
import B04829_Ch06_ToolTip as tt

from threading import Thread

GLOBAL_CONST = 42

让我们在OOP类中添加一个在线程中创建的方法。

class OOP():
    def methodInAThread(self):
        print('Hi, how are you?')

我们现在可以在代码中调用我们的线程方法,并将实例保存在一个变量中。

#======================
# Start GUI
#======================
oop = OOP()

# Running methods in Threads
runT = Thread(target=oop.methodInAThread)
oop.win.mainloop())

现在我们有一个线程化的方法,但当我们运行代码时,控制台没有任何输出!

我们必须首先启动线程,然后它才能运行,下一道菜谱将展示如何进行这一操作。

然而,在 GUI 主事件循环之后设置断点证明我们确实创建了一个Thread对象,正如可以在 Eclipse IDE 调试器中看到的那样。

如何做...

它是如何工作的...

在这个菜谱中,我们首先通过增加 GUI 的大小来准备使用线程,这样我们就能更好地看到打印到ScrolledText小部件的结果。

我们随后从 Python 的 threading 模块中导入了 Thread 类。

之后,我们在 GUI 内部创建了一个我们称之为线程的方法。

开始一个线程

这个菜谱将向我们展示如何启动一个线程。它还将演示为什么在长时间运行的任务中,线程对于保持我们的 GUI 响应性是必要的。

准备就绪

让我们先看看当我们调用我们的 GUI 中与睡眠相关联的函数或方法,但没有使用线程时会发生什么。

注意事项

我们在这里使用睡眠来模拟一个可能需要等待网络服务器或数据库响应、大文件传输或复杂计算完成任务的真实世界应用。

睡眠是一个非常实际的占位符,并展示了其中涉及的原则。

在我们的按钮回调方法中添加一个循环并设置一些休眠时间会导致我们的 GUI 变得无响应,当我们尝试关闭 GUI 时,情况变得更糟。

# Button callback
def clickMe(self):
  self.action.configure(text='Hello ' + self.name.get())
  # Non-threaded code with sleep freezes the GUI
  for idx in range(10):
    sleep(5)
    self.scr.insert(tk.INSERT, str(idx) + '\n')

准备就绪

如果我们等待足够长的时间,该方法最终会完成,但在这一过程中,我们所有的 GUI 小部件都不会对点击事件做出响应。我们通过使用线程来解决这个问题。

注意事项

在上一个菜谱中,我们创建了一个在线程中运行的方法,但到目前为止,该线程还没有运行!

与常规的 Python 函数和方法不同,我们必须在它自己的线程中启动一个将要运行的方法!

这是我们接下来要做的。

如何去做……

首先,让我们将创建线程的操作移入一个独立的方法中,然后从按钮回调方法中调用这个方法。

# Running methods in Threads
def createThread(self):
  runT = Thread(target=self.methodInAThread)
  runT.start()
# Button callback
def clickMe(self):
  self.action.configure(text='Hello ' + self.name.get())
  self.createThread()

点击按钮现在会导致调用createThread方法,该方法反过来又调用methodInAThread方法。

首先,我们创建一个线程并将其指向一个方法。接下来,我们启动一个新线程来运行目标方法。

注意事项

GUI 本身在其自己的线程中运行,这是应用程序的主线程。

如何做...

我们可以打印出线程的实例。

# Running methods in Threads
def createThread(self):
  runT = Thread(target=self.methodInAThread)
  runT.start()
  print(runT)

点击按钮现在将生成以下打印输出:

如何做...

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

如何做...

现在我们将带有sleep的代码放入methodInAThread方法中,以验证线程确实解决了我们的问题。

def methodInAThread(self):
  print('Hi, how are you?')
  for idx in range(10):
    sleep(5)
    self.scr.insert(tk.INSERT, str(idx) + '\n')

当点击按钮时,在数字以五秒延迟打印到ScrolledText小部件的过程中,我们可以在我们的 GUI 中任意点击,切换标签页等等。由于我们使用了线程,我们的 GUI 再次变得响应了!

如何做...

它是如何工作的...

在这个菜谱中,我们在自己的线程中调用了 GUI 类的函数,并了解到我们必须启动线程。否则,线程会被创建,但只是在那里等待我们运行其目标方法。

我们注意到每个线程都被分配了一个唯一的名称和 ID。

我们通过在代码中插入sleep语句来模拟长时间运行的任务,这表明线程确实可以解决我们的问题。

停止一个线程

我们必须启动一个线程,通过调用start()方法来让它执行某些操作,所以直观上,我们可能会期望存在一个对应的stop()方法,但实际上并没有这样的方法。在本教程中,我们将学习如何将线程作为一个后台任务运行,这被称为守护线程。当关闭主线程,也就是我们的 GUI 时,所有守护线程也会自动停止。

准备就绪

当我们在线程中调用方法时,我们也可以向方法传递参数和关键字参数。我们通过做这件事来开始这个步骤。

如何做到这一点...

通过在线程构造函数中添加args=[8]并将目标方法修改为期望参数,我们可以向线程方法传递参数。args参数必须是一个序列,因此我们将我们的数字包裹在一个 Python 列表中。

def methodInAThread(self, numOfLoops=10):
  for idx in range(numOfLoops):
    sleep(1)
    self.scr.insert(tk.INSERT, str(idx) + '\n')

在以下代码中,runT 是一个局部变量,我们只在该方法的作用域内访问我们创建的 runT


# Running methods in Threads
def createThread(self):
  runT = Thread(target=self.methodInAThread, args=[8])
  runT.start()

通过将局部变量转换为成员,我们就可以通过在另一个方法中调用其isAlive方法来检查线程是否仍在运行。

# Running methods in Threads
def createThread(self):
  self.runT = Thread(target=self.methodInAThread, args=[8])
  self.runT.start()
  print(self.runT)
  print('createThread():', self.runT.isAlive())

在前面的代码中,我们将本地runT变量提升为我们的类成员。这样做的作用是使我们能够从我们的类中的任何方法访问self.runT变量。

这就是这样实现的:

    def methodInAThread(self, numOfLoops=10):
        for idx in range(numOfLoops):
            sleep(1)
            self.scr.insert(tk.INSERT, str(idx) + '\n')
        sleep(1)
        print('methodInAThread():', self.runT.isAlive())

当我们点击按钮然后退出 GUI 时,我们可以看到createThread方法中的打印语句被打印出来,但我们看不到来自methodInAThread的第二个打印语句。

相反,我们得到一个 RuntimeError。

如何做...

线程预计将完成其分配的任务,因此当我们关闭 GUI 而线程尚未完成时,Python 会告诉我们我们启动的线程不在主事件循环中。

我们可以通过将线程转换为守护进程来解决此问题,然后它将作为一个后台任务执行。

这给我们带来的结果是,当我们关闭我们的 GUI(图形用户界面),也就是我们的主线程启动其他线程时,守护线程将干净地退出。

我们可以在启动线程之前,通过调用线程的setDaemon(True)方法来实现这一点。

# Running methods in Threads
def createThread(self):
  runT = Thread(target=self.methodInAThread)
  runT.setDaemon(True)
  runT.start()
  print(runT)

当我们现在点击按钮并退出我们的 GUI,而线程尚未完成其分配的任务时,我们不再收到任何错误。

如何做...

它是如何工作的...

虽然有启动方法可以让线程运行,但令人惊讶的是,并没有真正等效的停止方法。

在这个菜谱中,我们正在一个线程中运行一个方法,该方法将数字打印到我们的ScrolledText小部件中。

当我们退出我们的图形用户界面时,我们就不再对曾经用于向我们的小部件打印的线程感兴趣了,因此,通过将线程转换为后台守护进程,我们可以干净地退出我们的 GUI。

如何使用队列

Python 队列是一种实现先进先出(FIFO)范式的数据结构,基本上就像一个管道。你在一侧把东西铲进管道,它就会从管道的另一侧掉出来。

与将泥土铲入物理管道相比,这种队列铲雪的主要区别在于,在 Python 队列中,事物不会混淆。你放入一个单元,这个单元就会从另一端出来。接下来,你放入另一个单元(比如,一个类的实例),这个整个单元将作为一个整体从另一端出来。

它以我们插入队列中代码的精确顺序从另一端返回。

注意事项

队列并非是我们可以进行入栈和出栈操作的栈。栈是一种后进先出(LIFO)的数据结构。

队列是用于存储从可能不同的数据源中输入的数据的容器。当客户端有可用数据时,我们可以让不同的客户端向队列提供数据。无论哪个客户端准备好向我们的队列发送数据,它就会发送,然后我们可以将此数据显示在小部件中或将其转发到其他模块。

使用多个线程在队列中完成分配的任务,在接收处理结果的最终结果并显示它们时非常有用。数据被插入到队列的一端,然后以有序的方式从另一端出来,即先进先出(FIFO)。

我们的图形用户界面可能有五个不同的按钮小部件,每个小部件都会启动不同的任务,我们希望在 GUI 中的小部件(例如,滚动文本小部件)中显示这些任务。

这五项不同的任务需要不同时间来完成。

每当一项任务完成时,我们立即需要知道这一点,并在我们的图形用户界面中显示此信息。

通过创建一个共享的 Python 队列,并让五个任务将它们的结果写入这个队列,我们可以使用先进先出(FIFO)的方法立即显示已完成任务的任何结果。

准备就绪

随着我们的图形用户界面(GUI)在功能和实用性上不断增长,它开始与网络、进程和网站进行交互,并且最终必须等待数据变得可用,以便图形用户界面能够展示。

在 Python 中创建队列可以解决在 GUI 中等待数据显示的问题。

如何做到这一点...

为了在 Python 中创建队列,我们必须从queue模块中导入Queue类。将以下语句添加到我们的 GUI 模块顶部:

from threading import Thread
from time import sleep
from queue import Queue

这就让我们开始了。

接下来,我们创建一个队列实例。

def useQueues(self):
    guiQueue = Queue()     # create queue instance

注意

在前面的代码中,我们创建了一个局部Queue实例,该实例仅在此方法内部可访问。如果我们希望从其他地方访问这个队列,我们必须使用self关键字将其变成我们类的一个成员,这会将局部变量绑定到整个类,使其可以在我们类中的任何其他方法中访问。在 Python 中,我们通常在__init__(self)方法中创建类实例变量,但 Python 非常实用,允许我们在代码的任何地方创建这些成员变量。

现在我们有一个队列的实例。我们可以通过打印它来证明这是有效的。

如何做...

为了将数据放入队列,我们使用put命令。为了从队列中获取数据,我们使用get命令。

# Create Queue instance  
def useQueues(self):
    guiQueue = Queue()
    print(guiQueue)
    guiQueue.put('Message from a queue')
    print(guiQueue.get())

运行修改后的代码会导致消息首先被放入队列中,然后从队列中取出,接着打印到控制台。

如何做...

我们可以将许多消息放入队列中。

# Create Queue instance  
def useQueues(self):
    guiQueue = Queue()
    print(guiQueue)
    for idx in range(10):
        guiQueue.put('Message from a queue: ' + str(idx))
    print(guiQueue.get())

我们已经将 10 条消息放入了队列,但我们只取出了第一条。其他消息仍然在队列中,等待以 FIFO(先进先出)的方式被取出。

如何做...

为了获取所有已放入队列的消息,我们可以创建一个无限循环。

# Create Queue instance
def useQueues(self):
    guiQueue = Queue()
    print(guiQueue)
    for idx in range(10):
        guiQueue.put('Message from a queue: ' + str(idx))

    while True: 
        print(guiQueue.get())

如何做...

虽然这段代码能工作,但不幸的是它会冻结我们的图形用户界面。为了解决这个问题,我们必须在它自己的线程中调用该方法,就像我们在之前的菜谱中做的那样。

让我们在一个线程中运行我们的方法,并将其绑定到按钮事件:

# Running methods in Threads
def createThread(self, num):
    self.runT = Thread(target=self.methodInAThread, args=[num])
    self.runT.setDaemon(True)
    self.runT.start()
    print(self.runT)
    print('createThread():', self.runT.isAlive())

    # textBoxes are the Consumers of Queue data
    writeT = Thread(target=self.useQueues, daemon=True)
    writeT.start()

# Create Queue instance  
def useQueues(self):
    guiQueue = Queue()
    print(guiQueue)
    for idx in range(10):
        guiQueue.put('Message from a queue: ' + str(idx))
    while True: 
        print(guiQueue.get())

当我们现在点击操作按钮,我们不再得到一个多余的弹出窗口,代码也能正常工作。

如何做...

它是如何工作的...

我们创建了一个队列,以先进先出(即 FIFO)的方式将消息放入队列的一侧。我们从队列中取出消息,然后将其打印到控制台(stdout)。

我们意识到我们必须在自己的Thread中调用该方法。

在不同模块之间传递队列

在这个菜谱中,我们将队列传递到不同的模块中。随着我们的 GUI 代码复杂性增加,我们希望将 GUI 组件从业务逻辑中分离出来,将它们分别放入不同的模块中。

模块化使我们能够复用代码,同时也使得代码更加易于阅读。

当我们在 GUI 中显示的数据来自不同的数据源时,我们将面临延迟问题,这正是队列解决的问题。通过在不同 Python 模块之间传递队列的实例,我们正在分离模块功能的不同关注点。

注意事项

GUI 代码理想情况下只需关注创建和显示小部件。

业务逻辑模块的职责仅限于执行业务逻辑。

我们必须结合这两个元素,理想情况下使用尽可能少的模块间关系,减少代码之间的依赖性。

注意事项

避免不必要的依赖的编码原则通常被称为“松耦合”。

为了理解松耦合的重要性,我们可以在白板上或一张纸上画一些方框。一个方框代表我们的 GUI 类和代码,而其他方框则代表业务逻辑、数据库等。

接下来,我们在这些方框之间画线,绘制出这些方框(即我们的 Python 模块)之间的相互依赖关系。

注意事项

我们 Python 盒子之间的行数越少,我们的设计就越松散耦合。

准备就绪

在上一个菜谱中,我们已经开始使用 队列。在这个菜谱中,我们将从主 GUI 线程传递 队列 的实例到其他 Python 模块,这将使我们能够从另一个模块写入 ScrolledText 小部件,同时保持我们的 GUI 响应。

如何做到这一点...

首先,我们在项目中创建一个新的 Python 模块。让我们称它为 Queues.py。我们将一个函数放入其中(目前不需要面向对象编程)并传递一个队列实例。

我们还传递了创建 GUI 表单和控件的类的自引用,这使得我们能够从另一个 Python 模块中使用所有的 GUI 方法。

我们在按钮回调中这样做

注意

这就是面向对象编程(OOP)的魔力。在类的中间,我们使用self关键字将自身传递给类内部调用的函数。

代码现在看起来是这样的。

import B04829_Queues as bq

class OOP():
    # Button callback
    def clickMe(self):
      # Passing in the current class instance (self)
        print(self)
        bq.writeToScrol(self)

导入的模块包含我们正在调用的函数,

def writeToScrol(inst):
    print('hi from Queue', inst)
    inst.createThread(6)

我们在按钮回调中注释掉了对createThread的调用,因为我们现在是从我们新的模块中调用它的。

# Threaded method does not freeze our GUI
# self.createThread()

通过将类实例的自引用传递给类在另一个模块中调用的函数,我们现在可以访问来自其他 Python 模块的所有 GUI 元素。

运行代码会生成以下结果。

如何做...

接下来,我们将创建一个Queue作为我们类的一个成员,并在类的__init__方法中放置对其的引用。

class OOP():
    def __init__(self):
        # Create a Queue
        self.guiQueue = Queue()

现在我们可以通过简单地使用传递给我们的 GUI 的类引用,将消息放入队列中。

def writeToScrol(inst):
    print('hi from Queue', inst)
    for idx in range(10):
        inst.guiQueue.put('Message from a queue: ' + str(idx))
    inst.createThread(6)

我们 GUI 代码中的createThread方法现在只从队列中读取,该队列是由我们新模块中的业务逻辑填充的,该模块已经将逻辑从我们的 GUI 模块中分离出来。

def useQueues(self):
    # Now using a class member Queue
    while True:
        print(self.guiQueue.get())

运行我们修改后的代码会产生相同的结果。我们还没有破坏任何东西(目前还没有)!

它是如何工作的...

为了将图形用户界面(GUI)小部件与表达业务逻辑的功能分离,我们创建了一个类,将队列作为这个类的成员,并通过将这个类的实例传递给位于不同 Python 模块中的函数,我们现在可以访问所有的 GUI 小部件以及Queue

这个菜谱是一个例子,说明在面向对象编程(OOP)中编程是有意义的。

使用对话框小部件将文件复制到您的网络

这个菜谱展示了如何将文件从您的本地硬盘复制到网络位置。

我们将通过使用 Python 的 tkinter 内置对话框之一来完成这项工作,它使我们能够浏览我们的硬盘驱动器。然后我们可以选择要复制的文件。

这个菜谱还向我们展示了如何将Entry小部件设置为只读,并将我们的Entry默认设置为指定位置,这可以加快我们浏览硬盘的速度。

准备就绪

我们将扩展之前菜谱中构建的图形用户界面(GUI)的Tab 2

如何做到这一点...

将以下代码添加到我们的 GUI 中,在def createWidgets(self)方法底部,即我们创建 Tab Control 2 的地方。

新的 widget 框架的父元素是 tab2,这是我们最初在 createWidgets() 方法中创建的。只要您将以下代码物理地放置在创建 tab2 之下,它就会正常工作。

###########################################################
    def createWidgets(self):
        tabControl = ttk.Notebook(self.win)  # Create Tab  
        tab2 = ttk.Frame(tabControl)         # Add a second tab
        tabControl.add(tab2, text='Tab 2')

# Create Manage Files Frame 
mngFilesFrame = ttk.LabelFrame(tab2, text=' Manage Files: ')
mngFilesFrame.grid(column=0, row=1, sticky='WE', padx=10, pady=5)

# Button Callback
def getFileName():
    print('hello from getFileName')

# Add Widgets to Manage Files Frame
lb = ttk.Button(mngFilesFrame, text="Browse to File...", command=getFileName)
lb.grid(column=0, row=0, sticky=tk.W) 

file = tk.StringVar()
self.entryLen = scrolW
self.fileEntry = ttk.Entry(mngFilesFrame, width=self.entryLen, textvariable=file)
self.fileEntry.grid(column=1, row=0, sticky=tk.W)

logDir = tk.StringVar()
self.netwEntry = ttk.Entry(mngFilesFrame, width=self.entryLen, textvariable=logDir)
self.netwEntry.grid(column=1, row=1, sticky=tk.W) 
        def copyFile():
        import shutil   
        src  = self.fileEntry.get()
        file = src.split('/')[-1]  
        dst  = self.netwEntry.get() + '\\'+ file
        try:
            shutil.copy(src, dst)   
            mBox.showinfo('Copy File to Network', 'Success: File copied.')
        except FileNotFoundError as err:
            mBox.showerror('Copy File to Network', '*** Failed to copy file! ***\n\n' + str(err))
        except Exception as ex:
            mBox.showerror('Copy File to Network', '*** Failed to copy file! ***\n\n' + str(ex))

        cb = ttk.Button(mngFilesFrame, text="Copy File To :   ", command=copyFile)
        cb.grid(column=0, row=1, sticky=tk.E)

        # Add some space around each label
        for child in mngFilesFrame.winfo_children(): 
            child.grid_configure(padx=6, pady=6)

这将在我们的 GUI 的Tab 2标签页中添加两个按钮和两个输入框。

我们尚未实现按钮回调函数的功能。

运行代码将创建以下图形用户界面:

如何做...

点击浏览到文件…按钮当前会打印到控制台。

如何做...

我们可以使用 tkinter 内置的文件对话框,所以让我们将以下import语句添加到我们的 Python GUI 模块顶部。

from tkinter import filedialog as fd
from os import path

我们现在可以在代码中使用对话框。我们不再需要硬编码路径,而是可以使用 Python 的 os 模块来找到我们的 GUI 模块所在的全路径。

def getFileName():
    print('hello from getFileName')
    fDir  = path.dirname(__file__)
    fName = fd.askopenfilename(parent=self.win, initialdir=fDir)

点击浏览按钮现在将打开askopenfilename对话框。

如何做...

我们现在可以在这个目录中打开一个文件或浏览到不同的目录。在对话框中选择一个文件并点击打开按钮后,我们将把文件的完整路径保存到fName局部变量中。

如果在我们打开 Python 的 askopenfilename 对话框小部件时,能自动默认到一个目录,这样我们就不必浏览到我们要打开的特定文件所在的位置了,那就太好了。

最好的方法是回到我们的 GUI 标签 1,这是我们接下来要做的。

我们可以将默认值设置到输入框小部件中。回到我们的标签页 1,这非常简单。我们只需在创建Entry小部件时添加以下两行代码即可。

# Adding a Textbox Entry widget
self.name = tk.StringVar()
nameEntered = ttk.Entry(self.monty, width=24, textvariable=self.name)
nameEntered.grid(column=0, row=1, sticky='W')
nameEntered.delete(0, tk.END)
nameEntered.insert(0, '< default name >')

当我们现在运行 GUI 时,nameEntered输入框有一个默认值。

如何做...

我们可以使用以下 Python 语法获取我们正在使用的模块的完整路径,然后我们可以在其下方创建一个新的子文件夹。我们可以将其作为一个模块级别的全局操作,或者我们可以在一个方法内部创建该子文件夹。

# Module level GLOBALS
GLOBAL_CONST = 42
fDir   = path.dirname(__file__)
netDir = fDir + '\\Backup'

def __init__(self):
    self.createWidgets()       
    self.defaultFileEntries()

def defaultFileEntries(self):
    self.fileEntry.delete(0, tk.END)
    self.fileEntry.insert(0, fDir) 
    if len(fDir) > self.entryLen:
        self.fileEntry.config(width=len(fDir) + 3)
        self.fileEntry.config(state='readonly')

    self.netwEntry.delete(0, tk.END)
    self.netwEntry.insert(0, netDir) 
    if len(netDir) > self.entryLen:
        self.netwEntry.config(width=len(netDir) + 3)

我们为输入小部件设置默认值,并在设置后,使本地文件输入小部件为只读状态。

注意事项

这个顺序很重要。我们必须先填充条目,然后才能将其设置为只读。

在调用主事件循环之前,我们也在选择Tab 2,并且不再将焦点设置到Tab 1Entry中。在我们的 tkinter notebook上调用select是零基索引的,所以通过传入值 1,我们选择Tab 2

# Place cursor into name Entry
# nameEntered.focus()             
tabControl.select(1)

如何做...

由于我们并不都在同一个网络中,这个食谱将以本地硬盘为例来展示网络的使用。

UNC 路径是一种通用命名约定,这意味着我们可以通过使用双反斜杠来访问网络服务器,而不是在 Windows PC 上访问本地硬盘驱动器时使用的典型C:\,从而访问我们网络上的服务器。

注意事项

您只需使用 UNC 路径,并将C:\替换为\\<server name> \<folder>\

此示例可用于将我们的代码备份到备份目录,如果该目录不存在,我们可以通过使用os.makedirs来创建它。

# Module level GLOBALS
GLOBAL_CONST = 42

from os import makedirs
fDir   = path.dirname(__file__)
netDir = fDir + '\\Backup' 
if not path.exists(netDir):
    makedirs(netDir, exist_ok = True)

在选择一个文件复制到其他位置后,我们导入 Python 的shutil模块。我们需要复制文件的源路径的完整路径,一个网络或本地目录路径,然后使用shutil.copy将文件名追加到我们要复制到的路径中。

注意事项

Shutil 是 shell 工具的简写表示。

我们还通过消息框向用户反馈复制是否成功或失败。为了实现这一点,导入messagebox并将其重命名为mBox

在下面的代码中,我们将混合两种不同的方法来放置我们的导入语句。在 Python 中,我们有其他语言所不具备的灵活性。

我们通常将所有的导入语句放置在每个 Python 模块的顶部,以便清楚地知道我们正在导入哪些模块。

同时,现代编码方法是将变量的创建放置在它们首次被使用的函数或方法附近。

在以下代码中,我们在 Python 模块的顶部导入消息框,但随后在函数中又导入了 shutil Python 模块。

我们为什么要做这件事呢?

这甚至能行得通吗?

答案是,是的,它确实有效,我们将这个导入语句放入函数中,因为这是我们代码中唯一真正需要这个模块的地方。

如果我们从未调用这个方法,那么我们就永远不会导入这个方法所需的模块。

在某种意义上,你可以将这种技术视为懒加载设计模式。

如果我们不需要它,我们不会在真正需要在我们的 Python 代码中导入它之前就导入它。

这里的想法是,我们整个代码可能需要,比如说,二十个不同的模块。在运行时,哪些模块真正需要取决于用户交互。如果我们从未调用copyFile()函数,那么就没有必要导入shutil

一旦我们点击调用此函数中copyFile()函数的按钮,我们就导入所需的模块。

from tkinter import messagebox as mBox

def copyFile():
    import shutil   
    src = self.fileEntry.get()
    file = src.split('/')[-1]  
    dst = self.netwEntry.get() + '\\'+ file
    try:
      shutil.copy(src, dst)   
      mBox.showinfo('Copy File to Network', 'Success: File copied.')
    except FileNotFoundError as err:
      mBox.showerror('Copy File to Network', '*** Failed to copy file! ***\n\n' + str(err))
    except Exception as ex:
      mBox.showerror('Copy File to Network', '*** Failed to copy file! ***\n\n' + str(ex))

当我们现在运行我们的 GUI 并浏览到一个文件并点击复制时,文件会被复制到我们在Entry小部件中指定的位置。

如何做...

如果文件不存在,或者我们忘记浏览到文件而试图复制整个父文件夹,代码也会通知我们这一点,因为我们正在使用 Python 内置的异常处理功能。

如何做...

它是如何工作的...

我们正在使用 Python 壳工具将文件从本地硬盘复制到网络上。由于我们大多数人没有连接到同一个局域网,我们通过将代码备份到不同的本地文件夹来模拟复制过程。

我们正在使用 tkinter 的一个对话框控件,并且通过默认目录路径,我们可以提高复制文件的效率。

使用 TCP/IP 通过网络进行通信

这个菜谱展示了如何使用套接字通过 TCP/IP 进行通信。为了实现这一点,我们需要一个 IP 地址和一个端口号。

为了使事情简单且不依赖于不断变化的互联网 IP 地址,我们将创建自己的本地 TCP/IP 服务器,并且作为客户端,学习如何连接到它并从 TCP/IP 连接中读取数据。

我们将通过使用在前面的菜谱中创建的队列,将这种网络功能整合到我们的图形用户界面中。

准备就绪

我们将创建一个新的 Python 模块,它将成为 TCP 服务器。

如何做到这一点...

在 Python 中实现 TCP 服务器的一种方法是从 socketserver 模块继承。我们子类化 BaseRequestHandler 并然后覆盖继承的 handle 方法。在非常少的 Python 代码行中,我们就能实现一个 TCP 服务器模块。

from socketserver import BaseRequestHandler, TCPServer

class RequestHandler(BaseRequestHandler):
    # override base class handle method
    def handle(self):
        print('Server connected to: ', self.client_address)
        while True:
            rsp = self.request.recv(512)
            if not rsp: break
            self.request.send(b'Server received: ' + rsp)

def startServer():
    serv = TCPServer(('', 24000), RequestHandler)
    serv.serve_forever()

我们将我们的RequestHandler类传递给TCPServer初始化器。空的单引号是一个快捷方式,用于传递本地主机,也就是我们自己的电脑。这是 127.0.0.1 的 IP 地址。元组中的第二个项目是端口号。我们可以在我们的本地电脑上选择任何未使用的端口号。

我们只需确保在 TCP 连接的客户端使用相同的端口,否则我们就无法连接到服务器。当然,在客户端能够连接之前,我们必须首先启动服务器。

我们将修改我们的Queues.py模块,使其成为 TCP 客户端。

from socket import socket, AF_INET, SOCK_STREAM

def writeToScrol(inst):
    print('hi from Queue', inst)
    sock = socket(AF_INET, SOCK_STREAM)
    sock.connect(('localhost', 24000))
    for idx in range(10):
        sock.send(b'Message from a queue: ' + bytes(str(idx).encode()) )
        recv = sock.recv(8192).decode()
        inst.guiQueue.put(recv)      
    inst.createThread(6)

这是我们需要与 TCP 服务器通信的所有代码。在这个例子中,我们只是向服务器发送一些字节,服务器在返回响应之前会预先添加一些字符串。

注意

这展示了 TCP 通过网络通信的工作原理。

一旦我们知道了如何通过 TCP/IP 连接到远程服务器,我们就会使用我们感兴趣与之通信的程序协议所设计的任何命令。第一步是在向服务器上特定应用程序发送命令之前建立连接。

writeToScrol函数中,我们将使用之前相同的循环,但现在我们将消息发送到 TCP 服务器。服务器修改接收到的消息,然后将其发送回我们这里。接下来,我们将它放入 GUI 成员队列中,正如之前的菜谱中所述,它在自己的Thread中运行。

注意

在 Python 3 中,我们必须以二进制格式通过套接字发送字符串。现在添加整数索引变得有些复杂,因为我们不得不将其转换为字符串,对其进行编码,然后将编码后的字符串转换为字节!

sock.send(b'Message from a queue: ' + bytes(str(idx).encode()) )

注意字符串前面的b,然后,嗯,接下来就是所有必要的转换……

我们在 OOP 类的初始化器中在自己的线程中启动 TCP 服务器。

class OOP():
    def __init__(self):
    # Start TCP/IP server in its own thread
        svrT = Thread(target=startServer, daemon=True)
        svrT.start()

点击点击我!按钮在标签 1上现在会在我们的ScrolledText小部件以及控制台上创建以下输出,并且由于使用了Threads,响应速度非常快。

如何做...

它是如何工作的...

我们创建了一个 TCP 服务器来模拟连接到我们局域网或互联网上的服务器。我们将队列模块转换成了 TCP 客户端。我们分别在各自的背景线程中运行队列和服务器,这使得我们的 GUI 界面非常响应灵敏。

使用 URLOpen 从网站读取数据

这个菜谱展示了我们如何通过使用 Python 内置模块轻松地阅读整个网页。我们首先以原始格式显示网页数据,然后对其进行解码,接着我们将它在我们的图形用户界面中显示。

准备就绪

我们将从网页读取数据,然后将其显示在我们 GUI 的ScrolledText小部件中。

如何做到这一点...

首先,我们创建一个新的 Python 模块,并将其命名为 URL.py

我们随后导入所需的 Python 功能来读取网页。

我们可以用非常少的代码行来实现这一点。

我们将代码包裹在类似于 Java 和 C# 的 try…except 块中。这是 Python 支持的现代编程方法。

每当我们有可能会未完成的代码时,我们可以对这个代码进行实验,如果它工作正常,那就一切顺利。

如果try…except代码块中的代码无法执行,Python 解释器将抛出几种可能的异常之一,然后我们可以捕获这些异常。一旦我们捕获了异常,我们就可以决定接下来要做什么。

Python 中存在异常的层次结构,我们还可以创建自己的类,这些类继承并扩展了 Python 的异常类。

在以下展示的代码中,我们主要关注的是我们试图打开的 URL 可能不可用,因此我们将代码包裹在try…except代码块中。

如果代码成功打开请求的 URL,一切正常。

如果失败,可能是因为我们的网络连接中断,我们就会进入代码的异常部分,并打印出异常已发生的信息。

注意事项

您可以在docs.python.org/3.4/library/exceptions.html了解更多关于 Python 异常处理的信息。

from urllib.request import urlopen
link = 'http://python.org/' 
try:
    f = urlopen(link)
    print(f)
    html = f.read()
    print(html)
    htmldecoded = html.decode()
    print(htmldecoded)

except Exception as ex:
    print('*** Failed to get Html! ***\n\n' + str(ex))

通过在官方 Python 网站上调用urlopen,我们得到整个数据作为一个长字符串。

第一条打印语句将这个长字符串输出到控制台。

我们随后在结果上调用decode函数,这次我们得到了超过 1,000 行的网页数据,包括一些空白字符。

我们还打印出了调用类型 urlopen,它是一个 http.client.HTTPResponse 对象。实际上,我们首先打印它。

如何做...

这里是我们刚刚阅读的官方 Python 网页。如果你是一名网页开发者,你可能已经有一些关于如何处理解析数据的不错想法。

如何做...

我们接下来在 GUI 中的ScrolledText小部件中展示这些数据。为了做到这一点,我们必须将我们的新模块连接到从网页读取数据到我们的 GUI。

为了做到这一点,我们需要一个对我们 GUI 的引用,而实现这一目标的一种方法是将我们的新模块与Tab 1按钮回调函数关联起来。

我们可以将解码后的 HTML 数据从 Python 网页返回到Button小部件,然后我们可以将其放置在ScrolledText控件中。

因此,让我们将我们的代码转换成一个函数,并将数据返回给调用代码。

from urllib.request import urlopen
link = 'http://python.org/'
def getHtml():
    try:
        f = urlopen(link)
        #print(f)
        html = f.read()
        #print(html)
        htmldecoded = html.decode()
        #print(htmldecoded)     
    except Exception as ex:
        print('*** Failed to get Html! ***\n\n' + str(ex))
    else:
        return htmldecoded  

我们现在可以通过首先导入新模块,然后将数据插入到小部件中,将数据写入我们的button回调方法到ScrolledText控件中。我们还在调用writeToScrol之后给它一些休眠时间。

import B04829_Ch06_URL as url

# Button callback
def clickMe(self):
  bq.writeToScrol(self)       
  sleep(2)
  htmlData = url.getHtml()
  print(htmlData)
  self.scr.insert(tk.INSERT, htmlData)

HTML 数据现在显示在我们的 GUI 小部件中。

如何做...

它是如何工作的...

我们创建了一个新的模块,用于将获取网页数据的代码与我们的 GUI 代码分离。这始终是一件好事。我们读取网页数据,然后在解码后将其返回给调用代码。然后我们使用按钮回调函数将返回的数据放置在ScrolledText控件中。

本章向我们介绍了一些高级的 Python 编程概念,我们将这些概念结合起来,创建了一个功能性的图形用户界面程序。

第七章. 通过我们的 GUI 在我们的 MySQL 数据库中存储数据

在本章中,我们将通过连接到 MySQL 数据库来增强我们的 Python GUI。

  • 从 Python 连接到 MySQL 数据库

  • 配置 MySQL 连接

  • 设计 Python 图形用户界面数据库

  • 使用 SQL 插入命令

  • 使用 SQL 更新命令

  • 使用 SQL 删除命令

  • 从我们的 MySQL 数据库存储和检索数据

简介

在我们能够连接到 MySQL 服务器之前,我们必须能够访问一个 MySQL 服务器。本章的第一个菜谱将向您展示如何安装免费的 MySQL Server Community Edition。

在成功连接到我们的 MySQL 服务器运行实例后,我们将设计和创建一个数据库,该数据库将接受一个书名,这可能是我们自己的日记或我们在互联网上找到的引言。我们需要一个书的页码,这可能为空,然后我们将使用内置在 Python 3 中的 GUI 将我们从书籍、期刊、网站或朋友那里喜欢的引言插入到我们的 MySQL 数据库中。

我们将使用我们的 Python GUI 插入、修改、删除并显示我们最喜欢的引语,通过发出这些 SQL 命令并显示数据。

注意事项

CRUD 是一个数据库术语,你可能遇到过,它是四个基本 SQL 命令的缩写,代表 创建读取更新删除

从 Python 连接到 MySQL 数据库

在我们能够连接到 MySQL 数据库之前,我们必须先连接到 MySQL 服务器。

为了做到这一点,我们需要知道 MySQL 服务器的 IP 地址以及它监听的端口号。

我们还必须是一个注册用户,并拥有密码,以便通过 MySQL 服务器进行身份验证。

准备就绪

您需要访问一个正在运行的 MySQL 服务器实例,并且您还需要拥有管理员权限,以便创建数据库和表。

可以从官方 MySQL 网站免费获取 MySQL Community Edition。您可以从以下链接下载并安装到您的本地电脑:dev.mysql.com/downloads/

注意事项

在本章中,我们使用的是 MySQL Community Server (GPL) 版本:5.6.26。

如何做到……

为了连接到 MySQL,我们首先需要安装一个特殊的 Python 连接器驱动程序。这个驱动程序将使我们能够从 Python 与 MySQL 服务器进行通信。

驱动程序在 MySQL 网站上免费提供,并附带一个非常好的在线教程。您可以从以下链接安装它:

MySQL 连接器 Python 文档

注意事项

确保你选择与已安装的 Python 版本匹配的安装程序。在本章中,我们使用的是 Python 3.4 的安装程序。

如何做…

在安装过程的最后出现了一点小小的惊喜。当我们启动.msi安装程序时,我们会短暂地看到一个显示安装进度的消息框,但随后它就消失了。我们没有收到安装实际成功的确认。

验证我们是否安装了正确的驱动程序,该驱动程序能让 Python 与 MySQL 通信的一种方法,是查看 Python 的 site-packages 目录。

如果你的 site-packages 目录看起来与以下截图相似,并且你看到一些名字中包含mysql_connector_python的新文件,那么,我们确实安装了某些东西……

如何做…

如上所述的官方 MySQL 网站提供了一个教程,网址如下:

MySQL 连接器 Python 教程

在线教程示例中,如何验证安装 Connector/Python 驱动程序是否成功的工作示例有些误导,因为它试图连接到一个未自动创建的员工数据库,至少在我的社区版中是这样的。

验证我们的 Connector/Python 驱动程序是否真正安装成功的方法是,在不指定特定数据库的情况下连接到 MySQL 服务器,然后打印出连接对象。

注意事项

将占位符括号名称 <adminUser><adminPwd> 替换为您在 MySQL 安装中使用的实际凭据。

如果你安装了 MySQL 社区版,你就是管理员,并在 MySQL 安装过程中选择了用户名和密码。

import mysql.connector as mysql

conn = mysql.connect(user=<adminUser>, password=<adminPwd>,
                     host='127.0.0.1')
print(conn)

conn.close()

如果运行前面的代码导致以下输出打印到控制台,那么我们就没问题。

如何做…

如果你无法连接到 MySQL 服务器,那么很可能是在安装过程中出现了问题。如果是这种情况,请尝试卸载 MySQL,重启你的电脑,然后再次运行 MySQL 安装程序。请确保你下载的 MySQL 安装程序与你的 Python 版本相匹配。如果你安装了多个 Python 版本,有时这会导致混淆,因为最后安装的版本会被添加到 Windows 路径环境变量中,而一些安装程序只是使用它们在这个位置找到的第一个 Python 版本。

那是我安装了 Python 32 位版本,除了我的 64 位版本之外,我困惑的是为什么我下载的一些模块无法工作。

安装程序下载了 32 位模块,这些模块与 64 位版本的 Python 不兼容。

它是如何工作的…

为了将我们的图形用户界面连接到 MySQL 服务器,如果我们想创建自己的数据库,我们需要能够以管理员权限连接到服务器。

如果数据库已经存在,我们只需要连接、插入、更新和删除数据的授权权限。

在下一个菜谱中,我们将在 MySQL 服务器上创建一个新的数据库。

配置 MySQL 连接

在之前的配方中,我们通过将认证所需的凭据硬编码到connection方法中来使用连接到 MySQL 服务器的最短路径。虽然这是一种早期开发中的快速方法,但我们绝对不希望将我们的 MySQL 服务器凭据暴露给任何人,除非我们授权特定用户对数据库、表、视图和相关数据库命令的访问权限。

通过将凭据存储在配置文件中来验证 MySQL 服务器是一种更安全的方法,这正是我们在本食谱中将要做的。

我们将使用我们的配置文件连接到 MySQL 服务器,然后在 MySQL 服务器上创建我们自己的数据库。

注意事项

我们将在以下所有菜谱中使用这个数据库。

准备就绪

运行此菜谱中所示代码需要访问具有管理员权限的运行中的 MySQL 服务器。

注意事项

之前的配方展示了如何安装 MySQL 服务器的免费社区版。管理员权限将使您能够实施此配方。

如何做到……

首先,我们在MySQL.py代码的同一模块中创建一个字典。

# create dictionary to hold connection info
dbConfig = {
    'user': <adminName>,      # use your admin name 
    'password': <adminPwd>,   # use your admin password
    'host': '127.0.0.1',      # IP address of localhost
    }

接下来,在连接方法中,我们解包字典值。而不是编写,

mysql.connect('user': <adminName>,  'password': <adminPwd>, 'host': '127.0.0.1') 

我们使用(**dbConfig),它和上面的一样,但更简洁。

import mysql.connector as mysql
# unpack dictionary credentials 
conn = mysql.connect(**dbConfig)
print(conn)

这导致与 MySQL 服务器建立相同的成功连接,但不同之处在于连接方式不再暴露任何关键任务信息。

注意事项

数据库服务器对你的任务至关重要。一旦你失去了宝贵的资料……而且找不到任何最近的备份!

如何做…

现在,将相同的用户名、密码、数据库等信息放入同一 Python 模块的字典中,并不能消除凭证被任何查看代码的人看到的危险。

为了提高数据库安全性,我们首先将字典移动到它自己的 Python 模块中。让我们称这个新的 Python 模块为GuiDBConfig.py

我们随后导入这个模块并解包凭证,就像之前做的那样。

import GuiDBConfig as guiConf
# unpack dictionary credentials 
conn = mysql.connect(**guiConf.dbConfig)
print(conn)

注意事项

一旦我们将此模块放置到一个安全的地方,与代码的其他部分分离,我们就为我们的 MySQL 数据实现了更高的安全级别。

现在我们已经知道了如何连接到 MySQL 并拥有管理员权限,我们可以通过以下命令创建自己的数据库:

GUIDB = 'GuiDB'

# unpack dictionary credentials 
conn = mysql.connect(**guiConf.dbConfig)

cursor = conn.cursor()

try:
    cursor.execute("CREATE DATABASE {} DEFAULT CHARACTER SET 'utf8'".format(GUIDB))

except mysql.Error as err:
    print("Failed to create DB: {}".format(err))

conn.close()

为了执行对 MySQL 的命令,我们从连接对象创建一个游标对象。

光标通常是在数据库表中的特定行中的一个位置,我们可以将其在表中上下移动,但在这里我们使用它来创建数据库本身。

我们将 Python 代码封装在一个 try…except 块中,并使用 MySQL 内置的错误代码来告诉我们是否出现了任何问题。

我们可以通过执行创建数据库的代码两次来验证这个块是否工作。第一次,它将在 MySQL 中创建一个新的数据库,第二次它将打印出一个错误信息,指出该数据库已存在。

我们可以通过执行以下 MySQL 命令,使用完全相同的游标对象语法来验证哪些数据库存在。

我们不是使用CREATE DATABASE命令,而是创建一个游标并使用它来执行SHOW DATABASES命令,然后将结果检索并打印到控制台输出。

import mysql.connector as mysql
import GuiDBConfig as guiConf

# unpack dictionary credentials 
conn = mysql.connect(**guiConf.dbConfig)

cursor = conn.cursor()

cursor.execute("SHOW DATABASES")
print(cursor.fetchall())

conn.close()

注意事项

我们通过在游标对象上调用fetchall方法来检索结果。

运行此代码可显示我们 MySQL 服务器实例中当前存在的数据库。从输出结果中我们可以看到,MySQL 自带了几个内置数据库,例如information_schema等。我们已经成功创建了我们的guidb数据库,这在输出结果中有所显示。所有其他展示的数据库都是随 MySQL 一起提供的。

如何做…

注意,尽管我们在创建数据库时使用了混合大小写的字母将其命名为 GuiDB,但 SHOW DATABASES 命令在 MySQL 中显示所有现有数据库时都使用小写字母,并将我们的数据库显示为 guidb

它是如何工作的…

为了将我们的 Python 图形用户界面连接到 MySQL 数据库,我们首先必须了解如何连接到 MySQL 服务器。这需要建立连接,并且只有当我们能够提供所需的凭据时,MySQL 才会接受这个连接。

虽然在 Python 代码中放置字符串很容易,但当我们处理数据库时,我们必须非常谨慎,因为今天的个人沙盒开发环境,到明天,可能会轻易地被公之于众的万维网上。

你不希望妥协数据库的安全性,本食谱的第一部分展示了通过将连接凭证放入一个单独的文件,并将此文件放置在一个外部世界无法访问的位置,来提高安全性的方法。

在实际的生产环境中,MySQL 服务器的安装、连接凭证以及此 dbConfig 文件将由 IT 系统管理员处理,他们会允许您导入 dbConfig 文件以连接到 MySQL 服务器,而无需您知道实际的凭证是什么。解压 dbConfig 不会像我们代码中那样暴露凭证。

第二部分我们在 MySQL 服务器实例中创建了自己的数据库,我们将在接下来的菜谱中扩展并使用这个数据库,将其与我们的 Python 图形用户界面结合使用。

设计 Python 图形用户界面数据库

在我们开始创建表格并将数据插入其中之前,我们必须设计数据库。与更改本地 Python 变量名不同,一旦数据库创建并加载了数据,更改数据库模式就不是那么容易了。

我们将不得不DROP掉该表,这意味着我们会丢失表中所有的数据。因此,在删除表之前,我们必须先提取数据,然后DROP掉该表,并在不同的名称下重新创建它,最后再重新导入原始数据。

你明白我的意思了……

设计我们的 GUI MySQL 数据库意味着首先思考我们的 Python 应用程序将如何使用它,然后为我们的表选择与预期用途相匹配的名称。

准备就绪

我们正在使用在前一个菜谱中创建的 MySQL 数据库。需要一个正在运行的 MySQL 实例,前两个菜谱展示了如何安装 MySQL 以及所有必要的附加驱动程序,以及如何创建本章中使用的数据库。

如何做到……

首先,我们将小部件从我们在上一道菜谱中创建的两个标签页之间移动,以便更好地组织我们的 Python GUI,以便连接到 MySQL 数据库。

我们重命名了几个小部件,并将访问 MySQL 数据的代码分离到之前称为标签 1 的部分,同时将无关的小部件移动到我们在早期菜谱中称为标签 2 的部分。

我们还调整了一些内部 Python 变量名,以便更好地理解我们的代码。

注意事项

代码可读性是一种编程美德,而不是浪费时间。

我们重构的 Python GUI 现在看起来如下截图所示。我们将第一个标签重命名为 MySQL,并创建了两个 tkinter LabelFrame 小部件。我们标记了上面的一个,命名为 Python 数据库,它包含两个标签和六个 tkinter 输入小部件以及三个按钮,我们使用 tkinter 网格布局管理器将它们排列成四行三列。

我们将在条目小部件中输入书籍标题和页码,点击按钮将导致插入、检索或修改书籍引用。

底部的 LabelFrame 标签为书籍引用,该框架中的 ScrolledText 小部件将显示我们的书籍和引用。

如何做…

我们将创建两个 SQL 表来存储我们的数据。第一个表将存储书籍标题和书籍页码的数据。然后我们将与第二个表进行连接,该表将存储书籍引文。

我们将通过主键与外键关系将两个表连接起来。

那么,我们现在就创建第一个数据库表。

在我们进行之前,让我们首先验证一下我们的数据库确实没有表。根据在线 MySQL 文档,查看数据库中存在的表的命令如下。

注意

13.7.5.38 SHOW TABLES 语法:

SHOW [FULL] TABLES [{FROM | IN} db_name]
    [LIKE 'pattern' | WHERE expr]

需要注意的是,在前面的语法中,方括号中的参数如FULL是可选的,而在花括号中的参数如FROMSHOW TABLES命令的描述中是必需的。FROMIN之间的管道符号表示 MySQL 语法要求其中之一。

# unpack dictionary credentials 
conn = mysql.connect(**guiConf.dbConfig)
# create cursor 
cursor = conn.cursor()
# execute command
cursor.execute("SHOW TABLES FROM guidb")
print(cursor.fetchall())

# close connection to MySQL
conn.close()

当我们在 Python 中执行 SQL 命令时,我们得到了预期的结果,即一个空列表,这表明我们的数据库目前没有任何表。

如何做…

我们也可以首先通过执行USE <DB>命令来选择数据库。现在,我们不需要将其传递给SHOW TABLES命令,因为我们已经选择了我们想要与之通信的数据库。

以下代码产生的结果与上一个代码相同:

cursor.execute("USE guidb")
cursor.execute("SHOW TABLES")

现在我们知道了如何验证我们的数据库中没有表,那么让我们创建一些表。在我们创建了两个表之后,我们将使用之前相同的命令来验证它们确实已经进入了我们的数据库。

我们通过执行以下代码创建了第一个名为 Books 的表。

# connect by unpacking dictionary credentials
conn = mysql.connect(**guiConf.dbConfig)

# create cursor 
cursor = conn.cursor()

# select DB
cursor.execute("USE guidb")

# create Table inside DB
cursor.execute("CREATE TABLE Books (       \
      Book_ID INT NOT NULL AUTO_INCREMENT, \
      Book_Title VARCHAR(25) NOT NULL,     \
      Book_Page INT NOT NULL,              \
      PRIMARY KEY (Book_ID)                \
    ) ENGINE=InnoDB")

# close connection to MySQL
conn.close()

我们可以通过执行以下命令来验证该表是否已创建在我们的数据库中。

如何做…

现在的结果不再是空列表,而是一个包含元组的列表,显示了刚刚创建的books表。

我们可以使用 MySQL 命令行客户端查看我们表中的列。为了做到这一点,我们必须以 root 用户身份登录。我们还需要在命令末尾添加一个分号。

注意事项

在 Windows 上,您只需双击在 MySQL 安装过程中自动安装的 MySQL 命令行客户端快捷方式。

如果您桌面上没有快捷方式,您可以在以下路径找到典型默认安装的可执行文件:

C:\Program Files\MySQL\MySQL Server 5.6\bin\mysql.exe

没有快捷方式运行 MySQL 客户端时,您必须传递给它一些参数:

  • C:\Program Files\MySQL\MySQL Server 5.6\bin\mysql.exe

  • --defaults-file=C:\ProgramData\MySQL\MySQL Server 5.6\my.ini

  • -uroot

  • -p

双击快捷方式,或者使用带有完整路径的执行文件并通过传递所需参数的命令行,都会启动 MySQL 命令行客户端,并提示您输入 root 用户的密码。

如果你记得在安装过程中分配给 root 用户的密码,那么你可以运行以下所示的SHOW COLUMNS FROM books;命令。这将显示我们 guidb 中的books表的列。

注意事项

当在 MySQL 客户端执行命令时,语法不是 Pythonic。

如何做…

接下来,我们将创建第二个表格,用于存储书籍和期刊引用。我们将通过执行以下代码来创建它:

# select DB
cursor.execute("USE guidb")

# create second Table inside DB
cursor.execute("CREATE TABLE Quotations ( \
        Quote_ID INT,                     \
        Quotation VARCHAR(250),           \
        Books_Book_ID INT,                \
        FOREIGN KEY (Books_Book_ID)       \
            REFERENCES Books(Book_ID)     \
            ON DELETE CASCADE             \
    ) ENGINE=InnoDB")

执行SHOW TABLES命令现在显示我们的数据库有两个表。

如何做…

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

如何做…

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

如何做…

MySQL 客户端仍然以更清晰的格式显示我们的列,当你运行此客户端时可以观察到这一点。

它是如何工作的…

我们设计了我们的 Python 图形用户界面数据库,并对我们的 GUI 进行了重构,以准备使用我们新的数据库。然后我们创建了一个 MySQL 数据库,并在其中创建了两个表。

我们通过使用 Python 和 MySQL 服务器附带的自带 MySQL 客户端,验证了表格已成功录入我们的数据库。

在下一个菜谱中,我们将向我们的表格中插入数据。

使用 SQL 插入命令

本食谱展示了整个 Python 代码,展示了如何创建和删除 MySQL 数据库和表,以及如何显示现有数据库、表、列和我们的 MySQL 实例中的数据。

在创建数据库和表之后,我们将在此菜谱中创建的两个表中插入数据。

注意事项

我们正在使用主键到外键的关系来连接两个表的数据。

我们将在接下来的两个菜谱中详细介绍这是如何工作的,其中我们将修改和删除我们的 MySQL 数据库中的数据。

准备就绪

本菜谱基于我们在上一菜谱中创建的 MySQL 数据库,同时也展示了如何删除并重新创建 GuiDB。

注意事项

删除数据库当然会删除数据库中所有表中的数据,因此我们也会向您展示如何重新插入这些数据。

如何做到……

我们MySQL.py模块的完整代码位于本章的代码文件夹中,您可以从 Packt Publishing 的网站上下载。它创建数据库,向其中添加表,然后向我们所创建的两个表中插入数据。

在这里,我们将概述代码,而不展示所有实现细节,以节省空间,因为展示整个代码将占用太多页面。

import mysql.connector as mysql
import GuiDBConfig as guiConf

class MySQL():
    # class variable
    GUIDB  = 'GuiDB'   

    #------------------------------------------------------
    def connect(self):
        # connect by unpacking dictionary credentials
        conn = mysql.connector.connect(**guiConf.dbConfig)

        # create cursor 
        cursor = conn.cursor()    

        return conn, cursor

    #------------------------------------------------------
    def close(self, cursor, conn):
        # close cursor

    #------------------------------------------------------
    def showDBs(self):
        # connect to MySQL

    #------------------------------------------------------
    def createGuiDB(self):
        # connect to MySQL

    #------------------------------------------------------
    def dropGuiDB(self):
        # connect to MySQL

    #------------------------------------------------------
    def useGuiDB(self, cursor):
        '''Expects open connection.'''
        # select DB

    #------------------------------------------------------
    def createTables(self):
        # connect to MySQL

        # create Table inside DB

    #------------------------------------------------------
    def dropTables(self):
        # connect to MySQL

    #------------------------------------------------------
    def showTables(self):
        # connect to MySQL

    #------------------------------------------------------
    def insertBooks(self, title, page, bookQuote):
        # connect to MySQL

        # insert data

    #------------------------------------------------------
    def insertBooksExample(self):
        # connect to MySQL

        # insert hard-coded data

    #------------------------------------------------------
    def showBooks(self):
        # connect to MySQL

    #------------------------------------------------------
    def showColumns(self):
        # connect to MySQL

    #------------------------------------------------------
    def showData(self):
        # connect to MySQL

#------------------------------------------------------
if __name__ == '__main__': 

    # Create class instance
    mySQL = MySQL()

运行前面的代码会在我们创建的数据库中创建以下表格和数据。

如何做…

它是如何工作的…

我们已经创建了一个 MySQL 数据库,建立了与它的连接,然后创建了两个表格,用于存储喜爱的书籍或期刊引文的资料。

我们将数据分布在两个表中,因为引文往往相当长,而书名和书页码则非常短。通过这样做,我们可以提高我们数据库的效率。

注意事项

在 SQL 数据库语言中,将数据分离到单独的表中称为规范化。

使用 SQL 更新命令

这个菜谱将使用前一个菜谱中的代码,对其进行更详细的解释,然后扩展代码以更新我们的数据。

为了更新我们之前插入到 MySQL 数据库表中的数据,我们使用 SQL 的UPDATE命令。

准备就绪

这个菜谱是在上一个菜谱的基础上构建的,因此请阅读并学习上一个菜谱,以便跟随本菜谱中的编码,其中我们将修改现有数据。

如何做到……

首先,我们将通过运行以下 Python 到 MySQL 命令来显示要修改的数据:

import mysql.connector as mysql
import GuiDBConfig as guiConf

class MySQL():
    # class variable
    GUIDB  = 'GuiDB'
    #------------------------------------------------------
    def showData(self):
        # connect to MySQL
        conn, cursor = self.connect()   

        self.useGuiDB(cursor)      

        # execute command
        cursor.execute("SELECT * FROM books")
        print(cursor.fetchall())

        cursor.execute("SELECT * FROM quotations")
        print(cursor.fetchall())

        # close cursor and connection
        self.close(cursor, conn)
#==========================================================
if __name__ == '__main__': 
    # Create class instance
    mySQL = MySQL()
    mySQL.showData()

运行代码会生成以下结果:

如何做…

我们可能不同意“四人帮”的观点,所以让我们改变他们著名的编程名言。

注意事项

四大天王是四位创建了世界著名的书籍《设计模式》,这本书极大地影响了我们整个软件行业,使我们认识到、思考和编写使用软件设计模式的方法。

我们将通过更新我们最喜欢的引言数据库来完成这项工作。

首先,我们通过搜索书名来检索主键值,然后我们将该值传递到我们的引言搜索中。

    #------------------------------------------------------
    def updateGOF(self):
        # connect to MySQL
        conn, cursor = self.connect()   

        self.useGuiDB(cursor)      

        # execute command
        cursor.execute("SELECT Book_ID FROM books WHERE Book_Title = 'Design Patterns'")
        primKey = cursor.fetchall()[0][0]
        print(primKey)

        cursor.execute("SELECT * FROM quotations WHERE Books_Book_ID = (%s)", (primKey,))
        print(cursor.fetchall())

        # close cursor and connection
        self.close(cursor, conn) 
#==========================================================
if __name__ == '__main__': 
    # Create class instance
    mySQL = MySQL()
    mySQL.updateGOF()

这给我们以下结果:

如何做…

现在我们知道了引用的主键,我们可以通过执行以下命令来更新引用。

    #------------------------------------------------------
    def updateGOF(self):
        # connect to MySQL
        conn, cursor = self.connect()   

        self.useGuiDB(cursor)      

        # execute command
        cursor.execute("SELECT Book_ID FROM books WHERE Book_Title = 'Design Patterns'")
        primKey = cursor.fetchall()[0][0]
        print(primKey)

        cursor.execute("SELECT * FROM quotations WHERE Books_Book_ID = (%s)", (primKey,))
        print(cursor.fetchall())

        cursor.execute("UPDATE quotations SET Quotation = (%s) WHERE Books_Book_ID = (%s)", \
                       ("Pythonic Duck Typing: If it walks like a duck and talks like a duck it probably is a duck...", primKey))

        # commit transaction
        conn.commit ()

        cursor.execute("SELECT * FROM quotations WHERE Books_Book_ID = (%s)", (primKey,))
        print(cursor.fetchall())

        # close cursor and connection
        self.close(cursor, conn)
#==========================================================
if __name__ == '__main__': 
    # Create class instance
    mySQL = MySQL()
    #------------------------
    mySQL.updateGOF()
    book, quote = mySQL.showData()    
    print(book, quote)

通过运行前面的代码,我们使这个编程经典更加符合 Python 风格。

如下截图所示,在我们运行前面的代码之前,我们的标题“Book_ID 1”通过主键与外键关系与引用表中的“Books_Book_ID”列中的引用相关联。

这是从《设计模式》这本书中的原文引用。

我们随后通过 SQL 的 UPDATE 命令更新了与此 ID 相关的引用。

所有 ID 都没有改变,但现在与Book_ID 1关联的引用已经改变,如第二个 MySQL 客户端窗口所示,如下所示。

如何做…

它是如何工作的…

在这个菜谱中,我们从数据库和之前菜谱中创建的数据库表中检索了现有数据。我们使用 SQL 的UPDATE命令将数据插入到表中并更新了我们的数据。

使用 SQL 删除命令

在这个菜谱中,我们将使用 SQL 的 DELETE 命令来删除之前菜谱中创建的数据。

虽然乍一看删除数据可能显得微不足道,但一旦我们在生产环境中拥有一个相当大的数据库设计,事情可能就不再那么简单了。

因为我们通过将两个表通过主键与外键关系关联来设计我们的 GUI 数据库,所以在删除某些数据时,我们不会留下孤立的记录,因为这个数据库设计考虑了级联删除。

准备就绪

本菜谱使用了 MySQL 数据库、表以及本章之前菜谱中插入到这些表中的数据。为了展示如何创建孤立记录,我们不得不更改我们数据库表的设计之一。

如何做到……

我们通过仅使用两个数据库表来简化了数据库设计。

当我们删除数据时,虽然这种方法可行,但总有可能最终留下孤立记录。这意味着我们在一个表中删除了数据,但不知何故没有删除另一个 SQL 表中的相关数据。

如果我们在创建quotations表格时没有与books表格的外键关系,我们可能会得到孤立记录。

        # create second Table inside DB -- 
        # No FOREIGN KEY relation to Books Table
        cursor.execute("CREATE TABLE Quotations ( \
                Quote_ID INT AUTO_INCREMENT,      \
                Quotation VARCHAR(250),           \
                Books_Book_ID INT,                \
                PRIMARY KEY (Quote_ID)            \
            ) ENGINE=InnoDB")  

在将数据插入到booksquotations表之后,如果我们执行与之前相同的delete语句,我们只会删除Book_ID 1的书籍,而与Books_Book_ID 1相关的引用却被留在了后面。

这是一个孤立的记录。不再存在一个具有Book_ID1的书籍记录。

如何做…

这种情况可能会造成混乱,我们可以通过使用级联删除来避免。

我们通过添加某些数据库约束来实现这一点。在之前的一个菜谱中创建包含引文的表时,我们创建了一个带有外键约束的quotations表,该约束明确引用了books表的主键,从而将两者联系起来。

        # create second Table inside DB
        cursor.execute("CREATE TABLE Quotations ( \
                Quote_ID INT AUTO_INCREMENT,      \
                Quotation VARCHAR(250),           \
                Books_Book_ID INT,                \
                PRIMARY KEY (Quote_ID),           \
                FOREIGN KEY (Books_Book_ID)       \
                    REFERENCES Books(Book_ID)     \
                    ON DELETE CASCADE             \
            ) ENGINE=InnoDB")  

外键关系包含ON DELETE CASCADE属性,这基本上告诉我们的 MySQL 服务器,当与这个外键相关的记录被删除时,要删除该表中相关的记录。

注意事项

在创建我们的表时如果没有指定ON DELETE CASCADE属性,我们既不能删除也不能更新我们的数据,因为UPDATE是一个DELETE操作后跟一个INSERT操作。

由于这种设计,不会留下任何孤立的记录,这正是我们所希望的。

注意事项

在 MySQL 中,我们必须指定ENGINE=InnoDB才能使用外键。

让我们在数据库中显示数据。

#==========================================================
if __name__ == '__main__': 
    # Create class instance
    mySQL = MySQL()
      mySQL.showData()

这显示了我们在数据库表中的以下数据:

如何做…

这表明我们有两个通过主键到外键关系相关联的记录。

当我们现在在books表中删除一条记录时,我们期望quotations表中的相关记录也会通过级联删除被删除。

让我们通过在 Python 中执行以下 SQL 命令来尝试这个方法:

import mysql.connector as mysql
import GuiDBConfig as guiConf

class MySQL():
    #------------------------------------------------------
    def deleteRecord(self):
        # connect to MySQL
        conn, cursor = self.connect()   

        self.useGuiDB(cursor)      

        # execute command
        cursor.execute("SELECT Book_ID FROM books WHERE Book_Title = 'Design Patterns'")
        primKey = cursor.fetchall()[0][0]
        # print(primKey)

        cursor.execute("DELETE FROM books WHERE Book_ID = (%s)", (primKey,))

        # commit transaction
        conn.commit ()

        # close cursor and connection
        self.close(cursor, conn)    
#==========================================================
if __name__ == '__main__': 
    # Create class instance
    mySQL = MySQL()
    #------------------------
    mySQL.deleteRecord()
    mySQL.showData()   

执行完前面的删除记录命令后,我们得到以下新的结果:

如何做…

注意

著名的 设计模式 已从我们的精选语录数据库中消失……

它是如何工作的…

我们通过将数据库设计成通过主键到外键关系实现级联删除的方式,在这个菜谱中触发了级联删除。

这保持了我们的数据正常和完整。

注意

在这个菜谱和示例代码中,我们有时使用相同的表名,有时首字母大写,有时全部小写。

这适用于 Windows 默认安装的 MySQL,但如果不更改设置,可能在 Linux 上可能不起作用。

这里有一个链接到官方 MySQL 文档:dev.mysql.com/doc/refman/5.0/en/identifier-case-sensitivity.html

在下一个菜谱中,我们将使用我们 Python GUI 中的MySQL.py模块的代码。

从我们的 MySQL 数据库存储和检索数据

我们将使用我们的 Python 图形用户界面将数据插入到我们的 MySQL 数据库表中。我们已经重构了在之前的菜谱中构建的 GUI,为连接和使用数据库做准备。

我们将使用两个文本框输入小部件,在其中我们可以输入书籍或期刊的标题和页码。我们还将使用一个滚动文本小部件来输入我们最喜欢的书籍引文,然后将其存储在我们的 MySQL 数据库中。

准备就绪

本菜谱将基于我们在之前的菜谱中创建的 MySQL 数据库和表进行构建。

如何做到……

我们将使用我们的 Python 图形用户界面插入、检索和修改我们最喜欢的引文。为此,我们已经重构了 GUI 中的 MySQL 标签页。

如何做…

为了让按钮执行某些操作,我们将它们连接到回调函数,就像我们在之前的菜谱中做的那样。

我们将在按钮下方显示 ScrolledText 小部件中的数据。

为了完成这个任务,我们将导入MySQL.py模块,就像我们之前做的那样。所有与我们的 MySQL 服务器实例和数据库交互的代码都驻留在该模块中,这是面向对象编程精神中封装代码的一种形式。

我们将插入引用按钮连接到以下回调函数。

        # Adding a Button
        self.action = ttk.Button(self.mySQL, text="Insert Quote", command=self.insertQuote)   
        self.action.grid(column=2, row=1)
    # Button callback
    def insertQuote(self):
        title = self.bookTitle.get()
        page = self.pageNumber.get()
        quote = self.quote.get(1.0, tk.END)
        print(title)
        print(quote)
        self.mySQL.insertBooks(title, page, quote)  

当我们现在运行我们的代码时,我们可以将我们的 Python GUI 中的数据插入到我们的 MySQL 数据库中。

如何做…

在输入书籍标题、书籍页码以及书籍或电影中的引用后,我们通过点击插入引用按钮将数据插入到我们的数据库中。

我们当前的设计允许添加标题、页码和引文。我们还可以插入电影中的我们喜欢的引言。虽然电影没有页码,但我们可以使用页码列来插入引言在电影中出现的近似时间。

接下来,我们可以通过发出之前使用的相同命令来验证所有这些数据是否已进入我们的数据库表。

如何做…

在插入数据后,我们可以通过点击获取报价按钮来验证数据是否已成功进入我们的两个 MySQL 表,该按钮随后会显示我们插入到两个 MySQL 数据库表中的数据,如上图所示。

点击获取报价按钮将调用我们与按钮点击事件关联的回调方法。这为我们提供了在 ScrolledText 小部件中显示的数据。

# Adding a Button
        self.action1 = ttk.Button(self.mySQL, text="Get Quotes", command=self.getQuote)   
        self.action1.grid(column=2, row=2)
    # Button callback
    def getQuote(self):
        allBooks = self.mySQL.showBooks()  
        print(allBooks)
        self.quote.insert(tk.INSERT, allBooks)

我们使用self.mySQL类实例变量来调用showBooks()方法,该方法是我们导入的 MySQL 类的一部分。

from B04829_Ch07_MySQL import MySQL
class OOP():
    def __init__(self):
        # create MySQL instance
        self.mySQL = MySQL()

class MySQL():
    #------------------------------------------------------
    def showBooks(self):
        # connect to MySQL
        conn, cursor = self.connect()    

        self.useGuiDB(cursor)    

        # print results
        cursor.execute("SELECT * FROM Books")
        allBooks = cursor.fetchall()
        print(allBooks)

        # close cursor and connection
        self.close(cursor, conn)   

        return allBooks  

它是如何工作的…

在这个菜谱中,我们导入了包含所有连接到我们的 MySQL 数据库的编码逻辑的 Python 模块,并且知道如何插入、更新、删除和显示数据。

我们已经将我们的 Python GUI 连接到这个 SQL 逻辑。

第八章:国际化和测试

在本章中,我们将国际化并测试我们的 Python GUI,涵盖以下菜谱:

  • 在不同语言中显示小部件文本

  • 一次性更改整个 GUI 语言

  • 本地化 GUI

  • 准备 GUI 进行国际化

  • 如何以敏捷的方式设计 GUI

  • 我们需要测试 GUI 代码吗?

  • 设置调试监视器

  • 配置不同的调试输出级别

  • 使用 Python 的 main 部分 创建自测代码

  • 使用单元测试创建健壮的 GUI

  • 如何使用 Eclipse PyDev IDE 编写单元测试

简介

在本章中,我们将通过在标签、按钮、选项卡和其他小部件上显示文本,以不同的语言国际化我们的图形用户界面。

我们将简单开始,然后探讨如何在设计层面为我们的 GUI 进行国际化准备。

我们还将本地化 GUI,这与国际化略有不同。

注意事项

由于这些单词较长,它们已被缩写为使用单词的首字母,接着是首字母和最后一个字母之间的总字符数,最后是单词的最后一个字母。

因此,国际化变为 I18N,而本地化变为 L10N。

我们还将测试我们的 GUI 代码,编写单元测试,并探索单元测试在我们开发努力中可以提供的价值,这将引导我们走向重构代码的最佳实践。

在不同语言中显示小部件文本

在 Python 中将文本字符串国际化最简单的方法是将它们移动到一个单独的 Python 模块中,然后通过向该模块传递一个参数来选择在 GUI 中显示的语言。

虽然这种方法并不特别推荐,但根据在线搜索结果,根据您正在开发的特定应用需求,这种方法可能仍然是实现起来最实用和最快的方法。

准备就绪

我们将重用之前创建的 Python GUI。我们注释掉了一行创建 MySQL 选项卡的 Python 代码,因为在本章中我们不与 MySQL 数据库进行交互。

如何做到这一点...

在这个菜谱中,我们将开始通过将窗口标题从英语更改为其他语言来国际化我们的 GUI。

由于“GUI”这个名称在其它语言中也是相同的,我们首先扩展这个名称,以便我们能够看到我们更改的视觉效果。

让我们更改之前的代码行:

self.win.title("Python GUI")

到:

self.win.title("Python Graphical User Interface")

之前的代码更改导致我们的 GUI 程序出现以下标题:

如何做...

注意事项

在本章中,我们将使用英语和德语来举例说明国际化我们的 Python 图形用户界面(GUI)的原则。

将字符串硬编码到代码中从来不是一个好主意,因此我们可以采取的第一步是,将我们 GUI 中可见的所有字符串分离到一个独立的 Python 模块中。这是开始国际化我们 GUI 可见方面的第一步。

注意事项

当我们进行国际化(I18N)时,我们将一次性完成这个非常积极的重构和语言翻译。

让我们创建一个新的 Python 模块,并将其命名为 Resources.py。接下来,我们将我们的 GUI 标题的英文字符串移动到这个模块中,然后将其导入到我们的 GUI 代码中。

注意事项

我们正在将 GUI 与其显示的语言分离,这是一个面向对象设计原则。

我们的新 Python 模块,包含国际化字符串,现在看起来是这样的:

Class I18N():
'''Internationalization'''
    def __init__(self, language):
        if    language == 'en': self.resourceLanguageEnglish()
        elif  language == 'de': self.resourceLanguageGerman()
        else: raise NotImplementedError('Unsupported language.')

    def resourceLanguageEnglish(self):
        self.title = "Python Graphical User Interface"

    def resourceLanguageGerman(self):
        self.title = 'Python Grafische Benutzeroberflaeche'

我们将这个新的 Python 模块导入到我们的主要 Python GUI 代码中,然后使用它。

from B04829_Ch08_Resources import I18N
class OOP():
    def __init__(self): 
        self.win = tk.Tk()                # Create instance
        self.i18n = I18N('de')            # Select language
        self.win.title(self.i18n.title)   # Add a title

根据我们传递给 I18N 类的语言,我们的 GUI 将以该语言显示。

运行前面的代码,我们得到以下国际化结果:

如何做...

它是如何工作的...

我们正在将作为我们 GUI 一部分的硬编码字符串拆分到它们自己的独立模块中。我们通过创建一个类来实现这一点,并在类的__init__()方法中,根据传入的语言参数选择我们的 GUI 将显示哪种语言。

这有效。

我们可以通过将国际化字符串分离到单独的文件中进一步模块化我们的代码,这些文件可能是 XML 格式或其他格式。我们还可以从 MySQL 数据库中读取它们。

注意事项

这是一个“关注点分离”的编码方法,它是面向对象编程的核心。

一次性更改整个 GUI 语言

在这个菜谱中,我们将通过重构之前所有硬编码的英文字符串到一个单独的 Python 模块中,然后对这些字符串进行国际化,一次性更改整个 GUI 显示名称。

这个示例表明,避免在 GUI 显示的字符串中硬编码,而是将 GUI 代码与 GUI 显示的文本分离,是一个良好的设计原则。

注意

以模块化方式设计我们的 GUI 使得国际化它变得更加容易。

准备就绪

我们将继续使用之前菜谱中的 GUI。在那个菜谱中,我们已将 GUI 的标题进行了国际化。

如何做到这一点...

为了国际化我们所有 GUI 小部件中显示的文本,我们必须将所有硬编码的字符串移动到一个单独的 Python 模块中,这就是我们接下来要做的。

之前,我们 GUI 显示的单词字符串散布在我们的 Python 代码中。

这是我们未进行国际化(I18N)的图形用户界面(GUI)的样子。

如何做...

每个小部件的每一行字符串,包括我们 GUI 的标题、标签控制名称等等,都是硬编码的,并且与创建 GUI 的代码混合在一起。

注意事项

在我们 GUI 软件开发过程的设计阶段,考虑如何最好地国际化我们的 GUI 是一个好主意。

以下是我们代码的摘录。

WIDGET_LABEL = ' Widgets Frame '
class OOP():
    def __init__(self): 
        self.win = tk.Tk()              # Create instance
        self.win.title("Python GUI")    # Add a title

    # Radiobutton callback function
    def radCall(self):
        radSel=self.radVar.get()
        if   radSel == 0: self.monty2.configure(text='Blue')
        elif radSel == 1: self.monty2.configure(text='Gold')
        elif radSel == 2: self.monty2.configure(text='Red')

在这个菜谱中,我们正在国际化我们 GUI 小部件中显示的所有字符串。我们不会国际化输入到我们的 GUI 中的文本输入,因为这取决于您电脑上的本地设置。

以下为英文国际化字符串的代码:

classI18N():
'''Internationalization'''

    def __init__(self, language):
        if   language == 'en': self.resourceLanguageEnglish()
        elif language == 'de': self.resourceLanguageGerman()
        else: raiseNotImplementedError('Unsupported language.')

    def resourceLanguageEnglish(self):
        self.title = "Python Graphical User Interface"

        self.file  = "File"
        self.new   = "New"
        self.exit  = "Exit"
        self.help  = "Help"
        self.about = "About"

        self.WIDGET_LABEL = ' Widgets Frame '

        self.disabled  = "Disabled"
        self.unChecked = "UnChecked"
        self.toggle    = "Toggle"

        # Radiobutton list
        self.colors   = ["Blue", "Gold", "Red"]
        self.colorsIn = ["in Blue", "in Gold", "in Red"]

        self.labelsFrame  = ' Labels within a Frame '
        self.chooseNumber = "Choose a number:"
        self.label2       = "Label 2"

        self.mgrFiles = ' Manage Files '

        self.browseTo = "Browse to File..."
        self.copyTo   = "Copy File To :   "

在我们的 Python GUI 模块中,所有之前硬编码的字符串现在都已被我们新的 I18N 类实例所替换,该类位于 Resources.py 模块中。

这里是我们重构的 GUI.py 模块的一个示例:

from B04829_Ch08_Resources import I18N

class OOP():
    def __init__(self): 
        self.win = tk.Tk()              # Create instance
        self.i18n = I18N('de')          # Select language
        self.win.title(self.i18n.title) # Add a title

    # Radiobutton callback function
    def radCall(self):
          radSel = self.radVar.get()
        if   radSel == 0: self.widgetFrame.configure(text=self.i18n.WIDGET_LABEL + self.i18n.colorsIn[0])
        elif radSel == 1: self.widgetFrame.configure(text=self.i18n.WIDGET_LABEL + self.i18n.colorsIn[1])
        elif radSel == 2: self.widgetFrame.configure(text=self.i18n.WIDGET_LABEL + self.i18n.colorsIn[2])

注意到之前所有硬编码的英文字符串都已替换为对我们新 I18N 类实例的调用。

一个例子是 self.win.title(self.i18n.title)

这为我们提供了国际化 GUI 的能力。我们只需使用相同的变量名,并通过传递一个参数来组合它们,以选择我们希望显示的语言。

我们也可以在 GUI 中动态更改语言,或者我们可以读取本地 PC 设置,并根据这些设置决定我们的 GUI 文本应该显示哪种语言。

我们现在可以通过简单地用相应的单词填充变量名来实现对德语的翻译。


class I18N():
    '''Internationalization'''
    def __init__(self, language):      
        if   language == 'en': self.resourceLanguageEnglish()
        elif language == 'de': self.resourceLanguageGerman()
        else: raise NotImplementedError('Unsupported language.')

def resourceLanguageGerman(self):
        self.file  = "Datei"
        self.new   = "Neu"
        self.exit  = "Schliessen"
        self.help  = "Hilfe"
        self.about = "Ueber"

        self.WIDGET_LABEL = ' Widgets Rahmen '

        self.disabled  = "Deaktiviert"
        self.unChecked = "NichtMarkiert"
        self.toggle    = "Markieren"

        # Radiobutton list
        self.colors   = ["Blau", "Gold", "Rot"]    
        self.colorsIn = ["in Blau", "in Gold", "in Rot"]  

        self.labelsFrame  = ' EtikettenimRahmen '
        self.chooseNumber = "WaehleeineNummer:"
        self.label2       = "Etikette 2"

        self.mgrFiles = ' DateienOrganisieren '

        self.browseTo = "WaehleeineDatei... "
        self.copyTo   = "KopiereDateizu :     "

在我们的 GUI 代码中,现在我们可以通过一行 Python 代码来更改整个 GUI 显示语言。

class OOP():
    def __init__(self): 
        self.win = tk.Tk()        # Create instance
        self.i18n = I18N('de')    # Pass in language

运行前面的代码将创建以下国际化 GUI:

如何做...

它是如何工作的...

为了国际化我们的图形用户界面,我们将硬编码的字符串重构为一个独立的模块,然后通过传递一个字符串作为我们的 I18N 类初始化器,使用相同的类成员来国际化我们的 GUI,从而有效控制 GUI 显示的语言。

本地化 GUI

在完成我们 GUI 国际化的第一步之后,下一步就是本地化它。我们为什么要这样做呢?

嗯,在美国,我们都是牛仔,我们生活在不同的时区。

所以虽然我们“国际化”到了美国,但我们的马儿还是会根据不同的时区醒来(并且会期待按照它们自己内在的马儿时区时间表被喂食)。

这就是本地化的作用所在。

准备工作

我们通过本地化扩展了在上一道菜谱中开发的图形用户界面。

如何做到这一点...

我们首先通过使用 pip 安装 Python 的 pytz 时区模块。我们在命令处理器提示符中输入以下命令:

pip install pytz

注意事项

在这本书中,我们使用的是 Python 3.4 版本,该版本内置了 pip 模块。如果你使用的是较旧版本的 Python,那么你可能需要先安装 pip 模块。

当成功时,我们得到以下结果。

如何做...

注意

截图显示命令已下载.whl格式的文件。如果您尚未这样做,可能需要先安装 Python 的wheel模块。

这将 Python 的 pytz 模块安装到了 site-packages 文件夹中,因此现在我们可以从我们的 Python GUI 代码中导入此模块。

我们可以通过运行以下代码列出所有现有的时区,该代码将在我们的ScrolledText小部件中显示时区。首先,我们在 GUI 中添加一个新的Button小部件。

import pytz
class OOP():

    # TZ Button callback
    def allTimeZones(self):
        for tz in all_timezones:
            self.scr.insert(tk.INSERT, tz + '\n')

    def createWidgets(self):
        # Adding a TZ Button
        self.allTZs = ttk.Button(self.widgetFrame, 
                                 text=self.i18n.timeZones, 
                                 command=self.allTimeZones)
        self.allTZs.grid(column=0, row=9, sticky='WE')

点击我们新的按钮小部件将产生以下输出:

如何做...

在我们安装了 tzlocal Python 模块之后,我们可以通过运行以下代码来打印我们的当前区域设置:

    # TZ Local Button callback
    def localZone(self):   
        from tzlocal import get_localzone
        self.scr.insert(tk.INSERT, get_localzone())

    def createWidgets(self):
        # Adding local TZ Button
        self.localTZ = ttk.Button(self.widgetFrame, 
                                  text=self.i18n.localZone, 
                                  command=self.localZone
        self.localTZ.grid(column=1, row=9, sticky='WE')

我们已经将Resources.py中两个新动作Buttons的字符串进行了国际化处理。

        self.timeZones = "All Time Zones"
        self.localZone = "Local Zone"
        self.timeZones = "Alle Zeitzonen"
        self.localZone = "Lokale Zone"

点击我们新的按钮现在会告诉我们我们所在的时区(嘿,我们不知道这个,不是吗……)。

如何做...

我们现在可以将本地时间转换为不同的时区。让我们以美国东部标准时间为例。

我们通过改进现有代码,在未使用的标签 2 中显示我们当前的本地时间。

import pytz
from datetime import datetime
class OOP():
    # Format local US time
    def getDateTime(self):
        fmtStrZone = ""%Y-%m-%d %H:%M:%S""
        self.lbl2.set(datetime.now().strftime(fmtStrZone))

        # Place labels into the container element
        ttk.Label(labelsFrame, text=self.i18n.chooseNumber).grid(column=0, row=0)
        self.lbl2 = tk.StringVar()
        self.lbl2.set(self.i18n.label2)
        ttk.Label(labelsFrame, textvariable=self.lbl2).grid(column=0, row=1)

        # Adding getTimeTZ Button
        self.dt = ttk.Button(self.widgetFrame, text=self.i18n.getTime, command=self.getDateTime)
        self.dt.grid(column=2, row=9, sticky='WE')

当我们运行代码时,我们的国际化标签 2(在德语中显示为Etikette 2)将显示当前本地时间。

如何做...

我们现在可以通过先将本地时间转换为协调世界时UTC),然后应用从导入的pytz模块中的timezone函数来更改我们的本地时间为美国东部标准时间(US EST)。

import pytz
class OOP():
    # Format local US time with TimeZone info
    def getDateTime(self):
        fmtStrZone = "%Y-%m-%d %H:%M:%S %Z%z"
        # Get Coordinated Universal Time
        utc = datetime.now(timezone('UTC'))
        print(utc.strftime(fmtStrZone))

        # Convert UTC datetime object to Los Angeles TimeZone
        la = utc.astimezone(timezone('America/Los_Angeles'))
        print(la.strftime(fmtStrZone))

        # Convert UTC datetime object to New York TimeZone
        ny = utc.astimezone(timezone('America/New_York'))
        print(ny.strftime(fmtStrZone))

        # update GUI label with NY Time and Zone
        self.lbl2.set(ny.strftime(fmtStrZone))

点击现在重命名为“纽约”的按钮将产生以下输出:

如何做...

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

如何做...

注意事项

UTC(协调世界时)从不观察夏令时。在东部夏令时EDT)UTC 比当地时间快四小时,而在标准时间EST)UTC 比当地时间快五小时。

它是如何工作的...

为了本地化日期和时间信息,我们首先需要将我们的本地时间转换为协调世界时(UTC)。然后,我们应用timezone信息,并使用pytz Python 时区模块中的astimezone函数将时间转换为世界上任何时区!

在这个菜谱中,我们将美国西海岸的当地时间转换成了协调世界时(UTC),然后在我们的图形用户界面(GUI)的标签 2 中显示了美国东海岸的时间。

准备 GUI 进行国际化

在这个菜谱中,我们将通过实现认识到,将英语翻译成外语并不像预期的那样事事都那么简单,来为我们的 GUI 国际化做准备。

我们仍然有一个问题需要解决,那就是如何正确显示来自外语的非英语 Unicode 字符。

人们可能预期 Python 3 会自动处理显示德语的ä、ö和ü Unicode 重音字符,但实际上并非如此。

准备就绪

我们将继续使用我们在最近章节中开发的 Python GUI。首先,我们将把 GUI.py 初始化代码中的默认语言更改为德语。

我们通过取消注释行self.i18n = I18N('de')来实现这一点。

如何做到这一点...

当我们将单词 Ueber 改为正确的德语 Űber 并使用重音符号时,Eclipse PyDev 插件不太高兴。

如何做...

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

如何做...

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

如何做...

注意事项

我们当然可以始终求助于 Unicode 的直接表示。

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

如何做...

虽然这个解决方案确实很丑陋,但它确实有效。我们不需要直接输入字符Ü,而是可以传递 Unicode 编码\u00DC 来正确地在我们的 GUI 中显示这个字符。

如何做...

我们也可以使用 Eclipse 中的 PyDev 直接接受默认编码从 Cp1252 更改为 UTF-8,但我们可能并不总是收到这样的提示。

相反,我们可能会看到以下错误信息显示:

如何做...

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

如何做...

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

如何做...

它是如何工作的...

国际化和处理外文 Unicode 字符通常不像我们希望的那样简单直接。有时,我们不得不寻找解决方案,通过 Python 直接使用前缀\u来表示 Unicode 字符可以解决这个问题。

在其他时候,我们只需找到我们的开发环境设置进行调整。

如何以敏捷的方式设计 GUI

现代敏捷软件开发的设计和编码方法源于软件专业人士的经验教训。这种方法适用于图形用户界面(GUI)和其他任何代码。敏捷软件开发的主要关键之一是持续进行的重构过程。

代码重构如何帮助我们进行软件开发的一个实际例子是首先通过使用函数来实现一些简单的功能。

随着我们的代码复杂性增加,我们可能希望将我们的函数重构为类的成员方法。这种方法将使我们能够移除全局变量,并且使我们在类内部放置方法的位置更加灵活。

虽然我们代码的功能没有改变,但其结构已经发生了变化。

在这个过程中,我们进行编码、测试、重构,然后再进行测试。我们以短周期进行,通常从实现某些功能所需的最小代码量开始。

注意事项

测试驱动软件开发是敏捷开发方法中的一种特定风格。

虽然我们的图形用户界面(GUI)运行得很好,但我们的主要GUI.py代码的复杂性一直在不断增加,开始变得有点难以对代码有一个全面的了解。

这意味着我们需要重构我们的代码。

准备就绪

我们将重构在前几章中创建的 GUI。我们将使用 GUI 的英文版本。

如何做到这一点...

我们在之前的菜谱中已经将 GUI 显示的所有名称都进行了国际化处理。这为我们重构代码提供了一个非常好的起点。

注意事项

重构是改进现有代码结构、可读性和可维护性的过程。我们并不是在添加新功能。

在前几章和食谱中,我们一直采用“自上而下”的瀑布式开发方法扩展我们的 GUI,将import语句添加到顶部,并将代码编写到现有代码的底部。

当查看代码时这很有用,但现在看起来有点杂乱,我们可以改进这一点以帮助我们的未来发展。

让我们先清理一下我们的import语句部分,它目前看起来是这样的:

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu
from tkinter import Spinbox
import B04829_Ch08_ToolTip as tt
from threading import Thread
from time import sleep
from queue import Queue
from tkinter import filedialog as fd
from os import path 
from tkinter import messagebox as mBox
from B04829_Ch08_MySQL import MySQL
from B04829_Ch08_Resources import I18N
from datetime import datetime
from pytz import all_timezones, timezone

# Module level GLOBALS
GLOBAL_CONST = 42

通过简单地分组相关导入,我们可以减少代码行的数量,这提高了我们导入的可读性,使它们看起来不那么令人压倒。

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk, scrolledtext, Menu, Spinbox, filedialog as fd, messagebox as mBox
from queue import Queue
from os import path 
import B04829_Ch08_ToolTip as tt
from B04829_Ch08_MySQL import MySQL
from B04829_Ch08_Resources import I18N
from B04829_Ch08_Callbacks_Refactored import Callbacks
from B04829_Ch08_Logger import Logger, LogLevel

# Module level GLOBALS
GLOBAL_CONST = 42

我们可以通过将回调方法拆分到它们自己的模块中来进一步重构我们的代码。这样做通过将不同的导入语句分离到它们所需的模块中,提高了可读性。

让我们将我们的 GUI.py 重命名为 GUI_Refactored.py 并创建一个新的模块,我们将其命名为 Callbacks_Refactored.py

这为我们带来了这个新的架构。

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk, scrolledtext, Menu, Spinbox, \
                    filedialog as fd, messagebox as mBox
from queue import Queue
from os import path 
import B04829_Ch08_ToolTip as tt
from B04829_Ch08_MySQL import MySQL
from B04829_Ch08_Resources import I18N
from B04829_Ch08_Callbacks_Refactored import Callbacks

# Module level GLOBALS
GLOBAL_CONST = 42

class OOP():
    def __init__(self): 

        # Callback methods now in different module
        self.callBacks = Callbacks(self)

注意我们在调用Callbacks初始化器时传递了我们自己的 GUI 类实例(self)。

我们的新回调类如下:

#======================
# imports
#======================
import tkinter as tk
from time import sleep
from threading import Thread
from pytz import all_timezones, timezone
from datetime import datetime

class Callbacks():
    def __init__(self, oop):
        self.oop = oop

    def defaultFileEntries(self): 
        self.oop.fileEntry.delete(0, tk.END)
        self.oop.fileEntry.insert(0, 'Z:\\')        # bogus path
        self.oop.fileEntry.config(state='readonly')         
        self.oop.netwEntry.delete(0, tk.END)
        self.oop.netwEntry.insert(0, 'Z:\\Backup')  # bogus path

    # Combobox callback 
    def _combo(self, val=0):
        value = self.oop.combo.get()
        self.oop.scr.insert(tk.INSERT, value + '\n')

在我们新类的初始化器中,传入的 GUI 实例被命名为self.oop并在此新的 Python 类模块中全程使用。

运行重构后的 GUI 代码仍然有效。我们只是提高了代码的可读性,并简化了代码的复杂性,为后续的开发工作做准备。

它是如何工作的...

我们首先通过将相关的导入语句分组来提高了代码的可读性。接下来,我们将回调方法独立成自己的类和模块,以进一步降低代码的复杂性。

我们在之前的菜谱中已经采用了相同的面向对象方法,即让ToolTip类位于自己的模块中,并且国际化了所有 GUI 字符串。

在这个菜谱中,我们在重构方面更进一步,通过将我们自己的实例传递给 GUI 依赖的回调方法的类。

注意事项

现在我们更好地理解了模块化方法在软件开发中的价值,我们很可能会在未来的软件设计中采用这种方法。

我们需要测试 GUI 代码吗?

在编码阶段以及发布服务包或修复漏洞时,测试我们的软件是一项重要的活动。

测试有不同的级别。第一个级别是开发者测试,通常从编译器或解释器不允许我们运行有错误的代码开始,迫使我们测试代码中单个方法的各个小部分。

这是第一层防御。

防御性编码的第二层是当我们的源代码控制系统通知我们有一些冲突需要解决,并且不允许我们提交修改后的代码。

这在我们以专业方式在开发者团队中工作时非常有用且绝对必要。源代码控制系统是我们的朋友,它会指出是 ourselves 或其他开发者提交到特定分支或树顶部的更改,并告诉我们我们的本地代码版本已经过时,并且存在一些需要在我们提交代码到仓库之前解决的冲突。

本部分假设您使用源代码控制系统来管理和存储您的代码。例如,包括 git、mercurial、svn 以及其他几个。Git 是一个非常流行的源代码控制工具,并且对于单个用户是免费的。

第三级是 API 级别,我们通过只允许通过发布的接口与我们的代码进行交互,来封装我们代码中可能出现的未来变化。

注意事项

请参阅“面向接口编程,而非实现”,《设计模式》,第 17 页。

另一个测试级别是集成测试,当最终建成的桥梁的一半与其它开发团队创建的另一半相遇时,这两部分并不处于相同的高度(比如说,一半最终比另一半高出两米或码……)。

然后,还有最终用户测试。虽然我们按照他们指定的来构建,但这并不是他们真正想要的。

哎呀……我想所有前面的例子都是我们需要在设计阶段和实现阶段测试代码的有效理由。

准备就绪

我们将测试我们在最近几道菜谱和章节中创建的图形用户界面。我们还将展示一些可能出错的情况以及为什么我们需要持续测试我们的代码以及通过 API 调用的代码。

如何做到这一点...

虽然许多经验丰富的开发者习惯在调试代码时到处撒播 printf() 语句,但 21 世纪的许多开发者已经习惯了现代 IDE 开发环境,这些环境能有效地加快开发速度。

在这本书中,我们使用的是 Eclipse IDE 的 PyDev Python 插件。

如果你刚开始使用带有 PyDev 插件的 IDE,如 Eclipse,一开始可能会觉得有点令人不知所措。Python 3 自带的 Python IDLE 工具也拥有一个更简单的调试器,你可能希望先探索一下它。

当我们的代码出现问题时,我们必须进行调试。进行这一过程的第一步是设置断点,然后逐行或逐方法地逐步执行我们的代码。

在代码中进进出出是日常活动,直到代码运行顺畅为止。

在 Python 图形用户界面编程中,可能出现的第一种错误之一是遗漏导入所需的模块或导入现有的模块。

这里有一个简单的例子:

如何做...

我们正在尝试创建一个 tkinter 类的实例,但事情并没有像预期的那样工作。

好吧,我们只是忘记导入模块了,我们可以在类创建上方添加一行 Python 代码来修复这个问题,那里是导入语句所在的位置。

#======================
# imports
#======================
import tkinter as tk

这是一个例子,其中我们的开发环境为我们进行测试。我们只需进行调试和代码修复。

另一个与开发者测试更为密切相关的例子是,当我们编写条件语句时,在常规开发过程中,并没有对所有逻辑分支进行测试。

以上一章的例子来说明,假设我们点击了获取报价按钮,并且这个操作成功了,但我们从未点击过修改报价按钮。第一个按钮的点击产生了预期的结果,但第二个按钮的点击抛出了一个异常(因为我们还没有实现这段代码,可能完全忘记了它)。

如何做...

点击Mody Quote按钮将生成以下结果:

如何做...

另一个潜在的错误区域是当一个函数或方法突然不再返回预期的结果。比如说,我们正在调用以下这个函数,它原本会返回预期的结果。

如何做...

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

如何做...

我们不是通过乘法,而是通过传入数字的幂来计算,结果也不再是以前的样子了。

注意事项

在软件测试中,这种错误被称为回归。

它是如何工作的...

在这个菜谱中,我们通过展示代码可能出错和引入软件缺陷(即错误)的几个示例,强调了在软件开发生命周期的几个阶段进行软件测试的重要性。

设置调试监视器

在现代集成开发环境IDEs)如 Eclipse 中的 PyDev 插件或另一个 IDE 如 NetBeans 中,我们可以在代码执行期间设置调试监视器来监控我们的 GUI 状态。

这与微软的 Visual Studio IDE 以及更近期的 Visual Studio.NET 版本非常相似。

注意事项

设置调试监视器是一种非常方便的方式来帮助我们开发工作。

准备就绪

在这个菜谱中,我们将重用我们在早期菜谱中开发的 Python GUI。我们正在逐步检查我们之前开发的代码并设置调试监视器。

如何做这件事...

注意事项

虽然这个菜谱适用于基于 Java 的 Eclipse IDE 中的 PyDev 插件,但其原则也适用于许多现代 IDE。

我们可能希望设置断点的第一个位置是在我们通过调用 tkinter 主事件循环使 GUI 可见的地方。

左侧的绿色气球符号是 PyDev/Eclipse 中的断点。当我们以调试模式执行代码时,一旦执行达到断点,代码的执行将会停止。此时,我们可以看到当前作用域内所有变量的值。我们还可以在调试窗口中输入表达式,这些表达式将会被执行,并显示结果。如果结果是我们所期望的,我们可能会决定使用刚刚学到的知识来修改我们的代码。

我们通常通过在 IDE 工具栏中点击图标或使用键盘快捷键(例如按下F5进入代码,F6跳过,F7退出当前方法)来逐步执行代码。

如何做...

将断点放置在我们所在的位置然后进入这段代码,结果却成了一个麻烦,因为我们最终进入了一些我们目前并不想调试的低级 tkinter 代码。我们可以通过点击“Step-Out”工具栏图标(位于项目菜单下方右侧的第三个黄色箭头)或者按F7键(假设我们在使用 Eclipse 中的 PyDev)来退出低级 tkinter 代码。

我们通过点击截图右侧的调试工具栏图标开始调试会话。如果我们不进行调试而执行,则点击带有白色三角形的绿色圆圈,这是位于 bug 图标右侧的图标。

如何做...

一个更好的想法是将我们的断点放置得更靠近我们自己的代码,以便观察我们自己的 Python 变量的一些值。

在现代图形用户界面的事件驱动世界中,我们必须将断点放置在事件发生时被调用的代码上,例如按钮点击。

目前,我们主要的功能之一位于一个按钮点击事件中。当我们点击标有纽约的按钮时,我们创建一个事件,然后导致我们的 GUI 发生某些操作。

让我们在名为 getDateTime()纽约 按钮回调方法处设置一个断点。

当我们现在运行调试会话时,我们将停在断点处,然后我们可以启用作用域内变量的监视。

在 Eclipse 中使用 PyDev,我们可以右键点击一个变量,然后从弹出菜单中选择监视命令。变量的名称、其类型和当前值将在下一张截图所示的“表达式调试窗口”中显示。我们还可以直接在表达式窗口中输入。

我们所关注的变量不仅限于简单的数据类型。我们可以观察类实例、列表、字典等等。

当观察这些更复杂对象时,我们可以在表达式窗口中展开它们,并深入到类实例、字典等所有值的细节中。

我们通过点击位于每个变量旁边名称列最左侧出现的观察变量左侧的三角形来完成这个操作。

如何做...

当我们在打印不同时区位置的值时,从长远来看,设置调试监视器要方便和高效得多。我们不必用过时的 C 风格printf()语句来使我们的代码变得杂乱。

注意事项

如果你感兴趣,想学习如何安装带有 PyDev 插件的 Eclipse 用于 Python,有一个很棒的教程会指导你开始安装所有必要的免费软件,然后通过创建一个简单、可工作的 Python 程序来介绍 Eclipse 中的 PyDev。www.vogella.com/tutorials/Python/article.html

它是如何工作的...

我们在 21 世纪使用现代的集成开发环境(IDEs),这些环境是免费提供的,帮助我们创建稳固的代码。

这个菜谱展示了如何设置调试监视器,这是每个开发者技能集中的一个基本工具。即使在不是寻找错误的情况下逐步执行我们的代码,也能确保我们理解我们的代码,并且可以通过重构来改进我们的代码。

以下是我读过的第一本编程书籍《Java 编程思想》中的一段引言,该书由布鲁斯·埃克尔所著。

*"抵制急于求成的冲动,它只会让你更慢。"
--布鲁斯·埃克尔

几乎二十年后,这些建议经受了时间的考验。

注意事项

调试监视器帮助我们创建稳固的代码,并且不是浪费时间。

配置不同的调试输出级别

在这个菜谱中,我们将配置不同的调试级别,我们可以在运行时选择和更改这些级别。这使我们能够控制在调试代码时我们想要深入到代码中的程度。

我们将创建两个新的 Python 类,并将它们都放入同一个模块中。

我们将使用四种不同的日志级别,并将我们的调试输出写入我们将创建的日志文件。如果日志文件夹不存在,我们也将自动创建它。

日志文件的名称是执行脚本的名称,即我们重构后的GUI.py。我们也可以通过传递日志类初始化器的完整路径来为我们的日志文件选择其他名称。

准备就绪

我们将继续使用之前菜谱中重构的 GUI.py 代码。

如何做到这一点...

首先,我们创建一个新的 Python 模块,并将两个新的放入其中。第一个非常简单,用于定义日志级别。这基本上是一个枚举

class LogLevel:
'''Define logging levels.'''
    OFF     = 0
    MINIMUM = 1
    NORMAL  = 2
    DEBUG   = 3

第二个class通过使用传入的文件名完整路径创建一个日志文件,并将其放置在logs文件夹中。在首次运行时,logs文件夹可能不存在,因此代码会自动创建该文件夹。

class Logger:   
    ''' Create a test log and write to it. '''     
    #-------------------------------------------------------
    def __init__(self, fullTestName, loglevel=LogLevel.DEBUG):
        testName = os.path.splitext(os.path.basename(fullTestName))[0]
        logName  = testName  + '.log'    

        logsFolder = 'logs'          
        if not os.path.exists(logsFolder):                     
            os.makedirs(logsFolder, exist_ok = True)

        self.log = os.path.join(logsFolder, logName)           
        self.createLog()

        self.loggingLevel = loglevel
        self.startTime    = time.perf_counter()

    #------------------------------------------------------
    def createLog(self):    
        with open(self.log, mode='w', encoding='utf-8') as logFile:
            logFile.write(self.getDateTime() + 
                          '\t\t*** Starting Test ***\n')
        logFile.close()

为了写入我们的日志文件,我们使用writeToLog()方法。在方法内部,我们首先做的事情是检查消息的日志级别是否高于我们设定的目标日志输出限制。如果消息的级别较低,我们就丢弃它并立即从方法返回。

如果消息具有我们想要显示的日志级别,我们接着检查它是否以换行符开头,如果是的话,我们就通过使用 Python 的切片操作符(msg = msg[1:])从索引 1 开始切片来丢弃换行符。

我们随后将一行写入日志文件,包括当前日期时间戳、两个制表符空格、我们的消息,并以换行符结束。

    def writeToLog(self, msg='', loglevel=LogLevel.DEBUG): 
        # control how much gets logged
        if loglevel > self.loggingLevel:
            return

        # open log file in append mode 
        with open(self.log, mode='a', encoding='utf-8') as logFile:
            msg = str(msg)
            if msg.startswith('\n'):
                msg = msg[1:]
            logFile.write(self.getDateTime() + '\t\t' + msg + '\n')

        logFile.close()

我们现在可以导入我们的新 Python 模块,并在我们的 GUI 代码的__init__部分创建Logger类的实例。

from os import path 
from B04829_Ch08_Logger import Logger
class OOP():
    def __init__(self): 
        # create Logger instance
        fullPath = path.realpath(__file__)
        self.log = Logger(fullPath)
        print(self.log)

我们通过 path.realpath(__file__) 获取正在运行的 GUI 脚本的完整路径,并将其传递给 Logger 类的初始化器。如果 logs 文件夹不存在,我们的 Python 代码会自动创建它。

这产生了以下结果:

如何做...

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

如何做...

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

如何做...

它是如何工作的...

在这个菜谱中,我们创建了自己的日志类。虽然 Python 自带了日志模块,但创建自己的日志类非常简单,这使我们能够完全控制日志的格式。当我们将自己的日志输出与 MS Excel 或我们在前一章中探索的 Matplotlib 结合使用时,这非常有用。

在下一个示例中,我们将使用 Python 内置的 __main__ 功能来使用我们刚刚创建的四个不同的日志级别。

使用 Python 的 main 部分 创建自测代码

Python 自带一个非常实用的功能,允许每个模块进行自我测试。利用这个功能是确保我们的代码更改不会破坏现有代码的绝佳方式,此外,__main__自我测试部分还可以作为每个模块工作方式的文档。

注意事项

几个月或几年后,我们有时会忘记我们的代码在做什么,所以代码本身有注释说明确实非常有帮助。

在可能的情况下,为每个 Python 模块添加一个自我测试部分是个好主意。有时可能做不到,但在大多数模块中,这样做是可行的。

准备就绪

我们将扩展之前的配方,因此,为了理解这个配方中的代码在做什么,我们首先必须阅读并理解之前配方的代码。

如何做到这一点...

首先,我们将通过将这个自测部分添加到我们的Resources.py模块中,来探索 Python __main__自测部分的力量。每当运行一个包含此自测部分且位于模块底部的模块时,当模块自行执行时,此代码将会运行。

当模块从其他模块导入和使用时,__main__ 自测部分的代码将不会执行。

这就是随后截图中所展示的代码:

if __name__ == '__main__':
    language = 'en'
    inst = I18N(language)
    print(inst.title)

    language = 'de'
    inst = I18N(language)
    print(inst.title)

在添加自我测试部分后,我们现在可以单独运行这个模块,并且它会产生有用的输出,同时,也向我们展示了我们的代码按预期工作。

如何做...

我们首先传入英语作为在 GUI 中显示的语言,然后传入德语作为 GUI 将显示的语言。

我们正在打印出我们 GUI 的标题,以显示我们的 Python 模块按预期工作。

注意事项

下一步是使用我们在上一个菜谱中创建的日志功能。

我们通过首先在我们的重构GUI.py模块中添加一个__main__自我测试部分来实现这一点,然后我们验证是否创建了我们Logger类的实例。

如何做...

我们接下来使用显示的命令将信息写入日志文件。我们设计的日志级别默认为记录每条消息,即 DEBUG 级别,因此我们无需进行任何更改。我们只需将需要记录的消息传递给writeToLog方法即可。

if __name__ == '__main__':
#======================
# Start GUI
#======================
oop = OOP()
    print(oop.log)
    oop.log.writeToLog('Test message')
    oop.win.mainloop()

这将被写入我们的日志文件,如下日志截图所示:

如何做...

现在我们可以通过向我们的日志语句添加日志级别来控制日志记录,并设置我们希望输出的级别。让我们将这个功能添加到我们的Callbacks.py模块中的纽约按钮回调方法getDateTime中。

我们将之前的print语句更改为使用不同调试级别的log语句。

GUI.py 文件中,我们导入了来自我们日志模块的新类。

from B04829_Ch08_Logger import Logger, LogLevel

接下来,我们创建这些类的本地实例。

# create Logger instance
fullPath = path.realpath(__file__)
self.log = Logger(fullPath)

# create Log Level instance
self.level = LogLevel()

由于我们将 GUI 类的实例传递给Callbacks.py初始化器,我们可以根据我们创建的LogLevel类使用日志级别约束。

    # Format local US time with TimeZone info
    def getDateTime(self):
        fmtStrZone = "%Y-%m-%d %H:%M:%S %Z%z"
        # Get Coordinated Universal Time
        utc = datetime.now(timezone('UTC'))
        self.oop.log.writeToLog(utc.strftime(fmtStrZone), 
                                self.oop.level.MINIMUM)

        # Convert UTC datetime object to Los Angeles TimeZone
        la = utc.astimezone(timezone('America/Los_Angeles'))
        self.oop.log.writeToLog(la.strftime(fmtStrZone), 
                                self.oop.level.NORMAL)

        # Convert UTC datetime object to New York TimeZone
        ny = utc.astimezone(timezone('America/New_York'))
        self.oop.log.writeToLog(ny.strftime(fmtStrZone), 
                                self.oop.level.DEBUG)

        # update GUI label with NY Time and Zone
        self.oop.lbl2.set(ny.strftime(fmtStrZone))

当我们点击纽约按钮时,根据选择的日志级别,我们会在日志文件中得到不同的输出。默认的日志级别是 DEBUG,这意味着所有内容都会被写入日志。

如何做...

当我们更改日志级别时,我们控制了写入日志的内容。我们通过调用Logger类的setLoggingLevel方法来完成这个操作。

    #----------------------------------------------------------------
    def setLoggingLevel(self, level):  
        '''change logging level in the middle of a test.''' 
        self.loggingLevel = level

在我们 GUI 的__main__部分,我们将日志级别更改为MINIMUM,这导致写入日志文件的输出减少。

if __name__ == '__main__':
#======================
# Start GUI
#======================
oop = OOP()
    oop.log.setLoggingLevel(oop.level.MINIMUM)
    oop.log.writeToLog('Test message')
    oop.win.mainloop()

现在,我们的日志文件不再显示测试消息,只显示符合设置日志级别的消息。

如何做...

它是如何工作的...

在这个菜谱中,我们充分利用了 Python 内置的__main__自我测试部分。我们引入了自己的日志文件,同时介绍了如何创建不同的日志级别。

通过这样做,我们完全控制着写入日志文件的内容。

使用单元测试创建健壮的 GUI

Python 自带了一个内置的单元测试框架,在这个菜谱中,我们将开始使用这个框架来测试我们的 Python GUI 代码。

在我们开始编写单元测试之前,我们希望设计我们的测试策略。我们很容易将单元测试与它们所测试的代码混合在一起,但更好的策略是将应用程序代码与单元测试代码分开。

注意事项

PyUnit 是根据所有其他 xUnit 测试框架的原则设计的。

准备就绪

我们将测试本章中早期创建的国际化 GUI。

如何做到这一点...

为了使用 Python 的内置单元测试框架,我们必须导入 Python 的 unittest 模块。让我们创建一个新的模块,并将其命名为 UnitTests.py

我们首先导入unittest模块,然后创建我们自己的类,并在该类中继承并扩展unittest.TestCase类。

完成此任务的最简单代码如下:

import unittest

class GuiUnitTests(unittest.TestCase):
    pass

if __name__ == '__main__':
    unittest.main()

代码目前还没有做太多,但当我们运行它时,我们没有得到任何错误,这是一个好兆头。

如何做...

我们实际上确实在控制台看到一条输出信息,表明我们成功运行了零个测试…

嗯,这个输出有点误导,因为我们到目前为止所做的只是创建了一个不包含实际测试方法的类。

我们添加了测试方法,这些方法通过遵循所有测试方法的默认命名规则(以单词 "test" 开头)来进行实际的单元测试。这是一个可以更改的选项,但似乎坚持这种命名约定要容易和清晰得多。

让我们添加一个测试方法来测试我们 GUI 的标题。这将验证通过传递预期的参数,我们能够得到预期的结果。

import unittest
from B04829_Ch08_Resources import I18N

class GuiUnitTests(unittest.TestCase):

    def test_TitleIsEnglish(self):
        i18n = I18N('en')
        self.assertEqual(i18n.title, 
                       "Python Graphical User Interface")

我们从Resources.py模块导入我们的I18N类,传入英语作为在 GUI 中显示的语言。由于这是我们第一次单元测试,我们还打印出标题结果,以确保我们知道我们得到了什么。接下来,我们使用unittest assertEqual方法来验证我们的标题是否正确。

运行此代码会显示一个OK,这意味着单元测试通过了。

如何做...

单元测试运行并成功,这通过一个点和单词“OK”来表示。如果它失败了或者出现了错误,我们就不会得到这个点,而是输出“F”或“E”。

我们现在可以通过验证我们 GUI 德语版本的标题来进行相同的自动化单元测试检查。

我们只是复制、粘贴并修改我们的代码。

import unittest
from B04829_Ch08_Resources import I18N

class GuiUnitTests(unittest.TestCase):

    def test_TitleIsEnglish(self):
        i18n = I18N('en')
        self.assertEqual(i18n.title, 
                         "Python Graphical User Interface")

    def test_TitleIsGerman(self):
        i18n = I18N('en')           
        self.assertEqual(i18n.title, 
                         'Python Grafische Benutzeroberfl' 
                       + "\u00E4" + 'che')

现在我们正在测试我们的国际化 GUI 标题在两种语言中的效果,运行代码后得到以下结果:

如何做...

我们运行了两个单元测试,但不是得到一个 OK,而是出现了失败。发生了什么?

我们的断言在 GUI 的德语版本中失败了…

在调试我们的代码时,我们发现,在我们的单元测试代码的复制、粘贴和修改方法中,我们忘记传入德语作为语言参数。我们可以轻松解决这个问题。

    def test_TitleIsGerman(self):
        # i18n = I18N('en')           # <= Bug in Unit Test
        i18n = I18N('de') 
        self.assertEqual(i18n.title, 
                         'Python Grafische Benutzeroberfl' 
                         + "\u00E4" + 'che')

当我们再次运行我们的单元测试时,我们再次得到了所有测试通过预期的结果。

如何做...

注意事项

单元测试代码也是代码,也可能存在错误。

虽然编写单元测试的真正目的是测试我们的应用程序代码,但我们必须确保我们的测试被正确编写。测试驱动开发TDD)方法中的一种方法可能对我们有所帮助。

注意事项

在 TDD(测试驱动开发)中,我们在实际编写应用程序代码之前先开发单元测试。现在,如果一个测试对一个甚至不存在的函数通过了,那么就说明有问题。下一步是创建这个不存在的函数,并确保它将失败。之后,我们就可以编写最少的代码来使单元测试通过。

它是如何工作的...

在这个菜谱中,我们已经开始测试我们的 Python 图形用户界面,使用 Python 编写单元测试。我们看到了 Python 单元测试代码只是代码,也可能包含需要纠正的错误。在下一个菜谱中,我们将扩展这个菜谱的代码,并使用随 Eclipse IDE 的 PyDev 插件一起提供的图形单元测试运行器。

如何使用 Eclipse PyDev IDE 编写单元测试

在上一个菜谱中,我们开始使用 Python 的单元测试功能,在这个菜谱中,我们将通过进一步使用这一功能来确保我们 GUI 代码的质量。

我们将对我们的 GUI 进行单元测试,以确保 GUI 显示的国际化字符串符合预期。

在上一个菜谱中,我们在单元测试代码中遇到了一些错误,但通常情况下,我们的单元测试会发现由修改现有应用程序代码引起的回归错误,而不是单元测试代码。一旦我们验证了我们的单元测试代码是正确的,我们通常不会对其进行更改。

注意事项

我们的单元测试也充当了我们期望代码要做什么的文档。

默认情况下,Python 的单元测试使用文本单元测试运行器执行,并且我们可以在 Eclipse IDE 的 PyDev 插件中运行它。我们还可以从控制窗口运行完全相同的单元测试。

除了本食谱中的文本运行器之外,我们还将探索 PyDev 在 Eclipse IDE 内部使用的图形单元测试功能。

准备就绪

我们正在扩展之前的配方,其中我们开始使用 Python 单元测试。

如何做到这一点...

Python 单元测试框架附带了一些被称为“固定装置”(fixtures)的功能。

请参考以下网址以了解测试夹具的描述:

这意味着我们可以创建setup()teardown()单元测试方法,以便在执行任何单个测试之前,先调用setup()方法,并在每个单元测试结束时调用teardown()方法。

注意事项

此固定功能为我们提供了一个非常可控的环境,在其中我们可以运行我们的单元测试。这类似于使用前置和后置条件。

让我们搭建我们的单元测试环境。我们将创建一个新的测试类,该类专注于之前提到的代码正确性。

注意事项

unittest.main() 运行任何以“test”前缀开头的函数,无论在给定的 Python 模块中我们创建了多少个类。

import unittest
from B04829_Ch08_Resources import I18N
from B04829_Ch08_GUI_Refactored import OOP as GUI

class GuiUnitTests(unittest.TestCase):

    def test_TitleIsEnglish(self):
        i18n = I18N('en')
        self.assertEqual(i18n.title, 
                         "Python Graphical User Interface")

    def test_TitleIsGerman(self):
        # i18n = I18N('en')           # <= Bug in Unit Test
        i18n = I18N('de') 
        self.assertEqual(i18n.title, 
                         'Python Grafische Benutzeroberfl' 
                         + "\u00E4" + 'che')

class WidgetsTestsEnglish(unittest.TestCase):

    def setUp(self):
        self.gui = GUI('en')

    def tearDown(self):
        self.gui = None

    def test_WidgetLabels(self):
        self.assertEqual(self.gui.i18n.file, "File")
        self.assertEqual(self.gui.i18n.mgrFiles, ' Manage Files ')
        self.assertEqual(self.gui.i18n.browseTo, 
                                            "Browse to File...")
if __name__ == '__main__':
    unittest.main()

这将生成以下输出:

如何做...

前面的单元测试代码显示,我们可以创建多个单元测试类,并且通过调用unittest.main,它们都可以在同一个模块中运行。

这也表明,在单元测试报告的输出中,setup() 方法并不算作一个测试(测试数量为 3),同时,它也完成了预期的任务,因为我们现在可以从单元测试方法中访问我们的类实例变量 self.gui

我们对测试所有标签的正确性感兴趣,尤其是在我们对代码进行更改时捕捉到错误。

如果我们从应用程序代码中复制并粘贴了字符串到测试代码中,点击单元测试框架的按钮就能捕捉到任何意外的更改。

我们还想要测试在任何语言中调用我们的Radiobutton小部件时,都会导致labelframe小部件的text属性被更新。为了自动进行这项测试,我们必须做两件事。

首先,我们必须检索labelframe text小部件的值,并将该值分配给一个我们命名为labelFrameText的变量。我们必须使用以下语法,因为该小部件的属性是通过字典数据类型传递和检索的:

self.gui.widgetFrame['text']

我们现在可以通过程序方式点击其中一个单选按钮小部件来验证默认文本,然后是国际化版本。

class WidgetsTestsGerman(unittest.TestCase):

    def setUp(self):
        self.gui = GUI('de')

    def test_WidgetLabels(self):
        self.assertEqual(self.gui.i18n.file, "Datei")
        self.assertEqual(self.gui.i18n.mgrFiles, 
                                        ' Dateien Organisieren ')
        self.assertEqual(self.gui.i18n.browseTo, 
                                        "Waehle eine Datei... ")

    def test_LabelFrameText(self):
        labelFrameText = self.gui.widgetFrame['text']
        self.assertEqual(labelFrameText, " Widgets Rahmen ")
        self.gui.radVar.set(1)
        self.gui.callBacks.radCall()
        labelFrameText = self.gui.widgetFrame['text']
        self.assertEqual(labelFrameText, 
                                    " Widgets Rahmen in Gold")

在验证了默认的labelFrameText之后,我们通过程序设置单选按钮为索引 1,然后程序调用单选按钮的回调方法。

        self.gui.radVar.set(1)
        self.gui.callBacks.radCall()

注意

这基本上与在图形用户界面中点击单选按钮的动作相同,但我们通过代码在单元测试中执行这个按钮点击事件。

然后我们验证labelframe小部件中的文本是否已按预期更改。

当我们在 Eclipse 中使用 Python PyDev 插件运行单元测试时,以下输出将被写入 Eclipse 控制台。

如何做...

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

如何做...

使用 Eclipse,我们还可以选择以 Python 单元测试脚本的形式运行我们的单元测试,而不是简单的 Python 脚本,这样我们可以得到一些彩色的输出,而不是老式 DOS 提示符的黑白世界。

如何做...

单元测试结果条是绿色的,这意味着我们所有的单元测试都通过了。前一个截图也显示,GUI 测试运行器比文本测试运行器慢得多:在 Eclipse 中,前者为 1.01 秒,而后者为 0.466 秒。

它是如何工作的...

我们通过测试labels扩展了我们的单元测试代码,通过程序调用一个Radiobutton,然后在我们的单元测试中验证labelframe小部件的相应text属性是否如预期地发生了变化。我们已经测试了两种不同的语言。

我们随后转向使用内置的 Eclipse/PyDev 图形单元测试运行器。

第九章:使用 wxPython 库扩展我们的 GUI

在本章中,我们将通过使用 wxPython 库来增强我们的 Python GUI。

  • 如何安装 wxPython 库

  • 如何在 wxPython 中创建我们的 GUI

  • 使用 wxPython 快速添加控件

  • 尝试将主 wxPython 应用嵌入到主 tkinter 应用中

  • 尝试将我们的 tkinter GUI 代码嵌入到 wxPython 中

  • 如何使用 Python 控制两个不同的 GUI 框架

  • 如何在两个连接的 GUI 之间进行通信

简介

在本章中,我们将介绍另一个目前不随 Python 一起提供的 Python GUI 工具包。它被称为 wxPython。

这个库有两个版本。原始版本被称为 Classic,而最新版本则被称为其开发项目的代号,Phoenix。

在这本书中,我们仅使用 Python 3 进行编程,并且由于新的凤凰项目旨在支持 Python 3,因此这是我们本章使用的 wxPython 版本。

首先,我们将创建一个简单的 wxPython 图形用户界面,然后我们将尝试将本书中开发的基于 tkinter 的 GUI 与新的 wxPython 库连接起来。

注意

wxPython 是 wxWidgets 的 Python 绑定。

wxPython 中的 w 代表 Windows 操作系统,而 x 代表基于 Unix 的操作系统,例如 Linux 和 OS X。

如果使用这两个 GUI 工具包同时操作不成功,我们将尝试使用 Python 来解决任何问题,然后我们将在 Python 中使用进程间通信IPC)来确保我们的 Python 代码按预期工作。

如何安装 wxPython 库

wxPython 库并非随 Python 一起安装,因此,为了使用它,我们首先必须安装它。

这个菜谱将向我们展示在哪里以及如何找到正确的版本进行安装,以确保与所安装的 Python 版本和正在运行的操作系统相匹配。

注意事项

wxPython 第三方库已经存在超过 17 年,这表明它是一个健壮的库。

准备就绪

为了使用 wxPython 与 Python 3 兼容,我们必须安装 wxPython Phoenix 版本。

如何做到这一点...

在网上搜索 wxPython 时,我们可能会找到官方网站 www.wxpython.org

如何做...

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

如何做...

要在 Python 3 中使用 wxPython,我们必须安装 wxPython/Phoenix 库。我们可以在快照构建链接中找到安装程序:

wxpython.org/Phoenix/snapshot-builds/

从这里,我们可以选择与我们的 Python 版本和操作系统版本相匹配的 wxPython/Phoenix 版本。我正在使用运行在 64 位 Windows 7 操作系统上的 Python 3.4。

如何做...

Python 轮子(.whl)安装包有一个编号方案。

对于我们来说,这个方案最重要的部分是我们正在安装适用于 Python 3.4(安装程序名称中的 cp34)和 Windows 64 位操作系统(安装程序名称中的 win_amd64 部分)的 wxPython/Phoenix 构建。

如何做...

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

如何做...

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

如何做...

注意

wx 是 wxPython/Phoenix 库安装到的文件夹名称。我们将把这个模块导入到我们的 Python 代码中。

我们可以通过执行来自官方 wxPython/Phoenix 网站的简单演示脚本来验证我们的安装是否成功。官方网站的链接是 wxpython.org/Phoenix/docs/html/

import wx
app = wx.App()
frame = wx.Frame(None, -1, "Hello World")
frame.Show()
app.MainLoop()

运行前面的 Python 3 脚本将使用 wxPython/Phoenix 创建以下 GUI。

如何做...

它是如何工作的...

在这个菜谱中,我们成功安装了适用于 Python 3 的正确版本的 wxPython 工具包,我们可以用它来使用 Python 3。我们找到了这个 GUI 工具包的 Phoenix 项目,它是当前活跃的开发线。Phoenix 将在适当的时候取代 Classic wxPython 工具包,并且特别针对与 Python 3 的良好兼容性。

在成功安装了 wxPython/Phoenix 工具包之后,我们只用五行代码就创建了图形用户界面。

注意事项

我们之前通过使用 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 按钮小部件的文档。

如何做...

我们可以非常快速地创建一个带有标题、菜单栏和状态栏的工作窗口。当鼠标悬停在菜单项上时,状态栏会显示该菜单项的文本。这可以通过编写以下代码实现:

# Import wxPython GUI toolkit
import wx

# Subclass wxPython frame
class GUI(wx.Frame):
    def __init__(self, parent, title, size=(200,100)):
        # Initialize super class
        wx.Frame.__init__(self, parent, title=title, size=size)

        # Change the frame background color 
        self.SetBackgroundColour('white')

        # Create Status Bar
        self.CreateStatusBar() 

        # Create the Menu
        menu= wx.Menu()

        # Add Menu Items to the Menu
        menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
        menu.AppendSeparator()
        menu.Append(wx.ID_EXIT,"Exit"," Exit the GUI")

        # Create the MenuBar
        menuBar = wx.MenuBar()

        # Give the MenuBar a Title
        menuBar.Append(menu,"File") 

        # Connect the MenuBar to the frame
        self.SetMenuBar(menuBar)  

        # Display the frame
        self.Show()

# Create instance of wxPython application
app = wx.App()

# Call sub-classed wxPython GUI increasing default Window size
GUI(None, "Python GUI using wxPython", (300,150))

# Run the main GUI event loop
app.MainLoop()

这创建了一个以下 GUI,它使用 wxPython 库用 Python 编写。

如何做...

在之前的代码中,我们继承了wx.Frame。在下面的代码中,我们继承了wx.Panel并将wx.Frame传递给我们的类__init__()方法。

注意事项

在 wxPython 中,顶级 GUI 窗口被称为框架。没有框架就无法创建 wxPython GUI,并且框架必须作为 wxPython 应用程序的一部分来创建。

我们在我们的代码底部创建了应用和框架。

为了将小部件添加到我们的图形用户界面(GUI)中,我们必须将它们附加到一个面板上。面板的父级是框架(我们的顶级窗口),而我们放置到面板中的小部件的父级是面板。

以下代码向一个父级为框架的面板添加了一个多行文本框小部件。我们还向面板小部件添加了一个按钮小部件,当点击该按钮时,会在文本框中打印出一些文本。

这里是完整的代码:

import wx               # Import wxPython GUI toolkit
class GUI(wx.Panel):    # Subclass wxPython Panel
    def __init__(self, parent):

        # Initialize super class
        wx.Panel.__init__(self, parent)

        # Create Status Bar
        parent.CreateStatusBar() 

        # Create the Menu
        menu= wx.Menu()

        # Add Menu Items to the Menu
        menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
        menu.AppendSeparator()
        menu.Append(wx.ID_EXIT,"Exit"," Exit the GUI")

        # Create the MenuBar
        menuBar = wx.MenuBar()

        # Give the Menu a Title
        menuBar.Append(menu,"File") 

        # Connect the MenuBar to the frame
        parent.SetMenuBar(menuBar)  

        # Create a Print Button
        button = wx.Button(self, label="Print", pos=(0,60))

        # Connect Button to Click Event method 
        self.Bind(wx.EVT_BUTTON, self.printButton, button)

        # Create a Text Control widget 
        self.textBox = wx.TextCtrl(
self, size=(280,50), style=wx.TE_MULTILINE)

    def printButton(self, event):
        self.textBox.AppendText(
"The Print Button has been clicked!") 

app = wx.App()      # Create instance of wxPython application
frame = wx.Frame(None, title="Python GUI using wxPython", size=(300,180))     # Create frame
GUI(frame)          # Pass frame into GUI
frame.Show()        # Display the frame
app.MainLoop()      # Run the main GUI event loop

运行前面的代码并点击我们的 wxPython 按钮小部件,将产生以下 GUI 输出:

如何做...

它是如何工作的...

在这个菜谱中,我们使用成熟的 wxPython GUI 工具包创建了我们自己的 GUI。仅用几行 Python 代码,我们就能够创建一个带有 最小化最大化退出 按钮的完整功能 GUI。我们添加了一个菜单栏、一个多行文本控件和一个按钮。我们还创建了一个状态栏,当选择菜单项时它会显示文本。我们将所有这些小部件放置在一个面板容器小部件中。

我们将打印按钮连接到了文本控件。

当鼠标悬停在菜单项上时,一些文本会在状态栏中显示。

使用 wxPython 快速添加控件

在这个菜谱中,我们将重新创建本书早期使用 tkinter 创建的 GUI,但这次我们将使用 wxPython 库。我们将看到使用 wxPython GUI 工具包创建自己的 Python GUI 是多么简单快捷。

我们不会重新创建在前面章节中创建的全部功能。例如,我们不会国际化我们的 wxPython GUI,也不会将其连接到 MySQL 数据库。我们将重新创建 GUI 的视觉方面,并添加一些功能。

注意事项

比较不同的库为我们提供了选择用于我们自己的 Python GUI 开发的工具包,我们可以在自己的 Python 代码中结合使用这些工具包中的几个。

准备就绪

确保您已安装 wxPython 模块以遵循此配方。

如何做到这一点...

首先,我们像之前使用 tkinter 一样创建我们的 Python OOP 类,但这次我们继承并扩展了 wx.Frame 类。为了清晰起见,我们不再将我们的类命名为 OOP,而是将其重命名为 MainFrame

注意

在 wxPython 中,主要的 GUI 窗口被称为 Frame。

我们还创建了一个回调方法,当点击退出菜单项时关闭 GUI,并声明浅灰色元组作为我们 GUI 的背景颜色。

import wx
BACKGROUNDCOLOR = (240, 240, 240, 255)

class MainFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        wx.Frame.__init__(self, *args, **kwargs)

        self.createWidgets()
        self.Show()

    def exitGUI(self, event):     # callback
        self.Destroy()

    def createWidgets(self):   
        self.CreateStatusBar()      # wxPython built-in method
        self.createMenu()
       self.createNotebook()

接下来,我们通过创建一个 wxPython Notebook 类的实例并将其分配为我们自定义的名为 Widgets 的类的父类,向我们的 GUI 添加一个标签控制。

notebook 类实例变量以 wx.Panel 作为其父类。

    def createNotebook(self):
        panel = wx.Panel(self)
        notebook = wx.Notebook(panel)
        widgets = Widgets(notebook) # Custom class explained below
        notebook.AddPage(widgets, "Widgets")
        notebook.SetBackgroundColour(BACKGROUNDCOLOR) 
        # layout
        boxSizer = wx.BoxSizer()
        boxSizer.Add(notebook, 1, wx.EXPAND)
        panel.SetSizerAndFit(boxSizer)  

注意事项

在 wxPython 中,标签控件被命名为Notebook,就像在 tkinter 中一样。

每个Notebook小部件都需要有一个父级,并且为了在 wxPython 中的Notebook中布局小部件,我们使用不同类型的布局管理器。

注意事项

wxPython 的 sizer 是类似于 tkinter 的网格布局管理器的布局管理器。

接下来,我们在笔记本页面中添加控件。我们通过创建一个继承自wx.Panel的单独类来实现这一点。

class Widgets(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        self.createWidgetsFrame()
        self.addWidgets()
        self.layoutWidgets()

我们通过将 GUI 代码拆分成小方法来模块化它,遵循 Python 面向对象编程的最佳实践,这使得我们的代码易于管理和理解。

    #------------------------------------------------------
    def createWidgetsFrame(self):
        self.panel = wx.Panel(self)
        staticBox = wx.StaticBox( self.panel, -1, "Widgets Frame" )    
        self.statBoxSizerV = wx.StaticBoxSizer(staticBox, 
                                               wx.VERTICAL)
    #-----------------------------------------------------
    def layoutWidgets(self):         
        boxSizerV = wx.BoxSizer( wx.VERTICAL )
        boxSizerV.Add( self.statBoxSizerV, 1, wx.ALL )
        self.panel.SetSizer( boxSizerV )
        boxSizerV.SetSizeHints( self.panel )

    #------------------------------------------------------
    def addWidgets(self):
        self.addCheckBoxes()        
        self.addRadioButtons()
        self.addStaticBoxWithLabels()

注意事项

当使用 wxPython StaticBox 小部件时,为了成功布局它们,我们使用 StaticBoxSizer 和常规的 BoxSizer 的组合。wxPython 的 StaticBox 小部件与 tkinter 的 LabelFrame 小部件非常相似。

在 tkinter 中,将一个StaticBox嵌入另一个StaticBox是直接的,但使用 wxPython 则稍微不那么直观。使其工作的一种方法如下所示:

    def addStaticBoxWithLabels(self):
        boxSizerH = wx.BoxSizer(wx.HORIZONTAL)
        staticBox = wx.StaticBox( self.panel, -1, 
"Labels within a Frame" )
        staticBoxSizerV = wx.StaticBoxSizer( staticBox, wx.VERTICAL )
        boxSizerV = wx.BoxSizer( wx.VERTICAL )
        staticText1 = wx.StaticText( self.panel, -1,
"Choose a number:" )
        boxSizerV.Add( staticText1, 0, wx.ALL)
        staticText2 = wx.StaticText( self.panel, -1,"Label 2")
        boxSizerV.Add( staticText2, 0, wx.ALL )
        #------------------------------------------------------
        staticBoxSizerV.Add( boxSizerV, 0, wx.ALL )
        boxSizerH.Add(staticBoxSizerV)
        #------------------------------------------------------
        boxSizerH.Add(wx.TextCtrl(self.panel))
        # Add local boxSizer to main frame
        self.statBoxSizerV.Add( boxSizerH, 1, wx.ALL )

首先,我们创建一个水平 BoxSizer。接下来,我们创建一个垂直 StaticBoxSizer,因为我们想在这个框架中将两个标签以垂直布局排列。

为了将另一个小部件安排在嵌入的StaticBox右侧,我们必须将嵌入的StaticBox及其子控件以及下一个小部件都分配给水平BoxSizer,然后将这个BoxSizer(现在包含我们的嵌入StaticBox和我们的其他小部件)分配给主StaticBox

这听起来是不是很困惑?

您必须亲自尝试这些尺寸调整器,以了解如何使用它们。从本菜谱的代码开始,注释掉一些代码,或者修改一些 x 和 y 坐标,以观察效果。

也可以阅读官方的 wxPython 文档来获取更多信息。

注意事项

重要的是要知道在代码中添加到不同尺寸调整器的位置,以便实现我们想要的布局。

为了在第一个StaticBox下方创建第二个,我们创建单独的StaticBoxSizers并将它们分配到同一个面板。

class Widgets(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        self.panel = wx.Panel(self)
        self.createWidgetsFrame()
        self.createManageFilesFrame()
        self.addWidgets()
        self.addFileWidgets()
        self.layoutWidgets()

    #----------------------------------------------------------
    def createWidgetsFrame(self):
        staticBox = wx.StaticBox( 
self.panel, -1, "Widgets Frame", size=(285, -1) )   
        self.statBoxSizerV = wx.StaticBoxSizer(
staticBox, wx.VERTICAL)   

    #----------------------------------------------------------
    def createManageFilesFrame(self):
        staticBox = wx.StaticBox( 
self.panel, -1, "Manage Files", size=(285, -1) )   
        self.statBoxSizerMgrV = wx.StaticBoxSizer(
staticBox, wx.VERTICAL)

    #----------------------------------------------------------
    def layoutWidgets(self):         
        boxSizerV = wx.BoxSizer( wx.VERTICAL )
        boxSizerV.Add( self.statBoxSizerV, 1, wx.ALL )
        boxSizerV.Add( self.statBoxSizerMgrV, 1, wx.ALL )

        self.panel.SetSizer( boxSizerV )
        boxSizerV.SetSizeHints( self.panel )

    #----------------------------------------------------------
    def addFileWidgets(self):   
        boxSizerH = wx.BoxSizer(wx.HORIZONTAL)
        boxSizerH.Add(wx.Button(
self.panel, label='Browse to File...'))   
        boxSizerH.Add(wx.TextCtrl(
self.panel, size=(174, -1), value= "Z:\\" ))

        boxSizerH1 = wx.BoxSizer(wx.HORIZONTAL)
        boxSizerH1.Add(wx.Button(
self.panel, label='Copy File To:    ')) 
        boxSizerH1.Add(wx.TextCtrl(
self.panel, size=(174, -1), value= "Z:\\Backup" ))    

        boxSizerV = wx.BoxSizer(wx.VERTICAL)
        boxSizerV.Add(boxSizerH)
        boxSizerV.Add(boxSizerH1)        

        self.statBoxSizerMgrV.Add( boxSizerV, 1, wx.ALL )

以下代码实例化了主事件循环,该循环运行我们的 wxPython 图形用户界面程序。

#======================
# Start GUI
#======================
app = wx.App()
MainFrame(None, title="Python GUI using wxPython", size=(350,450))
app.MainLoop()

我们使用 wxPython 构建的 GUI 最终效果如下:

如何做...

它是如何工作的...

我们在几个类中设计和布局我们的 wxPython GUI。

一旦我们在 Python 模块的底部部分完成这个操作,我们就创建一个 wxPython 应用程序的实例。接下来,我们实例化我们的 wxPython GUI 代码。

之后,我们调用主 GUI 事件循环,该循环执行此应用程序进程内运行的所有 Python 代码。这显示了我们的 wxPython GUI。

注意事项

无论我们在创建应用程序和调用其主事件循环之间放置什么代码,它都将成为我们的 wxPython GUI。

真正熟悉 wxPython 库及其 API 可能需要一些时间,但一旦我们了解了如何使用它,这个库就变得非常有趣,并且是构建我们自己的 Python 图形用户界面(GUIs)的强大工具。wxPython 还有一个可以与之配合使用的可视化设计工具:www.cae.tntech.edu/help/programming/wxdesigner-getting-started/view

这个菜谱使用面向对象编程(OOP)来学习如何使用 wxPython 图形用户界面(GUI)工具包。

尝试将主 wxPython 应用嵌入到主 tkinter 应用中

现在我们已经使用 Python 内置的 tkinter 库以及 wxWidgets 库的 wxPython 包装器创建了相同的 GUI,我们确实需要将这些技术创建的 GUI 结合起来。

注意事项

wxPython 和 tkinter 库各自都有其优势。在诸如 stackoverflow.com/ 这样的在线论坛中,我们经常看到类似的问题,比如:哪一个更好?我应该使用哪个 GUI 工具包?这表明我们必须做出一个“非此即彼”的选择。我们不必做出这样的决定。

在这样做的主要挑战之一是,每个 GUI 工具包都必须有自己的事件循环。

在这个菜谱中,我们将尝试通过从我们的 tkinter GUI 中调用它来嵌入一个简单的 wxPython GUI。

准备就绪

我们将重用我们在第一章中构建的 tkinter GUI,即创建 GUI 表单和添加小部件

如何做到这一点...

我们从一个看起来是这样的简单 tkinter GUI 开始:

如何做...

接下来,我们将尝试调用一个简单的 wxPython 图形用户界面(GUI),这是我们在这个章节之前的一个菜谱中创建的。

这是用简单、非面向对象的方式完成此操作的完整代码:

#===========================================================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext

win = tk.Tk()    

win.title("Python GUI")
aLabel = ttk.Label(win, text="A Label")
aLabel.grid(column=0, row=0)    
ttk.Label(win, text="Enter a name:").grid(column=0, row=0)
name = tk.StringVar()
nameEntered = ttk.Entry(win, width=12, textvariable=name)
nameEntered.grid(column=0, row=1)
ttk.Label(win, text="Choose a number:").grid(column=1, row=0)
number = tk.StringVar()
numberChosen = ttk.Combobox(win, width=12, textvariable=number)
numberChosen['values'] = (1, 2, 4, 42, 100)
numberChosen.grid(column=1, row=1)
numberChosen.current(0)
scrolW  = 30
scrolH  =  3
scr = scrolledtext.ScrolledText(win, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=0, sticky='WE', columnspan=3)
nameEntered.focus()  

#===========================================================
def wxPythonApp():
    import wx
    app = wx.App()
    frame = wx.Frame(None, -1, "wxPython GUI", size=(200,150))
    frame.SetBackgroundColour('white')
    frame.CreateStatusBar()
    menu= wx.Menu()
    menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
    menuBar = wx.MenuBar()
    menuBar.Append(menu,"File") 
    frame.SetMenuBar(menuBar)     
    frame.Show()
    app.MainLoop()

action = ttk.Button(win, text="Call wxPython GUI", command= wxPythonApp ) 
action.grid(column=2, row=1)

#======================
# Start GUI
#======================
win.mainloop()

运行前面的代码,在点击 tkinter 的Button控件后,将从一个 tkinter GUI 启动一个 wxPython GUI。

如何做...

它是如何工作的...

重要的是,我们将整个 wxPython 代码放入了一个名为 def wxPythonApp() 的独立函数中。

在按钮点击事件的回调函数中,我们只需调用此代码。

注意事项

需要注意的一点是,在继续使用 tkinter GUI 之前,我们必须关闭 wxPython GUI。

尝试将我们的 tkinter GUI 代码嵌入到 wxPython 中

在这个菜谱中,我们将与之前的菜谱相反的方向,尝试从 wxPython GUI 内部调用我们的 tkinter GUI 代码。

准备就绪

我们将重用本章之前的一个菜谱中创建的一些 wxPython GUI 代码。

如何做到这一点...

我们将从简单的 wxPython 图形用户界面(GUI)开始,它看起来像这样:

如何做...

接下来,我们将尝试调用一个简单的 tkinter GUI。

这是用简单、非面向对象的方式完成此操作的完整代码:

#=============================================================
def tkinterApp():
    import tkinter as tk
    from tkinter import ttk
    win = tk.Tk()    
    win.title("Python GUI")
    aLabel = ttk.Label(win, text="A Label")
    aLabel.grid(column=0, row=0)    
    ttk.Label(win, text="Enter a name:").grid(column=0, row=0)
    name = tk.StringVar()
    nameEntered = ttk.Entry(win, width=12, textvariable=name)
    nameEntered.grid(column=0, row=1)
    nameEntered.focus()  
    def buttonCallback():
        action.configure(text='Hello ' + name.get())
    action = ttk.Button(win, text="Print", command=buttonCallback)
    action.grid(column=2, row=1)
    win.mainloop()

#=============================================================
import wx
app = wx.App()
frame = wx.Frame(None, -1, "wxPython GUI", size=(200,180))
frame.SetBackgroundColour('white')
frame.CreateStatusBar()
menu= wx.Menu()
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menuBar = wx.MenuBar()
menuBar.Append(menu,"File") 
frame.SetMenuBar(menuBar) 
textBox = wx.TextCtrl(frame, size=(180,50), style=wx.TE_MULTILINE)

def tkinterEmbed(event):
    tkinterApp()

button = wx.Button(frame, label="Call tkinter GUI", pos=(0,60)) 
frame.Bind(wx.EVT_BUTTON, tkinterEmbed, button)
frame.Show()

#======================
# Start wxPython GUI
#======================
app.MainLoop()

运行前面的代码,在点击 wxPython 的 Button 控件后,将启动一个 tkinter GUI。然后我们可以将文本输入到 tkinter 的文本框中。通过点击其按钮,按钮文本会更新为相应的名字。

如何做...

在启动 tkinter 事件循环后,wxPython GUI 仍然可以响应,因为我们可以在 tkinter GUI 运行时输入到 TextCtrl 小部件中。

注意事项

在之前的配方中,我们无法使用我们的 tkinter GUI,直到我们关闭了 wxPython GUI。意识到这种差异可以帮助我们做出设计决策,如果我们想要结合这两种 Python GUI 技术的话。

我们也可以通过多次点击 wxPython GUI 按钮来创建几个 tkinter GUI 实例。然而,当任何 tkinter GUI 还在运行时,我们无法关闭 wxPython GUI。我们必须先关闭它们。

如何做...

它是如何工作的...

在这个菜谱中,我们与之前的菜谱相反,首先使用 wxPython 创建了一个 GUI,然后,在它内部,创建了几个使用 tkinter 构建的 GUI 实例。

当一个或多个 tkinter GUI 运行时,wxPython GUI 仍然保持响应。然而,点击 tkinter 按钮只在其第一次点击时更新其按钮文本。

如何使用 Python 控制两个不同的 GUI 框架

在这个菜谱中,我们将探讨从 Python 控制 tkinter 和 wxPython GUI 框架的方法。在前一章中,我们已经使用了 Python 线程模块来保持我们的 GUI 响应,因此在这里我们将尝试使用相同的方法。

我们将看到事情并不总是以直观的方式运作。

然而,当我们从其中调用 wxPython GUI 的实例时,我们将改进我们的 tkinter GUI,使其不再无响应。

准备就绪

这个菜谱将扩展本章中之前的一个菜谱,其中我们尝试将一个主要的 wxPython GUI 嵌入我们的 tkinter GUI。

如何做到这一点...

当我们从 tkinter GUI 创建了一个 wxPython GUI 实例后,我们就不能再使用 tkinter GUI 控件,直到关闭 wxPython GUI 的那个实例。现在让我们来改进这一点。

我们第一次尝试可能是使用来自 tkinter 按钮回调函数的线程。

例如,我们的代码可能看起来像这样:

def wxPythonApp():
    import wx
    app = wx.App()
    frame = wx.Frame(None, -1, "wxPython GUI", size=(200,150))
    frame.SetBackgroundColour('white')
    frame.CreateStatusBar()
    menu= wx.Menu()
    menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
    menuBar = wx.MenuBar()
    menuBar.Append(menu,"File") 
    frame.SetMenuBar(menuBar)     
    frame.Show()
    app.MainLoop()

def tryRunInThread():
    runT = Thread(target=wxPythonApp)
    runT.setDaemon(True)    
    runT.start()
    print(runT)
    print('createThread():', runT.isAlive())    

action = ttk.Button(win, text="Call wxPython GUI", command=tryRunInThread)

首先,这似乎是有效的,这很直观,因为 tkinter 控件不再被禁用,我们可以通过点击按钮创建几个 wxPython GUI 的实例。我们还可以在其他的 tkinter 小部件中输入和选择内容。

如何做...

然而,一旦我们尝试关闭 GUIs,我们就会从 wxWidgets 那里收到一个错误,并且我们的 Python 可执行文件会崩溃。

如何做...

为了避免这种情况,我们不必尝试在单独的线程中运行整个 wxPython 应用程序,而是可以将代码修改为仅让 wxPython 的 app.MainLoop 在线程中运行。

def wxPythonApp():
    import wx
    app = wx.App()
    frame = wx.Frame(None, -1, "wxPython GUI", size=(200,150))
    frame.SetBackgroundColour('white')
    frame.CreateStatusBar()
    menu= wx.Menu()
    menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
    menuBar = wx.MenuBar()
    menuBar.Append(menu,"File") 
    frame.SetMenuBar(menuBar)     
    frame.Show()

    runT = Thread(target=app.MainLoop)
    runT.setDaemon(True)    
    runT.start()
    print(runT)
    print('createThread():', runT.isAlive())

action = ttk.Button(win, text="Call wxPython GUI", command=wxPythonApp) 
action.grid(column=2, row=1)

它是如何工作的...

我们最初尝试在一个线程中运行整个 wxPython GUI 应用程序,但这样做并没有成功,因为 wxPython 的主事件循环期望成为应用程序的主线程。

我们通过仅在一个线程中运行 wxPython 的 app.MainLoop 来解决这个问题,这让它误以为自己是主线程。

这种方法的一个副作用是,我们不能再单独关闭所有的 wxPython GUI 实例。至少有一个实例只有在关闭创建线程作为守护进程的 wxPython GUI 时才会关闭。

我并不完全清楚这是为什么。直观上,人们可能会期望能够关闭所有守护线程,而无需等待创建它们的父线程先关闭。

这可能是因为在我们主线程仍在运行时,引用计数器没有被设置为零。

在实用层面上,这是它目前的工作方式。

如何在两个连接的 GUI 之间进行通信

在之前的菜谱中,我们找到了连接 wxPython GUI 和 tkinter GUI 的方法,可以从一个调用另一个,反之亦然。

虽然这两个图形用户界面(GUIs)同时成功运行,但它们实际上并没有相互通信,因为它们只是互相启动。

在这个菜谱中,我们将探讨如何让这两个图形用户界面相互通信。

准备就绪

阅读之前的某个食谱可能是为这个食谱做准备的一个好方法。

在这个菜谱中,我们将使用与上一个菜谱略有修改的 GUI 代码,但大部分基本的 GUI 构建代码是相同的。

如何做到这一点...

在之前的菜谱中,我们面临的主要挑战之一是如何结合两种旨在成为应用程序唯一 GUI 工具包的 GUI 技术。我们发现了很多简单的方法来实现它们的结合。

我们将再次从 tkinter GUI 主事件循环中启动 wxPython GUI,并在 tkinter 进程内部启动其自己的线程来运行 wxPython GUI。

为了做到这一点,我们将使用一个共享的全局多进程 Python 队列。

注意事项

虽然在这个菜谱中通常最好避免使用全局数据,但它们是一个实用的解决方案,并且 Python 的全局变量实际上仅在它们被声明的模块中是全局的。

这里是使两个 GUI 在某种程度上相互通信的 Python 代码。为了节省空间,这并非纯面向对象编程(OOP)代码。

我们也没有展示所有小部件的创建代码。该代码与之前的食谱相同。

# Ch09_Communicate.py
import tkinter as tk
from tkinter import ttk
from threading import Thread

win = tk.Tk()       
win.title("Python GUI")   

from multiprocessing import Queue
sharedQueue = Queue()
dataInQueue = False

def putDataIntoQueue(data):
    global dataInQueue
    dataInQueue =  True
    sharedQueue.put(data)

def readDataFromQueue():
    global dataInQueue
    dataInQueue = False
    return sharedQueue.get() 
#===========================================================
import wx               
class GUI(wx.Panel):    
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        parent.CreateStatusBar() 
        button = wx.Button(self, label="Print", pos=(0,60))
        self.Bind(wx.EVT_BUTTON, self.writeToSharedQueue, button)

    #--------------------------------------------------------
    def writeToSharedQueue(self, event):
        self.textBox.AppendText(
                        "The Print Button has been clicked!\n") 
        putDataIntoQueue('Hi from wxPython via Shared Queue.\n')
        if dataInQueue: 
            data = readDataFromQueue()
            self.textBox.AppendText(data)

            text.insert('0.0', data) # insert data into GUI

#============================================================
def wxPythonApp():
        app = wx.App()
        frame = wx.Frame(
            None, title="Python GUI using wxPython", size=(300,180))
        GUI(frame)          
        frame.Show()        
        runT = Thread(target=app.MainLoop)
        runT.setDaemon(True)    
        runT.start()
        print(runT)
        print('createThread():', runT.isAlive())
#============================================================
action = ttk.Button(win, text="Call wxPython GUI", command=wxPythonApp) 
action.grid(column=2, row=1)

#======================
# Start GUI
#======================
win.mainloop()

首先运行前面的代码会创建程序中的 tkinter 部分,当我们点击这个 GUI 中的按钮时,它将运行 wxPython GUI。这两个 GUI 仍然像之前一样同时运行,但这次,两个 GUI 之间增加了一个额外的通信层级。

如何做...

前一个截图的左侧显示了 tkinter 图形用户界面,通过点击调用 wxPython GUI按钮,我们可以调用 wxPython GUI 的一个实例。我们可以通过多次点击按钮来创建多个实例。

注意事项

所有创建的 GUI 都保持响应。它们不会崩溃也不会冻结。

点击任何 wxPython GUI 实例上的 打印 按钮,会在其自身的 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 的 打印 按钮的 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 的精神中,我们找到了一个对我们来说简单有效的解决方案。一旦我们的代码变得复杂,我们可能需要重构它,但这是一个良好的开端。

第十章. 使用 PyOpenGL 和 PyGLet 创建惊人的 3D GUIs

在本章中,我们将创建令人惊叹的 Python GUI,它能够显示真正的三维图像,这些图像可以围绕自身旋转,以便我们从各个角度观察它们。

  • PyOpenGL 将我们的 GUI 进行转换

  • 我们的 3D 图形用户界面!

  • 使用位图使我们的 GUI 看起来更美观

  • PyGLet 比 PyOpenGL 更容易转换我们的 GUI

  • 我们的用户界面色彩绚丽

  • 使用 tkinter 创建幻灯片

简介

在本章中,我们将通过赋予它真正的三维能力来转换我们的 GUI。我们将使用两个 Python 第三方库。PyOpenGL 是 OpenGL 标准的 Python 绑定,它是一个所有主要操作系统都内置的图形库。这使得生成的控件具有原生外观和感觉。

Pyglet 是 OpenGL 库的另一个 Python 绑定,但它也可以创建 GUI 应用程序,这使得使用 Pyglet 进行编码比使用 PyOpenGL 更为简便。

PyOpenGL 转换我们的 GUI

在这个菜谱中,我们将成功创建一个 Python GUI,它实际上会导入 PyOpenGL 模块并正常工作!

为了做到这一点,我们需要克服一些初始挑战。

这个菜谱将展示一种经过验证确实有效的方法。如果你在自己的实验中遇到困难,请记住托马斯·爱迪生所说的著名话语。

注意事项

托马斯·爱迪生,白炽灯泡的发明者,回答了一位记者关于爱迪生失败的问题。爱迪生回答说:

"我没有失败,我只是找到了一万种行不通的方法。"

首先,我们必须安装 PyOpenGL 扩展模块。

在成功安装与我们的操作系统架构相匹配的 PyOpenGL 模块后,我们将创建一些示例代码。

准备就绪

我们将安装 PyOpenGL 包。在这本书中,我们使用的是 Windows 7 64 位操作系统和 Python 3.4。下面跟随的下载截图是为了这个配置。

我们还将使用 wxPython。如果您尚未安装 wxPython,您可以阅读前一章中关于如何安装 wxPython 以及如何使用此 GUI 框架的一些食谱。

注意事项

我们正在使用 wxPython Phoenix 版本,这是最新的版本,并计划在未来取代原始的 Classic wxPython 版本。

如何做到这一点...

为了使用 PyOpenGL,我们首先需要安装它。以下网址是官方的 Python 软件包安装网站:

pypi.python.org/pypi/PyOpenGL/3.0.2#downloads

如何做...

这看起来是正确的安装方式,但结果证明,它不适用于 Windows 7 64 位操作系统和 Python 3.4.3 64 位。

在前一章的食谱中提到了一个更好的地方来查找 Python 安装包。你可能已经熟悉它了。我们下载与我们的操作系统和 Python 版本相匹配的包。它带有新的 .whl 格式,因此我们首先需要安装 Python 轮子包。

注意事项

如何安装 Python 轮子包在之前的菜谱中有描述。

使用pip命令通过PyOpenGL-3.1.1a1-cp34-none-win_amd64.whl文件安装 PyOpenGL 既成功又安装了我们所需的全部 64 位模块。

<your full path> 替换为你下载 wheel 安装程序的完整路径。

pip install <your full path> PyOpenGL-3.1.1a1-cp34-none-win_amd64.whl

当我们现在尝试导入一些 PyOpenGL 模块时,它工作正常,如下代码示例所示:

# Ch10_import_OpenGL.py
import wx                  
from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLUT import *

所有这些代码所做的只是导入几个 OpenGL Python 模块。它没有做其他任何事情,但是当我们运行我们的 Python 模块时,我们没有得到任何错误。

这证明了我们已经成功地将 OpenGL 绑定安装到了 Python 中。

现在我们的开发环境已经成功搭建完成,我们可以使用 wxPython 来尝试它。

注意事项

许多在线示例仅限于使用 Python 2.x,以及使用 wxPython 的经典版本。我们正在使用 Python 3 和 Phoenix。

使用基于 wxPython 示例代码创建了一个工作的 3D 立方体。相比之下,运行圆锥示例没有成功,但这个示例让我们走上了正确的道路。

这里是网址:

wiki.wxpython.org/GLCanvas%20update

这里是对代码的一些修改:

import wx
from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLUT import *

class MyCanvasBase(glcanvas.GLCanvas):
    def __init__(self, parent):
        glcanvas.GLCanvas.__init__(self, parent, -1)

# This context was missing from the code
        self.context = glcanvas.GLContext(self)  # <- added

    def OnPaint(self, event):
        dc = wx.PaintDC(self)
# We have to pass in a context ------
#         self.SetCurrent()                      # commented out
        self.SetCurrent(self.context)            # <- changed

我们现在可以创建以下 GUI:

如何做...

在 wxPython 的古典版本中,SetCurrent()函数不需要上下文。以下是在网上搜索时可能会找到的一些代码。

    def OnPaint(self, event):

        dc = wx.PaintDC(self)
        self.SetCurrent()
        if not self.init:
            self.InitGL()
            self.init = True
        self.OnDraw()

在使用 wxPython Phoenix 时,前面的代码无法正常工作。我们可以在网上查找 Phoenix 的正确语法。

如何做...

它是如何工作的...

在这个菜谱中,我们第一次使用 PyOpenGL Python 绑定库体验了 OpenGL。虽然 OpenGL 能够在真正的 3D 中创建真正惊人的图像,但在过程中我们遇到了一些挑战,然后找到了解决这些挑战的方法,使它得以工作。

注意

我们正在用 Python 编写代码,创建 3D 图像!

我们的三维 GUI!

在这个菜谱中,我们将使用 wxPython 创建自己的图形用户界面。我们正在重用一些来自 wxPython 演示示例的代码,我们将这些代码简化到仅显示 3D OpenGL 所需的最小代码量。

注意事项

OpenGL 是一个非常庞大的库。我们不会对这个库进行详细的解释。如果你想要进一步学习 OpenGL,有很多书籍和在线文档可供参考。它有自己的着色语言。

准备就绪

阅读之前的食谱可能是为这个食谱做的良好准备。

如何做到这一点...

由于整个 Python 代码在这里有点长,我们将只展示一小部分代码。

整个代码可在网上找到,这个 Python 模块被称为:

# Ch10_wxPython_OpenGL_GUI
import wx                  
from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLUT import *

#---------------------------------------------------
class CanvasBase(glcanvas.GLCanvas):
    def __init__(self, parent):
        glcanvas.GLCanvas.__init__(self, parent, -1)
        self.context = glcanvas.GLContext(self)
        self.init = False

        # Cube 3D start rotation
        self.last_X = self.x = 30
        self.last_Y = self.y = 30

        self.Bind(wx.EVT_SIZE, self.sizeCallback)
        self.Bind(wx.EVT_PAINT, self.paintCallback)
        self.Bind(wx.EVT_LEFT_DOWN, self.mouseDownCallback)
        self.Bind(wx.EVT_LEFT_UP, self.mouseUpCallback)
        self.Bind(wx.EVT_MOTION, self.mouseMotionCallback)

    def sizeCallback(self, event):
        wx.CallAfter(self.setViewport)
        event.Skip()

    def setViewport(self):
        self.size = self.GetClientSize()
        self.SetCurrent(self.context)
        glViewport(0, 0, self.size.width, self.size.height)

    def paintCallback(self, event):
        wx.PaintDC(self)
        self.SetCurrent(self.context)
        if not self.init:
            self.initGL()
            self.init = True
        self.onDraw()

    def mouseDownCallback(self, event):
        self.CaptureMouse()
        self.x, self.y = self.last_X, self.last_Y = event.GetPosition()

    def mouseUpCallback(self, evt):
        self.ReleaseMouse()

    def mouseMotionCallback(self, evt):
        if evt.Dragging() and evt.LeftIsDown():
            self.last_X, self.last_Y = self.x, self.y
            self.x, self.y = evt.GetPosition()
            self.Refresh(False)

#-----------------------------------------------------
class CubeCanvas(CanvasBase):
    def initGL(self):
        # set viewing projection
        glMatrixMode(GL_PROJECTION)
        glFrustum(-0.5, 0.5, -0.5, 0.5, 1.0, 3.0)

        # position viewer
        glMatrixMode(GL_MODELVIEW)
        glTranslatef(0.0, 0.0, -2.0)

        # position object
        glRotatef(self.y, 1.0, 0.0, 0.0)
        glRotatef(self.x, 0.0, 1.0, 0.0)

        glEnable(GL_DEPTH_TEST)
        glEnable(GL_LIGHTING)
        glEnable(GL_LIGHT0)

    def onDraw(self):
        # clear color and depth buffers
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        # draw six faces of a cube
        glBegin(GL_QUADS)
        glNormal3f( 0.0, 0.0, 1.0)
        glVertex3f( 0.5, 0.5, 0.5)
        glVertex3f(-0.5, 0.5, 0.5)
        glVertex3f(-0.5,-0.5, 0.5)
        glVertex3f( 0.5,-0.5, 0.5)

        glNormal3f( 0.0, 0.0,-1.0)
        glVertex3f(-0.5,-0.5,-0.5)

#===========================================================
app = wx.App()
frame = wx.Frame(None, title="Python GUI using wxPython", size=(300,230))
GUI(frame)
frame.Show()        
app.MainLoop()      

如何做...

前面的截图展示了我们的 wxPython 图形用户界面。当我们点击按钮控件时,以下第二个窗口就会出现。

如何做...

注意

我们现在可以使用鼠标来旋转立方体,以便看到它的六个面。

如何做...

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

如何做...

这个立方体也可以是一艘《星际迷航》太空船!

如果这是我们想要发展的,我们只需成为这个技术的高级程序员即可。

注意事项

许多视频游戏正在使用 OpenGL 进行开发。

它是如何工作的...

我们首先创建了一个常规的 wxPython 图形用户界面,并在其上放置了一个按钮控件。点击此按钮将调用导入的 OpenGL 3D 库。所使用的代码是 wxPython 示例代码的一部分,我们稍作修改以使其与 Phoenix 兼容。

注意

这个配方将我们自己的 GUI 粘合到这个库上。

OpenGL 是一个如此庞大且令人印象深刻的库。这个菜谱展示了如何在 Python 中创建一个工作示例的技巧。

注意事项

通常,一个工作示例就足以让我们开始我们的旅程。

使用位图使我们的 GUI 看起来更美观

这份食谱灵感来源于一个曾经工作过的 wxPython IDE 构建框架。

它不适用于 Python 3 和 wxPython Phoenix,但代码非常酷。

我们将重用该项目提供的代码中的大量位图图像。

在时间耗尽之前,你可以在 GitHub 上 fork 谷歌的代码。

使用位图使我们的 GUI 更美观

准备就绪

我们将继续在本食谱中使用 wxPython,因此阅读前一章的部分内容可能有助于为制作本食谱做准备。

如何做到这一点...

在对 gui2py 代码进行逆向工程并对该代码进行其他修改后,我们可能会得到以下窗口小部件,它显示了一个漂亮的、拼贴式的背景。

如何做...

当然,我们在重构之前提到的网站上的代码时丢失了很多小部件,但这也确实给了我们一个酷炫的背景,点击退出按钮仍然有效。

下一步是弄清楚如何将代码中的有趣部分整合到我们自己的图形用户界面中。

我们通过在之前菜谱的 GUI 中添加以下代码来实现这一点。

#----------------------------------------------------------
class GUI(wx.Panel):              # Subclass wxPython Panel
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)

        imageFile = 'Tile.bmp'
        self.bmp = wx.Bitmap(imageFile)
        # react to a resize event and redraw image
        parent.Bind(wx.EVT_SIZE, self.canvasCallback)

    def canvasCallback(self, event=None):
        # create the device context
        dc = wx.ClientDC(self)
        brushBMP = wx.Brush(self.bmp)
        dc.SetBrush(brushBMP)
        width, height = self.GetClientSize()
        dc.DrawRectangle(0, 0, width, height)

注意事项

我们必须绑定到父级,而不是自身,否则我们的位图将不会显示。

现在运行我们的改进代码,将位图作为我们 GUI 的背景。

如何做...

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

如何做...

它是如何工作的...

在这个菜谱中,我们通过使用位图作为背景来增强了我们的图形用户界面。我们将位图图像平铺,当我们调整 GUI 窗口大小时,位图会自动调整自身以填充我们在设备上下文中绘制的 Canvas 整个区域。

注意事项

前面的 wxPython 代码可以加载不同的图像文件格式。

PyGLet 比 PyOpenGL 更容易转换我们的 GUI

在这个菜谱中,我们将使用 PyGLet 图形用户界面开发框架来创建我们的 GUI。

PyGLet 比 PyOpenGL 更易于使用,因为它自带了 GUI 事件循环,所以我们不需要使用 tkinter 或 wxPython 来创建我们的 GUI。

如何做到这一点...

为了使用 Pyglet,我们首先必须安装这个第三方 Python 插件。

使用pip命令,我们可以轻松地安装库,一个成功的安装在我们 Python 的site-packages文件夹中看起来是这样的:

如何做...

在线文档位于当前版本的此网站:

pyglet.readthedocs.org/en/pyglet-1.2-maintenance/

如何做...

首次使用 Pyglet 库的经验可能看起来是这样的:

import pyglet

window = pyglet.window.Window()
label = pyglet.text.Label('PyGLet GUI', 
                          font_size=42,
                          x=window.width//2, y=window.height//2,
                          anchor_x='center', anchor_y='center')

@window.event
def on_draw():
    window.clear()
    label.draw()

pyglet.app.run()

上述代码来自官方 pyglet.org 网站,并生成以下功能齐全的 GUI:

如何做...

它是如何工作的...

在这个菜谱中,我们使用了另一个封装了 OpenGL 库的第三方 Python 模块。

这个库自带事件循环处理能力,这使得我们无需依赖另一个库来创建一个运行的 Python GUI。

我们已经探索了展示我们如何安装和使用这个出色的 GUI 库的官方网站。

我们的用户界面色彩绚丽

在这个菜谱中,我们将扩展之前菜谱中使用的 Pyglet 编写的 GUI,将其转变为真正的 3D。

我们还将为它添加一些炫酷的颜色。这个食谱灵感来源于《OpenGL SuperBible》系列书籍中的某些示例代码。它创建了一个非常多彩的立方体,我们可以使用键盘的上、下、左、右按钮在三维空间中旋转它。

我们对示例代码进行了轻微改进,使得在按下任意一个键时图片会转动,而不是需要按下并释放键。

准备就绪

之前的配方解释了如何安装 PyGLet,并为您介绍了这个库。如果您还没有这样做,浏览那一章可能是个不错的主意。

注意事项

在在线文档中,PyGLet 通常全部使用小写字母拼写。虽然这可能是一种 Python 风格的做法,但我们将类名首字母大写,并且对于变量、方法和函数名,我们使用小写字母来开始每个名称。

在这本书中,除非为了澄清代码,我们不会使用下划线。

如何做到这一点...

以下代码创建了下方显示的 3 维彩色立方体。这次,我们将使用键盘方向键来旋转图像,而不是鼠标。

import pyglet
from pyglet.gl import *
from pyglet.window import key
from OpenGL.GLUT import *

WINDOW    = 400
INCREMENT = 5

class Window(pyglet.window.Window):

    # Cube 3D start rotation
    xRotation = yRotation = 30    

    def __init__(self, width, height, title=''):
        super(Window, self).__init__(width, height, title)
        glClearColor(0, 0, 0, 1)
        glEnable(GL_DEPTH_TEST)    

    def on_draw(self):
        # Clear the current GL Window
        self.clear()

        # Push Matrix onto stack
        glPushMatrix()

        glRotatef(self.xRotation, 1, 0, 0)
        glRotatef(self.yRotation, 0, 1, 0)

        # Draw the six sides of the cube
        glBegin(GL_QUADS)

        # White
        glColor3ub(255, 255, 255)
        glVertex3f(50,50,50)

        # Yellow
        glColor3ub(255, 255, 0)
        glVertex3f(50,-50,50)

        # Red
        glColor3ub(255, 0, 0)
        glVertex3f(-50,-50,50)
        glVertex3f(-50,50,50)

        # Blue
        glColor3f(0, 0, 1)
        glVertex3f(-50,50,-50)

        # <… more color defines for cube faces>

        glEnd()

        # Pop Matrix off stack
        glPopMatrix()

    def on_resize(self, width, height):
        # set the Viewport
        glViewport(0, 0, width, height)

        # using Projection mode
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()

        aspectRatio = width / height
        gluPerspective(35, aspectRatio, 1, 1000)

        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        glTranslatef(0, 0, -400)

    def on_text_motion(self, motion): 
        if motion == key.UP:
            self.xRotation -= INCREMENT
        elif motion == key.DOWN:
            self.xRotation += INCREMENT
        elif motion == key.LEFT:
            self.yRotation -= INCREMENT
        elif motion == key.RIGHT:
            self.yRotation += INCREMENT

if __name__ == '__main__':
    Window(WINDOW, WINDOW, 'Pyglet Colored Cube')
    pyglet.app.run()

如何做...

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

如何做...

它是如何工作的...

在这个菜谱中,我们使用了 pyglet 来创建一个彩色的立方体,我们可以通过键盘的方向键在三维空间中旋转它。

我们为立方体的六个面定义了多种颜色,并使用 pyglet 创建了主窗口框架。

代码与本章中之前的配方类似,其中我们使用了 wxPython 库来创建一个立方体。这样做的原因是,在底层,wxPython 和 pyglet 都使用了 OpenGL 库。

使用 tkinter 创建幻灯片

在这个菜谱中,我们将使用纯 Python 创建一个不错的可工作幻灯片 GUI。

我们将看到 Python 核心内置函数的限制,然后我们将探索另一个名为 Pillow 的第三方模块,该模块扩展了 tkinter 内置的功能,特别是在图像处理方面。

虽然 Pillow 这个名字一开始听起来可能有点奇怪,但实际上它背后有着丰富的历史。

注意事项

本书仅使用 Python 3.4 及以上版本。

我们不会回到 Python 2。

吉多已经表达了他有意打破向后兼容性的决定,并决定 Python 3 是 Python 编程的未来。

对于 GUI 和图像,Python 2 的老版本有一个名为 PIL 的非常强大的模块,代表 Python Image Library。这个库包含大量功能,在 Python 3 非常成功创建后的几年里,这些功能尚未被翻译成 Python 3。

许多开发者仍然选择使用 Python 2 而不是按照 Python 的仁慈独裁者所设计的未来版本,因为 Python 2 仍然拥有更多的库可用。

这有点令人难过。

幸运的是,已经创建了一个新的图像库,它可以与 Python 3 一起使用,并且它被命名为 PIL 加上一些东西。

注意事项

Pillow 与 Python 2 PIL 库不兼容。

准备就绪

在本食谱的第一部分,我们将使用纯 Python。为了提高代码质量,我们将使用 pip 功能安装另一个 Python 模块。因此,尽管你很可能熟悉 pip,但了解如何使用它可能是有用的。

如何做到这一点...

首先,我们将使用纯 Python 创建一个工作界面,该界面可以在窗口框架内随机排列幻灯片。

这里是工作代码,以下是运行此代码的结果截图:

from tkinter import Tk, PhotoImage, Label
from itertools import cycle
from os import listdir

class SlideShow(Tk):
    # inherit GUI framework extending tkinter
    def __init__(self, msShowTimeBetweenSlides=1500):
        # initialize tkinter super class
        Tk.__init__(self)

        # time each slide will be shown
        self.showTime = msShowTimeBetweenSlides

        # look for images in current working directory 
        listOfSlides = [slide for slide in listdir() if slide.endswith('gif')]

        # cycle slides to show on the tkinter Label 
        self.iterableCycle = cycle((PhotoImage(file=slide), slide) for slide in listOfSlides)

        # create tkinter Label widget which can display images
        self.slidesLabel = Label(self)

        # create the Frame widget
        self.slidesLabel.pack()

    def slidesCallback(self):
        # get next slide from iterable cycle
        currentInstance, nameOfSlide = next(self.iterableCycle)

        # assign next slide to Label widget
        self.slidesLabel.config(image=currentInstance)

        # update Window title with current slide
        self.title(nameOfSlide)

        # recursively repeat the Show
        self.after(self.showTime, self.slidesCallback)

#=================================
# Start GUI
#=================================
win = SlideShow()
win.after(0, win.slidesCallback())
win.mainloop()

如何做...

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

如何做...

虽然幻灯片滑动确实令人印象深刻,但纯 Python tkinter GUIs 的内置功能不支持非常流行的.jpg格式,因此我们不得不求助于另一个 Python 库。

为了使用 Pillow,我们首先必须使用pip命令进行安装。

成功安装后的样子如下:

如何做...

Pillow 支持.jpg格式,为了使用它,我们不得不稍微改变我们的语法。

使用 Pillow 是一个高级主题,本书的这一版不会涉及。

它是如何工作的...

Python 是一个非常出色的工具,在这份食谱中,我们探讨了多种使用和扩展它的方法。

注意事项

当手指指向月亮时,那不是月亮本身,而只是一个指示者。

第十一章:最佳实践

在本章中,我们将探讨与我们的 Python 图形用户界面相关的最佳实践。

  • 避免意大利面式代码

  • 使用 __init__ 连接模块

  • 混合面向对象编程(OOP)和瀑布式开发

  • 使用代码命名约定

  • 何时不使用面向对象编程

  • 如何成功使用设计模式

  • 避免复杂性

简介

在本章中,我们将探讨不同的最佳实践,这些实践可以帮助我们以高效的方式构建我们的图形用户界面(GUI),并保持其可维护性和可扩展性。

这些最佳实践也将帮助我们调试我们的 GUI,使其达到我们想要的样子。

避免意大利面式代码

在这个菜谱中,我们将探讨创建 spaghetti 代码的典型方法,然后我们将看到一种避免此类代码的更好方法。

注意事项

意大利面代码是指功能相互交织在一起的代码。

准备就绪

我们将使用内置的 Python 库 tkinter 创建一个新且简单的 GUI。

如何做到这一点...

在网上搜索并阅读了文档之后,我们可能首先会编写以下代码来创建我们的图形用户界面:

# Spaghetti Code #############################
def PRINTME(me):print(me)
import tkinter 
x=y=z=1
PRINTME(z) 
from tkinter import *
scrolW=30;scrolH=6
win=tkinter.Tk()
if x:chVarUn=tkinter.IntVar()
from tkinter import ttk
WE='WE'
import tkinter.scrolledtext
outputFrame=tkinter.ttk.LabelFrame(win,text=' Type into the scrolled text control: ')
scr=tkinter.scrolledtext.ScrolledText(outputFrame,width=scrolW,height=scrolH,wrap=tkinter.WORD)
e='E'
scr.grid(column=1,row=1,sticky=WE)
outputFrame.grid(column=0,row=2,sticky=e,padx=8)
lFrame=None
if y:chck2=tkinter.Checkbutton(lFrame,text="Enabled",variable=chVarUn)
wE='WE'
if y==x:PRINTME(x) 
lFrame=tkinter.ttk.LabelFrame(win,text="Spaghetti")
chck2.grid(column=1,row=4,sticky=tkinter.W,columnspan=3)  
PRINTME(z)
lFrame.grid(column=0,row=0,sticky=wE,padx=10,pady=10) 
chck2.select()
try: win.mainloop()
except:PRINTME(x)
chck2.deselect()
if y==x:PRINTME(x) 
# End Pasta #############################

运行前面的代码将产生以下图形用户界面:

如何做...

这并不是我们想要的图形用户界面。我们希望它看起来更像是这样:

如何做...

虽然这段意大利面代码创建了一个图形用户界面,但由于代码中存在很多混乱,这段代码非常难以调试。

以下为生成所需图形用户界面的代码:

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext

#======================
# Create instance
#======================
win = tk.Tk()   

#======================
# Add a title       
#====================== 
win.title("Python GUI")

#=========================
# Disable resizing the GUI
#=========================
win.resizable(0,0)

#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox  
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)

#=============================================================
# Using a scrolled Text control    
#=============================================================
outputFrame = ttk.LabelFrame(win, text=' Type into the scrolled text control: ')
outputFrame.grid(column=0, row=2, sticky='E', padx=8)
scrolW  = 30
scrolH  =  6
scr = scrolledtext.ScrolledText(outputFrame, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=1, row=0, sticky='WE')

#=============================================================
# Creating a checkbutton
#=============================================================
chVarUn = tk.IntVar()
check2 = tk.Checkbutton(lFrame, text="Enabled", variable=chVarUn)
check2.deselect()
check2.grid(column=1, row=4, sticky=tk.W, columnspan=3) 

#======================
# Start GUI
#======================
win.mainloop()

它是如何工作的...

在这个菜谱中,我们比较了面条代码和良好代码。良好代码相对于面条代码具有许多优势。

它有清晰的注释部分。

意大利面代码:

def PRINTME(me):print(me)
import tkinter 
x=y=z=1
PRINTME(z) 
from tkinter import *

好的代码:

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk

它具有一种自然流动,遵循小部件在 GUI 主表单中的布局方式。

在面条代码中,底部的 LabelFrame 在顶部 LabelFrame 之前被创建,并且它与一个导入语句和一些小部件创建混合在一起。

意大利面代码:

import tkinter.scrolledtext
outputFrame=tkinter.ttk.LabelFrame(win,text=' Type into the scrolled text control: ')
scr=tkinter.scrolledtext.ScrolledText(outputFrame,width=scrolW,height=scrolH,wrap=tkinter.WORD)
e='E'
scr.grid(column=1,row=1,sticky=WE)
outputFrame.grid(column=0,row=2,sticky=e,padx=8)
lFrame=None
if y:chck2=tkinter.Checkbutton(lFrame,text="Enabled",variable=chVarUn)
wE='WE'
if y==x:PRINTME(x) 
lFrame=tkinter.ttk.LabelFrame(win,text="Spaghetti")

好的代码:

#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox  
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)

#=============================================================
# Using a scrolled Text control    
#=============================================================
outputFrame = ttk.LabelFrame(win, text=' Type into the scrolled text control: ')
outputFrame.grid(column=0, row=2, sticky='E', padx=8)

它不包含不必要的变量赋值,也没有一个print函数,这个函数并不能完成阅读代码时人们期望它所做的调试工作。

意大利面代码:

def PRINTME(me):print(me)
x=y=z=1
e='E'
WE='WE'
scr.grid(column=1,row=1,sticky=WE)
wE='WE'
if y==x:PRINTME(x) 
lFrame.grid(column=0,row=0,sticky=wE,padx=10,pady=10) 
PRINTME(z)
try: win.mainloop()
except:PRINTME(x)
chck2.deselect()
if y==x:PRINTME(x) 

好的代码:

没有以上任何一种。

import 语句仅导入所需的模块。它们不会在代码中造成混乱。没有重复的 import 语句。没有 import * 语句。

意大利面代码:

import tkinter 1
x=y=z=1
PRINTME(z) 
from tkinter import *
scrolW=30;scrolH=6
win=tkinter.Tk()
if x:chVarUn=tkinter.IntVar()
from tkinter import ttk
WE='WE'
import tkinter.scrolledtext

好的代码:

import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext

选择的变量名非常有意义。没有不必要的if语句使用数字1代替True

意大利面代码:

x=y=z=1
if x:chVarUn=tkinter.IntVar()
wE='WE'

好的代码:

#=============================================================
# Using a scrolled Text control    
#=============================================================
outputFrame = ttk.LabelFrame(win, text=' Type into the scrolled text control: ')
outputFrame.grid(column=0, row=2, sticky='E', padx=8)
scrolW  = 30
scrolH  =  6
scr = scrolledtext.ScrolledText(outputFrame, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=1, row=0, sticky='WE')

我们没有丢失预期的窗口标题,并且我们的复选按钮最终位于正确的位置。我们还使围绕复选按钮的LabelFrame可见。

意大利面代码:

我们丢失了窗口标题,并且没有显示顶部的LabelFrame。检查按钮最终出现在了错误的位置。

优秀的代码:

#======================
# Create instance
#======================
win = tk.Tk()   

#======================
# Add a title       
#====================== 
win.title("Python GUI")

#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox  
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)

#=============================================================
# Creating a checkbutton
#=============================================================
chVarUn = tk.IntVar()
check2 = tk.Checkbutton(lFrame, text="Enabled", variable=chVarUn)
check2.deselect()
check2.grid(column=1, row=4, sticky=tk.W, columnspan=3) 

#======================
# Start GUI
#======================
win.mainloop()

使用 __init__ 连接模块

当我们使用 Eclipse IDE 的 PyDev 插件创建一个新的 Python 项目时,它会自动创建一个 __init__.py 模块。如果我们不使用 Eclipse,我们也可以手动创建它。

注意事项

__init__.py 模块通常为空,其大小为 0 千字节。

我们可以使用这个通常为空的模块,通过向其中输入代码来连接不同的 Python 模块。本食谱将展示如何进行这一操作。

准备就绪

我们将创建一个与之前菜谱中创建的类似的新的图形用户界面。

如何做到这一点...

随着我们的项目规模越来越大,我们自然会将其拆分成几个 Python 模块。使用像 Eclipse 这样的现代 IDE,查找位于不同子文件夹中、要么在需要导入它的代码之上要么之下的模块,竟然出奇地复杂。

克服这一限制的一个实用方法是使用__init__.py模块。

注意事项

在 Eclipse 中,我们可以将 Eclipse 内部项目环境设置为特定的文件夹,我们的 Python 代码将会找到它。但 outside of Eclipse,例如从命令窗口运行时,Python 模块导入机制有时会出现不匹配,导致代码无法运行。

这里是空的 __init__.py 模块的截图,当在 Eclipse 代码编辑器中打开时,它显示的不是 __init__ 这个名字,而是它所属的 PyDev 包的名字。"1" 位于代码编辑器的左侧是行号,而不是在这个模块中编写的任何代码。这个空的 __init__.py 模块中绝对没有任何代码。

如何做...

此文件为空,但它确实存在。

如何做...

当我们运行以下代码并点击clickMe 按钮,我们会得到代码下方显示的结果。这是一个尚未使用__init__.py模块的常规 Python 模块。

注意事项

__init__.py 模块与 Python 类的 __init__(self) 方法并不相同。

#  Ch11_GUI__init.py
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk

#======================
# Create instance
#======================
win = tk.Tk()   

#======================
# Add a title       
#====================== 
win.title("Python GUI")

#=============================================================
# Adding a LabelFrame and a Button
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)

def clickMe():
    from tkinter import messagebox
    messagebox.showinfo('Message Box', 'Hi from same Level.')

button = ttk.Button(lFrame, text="Click Me ", command=clickMe)
button.grid(column=1, row=0, sticky=tk.S)  

#======================
# Start GUI
#======================
win.mainloop()

如何做...

在前面的代码中,我们创建了以下函数,该函数导入 Python 的消息框,然后使用它来显示消息框窗口:

def clickMe():
    from tkinter import messagebox
    messagebox.showinfo('Message Box', 'Hi from same Level.')

当我们将clickMe()消息框代码移动到嵌套的目录文件夹中,并尝试将其导入到我们的 GUI 模块时,我们遇到了一些问题。

我们在 Python 模块所在的下方创建了三个子文件夹。然后我们将clickMe()消息框代码放入了一个新的 Python 模块中,我们将其命名为MessageBox.py。这个模块位于Folder3中,距离我们的 Python 模块有三层。

我们需要导入这个MessageBox.py模块,以便使用该模块包含的clickMe()函数。

首先,它似乎可以正常工作,因为我们从 Eclipse IDE 没有收到任何错误或警告,看起来我们可以像导入其他模块一样导入这个新的嵌套模块。

我们正在使用 Python 的相对导入语法:

from .Folder1.Folder2.Folder3.MessageBox import clickme

这可以在以下屏幕截图中看到:

如何做...

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

如何做...

我们可以通过访问 PyDev 项目的属性,并将我们自己的新函数所在的子文件夹添加为外部库,在 Eclipse 中将其添加为外部库。这看起来可能不太直观,但它确实有效。

如何做...

当我们现在注释掉文件夹结构,而是直接从嵌套了三层的模块中导入函数时,代码按预期工作。

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
# from .Folder1.Folder2.Folder3.MessageBox import clickMe
from MessageBox import clickMe

此函数在消息框中显示不同的文本:

如何做...

实现相同结果的一个更好的方法是使用 Python 内置的 __init__.py 模块。

删除了之前特定的 Eclipse 外部库依赖后,我们现在可以直接使用此模块。

注意事项

我们将代码放入此模块,如果我们在程序中导入__init__.py模块,那么在 Python 3.4.3 版本中,这段代码将先于我们其他所有代码执行。

忽略 PyDev 未解析导入(带叉号的红色圆圈)错误。这个 import 是必要的;它使我们的代码运行,并使整个 Python 导入机制正常工作。

如何做...

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

如何做...

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

如何做...

现在,我们可以在本地的__init__.py模块中初始化 Python 搜索路径,只需将以下代码添加到相同的__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
for _ in range(10):
    curFull = getcwd()
    curDir = curFull.split('\\')[-1] 
    if 'B04829_Ch11_Code' == curDir:
        addsitedir(curFull)
        addsitedir(curFull + '\\Folder1\\Folder2\\Folder3\\')
        break
    chdir(pardir)
pprint(path)

当我们现在运行我们的 GUI 代码时,我们得到相同的预期窗口,但我们已经移除了对 Eclipse PYTHONPATH变量的依赖。

现在我们可以在 Eclipse PyDev 插件之外成功运行相同的代码。

注意

我们的代码变得更加 Pythonic。

它是如何工作的...

在这个菜谱中,我们发现了一个使用免费随 Eclipse IDE 提供的出色且免费的 PyDev 插件的局限性。

我们首先在 Eclipse IDE 中找到了一种解决方案,然后通过变得 Pythonic,我们摆脱了这一 IDE 的依赖。

注意事项

使用纯 Python 通常是最佳选择。

混合面向对象编程(OOP)和瀑布式开发

Python 是一种面向对象的编程语言,但并不总是使用面向对象编程(OOP)有意义。对于简单的脚本任务,传统的瀑布式编码风格仍然适用。

在这个菜谱中,我们将创建一个新的图形用户界面(GUI),它结合了传统的逐行编码风格和更现代的面向对象编程(OOP)风格。

我们将创建一个面向对象的类,当我们将鼠标悬停在用瀑布式创建的 Python GUI 中的小部件上时,它将显示一个工具提示。

注意事项

下降式和瀑布式编码风格是相同的。这意味着我们必须在物理上将代码放置在代码之上,然后才能从下面的代码中调用它。在这个范式下,当我们执行代码时,代码实际上是从程序顶部逐行下降到程序底部的。

准备就绪

在这个菜谱中,我们将使用 tkinter 创建一个 GUI,这与本书第一章中创建的 GUI 类似。

如何做到这一点...

在 Python 中,我们可以通过使用self关键字将函数绑定到类中,将其转换为方法。这是 Python 真正美妙的功能之一,它使我们能够创建可理解和可维护的大型系统。

有时候,当我们只编写简短的脚本时,面向对象编程(OOP)并不适用,因为我们发现自己需要在很多变量前加上self关键字,而当代码不需要这样大时,它就会变得不必要地庞大。

让我们先使用 tkinter 创建一个 Python GUI,并以瀑布式进行编码。

以下代码创建 GUI:

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox

#======================
# Create instance
#======================
win = tk.Tk()   

#======================
# Add a title       
#====================== 
win.title("Python GUI")

#=========================
# Disable resizing the GUI
#=========================
win.resizable(0,0)

#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox  
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)

#=============================================================
# Labels
#=============================================================
ttk.Label(lFrame, text="Enter a name:").grid(column=0, row=0)
ttk.Label(lFrame, text="Choose a number:").grid(column=1, row=0, sticky=tk.W)

#=============================================================
# Buttons click command
#=============================================================
def clickMe(name, number):
    messagebox.showinfo('Information Message Box', 'Hello '+name+
                        ', your number is: ' + number)

#=============================================================
# Creating several controls in a loop
#=============================================================
names         = ['name0', 'name1', 'name2']
nameEntries   = ['nameEntry0', 'nameEntry1', 'nameEntry2']

numbers       = ['number0', 'number1', 'number2']
numberEntries = ['numberEntry0', 'numberEntry1', 'numberEntry2']

buttons = []

for idx in range(3):
    names[idx] = tk.StringVar()
    nameEntries[idx] = ttk.Entry(lFrame, width=12, textvariable=names[idx])
    nameEntries[idx].grid(column=0, row=idx+1)
    nameEntries[idx].delete(0, tk.END)
    nameEntries[idx].insert(0, '<name>')

    numbers[idx] = tk.StringVar()
    numberEntries[idx] = ttk.Combobox(lFrame, width=14, textvariable=numbers[idx])
    numberEntries[idx]['values'] = (1+idx, 2+idx, 4+idx, 42+idx, 100+idx)
    numberEntries[idx].grid(column=1, row=idx+1)
    numberEntries[idx].current(0)

    button = ttk.Button(lFrame, text="Click Me "+str(idx+1), command=lambda idx=idx: clickMe(names[idx].get(), numbers[idx].get()))
    button.grid(column=2, row=idx+1, sticky=tk.W)  
    buttons.append(button)
#======================
# Start GUI
#======================
win.mainloop()

当我们运行代码时,我们会得到图形用户界面,它看起来是这样的:

如何做...

我们可以通过添加工具提示来改进我们的 Python 图形用户界面。实现这一点的最佳方式是将创建工具提示功能的代码从我们的 GUI 中分离出来。

我们通过创建一个具有提示功能的不同类来实现这一点,然后我们在创建我们 GUI 的同一 Python 模块中创建这个类的实例。

使用 Python,我们无需将我们的ToolTip类放入一个单独的模块中。我们只需将它放置在过程代码之上,然后从该代码下方调用它。

代码现在看起来是这样的:

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox

#-----------------------------------------------
class ToolTip(object):
    def __init__(self, widget):
        self.widget = widget
        self.tipwindow = None
        self.id = None
        self.x = self.y = 0

#-----------------------------------------------
def createToolTip(widget, text):
    toolTip = ToolTip(widget)
    def enter(event): toolTip.showtip(text)
    def leave(event): toolTip.hidetip()
    widget.bind('<Enter>', enter)
    widget.bind('<Leave>', leave)

#-----------------------------------------------
# further down the module we call the createToolTip function
#-----------------------------------------------

for idx in range(3):
    names[idx] = tk.StringVar()
    nameEntries[idx] = ttk.Entry(
lFrame, width=12, textvariable=names[idx])
    nameEntries[idx].grid(column=0, row=idx+1)
    nameEntries[idx].delete(0, tk.END)
    nameEntries[idx].insert(0, '<name>')

    numbers[idx] = tk.StringVar()
    numberEntries[idx] = ttk.Combobox(
lFrame, width=14, textvariable=numbers[idx])
    numberEntries[idx]['values'] = (
1+idx, 2+idx, 4+idx, 42+idx, 100+idx)
    numberEntries[idx].grid(column=1, row=idx+1)
    numberEntries[idx].current(0)

    button = ttk.Button(
lFrame, text="Click Me "+str(idx+1), command=lambda idx=idx: clickMe(names[idx].get(), numbers[idx].get()))
    button.grid(column=2, row=idx+1, sticky=tk.W)  
    buttons.append(button)

#-----------------------------------------------
    # Add Tooltips to more widgets
    createToolTip(nameEntries[idx], 'This is an Entry widget.') 
    createToolTip(
numberEntries[idx], 'This is a DropDown widget.') 
    createToolTip(buttons[idx], 'This is a Button widget.')
#-----------------------------------------------

运行代码会在我们鼠标悬停在组件上时为我们的组件创建工具提示。

如何做...

它是如何工作的...

在这个菜谱中,我们以过程式的方式创建了一个 Python GUI,后来,我们在模块顶部添加了一个类。

我们可以非常容易地在同一个 Python 模块中混合使用过程式和面向对象编程。

使用代码命名约定

本书之前提供的食谱尚未使用结构化的代码命名约定。本食谱将向您展示遵循代码命名方案的价值,因为它有助于我们找到想要扩展的代码,同时也提醒我们关于程序设计的考虑。

准备就绪

在这个菜谱中,我们将查看本书第一章中的 Python 模块名称,并将它们与更好的命名约定进行比较。

如何做到这一点...

在本书的第一章中,我们创建了我们的第一个 Python 图形用户界面。我们通过递增不同的代码模块名称的序号来改进我们的 GUI。

它看起来是这样的:

如何做...

虽然这是一种典型的编码方式,但它并没有提供太多意义。当我们开发时编写 Python 代码,很容易增加数字。

后来,当我们回到这段代码时,我们并不清楚哪个 Python 模块提供了哪些功能,有时,我们最近更新的模块并不如早期版本好。

注意

明确的命名规范确实有帮助。

我们可以将第一章中的模块名称创建 GUI 表单和添加小部件与第八章中的名称国际化与测试进行比较,后者要更有意义。

如何做...

虽然并不完美,但为不同的 Python 模块选定的名称表明了每个模块的责任所在。当我们想要添加更多单元测试时,可以清楚地知道它们位于哪个模块中。

以下是如何使用代码命名规范在 Python 中创建 GUI 的另一个示例:

如何做...

注意

将单词 PRODUCT 替换为你目前正在工作的产品名称。

整个应用程序是一个图形用户界面。所有部分都是相互连接的。DEBUG.py模块仅用于调试我们的代码。与所有其他模块相比,调用 GUI 的主要函数名称是反向的。它以Gui开头,以.pyw扩展名结尾。

这是唯一具有此扩展名的 Python 模块。

从这个命名规范来看,如果你对 Python 足够熟悉,那么很明显,为了运行这个图形用户界面,你需要双击 Gui_PRODUCT.pyw 模块。

所有其他 Python 模块都包含向 GUI 提供功能以及执行底层业务逻辑以实现 GUI 所针对目的的功能。

它是如何工作的...

Python 代码模块的命名规范对我们保持高效和记住原始设计非常有帮助。当我们需要调试和修复缺陷或添加新功能时,它们是我们首先查阅的资源。

注意事项

通过数字递增模块名称并不十分有意义,最终还会浪费开发时间。

另一方面,Python 变量的命名更偏向自由形式。Python 会推断类型,因此我们不必指定一个变量将是 <list> 类型(它可能不是,或者实际上,在代码的后续部分,它可能变成另一种类型)。

变量命名的良好建议是使它们具有描述性,同时也不宜过度缩写。

如果我们想要指出某个变量被设计为类型<list>,那么使用完整的单词list而不是lst会更加直观。

对于numbernum来说,情况类似。

虽然给变量起非常描述性的名字是个好主意,但有时名字可能会变得过长。在苹果的 Objective-C 语言中,一些变量和函数的名字非常极端:thisIsAMethodThatDoesThisAndThatAndAlsoThatIfYouPassInNIntegers:1:2:3

注意

在命名变量、方法和函数时,请使用常识。

何时不使用面向对象编程(OOP)

Python 自带面向对象编程的能力,但与此同时,我们也可以编写不需要使用 OOP 的脚本。

对于某些任务,面向对象编程(OOP)并不适用。

这个菜谱将展示何时不使用面向对象编程(OOP)。

准备就绪

在这个菜谱中,我们将创建一个类似于之前菜谱的 Python GUI。我们将比较面向对象(OOP)代码与非面向对象编程的替代方式。

如何做到这一点...

让我们先使用面向对象的方法创建一个新的 GUI。下面的代码将创建显示在代码下面的 GUI:

import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu

class OOP():
    def __init__(self): 
        self.win = tk.Tk()         
        self.win.title("Python GUI")      
        self.createWidgets()

    def createWidgets(self):    
        tabControl = ttk.Notebook(self.win)     
        tab1 = ttk.Frame(tabControl)            
        tabControl.add(tab1, text='Tab 1')    
        tabControl.pack(expand=1, fill="both")  
        self.monty = ttk.LabelFrame(tab1, text=' Monty Python ')
        self.monty.grid(column=0, row=0, padx=8, pady=4)        

        ttk.Label(self.monty, text="Enter a name:").grid(
column=0, row=0, sticky='W')
        self.name = tk.StringVar()
        nameEntered = ttk.Entry(
self.monty, width=12, textvariable=self.name)
        nameEntered.grid(column=0, row=1, sticky='W')

        self.action = ttk.Button(self.monty, text="Click Me!")   
        self.action.grid(column=2, row=1)

        ttk.Label(self.monty, 
text="Choose a number:").grid(column=1, row=0)
        number = tk.StringVar()
        numberChosen = ttk.Combobox(self.monty, 
width=12, textvariable=number)
        numberChosen['values'] = (42)
        numberChosen.grid(column=1, row=1)
        numberChosen.current(0)

        scrolW = 30; scrolH = 3
        self.scr = scrolledtext.ScrolledText(
self.monty, width=scrolW, height=scrolH, wrap=tk.WORD)
        self.scr.grid(column=0, row=3, sticky='WE', columnspan=3)

        menuBar = Menu(tab1)
        self.win.config(menu=menuBar)
        fileMenu = Menu(menuBar, tearoff=0)
        menuBar.add_cascade(label="File", menu=fileMenu)
        helpMenu = Menu(menuBar, tearoff=0)
        menuBar.add_cascade(label="Help", menu=helpMenu)

        nameEntered.focus()     
#==========================
oop = OOP()
oop.win.mainloop()

如何做...

我们可以通过稍微重构我们的代码,在不使用面向对象编程(OOP)方法的情况下实现相同的图形用户界面(GUI)。首先,我们移除OOP类及其__init__方法。

接下来,我们将所有方法移动到左侧,并移除self类引用,这样它们就变成了未绑定函数。

我们还移除了之前代码中所有的self引用。然后,我们将createWidgets函数调用移至函数声明之下。我们将其放置在mainloop调用之上。

最后,我们实现了相同的 GUI,但没有使用面向对象编程(OOP)。

重构后的代码如下所示:

import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu   

def createWidgets():    
    tabControl = ttk.Notebook(win)     
    tab1 = ttk.Frame(tabControl)            
    tabControl.add(tab1, text='Tab 1')    
    tabControl.pack(expand=1, fill="both")  
    monty = ttk.LabelFrame(tab1, text=' Monty Python ')
    monty.grid(column=0, row=0, padx=8, pady=4)        

    ttk.Label(monty, text="Enter a name:").grid(
column=0, row=0, sticky='W')
    name = tk.StringVar()
    nameEntered = ttk.Entry(monty, width=12, textvariable=name)
    nameEntered.grid(column=0, row=1, sticky='W')

    action = ttk.Button(monty, text="Click Me!")   
    action.grid(column=2, row=1)

    ttk.Label(monty, text="Choose a number:").grid(
column=1, row=0)
    number = tk.StringVar()
    numberChosen = ttk.Combobox(
monty, width=12, textvariable=number)
    numberChosen['values'] = (42)
    numberChosen.grid(column=1, row=1)
    numberChosen.current(0)

    scrolW = 30; scrolH = 3
    scr = scrolledtext.ScrolledText(
monty, width=scrolW, height=scrolH, wrap=tk.WORD)
    scr.grid(column=0, row=3, sticky='WE', columnspan=3)

    menuBar = Menu(tab1)
    win.config(menu=menuBar)
    fileMenu = Menu(menuBar, tearoff=0)
    menuBar.add_cascade(label="File", menu=fileMenu)
    helpMenu = Menu(menuBar, tearoff=0)
    menuBar.add_cascade(label="Help", menu=helpMenu)

    nameEntered.focus()     
#======================
win = tk.Tk()         
win.title("Python GUI")   
createWidgets()
win.mainloop()

它是如何工作的...

Python 使我们能够在合理的情况下使用面向对象编程(OOP)。其他语言如 Java 和 C#则强制我们始终使用面向对象编程的编码方法。在本食谱中,我们探讨了一种不适合使用面向对象编程的情况。

注意事项

如果代码库不断增长,面向对象(OOP)方法将更具可扩展性,但如果确定只需要这一种代码,那么就没有必要采用面向对象(OOP)方法。

如何成功使用设计模式

在这个菜谱中,我们将通过使用工厂设计模式来创建我们的 Python GUI 小部件。

在之前的菜谱中,我们手动逐个创建小部件,或者通过循环动态创建。

使用工厂设计模式,我们将使用工厂来创建我们的小部件。

准备就绪

我们将创建一个具有三个按钮的 Python 图形用户界面,每个按钮都有不同的样式。

如何做到这一点...

在我们的 Python GUI 模块顶部,紧接在导入语句下方,我们创建了几个类:

import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu

class ButtonFactory():
    def createButton(self, type_):
        return buttonTypes[type_]()

class ButtonBase():     
    relief     ='flat'
    foreground ='white'
    def getButtonConfig(self):
        return self.relief, self.foreground

class ButtonRidge(ButtonBase):
    relief     ='ridge'
    foreground ='red'        

class ButtonSunken(ButtonBase):
    relief     ='sunken'
    foreground ='blue'        

class ButtonGroove(ButtonBase):
    relief     ='groove'
    foreground ='green'        

buttonTypes = [ButtonRidge, ButtonSunken, ButtonGroove] 

class OOP():
    def __init__(self): 
        self.win = tk.Tk()         
        self.win.title("Python GUI")      
        self.createWidgets()

我们创建了一个基类,我们的不同按钮样式类都继承自这个基类,并且每个子类都覆盖了reliefforeground配置属性。所有子类都从该基类继承getButtonConfig方法。此方法返回一个元组。

我们还创建了一个按钮工厂类和一个列表,用于存储我们按钮子类的名称。我们将这个列表命名为buttonTypes,因为我们的工厂将创建不同类型的按钮。

在模块的下方,我们使用相同的buttonTypes列表创建按钮小部件。

    def createButtons(self):

        factory = ButtonFactory()

        # Button 1
        rel = factory.createButton(0).getButtonConfig()[0]
        fg  = factory.createButton(0).getButtonConfig()[1]
        action = tk.Button(self.monty, 
text="Button "+str(0+1), relief=rel, foreground=fg)   
        action.grid(column=0, row=1)  

        # Button 2
        rel = factory.createButton(1).getButtonConfig()[0]
        fg  = factory.createButton(1).getButtonConfig()[1]
        action = tk.Button(self.monty, 
text="Button "+str(1+1), relief=rel, foreground=fg)   
        action.grid(column=1, row=1)  

        # Button 3
        rel = factory.createButton(2).getButtonConfig()[0]
        fg  = factory.createButton(2).getButtonConfig()[1]
        action = tk.Button(self.monty, 
text="Button "+str(2+1), relief=rel, foreground=fg)   
       action.grid(column=2, row=1)   

首先,我们创建一个按钮工厂的实例,然后我们使用我们的工厂来创建我们的按钮。

注意事项

buttonTypes 列表中的项是我们子类的名称。

我们调用createButton方法,然后立即调用基类的getButtonConfig方法,并使用点符号获取配置属性。

当我们运行整个代码时,我们得到以下 Python tkinter 图形用户界面:

如何做...

我们可以看到,我们的 Python GUI 工厂确实创建了不同的按钮,每个按钮都有不同的样式。它们在文字颜色和浮雕属性上有所不同。

它是如何工作的...

在这个示例中,我们使用了工厂设计模式来创建具有不同样式的几个小部件。我们可以轻松地使用这种设计模式来创建整个 GUI。

设计模式是我们软件开发工具箱中一个非常激动人心的工具。

避免复杂性

在这个菜谱中,我们将扩展我们的 Python 图形用户界面,并学习处理软件开发工作中不断增长的复杂性的方法。

我们的合作者和客户都喜欢我们用 Python 创建的图形用户界面,并不断要求添加更多功能到我们的 GUI 中。

这增加了复杂性,并且很容易毁掉我们原本良好的设计。

准备就绪

我们将创建一个新的 Python GUI,类似于之前食谱中的那些,并且将以小部件的形式添加许多新功能。

如何做到这一点...

我们将从一个具有两个标签页的 Python 图形用户界面开始,其外观如下:

如何做...

我们收到的第一个新功能请求是向Tab 1添加功能,该功能清除scrolledtext小部件。

简单得很。我们只需在标签 1中添加另一个按钮。

        # Adding another Button
        self.action = ttk.Button(.
self.monty, text="Clear Text", command=self.clearScrol)   
        self.action.grid(column=2, row=2)

我们还必须创建回调方法以添加所需的功能,我们将在类的顶部定义该方法,并在创建我们的小部件的方法之外定义它。

    # Button callback
    def clickMe(self):
        self.action.configure(text='Hello ' + self.name.get())

    # Button callback Clear Text   
    def clearScrol(self):
        self.scr.delete('1.0', tk.END)

现在我们的 GUI 有一个新的按钮,当我们点击它时,我们将ScrolledText小部件的文本清空。

如何做...

为了添加这个功能,我们不得不在同一个 Python 模块的两个地方添加代码。

我们在createWidgets方法(未显示)中插入了新的按钮,然后创建了一个新的回调方法,当我们的新按钮被点击时,它会调用这个方法。我们将这段代码放置在第一个按钮的回调方法下方。

我们下一个功能请求是添加更多功能。业务逻辑封装在另一个 Python 模块中。我们通过在Tab 1中添加三个更多按钮来调用这个新功能。我们使用循环来完成这个操作。

        # Adding more Feature Buttons
        for idx in range(3):
            b = ttk.Button(
self.monty, text="Feature" + str(idx+1))   
            b.grid(column=idx, row=4)

我们的用户界面现在看起来是这样的:

如何做...

接下来,我们的客户要求更多功能,我们采用同样的方法。我们的图形用户界面现在看起来如下:

如何做...

注意

这还不错。当我们收到关于另外 50 个新功能的新的功能请求时,我们开始怀疑我们的方法是否仍然是最佳使用方法……

处理我们 GUI 必须处理的日益增加的复杂性的一个方法是通过添加标签页。通过添加更多标签页并将相关功能放入各自的标签页中,我们能够掌控复杂性,并使我们的 GUI 更加直观。

这里是创建我们新的Tab 3的代码,在其下方是我们的新 Python GUI:

# Tab Control 3  -----------------------------------------
        tab3 = ttk.Frame(tabControl)          # Add a tab
        tabControl.add(tab3, text='Tab 3')    # Make tab visible

        monty3 = ttk.LabelFrame(tab3, text=' New Features ')
        monty3.grid(column=0, row=0, padx=8, pady=4)

        # Adding more Feature Buttons
        startRow = 4
        for idx in range(24):
            if idx < 2:
                colIdx = idx
                col = colIdx
            else:
                col += 1
            if not idx % 3: 
                startRow += 1
                col = 0

            b = ttk.Button(monty3, text="Feature " + str(idx+1))
            b.grid(column=col, row=startRow)    

        # Add some space around each label
        for child in monty3.winfo_children(): 
            child.grid_configure(padx=8)

如何做...

它是如何工作的...

在这个菜谱中,我们向我们的 GUI 添加了几个新的小部件,以便为我们的 Python GUI 添加更多功能。我们看到了随着越来越多的新功能请求,我们的精美 GUI 设计很容易陷入一种状态,使得如何使用 GUI 变得不那么清晰。

注意事项

突然,小玩意儿们统治了世界…

我们看到了如何通过模块化我们的 GUI 来处理复杂性,即将大型功能拆分成更小的部分,并使用标签将它们安排在功能相关区域。

虽然复杂性有许多方面,但模块化和重构代码通常是处理软件代码复杂性的一个非常有效的方法。

注意事项

在编程中,有时我们会遇到一道墙,陷入困境。我们不断地撞击这堵墙,但没有任何进展。

有时候我们感觉好像想要放弃。

然而,奇迹确实会发生……

如果我们持续不断地撞击这堵墙,在某个时刻,墙将会倒塌,道路就会畅通。

在那个时刻,我们可以在软件宇宙中留下积极的印记。

posted @ 2025-09-22 13:21  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报