Tkinter-GUI-应用开发热点-全-
Tkinter GUI 应用开发热点(全)
原文:
zh.annas-archive.org/md5/0d2cdb9212d8c0461b491c8e1ed128ba译者:飞龙
前言
《Tkinter GUI 应用程序开发高手指南》 是一本循序渐进的指南,将引导你通过使用 Python 和 Tkinter(Python 的内置 GUI 模块)开发真实世界的图形应用程序的过程。
本书试图突出 Tkinter 的特性和功能,同时演示了多种使用 Tkinter 和 Python 开发令人兴奋、有趣和有用的 GUI 应用程序的方法。
我们希望带您踏上超过 10 个来自不同问题领域的项目的愉快之旅。随着我们在每个项目中开发新的应用程序,本书还建立了一个用于开发真实世界应用程序的一些常用策略的目录。
本书涵盖的内容
项目 1,认识 Tkinter,从零开始,提供了 Tkinter 的概述,包括如何创建根窗口、如何向根窗口添加小部件、如何使用几何管理器处理布局以及如何处理事件。
项目 2,创建类似记事本的文字编辑器,以程序设计风格开发了一个文本编辑器。它让读者初次体验了 Tkinter 的几个特性,以及开发真实应用程序的感觉。
项目 3,可编程鼓机,使用面向对象编程开发了一个能够播放用户创作的节奏的鼓机。该应用程序还可以保存这些创作,并在以后编辑或重新播放它们。在这里,你还可以学习编写多线程 GUI 应用程序。
项目 4,国际象棋游戏,开发了一个国际象棋游戏,介绍了将 GUI 应用程序作为模型-视图程序结构化的关键方面。它还教授了将真实世界对象(国际象棋)建模为程序可以操作记号的艺术。它还向读者介绍了 Tkinter Canvas 小部件的强大功能。
项目 5,音频播放器,承担了构建音频播放器的任务。此项目介绍了使用外部库的概念,同时展示了如何使用许多不同的 Tkinter 小部件。
项目 6,绘图应用程序,开发了一个绘图和图形编辑器。此项目还展示了如何开发和操作 GUI 框架,从而为你的所有未来程序创建可重用代码。
项目 7,一些有趣的项目想法,通过一系列小型但实用的项目进行操作,展示了来自不同领域的问题,如网络编程、数据库编程、绘图、基本动画和多线程编程。
附录 A, 杂项提示 讨论了一些在先前项目中未涵盖但许多 GUI 程序中常见的 GUI 编程的重要方面。
附录 B, 快速参考表 列出了所有 Tkinter 和 ttk 选项和方法的便捷参考表,以及它们输入、用法和输出的简要描述。
您需要为本书准备的东西
本书讨论的程序是在 Windows 平台上开发的。然而,鉴于 Tkinter 的多平台能力,您可以在 Linux 发行版或 Mac OS 等其他平台上轻松地进行工作。
以下软件是本书所需的:
- 包含在发行版中的 Python 2.7 版本和 Tkinter 8.5
在相应的项目中提到了下载和安装其他项目特定模块和软件的链接。
适用于本书的读者
本书假设您熟悉 Python 编程语言,处于入门水平。然而,一个有编程背景的积极 Python 新手可以通过一点外部研究来填补知识上的空白。
术语
在本书中,您将找到多种文本样式,用以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词如下所示:“我们可以通过使用include指令来包含其他上下文。”
代码块如下设置:
from Tkinter import *
class MyFirstGUI():
def __init__(self):
self.root = Tk()
self.root.mainloop()
if __name__ == '__main__':
app = MyFirstGUI()
当我们希望将您的注意力引向代码块的一个特定部分时,相关的行或项目将以粗体显示:
from Tkinter import *
class MyFirstGUI():
def __init__(self):
self.root = Tk()
self.root.mainloop()
if __name__ == '__main__':
app = MyFirstGUI()
关于 Python 交互式外壳的任何输入都如下所示:
>>> import Tkinter
>>> help(Tkinter.Label)
新术语和重要词汇以粗体显示。您在屏幕上看到的词汇,例如在菜单或对话框中,在文本中如下所示:“当用户指定一个新数字并点击更新记录按钮时,它调用一个方法。”
注意
警告或重要注意事项如下所示。
小贴士
小贴士和技巧如下所示。
读者反馈
我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大收益的标题非常重要。
要向我们发送一般反馈,只需发送一封电子邮件到 <feedback@packtpub.com>,并在邮件的主题中提及书名。
如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您是 Packt 图书的骄傲拥有者,我们有一些东西可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您在 www.packtpub.com 的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误详情来报告它们。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站,或添加到该标题的现有错误清单中,在“错误清单”部分下。您可以通过从 www.packtpub.com/support 选择您的标题来查看任何现有错误清单。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
如果您发现了疑似盗版材料,请通过 <copyright@packtpub.com> 联系我们,并提供链接。
我们感谢您在保护我们作者以及为我们带来有价值内容方面的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。
第一章。认识 Tkinter
欢迎来到 Tkinter 的激动人心的 GUI 编程世界。本项目旨在让您熟悉 Tkinter,这是所有标准 Python 发行版内置的图形用户界面(GUI)接口。
Tkinter(发音为“tea-kay-inter”)是 Python 对 Tk 的接口,Tk 是 Tcl/Tk 的 GUI 工具包。
Tcl(发音为“tickle”,是工具命令语言的缩写)是在嵌入式应用、测试、原型设计和 GUI 开发领域流行的脚本语言。另一方面,Tk 是一个开源的多平台小部件工具包,被许多不同的语言用于构建 GUI 程序。
Tkinter 接口作为 Python 模块Tkinter.py实现,它只是围绕使用 Tcl/Tk 库的 C 扩展的包装器。
Tkinter 适用于广泛应用于各种领域的应用,从小型桌面应用程序到跨多个学科的科学建模和研究工作。
我们相信,您在这里将开发的概念将使您能够将 GUI 应用程序应用于您感兴趣的领域。让我们开始吧!
任务简报
本项目的目的是让您对 Tkinter 感到舒适。它旨在向您介绍 Tkinter 的 GUI 编程的各种组件。
到这个项目结束时,您将开发出几个部分功能性的模拟应用程序,如下所示:

本项目中开发的应用程序是“模拟应用程序”,因为它们不是完全功能性的。实际上,每个小型模拟应用程序的目的是向您介绍 Tkinter 编程的一些特定方面。这将为您从项目 2、“制作文本编辑器”开始的一些有趣且完全功能性的项目想法奠定基础。
为什么它如此出色?
编程 GUI 应用程序的能力(与简单的控制台应用程序相比)为程序员打开了一个全新的世界。它将程序的焦点从程序员转移到最终用户,使程序员能够接触到更广泛的受众。
当学习 Python 的人需要过渡到 GUI 编程时,Tkinter 似乎是最简单、最快完成任务的方式。Tkinter 是 Python 中编程 GUI 应用程序的出色工具。
使 Tkinter 成为 GUI 编程优秀选择的特性包括:
-
它易于学习(比任何其他 Python GUI 包都简单)
-
相对少量的代码可以产生强大的 GUI 应用程序
-
分层设计确保了它易于掌握
-
它可以在所有操作系统上运行
-
它易于访问,因为它随标准 Python 发行版预安装
没有其他 GUI 工具包同时具备所有这些功能。
您的热门目标
我们希望您从这个项目中吸取的关键概念包括:
-
理解根窗口和主循环的概念
-
理解小部件——您程序的构建块
-
熟悉可用的小部件列表
-
使用三个几何管理器:pack、grid 和 place 来开发布局
-
学习如何应用事件和回调使您的程序功能化
-
使用样式选项对您的部件进行样式设置并配置根部件
任务清单
假设您对 Python 的数据结构、语法和语义有基本了解。为了与本项目一起工作,您必须在您的计算机上安装一个可工作的 Python 2.7.3 副本。
Python 下载包和不同平台的下载说明可在www.Python.org/getit/releases/2.7.3/找到。
我们将在 Windows 7 平台上开发我们的应用程序。然而,由于 Tkinter 真正是跨平台的,您可以在 Mac 或 Linux 发行版上跟随,而无需对我们的代码进行任何修改。
安装后,打开 IDLE 窗口并输入:
>>>from Tkinter import *
如果您已安装 Python 2.7,此 shell 命令应无错误执行。
如果没有错误消息,Tkinter 模块已安装到您的 Python 发行版中。当使用本书中的示例时,我们不支持除 Python 2.7 以外的任何 Python 版本,Python 2.7 捆绑了 Tkinter Tcl/Tk 版本 8.5。
要测试您的 Python 安装中是否有正确的 Tkinter 版本,请在您的 IDLE 或交互式 shell 中输入以下命令:
>>> import Tkinter
>>>Tkinter._test()
这应该会弹出一个窗口,窗口中的第一行显示这是 Tcl/Tk 版本 8.5。请确保它不是 8.4 或任何更早的版本,因为版本 8.5 在其先前版本上有了很大的改进。
如果您的版本测试确认它是 Tcl/Tk 版本 8.5,那么您就可以开始编写 Tkinter GUI 应用程序了。让我们开始吧!
根窗口 – 您的绘图板
GUI 编程是一门艺术,就像所有艺术一样,您需要一个画板来捕捉您的想法。您将使用的画板被称为根窗口。我们的第一个目标是准备好根窗口。
Engage Thrusters
以下截图展示了我们将要创建的根窗口:

绘制根窗口很简单。您只需要以下三行代码:
from Tkinter import *
root = Tk()
root.mainloop()
将其保存为.py文件扩展名或查看代码1.01.py。在 IDLE 窗口中打开它,并从运行菜单(IDLE 中的F5)运行程序。运行此程序应生成一个如前截图所示的空白根窗口。此窗口配备了功能化的最小化、最大化和关闭按钮,以及一个空白框架。
小贴士
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
代码的描述如下:
-
第一行将 Tkinter 的所有(
*)类、属性和方法导入当前工作区。 -
第二行创建了一个
Tkinter.Tk类的实例。这创建了你在提供的截图中所看到的“根”窗口。按照惯例,Tkinter 中的根窗口通常被称为“root”,但你也可以用任何其他名字来调用它。 -
第三行执行了
root对象的mainloop(即事件循环)方法。mainloop方法使根窗口保持可见。如果你删除第三行,第 2 行创建的窗口将立即消失,因为脚本停止运行。这会发生得如此之快,以至于你甚至看不到窗口出现在你的屏幕上。保持mainloop运行还让你能够保持程序运行,直到你按下关闭按钮,这将退出主循环。
目标完成 - 简短回顾
恭喜!你已经完成了第一个目标,即绘制根窗口。你现在已经准备好了你的绘图画布(根窗口)。现在准备好用你的想象力来绘制它吧!
注意
将以下三行代码(如code 1.01.py所示)记住。这三行代码生成了你的根窗口,它将容纳所有其他图形组件。这些行构成了你将在 Tkinter 中开发的任何 GUI 应用程序的骨架。所有使你的 GUI 应用程序功能化的代码都将位于此代码的第 2 行(新对象创建)和第 3 行(mainloop)之间。
分类情报
本节描述了导入 Tkinter 模块的不同方式。
在前面的例子中,我们使用以下命令导入 Tkinter:
from Tkinter import *
这种导入方法简化了模块中定义的方法的处理。也就是说,你可以直接访问这些方法。通常,被认为是不好的做法像我们这里这样做,导入模块的所有(*)方法。这是因为如果你从其他模块导入具有相同方法名的所有方法,会导致方法的覆盖。
有几种导入 Tkinter 的方法可以避免这种重叠,其中一种常见的方法是:
import Tkinter
这种导入方式不会将 Tkinter 内部定义的所有方法列表污染命名空间。然而,现在 Tkinter 中的每个方法都必须使用格式Tkinter.methodA来调用,而不是直接调用方法。
另一种常用的导入方式如下:
import Tkinter as Tk
在这里,你也不应该将所有 Tkinter 方法都污染当前命名空间,现在你可以访问如Tk.methodA这样的方法。"Tk"是一个方便、易于输入的别名,许多开发者常用它来导入 Tkinter。
整体情况
作为 GUI 程序员,你通常需要决定你程序的三方面:
-
屏幕上应该显示哪些组件?:这涉及到选择构成用户界面的组件。典型组件包括按钮、输入字段、复选框、单选按钮、滚动条等。在 Tkinter 中,你添加到 GUI 的组件被称为小部件。
-
组件应该放在哪里?:这涉及到决定每个组件在整体设计结构中的位置或放置。这包括对位置和各个组件的结构布局问题所做的决定。在 Tkinter 中,这被称为几何管理。
-
组件如何交互和表现?:这涉及到为每个组件添加功能。每个组件或小部件都执行一些工作。例如,按钮在被点击时做出响应;滚动条处理滚动;复选框和单选按钮使用户能够做出一些选择。在 Tkinter 中,各种小部件的功能是通过
command绑定或使用回调函数的event绑定来管理的。
让我们深入探讨 Tkinter 中的这三个组件。
小部件——你的 GUI 程序的基本构建块
现在我们已经准备好了 Toplevel 窗口,是时候思考问题,窗口中应该出现哪些组件?在 Tkinter 术语中,这些组件被称为小部件。
启动推进器
添加小部件的语法如下:
mywidget = Widget-name (its container window,**configuration options)
在以下示例(参考代码01.02.py)中,我们向根框架添加了两个小部件,一个标签和一个按钮。注意所有小部件是如何添加到我们在第一个示例中定义的骨架代码之间的。
from Tkinter import *
root = Tk()
mylabel = Label(root,text="I am a label widget")
mybutton = Button(root,text="I am a button")
mylabel.pack()
mybutton.pack()
root.mainloop()
代码的描述如下:
-
这段代码为标签小部件添加了一个新的实例,
mylabel。第一个参数定义root为其父或容器。第二个参数配置其文本选项为"I am a label widget"。 -
我们同样定义了一个按钮小部件的实例。这也绑定到根窗口作为其父。
-
我们使用
pack()方法,这是在窗口中定位标签和按钮小部件的基本要求。我们将在“几何管理任务”下讨论pack()方法和几个其他相关概念。然而,你必须注意,某种形式的几何规范对于小部件在 Toplevel 窗口中显示是必不可少的。 -
运行此代码将生成以下截图所示的窗口。它将有一个自定义标签和一个自定义按钮:
![启动推进器]()
目标完成 - 简短回顾
在这次迭代中,我们学习了以下内容:
-
小部件是什么。
-
如何在容器窗口框架内实例化和显示 widget。
-
如何在实例化时设置 widget 的选项。
-
指定几何选项(如
pack())以显示 widget 的重要性。我们将在后续任务中进一步讨论这一点。
分类情报
-
所有 widget 实际上都是它们各自widget 类的派生对象。因此,
mybutton = Button(myContainer)这样的语句实际上是从Button类创建按钮实例。 -
每个 widget 都有一组选项,决定了其行为和外观。这包括文本标签、颜色、字体大小等属性。例如,按钮 widget 有管理其标签、控制其大小、更改前景色和背景色、更改边框大小等属性。
-
要设置这些属性,你可以在创建 widget 时直接设置值,就像我们在前面的例子中所做的那样。或者,你可以稍后通过使用
.config()或.configure()方法设置或更改 widget 的选项。请注意,.config()或.configure()方法是可互换的,并提供相同的功能。
小贴士
你也可以在创建 widget 的新实例时在同一行添加pack()方法。例如,考虑以下代码:
mylabel = Label(root,text="I am a label widget")
mylabel.pack()
如果你直接实例化 widget,你可以将这两行代码一起写,如下所示:
Label(root,text="I am a label widget").pack()
你可以保留创建的 widget 的引用(如第一个例子中的mylabel),或者你可以创建一个没有保留任何引用的 widget(如第二个例子)。
如果 widget 的内容可能在程序的稍后阶段被某些操作修改,理想情况下你应该保留引用。如果 widget 的状态在创建后保持静态,你不需要保留 widget 的引用。
此外,请注意,调用pack()(或其他几何管理器)总是返回None。因此,考虑你创建一个 widget 并保留对其的引用,并在同一行添加几何管理器(例如pack()),如下所示:
mylabel = Label(…).pack()
在这种情况下,你实际上并没有创建 widget 的引用,而是为变量mylabel创建了一个None类型对象。
因此,当你稍后尝试通过引用修改 widget 时,你会得到一个错误,因为你实际上正在尝试对一个None类型对象进行操作。
这是最常见的初学者错误之一。
了解核心 Tkinter widget
在这次迭代中,我们将了解所有核心 Tkinter widget。在前面的例子中,我们已经看到了其中的两个——标签和按钮 widget。现在让我们看看所有其他核心 Tkinter widget。
准备起飞
Tkinter 包括 21 个核心 widget。具体如下:
| Toplevel widget | Label widget | Button widget |
|---|---|---|
| Canvas widget | Checkbutton widget | Entry widget |
| Frame widget | LabelFrame widget | Listbox widget |
| 菜单 widget | Menubutton widget | 消息 widget |
| OptionMenu widget | PanedWindow widget | Radiobutton widget |
| 滑块小部件 | 滚动条小部件 | 滚动框小部件 |
| 文本小部件 | 位图类小部件 | 图像类小部件 |
让我们编写一个程序,将这些小部件包含在我们的根窗口中。
启动推进器
添加小部件的格式与我们之前讨论的任务中相同。为了给你一个感觉,这里有一些添加一些常见小部件的示例代码:
Label(parent, text=" Enter your Password:")
Button(parent, text="Search")
Checkbutton(parent, text='RememberMe', variable=v, value=True)
Entry(parent, width=30)
Radiobutton(parent, text=Male, variable=v, value=1)
Radiobutton(parent, text=Female, variable=v, value=2)
OptionMenu(parent, var, "Select Country", "USA", "UK", "India", Others")
Scrollbar(parent, orient=VERTICAL, command=mytext.yview)
你能发现每个小部件的共同模式吗?你能发现差异吗?
作为提醒,添加小部件的语法是:
Widget-name (its container window, *configuration options)
小贴士
创建所有之前提到的小部件的方法是相同的。大多数配置选项也将相似。然而,一些配置选项会根据小部件的不同而有所变化。
例如,按钮和标签小部件将有一个配置文本的选项,但滚动条没有文本配置选项。
使用相同的模式,现在让我们将所有 21 个核心 Tkinter 小部件添加到一个虚拟应用程序(代码01.03.py)中。
小贴士
不要因为程序的大小而感到害怕。相反,寻找用于初始化和显示所有小部件的共同模式。为了重申,添加小部件的语法是:
mywidget = Widget-name (container, all widget-options)
注意到每个小部件的配置选项如何根据初始化的小部件类型略有不同。
请参考代码1.03.py以查看所有 Tkinter 小部件的演示。1.03.py的代码摘要如下:
-
我们创建了一个顶层窗口并创建了一个主循环,就像之前示例中看到的那样。
-
我们添加了一个名为
menubar的框架小部件。请注意,框架小部件只是持有其他小部件的容器小部件。框架小部件非常适合将小部件分组在一起。添加框架的语法与其他所有小部件的语法相同:myframe = Frame(root) myframe.pack() -
将
menubar框架作为容器,我们向其中添加了两个小部件,菜单按钮和菜单小部件。 -
我们创建了一个新的框架并命名为
myframe1。将myframe1作为容器/父小部件,我们向其中添加了七个小部件:- 标签、输入框、按钮、复选框、单选按钮、选项菜单和位图类小部件。
-
然后,我们继续创建
myframe2,另一个框架小部件。我们向其中添加了六个更多的小部件:- 图像类、列表框、滚动框、滑块、标签框架和信息小部件。
-
我们接着创建
myframe3,另一个框架小部件。我们向其中添加了两个更多的小部件,文本和滚动条小部件。 -
最后,我们创建了最后一个框架
myframe4,另一个框架小部件。我们向其中添加了两个更多的小部件,画布和分割窗口小部件。
所有这些小部件构成了 Tkinter 的 21 个核心小部件。
注意
仔细阅读代码解释,并在示例代码01.03.py中找到相应的代码片段。观察每个小部件是如何创建的。尝试识别每个小部件在 Tkinter 中使用的类名。看看所有小部件中哪些是相同的,以及不同的小部件之间有哪些变化?
在1.03.py中花几分钟阅读和理解代码,将真正帮助你欣赏 Tkinter 程序的简洁性和整体结构。
最后,请注意,我们已经在每个部件上使用了.pack()来显示它在其容器框架内。我们将在下一个任务中讨论.pack()。然而,现在只需注意,我们已经使用了名为pack()的东西,没有它,部件根本不会显示。
目标完成 - 简短总结
你在 GUI 编程工作中已经达到了一个重要的里程碑。
你现在已经了解了 Tkinter 的所有 21 个核心部件。你可以通过它们的类名来识别它们,你可以在根框架或根框架内的子框架中创建它们。你现在知道如何配置部件的选项。
通过这种方式,你现在已经看到了 Tkinter 程序的第一块也是最重要的构建块。你已经掌握了 Tkinter 部件。
分类情报
与我们迄今为止的示例一样,部件选项可以在实例化时设置。或者,可以使用以下语法在实例化后配置选项:
widget.configure(**options)
这是一个非常实用的工具,它允许你在创建部件后动态地更改部件选项。我们将在所有项目中非常频繁地使用它。
对于常见的部件配置选项,请参阅附录 B 中的部件的通用选项部分,快速参考表。
几何管理
在了解了所有核心 Tkinter 部件之后,现在让我们将注意力转向 GUI 编程的第二个组成部分——放置这些部件的问题。
这是由 Tkinter 的几何管理器选项处理的。GUI 编程的这部分涉及决定部件的位置、整体布局以及各种部件在屏幕上的相对位置。
准备起飞
回想一下,我们使用了pack()方法来向上一节中开发的虚拟应用程序添加部件。pack()是 Tkinter 中几何管理的一个例子。
pack()不是管理界面几何形状的唯一方法。事实上,Tkinter 中有三个几何管理器允许你指定 Toplevel 或父窗口内部件的位置。
几何管理器如下:
-
pack:这是我们迄今为止使用过的。对于简单的布局来说很简单,但对于稍微复杂的布局可能会变得非常复杂。 -
grid:这是最常用的几何管理器,它提供了一种类似于表格的布局管理功能,便于布局管理。 -
place:这是最不受欢迎的,但提供了对部件绝对定位的最佳控制。
启动推进器
现在,让我们看看所有三种几何管理器在实际操作中的例子。
pack几何管理器
pack几何管理器的名称来源于它实际上是在主框架中按先来先服务的原则将部件打包到可用的空间中。
pack几何管理器将“从属部件”放入“父空间”。在打包从属部件时,pack管理器区分三种空间:
-
未声明的空间
-
声称但未使用的空间
-
声称和使用的空间
在pack()中最常用的选项包括:
-
side:LEFT,TOP,RIGHT, 和BOTTOM(这些决定了小部件的对齐方式) -
fill:X,Y,BOTH,和NONE(这些决定了小部件是否可以增长尺寸) -
expand:1/0或Yes/No(对应于相应的值) -
anchor:NW,N,NE,E,SE,S,SW,W, 和CENTER(对应于基本方向) -
内部填充(
ipadx和ipady)和外部填充(padx和pady),它们都默认为 0 值
让我们看看一些演示代码,这些代码说明了pack的一些功能。以下是一个代码片段(代码1.04.py),它生成一个类似于以下屏幕截图的 GUI:

from Tkinter import *
root = Tk()
Button(root, text="A").pack(side=LEFT, expand=YES, fill=Y)
Button(root, text="B").pack(side=TOP, expand=YES, fill=BOTH)
Button(root, text="C").pack(side=RIGHT, expand=YES, fill=NONE, anchor=NE)
Button(root, text="D").pack(side=LEFT, expand=NO, fill=Y)
Button(root, text="E").pack(side=TOP, expand=NO, fill=BOTH)
Button(root, text="F").pack(side=RIGHT, expand=NO, fill=NONE)
Button(root, text="G").pack(side=BOTTOM, expand=YES, fill=Y)
Button(root, text="H").pack(side=TOP, expand=NO, fill=BOTH)
Button(root, text="I").pack(side=RIGHT, expand=NO)
Button(root, text="J").pack(anchor=SE)
root.mainloop()
代码的描述如下所示:
-
当你在
root框架中插入按钮A时,它捕获框架的最左侧区域,它扩展并填充了Y维度。因为扩展和填充选项被指定为肯定,它声称它想要的全部区域并填充了Y维度。如果你通过向下拖动根窗口来增加其大小,你会注意到按钮A在向下方向(沿着Y坐标)扩展,但窗口的侧向增加并不会导致按钮A的横向尺寸增加。 -
当你在根窗口中插入下一个按钮B时,它会从剩余区域中获取空间,但将其自身对齐到
TOP,扩展填充可用区域,并填充可用空间的X和Y坐标。 -
第三个按钮C调整到剩余空间的右侧。但由于填充被指定为
NONE,它只占用容纳按钮内文本所需的空间。如果你扩展根窗口,按钮C的大小不会改变。 -
在某些行中使用的
anchor属性提供了一种将小部件相对于参考点定位的方法。如果没有指定anchor属性,pack管理器将小部件放置在可用空间或填充框的中心。其他允许的选项包括四个基本方向(N,S,E和W)以及任意两个方向的组合。因此,anchor属性的合法值包括:CENTER(默认),N,S,E,W,NW,NE,SW和SE。
其余行的描述留给你作为练习去探索。研究这段代码的最佳方式是注释掉所有代码行,然后逐个引入每个连续的按钮。在每一步,尝试调整窗口大小以查看它对各种按钮的影响。

我们将在我们的项目中使用pack几何管理器,因此熟悉pack及其选项将是一项值得做的练习。
注意
注意,大多数 Tkinter 几何管理器属性的值可以是大写字母(无需引号,如side=TOP,anchor=SE)或小写字母(但需在引号内,如side='top',anchor='se')。
要获取完整的pack管理器参考,请参阅附录 B 中的《pack 管理器》部分,快速参考表。
小贴士
你应该在哪里使用pack()几何管理器?
与我们接下来要讨论的grid方法相比,使用pack管理器稍微复杂一些,但在以下情况下它是一个很好的选择:
-
使小部件填充完整的容器框架
-
将多个小部件堆叠或并排放置(如前一个屏幕截图所示)。请参阅代码
1.05.py。
虽然您可以通过在多个框架中嵌套小部件来创建复杂的布局,但您会发现grid几何管理器更适合大多数复杂布局。
网格几何管理器
grid几何管理器是最容易理解,也许也是 Tkinter 中最有用的几何管理器。grid几何管理器的核心思想是将容器框架划分为一个二维表格,该表格由多个行和列组成。然后,表中的每个单元格都可以用来放置小部件。在这种情况下,单元格是虚拟行和列的交点。请注意,在grid方法中,每个单元格只能放置一个小部件。然而,小部件可以被设置为跨越多个单元格。
在每个单元格内,您可以使用STICKY选项进一步对齐小部件的位置。sticky选项决定了当其容器单元格大于包含的小部件大小时,小部件如何扩展。sticky选项可以使用一个或多个N、S、E和W,或NW、NE、SW和SE选项来指定。
未指定粘性则默认为将小部件粘附在单元格中心。
现在我们来看一个演示代码,它展示了grid几何管理器的一些功能。1.06.py中的代码生成一个类似于 GUI 的图形,如图所示:

from Tkinter import *
root = Tk()
Label(root, text="Username").grid(row=0, sticky=W)
Label(root, text="Password").grid(row=1, sticky=W)
Entry(root).grid(row=0, column=1, sticky=E)
Entry(root).grid(row=1, column=1, sticky=E)
Button(root, text="Login").grid(row=2, column=1, sticky=E)
root.mainloop()
代码的描述如下:
-
查看在表示整个框架的虚拟网格表中按行和列位置定义的网格位置。看看
sticky=W在两个标签上的使用如何使它们粘附在西部或左侧,从而实现整洁的布局。 -
每列的宽度(或每行的长度)自动由单元格中包含的小部件的高度或宽度决定。因此,您无需担心指定行或列的宽度相等。如果您需要额外的控制,可以指定小部件的宽度。
-
您可以使用参数
sticky=N+S+E+W使小部件可扩展以填充整个网格单元格。
在更复杂的场景中,您的部件可能跨越网格中的多个单元格。为了使网格跨越多个单元格,grid方法提供了非常方便的选项,如rowspan和columnspan。
此外,您可能经常需要在网格单元格之间提供一些填充。grid管理器提供了padx和pady选项,以在单元格中为部件提供填充。
类似地,还有ipadx和ipady选项用于内部填充。外部和内部填充的默认值是0。
让我们看看grid管理器的例子,其中我们使用了grid方法的大部分常见参数,如row、column、padx、pady、rowspan和columnspan的实际应用。
1.08.py代码是grid()几何管理器选项的演示:
from Tkinter import *
top = Tk()
top.title('Find & Replace')
Label(top,text="Find:").grid(row=0, column=0, sticky='e')
Entry(top).grid(row=0,column=1,padx=2,pady=2,sticky='we',columnspan=9)
Label(top, text="Replace:").grid(row=1, column=0, sticky='e')
Entry(top).grid(row=1,column=1,padx=2,pady=2,sticky='we',columnspan=9)
Button(top, text="Find").grid(row=0, column=10, sticky='ew', padx=2, pady=2)
Button(top, text="Find All").grid(row=1, column=10, sticky='ew', padx=2)
Button(top, text="Replace").grid(row=2, column=10, sticky='ew', padx=2)
Button(top, text="Replace All").grid(row=3, column=10, sticky='ew', padx=2)
Checkbutton(top, text='Match whole word only').grid(row =2, column=1, columnspan=4, sticky='w')
Checkbutton(top, text='Match Case').grid(row =3, column=1, columnspan=4, sticky='w')
Checkbutton(top, text='Wrap around').grid(row =4, column=1, columnspan=4, sticky='w')
Label(top, text="Direction:").grid(row=2, column=6, sticky='w')
Radiobutton(top, text='Up', value=1).grid(row=3, column=6, columnspan=6, sticky='w')
Radiobutton(top, text='Down', value=2).grid(row=3, column=7, columnspan=2, sticky='e')
top.mainloop()
注意,仅仅 14 行核心grid管理器代码就能生成如以下截图所示的复杂布局。相比之下,使用pack管理器开发将会更加繁琐:

另一个您有时可以使用的grid选项是widget.grid_forget()方法。此方法可以用来从屏幕上隐藏部件。当您使用此选项时,部件仍然存在于其位置,但变得不可见。隐藏的部件可以再次变得可见,但您最初分配给部件的任何grid选项都将丢失。
类似地,还有一个widget.grid_remove()方法可以移除部件,但在这个情况下,当您再次使部件可见时,所有其grid选项都将被恢复。
对于完整的grid()参考,请参阅附录 B 中的“网格管理器”部分,快速参考表。
小贴士
您应该在何处使用grid()几何管理器?
grid管理器是开发复杂布局的强大工具。通过将容器部件分解成行和列的网格,然后将部件放置在所需的网格中,可以轻松实现复杂结构。
它也常用于开发不同类型的对话框。
现在,我们将深入了解配置网格列和行的尺寸。
不同的部件有不同的高度和宽度。因此,当您以行和列的术语指定部件的位置时,单元格会自动扩展以容纳部件。
通常,所有网格行的自动调整高度以适应其最高的单元格。同样,所有网格列的宽度会调整到最宽部件单元格的宽度。
如果您想要一个更小的部件填充更大的单元格或保持在单元格的任何一边,您可以使用部件上的sticky属性来控制这一点。
然而,您可以使用以下代码来覆盖列和行的自动尺寸:
w.columnconfigure(n, option=value, ...) AND
w.rowconfigure(N, option=value, ...)
使用这些选项来配置给定的小部件w在列n中的选项,指定minsize、pad和weight的值。
这里可用的选项如以下表格中所述:
| 选项 | 描述 |
|---|---|
minsize |
列或行的最小像素大小。如果给定列或行中没有小部件,则即使有此 minsize 指定,单元格也不会出现。 |
pad |
在指定列或行的大小之外,添加到最大单元格大小的像素外部填充。 |
| weight | 这指定了行或列的相对权重,然后分配额外的空间。这允许使行或列可伸缩。例如,以下代码将五分之二的额外空间分配给第一列,将五分之三分配给第二列:
w.columnconfigure(0, weight=2)
w.columnconfigure(1, weight=3)
|
columnconfigure() 和 rowconfigure() 方法通常用于实现小部件的动态调整大小,尤其是在调整根窗口时。
注意
你不能在同一个容器窗口中使用 grid 和 pack 方法。如果你尝试这样做,你的程序将进入无限协商循环。
place 几何管理器
place 几何管理器是 Tkinter 中最不常用的几何管理器。尽管如此,它在某些情况下有其用途,因为它允许你使用 X-Y 坐标系精确地在父框架内定位小部件。
可以使用 place() 方法对所有标准小部件进行 place 管理器的评估。
place 几何管理器的重要选项包括:
-
绝对定位(以
x=N或y=N的形式指定) -
相对定位(关键选项包括
relx、rely、relwidth和relheight)
与 place() 常一起使用的其他选项包括 width 和 anchor(默认为 NW)。请参考 1.09.py 中的代码以演示常见的 place 选项:
from Tkinter import *
root = Tk()
# Absolute positioning
Button(root,text="Absolute Placement").place(x=20, y=10)
# Relative positioning
Button(root, text="Relative").place(relx=0.8, rely=0.2, relwidth=0.5, width=10, anchor = NE)
root.mainloop()
仅通过查看代码或窗口框架,你可能看不到绝对位置和相对位置之间太大的区别。然而,如果你尝试调整窗口大小,你会注意到绝对定位的按钮不会改变其坐标,而相对定位的按钮会改变其坐标和大小以适应根窗口的新大小。

对于完整的 place() 参考,请查看 附录 B 中的 place 管理器 部分,快速参考表。
提示
你应该在什么情况下使用 place 管理器?
在需要实现自定义几何管理器且小部件位置由最终用户决定的情况下,place 管理器非常有用。
虽然 pack() 和 grid() 管理器不能在同一个框架中一起使用,但 place() 管理器可以与同一容器框架内的任何其他几何管理器一起使用。
place 管理器很少被使用。这是因为如果你使用它,你必须担心确切的坐标。比如说,如果你对一个小部件进行了一些小的修改,那么你很可能还需要更改其他小部件的 X-Y 值,这可能会非常繁琐。
我们不会在我们的项目中使用place管理器。然而,了解基于坐标定位的选项存在可以在某些情况下有所帮助。
目标完成 – 简短总结
这就结束了我们对 Tkinter 中几何管理的讨论。
在本节中,你实现了pack、grid和place几何管理器的示例。你还了解了每个几何管理器的优点和缺点。
你了解到pack最适合简单的侧向或自上而下的小部件定位。你还看到grid管理器最适合处理复杂布局。你看到了place几何管理器的示例以及为什么它很少使用的原因。
现在,你应该能够使用 Tkinter 的这些几何管理器来规划和执行你程序的不同布局。
事件和回调——为程序注入活力
现在我们已经学会了如何将小部件添加到屏幕上以及如何将它们定位在我们想要的位置,让我们将注意力转向 GUI 编程的第三个组成部分。这解决了如何使小部件具有功能的问题。
使小部件具有功能涉及使它们对按钮按下、键盘上的按键、鼠标点击等事件做出响应。这需要将回调函数与特定事件关联起来。
启动推进器
回调通常与特定的小部件事件相关联,使用command绑定规则,这将在下一节中详细说明。
命令绑定
向按钮添加功能的最简单方法称为command绑定,其中回调函数以command = some_callback的形式在小部件选项中提及。
看一下下面的示例代码:
def my_callback ():
# do something
Button(root,text="Click",command= my_callback)
注意,my_callback是在小部件的command选项中不带括号()调用的。这是因为当回调函数被设置时,必须传递一个函数的引用而不是实际调用它。
向回调传递参数
如果回调函数不接受任何参数,它可以像我们刚才使用的那样用简单的函数处理。然而,如果回调函数需要接受一些参数,我们可以使用下面的代码片段中展示的lambda函数:
def my_callback (somearg):
#do something with argument
Button(root,text="Click",command=lambda: my_callback ('some argument'))
Python 从名为lambda的功能性程序中借用语法。lambda函数允许你动态定义一个单行、无名的函数。
使用lambda的格式是lambda arg: #do something with arg in a single line,例如:
lambda x: return x²
注意
请注意,与按钮小部件一起提供的command选项实际上是一个替代函数,用于简化按钮事件的编程。许多其他小部件不提供任何等效的command绑定选项。
默认情况下,命令按钮绑定到左键点击和空格键。它不会绑定到回车键。因此,如果你使用 command 函数绑定按钮,它将响应空格键而不是回车键。这对许多 Windows 用户来说可能不太直观。更糟糕的是,你不能改变 command 函数的这个绑定。教训是,尽管 command 绑定是一个非常方便的工具,但它并不提供让你自己决定绑定的独立性。
事件绑定
幸运的是,Tkinter 提供了一种名为 bind() 的替代事件绑定机制,让你可以处理不同的事件。绑定事件的常规语法如下:
widget.bind(event, handler)
当与事件描述相对应的事件在控件中发生时,它会调用相关处理程序,并将事件对象的实例及其详细信息作为参数传递。
让我们看看 bind() 方法的示例(参考代码文件 1.10.py):
from Tkinter import *
root = Tk()
Label(root, text='Click at different\n locations in the frame below').pack()
def mycallback(event):
print dir(event)
print "you clicked at", event.x, event.y
myframe = Frame(root, bg='khaki', width=130, height=80)
myframe.bind("<Button-1>", mycallback)
myframe.pack()
root.mainloop()
代码的描述如下:
-
我们将 Frame 小部件绑定到事件
<Button-1>,这对应于鼠标的左键点击。当这个事件发生时,它会调用函数mycallback,并将一个对象实例作为其参数传递。 -
我们定义了函数
mycallback(event)。注意,它将事件对象作为参数。 -
我们使用
dir(event)来检查事件对象,它返回传递给它的事件对象的属性名称的排序列表。这会打印出列表:['__doc__', '__module__', 'char', 'delta', 'height', 'keycode', 'keysym', 'keysym_num', 'num', 'send_event', 'serial', 'state', 'time', 'type', 'widget', 'width', 'x', 'x_root', 'y', 'y_root']。
-
从对象生成的属性列表中,我们使用了两个属性,
event.x和event.y,来打印点击点的坐标。
当你运行此代码时,它会生成一个类似于图中所示的窗口。当你在此框架的任何地方左键点击时,它会向控制台输出消息。传递给控制台的一个示例消息如下:

['__doc__', '__module__', 'char', 'delta', 'height', 'keycode', 'keysym', 'keysym_num', 'num', 'send_event', 'serial', 'state', 'time', 'type', 'widget', 'width', 'x', 'x_root', 'y', 'y_root']
You clicked at 63 36.
事件模式
在前面的例子中,你看到了我们如何使用事件 <Button-1> 来表示鼠标的左键点击。这是 Tkinter 中的一个内置模式,将其映射到鼠标的左键点击事件。Tkinter 有一个详尽的映射方案,可以精确地识别此类事件。
这里有一些示例,以给你一个关于事件模式的概念:
| 事件模式 | 关联事件 |
|---|---|
<Button-1> |
鼠标左键点击 |
<KeyPress-B> |
按键 B 的键盘按下 |
<Alt-Control-KeyPress- KP_Delete> |
按下 Alt + Ctrl + Delete 的键盘 |
通常,映射模式具有以下形式:
<[event modifier-]...event type [-event detail]>
通常,事件模式将包括:
-
事件类型(必需):一些常见的事件类型包括
Button、ButtonRelease、KeyRelease、Keypress、FocusIn、FocusOut、Leave(鼠标离开小部件)和MouseWheel。对于事件类型的完整列表,请参阅附录 B 中的事件类型部分,快速参考表。 -
事件修饰符(可选):一些常见的事件修饰符包括
Alt、Any(如<Any-KeyPress>中使用)、Control、Double(如<Double-Button-1>表示左鼠标按钮的双击)、Lock和Shift。对于事件修饰符的完整列表,请参阅附录 B 中的事件修饰符部分,快速参考表。 -
事件细节(可选):鼠标事件细节通过数字
1表示左键点击,数字2表示右键点击。同样,每个键盘按键通过键字母本身(例如<KeyPress-B>中的 B)或使用缩写为keysym的键符号来表示。例如,键盘上的上箭头键由keysym值KP_Up表示。对于完整的keysym映射,请参阅附录 B 中的事件细节部分,快速参考表。

让我们来看一个关于小部件上event绑定的实际例子。(完整的示例代码请参考1.11.py)。以下是对常用event绑定的代码片段的修改,以供参考:
widget.bind("<Button-1>",callback) #bind widget to left mouse click
widget.bind("<Button-2>", callback) # bind to right mouse click
widget.bind("<Return>", callback)# bind to Return(Enter) Key
widget.bind("<FocusIn>", callback) #bind to Focus in Event
widget.bind("<KeyPress-A>", callback)# bind to keypress A
widget.bind("<KeyPress-Caps_Lock>", callback)# bind to CapsLockkeysym
widget.bind("<KeyPress-F1>", callback)# bind widget to F1 keysym
widget.bind("<KeyPress-KP_5>", callback)# bind to keypad number 5
widget.bind('<Motion>', callback) # bind to motion over widget
widget.bind("<Any-KeyPress>", callback) # bind to any keypress
而不是将事件绑定到特定的小部件,你也可以将其绑定到 Toplevel 窗口。语法保持不变,但现在你需要在根窗口的根实例上调用它,如root.bind()。
绑定级别
在上一节中,你看到了如何将事件绑定到小部件的实例。这可以称为实例级绑定。
然而,有时你可能需要将事件绑定到整个应用程序。在其他时候,你可能希望将事件绑定到特定类的小部件。Tkinter 为此提供了不同级别的绑定选项:
-
应用级绑定:应用级绑定将允许你在应用程序的所有窗口和控件中使用相同的绑定,只要应用程序中的任何一个窗口处于焦点状态。
应用级绑定的语法如下:
w.bind_all(event, callback)典型的使用模式如下:
root.bind_all('<F1>', show_help)在这里,应用级绑定意味着无论当前焦点下的哪个小部件被选中,只要应用程序处于活动焦点状态,按下F1键总是会触发
show_help回调函数。 -
类级绑定:你还可以在特定类级别绑定事件。这通常用于设置特定小部件类的所有实例的相同行为。
类级绑定的语法如下:
w.bind_class(className, event, callback)典型的使用模式如下:
myentry.bind_class('Entry', '<Control-V>', paste)在前面的示例中,所有输入小部件都将绑定到
<Control-V>事件,该事件将调用名为'paste (event)'的方法。
注意
事件传播
大多数键盘事件和鼠标事件在操作系统级别发生。它从事件源向上级联,直到找到一个具有相应绑定的事件窗口。事件传播不会在这里停止。它会向上传播,寻找其他小部件的其他绑定,直到达到根窗口。如果它达到了根窗口并且没有发现绑定,则该事件将被忽略。
处理特定小部件的变量
你需要与各种小部件一起使用变量。你可能需要一个字符串变量来跟踪用户输入到输入小部件或文本小部件中的内容。你很可能需要一个布尔变量来跟踪用户是否选中了复选框小部件。你需要整数变量来跟踪在 Spinbox 或滑块小部件中输入的值。
为了响应特定小部件变量的变化,Tkinter 提供了自己的变量类。你用来跟踪特定小部件值的变量必须从这个 Tkinter 变量类中派生。Tkinter 提供了一些常用的预定义变量。它们是 StringVar、IntVar、BooleanVar 和 DoubleVar。
你可以使用这些变量在回调函数内部捕获和操作变量值的更改。如果需要,你也可以定义自己的变量类型。
创建 Tkinter 变量很简单。你只需调用所需的构造函数:
mystring = StringVar()
ticked_yes = BooleanVar()
option1 = IntVar()
volume = DoubleVar()
变量创建后,你可以将其用作小部件选项,如下所示:
Entry(root, textvariable = mystring)
Checkbutton(root, text="Remember Me", variable=ticked_yes)
Radiobutton(root, text="Option1", variable=option1, value="option1") #radiobutton
Scale(root, label="Volume Control", variable=volume, from =0, to=10) # slider
此外,Tkinter 通过 set() 和 get() 方法提供对变量值的访问:
myvar.set("Wassup Dude") # setting value of variable
myvar.get() # Assessing the value of variable from say a callback
Tkinter 变量类的演示可以在代码文件 1.12.py 中找到。该代码生成一个类似于以下截图的窗口:

目标完成 - 简短回顾
在本课中,你学习了:
-
将简单小部件绑定到特定函数的
command绑定 -
使用
lambda函数,如果你需要处理参数 -
使用
widget.bind(event, callback)方法绑定键盘和鼠标事件到你的小部件,并在某些事件发生时调用回调函数 -
如何向回调传递额外的参数
-
如何使用
bind_all()和bind_class()将事件绑定到整个应用程序或特定类的小部件 -
如何使用 Tkinter 变量类设置和获取特定小部件变量的值
简而言之,你现在知道如何让你的 GUI 程序变得功能齐全!
分类英特尔
除了我们之前看到的 bind 方法之外,你可能会在某些情况下发现这两个与事件相关的选项很有用:
-
unbind:Tkinter 提供了unbind选项来撤销之前绑定的效果。语法如下:widget.unbind(event)以下是一些使用示例:
entry.unbind('<Alt-Shift-5>') root.unbind_all('<F1>') root.unbind_class('Entry', '<KeyPress-Del>') -
虚拟事件:Tkinter 还允许你创建自己的事件。你可以给这些虚拟事件起任何你想要的名字。
例如,假设你想创建一个名为
<<commit>>的新事件,该事件由F9键触发。要在给定的小部件上创建这个虚拟事件,请使用以下语法:widget.event_add('<<commit>>', '<F-9>')然后,你可以使用正常的
bind()方法将<<commit>>绑定到任何回调函数上:widget.bind('<<commit>>', callback)
其他与事件相关的方法列在附录 B 的其他与事件相关的方法部分,快速参考表。
现在你已经准备好使用 Tkinter 进行实际的应用程序开发,让我们花些时间探索 Tkinter 提供的几个自定义样式选项。我们还将查看一些与根窗口一起常用的配置选项。
做得有风格
到目前为止,我们一直依赖 Tkinter 为我们的小部件提供特定平台的基础样式。然而,你可以根据自己的需求指定小部件的样式,包括颜色、字体大小、边框宽度和浮雕效果。Tkinter 中可用的样式功能将在以下任务中进行简要介绍。
准备起飞
回想一下,我们可以在小部件实例化时指定小部件选项,如下所示:
mybutton = Button(parent, **configuration options)
或者,你也可以使用configure()函数来指定小部件选项:
mybutton.configure(**options)
样式选项也可以作为小部件的选项指定,无论是在实例化时还是在之后使用 configure 选项。
启动推进器
在样式的范畴内,我们将介绍如何将不同的颜色、字体、边框宽度、浮雕效果、光标和位图图标应用到我们的小部件上。我们还将在本节稍后查看一些根配置。
让我们先看看如何指定小部件的颜色选项。对于大多数小部件,你可以指定两种类型的颜色:
-
背景颜色
-
前景颜色
你可以使用红色、绿色和蓝色的比例使用十六进制颜色代码来指定颜色。常用的表示方式有#rgb(4 位)、#rrggbb(8 位)和#rrrgggbbb(12 位)。
例如,#fff是白色,#000000是黑色,#fff000000是红色。
或者,Tkinter 提供了标准颜色名称的映射。要查看预定义的颜色列表,请打开 Python 安装目录(在我的情况下,是C:\Python27\Tools\pynche)中的Tools文件夹内的名为pynche的程序。在程序中点击查看 | 颜色列表窗口。
接下来,指定字体最简单和最常见的方式是将它表示为一个元组。标准的表示方式如下:
widget.configure( font= 'font family, fontsize, optional style modifiers like bold, italic, underline and overstrike')
下面是一些示例,用于说明指定字体的方法:
widget.configure (font='Times, 8')
widget.configure (font = 'Helvetica 24 bold italic')
注意
如果你将 Tkinter 的尺寸设置为纯整数,则测量单位为像素。或者,Tkinter 接受四种其他测量单位:m(毫米)、c(厘米)、i(英寸)和 p(打印点,大约是 1/72")。
大多数 Tkinter 小部件的默认边框宽度为 2 像素。您可以通过明确指定来更改小部件的边框宽度,如下所示:
button.configure (borderwidth=5)
小部件的浮雕样式指的是小部件中最高和最低海拔之间的差异。Tkinter 提供了五种可能的浮雕样式:flat、raised、sunken、groove 和 ridge。
button.configure (relief='raised')
Tkinter 允许您在悬停在特定小部件上时更改鼠标光标的样式。这可以通过使用选项光标来完成,如下例所示:
button.configure (cursor='cross')
对于可用光标的完整列表,请参阅 附录 B 中的 可用光标列表 部分,快速参考表。
虽然您可以在每个小部件级别指定样式选项,但有时为每个小部件单独这样做可能会很繁琐。特定小部件的样式有几个缺点:
-
它将逻辑和展示混合在一个文件中,使得代码庞大且难以管理
-
任何样式上的更改都应应用于每个小部件单独
-
由于您为大量小部件指定相同的样式,这违反了有效编码的 不要重复自己(DRY)原则
幸运的是,Tkinter 现在提供了一种将展示与逻辑分离并指定称为外部“选项数据库”中的样式的方法。这仅仅是一个文本文件,您可以在其中指定通用样式选项。
一个典型的选项数据库文本文件可能看起来如下:
*font: Arial 10
*Label*font: Times 12 bold
*background: AntiqueWhite1
*Text*background: #454545
*Button*foreground:gray55
*Button*relief: raised
*Button*width: 3
这里的星号 (*) 符号表示特定的样式适用于给定小部件的所有实例。
这些条目放置在外部文本 (.txt) 文件中。要将此样式应用于特定的代码片段,您只需在代码早期使用 option_readfile() 调用即可,如下所示:
root.option_readfile('optionDB.txt')
既然我们已经讨论了样式选项,让我们以一些常用的根窗口选项的讨论来结束:
| 方法 | 描述 |
|---|
|
root.title("title of my program")
| 指定标题栏的标题 |
|---|
|
root.geometry('142x280+150+200')
您可以使用 widthxheight + xoffset + yoffset 形式的字符串来指定根窗口的大小和位置 |
|---|
|
self.root.wm_iconbitmap('mynewicon.ico')
或者
self.root.iconbitmap('mynewicon.ico ')
| 将标题栏图标更改为与默认 Tk 图标不同的图标 |
|---|
|
root.overrideredirect(1)
| 移除根边框框架 |
|---|
现在让我们看看一个例子,其中我们应用了之前讨论的所有样式选项和根窗口选项(参见代码 01.13.py):
from Tkinter import *
root = Tk()
#demo of some important root methods
root.geometry('142x280+150+200') #specify root window size and position
root.title("Style Demo") #specifying title of the program
self.root.wm_iconbitmap('brush1.ico')#changing the default icon
#root.overrideredirect(1) # remove the root border - uncomment #this line to see the difference
root.configure(background='#4D4D4D')#top level styling
# connecting to the external styling optionDB.txt
root.option_readfile('optionDB.txt')
#widget specific styling
mytext = Text(root, background='#101010', foreground="#D6D6D6", borderwidth=18, relief='sunken', width=16, height=5 )
mytext.insert(END, "Style is knowing \nwho you are, what \nyou want to say, \nand not giving a \ndamn.")
mytext.grid(row=0, column=0, columnspan=6, padx=5, pady=5)
# all the below widgets derive their styling from optionDB.txt file
Button(root, text='*' ).grid(row=1, column=1)
Button(root, text='^' ).grid(row=1, column=2)
Button(root, text='#' ).grid(row=1, column=3)
Button(root, text='<' ).grid(row=2, column=1)
Button(root, text='OK', cursor='target').grid(row=2, column=2)
Button(root, text='>').grid(row=2, column=3)
Button(root, text='+' ).grid(row=3, column=1)
Button(root, text='v', font='Verdana 8').grid(row=3, column=2)
Button(root, text='-' ).grid(row=3, column=3)
fori in range(0,10,1):
Button(root, text=str(i) ).grid( column=3 if i%3==0 else (1 if i%3==1 else 2), row= 4 if i<=3 else (5 if i<=6 else 6))
#styling with built-in bitmap images
mybitmaps = ['info', 'error', 'hourglass', 'questhead', 'question', 'warning']
for i in mybitmaps:
Button(root, bitmap=i, width=20,height=20).grid(row=(mybitmaps.index(i)+1), column=4,sticky='nw')
root.mainloop()
上述代码的描述如下:
-
代码的第一部分使用一些重要的根方法来定义几何形状、程序标题、程序图标以及移除根窗口边框的方法。
-
代码随后连接到一个名为
optionDB.txt的外部样式文件,该文件定义了小部件的通用样式。 -
下一部分代码创建了一个 Text 小部件,并在小部件级别指定了样式。
-
下一段代码有几个按钮,所有这些按钮的风格都源自集中的
optionDb.txt文件。其中一个按钮还定义了一个自定义光标。 -
代码的最后一段使用内置的位图图像样式了一些按钮。
运行这个程序会产生如下截图所示的窗口:

目标完成 - 简短总结
在这个任务中,我们探讨了如何使用样式选项来修改 Tkinter 的默认样式。我们看到了如何为我们的 GUI 程序指定自定义颜色、字体、浮雕效果和光标。我们还看到了如何使用选项数据库将样式与逻辑分离。最后,我们探索了一些配置我们的根窗口的常见选项。
任务完成
这使我们到达了项目 1,“认识 Tkinter”的结尾。该项目旨在提供一个 Tkinter 的高级概述。我们已经逐步了解了驱动 Tkinter 程序的所有重要概念。现在我们知道了:
-
根窗口是什么以及如何设置它
-
21 个核心 Tkinter 小部件是什么以及如何设置它们
-
如何使用
pack、grid和place布局管理器来布局我们的程序 -
如何使用事件和回调使我们的程序功能化
-
如何将自定义样式应用到我们的 GUI 程序中
总结一下,我们现在可以开始考虑使用 Tkinter 制作有趣、功能性强且风格独特的 GUI 程序了!
热身挑战
是时候接受你的第一个 Hotshot 挑战了!你的任务是构建一个简单的计算器(如果你有雄心,可以是一个科学计算器)。它应该是完全功能性的,并且应该有自定义样式的按钮和屏幕。尽量让它看起来尽可能接近真实的物理计算器。
当你完成时,我们邀请你搜索你电脑上的复杂 GUI 程序。这些可以从你的操作系统程序,如搜索栏,到一些基于简单对话框的小部件。尝试使用 Tkinter 复制任何选定的 GUI。
第二章.制作类似记事本的文本编辑器
在上一个项目中,我们得到了 Tkinter 的一个相当高级的概述。现在,我们了解了一些关于 Tkinter 核心小部件、几何管理和将命令和事件绑定到回调函数的知识,让我们将这些技能应用到本项目中的文本编辑器制作中。
在此过程中,我们还将更深入地研究各个小部件,并学习如何调整这些小部件以满足我们的定制需求。
任务简报
在这个项目中,我们的目标是构建一个功能齐全的文本编辑器,并包含一些酷炫的功能。在其最终形态下,所提出的编辑器应该看起来如下:

我们打算在记事本中包含以下功能:
-
创建新文档、打开和编辑现有文档以及保存文档
-
实现常见的编辑选项,如剪切、复制、粘贴、撤销和重做
-
在文件中搜索给定的搜索词
-
实现行号和显示/隐藏行号的能力
-
实现主题选择,让用户选择自定义颜色主题
-
实现关于和帮助窗口等更多功能
为什么它很棒?
在这个项目中,你将构建你的第一个真正有用的项目。这个项目将帮助你更深入地了解 Tkinter 的世界。它将深入探讨一些常用小部件的功能,例如菜单、菜单按钮、文本、输入框、复选框和按钮小部件。
尤其是我们将深入了解菜单、菜单栏和文本小部件的细节。我们还将学习如何轻松处理自定义对话框,如打开、保存、错误、警告和信息对话框。
你的热辣目标
该项目将分七个连续迭代进行开发。每个迭代的目的是如下:
-
使用
pack几何布局和菜单、菜单栏、文本、输入框、按钮、复选框等小部件设置用户界面 -
使用 Tkinter 内置的小部件选项实现一些功能
-
使用
ttk对话框和不同类型的 Toplevel 小部件实现对话框 -
应用一些文本小部件功能,如文本索引、标签和标记,以实现一些自定义功能
-
使用复选框和单选按钮小部件应用一些功能
-
应用一些自定义事件绑定和协议绑定,使应用程序更易于使用
-
添加一些杂项功能
设置小部件
我们的首要目标是实现文本编辑器的视觉元素。作为程序员,我们所有人都使用过记事本或某些代码编辑器来编辑我们的代码。我们对文本编辑器的常见 GUI 元素大多有所了解。因此,无需过多介绍,让我们开始吧。
准备发射
第一阶段实现了以下六个小部件:
-
菜单
-
菜单按钮
-
标签
-
按钮
-
文本
-
滚动条
虽然我们将详细涵盖所有这些内容,但你可能会发现查看 Tkinter 作者 Frederick Lundh 维护的文档中的小部件特定选项很有帮助。effbot.org/tkinterbook/
您可能还想将位于www.tcl.tk/man/tcl8.5/TkCmd/contents.htm的 Tck/Tk 官方文档页面添加到书签。
后者网站包括原始 Tcl/Tk 参考。虽然它与 Python 无关,但它提供了每个小部件的更详细概述,同样是一个有用的参考。(记住,Tkinter 只是 Tk 的包装器)
您也可以通过在交互式 Python shell 中输入以下两行来阅读 Tkinter 原始源代码提供的文档:
>>> import Tkinter
>>>help(Tkinter)
启动推进器
在这个迭代中,我们将完成程序大多数视觉元素的实现。
注意
我们将使用pack()布局管理器来放置所有小部件。我们选择pack管理器,因为它非常适合放置小部件并排或自上而下排列。幸运的是,在文本编辑器中,所有小部件都放置在并排或自上而下的位置。因此,使用pack管理器是合适的。我们也可以使用grid管理器做到同样的事情。
-
首先,我们将从添加 Toplevel 窗口开始,该窗口将包含所有其他小部件,使用以下代码:
from Tkinter import * root = Tk() # all our code is entered here root.mainloop() -
在这一步中,我们将向我们的代码中添加顶级菜单按钮。请参阅
2.01.py中的代码。菜单提供了一种非常紧凑的方式来向用户展示大量选择,而不会使界面杂乱。Tkinter 提供了两个小部件来处理菜单。-
菜单按钮小部件——它是菜单的一部分,出现在应用程序的顶部,始终对最终用户可见
-
菜单小部件——当用户点击任何菜单按钮时显示选择列表的小部件
![启动推进器]()
要添加顶级菜单按钮,您可以使用以下命令:
mymenu = Menu(parent, **options)例如,要添加文件菜单,我们使用以下代码:
# Adding Menubar in the widget menubar = Menu(root) filemenu = Menu(menubar, tearoff=0 ) # File menu root.config(menu=menubar) # this line actually displays menu类似地,我们在顶部添加了编辑、视图和关于菜单。请参考
2.01.py的第 2 步。大多数 Linux 平台支持可撕菜单。当
tearoff设置为1(启用)时,菜单选项上方会出现一条虚线。点击虚线允许用户实际上撕下或分离菜单与顶部的连接。然而,由于这不是一个跨平台特性,我们决定禁用撕下功能,将其标记为tearoff = 0。 -
-
现在,我们将向每个四个菜单按钮中添加菜单项。如前所述,所有下拉选项都应添加在菜单实例中。在我们的示例中,我们在文件菜单中添加了五个下拉菜单选择,即新建、打开、保存、另存为和退出菜单项。请参阅
2.02.py的第 3 步。类似地,我们为其他菜单添加以下菜单选择:
-
在编辑下,我们有撤销、重做、剪切、复制、粘贴、查找全部和全选
-
在视图下,我们有显示行号、在底部显示信息栏、高亮当前行和主题
-
在关于下,我们有关于和帮助
添加菜单项的格式如下:
mymenu.add_command(label="Mylabel", accelerator='KeyBoard Shortcut', compound=LEFT, image=myimage, underline=0, command=callback)例如,您可以使用以下语法创建撤销菜单项:
mymenu.add_command(label="Undo", accelerator='Ctrl + Z', compound=LEFT, image=undoimage, command=undocallback)![启动推进器]()
-
-
接下来我们将添加一些标签。我们将添加顶部标签,它将后来包含快捷按钮。我们还将添加一个标签到左侧以显示行号:
![启动推进器]()
为了说明目的,顶部标签被标记为绿色背景,侧标签被标记为浅奶油色背景。
注意
当使用
pack布局管理器时,按小部件将出现的顺序添加小部件非常重要。这是因为pack()使用可用空间的概念来适应小部件。如果我们不保持顺序,小部件将按照它们被引入的顺序开始占用位置。这就是为什么我们不能在两个标签小部件之前引入文本小部件,因为它们在显示中位置更高。保留空间后,我们可以在保持标签作为父小部件的同时添加快捷图标或行号。添加标签很容易,我们以前已经这样做过了。请参阅
2.02.py步骤 4 中的代码。代码如下:shortcutbar = Frame(root, height=25, bg='light sea green') shortcutbar.pack(expand=NO, fill=X) lnlabel = Label(root, width=2, bg = 'antique white') lnlabel.pack(side=LEFT, anchor='nw', fill=Y)目前,我们已为这两个标签应用了彩色背景,以区分 Toplevel 窗口的主体。
-
最后,让我们将文本小部件和滚动条小部件添加到我们的代码中。请参考
2.02.py代码的步骤 5。textPad = Text(root) textPad.pack(expand=YES, fill=BOTH) scroll=Scrollbar(textPad) textPad.configure(yscrollcommand=scroll.set) scroll.config(command=textPad.yview) scroll.pack(side=RIGHT, fill=Y)代码与我们迄今为止使用过的所有其他代码类似,用于实例化小部件。请注意,然而,滚动条被配置为 Text 小部件的
yview,而 Text 小部件被配置为连接到滚动条小部件。这样,我们就将两个小部件相互交叉连接。现在当你向下滚动文本小部件时,滚动条会做出反应。或者,当你拉动滚动条时,文本小部件也会做出相应的反应。
这里引入了一些新的菜单特定选项,如下所示:
-
加速器: 此选项用于指定一个字符串,通常是键盘快捷键,可以用来调用菜单。作为加速器指定的字符串将出现在菜单项文本旁边。请注意,这不会自动创建键盘快捷键的绑定。我们稍后将手动设置它们。 -
compound: 将compound选项指定给菜单项可以让您在菜单的常见文本标签旁边添加图像。例如,Compound=LEFT, label= 'mytext', image=myimage的指定意味着菜单项有一个复合标签,由文本标签和图像组成,其中图像将放置在文本的左侧。我们在这里使用的图像存储和引用自一个名为icons的单独文件夹。 -
underline:underline选项允许您指定菜单文本中字符的索引以进行下划线。索引从 0 开始,这意味着指定underline=1将下划线文本的第二个字符。除了下划线外,Tkinter 还使用它来定义菜单键盘遍历的默认绑定。这意味着我们可以用鼠标指针或使用Alt +<下划线字符索引处的字符>快捷键来选择菜单。
因此,要在文件菜单中添加新菜单项,我们使用以下代码:
filemenu.add_command(label="New", accelerator='Ctrl+N', compound=LEFT, image=newicon, underline=0, command=new_file)
同样,我们为编辑菜单添加菜单选择项。
注意
菜单分隔符
在您的菜单项中,您可能会遇到mymenu.add_separator()之类的代码。此小部件显示一个分隔条,仅用于将相似的菜单项分组,通过水平条分隔组。
除了我们为新和编辑菜单实现的常规菜单类型外,Tkinter 还提供了三种其他类型的菜单:
-
复选框菜单:此菜单允许您通过勾选/取消勾选菜单来做出是/否的选择
-
单选按钮菜单:此菜单允许您从许多不同的选项中选择一个
-
级联菜单:此菜单仅展开以显示另一组选择
我们的查看菜单展示了以下截图中的这三种类型的菜单:

查看菜单下的前三个选项允许用户选择是否希望发生某些操作。用户可以检查/取消检查这些菜单中的选项,它们是复选框菜单的示例。
查看菜单下的第四个菜单项为主题。将鼠标悬停在此菜单上会打开另一组选择。这是一个级联菜单的示例,因为它仅用于打开另一组选择。
在级联菜单中,您将看到一组用于编辑主题的选择。然而,您只能选择其中一个主题。选择一个主题将取消任何之前的选中状态。这是一个单选按钮菜单的示例。
添加这三种类型菜单的示例格式如下:
viewmenu.add_checkbutton(label="Show Line Number", variable=showln)
viewmenu.add_cascade(label="Themes", menu=themesmenu)
themesmenu.add_radiobutton(label="Default White", variable=theme)
现在我们需要跟踪是否已做出选择,我们通过添加一个变量来跟踪,该变量可以是BooleanVar()、IntVar()或StringVar(),正如我们在项目 1 中讨论的那样,认识 Tkinter。
对于 Menubutton 和 Menu 小部件的完整配置选项列表,请参阅附录 B 中的基本小部件方法部分,快速参考表。
目标完成 - 简短总结
这标志着我们的第一次迭代结束。在本迭代中,我们已经完成了文本编辑器大多数视觉元素的布局。
利用内置 Text 小部件选项的强大功能
Tkinter 的 Text 小部件自带一些方便的内置功能来处理常见的文本相关功能。让我们利用这些功能来实现我们文本编辑器中的常见功能。
启动推进器
-
让我们首先实现剪切、复制和粘贴功能。我们现在已经准备好了编辑器 GUI。如果你打开程序并玩 Text 小部件,你会注意到你可以在文本区域使用键盘快捷键Ctrl + X、Ctrl + C和Ctrl + V执行基本功能,如剪切、复制和粘贴。所有这些功能都存在,我们不必为这些功能添加任何代码。
显然,文本小部件内置了这些事件。我们不必自己编写这些函数,而是使用内置函数将这些功能添加到我们的文本编辑器中。
Tcl/Tk "通用小部件方法"的文档告诉我们,我们可以使用以下命令来触发事件,而无需任何外部刺激:
widget.event_generate(sequence, **kw)要触发
textPad小部件的剪切事件,我们只需要一行代码,如下所示:textPad.event_generate("<<Cut>>")让我们称它为使用一个函数 cut,并将其与我们的 cut 菜单使用命令回调关联起来。参见包含以下代码的
2.03.py代码:def cut(): textPad.event_generate("<<Cut>>") # then define a command callback from our existing cut menu like: editmenu.add_command(label="Cut", compound=LEFT, image=cuticon, accelerator='Ctrl+X', command=cut)同样,我们从各自的菜单项触发复制和粘贴功能。
-
接下来,我们将继续实现撤销和重做功能。Tcl/Tk 文本文档告诉我们,Text 小部件具有无限撤销和重做机制,前提是我们将
-undo选项设置为true。为了利用此选项,让我们首先将 Text 小部件的undo选项设置为true,如下面的截图所示:textPad = Text(root, undo=True)现在如果你打开你的文本编辑器并尝试使用Ctrl + Z和Ctrl + Y来尝试撤销和重做功能,你会看到它们工作得很好。我们现在只需要将事件与函数关联起来,并分别从我们的撤销和重做菜单回调函数。这与我们对剪切、复制和粘贴所做的是类似的。请参阅
2.03.py中的代码。
目标完成 - 简报
利用一些内置 Text 小部件选项,我们已成功地将剪切、复制、粘贴、撤销和重做功能实现到我们的文本编辑器中,而代码量最小化。
索引和标记
虽然我们成功地利用了一些内置功能来获得快速的优势,但我们需要对文本区域有更精确的控制,以便按照我们的意愿弯曲它。这将需要能够以精确的方式定位文本中的每个字符或位置。
准备起飞
Text 小部件为我们提供了使用索引、标记和标记来操作其内容的能力,这使得我们可以定位文本区域中的位置或位置进行操作。
索引
索引可以帮助你定位文本中的特定位置。例如,如果你想以粗体样式或红色或不同的字体大小标记一个特定的单词,如果你知道起始点和目标结束点的索引,你可以这样做。
索引必须指定在以下格式之一中:
| 索引格式 | 描述 |
|---|---|
x.y |
第 x 行的第 y 个字符。 |
@x,y |
在文本窗口内覆盖 x、y 坐标的字符。 |
end |
文本的末尾。 |
mark |
命名标记后的字符。 |
tag.first |
文本中带有给定标签的第一个字符。 |
tag.last |
文本中带有给定标签的最后一个字符。 |
selection (SEL_FIRST, SEL_LAST) |
这对应于当前选择。常量 SEL_FIRST 和 SEL_LAST 分别指选择中的起始位置和结束位置。如果没有任何选择,Tkinter 会引发 TclError 异常。 |
windowname |
嵌入窗口的名称为 windowname 的位置。 |
imagename |
嵌入图像的名称为 imageName 的位置。 |
INSERT |
插入光标的位置。 |
CURRENT |
鼠标指针最近的位置的字符。 |
索引可以使用修饰符和子修饰符进一步操作。以下是一些修饰符和子修饰符的示例:
-
end - 1 chars或end - 1 c指的是末尾前一个字符的索引 -
insert +5lines指的是插入光标前方五行的索引 -
insertwordstart - 1 c指的是包含插入光标的第一字的字符之前 -
end linestart指的是末尾行起始的索引
索引通常用作函数的参数。例如,参考以下列表:
-
text.delete(1.0,END): 这意味着您可以从第 1 行第 0 列删除到末尾 -
text.get(0.0, END): 这将获取从 0.0 到末尾的内容 -
text.delete(insert-1c, INSERT): 这将在插入光标处删除一个字符
标签
标签用于使用识别字符串注释文本,然后可以使用该字符串来操作标记的文本。Tkinter 有一个内置的标签称为 SEL,它自动应用于所选文本。除了 SEL 之外,您还可以定义自己的标签。文本范围可以与多个标签相关联,相同的标签可以用于许多不同的文本范围。
以下是一些标记的示例:
mytext.tag_add('sel', '1.0', 'end') # add SEL tag from start(1.0) to end
mytext.tag_add("danger", "insert linestart", "insert lineend+1c")
mytext.tag_remove("danger", 1.0, "end")
mytext.tag_config('danger', background=red)
mytext.tag_config('outdated', overstrike=1)
注意
您可以使用 tag_config 选项指定给定标签的视觉样式,例如 background(color)、bgstipple (bitmap)、borderwidth (distance)、fgstipple (bitmap)、font (font)、foreground (color)、justify (constant)、lmargin1 (distance)、lmargin2 (distance)、offset (distance)、overstrike (flag)、relief (constant)、rmargin (distance)、spacing1 (distance)、tabs (string)、underline (flag) 和 wrap (constant)。
要获取文本索引和标记的完整参考,请在您的 Python 交互式外壳中输入以下命令:
>>> import Tkinter
>>> help(Tkinter.Text)
启动推进器
在掌握了基本的索引和标记知识后,让我们在我们的代码编辑器中实现一些更多功能。
-
我们将要实现的第一项功能是“全选”功能。我们知道 Tkinter 有一个内置的
SEL标签,它将选择应用于给定的文本范围。我们希望将此sel标签应用于我们小部件中包含的完整文本。我们只需定义一个函数来处理这种情况。请参考以下代码片段中的
2.04.py代码:def select_all(): textPad.tag_add('sel', '1.0', 'end')在此之后,我们向我们的全选菜单项添加了一个回调:
editmenu.add_command(label="Select All", underline=7, accelerator='Ctrl+A', command=select_all)现在,我们已经完成了向我们的代码编辑器添加全选功能。如果你现在向文本小部件中添加一些文本,然后点击菜单项全选,它应该选择编辑器中的全部文本。请注意,我们没有在菜单选项中绑定Ctrl + A快捷键。因此,键盘快捷键将不起作用。我们将在单独的步骤中实现
accelerator函数。 -
接下来,让我们完成查找菜单项的功能。
![Engage Thrusters]()
这里是一个快速的功能总结。当用户点击查找菜单项时,会打开一个新的 Toplevel 窗口。用户输入一个搜索关键字,并指定搜索是否区分大小写。当用户点击查找全部按钮时,所有匹配项都会被突出显示。
对于在文档中搜索,我们将依赖于
text.search()方法。search方法接受以下参数:search(pattern, startindex, stopindex=None, forwards=None, backwards=None, exact=None, regexp=None, nocase=None, count=None)对于我们的编辑器,我们定义了一个名为
on_find的函数,并将其作为回调附加到我们的查找菜单项(请参考2.04.py中的代码):editmenu.add_command(label="Find", underline= 0, accelerator='Ctrl+F', command=on_find)然后,我们定义我们的函数
on_find如下(请参考2.04.py中的代码):def on_find(): t2 = Toplevel(root) t2.title('Find') t2.geometry('262x65+200+250') t2.transient(root) Label(t2, text="Find All:").grid(row=0, column=0, sticky='e') v=StringVar() e = Entry(t2, width=25, textvariable=v) e.grid(row=0, column=1, padx=2, pady=2, sticky='we') e.focus_set() c=IntVar() Checkbutton(t2, text='Ignore Case', variable=c).grid(row=1, column=1, sticky='e', padx=2, pady=2) Button(t2, text="Find All", underline=0, command=lambda: search_for(v.get(), c.get(), textPad, t2, e)).grid(row=0, column=2, sticky='e'+'w', padx=2, pady=2) def close_search(): textPad.tag_remove('match', '1.0', END) t2.destroy() t2.protocol('WM_DELETE_WINDOW', close_search)#override close上一段代码的描述如下:
-
当用户点击查找菜单项时,它调用一个回调
on_find。 -
on_find()函数的前四行创建了一个新的 Toplevel 窗口,添加了一个标题查找,指定了它的几何形状(大小、形状和位置),并将其设置为临时窗口。将其设置为临时窗口意味着它始终位于其父窗口或根窗口之上。如果你取消注释此行并点击根编辑窗口,查找窗口将位于根窗口之后。 -
以下八行代码相当直观,它们设置了查找窗口的控件。它添加了 Label、Entry、Button 和 Checkbutton 控件,并为两个变量
e和c提供了跟踪用户输入到 Entry 控件中的值以及用户是否检查了复选框的功能。这些控件使用grid几何管理器排列,以适应查找窗口。 -
查找全部按钮有一个
command选项,它调用一个函数search_for(),将搜索字符串作为第一个参数传递,并将搜索是否区分大小写作为第二个参数传递。第三个、第四个和第五个参数将 Toplevel 窗口、Text 小部件和 Entry 小部件作为参数传递。 -
在
search_for()方法之前,我们覆盖了查找窗口的关闭按钮,并将其重定向到名为close_search()的回调。close_search()方法定义在on_find()函数中。此函数负责删除在搜索过程中添加的match标签。如果我们不覆盖关闭按钮并删除这些标签,即使我们的搜索已经结束,匹配的字符串也会继续用红色和黄色标记。
-
-
接下来,我们有一个
search_for()函数,它执行实际的搜索。代码如下:def search_for(needle, cssnstv, textPad, t2, e) : textPad.tag_remove('match', '1.0', END) count =0 if needle: pos = '1.0' while True: pos = textPad.search(needle, pos, nocase=cssnstv,stopindex=END) if not pos: break lastpos = '%s+%dc' % (pos, len(needle)) textPad.tag_add('match', pos, lastpos) count += 1 pos = lastpos textPad.tag_config('match', foreground='red',background='yellow') e.focus_set() t2.title('%d matches found' %count)代码的描述如下:
-
这段代码是搜索功能的核心。它使用
while True循环遍历整个文档,只有当没有更多文本项需要搜索时才会退出循环。 -
代码首先删除任何先前的搜索相关
match标签,因为我们不希望将新搜索的结果附加到先前的搜索结果上。该函数使用 Tkinter 在 Text 小部件上提供的search()方法。search()函数接受以下参数:search(pattern, index, stopindex=None, forwards=None, backwards=None, exact=None, regexp=None, nocase=None, count=None)该方法返回第一个匹配项的起始位置。我们将其存储在一个名为
pos的变量中,并计算匹配单词中最后一个字符的位置,并将其存储在变量lastpos中。 -
对于它找到的每个搜索匹配项,它都会在从第一个位置到最后一个位置的文本范围内添加一个名为
match的标签。每次匹配后,我们将pos的值设置为等于lastpos。这确保了下一次搜索从lastpos之后开始。 -
循环还使用
count变量跟踪匹配的数量。 -
在循环外部,
match标签被配置为红色字体颜色,背景为黄色。此函数的最后一行更新查找窗口的标题,显示找到的匹配数量。
注意
在事件绑定的情况下,您的输入设备(键盘/鼠标)与您的应用程序之间发生交互。除了事件绑定之外,Tkinter 还支持协议处理。
“协议”一词指的是您的应用程序与窗口管理器之间的交互。一个协议的例子是
WM_DELETE_WINDOW,它处理窗口管理器的close窗口事件。Tkinter 允许您通过指定根或 Toplevel 小部件的自己的处理程序来覆盖这些协议处理程序。要覆盖我们的窗口退出协议,我们使用以下命令:root.protocol("WM_DELETE_WINDOW", callback)一旦添加此命令,Tkinter 就会绕过您指定的回调/处理程序进行协议处理。
-
目标完成 - 简报
恭喜!在这个迭代中,我们已经将全选和查找功能编码到我们的程序中。
更重要的是,我们介绍了索引和标签——这两个与许多 Tkinter 小部件相关的非常强大的概念。您将在您的项目中经常使用这两个概念。
分类智能
在之前的代码中,我们使用了一条读取为t2.transient(root)的行。让我们来理解这里的含义。
Tkinter 支持四种类型的 Toplevel 窗口:
-
主 Toplevel 窗口:这是我们迄今为止构建的窗口。
-
子 Toplevel 窗口:这些是独立于根的窗口。子 Toplevel 窗口独立于其根窗口,但如果其父窗口被销毁,它也会被销毁。
-
瞬态 Toplevel 窗口:这个窗口始终位于其父窗口之上。如果父窗口最小化,瞬态窗口将被隐藏;如果父窗口被销毁,瞬态窗口也将被销毁。
-
未装饰的 Toplevel 窗口:如果一个 Toplevel 窗口周围没有窗口管理器装饰,则称为未装饰的。它通过将
overrideredirect标志设置为1来创建。未装饰的窗口不能调整大小或移动。
请查看2.05.py中的代码,以演示这四种类型的 Toplevel 窗口。
与表单和对话框一起工作
本次迭代的目的是完成文件菜单中的打开、保存和另存为选项的功能。
准备起飞
我们经常使用打开和保存对话框。它们在许多程序中都很常见。我们知道这些菜单项的行为。例如,当你点击打开菜单时,它会打开一个对话框,让你导航到你想要打开的文件的位置。当你选择一个特定的文件并点击打开时,它会在你的编辑器中打开。同样,我们还有保存对话框。
虽然我们可以使用标准的 Tkinter 小部件实现这些对话框,但它们被如此频繁地使用,以至于一个名为tkFileDialog的特定 Tkinter 模块已被包含在标准的 Python 发行版中。我们不会试图重新发明轮子,并且本着少编码的精神,我们将使用tkFileDialog模块来实现文本编辑器的打开和保存功能,如下面的截图所示:

要使用该模块,我们只需将其导入当前命名空间,如2.06.py代码文件中所示:
import tkFileDialog
您可以为tkFileDialog指定以下附加选项:
| 文件对话框 | 可配置选项 | 描述 |
|---|---|---|
askopenfile(mode='r', **options) |
parent, title, message, defaultextension, filetypes, initialdir, initialfile, 和 multiple |
询问要打开的文件名,然后返回打开的文件 |
askopenfilename(**options) |
parent, title, message, defaultextension, filetypes, initialdir, initialfile, 和 multiple |
询问要打开的文件名,但不返回任何内容 |
asksaveasfile(mode='w', **options) |
parent, title, message, defaultextension, filetypes, initialdir, initialfile, 和 multiple |
询问要保存的文件名,并返回打开的文件 |
asksaveasfilename(**options**) |
parent, title, message, defaultextension, filetypes, initialdir, initialfile, 和 multiple |
询问要保存的文件名,但返回空值 |
askdirectory(**options**) |
parent, title, initialdir, must exist |
询问目录,并返回文件名 |
然后,我们启动推进器。
-
让我们现在使用
tkDialogBox(参考代码2.07.py)开发我们的打开函数:import tkFileDialog import os def open_file(): global filename filename = tkFileDialog.askopenfilename(defaultextension=".txt",filetypes =[("All Files","*.*"),("Text Documents","*.txt")]) if filename == "": # If no file chosen. filename = None # Absence of file. else: root.title(os.path.basename(filename) + " - pyPad") # #Returning the basename of 'file' textPad.delete(1.0,END) fh = open(filename,"r") textPad.insert(1.0,fh.read()) fh.close()然后,我们修改打开菜单,为此新定义的方法添加一个
command回调。filemenu.add_command(label="Open", accelerator='Ctrl+O', compound=LEFT, image=openicon, underline =0, command=open_file)代码的描述如下:
-
我们将
tkfileDialog和os模块导入当前命名空间。 -
我们定义了我们的函数
open_file()。 -
我们在全局范围内声明一个变量来跟踪打开文件的文件名。这是为了跟踪文件是否已被打开。我们需要这个变量在全局范围内,因为我们希望这个变量对其他方法如
save()和save_as()可用。如果不指定为全局,则意味着它仅在函数内部可用。所以我们的save()和save_as()函数将无法检查编辑器中是否已打开文件。 -
我们使用
tkFileDialog.askopenfilename获取打开文件的文件名。如果用户取消打开文件或没有选择文件,返回的文件名是None。在这种情况下,我们不做任何事情。 -
然而,如果
tkFileDialog返回一个有效的文件名,我们使用os模块隔离文件名,并将其添加为我们根窗口的标题。 -
如果文本小部件已经包含一些之前的文本,我们将其全部删除。
-
然后,我们以读取模式打开指定的文件,并将所有内容插入到文本区域中。
-
然后,我们关闭文件句柄
fh。 -
最后,我们向我们的文件 | 打开菜单项添加一个
command回调。
这完成了文件 | 打开的编码。如果你现在去点击文件 | 打开,选择一个文本文件并点击打开,文本区域将填充文本文件的内容。
注意
通常认为使用全局变量是一种不良的编程实践,因为它很难理解使用大量全局变量的程序。
全局变量可以在程序中的许多不同地方修改或访问,因此很难记住或确定变量的所有可能用途。
全局变量不受任何访问控制的约束,这可能在某些情况下造成安全风险,例如当此程序与第三方代码交互时。
然而,当你像这样在程序中以过程式风格工作时,全局变量有时是不可避免的。
编程的另一种方法是编写类结构中的代码(也称为面向对象编程),其中变量只能由预定义类的成员访问。在下一个项目中,我们将看到许多面向对象编程的例子。
-
-
接下来,我们将看到如何保存文件。保存文件有两个组件:
-
保存文件
-
另存为
如果文本区域已经包含文件,我们不会提示用户输入文件名。我们只是简单地覆盖现有文件的内容。如果文本区域的当前内容没有关联的文件名,我们将使用 另存为 对话框提示用户。此外,如果文本区域有一个打开的文件,并且用户点击 另存为,我们仍然会提示他们使用 另存为 对话框,以便他们可以将内容写入不同的文件名。
保存和另存为的代码如下(请参阅
2.07.py中的代码):#Defining save method def save(): global filename try: f = open(filename, 'w') letter = textPad.get(1.0, 'end') f.write(letter) f.close() except: save_as() #Defining save_as method def save_as(): try: # Getting a filename to save the file. f = tkFileDialog.asksaveasfilename(initialfile = 'Untitled.txt', defaultextension=".txt",filetypes=[("All Files","*.*"),("Text Documents","*.txt")]) fh = open(f, 'w') textoutput = textPad.get(1.0, END) fh.write(textoutput) fh.close() root.title(os.path.basename(f) + " - pyPad") except: pass filemenu.add_command(label="Save", accelerator='Ctrl+S', compound=LEFT, image=saveicon, underline=0, command=save) filemenu.add_command(label="Save as", accelerator='Shift+Ctrl+S', command=save_as)代码描述如下:
-
save函数首先尝试使用try块定位文本区域是否已打开文件。如果文件已打开,它将简单地用文本区域的当前内容覆盖文件内容。 -
如果文本区域没有关联的文件名,它将简单地将工作传递给我们的
save_as函数。 -
save_as函数使用tkFileDialog.asksaveasfilename打开一个对话框,并尝试获取用户为给定文件提供的文件名。如果成功,它将以写入模式打开新文件,并将文本内容写入此新文件名。写入后,它关闭当前文件句柄,并将窗口标题更改为反映新文件名。 -
为了获取新的文件名,我们的
save_as函数使用了os模块。因此,在我们能够使用它来提取当前文件名之前,我们需要将os模块导入到我们的命名空间中。 -
如果用户没有指定文件名或用户取消
save_as操作,它将简单地使用pass命令忽略该过程。 -
最后,我们向现有的 保存 和 另存为 菜单项添加了一个
command回调,以调用这两个函数。
现在我们已经完成了向代码编辑器添加保存和另存为功能。
-
-
在此同时,让我们完成 文件 | 新建 的功能。代码很简单。为此,请参阅
2.07.py中的代码:def new_file(): root.title("Untitled") global filename filename = None textPad.delete(1.0,END) filemenu.add_command(label="New", accelerator='Ctrl+N', compound=LEFT, image=newicon, underline=0, command=new_file )此代码的描述如下:
-
new_file函数首先将根窗口的title属性更改为Untitled。 -
然后它将全局变量
filename的值设置为None。这很重要,因为我们的save和save_As功能使用这个全局变量名来跟踪文件是否存在或是否为新文件。 -
然后我们的函数删除了 Text 小部件的所有内容,在其位置创建了一个新的文档。
-
最后,我们向 文件 | 新建 菜单项添加了一个
command回调函数。
这完成了我们将代码编辑器中的 文件 | 新建 的编码。
-
目标完成 - 简报
在这次迭代中,我们完成了编辑器下 文件 菜单中 新建、打开、保存 和 另存为 子菜单的功能编码。
更重要的是,我们看到了如何使用tkFileDialog模块来实现程序中的某些常用功能。我们还看到了如何使用索引来实现我们程序的各种任务。
与消息框一起工作
在这个迭代中,让我们完成关于和帮助菜单的代码。功能很简单。当用户点击帮助或关于菜单时,它会弹出一个消息窗口并等待用户通过点击按钮进行响应。虽然我们可以轻松地编写新的 Toplevel 窗口来显示我们的关于和帮助弹出窗口,但我们将使用一个名为tkMessageBox的模块来实现此功能。这是因为该模块提供了一种以最小编码方式处理此类和类似功能的高效方法。
我们还将在这个迭代中完成退出按钮功能的编码。目前,当用户点击关闭按钮时,窗口只是简单地关闭。我们希望询问用户他们是否真的想要退出,或者他们是否意外地点击了关闭按钮。
准备起飞
tkMessageBox模块提供了现成的消息框,可以在您的应用程序中显示各种消息。其中一些函数是showinfo、showwarning、showerror、askquestion、askyesno、askokcancel和askretryignore。以下截图展示了它们在使用时的示例:

要使用该模块,我们只需将其导入到当前命名空间中,如下所示:
import tkMessageBox
tkMessageBox的常用功能在2.08.py中进行了演示。以下是一些常见的使用模式:
tkMessageBox.showwarning("Beware", "You are warned")
tkMessageBox.showinfo("FYI", "This is FYI")
tkMessageBox.showerror("Err..", "its leaking.")
tkMessageBox.askquestion("?", "Can you read this ?")
tkMessageBox.askokcancel("OK", "Quit Postponing ?")
tkMessageBox.askyesno("Yes or No", " What Say ?")
tkMessageBox.askretrycancel("Retry", "Load Failed")
使用此模块显示消息有以下优点:
-
最小化编码即可实现功能特性
-
消息可以轻松配置
-
消息带有图标
-
它在每个平台上呈现了常见消息的标准化视图
启动推进器
-
现在,让我们为我们的代码编辑器编写
about和help函数。用例很简单。当用户点击关于菜单时,它会弹出一个带有OK按钮的消息。同样,当用户点击帮助按钮时,他们也会被提示一个带有OK按钮的消息。为了实现这些功能,我们在编辑器中包含了以下代码。(参见
2.09.py中的代码)import tkMessageBox def about(event=None): tkMessageBox.showinfo("About","Tkinter GUI Application\n Development Hotshot") def help_box(event=None): tkMessageBox.showinfo("Help","For help refer to book:\n Tkinter GUI Application\n Development Hotshot ", icon='question') aboutmenu.add_cascade(label="Help", command=help_box) -
接下来,我们将探讨添加退出确认功能。当用户点击文件 | 退出时,它将弹出一个
Ok-Cancel对话框以确认退出操作。def exit_editor(event=None): if tkMessageBox.askokcancel("Quit", "Do you really want to quit?"): root.destroy() root.protocol('WM_DELETE_WINDOW', exit_command) # override close filemenu.add_command(label="Exit", accelerator='Alt+F4', command=exit_editor)代码描述如下:
-
首先,我们将
tkMessageBox导入到当前命名空间中。 -
然后,我们定义我们的
about函数来显示一个showinfo消息框。 -
同样,我们定义了我们的
help_box函数来显示一个showinfo消息框。 -
然后,我们定义了一个带有
askokcancel框的exit命令。如果用户点击OK,则exit命令销毁根窗口以关闭窗口。 -
我们随后覆盖了关闭按钮的协议,并将其重定向到我们定义的
exit命令处理。 -
最后,我们向关于、帮助和退出菜单项添加
command回调。
-
目标完成 - 简要说明
在这次迭代中,我们完成了代码编辑器中文件|退出、关于|关于和关于|帮助菜单项的功能编码。我们还看到了如何使用tkMessageBox模块来显示不同格式的常用消息框。
图标工具栏和视图菜单功能
在这次迭代中,我们将向我们的文本编辑器添加更多功能:
-
显示快捷图标工具栏
-
显示行号
-
高亮当前行
-
更改编辑器的颜色主题

在这个过程中,我们将看到更多索引和标记的使用。
启动推进器
让我们首先从一项简单的任务开始。在这个步骤中,我们将快捷图标工具栏添加到我们的编辑器中。回想一下,我们已经创建了一个框架来容纳这些工具栏图标。现在让我们添加这些图标。
-
让我们从添加快捷图标工具栏开始。在添加这些图标时,我们已经遵循了一个约定。所有图标都已放置在
icons文件夹中。此外,图标的命名与处理它们的相应函数完全一致。遵循这个约定使我们能够同时在一个列表中循环,将图标图像应用于每个按钮,并在循环中添加command回调。代码已经被放置在我们之前创建的快捷框架中,用于放置这些图标。代码如下(参考
2.10.py中的代码):shortcutbar = Frame(root, height=25, bg='light sea green') #creating icon toolbar icons = ['new_file', 'open_file', 'save', 'cut', 'copy', 'paste', 'undo', 'redo', 'on_find', 'about'] for i, icon in enumerate(icons): tbicon = PhotoImage(file='icons/'+icon+'.gif') cmd = eval(icon) toolbar = Button(shortcutbar, image=tbicon, command=cmd) toolbar.image = tbicon toolbar.pack(side=LEFT) shortcutbar.pack(expand=NO, fill=X)代码的描述如下:
-
在我们的第一次迭代中,我们已经创建了一个快捷栏。现在我们将代码放置在我们创建框架和行的行之间,以及我们使用
pack管理器显示它的地方。 -
我们创建了一个图标列表,注意要将其命名为与图标名称完全一致。
-
然后,我们通过一个长度等于图标列表中项目数量的循环进行迭代。在每次循环中,我们创建一个按钮小部件,获取相应的图像并添加相应的
command回调。 -
在添加
command回调之前,我们必须使用eval命令将字符串转换为等效的表达式。如果我们不应用eval,它就不能作为表达式应用于我们的command回调。
这完成了我们快捷图标工具栏的编码。现在,如果您运行代码(代码
2.10.py),它应该会在顶部显示快捷图标工具栏。此外,由于我们已经将每个按钮链接到回调,所有这些快捷图标都应该按预期工作。 -
-
让我们现在努力在文本小部件的左侧框架上显示行号。这需要我们在代码的多个地方进行一些调整。因此,在我们开始编码之前,让我们看看我们在这里试图实现什么:
![启动推进器]()
-
视图菜单中有一个菜单项,允许用户选择是否显示行号。我们只想在选中选项时显示行号。
-
如果选中了选项,我们需要在之前创建的左侧框架中显示行号。
-
每当用户输入新行、删除行、从行剪切或粘贴文本、执行撤销或重做操作、打开现有文件或点击新菜单项时,行号都应该更新。简而言之,行号应该在可能影响行号的任何活动之后更新。
因此,我们需要定义一个名为
update_line_number()的函数。这个函数应该在每次按键、剪切、粘贴、撤销、重做、新建和打开定义之后被调用,以检查文本区域中是否有行被添加或删除,并相应地更新行号。我们通过以下两种策略实现这一点(请参阅2.10.py中的代码):-
将任何按键事件绑定到我们的
update_line_number()函数:textPad.bind("<Any-KeyPress>", update_line_number) -
在我们的剪切、粘贴、撤销、重做、新建和打开的定义中添加对
update_line_number()函数的调用
最后,我们按照以下方式定义我们的
update_line_number()函数:def update_line_number(event=None): txt = '' if showln.get(): endline, endcolumn = textPad.index('end-1c').split('.') txt = '\n'.join(map(str, range(1, int(endline)))) lnlabel.config(text=txt, anchor='nw')代码的描述如下:
-
回想一下,我们之前已经将一个变量
showln分配给了我们的菜单项:showln = IntVar() showln.set(1) viewmenu.add_checkbutton(label="Show Line Number", variable=showln) update_line_number -
我们首先将标签的文本配置标记为空白。
-
如果
showline选项设置为1(也就是说,在菜单项中勾选了),我们计算文本中的最后一行和最后一列。 -
然后,我们创建一个由数字组成的文本字符串,从 1 到最后一行的数字,每个数字之间用换行符
\n分隔。然后,我们使用textPad.config()方法将这个字符串添加到左侧标签。 -
如果菜单中的 显示行号 未选中,变量文本保持空白,因此不显示行号。
-
最后,我们将之前定义的剪切、粘贴、撤销、重做、新建和打开函数更新,在它们的末尾调用
update_line_number()函数。
我们现在已经完成了向我们的文本编辑器添加行号功能。
注意
你可能已经注意到了我们之前给出的函数定义中的
event=None参数。我们需要在这里指定它,因为这个函数可以从两个地方调用:-
从事件绑定(我们将其绑定到
<Any-KeyPress>事件) -
来自其他函数,如剪切、复制、粘贴、撤销、重做等
当函数从其他函数调用时,不传递任何参数。然而,当函数从事件绑定调用时,事件对象作为参数传递。如果我们不指定
event=None参数,并且函数从事件绑定调用,它将给出以下错误:TypeError: myfunction() takes no arguments (1 given) -
-
在这个迭代的最后,我们将实现一个功能,允许用户选择在当前行添加高亮。(请参阅
2.10.py中的代码)这个想法很简单。我们需要定位光标所在的行并给该行添加一个标签。最后,我们需要配置这个标签以不同的颜色背景显示,以突出显示。
回想一下,我们已经为用户提供了一个菜单选项来决定是否突出显示当前行。现在,我们将从这个菜单项添加一个
command回调到一个我们定义的toggle_highlight函数:hltln = IntVar() viewmenu.add_checkbutton(label="Highlight Current Line", onvalue=1, offvalue=0, variable=hltln, command=toggle_highlight)我们定义了三个函数来帮我们处理这个问题:
#line highlighting def highlight_line(interval=100): textPad.tag_remove("active_line", 1.0, "end") textPad.tag_add("active_line", "insert linestart", "insert lineend+1c") textPad.after(interval, toggle_highlight) def undo_highlight(): textPad.tag_remove("active_line", 1.0, "end") def toggle_highlight(event=None): val = hltln.get() undo_highlight() if not val else highlight_line()代码的描述如下:
-
每次用户勾选/取消勾选视图 | 突出显示当前行时,都会调用我们的
toggle_highlight函数。这个函数检查菜单项是否被勾选。如果被勾选,它调用highlight_line函数;否则,如果菜单项未被勾选,它调用撤销突出显示的函数。 -
我们的
highlight_line函数简单地将一个名为active_line的标签添加到当前行,并且每过一秒钟它调用切换突出显示的函数来检查当前行是否应该仍然被突出显示。 -
当用户在视图菜单中取消勾选突出显示时,会调用我们的
undo_highlight函数。一旦被调用,它简单地从整个文本区域中移除active_line标签。 -
最后,我们配置我们的标签
active_line以不同的背景颜色显示:textPad.tag_configure("active_line", background="ivory2")
注意
在我们的代码中,我们使用了
.widget.after(ms, callback)处理程序。这种让我们执行一些周期性操作的方法被称为闹钟处理程序。一些常用的 Tkinter 闹钟处理程序包括:-
after(delay_ms, callback, args...): 在给定毫秒数后注册一个闹钟回调 -
after_cancel(id): 取消给定的闹钟回调 -
after_idle(callback, args...): 只有在主循环中没有更多事件要处理时才调用回调,即系统空闲后
-
-
信息栏只是 Text 小部件右下角的一个小区域,显示光标位置的当前行号和列号,如下面的截图所示:
![Engage Thrusters]()
用户可以选择从视图菜单中显示/隐藏这个信息栏;请参考
2.11.py中的代码。我们首先在 Text 小部件内创建一个 Label 小部件,并将其打包在东南角。infobar = Label(textPad, text='Line: 1 | Column: 0') infobar.pack(expand=NO, fill=None, side=RIGHT, anchor='se')在很多方面,这与显示行号类似。在这里,同样需要在每次按键或剪切、粘贴、撤销、重做、新建、打开或其他导致光标位置变化的操作之后计算位置。因为这与我们的行号代码非常相似,我们将使用现有的绑定和现有的
update_line_number()函数来更新这个功能。为此,我们只需在我们的update_line_number()函数的定义中添加两行代码:currline, curcolumn = textPad.index("insert").split('.') infobar.config(text= 'Line: %s | Column: %s' %(currline, curcolumn))这会持续更新标签,显示当前光标位置的行和列。
最后,如果用户从 View 菜单中取消选中选项,我们需要隐藏这个小部件。我们通过定义一个名为
show_info_bar的函数来实现这一点,该函数根据用户选择的选项,要么应用pack,要么应用pack_forget到infobar标签。def show_info_bar(): val = showinbar.get() if val: infobar.pack(expand=NO, fill=None, side=RIGHT, anchor='se') elif not val: infobar.pack_forget()这个函数随后通过
command回调连接到现有的菜单项:viewmenu.add_checkbutton(label="Show Info Bar at Bottom", variable=showinbar ,command=show_info_bar) -
记住,在定义我们的 Themes 菜单时,我们定义了一个包含名称和十六进制颜色代码作为键值对的颜色方案字典。实际上,我们需要为每个主题指定两种颜色,一种用于背景,另一种用于前景颜色。让我们修改我们的颜色定义,以指定由点字符(
.)分隔的两种颜色。请参考代码2.11.py:clrschms = { '1\. Default White': '000000.FFFFFF', '2\. Greygarious Grey': '83406A.D1D4D1', '3\. Lovely Lavender': '202B4B.E1E1FF' , '4\. Aquamarine': '5B8340.D1E7E0', '5\. Bold Beige': '4B4620.FFF0E1', '6\. Cobalt Blue': 'ffffBB.3333aa', '7\. Olive Green': 'D1E7E0.5B8340', }我们的主题选择菜单已经定义在前面。现在让我们添加一个
command回调来处理选定的菜单:themechoice= StringVar() themechoice.set('1\. Default White') for k in sorted(clrschms): themesmenu.add_radiobutton(label=k, variable=themechoice, command=theme) menubar.add_cascade(label="View", menu=viewmenu)最后,让我们定义我们的
theme函数来处理主题的更改:def theme(): global bgc,fgc val = themechoice.get() clrs = clrschms.get(val) fgc, bgc = clrs.split('.') fgc, bgc = '#'+fgc, '#'+bgc textPad.config(bg=bgc, fg=fgc)函数很简单。它从我们定义的颜色方案字典中获取键值对。它将颜色分成两个组成部分,并使用
widget.config()将每种颜色分别应用于 Text 小部件的前景和背景。现在如果您从 Themes 菜单中选择不同的颜色,背景和前景颜色将相应地改变。
目标完成 – 简报
在这次迭代中,我们完成了快捷图标工具栏的编码,以及 View 菜单的所有功能。在这个过程中,我们学习了如何处理 Checkbutton 和 Radiobutton 菜单项,还看到了如何制作复合按钮,同时强化了之前章节中涵盖的几个 Tkinter 选项。
事件处理和上下文菜单
在这次最后的迭代中,我们将向我们的编辑器添加以下功能:
-
事件处理
-
上下文菜单
-
标题栏图标
启动推进器
让我们在这次最后的迭代中完成我们的编辑器。
-
首先,我们将添加事件处理功能。我们已经将加速器键盘快捷键添加到我们大量菜单项中。然而,仅仅添加加速键并不能添加所需的功能。例如,按下 Ctrl + N 应该创建一个新文件,但仅仅将其添加为加速键并不能使其生效。让我们将这些事件处理功能添加到我们的代码中。
注意
注意,我们所有的功能已经完成。现在我们只需要将事件映射到它们相关的回调函数上。(参考
2.12.py中的代码。)textPad.bind('<Control-N>', new_file) textPad.bind('<Control-n>', new_file) textPad.bind('<Control-O>', open_file) textPad.bind('<Control-o>', open_file) textPad.bind('<Control-S>', save) textPad.bind('<Control-s>', save) textPad.bind('<Control-A>', select_all) textPad.bind('<Control-a>', select_all) textPad.bind('<Control-f>', on_find) textPad.bind('<Control-F>', on_find) textPad.bind('<KeyPress-F1>', help_box)注意
简单地添加这些行就处理了我们的事件绑定。然而,这给我们带来了一个新的问题。我们已经讨论过,事件绑定将事件对象作为参数传递给绑定的回调函数。我们之前的所有函数都没有配备处理传入参数的能力。为了做到这一点,我们需要添加
event=None参数。添加这个可选参数允许我们使用这些函数,无论是否有事件参数。
或者,你也可以添加
textPad.bind (event, lambda e: callback())来完全忽略event参数。现在,您可以通过键盘快捷键访问这些功能。
注意,我们没有为剪切、复制和粘贴绑定键盘快捷键。这是因为文本小部件自带对这些事件的自动绑定。如果您为这些事件添加绑定,将会导致剪切、复制和粘贴事件发生两次;一次来自内置小部件,一次来自您自己定义的事件处理器。
-
接下来,我们将添加上下文菜单。但在那之前,我们需要了解上下文菜单是什么。
在鼠标光标位置右键单击弹出的菜单称为上下文菜单或o。这在上面的屏幕截图中显示:
![启动推进器]()
让我们在文本编辑器中编码这个功能。我们首先定义我们的上下文菜单:
cmenu = Menu(textPad) for i in ('cut', 'copy', 'paste', 'undo', 'redo'): cmd = eval(i) cmenu.add_command(label=i, compound=LEFT, command=cmd) cmenu.add_separator() cmenu.add_command(label='Select All', underline=7, command=select_all)我们然后将鼠标右键与名为
popup的回调函数绑定:textPad.bind("<Button-3>", popup)最后,我们定义了
popup方法:def popup(event): cmenu.tk_popup(event.x_root, event.y_root, 0) -
作为我们应用程序的最后一笔,我们使用以下代码为编辑器添加了一个标题栏图标:
root.iconbitmap('icons/pypad.ico')
目标完成 - 简要说明
在这次迭代中,我们添加了对事件处理的支持,并为我们的编辑程序添加了上下文菜单和标题栏图标。
任务完成
我们已经完成了编辑器的七次迭代编码。我们首先将所有小部件放置在我们的 Toplevel 窗口中。然后我们利用文本小部件的一些内置功能来编码一些功能。我们学习了索引和标记的一些非常重要的概念,您将在 Tkinter 项目中经常使用到这些概念。
我们还看到了如何使用tkfileDialog和tkMessageBox模块来快速在我们的程序中编码一些常见功能。
恭喜!您现在已经完成了文本编辑器的编码。
热身挑战
这是您的热身挑战:
-
您的目标是将这个文本编辑器转变为一个 Python 代码编辑器。您的编辑器应该允许打开和保存
.py文件扩展名。 -
如果文件具有
.py扩展名,您的编辑器应该实现语法高亮和制表符缩进。 -
虽然这可以通过外部库轻松完成,但您应该尝试使用我们迄今为止看到的内置 Tkinter 选项来自己实现这些功能。如果您需要提示,可以查看 Python 内置编辑器 IDLE 的源代码,它是用 Tkinter 编写的。
第三章:可编程鼓机
在上一个项目中,我们构建了一个文本编辑器。在这个过程中,我们查看了一些常见的 Tkinter 小部件,如 Menu、Buttons、Label 和 Text。现在,让我们做一些音乐。让我们使用 Tkinter 和一些其他 Python 模块构建一个跨平台鼓机。
任务简报
在这个项目中,我们将构建一个可编程的鼓机。鼓机的图形用户界面基于 Tkinter。您可以使用无限数量的鼓样本创建无限数量的节拍模式。然后您可以在项目中存储多个 riff,并在以后播放或编辑项目。

要创建自己的鼓点模式,只需使用左侧的按钮加载一些鼓样本。您可以更改构成节拍模式的单位,这反过来又决定了节奏的速度。您还可以决定每个单位中的节拍数。大多数西方节拍每个单位有四个节拍,华尔兹每个单位有三个节拍,我在这台机器上创作的某些印度和阿拉伯节奏每个单位有 3 到 16 个节拍!
为什么它如此出色?
不要被 GUI 的小尺寸所误导。这是一个功能强大的鼓机,可以与一些大型商业鼓机程序提供的功能相媲美。到这个项目的结束时,您应该能够扩展它,超越一些商业鼓程序。
该机器的一些关键特性包括:
-
大量节拍
-
大量模式以伴随歌曲
-
每个模式中节拍数可变
-
使用 16 位,44100 kHz WAV 样本(单声道或立体声)
-
支持各种文件格式
-
能够保存包含多个模式的多个项目
在 Loops 子目录中提供了一些鼓样本;然而,您可以加载任何其他鼓样本。您可以从互联网上免费下载大量样本。
在开发这个程序的过程中,我们进一步调整 Tkinter,并查看在 GUI 编程中通常遇到的一些重要概念和想法。
您的热门目标
承担这个项目的关键目标包括:
-
理解 Tkinter 在面向对象上下文中的应用
-
使用一些其他 Tkinter 小部件,如 Spinbox、Button、Entry 和 Checkbutton
-
使用
grid几何管理器 -
使用 ttk 主题小部件
-
理解与 Tkinter 相关的线程编程
-
使用 Python 标准库中的其他常见模块
-
使用
pickle模块进行对象持久化
除了这些关键概念之外,我们在项目过程中讨论了其他几个 GUI 编程的重要要点。
任务清单
在这个项目中,我们将使用一些标准 Python 分发中的内置库。这包括 Tkinter、ttk、tkFileDialog、tkMessageBox、os、time、threading、wave 和 pickle 模块。
要验证这些模块是否存在,只需在 IDLE 交互式提示符中运行以下语句:
>>> import Tkinter, ttk, os, time, threading, wave, pickle, tkFileDialog, tkMessageBox
这不应该导致错误,因为标准的 Python 发行版已经将这些模块内置到发行版中。
除了这个之外,您还需要添加一个名为pymedia的额外 Python 模块。
可以在pymedia.org/下载pymedia模块。
安装模块后,您可以通过导入它来验证:
>>> import pymedia
如果没有报告错误,你就可以开始编程鼓机了。让我们开始吧!
使用面向对象编程设置 GUI
我们作为上一个项目开发的文本编辑程序是使用过程式代码设置的。虽然它提供了一些快速编码的好处,但它本质上是一个单一的过程。
我们开始遇到全局变量。函数定义需要在调用它们的代码之上定义,最重要的是,代码不可重用。
因此,我们需要某种方式来确保我们的代码更具可重用性。这就是为什么程序员更喜欢使用面向对象编程(OOP)来组织他们的代码到类中。
面向对象编程(OOP)是一种编程范式,它将重点从操纵它们的逻辑转移到我们想要操纵的对象上。
这与将程序视为一个逻辑过程,该过程接受输入,处理它并产生一些输出的过程式编程相反。
面向对象编程提供了诸如数据抽象、封装、继承和多态等好处。此外,面向对象编程为程序提供了清晰的模块化结构。由于可以创建新的对象而不必修改现有的对象,因此代码修改和维护变得容易。
让我们使用面向对象编程来构建我们的鼓程序,以展示这些功能的一些示例。
准备起飞
我们鼓程序的指示性 OOP 结构可能如下所示(参见3.01.py中的代码):
from Tkinter import *
class DrumMachine():
def app(self):
self.root = Tk()
# all other code are called from here
self.root.mainloop()
if __name__ == '__main__':
dm = DrumMachine()
dm.app()
代码的描述如下所示:
-
我们创建一个名为
DrumMachine的类,并定义一个app()方法来初始化顶层窗口 -
如果程序作为独立程序运行,将创建一个新的对象,并调用
app方法来创建顶层窗口 -
此代码创建一个空的顶层窗口
现在我们已经准备好了顶层窗口,让我们向其中添加一些小部件。在这个迭代中,我们将放置顶部栏、左侧栏(允许我们上传鼓样本的区域)、右侧栏(具有定义节拍模式的按钮)和底部的播放栏(其中有一个播放按钮、一个停止按钮和一个循环复选按钮)。
四个区域已在不同的方框中划分,以将小部件分组到单独的框架中,如下面的截图所示:

启动推进器
-
首先,我们将创建顶部栏。顶部栏是包含 Spinbox 小部件的栏,允许用户在节奏模式中更改单位和每单位节拍。这两个一起决定了节奏的速度和循环模式,如下所示(参见
3.02.py中的代码):def create_top_bar(self): top_bar_frame = Frame(self.root) top_bar_frame.config(height=25) top_bar_frame.grid(row=0, columnspan=12, rowspan=10, padx=5, pady=5) Label(top_bar_frame, text='Units:').grid(row=0, column=4) self.units = IntVar() self.units.set(4) self.bpu_widget = Spinbox(top_bar_frame, from_=1, to=10, width=5, textvariable=self.units) self.bpu_widget.grid(row=0, column=5) Label(top_bar_frame, text='BPUs:').grid(row=0, column=6) self.bpu = IntVar() self.bpu.set(4) self.units_widget = Spinbox(top_bar_frame, from_=1, to=8, width=5, textvariable=self.bpu) self.units_widget.grid(row=0, column=7)代码的描述如下:
-
我们首先创建一个新的方法来创建顶部栏。我们添加一个框架
top_bar_frame用于顶部栏,然后添加两个 spin boxes 来跟踪单位和每单位节拍值。现在我们不添加command回调。回调将在稍后添加。 -
我们定义了两个 Tkinter 变量
self.units和self.bpu来保存 Spinbox 小部件的当前值。这是因为我们将在该方法的作用域之外需要这些变量,所以将其定义为对象变量(self)。 -
小部件使用
grid几何管理器进行放置。
-
-
接下来,我们将创建左侧栏。左侧栏是允许用户加载鼓样本的栏。左侧栏的每一行都允许加载一个独特的鼓样本。鼓样本通常是不同鼓(如贝斯、钹、军鼓、钟、克拉韦斯)的小型
.wav或.ogg文件样本,或者用户决定的其他样本。左侧栏上的按钮将打开上传文件。当用户上传鼓样本时,鼓样本的名称将自动填充到该按钮相邻的 Entry 小部件中。
因此,每一行都有一个按钮和一个 Entry 小部件(参考
3.02.py中的代码):MAX_DRUM_NUM = 5 def create_left_pad(self): '''creating actual pattern editor pad''' left_frame = Frame(self.root) left_frame.grid(row=10, column=0, columnspan=6, sticky=W+E+N+S) tbicon = PhotoImage(file='images/openfile.gif') for i in range(0, MAX_DRUM_NUM): button = Button(left_frame, image=tbicon) button.image = tbicon button.grid(row=i, column=0, padx=5, pady=2) self.drum_entry = Entry(left_frame) self.drum_entry.grid(row=i, column=4, padx=7, pady=2)代码的描述如下:
-
可以加载的最大鼓样本数被定义为常量
MAX_DRUM_NUM -
我们创建另一个名为
left_frame的框架来容纳这个区域的各种小部件 -
通过循环,我们为需要允许用户加载的鼓样本数量创建 Button 和 Entry 小部件
-
-
接下来,我们将创建右侧栏。右侧栏是允许用户定义节拍模式的区域。这个区域由一系列按钮组成。按钮的行数等于可以加载的鼓样本数。按钮的列数由用户在顶部栏的 spin boxes 中选择的单位和每单位节拍数决定。按钮的列数等于单位和每单位节拍数的乘积。
目前我们还没有将旋转框与按钮连接起来。现在,让我们为每个可以加载的鼓样本放置四个按钮列,如下所示(参考
3.02.py中的代码):def create_right_pad(self): right_frame = Frame(self.root) right_frame.grid(row=10, column=6,sticky=W+E+N+S, padx=15, pady=2) self.button = [[0 for x in range(4)] for x in range(MAX_DRUM_NUM)] for i in range(MAX_DRUM_NUM): for j in range(4): self.button[i][j] = Button(right_frame, bg='grey55') self.button[i][j].grid(row=i, column=j)代码的描述如下:
-
我们创建另一个框架
right_frame来容纳这些按钮。 -
使用列表推导,我们创建了一个大小为
4 * MAX_DRUM_NUM的空列表。 -
目前,我们只是简单地添加四个按钮列来占据空间。按钮的行数保持等于最大鼓样本数,以便每行按钮对应一个样本。
备注
将小部件分组到不同的方法中是有原因的。
例如,我们使用两个独立的方法
create_left_pad和create_right_pad创建了左侧鼓垫和右侧鼓垫。如果我们在这两个相同的方法中定义了这两组小部件,由于 BPU 和单元的变化,每次左侧按钮改变时,用户都需要重新加载鼓样本。这对最终用户来说将是事倍功半。根据经验法则,始终建议将相关的部件放在单个方法中。然而,决定类结构更多的是一种艺术,而不是科学,需要一生去学习和完善。
-
-
接下来我们将创建播放条。底部的播放条包括 播放 按钮、停止 按钮和一个 循环 复选按钮。请参考
3.02.py中的代码,如下所示:def create_play_bar(self): playbar_frame = Frame(self.root, height=15) ln = MAX_DRUM_NUM+10 playbar_frame.grid(row=ln, columnspan=13, sticky=W+E, padx=15,pa dy=10) button = Button( playbar_frame, text ='Play') button.grid(row= ln, column=1, padx=1) button = Button( playbar_frame, text ='Stop')='Stop') button.grid(row= ln, column=3, padx=1) loop = BooleanVar() loopbutton = Checkbutton(playbar_frame, text='Loop',variable=loop) loopbutton.grid(row=ln, column=16, padx=1)代码的描述如下:
-
代码相当直观。它创建了一个框架
playbar_frame,并在其中放置了两个按钮和一个复选按钮。 -
创建了一个 Tkinter
BooleanVar()来跟踪 Checkbutton 的状态。
-
-
现在我们已经创建了所有的小部件,现在是时候通过显式调用创建它们的那些方法来实际显示它们了。我们在程序的主循环中这样做,如下所示(请参考
3.02.py中的代码):def app(self): self.root = Tk() self.root.title('Drum Beast') self.create_top_bar() self.create_left_pad() self.create_right_pad() self.create_play_bar() self.root.mainloop()注意
而不是定义一个单独的方法
app()来运行我们的主循环,我们也可以通过创建一个名为__init__的初始化方法来运行主循环。在那种情况下,我们就不需要显式调用
app()方法来运行程序。然而,如果有人需要在另一个程序中使用这个类,它将无谓地创建一个 GUI。从
app()方法中显式调用mainloop函数为我们使用代码作为其他程序的库留出了空间。
目标完成 – 简短总结
这完成了我们的第一次迭代。在这个迭代中,我们成功地创建了鼓程序的基本结构。这包括创建顶部、左侧、右侧和底部的框架,这些框架根据鼓程序的要求持有不同的小部件。
我们还看到了以面向对象编程风格构建 Tkinter GUI 程序的最常见方式之一。
完成模式编辑器
在上一个迭代中,我们编写了一个虚拟的 create_right_pad,其中包含四列按钮。然而,在我们的程序方案中,按钮的列数取决于最终用户选择的 单元 和每单位节拍数(BPU)值。
按钮的列数应该等于:
单元数量 x BPU
此外,为了区分每个单元,每个连续的按钮单元应以不同的颜色显示。此外,当按钮被点击时,其颜色应改变以跟踪用户定义的模式,如下面的截图所示:

让我们将这三个功能添加到我们的鼓编辑器中。
启动推进器
-
首先,我们将从将按钮连接到单位和BPU Spinbox 小部件开始。代码很简单。我们从顶部栏中的两个 Spinbox 小部件添加
command回调来调用我们的create_right_pad方法。请参考3.03.py中的代码:self.units_widget = Spinbox(topbar_frame, from_=1, to=8, width=5, textvariable=self.units, command= self.create_right_pad) self.bpu_widget = Spinbox(topbar_frame, from_=1, to=10, width=5, textvariable=self.bpu, command= self.create_right_pad)然后,我们按照以下方式修改现有的
create_right_pad方法,并在3.03.py代码中:def create_right_pad(self): bpu = self.bpu.get() units = self.units.get() c = bpu * units right_frame = Frame(self.root) right_frame.grid(row=10, column=6,sticky=W+E+N+S, padx=15, pady=2) self.button = [[0 for x in range(c)] for x inrange(MAX_DRUM_NUM)] for i in range(MAX_DRUM_NUM): for j in range(c): color = 'grey55' if (j/bpu)%2 else 'khaki' self.button[i][j] = Button(right_frame, bg=color, width=1, command=self.button_clicked(i,j,bpu)) self.button[i][j].grid(row=i, column=j)代码的描述如下所示:
-
在我们的
right_frame框架中,我们通过双重嵌套循环创建一个二维矩阵,其中行数等于常量MAX_DRUM_NUM,而列数等于单位和BPU的乘积。 -
每个按钮的颜色配置为
grey55或khaki,具体取决于因子j/bpu是偶数还是奇数。 -
现在如果您运行代码(代码
3.03.py),您会发现按钮的数量会根据您在单位和 bpu Spinbox 中做出的选择而变化。此外,每个单位将以交替的土色和灰色着色。 -
注意我们是如何用变量
i和j定义按钮的grid几何位置的。
-
-
现在按钮对单位和 bpu 的变化做出响应,是时候将这些按钮更改为切换按钮了。当用户点击任何按钮时,按钮的颜色应变为绿色。当按钮再次被点击时,颜色恢复到其原始颜色。我们需要这个功能来定义节拍模式。
我们首先为我们的按钮添加一个
command回调,将按钮的行、列和 bpu 作为参数传递给新的button_clicked方法(请参考3.03.py中的代码),如下所示:self.button[i][j] = (Button(right_frame, bg='grey55', width=1, command=self.Button_clicked(i,j,bpu)))我们然后定义
button_clicked方法如下所示:def button_clicked(self,i,j,bpu): def callback(): btn = self.button[i][j] color = 'grey55' if (j/bpu)%2 else 'khaki' new_color = 'green' if btn.cget('bg') != 'green' else color btn.config(bg=new_color) return callback代码的描述如下所示:
-
我们的
button_clicked方法接受三个参数:i, j和bpu。 -
变量
i和j让我们跟踪哪个按钮被点击。然而,请注意,当按钮尚未创建时,command回调self.Button_clicked(i,j,bpu)会引用i和j。为了跟踪用户点击的按钮,我们在self.button_clicked函数中封装了一个单独的callback()函数,然后它返回一个回调。现在,我们的方法将为每个按钮记录返回不同的i和j值。 -
bpu参数用于计算按钮的原始颜色。如果按钮被切换,则需要此参数以将按钮的颜色恢复到其原始颜色。在我们将按钮的颜色更改为绿色之前,我们将原始颜色存储在变量color中。
-
目标完成 - 简短总结
我们现在已经完成了右侧鼓垫的编码。在这个过程中,我们创建了一个按钮的二维列表self.button,其中self.button[i][j]指的是第i行和j列的按钮。
这些按钮中的每一个都可以打开或关闭,以表示是否为该特定按钮播放鼓样本。
当按钮处于开启状态时,其颜色会变为绿色。如果它被关闭,它会恢复到原始颜色。这种结构可以很容易地用来定义一个节拍模式。
在此过程中,我们看到了对 Spinbox 和 Button 小部件的更高级使用。
加载鼓样本
我们的主要目标是播放用户决定的节拍模式顺序中的声音文件。为此,我们需要将声音文件添加到鼓机中。
我们的程序没有预加载任何鼓文件。相反,我们希望让用户从广泛的鼓文件中选择。因此,除了正常的鼓之外,你可以播放日本的小太鼓、印度的手鼓、拉丁美洲的邦戈鼓,或者几乎任何你想添加到你的节奏中的声音。你所需要的只是一个包含该声音样本的小.wav或.ogg文件。

让我们编写代码来添加这个鼓样本到我们的程序中。
鼓样本应该加载在左侧的条上,如图中所示的前一个屏幕截图。我们已经在鼓垫的左侧创建了带有文件夹图标的按钮。所需的功能很简单。
当用户点击任何一个左侧按钮时,应该打开一个文件对话框,让用户选择一个.wav或.ogg文件。当用户选择文件并点击打开时,该按钮旁边的 Entry 小部件应该填充文件的名称。此外,鼓样本文件的存储位置应该添加到一个列表中,以便稍后播放。
启动推进器
-
首先,我们将导入所需的模块。为了打开声音文件,我们将使用
tkFileDialog模块。我们还将使用tkMessageBox模块来显示某些弹出消息。我们还需要使用os模块提取给定声音样本的文件名。让我们首先将以下代码中的三个模块(参考3.04.py中的相同代码)导入到我们的当前命名空间中:import tkFileDialog import tkMessageBox import os -
接下来,我们将添加属性来跟踪加载的样本。用户不可避免地会加载多个鼓样本。因此,我们需要跟踪鼓样本加载的 Entry 小部件、每个鼓样本的位置以及表示当前鼓号的数字。相应地,我们创建了两个名为
self.widget_drum_name和self.widget_drum_file_name的列表来存储 Entry 小部件实例和文件位置。我们还声明了一个变量
self.current_drum_no来跟踪当前鼓号。我们选择在初始化方法
__init__(参考3.04.py中的代码)下初始化这些变量和列表:def __init__(self): self.widget_drum_name = [] self.widget_drum_file_name = [0]*MAX_DRUM_NUM self.current_drum_no = 0然后,我们修改我们的
create_left_pad方法,包括一个将所有鼓条 Entry 小部件的列表追加到我们新创建的列表self.widget_drum_name中的行:self.widget_drum_name.append(self.drum_entry) -
然后,我们在
create_left_pad方法中的按钮上添加一个command回调来加载鼓样本,如下面的代码片段所示:button = Button(left_frame, image=tbicon, command=self.drum_load(i)) -
最后,我们按照以下方式编写我们的
drum_load方法(参考3.04.py中的代码):def drum_load(self, drum_no): def callback(): self.current_drum_no = drum_no try: file_name = tkFileDialog.askopenfilename(defaultextension=".wav", filetypes=[("Wave Files","*.wav"),("OGG Files","*.ogg")])Files","*.ogg")]) if not file_name: return try: delself.widget_drum_file_name[drum_no] except: pass self.widget_drum_file_name.insert(drum_no, file_name) drum_name = os.path.basename(file_name) self.widget_drum_name[drum_no].delete(0, END) self.widget_drum_name[drum_no].insert(0, drum_name) except: tkMessageBox.showerror('Invalid', "Error loading drum samples") return callback代码的描述如下:
-
我们在函数内部定义一个回调函数,因为我们需要跟踪几个鼓样本。
-
为了跟踪通过哪个小部件加载了声音样本,我们将
self.current_drum_no的值设置为从按钮command回调接收到的drum_num值。 -
在一个
try块中,我们使用tkFileDialog.askopenfilename获取鼓样本的文件名。然后我们检查文件名是否已经存在于我们的文件名列表中。如果是,我们将其删除。 -
使用
os.path.basename模块从os模块中获取文件名,并将其插入到相应的 Entry 小部件中。 -
如果
askopenfilename失败,我们使用tkMessageBox.showerror来显示自定义错误消息。
-
目标完成 – 简短总结
在这次迭代中,我们导入了处理对话框和消息框的模块。然后我们添加了跟踪鼓样本的属性。最后,我们为按钮添加了command回调,当按钮被点击时,会打开一个对话框供用户选择鼓样本。
我们现在的代码能够加载鼓样本并存储所有必要的记录,我们将需要这些记录来播放鼓点模式。
接下来,让我们将注意力转向根据用户定义的模式播放鼓样本。
播放鼓机
现在我们已经有了加载鼓样本和定义鼓点模式的机制,让我们添加播放这些鼓点模式的能力。在许多方面,这是我们程序的核心。
让我们先了解我们想要实现的功能。一旦用户加载了一个或多个鼓样本,并使用切换按钮定义了鼓点模式,我们需要扫描模式的每一列,看看是否找到一个绿色按钮。如果找到了,我们的代码应该在继续之前播放相应的鼓样本。此外,同一列上的绿色按钮应该几乎同时播放,而每一列之间应该有一定的间隔时间,这将定义音乐的节奏。
准备起飞
我们将使用pymedia模块来播放声音文件。pymedia模块可以在多个操作系统上播放多种声音格式,如.wav、.ogg、.mp3、.avi、.divx、.dvd和.cdda。
不深入探讨 pymedia 如何播放声音文件,官方文档告诉我们可以使用以下代码示例来播放音频文件:
import time, wave, pymedia.audio.sound as sound
f= wave.open( 'YOUR FILE NAME', 'rb' )
sampleRate= f.getframerate()
channels= f.getnchannels()
format= sound.AFMT_S16_LE
snd= sound.Output( sampleRate, channels, format )
s= f.readframes( 300000 )
snd.play( s )
如果你将此段代码作为一个独立的脚本运行,并用支持音频文件的文件位置替换'YOUR FILE NAME',这应该在你的电脑上播放媒体文件。
使用此代码示例,我们将实现鼓机的播放功能。
启动推进器
-
首先,让我们将所有必要的模块导入到我们的命名空间中(参考
3.05.py中的代码):import time import wave import pymedia.audio.sound as sound -
接下来,我们将定义
play_sound方法如下:def play_sound(self, sound_filename): try: self.s = wave.open(sound_filename, 'rb') sample_rate = self.s.getframerate() channels = self.s.getnchannels() frmt = sound.AFMT_S16_LE self.snd= sound.Output(sample_rate, channels, frmt) s = self.s.readframes(300000) self.snd.play(s) except: pass这种方法只是简单地使用
pymedia提供的 API,并将其包装成一个接受文件名并播放的方法。 -
现在我们来定义
play方法,它实际上播放节拍样本:def play(self): for i in range(len(self.button[0])): for item in self.button: try: if item[i].cget('bg') == 'green': if not self.widget_drum_file_name [self.button.index(item)]:continue sound_filename = self.widget_drum_file_name [self.button.index(item)] self.play_sound(sound_filename) except: continue time.sleep(3/4.0)代码的描述如下:
-
我们遍历所有按钮,在移动到下一列之前扫描每一列。对于每个按钮,我们使用
widget.cget()来检查其颜色是否为绿色。 -
如果颜色是绿色,我们检查是否有相应的鼓样本已加载。如果没有,我们忽略绿色按钮,并使用
continue跳过循环中的下一个项目。 -
如果颜色是绿色,并且已加载相应的鼓样本,我们使用之前定义的
pymedia包装方法来播放音频播放该样本。 -
在移动到下一列之前,代码被设置为暂停一小段时间。如果代码没有暂停一小段时间,程序会以非常快的速度连续播放所有样本。
-
我们选择让代码暂停八分之一秒的时间。你可以改变这个暂停时间来调整节奏。
-
目标完成 - 简短总结
在这次迭代中,我们添加了播放已加载鼓样本的功能。
我们的音乐机现在可以正常工作了。你可以加载鼓样本,定义节拍模式,当你点击播放按钮时,音乐机就会播放那个节拍模式!
注意
在这个例子中,我们根据按钮的颜色来决定是否播放鼓样本。这在这里是为了演示目的。然而,将逻辑与外观混合并不是一个好的实践。更好的想法是为按钮实现一个数据结构,它可以跟踪按钮状态为“点击”或“未点击”,然后根据这个按钮的状态播放音频。实现这种双重按钮状态留给你作为练习去探索。
分类英特尔
在我们之前的代码中,我们使用widget.cget()来获取按钮bg选项的当前值,以检查它是否为绿色。你可以使用w.cget(key)来返回小部件选项的当前值。另外,请注意,即使你在配置小部件选项时给出了非字符串值,cget()也总是以字符串的形式返回值。
与widget.cget()方法类似,Tkinter 为所有小部件提供了各种各样的方法。有关基本小部件方法的列表,请参阅附录 B 中的基本小部件方法部分,快速参考表。
如果你想知道特定小部件配置的所有选项,你可以使用widget.config()方法,如下所示:(参见3.06.py中的代码)
from Tkinter import *
root = Tk()
widget = Button(root, text="#", bg='green')
widget.pack()
print widget.config()
print widget.config('bg')
root.mainloop()
这段代码将打印一个字典,显示所有小部件选项及其值的元组列表。例如,在前面的代码中,print widget.config('bg')这一行打印了一个元组:
('background', 'background', 'Background', <border object at 022A1AC8>, 'green')
Tkinter 和线程
我们的鼓机以我们想要的方式播放模式。然而,有一个小问题。play方法阻塞了我们的 Tkinter 程序的主循环。它不会将控制权交还给主循环,直到播放完所有的声音样本。
这意味着,如果你现在想点击停止按钮或更改其他小部件,你必须等待play循环完成。
你可能已经注意到,当你点击播放按钮时,它会在声音循环播放的时间内保持按下状态。在这段时间内,你无法访问 Toplevel 窗口中的任何其他小部件。
这显然是一个错误。在播放仍在进行时,我们需要一种方法将控制权交还给 Tkinter 主循环。
准备起飞
我们可以实现的 simplest 方法之一是在play循环中使用root.update()方法。这会在每个声音样本播放后更新root.mainloop()方法(参见3.07.py中的注释代码)。
然而,这是一个不太优雅的方法,因为控制权在 GUI 中传递时会有一些延迟。因此,你可能会在其他小部件的响应中体验到轻微的延迟。
此外,如果其他事件导致该方法被调用,可能会导致嵌套的事件循环。
一个更好的解决方案是从单独的线程中运行play方法。为此,让我们使用 Python 的threading模块。
启动推进器
-
让我们先在我们的命名空间中导入
threading模块(参考3.07.py中的代码):import threading -
现在,让我们创建一个方法,它调用
self.play()方法在单独的线程中运行。这通过线程模型重定向play:def play_in_thread(self): self.thread = threading.Thread(None, self.play, None, (), {}) self.thread.start() -
最后,将
play_bar方法中播放按钮的command回调从现有的self.play()方法更改为self.play_in_thread():button=Button(playbar_frame, text ='Play', command= self.play_in_thread)现在,如果你加载一些鼓样本,定义节拍模式,并点击播放按钮,声音将在单独的线程中播放,而不会阻止主循环更新(参考
3.07.py中的代码)。 -
下一步将是编写停止按钮的代码。停止按钮的作用很简单;它只是停止当前正在播放的模式。为此,我们首先为停止按钮添加一个
command回调,调用stop_play方法,如下所示(参见3.07.py中的代码):button=Button(playbar_frame, text='Stop', command= self.stop_play)然后,我们定义
stop_play方法如下:def stop_play(self): self.keep_playing = False -
我们现在从单独的线程中运行
play方法。然而,如果用户多次点击按钮,这将产生更多的线程,播放节拍。为了避免这种情况,按钮应该配置为state='disabled',并在序列完成后再次启用。要在程序开始运行时禁用播放按钮,我们在
play_in_thread方法中添加以下行(参考3.07.py中的代码):self.start_button.config(state='disabled')类似地,当序列播放完毕或点击停止按钮时,我们希望再次启用播放按钮。为了启用它,我们在
play和stop_play方法中添加了以下行:self.start_button.config(state='normal')注意
Tkinter 和线程安全
Tkinter 不是线程安全的。Tkinter 解释器仅在运行主循环的线程中有效。任何对小部件的调用理想情况下应该从创建主循环的线程中进行。从其他线程调用特定于小部件的命令是可能的(正如我们在这里所做的那样),但并不可靠。
当从另一个线程调用小部件时,事件会被排队到解释器线程,该线程执行命令并将结果传回调用线程。如果主循环正在运行但未处理事件,有时会导致不可预测的异常。
我们对现有的
play方法所做的唯一更改是将整个代码包含在一个try-except块中。我们这样做是因为 Tkinter 不是线程安全的,在处理play线程时可能会引起一些不希望的异常。我们能做的最好的事情就是使用try-except块忽略这些情况。小贴士
mtTkinter – Tkinter 的线程安全版本
如果您发现自己正在处理一个本质上多线程的项目,您可能需要考虑查看mtTkinter —Tkinter 的线程安全版本。有关 mtTkinter 的更多信息,请访问
Tkinter.unPythonic.net/wiki/mtTkinter。对于更专业的多进程需求,您还可以查看multiprocessing 模块或一个事件模型,如Twisted。
-
最后一步是编写循环复选框的代码。循环复选框的作用很简单。如果循环复选框未选中,则模式只播放一次。如果选中,模式将无限循环播放。只有当循环复选框未选中或按下停止按钮时,模式才会停止播放。
我们向循环复选框添加了一个
command回调:loopbutton = Checkbutton(playbar_frame, text='Loop', variable=loop, command=lambda: self.LoopPlay(loop.get())) )我们随后定义了
loop_play方法如下:def loop_play(self, xval): self.loop = xval配备这两个变量,我们修改了
play方法,使其在self.keep_playing等于True时继续播放(参见3.07.py中的代码)。如果
self.loop的值等于False,我们将self.keep_playing的值设置为False,从而跳出播放循环。
目标完成 – 简短总结
这完成了项目迭代。在本轮中,我们改进了play方法,使其能够在单独的线程中播放音频文件。
我们使用了 Python 内置的线程模块在单独的线程中播放循环。我们研究了 Tkinter 的一些线程相关限制以及一些克服这些限制的方法。
我们还编写了停止按钮和循环复选框的功能。
更多节奏模式
我们的音乐鼓程序现在功能齐全。您可以加载鼓样本并定义一个节拍模式,我们的鼓机将播放它。现在让我们扩展我们的鼓程序,以便我们能够在同一个程序中创建多个模式。
而不是单个鼓模式,现在我们将有一个模式列表。在播放模式时,用户将能够在许多不同的节拍模式之间切换。这将允许鼓手在表演中添加变化。
启动推进器
-
我们需要做的第一件事是在顶部栏中添加一个 Spinbox 小部件(如下面的截图所示),它将记录模式数量。我们还在 Spinbox 小部件旁边添加了一个 Entry 小部件,以跟踪模式名称,该名称由在 Spinbox 中选择的数字决定。
![启动推进器]()
这段代码被添加到
create_top_bar方法中(参考3.08.py中的代码):Label(top_bar_frame, text='Pattern Number:').grid(row=0, column=1) self.patt = IntVar() self.patt.set(0) self.prevpatvalue = 0 # to trace last click Spinbox(top_bar_frame, from_=0, to=9, width=5, textvariable=self.patt, command=self.record_pattern).grid(row=0, column=2) self.pat_name = Entry(top_bar_frame) self.pat_name.grid(row=0, column=3, padx=7,pady=2) self.pat_name.insert(0, 'Pattern %s'%self.patt.get()) self.pat_name.config(state='readonly')代码的描述如下:
-
模式编号存储在 Tkinter 整数变量
self.patt中。 -
存储相应模式名称的 Entry 小部件称为
self.pat_name。这个小部件被标记为“只读”,因为我们不希望允许用户修改名称。 -
Spinbox 小部件有一个
command回调到新的record_pattern方法。
-
-
现在我们来编写
record_pattern方法。这个方法的作用是跟踪给定模式的状态。因此,对于每个模式,它需要跟踪模式编号、单元、BPU、加载的鼓样本以及用户为该模式编号定义的节拍模式。我们将这些信息存储在一个名为self.pattern_list的列表中。我们的图案 Spinbox 允许添加 10 个模式。因此,我们首先初始化
self.pattern_list为一个包含 10 个空格的空列表。我们在类的
__init__方法中初始化它,如下所示(也见于3.08.py中的代码):self.pattern_list = [None]*10现在我们来编写
record_pattern方法:def record_pattern(self): pattern_num, bpu, units = self.patt.get(),self.bpu.get(), self.units.get() self.pat_name.config(state='normal') self.pat_name.delete(0, END) self.pat_name.insert(0, 'Pattern %s'%pattern_num) self.pat_name.config(state='readonly') prevpval = self.prevpatvalue self.prevpatvalue = pattern_num c = bpu*units self.buttonpickleformat =[[0] * c for x in range MAX_DRUM_NUM)] for i in range(MAX_DRUM_NUM): for j in range(c): if self.button[i][j].config('bg')[-1] == 'green': self.buttonpickleformat[i][j] = 'active' self.pattern_list[prevpval] = {'df': self.widget_drum_file_name, 'bl': self.buttonpickleformat, 'bpu':bpu, 'units':units} self.reconstruct_pattern(pattern_num, bpu, units)代码的描述如下:
-
第一行简单地获取当前模式编号、bout 和要记录的模式单元的值。
-
这段代码的接下来的四行执行一个简单的任务。对于每个模式的变化,它只是将新的模式名称更新到相应的 Entry 小部件中。由于 Entry 小部件是“只读”的,我们首先将其状态配置为
normal,以便我们可以在 Entry 小部件中输入文本。然后我们删除可能已经写入小部件中的任何内容,并使用 Python 字符串格式化pattern_num'Pattern %s'%pattern_num输入新的模式名称。最后,我们将 entry 小部件恢复到read only状态。 -
接下来的两行代码跟踪最后一个 Spinbox 小部件的编号。
-
接下来的四行代码实际上在名为
self.buttonpickleformat的二维列表中记录用户定义模式的当前状态。该列表首先初始化为一个空的二维矩阵,考虑到模式制作器的尺寸。 -
循环接着遍历当前模式中的每个按钮。如果按钮未被选中(不是绿色),它将保留值为
0。如果按钮被选中(绿色),相应位置的值将从0更改为active。使用这个列表,我们可以在以后轻松地重现用户定义的模式。 -
最后,所有这些与模式相关的数据都存储为字典的列表:
self.pattern_list[prevpval] = {'df': self.widget_drum_file_name, 'bl': self.buttonpickleformat, 'bpu':bpu, 'units':units} -
键
df存储鼓文件名列表。键bl存储按钮定义的模式。键bpu存储该模式的 BPU,键units存储该模式的单位。 -
现在所有这些与模式相关的项都已存储为字典,我们可以轻松地使用字典来重建模式。最后一行调用
reconstruct_pattern()方法,它实际上为我们完成了重建。
-
-
现在我们已经存储了模式记录,我们需要一种方法在鼓板上重建这些模式。我们定义了一个新的方法
reconstruct_pattern来处理它,如下所示(参见3.08.py中的代码):def reconstruct_pattern(self,pattern_num, bpu, units): self.widget_drum_file_name = [0]*MAX_DRUM_NUM try: self.df = self.pattern_list[pattern_num]['df'] for i in range(len(self.df)): file_name = self.df[i] if file_name == 0: self.widget_drum_name[i].delete(0, END) continue self.widget_drum_file_name.insert(i, file_name) drum_name = os.path.basename(file_name) self.widget_drum_name[i].delete(0, END) self.widget_drum_name[i].insert(0, drum_name) except: for i in range(MAX_DRUM_NUM): try: self.df except:self.widget_drum_name[i].delete(0, END) try: bpu = self.pattern_list[pattern_num]['bpu'] units = self.pattern_list[pattern_num]['units'] except: return self.bpu_widget.delete(0, END) self.bpu_widget.insert(0, bpu) self.units_widget.delete(0, END) self.units_widget.insert(0, units) self.create_right_pad() c = bpu * units self.create_right_pad() try: for i in range(MAX_DRUM_NUM): for j in range(c): if self.pattern_list[pattern_num]['bl'][i][j] == 'active': self.button[i][j].config(bg='green') except:return这段代码可以分为三个主要部分:
-
重建鼓样本上传
-
重建 BPU 和单位
-
重建节拍模式
在重建了这三样东西之后,我们可以轻松地重放任何节拍模式。以下是对每个部分的简要描述:
-
对于给定的模式,可以从字典项
self.pattern_list[pattern_num]['df']的键值对中轻松获取鼓文件名列表。然后我们遍历这个列表,并用每个鼓样本的文件名填充条目小部件。 -
然后,我们从字典键
self.pattern_list[pattern_num]['bpu']和self.pattern_list[pattern_num]['units']中获取 BPU 和单位的值。我们将这些值插入到相应的 Spinbox 小部件中,然后调用create_right_pad()方法,该方法在右侧面板上放置所需数量的按钮。 -
在最后一次迭代中,我们获取字典键
self.pattern_list[pattern_num]['bl']的值,它给出了绿色按钮的位置。通过循环迭代,我们检查是否需要将特定的按钮设置为active。如果是,我们改变按钮的颜色为绿色。 -
结合起来,我们现在可以加载之前记录的鼓样本,设置它们的单位和BPU值,并根据之前设置的值重建节拍模式。
-
在每个阶段,代码检查是否因为无效的文件标记而无法重建特定的模式。如果它发现一些无效的标记,它将使用适当的异常处理跳出代码。
-
点击播放按钮,鼓机将开始播放声音。更改模式编号并定义一个新的节拍模式。新模式将开始播放。回到旧的模式,旧的模式将再次播放(参考 3.08.py 中的代码)。
目标完成 - 简短总结
我们已经完成了代码编写,使鼓机支持存储多个节拍模式,并且只需更改模式编号即可播放这些模式。这使用户能够为歌曲的引子、副歌、桥段和其他部分制作不同的节奏。
在这个过程中,我们看到了如何使用 Python 的内置数据类型来存储自定义数据,并以任何需要的方式重现它们。
对象持久化
在先前的迭代中,我们添加了定义多个节拍模式的功能。然而,节拍模式只能在单个脚本运行时播放。当程序关闭并重新启动时,所有之前的模式数据都会丢失。
我们需要一种方法来持久化或存储节拍模式,使其超出单个程序运行的范围。我们需要将值以某种形式存储在文件存储中,并重新加载、播放甚至编辑模式。我们需要某种形式的对象持久化。
准备起飞
Python 提供了几个用于对象持久化的模块。我们将用于持久化的模块称为pickle 模块。这是一个 Python 的标准库。
在 Python 中,表示为字节串的对象称为pickle。Pickling,也称为对象序列化,允许我们将我们的对象转换为字节串。从字节串重新构建对象的过程称为unpickling或反序列化。
关于pickle模块的更多信息可在docs.python.org/2/library/pickle.html找到。
让我们用一个简单的例子来说明:
import pickle
party_menu= ['Bread', 'Salad', 'Bordelaise','Wine', 'Truffles']
pickle.dump(party_menu, open( "mymenu.p", "wb" ) )
首先,我们使用pickle.dump序列化或 pickle 我们的列表PartyMenu,并将其保存到外部文件mymenu.p中。
我们稍后使用pickle.load检索对象:
import pickle
menu= pickle.load( open( "mymenu.p", "rb" ) )
print menu # ['Bread', 'Salad', 'Bordelaise', 'Wine', 'Truffles']
记住,在我们之前的迭代中,我们创建了一个列表,称为self.pattern_list,其中列表的每个项目都是一个字典,用于存储关于单个节拍模式的详细信息。
如果我们需要重用这些信息,我们只需要 pickle 这个self.pattern_list。保存了对象之后,我们稍后可以轻松地反 pickle 文件来重建我们的节拍模式。
启动推进器
-
我们首先需要向我们的程序中添加三个顶级菜单项,如下面的截图所示:
![Engage Thrusters]()
-
文件 | 加载项目
-
文件 | 保存项目
-
文件 | 退出
当我们创建菜单项时,让我们也添加一个关于菜单项:
- 关于 | 关于
在这里,我们特别关注保存项目(pickle)和加载项目回(unpickle)。菜单项的代码定义在名为
create_top_menu的单独方法中,如下面的代码所示(也请参阅3.09.py中的代码):def create_top_menu(self): self.menubar = Menu(self.root) self.filemenu = Menu(self.menubar, tearoff=0 ) self.filemenu.add_command(label="Load Project",command=self.load_project ) self.filemenu.add_command(label="Save Project",command=self.save_project) self.filemenu.add_separator() self.filemenu.add_command(label="Exit",command=self.exit_app) self.menubar.add_cascade(label="File",menu=self.filemenu) self.aboutmenu = Menu(self.menubar, tearoff=0 ) self.aboutmenu.add_command(label="About",command=self.about) self.menubar.add_cascade(label="About",menu=self.aboutmenu) self.root.config(menu=self.menubar)代码是自我解释的。我们在过去两个项目中创建了类似的菜单项。最后,为了显示这个菜单,我们从
Main方法中调用这个方法。 -
-
要序列化我们的对象,我们首先将
pickle模块导入当前命名空间,如下所示(参见3.09.py中的代码):import pickle保存项目菜单有一个
command回调函数附加到self.save_project,这是我们定义序列化过程的地方:def save_project(self): self.record_pattern() #make sure last pattern is recorded file_name = tkFileDialog.asksaveasfilename(filetypes=[('Drum Beat File','*.bt')] , title="Save project as...") pickle.dump(self.pattern_list,open( file_name, "wb" ) "wb" ) ) self.root.title(os.path.basename(filenamefile_name) + " - DrumBeast")代码的描述如下:
-
回想一下,只有在用户更改模式编号时,模式才会添加到
self.pattern_list中。在用户可能已经定义了打击模式但可能没有点击模式编号的 Spinbox 小部件的情况下,该模式不会包含在self.pattern_list中。为了确保它被添加,我们首先调用self.record_pattern来捕获这个打击模式。 -
当用户点击保存项目菜单时,会调用
save_project方法,因此我们需要给用户一个选项将项目保存到文件中。我们选择定义一个新的文件扩展名(.bt)来跟踪我们的打击模式。 -
当用户指定具有
.bt扩展名的文件名时,使用pickle.dump将self.pattern_list对象中的数据写入文件。 -
最后,将 Toplevel 窗口的标题更改以反映文件名。
-
-
我们已经完成了对象的序列化。现在让我们编写反序列化过程。
反序列化过程由一个名为
load_project的方法处理,该方法从加载项目菜单调用如下:def load_project(self): file_name = tkFileDialog.askopenfilename(filetypes=[('Drum Beat File','*.bt')], title='Load Project') if file_name == '':return self.root.title(os.path.basename(file_name) + " - DrumBeast") fh = open(file_name,"rb") # open the file in reading mode try: while True: # load from the file until EOF is reached self.pattern_list = pickle.load(fh) exceptEOFError: pass fh.close() try: self.Reconstruct_pattern(0,pattern_listself.pattern_list[0]['bpu'],pattern_listself.pattern_list[0]['units']) except: tkMessageBox.showerror("Error","An unexpected erroroccurred trying to reconstruct patterns")代码的描述如下:
-
当用户点击加载项目菜单时,方法的第一行会弹出一个打开文件窗口。当用户指定一个具有
.bt扩展名的已保存文件时,文件名被存储在一个名为file_name的变量中。 -
如果返回的文件名为
none,因为用户取消了打开文件对话框,则不执行任何操作。 -
如果提供了文件名,Toplevel 窗口的标题会更改以添加文件名。然后以读取模式打开文件,并将文件内容读取到
self.pattern_list中,使用pickle.load。 -
self.pattern_list现在包含了之前序列化中定义的打击模式列表。文件被关闭,self.pattern_list中的第一个模式在鼓机中重建。如果在序列化文件中定义了多个模式,你可以通过更改模式编号的 Spinbox 小部件来查看每个模式。 -
尝试播放任何模式,你应该能够精确地回放该模式,就像在保存时定义的那样。
小贴士
虽然序列化很棒,但序列化(pickle)容易受到恶意或错误数据的影响。你可能只想在数据来自可信来源或设置了适当的验证机制时使用 pickle。
你也可能发现
json模块对于序列化JSON和ElementTree对象很有用,或者对于解析 XML 数据的xml.minidom库相关。
-
-
现在,让我们完成我们的
exit和about命令的编码:def about(self): tkMessageBox.showinfo("About", "About Info") def exit_app(self): if tkMessageBox.askokcancel("Quit", "Really Quit?"): self.root.destroy()并将此行添加到我们的
app方法中,以覆盖 Toplevel 窗口的关闭按钮:self.root.protocol('WM_DELETE_WINDOW', self.exit_app)这一点不言自明。我们在之前的项目中已经进行了类似的编码。
目标完成 – 简短总结
在这次迭代中,我们使用了 Python 内置的 pickle 模块来序列化和反序列化用户定义的节奏模式。
这现在使我们能够保存用户定义的模式。我们还提供了加载、回放和编辑项目的功能。
现在,如果你在程序中定义了一个或多个节奏模式,你可以使用 .bt 文件扩展名保存项目。你可以在以后加载项目,并从你上次停止的地方开始工作。
在处理顶部菜单的同时,我们也完成了 关于 和 退出 菜单项的代码。
ttk 主题小部件
我们几乎完成了鼓机的编程。然而,我们希望通过介绍 ttk 主题小部件来结束这个项目。
准备发射升空
在许多平台如 Windows 和 X11 上,Tkinter 并未绑定到本地平台小部件。Tk 工具包(以及 Tkinter)最初出现在 X-Window 系统上,因此,它采用了 Motif 风格,这是 X-Window 系统上 GUI 开发的既定标准。当 Tk 被移植到其他平台,如 Windows 和 Mac OS 时,这种 Motif 风格开始与这些平台的外观格格不入。
由于这一点,有些人甚至认为 Tkinter 小部件相当丑陋,并且与这样的桌面环境不太融合。
Tkinter 的另一个批评基于这样一个事实:Tkinter 允许通过将逻辑和样式作为小部件选项来更改,从而将逻辑和样式混合在一起。
它还被批评缺乏任何类型的主题支持。虽然我们通过选项数据库看到了集中式样式的示例,但这种方法要求在组件级别进行样式设置。例如,它不允许对两个按钮组件进行不同的选择性样式设置。这使得开发者在实现类似组的小部件的视觉一致性时区分它们与其他组的小部件变得困难。因此,许多 GUI 开发者转向了 Tkinter 的替代品,如 wxPython、glade、PyQT 等。
在 Tkinter 8.5 中,Tkinter 的制作者通过引入 ttk 模块来尝试解决所有这些担忧,ttk 模块可以被视为对原始 Tkinter 模块的一种改进。
让我们来看看 ttk 主题小部件模块提供的某些功能。
ttk 做的第一件事是提供一组内置主题,允许 Tk 小部件看起来像应用程序正在运行的本地桌面环境。
此外,它还引入了六个新的小部件:Combobox、Notebook、Progressbar、Separator、Sizegrip 和 Treeview,并将它们添加到小部件列表中,同时支持 11 个核心 Tkinter 小部件,包括 Button、Checkbutton、Entry、Frame、Label、LabelFrame、Menubutton、PanedWindow、Radiobutton、Scale 和 Scrollbar。
要使用 ttk 模块,我们首先将其导入当前命名空间:
import ttk
你可以按如下方式显示 ttk 小部件(参见 3.10.py 中的代码):
ttk.Button(root, text='ttk Button').grid(row=1, column=1)
ttk.Checkbutton(root, text='tkCheckButton').grid(row=2, column=1)
要比较正常 Tkinter 小部件和对应 ttk 小部件之间的显示效果,请参阅 3.10.py 中的代码,该代码生成一个窗口,如下面的截图所示。注意小部件在你的平台上看起来如何更像本地小部件。

3.10.py 还展示了 ttk 模块中引入的所有新小部件的示例。
小贴士
你甚至可以通过以下方式在导入 Tkinter 之后导入 ttk 来覆盖基本 Tkinter 小部件:
from Tkinter import *
from ttk import *
这会导致所有属于 Tk 和 ttk 的公共小部件都被 ttk 小部件替换。
这直接的好处是使用新小部件,这为跨平台提供了更好的外观和感觉。
然而,这种导入方式的缺点是,你无法区分小部件类是从哪个模块导入的。这很重要,因为 Tkinter 和 ttk 小部件类并不完全可互换。在这种情况下,一个明确的解决方案是按照以下代码导入它们:import Tkinter as tk
import ttk
尽管 Tkinter 和 ttk 小部件的大多数配置选项都是通用的,但 ttk 主题小部件不支持 fg、bg、relief、border 等样式选项。这是故意从 ttk 中移除的,以尝试将逻辑和样式分开。
相反,所有与样式相关的选项都由相应的样式名称处理。在标准的 ttk 模块中,每个小部件都有一个关联的样式名称。你可以使用 widget.winfo_class() 方法检索小部件的默认样式名称。
例如,考虑一个 ttk 按钮:
>>> b = ttk.Button()
>>> b.winfo_class()
这会打印出 Tbutton,这是 ttk.Button 的默认样式名称。有关不同小部件的默认 ttk 样式名称列表,请参阅 附录 B 中的 The ttk widgets 部分,快速参考表。
除了默认样式外,你还可以将自定义样式类分配给小部件或小部件组。要设置新样式,请使用以下命令:
x = ttk.Style()
要配置默认样式的样式选项,请使用以下命令:
x.configure('mystyle.Defaultstyle', **styling options)
要在某个小部件上使用新样式,请使用以下命令:
ttk.Widget(root, style='mystyle.Defaultstyle')
接下来我们将讨论 ttk 主题。
样式用于控制单个小部件的外观。另一方面,主题控制整个 GUI 的外观。更简单地说,主题是一组样式的集合。将样式分组到主题中允许用户一次性切换整个 GUI 的设计。像样式一样,所有主题都通过它们的名称唯一标识。可以通过以下方式获取可用主题的列表:
>>> x = ttk.Style()
>>>x.theme_names()
('winnative', 'clam', 'alt', 'default', 'classic', 'xpnative')
要获取当前活动主题的名称:
>>>x.theme_use()
'xpnative'
你可以使用以下命令切换到另一个主题:
x.theme_use('yournewthemename')
让我们通过一个示例(参考 3.11.py 中的代码,该代码生成如下截图所示的窗口)来查看 ttk 的各种样式和主题相关选项:

from Tkinter import *
import ttk
root= Tk()
x = ttk.Style()
x.configure('.', font='Arial 14', foreground='brown', background='yellow')
x.configure('danger.TButton', font='Times 12', foreground='red', padding=1)
ttk.Label(root, text='global style').pack()
ttk.Button(root, text='custom style', style='danger.TButton').pack()
# Different styling for different widget states
x.map("s.TButton", foreground=[('pressed', 'red'), ('active', 'blue')])
ttk.Button(text="state style", style="s.TButton").pack()
# Overriding current theme styles
curr_theme = x.theme_use()
x.theme_settings(curr_theme, { "TEntry": { "configure": {"padding": 2}, "map": {"foreground": [("focus", "red")]} }})
ttk.Entry().pack()
root.mainloop()
代码的描述如下列所示:
-
代码的前三行导入
Tkinter和ttk,并设置一个新的根窗口。 -
下一行
x = ttk.Style()是您为您的样式赋予名称x的地方。 -
下一行使用
x.configure配置了一个程序范围内的样式配置。配置的第一个参数点字符(.)表示此样式将应用于 Toplevel 窗口及其所有子元素。这就是为什么所有我们的小部件都得到了黄色的背景。 -
下一行创建了一个默认样式(
TButton)的扩展(danger)。这就是创建自定义样式的方法,它是基于基本默认样式的变体。 -
下一行创建了一个
ttk.label小部件。由于我们没有为这个小部件指定任何样式,它继承了为 Toplevel 窗口指定的全局样式。 -
下一行创建一个
ttk.button小部件,并指定它使用我们自定义的样式定义'danger.TButton.'。这就是为什么这个按钮的前景色变成了红色。注意,它仍然继承了我们在之前定义的全局 Toplevel 样式的背景颜色,黄色。 -
下一两行代码演示了 ttk 如何允许对不同的小部件状态进行样式化。在这个例子中,我们对一个
ttk.button小部件的不同状态进行了样式化,以显示不同的颜色。请点击这个第二个按钮,看看不同的样式如何应用于按钮的不同状态。在这里,我们使用map(style, query_options, **kw)来指定小部件状态变化时的动态样式值。 -
下一行获取当前适用的主题。然后使用以下方式覆盖主题的 Entry 小部件的一些选项:
x.theme_settings('themename', ***options)
现在我们知道了如何使我们的小部件看起来更像原生平台小部件,让我们将鼓机的播放和停止按钮从 Tkinter Checkbutton 更改为ttk.button。同时,也将循环复选按钮从 Tkinter Checkbutton 更改为 ttk Checkbutton。
启动推进器
-
我们首先将
ttk导入我们的命名空间,并将ttk附加到play和stop按钮上,如下所示(参见3.12.py中的代码):import ttk -
然后,我们简单地修改
create_play_bar方法中的按钮和复选按钮,如下所示:button = ttk.Button() loopbutton = ttk.Checkbutton(**options)注意
注意,这些更改使按钮和复选按钮看起来更接近您工作平台的原生小部件。
此外,请注意,我们无法修改我们在模式编辑器中使用的 Tkinter 按钮。这是因为我们的代码大量使用按钮的背景颜色来决定逻辑。ttk 按钮没有可配置的
bg选项,因此不能用于我们的右侧鼓垫按钮。 -
作为快速结束练习,让我们在播放条的右侧添加一个图像。同时,也添加一个 Toplevel 窗口的图标(参见
3.12.py中的代码):要添加图像,我们将以下内容添加到
create_play_bar方法中:photo = PhotoImage(file='images/sig.gif') label = Label(playbar_frame, image=photo) label.image = photo label.grid(row=ln, column=35, padx=1, sticky=E)要添加一个顶层图标,我们在
Main方法中添加以下行:if os.path.isfile('images/beast.ico'):self.root.iconbitmap('images/beast.ico')
目标完成 – 简短总结
这标志着这个项目的最后迭代。在这个迭代中,我们首先了解了如何以及为什么使用 ttk 主题小部件来改善我们程序的外观和感觉。
我们在鼓程序中使用了 ttk 按钮和 ttk 复选框按钮来改善其外观。我们还了解了为什么我们程序中的某些 Tkinter 按钮不能被 ttk 按钮替换。
任务完成
在 Tkinter 的实验中我们已经走了很长的路。在这个项目中,我们制作了一个功能强大的鼓机,拥有众多特性。
在这个过程中,我们触及了构建 Tkinter GUI 程序所需的一些关键概念。
总结来说,我们探讨了 Tkinter 基于 GUI 程序的关键概念:
-
将 Tkinter 程序作为类和对象来构建
-
与 Tkinter 的 Spinbox、Button、Entry 和 Checkbutton 等小部件一起工作
-
使用
grid布局管理器来构建复杂的布局 -
理解与 Tkinter 相关的线程编程
-
与 Python 标准库中的其他常见模块一起工作
-
使用
pickle模块实现对象持久化 -
与 ttk 主题小部件一起工作
热身挑战
鼓机需要你的关注。作为你的热身挑战的一部分,请向你的鼓机添加以下功能:
-
当前应用程序检查按钮是否为绿色来决定按钮是否处于按下状态。修改代码,使这种逻辑不是基于按钮的颜色,而是通过一个单独的变量来跟踪选中的按钮。
-
在你的鼓机中添加一个节拍速度刻度,用户可以使用滑块来改变节拍的速度。
-
为每个鼓样本添加音量控制,允许用户分别调整每个鼓样本的音量。
-
为每个鼓样本添加一个静音按钮。如果用户点击了某个鼓样本的 Checkbutton,则该行的声音将不会播放。这样,用户可以停止整行播放,而无需更改该行的模式。
-
在你的鼓机中添加一个计时器,显示自上次按下播放按钮以来经过的时间。
第四章:棋盘游戏
现在我们将在 Tkinter 中构建一个棋盘游戏。你不需要成为棋艺大师就能构建这个游戏。如果你曾经玩过棋类游戏,并且了解控制棋子的基本规则,你就准备好编写这个程序了。
如果你从未玩过棋类游戏,并且不知道基本规则,你最好在开始编程这个应用程序之前先从互联网上阅读这些规则。
任务简报
在其最终形式中,我们的棋盘游戏将看起来像以下截图:

我们的棋盘游戏将强制执行适用于棋类游戏的所有标准规则。一些高级规则和功能留给你作为练习来完成。
为什么它如此出色?
在构建我们的棋盘游戏应用的过程中,我们接触到了 Tkinter Canvas 小部件,这被认为是 Tkinter 中最强大和最灵活的功能之一。
正如你在项目过程中将看到的,Canvas 小部件是一个真正的强大工具,对于 GUI 程序员来说非常有用。它可以用来使用线条、矩形、椭圆和多边形绘制复合对象。它还将允许你以极高的精度在画布上定位图像。
此外,Canvas 小部件将允许你将任何其他小部件(如标签、按钮、刻度和其他小部件)放置在其上。这使得它成为容纳各种不同 GUI 程序小部件的理想容器。
除了学习 Canvas 小部件外,你还将了解如何使用 Python 内置类型来结构化你的数据。你还将被介绍到涉及选择相关对象并将它们按适当的粒度组织到类和模块中的概念。
随着应用程序的发展,我们还介绍了几个你将在各种应用程序开发项目中经常使用的 Python 模块。
你的高目标
以下是这个项目的关键目标:
-
如何将程序结构化为模型和视图组件
-
如何用期望的符号表示问题域
-
探索 Tkinter Canvas 小部件的灵活性和强大功能
-
Canvas 坐标、对象 ID 和标签的基本用法
-
如何处理 Tkinter photo image 类不支持的新图像格式
-
GUI 程序中逻辑层和表示层之间的典型交互
任务清单
在我们的程序中,我们需要处理 PNG 图像。Tkinter photo image 类和其他 Python 标准库不支持 PNG 处理。我们将使用Python Imaging Library(PIL)来渲染 PNG 文件。
要安装 PIL 包,请访问:
www.pythonware.com/products/pil/
如果你正在 Windows x64(64 位)或 MacOSX 机器上工作,你可能需要安装并使用 Pillow,它是 PIL 的替代品,从:
www.lfd.uci.edu/~gohlke/pythonlibs/#pillow
在你安装了包之后,转到你的 Python 交互式提示符并输入:
>>from PIL import ImageTk
如果没有错误信息执行,你就准备好制作棋类应用了。
构建我们的程序结构
我们的所有先前项目都结构化为一个单独的文件。然而,随着程序复杂性的增加,我们需要将我们的程序分解为模块和类结构。
大型应用程序的开发通常从记录软件需求规格说明书(SRS)开始。这通常随后是使用几个建模工具对结构进行图形表示,例如类、组合、继承和信息隐藏。这些工具可以是流程图、统一建模语言(UML)、数据流图、维恩图(用于数据库建模)以及几种其他工具。
当问题域不是很清晰时,这些工具非常有用。然而,如果你曾经玩过棋类游戏,你对问题域非常熟悉。此外,我们的棋类程序可能被归类为中等规模程序,跨越几百行代码。因此,让我们跳过这些视觉工具,直接进入实际程序设计。
准备起飞
在这个迭代中,我们决定我们程序的整体结构。
在面向对象编程(OOP)的真正精神中,让我们首先列出我们将在程序中遇到的物体类型。直观地看一个棋盘告诉我们,我们有两组对象需要处理:
-
棋盘:它是一个 8 x 8 的方格棋盘,有交替着色的方格
-
棋子:它们是国王、王后、主教、骑士、车和兵
随着我们继续前进,我们可能会遇到也可能不会遇到其他对象。但我们肯定会遇到这两种对象。因此,无需进一步延迟,让我们在我们的项目文件夹中创建两个名为chessboard.py和pieces.py的文件。(见代码文件夹 4.01)
我们将使用这两个文件来定义相应的类,以保持与这两个对象相关的逻辑。请注意,这些文件将不会显示棋盘或其棋子;它将保留所有与棋盘和棋子相关的逻辑。在编程术语中,这通常被称为模型。
棋盘和棋子的实际显示将保存在一个单独的文件中,该文件将处理与程序相关的所有视图。
小贴士
将逻辑与表示分离的规则不仅应该用于决定你的文件结构,还应该在定义文件内的方法时应用。
每次你编写一个方法时,都尽量将其表示与逻辑分离。如果你发现一个方法混合了逻辑和表示,重构你的代码以分离这两个部分。避免将表示和逻辑耦合到同一个方法中。
将表示层(视图)与逻辑(模型)分开是一个好主意。因此,我们将创建一个名为 gui.py 的新文件来编写程序的所有可见组件,包括所有小部件。此文件将主要负责生成视图。
注意
除了模型和视图文件外,许多程序还保留一个单独的控制器文件,以将程序的行为方面与逻辑(模型)和展示(视图)解耦。这种结构分离被称为 模型-视图-控制器(MVC)编程风格。
然而,我们的棋类程序只有一个事件需要处理:移动棋子的鼠标点击。仅为此事件创建一个单独的控制器可能会使程序比应有的更复杂。
考虑到这种限制,我们将从名为 GUI 的单个类中处理展示(视图)和事件处理(控制器)。
现在我们已经准备好了文件结构,让我们开始编码。首先,让我们为棋盘编写 GUI 类,如下面的截图所示。因为这与视图部分相关,所以我们将此代码放在 gui.py 文件中。

启动推进器
第 1 步 – 创建 GUI 类
我们首先创建一个 GUI 类,并分配诸如行、列、方格颜色以及每个方格的像素尺寸等属性。我们初始化 GUI 类以创建棋盘的画布,如下所示(见 代码 4.01 gui.py):
from Tkinter import *
class GUI():
rows = 8
columns = 8
color1 = "#DDB88C"
color2 = "#A66D4F"
dim_square = 64
def __init__(self, parent):
self.parent = parent
canvas_width = self.columns * self.dim_square
canvas_height = self.rows * self.dim_square
self.canvas = Canvas(parent, width=canvas_width, height=canvas_height, background="grey")
self.canvas.pack(padx=8, pady=8)
self.draw_board()
代码的描述如下:
-
我们创建一个类
GUI来处理视图文件的渲染。GUI类的init方法在对象实例化时立即被调用。init方法设置所需大小的 Canvas 小部件。这个画布将作为我们所有对象的容器,例如棋盘方格区域和最终的棋子。 -
我们使用了 Canvas 小部件作为容器,因为它为我们提供了处理基于事件精确位置坐标的任务的能力,例如鼠标按钮的点击。
-
然后
init方法调用draw_board()方法,该方法负责创建类似棋盘的交替颜色的方块。
第 2 步 – 创建棋盘
现在,我们使用 canvas.create_rectangle 方法在棋盘上绘制方格,交替填充我们之前定义的两种颜色。
def draw_board(self):
color = self.color2
for r in range(self.rows):
color = self.color1 if color == self.color2
else self.color2 # alternating between two colors
for c in range(self.columns):
x1 = (c * self.dim_square)
y1 = ((7-r) * self.dim_square)
x2 = x1 + self.dim_square
y2 = y1 + self.dim_square
self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, tags="area")
color = self.color1 if color == self.color2 else self.color2
代码的描述如下:
-
要在棋盘上绘制方格,我们使用
canvas.create_rectangle()方法,该方法根据矩形的对角线相对角(上左和下右边缘的坐标)绘制矩形。 -
我们需要针对棋盘进行操作。因此,我们在棋盘上创建的每个方格上添加一个名为
area的标签。这与我们在文本编辑程序中进行的文本小部件的标记类似。
第 3 步 – 创建 Tkinter 主循环
现在,我们将创建 Tkinter 主循环,如下所示:
def main():
root = Tk()
root.title("Chess")
gui = GUI(root)
root.mainloop()
if __name__ == "__main__":
main()
代码的描述如下:
- 在类外部,我们有一个主方法,它设置 Toplevel 窗口,启动 Tkinter 主循环,实例化一个
GUI对象,并调用drawboard()方法。
注意
Tkinter Canvas 小部件允许你在指定的坐标位置绘制线条、椭圆形、矩形、圆弧和多边形形状。你还可以指定各种配置选项,例如填充、轮廓、宽度和这些形状的其它几个选项。
此外,Canvas 小部件还有一个巨大的方法列表和可配置选项。要获取与画布相关的完整选项列表,请在 Python 交互式 shell 中输入以下内容:
>>> import Tkinter
>>> help(Tkinter.Canvas)
你还可以在核心 Python 安装目录中访问 Tkinter 的文档。文档位于path\to\python\installation\Doc\Python273。
这是一个编译的 HTML 帮助文件。在帮助文件中搜索 Tkinter,你可以获得一个全面的参考,其中包含所有小部件的详细信息。
目标完成 – 简短总结
这完成了我们的第一次迭代。在这个迭代中,我们决定了象棋程序类的结构。我们创建了一个GUI类,并添加了通常期望棋盘拥有的属性。
我们也尝到了 Canvas 小部件的第一口。我们创建了一个空白画布,然后使用canvas.create_rectangle方法添加方形区域来创建我们的棋盘。
我们还创建了 Tkinter 主循环,并在主循环中从GUI类创建了一个对象。现在,如果你运行code 4.01 gui.py,你会看到一个棋盘。
分类情报
Canvas 小部件附带丰富的方法和可配置选项。然而,关于 Canvas 小部件有三个重要事项需要注意:
-
它使用坐标系来指定小部件上对象的位置。坐标以像素为单位测量。画布的左上角坐标为(0,0)。
-
它提供了添加图像和绘制基本形状(如线条、圆弧、椭圆形和多边形)的方法。
-
绘制在 Canvas 小部件上的对象通常通过分配一个 ID 或标签来处理。
结构化棋盘和棋子相关数据
在我们的鼓程序中,我们决定使用一种符号来描述一组节奏模式。然后我们可以存储(pickle)这种节奏模式符号,并在以后重新生成(unpickle)。象棋程序也是如此。它也需要一种合适的符号来描述棋子和它们在棋盘上的位置。
准备起飞
我们可以为表示棋子和它们的棋盘位置定义自己的符号,但结果证明,已经存在一个全球公认、简单、紧凑且标准的棋盘表示符号。这种符号称为福斯思-爱德华斯符号(FEN),可在en.wikipedia.org/wiki/Forsyth-Edwards_Notation找到。
我们可能已经决定定义我们的符号,但在这里我们更倾向于不重新发明轮子。
棋局起始位置的 FEN 记录写作如下:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
关于这种记法的要点如下:
-
这种记法显示了一个棋局的六个记录。每个记录之间由一个空格分隔。
-
第一条记录显示了棋盘上棋子的位置。棋盘的每一行(等级)都由一个由
/符号分隔的区域表示。 -
在第一条记录中,每个棋子由一个单独的字母识别(兵 =
p,马 =n,象 =b,车 =r,后 =q和王 =k)。 -
白方棋子使用大写字母表示(
PNBRQK),但黑方棋子使用小写字母表示(pnbrqk)。 -
没有棋子的方格使用数字
1到8(空白方格的数量)表示。 -
第二条记录表示玩家的回合。字母
w表示白方回合,字母b表示黑方回合。 -
第三条记录
KQkq表示王车易位功能是否可用。如果没有王车易位,则为-。否则,它包含一个或多个字母:K(白方可以王车易位至王翼),Q(白方可以王车易位至后翼),k(黑方可以王车易位至王翼),以及/或q(黑方可以王车易位至后翼)。 -
第四条记录
_捕获游戏中的吃过路兵细节。我们将在游戏中不实现王车易位和吃过路兵功能,因此现在可以安全地忽略这两个记录。 -
第五条记录跟踪棋局的半回合时钟。半回合时钟跟踪自上次兵的推进或上次捕获以来的回合数。这用于确定是否可以根据五十回合规则要求和棋。
-
第六条记录跟踪全回合数,每次黑方走棋后增加 1。这用于跟踪游戏的总长度。
如前所述的记法可以如下沿 x 轴和 y 轴表示:

使用这种记法,我们可以准确地表示棋盘上的任何特定方格。
棋子的颜色取决于字母是否为小写(黑色)或大写(白色)。
因此 A1 表示棋盘上最底部的最左方方格。目前,它被一个白方车占据。C3 位置目前为空,E8 有黑方王,A8 有黑方车。
依照这些规则,以下是如何在以下指示性回合后更改 FEN 记法(en.wikipedia.org/wiki/Forsyth-Edwards_Notation):
第一步棋后,兵从 P 移至 e4:
rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1
第二步棋后,兵从 p 移至 c5:
rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2
第三步棋后,将棋子从 N 移至 f3:
rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2
我们所有的棋盘和棋子相关逻辑都将使用前面的表示法。因此,在我们继续编写游戏代码之前,完全理解这个表示法非常重要。
既然我们已经清楚地了解了前面的表示法,让我们将这个表示法应用到表示我们的棋盘上。关键思想是,给定一个 FEN 表示法,我们应该能够在棋盘上表示它。
启动推进器
第 1 步 - 创建棋子超类
现在,让我们首先编写pieces.py的模型代码(见代码 4.02 pieces.py),通过创建一个Piece超类如下:
class Piece():
def __init__(self, color):
if color == 'black':
self.shortname = self.shortname.lower()
elif color == 'white':
self.shortname = self.shortname.upper()
self.color = color
def ref(self, board):
''' Get a reference of chessboard instance'''
self.board = board
代码描述如下:
-
我们定义了一个名为
Piece的类。它的__init__方法,接受一个颜色作为参数。根据我们的 FEN 表示法,它将黑棋的简称改为小写字母,白棋的简称改为大写字母。颜色处理在超类Piece中完成,因为它是对所有棋子的共同特性。 -
我们还定义了一个名为
ref的方法。它的唯一目的是获取棋盘实例到对象命名空间中,以便棋盘和棋子可以交互。我们需要这个方法,因为我们的棋子最终将与棋盘交互。因此,我们需要在Piece类中有一个棋盘实例的引用。
第 2 步 - 为所有棋子创建单独的子类
我们可以创建所有棋子的单独子类如下:
class King(Piece): shortname = 'k'
class Queen(Piece): shortname = 'q'
class Rook(Piece): shortname = 'r'
class Knight(Piece): shortname = 'n'
class Bishop(Piece): shortname = 'b'
class Pawn(Piece): shortname = 'p'
代码描述如下:
-
我们为棋盘上找到的每个棋子定义了类。因此,我们有名为
King、Queen、Rook、Knight、Bishop和Pawn的类。这些类是从Piece超类派生出来的。 -
目前,这些子类仅定义了与它们相关的简称。我们将在以后扩展这些子类,以定义和强制执行每个这些棋子的移动规则。
第 3 步 - 定义一个返回棋子实例的方法
我们将定义一个方法来返回棋子实例,如下所示:
import sys
SHORT_NAME = {'R':'Rook', 'N':'Knight', 'B':'Bishop', 'Q':'Queen', 'K':'King', 'P':'Pawn'}
def create_piece(piece, color='white'):
if piece in (None, ''): return
if piece.isupper(): color = 'white'
else: color = 'black'
piece = SHORT_NAME[piece.upper()]
module = sys.modules[__name__]
return module.__dict__piece
代码描述如下:
-
代码定义了一个字典,其中包含棋子的简称和全名作为键值对。
-
然后,我们定义了一个名为
piece的方法,它接受一个棋子简称并返回相应的棋子实例。
第 4 步 - 创建 Board 类
现在我们已经准备好了一个基本的棋子模型,让我们编写代码来处理它们在棋盘上的放置。我们在chessboard.py中编写这个模型(见代码 4.02 chessboard.py),通过创建一个Board类如下:
import pieces
import re
START_PATTERN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0 1'
class Board(dict):
y_axis = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H')
x_axis = (1,2,3,4,5,6,7,8)
def __init__(self, patt = None):
self.process_notation(START_PATTERN)
代码描述如下:
-
我们的代码从定义根据前面讨论的 FEN 表示法开始的起始模式。我们不包含王车易位和过路兵相关的表示法,因为我们不会在我们的程序中编写这些。
-
然后,我们将
Board类定义为内置dict类型的子类。这是因为我们将存储模式作为字典。 -
然后,我们将
x_axis和y_axis定义为棋盘的非不可变元组。 -
我们类的
__init__方法只是简单地调用类的process_notation方法。
第 5 步 – 在棋盘上显示给定 FEN 表示的棋子
对于给定 FEN 表示的Board上的棋子,可以按以下方式显示:
def process_notation(self, patt):
self.clear()
patt = patt.split('')
# expand_whitespaces blanks
def expand_whitespaces(match): return '' * int(match.group(0))
patt[0] = re.compile(r'\d').sub(expand_whitespaces, patt[0])
for x, row in enumerate(patt[0].split('/')):
for y, alphabet in enumerate(row):
if alphabet == '': continue
xycoord = self.alpha_notation((7-x,y))
self[xycoord] = pieces.piece(alphabet)
self[xycoord].ref(self)
if patt[1] == 'w': self.player_turn = 'white'
else: self.player_turn = 'black'
代码的描述如下:
-
process_notation方法的任务是首先将表示为整数的空白空间扩展为实际空间。它使用 Python 内置的正则表达式模块(re)来扩展给定 FEN 表示中的空白空间。 -
代码
expand_whitespaces做了一些对于 Python 初学者可能有点棘手的事情。它将每个数字替换为相应数量的空白空间,这样你就可以假设空白空间是一个空方格。然后,它将 FEN 表示转换为对应于每个棋子的 x 和 y 字母数字坐标的字符串。为此,它调用另一个名为alpha_notation的方法,该方法在第 7 步中定义。 -
最后两行记录了玩家的回合。
第 6 步 – 检查给定坐标是否在棋盘上
最后,让我们通过定义一个方法来检查给定坐标是否在棋盘上结束这次迭代,如下所示(参见 代码 4.02 chessboard.py):
def is_on_board(self, coord):
ifcoord[1] < 0 or coord[1] > 7 or coord[0] < 0 or coord[0] >7:
return False
else: return True
第 7 步 – 生成字母和数字表示
我们需要一种方法来将棋子的 x 和 y 坐标转换为字母等效表示,例如,A1,D5,E3,等等。因此,我们定义了alpha_notation方法如下:
def alpha_notation(self,xycoord):
if not self.is_on_board(xycoord): return
return self.y_axis[xycoord[1]] + str(self.x_axis[xycoord[0]])
类似地,我们定义了一个方法,它接受 x,y 坐标作为输入,并返回其等效的数字表示,如下所示:
def num_notation(self, xycoord):
return int(xycoord[1])-1, self.y_axis.index(xycoord[0])
第 8 步 – 检查棋盘上占据的位置
在每次移动之前,我们需要检查给定颜色所有棋子所占据的所有位置。这不仅是为了计算有效移动,而且为了确保其他棋子的移动不会对国王造成将军。
因此,让我们定义一个方法来返回给定颜色所占据的坐标列表(参见 代码 4.02 chessboard.py),如下所示:
def occupied(self, color):
result = []
for coord in self:
if self[coord].color == color:
result.append(coord)
return result
第 9 步 – 处理错误和异常
为了处理错误和异常,我们定义了一个名为ChessError的自定义异常类,所有其他异常都将后来子类化到它,如下所示:
classChessError(Exception): pass
目标完成 – 简短总结
在这次迭代中,我们创建了一个基本的Piece类和每个在棋盘上找到的棋子的虚拟子类。单个棋子类从父Piece类继承。我们在父类中处理颜色识别,因为这是我们需要为所有子类做的事情。
然后,我们定义了我们的Board类,并添加了一些我们每次在棋盘上移动棋子时都需要的某些方法。
我们还没有在棋盘上显示这些棋子。我们将在下一次迭代中这样做。
在棋盘上添加棋子
我们现在有一个将 FEN 符号转换为基于坐标的扩展表示的代码。现在,让我们编写代码来在棋盘上显示棋子,基于给定的 FEN 符号,如下截图所示:

准备起飞
我们将使用 PNG 图像来显示棋盘上的棋子。我们在名为 pieces_image 的文件夹中为每个棋子提供了 PNG 图像。图像是通过在每个棋子的短名后附加棋子的颜色来命名的。例如,黑后保存为 qblack.png,白骑士保存为 nwhite.png。
我们选择 PNG 而不是 GIF,因为与 GIF 不同,PNG 允许使用 alpha 通道(可变透明度)、在不同平台上自动进行伽玛校正以及颜色校正。
然而,TkinterPhotoImage 类不支持 PNG 格式。因此,我们使用 PIL 来处理 PNG 文件。
小贴士
目前,TkinterPhotoImage 类仅支持 GIF、PPM、XBM 和 PGM 格式的图像。这些格式目前都不流行。不幸的是,只有当 Tcl/Tk 开始支持这些格式时,才能添加对新格式的支持。
到那时,使用 PIL 可能会很有用,因为它支持包括 PNG、JPEG、GIF、TIFF 和 BMP 在内的几乎所有流行的图像格式。
除了在 Tkinter 中显示图像外,PIL 模块还可以用于图像处理,例如尺寸变换、格式转换、缩略图创建以及其他几个图像操作需求。
我们将在我们的视图文件 gui.py 中添加显示棋子的代码。
点火推进器
第 1 步 – 导入 PIL
由于我们将使用 PIL 模块来显示棋子的 PNG 图像,我们首先按照以下方式从 PIL 模块导入 ImageTk:
from PIL import ImageTk
第 2 步 – 定义在棋盘上绘制棋子的方法
在棋盘上添加棋子的代码如下(见 代码 4.03: gui.py):
def draw_pieces(self):
self.canvas.delete("occupied")
for xycoord, piece in self.chessboard.iteritems():
x,y = self.chessboard.num_notation(xycoord)
if piece is not None:
filename = "../pieces_image/%s%s.png" % (piece.shortname.lower(), piece.color)
piecename = "%s%s%s" % (piece.shortname, x, y)
if (filename not in self.images):
self.images[filename] = ImageTk.PhotoImage(file=filename)
self.canvas.create_image(0,0, image=self.images[filename], tags=(piecename, "occupied"), anchor="c")
x0 = (y * self.dim_square) + int(self.dim_square/2)
y0 = ((7-x) * self.dim_square) + int(self.dim_square/2)
self.canvas.coords(piecename, x0, y0)
代码的描述如下:
-
我们首先从 PIL 模块导入
ImageTk。我们需要这个来处理 PNG 图像。 -
我们定义了我们的
draw_pieces()方法,其作用是为给定的 FEN 符号在棋盘上绘制棋子。因为 FEN 符号对所有类方法都是可用的,所以我们不需要将其作为参数传递。 -
请记住,我们已经创建了一个棋盘实例,它产生一个字典,包含一个棋子的基于坐标的位置和相应的棋子实例作为键值对。
-
我们使用
iteritems()遍历字典,并将 x 和 y 坐标字符串分解为相应的基于 x 和 y 的数字表示法。 -
如果给定坐标存在棋子,我们使用
canvas.create_image()将其添加到 Canvas 小部件中。 -
在这里需要注意的最重要的事情之一是我们为每个棋子添加了两个标签:棋子的名称和一个静态字符串名称
occupied。标签是在 Canvas 小部件内操作对象时可以使用的最重要的工具。 -
接下来的两行创建了给定棋盘大小的 x,y 坐标。
-
方法的最后一行使用
self.canvas.coords将棋子放置在计算出的坐标上。 -
最后,我们需要调用我们新定义的方法。我们从
show()方法中这样做,以调用棋子。
注意
让我们通过分析这里使用的两个与 canvas 相关的方
canvas.create_image(x, y, *options): create_image方法接受两个参数,用于指定图像的位置坐标 x 和 y。在坐标之后,你可以指定任意数量的选项值对。在我们的例子中,我们使用了anchor="c"选项来保持图像在中心。
canvas.coords(tag/id, x0, y0, x1, y1, ..., xn, yn): coords()方法决定或修改与给定标签或 ID 关联的项目坐标。如果没有指定坐标,它将返回一个元组,指定由给定标签或 ID 引用的项目坐标。如果指定了坐标,则它们将替换命名项目的当前坐标。如果标签或 ID 关联到多个项目,则只使用第一个项目。
随着我们不断深入,我们将更详细地了解 Canvas 小部件。然而,查看 Canvas 小部件的交互式帮助或可用方法和可配置选项列表可能很有用。
目标完成 – 简短总结
我们现在的代码可以接受 FEN 表示法,并在棋盘上显示对应的棋子图像。如果你修改了 FEN 表示法,棋盘上的棋子将相应地改变位置。
在这个过程中,我们熟悉了 Canvas 小部件的基本功能。我们还看到了两个与 canvas 相关的创建图像和更改坐标的方法。
最后,我们看到了如何通过使用 PIL 模块来处理 Tkinter 不支持格式的图像,从而克服 Tkinter 在图像处理上的限制。
强制棋子移动规则
在我们让这些棋子在鼠标点击时移动之前,我们需要知道给定棋子可以移动多少格。我们需要为每个棋子强制执行规则。
准备起飞
在我们开始编写规则之前,让我们快速回顾一下国际象棋的规则:
-
国王只能向任意方向移动一格:向上、向下、向侧和斜向。
-
后可以沿任意一个直线方向移动:向前、向后、向侧或斜向;只要不经过自己的棋子,可以尽可能远地移动。
-
车可以移动任意远,但只能向前、向后和向侧移动
-
象可以移动任意远,但只能斜着走。
-
马与其他棋子不同。它们必须先沿一个方向移动两格,然后以 90 度角再移动一格,遵循 L 的形状。马也是唯一可以跳过其他棋子的棋子。
-
兵向前移动,但可以斜向捕获。兵每次只能向前移动一个方格,除了它们的第一次移动,那时它们可以向前移动两个方格。兵只能斜向捕获它们前方的一个方格。
这里最重要的是,我们需要跟踪每个棋子的三个常见事物:
-
它的当前位置
-
移动允许的方向
-
棋子可以移动的距离
启动推进器
第 1 步 – 从Pieces超类跟踪所有棋子的可用移动
因为前面的东西可以在一个中心位置跟踪,让我们在我们的超类Pieces中定义一个名为moves_available的方法,用于跟踪所有棋子的可用移动,如下所示:
def moves_available(self, pos, diagonal, orthogonal, distance):
board = self.board
allowed_moves = []
orth = ((-1,0),(0,-1),(0,1),(1,0))
diag = ((-1,-1),(-1,1),(1,-1),(1,1))
piece = self
beginningpos = board.num_notation(pos.upper())
if orthogonal and diagonal:
directions = diag+orth
elif diagonal:
directions = diag
elif orthogonal:
directions = orth
for x,y in directions:
collision = False
for step in range(1, distance+1):
if collision: break
dest = beginningpos[0]+step*x, beginningpos[1]+step*y
if self.board.alpha_notation(dest) not in board.occupied('white') + board.occupied('black'):
allowed_moves.append(dest)
elif self.board.alpha_notation(dest) in board.occupied(piece.color):
collision = True
else:
allowed_moves.append(dest)
collision = True
allowed_moves = filter(board.is_on_board, allowed_moves)
return map(board.alpha_notation, allowed_moves)
代码的描述如下:
-
该方法接受四个参数:棋子的当前位置,两个表示是否允许棋子进行对角线和正交移动的布尔值,以及棋子一次可以移动的方格数。
-
根据这些参数,该方法收集给定棋子的所有允许移动到一个列表中,
allowed_moves。 -
收集了所有移动方向后,代码会遍历所有位置以检测任何可能的碰撞。如果检测到碰撞,它会跳出循环,否则会将坐标添加到
allowed_moveslist中。 -
collision = True是我们跳出循环的方式。我们需要在两种情况下跳出循环:当目的地被占用时,以及当它没有被占用,并且我们已经将那个位置添加到我们的可能移动列表中。 -
第二行最后过滤掉那些超出棋盘的移动,最后一行返回所有允许移动的等效棋盘表示。
注意
定义了我们的moves_available方法后,我们现在只需从不同的棋子类中调用它。(见 代码 4.04: pieces.py)。
第 2 步 – 国王、皇后、车和象类的规则
棋盘上的国王、皇后、车和象有相对简单的规则来规范它们。这些棋子只能在其移动方向上捕获。
此外,它们可以在正交、对角或这两种方向的组合中移动。我们已经在我们的超类中编码了moves_available来处理这些方向。
因此,决定它们的可用移动只需将正确的参数传递给我们的moves_available方法。
class King(Piece):
shortname = 'k'
def moves_available(self,pos):
return super(King, self).moves_available(pos.upper(), True, True, 1)
class Queen(Piece):
shortname = 'q'
def moves_available(self,pos):
return super(Queen,self).moves_available(pos.upper(), True, True, 8)
class Rook(Piece):
shortname = 'r'
def moves_available(self,pos):
return super(Rook, self).moves_available(pos.upper(), False, True, 8)
class Bishop(Piece):
shortname = 'b'
def moves_available(self,pos):
return super(Bishop,self).moves_available(pos.upper(), True, False, 8)
第 3 步 – 马的规则
马是一种不同的生物,因为它既不沿正交也不沿对角移动。它还可以跳过其他棋子。
因此,让我们从我们的Knight类中重写moves_available方法。
Knight类定义如下(见 代码 4.04: pieces.py):
class Knight(Piece):
shortname = 'n'
def moves_available(self,pos):
board = self.board
allowed_moves = []
beginningpos = board.num_notation(pos.upper())
piece = board.get(pos.upper())
changes=((-2,-1),(-2,1),(-1,-2),(-1,2),(1,-2),(1,2),(2,-1),(2,1))
for x,y in changes:
dest = beginningpos[0]+x, beginningpos[1]+y
if(board.alpha_notation(dest) not in board.occupied(piece.color)):
allowed_moves.append(dest)
allowed_moves = filter(board.is_on_board, allowed_moves)
return map(board.alpha_notation, allowed_moves)
代码的描述如下:
-
该方法与我们的上一个超类方法非常相似。然而,与超类方法不同,变化被表示为捕获一个方向上的两个方格的移动,然后在一个 90 度的角度上再移动一次。
-
与超级类不同,我们不需要跟踪碰撞,因为马可以跳过其他棋子。
第 4 步 - 兵的规则
兵也有独特的移动方式,它向前移动,但可以向前方对角线捕获。
我们同样可以在 Pawn 类内部覆盖 moves_available 类,如下所示(见 代码 4.04: pieces.py):
class Pawn(Piece):
shortname = 'p'
def moves_available(self, pos):
board = self.board
piece = self
if self.color == 'white':
startpos, direction, enemy = 1, 1, 'black'
else:
startpos, direction, enemy = 6, -1, 'white'
allowed_moves = []
prohibited = board.occupied('white') + board.occupied('black')
beginningpos = board.num_notation(pos.upper())
forward = beginningpos[0] + direction, beginningpos[1]
# Can a piece move forward?
if board.alpha_notation(forward) not in prohibited:
allowed_moves.append(forward)
if beginningpos[0] == startpos:
# If pawn in starting pos allow a double move
double_forward = (forward[0] + direction, forward[1])
if board.alpha_notation(double_forward) not in prohibited:
allowed_moves.append(double_forward)
# Check for Capturing Moves Available
for a in range(-1, 2, 2):
attack = beginningpos[0] + direction, beginningpos[1] + a
if board.letter_notation(attack) in board.occupied(enemy):
allowed_moves.append(attack)
allowed_moves = filter(board.is_on_board, allowed_moves)
return map(board.alpha_notation, allowed_moves)
代码的描述如下:
-
我们首先根据兵是黑色还是白色分配变量
startpos、direction和enemy。 -
与我们之前的
moves_allowed方法类似,此方法也收集所有允许的走法到一个空白列表allowed_moves中。 -
我们随后通过连接所有黑白棋子占据的方格列表来收集所有禁止的走法。
-
我们定义了一个列表
forward,它保存了兵当前位置前方一个方格的位置。 -
如果前方有棋子,兵不能向前移动。如果前方位置没有被禁止,该位置将被添加到我们的
allowed_moves列表中。 -
兵可以从起始位置向前移动两步。我们检查当前位置是否是起始位置,如果是,我们将双重移动添加到我们的
allowed_moves列表中。 -
兵只能向前方对角线相邻的棋子进行捕获。因此,我们分配一个变量
attack来跟踪棋盘上的对角线相邻位置。如果对角线相邻的方格被敌人占据,该位置符合添加到我们的列表allowed_moves中。 -
我们随后过滤我们的列表,移除所有可能超出棋盘边界的位置。
-
最后一行返回所有允许的走法,作为一个对应字母标记的列表,正如我们在所有之前的定义中所做的那样。
目标完成 - 简短总结
在这次迭代中,我们编写了与棋盘上棋子移动相关的规则执行逻辑。
棋盘逻辑
在我们允许棋子在鼠标点击时移动之前,我们必须记录棋盘上所有可能的移动选项。在每次移动时,我们还需要检查这是否是给定玩家的合法回合,以及提议的走法是否不会导致对国王的将军。
现在,对国王的将军可能不仅来自移动的棋子,还可能来自棋盘上任何其他棋子,这是由于这种移动造成的。因此,在每一步之后,我们需要计算对手所有棋子的可能走法。
因此,我们需要两种方法来:
-
跟踪一个玩家所有可用走法
-
验证是否有对国王的将军
让我们把前面方法的代码添加到我们的 Board 类中。(见 代码 4.05: chessboard.py)
启动推进器
第 1 步:跟踪所有可用走法
跟踪一个玩家所有可用走法的代码如下:
def all_moves_available(self, color):
result = []
for coord in self.keys():
if (self[coord] is not None) and self[coord].color == color:
moves = self[coord].moves_available(coord)
if moves: result += moves
return result
代码的描述如下:
- 我们已经在之前的迭代中编写了我们的
moves_available方法。该方法简单地遍历字典中的每个项,并将给定颜色的每个棋子的moves_available结果追加到名为result的列表中。
第 2 步:获取国王当前位置
在我们编写验证国王是否受将军的方法之前,我们首先需要知道国王的确切位置。让我们定义一个方法来获取国王的当前位置,如下(见代码 4.05:chessboard.py):
def position_of_king(self, color):
for pos in self.keys():
if is instance(self[pos], pieces.King) and self[pos].color == color:
return pos
上述代码简单地遍历字典中的所有项。如果给定位置是King类的实例,它简单地返回其位置。
第 3 步:验证国王是否受到将军
最后,我们定义了一种方法来验证国王是否受到对手的将军,如下所示:
def king_in_check(self, color):
kingpos = self.position_of_king(color)
opponent = ('black' if color =='white' else 'white')
for pieces in self.iteritems():
if kingpos in self.all_moves_available(opponent):
return True
else:
return False
代码的描述如下:
-
我们首先获取国王的当前位置和对手的颜色。
-
然后,我们遍历对手所有棋子的所有可能走法。如果国王的位置与所有可能走法中的任何一个位置重合,则国王处于将军状态,我们返回
True,否则返回False。
目标完成 - 简短总结
这完成了我们的迭代目标。我们现在可以检查游戏中某个特定点的所有可用走法。我们还可以验证国王是否受到对手团队的将军。
使棋盘功能化
现在我们已经设置了所有棋子和棋盘相关的验证规则,让我们现在给我们的棋盘注入活力。在这个迭代中,我们将使我们的棋盘游戏完全功能化。
在两名玩家之间的游戏中,我们的棋盘将类似于以下截图所示:

本迭代的目的是在左键点击时移动棋子。当玩家点击一个棋子时,我们的代码应首先检查这是否是该棋子的合法回合。
在第一次点击时,选择要移动的棋子,并在棋盘上突出显示该棋子的所有允许走法。第二次点击应该在目标方格上。如果第二次点击在有效的目标方格上,则棋子应从源方格移动到目标方格。
我们还需要编写捕获棋子和将军国王的事件。其他需要跟踪的属性包括捕获的棋子列表、半回合时钟计数、全回合数计数以及所有先前走法的记录。
启动推进器
第 1 步 - 更新 FEN 表示法变化后的棋盘
到目前为止,我们已经有能力将原始 FEN 表示法显示在棋盘上。然而,我们需要一种方法,可以接受任何 FEN 表示法并更新棋盘上的显示。我们定义了一个名为show()的新方法来完成此操作,如下所示:
def show(self, pat):
self.clear()
pat = pat.split(' ')
def expand(match): return ' ' * int(match.group(0))
pat[0] = re.compile(r'\d').sub(expand, pat[0])
for x, row in enumerate(pat[0].split('/')):
for y, letter in enumerate(row):
if letter == ' ': continue
coord = self.alpha_notation((7-x,y))
self[coord] = pieces.create_piece(letter)
self[coord].place(self)
if pat[1] == 'w': self.player_turn = 'white'
else: self.player_turn = 'black'
self.halfmove_clock = int(pat[2])
self.fullmove_number = int(pat[3])
第 2 步 - 绑定鼠标点击事件
棋子需要在鼠标点击时移动。因此,我们需要跟踪鼠标点击事件。我们只需要跟踪在 Canvas 小部件上的鼠标点击。因此,我们将在 init 方法中创建 Canvas 小部件的代码之后立即添加一个事件处理器到我们的 GUI 类中,如下所示(见 代码 4.06: gui.py, init 方法):
self.canvas.bind("<Button-1>", self.square_clicked)
这将把左键点击事件绑定到一个新方法 square_clicked。然而,在我们坐下来定义这个方法之前,让我们暂停一下,思考我们需要跟踪程序的一些属性。
第 3 步 – 添加属性以跟踪所选棋子和剩余棋子
首先,我们需要跟踪每次移动后棋盘上剩余的所有棋子。因此,我们将创建一个名为 pieces 的字典来跟踪这一点。我们还需要跟踪鼠标点击所选的棋子名称。我们将其存储在一个属性 selected_piece 中。当玩家点击一个棋子时,我们需要突出显示该棋子的所有有效移动。我们将该棋子的所有有效移动存储在一个名为 focused 的列表中。在我们定义任何方法之前,让我们在 GUI 类中定义这三个属性。我们修改 GUI 类以包括这些属性,如下所示:
class GUI:
pieces = {}
selected_piece = None
focused = None
#other attributes from previous iterations
第 4 步 – 识别点击的方格
我们将编写 square_clicked 方法,该方法由我们之前定义的事件处理器调用。
该方法的预期功能有两方面。我们应该能够定位被点击棋子的坐标。第一次点击应选择一个给定的棋子。第二次点击应将棋子从源方格移动到目标方格。
该方法定义为如下(见 代码 4.06: gui.py):
def square_clicked(self, event):
col_size = row_size = self.dim_square
selected_column = event.x / col_size
selected_row = 7 - (event.y / row_size)
pos = self.chessboard.alpha_notation((selected_row, selected_column))
try:
piece = self.chessboard[pos]
except:
pass
if self.selected_piece:
self.shift(self.selected_piece[1], pos)
self.selected_piece = None
self.focused = None
self.pieces = {}
self.draw_board()
self.draw_pieces()
self.focus(pos)
self.draw_board()
代码的描述如下:
-
代码的第一部分计算被点击棋子的坐标。根据计算出的坐标,它将相应的字母表示存储在一个名为
pos的变量中。 -
然后尝试将变量 piece 分配给相应的棋子实例。如果点击的方格上没有棋子实例,它将简单地忽略点击。
-
方法的第二部分检查这是否是第二个点击,目的是将棋子移动到目标方格。如果是第二个点击,它将调用
shift方法,传入源坐标和目标坐标作为其两个参数。 -
如果位移成功,它将所有之前设置的属性重置为其原始的空值,并调用我们的
draw_board和draw_pieces方法来重新绘制棋盘和棋子。 -
如果这是第一次点击,它将调用一个名为
focus的方法来突出显示第一次点击的所有可用移动,然后调用绘制新棋盘。
在编写 square_clicked 方法的所需功能时,我们在其中调用了几个新的方法。我们需要定义这些新方法。
第 5 步 – 获取源和目标位置
我们从 square_clicked 方法中调用了 shift 方法。以下实现的 shift 代码仅负责收集移位操作所需的必要参数。
在保持逻辑与展示分离的精神下,我们在这个视图类中不处理移位相关的规则。相反,我们将 shift 方法的任务从 GUI 类委托给 Board 类。一旦移位的逻辑或验证被实现,棋子移位的可见部分再次在我们的 GUI 类的 draw_board 方法中发生。
虽然一开始这可能看起来有些过度,但将逻辑和展示结构化在不同的层中对于代码的重用、可扩展性和可维护性非常重要。
代码如下:
def shift(self, p1, p2):
piece = self.chessboard[p1]
try:
dest_piece = self.chessboard[p2]
except:
dest_piece = None
if dest_piece is None or dest_piece.color != piece.color:
try:
self.chessboard.shift(p1, p2)
except:
pass
代码首先检查目标位置是否存在棋子。如果目标位置没有棋子,它将调用来自 chessboard.py 的 shift 方法。
第 6 步 – 收集要突出显示的移动列表
我们还从 square_clicked 方法中调用了 focus 方法。这个方法的目的在于将给定棋子的所有可能移动收集到一个名为 focused 的列表中。实际的可移动性聚焦发生在我们 GUI 类的 draw_board 方法中。
代码如下(见代码 4.06:gui.py):
def focus(self, pos):
try:
piece = self.chessboard[pos]
except:
piece=None
if piece is not None and (piece.color == self.chessboard.player_turn):
self.selected_piece = (self.chessboard[pos], pos)
self.focused = map(self.chessboard.num_notation, (self.chessboard[pos].moves_available(pos)))
第 7 步 – 修改 draw_board 以突出显示允许的移动
在 square_clicked 方法中,我们调用了 draw_board 方法来处理重新绘制或更改棋子的坐标。我们当前的 draw_board 方法还没有配备处理这个功能,因为我们最初只设计它来提供一个空白棋盘。让我们首先修改我们的 draw_board 方法来处理这个功能,如下(见 代码 4.06:gui.py):
highlightcolor ="khaki"
def draw_board(self):
color = self.color2
for row in range(self.rows):
color = self.color1 if color == self.color2 else self.color2
for col in range(self.columns):
x1 = (col * self.dim_square)
y1 = ((7-row) * self.dim_square)
x2 = x1 + self.dim_square
y2 = y1 + self.dim_square
if(self.focused is not None and (row, col) in self.focused):
self.canvas.create_rectangle(x1, y1, x2, y2, fill=self.highlightcolor, tags="area")
else:
self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, tags="area")
color = self.color1 if color == self.color2 else self.color2
for name in self.pieces:
self.pieces[name] = (self.pieces[name][0], self.pieces[name][1])
x0 = (self.pieces[name][1] * self.dim_square) + int(self.dim_square/2)
y0 = ((7-self.pieces[name][0]) * self.dim_square) + int(self.dim_square/2)
self.canvas.coords(name, x0, y0)
self.canvas.tag_raise("occupied")
self.canvas.tag_lower("area")
代码的描述如下:
-
在现有的
draw_board方法中添加的内容在上面的代码中突出显示。我们首先定义了一个名为highlightcolor的属性,并为其分配了一个颜色。 -
代码的本质修改是为了处理点击。高亮显示代码的第一部分用不同的颜色突出显示所有可用的移动。
-
高亮显示代码的第二部分更改了棋子实例的坐标,以便位于新的坐标上。注意使用
canvas.coords(name, x0, y0)来更改坐标。 -
最后两行更改了由标签指定的选项的优先级。
注意
如果画布上的对象被标记为多个标签,则堆栈顶部的标签定义的选项具有更高的优先级。然而,你可以通过使用 tag_raise(name) 或 tag_lower(name) 来更改标签的优先级。
要获取与画布相关的选项的完整列表,请参考使用命令行中的 help(Tkinter.Canvas) 对 Canvas 小部件的交互式帮助。
第 8 步 – 定义用于保持游戏统计信息的属性
由于为我们的棋子添加了移动性,我们需要向我们的 Board 类添加以下新属性以保持游戏统计信息,如下(见 代码 4.06:chessboard.py):
Class Board(dict):
#other attributes from previous iteration
captured_pieces = { 'white': [], 'black': [] }
player_turn = None
halfmove_clock = 0
fullmove_number = 1
history = []
第 9 步 – 预移动验证
为了这个,我们将编写 Board 类的 shift 方法,如下所示(见 代码 4.06: chessboard.py):
def shift(self, p1, p2):
p1, p2 = p1.upper(), p2.upper()
piece = self[p1]
try:
dest = self[p2]
except:
dest = None
if self.player_turn != piece.color:
raise NotYourTurn("Not " + piece.color + "'s turn!")
enemy = ('white' if piece.color == 'black' else 'black' )
moves_available = piece.moves_available(p1)
if p2 not in moves_available:
raise InvalidMove
if self.all_moves_available(enemy):
if self.is_in_check_after_move(p1,p2):
raise Check
if not moves_available and self.king_in_check(piece.color):
raise CheckMate
elif not moves_available:
raise Draw
else:
self.move(p1, p2)
self.complete_move(piece, dest, p1,p2)
代码的描述如下:
-
代码首先检查目标位置上是否存在棋子。
-
然后它检查是否是玩家的有效回合。如果不是,它会抛出一个异常。
-
然后检查移动是否被提议到有效位置。如果玩家试图将棋子移动到无效位置,它会抛出一个相应的异常。
-
然后它检查国王是否受到攻击。为了做到这一点,它调用一个名为
is_in_check_after_move的方法,该方法定义如下:def is_in_check_after_move(self, p1, p2): temp = deepcopy(self) temp.unvalidated_move(p1,p2) returntemp.king_in_check(self[p1].color) -
此方法创建对象的深拷贝,并在临时拷贝上尝试移动棋子。作为备注,集合的浅拷贝是集合结构的拷贝,而不是元素的拷贝。当你进行浅拷贝时,两个集合现在共享单个元素,所以一个地方的改变也会影响另一个。相比之下,深拷贝会复制一切,包括结构和元素。我们需要创建棋盘的深拷贝,因为我们想在移动之前检查国王是否做出有效移动,并且我们想在不以任何方式修改原始对象状态的情况下做到这一点。
-
在临时拷贝上执行移动后,它检查国王是否受到攻击以返回
True或False。如果临时棋盘上的国王受到攻击,它会抛出一个异常,不允许在我们的实际棋盘上进行这样的移动。 -
类似地,它检查是否有可能发生将死或平局,并相应地抛出异常。
-
如果没有抛出异常,它最终会调用一个名为
move的方法,该方法实际上执行移动操作。
第 10 步 – 棋子的实际移动
棋子的实际移动可以编码如下:
def move(self, p1, p2):
piece = self[p1]
try:
dest = self[p2]
except:
pass
del self[p1]
self[p2] = piece
第 11 步 – 移动后的更新
实际执行移动后,它调用另一个名为 complete_move 的方法,按照以下方式更新游戏统计信息:
def complete_move(self, piece, dest, p1, p2):
enemy = ('white' if piece.color == 'black' else 'black' )
if piece.color == 'black':
self.fullmove_number += 1
self.halfmove_clock +=1
self.player_turn = enemy
abbr = piece.shortname
if abbr == 'P':
abbr = ''
self.halfmove_clock = 0
if dest is None:
movetext = abbr + p2.lower()
else:
movetext = abbr + 'x' + p2.lower()
self.halfmove_clock = 0
self.history.append(movetext)
前面的方法执行以下任务:
-
跟踪统计信息,例如移动次数、半回合时钟
-
改变玩家的回合
-
检查是否有棋子被移动以重置半回合时钟
-
最后,将最后一步移动添加到我们的历史列表中
第 12 步 – 处理异常和错误的类
最后,我们添加了以下空类以处理我们抛出的各种异常:
class Check(ChessError): pass
classInvalidMove(ChessError): pass
classCheckMate(ChessError): pass
class Draw(ChessError): pass
classNotYourTurn(ChessError): pass
目标完成 – 简短总结
让我们总结一下在这个迭代中我们所做的事情
-
我们首先将鼠标点击事件绑定到一个名为
square_clicked的方法上。 -
我们添加了属性来跟踪选中的棋子和棋盘上剩余的棋子。
-
然后我们识别了被点击的方块,随后收集了起始和目标位置。
-
我们还收集了所选棋子的所有可能移动列表,并将它们突出显示。
-
然后我们定义了属性以保持重要的游戏统计信息。
-
然后我们进行了一些预移动验证,接着在棋盘上实际移动棋子。
-
在一个棋子被移动后,我们更新了关于游戏的统计数据。
-
在这次迭代中,我们定义了几个异常。我们只是定义了空类来静默处理它们。
我们的国际象棋游戏现在功能齐全。现在,两位玩家可以在我们的应用程序上玩一局棋。
添加菜单和信息框架
虽然我们的游戏已经完全可用,但让我们给它添加两个小功能。
让我们在文件 | 新游戏下添加一个顶部菜单项。当点击时,它应该将棋盘重置为新游戏。
此外,让我们在底部添加一个小的框架来显示与游戏相关的信息,例如最后一步移动、下一个回合、检查、和棋以及将军。

启动推进器
第 1 步 – 创建顶部菜单
我们的 Canvas 小部件是在GUI类的__init__方法中设置的。
让我们修改它以包括顶部菜单,如下所示(见 代码 4.06: gui.py):
def __init__(self, parent, chessboard):
self.chessboard = chessboard
self.parent = parent
self.menubar = Menu(parent)
self.filemenu = Menu(self.menubar, tearoff=0)
self.filemenu.add_command(label="New Game", command=self.new_game)
self.menubar.add_cascade(label="File", menu=self.filemenu)
self.parent.config(menu=self.menubar)
第 2 步 – 添加底部框架以显示游戏统计信息
让我们也添加一个底部框架来显示游戏统计信息,如下所示(见 代码 4.06: gui.py):
self.btmfrm = Frame(parent, height=64)
self.info_label = Label(self.btmfrm, text=" White to Start the Game ", fg=self.color2)
self.info_label.pack(side=RIGHT, padx=8, pady=5)
self.btmfrm.pack(fill="x", side=BOTTOM)
对现有初始化方法的修改被突出显示。代码是自我解释的。我们在所有之前的项目中都做了类似的事情。
第 3 步 – 从文件 | 新游戏菜单开始新游戏
文件 | 新游戏菜单项调用我们的new_game()方法。new_game()的代码如下(见 代码 4.06: gui.py):
def new_game(self):
self.chessboard.show(chessboard.START_PATTERN)
self.draw_board()
self.draw_pieces()
self.info_label.config(text="White to Start the Game", fg='red')
第 4 步 – 每次移动后更新底部标签
最后,在每次移动后,我们希望更新标签以显示移动的详细信息以及关于下一位玩家回合的信息。我们还想更新框架以显示在移动尝试过程中可能发生的任何错误或异常。因此,我们相应地修改了GUI类的shift方法,如下所示以完成此更新:
def shift(self, p1, p2):
piece = self.chessboard[p1]
try:
dest_piece = self.chessboard[p2]
except:
dest_piece = None
if dest_piece is None or dest_piece.color != piece.color:
try:
self.chessboard.shift(p1,p2)
exceptchessboard.ChessError as error:
self.info_label["text"] = error.__class__.__name__
else:
turn = ('white' if piece.color == 'black' else 'black')
self.info_label["text"] = '' + piece.color.capitalize() +" : "+ p1 + p2 + '' + turn.capitalize() + '\'s turn'
代码的描述如下所示:
-
我们对
shift方法的修改被突出显示。我们只是将Board类的shift方法包含在一个 try except 块中。如果位移成功,标签小部件将更新以显示当前移动和下一位玩家的回合。 -
如果位移不成功,无论是由于无效的移动还是对国王的检查,相应的错误类名将显示在带有
error.__class__.__name__的标签上。
目标完成 – 简短总结
这完成了我们这次迭代的目标。现在,在棋局过程中,应用程序会向玩家显示一些有用的信息。
我们还添加了一个文件 | 新菜单项,可以用来将棋盘重置到起始位置。
任务完成
我们现在来到了项目的尾声。
那么,我们在这里取得了什么成果呢?让我们列出从这个项目中获得的所有关键学习点:
-
如何将程序结构化为模型和视图组件
-
如何用期望的符号表示问题域
-
探索 Tkinter Canvas 小部件的多样性和强大功能
-
Canvas 坐标、对象 ID 和标签的基本用法
-
如何处理新的图像格式
-
在 GUI 程序中逻辑层和表示层之间的典型交互
从下一个项目开始,我们将更详细地查看不同的 Tkinter 小部件。
热门挑战
这里有两个热门挑战供您挑战:
-
添加并实现以下菜单项:
-
文件| 保存:保存游戏状态
-
文件| 打开:加载之前保存的游戏
-
编辑| 撤销:允许玩家撤销已进行的回合
-
编辑|重做:允许玩家重做任何之前的撤销操作
-
查看| 移动历史:打开一个新的 Toplevel 窗口以显示游戏的历史
-
关于| 关于:显示有关游戏的信息
-
-
在游戏中实现王车易位和过路兵的功能。
第五章:音频播放器
现在我们来构建一个音频媒体播放器!
我们的应用程序应具备典型媒体播放器的功能,如播放、暂停、快进、快退、下一曲、静音、音量更新等。我们的播放器应允许听众轻松访问其本地驱动器中的单个媒体文件或媒体库。
此外,我们的播放器还应能够扫描整个目录以查找歌曲,并相应地自动更新包含所有支持格式的播放列表。所有这些以及更多。
让我们开始我们的项目!
任务简报
完成后,我们的播放器将如下所示:

我们的音频播放器将能够播放 AU、MP2、MP3、OGG/Vorbis、WAV 和 WMA 格式的音频文件。它将拥有您期望的小型媒体播放器所具有的所有控件。
我们将使用跨平台模块来编写我们的代码。这将确保我们的播放器可以在 Windows、Mac OS X 和 Linux 平台上播放音频文件。
为什么它如此出色?
除了在测试代码的同时能够听到好音乐,这个项目还将向我们介绍与 Tkinter GUI 编程相关的几个新想法。
首先,我们将与新的小部件集一起工作,例如 Listbox、Progressbar、Scale、Radiobutton 和 PMW Balloon 小部件。
我们还研究了 Canvas 小部件在容纳和精确定位其中其他小部件方面的强大功能。
在项目的后期,我们将查看一个名为 PMW 的 Tkinter 扩展。我们还讨论了一些我们没有使用但值得在 GUI 编程工具箱中拥有的 Tkinter 扩展。
虽然这不是本书的主题,但我们还简要了解了使用 Python 进行音频编程的世界,这必然涉及到如何与外部库和 API 实现协同工作。
你的高目标
为这个项目概述的一些关键目标包括:
-
巩固我们之前的经验和从之前的项目中学习
-
与新的小部件集一起工作,例如 Listbox、Scale、Progressbar 和 Radiobutton
-
了解 Canvas 小部件的更多功能
-
与外部 API 协同工作
-
了解一些常见的 Tkinter 扩展,如 PMW、WCK、Tix 等
-
在开发的每个阶段学习如何重构代码
任务清单
我们将使用以下额外的库来完成这个项目:
Pyglet 音频处理
窗口用户可以从以下位置下载并安装 pyglet 的二进制包:
www.lfd.uci.edu/~gohlke/pythonlibs/#pyglet
Mac OS X 和 Linux 用户应从以下源 ZIP 文件下载并编译 pyglet:
当从源代码安装时,你还需要将AVbin.dll添加到你的当前程序目录中。DLL 文件的链接也可以在先前的下载页面找到。
PMW Tkinter 扩展
我们将使用Python mega widgets(PMW)Tkinter 扩展来编写一些在核心 Tkinter 中不可用的小部件功能。PMW 必须从源包安装到所有平台。该包可以从以下网址下载:
sourceforge.net/projects/pmw/files/Pmw/Pmw.1.3.3/
注意
我们在我们的应用程序中使用版本 1.3.3,其他版本的 PMW 可能不与我们的代码兼容。
额外字体
这是一个可选组件,仅用于增强我们的样式。我们安装了一个字体来模仿数字时钟的字体。我们在这个项目中使用了以下字体:
www.dafont.com/ds-digital.font
在您安装了 pyglet 和 PMW 之后,从您的 Python shell 中执行以下命令:
>>> import pyglet, Pmw
如果命令执行没有错误信息,您就可以开始编写您的媒体播放器代码了。
获取音频播放
我们项目的第一个目标是添加播放音频文件的能力。像往常一样,我们将把音频相关逻辑与我们的 GUI 部分分开。因此,我们创建了两个单独的文件:main-gui.py和player.py。(见代码 5.01)
准备起飞
我们首先编写一个基本的 GUI,其中包括一个播放按钮(在播放和停止功能之间切换)和一个添加文件按钮。在这个迭代的最后,我们应该能够加载一个文件,播放它,并停止它。到本节结束时,我们的应用程序将看起来像以下截图:

启动推进器
第 1 步 – 创建 GUI 类
让我们创建GUI类。main-gui.py的代码如下(见代码 5.01 main-gui.py):
from Tkinter import *
import tkFileDialog
import player
class GUI:
def __init__(self, player):
self.player = player
player.parent = self
self.root = Tk()
self.create_button_frame()
self.create_bottom_frame()
self.root.mainloop()
代码的描述如下:
-
我们创建了一个名为
GUI的类,并在其__init__方法中运行 Tkinter 主循环。 -
我们将把实际的音频操作逻辑,如播放、暂停、倒带、快进等,放在一个稍后定义的单独类中。然而,因为我们希望这些功能在
GUI类中可用,所以我们把从那个player类实例化的对象作为参数传递给我们的__init__方法。 -
在
__init__方法中的self.player = player这一行确保了player类实例在整个GUI类中可用。 -
就像我们要从
GUI类中访问player类的属性和方法一样,我们也希望GUI类的方法和属性在player类中可用。因此,我们在__init__方法中使用player.parent = self这一行。这创建了对 self 的引用,因此可以使用parent.attribute和parent.method()语法在player类内部评估所有其方法。 -
使用这两行代码,我们确保了
GUI类的所有属性都将可用在player类中,反之亦然;player类的所有属性也将可用在GUI类中。
第 2 步 – 创建播放按钮和添加文件按钮
因此,我们添加了两个方法:create_button_frame和create_bottom_frame。create_button_frame方法包含播放按钮,而create_bottom_frame方法包含添加文件按钮,如下所示:
def create_button_frame(self):
buttonframe= Frame(self.root)
self.playicon = PhotoImage(file='../icons/play.gif')
self.stopicon = PhotoImage(file='../icons/stop.gif')
self.playbtn=Button(buttonframe, text ='play', image=self.playicon, borderwidth=0, command=self.toggle_play_pause)
self.playbtn.image = self.playicon
self.playbtn.grid(row=3, column=3)
buttonframe.grid(row=1, pady=4, padx=5)
def create_bottom_frame(self):
bottomframe = Frame(self.root)
add_fileicon = PhotoImage(file='../icons/add_file.gif')
add_filebtn=Button(bottomframe, image=add_fileicon, borderwidth=0, text='Add File', command=self.add_file)
add_filebtn.image = add_fileicon
add_filebtn.grid(row=2, column=1)
bottomframe.grid(row=2, sticky='w', padx=5)
代码的描述如下:
- 每个按钮都与一个
TkinterPhotoImage类图标相关联。我们已经在名为icons的单独文件夹中提供了一套图标。
第 3 步 – 在播放和暂停之间切换
播放按钮有一个命令回调,用于在播放和停止功能之间切换按钮。toggle方法定义如下:
def toggle_play_pause(self):
if self.playbtn['text'] =='play':
self.playbtn.config(text='stop', image=self.stopicon)
self.player.start_play_thread()
elif self.playbtn['text'] =='stop':
self.playbtn.config(text ='play', image=self.playicon)
self.player.pause()
代码的描述如下:
toggle_play_pause方法会在播放和暂停图标之间交替更改图标。它还会调用player类的play和pause方法来播放和暂停歌曲。
第 4 步 – 添加文件对话框
添加文件按钮打开tkFileDialog,将打开的文件与一个名为currentTrack的类属性相关联,如下所示:
def add_file(self):
tfile = tkFileDialog.askopenfilename(filetypes=[('All supported', '.mp3 .wav .ogg'), ('All files', '*.*')])
self.currentTrack = tfile
第 5 步 – 创建播放器类
现在,让我们编写基本的player类代码。目前,我们只将播放和暂停功能添加到该类。我们的player类代码建立在 pyglet 库的基础上。
注意
Pyglet 提供了一个面向对象的接口,用于开发丰富的媒体应用程序,如游戏、音频和视频工具等。它是 Python 程序员在媒体处理方面的热门选择,因为它没有外部依赖,支持大量格式,并且可在所有主要操作系统上使用。
在我们继续之前,你可能想查看 pyglet 播放器的 API 文档,该文档可在以下位置找到:
www.pyglet.org/doc/api/pyglet.media.Player-class.html
文档告诉我们可以使用以下代码播放音频文件:
myplayer= pyglet.media.Player()
source = pyglet.media.load(<<audio file to be played>>)
myplayer.queue(source)
myplayer.play()
pyglet.app.run()
我们将使用此代码片段来播放音频文件。因此,我们的Player类代码如下(见代码 5.01 player.py):
import pyglet
from threading import Thread
class Player():
parent = None
def play_media(self):
try:
self.myplayer= pyglet.media.Player()
self.source = pyglet.media.load(self.parent.currentTrack)
self.myplayer.queue(self.source)
self.myplayer.play()
pyglet.app.run()
except:
pass
def start_play_thread(self):
player_thread = Thread(target=self.play_media)
player_thread.start()
def pause(self):
try:
self.myplayer.pause()
self.paused = True
except: pass
代码的描述如下:
-
我们创建了一个名为
Player的类,并将其父类初始化为None。回想一下,在我们的GUI类中,我们定义了一个引用player.parent = self,以便在player类内部评估我们的GUI类属性。 -
我们然后定义我们的
play_media方法,该方法负责实际播放声音。该方法访问GUI类的currentTrack属性并尝试播放它。 -
虽然这段代码可以播放音频文件,但 pyglet 需要运行它自己的事件循环来播放音频。这意味着它将在播放完整个声音后,将控制权返回到我们的 GUI 主循环,如果在直接运行时,将会冻结 Tkinter 主循环。
-
因此,我们需要在单独的线程中调用播放方法。我们使用 threading 模块定义了一个名为
start_play_thread的新方法,它简单地在一个单独的线程中调用我们的play_media方法,从而防止 GUI 冻结。 -
最后,我们定义了暂停方法,该方法可以暂停或停止当前播放的音频文件。Pyglet 不区分暂停和停止功能。因此,我们通常使用暂停命令来停止音频。
第 6 步 – 运行应用程序
我们最终通过创建一个GUI类的对象来运行应用程序。因为这个GUI类需要一个player类的对象,所以我们实例化了一个播放器对象,并将其作为参数传递给我们的GUI类,如下所示:
if __name__ == '__main__':
playerobj = player.Player()
app = GUI(playerobj)
代码的描述如下所示:
-
代码的最后部分创建了一个我们尚未定义的
player类的对象。player类将负责使用 pyglet 进行所有音频操作。 -
我们首先创建了一个
player类的对象,并将其作为参数传递给我们的GUI类的__init__方法。这确保了player类的所有属性和方法都可以在GUI类中使用player.attribute和player.method()的语法在内部使用。
目标完成 – 简短总结
这完成了我们的第一次迭代。
在本节中,我们创建了一个GUI类,添加了一个在播放和暂停之间切换的按钮。我们还添加了一个按钮,使用tkFileDialog来添加文件。
我们还创建了一个Player类,该类使用 pyglet 来播放音频文件。文件在单独的线程中播放,以避免在播放音频时冻结 Tkinter 主循环。
最后,我们通过首先创建一个播放器对象,并将其作为参数传递给由我们的GUI类创建的另一个对象来运行我们的应用程序。
现在我们有一个功能齐全的音频播放器,你可以使用tkFileDialog加载单个文件。加载后,你可以按下播放按钮,音频文件开始播放。你可以通过点击播放按钮来停止音频,该按钮在播放和暂停功能之间切换。
添加播放列表
现在我们有了播放单个音频文件的能力,但如果一个音频播放器不支持播放列表,那它又是什么呢?
让我们在播放器中添加播放列表功能。一旦添加了播放列表,我们就需要相应地提供按钮来添加文件到播放列表,从播放列表中删除文件,以及从所选目录添加所有支持的文件,并能够一次性删除列表中的所有项目。
在这一迭代的最后,我们将有一个如下截图所示的播放器:

准备起飞
我们将使用 Tkinter Listbox 小部件来提供播放列表。让我们看看 Listbox 小部件的一些关键特性:
-
你创建一个 Listbox 就像创建任何其他小部件一样,如下所示:
mylist = ListBox(parent, **configurable options) -
当您最初创建列表小部件时,它是空的。要将一行或多行文本插入列表小部件,您使用
insert()方法,该方法需要两个参数:文本要插入的位置的索引以及要插入的实际字符串,如下所示:mylist.insert(0, "First Item") mylist.insert(END, "Last Item") -
curselection()方法返回列表中所有选中项的索引,而get()方法返回给定索引的列表项,如下所示:mylist.curselection() # returns a tuple of all selected items mylist.curselection()[0] # returns first selected item mylist.get(1) # returns second item from the list mylist.get(0, END) # returns all items from the list -
此外,列表小部件还有几个其他可配置的选项。要获取完整的列表小部件参考信息,请在您的 Python 交互式 shell 中输入以下内容:
>>> import Tkinter >>> help(Tkinter.Listbox)
启动推进器
第 1 步 – 添加一个空的列表小部件
让我们先添加一个空的列表小部件,如下所示(见code 5.02 main-gui.py):
def create_list_frame(self):
list_frame = Frame(self.root)
self.Listbox = Listbox(list_frame, activestyle='none', cursor='hand2', bg='#1C3D7D', fg='#A0B9E9', selectmode=EXTENDED, width=60, height =10)
self.Listbox.pack(side=LEFT, fill=BOTH, expand=1)
self.Listbox.bind("<Double-Button-1>", self.identify_track_to_play)
scrollbar = Scrollbar(list_frame)
scrollbar.pack(side=RIGHT, fill=BOTH)
self.Listbox.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=self.Listbox.yview)
list_frame.grid(row=4, padx=5)
代码的描述如下:
-
我们创建了一个新的框架,名为
list_frame,用于存放我们的列表小部件。 -
在这个框架内,我们创建了一个列表小部件并设置了一些样式选项,例如背景颜色、前景颜色和鼠标光标。使用列表小部件选项
activestyle设置活动行的样式,这意味着我们不想在选中项下划线。 -
selectmode选项配置为扩展。请参阅以下信息框以获取可用选项及其含义。我们将使用EXTENDED选择模式,因为尽管一次可以播放一个文件,但我们希望允许用户一次选择多个文件进行删除。 -
我们向列表小部件添加了一个滚动条,这与我们在文本编辑器项目中的做法类似。
-
我们将鼠标的双击绑定到另一个名为
identify_track_to_play的方法。
注意
列表小部件提供了四种选择模式,使用selectmode选项如下:
SINGLE:它允许一次只选择一行。
BROWSE(默认模式):它与SINGLE类似,但允许通过拖动鼠标移动选择。
MULTIPLE:它允许通过逐个点击项目进行多选。
EXTENDED:它允许使用Shift和Control键选择多个范围的项。
第 2 步 – 识别要播放的曲目
在第一次迭代中,我们的程序比较简单,因为我们只有一首歌要播放。然而,给定一个播放列表,现在我们必须确定从给定列表中需要播放哪首歌。
规则很简单。如果用户点击一首特定的歌曲,它就成为我们的选中曲目。如果用户没有做出选择并点击播放按钮,播放列表中的第一首歌曲应该被播放。用代码表示如下(见code 5.02 main-gui.py):
def identify_track_to_play(self, event=None):
try:
indx = int(self.Listbox.curselection()[0])
if self.Listbox.get(indx) == "":
self.del_selected()
except:
indx = 0
self.currentTrack =self.Listbox.get(indx)
self.player.start_play_thread()
第 3 步 – 向列表添加项目
现在我们有了列表小部件,并且可以通过双击来播放任何项目,让我们添加填充和从列表中删除项目的方法。
然而,在我们对列表进行任何修改之前,让我们首先定义一个名为alltracks的空列表来跟踪播放列表中的所有项目。在列表发生任何更改后,我们需要更新此列表,如下所示(见code 5.02 main-gui.py):
alltracks = []
我们在上一个部分中已经创建了一个添加文件方法。现在让我们稍作修改,以便选定的文件不会成为选定的曲目,而是被添加到播放列表中,如下所示(见代码 5.02 main-gui.py):
def add_file(self):
filename = tkFileDialog.askopenfilename(filetypes=[('All supported', '.mp3 .wav'), ('.mp3 files', '.mp3'), ('.wav files', '.wav')])
if filename:
self.Listbox.insert(END, filename)
self.alltracks = list(self.Listbox.get(0, END))
代码的描述如下列所示:
-
通过
tkFileDialog选定的文件被插入到列表框的末尾,并且我们的属性alltracks被更新为 Listbox 小部件中的所有元素。 -
注意,
get()方法返回一个包含所有项目的元组。因为元组是不可变的,所以我们通过使用list类型声明显式地将元组转换为列表。
第 4 步 – 从列表中删除项目
让我们添加一个新按钮来删除选定的文件。这添加到我们现有的create_bottom_frame方法中,如下所示(见代码 5.02 main-gui.py):
del_selectedicon = PhotoImage(file='../icons/del_selected.gif')
del_selectedbtn=Button(bottomframe, image=del_selectedicon, padx=0, borderwidth=0, text='Delete', command=self.del_selected)
del_selectedbtn.image = del_selectedicon
del_selectedbtn.grid(row=5, column=2)
此按钮有一个名为del_selected的方法的命令回调。del_selected的代码如下:
def del_selected(self):
whilelen(self.Listbox.curselection())>0:
self.Listbox.delete(self.Listbox.curselection()[0])
self.alltracks = list(self.Listbox.get(0, END))
如往常一样,我们在从 Listbox 小部件中删除项目后更新我们的alltracks列表。
现在,您可以从列表框中选择一个项目,然后点击删除按钮,从列表框中删除所有选定的项目。
第 5 步 – 向列表中添加多个项目
向播放列表中添加单个音频文件可能会变得繁琐。我们希望允许用户选择一个目录,并且我们的列表应该填充来自该目录的所有支持的媒体格式。
因此,我们添加了一个新的按钮,允许添加给定目录中的所有媒体文件。这也添加到我们现有的create_bottom_frame方法中,如下所示(见代码 5.02 main-gui.py):
add_diricon = PhotoImage(file='../icons/add_dir.gif')
add_dirbtn=Button(bottomframe, image=add_diricon, borderwidth=0, padx=0, text='Add Dir', command=self.add_dir)
add_dirbtn.image = add_diricon
add_dirbtn.grid(row=5, column=3)
我们需要使用os模块来获取所有支持的类型。让我们首先将os模块导入到当前命名空间中,如下所示:
import os
现在相关的命令回调如下:
def add_dir(self):
path = tkFileDialog.askdirectory()
if path:
tfileList = []
for (dirpath, dirnames, filenames) in os.walk(path):
for tfile in filenames:
if tfile.endswith(".mp3") or tfile.endswith(".wav") or tfile.endswith(".ogg"):
tfileList.append(dirpath+"/"+tfile)
for item in tfileList:
self.listbox.insert(END, item)
self.alltracks = list(self.listbox.get(0, END))
代码的描述如下列所示:
-
add_dir方法首先创建一个临时列表,tfilelist。 -
然后它遍历通过
tkFileDialog.askdirectory()方法获取的所有文件名。如果它遇到支持的文件格式,它将文件追加到临时列表中。 -
然后它遍历
tfilelist中的所有项目,将它们插入到我们的 Listbox 中。 -
最后,它使用新修改的列表中的所有项目更新我们的
alltracks属性。
第 6 步 – 删除所有项目
最后,我们添加一个按钮来删除播放列表中的所有项目。相关的按钮添加到create_bottom_frame方法中,如下所示:
delallicon = PhotoImage(file='../icons/delall.gif')
delallbtn = Button(bottomframe, image=delallicon, borderwidth=0, padx=0, text='Clear All', command=self.clear_list)
delallbtn.image = delallicon
delallbtn.grid(row=5, column=4)
现在它的相关命令回调如下:
def clear_list(self):
self.Listbox.delete(0, END)
self.alltracks =list(self.Listbox.get(0, END))
目标完成 – 简短总结
这完成了我们的第二次迭代。
在这次迭代中,我们学习了如何使用 Listbox 小部件。特别是,我们学会了如何向 Listbox 小部件添加项目,向其中添加项目,从 Listbox 小部件中选择特定项目,以及从中删除一个或多个项目。
现在,您有一个可以添加和删除项目的播放列表。
Listbox 小部件在项目上双击鼠标按钮时有一个事件绑定。这个相关的事件回调函数选择被点击的项目,并将其发送到另一个线程进行播放。
在这个过程中,我们看到了在 Listbox 小部件上执行的一些常见操作。
向播放器添加更多控制
现在我们有了播放列表,我们需要确保歌曲按队列播放。我们还需要添加一些在音频播放器中通常可以看到的控制按钮,如“下一曲”、“上一曲”、“快进”、“回放”和“静音”按钮。我们还需要提供一个方法来更改播放音量。
在这个迭代结束时,我们的播放器在顶部按钮框架中会有以下额外的控制按钮:

pyglet API 文档为所有这些控制提供了简单的接口。为了您的参考,文档可在以下网址找到:
www.pyglet.org/doc/api/pyglet.media.Player-class.html
让我们从向我们的Player类添加处理这些方法的方法开始。
启动推进器
第 1 步 – 快进曲目
我们可以这样快进曲目(见 代码 5.03 player.py):
FWDREWNDTIME = 20
#time to seek ahead or backwards in seconds
def fast_fwd(self):
try:
current_time = self.myplayer.time
self.myplayer.seek(current_time+FWDREWNDTIME)
except:pass
第 2 步 – 回放曲目
我们可以这样回放曲目:
def rewind(self):
try:
current_time = self.myplayer.time
self.myplayer.seek(current_time-FWDREWNDTIME)
except:pass
第 3 步 – 暂停曲目
我们可以这样暂停一个曲目:
def pause(self):
try:
self.myplayer.pause()
self.paused = True
except: pass
第 4 步 – 设置播放音量
我们可以这样设置播放音量:
def set_vol(self, vol):
try:
self.myplayer.volume = vol
except:pass
第 5 步 – 静音和取消静音曲目
我们可以这样静音和取消静音曲目:
def mute(self):
try:
self.myplayer.volume = 0.0
self.parent.volscale.set(0.0)
except:pass
def unmute(self):
self.set_vol(self.vol)
self.parent.volscale.set(0.3)
我们在这里不会详细讨论代码。为了实现这些功能,我们使用了 pyglet 的 API 文档,该文档可在以下网址找到:
www.pyglet.org/doc/api/pyglet.media.Player-class.html
你也可以通过在 Python 交互式外壳中输入这两行来访问 pyglet 媒体播放器类的文档:
>>> import pyglet
>>> help (pyglet.media.Player)
注意
我们一直在程序中使用 try/except 块来隐藏来自player类的所有错误。
这可能不是最好的编程实践,但我们忽略所有player类的错误,以免偏离我们对 Tkinter 的讨论。
在正常情况下,你会使用不同的 except 块来处理所有不同类型的错误。
第 6 步 – 添加控制按钮
现在我们有了处理事件的后端代码,如快进、回放、音量更改、静音等,现在是时候向我们的GUI类添加每个这些控制的按钮了。我们将每个按钮链接到其相应的命令回调。
因此,我们修改我们的create_button_frame小部件以添加这些新控制按钮的按钮。
在我们之前的项目中已经添加了数百个按钮。因此,为了简洁起见,我们在这里不重复整个代码。相反,我们仅展示作为其样本之一的“上一曲目”按钮的实现,以及它是如何调用player类的previous()方法的关联命令回调(见 代码 5.03 GUI.py):
previcon = PhotoImage(file='../icons/previous.gif')
prevbtn=Button(buttonframe, image=previcon, borderwidth=0, padx=0, command=self.prev_track)
prevbtn.image = previcon
prevbtn.grid(row=3, column=1, sticky='w')
第 7 步 - 使用 ttk Scale 小部件更改音量
除了这些按钮外,我们还使用 ttk Scale 小部件允许用户更改音量。Tkinter 核心中的原生 Scale 小部件实现看起来相当过时,所以我们选择了 ttk Scale 小部件,它具有与核心 Tkinter Scale 小部件相同的可配置选项,如下所示:
self.volscale = ttk.Scale(buttonframe, from_=0.0, to =1.0 , command=self.vol_update)
self.volscale.set(0.6)
self.volscale.grid(row=3, column=7, padx=5)
根据 pyglet 文档,播放音量必须指定为一个范围从0.0(无声音)到1.0(最大声音)的浮点数,我们的updateVolume方法就是基于这个。
这有一个附加的回调到GUI类中的另一个方法vol_update,它简单地将任务委托给player方法来处理音量变化。
def vol_update(self, e):
vol = float(e)
self.player.set_vol(vol)
代码的描述如下:
- pyglet 的
Player类期望音量被指定为一个浮点数,但这里的命令接收到的 scale 的新值是一个字符串。因此,我们首先将其转换为浮点数,然后将其传递给player类的set_vol方法。
目标完成 - 简短总结
这完成了第二次迭代,我们在程序中添加了播放控制功能。
这一节更多的是关于坚持 pyglet 的 API 文档,并把它作为一个黑盒来信任它所承诺的功能:即能够播放和控制音频。
我们还在构建音量控制的实际演示中看到了如何使用 ttk Scale 小部件。
分类情报
当涉及到选择外部实现(就像我们在音频 API 中所做的那样)时,我们首先在以下位置搜索 Python 标准库:
由于标准库没有适合我们的合适包,我们将注意力转向 Python 包索引,看看是否存在另一个高级音频接口实现。Python 包索引位于:
幸运的是,我们遇到了几个音频包。在比较这些包与我们的需求以及看到其社区活跃度后,我们选择了 pyglet。虽然可以用几个其他包实现相同的程序,但复杂度各不相同。
小贴士
通常,你越深入协议堆栈,你的程序就会越复杂。
然而,在协议的较低层,你可以在增加学习曲线的成本下获得对实现的更精细控制。
例如,由于 pyglet 的player类没有区分暂停和停止功能,我们不得不完全放弃暂停功能,并接受一个更简单的实现,其中暂停和停止意味着相同。
为了更精细地控制音频源,我们不得不深入研究协议堆栈,但我们现在将避免这样做,以免偏离主题。
添加顶级显示控制台
在这个迭代中,我们将在玩家顶部添加一个显示控制台。这个控制台将显示音乐播放器的计时器。它还将显示当前播放的曲目。
我们还将编写一个进度条,它将显示当前播放曲目的进度。
在这个迭代的最后,我们玩家的顶部框架将看起来像以下截图:

准备起飞
我们需要精确地将计时器时钟文本和当前播放曲目文本放置在图像的顶部。
请记住,Canvas 小部件允许在内部以精确的坐标控制方式深度嵌套其他小部件。这正是我们想要显示控制台的方式。因此,我们将使用 Canvas 小部件作为控制台的容器。
启动推进器
第 1 步 – 创建顶部控制台和进度条
因此,我们在GUI类中定义了一个名为create_console_frame的新方法,它包含我们的图像、时钟文本和当前播放文本,以创建顶部控制台和进度条,如下所示(见代码 5.04 GUI.py):
def create_console_frame(self):
consoleframe = Frame(self.root)
self.canvas = Canvas(consoleframe, width=370, height=90)
self.canvas.grid(row=1)
photo = PhotoImage(file='../icons/glassframe.gif')
self.canvas.image = photo
self.console = self.canvas.create_image(0, 10, anchor=NW, image=photo)
self.clock = self.canvas.create_text(32, 34, anchor=W, fill='#CBE4F6', font="DS-Digital 20", text="00:00")
self.songname = self.canvas.create_text(115, 37, anchor=W, fill='#9CEDAC', font="Verdana 10", text='\"Currently playing: none [00.00] \"')
self.progressBar = ttk.Progressbar(consoleframe, length =1, mode="determinate")
self.progressBar.grid(row=2, columnspan=10, sticky='ew', padx=5)
consoleframe.grid(row=1, pady=1, padx=0)
代码的描述如下:
-
代码定义了一个新的框架,
consoleframe,并将所需高度和宽度的 Canvas 小部件添加到框架中。 -
我们使用
canvas.create_image来添加背景图像。背景图像位于icons文件夹中。 -
我们使用
canvas.create_text添加一个用于显示时钟的文本和一个用于显示当前播放曲目的文本。每个文本的期望位置都使用 x,y 坐标指定。 -
我们还指定了一个特殊的字体来显示我们的时钟。如果这个字体安装在了电脑上,文本将以指定的字体显示。如果字体未安装,则使用默认字体显示。
-
最后,我们显示一个 ttkProgressbar 小部件,它将用于显示曲目播放时的进度。我们使用进度条的确定模式,因为我们想显示相对于整个长度的完成情况。目前,曲目的整体长度初始化为
1。随着歌曲的开始播放,它将被更新。
注意
ttkProgressbar 小部件显示操作的进度状态。进度条可以运行在两种模式:
Determinate:这种模式显示了相对于总工作量的完成量。
Indeterminate:这种模式提供了一个动画的进度显示,但不会显示完成的工作的相对量。
第 2 步 – 获取曲目的总时长
每次新歌曲开始播放时,显示面板的内容和进度条上的进度都需要更新。在我们的当前代码中,当用户点击播放按钮或双击特定曲目,或者点击下一曲或上一曲按钮时,新歌曲开始播放。
在我们更新时钟或进度条之前,我们需要知道两件事:
-
当前曲目的总长度
-
当前曲目播放的持续时间
幸运的是,pyglet 提供了 API 调用来确定这两件事。根据其文档,当前播放歌曲的总长度可以通过以下代码获得:source.duration。
同样,可以使用myplayer.time获取当前播放的持续时间。
因此,我们在Player类中定义了两个新方法来获取这两个变量的值,如下(代码 5.04player.py):
def song_len(self):
try:
self.song_length = self.source.duration
except:
self.song_length = 0
return self.song_length
def current_time(self):
try:
current_time = self.myplayer.time
except:
current_time = 0
return current_time
现在,我们稍微修改一下start_play_thread方法,使其调用我们的song_len方法,以便我们的song_length属性能够更新为歌曲长度值。
def start_play_thread(self):
player_thread = Thread(target=self.play_media)
player_thread.start()
time.sleep(1)
self.song_len()
注意,我们使方法休眠一秒钟,以便让长度元数据填充。如果我们不使其休眠一秒钟,代码会执行得太快,以至于在song_length变量被 pyglet 更新之前就会结束。
现在,我们可以访问总长度和当前播放的持续时间。我们现在希望在每次播放新曲目时更新当前曲目。
第 3 步 – 在播放启动时更新控制台
当用户点击播放按钮、双击特定歌曲或点击下一曲或上一曲按钮时,会播放新曲目。
如果你查看之前的迭代代码(代码 5.03 GUI.py),所有这些方法都调用self.player.start_play_thread()功能来启动播放。然而,现在我们需要在每次启动新的播放线程时更新控制台显示。
因此,我们需要重构我们的代码。首先,我们将所有调用player.start_play_thread()的路由到一个单一的方法中,该方法将在播放线程启动时更新显示。
第 4 步 – 定期更新计时器和进度条
因此,我们定义了一个名为launch_play的新方法,并将之前代码中所有player.start_play_thread()的实例替换为现在调用我们的launch_play方法,如下(见代码 5.04.py main-gui.py):
def launch_play(self):
try:
self.player.pause()
except:
pass
self.player.start_play_thread()
song_lenminute = str(int(self.player.song_length/60))
song_lenseconds = str (int(self.player.song_length%60))
filename = self.currentTrack.split('/')[-1] + '\n ['+ song_lenminute+':'+song_lenseconds+']'
self.canvas.itemconfig(self.songname, text=filename)
self.progressBar["maximum"]=self.player.song_length
self.update_clock_and_progressbar()
代码的描述如下:
-
代码首先尝试停止当时可能正在播放的任何其他曲目,因为我们不希望在特定时间内播放多个曲目。
-
然后它开始在另一个线程中播放下一首曲目。线程方法会自动更新歌曲长度,现在我们可以通过变量
self.player.song_len访问歌曲长度。 -
接下来的两行将歌曲长度转换为等价的分钟和秒。
-
下一行将文件名分割,并从完整路径中获取歌曲名称。然后它将计算出的分钟和秒数追加到文件名中。
-
我们为进度条设置了
maximum值,这是一个浮点数,指定了进度条的最大值。要查看 ttk Progressbar 的所有可配置选项,请在你的 Python 交互式控制台中输入以下内容:>>> import ttk >>> help(ttk.Progressbar) -
下一行使用
canvas.itemconfig更新显示控制台中的歌曲名称和歌曲长度。注意
就像我们使用
config来更改与小部件相关选项的值一样,Canvas 小部件使用itemconfig来更改画布内单个项目的选项。itemconfig的格式如下:itemconfig(itemid, **options)。
在这一步,我们将学习如何定期更新计时器和进度条。尽管对于一首特定的歌曲,歌曲名称和歌曲长度只需更新一次,但播放时间和进度条需要在小间隔内更新。因此,我们通过一个名为 update_clock_and_progressbar() 的独立方法来处理它。
我们希望以你通常在数字时钟中找到的格式显示时间。因此,我们定义了一个名为 timepattern 的字符串格式,如下所示:
timepattern = '{0:02d}:{1:02d}'。
现在,让我们将注意力转向更新显示时钟和进度条。我们已经调用了 update_clock_and_progressbar(),它应该负责这项工作。现在让我们编写这个方法,如下所示(见 Code 5.04.py main-gui.py):
def update_clock_and_progressbar(self):
current_time = self.player.current_time()
song_len = (self.player.song_len())
currtimeminutes = int(current_time/60)
currtimeseconds = int(current_time%60)
currtimestrng = self.timepattern.format(currtimeminutes, currtimeseconds)
self.canvas.itemconfig(self.clock, text= currtimestrng)
self.progressBar["value"] = current_time
self.root.update()
if current_time == song_len: #track is over
self.canvas.itemconfig(self.clock, text= '00:00')
self.timer=[0,0]
self.progressBar["value"] = 0
else:
self.canvas.after(1000, self.update_clock_and_progressbar)
此代码每 1000 毫秒运行一次,强制根更新,并更改时间和进度条值。为了保持定期运行,它在每 1000 毫秒后调用自己。
当一首曲目结束时,它会将计时器和进度条的重置为零,并退出更新循环。
注意
我们使用了 canvas.after 方法以一秒的间隔调用相同的方法。因此,在整个当前曲目的播放过程中,该方法将以一秒的间隔被调用。我们还设置了一个条件,当当前曲目播放结束时跳出循环。
目标完成 - 简短总结
这完成了这一迭代。在这个迭代中,我们构建了一个功能性的显示控制台和进度条来显示时间和当前曲目的信息。
我们首先在根窗口的顶部创建了一个空白画布。然后我们添加了一个类似于显示控制台的画面。然后我们使用 canvas.create_text 来精确地定位计时器时钟、当前播放曲目的名称和总曲目长度在控制台中。
我们还创建了一个 ttk 进度条。
我们然后使用 pyglet API 计算了曲目长度。接下来,我们通过一个中间方法播放曲目,该方法更新控制台上的当前播放曲目信息。
我们还添加了一个方法来定期更新时钟和进度条。
循环播放曲目
因此,现在我们有一个功能性的播放器。尽管它缺少一个关键功能。没有循环跟踪。这意味着每次用户收听曲目时,播放器在播放完该曲目后会停止。它不会跳转到播放列表中的下一曲目。
让我们提供一些单选按钮,让用户选择循环结构。在这个迭代的结束时,我们将向我们的播放器添加以下功能:

从本质上讲,我们的播放器应该提供以下选择:
-
无循环:播放一首曲目并结束
-
循环当前:重复播放单个曲目
-
全部循环:播放整个播放列表,一首接一首
让我们来编写这个功能。
启动推进器
第 1 步 – 创建单选按钮
在GUI类中创建单选按钮的对应代码如下(见代码 5.05 main-gui.py):
#radio buttons added to create_bottom_frame
self.loopv = IntVar()
self.loopv.set(3)
for txt, val in self.loopchoices:
Radiobutton(bottomframe, text=txt, variable=self.loopv, value=val).grid(row=5, column=4+val, pady=3)
第 2 步 – 歌曲结束回调
让我们先看看当歌曲结束时播放器结束逻辑的代码。我们需要一种方法在歌曲播放完成后调用一个方法。幸运的是,pyglet 播放器允许一个on_eos(歌曲结束)回调。
我们首先修改了player类中现有的play_media方法,以包含这个回调函数。(见代码 5.05 player.py)
self.myplayer.push_handlers(on_eos=self.what_next)
这个回调在给定歌曲结束时执行。我们将回调添加到名为what_next的方法中。
第 3 步 – 下一步是什么?
这个what_next方法本质上是在循环中查找所选选项并相应地采取一些行动。what_next的代码如下:
def what_next(self):
if self.stopped:
self.stopped = False
return None
if self.parent.loopv.get() == 1:
# No Loop
return None
if self.parent.loopv.get() == 2:
# Loop current
self.parent.launch_play()
if self.parent.loopv.get() == 3:
# Loop All
self.fetch_next_track()
代码的描述如下:
-
如果在中间停止了曲目,
on_eos回调也会被调用。这意味着如果发生停止操作,我们不想做任何事情。因此,我们通过调用一个空返回来跳出方法。 -
然后代码检查
self.parent.selectedloopchoice的值。 -
如果选定的循环值是
1(不循环),它不会播放下一首歌曲,而是通过返回语句跳出方法。 -
如果循环值是
2(循环当前歌曲),它再次调用launch_play方法而不更改当前曲目。 -
如果循环值是
3(全部循环),它将调用另一个名为fetch_next_track的方法。
第 4 步 – 获取下一曲目
获取下一曲目的fetch_next_track方法的代码如下:
def fetch_next_track(self):
try:
next_trackindx = self.parent.alltracks.index(self.parent.currentTrack) +1
self.parent.currentTrack = self.parent.alltracks[next_trackindx]
self.parent.launch_play()
except:pass
# end of list – do nothing
代码的描述如下:
- 这段代码简单地增加索引值,将当前曲目变量设置为所有歌曲列表中的下一个项目,并调用
launch_play()来播放下一曲目。
目标完成 – 简短总结
这完成了我们在播放器中循环的编码。
这次迭代依赖于 pyglet 允许一个on_oes(歌曲结束)回调。在曲目结束时,我们使用这个回调来检查用户指定的循环选择。
如果用户不想循环播放列表,我们传递一个空返回语句。如果用户想要循环当前歌曲,我们调用launch_play方法而不增加当前曲目。如果用户想要循环整个列表,我们调用一个名为fetch_next_track的方法,该方法将歌曲索引增加一个,然后调用launch_play方法来播放下一首歌曲。
在这次迭代中,我们还看到了单选按钮的一个示例用法。
我们的播放器现在可以根据用户提供的偏好循环播放播放列表。
添加上下文菜单
在这次快速迭代中,我们添加了一个上下文弹出菜单或右键单击菜单,带有对播放器上一些常见操作的快捷方式。
目前,我们将在右键菜单中添加两个功能:播放和删除。
完成后,右键菜单将打开,如下面的截图所示:

启动推进器
第 1 步 – 创建上下文菜单
我们在我们的文本编辑器中做了类似的功能上下文菜单,所以这里做一个快速总结。
我们添加了一个新方法context_menu,并从GUI __init__方法中调用它,如下所示(见代码 5.06 main-gui.py):
def create_context_menu(self):
self.context_menu = Menu(self.root, tearoff=0)
self.context_menu.add_command(label="Play", command=self.identify_track_to_play)
self.context_menu.add_command(label="Delete", command=self.del_selected)
我们还定义了一个show_context_menu方法,并将其绑定到create_list_frame中的鼠标右键点击<<Button-3>>,紧挨着 Listbox 控件定义的地方,如下所示:
def show_context_menuContext_menu(self,event):
self.context_menu.tk_popup(event.x_root+45, event.y_root+10,0)
第 2 步:重写关闭按钮
当我们做到这一点时,让我们编写一个被忽视的小功能。现在我们已经有了遍历整个播放列表的能力,我们不希望播放器在停止播放的歌曲之前关闭。因此,让我们重写root.destroy()方法,在退出之前停止曲目。
要重写 destroy 方法,我们首先在GUI __init__方法中添加一个协议重写方法,如下所示(见代码 5.06 main-gui.py):
self.root.protocol('WM_DELETE_WINDOW', self.close_player)
最后,让我们定义我们的close_player方法,如下所示:
def close_player(self):
if tkMessageBox.askokcancel("Quit", "Really want to quit?"):
try:
self.player.pause()
except:
pass
self.root.destroy()
目标完成 – 简短总结
现在上下文菜单已添加到我们的程序中。用户现在可以右键单击一个项目,选择播放或删除它。
我们还重写了我们的关闭按钮,以确保在我们退出播放器之前,任何正在播放的曲目都会停止。
添加工具提示并完成我们的播放器
在这次迭代中,我们将工具提示(也称为气球控件)添加到我们播放器中的所有按钮上。
工具提示是一个小弹出窗口,当你在绑定控件(在我们的例子中是按钮)上悬停鼠标时会出现。我们应用程序上的典型工具提示如下面的截图所示:

准备起飞
虽然 Tkinter 的核心库有许多有用的控件,但它远非完整。对我们来说,工具提示或气球控件并不是作为 Tkinter 的核心控件提供的。因此,我们在所谓的Tkinter 扩展中寻找这些控件。
这些扩展实际上只是经过修改的 Tkinter 控件,以实现 Tkinter 未提供的新功能。
实际上,有数百个 Tkinter 扩展。事实上,我们可以编写自己的 Tkinter 扩展。然而,一些流行的 Tkinter 扩展如下:
-
Python Mega Widgets(PMW)可在
pmw.sourceforge.net找到 -
Tix可在
wiki.Python.org/moin/Tix找到 -
TkZinc可在
wiki.Python.org/moin/TkZinc找到 -
Widget Construction Kit(WCK)可在
effbot.org/zone/wck.htm找到
PMW 扩展列表
谈到 PMW,以下是从该包中提供的快速控件扩展列表:
控件
-
ButtonBox
-
ComboBox
-
计数器
-
EntryField
-
Group
-
HistoryText
-
LabeledWidget
-
MainMenuBar
-
MenuBar
-
MessageBar
-
NoteBook
-
OptionMenu
-
PanedWidget
-
RadioSelect
-
ScrolledCanvas
-
ScrolledField
-
ScrolledFrame
-
ScrolledListBox
-
ScrolledText
-
时间计数器
对话框
-
AboutDialog
-
ComboBoxDialog
-
CounterDialog
-
Dialog
-
MessageDialog
-
PromptDialog
-
SelectionDialog
-
TextDialog
杂项
-
Balloon
-
Blt(用于图形生成)
-
颜色模块函数
提示
PMW 提供了大量扩展小部件的列表。要演示所有这些小部件,请浏览您之前安装的 PMW 包,并查找名为 demo 的目录。在 demo 目录中,查找名为 all.py 的文件,该文件演示了所有 PMW 扩展及其示例代码。
启动推进器
第 1 步 – 导入 PMW
PMW 提供了气球小部件的实现,但它不是 Tkinter 标准库的一部分。我们需要添加它。要添加 PMW,请参考我们在 任务清单 部分的讨论。一旦添加,您需要将 PMW 导入到您的命名空间中,如下所示(参见 代码 5.07 main-gui.py):
import Pmw
第 2 步 – 实例化气球小部件
然后,我们在 __init__ 方法中从主循环中实例化气球小部件,如下所示:
self.balloon = Pmw.Balloon(self.root)
第 3 步 – 为我们播放器中的所有按钮添加气球工具提示
最后,我们将气球小部件绑定到我们播放器中的每个按钮小部件上。我们不会为每个按钮重复代码。然而,格式如下:
balloon.bind(name of widget, 'Description for the balloon')
因此,我们的“添加文件”按钮将具有以下气球绑定:
self.balloon.bind(add_filebtn, 'Add New File')
我们在 5.07 main-gui.py 中的每个按钮上添加了类似的代码。
在结束这个迭代之前,让我们给我们的播放器添加一个标题,并添加一个标题栏图标,如下所示:
self.root.title('Media Player')
self.root.iconbitmap('../icons/mp.ico')
目标完成 – 简短总结
这完成了迭代。我们使用 PMW Tkinter 扩展为我们的播放器按钮添加了气球工具提示。
最重要的是,我们了解了 Tkinter 扩展及其何时使用。
提示
当您遇到一个作为核心小部件不可用的小部件实现需求时,请尝试在 PMW 或 Tix 中查找其实现。如果您找不到适合您需求的一个,请在互联网上搜索其他 Tkinter 扩展。
如果您仍然找不到您想要实现的功能,请尝试使用 WCK,它允许您实现所有类型的自定义小部件。然而,请注意 WCK 已经长时间没有活跃开发了。
任务完成
这标志着本项目的结束。我们的音频媒体播放器已经准备好了!
让我们回顾一下在这个项目中我们提到的事情。
我们在这个项目中涉及的一些主题可以总结如下:
-
我们加强了许多在前一个项目中讨论过的 GUI 编程技术
-
我们学习了如何使用更多的小部件,例如 Listbox、ttk Scale、Progressbar 和 Radiobutton
-
我们进一步了解了 Canvas 小部件的强大功能
-
我们看到了如何使用外部 API 来简化程序开发
-
我们了解了一些常见的 Tkinter 扩展,例如 PMW、WCK、Tix 等
-
我们还看到了如何在开发的每个阶段重构代码
热身挑战
这里有一些你可以尝试的挑战:
-
目前,我们的代码是单独添加每个按钮的。这使得程序变得冗长,并添加了不必要的样板代码。重构此代码,使用循环来添加所有按钮。这应该会显著缩短我们
GUI类的长度,同时简化按钮的处理,使其可以通过一个小循环来处理。 -
目前,程序仅在单个程序运行期间记录歌曲。歌曲需要在后续运行中加载。尝试通过对象持久化,根据上次运行的播放列表历史自动加载播放列表。
-
找到一个 Python 包,让你能够从音频文件中提取有用的元数据:例如作者、流派、频率和声道数。使用这些元数据在显示控制台中显示更多关于当前曲目信息。
-
为播放器添加皮肤功能,让用户可以为播放器选择不同的皮肤。
-
注意查找一些支持在线音频流的网络相关包。整合能够收听在线电台的功能。
第六章。绘制应用程序
我们现在正在开发我们的最后一个主要 Tkinter 应用程序。在这个项目中,我们将开发一个绘图应用程序,广泛使用 Tkinter Canvas 小部件,并应用我们迄今为止所学的一切。
任务简报
我们的绘图程序将使用户能够绘制基本形状,如线条、圆圈、矩形和多边形。它还将允许用户使用画笔工具以不同的颜色绘制,这些颜色可以从调色板中选择。
在其最终形式中,我们的绘图程序将看起来像以下截图:

为什么它很棒?
尽管应用程序本身很简单,但它足以展示与 GUI 编程相关的一些重要方面。
本项目旨在强调两个重要的教训。首先,我们将体验 Canvas 小部件的力量。其次,我们将学习如何为我们的应用程序开发高级自定义 GUI 框架。
正如我们将看到的,自定义 GUI 框架使我们能够用最少的代码重复快速开发程序。
到这个项目结束时,你不仅应该能够扩展这个应用程序以添加更多功能,还应该能够承担并实现越来越复杂的 GUI 项目。
你的热手目标
本项目的关键学习目标可以概述如下:
-
学习构建用于快速应用程序开发的自定义 GUI 框架
-
为我们的代码编写小的单元测试
-
理解如何在我们的项目中使用继承
-
了解其他 Tkinter 模块,例如
tkColorChooser -
在 Canvas 小部件上创建和操作项目
-
使用 tk ComboBox 小部件
-
了解可用的
winfo方法 -
在 Canvas 小部件上处理鼠标事件
-
巩固我们在以前项目中学习到的内容
任务清单
如果你已经开发了棋盘游戏,你可能已经安装了Python Imaging Library(PIL)来渲染 PNG 文件。这是我们程序的唯一外部依赖。如果你还没有这样做,请从以下链接下载并安装 PIL:
www.pythonware.com/products/pil/
如果你正在使用 Windows x64(64 位)或 MacOSX 机器,你可能需要安装并使用 Pillow,它是 PIL 的替代品,可在以下链接找到:
www.lfd.uci.edu/~gohlke/pythonlibs/#pillow
安装完包后,转到你的 Python 交互式提示符并输入:
>>from PIL import ImageTk
如果这个程序没有错误信息执行,你就准备好制作我们的绘图应用程序了。
开发一个裸骨 GUI 框架
在这个项目中,最重要的教训之一是学习如何开发自定义 GUI 框架。Tkinter 本身就是一个 GUI 框架。然而,我们打算在这里构建的框架是一个更高级的框架,它建立在 Tkinter 之上,以满足我们的自定义编程需求。
我们不会开发一个完整的框架。相反,我们只会开发其中的一小部分,以便让你了解构建自定义框架的感觉。
准备起飞
那么为什么我们需要在 Tkinter 之上再构建一个框架呢?
考虑一个拥有 10 个不同菜单的大程序,每个菜单有 10 个菜单项。我们可能需要编写 100 行代码仅仅是为了显示这 100 个菜单项。
你不仅需要手动制作每个小部件,而且还需要将它们手动链接到其他命令,同时还需要为每个小部件设置大量的选项。
如果我们对所有的小部件都这样做,我们的 GUI 编程就变成了打字练习。你写的每一行额外的代码都会增加程序的复杂性,从某种意义上说,它变得对其他人来说更难阅读、维护、修改和/或调试代码。
这就是开发自定义框架如何帮助我们。让我们看看这意味着什么。
假设我们预计我们的绘图程序将有大量的菜单项。现在我们知道如何添加菜单和菜单项。每个新的菜单项至少需要一行代码来显示。
为了避免编写这么多行代码,让我们首先构建一个框架来解决这个问题。
为了简化菜单创建的过程,我们将编写一段代码,它接受作为元组的菜单项列表,并将其转换为等效的菜单代码。
所以给定以下元组:
menuitems = ('File- &New/Ctrl+N/self.new_file, &Open/Ctrl+O/self.open_file','Edit- Undo/Ctrl+Z/self.undo, Sep','About- About//self.about')
应该生成相应的菜单项,其中字符串的第一个项目(在破折号-之前)代表菜单按钮,字符串的每个后续部分,由逗号分隔,代表一个菜单项、其加速键和附加的命令回调。符号&的位置代表要下划线的快捷键的位置。
我们还需要注意在菜单项之间添加分隔符。要添加分隔符,我们会在需要的位置添加字符串Sep。更精确地说,字符串Sep必须是大写的。
简而言之,通过我们的方法传递这个元组应该生成一个 GUI,如下面的截图所示:

为了扩展我们的菜单项,我们只需要扩展前面的元组,并同时添加相应的命令回调方法。
启动推进器
第 1 步 – 创建 GUI 框架类
我们在名为framework.py的文件中构建我们的框架,其中我们定义了一个名为GUIFramework的新类,如下所示:
import Tkinter as tk
class GUIFramework(object):
menuitems = None
def __init__(self, root):
self.root = root
if self.menuitems is not None:
self.build_menu()
第 2 步 – 创建菜单构建器
GUIFramework中用于创建菜单构建器的两种方法如下:
def build_menu(self):
self.menubar = tk.Menu(self.root)
for v in self.menuitems:
menu = tk.Menu(self.menubar, tearoff=0)
label, items = v.split('-')
items = map(str.strip, items.split(','))
for item in items:
self.__add_menu_command(menu, item)
self.menubar.add_cascade(label=label, menu=menu)
self.root.config(menu=self.menubar)
def __add_menu_command(self, menu, item):
if item == 'Sep':
menu.add_separator()
else:
name, acc, cmd = item.split('/')
try:
underline = name.index('&')
name = name.replace('&', '', 1)
except ValueError:
underline = None
menu.add_command(label=name, underline=underline,
accelerator=acc, command=eval(cmd))
代码的描述如下所示:
-
build_menu方法通过名为self.menubar的元组进行操作,必须指定所有所需的菜单和菜单项,格式必须与之前讨论的完全一致。 -
它遍历元组中的每个项目,根据
-分隔符拆分项目,为-分隔符左侧的每个项目构建顶级菜单按钮。 -
然后,它根据
,(逗号)分隔符拆分字符串的第二部分。 -
然后,它遍历这个第二部分,为每个部分创建菜单项,使用另一个方法
__add_menu_command添加加速键、命令回调和下划线键。 -
__add_menu_command方法遍历字符串,如果找到字符串Sep,则添加分隔符。如果没有找到,它将在字符串中搜索&符号。如果找到,它将计算其索引位置并将其分配给下划线变量。然后它将&值替换为空字符串,因为我们不希望在菜单项中显示&符号。 -
如果字符串中没有找到
&符号,代码将None赋值给下划线变量。 -
最后,代码将命令回调、加速键和下划线值添加到菜单项中。
提示
我们用来定义菜单构建器的逻辑是完全任意的表示。我们也可以使用字典或列表。我们也可以有完全不同的逻辑来表示我们的菜单项,只要它能够为我们生成菜单项。
第 3 步 – 测试我们的新框架
最后,我们在文件中添加一个TestThisFramework类来测试我们的框架是否按预期工作,特别是我们的build_menu方法。
TestThisFramework类的代码如下(framework.py):
class TestThisFramework(GUIFramework):
menuitems = (
'File- &New/Ctrl+N/self.new_file,&Open/Ctrl+O/self.openFile',
'Edit- Undo/Ctrl+Z/self.undo, Sep',
'About- About//self.about'
)
def new_file(self):
print 'newfile tested OK'
def openFile(self):
print 'openfile tested OK'
def undo(self):
print 'undo tested OK'
def about(self):
print 'about tested OK'
if __name__ == '__main__':
root= tk.Tk()
app = TestThisFramework(root)
root.mainloop()
代码的描述如下:
-
我们的
TestThisFramework类继承自GUIFramework类,因此可以调用父类中定义的build_menu方法。 -
然后,它添加一个菜单项列表
menuitems并调用build_menu()方法。重要的是元组必须由名称menuitems定义,因为我们的build_menu()方法在父GUIFramework类中结构化,仅构建名为menuitems的元组上的菜单。 -
测试类还添加了虚拟命令来处理每个菜单项的命令回调。
-
运行此测试将根据元组中指定的内容构建菜单项。尝试扩展元组以添加更多菜单项,框架将成功将这些项包含在菜单中。
提示
就像我们添加了生成菜单的代码一样,我们也可以为其他我们预见将在程序中重复使用的控件添加类似的代码。但我们将框架开发留在这里,并继续开发我们的绘图应用程序。
为较小的程序开发框架可能是过度设计,但它们对于较长的程序是无价之宝。希望你现在应该能够欣赏为大型程序编写自定义框架的好处。
目标完成 – 简短总结
现在我们已经准备好了build_menu,我们可以扩展它以添加所需的任何菜单项,而无需为每个项编写重复且类似的代码。
这标志着我们的第一次迭代结束,我们为自定义 GUI 框架奠定了基础。我们不会进一步扩展框架,但希望您现在应该能够根据需要扩展它以用于其他小部件。
分类智能
在本节中,您也看到了我们的TestThisFramework类如何从我们的GUIFramework类继承特性。这是我们第一次在我们的程序中使用继承。
到目前为止,我们总是将作为类创建的对象传递给其他类作为参数,然后使用.(点)符号来使用它们。这被称为组合。
使用继承,我们不需要使用点符号来访问另一个类的成员方法。我们可以在子类中使用超类的方法,就像它们属于子类一样。
继承带来了动态绑定和多态的优势。
动态绑定意味着要调用的方法是在运行时决定的,从而为我们的代码设计提供了更大的灵活性。多态意味着超类变量持有从自身或其任何子类创建的对象的引用。
小贴士
继承适用于子类对象与超类对象类型相同的情况。在我们的例子中,无论您是从超类还是从子类定义菜单项,菜单项都将保持不变。因此,我们在超类中定义了它,并在子类中继承了它。
然而,如果对象需要根据对象的条件或状态以不同的方式出现或表现,则组合更可取。
构建我们的绘图程序
现在我们来设置绘图程序的基本结构。我们希望达到的结构,如下面的截图所示:

此处的结构主要包含一个顶部菜单,它继承自我们在上一次迭代中创建的 GUI 框架build_menu方法。
此外,我们在顶部创建了一个带有create_top_bar()方法的顶部栏框架(用黄色标记),以及一个带有create_tool_bar()的左侧工具栏框架(用深灰色背景标记)。我们还使用create_drawing_canvas()方法在右侧创建了一个 Canvas 小部件,它将作为我们的绘图区域。
我们将不会重新生成创建框架和画布区域的代码,因为我们已经在以前的项目中做过类似的事情,并且您现在应该能够轻松地完成它们。然而,您可以在文件6.01.py中查看实际的代码。
启动推进器
第一步 – 导入框架
这里要注意的第一点是,我们在这里导入之前创建的框架模块,并在主类中继承其属性,如下所示:
import framework
class GUI(framework.GUIFramework):
这使我们能够像它属于子类一样使用我们在框架中定义的 build_menu 方法。
第 2 步:构建顶部菜单
接下来,我们定义实际的菜单项以在 create_menu 方法中构建顶部菜单,如下所示:
def create_menu(self):
self.menubar = Menu(self.root)
self.menuitems = (
'File- &New/Ctrl+N/self.new_file, &Open/Ctrl+O/self.open_file,
Save/Ctrl+S/self.save,
SaveAs//self.save_as,
Sep,
Exit/Alt+F4/self.close',
'Edit- Undo/Ctrl+Z/self.undo, Sep',
'About- About//self.about')
self.build_menu()
self.root.config(menu=self.menubar)
代码描述如下:
-
这实际上创建了三个菜单按钮:文件、编辑和关于,并将前一个提供的元组中的菜单项添加到每个按钮中。
-
创建菜单也需要再次创建它们相关的命令回调,如前一个元组中定义的那样。因此,我们创建了与这些命令回调相关的方法。
-
我们将不会重现
new_file、open_file、close、undo和about等功能的代码,因为我们已经在之前的项目中进行了类似的编码。然而,让我们看看撤销和保存操作。
第 3 步 – 在 Canvas 小部件上执行撤销操作
回想一下,Tkinter 文本小部件具有内置的无限制撤销/重做功能。然而,Canvas 小部件没有这个内置功能。
在这里,我们实现了一个非常基本的撤销操作,允许我们删除画布上最后绘制的项目,如下所示:
def undo(self, event=None):
self.canvas.delete(self.currentobject)
代码描述如下:
-
Canvas 小部件提供了一个
widget.delete(items)方法,可以从画布中删除指定的项目。注意
但是,一旦您删除了画布项目,它就永远消失了。除非您在删除之前实现了存储该项目所有可配置选项的方法,否则您无法恢复它。
虽然可以通过存储要删除的项目所有配置来实施完整的撤销/重做操作,但我们不会在这里实施它,因为这会偏离我们的核心主题。
第 4 步 – 保存画布对象
Tkinter 允许您使用 postscript() 命令将画布对象保存为 postscript 文件,如下所示(参见 代码 6.01.py):
def actual_save(self):
self.canvas.postscript(file=self.filename, colormode='color')
self.root.title(self.filename)
然而,请注意,此命令不包括画布上的图像和嵌入的小部件。
第 5 步 – 在左侧工具栏中创建按钮
在为所有我们的菜单项编码了命令回调之后,我们现在将在左侧工具栏上创建按钮。根据我们的原始计划,我们需要在工具栏上放置八个按钮。目前,让我们将按钮文本显示为 0 到 7,如下所示:
def create_tool_bar_buttons(self):
for i in range(8):
self.button = Button(self.toolbar, text=i, command=lambda i=i:self.selected_tool_bar_item(i))
self.button.grid(row=i/2, column=1+i%2, sticky='nsew')
这创建了八个按钮,根据按钮编号是奇数还是偶数将它们排列成两列。
第 6 步 – 将命令回调添加到按钮
所有按钮都连接到同一个命令回调 selected_tool_bar_item,它将按钮编号作为其参数。回调方法将在下一次迭代中继续。然而,现在让我们简单地定义回调以打印被点击的按钮编号,如下所示:
def selected_tool_bar_item(self, i):
print'You selected button {}'.format(i)
第 7 步 – 创建调色板和颜色选择对话框
最后,让我们创建两个调色板来跟踪两个名为背景色和前景色的颜色。
注意
Tkinter 提供了一个tkColorChooser模块,该模块弹出颜色选择对话框。当用户选择一种颜色并点击确定按钮时,该模块返回一个元组,其形式如下:
((r,g,b), 'hex color code')
返回元组的第一个元素本身是一个元组,指定了给定颜色的 RGB 坐标,而第二个元素是所选颜色的十六进制颜色代码。
这里的想法是点击调色板应该打开一个颜色选择器。当用户选择一个给定的颜色时,它应该更新对象的背景色和前景色属性,如下面的截图所示:

实现此功能所需的代码如下(见 code 6.01.py):
from tkColorChooser import askcolor
def create_color_pallete(self):
self.colorpallete= Canvas(self.toolbar, height=55, width =55)
self.colorpallete.grid(row=10, column=1, columnspan=2, pady=5, padx=3)
self.backgroundpallete = self.colorpallete.create_rectangle (15, 15,48,48,tags="backgroundpallete", outline=self.background, fill=self.background)
self.foregroundpallete = self.colorpallete.create_rectangle(1,1,33,33,tags="foregroundpallete", outline=self.foreground, fill=self.foreground)
self.colorpallete.tag_bind(self.backgroundpallete,
"<Button-1>", self.set_background_color)
self.colorpallete.tag_bind(self.foregroundpallete, "<Button-1>", self.set_foreground_color)
代码的描述如下:
-
我们为每个正方形部分添加两个不同的标签,然后使用
tag_bind命令将它们绑定到鼠标按钮的点击事件上。请注意 widget 级绑定(widget.bind)和使用tag_bind方法的项特定绑定的区别 -
要创建颜色调色板,我们首先在工具栏框架内创建一个 Canvas 小部件。在这个画布内,我们使用
canvas.create_rectangle创建两个正方形区域,并将它们绑定到单个鼠标点击事件,分别调用set_background_color和set_foreground_color。
第 8 步 – 设置背景和前景调色板的颜色
背景和前景调色板的颜色可以设置如下:
def set_background_color(self, event=None):
self.background = askcolor()[-1]
self.colorpallete.itemconfig(self.backgroundpallete, outline=self.background, fill=self.background)
def set_foreground_color(self, event=None):
self.foreground = askcolor()[-1]
self.colorpallete.itemconfig(self.foregroundpallete,outline=self.foreground, fill=self.foreground)
第 9 步 – 显示鼠标移动的 x 和 y 坐标
最后,我们在工具栏框架中添加了一个静态标签来跟踪鼠标移动的 x 和 y 坐标。实际的跟踪功能将在以后创建,但现在让我们通过放置一个静态标签来预留空间,如下所示:
self.curcoordlabel = Label(self.toolbar, text='x: 0\ny:0') self.curcoordlabel.grid(row=13, column=1, columnspan=2, pady=5, padx=1, sticky='w')
目标完成 – 简短总结
这完成了我们的第二次迭代。在这个迭代中,我们为我们的绘图程序设置了基本结构。
重要的是,我们看到了如何从先前创建的框架中继承功能,以最小化编码创建菜单项。
我们还添加了颜色选择对话框,使用tkColorChooser模块,该模块设置了两个属性self.background和self.foreground,以供应用程序范围使用。
处理鼠标事件
在我们让用户在画布上绘制之前,我们需要将画布事件绑定到鼠标移动和鼠标点击。
在画布小部件上绘制或添加任何项目之前,我们首先需要知道项目放置位置的坐标。
注意
Canvas 小部件使用两个坐标系来跟踪位置:
窗口坐标系:坐标表示为相对于根窗口的关系
画布坐标系:坐标表示为画布内项目的位置
您可以使用canvasx和canvasy方法将窗口坐标转换为画布坐标,如下所示:
canx = canvas.canvasx(event.x)
cany = canvas.canvasy(event.y)
启动推进器
第 1 步 – 在画布上绑定鼠标按下、鼠标移动和鼠标释放
在画布上绘制任何项目都将从用户点击鼠标按钮开始。绘制需要一直持续到鼠标移动且按钮被按下,直到鼠标按钮被释放。
因此,我们需要跟踪初始鼠标按下事件的位置。这将随后跟踪鼠标在按钮按下时的移动,直到最终的按钮释放事件。
因此,我们在画布上添加了以下小部件绑定(见代码 6.02.py):
self.canvas.bind("<Button-1>", self.mouse_down)
self.canvas.bind("<Button1-Motion>", self.mouse_down_motion)
self.canvas.bind("<Button1-ButtonRelease>", self.mouse_up)
第 2 步 – 计算鼠标移动的坐标
绑定了鼠标点击、鼠标移动和鼠标释放事件后,现在需要定义它们对应的回调方法。
特别是,我们希望mouse_down方法给我们提供第一次鼠标点击事件的 x 和 y 坐标,如下所示:
def mouse_down(self, event):
self.currentobject = None
self.lastx = self.startx = self.canvas.canvasx(event.x)
self.lasty = self.starty = self.canvas.canvasy(event.y)
我们希望持续更新lastx和lasty坐标,直到鼠标停止移动,如下所示:
def mouse_down_motion(self, event):
self.lastx = self.canvas.canvasx(event.x)
self.lasty = self.canvas.canvasy(event.y)
我们的mouse_up方法应该对lastx和lasty坐标进行最后的更新,如下所示:
def mouse_up(self, event):
self.lastx = self.canvas.canvasx(event.x)
self.lasty = self.canvas.canvasy(event.y)
代码的描述如下:
-
mouse_down方法简单地将startx、starty、lastx和lasty的值初始化为鼠标点击位置的坐标。 -
mouse_down_motion方法在鼠标移动发生时改变lastx和lasty的值。 -
最后,
mouse_up方法将lastx和lasty的值设置为鼠标按钮释放点的坐标。 -
因此,使用三个事件:
mouse_down、mouse_down_motion和mouse_up,我们成功地得到了起点坐标、鼠标指针穿越的点的坐标以及终点坐标。 -
现在,我们可以使用这些值在给定的坐标上放置画布上的任何项目。
第 3 步 – 更新左侧工具栏中的当前鼠标位置标签
此外,我们还想跟踪鼠标在画布上的移动,即使鼠标按钮没有被按下。我们需要跟踪这一点以更新左侧工具栏中的当前鼠标位置。这很简单,如下面的代码片段所示:
self.canvas.bind("<Motion>", self.show_current_coordinates)
def show_current_coordinates(self, event = None):
lx = self.canvas.canvasx(event.x)
ly = self.canvas.canvasy(event.y)
cord = 'x: %d \ny: %d '%(lx, ly)
self.curcoordlabel.config(text = cord)
此代码将确保任何在画布上移动的鼠标都会更新左侧工具栏中的当前鼠标位置标签。
目标完成 – 简短总结
现在我们的 Canvas 小部件已经对鼠标移动和鼠标点击做出了响应。每次我们在画布上点击鼠标按钮并拖动鼠标指针到新的位置时,startx、starty、lastx和lasty的值都会更新,以反映鼠标移动的坐标。
注意
这些坐标共同构成了一个项目的边界框。实际上,如果画布上有项目,你可以使用 API 检索任何给定项目的坐标:
canvas.bbox(item=itemName)
这将返回一个包含四个元素的坐标元组。
如果未指定项目名称,此方法将返回画布上所有元素的边界框。
现在我们有了坐标,我们可以考虑在画布上绘制项目。我们将在下一次迭代中进行一些绘制。
在画布上绘制项目
让我们现在在画布上绘制一些项目。Canvas 小部件原生支持绘制以下项目:
| 项目 | 添加项目的代码 |
|---|---|
| 弧线 | w.create_arc( bbox, **options) |
| 位图 | w.create_bitmap( bbox, **options) |
| 图像 | w.create_image( bbox, **options) |
| 线 | w.create_line( bbox, **options) |
| 椭圆 | w.create_oval( bbox, **options) |
| 多边形 | w.create_polygon( bbox, **options) |
| 矩形 | w.create_rectangle( bbox, **options) |
| 文本 | w.create_text( bbox, **options) |
| 窗口 | w.create_window( bbox, **options) |
让我们添加绘制线条、矩形和椭圆的能力到我们的绘图程序中。我们还将添加一个画笔描边功能到我们的程序中,如下面的截图所示:

启动推进器
第 1 步 – 创建方法元组
我们首先创建一个方法元组,如下所示:
all_toolbar_functions = ('draw_line', 'draw_rectangle', 'draw_oval', 'draw_brush')
这样做可以确保我们不需要从代码中显式调用每个方法。我们可以使用元组的索引来检索方法名称,并通过以下方式动态调用它:
getattr(self, self.all_toolbar_functions[index])
这在这里是有意义的,因为我们最终将通过扩展我们的all_toolbar_functions来简单地添加更多功能到我们的绘图程序中。
第 2 步 – 为工具栏按钮添加图标
我们接下来的任务是给左侧工具栏添加绘制这些项目的图标。
我们将图标添加到icons文件夹中。我们还确保将每个图标文件重命名为它调用的方法的名称。这种命名再次有助于动态调用方法,这种编程风格可以称为约定优于配置的编程。
我们当前的create_tool_bar_buttons()方法使用 for 循环创建了八个按钮。然而,我们现在将修改我们的create_tool_bar_buttons()方法,使用enumerate()方法遍历all_toolbar_functions元组中的所有项,为每个方法添加图标,如下所示(见code 6.03.py):
def create_tool_bar_buttons(self):
for i, item in enumerate(self.all_toolbar_functions):
tbicon = PhotoImage(file='icons/'+item+'.gif')
self.button = Button(self.toolbar, image=tbicon, command=lambda i=i:self.selected_tool_bar_item(i))
self.button.grid(row=i/2, column=1+i%2, sticky='nsew')
self.button.image = tbicon
第 3 步 – 跟踪当前选定的按钮
接下来,我们修改selected_tool_bar_item(i)方法;它的唯一目的是跟踪当前选定的按钮。有了这个信息,我们可以稍后通过使用此索引从all_toolbar_functions调用相关方法,如下所示(见code 6.03.py):
def selected_tool_bar_item(self, i):
self.selected_toolbar_func_index = i
第 4 步 – 绘制线条、矩形和椭圆形状的代码
现在是时候编写绘制这些基本形状的方法了。请注意,这不会自动创建绘图。最终,这些方法必须从某处调用以实际进行绘图。我们将在第 6 步中这样做。
def draw_line(self, x, y, x2, y2):
self.currentobject = self.canvas.create_line(x, y, x2, y2, fill= self.foreground )
def draw_rectangle(self, x, y, x2, y2):
self.currentobject = self.canvas.create_rectangle(x, y, x2, y2, fill= self.foreground)
def draw_oval(self, x, y, x2, y2):
self.currentobject= self.canvas.create_oval(x, y, x2, y2, fill= self.foreground)
第 5 步 – 连续绘制代码
连续绘制与绘制线条类似,但新线条会在坐标的每次小变化后重新绘制。在当前的状态下,lastx 和 lasty 的值仅在鼠标按钮释放时更新。但在这里,我们需要在鼠标移动时更新 lastx 和 lasty 的值。为了实现这一点,我们将鼠标移动绑定到一个新定义的方法 draw_brush_update_xy,该方法在每次后续循环迭代中更新 x 和 y 坐标。
之前,我们将鼠标按下移动绑定到了另一个名为 mouse_down_motion 的方法。为了绘制连续的笔触,我们现在将其绑定到名为 draw_brush_update_xy 的方法。
注意
将事件绑定添加到多个方法中会清除之前的绑定,新的绑定将替换任何现有的绑定。因此,当你退出 draw_brush 循环时,你需要重新绑定事件到 mouse_down_motion 方法。
或者,你可以使用 add="+" 作为额外的参数来保持对同一事件的多个绑定,如下所示:
mywidget.bind("<SomeEvent>", method1, add="+")
mywidget.bind("<SameEvent>", method2, add="+")
因此,我们创建了一个循环,其中 draw_brush 方法在连续的鼠标移动中调用另一个方法 draw_brush_update_xy 来更新 x 和 y 坐标,如下所示(见 代码 6.03.py):
def draw_brush(self, x, y, x2, y2):
if not self.all_toolbar_functions[ self.selected_toolbar_func_index] == 'draw_brush':
self.canvas.bind("<Button1-Motion>", self.mouse_down_motion)
return# if condition to break out of draw_brush loop
self.currentobject = self.canvas.create_line(x,y,x2,y2,fill=self.foreground)
self.canvas.bind("<B1-Motion>", self.draw_brush_update_xy)
def draw_brush_update_xy(self, event):
self.startx, self.starty = self.lastx, self.lasty
self.lastx, self.lasty = event.x, event.y
self.draw_brush(self.startx, self.starty,self.lastx, self.lasty)
如果“绘制画笔”按钮未选中,我们将退出循环并将鼠标移动重新绑定到画布的 mouse_down_motion。
第 6 步 – 动态执行代码
我们计划根据名为 all_toolbar_functions 的元组中给出的方法名称的索引动态执行方法。然而,这些名称被存储为字符串,我们无法仅取字符串的一部分并期望 Python 评估它。为了做到这一点,我们将使用 Python 的内置 getattr() 方法。
现在,我们定义了一个方法,该方法接受一个字符串并将其转换为适合执行的方法,如下所示:
def execute_method():
fnc = getattr(self, self.all_toolbar_functions [self.selected_toolbar_func_index])
fnc(self.startx, self.starty,self.lastx, self.lasty)
第 7 步 – 进行实际绘图
定义了绘制线条、矩形、椭圆和画笔笔触的方法后,我们需要从某处调用它们以进行绘图。直观上,绘图必须从第一次鼠标按下开始,并且绘图必须在鼠标按钮释放之前被删除并重新绘制。
因此,这些方法必须从我们的 mouse_down_motion 方法中调用。因此,我们修改了 mouse_down_motion 和 mouse_up 方法来完成这项工作,如下所示:
def mouse_down_motion(self, event):
self.lastx = self.canvas.canvasx(event.x)
self.lasty = self.canvas.canvasy(event.y)
if self.selected_toolbar_func_index:
self.canvas.delete(self.currentobject)
self.execute_method()
def mouse_up(self, event):
self.lastx = self.canvas.canvasx(event.x)
self.lasty = self.canvas.canvasy(event.y)
self.canvas.delete(self.currentobject)
self.currentobject = None
self.execute_method()
目标完成 – 简要回顾
这完成了本次迭代的任务。
我们首先创建了一个方法名称的元组,以便能够通过指定元组中的索引动态调用方法。
然后,我们为工具栏按钮添加了图标。然后,我们将按钮点击与一个方法关联起来,该方法通过将索引分配给变量 self.selected_toolbar_func_index 来跟踪当前选定的按钮。然后我们定义了在画布上绘制线条、矩形和椭圆形状的方法。我们还展示了如何利用绘制线条的能力进行连续绘制。
最后,我们从 mouse_down_motion 和 mouse_release 方法中调用了所有绘图方法来进行实际的绘图。
用户现在可以在画布上绘制基本形状,例如线条、矩形、椭圆形和笔触。这些形状将以当前设置的背景色绘制。
设置顶部选项工具栏
虽然我们的程序可以绘制基本形状,但这些形状目前填充的是前景色,形状的轮廓是用黑色完成的。
Canvas 小部件允许您指定大多数形状的填充颜色、轮廓颜色和边框宽度作为其可配置选项。
此外,Canvas 小部件还有许多其他基本形状的可配置选项。例如,对于线条,您可以指定它是否在末端有箭头形状,或者是否为虚线。
让我们相应地修改我们的程序,以便用户可以为四种基本形状中的每一种选择可配置的选项,如下面的截图所示:

启动推进器
第 1 步 – 在顶部显示所选按钮图标
让我们先从简单的事情开始。当用户在左侧工具栏中点击按钮时,顶部框架应显示文本 所选工具: 然后是所选按钮的图标表示。
因为这个事件必须在任何按钮的点击时发生,所以我们修改了 selected_tool_bar_item 方法,包括对两个方法的调用,如下面的代码所示(见 code 6.04.py):
def selected_tool_bar_item(self, i):
self.selected_toolbar_func_index = i
self.remove_options_from_topbar()
self.show_selected_tool_icon_in_topbar()
def remove_options_from_topbar(self):
for child in self.topbar.winfo_children():
child.destroy()
def show_selected_tool_icon_in_topbar(self):
Label(self.topbar,text='Selected Tool:').pack(side=LEFT)
photo = PhotoImage(file='icons/'+ 'self.all_toolbar_functions[self.selected_toolbar_func_index]+'.gif')
label = Label(self.topbar, image=photo)
label.image = photo
label.pack(side=LEFT)
代码的描述如下:
-
remove_options_from_topbar方法确保当点击新的按钮时,删除之前按钮的选项。show_selected_tool_icon_in_topbar方法实际上显示了当前所选按钮的图标。注意
widget.winfo_children()返回给定小部件的所有子小部件列表,按照从下到上的堆叠顺序。您可以使用许多
winfo方法中的任何一个来提取大量与窗口相关的信息。有关winfo方法的完整列表,请参阅附录 B 中的 基本小部件方法 部分,快速参考表。或者,每个小部件也有自己的子属性,它是一个字典,键是 ID,值是小部件。所以如果顺序不重要,这和
widget.children.values()是一样的。
第 2 步 – 添加 Combobox 小部件以让用户选择不同的填充选项
接下来,我们需要定义一个选择组合框,让用户可以选择填充、轮廓、宽度、箭头和虚线的选项。我们将使用 ttk Combobox 允许用户进行选择,因此我们将其导入到当前文件中,如下所示:
import ttk
我们在这里不会复制整个代码。然而,对于上述每个选项,我们定义了两个方法:一个用于显示组合框,另一个用于设置用户当前选择的值。
因此,我们为填充选项设置了以下两个定义,如下所示(见 代码 6.04.py):
def fill_options_combobox(self):
Label(self.topbar,text='Fill:').pack(side=LEFT)
self.fillcmbobx = ttk.Combobox(self.topbar, state='readonly', width=5)
self.fillcmbobx.pack(side=LEFT)
self.fillcmbobx['values'] = ('none', 'fg', 'bg', 'black', 'white' )
self.fillcmbobx.bind('<<ComboboxSelected>>', self.set_fill)
self.fillcmbobx.set(self.fill)
def set_fill(self, event=None):
fl = self.fillcmbobx.get()
if fl == 'none': self.fill = '' #transparent
elif fl == 'fg': self.fill = self.foreground
elif fl == 'bg': self.fill = self.background
else: self.fill = fl
我们以类似的方式为每个集合定义其他方法对,即(见 代码 6.04.py):
-
outline_options_combobox:set_outline -
width_options_combobox:set_width -
arrow_options_combobox:set_arrow -
dash_options_combobox:set_dash
第三步 – 修改绘图方法以添加可配置选项
现在我们有了设置填充、轮廓、箭头和虚线可配置选项不同值的方法,让我们修改我们的绘图代码,以包括实际的绘图,如下所示(见 代码 6.04.py):
def draw_line(self, x, y, x2, y2):
self.currentobject = self.canvas.create_line(x, y, x2, y2,
fill= self.fill, arrow=self.arrow, width=self.width, dash=self.dash )
def draw_rectangle(self, x, y, x2, y2):
self.currentobject = self.canvas.create_rectangle(x, y,x2, y2, outline=self.outline, fill=self.fill, width=self.width)
def draw_oval(self, x, y, x2, y2):
self.currentobject= self.canvas.create_oval(x, y, x2, y2, outline=self.outline, fill=self.fill, width=self.width)
def draw_brush(self, x, y, x2, y2):
if not self.all_toolbar_functions[self.selected_toolbar_func_index]=='draw_brush':
self.canvas.bind("<Button1-Motion>", self.mouse_down_motion)
return
self.currentobject = self.canvas.create_line(x,y,x2,y2, fill=self.fill, width=self.width)
self.canvas.bind("<B1-Motion>", self.draw_brush_update_xy)
定义了所有这些方法后,现在是时候从某处调用它们了。
虽然填充组合框适用于所有四个基本形状,但箭头选项仅适用于绘制线条。因为将会有不同选择的组合框集,我们定义以下方法(见 代码 6.04.py):
def draw_line_options(self):
self.fill_options_combobox()
self.width_options_combobox()
self.arrow_options_combobox()
self.dash_options_combobox()
def draw_rectangle_options(self):
self.fill_options_combobox()
self.outline_options_combobox()
self.width_options_combobox()
def draw_oval_options(self):
self.fill_options_combobox()
self.outline_options_combobox()
self.width_options_combobox()
def draw_brush_options(self):
self.fill_options_combobox()
self.width_options_combobox()
最后,这些方法必须从某处调用,这取决于所做的选择。因此,我们修改了 selected_tool_bar_item 方法,以动态调用一个方法,方法名是通过在所选方法名称后附加字符串 _options 来命名的,如下所示(见 代码 6.04.py):
def selected_tool_bar_item(self, i):
self.selected_toolbar_func_index = i
self.remove_options_from_topbar()
self.show_selected_tool_icon_in_topbar()
opt = self.all_toolbar_functions[ self.selected_toolbar_func_index] +'_options'
fnc = getattr(self, opt)
fnc()
目标完成 – 简短总结
程序用户现在可以从为每个工具栏按钮提供的各种选项中进行选择(见 代码 6.04.py)。
更重要的是,我们看到了在 Tkinter Canvas 小部件上绘制的项目可用的配置选项。我们还介绍了 winfo 方法。这些方法可以用来提取有关小部件的大量数据,当在 Tkinter 中编程 GUI 应用程序时,这是一个有用的工具。
添加更多功能
接下来,让我们向我们的绘图程序添加一些更多功能。特别是,我们将添加从画布中删除对象的能力,添加一个油漆桶,以及移动项目上下堆叠的能力,以及拖动画布上项目的能力,如下面的截图所示:

启动推进器
第一步 – 扩展我们的方法元组
首先,让我们扩展我们的 all_toolbar_functions 方法,为我们将在此定义的新方法提供便利,如下所示(见 代码 6.05.py):
all_toolbar_functions = ('draw_line', 'draw_rectangle', 'draw_oval', 'draw_brush', 'delete_object', 'fill_object', 'move_to_top', 'drag_item')
如往常一样,我们通过添加与处理该功能的方法同名的方法到 icon 文件夹中,为 icon 文件夹添加了图标。通过向这个元组添加新方法,并添加相应的图标到我们的 icon 文件夹,由于我们设计的 create_tool_bar_buttons 方法,按钮会自动显示在我们的左侧工具栏中。
第二步 – 定位画布上的特定项目
在定义处理新功能的方法之前,让我们暂停一下,思考一下这里需要做的工作。
我们现在想要执行的操作与其前辈略有不同。以前,我们在画布上创建项目。现在我们必须针对画布上已经存在的项目。
需要被针对的项目是用户用鼠标点击的项目。
因此,在我们对项目本身进行任何修改之前,我们需要先识别鼠标点击的项目。为此,我们修改了我们的 mouse_down 方法,如下所示(见 code 6.05.py):
def mouse_down(self, event):
self.currentobject = None
self.lastx = self.startx = self.canvas.canvasx(event.x)
self.lasty = self.starty = self.canvas.canvasy(event.y)
if self.all_toolbar_functions[ self.selected_toolbar_func_index]
in ['fill_object', 'delete_object', 'move_to_top', drag_item']:
try:
self.selected_object = self.canvas.find_closest(self.startx, self.starty)[0]
except:
self.selected_object = self.canvas
代码的描述如下:
-
这个对
mouse_down方法的微小修改意味着,如果点击了最后四个按钮中的任何一个,代码会定位到点击位置最近的项目,并将其分配给我们的新定义属性selected_object,代表当前选中的对象。 -
如果画布上没有项目,整个画布被设置为
selected_obj``ect属性。注意
画布方法有一个名为:
find_closest(x, y, halo=None, start=None)的方法。它返回画布上给定位置最近项目的标识符。这意味着,如果画布上只有一个项目,无论你点击得有多近或多远,它都会被选中。
如果另一方面,你只想选择一定距离内的对象,Canvas 小部件提供了一个名为
find_overlapping的替代实现。然而,你必须放置一个位于该位置中心的小矩形才能使用这个功能。
现在我们已经掌握了要操作的项目,我们可以继续进行我们想要对项目做的任何操作。
第 3 步 – 从画布中删除项目
从画布中删除项目的第一个方法是 delete_object,它简单地删除所选项目。因此,我们的 delete_object 方法定义如下(见 code 6.05.py):
def delete_object(self, x0, y0, x1, y1):
self.canvas.delete(self.selected_object)
此外,因为我们的早期代码需要为每个我们定义选项方法的函数,所以我们在这里定义了 delete_object_options 方法。然而,因为我们不想在顶部的选项栏中显示任何内容,所以我们简单地使用 pass 语句忽略它,如下所示:
def delete_object_options(self):
pass
第 4 步 – 画桶功能
接下来,我们编写 fill_object 方法,它在常规绘图程序中类似于画桶。
这同样很简单。你只需要在所选项目的背景上填充颜色。如果没有项目在画布上,它将简单地填充整个画布,如下所示:
def fill_object(self,x0,y0,x1,y1):
if self.selected_object == self.canvas:
self.canvas.config(bg=self.fill)
else:
self.canvas.itemconfig(self.selected_object, fill=self.fill)
而在这里,我们希望让用户选择画桶的填充颜色。因此,我们在 fill_object_options 方法中调用我们之前定义的方法 fill_options_combobox。
def fill_object_options(self):
self.fill_options_combobox()
第 5 步 – 将项目移动到彼此之上
现在我们来定义下一个按钮的方法。带有小手图标标记的按钮可以用来将项目置于其他项目之上。
注意
当你在画布上绘制多个项目时,项目会被放置在一个堆栈中。默认情况下,新项目会被添加到之前绘制在画布上的项目之上。然而,你可以使用:canvas.tag_raise(item) 来改变堆叠顺序。
如果有多个项目匹配,它们都会移动,同时保持它们的相对顺序。
然而,此方法不会更改画布内绘制的任何新窗口项目的堆叠顺序。
然后还有find_above和find_below方法,你可以使用它们在画布堆叠顺序中查找位于项目上方或下方的项目。
此外,还有一个find_all方法,它返回一个包含画布上所有项目标识符的元组。
因此,将项目移动到堆叠顶部的代码如下(见code 6.05.py):
def move_to_top(self,x0,y0,x1,y1):
self.canvas.tag_raise(self.selected_object)
def move_to_top_options(self):
pass # no items to display on the top bar
第 6 步 – 在画布上拖动项目
最后,让我们为画布上的项目添加拖放功能。在画布上拖动一个项目的能力要求在选择了要拖动的对象之后,我们重新计算鼠标移动的 x 和 y 坐标,并在小间隔内将对象移动到鼠标移动提供的新坐标。
在许多方面,这里的理念与我们用来定义画笔的概念相似。
策略是在每次小鼠标移动后使用另一个方法drag_item_update_xy调用我们的drag_items方法,该方法在小鼠标移动后重新计算 x 和 y 坐标,每次移动项目到新计算的坐标。
然后,我们有一个条件检查,如果从工具栏中选择其他按钮,则跳出此循环,如下所示(见code 6.05.py):
def drag_item(self,x0,y0,x1,y1):
if not self.all_toolbar_functions[ self.selected_toolbar_func_index] == 'drag_item':
self.canvas.bind("<Button1-Motion>", self.mouse_down_motion)
return # break out of loop
self.currentobject = self.canvas.move( self.selected_object, x1-x0, y1- y0)
self.canvas.bind("<B1-Motion>", self.drag_item_update_xy)
def drag_item_update_xy(self, event):
self.startx, self.starty = self.lastx, self.lasty
self.lastx, self.lasty = event.x, event.y
self.drag_item(self.startx, self.starty,self.lastx, self.lasty)
def drag_item_options(self):
pass # we want no options to be displayed at the top
注意
Canvas 小部件提供了一个方法:canvas.move(item, dx, dy)。
前面的方法通过水平和垂直偏移(dx和dy)移动任何匹配的项目。
目标完成 – 简短总结
这就结束了这一迭代。现在我们已经成功地为我们的绘图程序添加了四个新功能,即:delete_object、fill_object、move_to_top和drag_item。
在此过程中,我们看到了 Canvas 小部件提供的用于项目操作的一些方法。我们还看到了在 Canvas 小部件上处理现有项目时可能采用的战略。
分类情报
在这个程序中,我们广泛使用了项目标识符 ID 来定位画布上的特定项目。回想一下,项目标识符是创建对象时由 Canvas 方法返回的唯一整数 ID。
例如,当你创建画布上的椭圆项目时,创建对象后会返回一个整数 ID。这被称为项目标识符或项目句柄,如下所示:
my_item_identifier = self.canvas.create_oval(x, y, x2, y2)
现在,你可以使用句柄 my_item_identifier 对这个椭圆进行操作。
然而,这并不是唯一可以识别画布上项目的方法。此外,你可以给项目添加标签,然后使用这些标签来识别用于操作的对象。
处理项目标签
让我们现在看看在处理 Canvas 标签时涉及的一些常见操作。
添加标签
要给一个项目添加标签,你可以在创建对象时或之后使用itemconfig方法指定标签(这是一个字符串)作为其配置选项,或者使用addtag_withtag方法添加它们,如下所示:
rectid = canvas.create_rectangle(10, 10, 50, 50, tags="myshiny")
canvas.itemconfig(rectid, tags="shiv")
canvas.addtag_withtag("shiv", "takeonemore")
同一个标签可以应用于画布上的多个项目。
你可以通过传递标签作为字符串元组来一起给一个项目添加多个标签,如下所示:
canvas.itemconfig(rectid, tags=("tagA", "tagB"))
小贴士
使用标签来识别要操作的项目特别有用,当你需要同时操作多个项目,或者你想根据某些条件操作项目时。
获取标签
要获取与特定项目句柄相关联的所有标签,请使用以下gettags:
printcanvas.gettags(rectid)
这将返回与该项目句柄相关联的所有标签的元组,如下所示:
("myshiny", "shiv", "takeonemore", "tagA", "tagB")
获取具有特定标签的项目
要获取所有具有给定标签的项目句柄,请使用以下find_withtag:
print canvas.find_withtag("shiv")
这将返回所有项目的项目句柄作为元组。
内置标签
画布小部件提供了两个内置标签:
-
ALL 或 all:匹配画布上的所有项目
-
CURRENT 或 current:如果有,则返回鼠标指针下的项目
任务完成
这样你就有了自己的绘图程序!你可以轻松扩展它以添加更多功能。
下面是本项目所见内容的快速总结:
-
构建用于快速应用开发的自定义 GUI 框架
-
理解如何在我们的项目中使用继承
-
了解
tkColoChooser模块 -
学习在画布小部件上创建和操作项目
-
与 tk ComboBox 小部件一起工作
-
了解可用的
winfo方法 -
在画布小部件上处理鼠标事件
-
巩固我们在以前项目中学习到的内容
热身挑战
将以下功能添加到你的绘图程序中:
-
加速键对我们的菜单项不起作用,因为我们尚未将它们绑定到键事件。将菜单项的加速键绑定到它们相关的命令回调。
-
创建一个橡皮擦按钮并添加其相关功能。
-
我们尚未实现绘制一些其他基本形状,例如弧和多边形,尽管画布小部件提供了绘制它们的方法。将绘制弧和多边形的能力添加到绘图程序中。
-
在右侧创建一个新的工具栏。利用画布项的堆叠顺序,将每个项目作为工具栏中的单独一层显示。
-
通过使用 Python 的交互式帮助功能遍历你 IDE 中所有可用的画布小部件选项。尝试通过利用一个或多个选项来添加更多功能到程序中。
-
我们已经通过导航到文件 | 打开来包含了将图像添加到程序中的能力。添加一些菜单项来操作这些图像。使用某些图像库,添加图像处理功能,例如颜色调整、亮度、对比度、灰度以及其他由你选择的图像库提供的图像处理功能。
-
画布小部件通常用于绘制自定义小部件。使用画布小部件制作一个进度条小部件。将其附加到某个函数上并运行,以查看随着函数的进行,椭圆形应该被某种颜色填充。你可以使用画布小部件的填充选项来显示进度增加。
第七章:一些有趣的项目想法
在先前的项目中,我们已经探索了 Tkinter 的大部分重要功能。现在开发新项目就是扩展我们迄今为止所学的内容。在这个项目中,我们将构建几个部分功能的应用程序,您可以继续开发。
任务简报
在这个项目中,我们将为来自不同领域的几个应用程序开发“裸骨结构”。我们将构建的应用程序包括:
-
屏幕保护程序
-
贪吃蛇游戏
-
天气预报员
-
电话簿应用程序
-
使用 Tkinter 绘图
为什么它如此出色?
您会发现这个项目很有用,因为我们将进一步深入了解 Tkinter Canvas小部件的力量,并为我们的屏幕保护程序程序开发一些基本动画。
在开发贪吃蛇游戏时,我们将学习如何高效地使用队列实现来开发多线程 Python 应用程序。正如您将看到的,当处理多线程应用程序时,这是一个非常有用的工具。
天气预报员应用程序将向您介绍网络编程的基础。您将学习如何挖掘互联网上看似无限的资源。
电话簿应用程序将向您展示如何与数据库一起工作。这对于开发任何需要持久性的大型应用程序至关重要。
最后,我们将探讨 Tkinter 的基本绘图能力。我们还将探讨在 Tkinter 中嵌入 matplotlib 图表的方法。
您的热门目标
为此项目概述的关键目标包括开发和理解以下内容:
-
Tkinter 画布的基本动画
-
为多线程 Tkinter 应用程序实现队列
-
网络编程和利用互联网资源
-
与数据交换格式(如 JSON 和 XML)一起工作
-
数据库编程和数据库的基本 CRUD 操作
-
使用 Tkinter 绘图
创建屏幕保护程序
我们将首先为我们的桌面创建一个屏幕保护程序。屏幕保护程序将包含几个随机颜色和随机大小的球,以随机速度在屏幕上弹跳,如下面的截图所示:

启动推进器
执行以下步骤以创建屏幕保护程序:
-
让我们创建一个类来生成具有随机属性的球。相应地,我们定义一个新的类名为
RandomBall来实现这一点(请参阅代码包中的7.01 screensaver.pyPython 文件):from random import randint class RandomBall: def __init__(self, canvas, scrnwidth, scrnheight): self.canvas = canvas self.xpos = randint(10, int(scrnwidth)) self.ypos = randint(10, int(scrnheight)) self.xvelocity = randint(6,12) self.yvelocity = randint(6,12) self.scrnwidth = scrnwidth self.scrnheight = scrnheight self.radius = randint(40,70) r = lambda: randint(0,255) self.color = '#%02x%02x%02x' % (r(),r(),r())以下是代码的描述:
-
__init__方法接受三个参数,即 Canvas 小部件的实例、屏幕宽度和屏幕高度。然后它将球的初始x和y位置初始化为随机数,从0开始,到最大屏幕坐标。 -
它还初始化了球在x和y方向上的速度,球的半径和颜色以随机方式变化。
-
因为十六进制颜色编码系统为红色、绿色和蓝色中的每一个颜色使用两个十六进制数字,所以每种颜色有 16²(256)种可能性。因此,我们创建了一个生成 0-255 之间随机数的 lambda 函数,并使用这个函数生成三个随机数。我们使用格式%02x 将这个十进制数转换为它的两位等效十六进制表示,以生成球的随机颜色。
-
-
第二个方法使用画布的
create_oval方法创建实际的球(参考代码包中可用的7.01 screensaver.pyPython 文件):def create_ball(self): x1 = self.xpos-self.radius y1 = self.ypos-self.radius x2 = self.xpos+self.radius y2 = self.ypos+self.radius self.itm = canvas.create_oval (x1, y1, x2, y2, fill=self.color, outline=self.color) -
现在,让我们编写处理屏幕上球移动的方法。
该方法还会检查球是否已经到达屏幕的任何一边的尽头。如果球实际上已经到达屏幕的尽头,它将简单地通过给球的速率添加负号来改变方向。
该方法最终使用
canvas.move方法移动球(参考7.01 screensaver.py):def move_ball(self): self.xpos += self.xvelocity self.ypos += self.yvelocity #Check if the Direction of ball movement is to be changed if self.ypos>= self.scrnheight - self.radius: self.yvelocity = - self.yvelocity # change direction if self.ypos<= self.radius : self.yvelocity = abs(self.yvelocity) if self.xpos>= self.scrnwidth- self.radius or self.xpos<= self.radius: self.xvelocity = -self.xvelocity # change direction self.canvas.move(self.itm , self.xvelocity, self.yvelocity)这就是我们的
RandomBall类的全部内容。我们可以使用这个类来创建我们想要在屏幕保护程序中显示的任意数量的球对象。 -
现在,我们已经编写了生成球和移动它们的代码方法,让我们创建我们的屏幕保护程序。我们现在创建一个名为
ScreenSaver的类,它将显示实际的屏幕保护程序:class ScreenSaver: balls = [] def __init__(self, num_balls): self.root = Tk() w, h = self.root.winfo_screenwidth(),self.root.winfo_screenheight() self.root.overrideredirect(1) self.root.geometry("%dx%d+0+0" % (w, h)) self.root.attributes('-alpha', 0.3) self.root.bind('<Any-KeyPress>', quit) self.root.bind('<Any-Button>', quit) self.root.bind('<Motion>', quit) self.canvas = Canvas(self.root, width=w, height=h) self.canvas.pack() for i in range(num_balls): ball = RandomBall(self.canvas, scrnwidth=w, scrnheight=h) ball.create_ball() self.balls.append(ball) self.run_screen_saver() self.root.mainloop()代码的描述如下:
-
ScreenSaver类的__init__方法接受球的数量(num_balls)作为其参数。 -
然后,我们创建一个根窗口并使用
winfo方法计算屏幕的高度和宽度。 -
我们使用
root.overrideredirect(1)来从父窗口中移除封装的框架。 -
然后,我们指定父窗口的几何形状以填充整个屏幕。
-
我们使用
root.attributes('-alpha', 0.3)使父窗口透明。我们添加了0.3的透明度,使窗口半透明。 -
然后,我们将根绑定到在点击鼠标按钮、按下任何键盘按钮或鼠标移动时调用我们的
quit命令。这是为了确保我们的程序表现得像屏幕保护程序,在用户端有任何交互时退出。 -
然后,我们创建一个画布来覆盖整个屏幕,使用
Canvas(self.root, width=w, height=h)。 -
我们从
RandomBall类中创建了几个随机球对象,并将画布小部件实例、屏幕的宽度和高度作为其参数传递。 -
我们最终在
ScreenSaver类中调用run_screen_saver()方法来运行屏幕保护程序,这将在下文中讨论。
-
-
在这一步,我们将运行
ScreenSaver类:def run_screensaver(): for ball in balls: ball.move_ball() canvas.after(20, runScreenSaver)代码的描述如下:
-
run_screensaver()方法通过每隔 20 毫秒调用自身来简单地移动每个球。 -
我们还在
ScreenSaver类中定义了quit方法,用于从主循环退出并退出程序:def quit(event): root.destroy() -
要运行屏幕保护程序,我们从一个
ScreenSaver类实例中实例化一个对象,并将球的数量作为其参数传递:if __name__ == "__main__": ScreenSaver(18) ##18 is the number of balls
-
注意
我们在之前的代码中使用了两个顶层窗口方法 root.overrideredirect 和 root.attributes。
有关可以应用于顶层窗口的方法的完整列表,请参阅附录 B 中的 The Toplevel window methods 部分,快速参考表。
目标完成 - 简短总结
我们的屏保已经准备好了!
实际上,如果你在 Windows 平台上工作,并且当你学习如何从 Python 程序(在附录 A 中讨论,杂项提示)创建可执行程序时,你可以为这个屏保创建一个具有.exe扩展名的可执行文件。然后,你可以将其扩展名从.exe更改为.scr,右键单击,并选择安装将其添加到你的屏保列表中!
构建贪吃蛇游戏
现在我们来构建一个简单的贪吃蛇游戏。像往常一样,我们将使用画布小部件为我们的贪吃蛇程序提供平台。
我们将使用 canvas.create_line 来绘制我们的蛇,并使用 canvas.create_rectangle 来绘制蛇的食物。
准备起飞
本项目的首要目标之一是介绍 Python 中的队列实现,正如我们与线程模块结合使用的那样。
到目前为止,我们构建了单线程应用程序。然而,当应用程序中有多个线程时,线程处理可能会变得困难,并且这些线程需要在它们之间共享属性或资源。在这种情况下,你根本无法预测线程的执行顺序。操作系统每次都非常随机和迅速地执行它。
为了处理这种复杂性,线程模块提供了一些同步工具,例如锁、join、信号量、事件和条件变量。然而,在大多数情况下,使用队列更安全、更简单。简单来说,队列是一个线程安全的复合内存结构;队列有效地以顺序方式将资源访问渠道传递给多个线程,并且是推荐的设计模式,适用于大多数需要并发的场景。
队列模块提供了一种实现不同类型的队列的方法,例如FIFO(默认实现)、LIFO队列和优先级队列,并且此模块包含运行多线程程序所需的所有锁定语义的内置实现。
注意
关于队列模块的更多信息可以在以下链接中找到:
docs.Python.org/2/library/queue.html
这里是对队列模块基本用法的一个快速总结:
myqueue = Queue() #create empty queue
myqueue.put(data)# put items into queue
task = myqueue.get () #get the next item in the queue
myqueue.task_done() # called when a queued task has completed
myqueue.join() # called when all tasks in queue get completed
让我们看看使用队列实现多线程应用程序的一个简单示例(请参阅代码包中可用的 7.02 threading with queue.py):
import Queue
import threading
class Worker(threading.Thread):
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def run(self):
while True:
task = self.queue.get()
self.taskHandler(task)
def taskHandler(self, job):
print'doing task %s'%job
self.queue.task_done()
def main(tasks):
queue = Queue.Queue()
for task in tasks:
queue.put(task)
# create some threads and assign them queue
for i in range(6):
mythread = Worker(queue)
mythread.setDaemon(True)
mythread.start()
queue.join()
print'all tasks completed'
if __name__ == "__main__":
tasks = 'A B C D E F'.split()
main(tasks)
代码的描述如下:
-
我们首先创建一个
Worker类,它继承自 Python 的threading模块。__init__方法接受一个队列作为其参数。 -
然后,我们覆盖了
threading模块的run方法,使用queue.get()从队列中获取每个项目,然后将其传递给taskHandler方法,该方法实际上执行当前队列项中指定的任务。在我们的例子中,它没有做任何有用的事情,只是打印出任务的名称。 -
在
taskHandler方法完成特定线程的工作后,它通过使用queue.task_done()方法向队列发送一个信号,表明任务已经完成。 -
在我们的
Worker类外部,我们在main()方法中创建了一个空队列。这个队列使用queue.put(task)用一系列任务填充。 -
然后,我们创建了六个不同的线程,并将这个填充好的队列作为其参数传递。现在,由于任务由队列处理,所有线程都会自动确保任务按照线程遇到的顺序完成,而不会造成任何死锁或两个不同的线程试图处理同一个队列任务。
-
在创建每个线程的时候,我们也使用
mythread.setDaemon(True)方法创建了一个守护线程池。这样做会在所有线程完成执行后将控制权交回主程序。如果你注释掉这一行,程序仍然会运行,但在所有线程完成队列中的任务执行后,程序将无法退出。如果没有守护线程,你将不得不跟踪所有线程,并在你的程序完全退出之前告诉它们退出。 -
最后,
queue.join()方法确保程序流程等待在那里,直到队列变为空。
现在我们知道了如何使用队列有效地处理多线程应用程序,让我们构建我们的蛇游戏。在其最终形式中,游戏将类似于以下截图所示(请参考代码包中可用的7.03 game of snake.py Python 文件):

启动推进器
-
让我们开始编写我们的游戏,首先创建一个基本的
GUI类。class GUI(Tk): def __init__(self, queue): Tk.__init__(self) self.queue = queue self.is_game_over = False self.canvas = Canvas(self, width=495, height=305, bg='#FF75A0') self.canvas.pack() self.snake = self.canvas.create_line((0, 0), (0,0), fill='#FFCC4C', width=10) self.food = self.canvas.create_rectangle(0, 0, 0, 0, fill='#FFCC4C', outline='#FFCC4C') self.points_earned = self.canvas.create_text(455, 15, fill='white', text='Score: 0') self.queue_handler()代码的描述如下:
-
到现在为止,这段代码对你来说应该已经很熟悉了,因为我们之前已经多次创建了类似的
GUI类。 -
然而,我们的 GUI 类现在不是将根实例作为参数传递给其
__init__方法,而是从 Tk 类继承。Tk.__init__(self)这一行确保根窗口对所有这个类的方法都是可用的。这样我们就可以通过简单地引用self来避免在每一行都写上root属性。 -
然后,我们初始化画布、线条(蛇)、矩形(食物)和文本(用于显示分数)。
-
然后,我们调用函数
queueHandler()。这个尚未定义的方法将与之前队列示例中定义的main方法类似。这将是一个中心方法,它将处理队列中的所有任务。一旦我们向队列中添加了一些任务,我们就会回来定义这个方法。
-
-
现在,我们将创建一个
Food类,如下面的代码片段所示:class Food(): def __init__(self, queue): self.queue = queue self.generate_food() def generate_food(self): x = random.randrange(5, 480, 10) y = random.randrange(5, 295, 10) self.position = x, y self.exppos = x - 5, y - 5, x + 5, y + 5 self.queue.put({'food': self.exppos})以下是代码的描述:
-
因为我们希望从队列内部集中处理所有数据,所以我们把队列作为一个参数传递给
Food类的__init__方法。我们选择在主程序线程中运行这个操作,以展示一个在主线程中执行的字节码如何与其他线程的属性和方法进行通信。 -
__init__方法调用另一个名为generate_food()的方法,该方法负责在画布上随机位置生成蛇食物。 -
generate_food方法在画布上生成一个随机的(x, y)位置。然而,因为坐标重合的地方只是画布上的一个小点,几乎看不见。因此,我们生成一个扩展的坐标(self.exppos),范围从(x,y)坐标的五值以下到五值以上。使用这个范围,我们可以在画布上创建一个小的矩形,这将很容易看见,并代表我们的食物。
注意
然而,我们在这里并没有创建矩形。相反,我们使用
queue.put将食物(矩形)的坐标传递到我们的队列中。因为这个队列将被所有我们的类使用,我们将有一个名为queue_handler()的集中式工作者,它将处理这个队列,并在稍后从我们的 GUI 类生成矩形。这是队列实现背后的核心思想。 -
-
现在我们来创建
Snake类。我们已经将生成食物的任务传递给了中央队列。然而,这个任务没有涉及任何线程。我们也可以不使用线程来生成我们的蛇类。然而,因为我们正在讨论实现多线程应用程序的方法,所以让我们将我们的蛇类实现为从单独的线程中工作(参考7.03 game of snake.py):class Snake(threading.Thread): def __init__(self, gui, queue): threading.Thread.__init__(self) self.gui = gui self.queue = queue self.daemon = True self.points_earned = 0 self.snake_points = [(495, 55), (485, 55), (475, 55), (465, 55), (455, 55)] self.food = Food(queue) self.direction = 'Left' self.start() def run(self): while not self.gui.is_game_over: self.queue.put({'move':self.snake_points}) time.sleep(0.1) self.move()以下是代码的描述:
-
首先,我们创建一个名为
Snake的类,从单独的线程中运行。这个类接受 GUI 和队列作为它的输入参数。 -
我们初始化玩家获得的成绩为零,并使用属性
self.snake_points设置蛇的初始位置。 -
最后,我们启动线程并创建一个无限循环,以小间隔调用
move()方法。在每次循环运行中,该方法通过self.snake_points属性将一个包含键为'move'和值为蛇更新位置的字典填充到队列中。
-
-
在这一步,我们将使蛇移动。
上面的线程初始化调用
Snake类的move()方法,在画布上移动蛇。然而,在我们能够移动蛇之前,我们需要知道蛇应该移动的方向。这显然取决于用户按下的特定键(左/右/上/下键)。因此,我们需要将这些四个事件绑定到 Canvas 小部件上。我们将在稍后进行实际的绑定。然而,我们现在可以创建一个名为
key_pressed的方法,该方法将key_press事件本身作为其参数,并根据按下的键设置方向值。def key_pressed(self, e): self.direction = e.keysym现在我们有了方向,让我们编写
move方法:def move(self): new_snake_point = self.calculate_new_coordinates() if self.food.position == new_snake_point: self.points_earned += 1 self.queue.put({'points_earned':self.points_earned }) self.food.generate_food() else: self.snake_points.pop(0) self.check_game_over(new_snake_point) self.snake_points.append(new_snake_point) def calculate_new_coordinates(self): last_x, last_y = self.snake_points[-1] if self.direction == 'Up': new_snake_point = last_x, (last_y - 10) elif self.direction == 'Down': new_snake_point = last_x, (last_y + 10) elif self.direction == 'Left': new_snake_point = (last_x - 10), last_y elif self.direction == 'Right': new_snake_point = (last_x + 10), last_y return new_snake_point def check_game_over(self, snake_point): x,y = snake_point[0], snake_point[1] if not -5 < x < 505 or not -5 < y < 315 or snake_point in self.snake_points: self.queue.put({'game_over':True})代码的描述如下:
-
首先,
move方法根据键盘事件获取蛇的最新坐标。它使用一个名为calculate_new_coordinates的单独方法来获取最新坐标。 -
然后它会检查新坐标的位置是否与食物的位置一致。如果它们匹配,它会将玩家的得分增加一分,并调用
Food类的generate_food方法在新的位置生成新的食物。 -
如果当前点与食物坐标不一致,它将使用
self.snake_points.pop(0)从蛇坐标中删除最后一个项目。 -
然后,它会调用另一个名为
check_game_over的方法来检查蛇是否撞到墙壁或撞到自己。如果蛇确实撞到了,它会在队列中追加一个新的字典项,其值为'game_over':True。 -
最后,如果游戏没有结束,它会将蛇的新位置追加到列表
self.snake_points中。这会自动添加到队列中,因为我们已经在Snake类的run()方法中定义了self.queue.put({'move':self.snake_points}),以便在游戏没有结束时每 0.1 秒更新一次。
-
-
现在,让我们创建队列处理程序。
现在,我们有一个
Food类从主程序线程中向集中队列提供食物。我们还有一个Snake类从一个线程中向队列添加数据,以及一个GUI类从另一个线程中运行queue_handler方法。因此,队列是这三个线程之间交互的中心点。现在,是时候处理这些数据以更新画布上的内容了。因此,我们在
GUI类中相应地定义了queue_handler()方法来处理队列中的项目。def queue_handler(self): try: while True: task = self.queue.get(block=False) if task.has_key('game_over'): self.game_over() elif task.has_key('move'): points = [x for point in task['move'] for x in point] self.canvas.coords(self.snake, *points) elif task.has_key('food'): self.canvas.coords(self.food, *task['food']) elif task.has_key('points_earned'): self.canvas.itemconfigure(self.points_earned, text='Score:{}'.format(task['points_earned'])) self.queue.task_done() except Queue.Empty: if not self.is_game_over: self.canvas.after(100, self.queue_handler)代码的描述如下:
-
queue_handler方法进入一个无限循环,使用task = self.queue.get(block=False)在队列中查找任务。如果队列变为空,循环会使用canvas.after重新启动。 -
一旦从队列中获取了一个任务,该方法会检查其键。
-
如果键是
'game_over',它调用我们定义的另一个名为game_over()的方法。 -
如果任务的键是
'move',它使用canvas.coords将线移动到新的位置。 -
如果键是
'points_earned',它会在画布上更新得分。 -
当一个任务的执行完成后,它会使用
task_done()方法向线程发出信号。
注意
queue.get可以接受block=True(默认)和block=False作为其参数。当块设置为
False时,如果队列中有可用项,它会移除并返回一个项。如果队列为空,它将引发Queue.Empty。当块设置为True时,queue.get通过如果需要暂停调用线程,直到队列中有可用项。 -
-
在这一步,我们将编写处理游戏
game_over功能的方法。queue_handler方法在匹配队列键的情况下调用game_over方法:def game_over(self): self.is_game_over = True self.canvas.create_text(200, 150, fill='white', text='Game Over') quitbtn = Button(self, text='Quit', command =self.destroy) self.canvas.create_window(200, 180, anchor='nw', window=quitbtn)以下是代码的描述:
-
我们首先将
game_over属性设置为True。这有助于我们退出queue_handler的无限循环。然后,我们在画布上添加一个显示内容为 游戏结束 的文本。 -
我们还在画布内添加了一个 退出 按钮,该按钮附加了一个命令回调,用于退出根窗口。
小贴士
记住如何在画布小部件内附加其他小部件。
-
-
让我们运行游戏。游戏现在已准备就绪。要运行游戏,我们在所有其他类之外创建一个名为
main()的函数:def main(): queue = Queue.Queue() gui = GUI(queue) snake = Snake(gui, queue) gui.bind('<Key-Left>', snake.key_pressed) gui.bind('<Key-Right>', snake.key_pressed) gui.bind('<Key-Up>', snake.key_pressed) gui.bind('<Key-Down>', snake.key_pressed) gui.mainloop() if __name__ == '__main__': main()我们创建一个空队列,并将其作为参数传递给我们的三个类,以便它们可以将任务喂送到队列中。我们还绑定四个方向键到之前在
Snake类中定义的key_pressed方法。
目标完成 - 简要回顾
我们的游戏现在已功能正常。去尝试控制蛇,同时保持它的肚子饱饱的。
总结来说,我们创建了三个类,如 Food、Snake 和 GUI。这三个类将它们各自类相关的任务信息喂送到一个集中的队列中,这个队列作为参数传递给所有类。
然后,我们创建一个名为 queue_handler 的集中式方法,该方法通过轮询任务一次并以非阻塞方式完成任务来处理队列中的任务。
这个游戏可以不使用线程和队列来实现,但会慢一些,更长,也更复杂。通过使用队列来有效管理多个线程的数据,我们能够将程序代码控制在 150 行以下。
希望你现在能够实现队列来管理你在工作中设计的其他程序。
创建天气报告
现在我们来构建一个简单的天气报告应用。这个项目的目标是向您介绍网络编程的基础,这是与 Tkinter 结合使用的。
准备起飞
Python 对网络编程有很好的支持。在最低级别,Python 提供了一个套接字模块,它允许你使用简单易用的面向对象接口连接和与网络交互。
对于不了解网络编程的人来说,套接字是计算机进行任何类型网络通信的基本概念。这是程序员可以访问网络的最低级别。在套接字层之下是原始的 UDP 和 TCP 连接,这些连接由计算机的操作系统处理,程序员没有直接的访问点。例如,当你将 www.packtpub.com 输入到浏览器中时,你的计算机操作系统打开一个套接字并连接到 packtpub.com 来获取网页并显示给你。任何需要连接到网络的应用程序都会发生同样的事情。
让我们简要地看看套接字模块中可用的某些 API:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # create a #socket
socket.gethostbyname( host ) # resolving host IP from host name
s.connect((ip , port)) #Connect to remote server
s.sendall(message)
s.recv(message_size)
如果你查看此项目的代码包中的 7.04 socket demo.py Python 文件,你会发现它发送了一个非常晦涩的 GET 请求来从以下代码行获取 URL 的内容:
message = "GET / HTTP/1.1\r\n\r\n"
从服务器接收的数据也以数据包的形式发送,我们的任务是收集所有数据并在我们这一端组装它们。所有这些使得直接套接字编程变得繁琐。我们不希望编写代码来从网络中获取数据。
因此,我们将使用一个名为 urllib 的高级模块,该模块建立在套接字模块之上,但使用起来更简单。urllib 模块是 Python 标准库的一部分。使用此协议,从网页获取内容变为四行代码(请参阅 7.05 urllib demo.py 中的代码):
import urllib
data = urllib.urlopen('http://www.packtpub.com')
print data.read()
data.close()
这将打印整个 HTML 源代码或网页的响应。本质上,这是从网络中挖掘数据和信息的核心。
现在我们知道了如何从 URL 获取数据,让我们将其应用于构建一个小型天气报告应用程序。
此应用程序应从用户处获取位置作为输入,并获取相关的天气数据。

启动推进器
-
首先,我们将创建应用程序的 GUI。现在这对你来说应该很容易。我们创建一个名为
WeatherReporter的类,并在主循环外部调用它。请参阅7.06 weather reporter.py的代码:def main(): root=Tk() WeatherReporter(root) root.mainloop() if __name__ == '__main__': main()WeatherReporter类的 GUI 组件由两个方法组成:top_frame()和display_frame()。top_frame()方法创建一个输入小部件和一个显示 显示天气信息 的按钮。display_frame()方法创建一个画布,实际天气数据将在其中显示:class WeatherReporter: def __init__(self, root): self.root = root self.top_frame() self.display_frame() def top_frame(self): topfrm = Frame(self.root) topfrm.grid(row=1, sticky='w') Label(topfrm, text='Enter Location').grid(row=1, column=2, sticky='w') self.enteredlocation = StringVar() Entry(topfrm, textvariable=self.enteredlocation).grid(row=1, column=2, sticky='w') ttk.Button(topfrm, text='Show Weather Info', command=self.show_weather_button_clicked).grid(row=1, column=3, sticky='w') def display_frame(self): displayfrm = Frame(self.root) displayfrm.grid(row=2, sticky='ew', columnspan=5) self.canvas = Canvas(displayfrm, height='410',width='300', background='black', borderwidth=5) self.canvas.create_rectangle(5, 5, 305, 415,fill='#F6AF06') self.canvas.grid(row=2, sticky='w', columnspan=5) -
在第二步中,我们将从网站上获取天气数据。
从网站获取数据有两种方式。第一种方法涉及从网站获取 HTML 响应,然后解析收到的 HTML 响应以获取与我们相关的数据。这种数据提取称为 网站抓取。
网站抓取是一种相当原始的方法,仅在给定网站不提供结构化数据检索方式时使用。另一方面,一些网站愿意通过一组 API 共享数据,前提是你使用指定的 URL 结构查询数据。这显然比网站抓取更优雅,因为数据以可靠和“相互同意”的格式交换。
对于我们的天气报告应用,我们希望查询给定位置的某些天气频道,并相应地检索和显示数据在我们的画布上。幸运的是,有几个天气 API 允许我们这样做。
注意
在我们的示例中,我们将使用以下网站提供的天气数据:
OpenWeatherMap 服务提供免费的天气数据和预报 API。该网站从全球超过 40,000 个气象站收集天气数据,数据可以通过城市名称、地理坐标或其内部城市 ID 进行评估。
该网站提供两种数据格式的天气数据:
-
JSON(JavaScript 对象表示法)
-
XML
注意
XML 和 JSON 是两种广泛使用的可互换数据序列化格式,广泛用于不同平台和不同编程语言的应用程序之间的数据交换,从而提供了互操作性的好处。
JSON 比 XML 简单,因为它的语法更简单,并且它更直接地映射到现代编程语言中使用的数结构。JSON 更适合数据交换,但 XML 更适合文档交换。
网站的 API 文档告诉我们,查询如
api.openweathermap.org/data/2.5/weather?q=London,uk将返回以 JSON 格式表示的伦敦天气数据,如下所示:{"coord":{"lon":-0.12574,"lat":51.50853},"sys":{"country":"GB","sunrise":1377147503,"sunset":1377198481},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"base":"gdps stations","main":{"temp":294.2,"pressure":1020,"humidity":88,"temp_min":292.04,"temp_max":296.48},"wind":{"speed":1,"deg":0},"rain":{"1h":0.25},"clouds":{"all":40},"dt":1377178327,"id":2643743,"name":"London","cod":200}JSON 的语法很简单。任何 JSON 数据都是一个名称/值对,其中每个数据项通过逗号与其他数据项分隔。JSON 使用花括号
{}来包含对象,使用方括号[ ]来包含数组。因此,我们在我们的应用程序中定义了一个方法来获取 JSON 格式的天气数据(参考此项目代码包中的7.06 weather reporter.py):def get_weather_data(self): try: apiurl = 'http://api.openweathermap.org/data /2.5/weather?q=%s'%self.enteredlocation.get() data = urllib.urlopen(apiurl) jdata= data.read() returnjdata except IOError as e: tkMessageBox.showerror('Unable to connect', 'Unable toconnect %s'%e)此方法使用
urllib从网站检索响应。它以 JSON 格式返回响应。 -
-
现在,我们将开始处理 JSON 数据。通过 API 返回的天气数据是编码在 JSON 格式的。我们需要将此数据转换为 Python 数据类型。Python 提供了一个内置的
json模块,它简化了“编码-解码”JSON 数据的过程。因此,我们将json模块导入到当前命名空间中。然后,我们将使用此模块将检索到的 JSON 数据转换为 Python 字典格式(参考
7.06 weather reporter.py):def json_to_dict(self, jdata): mydecoder = json.JSONDecoder() decodedjdata = mydecoder.decode(jdata) flatteneddict = {} for key, value in decodedjdata.items(): if key == 'weather': forke,va in value[0].items(): flatteneddict[str(ke)] = str(va).upper() continue try: fork,v in value.items(): flatteneddict[str(k)] = str(v).upper() except: flatteneddict[str(key)] = str(value).upper() returnflatteneddict -
最后,我们将显示检索到的天气数据。现在,我们已经有了 API 提供的所有与天气相关的信息的字典,让我们给按钮添加一个命令回调:
def show_weather_button_clicked(self): if not self.enteredlocation.get(): return self.canvas.delete(ALL) self.canvas.create_rectangle( 5, 5,305,415,fill='#F6AF06') data = self.get_weather_data() data =self.json_to_dict(data) self.display_final(data)display_final方法简单地从字典中取出每个条目,并使用create_text在画布上显示它。我们不包含display_final的代码,因为它仅仅是在画布上显示数据,这个想法现在应该已经很明确了。API 还提供了与图标相关的数据。图标存储在名为weatherimages的文件夹中(参考代码包中提供的同名文件夹),并使用canvas.create_image显示适当的图标。
目标完成 - 简要回顾
我们的天气报道应用程序现在已功能齐全。本质上,该应用程序使用urllib模块查询我们数据提供商提供的天气 API。数据以 JSON 格式获取。然后 JSON 数据被解码成 Python 可读的格式(字典)。
转换后的数据随后使用create_text和create_image方法在画布上显示。
分类情报
当您从 Python 程序访问服务器时,在发送请求后保持小的时间间隔非常重要。
一个典型的 Python 程序每秒可以运行数百万条指令。然而,在另一端向您发送数据的服务器永远不会配备以这种速度工作的能力。
如果您在短时间内有意或无意地发送大量请求到服务器,可能会妨碍它为正常网络用户处理常规请求。这构成了对服务器的拒绝服务(DOS)攻击。如果您的程序没有发送有限数量的良好行为请求,您可能会被禁止,或者在更糟糕的情况下,因破坏服务器而起诉。
创建电话簿应用程序
现在我们来构建一个简单的电话簿应用程序,允许用户存储姓名和电话号码。用户应该能够使用此应用程序创建新记录、读取现有记录、更新现有记录以及从数据库中删除记录。这些活动共同构成了在数据库上所知的CRUD(创建、读取、更新和删除)操作。
本项目的核心学习目标是与 Tkinter 一起使用关系数据库存储和操作记录。
我们已经看到了一些使用序列化的基本对象持久化示例。关系数据库通过关系代数的规则扩展这种持久性,将数据存储到表中。
Python 为广泛的数据库引擎提供了数据库接口。此外,Python 提供了一个通用的接口标准,可以用来访问数据库引擎,但它不是作为 Python 模块原生的。
一些常用的数据库引擎包括 MySQL、SQLite、PostgreSQL、Oracle、Ingres、SAP DB、Informix、Sybase、Firebird、IBM DB2、Microsoft SQL Server、Microsoft Access 等等。
我们将使用 SQLite 存储电话簿应用程序的数据。
准备起飞
SQLite 是一个无服务器、零配置、自包含的 SQL 数据库引擎,适用于开发嵌入式应用程序。SQLite 的源代码属于公共领域,这使得它可以在所有商业和非商业项目中免费使用。
与许多其他 SQL 数据库不同,SQLite 不需要运行单独的服务器进程。相反,SQLite 将所有数据直接存储到计算机磁盘上的平面文件中。这些文件在不同平台上易于移植,使其成为小型和简单数据库实现需求的热门选择。
Python 2.7 内置了 sqlite3 支持的标准库。然而,我们需要下载 sqlite3 命令行工具,这样我们就可以使用命令行工具创建、修改和访问数据库。Windows、Linux 和 Mac OS X 的命令行壳可以从 sqlite.org/download.html 下载。
按照网站上的说明,将 SQLite 命令行工具安装到您选择的任何位置。
现在我们来实现我们的电话簿应用程序。该应用程序将类似于以下截图所示。该应用程序将演示数据库编程中的一些常见操作,如下所示:

启动推进器
-
为了创建数据库,我们打开操作系统的命令行工具。在 Windows 上,我们通常通过在运行控制台中键入
cmd来调用命令行。在命令行中,我们首先导航到需要创建新数据库文件的目录。为了创建数据库,我们只需使用此命令:
sqlite3 phonebook.db这将在我们执行命令的文件夹中创建一个名为
phonebook.db的数据库文件。它还会显示类似于以下的消息:SQLite version 3.7.17 2013-05-20 00:56:22 Enter ".help" for instructions Enter SQL statements terminated with a ";" sqlite>我们现在已经创建了一个名为
phonebook.db的数据库。然而,数据库文件目前是空的。它不包含任何表或数据。因此,如果我们运行以下命令,我们将得到没有结果:sqlite> .tables现在让我们通过键入以下内容退出命令行工具:
sqlite> .exit -
我们想在数据库中存储联系人,这就是为什么我们需要创建
contacts表。直观上,我们的数据库表应该存储一个人的姓名和电话号码。此外,为每个人或表中的每个条目保留一个唯一的识别号码是一个好习惯。这是因为可能有多个人的名字或联系号码相同。要在
phonebook.db数据库中创建一个表,我们再次打开命令行工具并导航到创建数据库的目录。我们再次通过键入以下内容进入 sqlite3 终端:sqlite3 phonebook.db这次没有创建新的数据库。相反,该命令现在打开现有的
phonebook.db数据库,因为它已经存在于磁盘上。接下来,我们创建一个名为
contacts的表,并从命令行向表中添加三个列:sqlite> CREATE TABLE contacts ( contactid INTEGER PRIMARY KEY AUTOINCREMENT, name STRINGNOT NULL, contactnumber INTEGER NOT NULL );您可以通过输入以下命令来验证是否创建了
contacts表:sqlite>.table这将打印出当前打开的数据库中所有表的名字。您将得到以下输出:
sqlite>.table contacts -
让我们先创建一个基本的 GUI,这样我们就可以添加、查看、删除和修改记录。我们创建一个名为
PhoneBook的类,并在其__init__方法中创建所有 GUI 小部件(参考7.07 phonebook.py):class PhoneBook: def __init__(self, master): # all widgets created here我们在这里不重写代码,因为我们已经在所有之前的项目中创建了类似的部件。
-
让我们开始创建我们创建的数据库文件中的记录。每次用户在提供的输入小部件中输入新的姓名和电话号码,并点击 添加记录 按钮时,都会创建一个新的记录。
def create_record(self): name = self.namefield.get() num = self.numfield.get() if name == "": self.msg["text"] = "Please Enter name" return if num == "": self.msg["text"] = "Please Enter Number" return conn = sqlite3.connect('phonebook.db') c = conn.cursor() c.execute("INSERT INTO contacts VALUES(NULL,?, ?)", (name,num)) conn.commit() c.close() self.namefield.delete(0, END) self.numfield.delete(0, END) self.msg["text"] = "Phone Record of %s Added" %name代码的描述如下:
-
如上所述定义的
create_record方法被附加为 添加记录 按钮的命令回调。 -
当调用
create_record方法时,它会检索在 姓名 和 联系号码 输入字段中输入的姓名和号码值。 -
如果姓名或号码字段为空,它将打印一条错误消息并退出。
-
如果姓名和号码字段有效,该方法将连接到我们之前创建的
phonebook.db数据库。 -
下一个行,
c = conn.cursor(),创建了一个游标对象。游标是一个按照 SQL 标准要求的控制结构,它使我们能够遍历数据库中的记录。 -
下一个行,
c.execute(query)是实际将姓名和电话号码插入数据库的行。注意,它包括三个插入值:第一个是自动递增的联系人 ID 对应的 NULL 值,这是通过我们在contacts表中创建的来添加的。 -
conn.commit()这一行实际上将更改提交到数据库,而c.close()行则关闭了与数据库的连接。
-
-
在执行上述步骤之后,我们将查看存储在数据库中的记录。该方法负责从数据库中检索所有记录并在树形小部件中显示它们。
def view_records(self): x = self.tree.get_children() for item in x: self.tree.delete(item) conn = sqlite3.connect('phonebook.db') c = conn.cursor() list = c.execute("SELECT * FROM contacts ORDER BY namedesc") for row in list: self.tree.insert("",0,text=row[1],values=row[2]) c.close()代码的描述如下:
-
view_records方法首先删除在树形小部件中显示的所有现有项目 -
然后,它建立数据库连接并查询数据库以按姓名降序获取所有数据
-
最后,它遍历检索到的记录以更新树形小部件的内容
-
-
现在,在电话簿应用程序中,我们将删除一些记录。
delete_record方法简单地负责根据给定的姓名标准从数据库中删除一行:def delete_record(self): self.msg["text"] = "" conn = sqlite3.connect('phonebook.db') c = conn.cursor() name = self.tree.item(self.tree.selection())['text'] query = "DELETE FROM contacts WHERE name = '%s';" %name c.execute(query) conn.commit() c.close() self.msg["text"] = "Phone Record for %s Deleted" %name小贴士
尽管我们根据姓名创建了此删除查询,但如果两个人或更多人具有相同的姓名,这种方法会存在删除多个条目的风险。更好的方法是根据主键或联系人 ID 来删除条目,因为每个条目在表中的联系人 ID 都是唯一的。
-
电话簿应用程序中的最终操作是修改记录。当用户选择一个特定的记录并点击修改所选按钮时,它会打开一个新顶层窗口,就像这里显示的那样:
![Engage Thrusters]()
此窗口是通过
open_modify_window方法创建的,该方法定义在7.07 phonebook.pyPython 文件中。我们不会复制此方法的代码,因为现在你应该已经能够舒适地创建这样的窗口。当用户指定一个新的数字并点击更新记录按钮时,它会调用
update_record方法,该方法定义如下:def update_record(self, newphone,oldphone, name): conn = sqlite3.connect('phonebook.db') c = conn.cursor() c.execute("UPDATE contacts SET contactnumber=? WHEREcontactnumber=? AND name=?", (newphone, oldphone, name)) conn.commit() c.close() self.tl.destroy() self.msg["text"] = "Phone Number of %s modified" %name
目标完成 - 简短总结
我们已经完成了基本电话簿应用程序的编码。
更重要的是,我们已经看到了如何与数据库一起工作。我们的电话簿应用程序展示了如何在数据库上执行基本的创建、读取、更新和删除(CRUD)操作。
我们已经看到了如何创建数据库,向数据库添加表,以及查询数据库以添加、修改、删除和查看数据库中的项目。
此外,由于基本数据库操作相似,你现在可以考虑使用其他数据库系统,例如 MySQL、PostgreSQL、Oracle、Ingres、SAP DB、Informix、Sybase、Firebird、IBM DB2、Microsoft SQL Server 和 Microsoft Access。
使用 Tkinter 进行绘图
让我们通过查看 Tkinter canvas 小部件的绘图能力来总结这个项目。
Engage Thrusters
在这个菜谱中,我们将看到我们如何可以绘制:
-
饼图
-
散点图
-
条形图
-
嵌入 matplotlib 图形
让我们先看看饼图:

-
您可以使用 Canvas 小部件的
create_arc方法轻松地在 Tkinter 中创建饼图。示例饼图代码在7.08 pie chart.py中提供:import Tkinter root = Tkinter.Tk() def prop(n): return 360.0 * n / 1000 Tkinter.Label(root, text='Pie Chart').pack() c = Tkinter.Canvas(width=154, height=154) c.pack() c.create_arc((2,2,152,152), fill="#FAF402", outline="#FAF402", start=prop(0), extent = prop(200)) c.create_arc((2,2,152,152), fill="#00AC36", outline="#00AC36", start=prop(200), extent = prop(400)) c.create_arc((2,2,152,152), fill="#7A0871", outline="#7A0871", start=prop(600), extent = prop(50)) c.create_arc((2,2,152,152), fill="#E00022", outline="#E00022", start=prop(650), extent = prop(200)) c.create_arc((2,2,152,152), fill="#294994", outline="#294994", start=prop(850), extent = prop(150)) root.mainloop()代码的描述如下:
-
饼图的每个部分都是通过改变以下两个
create_arc选项来绘制的:start:此选项指定起始角度。默认为0.0。extent:此选项指定arc相对于起始角度的大小。默认为90.0。
-
-
接下来,我们将绘制一个示例散点图:
![Engage Thrusters]()
类似地,我们可以使用
create_line来绘制x和y轴,并使用create_oval来绘制散点图,如前述截图所示。示例散点图代码在7.09 scatter plot.pyPython 文件中提供:import Tkinter import random root = Tkinter.Tk() c = Tkinter.Canvas(root, width=350, height=280, bg='white') c.grid() #create x-axis c.create_line(50, 250, 300, 250, width=3) for i in range(12): x = 50 + (i * 20) c.create_text(x, 255, anchor='n', text='%d'% (20*i)) # create y-axis c.create_line(50, 250, 50, 20, width=3) for i in range(12): y = 250 - (i * 20) c.create_text(45, y, anchor='e', text='%d'% (20*i)) #create scatter plots from random x-y values for i in range(35): x,y = random.randint(100,210), random.randint(50,250) c.create_oval(x-3, y-3, x+3, y+3, width=1, fill='red') root.mainloop() -
现在,让我们绘制一个示例条形图:
![Engage Thrusters]()
可以使用 Canvas 小部件的
create_rectangle方法轻松生成条形图。示例条形图代码在7.10 bar graph.py中提供:import Tkinter import random root = Tkinter.Tk() cwidth = 250 cheight = 220 barWidth = 20 canv = Tkinter.Canvas(root, width=cwidth, height=cheight, bg= 'white') canv.pack() plotdata= [random.randint(0,200) for r in xrange(12)] for x, y in enumerate(plotdata): x1 = x + x * barWidth y1 = cheight - y x2 = x + x * barWidth + barWidth y2 = cheight canv.create_rectangle(x1, y1, x2, y2, fill="blue") canv.create_text(x1+3, y1, text=str(y), anchor='sw') root.mainloop() -
最后,我们将探讨如何在 Tkinter Toplevel 窗口中嵌入 matplotlib 图形。
使用 Tkinter Canvas 绘制图形对于简单情况可能工作得很好。然而,当涉及到绘制更复杂和交互式图形时,Tkinter 可能不是最好的库。
事实上,当涉及到使用 Python 生成专业质量的图形时,matplotlib 与 NumPy 模块结合使用是首选选择。
![Engage Thrusters]()
尽管对 matplotlib 的详细讨论超出了本书的范围,但我们将简要看看如何在 Tkinter 画布上嵌入由 matplotlib 生成的图形。
小贴士
如果你对探索 Python 的高级绘图感兴趣,你可以通过在
matplotlib.org/users/installing.html可用的安装说明的帮助下安装 matplotlib 和 NumPy(matplotlib 的依赖项)。import Tkinter as Tk from numpy import arange, sin, pi from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg from matplotlib.figure import Figure root = Tk.Tk() #creating the graph f = Figure(figsize=(5,4), dpi=100) a = f.add_subplot(111) t = arange(-1.0,1.0,0.001) s = t*sin(1/t) a.plot(t,s) # embedding matplotlib figure 'f' on a tk.DrawingArea canvas = FigureCanvasTkAgg(f, master=root) canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) #creating toolbar toolbar = NavigationToolbar2TkAgg( canvas, root ) toolbar.update() root.mainloop()
目标完成 – 简要回顾
这完成了我们对 Tkinter 绘图能力的简要讨论。
在这次迭代中,我们看到了如何使用 Tkinter Canvas 绘制基本的图表,如饼图、散点图和条形图。
我们还看到了如何在 Tkinter 绘图区域嵌入更复杂的 matplotlib 图形。
任务完成
这标志着本项目的结束。在这个项目中,我们深入研究了 Tkinter Canvas 小部件可以完成的一些众多事情。
我们还学习了如何使用 Queue 实现来编写多线程应用程序。
天气预报员应用程序向我们介绍了网络编程的基础知识以及如何利用互联网来满足我们的数据需求。
电话簿应用程序展示了我们如何与数据库协同工作。
最后,我们探讨了 Tkinter 的基本绘图能力,以及如何在 Tkinter 中嵌入 matplotlib 图形。
热门挑战
-
屏保挑战:我们在屏保程序中使用了 Canvas 小部件的
create_oval方法来创建多个球。尝试通过用其他 Canvas 支持的形状(如线条、矩形和弧线)替换椭圆来实验。事实上,因为你可以使用 Canvas 上的
create_image方法,那么创建一个充满不同种类鱼、蜗牛、水生动物和植物的鱼缸怎么样?你甚至可以添加正在海洋生物中穿梭的跳伞者! -
蛇游戏挑战:通过在画布上引入迷宫来实现蛇游戏的多个级别。
-
网络编程挑战:实现任何其他利用互联网上可用数据为最终用户提供价值的程序。
-
数据库挑战:回顾你的媒体播放器程序,并实现一个数据库来存储播放列表,并在程序运行时自动填充媒体播放器。
-
绘图挑战:探索 matplotlib 的高级绘图功能。
附录 A. 杂项技巧
我们现在进入了本书的最后一部分。让我们通过讨论在许多 GUI 应用程序中形成一个共同主题但未出现在我们的应用程序中的概念来结束。
任务简报
这里涵盖的主题包括:
-
跟踪 Tkinter 变量
-
小部件遍历
-
验证用户输入
-
格式化小部件数据
-
更多关于字体
-
与 Unicode 字符一起工作
-
Tkinter 类层次结构
-
定制的混入
-
代码清理和程序优化的技巧
-
分发 Tkinter 应用程序
-
Tkinter 的局限性
-
Tkinter 的替代方案
-
获取交互式帮助
-
Python 3.x 中的 Tkinter
跟踪 Tkinter 变量
当你指定一个 Tkinter 变量作为小部件的 textvariable (textvariable = myvar)时,小部件会自动更新,每当变量的值发生变化时。然而,有时除了更新小部件外,你还需要在读取或写入(或修改)变量时进行一些额外的处理。
Tkinter 提供了一种方法来附加一个回调方法,该方法会在每次访问变量的值时被触发。因此,回调充当 变量观察者。回调方法名为 trace_variable(self, mode, callback),或简单地 trace(self, mode, callback)。
模式参数可以取 'r', 'w', 'u' 中的任何一个值,分别代表读取、写入或未定义。根据模式指定,如果变量被读取或写入,则触发回调方法。
回调方法默认接收三个参数。参数的顺序是:
-
Tkinter 变量的名称
-
如果 Tkinter 变量是一个数组,则变量的索引,否则为空字符串
-
访问模式 (
'w', 'r', 或 'u')
注意,触发的回调函数也可能修改变量的值。然而,这种修改并不会触发任何额外的回调。
让我们看看 Tkinter 中变量跟踪的一个小例子,其中将 Tkinter 变量写入输入小部件会触发一个回调函数(请参阅代码包中可用的 8.01 trace variable.py Python 文件):
from Tkinter import *
root = Tk()
myvar = StringVar()
def trace_when_myvar_written(var,indx,mode):
print"Traced variable %s"%myvar.get()
myvar.trace_variable("w", trace_when_myvar_written)
Label(root, textvariable=myvar).pack(padx=5, pady=5)
Entry(root, textvariable=myvar).pack(padx=5, pady=5)
root.mainloop()
下列代码的描述如下:
-
此代码在 Tkinter 变量
myvar上创建了一个跟踪变量,模式为写入("w") -
跟踪变量附加到名为
trace_when_myvar_written的回调方法(这意味着每次myvar的值发生变化时,回调方法都会被触发)
现在,每次你向输入小部件写入内容时,它都会修改 myvar 的值。因为我们已经对 myvar 设置了跟踪,所以它触发了回调方法,在我们的例子中,这个方法只是简单地在新值中打印到控制台。
代码创建了一个类似于这里所示的 GUI 窗口:

它还在 IDLE 中产生控制台输出,一旦你在 GUI 窗口中开始输入,就会显示如下:
Traced variable T
Traced variable Tr
Traced variable Tra
Traced variable Trac
Traced variable Traci
Traced variable Tracin
Traced variable Tracing
注意
变量的跟踪是活跃的,直到它被显式删除。你可以使用以下方式删除跟踪:
trace_vdelete(self, mode, callbacktobedeleted)
The trace method returns the name of the callback method. This can be used to get the name of the callback method that is to be deleted.
小部件遍历
当 GUI 有多个小部件时,给定的小部件可以通过在 GUI 上显式鼠标单击来获得焦点。或者,可以通过在键盘上按Tab键,按照程序中小部件创建的顺序将焦点转移到另一个给定的小部件。
因此,按照我们希望用户遍历的顺序创建小部件至关重要,否则用户在使用键盘在各个小部件之间导航时将遇到困难。
不同的小部件被设计为对不同键盘按键有不同的行为。因此,让我们花些时间尝试理解使用键盘遍历小部件的规则。
让我们看看8.02 widget traversal.py Python 文件的代码,以了解不同小部件的键盘遍历行为。一旦运行提到的.py文件,它显示的窗口类似于以下内容:

代码很简单。它添加了一个输入小部件、几个按钮、几个单选按钮、一个文本小部件和一个缩放小部件。然而,它还演示了这些小部件的一些最重要的键盘遍历行为。
这里有一些重要的要点需要注意(参考8.02 widget traversal.py):
-
Tab键可以用来向前遍历,而Shift + Tab可以用来向后遍历。
-
文本小部件不能使用Tab键遍历。这是因为文本小部件可以包含制表符作为其内容。相反,可以使用Ctrl + Tab遍历文本小部件。
-
可以使用空格键按下小部件上的按钮。同样,复选框和单选按钮也可以使用空格键切换。
-
你可以使用上下箭头在列表框小部件中的条目上下移动。
-
缩放小部件对左右键或上下键都做出响应。同样,滚动条小部件根据其方向对左右/上下键做出响应。
-
大多数小部件(除了框架、标签和菜单)在将焦点设置在其上时默认获得轮廓。这个轮廓通常显示为围绕小部件的细黑边框。你甚至可以将框架和标签小部件设置为显示这个轮廓,通过为这些小部件指定非零的
Integer值作为highlightthickness选项。 -
我们在代码中使用
highlightcolor= 'red'更改轮廓的颜色。 -
框架、标签和菜单不包括在标签导航路径中。然而,可以通过使用
takefocus = 1选项将它们包含在导航路径中。你可以通过设置takefocus= 0选项显式排除小部件从标签导航路径。 -
Tab键按照小部件创建的顺序遍历小部件。它首先访问父小部件(除非使用
takefocus = 0排除),然后是所有其子小部件。 -
你可以使用
widget.focus_force()强制输入焦点到小部件。
验证用户输入
现在我们来讨论输入数据验证。
我们在书中开发的大部分应用程序都是基于点击的(鼓机、棋类游戏、绘图应用程序),在这些应用中不需要验证用户输入。
然而,在像我们的电话簿应用程序这样的程序中,数据验证是必须的,因为用户输入一些数据,我们将它们存储在数据库中。
在此类应用程序中忽略用户输入验证可能是危险的,因为输入数据可能被误用于 SQL 注入。一般来说,任何用户可以输入文本数据的应用程序都是验证用户输入的良好候选者。事实上,几乎可以认为不信任用户输入是一条准则。
错误的用户输入可能是故意的或偶然的。在任何情况下,如果您未能验证或清理数据,您可能会在程序中引起意外的错误。在最坏的情况下,用户输入可能被用来注入有害代码,这些代码可能足以使程序崩溃或删除整个数据库。
列表框、组合框和单选按钮等控件允许有限的输入选项,因此通常不能被误用来输入错误的数据。另一方面,输入框控件、微调框控件和文本控件允许用户输入的可能性很大,因此需要验证其正确性。
要在控件上启用验证,您需要向控件指定一个额外的选项,形式为 validate = 'validationmode'。
例如,如果您想在一个输入框控件上启用验证,您首先指定验证选项如下:
Entry( root, validate="all", validatecommand=vcmd)
验证可以在以下 验证模式 之一中发生:
| 验证模式 | 说明 |
|---|---|
none |
这是默认模式。如果将 validate 设置为 "none",则不会发生验证 |
focus |
当 validate 设置为 "focus" 时,validate 命令被调用两次;一次当控件获得 focus 时,一次当 focus 失去时 |
focusin |
当控件获得 focus 时调用 validate 命令 |
focusout |
当控件失去 focus 时调用 validate 命令 |
key |
当输入被 edited 时调用 validate 命令 |
all |
在所有上述情况下调用 validate 命令 |
8.03 validation mode demo.py 文件的代码通过将它们附加到单个验证方法上来演示所有这些验证模式。注意不同输入框控件对不同事件的响应方式。一些输入框控件在焦点事件上调用验证方法,而其他输入框控件在将按键输入到控件中时调用验证方法,还有一些输入框控件使用焦点和按键事件的组合。
尽管我们确实设置了验证模式以触发validate方法,但我们仍需要某种数据来与我们的规则进行验证。这是通过百分比替换传递给validate方法的。例如,我们通过在validate命令上执行百分比替换,将模式作为参数传递给我们的validate方法,如下所示:
vcmd = (self.root.register(self.validate), '%V')
我们随后将v的值作为参数传递给我们的验证方法:
def validate(self, v)
除了%V之外,Tkinter 还识别以下百分比替换:
| 百分比替换 | 说明 |
|---|---|
%d |
在小部件上发生的行为类型——1为插入,0为删除,-1为焦点、强制或 textvariable 验证。 |
%i |
如果有,则插入或删除的char字符串的索引,否则为-1。 |
%P |
如果允许编辑,则输入的值。如果你正在配置 Entry 小部件以具有新的 textvariable,这将是该 textvariable 的值。 |
%s |
编辑前的当前输入值。 |
%S |
如果有,则插入或删除的文本字符串,否则为{}。 |
%v |
当前设置的验证类型。 |
%V |
触发回调方法的验证类型(键、focusin、focusout 和强制)。 |
%W |
Entry 小部件的名称。 |
这些验证为我们提供了我们可以用来验证输入的必要数据。
现在我们将这些数据全部传递,并通过一个虚拟的validate方法打印出来,以便查看我们可以期望得到哪些数据来进行我们的验证(参考8.04 percent substitutions demo.py的代码):
提示
特别注意由%P和%s返回的数据,因为它们与用户在 Entry 小部件中实际输入的数据有关。
在大多数情况下,你将检查以下两种数据之一以符合你的验证规则。
现在我们已经了解了数据验证的规则背景,让我们看看两个实际示例,这些示例展示了输入验证。
关键验证
假设我们有一个要求用户输入姓名的表单。我们希望用户只输入字母或空格字符。因此,不允许数字或特殊字符,如下面的小部件截图所示:

这显然是一个'key'验证模式的案例,因为我们希望在每次按键后检查输入是否有效。我们需要检查的百分比替换是%S,因为它会返回在 Entry 小部件中插入或删除的文本字符串。因此,验证 Entry 小部件的代码如下(参考8.05 key validation.py):
import Tkinter as tk
class KeyValidationDemo():
def __init__(self):
root = tk.Tk()
tk.Label(root, text='Enter your name').pack()
vcmd = (root.register(self.validate_data), '%S')
invcmd = (root.register(self.invalid_name), '%S')
tk.Entry(root, validate="key", validatecommand=vcmd,invalidcommand=invcmd).pack(pady=5, padx=5)
self.errmsg = tk.Label(root, text= '', fg='red')
self.errmsg.pack()
root.mainloop()
def validate_data(self, S):
self.errmsg.config(text='')
return (S.isalpha() or S =='') # always return True or False
def invalid_name(self, S):
self.errmsg.config(text='Invalid characters \n name canonly have alphabets'%S)
app= KeyValidationDemo()
以下是对前面代码的描述:
-
我们首先注册了两个选项
validatecommand(vcmd)和invalidcommand(invcmd)。 -
在我们的例子中,
validatecommand被注册为调用validate_data方法,而invalidcommand选项被注册为调用另一个名为invalid_name的方法。 -
validatecommand选项指定了一个用于验证输入的方法。验证方法必须返回一个布尔值,其中True表示输入的数据有效,而False返回值表示数据无效。 -
如果验证方法返回
False(无效数据),则不会将数据添加到 Entry 小部件中,并且会评估为invalidcommand注册的脚本。在我们的例子中,一个False的验证会调用invalid_name方法。invalidcommand方法通常负责显示错误消息或将焦点设置回 Entry 小部件。
注意
让我们看看代码 register(self, func, subst=None, needcleanup=1)。
register 方法返回一个新创建的 Tcl 函数。如果调用此函数,则执行 Python 函数 func。如果提供了可选函数 subst,则它会在 func 执行之前执行。
Focus Out Validation
之前的示例演示了 'key' 模式下的验证。这意味着验证方法在每次按键后都会被调用,以检查输入是否有效。
然而,在某些情况下,你可能希望检查小部件中输入的整个字符串,而不是检查单个按键输入。
例如,如果 Entry 小部件接受有效的电子邮件地址,我们希望在用户输入整个电子邮件地址后检查其有效性,而不是在每次按键输入后检查。这可以视为 'focusout' 模式下的验证。

查看 8.06 focus out validation.py 的代码,以了解在 'focusout' 模式下的电子邮件验证示例:
import Tkinter as tk
import re
class FocusOutValidationDemo():
def __init__(self):
self.master = tk.Tk()
self.errormsg = tk.Label(text='', fg='red')
self.errormsg.pack()
tk.Label(text='Enter Email Address').pack()
vcmd = (self.master.register(self.validate_email), '%P')
invcmd = (self.master.register(self.invalid_email), '%P')
self.emailentry = tk.Entry(self.master, validate ="focusout", validatecommand=vcmd, invalidcommand=invcmd)
self.emailentry.pack()
tk.Button(self.master, text="Login").pack()
tk.mainloop()
def validate_email(self, P):
self.errormsg.config(text='')
x = re.match(r"[^@]+@[^@]+\.[^@]+", P)
return (x != None)# True(valid email)/False(invalid email)
def invalid_email(self, P):
self.errormsg.config(text='Invalid Email Address')
self.emailentry.focus_set()
app = FocusOutValidationDemo()
之前代码的描述如下:
代码与之前的验证示例有很多相似之处。然而,请注意以下差异:
-
验证模式设置为
'focusout',与之前示例中的'key'模式不同。这意味着只有在 Entry 小部件失去焦点时才会进行验证。 -
该程序使用
%P百分比替换提供的数据,而不是之前示例中使用的%S。这是可以理解的,因为%P提供了 Entry 小部件中输入的值,而%S提供了最后按键的值。 -
该程序使用正则表达式检查输入的值是否对应有效的电子邮件格式。验证通常依赖于正则表达式和大量的解释来涵盖这个主题,但这超出了本项目和本书的范围。有关正则表达式模块的更多信息,请访问以下链接:
这就结束了我们对 Tkinter 中输入验证的讨论。希望你现在能够实现满足你自定义需求的输入验证。
格式化小部件数据
日期、时间、电话号码、信用卡号码、网站 URL、IP 地址等多种输入数据都有相关的显示格式。例如,日期最好以 MM/DD/YYYY 格式表示。
幸运的是,当用户在组件中输入数据时,格式化所需格式的数据很容易(参考8.07 formatting entry widget to display date.py)。提到的 Python 文件会自动格式化用户输入,在需要的位置插入正斜杠,以便以 MM/DD/YYYY 格式显示用户输入的日期。

from Tkinter import *
class FormatEntryWidgetDemo:
def __init__(self, root):
Label(root, text='Date(MM/DD/YYYY)').pack()
self.entereddata = StringVar()
self.dateentrywidget =Entry(textvariable=self.entereddata)
self.dateentrywidget.pack(padx=5, pady=5)
self.dateentrywidget.focus_set()
self.slashpositions = [2, 5]
root.bind('<Key>', self.format_date_entry_widget)
def format_date_entry_widget(self, event):
entrylist = [c for c in self.entereddata.get() if c != '/']
for pos in self.slashpositions:
if len(entrylist) > pos:
entrylist.insert(pos, '/')
self.entereddata.set(''.join(entrylist))
# Controlling cursor
cursorpos = self.dateentrywidget.index(INSERT)
for pos in self.slashpositions:
if cursorpos == (pos + 1): # if cursor is on slash
cursorpos += 1
if event.keysym not in ['BackSpace', 'Right', 'Left','Up', 'Down']:
self.dateentrywidget.icursor(cursorpos)
root = Tk()
FormatEntryWidgetDemo(root)
root.mainloop()
上述代码的描述如下:
-
Entry 组件绑定到按键事件,每次新的按键都会调用相关的回调
format_date_entry_widget方法。 -
首先,
format_date_entry_widget方法将输入文本分解为名为entrylist的等效列表,同时忽略用户输入的任何斜杠'/'符号。 -
然后,它遍历
self.slashpositions列表,并在entrylist参数的所有必需位置插入斜杠符号。最终结果是,在所有正确位置都插入了斜杠的列表。 -
下一行使用
join()将此列表转换为等效的字符串,然后将 Entry 组件的值设置为该字符串。这确保了 Entry 组件的文本格式化为上述日期格式。 -
剩余的代码仅控制光标,确保光标在遇到斜杠符号时前进一个位置。它还确保正确处理按键,如
'BackSpace'、'Right'、'Left'、'Up'和'Down'。
注意,此方法不验证日期值,用户可以添加任何无效日期。这里定义的方法将简单地通过在第三位和第六位添加正斜杠来格式化它。将日期验证添加到此示例作为你的练习任务。
这就结束了我们对在组件内格式化数据的简要讨论。希望你现在能够创建用于广泛输入数据的格式化组件,这些数据可以在给定的格式中更好地显示。
更多关于字体
许多 Tkinter 组件允许你在组件创建时或稍后使用configure()选项指定自定义字体规范。在大多数情况下,默认字体提供了标准的视觉和感觉。然而,如果你想更改字体规范,Tkinter 允许你这样做。但有一个注意事项。
当你指定自己的字体时,你需要确保它在程序打算部署的所有平台上看起来都很好。这是因为字体可能在某个特定平台上看起来很好,匹配得很好,但在另一个平台上可能看起来很糟糕。除非你了解自己在做什么,否则始终建议坚持使用 Tkinter 的默认字体。
大多数平台都有自己的标准字体集,这些字体由平台的本地小部件使用。因此,而不是试图在特定平台上重新发明轮子,或者为特定平台提供什么字体可用,Tkinter 将这些标准平台特定字体分配给其小部件,从而在所有平台上提供本地外观和感觉。
注意
Tkinter 将九种字体分配给九个不同的名称,因此您可以在程序中使用这些名称。字体名称如下:
-
TkDefaultFont -
TkTextFont -
TkFixedFont -
TkMenuFont -
TkHeadingFont -
TkCaptionFont -
TkSmallCaptionFont -
TkIconFont -
TkTooltipFont
因此,您可以在程序中以以下方式使用它们:
Label(text="Sale Up to 50% Off !", font="TkHeadingFont 20")
Label(text="**Conditions Apply", font="TkSmallCaptionFont 8")
使用这些类型的字体标记,您可以确保您的字体在所有平台上看起来都是本地的。
更精细的字体控制
除了上述处理字体的方法外,Tkinter 还提供了一个单独的Font类实现。该类的源代码位于以下链接:<Python27_installtion_dir>\Lib\lib-tk\tkfont.py。
要使用此模块,您需要将tkFont导入到您的命名空间中。(参考8.08 tkfont demo.py):
from Tkinter import Tk, Label, Pack
import tkFont
root=Tk()
label = Label(root, text="Humpty Dumpty was pushed")
label.pack()
currentfont = tkFont.Font(font=label['font'])
print'Actual :' + str(currentfont.actual())
print'Family :' + currentfont.cget("family")
print'Weight :' + currentfont.cget("weight")
print'Text width of Dumpty : %d' %currentfont.measure("Dumpty")
print'Metrics:' + str(currentfont.metrics())
currentfont.config(size=14)
label.config (font=currentfont)
print'New Actual :' + str(currentfont.actual())
root.mainloop()
该程序的控制台输出如下:
Actual :{'family': 'Segoe UI', 'weight': 'normal', 'slant': 'roman', 'overstrike': 0, 'underline': 0, 'size': 9}
Family : Segoe UI
Weight : normal
Text width of Dumpty : 43
Metrics:{'fixed': 0, 'ascent': 12, 'descent': 3, 'linespace': 15}
如您所见,tkfont模块提供了对字体各个方面的更精细控制,这些方面在其他情况下是无法访问的。
字体选择器
现在我们已经看到了tkfont模块中可用的基本功能,让我们用它来实现一个字体选择器。字体选择器看起来就像这里展示的那样:

字体选择器的代码如下(参考8.09 font selector.py):
from Tkinter import *
import ttk
import tkFont
class FontSelectorDemo():
def __init__(self):
self.currentfont = tkFont.Font(font=('Times New Roman',12))
self.family = StringVar(value='Times New Roman')
self.fontsize = StringVar(value='12')
self.fontweight =StringVar(value=tkFont.NORMAL)
self.slant = StringVar(value=tkFont.ROMAN)
self.underlinevalue = BooleanVar(value=False)
self.overstrikevalue= BooleanVar(value=False)
self.gui_creator()
上述代码的描述如下:
-
我们导入
Tkinter(用于所有小部件)、ttk(用于组合框小部件)和tkfont来处理程序中与字体相关的方面。 -
我们创建了一个名为
FontSelectorDemo的类,并使用其__init__方法初始化我们打算在程序中跟踪的所有属性。 -
最后,
__init__方法调用另一个名为gui_creator()的方法,该方法负责创建程序的所有 GUI 元素。
创建 GUI
这里展示的代码是实际代码的高度精简版(参考8.09 font selector.py)。在这里,我们移除了所有创建基本小部件(如标签和复选框)的代码,以便只展示与字体相关的代码:
def gui_creator(self):
# create the top labels – code removed
fontList = ttk.Combobox(textvariable=self.family)
fontList.bind('<<ComboboxSelected>>', self.on_value_change)
allfonts = list(tkFont.families())
allfonts.sort()
fontList['values'] = allfonts
# Font Sizes
sizeList = ttk.Combobox(textvariable=self.fontsize)
sizeList.bind('<<ComboboxSelected>>', self.on_value_change)
allfontsizes = range(6,70)
sizeList['values'] = allfontsizes
# add four checkbuttons to provide choice for font style
# all checkbuttons command attached to self.on_value_change
#create text widget
sampletext ='The quick brown fox jumps over the lazy dog'
self.text.insert(INSERT,'%s\n%s'% (sampletext,sampletext.upper()),'fontspecs')
self.text.config(state=DISABLED)
上述代码的描述如下:
-
我们突出显示了创建两个组合框小部件的代码;一个用于字体家族选择,另一个用于字体大小选择。
-
我们使用
tkfont.families()获取计算机上安装的所有字体的列表。该列表被转换为列表格式并排序后,插入到fontList组合框小部件中。 -
类似地,我们在字体大小组合框中添加了从
6到70的字体大小范围。 -
我们还添加了四个 Checkbutton 小部件来跟踪字体样式 粗体、斜体、下划线 和 删除线。这个代码之前没有展示过,因为我们已经在之前的程序中创建了一些类似的复选按钮。
-
然后,我们添加一个 Text 小部件并将样本文本插入其中。更重要的是,我们在文本中添加了一个名为
fontspec的标签。 -
最后,我们所有的小部件都有一个命令回调方法,该方法连接回一个名为
on_value_change的公共方法。这个方法将负责在任何一个小部件的值发生变化时更新样本文本的显示。
更新样本文本
def on_value_change(self, event=None):
try:
self.currentfont.config(family=self.family.get(), size=self.fontsize.get(), weight=self.fontweight.get(), slant=self.slant.get(), underline=self.underlinevalue.get(), overstrike=self.overstrikevalue.get())
self.text.tag_config('fontspecs', font=self.currentfont)
except ValueError:
pass ### invalid entry - ignored for now. You can use a tkMessageBox dialog to show an error
上述代码的描述如下:
-
当任何小部件的状态发生变化时,将调用此方法
-
此方法只是获取所有字体数据,并使用更新的字体值配置我们的
currentfont属性 -
最后,它更新了标记为
fontspec的文本内容,并使用当前字体的值
处理 Unicode 字符
计算机只理解二进制数字。因此,你在电脑上看到的所有内容,例如文本、图像、音频、视频等,都需要用二进制数字来表示。
这就是编码发挥作用的地方。编码 是一组标准规则,为每个文本字符分配唯一的数值。
Python 2.x 的默认编码是 ASCII(美国信息交换标准代码)。ASCII 字符编码是一种 7 位编码,可以编码 2 ⁷(128)个字符。
由于 ASCII 编码是在美国开发的,它编码了英语字母表中的字符,即数字 0-9、字母 a-z 和 A-Z、一些常见的标点符号、一些电传打字机控制代码以及一个空格。
正是在这里,Unicode 编码来拯救我们。以下是 Unicode 编码的关键特性:
-
这是一种表示文本而不使用字节数的方式
-
它为每种语言的每个字符提供唯一的代码点
-
它定义了超过一百万个代码点,代表地球上所有主要文字的字符
-
在 Unicode 中,有几个 Unicode 转换格式(UTF)
-
UTF-8 是最常用的编码之一,其中 8 表示在编码中使用 8 位数字。
-
Python 也支持 UTF-16 编码,但使用频率较低,Python 2.x 不支持 UTF-32
假设你想在 Tkinter 标签小部件上显示一个印地文字符。你会直观地尝试运行以下代码:
from Tkinter import *
root = Tk()
Label(root, text = "
भारतमेंआपकास्वागतहै
").pack()
root.mainloop()
如果你尝试运行前面的代码,你会得到以下错误消息:
SyntaxError: Non-ASCII character '\xe0' in file 8.07.py on line 4, but no encoding declared; see http://www.Python.org/peps/pep-0263.html for details.
这意味着 Python 2.x 默认不能处理非 ASCII 字符。Python 标准库支持超过 100 种编码,但如果你尝试使用除 ASCII 编码以外的任何编码,你必须明确声明编码。
幸运的是,在 Python 中处理其他编码非常简单。您可以通过以下两种方式处理非 ASCII 字符。它们将在以下章节中描述:
声明行编码
第一种方法是在包含 Unicode 字符的字符串前显式标记前缀 u,如下面的代码片段所示(参考 8.10 行编码.py):
from Tkinter import *
root = Tk()
Label(root, text = u"भारतमेंआपकास्वागतहै").pack()
root.mainloop()
当您尝试从 IDLE 运行此程序时,您会收到类似于以下警告消息:

简单地点击确定以将此文件保存为 UTF-8 编码,并运行此程序以显示 Unicode 标签。
声明文件编码
或者,您可以通过在源文件中包含以下格式的头声明来显式声明整个文件具有 UTF-8 编码:
# -*- coding: <encoding-name> -*-
更精确地说,头声明必须匹配以下正则表达式:
coding[:=]\s*([-\w.]+)
注意
此声明必须包含在程序的第一行或第二行。如果您在第一行或第二行添加其他声明或注释,Python 不会将其识别为头声明。
因此,如果您正在处理 UTF-8 字符,您将在 Python 程序的第一行或第二行添加以下头声明:
# -*- coding: utf-8 -*-
通过添加此头声明,您的 Python 程序现在可以识别 Unicode 字符。因此,我们的代码可以重写为(参考 8.11 文件编码.py):
# -*- coding: utf-8 -*-
from Tkinter import *
root = Tk()
Label(root, text = "भारतमेंआपकास्वागतहै").pack()
root.mainloop()
上述两个代码示例生成的界面与下面所示类似:

注意
Python 3.x 的默认编码是 Unicode(UTF-8)。这意味着在 Python 3.x 中,您不需要显式 Unicode 声明来显示非 ASCII 字符。
Tkinter 类的层次结构
作为程序员,我们几乎不需要理解 Tkinter 的类层次结构。毕竟,我们到目前为止已经能够编写所有应用程序,而不必担心整体类层次结构。
然而,了解类层次结构使我们能够追踪源代码或方法源文档中方法的起源。对类层次结构进行简要回顾也有助于我们防止在程序中意外覆盖方法。
为了理解 Tkinter 的类层次结构,让我们看看 Tkinter 的源代码。在 Windows 安装中,Tkinter 的源代码位于 C:\Python27\Lib\lib-tk\Tkinter.py。
当我们在代码编辑器中打开此文件并查看其类定义列表时,我们可以看到以下结构:

那么,我们在这里注意到什么?我们为每个核心 Tkinter 小部件定义了类。此外,我们还为 Tkinter 内部定义的不同几何管理器和不同变量类型定义了类。这些类定义正是您通常期望存在的。
然而,除了这些之外,我们注意到一些看起来很奇怪的类名,例如 BaseWidget, Misc, Tk, Toplevel, Widget, 和 Wm。所有这些类在上面的截图中都圈出来了。那么这些类提供了什么服务,它们在更大的体系结构中处于什么位置?
让我们使用 inspect 模块来查看 Tkinter 的类层次结构。我们将首先检查 Frame 小部件的类层次结构,以代表所有其他小部件的类层次结构。我们还将查看 Tk 和 Toplevel 类的类层次结构,以估计它们在 Tkinter 整体类层次结构中的角色(参考 8.12 tkinter class hierarchy.py):
import Tkinter
import inspect
print 'Class Hierarchy for Frame Widget'
for i, classname in enumerate(inspect.getmro(Tkinter.Frame)):
print'%s: %s'%(i, classname)
print 'Class Hierarchy for Toplevel'
for i, classname in enumerate(inspect.getmro(Tkinter.Toplevel)):
print '%s: %s'%(i, classname)
print 'Class Hierarchy for Tk'
for i, classname in enumerate(inspect.getmro(Tkinter.Tk)):
print'%s: %s'%(i, classname)
上述程序输出如下:
Class Hierarchy for Frame Widget
0: Tkinter.Frame
1: Tkinter.Widget
2: Tkinter.BaseWidget
3: Tkinter.Misc
4: Tkinter.Pack
5: Tkinter.Place
6: Tkinter.Grid
Class Hierarchy for Toplevel
0: Tkinter.Toplevel
1: Tkinter.BaseWidget
2: Tkinter.Misc
3: Tkinter.Wm
Class Hierarchy for Tk
0: Tkinter.Tk
1: Tkinter.Misc
2: Tkinter.Wm
以下是对前面代码的描述:
-
inspect模块中的getmro(classname)函数返回一个元组,包含classname的所有祖先,按照 方法解析顺序(MRO)指定的顺序。方法解析顺序指的是在查找给定方法时搜索基类的顺序。 -
通过检查 MRO 和源代码,我们得知
Frame类继承自Widget类,而Widget类又继承自BaseWidget类。 -
此外,
Frame类还继承自Misc类,这是一个通用的 混合类,为我们提供了在应用程序中使用的大量功能。 -
要获取
Misc类提供的功能列表,请在您的 Python 交互式 shell 中运行以下命令:>>> import Tkinter >>> help(Tkinter.Misc) -
最后,我们所有的部件都从几何混合类(Pack、Grid 和 Place)获取属性。
-
接下来,让我们看一下
Tk和Toplevel类。 -
Tk类代表 Tkinter 的 Toplevel 小部件,它表示应用程序的主窗口。Toplevel类提供了一些方法来构建和管理具有给定父级的小部件。 -
要获取
Toplevel和Tk类提供的方法列表,请在您的 Python 交互式 shell 中运行以下命令:>>>help(Tkinter.Toplevel) >>>help(Tkinter.Tk) -
除了继承自
Misc混合类之外,Toplevel和Tk类还继承自Wm混合类。 -
Wm(窗口管理器)混合类提供了许多与窗口管理器通信的功能。要获取Wm类提供的功能列表,请在您的 Python 交互式 shell 中运行以下命令:>>>help(Tkinter.Wm)
将从上一个程序中获得类层次结构转换为图像后,我们得到一个类似于以下图像的层次结构图像:

除了正常继承关系(如前图所示的无标记线条所示),Tkinter 还提供了一系列混合类(或辅助类)。混合类是一种设计用来不直接使用,而是通过多重继承与其他类结合使用的类。
Tkinter 混合类可以大致分为两类:
-
几何混合类,包括 Grid、Pack 和 Place 类
-
实现混合类,包括:
-
Misc类,该类被根窗口和窗口小部件类使用,提供了几个 Tk 和窗口相关服务 -
Wm类,该类被根窗口和 Toplevel 窗口小部件使用,提供了几个窗口管理服务。
-
自定义混合类
我们创建了一个“裸骨”GUI 框架,以避免重复创建小部件的代码。类似于这个概念,还有一种方法可以通过使用所谓的自定义 GUI 混合来避免编写样板代码。以8.13 创建自定义混合.py的代码为例。这个程序创建了一个类似于下面展示的界面:

让我们看看8.13 创建自定义混合.py的代码:
from Tkinter import *
def frame(parent, row, col):
widget = Frame(parent)
widget.grid(row= row, column=col)
return widget
def label(parent, row, col, text):
widget = Label(parent, text=text)
widget.grid(row=row, column=col, sticky='w', padx=2)
return widget
def button(parent, row, col, text, command):
widget = Button(parent, text=text, command=command)
widget.grid(row= row, column=col, sticky='e', padx=5, pady=3)
return widget
def entry(parent, row, col, var):
widget = Entry(parent,textvariable= var)
widget.grid(row= row, column=col, sticky='w', padx=5)
return widget
def button_pressed(uname, pwd):
print'Username: %s' %uname
print'Password: %s'%pwd
if __name__ == '__main__':
root = Tk()
frm = frame(root, 0, 0)
label(frm, 1, 0, 'Username:')
uname= StringVar()
entry(frm, 1, 1, uname)
label(frm, 2, 0, 'Password:')
pwd= StringVar()
entry(frm, 2, 1, pwd)
button(frm, 3, 1, 'login', lambda: button_pressed(uname.get(), pwd.get()) )
root.mainloop()
上述代码的描述如下:
-
这个程序首先为不同的窗口小部件创建函数,例如 Frame、Label、Button 和 Entry。每个方法都可以命名为混合类,因为它使用网格方法同时处理窗口小部件的创建和几何形状管理。这些本质上是为了帮助我们避免为类似的一组窗口小部件编写相似代码的便利函数。
-
现在,在程序的主要部分,我们可以通过一行代码创建一个窗口小部件,而无需为处理其几何形状添加单独的一行。这样做的结果是,我们的实际程序中的代码行数更少。如果程序中有大量窗口小部件,这种策略可以减少程序的大小。
注意
然而,混合类非常特定于案例。为特定案例场景或应用程序定义的混合类可能不适用于另一个应用程序。例如,在定义前面提到的混合类时,我们做了一些假设,比如我们所有的窗口小部件都将使用网格几何形状管理器,同样,按钮会粘附在东部,而输入框会粘附在西部。这些假设可能不适用于不同的应用程序。
代码清理和程序优化的技巧
现在,让我们花些时间讨论一些技巧和窍门,这些技巧和窍门将有助于提高我们 Python 程序的性能。在正常的 GUI 编程案例中,这通常涉及加快对提高整体用户体验有贡献的程序部分。
程序优化通常被狂热地视为减少代码执行时间的练习。对于时间是一个关键因素的程序,这种狂热是真实的。然而,如果你正在开发一个简单的 GUI 应用程序,正确和一致的用户体验通常比仅仅快速的用户体验更重要。
在代码尚未功能化之前就尝试优化代码是过早优化,应该避免。然而,一个响应时间正确但相当长的 GUI 程序可能需要优化,这是下文讨论的主题。
选择合适的数据结构
选择合适的数据结构可以对程序的性能产生深远的影响。如果你的程序在查找上要花费大量时间,如果可能的话,使用字典。当你只需要遍历集合时,更倾向于选择列表而不是字典,因为字典占用更多空间。
当你的数据是不可变的,更倾向于选择元组而不是列表,因为元组比列表更快地遍历。
变量操作
你在程序中选择变量的方式可以显著影响程序执行的效率。例如,如果你在实例化小部件后不需要更改其内容或属性,那么不要创建小部件的全局实例。
例如,如果 Label 小部件要保持静态,使用Label(root, text='Name').pack(side=LEFT),而不是使用以下代码片段:
self.mylabel = Label(root, text='Name')
self.mylabel.pack(side=LEFT)
同样,如果你不打算多次使用局部变量,那么不要创建局部变量。例如,使用mylabel.config(text= event.keysym)而不是首先创建一个局部变量key然后只使用一次:
key = event.keysym
mylabel.config (text=key)
如果局部变量要被多次使用,可能有必要创建一个局部变量。
使用异常
现在有一个小的注意事项。为了集中说明 Tkinter 的核心概念,我们故意在本书的所有示例中忽略了干净的异常处理。
在我们的大部分项目中,我们使用了简单的 try-except 块来实现一个“捕获所有错误”的异常。然而,当你在编写应用程序时,你最好尽可能具体地说明你想要处理的异常。
Python 遵循 EAFP(请求原谅比请求许可更容易)的编码风格,这与大多数其他编程语言遵循的 LBYL(跳之前先看)风格相反。
因此,在 Python 中使用类似于以下示例的异常处理通常比使用 if-then 块检查条件要干净:
try:
doSomethingNormal()
except SomethingWrong:
doSomethingElse()
以下代码片段展示了 if-then 块的一个示例:
if SomethingWrong:
doSomethingElse()
else:
doSomethingNormal()
过滤和映射
Python 提供了两个内置函数filter和map,可以直接操作列表,而不是必须直接遍历列表中的每个项目。filter、map和reduce函数比使用循环更快,因为很多工作都是由底层用 C 语言编写的代码完成的。
-
过滤:
filter(function, list)函数返回一个列表(Python 3.x中的迭代器),其中包含所有函数返回 true 值的项。例如:print filter(lambda num: num>6, range(1,10))# prints [7, 8, 9]这比在列表上运行条件 if-then 检查要快。
-
映射:
map(func, list)函数将func应用于列表中的每个项目,并返回一个新列表(Python 3.x中返回迭代器而不是列表)。例如:print map(lambda num: num+5, range(1,5)) #prints [6, 7, 8, 9]这比通过循环遍历列表并给每个元素加 5 要快。
性能分析
性能分析涉及生成详细统计信息,以显示程序中各种例程执行的频率和持续时间。这有助于隔离程序中的问题部分,这些部分可能需要重新设计。
Python 2.7.x提供了一个名为cProfile的内置模块,该模块可以生成有关程序的详细统计信息。该模块提供了诸如总程序运行时间、每个函数的运行时间和每个函数被调用的次数等详细信息。这些统计信息使得确定需要优化的代码部分变得容易。
注意
特别是,cProfile为函数或脚本提供了以下数据:
-
ncalls:一个函数被调用的次数
-
tottime:一个函数上花费的时间,不包括调用其他函数所花费的时间
-
percall:
tottime除以ncalls -
cumtime:一个函数上花费的时间,包括调用其他函数
-
percall:
cumtime除以tottime
你可以用这个来分析一个单独的函数:
import cProfile
cProfile.run('spam()','spam.profile')
然后,你可以使用另一个名为pstats的模块查看性能分析的结果:
import pstats
stats = pstats.Stats('spam.profile')
stats.strip_dirs().sort_stats('time').print_stats()
更重要的是,你可以分析整个脚本。比如说,你想分析一个名为myscript.py的脚本。你只需使用命令行工具导航到脚本的目录,然后输入并运行:
Python -m cProfilemyscript.py
这会产生类似于以下输出的结果:
1014 function calls in 0.093 CPU seconds
Ordered by: standard name
ncallstottimepercallcumtimepercallfilename:lineno(function)
1 0.000 0.000 0.000 0.000 Tkinter.py:3686(Studbutton)
1 0.000 0.000 0.000 0.000 Tkinter.py:3694(Tributton)
416 0.001 0.000 0.002 0.000 Tkinter.py:74(_cnfmerge)
1 0.011 0.011 0.028 0.028 myscript.py:19(<module>)
2 0.058 0.029 0.086 0.043 myscript.py:20(setAge)
7520.105 0.0000.257 0.129 myscript.py:23(findAudio)
10.001 0.001 0.013 0.013 myscript.py:25(createGUI)
1 40.004 0.000 0.005 0.005 myscript.py:4(saveAudio)
1 0.000 0.000 0.000 0.000 myscript.py:49(<module>)
然后,你可以分析代码,以查看执行时间较长的函数。在我们的假设示例中,我们注意到findAudio和saveAudio函数执行时间最长。然后我们可以分析这两个函数,看看它们是否可以优化。
除了 cProfile 模块之外,还有其他模块,例如PyCallGraph和objgraph,它们为性能分析数据提供可视化的图表。
其他优化技巧
优化是一个广泛的话题,你可以做很多事情。如果你对代码优化感兴趣,你可能可以从以下链接的官方 Python 优化技巧开始:
wiki.python.org/moin/PythonSpeed/PerformanceTips
分发 Tkinter 应用程序
因此,你的新应用程序已经准备好了,现在你想与世界上其他人分享它。你该如何做呢?
当然,你需要 Python 安装来运行你的程序。Windows 没有预装 Python。大多数现代 Linux 发行版和 Mac OS X 都预装了 Python,但你需要的不仅仅是任何版本的 Python。你需要一个与程序最初编写的版本兼容的 Python 版本。
然后,如果你的程序使用第三方模块,你需要安装适当的模块以适应所需的 Python 版本。当然,这需要处理太多的多样性。
幸运的是,我们有工具,例如Freeze工具,这使我们能够将 Python 程序作为独立应用程序分发。
由于需要处理的平台多样性,有大量的 Freeze 工具选项可供选择。因此,对任何一种工具的详细讨论超出了本书的范围。
在接下来的几节中,我们将列出一些最先进的冻结工具。如果你找到一个符合你分发要求的工具,你可以查看其文档以获取更多信息。
py2exe
如果你只需要在 Windows 上分发你的 Python 应用程序,py2exe 可能是最坚固的工具。它将 Python 程序转换为可执行 Windows 程序,可以在不要求安装 Python 的情况下运行。更多信息、下载链接和教程可在 www.py2exe.org/ 查找。
py2app
py2app 在 Mac OS X 上执行的任务与 py2exe 在 Windows 上执行的任务相同。如果你只需要在 Mac OS X 上分发你的 Python 应用程序,py2app 是一个经过时间考验的工具。更多信息请参阅 svn.pythonmac.org/py2app/py2app/trunk/doc/index.html。
PyInstaller
PyInstaller 在过去几年中作为冻结工具获得了人气,部分原因在于它支持广泛的平台,例如 Windows、Linux、Mac OS X、Solaris 和 AIX。
此外,使用 PyInstaller 创建的可执行文件据称比其他冻结工具占用的空间更少,因为它使用了透明压缩。PyInstaller 的另一个重要特性是它与大量第三方软件包的即插即用兼容性。
功能列表、下载和文档的完整信息可以在 www.pyinstaller.org/ 查阅。
其他冻结工具
其他冻结工具包括:
-
冻结:此工具随标准 Python 发行版提供。冻结工具只能在 Unix 系统上编译可执行文件。然而,该程序过于简单,甚至无法处理常见的第三方库。更多信息请参阅此链接:
-
cx_Freeze:这个工具与 py2exe 和 py2app 类似,但声称可以在 Python 本身工作的所有平台上进行移植。更多信息请参阅此链接:
cx-freeze.sourceforge.net/index.html小贴士
如果你正在分发一个小程序,冻结工具可能正是你所需要的。然而,如果你有一个大型程序,比如有很多外部第三方库依赖项或现有冻结工具不支持依赖项,那么你的应用程序可能是将 Python 解释器与你的应用程序捆绑的正确候选。
Tkinter 的局限性
我们已经探讨了 Tkinter 的强大功能。也许 Tkinter 最大的优势在于其易用性和轻量级特性。
然而,Tkinter 的易用性和轻量级特性也导致了一些限制。
核心小部件数量有限
Tkinter 仅提供少量基本控件,并且缺乏现代控件的集合。它需要ttk、Pmw、Tix和其他扩展来提供一些真正有用的控件。即使有了这些扩展,Tkinter 也无法与其他 GUI 工具提供的控件范围相匹配,例如wxPython的高级控件集和PyQt。
例如,wxPython 的 HtmlWindow 控件可以轻松显示 HTML 内容。在 Tkinter 中也有尝试提供类似扩展,但它们远未令人满意。同样,wxPython 中还有来自高级用户界面库和混入的其他控件,例如浮动/停靠框架、视角加载和保存等,Tkinter 用户只能希望它们能在未来的版本中包含。
Tkinter 的支持者经常倾向于通过引用如何轻松地从一组基本控件中构建新控件来反驳这种批评。
无打印支持
Tkinter 因不提供打印功能而受到应有的批评。与之相比,wxPython 提供了一个完整的打印解决方案,即打印框架。
无新图像格式支持
Tkinter 原生不支持 JPEG 和 PNG 等图像格式。Tkinter 的PhotoImage类只能读取 GIF 和 PGM/PPM 格式的图像。
虽然有解决方案,例如使用 PIL 模块中的 ImageTk 和 Image 子模块,但如果 Tkinter 原生支持流行的图像格式会更好。
不活跃的开发社区
Tkinter 常被批评为拥有相对不活跃的开发社区。这在很大程度上是正确的。Tkinter 的文档多年来一直处于不断完善中。
年复一年,Tkinter 出现了大量的扩展,但其中大多数已经很长时间没有活跃开发了。
Tkinter 的支持者用逻辑来反驳这一点,即 Tkinter 是一种稳定且成熟的技术,不需要像一些新开发的 GUI 模块那样频繁修订。
Tkinter 的替代品
除了 Tkinter,还有其他几个流行的 Python GUI 工具包。最受欢迎的包括 wxPython、PyQt、PySide和PyGTK。以下是关于这些工具包的简要讨论。
wxPython
wxPython 是wxWidgets的 Python 接口,wxWidgets 是一个流行的开源 GUI 库。用 wxPython 编写的代码可以在大多数主要平台(如 Windows、Linux 和 Mac OS X)上移植。
wxPython 界面通常被认为比 Tkinter 更适合构建更复杂的 GUI,主要是因为它拥有大量原生支持的控件。然而,Tkinter 的支持者对此观点提出异议。
wxWidgets 接口最初是用 C++编程语言编写的,因此 wxPython 继承了 C++程序典型的很大一部分复杂性。wxPython 提供了一个非常大的类库,通常需要更多的代码来生成与 Tkinter 相同的界面。然而,作为这种复杂性的交换,wxPython 提供了比 Tkinter 更大的内置控件库。此外,有些人更喜欢 wxPython 小部件的外观,而不是 Tkinter 渲染的外观。
由于其固有的复杂性,wxPython 已经出现了几个 GUI 构建器工具包,例如wxGlade、wxFormBuilder、wxDesigner等。
wxPython 安装程序包含演示程序,可以帮助您快速开始使用此工具包。要下载工具包或获取有关 wxPython 的更多信息,请访问以下链接:
PyQt
PyQt 是跨平台 GUI 工具包 Qt 的 Python 接口,Qt 是由英国公司 Riverbank Computing 开发和维护的项目。
PyQt,拥有数百个类和数千个函数,可能是目前用于 Python GUI 编程功能最全面的 GUI 库。然而,这种功能负载带来了很多复杂性,学习曲线陡峭。
Qt(因此 PyQt)支持非常丰富的控件集。此外,它还包括对网络编程、SQL 数据库、线程、多媒体框架、正则表达式、XML、SVG 等内置支持。Qt 的设计器功能可以从 WYSIWYG(所见即所得)界面生成 GUI 代码。
PyQt 可在多种许可下使用,包括GNU、通用公共许可证(GPL)和商业许可。然而,它的最大缺点是,与 Qt 不同,它不可在 LGPL 下使用。
PySide
如果您正在寻找 Python 的 LGPL 版本 Qt 绑定,您可能想探索 PySide。PySide 最初于 2009 年 8 月由诺基亚(Qt 工具包的前所有者)以 LGPL 的形式发布。现在它由 Digia 拥有。有关 PySide 的更多信息,请从以下链接获取:
PyGTK
PyGTK是一组针对 GTK + GUI 库的 Python 绑定。PyGTK 应用程序是跨平台的,可以在 Windows、Linux、MacOS X 和其他操作系统上运行。PyGTK 是免费软件,并受 LGPL 许可。因此,您可以非常少地限制地使用、修改和分发它。
您可以通过以下链接获取有关 PyGTK 的更多信息:
其他选项
除了这些最受欢迎的工具包之外,还有一系列适用于 Python GUI 编程的工具包可供选择。
对于熟悉 Java GUI 库(如 Swing 和 AWT)的 Java 程序员,他们可以通过使用 Jython 无缝访问这些库。同样,C# 程序员可以使用 IronPython 从 .NET 框架中访问 GUI 构建功能。
要查看 Python 开发者可用的其他 GUI 工具的完整列表,请访问此链接:
wiki.python.org/moin/GuiProgramming
获取交互式帮助
这一部分不仅适用于 Tkinter,也适用于任何你需要帮助的 Python 对象。
假设你需要关于 Tkinter Pack 布局管理器的参考资料,你可以在 Python 交互式 shell 中使用 help 命令获取交互式帮助,如下面的命令行所示:
>>> import Tkinter
>>> help(Tkinter.Pack)
这提供了关于 Tkinter 中 Pack 类下定义的所有方法的详细帮助文档。
你可以类似地查看所有其他单独小部件的帮助。例如,你可以在交互式 shell 中输入以下内容来检查 Label 小部件的全面和权威帮助文档:
>>>help(Tkinter.Label)
这提供了一份列表:
-
在类
Label中定义的所有方法 -
Label 小部件的所有标准和特定于小部件的选项
-
从其他类继承的所有方法
最后,如果你对某个方法有疑问,可以查看位于 <location-of-python-installation>\lib\lib-tk\Tkinter.py> 的源文件。
小贴士
lib-tk 目录是许多优秀的 Tkinter 代码的家园。特别是,你可能还想查看以下源代码:
-
turtle.py:一种流行的向孩子介绍编程的方式。它包括一些酷炫的动画效果
-
Tkdnd.py:一个实验性代码,允许你在 Tkinter 窗口中拖放项目。
你还可能发现查看其他模块(如颜色选择器、文件对话框、ttk 模块等)的源代码实现很有用。
Tkinter 在 Python 3.x 中
在 2008 年,Python 的作者 Guido van Rossum 将语言分成了两个分支——2.x 和 3.x。这样做是为了清理和使语言更加一致。
Python 3.x 与 Python 2.x 不再兼容。例如,Python 2.x 中的 print 语句已被 print() 函数取代,该函数现在将参数作为参数接收。
我们使用 Python 版本 2.7 编写了所有的 Tkinter 程序,因为它比 Python 3.x 拥有更丰富的第三方库,而 Python 3.x 仍然被视为一个开发中的版本。
Tkinter 在 2.x 和 3.x 之间的核心功能保持不变。从 Python 2.x 迁移到 Python 3.x 时,Tkinter 的唯一重大变化是改变导入 Tkinter 模块的方式。
Tkinter 在 Python 3.x 中已被重命名为 tkinter(已移除大小写)。请注意,在 3.x 中,目录 lib-tk 已被重命名为 tkinter。在目录内部,文件 Tkinter.py 已被重命名为 __init__.py,从而使 tkinter 成为一个可导入的模块。
因此,最大的主要区别在于您将 Tkinter 模块导入当前命名空间的方式:
from Tkinter import * # for Python2
from tkinter import * # for Python3
此外,请注意以下变更:
| Python 2.x | Python 3.x |
|---|---|
import ttk |
import tkinter.ttk OR from tkinter import ttk |
import tkMessageBox |
import tkinter.messagebox |
import tkColorChooser |
import tkinter.colorchooser |
import tkFileDialog |
import tkinter.filedialog |
import tkSimpleDialog |
import tkinter.simpledialog |
import tkCommonDialog |
import tkinter.commondialog |
import tkFont |
import tkinter.font |
import ScrolledText |
import tkinter.scrolledtext |
import Tix |
import tkinter.tix |
结论
最后,让我们总结一下设计应用程序涉及的一些关键步骤:
-
根据您想要设计的内容,选择一个合适的数据结构来逻辑地表示您的需求。
-
如果需要,将原始数据结构组合成复杂结构,例如,一个字典列表或一个字典元组。
-
为构成您应用程序的对象创建类。添加需要操作的属性以及操作这些属性的方法。
-
使用丰富的 Python 标准库和外部库提供的不同 API 来操作属性。
我们在这本书中尝试构建了几个部分功能的应用程序。然后我们为代码提供了说明。然而,当您试图在顺序文本中解释软件开发过程时,您有时会误导读者,暗示软件开发程序是一个线性过程。这几乎是不正确的。
实际的编程通常不会这样进行。事实上,中小型程序通常是在增量试错过程中编写的,在这个过程中假设会改变,结构会修改,贯穿整个应用程序开发过程。
这就是您如何开发一个从小型到中型应用程序的方法:
-
从一个简单的脚本开始。
-
设定一个小的可实现目标,实现它,然后以增量方式考虑添加下一个功能到您的程序中。
-
您可能一开始不需要引入类结构。如果您对问题域有清晰的认识,您可能从一开始就引入类结构。
-
如果您一开始不确定类结构,从简单的过程式代码开始。随着您的程序开始增长,您可能会开始遇到很多全局变量。正是在这里,您将开始了解您程序的结构维度。现在是时候重构和重新结构化您的程序,引入类结构。
小贴士
如果您正在编写一个小程序,进化的试错策略效果很好。
然而,如果您开始开发中型到大型应用程序,在您坐下来编写代码之前进行一些严肃的前期规划会更好,因为大型程序失败的成本远远高于我们通常能承受的。
一个类比可以更好地解释这一点。你可以通过试错的方式搭建一个小棚屋,但如果没有一些严肃的规划,你不会尝试去建造一座摩天大楼。
也很重要的是不要被技术世界中不断演变的术语无谓地困扰。编程与其说是了解特定的 API 或特定的编程语言,不如说是找到一个解决你当前问题的工具。你实际上可以在短时间内了解编程语言的基本结构。编程更多的是一个寻找解决方案的工具。
这就带我们来到了本书的结尾。我希望这本书已经教会了你关于使用 Python 和 Tkinter 进行 GUI 编程的一些知识。
除了阅读书籍之外,真正替代原创 GUI 编程的方法几乎没有。所以,接受一个原创的编程挑战,并为了乐趣去执行它。
你如何实施它取决于个人的经验和品味。做让你感到舒适的事情,但保持自己在开发每个阶段都进行持续重构的想法开放。
附录 B. 快速参考表
附录的大部分内容是从内置 Tkinter 文档生成的,因此相应地归 Python 软件基金会版权所有(版权所有 © 2001-2013 Python 软件基金会;版权所有)。
小部件共有选项
以下表格包含大多数小部件共有的选项、它们的职能以及不适用于这些选项的小部件列表:
| 小部件选项 | 功能 | 不适用于小部件 |
|---|---|---|
background (bg) |
选择背景颜色。 | (无内容) |
borderwidth (bd) |
定义边框的像素宽度。 | (无内容) |
cursor |
指定小部件使用的鼠标光标。 | (无内容) |
relief |
指定小部件的边框样式。 | (无内容) |
takefocus |
如果窗口在键盘遍历期间接受焦点。 | (无内容) |
width |
一个整数,指定小部件的相对宽度。 | 菜单 |
font |
指定字体家族和字体大小。 | 最顶层、画布、框架和滚动条 |
foreground (fg) |
指定前景颜色。 | 最顶层、画布、框架和滚动条 |
highlightbackground |
颜色 | 菜单 |
highlightcolor |
颜色 | 菜单 |
highlightthickness |
以像素为单位进行测量。 | 菜单 |
relief |
指定应用于给定小部件的 3D 效果。有效值有 RAISED、SUNKEN、FLAT、RIDGE、SOLID 和 GROOVE。 |
(无内容) |
takefocus |
指定小部件在键盘标签遍历期间是否接受焦点。 | (无内容) |
width |
指定小部件宽度的整数。 | 菜单 |
以下表格包含大多数小部件共有的选项、它们的职能以及适用于这些选项的小部件列表:
| 小部件选项 | 功能 | 适用于 |
|---|---|---|
activebackground |
当小部件处于活动状态时的背景颜色。 | 菜单、菜单按钮、按钮、复选框、单选按钮、刻度和滚动条 |
activeforeground |
当小部件处于活动状态时的前景颜色。 | 菜单、菜单按钮、按钮、复选框和单选按钮 |
anchor |
指示文本或位图在窗口组件上显示的位置。有效值有 n、ne、e、se、s、sw、w、nw 或 center。 |
菜单按钮、按钮、复选框、单选按钮、标签和信息框 |
bitmap |
指示在窗口组件中显示的位图。 | 菜单按钮、按钮、复选框、单选按钮和标签 |
command |
指示与窗口关联的命令回调,通常在窗口上鼠标按钮 1 释放时调用。 | 按钮、复选框、单选按钮、刻度和滚动条 |
disabledforeground |
当小部件处于禁用状态时显示的前景色。 | 菜单、菜单按钮、按钮、复选框和单选按钮 |
height |
表示小部件的高度,以给定小部件指定的字体单位表示。 | 最顶层、菜单按钮、按钮、复选框、单选按钮、标签、框架、列表框和画布 |
image |
表示在部件中显示的图像。 | Menubutton、Button、Checkbutton、Radiobutton 和 Label |
justify |
当在部件中显示多行文本时适用。这决定了文本行之间的对齐方式。必须是 LEFT、CENTER 或 RIGHT 之一。 | Menubutton、Button、Checkbutton、Radiobutton、Label、Entry 和 Message |
selectbackground |
表示显示选中项时要显示的背景颜色。 | Text、Listbox、Entry 和 Canvas |
selectborderwidth |
表示显示选中项时要显示的边框宽度。 | Text、Listbox、Entry 和 Canvas |
selectforeground |
表示显示选中项时要显示的前景色。 | Text、Listbox、Entry 和 Canvas |
state |
表示部件可能处于的两个或三个状态之一。有效值 normal、active 或 disabled。 |
Menubutton、Button、Checkbutton、Radiobutton、Text、Entry 和 Scale |
text |
表示要在部件内显示的字符串。 | Menubutton、Button、Checkbutton、Radiobutton、Label 和 Message |
textvariable |
表示变量的名称。变量的值被更改为字符串以便在部件中显示。当变量值改变时,部件会自动更新。 | Menubutton、Button、Checkbutton、Radiobutton、Label、Entry 和 Message |
underline |
表示在部件中下划线的字符的整数索引。 | Menubutton、Button、Checkbutton、Radiobutton 和 Label |
wraplength |
表示具有单词换行的部件的最大行长度。 | Menubutton、Button、Checkbutton、Radiobutton 和 Label |
部件特定选项
我们并没有列出所有部件特定选项。您可以在 Python 交互式外壳中使用 help 命令获取给定部件的所有可用选项。
要获取任何 Tkinter 类的帮助,首先将 Tkinter 导入命名空间,如下所示:
>>>import Tkinter
然后,可以使用以下命令获取特定部件的信息:
| Widget Name | 获取帮助 |
|---|---|
| Label | help(Tkinter.Label) |
| Button | help(Tkinter.Button) |
| Canvas | help(Tkinter.Canvas) |
| CheckButton | help(Tkinter.Checkbutton) |
| Entry | help(Tkinter.Entry) |
| Frame | help(Tkinter.Frame) |
| LabelFrame | help(Tkinter.LabelFrame) |
| Listbox | help(Tkinter.Listbox) |
| Menu | help(Tkinter.Menu) |
| Menubutton | help(Tkinter.Menubutton) |
| Message | help(Tkinter.Message) |
| OptionMenu | help(Tkinter.OptionMenu) |
| PanedWindow | help(Tkinter.PanedWindow) |
| RadioButton | help(Tkinter.Radiobutton) |
| Scale | help(Tkinter.Scale) |
| Scrollbar | help(Tkinter.Scrollbar) |
| Spinbox | help(Tkinter.Spinbox) |
| Text | help(Tkinter.Text) |
| Bitmap Class | help(Tkinter.BitmapImage) |
| Image Class | help(Tkinter.Image) |
包管理器
Pack 几何管理器是 Tk 和 Tkinter 中可用的最古老的几何管理器。Pack 几何管理器将子小部件放置在主小部件中,按照子小部件被引入的顺序逐个添加。下表显示了可用的pack()方法和选项:
| 方法 | 描述 |
|---|
| config = configure = pack_configure(self, cnf={}, **kw) | 在父小部件中打包小部件。使用以下选项:
-
after=widget: 在打包小部件后打包它 -
anchor=NSEW(或子集): 根据给定方向定位小部件 -
before=widget: 在打包小部件之前打包它 -
expand=bool: 如果父小部件大小增加,则扩展小部件 -
fill=NONE(或X、Y或BOTH):如果小部件增长,则填充小部件 -
in=master: 使用主小部件包含此小部件 -
in_=master: 查看in选项描述 -
ipadx=amount: 在 x 方向添加内部填充 -
ipady=amount: 在 y 方向添加内部填充 -
padx=amount: 在 x 方向添加填充 -
pady=amount: 在 y 方向添加填充 -
side=TOP(或BOTTOM、LEFT或RIGHT):添加此小部件的位置
|
forget = pack_forget(self) |
解除映射此小部件,并不要用于打包顺序。 |
|---|---|
info = pack_info(self) |
返回此小部件的打包选项信息。 |
propagate =pack_propagate(self, flag=['_noarg_']) from Tkinter.Misc |
设置或获取几何信息传播的状态。布尔参数指定子小部件的几何信息是否将决定此小部件的大小。如果没有提供参数,则返回当前设置。 |
slaves = pack_slaves(self) from Tkinter.Misc |
返回此小部件所有子小部件的打包顺序列表。 |
网格管理器
网格易于实现且易于修改,使其成为大多数用例中最受欢迎的选择。以下是使用grid()几何管理器进行布局管理可用的方法和选项列表:
| 在此处定义的方法 | 描述 |
|---|---|
bbox = grid_bbox(self, column=None, row=None, col2=None, row2=None) from Tkinter.Misc |
返回由几何管理器网格控制的小部件边界框的整数坐标元组。如果提供了column和row,则边界框适用于从行和列 0 的单元格到指定的单元格。如果提供了col2和row2,则边界框从该单元格开始。返回的整数指定了主小部件中上左角的偏移量以及宽度和高度。 |
columnconfigure = grid_columnconfigure(self, index, cnf={}, **kw) from Tkinter.Misc |
配置网格的index列。有效资源包括 minsize(列的最小大小)、weight(额外空间传播到该列的程度)和 pad(额外空间量)。 |
| grid = config = configure = grid_configure(self, cnf={}, **kw) | 在父小部件中按网格定位小部件。使用以下选项:
-
column=number: 使用给定列标识的单元格(从 0 开始) -
columnspan=number: 此部件将跨越多个列 -
in=master: 使用主部件包含此部件 -
in_=master: 查看'in'选项描述 -
ipadx=amount: 在 x 方向添加内部填充 -
ipady=amount: 在 y 方向添加内部填充 -
padx=amount: 在 x 方向添加填充 -
pady=amount: 在 y 方向添加填充 -
row=number: 使用给定行标识的单元格(从 0 开始) -
rowspan=number: 此部件将跨越多个行 -
sticky=NSEW: 如果单元格在哪个方向上更大,则此部件将粘附到单元格边界
|
forget = grid_forget(self) |
取消映射此部件。 |
|---|---|
info = grid_info(self) |
返回有关在此网格中定位此部件的选项的信息。 |
grid_location(self, x, y) from Tkinter.Misc |
返回一个元组,表示列和行,这些列和行标识了主部件内 X 和 Y 位置像素所在的单元格。 |
grid_propagate(self, flag=['_noarg_']) from Tkinter.Misc |
设置或获取几何信息传播的状态。一个布尔参数指定是否由从属部件的几何信息确定此部件的大小。如果没有给出参数,将返回当前设置。 |
grid_remove(self) |
取消映射此部件,但记住网格选项。 |
grid_rowconfigure(self, index, cnf={}, **kw) from Tkinter.Misc |
配置网格的index行。有效的资源有 minsize(行的最小大小)、weight(额外空间传播到本行的程度)和 pad(额外允许的空间)。 |
size = grid_size(self) from Tkinter.Misc |
返回网格中列和行的数量 |
slaves = grid_slaves(self, row=None, column=None) from Tkinter.Misc |
返回此部件在其打包顺序中的所有从属部件的列表。 |
location = grid_location(self, x, y) from Tkinter.Misc |
返回一个元组,表示列和行,这些列和行标识了主部件内 X 和 Y 位置像素所在的单元格。 |
propagate = grid_propagate(self, flag=['_noarg_']) from Tkinter.Misc |
设置或获取几何信息传播的状态。一个布尔参数指定是否由从属部件的几何信息确定此部件的大小。如果没有给出参数,将返回当前设置。 |
rowconfigure = grid_rowconfigure(self, index, cnf={}, **kw) from Tkinter.Misc |
配置网格的INDEX行。有效的资源有 minsize(行的最小大小)、weight(额外空间传播到本行的程度)和 pad(额外允许的空间)。 |
位置管理器
place()几何管理器允许基于给定窗口的绝对或相对坐标精确定位部件。以下表格列出了在位置几何管理器下可用的方法和选项:
| 在此处定义的方法 | 描述 |
|---|
| config = configure = place_configure(self, cnf={}, **kw) | 在父小部件中放置一个小部件。使用以下选项:
-
in=master: 小部件放置的主控件相对位置 -
in_=master: 参见 'in' 选项描述 -
x=amount: 在主控件的 x 位置定位此小部件的锚点 -
y=amount: 在主控件的 y 位置定位此小部件的锚点 -
relx=amount: 在主控件的宽度范围内(0.0 到 1.0)定位此小部件的锚点(1.0 是右边缘) -
rely=amount: 在主控件的 0.0 到 1.0 之间定位此小部件的锚点(1.0 是底部边缘)
-
anchor=NSEW(或子集):根据给定方向定位锚点 -
width=amount: 此小部件的宽度(以像素为单位) -
height=amount: 此小部件的高度(以像素为单位) -
relwidth=amount: 此小部件的宽度,相对于主控件的 0.0 到 1.0(1.0 与主控件相同宽度) -
relheight=amount: 此小部件的高度,相对于主控件的 0.0 到 1.0(1.0 与主控件相同高度) -
bordermode="inside"(或"outside"):是否考虑主小部件的边框宽度
|
forget = place_forget(self) |
取消映射此小部件。 |
|---|---|
info = place_info(self) |
返回此小部件放置选项的信息。 |
slaves = place_slaves(self) from Tkinter.Misc |
返回此小部件所有子部件的列表,按其打包顺序排列。 |
事件类型
表示事件的通用格式如下:
<[event modifier-]...event type [-event detail]>
对于任何事件绑定,必须指定事件类型。此外,请注意,事件类型、事件修饰符和事件细节在不同平台上可能有所不同。以下表格表示事件类型及其描述:
| 事件类型 | 描述 |
|---|---|
| Activate | 小部件的状态选项从非活动状态(灰色)变为活动状态。 |
| Button | 鼠标按钮按下。事件详细部分指定了哪个按钮。 |
| ButtonRelease | 按下的鼠标按钮释放。 |
| Configure | 小部件大小的更改。 |
| Deactivate | 小部件的状态选项从活动状态变为非活动状态(灰色)。 |
| Destroy | 使用 widget.destroy 方法销毁小部件。 |
| Enter | 鼠标指针进入小部件的可见部分。 |
| Expose | 小部件至少有一部分在另一个窗口覆盖后变得可见。 |
| FocusIn | 小部件由于用户事件(如使用Tab键或鼠标点击)或在小部件上调用.focus_set()而获得输入焦点 |
| FocusOut | 焦点从小部件中移出。 |
| KeyPress/Key | 按键盘中按键。事件详细部分指定了哪个键。 |
| KeyRelease | 按下的键释放。 |
| Leave | 鼠标指针从小部件中移出。 |
| 地图 | 小部件被映射(显示)。例如,当你对一个小部件调用几何管理器时发生。 |
| Motion | 鼠标指针完全在小部件内移动。 |
| Un-map | 小部件取消映射(变为不可见)。例如,当你使用remove()方法时。 |
| 可见性 | 窗口的一部分变为可见。 |
事件修饰符
事件修饰符是创建事件绑定时的一个可选组件。以下列出了事件修饰符列表。然而,请注意,大多数事件修饰符是平台特定的,可能不会在所有平台上工作。
| 修饰符 | 描述 |
|---|---|
| Alt | 当按下 Alt 键时为真。 |
| 任何 | 通用事件类型。例如 <Any-KeyPress> 在任何键被按下时为真。 |
| 控制 | 当按下 Ctrl (控制) 键时为真。 |
| 双击 | 指定两个事件快速连续发生。例如,<Double-Button-1> 是鼠标按钮 1 的双击。 |
| 锁定 | 如果按下 Caps Lock/Shift 锁则为真 |
| Shift | 如果按下 Shift 键则为真 |
| 三击 | 与双击类似(三个事件快速连续发生) |
事件详情
事件详情是创建事件绑定时的一个可选组件。它们通常使用缩写为 keysym 的键符号表示鼠标按钮或键盘按键的详细信息。
| 所有可用事件详情列表如下:.keysym | .keycode | .keysym_num | 键 |
|---|---|---|---|
Alt_L |
64 | 65513 | 左 Alt 键 |
Alt_R |
113 | 65514 | 右 Alt 键 |
BackSpace |
22 | 65288 | Backspace |
Cancel |
110 | 65387 | 断开 |
Caps_Lock |
66 | 65549 | CapsLock |
Control_L |
37 | 65507 | 左 Ctrl 键 |
Control_R |
109 | 65508 | 右 Ctrl 键 |
Delete |
107 | 65535 | Delete |
Down |
104 | 65364 | 向下箭头键 |
End |
103 | 65367 | End |
Escape |
9 | 65307 | Esc |
Execute |
111 | 65378 | SysRq |
F1 – F11 |
67 to 95 | 65470 to 65480 | 功能键 F1 到 F11 |
F12 |
96 | 65481 | 功能键 F12 |
Home |
97 | 65360 | Home |
Insert |
106 | 65379 | Insert |
Left |
100 | 65361 | 左侧箭头键 |
Linefeed |
54 | 106 | 换行符/Ctrl + J |
KP_0 |
90 | 65438 | 键盘上的 0 |
KP_1 |
87 | 65436 | 键盘上的 1 |
KP_2 |
88 | 65433 | 键盘上的 2 |
KP_3 |
89 | 65435 | 键盘上的 3 |
KP_4 |
83 | 65430 | 键盘上的 4 |
KP_5 |
84 | 65437 | 键盘上的 5 |
KP_6 |
85 | 65432 | 键盘上的 6 |
KP_7 |
79 | 65429 | 键盘上的 7 |
KP_8 |
80 | 65431 | 键盘上的 8 |
KP_9 |
81 | 65434 | 键盘上的 9 |
KP_Add |
86 | 65451 | 键盘上的 + |
KP_Begin |
84 | 65437 | 键盘上的中心键(与键 5 相同) |
KP_Decimal |
91 | 65439 | 键盘上的小数 (.) 键 |
KP_Delete |
91 | 65439 | 键盘上的 Delete (Del) 键 |
KP_Divide |
112 | 65455 | 键盘上的 / |
KP_Down |
88 | 65433 | 键盘上的向下箭头键 |
KP_End |
87 | 65436 | 键盘上的 End |
KP_Enter |
108 | 65421 | 键盘上的 Enter |
KP_Home |
79 | 65429 | 键盘上的 Home |
KP_Insert |
90 | 65438 | 键盘上的 Insert |
KP_Left |
83 | 65430 | 键盘上的左箭头键 |
KP_Multiply |
63 | 65450 | 键盘上的 *** |
KP_Next |
89 | 65435 | 键盘上的 Page Down |
KP_Prior |
81 | 65434 | 键盘上的 Page Up |
KP_Right |
85 | 65432 | 键盘上的右箭头键 |
KP_Subtract |
82 | 65453 | 键盘上的 - |
KP_Up |
80 | 65431 | 键盘上的向上箭头键 |
Next |
105 | 65366 | Page Down |
Num_Lock |
77 | 65407 | Num Lock |
Pause |
110 | 65299 | Pause |
Print |
111 | 65377 | Prt Scr |
Prior |
99 | 65365 | Page Up |
Return |
36 | 65293 | Enter 键 / Ctrl + M |
Right |
102 | 65363 | 右箭头键 |
Scroll_Lock |
78 | 65300 | Scroll Lock |
Shift_L |
50 | 65505 | 左 Shift 键 |
Shift_R |
62 | 65506 | 右 Shift 键 |
Tab |
23 | 65289 | Tab 键 |
Up |
98 | 65362 | 向上箭头键 |
其他与事件相关的方法
使用 bind、bind_all、bind_class 和 tag_bind 可以在各个级别将处理程序绑定到事件。
如果将事件绑定注册到回调函数,则回调函数将使用事件作为其第一个参数被调用。事件参数具有以下属性:
| 属性 | 描述 | 适用于事件类型 |
|---|---|---|
event.serial |
事件的序列号。 | 所有 |
event.num |
按下的鼠标按钮。 | ButtonPress 和 ButtonRelease |
event.focus |
窗口是否有焦点。 | Enter 和 Leave |
event.height |
暴露窗口的高度。 | Configure 和 Expose |
event.width |
暴露窗口的宽度。 | Configure 和 Expose |
event.keycode |
按下的键的键码。 | KeyPress 和 KeyRelease |
event.state |
事件的状态作为数字。 | ButtonPress、ButtonRelease、Enter、KeyPress、KeyRelease、Leave 和 Motion |
event.state |
状态作为字符串。 | Visibility |
event.time |
事件发生的时间。 | 所有 |
event.x |
给出鼠标的 x 位置。 | 所有 |
event.y |
给出鼠标的 y 位置。 | 所有 |
event.x_root |
给出鼠标在屏幕上的 x 位置。 | ButtonPress、ButtonRelease、KeyPress、KeyRelease 和 Motion |
event.y_root |
给出鼠标在屏幕上的 y 位置。 | ButtonPress、ButtonRelease、KeyPress、KeyRelease 和 Motion |
event.char |
给出按下的字符。 | KeyPress 和 KeyRelease |
event.keysym |
以字符串形式给出事件的 keysym。 |
KeyPress 和 KeyRelease |
event.keysym_num |
给出事件的 keysym 作为数字。 |
KeyPress 和 KeyRelease |
event.type |
事件类型作为数字。 | 所有 |
event.widget |
发生事件的窗口小部件。 | 所有 |
event.delta |
滚轮移动的增量。 | MouseWheel |
可用光标列表
光标小部件选项允许 Tk 程序员更改特定小部件的鼠标光标。Tk 在所有平台上识别的光标名称是:
X_cursor |
arrow |
based_arrow_down |
based_arrow_up |
boat |
bogosity |
|---|---|---|---|---|---|
bottom_left_corner |
bottom_right_corner |
bottom_side |
box_spiral |
center_ptr |
circle |
clock |
coffee_mug |
cross |
cross_reverse |
crosshair |
diamond_cross |
dot |
dotbox |
double_arrow |
draft_large |
draft_small |
draped_box |
exchange |
fleur |
gobbler |
gumby |
hand1 |
hand2 |
heart |
icon |
iron_cross |
left_ptr |
left_side |
left_tee |
leftbutton |
ll_angle |
lr_angle |
man |
bottom_tee |
middlebutton |
mouse |
pencil |
pirate |
plus |
question_arrow |
right_ptr |
right_side |
right_tee |
rightbutton |
rtl_logo |
sailboat |
sb_down_arrow |
sb_h_double_arrow |
sb_left_arrow |
sb_right_arrow |
sb_up_arrow |
sb_v_double_arrow |
shuttle |
sizing |
spider |
spraycan |
star |
target |
tcross |
top_left_arrow |
top_left_corner |
top_right_corner |
top_side |
top_tee |
trek |
ul_angle |
umbrella |
ur_angle |
watch |
xterm |
查看 9.01 所有光标演示.py 以演示所有跨平台光标。
可携带性问题
-
Windows:在 Windows 上有原生映射的光标有,
arrow,center_ptr,crosshair,fleur,ibeam,icon,sb_h_double_arrow,sb_v_double_arrow,watch,和xterm。可用的以下附加光标有,
no,starting,size,size_ne_sw,size_ns,size_nw_se,size_we,uparrow,wait。可以指定
no光标来消除光标。 -
Mac OS X:在 Mac OS X 系统上有原生映射的光标有,
arrow,cross,crosshair,ibeam,plus,watch,xterm。可用的以下附加原生光标有,
copyarrow,aliasarrow,contextualmenuarrow,text,cross-hair,closedhand,openhand,pointinghand,resizeleft,resizeright,resizeleftright,resizeup,resizedown,resizeupdown,none,notallowed,poof,countinguphand,countingdownhand,countingupanddownhand,spinning。
基本小部件方法
这些方法在 Tkinter 模块的 Widget 类下提供。您可以使用以下命令在交互式 shell 中查看这些方法的文档:
>>> import Tkinter
>>>help(Tkinter.Widget)
Widgets 类下的可用方法如下:
| 方法 | 描述 |
|---|---|
after(self, ms, func=None, *args) |
在给定时间后调用函数一次。MS 指定时间为毫秒。FUNC给出要调用的函数。其他参数作为函数调用的参数给出。返回:使用after_cancel取消调度的标识符。 |
after_cancel(self, id) |
取消与 ID 标识的函数的调度。由 after 或after_idle返回的标识符必须作为第一个参数给出。 |
after_idle(self, func, *args) |
如果 Tcl 主循环没有事件要处理,则调用 FUNC 一次。返回一个标识符,用于使用after_cancel取消调度。 |
bbox = grid_bbox(self, column=None, row=None, col2=None, row2=None) |
返回由几何管理器 grid 控制的此小部件的边界框的整数坐标元组。如果提供了 COLUMN 和 ROW,则边界框适用于从行和列 0 的单元格到指定单元格的单元格。如果提供了 COL2 和 ROW2,则边界框从该单元格开始。返回的整数指定了主小部件中右上角的位置以及宽度和高度。 |
bind(self, sequence=None, func=None, add=None) |
在事件 SEQUENCE 上为此小部件绑定一个调用函数 FUNC。SEQUENCE 是连接事件模式的字符串。事件模式的形式为 <MODIFIER-MODIFIER-TYPE-DETAIL>。事件模式也可以是形式为 <<AString>> 的虚拟事件,其中 AString 可以是任意的。此事件可以通过 event_generate 生成。如果事件连接,它们必须彼此紧挨着。如果事件序列发生,并且以事件实例作为参数,则调用 FUNC。如果 FUNC 的返回值为 "break",则不会调用其他已绑定的函数。一个额外的布尔参数 ADD 指定 FUNC 是否将作为其他已绑定的函数调用,或者是否将替换先前的函数。bind 将返回一个标识符,允许使用 unbind 删除已绑定的函数,而不会发生内存泄漏。如果省略 FUNC 或 SEQUENCE,则返回已绑定的函数或已绑定的事件列表。 |
bind_all(self, sequence=None, func=None, add=None) |
在所有小部件上绑定事件 SEQUENCE,调用函数 FUNC。一个额外的布尔参数 ADD 指定 FUNC 是否将作为其他已绑定的函数调用,或者是否将替换先前的函数。参见 bind 了解返回值。 |
bind_class(self, className, sequence=None, func=None, add=None) |
在具有 bind 标签 CLASSNAME 的小部件上,在事件 SEQUENCE 上绑定一个调用函数 FUNC。一个额外的布尔参数 ADD 指定 FUNC 是否将作为其他已绑定的函数调用,或者是否将替换先前的函数。参见 bind 了解返回值。 |
bindtags(self, tagList=None) |
设置或获取此小部件的 bindtags 列表。如果没有参数,则返回与此小部件关联的所有 bindtags 列表。如果有字符串列表作为参数,则将 bindtags 设置为此列表。bindtags 决定了事件处理的顺序(参见 bind)。 |
cget(self, key) |
返回给定字符串形式的 Key 的资源值。 |
clipboard_append(self, string, **kw) |
将 String 添加到 Tk 剪贴板。关键字参数中指定的可选显示小部件指定了目标显示。可以通过 selection_get 获取剪贴板。 |
clipboard_clear(self, **kw) |
清除 Tk 剪贴板中的数据。关键字参数中指定的可选显示小部件指定了目标显示。 |
clipboard_get(self, **kw) |
从窗口的显示中检索剪贴板数据。窗口关键字默认为 Tkinter 应用程序的根窗口。类型关键字指定返回数据的形式,应为一个原子名称,例如 STRING 或 FILE_NAME。类型默认为 String。此命令等同于:selection_get(CLIPBOARD)。 |
columnconfigure = grid_columnconfigure(self, index, cnf={}, **kw) |
配置网格的 Index 列。有效的资源有 minsize(列的最小大小)、weight(额外空间传播到该列的程度)和 pad(额外空间)。 |
config = configure(self, cnf=None, **kw) |
配置窗口的资源。资源值作为关键字参数指定。要获取允许的关键字参数的概述,请调用方法 keys。 |
event_add(self, virtual, *sequences) |
将虚拟事件 virtual(形式为 <<Name>>)绑定到事件 sequence,使得虚拟事件在 SEQUENCE 发生时被触发。 |
event_delete(self, virtual, *sequences) |
从 sequence 解绑虚拟事件 virtual。 |
event_generate(self, sequence, **kw) |
生成事件 sequence。额外的关键字参数指定事件的参数(例如,x、y、rootx 和 rooty)。 |
event_info(self, virtual=None) |
返回所有虚拟事件列表或绑定到虚拟事件 virtual 的 sequence 的信息。 |
focus = focus_set(self) |
将输入焦点直接设置到这个小部件。如果应用程序当前没有焦点,并且通过窗口管理器获得焦点,则这个小部件将获得焦点。 |
focus_displayof(self) |
返回在当前小部件所在显示上具有焦点的窗口小部件。如果没有应用程序具有焦点,则返回 None。 |
focus_force(self) |
即使应用程序没有焦点,也将输入焦点直接设置到这个小部件。请谨慎使用! |
focus_get(self) |
返回当前在应用程序中具有焦点的窗口小部件。使用 focus_displayof 允许与多个显示一起工作。如果没有应用程序具有焦点,则返回 None。 |
focus_lastfor(self) |
返回如果此小部件的顶层获得窗口管理器的焦点,则将具有焦点的窗口小部件。 |
focus_set(self) |
将输入焦点直接设置到这个小部件。如果应用程序当前没有焦点,并且通过窗口管理器获得焦点,则这个小部件将获得焦点。 |
getboolean(self, s) |
对于作为参数给出的 Tclboolean 值 true 和 false,返回一个布尔值。 |
getvar(self, name='PY_VAR') |
返回 Tcl 变量 name 的值。 |
grab_current(self) |
返回在此应用程序中当前具有抓取的窗口小部件或 None。 |
grab_release(self) ) |
如果当前设置了抓取,则释放此小部件的抓取。 |
grab_set(self) |
为此小部件设置抓取。抓取将所有事件指向此小部件及其应用程序中的子小部件。 |
grab_set_global(self) |
为此小部件设置全局抓取。全局抓取将所有事件指向显示上的此小部件及其子小部件。请谨慎使用 - 其他应用程序不再接收事件。 |
grab_status(self) |
如果此小部件没有、局部或全局抓取,则返回 None、"local" 或 "global"。 |
grid_bbox(self, column=None, row=None, col2=None, row2=None) |
返回一个整数坐标元组,表示由网格管理器控制的此小部件的边界框。如果提供了 column 和 row,则边界框适用于从行和列 0 开始的指定单元格。如果提供了 col2 和 row2,则边界框从该单元格开始。返回的整数指定了主小部件中左上角的偏移量以及宽度和高度。 |
grid_columnconfigure(self, index, cnf={}, **kw) |
配置网格的列 index。有效的资源有 minsize(列的最小大小)、weight(额外空间传播到本列的程度)和 pad(额外留出的空间)。 |
grid_location(self, x, y) |
返回一个元组,表示列和行,用于标识主小部件中位置在 x 和 y 的像素所在的单元格。 |
grid_propagate(self, flag=['_noarg_']) |
设置或获取几何信息传播的状态。布尔参数指定是否由子小部件的几何信息确定此小部件的大小。如果没有给出参数,则返回当前设置。 |
grid_rowconfigure(self, index, cnf={}, **kw) |
配置网格的行 index。有效的资源有 minsize(行的最小大小)、weight(额外空间传播到本行的程度)和 pad(额外留出的空间)。 |
grid_size(self) |
返回网格中列和行的数量元组。 |
grid_slaves(self, row=None, column=None) |
返回一个列表,包含此小部件在其包装顺序中的所有子小部件。 |
image_names(self) |
返回所有现有图像名称列表。 |
image_types(self) |
返回所有可用图像类型列表(例如 photo bitmap)。 |
keys(self) |
返回此小部件的所有资源名称列表。 |
lift = tkraise(self, aboveThis=None) |
在堆叠顺序中提升此小部件。 |
lower(self, belowThis=None) |
在堆叠顺序中降低此小部件。 |
mainloop(self, n=0) |
调用 Tk 的 mainloop。 |
nametowidget(self, name) |
返回由其 Tcl 名称 NAME 标识的 Tkinter 实例的小部件。 |
option_add(self, pattern, value, priority=None) |
为选项模式 PATTERN(第一个参数)设置一个 value(第二个参数)。可选的第三个参数给出数字优先级(默认为 80)。 |
option_clear(self) |
清除选项数据库。如果调用 option_add,则将其重新加载。 |
option_get(self, name, className) |
返回此小部件具有 classname 的选项 NAME 的值。优先级较高的值覆盖较低优先级的值。 |
option_readfile(self, fileName, priority=None) |
将文件 filename 读取到选项数据库中。可选的第二个参数给出数字优先级。 |
propagate =pack_propagate(self, flag=['_noarg_']) |
设置或获取几何信息传播的状态。布尔参数指定奴隶的几何信息是否将决定此小部件的大小。如果没有给出参数,则返回当前设置。 |
pack_slaves(self) |
返回此小部件在其布局顺序中的所有奴隶列表。 |
quit(self) |
退出 Tcl 解释器。所有小部件将被销毁。 |
register = _register(self, func, subst=None, needcleanup=1) |
返回一个新创建的 Tcl 函数。如果调用此函数,Python 函数 func 将被执行。可以提供一个可选的函数 subst,它将在 func 执行之前执行。 |
rowconfigure = grid_rowconfigure(self, index, cnf={}, **kw) |
配置网格的 index 行。有效的资源有 minsize(行的最小大小)、weight(额外空间传播到本行的程度)和 pad(额外空间)。 |
selection_clear(self, **kw) |
清除当前的 X 选择。 |
selection_get(self, **kw) |
返回当前 X 选择的内容。关键字参数选择指定选择的名称,默认为 PRIMARY。关键字参数 display 指定要使用的显示上的小部件。 |
selection_handle(self, command, **kw) |
指定一个函数 command,如果此小部件拥有的 X 选择被另一个应用程序查询,则调用该函数。此函数必须返回选择的内 容。该函数将使用 OFFSET 和 LENGTH 参数调用,允许对非常长的选择进行分块。以下关键字参数可以提供:选择 - 选择的名称(默认 PRIMARY),类型 - 选择的类型(例如,string,FILE_NAME)。 |
selection_own(self, **kw) |
成为 X 选择的所有者。关键字参数选择指定选择的名称(默认 PRIMARY)。 |
selection_own_get(self, **kw) |
返回 X 选择的所有者。以下关键字参数可以提供:选择 - 选择的名称(默认 PRIMARY),类型 - 选择的类型(例如,STRING,FILE_NAME)。 |
send(self, interp, cmd, *args) |
将 Tcl 命令 CMD 发送到不同的解释器 INTERP 执行。 |
setvar(self, name='PY_VAR', value='1') |
将 Tcl 变量 NAME 设置为 VALUE。 |
size = grid_size(self) |
返回网格中列和行的元组。 |
slaves = pack_slaves(self) |
返回此小部件在其布局顺序中的所有奴隶列表。 |
tk_focusFollowsMouse(self) |
鼠标下的小部件将自动获得焦点。无法轻易禁用。 |
tk_focusNext(self) |
返回当前具有焦点的控件之后的下一个控件。焦点顺序首先指向下一个子控件,然后递归地指向子控件的子控件,最后指向堆叠顺序中较高的下一个兄弟控件。如果控件具有设置为 0 的 takefocus 资源,则该控件将被忽略。 |
tk_focusPrev(self) |
返回焦点顺序中的上一个控件。有关详细信息,请参阅tk_focusNext。 |
tk_setPalette(self, *args, **kw) |
为所有控件元素设置新的颜色方案。作为参数的单个颜色将导致 Tk 控件元素的所有颜色都由此颜色派生。或者,可以给出多个关键字参数及其关联的颜色。以下关键字是有效的:activeBackground、foreground、selectColor、activeForeground、highlightBackground、selectBackground、background、highlightColor、selectForeground、disabledForeground、insertBackground和troughColor。 |
tkraise(self, aboveThis=None) |
在堆叠顺序中提升此控件。 |
unbind(self, sequence, funcid=None) |
为此控件解绑事件 SEQUENCE 的由 FUNCID 标识的函数。 |
unbind_all(self, sequence)' |
为所有控件解绑事件 SEQUENCE 的所有函数。 |
unbind_class(self, className, sequence) |
解绑所有具有 bindtag classname的控件的事件 SEQUENCE 的所有函数。 |
update(self) |
进入事件循环,直到所有挂起的事件都被 Tcl 处理。 |
update_idletasks(self) |
进入事件循环,直到所有空闲回调都被调用。这将更新窗口的显示,但不会处理由用户引起的事件。 |
wait_variable(self, name='PY_VAR') |
等待直到变量被修改。必须给出类型为IntVar、StringVar、DoubleVar或BooleanVar的参数。 |
wait_visibility(self, window=None) |
等待直到一个 Widget 的可见性发生变化(例如,它出现)。如果没有给出参数,则使用 self。 |
wait_window(self, window=None) |
等待直到一个 Widget 被销毁。如果没有给出参数,则使用 self。 |
waitvar = wait_variable(self, name='PY_VAR') |
等待直到变量被修改。必须给出类型为IntVar、StringVar、DoubleVar或BooleanVar的参数。 |
winfo_atom(self, name, displayof=0) |
返回表示原子名称的整数。 |
winfo_atomname(self, id, displayof=0) |
返回具有标识符 ID 的原子名称。 |
winfo_cells(self) |
返回此控件颜色映射中的单元格数。 |
winfo_children(self) |
返回此控件的子控件列表。 |
winfo_class(self) |
返回此控件的窗口类名。 |
winfo_colormapfull(self) |
如果在最后的颜色请求中colormap已满,则返回 true。 |
winfo_containing(self, rootX, rootY, displayof=0) |
返回在根坐标 rootX、rootY处的控件。 |
winfo_depth(self) |
返回每像素的位数。 |
winfo_exists(self) |
如果此小部件存在,则返回 true。 |
winfo_fpixels(self, number) |
返回给定距离 NUMBER(例如:"3c")的像素数,以浮点数形式返回。 |
winfo_geometry(self) |
返回此小部件的几何字符串,格式为"widthxheight+X+Y"。 |
winfo_height(self) |
返回此小部件的高度。 |
winfo_id(self) |
返回此小部件的标识符 ID。 |
winfo_interps(self, displayof=0) |
返回此显示的所有 Tcl 解释器的名称。 |
winfo_ismapped(self) |
如果此小部件已映射,则返回 true。 |
winfo_manager(self) |
返回此小部件的窗口管理器名称。 |
winfo_name(self) |
返回此小部件的名称。 |
winfo_parent(self) |
返回此小部件的父级名称。 |
winfo_pathname(self, id, displayof=0) |
返回由 ID 指定的窗口的路径名。 |
winfo_pixels(self, num) |
winfo_fpixels 的舍入整数值。 |
winfo_pointerx(self) |
返回根窗口上指针的 x 坐标。 |
winfo_pointerxy(self) |
返回根窗口上指针的 x 和 y 坐标的元组。 |
winfo_pointery(self) |
返回根窗口上指针的 y 坐标。 |
winfo_reqheight(self) |
返回此小部件请求的高度。 |
winfo_reqwidth(self) |
返回此小部件请求的宽度。 |
winfo_rgb(self, color) |
返回此小部件中颜色color的红色、绿色、蓝色十进制值的元组。 |
winfo_rootx(self) / winfo_rooty(self) |
返回此小部件在根窗口上的左上角 x/y 坐标。 |
winfo_screen(self) |
返回此小部件的屏幕名称。 |
winfo_screencells(self) |
返回此小部件屏幕调色板中的单元格数。 |
winfo_screendepth(self) |
返回此小部件屏幕根窗口的每像素位数。 |
winfo_screenheight(self) |
返回此小部件屏幕高度(以像素为单位)的像素数。 |
winfo_screenmmheight(self) |
返回此小部件屏幕高度(以毫米为单位)的像素数。 |
winfo_screenmmwidth(self) |
返回此小部件屏幕宽度(以毫米为单位)的像素数。 |
winfo_screenwidth(self) |
返回此小部件屏幕宽度(以像素为单位)的像素数。 |
winfo_toplevel(self) |
返回此小部件的 Toplevel 小部件。 |
winfo_viewable(self) |
如果小部件及其所有更高祖先都已映射,则返回 true。 |
winfo_visual(self) = winfo_screenvisual(self) |
返回字符串之一directcolor、grayscale、pseudocolor、staticcolor、staticgray或truecolor,表示此小部件的colormodel。 |
winfo_visualid(self) |
返回此小部件视觉效果的 X 标识符。 |
winfo_visualsavailable(self, includeids=0) |
返回此小部件屏幕上所有可用的视觉效果的列表。 |
winfo_vrootheight(self) |
返回与此小部件关联的虚拟根窗口的高度(以像素为单位)。如果没有虚拟根窗口,则返回屏幕的高度。 |
winfo_vrootwidth(self) |
返回与此小部件关联的虚拟根窗口的宽度(以像素为单位)。如果没有虚拟根窗口,则返回屏幕的宽度。 |
winfo_vrootx(self) |
返回此小部件相对于屏幕根窗口的虚拟根的 x 偏移量。 |
winfo_vrooty(self) |
返回此小部件相对于屏幕根窗口的虚拟根的 y 偏移量。 |
winfo_width(self) |
返回此小部件的宽度。 |
winfo_x(self) |
返回此小部件在父窗口中的左上角 x 坐标。 |
winfo_y(self) |
返回此小部件在父窗口中的左上角 y 坐标。 |
ttk 小部件
ttk 小部件基于 TIP #48 (tip.tcl.tk/48) 指定的改进和增强版本的风格引擎。
文件:path\to\python27\\lib\lib-tk\ttk.py
基本思想是在尽可能的程度上将实现小部件行为的代码与实现其外观的代码分开。小部件类绑定主要负责维护小部件状态和调用回调,而小部件外观的所有方面都位于主题之下。
您可以将一些 Tkinter 小部件替换为其相应的 ttk 小部件(按钮、复选框、输入框、框架、标签、标签框架、菜单按钮、分割窗口、单选按钮、滑块和滚动条)。
然而,Tkinter 和 ttk 小部件并不完全兼容。主要区别是 Tkinter 小部件的样式选项(如 fg、bg、relief 等)不是 ttk 小部件的支持选项。这些样式选项被移动到 ttk.Style()。
这里是一个小的 Tkinter 代码示例:
Label(text="Who", fg="white", bg="black")
Label(text="Are You ?", fg="white", bg="black")
以下是它的等价 ttk 代码:
style = ttk.Style()
style.configure("BW.TLabel", foreground="white", background="black")
ttk.Label(text="Who", style="BW.TLabel")
ttk.Label(text="Are You ?", style="BW.TLabel")
ttk 还提供了六个新的小部件类,这些类在 Tkinter 中不可用。这些是 Combobox、Notebook、Progressbar、Separator、Sizegrip 和 Treeview。
ttk 风格名称如下:
| 小部件类 | 样式名称 |
|---|---|
Button |
TButton |
Checkbutton |
TCheckbutton |
Combobox |
TCombobox |
Entry |
TEntry |
Frame |
TFrame |
Label |
TLabel |
LabelFrame |
TLabelFrame |
Menubutton |
TMenubutton |
Notebook |
TNotebook |
PanedWindow |
TPanedwindow(注意窗口名称不区分大小写!) |
Progressbar |
Horizontal.TProgressbar 或 Vertical.TProgressbar,根据 orient 选项。 |
Radiobutton |
TRadiobutton |
Scale |
Horizontal.TScale 或 Vertical.TScale,根据 orient 选项。 |
Scrollbar |
Horizontal.TScrollbar 或 Vertical.TScrollbar,根据 orient 选项。 |
Separator |
TSeparator |
Sizegrip |
TSizegrip |
Treeview |
Treeview (注意只有一个 'T',表示不是 TTreview!) |
所有 ttk 小部件都有的选项如下:
| 选项 | 描述 |
|---|---|
class |
指定窗口类。当查询选项数据库以获取窗口的其他选项、确定窗口的默认 bindtags 以及选择小部件的默认布局和样式时使用该类。这是一个只读选项,只能在创建窗口时指定。 |
cursor |
指定小部件显示的鼠标光标 |
takefocus |
确定窗口在键盘遍历期间是否接受焦点。返回 0、1 或空字符串。如果为 0,则在键盘遍历期间应完全跳过窗口。如果为 1,则只要窗口是可见的,它就应该接收输入焦点。空字符串表示遍历脚本将决定是否将焦点放在窗口上。 |
style |
可用于指定自定义小部件样式。 |
所有可滚动 ttk 小部件接受的选项如下:
| 选项 | 描述 |
|---|---|
xscrollcommand |
用于与水平滚动条通信。当小部件窗口中的视图发生变化时,小部件将基于 scrollcommand 生成一个 Tcl 命令。通常,此选项由某个滚动条的 Scrollbar.set() 方法组成。这将导致滚动条在窗口中的视图发生变化时更新。 |
yscrollcommand |
垂直滚动条的命令。 |
ttk.Widget 类的方法及其描述如下:
| 方法 | 描述 |
|---|---|
identify(self, x, y) |
返回 x, y 位置处的元素名称,如果点不在任何元素内,则返回空字符串。x 和 y 是相对于小部件的像素坐标。 |
instate(self, statespec, callback=None, *args, **kw) |
测试小部件的状态。如果没有指定回调函数,如果小部件状态与 statespec 匹配则返回 True,否则返回 False。如果指定了回调函数,则当小部件状态与 statespec 匹配时,将使用 *args 和 **kw 调用它。statespec 预期是一个序列。 |
state(self, statespec=None) |
修改或查询小部件状态。如果 statespec 为 None,则返回小部件状态,否则根据 statespec 标志设置状态,然后返回一个新的状态规范,指示哪些标志已更改。statespec 预期是一个序列。 |
我们在此处不会显示所有 ttk 小部件特定选项。要获取 ttk 小部件可用选项的列表,请使用帮助命令。
要获取任何 ttk 小部件/类的帮助,请使用以下命令将 ttk 导入命名空间:
>>>import ttk
然后,可以使用以下命令获取特定小部件的信息:
| 小部件名称 | 获取帮助 |
|---|---|
| Label | help(ttk.Label) |
| Button | help(ttk.Button) |
| CheckButton | help(ttk.Checkbutton) |
| Entry | help(ttk.Entry) |
| Frame | help(ttk.Frame) |
| LabelFrame | help(ttk.LabelFrame) |
| Menubutton | help(ttk.Menubutton) |
| OptionMenu | help(ttk.OptionMenu) |
| PanedWindow | help(ttk.PanedWindow) |
| 单选按钮 | 帮助(ttk.Radiobutton) |
| 滚动条 | 帮助(ttk.Scale) |
| 滚动条 | 帮助(ttk.Scrollbar) |
| 组合框 | 帮助(ttk.Combobox) |
| 笔记本 | 帮助(ttk.Notebook) |
| 进度条 | 帮助(ttk.Progressbar) |
| 分隔符 | 帮助(ttk.Separator) |
| 大小调整手柄 | 帮助(ttk.Sizegrip) |
| 树形视图 | 帮助(ttk.Treeview) |
以下是一些 ttkVirtual 事件及其触发情况:
| 虚拟事件 | 触发时 |
|---|---|
<<ComboboxSelected>> |
用户从 Combobox 小部件的值列表中选择了一个元素 |
<<NotebookTabChanged>> |
在 Notebook 小部件中选择了新标签页 |
<<TreeviewSelect>> |
Treeview 小部件中的选择发生变化。 |
<<TreeviewOpen>> |
在将焦点项设置为打开 = True 前立即。 |
<<TreeviewClose>> |
在将焦点项设置为打开 = False 后立即。 |
ttk 中的每个小部件都分配了一个样式,该样式指定了组成小部件的元素集合以及它们的排列方式,以及元素选项的动态和默认设置。
默认情况下,样式名称与小部件的类名相同,但可能被小部件的样式选项覆盖。如果小部件的类名未知,请使用方法 Misc.winfo_class() (somewidget.winfo_class())。以下是一些 ttk 样式的方法及其描述:
| 方法 | 描述 |
|---|---|
configure(self, style, query_opt=None, **kw) |
查询或设置样式中指定选项的默认值。kw 中的每个键都是一个选项,每个值是标识该选项值的字符串或序列。 |
element_create(self, elementname, etype, *args, **kw) |
在给定的 etype 当前主题中创建一个新元素。 |
element_names(self) |
返回当前主题中定义的元素列表。 |
element_options(self, elementname) |
返回 elementname 选项的列表。 |
layout(self, style, layoutspec=None) |
定义给定样式的小部件布局。如果省略 layoutspec,则返回给定样式的布局规范。layoutspec 预期是一个列表或一个不同于 None 的对象,如果想要“关闭”该样式,则该对象应评估为 False。如果它是一个列表(或元组,或其他),则每个项应是一个元组,其中第一个项是布局名称,第二个项应具有以下描述的格式 |
布局可以是 None,如果它不包含选项,或者是一个选项字典,指定如何排列元素。布局机制使用简化版的 pack 几何管理器:给定一个初始内腔,每个元素都被分配一个包裹。
| 有效选项:值 | 描述 |
|---|---|
side: whichside |
指定放置元素的内腔的哪一侧;top、right、bottom 或 left 之一。如果省略,则元素占据整个内腔。 |
sticky: nswe |
指定元素在其分配的包裹内的放置位置。 |
children: [sublayout... ] |
指定要放置在元素内部的元素列表。每个元素是一个元组(或其他序列),其中第一个项目是布局名称,其余的是布局。 |
lookup(self, style, option, state=None, default=None) |
返回在样式中对选项指定的值。如果指定了状态,则它应是一个包含一个或多个状态的序列。如果设置了默认参数,则在找不到选项的指定时,它用作回退值。 |
map(self, style, query_opt=None, **kw) |
查询或设置指定选项(s)在样式中的动态值。kw 中的每个键是一个选项,每个值应是一个列表或元组(通常是),其中包含按元组、列表或其他您偏好的方式分组的 statespecs。statespec 是由一个或多个状态和值组成的复合体。 |
theme_create(self, themename, parent=None, settings=None) |
创建一个新的主题。如果 themename 已经存在,则是一个错误。如果指定了 parent,则新主题将从指定的父主题继承样式、元素和布局。如果存在设置,则它们应使用与 theme_settings 相同的语法。 |
theme_names(self) |
返回所有已知主题的列表。 |
theme_settings(self, themename, settings) |
临时将当前主题设置为 themename,应用指定的设置,然后恢复先前的主题。settings 中的每个键是一个样式,每个值可能包含 configure、map、layout 和 element create 的键,并且它们应具有与 configure、map、layout 和 element_create 方法指定的相同格式。 |
theme_use(self, themename=None) |
如果 themename 为 None,则返回正在使用的主题;否则,将当前主题设置为 themename,刷新所有小部件并发出 <<ThemeChanged>> 事件。 |
Toplevel 窗口方法
这些方法使与窗口管理器通信成为可能。它们在根窗口(Tk)和 Toplevel 实例上都是可用的。
注意,不同的窗口管理器表现不同。例如,一些窗口管理器不支持图标窗口;一些不支持窗口组,等等。
aspect = wm_aspect(self, minNumer=None, minDenom=None, maxNumer=None, maxDenom=None) |
指示窗口管理器将此小部件的纵横比(宽度/高度)设置为介于 minNumer/minDenom 和 maxNumer/maxDenom 之间。如果没有提供参数,则返回实际值的元组。 |
|---|---|
attributes = wm_attributes(self, *args) |
此子命令返回或设置平台特定的属性。第一种形式返回平台特定标志及其值的列表。第二种形式返回特定选项的值。第三种形式设置一个或多个值。值如下:在 Windows 上,-disabled 获取或设置窗口是否处于禁用状态。-toolwindow 获取或设置窗口转换为工具窗口的样式(如 MSDN 中定义)。-topmost 获取或设置此窗口是否为顶层窗口(显示在其他所有窗口之上)。在 Macintosh 上,XXXXX在 Unix 上,目前没有特殊的属性值。 |
client = wm_client(self, name=None) |
将名称存储在此小部件的 WM_CLIENT_MACHINE 属性中。返回当前值。 |
colormapwindows = wm_colormapwindows(self, *wlist) |
将窗口名称列表(wlist)存储在此小部件的 WM_COLORMAPWINDOWS 属性中。此列表包含与父窗口 colormaps 不同的窗口。如果 wlist 为空,则返回当前小部件列表。 |
command = wm_command(self, value=None) |
在 WM_COMMAND 属性中存储 value。这是用于调用应用程序的命令。如果 value 为 None,则返回当前命令。 |
deiconify = wm_deiconify(self) |
deiconify 此小部件。如果它从未映射,则不会映射。在 Windows 上,它将提升此小部件并使其获得焦点。 |
focusmodel = wm_focusmodel(self, model=None) |
将焦点模型设置为 model,"active" 表示此小部件将自行请求焦点,"passive" 表示窗口管理器应提供焦点。如果 model 为 None,则返回当前焦点模型。 |
frame = wm_frame(self) |
如果存在,则返回此小部件装饰框架的标识符。 |
geometry = wm_geometry(self, newGeometry=None) |
将 geometry 设置为 newgeometry,其形式为 =widthxheight+x+y。如果未提供,则返回当前值。 |
grid = wm_grid(self, baseWidth=None, baseHeight=None, widthInc=None, heightInc=None) |
指示窗口管理器,此小部件只能在网格边界上调整大小。widthInc 和 heightInc 是网格单元的宽度和高度(以像素为单位)。baseWidth 和 baseHeight 是在 Tk_GeometryRequest 中请求的网格单元数。 |
group = wm_group(self, pathName=None) |
将相关小部件的组领导者小部件设置为 pathName。如果未提供,则返回此小部件的组领导者。 |
iconbitmap = wm_iconbitmap(self, bitmap=None, default=None) |
将图标化小部件的位图设置为 BITMAP。如果未提供,则返回位图。在 Windows 上,可以使用 DEFAULT 参数设置小部件及其未显式设置图标的任何后代的图标。DEFAULT 可以是到 .ico 文件的相对路径(例如:root.iconbitmap(default='myicon.ico'))。有关更多信息,请参阅 Tk 文档。 |
iconify = wm_iconify(self) |
将小部件显示为图标。 |
iconmask = wm_iconmask(self, bitmap=None) |
设置此小部件图标位图的掩码。如果未提供,则返回掩码。 |
iconname = wm_iconname(self, newName=None) |
设置此小部件图标的名称。如果未提供,则返回名称。 |
iconposition = wm_iconposition(self, x=None, y=None) |
将此小部件图标的 X 和 Y 位置设置为 X 和 Y。如果未提供,则返回 X 和 Y 的当前值。 |
iconwindow = wm_iconwindow(self, pathName=None) |
将小部件pathName设置为显示图标。如果未提供,则返回当前值。 |
maxsize = wm_maxsize(self, width=None, height=None) |
设置此小部件的最大width和height。如果窗口是网格化的,则值以网格单位给出。如果未提供,则返回当前值。 |
minsize = wm_minsize(self, width=None, height=None) |
设置此小部件的最小width和height。如果窗口是网格化的,则值以网格单位给出。如果未提供,则返回当前值。 |
overrideredirect = wm_overrideredirect(self, boolean=None) |
指示窗口管理器在布尔值为 1 时忽略此小部件。如果未提供,则返回当前值。 |
positionfrom = wm_positionfrom(self, who=None) |
指示窗口管理器,如果who为"user",则由用户定义此小部件的位置,如果who为"program",则由其自身策略定义。 |
protocol = wm_protocol(self, name=None, func=None) |
将函数func绑定到此小部件的命令name。如果未提供,则返回绑定到name的函数。name可以是例如WM_SAVE_YOURSELF或WM_DELETE_WINDOW。 |
resizable = wm_resizable(self, width=None, height=None) |
指示窗口管理器是否可以在width或height中调整此宽度的大小。两个值都是布尔值。 |
sizefrom = wm_sizefrom(self, who=None) |
指示窗口管理器,如果who为"user",则由用户定义此小部件的大小,如果who为"program",则由其自身策略定义。 |
state = wm_state(self, newstate=None) |
查询或设置此小部件的状态,可以是正常、图标、图标化(参见wm_iconwindow)、撤回或缩放(仅限 Windows)。 |
title = wm_title(self, string=None) |
设置此小部件的标题。 |
transient = wm_transient(self, master=None) |
指示窗口管理器,此小部件相对于小部件master是瞬时的。 |
withdraw = wm_withdraw(self) |
将此小部件从屏幕上撤回,使其未映射并被窗口管理器遗忘。使用wm_deiconify重新绘制它。 |
wm_aspect(self, minNumer=None, minDenom=None, maxNumer=None, maxDenom=None) |
指示窗口管理器将此小部件的宽高比(宽度/高度)设置为介于minNumer/minDenom和maxNumer/maxDenom之间。如果没有提供参数,则返回实际值。 |
wm_attributes(self, *args) |
此子命令返回或设置平台特定的属性。第一种形式返回平台特定标志及其值的列表。第二种形式返回特定选项的值。第三种形式设置一个或多个值。值如下:在 Windows 上,-disabled 获取或设置窗口是否处于禁用状态。-toolwindow 获取或设置窗口到工具窗口的样式(如 MSDN 中定义)。-topmost 获取或设置此是否为顶层窗口(显示在其他所有窗口之上)。在 Macintosh 上,XXXXX。在 Unix 上,目前没有特殊的属性值。 |
wm_client(self, name=None) |
将 name 存储在此小部件的 WM_CLIENT_MACHINE 属性中。返回当前值。 |
wm_colormapwindows(self, *wlist) |
将窗口名称列表(wlist)存储在此小部件的 WM_COLORMAPWINDOWS 属性中。此列表包含与父窗口 colormaps 不同的窗口。如果 wlist 为空,则返回当前小部件的列表。 |
wm_command(self, value=None) |
将 value 存储在 WM_COMMAND 属性中。这是用于调用应用程序的命令。如果 value 为 None,则返回当前命令。 |
wm_deiconify(self) |
取消图标化此小部件。如果它从未被映射,则不会进行映射。在 Windows 上,它将提升此小部件并使其获得焦点。 |
wm_focusmodel(self, model=None) |
将焦点模型设置为 model。"active" 表示此小部件将自行请求焦点,"passive" 表示窗口管理器应提供焦点。如果 model 为 None,则返回当前焦点模型。 |
wm_frame(self) |
如果存在,则返回此小部件装饰框架的标识符。 |
wm_geometry(self, newGeometry=None) |
将 geometry 设置为 newgeometry 的形式 =widthxheight+x+y。如果未提供 None,则返回当前值。 |
wm_grid(self, baseWidth=None, baseHeight=None, widthInc=None, heightInc=None) |
指示窗口管理器,此小部件只能在网格边界上调整大小。widthInc 和 heightInc 是像素中网格单元的宽度和高度。baseWidth 和 baseHeight 是在 Tk_GeometryRequest 中请求的网格单元数。 |
wm_group(self, pathName=None) |
将相关小部件的组领导者小部件设置为 pathname。如果未提供 None,则返回此小部件的组领导者。 |
wm_iconbitmap(self, bitmap=None, default=None) |
将图标化小部件的位图设置为 bitmap。如果未提供 None,则返回位图。在 Windows 上,可以使用 default 参数设置小部件及其未显式设置图标的任何后代的图标。DEFAULT 可以是 .ico 文件的相对路径(例如:root.iconbitmap(default='myicon.ico'))。有关更多信息,请参阅 Tk 文档。 |
wm_iconify(self) |
将小部件显示为图标。 |
wm_iconmask(self, bitmap=None) |
设置此小部件图标位图的掩码。如果未提供 None,则返回掩码。 |
wm_iconname(self, newName=None) |
设置这个小部件图标的名称。如果未提供None,则返回名称。 |
wm_iconposition(self, x=None, y=None) |
将这个小部件图标的 X 和 Y 位置设置为 X 和 Y。如果未提供None,则返回 X 和 Y 的当前值。 |
wm_iconwindow(self, pathName=None) |
将小部件pathname设置为显示图标,而不是图标本身。如果未提供None,则返回当前值。 |
wm_maxsize(self, width=None, height=None) |
设置这个小部件的最大width和height。如果窗口是网格化的,则这些值以网格单位给出。如果未提供None,则返回当前值。 |
wm_minsize(self, width=None, height=None) |
设置这个小部件的最小width和height。如果窗口是网格化的,则这些值以网格单位给出。如果未提供None,则返回当前值。 |
wm_overrideredirect(self, boolean=None) |
指示窗口管理器,如果提供了布尔值 1,则忽略这个小部件。如果未提供None,则返回当前值。 |
wm_positionfrom(self, who=None) |
指示窗口管理器,如果who是"用户",则由用户定义这个小部件的位置,如果who是"程序",则由其自己的策略定义。 |
wm_protocol(self, name=None, func=None) |
将函数func绑定到这个小部件的命令name。如果未提供None,则返回绑定到name的函数。名称可以是例如WM_SAVE_YOURSELF或WM_DELETE_WINDOW。 |
wm_resizable(self, width=None, height=None) |
指示窗口管理器是否允许在width或height中调整这个小部件的大小。两个值都是布尔值。 |
wm_sizefrom(self, who=None) |
指示窗口管理器,如果who是"用户",则由用户定义这个小部件的大小,如果who是"程序",则由其自己的策略定义。 |
wm_state(self, newstate=None) |
查询或设置这个小部件的状态为normal、icon、iconic(见wm_iconwindow)、withdrawn或zoomed(仅限 Windows)。 |
wm_title(self, string=None) |
设置这个小部件的标题。 |
wm_transient(self, master=None) |
指示窗口管理器,这个小部件相对于小部件master是临时的。 |
wm_withdraw(self) |
将这个小部件从屏幕上移除,使其未被映射且被窗口管理器遗忘。使用wm_deiconify重新绘制它。 |
















浙公网安备 33010602011771号