TKinter-图形化编程库

目录

Python有很多GUI框架,但是Tkinter是Python标准库中唯一内置的框架。

Tkinter具有很多优点:它是跨平台的,因此相同的代码可在窗口,macOS和Linux上运行。视觉元素是使用本机操作系统元素呈现的,因此使用Tkinter构建的应用程序看起来像属于平台自身的。

Tkinter也有缺点:使用Tkinter构建的GUI看起来已经过时了。如果你想要一个fashion、现代化的界面,那么Tkinter可能暂时不能满足你的需求。

但是,与其他GUI框架相比,Tkinter轻巧且易上手。这使其在Python构建GUI应用程序中颇受欢迎,尤其是对于那些不需要“酷炫”外表的应用程序,并且优先考虑快速构建具有功能性和跨平台的应用程序。

在本教程中,你将学会:

  • 通过“ Hello,World!”,开始使用Tkinter
  • 学会使用小部件,例如按钮和文本框
  • 使用几何管理器控制应用程序布局
  • 通过将键盘单击与Python函数相关联,使你的应用程序具有交互性

在掌握了每个部分末尾的练习题后,会让你通过两个应用程序将所学内容融会贯通。第一个是温度转换器,第二个是文本编辑器。

⚠️本文IDE工具使用Pycharm与IDLE。

使用Tkinter构建第一个Python GUI应用程序

Tkinter GUI的基本元素是窗口窗口是所有其他GUI元素所在的容器。其他GUI元素(例如文本框、标签和按钮)被称为小部件。小部件包含在窗口内部。

首先,创建一个包含单个窗口小部件的窗口。

注意:本教程中的代码示例均已在Windows,macOS和带有Linux版本3.6、3.7和3.8的Ubuntu Linux 18.04上进行了测试。如果你已经安装Python,那么运行示例代码应该没有问题。

在打开Python Shell的情况下,你需要做的第一件事就是引入Python GUI Tkinter模块:

import tkinter as tk

一个窗口是Tkinter类的实例。创建一个新窗口,并将其分配给变量 window

window = tk.Tk()

窗口的外观取决于你的操作系统:

image

添加小部件

现在你有了一个窗口,你可以添加一个小部件。使用tk.Label向窗口添加一些文本。创建Label带有文本的小部件,"Hello, Tkinter"并将其分配给名为的变量greeting

>>> greeting = tk.Label(text="Hello, Tkinter")

之前创建的窗口不会更改。你刚刚创建了一个Label小部件,但尚未将其添加到窗口中。有几种方法可以将窗口小部件添加到窗口。可以使用Label小部件的.pack()方法:

>>> greeting.pack()

当你使用.pack()将小部件放入窗口时,Tkinter会将窗口的尺寸缩小到最小,同时仍将小部件完全包围。现在执行以下命令:

>>> window.mainloop()

窗口展示如下:

image.png

window.mainloop()告诉Python运行Tkinter事件循环。此方法侦听事件,例如单击按钮或按键,并阻止运行它之后的所有代码,直到关闭被调用的窗口为止。

Warning:如果你不将window.mainloop()包含在Python程序末尾文件中,则Tkinter应用程序将永远不会运行,并且不会显示任何内容。

用Tkinter创建一个窗口只需要几行代码。但是空白窗口不是很有用!在下一部分中,你将了解Tkinter中可用的一些小部件,以及如何自定义它们以满足应用程序的需求。

小测验

展开下面的代码块,以检查你的理解:

练习:创建一个Tkinter窗口

编写一个完整的Python脚本,用文本创建一个Tkinter窗口"Python rocks!"

该窗口应如下所示:

image.png

解决方案:

import tkinter as tk

window = tk.Tk()
label = tk.Label(text="Python rocks!")
label.pack()

window.mainloop()

使用小部件

小部件是Python GUI框架Tkinter的基础。它们是用户与程序进行交互的元素。Tkinter中的每个小部件都由一个类定义。以下是一些常用的小部件:

小部件类描述
Label用于在屏幕上显示文本的小部件
Button一个可以包含文本并在单击时可以执行操作的按钮
Entry文本输入小部件,仅允许单行文本
Text文本输入小部件,允许多行文本输入
Frame一个矩形区域,用于对相关小部件进行分组或在小部件之间提供填充

使用Label小部件显示文本和图像

Label小部件用于显示文本或图像Label窗口小部件显示的文本不能由用户编辑。如你在本教程开始时的示例中所见,可以通过实例化Label类并将字符串传递给text参数来创建窗口小部件:

label = tk.Label(text="Hello, Tkinter")

Label窗口小部件显示具有默认系统文本颜色和默认系统文本背景颜色的文本。它们通常分别是黑色和白色,但是如果你在操作系统中更改了这些设置,则可能会看到不同的颜色。

你可以Label使用foregroundbackground参数控制文本和背景颜色:

label = tk.Label(
    text="Hello, Tkinter",
    foreground="white",  # Set the text color to white
    background="black"  # Set the background color to black
)

有许多有效的颜色名称,包括:

  • "red"
  • "orange"
  • "yellow"
  • "green"
  • "blue"
  • "purple"

Tkinter可以使用许多HTML颜色名称。有关完整参考,包括由当前系统主题控制的macOS和窗口特定的系统颜色,请查看颜色手册页

你还可以使用十六进制RGB值指定颜色:

label = tk.Label(text="Hello, Tkinter", background="#34A2FE")

这会将标签背景设置为漂亮的浅蓝色。十六进制RGB值比命名颜色更神秘,但它们也更灵活。幸运的是,有一些工具可以使十六进制颜色代码变得相对轻松。

如果你不喜欢打字foregroundbackground所有的时间,那么你可以使用简写fgbg参数设置前景色和背景色:

label = tk.Label(text="Hello, Tkinter", fg="white", bg="black")

你还可以使用widthheight参数控制标签的宽度和高度:

label = tk.Label(
    text="Hello, Tkinter",
    fg="white",
    bg="black",
    width=10,
    height=10
)

这是该标签在窗口中的外观:

image

即使宽度和高度都设置为10,窗口中的标签显示不是正方形。这是因为宽度和高度是以文本单位测量的。一个水平文本单位由"0"默认系统字体中字符的宽度或数字零决定。同样,一个垂直文本单位由字符的高度确定"0"

注意: Tkinter使用文本单位(而不是英寸,厘米或像素)来测量宽度和高度,以确保跨平台的应用程序行为一致。

通过字符宽度来度量单位意味着小部件的大小相对于用户计算机上的默认字体。这样可以确保无论应用程序在何处运行,文本都可以正确地适合标签和按钮。

标签非常适合显示一些文本,但是它们并不能帮助你从用户那里获得输入。接下来要查看的三个小部件都用于获取用户输入。

显示带有Button小部件的可点击按钮

Button小部件用于显示可单击的按钮。可以将它们配置为在单击时调用一个函数。看看如何创建和设置样式Button

ButtonLabel小部件之间有许多相似之处。在许多方面,Button只是单击的Label!用于创建Label样式化的关键字参数同样适用于Button。例如,以下代码创建一个具有蓝色背景和黄色文本的Button,分别将width和height设置为255文本单位:

button = tk.Button(
    text="Click me!",
    width=25,
    height=15,
    bg="blue",
    fg="yellow",
)

这是窗口中按钮的外观:

image.png

使用Entry小部件获取用户输入

当你需要从用户那里获取一点点文字(例如姓名或电子邮件地址)时,请使用Entry小部件。它显示一个小的文本框,用户可以在其中输入一些文本。创建和样式化Entry窗口小部件的工作原理和LabelButton窗口小部件非常相似。例如,以下代码创建一个具有蓝色背景,一些黄色文本和一个宽度50单位的文本的窗口小部件:

entry = tk.Entry(fg="yellow", bg="blue", width=50)

