使用组件创建基本表单
好消息!你的设计已经过主任的审阅并获得批准。现在是时候开始实施了!在本章中,我们将创建一个非常简单的应用程序,它只提供规范中的核心功能,其他一概没有。这被称为最小可行性产品(MVP)。MVP将不具备生产就绪性,但它会给我们一些可以向用户展示的东西,并帮助我们更好地理解问题和我们正在使用的技术。我们将通过以下主题来介绍这一点:
- 在“Ttk小部件集”中,我们将了解Tkinter的一个更好的小部件集Ttk。
- 在“实现应用程序”中,我们将使用Python、Tkinter和Ttk来构建我们的表单设计。
让我们开始编码吧!
TTK 组件集合
在 第1章《Tkinter简介》 中,我们使用默认的Tkinter小部件创建了一个调查应用程序。这些小部件功能完善,仍在许多Tkinter应用程序中使用,但现代的Tkinter应用程序更倾向于使用一组改进的小部件,称为Ttk。Ttk是Tkinter的一个子模块,提供了许多(但并非全部)Tkinter小部件的主题版本。这些小部件与传统小部件大多相同,但提供了更高级的样式选项,旨在在Windows、macOS和Linux上看起来更现代、更自然。
在每个平台上,Ttk都包含特定于平台的主题,这些主题模仿了该平台的原生小部件。此外,Ttk还添加了一些额外的小部件,提供了默认库中没有的功能。
Tip
尽管本章将介绍Ttk小部件的基本用法,但有关Ttk小部件的字体、颜色和其他样式自定义的完整内容,请参阅第9章《使用样式和主题改善外观》。
Ttk已经作为Tkinter的一部分包含在内,因此我们不需要安装任何额外的东西。要在我们的Tkinter应用程序中使用Ttk小部件,我们需要这样导入Ttk:
python from tkinter import ttk 在本节中,我们将更深入地了解在我们的应用程序中有用的Ttk小部件。根据我们的设计,记得我们的应用程序需要以下类型的小部件:
- 标签(Labels)
- 日期输入(Date entry)
- 文本输入(Text entry)
- 数字输入(Number entry)
- 复选框(Check boxes)
- 单选按钮(Radio buttons)
- 选择列表(Select list)
- 长文本输入(Long text entry)
- 按钮(Buttons)
- 带标题的框架(Boxed frames with headers)
让我们看看我们可以使用哪些Ttk小部件来满足这些需求。
Label
在第1章《Tkinter简介》中,我们很好地使用了Tkinter的标签(Label)小部件,而Ttk版本的标签小部件基本相同。我们可以这样创建一个:
1 |
|
这将生成一个看起来像这样的标签:
Ttk的标签(Label)小部件与Tk版本的标签小部件共享大多数相同的选项,其中最常用的选项如下:
参数名称 | 类型 | 描述 |
---|---|---|
text |
String |
标签的文本内容 |
textvariable |
StringVar |
与标签内容绑定的变量 |
anchor |
Cardinal direction | 文本相对于内部边距的位置 |
justify |
left , right , or center |
文本行的相对对齐方式 |
foreground |
Color string | 文本的颜色 |
wraplength |
Integer |
文本换行前的像素数 |
underline |
Integer |
要下划线的文本中字符的索引 |
font |
Font string or tuple | 要使用的字体 |
注意,标签的文本可以直接使用text
参数指定,也可以绑定到一个StringVar
变量,以实现标签文本的动态更新。underline
参数允许在标签文本中为单个字符添加下划线;这对于指示用户按键绑定很有用,例如,激活由该标签标记的控制小部件。这个参数实际上并不会创建按键绑定;它仅仅是视觉上的效果。我们将在第10章《保持跨平台兼容性》中学习如何创建按键绑定。
Entry
ttk.Entry
小部件是一个简单的单行文本输入框,与Tkinter版本的输入框一样。它看起来像这样:
我们可以使用以下代码创建一个Entry小部件:
1 |
|
参数 | 描述 |
---|---|
textvariable |
StringVar ,Tkinter控件变量,用于绑定 |
show |
String ,用户输入时显示的字符或字符串,例如用于密码字段 |
justify |
left , right , or center ,文本在输入框中的对齐方式,默认为left |
foreground |
Color string,文本颜色 |
在未来的章节中,随着我们深入探索Ttk小部件的功能,我们将学习Entry小部件的更多选项。Entry小部件将被用于我们所有的文本输入框,以及日期字段。Ttk并没有专门的日期小部件,但我们将在第5章《通过验证和自动化减少用户错误》中学习如何将我们的Entry小部件转变为一个可以处理日期的输入框。
Spinbox
与Tkinter版本一样,Ttk的Spinbox
在标准Entry
小部件上添加了增加和减少按钮,使其适用于数值数据。
Ttk的Spinbox如下所示:
我们可以像这样创建一个Spinbox:
1 2 3 4 5 6 |
|
如这段代码所示,Ttk的Spinbox接受多个参数来控制其箭头按钮的行为,这些参数列在下表中:
参数名称 | 类型 | 描述 |
---|---|---|
from_ |
Float 或 Int | 箭头将递减到的最小值 |
to |
Float 或 Int | 箭头将递增到的最大值 |
increment |
Float 或 Int | 箭头将增加或减少的值 |
command |
Python function | 当任一按钮被按下时要执行的回调 |
textvariable |
Control variable | 绑定到字段值的控制变量 |
values |
List of strings 或 numbers | 按钮将滚动浏览的选择集。覆盖from_ 和to 值。 |
请注意,这些参数并不会限制可以输入到Spinbox中的内容;它们仅影响箭头的行为。此外,要注意,如果你只指定了from_
或to
中的一个,另一个会自动默认为0。这可能导致意外行为;例如,如果你设置了from_=1
而没有指定to
,那么to
将默认为0,你的箭头将仅在1和0之间切换。要显式设置无限制,可以使用from_='-infinity'
和to='infinity'
。
Spinbox小部件不仅限于数字使用,尽管这将是我们的主要用途。如你所见,它还可以接受一个values
参数,这是一个字符串或数字的列表,可以使用箭头按钮滚动浏览。因此,Spinbox可以绑定到任何类型的控制变量,而不仅仅是IntVar
或DoubleVar
变量。
Tip
请记住,这些参数实际上都不会限制可以输入到Spinbox小部件中的内容。它实际上只是一个带有附加按钮的Entry小部件,你不仅可以输入超出有效范围的数值,还可以输入字母和符号。如果你已将该小部件绑定到非字符串变量,这样做可能会导致异常。在第5章《通过验证和自动化减少用户错误》中,我们将学习如何使Spinbox小部件仅限制输入有效的数字字符。
Checkbutton
Ttk的Checkbutton小部件是一个带标签的复选框,非常适合用于输入布尔数据。可以这样创建:
1 2 3 4 5 6 |
|
除了上面列出的参数外,Checkbutton小部件还可以接受多个其他参数,如下表所示:
参数 | 值 | 描述 |
---|---|---|
variable |
可控变量 | 绑定复选框选中/未选中状态的变量 |
text |
字符串 | 标签文本 |
textvariable |
StringVar |
绑定标签文本的变量 |
command |
Python函数 | 当复选框被选中或未选中时执行的回调函数 |
onvalue |
任意 | 当复选框被选中时设置变量的值 |
offvalue |
任意 | 当复选框未被选中时设置变量的值 |
underline |
整数 | text 中下划线的字符索引 |
Checkbutton 中包含的标签可以直接使用 text 参数设置,或者可以使用 textvariable 将其绑定到一个控制变量。这允许对控件进行动态标签设置,在许多情况下都非常有用。
尽管 Checkbutton 非常适合布尔数据,并且默认将其绑定的变量设置为 True 或 False,但我们可以通过 onvalue 和 offvalue 参数覆盖此行为,使其能够与任何类型的控制变量一起使用。
例如,我们可以将其与 DoubleVar 一起使用,如下所示:
1 2 3 4 5 6 7 8 |
|
Ttk Checkbutton 将标签放置在复选框的右侧,如以下截图所示:
Radiobutton
与 Tkinter 中的对应控件类似,Ttk 单选按钮控件用于在一组互斥选项中进行选择。单独一个单选按钮控件并不是特别有用;相反,它们通常是成组创建的,如下所示:
以下代码展示了如何创建这些按钮:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
要分组单选按钮控件,只需为它们分配相同的控制变量,然后为每个按钮添加一个不同的值。在我们的示例中,我们还将它们放在同一个父控件上进行分组,但这仅仅是为了视觉上的原因,并不是严格必要的。
这个表格展示了一些你可以与单选按钮一起使用的各种参数:
参数 | 值 | 描述 |
---|---|---|
variable |
控制变量 | 一个绑定到按钮选中状态的变量 |
value |
任何 | 当按钮被选中时设置变量的值 |
command |
Python函数 | 当按钮被点击时执行的回调函数 |
text |
字符串 | 连接到单选按钮的标签 |
textvariable |
StringVar |
绑定到按钮标签文本的变量 |
underline |
整数 | 文本中需要下划线的字符的索引 |
Combobox
在第1章《Tkinter简介》中,我们了解了在不同选项之间进行选择的几种选项:Listbox
和OptionMenu
控件。Ttk为此目的提供了一个新控件:Combobox
。Combobox
控件是一个Entry
控件,增加了一个下拉列表框。它不仅允许使用鼠标选择,还允许键盘输入。尽管在某些方面,OptionMenu
似乎更适合我们的应用程序,但我们将利用Combobox
控件的键盘功能来构建一个更优秀的下拉控件。
我们可以这样创建一个Combobox
控件:
1 2 3 4 |
|
运行这段代码后,我们将得到一个如下所示的组合框:
请注意,虽然我们可以指定一个可能的值列表来填充下拉列表框,但Combobox
控件并不限于这些值。用户可以在框中输入他们想要的任何文本,并且绑定的变量将相应地更新。默认情况下,Combobox
不适合那些必须严格限制在固定列表中的值;但是,在第5章“通过验证和自动化减少用户错误”中,我们将学习如何解决这个问题。
这个表格展示了一些与Combobox
一起使用的常见参数:
参数 | 值 | 描述 |
---|---|---|
textvariable |
StringVar |
和Combobox 内容绑定的参数变量。 |
values |
List of strings | 下拉菜单中要填充的值。 |
postcommand |
Python函数 | 在listbox 显示之前执行的回调函数。 |
justify |
left ,right 或者center |
框中文本的对齐方式 |
Text
我们在第1章《Tkinter简介》中已经介绍过的Text小部件,是我们将要使用的唯一一个没有Ttk版本的小部件。虽然这个小部件最常用于多行文本输入,但它实际上提供的功能远不止于此。Text小部件可以用来显示或编辑包含图像、多色文本、超链接样式的可点击文本以及更多内容的文本。
我们可以按以下方式将其添加到应用程序中:
1 2 3 4 5 6 |
|
Text
小部件有大量的参数可以指定,以控制其外观和行为。其中一些比较有用的参数列在了这个表中:
参数 | 值 | 描述 |
---|---|---|
height |
Integer |
小部件的高度,以文本行数为单位。 |
width |
Integer |
小部件的宽度,以字符数为单位。对于可变宽度字体,使用“0”字符的宽度来计算宽度。 |
undo |
Boolean |
激活或停用撤销功能。撤销和重做操作使用平台的默认快捷键激活。 |
maxundo |
Integer |
将存储用于撤销的最大编辑次数。 |
wrap |
none , char 或者word |
指定当一行文本超过小部件的宽度时如何断行和换行。 |
spacing1 |
Integer |
在每行完整文本上方填充的像素数。 |
spacing2 |
Integer |
在显示的换行文本行之间填充的像素数。 |
spacing3 |
Integer |
在每行完整文本下方填充的像素数。 |
Text
小部件的更高级的可视化配置是通过Tags
来实现的。
我们将在第9章“使用样式和主题改善外观”中讨论标签。
文本小部件索引
请记住,文本小部件不能绑定到控制变量;要访问、设置或清除其内容,我们需要分别使用其get()
、insert()
和delete()
方法。
使用这些方法进行读取或修改时,需要传入一个或两个索引值来选择要操作的字符或字符范围。这些索引值是字符串,可以采用以下任何格式:
-
用点分隔的行号和字符号。行从1开始编号,字符从0开始编号,因此第一行的第一个字符是1.0,而第四行的第十二个字符是4.11。请注意,行是由换行符的存在来确定的;对于索引目的,换行的文本行仍仅被视为一行。
-
字符串字面量
end
,或Tkinter常量END
,表示文本的末尾。 - 一个数字索引加上
linestart
、lineend
、wordstart
或wordend
中的一个词,表示相对于数字索引的行或词的开始或结束。例如:6.2 wordstart
将是包含第6行第3个字符的单词的开始。2.0 lineend
将是第2行的末尾。
- 上述任何一种,加上加号或减号运算符,以及一定数量的字符或行。例如:
2.5 wordend - 1 chars
将是包含第2行第6个字符的单词末尾的前一个字符。
以下示例展示了这些索引的实际用法:
1 2 3 4 5 6 7 8 |
|
Tip
记住,这些索引应该是字符串,而不是浮点数!由于隐式类型转换,浮点数有时会起作用,但不要依赖这种行为。
Button
Ttk 按钮是一个简单的可点击按钮,可以激活一个回调函数。它看起来大概是这样的:
我们可以像这样创建一个按钮:
1 2 3 4 5 6 |
|
按钮是一个非常直观的控件,但它有一些选项可以用来配置。这些选项在下表中列出:
Arguments | Values | Description |
---|---|---|
text |
String | 按钮上的标签文本。 |
textvariable |
StringVar |
绑定到按钮标签文本的变量。 |
command |
Python function | 按钮被点击时要执行的回调函数。 |
default |
normal , active , disabled |
如果按钮在按下回车键时执行,active 表示它将响应回车键执行,normal 表示只有在被选中时才执行,disabled 表示它不会响应回车键。 |
underline |
Integer | 文本中要加下划线的字符的索引。 |
按钮也可以配置为显示图像而不是文本。我们将在第9章“使用样式和主题改善外观”中了解更多相关内容。
LabelFrame
在第1章《Tkinter简介》中,我们使用了Frame小部件将我们的小部件组合在一起。Ttk为我们提供了一个更强大的选项——LabelFrame,它提供了一个带有边框和标签的框架。这是一个非常有用的小部件,可用于在我们的GUI中为小部件提供可视化分组。
这段代码展示了一个 LabelFrame
的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
生成的图形用户界面将如下所示:
LabelFrame
控件为我们提供了一些用于配置的参数,如下所示:
Argument | Values | Description |
---|---|---|
text |
String | 要显示的标签的文本。 |
labelanchor |
Cardinal direction | 文本标签的锚定位置。 |
labelwidget |
ttk.Label object |
用于标签的标签小部件。会覆盖text。 |
underline |
Integer | 文本中要加下划线的字符的索引。 |
如你所见,我们可以通过指定 text
参数来配置 LabelFrame
的标签,或者通过创建一个 Label
小部件并使用 labelwidget
参数进行分配。如果我们想利用 Label
小部件的一些高级功能(例如绑定一个文本变量),那么后一种情况可能更为合适。如果我们使用了 labelwidget
,它将覆盖 text
参数。
Tip
Tkinter 和 Ttk 包含了许多其他小部件,其中一些我们将在本书的后面遇到。Python 还附带了一个名为 tix
的小部件库,其中包含了几十个小部件。然而,tix
已经非常过时,我们不会在本书中介绍它。不过,你应该知道它的存在。
实现该应用程序
到目前为止,我们已经学习了一些 Tkinter 的基础知识,调研了用户的需求,设计了我们的应用程序,并确定了在我们的应用程序中哪些 Ttk 小部件会派上用场。现在,是时候把所有这些整合起来,实际编写 ABQ 数据输入应用程序的第一个版本了。回想一下我们在第2章《设计GUI应用程序》中的设计,如下所示:
花点时间回顾一下我们需要创建的小部件,然后我们就开始编码。
第一步
在你的编辑器中打开一个新文件,命名为 data_entry_app.py
,然后我们这样开始:
1 2 3 4 5 6 7 |
|
我们的脚本以一个文档字符串开始,这是所有 Python 脚本都应该有的。这个字符串至少应该给出文件所属应用程序的名称,并且还可以包含有关用法、作者或其他未来维护者需要知道的事项的说明。
接下来,我们导入这个应用程序所需的 Python 模块:
- 当然是
tkinter
和ttk
,用于我们的 GUI 元素 datetime
模块中的datetime
类,我们将用它来生成文件名的日期字符串pathlib
模块中的Path
类,在我们的保存例程中用于一些文件操作csv
模块,我们将用它来与 CSV 文件交互
接下来,让我们创建一些全局变量,应用程序将使用这些变量来跟踪信息:
1 2 |
|
variables
字典将保存表单的所有控件变量。将它们放在字典中将使管理它们变得更容易,并保持我们的全局命名空间简洁干净。records_saved
变量将存储用户自打开应用程序以来保存了多少条记录。
现在是创建和配置根窗口的时候了:
1 2 3 |
|
我们已经为应用程序设置了窗口标题,并配置了其布局网格,以便允许第一列扩展。根窗口将只有一列,但通过这样设置,当窗口扩展时,表单将保持居中于应用程序。如果不这样做,当窗口扩展时,表单将固定在窗口的左侧。
现在,我们将为应用程序添加一个标题:
1 2 3 4 |
|
因为我们不需要再次引用这个小部件,所以我们不会将其分配给变量。这也允许我们在同一行上对 Label
调用 grid()
,使代码更简洁,命名空间也不那么杂乱。对于应用程序中的大多数小部件,我们都会这样做,除非有某种原因我们需要在代码的其他地方与小部件交互。
Tip
注意,我们为这个标签小部件使用了 TkDefaultFont
作为字体族值。这是 Tkinter 中定义的一个别名,指向你所在平台上的默认窗口字体。我们将在第9章“使用样式和主题改善外观”中了解更多关于字体的内容。
构建数据记录表单
随着初始应用程序窗口的设置完成,我们开始构建实际的数据输入表单。我们将创建一个框架来包含整个数据记录表单,称为drf:
1 2 3 |
|
drf框架被添加到主窗口中,并带有一些水平填充,sticky
参数确保当包含它的列被拉伸时,它也会跟着拉伸。我们还将配置其网格以扩展第一列。
Tip
对于使用网格布局的窗口或框架,如果你希望子控件在父控件拉伸时也随之拉伸,你需要确保两点:一是容器能够扩展(通过在父控件上使用columnconfigure
和rowconfigure
),二是子控件能够随着容器一起扩展(通过在子控件上调用grid()
时使用sticky
参数)。
记录信息部分
我们表单的第一部分是“记录信息”部分。让我们创建并配置一个LabelFrame
来存储它:
1 2 3 4 |
|
我们首先创建一个Ttk LabelFrame
小部件,其父级是数据记录表单。我们将其添加到父级网格中,并设置sticky
参数,以便在窗口调整大小时它能够扩展。此表单的每个框架都将有三列输入小部件,我们希望每列能够均匀扩展以填满框架的宽度。因此,我们使用了一个for
循环,将每列的weight
属性设置为1。现在,我们可以开始创建框架的内容,首先从第一个输入小部件“日期”字段开始:
1 2 3 |
|
首先,我们创建了一个控制变量,并将其放入variables
字典中。然后,我们为“日期”字段创建了Label
小部件,并将其添加到LabelFrame
小部件的网格中。我们将在这里使用明确的行和列值,即使在不是严格必要的情况下也是如此,因为我们将要稍微打乱顺序放置对象。如果没有明确的坐标,事情可能会变得混乱。
最后,我们创建了Entry
小部件,并传入了控制变量。请注意,如果我们可以使用变量来存储值,我们将不会保存对任何小部件的引用。这将使代码更加简洁。我们将小部件添加到网格中,通过指定下一行的第一列,将其放置在标签下方。对于Entry
和Label
,我们都使用了sticky
参数,以确保在GUI扩展时小部件能够拉伸。
现在,让我们添加第一行的其余部分:“时间”和“技术人员”字段:
1 2 3 4 5 6 7 |
|
再次,我们为每个项目创建了一个变量、一个Label
和一个输入小部件。请记住,Combobox
小部件的values
参数接受一个字符串列表,该列表将填充小部件的下拉部分。这样就完成了第一行。在第二行,我们从“实验室”输入开始:
1 2 3 4 5 6 |
|
与之前一样,我们创建了控制变量和Label
,但对于输入小部件,我们创建了一个Frame
来容纳三个Radiobutton
小部件。我们还使用for
循环创建了Radiobutton
小部件,以使代码更加简洁和一致。在这里,pack()
几何管理器非常方便,因为我们可以从左到右填充,而无需显式管理列号。expand
参数使得在窗口调整大小时,小部件能够使用额外空间;这将有助于我们的按钮利用可用空间,而不会被挤压到窗口的左侧。
现在,让我们完成第二行的剩余部分:“地块”和“种子样本”字段:
1 2 3 4 5 6 |
|
这里的过程相同:创建一个变量,创建一个Label
,创建输入小部件。请注意,对于Plot
值,我们使用range()
生成了一个列表,以保持代码的简洁性。
环境数据部分
表单的下一部分是“环境数据”框架。让我们按以下方式开始该部分:
1 2 3 4 |
|
这与我们为上一个LabelFrame所做的完全相同,只是名称进行了更新。接下来,我们开始在其中添加湿度、光照和温度控件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
很好!现在,在这个部分的第二行,我们只需要添加一个“设备故障”复选框:
1 2 3 4 |
|
前三个值都是浮点数,因此我们使用DoubleVar
控制变量和Spinbox
控件进行输入。不要忘记为Spinbox
控件填充from_
、to
和increment
值,以便箭头能够正常工作。我们的Checkbutton
使用一个BooleanVar
控制变量,并且由于其内置了标签,因此不需要额外的Label
控件。另外,请注意,由于我们已经开始了一个新的框架,所以网格的行和列编号重新开始。这是将表单拆分为更小框架的一个好处:我们不需要跟踪不断增加的行或列编号。
植物数据部分
我们将创建下一个框架“植物数据”,就像前两个一样:
1 2 3 4 |
|
现在,我们已经创建并配置了框架,接下来添加第一行输入:植物数量、花朵数量和果实数量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
这里没有什么新内容,只是因为我们使用了IntVar
控制变量,所以将Spinbox
的增量设置为1。这并不能真正阻止用户输入小数(或者任意字符串),但至少按钮不会误导用户。在第5章“通过验证和自动化减少用户错误”中,我们将看到如何更彻底地强制执行增量。现在,最后是我们最后一行输入:最小高度、最大高度和平均高度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
我们又创建了三个DoubleVar
对象、三个标签和三个Spinbox
控件。如果这感觉有点重复,不要惊讶;GUI代码往往会相当重复。在第4章“用类组织代码”中,我们将找到减少这种重复性的方法。
完成 GUI
我们已经完成了三个信息部分;现在我们需要添加Notes
输入。我们将直接在drf
框架中添加一个标签,如下所示:
1 2 3 |
|
由于我们无法为Text
小部件关联一个控制变量,因此我们需要保留一个常规的变量引用来访问它。
Tip
当你需要保存一个小部件的引用时,不要忘记将grid()
(以及其他几何管理器方法)单独调用!因为grid()
返回的是None
,如果你在一行语句中同时创建和定位小部件,那么你保存的小部件引用将会是None
。
我们几乎完成了表单!我们只需要添加一些按钮:
1 2 3 4 5 6 |
|
为了保持表单的网格布局更简洁,我们将两个按钮打包进了一个子框架,使用pack()
方法并通过side
参数将它们放在右侧。这样就完成了数据记录表单;为了完成应用程序的GUI,我们只需要添加一个状态栏以及一个关联的变量,如下所示:
1 2 |
|
状态栏只是一个Label
小部件,我们将其放置在根窗口的网格中的第99行,以确保它始终位于底部,以防将来对应用程序进行任何添加。请注意,我们没有将状态变量添加到variables
字典中;该字典是保留给将保存用户输入的变量的。这个变量只是用来向用户显示消息。
编写回调函数
现在我们的布局已经完成了,接下来让我们来实现应用程序的功能。我们的表单有两个按钮需要回调函数:重置和保存。
构建重置方法
我们的重置函数的任务是将整个表单恢复到空白状态,以便用户可以输入更多数据。我们不仅需要这个函数作为重置按钮的回调函数,还需要在用户保存记录后准备表单以输入下一条记录。否则,用户将不得不手动删除并覆盖每个字段中的数据以输入每条新记录。
由于我们需要在保存回调中调用重置回调,因此我们需要先编写重置函数。在data_entry_app.py
的末尾,开始一个新的函数,如下所示:
1 2 3 |
|
这个函数名为on_reset()
。回顾第1章《Tkinter简介》中的内容,按照惯例,回调函数通常命名为on_<事件名>
,其中事件名
指的是触发它的事件。由于这个函数将由点击重置按钮触发,因此我们将其命名为on_reset()
。
在函数内部,我们需要将所有小部件重置为空值。但是等等!除了备注输入外,我们没有保存任何其他小部件的引用。我们该怎么办?
很简单:我们将所有变量重置为空字符串,如下所示:
1 2 3 4 5 6 |
|
StringVar
、DoubleVar
和IntVar
对象可以被设置为空字符串,这将导致绑定到它们的任何小部件变为空白。如果我们尝试对BooleanVar
变量这样做,将会引发异常,因此我们将使用Python内置的isinstance()
函数检查变量是否是BooleanVar
。如果是,我们只需将其设置为False
。
对于备注输入,我们可以使用Text
小部件的delete()
方法来清除其内容。这个方法需要一个起始和结束位置,就像get()
方法一样。值1.0
和tk.END
表示小部件的全部内容。回顾我们之前关于Text
小部件的讨论,这个索引是字符串1.0
,而不是浮点数。这就是我们的重置回调中所需的全部内容。要将其绑定到按钮,请使用按钮的configure()
方法:
1 |
|
Tip
configure()
方法可以在任何Tkinter小部件上调用,以更改其属性。它接受与小部件构造函数相同的关键字参数。
构建保存方法
我们最后也是最重要的功能是保存回调。根据我们的程序规范,我们的应用程序需要将输入的数据追加到一个CSV(逗号分隔值)文件中,文件名格式为abq_data_record_CURRENTDATE.csv
,其中CURRENTDATE
为ISO格式的日期(年-月-日)。如果该文件不存在,则应创建它并写入表头行。因此,这个函数需要完成以下任务:
- 确定当前日期并生成文件名
- 判断文件是否存在,如果不存在则创建它并写入表头行
- 从表单中提取数据并进行必要的清理
- 将数据行追加到文件中
- 增加records_saved
计数并通知用户记录已保存
- 重置表单以输入下一条记录
让我们以这种方式开始编写函数:
1 2 3 |
|
再次使用on_<事件名>
的命名约定。我们首先声明records_saved
为全局变量。如果不这样做,Python会将records_saved
解释为局部变量,我们将无法更新它。
修改全局变量通常不是一个好的做法,但Tkinter在这里并没有给我们太多选择:我们不能使用返回值来更新变量,因为这是一个事件响应的回调函数,而不是我们代码中可以直接访问records_saved
的地方。在第4章《用类组织代码》中,我们将学习一种更好的方法来实现这个功能,而不需要使用全局变量;不过,现在我们只能这样处理。
接下来,让我们确定文件名的细节以及它是否存在:
1 2 3 |
|
datetime.today()
函数返回当前日期的Date
对象,其strftime()
方法允许我们将该日期格式化为我们指定的任何字符串格式。strftime()
的语法源自C语言编程,因此在某些情况下可能有些晦涩;但希望%Y
表示年、%m
表示月、%d
表示日是清晰的。这将返回ISO格式的日期;例如,2021年10月31日返回2021-10-31
。
有了日期字符串,我们就可以用它来构建当天CSV文件的文件名。在下一行中,Path(filename).exists()
告诉我们文件是否存在于当前工作目录中。它通过使用文件名构造一个Path
对象,然后调用其exists()
方法来查看文件是否已经在文件系统中。我们将这个信息保存到一个名为newfile
的变量中。现在是时候从表单中获取数据了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
我们将把数据存储在一个新的字典对象data
中。为此,我们将遍历我们的variables
字典,对每个变量调用get()
。当然,如果存在设备故障,我们希望跳过Light
、Humidity
和Temperature
的值,因此我们先获取Equipment Fault
的值,并在检索这些字段值之前进行检查。如果我们确实需要从变量中检索值,我们将在try
块中执行此操作。记住,如果变量中存在无效值,调用get()
方法时会引发TclError
,因此我们需要处理该异常。在这种情况下,我们会让用户知道该特定字段存在问题,并立即退出函数。最后,我们需要使用get()
从Notes
字段中获取数据。
现在我们有了数据,需要将其写入CSV。接下来添加以下代码:
1 2 3 4 5 |
|
首先,我们使用上下文管理器(with
关键字)打开文件。这样做可以确保在退出缩进块时关闭文件。我们以追加模式打开文件(由open
的a
参数指示),这意味着我们写入的任何数据都将简单地添加到已存在内容的末尾。注意newline
参数,我们将其设置为空字符串。这是为了解决Windows上CSV模块的一个bug,该bug会导致每条记录之间出现额外的空行。在其他平台上这样做没有坏处。
在块内,我们需要创建一个称为CSV Writer的对象。标准库csv
模块包含几种不同类型的对象,可以将数据写入CSV文件。DictWriter
类很方便,因为它可以接受任何顺序的值字典,并将它们写入CSV的适当字段,前提是第一行包含列名。我们可以通过传递data.keys()
来告诉DictWriter
这些表头值应该是什么,data.keys()
是我们所有数据值的名称。
追加模式会在文件不存在时创建文件,但它不会自动写入标题行。因此,我们需要检查文件是否是新文件(使用之前找到的newfile
值),如果是新文件,我们将写入标题行。DictWriter
对象有一个方法可以实现这一点,即只写入包含所有字段名的一行。
最后,我们可以使用DictWriter
对象的writerow()
方法,将我们的数据字典传入以写入文件。当我们退出缩进块时,Python会关闭文件并将其保存到磁盘。
这样,on_save()
函数中就只剩下几行代码了:
1 2 3 4 5 |
|
records_saved
变量的值,然后在状态栏中通知用户到目前为止已保存了多少条记录。这是一个很好的反馈,可以帮助用户知道他们的操作是成功的。最后,我们调用on_reset()
来准备表单,以便输入下一条记录。
实现了保存方法后,让我们将其绑定到按钮上:
1 |
|
最后,让我们重置表单并启动主事件循环:
1 2 |
|
就这样,你的第一个ABQ应用程序就完成了,可以运行了!
完成并测试
在我们将应用程序发布到世界之前,让我们先启动它并进行一下测试:
看起来不错!而且它也能正常工作。继续输入一些测试数据并保存。当然,这并不是终点——我们还没有完全解决程序规范中的所有问题,而且一旦用户开始使用这个应用程序,功能请求无疑会接踵而至。但现在,我们可以庆祝这个可工作的最小可行产品(MVP)的胜利。
总结
在这一章中,我们已经取得了很大的进展!你从规范和一些草图开始,设计出了一个已经运行的最小可行产品(MVP)应用程序,它已经涵盖了所需的基本功能。你学习了基本的Ttk小部件,如Entry、Spinbox、Combobox、Radiobutton和Checkbutton,以及Tkinter的Text小部件。你学会了如何使用嵌套的LabelFrame小部件将这些小部件组装成一个复杂但有序的图形用户界面(GUI),以及如何使用回调方法保存文件。
在下一章中,我们将利用类和面向对象编程技术来清理代码并扩展小部件的功能。