Tkinter GUI编程基础超级详解
1、什么是Tkinter
Python 有很多 GUI 框架,但 Tkinter 是唯一内置到 Python 标准库中的框架。 Tkinter 有几个优势。 它是跨平台的,因此相同的代码适用于 Windows、macOS 和 Linux。 视觉元素使用本机操作系统元素呈现,因此使用 Tkinter 构建的应用程序看起来像是属于运行它们的平台。
Tk 小部件库源自工具命令语言 (Tcl) 编程语言。 Tcl 和 Tk 是 John Ousterhout 在 1980 年代后期在伯克利担任教授时创建的,作为对大学使用的工程工具进行编程的更简单方法。 由于其速度和相对简单,Tcl/Tk 在学术、工程和 Unix 程序员中迅速普及。 就像 Python 本身一样,Tcl/Tk 起源于 Unix 平台,后来才迁移到 macOS 和 Windows。 Tk 的实际意图和 Unix 根源仍然影响着它的设计,与其他工具包相比,它的简单性仍然是一个主要优势。
Tkinter 是 Tk GUI 库的 Python 接口,自 1994 年发布 Python 1.1 版以来一直是 Python 标准库的一部分,使其成为 Python 的事实上的 GUI 库。 Tkinter 的文档以及进一步研究的链接可以在标准库文档中找到。
Tkinter 不仅适用于各种各样的应用程序,而且还有一些不容忽视的优点:
- Tkinter在标准库中:除了少数例外,只要 Python 可用,Tkinter 就可用。 无需安装 pip、创建虚拟环境、编译二进制文件或在 Web 上搜索安装包。 对于需要快速完成的简单项目,这是一个明显的优势。
- Tkinter 很稳定:虽然 Tkinter 的开发并没有停止,但它是缓慢而渐进的。 该 API 多年来一直很稳定,主要变化是附加功能和错误修复。 您的 Tkinter 代码可能会在未来几年或几十年内保持不变。
- Tkinter 只是一个 GUI 工具包:与其他一些 GUI 库不同,Tkinter 没有自己的线程库、网络堆栈或文件系统 API。 它依赖于常规的 Python 库来处理这些事情,因此非常适合将 GUI 应用于现有的 Python 代码。
- Tkinter 简单而严肃:Tkinter 非常基础和中肯; 它可以有效地用于过程和面向对象的 GUI 设计。 要使用 Tkinter,您不必学习数百个小部件类、标记或模板语言、新的编程范式、客户端-服务器技术或不同的编程语言。
当然,Tkinter 并不完美。 它也有一些缺点:
- Tkinter 的默认外观和感觉过时了:Tkinter 的默认外观早已落后于当前趋势,它仍然带有一些 1990 年代 Unix 世界的人工制品。 虽然它缺乏动画小部件、渐变或可缩放图形等细节,但由于 Tk 本身的更新和主题小部件库的添加,它在过去几年中有了很大的改进。
- Tkinter 缺少更复杂的小部件:Tkinter 缺少高级小部件,例如富文本编辑器、3D 图形嵌入、HTML 查看器或专用输入小部件。 但是可以通过自定义和组合简单的小部件来创建复杂的小部件。
尽管 Tkinter 被认为是事实上的 Python GUI 框架,但也并非没有批评。 一个值得注意的批评是用 Tkinter 构建的 GUI 看起来已经过时了。 如果你想要一个闪亮的、现代的界面,那么 Tkinter 可能不是你想要的。
然而,与其他框架相比,Tkinter 是轻量级的并且使用起来相对轻松。 这使得它成为在 Python 中构建 GUI 应用程序的一个令人信服的选择,特别是对于不需要现代光泽的应用程序,并且当务之急是快速构建功能性和跨平台的东西。
2、创建一个简单的Tkinter应用程序
第一步:导入Tkinter库
import tkinter as tk
第二步:创建窗口
每个 Tkinter 程序都必须有一个根窗口,它既代表我们应用程序的顶层窗口,也代表应用程序本身。
# 创建窗口
win = tk.Tk()
win
窗口是 Tk 类的一个实例。 我们通过调用 Tk() 来创建它,在我们可以创建任何其他 Tkinter 对象之前,该对象必须存在,并且当它被销毁时,应用程序将退出。
第三步:创建小部件
label = Label(win, text="Hello World")
上面代码创建了一个标签小部件,它只是一个可以显示一些文本的面板。 任何 Tkinter 小部件的第一个参数始终是父小部件(有时称为主小部件); 在这种情况下,我们传入了对win
窗口的引用。 父小部件是我们的标签将被放置在其上的小部件,因此该标签将直接位于应用程序的根窗口上。 Tkinter GUI 中的小部件按层次结构排列,每个小部件都被另一个小部件一直包含到根窗口。
我们还传入了一个关键字参数 text
。 当然,这个参数定义了将放置在小部件上的文本。 对于大多数 Tkinter 小部件,大部分配置都是使用这样的关键字参数完成的。
第四步:绘制小部件
现在我们已经创建了一个小部件,我们需要将它实际放置在 GUI 上:
label.pack()
Label 小部件的 pack()
方法称为几何管理器方法。它的工作是确定小部件将如何附加到其父小部件并在那里绘制它。 如果没有此调用,已经的小部件将存在,并不会在窗口的任何位置看到它。 pack() 是三个几何管理器之一。
第五步:启动窗口
win.mainloop()
上述代码开始了应用程序的事件循环。 事件循环是一个无限循环,它不断处理程序执行期间发生的任何事件。 事件可以是击键、鼠标点击或其他用户生成的活动。 这个循环一直运行到程序退出,所以这行之后的任何代码在主窗口关闭之前都不会运行。 出于这个原因,这一行通常是任何 Tkinter 程序中的最后一行。
完整代码如下:
import tkinter as tk
win = tk.Tk()
# 设置标题
win.title('Hello World')
# 设置几何参数:宽x高+位置
win.geometry('640x480')
win.resizable(False,False)
label = tk.Label(win,text='Helloworld',font='Arial 16 bold',bg='brown',fg='#FF0000')
label.pack()
win.mainloop()
3、小部件(Widgets)使用
小部件是 Python GUI 框架 Tkinter 的基础。 它们是用户与您的程序交互的元素。 Tkinter 中的每个小部件都由一个类定义。 以下是一些可用的小部件:
Widget类 | 描述 |
---|---|
Label | 用于在屏幕上显示文本的小部件 |
Button | 可以包含文本并在单击时可以执行操作的按钮 |
Entry | 仅允许单行文本的文本输入小部件 |
Text | 允许多行文本输入的文本输入小部件 |
Frame | 用于对相关小部件进行分组或在小部件之间提供填充的矩形区域 |
请注意,Tkinter 的小部件比此处列出的要多得多。 如需完整列表,请查看 TkDocs 教程中的基本小部件和更多小部件。 现在,仔细看看Label
小部件。
3.1 标签(Label)小部件
标签(Label)小部件用于显示文本或图像。 用户无法编辑 Label 小部件显示的文本。 它仅用于显示目的。 正如在本文开头的示例中看到的,可以通过实例化 Label 类并将字符串传递给 text 参数来创建 Label 小部件:
label = tk.Label(win,text="Hello, Tkinter")
标签小部件以默认系统文本颜色和默认系统文本背景颜色显示文本。 它们通常分别为黑色和白色,但如果在操作系统中更改了这些设置,可能会看到不同的颜色。
可以使用前景和背景参数控制标签文本和背景颜色:
label = tk.Label(
text="Hello, Tkinter",
foreground="white", # 将文本颜色设置为白色
background="black" # 将背景颜色设置为黑色
)
有许多有效的颜色名称,包括:
"red"
"orange"
"yellow"
"green"
"blue"
"purple"
许多 HTML 颜色名称都适用于 Tkinter。 此处提供了包含大多数有效颜色名称的图表。 如需完整参考,包括由当前系统主题控制的 macOS 和 Windows 特定系统颜色,请查看颜色手册页。
还可以使用十六进制 RGB 值指定颜色:
label = tk.Label(win,text="Hello, Tkinter", background="#34A2FE")
这会将标签背景设置为漂亮的浅蓝色。 十六进制 RGB 值比命名颜色更神秘,但它们也更灵活。 幸运的是,有一些工具可以让获取十六进制颜色代码变得相对轻松。
如果你不想一直输入前景色和背景色,那么你可以使用简写的 fg 和 bg 参数来设置前景色和背景色:
label = tk.Label(text="Hello, Tkinter", fg="white", bg="black")
还可以使用 width 和 height 参数控制标签的宽度和高度:
label = tk.Label(
win,
text="Hello, Tkinter",
fg="white",
bg="black",
width=10,
height=10
)
标签在窗口中的样子如下:
尽管宽度和高度都设置为 10,但窗口中的标签不是正方形可能看起来很奇怪。这是因为宽度和高度是以文本单位测量的。 一个水平文本单元由默认系统字体中字符“0”或数字零的宽度决定。 类似地,一个垂直文本单元由字符“0”的高度决定。
注意:Tkinter 使用文本单位来测量宽度和高度,而不是像英寸、厘米或像素这样的单位,以确保跨平台的应用程序行为一致。
按字符宽度测量单位意味着小部件的大小与用户机器上的默认字体相关。 无论应用程序在哪里运行,这都能确保文本正确地适合标签和按钮。
3.2 按钮(Button)小部件
按钮小部件用于显示可点击按钮。 可以将它们配置为在单击时调用函数。 将在下一节中介绍如何通过单击按钮调用函数。 现在,看看如何创建和设置 Button 样式。
Button 和 Label 小部件之间有许多相似之处。 在许多方面,按钮只是您可以单击的标签! 用于创建标签和设置标签样式的相同关键字参数将适用于按钮小部件。 例如,以下代码创建一个带有蓝色背景和黄色文本的 Button。 它还将宽度和高度分别设置为 25 和 5 个文本单位:
button = tk.Button(
win,
text="Click me!",
width=25,
height=5,
bg="blue",
fg="yellow",
)
效果如下:
3.3 输入(Entry)小部件
当需要从用户那里获取一些文本(例如姓名或电子邮件地址)时,请使用 Entry
小部件。 它们显示一个小文本框,用户可以在其中输入一些文本。 创建 Entry 小部件并为其设置样式与 Label 和 Button 小部件的工作方式非常相似。 例如,以下代码创建一个具有蓝色背景、一些黄色文本和 50 个文本单位宽度的小部件:
entry = tk.Entry(win,fg="yellow", bg="blue", width=50)
不过,关于 Entry 小部件的有趣之处不在于如何设置它们的样式。 这是如何使用它们来获取用户的输入。 可以使用 Entry 小部件执行三个主要操作:
- 使用** .get()** 检索文本
- 使用 .delete() 删除文本
- 使用 .insert() 插入文本
了解 Entry 小部件的最佳方式是创建一个小部件并与之交互。首先,导入 tkinter 并创建一个新窗口:
>>> import tkinter as tk
>>> window = tk.Tk()
现在创建一个Label和一个Entry小部件:
>>> label = tk.Label(text="Name")
>>> entry = tk.Entry()
Label 描述了 Entry 小部件中应该包含的文本类型。 它不会对条目强制执行任何类型的要求,但它会告诉用户您的程序希望他们放什么。 需要将小部件 .pack() 放入窗口中,以便它们可见:
>>> label.pack()
>>> entry.pack()
结果如下:
请注意,Tkinter 自动将 Label 置于窗口中 Entry 小部件上方的中心。 这是 .pack() 的一个功能,将在后面的部分中了解更多信息。
用鼠标在 Entry 小部件内单击并输入“Real Python”:
现在您已经在 Entry 小部件中输入了一些文本,但该文本尚未发送到您的程序。 可以使用 .get() 检索文本并将其分配给名为的变量
>>> name = entry.get()
>>> name
'Real Python'
您也可以使用 .delete() 文本。 这个方法接受一个整数参数,告诉 Python 要删除哪个字符。 例如,下面的代码块显示了 .delete(0) 如何从条目中删除第一个字符:
>>> entry.delete(0)
小部件中剩余的文本现在是“eal Python”:
Entry.delete() 就像字符串切片一样工作。 第一个参数确定起始索引,删除继续到但不包括作为第二个参数传递的索引。 使用特殊常量 tk.END 作为 .delete() 的第二个参数来删除条目中的所有文本:
>>> entry.delete(0, tk.END)
现在将看到一个空白文本框:
还可以将 .insert() 文本插入 Entry 小部件:
>>> entry.insert(0, "Python")
窗口现在看起来像这样:
第一个参数告诉 .insert() 在哪里插入文本。 如果 Entry 中没有文本,那么新文本将始终插入到小部件的开头,无论您作为第一个参数传递什么值。 例如,使用 100 作为第一个参数而不是 0 调用 .insert(),就像您在上面所做的那样,将生成相同的输出。
如果 Entry 已经包含一些文本,则 .insert() 将在指定位置插入新文本并将所有现有文本向右移动:
>>> entry.insert(0, "Real ")
小部件文本现在显示为“Real Python”:
Entry小部件非常适合从用户那里捕获少量文本,但由于它们只显示在一行上,因此它们不适合收集大量文本。 这就是文本小部件的用武之地!
3.4 使用文本(Text)小部件多选输入
文本(Text)小部件用于输入文本,就像条目小部件一样。 不同之处在于 Text 小部件可能包含多行文本。 使用文本小部件,用户可以输入整个段落甚至几页文本! 就像 Entry 小部件一样,可以使用 Text 小部件执行三个主要操作:
- 使用 .get() 检索文本
- 使用 .delete() 删除文本
- 使用 .insert() 插入文本
尽管方法名称与 Entry 方法相同,但它们的工作方式略有不同。下面将创建一个 Text 小部件并查看它的全部功能。
在 Python shell 中,创建一个新的空白窗口并将 .pack() 一个 Text() 小部件放入其中:
>>> window = tk.Tk()
>>> text_box = tk.Text()
>>> text_box.pack()
默认情况下,文本框比Entry小部件大得多。 上面创建的窗口如下所示:
单击窗口内的任意位置以激活文本框。 输入单词“你好”。 然后按 Enter键并在第二行输入“World”。 窗口现在应如下所示:
就像 Entry 小部件一样,可以使用 .get() 从 Text 小部件中检索文本。 但是,不带参数调用 .get() 不会像对 Entry 小部件那样返回文本框中的全文。 它引发了一个异常:
>>> text_box.get()
Traceback (most recent call last):
File "<pyshell#4>", line 1, in <module>
text_box.get()
TypeError: get() missing 1 required positional argument: 'index1'
Text.get() 至少需要一个参数。 使用单个索引调用 .get() 会返回单个字符。 要检索多个字符,需要传递一个开始索引和一个结束索引。 文本小部件中的索引与Entry小部件的工作方式不同。 由于 Text 小部件可以包含多行文本,因此索引必须包含两条信息:
- 字符的行号
- 字符在该行上的位置
行号从 1 开始,字符位置从 0 开始。要创建索引,请创建格式为“
使用索引“1.0”从您之前创建的文本框中获取第一个字母:
>>> text_box.get("1.0")
'H'
“Hello”这个词有五个字母,o的字符数是4,因为字符数是从0开始的,而“你好”这个词是从文本框中的第一个位置开始的。 就像 Python 字符串切片一样,为了从文本框中获取整个单词“Hello”,结束索引必须比要读取的最后一个字符的索引大一。
因此,要从文本框中获取单词“Hello”,对第一个索引使用“1.0”,对第二个索引使用“1.5”:
>>> text_box.get("1.0", "1.5")
'Hello'
要在文本框的第二行获取单词“World”,请将每个索引中的行号更改为 2:
>>> text_box.get("2.0", "2.5")
'World'
要获取文本框中的所有文本,请将起始索引设置为“1.0”并使用特殊的 tk.END 常量作为第二个索引:
>>> text_box.get("1.0", tk.END)
'Hello\nWorld\n'
请注意, .get() 返回的文本包括任何换行符。 还可以从这个示例中看到 Text 小部件中的每一行末尾都有一个换行符,包括文本框中的最后一行文本。
.delete() 用于从文本框中删除字符。 它就像 Entry 小部件的 .delete() 一样工作。 有两种使用 .delete() 的方法:
- 带一个参数
- 带两个参数
使用单参数版本,将要删除的单个字符的索引传递给 .delete()。 例如,以下从文本框中删除第一个字符 H:
>>> text_box.delete("1.0")
使用双参数版本,您传递两个索引以删除从第一个索引开始到但不包括第二个索引的字符范围。
例如,要删除文本框第一行剩余的“ello”,请使用索引“1.0”和“1.4”:
>>> text_box.delete("1.0", "1.4")
请注意,文本从第一行消失了。 这会在第二行的单词 World 后面留下一个空白行:
即使你看不到它,第一行仍然有一个字符。 这是一个换行符! 可以使用 .get() 验证这一点:
>>> text_box.get("1.0")
'\n'
如果删除该字符,则文本框的其余内容将向上移动一行:
>>> text_box.delete("1.0")
现在,“World”在文本框的第一行:
尝试清除文本框中的其余文本。 将“1.0”设置为起始索引,并使用 tk.END 作为第二个索引:
>>> text_box.delete("1.0", tk.END)
可以使用 .insert() 将文本插入文本框中:
>>> text_box.insert("1.0", "Hello")
这会在文本框的开头插入单词“Hello”,使用与 .get() 相同的“
看看如果尝试在第二行插入单词“World”会发生什么:
>>> text_box.insert("2.0", "World")
不是在第二行插入文本,而是在第一行的末尾插入文本:
如果要将文本插入新行,则需要手动将换行符插入到要插入的字符串中:
>>> text_box.insert("2.0", "\nWorld")
现在“World”在文本框的第二行:
.insert() 将做以下两件事之一:
- 如果该位置或之后已经有文本,则在指定位置插入文本。
- 如果字符数大于文本框中最后一个字符的索引,则将文本附加到指定行。
尝试跟踪最后一个字符的索引通常是不切实际的。 在 Text 小部件末尾插入文本的最佳方法是将 tk.END 传递给 .insert() 的第一个参数:
text_box.insert(tk.END, "Put me at the end!")
如果你想把它放在一个新行上,不要忘记在文本的开头包含换行符 (\n):
text_box.insert(tk.END, "\nPut me on a new line!")
Label、Button、Entry和Text小部件只是 Tkinter 中可用的一些小部件。 还有其他几个,包括复选框、单选按钮、滚动条和进度条的小部件。
3.5 Frame小部件使用
Frame小部件对于在应用程序中组织小部件的布局很重要。
在详细了解如何布置小部件的视觉呈现之前,请仔细查看 Frame 小部件的工作原理,以及如何将其他小部件分配给它们。 以下脚本创建一个空白 Frame 小部件并将其分配给主应用程序窗口:
import tkinter as tk
window = tk.Tk()
frame = tk.Frame()
frame.pack()
window.mainloop()
frame.pack() 将框架打包到窗口中,以便窗口自身的大小尽可能小以包含框架。 当你运行上面的脚本时,你会得到一些非常无趣的输出:
一个空的 Frame 小部件实际上是不可见的。 最好将Frame视为其他小部件的容器。 可以通过设置小部件的master属性将小部件分配给Frame:
frame = tk.Frame()
label = tk.Label(master=frame)
要了解它是如何工作的,下面编写了一个脚本来创建两个名为 frame_a 和 frame_b 的 Frame 小部件。 在此脚本中,frame_a 包含带有文本“I’m in Frame A”的Label,而 frame_b 包含“I’m in Frame B”的Label小部件。
import tkinter as tk
window = tk.Tk()
frame_a = tk.Frame()
frame_b = tk.Frame()
label_a = tk.Label(master=frame_a, text="I'm in Frame A")
label_a.pack()
label_b = tk.Label(master=frame_b, text="I'm in Frame B")
label_b.pack()
frame_a.pack()
frame_b.pack()
window.mainloop()
请注意,frame_a 在 frame_b 之前被打包到窗口中。 打开的窗口在 frame_b 中的标签上方显示 frame_a 中的标签:
现在看看当你交换 frame_a.pack() 和 frame_b.pack() 的顺序时会发生什么:
import tkinter as tk
window = tk.Tk()
frame_a = tk.Frame()
label_a = tk.Label(master=frame_a, text="I'm in Frame A")
label_a.pack()
frame_b = tk.Frame()
label_b = tk.Label(master=frame_b, text="I'm in Frame B")
label_b.pack()
# Swap the order of `frame_a` and `frame_b`
frame_b.pack()
frame_a.pack()
window.mainloop()
输出如下:
现在 label_b 位于顶部。 由于 label_b 被分配给 frame_b,它移动到 frame_b 所在的任何位置。
Label、Button、Entry、Text初始化里,可以使用master参数指定Frame。这样,您可以控制将小部件分配给哪个框架。 框架小部件非常适合以逻辑方式组织其他小部件。 相关的小部件可以分配给同一个框架,这样,如果框架在窗口中移动,则相关的小部件会保持在一起。
除了对小部件进行逻辑分组外,Frame 小部件还可以为应用程序的视觉呈现添加一点闪光。 继续阅读以了解如何为 Frame 小部件创建各种边框。
3.6 Frame小部件样式
Frame小部件的外观样式有如下类型:
- tk.FLAT:没有边框效果(默认值)。
- tk.SUNKEN:创建下沉效果。
- tk.RAISED:创建凸起效果。
- tk.GROOVE:创建凹槽边框效果。
- tk.RIDGE:创建脊状效果。
要应用边框效果,必须将borderwidth 属性设置为大于1 的值。该属性以像素为单位调整边框的宽度。 了解每种效果的最佳方式是亲自查看它们。 下面代码将五个 Frame 小部件打包到一个窗口中,每个小部件都有不同的参数值:
import tkinter as tk
border_effects = {
"flat": tk.FLAT,
"sunken": tk.SUNKEN,
"raised": tk.RAISED,
"groove": tk.GROOVE,
"ridge": tk.RIDGE,
}
window = tk.Tk()
for relief_name, relief in border_effects.items():
frame = tk.Frame(master=window, relief=relief, borderwidth=5)
frame.pack(side=tk.LEFT)
label = tk.Label(master=frame, text=relief_name)
label.pack()
window.mainloop()
结果如下:
3.7 小部件命名约定
当你创建一个小部件时,你可以给它起任何你喜欢的名字,只要它是一个有效的 Python 标识符。 在分配给小部件实例的变量名称中包含小部件类的名称通常是个好主意。 例如,如果标签小部件用于显示用户名,那么您可以将小部件命名为 label_user_name。 用于收集用户年龄的 Entry 小部件可能称为 entry_age。
当您在变量名称中包含小部件类名称时,您可以帮助自己(以及需要阅读您的代码的任何其他人)了解变量名称所指的小部件类型。 但是,使用小部件类的全名会导致变量名过长,因此您可能希望采用简写来引用每个小部件类型。 在本教程的其余部分,您将使用以下速记前缀来命名小部件:
Widget Class | Variable Name Prefix | Example |
---|---|---|
Label | lbl | lbl_name |
Button | btn | btn_submit |
Entry | ent | ent_age |
Text | txt | txt_notes |
Frame | frm | frm_address |
在本节中,您学习了如何创建窗口、使用小部件和使用框架。 此时,您可以制作一些显示消息的普通窗口,但您还没有创建完整的应用程序。 在下一节中,您将学习如何使用 Tkinter 强大的几何管理器来控制应用程序的布局。
4、使用几何管理器控制布局
到目前为止,我们一直在使用 .pack() 将小部件添加到窗口和Frame小部件,但还没有了解此方法的具体作用。 让我们把事情弄清楚! Tkinter 中的应用程序布局由几何管理器控制。 虽然 .pack() 是几何管理器的一个示例,但它并不是唯一的。 Tkinter 还有两个:
.place()
.grid()
应用程序中的每个窗口和框架只能使用一个几何管理器。 但是,不同的Frame可以使用不同的几何管理器,即使它们是使用另一个几何管理器分配给框架或窗口的。 首先仔细看看 .pack()。
4.1 .pack() 几何管理器
.pack() 使用打包算法以指定的顺序将小部件放置在框架或窗口中。 对于给定的小部件,打包算法有两个主要步骤:
- 计算一个称为包裹的矩形区域,该区域的高(或宽)足以容纳小部件,并用空白空间填充窗口中剩余的宽度(或高度)。
- 除非指定了不同的位置,否则将小部件居中。
.pack() 功能强大,但很难可视化。 了解 .pack() 的最佳方式是查看一些示例。 看看当你 .pack() 三个 Label 小部件到一个 Frame 时会发生什么:
import tkinter as tk
window = tk.Tk()
frame1 = tk.Frame(master=window, width=100, height=100, bg="red")
frame1.pack()
frame2 = tk.Frame(master=window, width=50, height=50, bg="yellow")
frame2.pack()
frame3 = tk.Frame(master=window, width=25, height=25, bg="blue")
frame3.pack()
window.mainloop()
.pack() 默认情况下将每个 Frame 放置在前一个 Frame 的下方,按照它们分配给窗口的顺序:
每个Frame都放置在最顶部的可用位置。 红色框架放置在窗口的顶部。 然后将黄色框架放置在红色框架的下方,将蓝色框架放置在黄色框架的下方。
三个不可见的地块包含三个 Frame 小部件中的每一个。 每个Parcel都与窗口一样宽,与它所包含的框架一样高。 由于在为每个 Frame 调用 .pack() 时没有指定锚点,因此它们都在它们的Parcel内居中。 这就是为什么每个Frame都在窗口中居中。
.pack() 接受一些关键字参数以更精确地配置小部件放置。 例如,可以设置 fill 关键字参数来指定帧应填充的方向。 选项是 tk.X 水平方向填充,tk.Y 垂直方向填充,tk.BOTH 双向填充。 以下是堆叠三个Frame的方法,以便每个Frame水平填充整个窗口:
import tkinter as tk
window = tk.Tk()
frame1 = tk.Frame(master=window, height=100, bg="red")
frame1.pack(fill=tk.X)
frame2 = tk.Frame(master=window, height=50, bg="yellow")
frame2.pack(fill=tk.X)
frame3 = tk.Frame(master=window, height=25, bg="blue")
frame3.pack(fill=tk.X)
window.mainloop()
请注意,没有在任何 Frame 小部件上设置宽度。 不再需要宽度,因为每个Frame都将 .pack() 设置为水平填充,覆盖可能设置的任何宽度。
上面脚本生成的窗口如下所示:
使用 .pack() 填充窗口的好处之一是填充响应窗口大小调整。 尝试扩大上一个脚本生成的窗口,看看它是如何工作的。 当扩大窗口时,三个 Frame 小部件的宽度会增大以填满窗口。
但请注意,Frame 小部件不会在垂直方向上展开。.pack() 的 side 关键字参数指定小部件应放置在窗口的哪一侧。 这些是可用的选项:
tk.TOP
tk.BOTTOM
tk.LEFT
tk.RIGHT
如果你不设置边,那么 .pack() 将自动使用 tk.TOP 并将新的小部件放置在窗口的顶部,或者放在窗口的最顶部尚未被小部件占用的部分。 例如,以下脚本从左到右并排放置三个框架,并扩展每个框架以垂直填充窗口:
import tkinter as tk
window = tk.Tk()
frame1 = tk.Frame(master=window, width=200, height=100, bg="red")
frame1.pack(fill=tk.Y, side=tk.LEFT)
frame2 = tk.Frame(master=window, width=100, bg="yellow")
frame2.pack(fill=tk.Y, side=tk.LEFT)
frame3 = tk.Frame(master=window, width=50, bg="blue")
frame3.pack(fill=tk.Y, side=tk.LEFT)
window.mainloop()
必须在至少一个Frame上指定 height 关键字参数,以强制窗口具有一定高度。生成的窗口如下所示:
就像设置 fill=tk.X 以在水平调整窗口大小时使框架响应一样,可以设置 fill=tk.Y 以使框架在垂直调整窗口大小时响应。
为了使布局真正响应,可以使用宽度和高度属性为Frame设置初始大小。 然后,将 .pack() 的 fill 关键字参数设置为 tk.BOTH 并将 expand 关键字参数设置为 True:
import tkinter as tk
window = tk.Tk()
frame1 = tk.Frame(master=window, width=200, height=100, bg="red")
frame1.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)
frame2 = tk.Frame(master=window, width=100, bg="yellow")
frame2.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)
frame3 = tk.Frame(master=window, width=50, bg="blue")
frame3.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)
window.mainloop()
当运行上述脚本时,会看到一个最初看起来与在上一个示例中生成的窗口相同的窗口。 不同之处在于,现在可以根据需要调整窗口大小,并且框架将响应地扩展并填充窗口:
4.2 .place() 几何管理器
可以使用 .place() 来控制小部件应在窗口或Frame中占据的精确位置。 必须提供两个关键字参数 x 和 y,它们指定小部件左上角的 x 和 y 坐标。 x 和 y 都以像素为单位,而不是文本单位。
请记住,原点(其中 x 和 y 均为 0)是 Frame 或窗口的左上角。 因此,可以将 .place() 的 y 参数视为距窗口顶部的像素数,将 x 参数视为距窗口左侧的像素数。
以下是 .place() 几何管理器如何工作的示例:
import tkinter as tk
window = tk.Tk()
frame = tk.Frame(master=window, width=150, height=150)
frame.pack()
label1 = tk.Label(master=frame, text="I'm at (0, 0)", bg="red")
label1.place(x=0, y=0)
label2 = tk.Label(master=frame, text="I'm at (75, 75)", bg="yellow")
label2.place(x=75, y=75)
window.mainloop()
以下是这段代码的工作原理:
- 第 5 行和第 6 行创建了一个名为 frame1 的新 Frame 小部件,它宽 150 像素,高 150 像素,并使用 .pack() 将其打包到窗口中。
- 第 8 行和第 9 行创建一个名为 label1 的新标签,其背景为黄色,并将其放置在 frame1 的位置 (0, 0)。
- 第 11 行和第 12 行创建了第二个 Label,名为 label2,背景为红色,并将其放置在 frame1 的位置 (75, 75)。
代码生成的窗口如下:
.place() 不经常使用。 它有两个主要缺点:
- 使用 .place() 可能难以管理布局。 如果应用程序有很多小部件,则尤其如此。
- 使用 .place() 创建的布局没有响应性。 它们不会随着窗口大小的改变而改变。
跨平台 GUI 开发的主要挑战之一是使布局无论在哪个平台上查看都看起来不错,而 .place() 是制作响应式和跨平台布局的糟糕选择。
这并不是说 .place() 永远不应该被使用! 在某些情况下,它可能正是您所需要的。 例如,如果您正在为地图创建 GUI 界面,那么 .place() 可能是确保小部件在地图上彼此之间保持正确距离的完美选择。
.pack() 通常是比 .place() 更好的选择,但即使 .pack() 也有一些缺点。 小部件的放置取决于调用 .pack() 的顺序,因此如果不完全理解控制布局的代码,就很难修改现有应用程序。 .grid() 几何管理器解决了很多此类问题,您将在下一节中看到。
4.3 .grid() 几何管理器
可能最常使用的几何管理器是 .grid(),它以更易于理解和维护的格式提供 .pack() 的所有功能。
.grid() 通过将窗口或Frame拆分为行和列来工作。 可以通过调用 .grid() 并将行和列索引分别传递给行和列关键字参数来指定小部件的位置。 行索引和列索引都从 0 开始,因此行索引 1 和列索引 2 告诉 .grid() 将小部件放置在第二行的第三列中。
以下脚本创建了一个 3 × 3 的框架网格,其中包含标签小部件:
import tkinter as tk
window = tk.Tk()
for i in range(3):
for j in range(3):
frame = tk.Frame(
master=window,
relief=tk.RAISED,
borderwidth=1
)
frame.grid(row=i, column=j)
label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
label.pack()
window.mainloop()
生成的窗口如下所示:
此示例中使用了两个几何管理器。 每个 Frame 都使用 .grid() 几何管理器附加到窗口:
import tkinter as tk
window = tk.Tk()
for i in range(3):
for j in range(3):
frame = tk.Frame(
master=window,
relief=tk.RAISED,
borderwidth=1
)
frame.grid(row=i, column=j)
label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
label.pack()
window.mainloop()
每个标签都使用 .pack() 附加到其主Frame:
import tkinter as tk
window = tk.Tk()
for i in range(3):
for j in range(3):
frame = tk.Frame(
master=window,
relief=tk.RAISED,
borderwidth=1
)
frame.grid(row=i, column=j)
label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
label.pack()
window.mainloop()
这里要意识到的重要一点是,即使在每个 Frame 对象上调用了 .grid() ,几何管理器也会应用于 window 对象。 类似地,每个Frame的布局由 .pack() 几何管理器控制。
上一个示例中的Frame紧挨着放置。 要在每个Frame周围添加一些空间,您可以设置网格中每个单元格的填充。 填充只是围绕小部件的一些空白空间,并将其从视觉上与其内容分开。
两种类型的填充是外部填充和内部填充。 外部填充在网格单元的外部周围增加了一些空间。 它由 .grid() 的两个关键字参数控制:
- padx 在水平方向添加填充。
- pady 在垂直方向添加填充。
padx 和 pady 都以像素为单位,而不是文本单位,因此将它们设置为相同的值将在两个方向上创建相同的填充量。 尝试在上一个示例中的Frame外部添加一些填充:
import tkinter as tk
window = tk.Tk()
for i in range(3):
for j in range(3):
frame = tk.Frame(
master=window,
relief=tk.RAISED,
borderwidth=1
)
frame.grid(row=i, column=j, padx=5, pady=5)
label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
label.pack()
window.mainloop()
生成的窗口如下:
.pack() 也有 padx 和 pady 参数。 下面的代码与前面的代码几乎相同,除了在 x 和 y 方向上在每个 Label 周围添加 5 个像素的额外填充:
import tkinter as tk
window = tk.Tk()
for i in range(3):
for j in range(3):
frame = tk.Frame(
master=window,
relief=tk.RAISED,
borderwidth=1
)
frame.grid(row=i, column=j, padx=5, pady=5)
label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
label.pack(padx=5, pady=5)
window.mainloop()
Label 小部件周围的额外填充使网格中的每个单元格在 Frame 边框和 Label 中的文本之间有一点喘息的空间:
这看起来很不错! 但是,如果您尝试向任何方向扩展窗口,那么您会注意到布局不是很敏感:
随着窗口的展开,整个网格保持在左上角。
您可以使用窗口对象上的 .columnconfigure() 和 .rowconfigure() 来调整网格的行和列的增长方式。 请记住,网格是附加到窗口的,即使您在每个 Frame 小部件上调用 .grid() 也是如此。 .columnconfigure() 和 .rowconfigure() 都采用三个基本参数:
- 要配置的网格列或行的索引(或同时配置多个行或列的索引列表)
- weight 参数,用于确定列或行相对于其他列和行应如何响应窗口大小调整
- minsize 参数,用于设置行高或列宽的最小尺寸(以像素为单位)
weight 默认设置为 0,这意味着当窗口调整大小时,列或行不会扩展。 如果每一列和每一行的权重为 1,那么它们都以相同的速度增长。 如果一列的权重为 1,另一列的权重为 2,则第二列的膨胀率是第一列的两倍。 调整之前的脚本以更好地处理窗口大小调整:
import tkinter as tk
window = tk.Tk()
for i in range(3):
window.columnconfigure(i, weight=1, minsize=75)
window.rowconfigure(i, weight=1, minsize=50)
for j in range(0, 3):
frame = tk.Frame(
master=window,
relief=tk.RAISED,
borderwidth=1
)
frame.grid(row=i, column=j, padx=5, pady=5)
label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
label.pack(padx=5, pady=5)
window.mainloop()
.columnconfigure() 和 .rowconfigure() 被放置在外部 for 循环的主体中。 (您可以在 for 循环之外显式配置每一列和每一行,但这需要额外编写六行代码。)
在循环的每次迭代中,第 i 列和第 i 行的权重都配置为 1。这样可以确保在调整窗口大小时每行和列都以相同的速率扩展。 minsize 参数设置为每列 75 和每行 50。 这可确保 Label 小部件始终显示其文本而不会截断任何字符,即使窗口非常小。
结果是一个网格布局,在调整窗口大小时平滑地扩展和收缩:
自己尝试一下,感受一下它的工作原理! 玩转 weight 和 minsize 参数,看看它们如何影响网格。
默认情况下,小部件在其网格单元格中居中。 例如,以下代码创建两个 Label 小部件,并将它们放置在一个一列两行的网格中:
import tkinter as tk
window = tk.Tk()
window.columnconfigure(0, minsize=250)
window.rowconfigure([0, 1], minsize=100)
label1 = tk.Label(text="A")
label1.grid(row=0, column=0)
label2 = tk.Label(text="B")
label2.grid(row=1, column=0)
window.mainloop()
每个网格单元宽 250 像素,高 100 像素。 标签放置在每个单元格的中心,如下图所示:
您可以使用sticky 参数更改网格单元内每个标签的位置。 sticky 接受包含以下一个或多个字母的字符串:
- “n” 或 “N” 与单元格的顶部中心部分对齐
- “e” 或 “E” 与单元格的右侧中心对齐
- “s” 或 “S” 与单元格的底部中心部分对齐
- “w” 或 “W” 与单元格的左侧中心对齐
字母“n”、“s”、“e”和“w”来自北、南、东和西的基本方向。 在前面的代码中将两个标签上的粘性设置为“n”,将每个标签定位在其网格单元的顶部中心:
import tkinter as tk
window = tk.Tk()
window.columnconfigure(0, minsize=250)
window.rowconfigure([0, 1], minsize=100)
label1 = tk.Label(text="A")
label1.grid(row=0, column=0, sticky="n")
label2 = tk.Label(text="B")
label2.grid(row=1, column=0, sticky="n")
window.mainloop()
您可以将多个字母组合在一个字符串中,以将每个标签定位在其网格单元的角落:
import tkinter as tk
window = tk.Tk()
window.columnconfigure(0, minsize=250)
window.rowconfigure([0, 1], minsize=100)
label1 = tk.Label(text="A")
label1.grid(row=0, column=0, sticky="ne")
label2 = tk.Label(text="B")
label2.grid(row=1, column=0, sticky="sw")
window.mainloop()
在本例中,label1 的sticky 参数设置为“ne”,这会将标签放置在其网格单元格的右上角。 通过将“sw”传递给sticky,label2 位于左下角。 这是窗口中的样子:
当一个小部件使用粘性定位时,小部件本身的大小刚好足以包含其中的任何文本和其他内容。 它不会填满整个网格单元。 为了填充网格,您可以指定“ns”强制小部件在垂直方向填充单元格,或“ew”在水平方向填充单元格。 要填充整个单元格,请将粘性设置为“nsew”。 以下示例说明了这些选项中的每一个:
import tkinter as tk
window = tk.Tk()
window.rowconfigure(0, minsize=50)
window.columnconfigure([0, 1, 2, 3], minsize=50)
label1 = tk.Label(text="1", bg="black", fg="white")
label2 = tk.Label(text="2", bg="black", fg="white")
label3 = tk.Label(text="3", bg="black", fg="white")
label4 = tk.Label(text="4", bg="black", fg="white")
label1.grid(row=0, column=0)
label2.grid(row=0, column=1, sticky="ew")
label3.grid(row=0, column=2, sticky="ns")
label4.grid(row=0, column=3, sticky="nsew")
window.mainloop()
生成的窗口如下:
上面的例子说明了 .grid() 几何管理器的粘性参数可以用来实现与 .pack() 几何管理器的填充参数相同的效果。 下表总结了sticky和fill参数的对应关系:
.grid() | .pack() |
---|---|
sticky="ns" | fill=tk.Y |
sticky="ew" | fill=tk.X |
sticky="nsew" | fill=tk.BOTH |
.grid() 是一个强大的几何管理器。 它通常比 .pack() 更容易理解,并且比 .place() 灵活得多。 当你创建新的 Tkinter 应用程序时,你应该考虑使用 .grid() 作为你的主要几何管理器。
注意: .grid() 提供了比您在此处看到的更多的灵活性。 例如,您可以将单元格配置为跨越多行和多列。 有关更多信息,请查看 [TkDocs](https://tkdocs.com/tutorial/index.html) 教程的 [Grid Geometry Manager](https://tkdocs.com/tutorial/grid.html) 部分。
现在您已经掌握了 Python GUI 框架 Tkinter 几何管理器的基础知识,下一步是为按钮分配操作以使您的应用程序栩栩如生。
5、使应用程序具有交互性
到目前为止,您已经对如何使用 Tkinter 创建一个窗口、添加一些小部件以及控制应用程序布局有了一个很好的了解。 这很好,但应用程序不应该只是看起来不错——它们实际上需要做一些事情! 在本节中,您将学习如何通过在发生某些事件时执行操作来使您的应用程序栩栩如生。
5.1 使用事件(Event)和事件处理程序(Event Handlers)
创建 Tkinter 应用程序时,必须调用 window.mainloop() 来启动事件循环。 在事件循环期间,您的应用程序会检查是否发生了事件。 如果是这样,则可以执行一些代码作为响应。
Tkinter 为您提供了事件循环,因此您不必自己编写任何代码来检查事件。 但是,您必须编写将执行以响应事件的代码。 在 Tkinter 中,您为应用程序中使用的事件编写称为事件处理程序的函数。
注意:事件是在事件循环期间发生的任何可能触发应用程序中某些行为的动作,例如按下键或鼠标按钮时。
当一个事件发生时,会发出一个事件对象,这意味着一个代表该事件的类的实例被实例化。 您无需担心自己创建这些类。 Tkinter 会自动为你创建事件类的实例。
下面将演示编写自定义的事件循环,以便更好地理解 Tkinter 的事件循环是如何工作的。 这样,可以看到 Tkinter 的事件循环如何适合您的应用程序,以及您需要自己编写哪些部分。
假设有一个名为 events_list 的列表,其中包含事件对象。 每次在程序中发生事件时,都会自动将新的事件对象附加到 events_list。 (您不需要实现这种更新机制。在这个概念示例中它会自动发生。)使用无限循环,您可以不断检查 events_list 中是否有任何事件对象:
# Assume that this list gets updated automatically
events_list = []
# Run the event loop
while True:
# If events_list is empty, then no events have occurred and you
# can skip to the next iteration of the loop
if events_list == []:
continue
# If execution reaches this point, then there is at least one
# event object in events_list
event = events_list[0]
现在,当前创建的事件循环对事件没有任何作用。 需要改变它。 假设应用程序需要响应按键,那么需要检查用户按下键盘上的某个键是否生成了事件,如果是,则将事件传递给事件处理函数以进行按键操作。
如果事件是按键事件对象,则假定该事件的 .type 属性设置为字符串“keypress”,并且 .char 属性包含被按下的键的字符。 创建一个新函数 handle_keypress() 并更新事件循环代码:
events_list = []
# Create an event handler
def handle_keypress(event):
"""Print the character associated to the key pressed"""
print(event.char)
while True:
if events_list == []:
continue
event = events_list[0]
# If event is a keypress event object
if event.type == "keypress":
# Call the keypress event handler
handle_keypress(event)
当调用 window.mainloop() 时,会为运行类似于上述循环的内容。 此方法处理循环的两个部分:
- 它维护一个已发生事件的列表。
- 每当有新事件添加到该列表时,它都会运行一个事件处理程序。
更新事件循环以使用 window.mainloop() 而不是自定义的事件循环:
import tkinter as tk
# Create a window object
window = tk.Tk()
# Create an event handler
def handle_keypress(event):
"""Print the character associated to the key pressed"""
print(event.char)
# Run the event loop
window.mainloop()
.mainloop() 处理了很多事情,但是上面的代码中缺少一些东西。 Tkinter 如何知道何时使用 handle_keypress()? Tkinter 小部件有一个名为 .bind() 的方法就是为了这个目的。
5.2 使用 .bind()
要在小部件上发生事件时调用事件处理程序,请使用 .bind()。 事件处理程序被称为绑定到事件,因为每次事件发生时都会调用它。 将继续上一节中的按键示例,并使用 .bind() 将 handle_keypress() 绑定到按键事件:
import tkinter as tk
window = tk.Tk()
def handle_keypress(event):
"""Print the character associated to the key pressed"""
print(event.char)
# Bind keypress event to handle_keypress()
window.bind("<Key>", handle_keypress)
window.mainloop()
在这里,handle_keypress() 事件处理程序使用 window.bind() 绑定到“”事件。 每当在应用程序运行时按下某个键,程序就会打印所按下键的字符。
注意:上述程序的输出不会打印在 Tkinter 应用程序窗口中。 它被打印到标准输出。
如果您在 IDLE 中运行程序,您将在交互式窗口中看到输出。 如果您从终端运行程序,您应该会在终端中看到输出。
.bind() 总是至少需要两个参数:
- 由“<event_name>”形式的字符串表示的事件,其中 event_name 可以是 Tkinter 的任何事件
- 一个事件处理程序,它是事件发生时要调用的函数的名称
事件处理程序绑定到调用 .bind() 的小部件。 当事件处理程序被调用时,事件对象被传递给事件处理程序函数。
在上面的示例中,事件处理程序绑定到窗口本身,但可以将事件处理程序绑定到应用程序中的任何小部件。 例如,可以将事件处理程序绑定到 Button 小部件,该小部件将在按下按钮时执行一些操作:
def handle_click(event):
print("The button was clicked!")
button = tk.Button(text="Click me!")
button.bind("<Button-1>", handle_click)
在此示例中,按钮小部件上的“”事件绑定到 handle_click 事件处理程序。 当鼠标悬停在窗口小部件上时,只要按下鼠标左键,就会发生“”事件。 鼠标按钮单击还有其他事件,包括鼠标中键的“”和鼠标右键的“”。
注意:有关常用事件的列表,请参阅 Tkinter 8.5 参考的事件类型部分。
可以使用 .bind() 将任何事件处理程序绑定到任何类型的小部件,但有一种更简单的方法可以使用 Button 小部件的命令属性将事件处理程序绑定到按钮单击。
5.3 使用命令
每个 Button 小部件都有一个可以分配给函数的命令属性。 每当按下按钮时,都会执行该功能。
看一个例子。 首先,将创建一个带有标签小部件的窗口,该小部件包含一个数值。 将在标签的左侧和右侧放置按钮。 左边的按钮将用于减少标签中的值,右边的按钮将增加值。 这是窗口的代码:
import tkinter as tk
window = tk.Tk()
window.rowconfigure(0, minsize=50, weight=1)
window.columnconfigure([0, 1, 2], minsize=50, weight=1)
btn_decrease = tk.Button(master=window, text="-")
btn_decrease.grid(row=0, column=0, sticky="nsew")
lbl_value = tk.Label(master=window, text="0")
lbl_value.grid(row=0, column=1)
btn_increase = tk.Button(master=window, text="+")
btn_increase.grid(row=0, column=2, sticky="nsew")
window.mainloop()
定义了应用程序布局后,可以通过给按钮一些命令来使其栩栩如生。 从左键开始。 当这个按钮被按下时,它应该将标签中的值减少 1。为了做到这一点,需要知道如何做两件事:
- 如何获取Label中的文本?
- 如何更新Label中的文本?
Label小部件没有像 Entry 和 Text 小部件那样的 .get() 。 但是,可以通过使用字典样式的下标表示法访问 text 属性来从标签中检索文本:
label = Tk.Label(text="Hello")
# Retrieve a Label's text
text = label["text"]
# Set new text for the label
label["text"] = "Good bye"
现在知道如何获取和设置标签的文本,编写一个函数 increase() 将 lbl_value 中的值增加 1:
def increase():
value = int(lbl_value["text"])
lbl_value["text"] = f"{value + 1}"
increase() 从 lbl_value 获取文本并使用 int() 将其转换为整数。 然后,它将这个值增加 1,并将Label的文本属性设置为这个新值。
还需要 reduction() 将 value_label 中的值减 1:
def decrease():
value = int(lbl_value["text"])
lbl_value["text"] = f"{value - 1}"
在 import 语句之后将 increase() 和 reduction() 放在代码中。
要将按钮连接到功能,请将功能分配给按钮的命令属性。 可以在实例化按钮时执行此操作。 例如,要将 increase() 分配给 increase_button,请将实例化按钮的行更新为以下内容:
btn_increase = tk.Button(master=window, text="+", command=increase)
现在将 reduction() 分配给 reduction_button:
btn_decrease = tk.Button(master=window, text="-", command=decrease)
这就是将按钮绑定到 increase() 和 reduction() 并使程序正常运行所需要做的一切。 尝试保存更改并运行应用程序! 单击按钮以增加和减少窗口中心的值:
这个应用程序并不是特别有用,但您在这里学到的技能适用于您将制作的每个应用程序:
- 使用小部件来创建用户界面的组件。
- 使用几何管理器来控制应用程序的布局。
- 编写与各种组件交互的函数,以捕获和转换用户输入。