但是,关于Entry小部件的有趣之处不是如何设置样式,而是使用它们从用户那里获取输入的方法。你可以使用Entry小部件执行以下三个主要操作:

  1. 检索文字.get()
  2. 删除的文字.delete()
  3. 插入文本.insert()

了解Entry小部件的最好方法是创建小部件并与其进行交互。打开一个Python shell,并按照本节中的示例进行操作。首先,导入tkinter并创建一个新窗口:

import tkinter as tk
window = tk.Tk()

现在创建一个Label和一个Entry小部件:

label = tk.Label(text="Name")
entry = tk.Entry()

Label描述Entry小部件中应包含的文本类型,它不会对Entry施加任何要求,但会告诉用户你的程序希望他们在哪里输入内容。你需要使用.pack()将小部件放到窗口中,以便可见:

label.pack()
entry.pack()

看起来像这样:

image.png

请注意,Tkinter会将窗口小部件Label上方的Entry窗口自动居中。这是.pack()的功能,你将在后面的内容中详细了解。

用鼠标在Entry小部件内单击并键入"Real Python"

image.png

现在,你已经在Entry小部件中输入了一些文本,但是该文本尚未传递到你的程序中。你可以使用.get()来检索文本并将其分配给名为的变量name

name = entry.get()
print(name)
'Real Python'

你也可以发.delete()删除数据。此方法采用一个整数参数,该参数告诉Python要删除的字符。例如,下面的代码块显示了如何.delete(0)从中删除第一个字符Entry

entry.delete(0)

现在,小部件中剩余的文本为"eal Python"

image.png

请注意,就像Python字符串对象一样Entry小部件中的文本以开头0

如果你需要从中删除多个字符Entry,请传递第二个整数参数来.delete()指示应该停止删除的字符的索引。例如,以下代码删除的前四个字母Entry

entry.delete(0, 3)

其余文本现在显示为"Python"

image.png

Entry.delete()就像字符串切片一样。第一个参数确定起始索引,删除操作一直进行到但不包括作为第二个参数传递的索引。使用特殊常量tk.END作为的第二个参数,.delete()以删除中的所有文本Entry

entry.delete(0, tk.END)

将看到一个空白文本框:

image.png

你也可以将.insert()文本发送到Entry小部件中:

entry.insert(0, "Python")

窗口现在看起来像这样:

image.png

第一个参数告诉你要.insert()在哪里插入文本。如果中没有文本Entry,则无论第一个参数传递什么值,新文本将始终插入小部件的开头。例如,像上面所做的那样,.insert()使用100作为第一个参数而不是进行调用0,将生成相同的输出。

如果一个Entry已经包含一些文本,则将.insert()在指定位置插入新文本并将所有现有文本向右移动:

entry.insert(0, "Real ")

小部件文本现在显示为"Real Python"

image.png

Entry小部件非常适合捕获用户的少量文本,但是由于它们仅显示在一行上,因此对于收集大量文本不是理想的选择。

使用Text小部件获取多行用户输入

Text窗口小部件用于输入文本,就像Entry窗口小部件一样。不同之处在于,Text小部件可能包含多行文本。使用Text窗口小部件,用户可以输入整个段落甚至几页文本!就像Entry小部件一样,你可以使用Text小部件执行以下三个主要操作:

  1. 检索文字.get()
  2. 删除文字.delete()
  3. 插入文字.insert()

尽管方法名称与方法相同Entry,但它们的工作方式略有不同。

注意:你是否仍打开上一节中的窗口?

如果是这样,则可以通过执行以下命令将其关闭:

window.destroy()

你也可以通过单击“关闭”按钮手动将其关闭

在Python Shell中,创建一个新的空白窗口,并.pack()在其中创建一个Text()小部件:

window = tk.Tk()
text_box = tk.Text()
text_box.pack()
window.mainloop()

默认情况下,文本框比Entry小部件大得多。上面创建的窗口如下所示:

image.png

单击窗口内的任何位置以激活文本框。输入单词"Hello"。然后按Enter并"World"在第二行上键入。窗口现在应如下所示:

image.png

就像Entry小部件一样,可以Text使用来从小部件中检索文本.get()。但是,.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()使用单个索引进行调用将返回单个字符。要检索几个字符,你需要传递一个开始索引和一个结束索引Text小部件中的索引与Entry小部件的工作方式不同。由于Text小部件可以包含多行文本,因此索引必须包含两条信息:

  1. 字符的行号
  2. 字符在该行上的位置

行号以开头1,字符位置以开头0。要创建索引,请创建形式为的字符串"<line>.<char>",用<line>行号和<char>字符号替换。例如,"1.0"在第一行上代表第一个字符,在第二行上"2.3"代表第四个字符。

使用索引"1.0"从你先前创建的文本框中获取第一个字母:

text_box.get("1.0")
'H'

该单词中有五个字母"Hello",并且字符编号o4,因为字符编号从开始0,并且单词"Hello"在文本框中的第一个位置开始。就像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()用于从文本框中删除字符。它的工作就像.delete()Entry小部件。有两种使用方法.delete()

  1. 有一个参数
  2. 两个参数

使用单参数版本,你将传递.delete()到要删除的单个字符的索引。例如,以下内容H从文本框中删除第一个字符:

text_box.delete("1.0")

窗口中的第一行文本现在显示为"ello"

image

对于两个参数,你传递两个索引以删除一系列字符,这些字符从第一个索引开始,一直到但不包括第二个索引。

例如,要删除"ello"文本框第一行中的其余内容,请使用索引"1.0""1.4"

text_box.delete("1.0", "1.4")

请注意,文本已从第一行删除。这样World在第二行上的单词之后留了一个空白行:

image

即使你看不到它,第一行仍然有一个字符。这是换行符!你可以使用.get()以下方法进行验证:

text_box.get("1.0")
'\n'

如果删除该字符,则文本框的其余内容将向上移动一行:

text_box.delete("1.0")

现在,"World"位于文本框的第一行:

image

尝试清除文本框中的其余文本。设置"1.0"为起始索引并tk.END用于第二个索引:

text_box.delete("1.0", tk.END)

文本框现在为空:

image

你可以使用以下命令将文本插入文本框.insert()

text_box.insert("1.0", "Hello")

这将在"Hello"文本框的开头插入单词,使用的"<line>.<column>"格式.get()与指定插入位置所用的格式相同:

image

如果你"World"在第二行中插入单词,请注意会发生什么情况:

text_box.insert("2.0", "World")

而不是在第二行插入文本,而是在第一行的末尾插入文本:

image

如果要在新行上插入文本,则需要在要插入的字符串中手动插入换行符:

text_box.insert("2.0", "\nWorld")

现在"World"在文本框的第二行:

image

.insert() 将执行以下两项操作之一:

  1. 如果在该位置或该位置之后已经有文本,将在该位置插入文本
  2. 如果字符数大于文本框中最后一个字符的索引,则将文本追加到指定的行

试图跟踪最后一个字符的索引通常是不切实际的。在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!")

LabelButtonEntry,和Text小部件只是少数中的Tkinter提供的小部件。还有其他几种,包括复选框小部件,单选按钮,滚动条和进度条。

将小部件分配给带有Frame小部件的框架

在本教程中,你将使用五个小部件。Frame小部件对于组织应用程序中小部件的布局很重要。

在详细了解如何布局窗口小部件的视觉呈现之前,请仔细研究Frame窗口小部件的工作方式以及如何为它们分配其他窗口小部件。以下脚本创建一个空白Frame窗口小部件,并将其分配给主应用程序窗口:

import tkinter as tk
window = tk.Tk()
frame = tk.Frame()
frame.pack()
window.mainloop()

frame.pack()将框架打包到窗口中,以使窗口自身尽可能小以包围框架。当运行上面的脚本时,会得到一些空白的输出:

image

空的Frame窗口小部件几乎是不可见的。最好将框架视为其他小部件的容器,可以通过设置窗口小部件的master属性来将窗口小部件分配给框架:

frame = tk.Frame()
label = tk.Label(master=frame)

要了解其工作原理,请编写一个脚本,该脚本创建两个Frame名为frame_a和的小部件frame_b。在此脚本中,frame_a包含带有文本的标签"I'm in Frame A",并frame_b包含label "I'm in Frame B"。这是执行此操作的一种方法:

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_a上方的位置显示标签frame_b

image.png

现在看看当你交换frame_a.pack()and的顺序时会发生什么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()

输出看起来像这样:

image.png

现在label_b是最重要的。由于label_b已分配给frame_b,因此它会移动到frame_b所处的位置。

我们学到的四个组件类型的LabelButtonEntry,和Text-它们的实例都具备一个master属性。这样,你可以控制Frame窗口小部件分配给哪个窗口。Frame小部件非常适合以逻辑方式组织其他小部件。可以将相关的窗口小部件分配给同一框架,这样,如果框架曾经在窗口中移动过,那么相关的窗口小部件将保持在一起。

除了按逻辑对小部件进行分组以外,Frame小部件还可以为应用程序的可视化外观增加一些亮点。继续阅读以了解如何为Frame小部件创建各种边框。

通过浮雕调整镜框外观

Frame可以使用relief在框架周围创建边框的属性来配置窗口小部件。你可以设置relief为以下任意值:

  • tk.FLAT没有边框效果(默认值)。
  • tk.SUNKEN产生凹陷的效果。
  • tk.RAISED产生凸起的效果。
  • tk.GROOVE创建带凹槽的边框效果。
  • tk.RIDGE创建脊状效果。

要应用边框效果,必须将borderwidth属性设置为大于的值1。此属性调整边框的宽度(以像素为单位)。感受每种效果的最佳方法是亲自观察它们。这是一个脚本,它将五个Frame小部件打包到一个窗口中,每个小部件的relief参数值都不同:

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至9行创建了一个dict,其键是Tkinter中可用的不同浮雕效果的名称。这些值是相应的Tkinter对象。该字典分配给border_effects变量。
  • 第13行开始for循环,循环遍历border_effects字典中的每个项目。
  • 第14行创建一个新的Frame小部件,并将其分配给该window对象。该relief属性设置为border_effects字典中相应的浮雕,并且该border属性设置为,5以便可以看到效果。
  • 第15行使用打包Frame到窗口中.pack()。的side关键字参数告诉Tkinter的哪个方向来包装frame对象。在下一部分中,你将了解有关其工作原理的更多信息。
  • 第16和17行创建了一个Label小部件,以显示凸版的名称并将其打包到frame刚创建的对象中。

上面的脚本产生的窗口如下所示:

image.png

在此图像中,可以看到以下效果:

  • tk.FLAT 创建一个看似平坦的效果frame。
  • tk.SUNKEN 添加边框,使边框看起来像沉入窗口中。
  • tk.RAISED 为frame提供边框,使其看起来从屏幕突出。
  • tk.GROOVE 添加一个边框,该边框看起来像是凹陷的凹槽,围绕着原本平坦的框架。
  • tk.RIDGE 在frame边缘周围产生凸起的外观。

这些效果使你的Python GUI Tkinter应用程序具有一定的视觉吸引力。

了解小部件命名约定

创建窗口小部件时,只要它是有效的Python标识符,就可以给它提供任何你喜欢的名称。通常,在你分配给小部件实例的变量名称中包括小部件类的名称是一个好主意。例如,如果使用Label窗口小部件来显示用户名,则可以将其命名为label_user_nameEntry用于收集用户年龄的小部件可能称为entry_age

当你在变量名称中包含窗口小部件类名称时,你可以帮助自己(以及需要阅读代码的其他任何人)了解变量名称所指的窗口小部件类型。但是,使用小部件类的全名可能会导致变量名过长,因此你可能希望采用一种简写形式来引用每种小部件类型。在本教程的其余部分中,你将使用以下速记前缀来命名小部件:

小部件类变量名前缀例子
Labellbllbl_name
Buttonbtnbtn_submit
Entryentent_age
Texttxttxt_notes
Framefrmfrm_address

在本部分中,你学习了如何创建窗口,使用小部件以及如何使用框架。此时,你可以制作一些普通的窗口来显示消息,但尚未创建功能完善的应用程序。在下一节中,你将学习如何使用Tkinter强大的几何管理器来控制应用程序的布局。

小测验

展开下面的代码块进行练习,以检查你的理解:

练习:创建一个Entry小部件并插入一些文本。

编写一个完整的脚本,该脚本显示一个Entry宽度为40个文本单元,具有白色背景和黑色文本的小部件。用于.insert()在显示为的窗口小部件中显示文本"What is your name?"

输出窗口应如下所示:

image.png

解决方案:创建一个Entry小部件并插入一些文本

import tkinter as tk

window = tk.Tk()
entry = tk.Entry(width=40, bg="white", fg="black")
entry.pack()
entry.insert(0, "What is your name?")
window.mainloop()

使用几何管理器控制布局

到目前为止,你一直在Frame使用窗口将小部件添加到窗口和小部件中.pack(),但是尚未了解此方法的确切作用。让我们清理一下!Tkinter中的应用程序布局由几何管理器控制。虽然.pack()是几何图形管理器的一个示例,但它并不是唯一的一个。Tkinter还有另外两个:

  • .place()
  • .grid()

每个窗口以及Frame应用程序中的每个窗口只能使用一个几何管理器。但是,即使使用其他几何管理器将它们分配给框架或窗口,不同的框架也可以使用不同的几何管理器。从仔细研究开始.pack()

.pack()

.pack()使用打包算法Frame指定顺序将小部件放置在或窗口中。对于给定的小部件,打包算法具有两个主要步骤:

  1. 计算一个矩形区域称为地块,这只是高(或宽)足以容纳窗口小部件并填补了空白空间窗口中剩余的宽度(或高度)。
  2. 除非指定其他位置,否则将小部件居中

.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默认情况下,将它们按照分配给窗口的顺序放置在前一个下面:

image.png

每个Frame都放置在最上面的可用位置。红色Frame放置在窗口的顶部。然后将黄色Frame放置在红色的下方,将蓝色Frame放置在黄色的下方。

有三个包含三个Frame小部件的不可见包裹。每个包裹都与窗户一样宽,与窗户Frame所含的一样高。由于未指定锚点.pack(),因此每个锚点Frame,都位于地块内部。这就是为什么每个Frame都在窗口中居中的原因。

.pack()接受一些关键字参数以更精确地配置小部件放置。例如,你可以设置fill关键字参数来指定框架应哪个方向填充。选项是tk.X在水平方向tk.Y上填充,在垂直方向上填充以及tk.BOTH在两个方向上填充。这是堆叠三个框架的方式,以便每个框架水平填充整个窗口:

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()

请注意,width未在任何Frame小部件上设置。width不再需要,因为每个框架都设置.pack()为水平填充,从而覆盖了你可以设置的任何宽度。

该脚本生成的窗口如下所示:

image

关于用窗口填充的好处之一.pack()是填充对窗口调整大小作出响应。尝试扩大由上一个脚本生成的窗口,以了解其工作原理。扩大窗口时,三个Frame小部件的宽度会增加以填充窗口:

image

但是请注意,这些Frame小部件不会在垂直方向上展开。

所述side的关键字参数.pack()指定在其上的窗口侧的窗口小部件应放置。这些是可用的选项:

  • tk.TOP
  • tk.BOTTOM
  • tk.LEFT
  • tk.RIGHT

如果你未设置side,则.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()

这次,你必须height在至少一个框架上指定关键字参数,以强制窗口具有一定高度。

出现的窗口如下所示:

image

就像设置fill=tk.X水平调整窗口大小时使框架响应一样,你可以设置fill=tk.Y垂直调整窗口大小时使框架响应:

image

为了使布局真正具有响应性,你可以使用widthheight属性为框架设置初始尺寸。然后,设置fill的关键字参数.pack()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()

运行上面的脚本时,你会看到一个窗口,该窗口最初与上一个示例中生成的窗口相同。区别在于,现在你可以根据需要调整窗口的大小,并且框架将相应地扩展和填充窗口:

image

.place()

可以.place()用来控制窗口小部件应在窗口或中占据的确切位置Frame。你必须提供两个关键字参数xy,它们为小部件的左上角指定x和y坐标。二者xy以像素,而不是文本为单位测量。

请记住,原点(xy均为0)是Frame或窗口的左上角。因此,你可以将y参数.place()视为从窗口顶部开始的像素数,将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行创建了一个Frame名为的新小部件frame1,其150宽度为150像素,高度为像素,并使用将其打包到窗口中.pack()
  • 第8和9行创建了一个带有黄色背景的新Label名称label1,并将其放置frame1在位置(0,0)。
  • 线11和12形成第二Label称为label2红色背景,并将其放置在frame1位于位置(75,75)。

这是代码生成的窗口:

image

.place()不经常使用。它有两个主要缺点:

  1. 使用可能难以管理布局.place()如果你的应用程序具有许多小部件,则尤其如此。
  2. 使用创建的布局.place()没有响应。它们不会随着窗口大小的改变而改变。

跨平台GUI开发的主要挑战之一是,无论在哪个平台上查看,都必须使外观看起来好看,并且.place()对于响应式和跨平台布局而言,这是一个糟糕的选择。

这并不是说.place()永远不应该使用!在某些情况下,这可能正是你所需要的。例如,如果要为地图创建GUI界面,则.place()可能是确保小部件在地图上彼此之间保持正确距离的理想选择。

.pack()通常是比更好的选择.place(),但也.pack()有一些缺点。窗口小部件的位置取决于.pack()调用的顺序,因此在不完全了解控制布局的代码的情况下修改现有应用程序可能会很困难。该.grid()几何管理器解决了很多的这些问题,你将在下一节中看到。

.grid()

你可能最常使用的几何图形管理器是.grid().pack()以易于理解和维护的格式提供的所有功能。

.grid()通过将窗口拆分Frame为行或列来工作。你可以通过分别调用.grid()行和列索引并将其传递给rowcolumn关键字参数来指定小部件的位置。行索引和列索引都始于0,因此行索引为1和列索引为2告诉.grid()将小部件放置在第二行的第三列中。

以下脚本创建一个3×3的框架网格,其中包含Label小部件:

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()

结果窗口如下所示:

image

在此示例中,使用了两个几何图形管理器。每个Framewindow通过.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()

每个都通过以下label方式附加到其主Frame节点.pack()

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()

这里要意识到的重要一点是,即使.grid()在每个Frame对象上都调用了几何图形管理器,它也适用于该window对象。同样,每个布局都由几何管理器frame控制.pack()

上一个示例中的框架紧紧挨着放置。要在每个周围增加一些空间Frame,可以设置网格中每个单元的填充。填充只是围绕小部件并在视觉上将其与其内容分隔开的一些空白区域。

填充的两种类型是外部填充内部填充。外部填充会在网格单元的外部周围增加一些空间。它受以下两个关键字参数的控制.grid()

  1. padx 在水平方向上添加填充。
  2. pady 在垂直方向上添加填充。

两者padxpady均以像素为单位,而非文本单位,因此将它们设置为相同的值将在两个方向上产生相同的填充量。在前面的示例中,尝试在框架外部添加一些填充:

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()

这是结果窗口:

image

.pack()也有padxpady参数。以下代码与上一个代码几乎相同,不同之处在于,你Labelxy方向上分别在其周围添加了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

image

看起来不错!但是,如果你尝试向任何方向扩展窗口,那么你会注意到布局的响应速度不是很好:

image

窗口扩展时,整个网格都位于左上角。

你可以使用.columnconfigure().rowconfigure()window对象上调整在调整窗口大小时网格的行和列的增长方式。请记住,window即使你.grid()在每个Frame小部件上调用,网格也都附在上。双方.columnconfigure().rowconfigure()采取三个基本参数:

  1. 要配置的网格列或行的索引(或同时配置多个行或列的索引列表)
  2. 称为关键字的参数weight,用于确定列或行相对于其他列和行应如何响应窗口调整大小
  3. 称为关键字的参数minsize,用于设置行高或列宽的最小尺寸(以像素为单位)

weight0默认情况下设置为,这意味着列或行不会随着窗口调整大小而扩展。如果每个列和行的权重为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第列和行被配置为具有weight1。这样可以确保在调整窗口大小时,每一行和每一列以相同的速率扩展。对于每一列和每一行,该minsize参数均设置7550。这样可以确保Label小部件始终显示其文本而不会截断任何字符,即使窗口大小非常小也是如此。

结果是网格布局随着窗口大小的调整而平滑地扩展和收缩:

image

自己尝试一下,以了解它的工作原理!试一下weightminsize参数,看看它们如何影响网格。

默认情况下,小部件在其网格单元中居中。例如,以下代码创建两个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像素高。标签位于每个单元格的中心,如下图所示:

image

你可以使用sticky参数更改每个标签在网格单元内的位置。sticky接受包含以下一个或多个字母的字符串:

  • "n""N"与单元格的顶部中心部分对齐
  • "e""E"与单元格的右中心对齐
  • "s""S"与单元格的底部中心部分对齐
  • "w""W"对齐到单元格的左中侧

这些信件"n""s""e",并"w"从基本方向北,南,东来,和西部。在之前的两个代码中都设置sticky为,将每个位置设置在其网格单元的顶部中心:"n"LabelsLabel

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()

这是输出:

image

你可以将多个字母组合在一个字符串中,以将每个字母放置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, sticky="ne")
label2 = tk.Label(text="B")
label2.grid(row=1, column=0, sticky="sw")
window.mainloop()

在此示例中,的sticky参数label1设置为"ne",这会将标签放置在其网格单元的右上角。label2通过使设置在左下角"sw"sticky。这是窗口中的样子:

image

当使用sticky放置小部件时,小部件本身的大小正好足以在其中包含任何文本和其他内容。它不会填充整个网格单元。为了填充网格,你可以指定"ns"强制窗口小部件在垂直方向"ew"上填充单元格,或在水平方向上填充单元格。要填充整个单元格,请设置sticky"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()

输出结果如下所示:

image

上面的示例说明,.grid()几何管理器的sticky参数可用于实现与.pack()几何管理器的fill参数相同的效果。下表汇总了stickyfill参数之间的对应关系:

.grid().pack()
sticky="ns"fill=tk.Y
sticky="ew"fill=tk.X
sticky="nsew"fill=tk.BOTH

.grid()是一个功能强大的几何图形管理器。它通常比容易理解,.pack()并且比灵活得多.place()。在创建新的Tkinter应用程序时,应考虑将其.grid()用作主要的几何图形管理器。

注意: .grid()提供的灵活性比你在此处看到的要大得多。例如,你可以配置单元格以跨越多个行和列。欲了解更多信息,请查看网格几何管理器部分的的TkDocs教程

既然你已经掌握了Python GUI框架Tkinter的几何图形管理器基础知识,那么下一步就是为按钮分配操作以使你的应用程序栩栩如生。

小测验

练习:创建地址输入表单显示隐藏

下面是使用Tkinter制作的地址输入表单的图像。

image.png

解决方案:创建一个地址输入表单显示隐藏

import tkinter as tk

# Create a new window with the title "Address Entry Form"
window = tk.Tk()
window.title("Address Entry Form")

# Create a new frame `frm_form` to contain the Label
# and Entry widgets for entering address information.
frm_form = tk.Frame(relief=tk.SUNKEN, borderwidth=3)
# Pack the frame into the window
frm_form.pack()

# Create the Label and Entry widgets for "First Name"
lbl_first_name = tk.Label(master=frm_form, text="First Name:")
ent_first_name = tk.Entry(master=frm_form, width=50)
# Use the grid geometry manager to place the Label and
# Entry widgets in the first and second columns of the
# first row of the grid
lbl_first_name.grid(row=0, column=0, sticky="e")
ent_first_name.grid(row=0, column=1)

# Create the Label and Entry widgets for "Last Name"
lbl_last_name = tk.Label(master=frm_form, text="Last Name:")
ent_last_name = tk.Entry(master=frm_form, width=50)
# Place the widgets in the second row of the grid
lbl_last_name.grid(row=1, column=0, sticky="e")
ent_last_name.grid(row=1, column=1)

# Create the Label and Entry widgets for "Address Line 1"
lbl_address1 = tk.Label(master=frm_form, text="Address Line 1:")
ent_address1 = tk.Entry(master=frm_form, width=50)
# Place the widgets in the third row of the grid
lbl_address1.grid(row=2, column=0, sticky="e")
ent_address1.grid(row=2, column=1)

# Create the Label and Entry widgets for "Address Line 2"
lbl_address2 = tk.Label(master=frm_form, text="Address Line 2:")
ent_address2 = tk.Entry(master=frm_form, width=5)
# Place the widgets in the fourth row of the grid
lbl_address2.grid(row=3, column=0, sticky=tk.E)
ent_address2.grid(row=3, column=1)

# Create the Label and Entry widgets for "City"
lbl_city = tk.Label(master=frm_form, text="City:")
ent_city = tk.Entry(master=frm_form, width=50)
# Place the widgets in the fifth row of the grid
lbl_city.grid(row=4, column=0, sticky=tk.E)
ent_city.grid(row=4, column=1)

# Create the Label and Entry widgets for "State/Province"
lbl_state = tk.Label(master=frm_form, text="State/Province:")
ent_state = tk.Entry(master=frm_form, width=50)
# Place the widgets in the sixth row of the grid
lbl_state.grid(row=5, column=0, sticky=tk.E)
ent_state.grid(row=5, column=1)

# Create the Label and Entry widgets for "Postal Code"
lbl_postal_code = tk.Label(master=frm_form, text="Postal Code:")
ent_postal_code = tk.Entry(master=frm_form, width=50)
# Place the widgets in the seventh row of the grid
lbl_postal_code.grid(row=6, column=0, sticky=tk.E)
ent_postal_code.grid(row=6, column=1)

# Create the Label and Entry widgets for "Country"
lbl_country = tk.Label(master=frm_form, text="Country:")
ent_country = tk.Entry(master=frm_form, width=50)
# Place the widgets in the eight row of the grid
lbl_country.grid(row=7, column=0, sticky=tk.E)
ent_country.grid(row=7, column=1)

# Create a new frame `frm_buttons` to contain the
# Submit and Clear buttons. This frame fills the
# whole window in the horizontal direction and has
# 5 pixels of horizontal and vertical padding.
frm_buttons = tk.Frame()
frm_buttons.pack(fill=tk.X, ipadx=5, ipady=5)

# Create the "Submit" button and pack it to the
# right side of `frm_buttons`
btn_submit = tk.Button(master=frm_buttons, text="Submit")
btn_submit.pack(side=tk.RIGHT, padx=10, ipadx=10)

# Create the "Clear" button and pack it to the
# right side of `frm_buttons`
btn_clear = tk.Button(master=frm_buttons, text="Clear")
btn_clear.pack(side=tk.RIGHT, ipadx=10)

# Start the application
window.mainloop()

使应用程序具有交互性

到目前为止,你已经对如何使用Tkinter创建窗口,添加一些小部件以及控制应用程序布局有了一个很好的认识。很好,但是应用程序不应该看起来不错,它们实际上需要做一些事情!在本节中,你将学习如何通过在发生某些事件时执行操作来使应用程序栩栩如生。

使用事件和事件处理程序

创建Tkinter应用程序时,必须调用window.mainloop()以启动事件循环。在事件循环中,你的应用程序检查是否发生了事件。如果是这样,那么可以执行一些代码作为响应。

Tkinter为你提供了事件循环,因此你无需编写任何代码即可检查事件。但是,你确实必须编写将响应事件而执行的代码。在Tkinter中,你为应用程序中使用的事件编写了称为事件处理程序的函数。

注:一个事件是事件循环可能引发应用程序中的一些行为,当按下一个键或鼠标按钮,如期间发生的任何行动。

当事件发生时,将发射事件对象,这意味着将实例化表示该事件的类的实例。你不必担心自己创建这些类。Tkinter将自动为你创建事件类的实例。

你将编写自己的事件循环,以便更好地了解Tkinter的事件循环的工作方式。这样,你可以看到Tkinter的事件循环如何适合你的应用程序,以及需要编写哪些部分。

假设有一个名为list的列表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]

目前,你创建的事件循环对并没有任何作用event。让我们改变它。假设你的应用程序需要响应按键。你需要检查event是由用户按下键盘上的一个键生成的,如果是,则将其传递event给事件处理程序函数以进行按键操作。

如果该事件是按键事件对象,则假定该属性event具有.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(),会为你运行类似于上述循环的操作。此方法为你处理了循环的两个部分:

  1. 它维护已发生事件的列表
  2. 每当有新事件添加到该列表时,它将运行事件处理程序

更新你的事件循环以使用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()为此目的而调用的方法。

.bind()

要在小部件上发生事件时调用事件处理程序,请使用.bind()。据说事件处理程序绑定到事件,因为每次事件发生时都会调用该事件处理程序。你将继续上一节中的keypress示例,并用于.bind()绑定handle_keypress()到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()事件处理程序"<Key>"使用绑定到事件window.bind()。在应用程序运行过程中,每当按下一个键时,你的程序就会打印出该键的字符。

注意:以上程序的输出在Tkinter应用程序窗口中打印。它被打印到stdout

如果你在IDLE中运行该程序,你将在交互式窗口中看到输出。如果从终端运行程序,则应该在终端中看到输出。

.bind() 总是至少接受两个参数:

  1. 由形式为的字符串表示的事件"<event_name>",其中event_name可以是Tkinter的任何事件
  2. 事件处理程序,即事件发生时要调用的函数的名称

事件处理程序绑定到在其.bind()上调用的窗口小部件。调用事件处理程序时,事件对象将传递给事件处理程序函数。

在上面的示例中,事件处理程序绑定到窗口本身,但是你可以将事件处理程序绑定到应用程序中的任何窗口小部件。例如,你可以将事件处理程序绑定到Button窗口小部件,只要按下按钮,该窗口小部件就会执行某些操作:

def handle_click(event):
    print("The button was clicked!")
button = tk.Button(text="Click me!")
button.bind("<Button-1>", handle_click)

在这个例子中,"<Button-1>"在事件button控件绑定到handle_click事件处理程序。"<Button-1>"当鼠标悬停在窗口小部件上方时,只要按下鼠标左键,就会发生该事件。鼠标按钮单击还有其他事件,包括"<Button-2>"鼠标中键和"<Button-3>"鼠标右键。

command

每个Button小部件都有一个command可以分配给函数的属性。每当按下按钮时,都会执行该功能。

看一个例子。首先,你将创建一个带有Label包含数值的窗口小部件的窗口。你将在标签的左侧和右侧放置按钮。左按钮将用于减小中的值,右按钮将Label增大该值。这是窗口的代码:

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()

该窗口如下所示:

image

定义了应用程序布局后,你可以通过向按钮提供一些命令来使其栩栩如生。从左按钮开始。按下此按钮时,应该将标签中的值减小1。要执行此操作,需要知道两件事:

  1. 你如何在中获取文字Label
  2. 如何更新中的文字Label

Label小部件没有.get()喜欢EntryText小部件没有。但是,你可以通过使用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_value1:

def increase():
    value = int(lbl_value["text"])
    lbl_value["text"] = f"{value + 1}"

increase()从中获取文本,lbl_value然后使用将其转换为整数int()。然后,它将这个值增加1并将标签的text属性设置为这个新值。

你还需要decrease()将其中的值减少value_label1:

def decrease():
    value = int(lbl_value["text"])
    lbl_value["text"] = f"{value - 1}"

在语句之后,将increase()anddecrease()放在你的代码中import

要将按钮连接到功能,请将功能分配给按钮的command属性。你可以在实例化按钮时执行此操作。例如,要分配increase()increase_button,请将实例化按钮的行更新为以下内容:

btn_increase = tk.Button(master=window, text="+", command=increase)

现在分配decrease()decrease_button

btn_decrease = tk.Button(master=window, text="-", command=decrease)

这就是所有你需要做的按钮结合increase(),并decrease()使得应用程序的功能。尝试保存更改并运行应用程序!单击按钮以增加或减少窗口中心的值:

image

这是完整的应用程序代码,供你参考:

计数器应用程序完整源代码显示隐藏

这个应用程序并不是特别有用,但是你在这里学到的技能适用于你将制作的每个应用程序:

  • 使用小部件创建用户界面的组件。
  • 使用几何管理器来控制应用程序的布局。
  • 编写与各种组件交互以捕获和转换用户输入的函数

在接下来的两个部分中,你将构建一些有用的应用程序。首先,你将构建一个温度转换器,将温度值从华氏温度转换为摄氏温度。之后,你将构建一个文本编辑器,可以打开,编辑和保存文本文件!

小测验

练习:模拟滚动六边形模具显示隐藏

编写一个模拟滚动六面模具的程序。文本应有一个按钮"Roll"。当用户单击按钮时,应显示从1到的随机整数6

image.png

解决方案:模拟滚动六面模具显示隐藏

import tkinter as tk
import random

def roll():
    lbl_result["text"] = str(random.randint(1, 6))


window = tk.Tk()
window.columnconfigure(0, minsize=150)
window.rowconfigure([0, 1], minsize=50)

btn_roll = tk.Button(text="Roll", command=roll)
lbl_result = tk.Label()

btn_roll.grid(row=0, column=0, sticky="nsew")
lbl_result.grid(row=1, column=0)

window.mainloop()

开发温度转换器

在本部分中,你将开发一个温度转换器应用程序,该应用程序允许用户输入以华氏度为单位的温度,并按一个按钮将该温度转换为摄氏温度。你将逐步浏览代码。你也可以在本节末尾找到完整的源代码,以供参考。

注意:要充分利用本节内容,请遵循Python shell

在开始编码之前,你将首先设计该应用程序。你需要三个要素:

  1. Entry名为ent_temperature输入华氏值小部件
  2. Label小部件称为lbl_result显示摄氏结果
  3. 一个Button插件叫做btn_convert从读取值Entry小部件,从华氏其转换为摄氏度,并设置文本Label当点击小工具的结果

你可以将它们排列在一个网格中,每个小部件的一行和一列。这样可以使你的应用程序最低限度地工作,但是它不是非常用户友好的。一切都需要有标签

你将直接在ent_temperature小部件的右侧放置一个标签,其中包含华氏符号(℉),以便用户知道该值ent_temperature应以华氏度为单位。为此,请将标签文本设置为"\N{DEGREE FAHRENHEIT}",使用Python的命名Unicode字符支持显示华氏符号。

你可以btn_convert通过将其文本设置为value来赋予一些个性,该值"\N{RIGHTWARDS BLACK ARROW}"显示一个指向右侧的黑色箭头。你还将确保lbl_result在标签文本后始终带有摄氏符号(℃),"\N{DEGREE CELSIUS}"以指示结果以摄氏度为单位。这是最终窗口的外观:

image

现在,你知道需要什么小部件以及窗口将是什么样,你可以开始对其进行编码了!首先,导入tkinter并创建一个新窗口:

import tkinter as tk
window = tk.Tk()
window.title("Temperature Converter")

window.title()设置现有窗口的标题。当你最终运行此应用程序时,该窗口的标题栏中将显示文本Temperature Converter。接下来,创建ent_temperature标签为的窗口小部件,lbl_temp并将两者都分配给Frame名为的窗口小部件frm_entry

frm_entry = tk.Frame(master=window)
ent_temperature = tk.Entry(master=frm_entry, width=10)
lbl_temp = tk.Label(master=frm_entry, text="\N{DEGREE FAHRENHEIT}")

ent_temperature是用户将输入华氏度值的位置。lbl_temp用于标ent_temperature有华氏符号。frm_entry是一个容器,该容器组ent_temperaturelbl_temp在一起。

你想lbl_temp直接放置在的右侧ent_temperature。你可以frm_entry.grid()几何图形管理器中使用一行和两列对它们进行布局:

ent_temperature.grid(row=0, column=0, sticky="e")
lbl_temp.grid(row=0, column=1, sticky="w")

你已将sticky参数设置为"e"for,ent_temperature以便它始终粘贴在其网格单元的最右边。你还设置sticky"w"forlbl_temp使其停留在其网格单元的最左边。这样可确保lbl_temp始终位于的右侧ent_temperature

现在,用btn_convertlbl_result来转换输入的温度ent_temperature并显示结果:

btn_convert = tk.Button(
    master=window,
    text="\N{RIGHTWARDS BLACK ARROW}"
)
lbl_result = tk.Label(master=window, text="\N{DEGREE CELSIUS}")

喜欢frm_entry,两者btn_convertlbl_result都分配给window。这三个小部件一起构成了主应用程序网格中的三个单元。使用.grid()先走,现在布置它们:

frm_entry.grid(row=0, column=0, padx=10)
btn_convert.grid(row=0, column=1, pady=10)
lbl_result.grid(row=0, column=2, padx=10)

最后,运行该应用程序:

window.mainloop()

看起来很棒!但是该按钮目前尚无任何作用。在脚本文件顶部的该import行下方,添加一个名为的函数fahrenheit_to_celsius()

def fahrenheit_to_celsius():
    """Convert the value for Fahrenheit to Celsius and insert the
    result into lbl_result.
    """
    fahrenheit = ent_temperature.get()
    celsius = (5/9) * (float(fahrenheit) - 32)
    lbl_result["text"] = f"{round(celsius, 2)} \N{DEGREE CELSIUS}"

此函数从中读取值ent_temperature,将其从华氏度转换为摄氏度,然后在中显示结果lbl_result

现在转到你定义的行,btn_convert并将其command参数设置为fahrenheit_to_celsius

btn_convert = tk.Button(
    master=window,
    text="\N{RIGHTWARDS BLACK ARROW}",
    command=fahrenheit_to_celsius  # <--- Add this line
)

你仅用26行代码就创建了一个功能齐全的温度转换器应用程序!

温度转换器完整源代码

import tkinter as tk

def fahrenheit_to_celsius():
    """Convert the value for Fahrenheit to Celsius and insert the
    result into lbl_result.
    """
    fahrenheit = ent_temperature.get()
    celsius = (5/9) * (float(fahrenheit) - 32)
    lbl_result["text"] = f"{round(celsius, 2)} \N{DEGREE CELSIUS}"

# Set-up the window
window = tk.Tk()
window.title("Temperature Converter")
window.resizable(width=False, height=False)

# Create the Fahrenheit entry frame with an Entry
# widget and label in it
frm_entry = tk.Frame(master=window)
ent_temperature = tk.Entry(master=frm_entry, width=10)
lbl_temp = tk.Label(master=frm_entry, text="\N{DEGREE FAHRENHEIT}")

# Layout the temperature Entry and Label in frm_entry
# using the .grid() geometry manager
ent_temperature.grid(row=0, column=0, sticky="e")
lbl_temp.grid(row=0, column=1, sticky="w")

# Create the conversion Button and result display Label
btn_convert = tk.Button(
    master=window,
    text="\N{RIGHTWARDS BLACK ARROW}",
    command=fahrenheit_to_celsius
)
lbl_result = tk.Label(master=window, text="\N{DEGREE CELSIUS}")

# Set-up the layout using the .grid() geometry manager
frm_entry.grid(row=0, column=0, padx=10)
btn_convert.grid(row=0, column=1, pady=10)
lbl_result.grid(row=0, column=2, padx=10)

# Run the application
window.mainloop()

开发文本编辑器

在本部分中,你将开发一个文本编辑器应用程序,该应用程序可以创建,打开,编辑和保存文本文件。应用程序中包含三个基本元素:

  1. 一个Button小部件,btn_open用于打开文件进行编辑
  2. 一个Buttonbtn_save用于保存文件的小部件
  3. 一个TextBoxtxt_edit用于创建和编辑文本文件的小部件

将排列三个小部件,以使两个按钮位于窗口的左侧,而文本框位于右侧。整个窗口的最小高度应为800像素,txt_edit最小宽度应为800像素。整个布局应具有响应性,以便在调整窗口大小的同时也要调整txt_edit大小。但是,Frame保持按钮的宽度不应改变。

这是窗口外观的草图:

image

你可以使用.grid()几何图形管理器来获得所需的布局。布局包含单行和两列:

  1. 按钮左侧的窄列
  2. 文本框右侧的较宽列

要设置窗口的最小容量,并且txt_edit,你可以设置minsize窗口方法的参数.rowconfigure().columnconfigure()以800要处理调整大小,你可以设置weight这些方法的参数1。

为了使两个按钮进入同一列,你需要创建一个Frame名为的小部件fr_buttons。根据草图,两个按钮应垂直堆叠在此框架的内部,并btn_open在顶部。你可以使用.grid().pack()几何管理器来执行此操作。现在,你将继续使用.grid()它,因为它使用起来更容易一些。

有了计划后,就可以开始对应用程序进行编码了。第一步是创建所需的所有小部件:

import tkinter as tk
window = tk.Tk()
window.title("Simple Text Editor")

window.rowconfigure(0, minsize=800, weight=1)
window.columnconfigure(1, minsize=800, weight=1)

txt_edit = tk.Text(window)
fr_buttons = tk.Frame(window)
btn_open = tk.Button(fr_buttons, text="Open")
btn_save = tk.Button(fr_buttons, text="Save As...")

这是此代码的细分:

  • 1行引入tkinter
  • 第3行和第4行创建一个带有标题的新窗口"Simple Text Editor"
  • 第6和7行设置行和列配置。
  • 第9到12行创建了文本框,框架以及打开和保存按钮所需的四个小部件。

仔细看一下第6行中的minsize参数.rowconfigure()设置为800weight并且设置为1

window.rowconfigure(0, minsize=800, weight=1)

第一个参数是0,它将第一行的高度设置为800像素,并确保该行的高度与窗口的高度成比例地增长。应用程序布局中只有一行,因此这些设置适用于整个窗口。

我们还需要在7行仔细看看这里,你可以使用.columnconfigure()设置widthweight索引列的属性1,以8001分别:

window.columnconfigure(1, minsize=800, weight=1)

请记住,行和列索引是从零开始的,因此这些设置仅适用于第二列。通过仅配置第二列,在调整窗口大小时,文本框将自然扩展和收缩,而包含按钮的列将保持固定宽度。

现在,你可以处理应用程序布局。首先,fr_buttons使用.grid()几何图形管理器将两个按钮分配给框架:

btn_open.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
btn_save.grid(row=1, column=0, sticky="ew", padx=5)

的代码这两行创建一个网格与两行和在一列中fr_buttons,因为两个框架btn_openbtn_save具有其master属性集到fr_buttonsbtn_open放置在第一行和btn_save第二行中,以便btn_open显示btn_save在布局的上方,就像你在草图中计划的那样。

btn_openbtn_savesticky属性都设置为"ew",这将迫使按钮在两个方向上水平扩展并填充整个框架。这样可以确保两个按钮的大小相同。

通过将和参数设置为5,可以在每个按钮周围放置5个像素填充。仅具有垂直填充。由于它位于顶部,因此垂直填充使按钮从窗口顶部向下偏移了一点,并确保它和之间有一个小的间隙。padxpadybtn_openbtn_save

现在fr_buttons已经布置好并可以使用了,你可以为窗口的其余部分设置网格布局:

fr_buttons.grid(row=0, column=0, sticky="ns")
txt_edit.grid(row=0, column=1, sticky="nsew")

这两行代码创建了一个网格,其中包含的一行和两列window。你将其放置fr_buttons在第一列和txt_edit第二列中,以便fr_buttons显示txt_edit在窗口布局的左侧。

sticky参数fr_buttons设置为"ns",这将迫使整个框架垂直扩展并填充其列的整个高度。txt_edit填充其整个网格单元,因为将其sticky参数设置为"nsew",这迫使其向各个方向扩展

现在,应用程序布局已完成,请添加window.mainloop()到程序底部,然后保存并运行文件。显示以下窗口:

image

看起来很棒!但这还没有做任何事情,因此你需要开始为按钮编写命令。btn_open需要显示一个文件打开对话框,并允许用户选择一个文件。然后,需要打开该文件,并将的文本设置为文件txt_edit的内容。这是一个执行此操作的函数open_file()

def open_file():
    """Open a file for editing."""
    filepath = askopenfilename(
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
    )
    if not filepath:
        return
    txt_edit.delete("1.0", tk.END)
    with open(filepath, "r") as input_file:
        text = input_file.read()
        txt_edit.insert(tk.END, text)
    window.title(f"Simple Text Editor - {filepath}")

这是此功能的详细接介绍:

  • 第3至5行使用模块中的askopenfilename对话框tkinter.filedialog显示文件打开对话框并将选定的文件路径存储到filepath
  • 第6和7行检查用户是否关闭对话框或单击“取消”按钮。如果是这样,filepath则将为None,并且该函数将return无需执行任何代码即可读取文件和设置的文本txt_edit
  • 第8行清除了txt_editusing的当前内容.delete()
  • 第9行和第10.read()行在将textas存储为字符串之前打开所选文件及其内容。
  • 第11行将他的字符串分配texttxt_editusing .insert()
  • 第12行设置窗口的标题,使其包含打开文件的路径。

现在,你可以更新程序,以便在单击该程序时立即btn_open调用open_file()它。你需要做一些事情来更新程序。首先,进口askopenfilename()tkinter.filedialog加入下面的导入到你的程序的开头:

import tkinter as tk
from tkinter.filedialog import askopenfilename
window = tk.Tk()
window.title("Simple Text Editor")
window.rowconfigure(0, minsize=800, weight=1)
window.columnconfigure(1, minsize=800, weight=1)
txt_edit = tk.Text(window)
fr_buttons = tk.Frame(window)
btn_open = tk.Button(fr_buttons, text="Open")
btn_save = tk.Button(fr_buttons, text="Save As...")

接下来,open_file()在import语句下面添加定义:

import tkinter as tk
from tkinter.filedialog import askopenfilename
def open_file():
    """Open a file for editing."""
    filepath = askopenfilename(
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
    )
    if not filepath:
        return
    txt_edit.delete("1.0", tk.END)
    with open(filepath, "r") as input_file:
        text = input_file.read()
        txt_edit.insert(tk.END, text)
    window.title(f"Simple Text Editor - {filepath}")
window = tk.Tk()
window.title("Simple Text Editor")
window.rowconfigure(0, minsize=800, weight=1)
window.columnconfigure(1, minsize=800, weight=1)
txt_edit = tk.Text(window)
fr_buttons = tk.Frame(window)
btn_open = tk.Button(fr_buttons, text="Open")
btn_save = tk.Button(fr_buttons, text="Save As...")

最后,将的command属性设置btn_opnopen_file

import tkinter as tk
from tkinter.filedialog import askopenfilename
def open_file():
    """Open a file for editing."""
    filepath = askopenfilename(
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
    )
    if not filepath:
        return
    txt_edit.delete("1.0", tk.END)
    with open(filepath, "r") as input_file:
        text = input_file.read()
        txt_edit.insert(tk.END, text)
    window.title(f"Simple Text Editor - {filepath}")
window = tk.Tk()
window.title("Simple Text Editor")
window.rowconfigure(0, minsize=800, weight=1)
window.columnconfigure(1, minsize=800, weight=1)
txt_edit = tk.Text(window)
fr_buttons = tk.Frame(window)
btn_open = tk.Button(fr_buttons, text="Open", command=open_file)
btn_save = tk.Button(fr_buttons, text="Save As...")

保存文件并运行它以检查一切是否正常。然后尝试打开一个文本文件!

完成btn_open工作后,就该开始使用的功能了btn_save。这需要打开一个保存文件对话框,以便用户可以选择他们想要保存文件的位置。你将为此使用模块中的asksaveasfilename对话框tkinter.filedialog。此功能还需要提取当前在其中的文本,txt_edit并将其写入所选位置的文件中。这是一个执行此操作的函数:

def save_file():
    """Save the current file as a new file."""
    filepath = asksaveasfilename(
        defaultextension="txt",
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")],
    )
    if not filepath:
        return
    with open(filepath, "w") as output_file:
        text = txt_edit.get("1.0", tk.END)
        output_file.write(text)
    window.title(f"Simple Text Editor - {filepath}")

此代码的工作方式如下:

  • 第3至6行使用asksaveasfilename对话框从用户那里获取所需的保存位置。所选文件路径存储在filepath变量中。
  • 第7和8行检查用户是否关闭对话框或单击“取消”按钮。如果是这样,filepath则将为None,并且该函数将返回而不执行任何代码将文本保存到文件中。
  • 第9行在选定的文件路径中创建一个新文件。
  • 第10行txt_editwith.get()方法中提取文本并将其分配给变量text
  • 第11行写入text输出文件。
  • 第12行更新了窗口的标题,以便新文件路径显示在窗口标题中。

现在,你可以更新程序,以便在单击该程序时btn_save调用save_file()它。同样,你需要做一些事情来更新程序。首先,通过更新脚本顶部的导入asksaveasfilename()tkinter.filedialog进行导入,如下所示:

import tkinter as tk
from tkinter.filedialog import askopenfilename, asksaveasfilename
def open_file():
    """Open a file for editing."""
    filepath = askopenfilename(
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
    )
    if not filepath:
        return
    txt_edit.delete(1.0, tk.END)
    with open(filepath, "r") as input_file:
        text = input_file.read()
        txt_edit.insert(tk.END, text)
    window.title(f"Simple Text Editor - {filepath}")
window = tk.Tk()
window.title("Simple Text Editor")
window.rowconfigure(0, minsize=800, weight=1)
window.columnconfigure(1, minsize=800, weight=1)
txt_edit = tk.Text(window)
fr_buttons = tk.Frame(window, relief=tk.RAISED, bd=2)
btn_open = tk.Button(fr_buttons, text="Open", command=open_file)
btn_save = tk.Button(fr_buttons, text="Save As...")
btn_open.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
btn_save.grid(row=1, column=0, sticky="ew", padx=5)
fr_buttons.grid(row=0, column=0, sticky="ns")
txt_edit.grid(row=0, column=1, sticky="nsew")
window.mainloop()

接下来,在定义的save_file()下面添加open_file()定义:

import tkinter as tk
from tkinter.filedialog import askopenfilename, asksaveasfilename
def open_file():
    """Open a file for editing."""
    filepath = askopenfilename(
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
    )
    if not filepath:
        return
    txt_edit.delete(1.0, tk.END)
    with open(filepath, "r") as input_file:
        text = input_file.read()
        txt_edit.insert(tk.END, text)
    window.title(f"Simple Text Editor - {filepath}")
def save_file():
    """Save the current file as a new file."""
    filepath = asksaveasfilename(
        defaultextension="txt",
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")],
    )
    if not filepath:
        return
    with open(filepath, "w") as output_file:
        text = txt_edit.get(1.0, tk.END)
        output_file.write(text)
    window.title(f"Simple Text Editor - {filepath}")
window = tk.Tk()
window.title("Simple Text Editor")
window.rowconfigure(0, minsize=800, weight=1)
window.columnconfigure(1, minsize=800, weight=1)
txt_edit = tk.Text(window)
fr_buttons = tk.Frame(window, relief=tk.RAISED, bd=2)
btn_open = tk.Button(fr_buttons, text="Open", command=open_file)
btn_save = tk.Button(fr_buttons, text="Save As...")
btn_open.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
btn_save.grid(row=1, column=0, sticky="ew", padx=5)
fr_buttons.grid(row=0, column=0, sticky="ns")
txt_edit.grid(row=0, column=1, sticky="nsew")
window.mainloop()

最后,将的command属性设置btn_savesave_file

import tkinter as tk
from tkinter.filedialog import askopenfilename, asksaveasfilename
def open_file():
    """Open a file for editing."""
    filepath = askopenfilename(
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
    )
    if not filepath:
        return
    txt_edit.delete(1.0, tk.END)
    with open(filepath, "r") as input_file:
        text = input_file.read()
        txt_edit.insert(tk.END, text)
    window.title(f"Simple Text Editor - {filepath}")
def save_file():
    """Save the current file as a new file."""
    filepath = asksaveasfilename(
        defaultextension="txt",
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")],
    )
    if not filepath:
        return
    with open(filepath, "w") as output_file:
        text = txt_edit.get(1.0, tk.END)
        output_file.write(text)
    window.title(f"Simple Text Editor - {filepath}")
window = tk.Tk()
window.title("Simple Text Editor")
window.rowconfigure(0, minsize=800, weight=1)
window.columnconfigure(1, minsize=800, weight=1)
txt_edit = tk.Text(window)
fr_buttons = tk.Frame(window, relief=tk.RAISED, bd=2)
btn_open = tk.Button(fr_buttons, text="Open", command=open_file)
btn_save = tk.Button(fr_buttons, text="Save As...", command=save_file)
btn_open.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
btn_save.grid(row=1, column=0, sticky="ew", padx=5)
fr_buttons.grid(row=0, column=0, sticky="ns")
txt_edit.grid(row=0, column=1, sticky="nsew")
window.mainloop()

保存文件并运行。现在,你已经有了一个最小但功能齐全的文本编辑器!

你可以展开下面的代码块以查看完整的脚本:

文本编辑器应用程序的完整源代码显示隐藏

现在,你已经用Python构建了两个GUI应用程序,并应用了你在本教程中学到的许多主题。这是一项不小的成就,因此请花点时间对自己的工作感到满意。你现在可以自行处理一些应用程序了!

结论

在本教程中,你学习了如何开始使用Python GUI编程。Tkinter是Python GUI框架的优秀的库,因为它已内置在Python标准库中,并且使用此框架开发应用程序相对比较容易。

在本教程中,你已经学到了一些重要的Tkinter概念:

  • 如何使用小部件
  • 如何使用几何图形管理器控制应用程序布局
  • 如何使你的应用程序具有交互性
  • 如何使用五个基本Tkinter的部件LabelButtonEntryText,和Frame

既然你已经掌握了使用Tkinter进行Python GUI编程的基础,那么下一步就是构建自己的应用程序。

​​​​​​​

posted @ 2022-07-24 18:24  QualityAssurance21  阅读(417)  评论(0编辑  收藏  举报