在本章中,我们将介绍以下配方:
- 使用框架对小部件进行分组
- 使用包几何图形管理器
- 使用栅格几何图形管理器
- 使用“放置几何图形管理器”
- 使用 FrameLabel 小部件对输入进行分组
- 动态布局小部件
- 创建水平和垂直滚动条
小部件确定用户可以使用 GUI 应用程序执行的操作;然而,我们应该注意它们的位置以及我们与该安排建立的关系。有效的布局帮助用户识别每个图形元素的含义和优先级,以便他们能够快速理解如何与我们的程序交互。
布局还决定了用户希望在整个应用程序中始终看到的视觉外观,例如始终将确认按钮放置在屏幕的右下角。尽管这些信息对于我们作为开发人员来说可能是显而易见的,但如果我们不按照自然顺序引导他们完成应用程序,最终用户可能会感到不知所措。
本章将深入探讨 Tkinter 提供的不同机制,这些机制用于布局和分组小部件以及控制其他属性,例如它们的大小或间距。
框架表示窗口的矩形区域,通常在复杂布局中用于包含其他窗口小部件。因为它们有自己的填充、边框和背景,所以您可以注意到小部件组在逻辑上是相关的。
框架的另一种常见模式是封装应用程序功能的一部分,以便您可以创建一个隐藏子窗口小部件实现细节的抽象。
我们将看到一个例子,它通过创建一个从Frame
类继承的组件来覆盖这两种情况,并公开包含小部件的某些信息。
我们将构建一个包含两个列表的应用程序,其中第一个列表包含项目列表,第二个列表最初为空。这两个列表都可以滚动,您可以使用两个中心按钮在它们之间移动项目,以转移当前选择:
我们将定义一个Frame
子类来表示一个可滚动列表,然后创建这个类的两个实例。这两个按钮也将直接添加到主窗口:
import tkinter as tk
class ListFrame(tk.Frame):
def __init__(self, master, items=[]):
super().__init__(master)
self.list = tk.Listbox(self)
self.scroll = tk.Scrollbar(self, orient=tk.VERTICAL,
command=self.list.yview)
self.list.config(yscrollcommand=self.scroll.set)
self.list.insert(0, *items)
self.list.pack(side=tk.LEFT)
self.scroll.pack(side=tk.LEFT, fill=tk.Y)
def pop_selection(self):
index = self.list.curselection()
if index:
value = self.list.get(index)
self.list.delete(index)
return value
def insert_item(self, item):
self.list.insert(tk.END, item)
class App(tk.Tk):
def __init__(self):
super().__init__()
months = ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"]
self.frame_a = ListFrame(self, months)
self.frame_b = ListFrame(self)
self.btn_right = tk.Button(self, text=">",
command=self.move_right)
self.btn_left = tk.Button(self, text="<",
command=self.move_left)
self.frame_a.pack(side=tk.LEFT, padx=10, pady=10)
self.frame_b.pack(side=tk.RIGHT, padx=10, pady=10)
self.btn_right.pack(expand=True, ipadx=5)
self.btn_left.pack(expand=True, ipadx=5)
def move_right(self):
self.move(self.frame_a, self.frame_b)
def move_left(self):
self.move(self.frame_b, self.frame_a)
def move(self, frame_from, frame_to):
value = frame_from.pop_selection()
if value:
frame_to.insert_item(value)
if __name__ == "__main__":
app = App()
app.mainloop()
我们的ListFrame
类只有两种方法可以与内部列表交互:pop_selection()
和insert_item()
。第一种方法返回并删除当前选择,如果没有选择项,则返回并删除“无”,而第二种方法在列表末尾插入一个新项。
这些方法在父类中用于将项目从一个列表转移到另一个列表:
def move(self, frame_from, frame_to):
value = frame_from.pop_selection()
if value:
frame_to.insert_item(value)
我们还利用父框架容器正确地使用适当的填充物进行包装:
# ...
self.frame_a.pack(side=tk.LEFT, padx=10, pady=10) self.frame_b.pack(side=tk.RIGHT, padx=10, pady=10)
由于有了这些框架,我们对几何体管理器的调用在我们的全局布局中更加孤立和有序。
这种方法的另一个好处是,它允许我们在每个容器小部件中使用不同的几何管理器,例如在一个框架内的小部件中使用grid()
和pack()
在主窗口中布局框架。
但是,请记住,Tkinter 中不允许在同一容器中混合这些几何体管理器,这会导致应用程序崩溃。
- 使用包装几何管理器配方
在前面的配方中,我们已经看到创建小部件不会自动在屏幕上显示它。我们在每个小部件上调用了pack()
方法来实现这一点,这意味着我们使用了 packgeometry 管理器。
这是 Tkinter 中三个可用的几何体管理器之一,非常适合于简单的布局,例如,当您希望将所有小部件彼此重叠或并排放置时。
假设我们希望在应用程序中实现以下布局:
它由三行组成,其中最后一行有三个并排放置的小部件。在这个场景中,packgeometry 管理器可以像预期的那样轻松地添加小部件,而不需要额外的框架。
我们将使用五个具有不同文本和背景颜色的Label
小部件来帮助我们识别每个矩形区域:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
label_a = tk.Label(self, text="Label A", bg="yellow")
label_b = tk.Label(self, text="Label B", bg="orange")
label_c = tk.Label(self, text="Label C", bg="red")
label_d = tk.Label(self, text="Label D", bg="green")
label_e = tk.Label(self, text="Label E", bg="blue")
opts = { 'ipadx': 10, 'ipady': 10, 'fill': tk.BOTH }
label_a.pack(side=tk.TOP, **opts)
label_b.pack(side=tk.TOP, **opts)
label_c.pack(side=tk.LEFT, **opts)
label_d.pack(side=tk.LEFT, **opts)
label_e.pack(side=tk.LEFT, **opts)
if __name__ == "__main__":
app = App()
app.mainloop()
我们还在opts
字典中添加了一些选项,以明确每个区域的大小:
为了更好地理解 packgeometry 管理器,我们将逐步解释它如何将小部件添加到父容器中。这里,我们特别注意side
选项的值,它指示小部件相对于下一个要打包的小部件的相对位置。
首先,我们将两个标签包装在屏幕顶部。虽然tk.TOP
常量是side
选项的默认值,但我们明确设置它是为了清楚地将其与使用tk.LEFT
值的调用区分开:
然后,我们将下面三个标签的side
选项设置为tk.LEFT
,这将导致它们并排放置:
在label_e
上指定边并不重要,只要它是我们添加到容器中的最后一个小部件。
请记住,这就是为什么在使用包几何图形管理器时顺序如此重要的原因。为了防止复杂布局中出现意外结果,通常会将小部件与框架分组,以便在将所有小部件打包到一个框架中时,不会干扰其他小部件的排列。
在这些情况下,我们强烈建议您使用网格几何体管理器,因为它允许您通过一次对几何体管理器的调用直接设置每个小部件的位置,并避免需要额外的帧。
除了tk.TOP
和tk.LEFT
之外,您还可以将tk.BOTTOM
和tk.RIGHT
常量传递给side
选项。正如它们的名字所暗示的,它们执行相反的堆叠;然而,这可能是违反直觉的,因为我们遵循的自然顺序是从上到下,从左到右。
例如,如果我们将最后三个小部件中的tk.LEFT
值替换为tk.RIGHT
,它们从左到右的顺序将是label_e
、label_d
和label_c
。
- 使用栅格几何管理器配方
- 使用 Place geometry manager配方
栅格几何图形管理器被认为是三种几何图形管理器中功能更为广泛的一种。它直接重组了用户界面设计中常用的网格概念——一个分为行和列的二维表格,其中每个单元格代表一个小部件的可用空间。
我们将演示如何使用栅格几何图形管理器实现以下布局:
这可以表示为一个 3x3 表,其中第二列和第三列中的小部件跨越两行,而底部行中的小部件跨越三列。
正如我们在前面的配方中所做的那样,我们将使用五个具有不同背景的标签来说明细胞的分布:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
label_a = tk.Label(self, text="Label A", bg="yellow")
label_b = tk.Label(self, text="Label B", bg="orange")
label_c = tk.Label(self, text="Label C", bg="red")
label_d = tk.Label(self, text="Label D", bg="green")
label_e = tk.Label(self, text="Label E", bg="blue")
opts = { 'ipadx': 10, 'ipady': 10 , 'sticky': 'nswe' }
label_a.grid(row=0, column=0, **opts)
label_b.grid(row=1, column=0, **opts)
label_c.grid(row=0, column=1, rowspan=2, **opts)
label_d.grid(row=0, column=2, rowspan=2, **opts)
label_e.grid(row=2, column=0, columnspan=3, **opts)
if __name__ == "__main__":
app = App()
app.mainloop()
我们还传递了一个选项字典,用于添加一些内部填充,并将小部件扩展到单元格中的所有可用空间。
label_a
和label_b
的位置几乎是不言自明的:它们分别占据第一列的第一行和第二行,记住网格位置是零索引的:
要通过多个单元格扩展label_c
和label_d
,我们将rowspan
选项设置为2
,这样它们将跨越两个单元格,从row
和column
选项指示的位置开始。最后,我们将使用columnspan
选项将label_e
设置为3
。
值得注意的是,与 Pack geometry manager 不同,可以在每个小部件上将调用顺序更改为grid()
,而无需修改最终布局。
sticky
选项指示小部件应该粘贴的边界,以基本方向表示:北、南、西和东。这些值由 Tkinter 常数tk.N
、tk.S
、tk.W
和tk.E
以及组合版本tk.NW
、tk.NE
、tk.SW
和tk.SE
表示。
例如,sticky=tk.N
将小部件与单元格的上边框对齐(北),而sticky=tk.SE
将小部件定位在单元格的右下角(东南)。
因为这些常量表示它们对应的小写字母,所以我们将tk.N + tk.S + tk.W + tk.E
表达式简化为"nswe"
字符串。这意味着小部件应该水平和垂直扩展,类似于 Pack geometry manager 的fill=tk.BOTH
选项。
如果没有向sticky
选项传递任何值,则小部件将在单元格中居中。
- 使用包装几何管理器配方
- 使用 Place geometry manager配方
Place geometry manager 允许您以绝对值或相对值设置小部件的位置和大小。
在三个几何图形管理器中,它是最不常用的一个。另一方面,它可以适合一些复杂的场景,在这些场景中,您希望自由定位小部件或重叠先前放置的小部件。
为了演示如何使用 Place geometry manager,我们将通过混合绝对和相对位置和大小来复制以下布局:
我们将显示的标签具有不同的背景,并按照从左到右和从上到下的顺序进行定义:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
label_a = tk.Label(self, text="Label A", bg="yellow")
label_b = tk.Label(self, text="Label B", bg="orange")
label_c = tk.Label(self, text="Label C", bg="red")
label_d = tk.Label(self, text="Label D", bg="green")
label_e = tk.Label(self, text="Label E", bg="blue")
label_a.place(relwidth=0.25, relheight=0.25)
label_b.place(x=100, anchor=tk.N,
width=100, height=50)
label_c.place(relx=0.5, rely=0.5, anchor=tk.CENTER,
relwidth=0.5, relheight=0.5)
label_d.place(in_=label_c, anchor=tk.N + tk.W,
x=2, y=2, relx=0.5, rely=0.5,
relwidth=0.5, relheight=0.5)
label_e.place(x=200, y=200, anchor=tk.S + tk.E,
relwidth=0.25, relheight=0.25)
if __name__ == "__main__":
app = App()
app.mainloop()
如果您运行前面的程序,您可以在屏幕中央看到label_c
和label_d
之间的重叠,这是我们使用其他几何体管理器无法实现的。
第一个标签的relwidth
和relheight
选项设置为0.25
,这意味着其宽度和高度为其父容器的 25%。默认情况下,小部件放置在x=0
和y=0
位置,并与西北方向对齐,即屏幕的左上角。
第二个标签放置在绝对位置-x=100
-并与上边框对齐,将anchor
选项设置为tk.N
(北)常量。在这里,我们还使用width
和height
指定了一个绝对大小。
使用相对定位并将anchor
设置为tk.CENTER
,第三个标签在窗口上居中。记住,relx
和relwidth
的值表示父对象宽度的一半,rely
的值表示0.5
,而relheight
表示父对象高度的一半。
通过将第四个标签作为in_
参数传递,将其置于label_c
之上(请注意,Tkinter 用下划线作为其后缀,因为in
是保留关键字)。当使用in_
时,您可能会注意到对齐在几何上并不精确。在我们的示例中,我们必须在每个方向上添加2
像素的偏移量,以完全重叠label_c
的右下角。
最后,第五个标签使用绝对定位和相对大小。正如您可能已经注意到的,这些维度可以很容易地切换,因为我们假设父容器为 200 x 200 像素;但是,如果调整主窗口的大小,则只有相对权重才能按预期工作。可以通过调整窗口大小来测试此行为。
Place geometry manager 的另一个重要优点是,它可以与块或栅格一起使用。
例如,假设您希望在右键单击小部件时在其上动态显示标题。您可以使用标签小部件表示此标题,标签小部件位于您单击小部件的相对位置:
def show_caption(self, event):
caption = tk.Label(self, ...)
caption.place(in_=event.widget, x=event.x, y=event.y)
# ...
作为一般建议,我们建议您在 Tkinter 应用程序中尽可能多地使用任何其他几何体管理器,并仅在需要自定义定位的特殊情况下使用。
- 使用包装几何管理器配方
- 使用栅格几何管理器配方
LabelFrame
类可用于对多个输入小部件进行分组,用它们表示的标签指示逻辑实体。它通常用于表单中,与Frame
小部件非常相似。
我们将用几个LabelFrame
实例构建一个表单,每个实例都有相应的子输入小部件:
由于本示例的目的是显示最终布局,因此我们将添加一些小部件,而不将其引用保留为属性:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
group_1 = tk.LabelFrame(self, padx=15, pady=10,
text="Personal Information")
group_1.pack(padx=10, pady=5)
tk.Label(group_1, text="First name").grid(row=0)
tk.Label(group_1, text="Last name").grid(row=1)
tk.Entry(group_1).grid(row=0, column=1, sticky=tk.W)
tk.Entry(group_1).grid(row=1, column=1, sticky=tk.W)
group_2 = tk.LabelFrame(self, padx=15, pady=10,
text="Address")
group_2.pack(padx=10, pady=5)
tk.Label(group_2, text="Street").grid(row=0)
tk.Label(group_2, text="City").grid(row=1)
tk.Label(group_2, text="ZIP Code").grid(row=2)
tk.Entry(group_2).grid(row=0, column=1, sticky=tk.W)
tk.Entry(group_2).grid(row=1, column=1, sticky=tk.W)
tk.Entry(group_2, width=8).grid(row=2, column=1,
sticky=tk.W)
self.btn_submit = tk.Button(self, text="Submit")
self.btn_submit.pack(padx=10, pady=10, side=tk.RIGHT)
if __name__ == "__main__":
app = App()
app.mainloop()
LabelFrame
小部件使用labelwidget
选项设置用作标签的小部件。如果不存在,则显示作为text
选项传递的字符串。例如,您可以使用以下语句替换它,而不是使用tk.LabelFrame(master, text="Info")
创建实例:
label = tk.Label(master, text="Info", ...)
frame = tk.LabelFrame(master, labelwidget=label)
# ...
frame.pack()
这将允许您进行任何类型的自定义,例如添加图像。请注意,我们没有对标签使用任何几何图形管理器,因为它是在放置框架时进行管理的。
Grid geometry manager 在简单和高级布局中都很容易使用,它也是一种强大的机制,可以与小部件列表相结合。
我们将研究如何减少行数,并使用几行调用 geometry manager 方法,这要归功于列表理解以及zip
和enumerate
内置函数。
我们将构建的应用程序包含四个Entry
小部件,每个小部件都有相应的标签,指示输入的含义。我们还将添加一个按钮来打印所有条目的值:
我们将使用小部件列表,而不是创建每个小部件并将其分配给单独的属性。由于我们将在迭代这些列表时跟踪索引,因此我们可以使用适当的column
选项轻松调用grid()
方法。
我们将使用zip
函数聚合标签和条目列表。该按钮将单独创建和显示,因为它不与其他小部件共享任何选项:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
fields = ["First name", "Last name", "Phone", "Email"]
labels = [tk.Label(self, text=f) for f in fields]
entries = [tk.Entry(self) for _ in fields]
self.widgets = list(zip(labels, entries))
self.submit = tk.Button(self, text="Print info",
command=self.print_info)
for i, (label, entry) in enumerate(self.widgets):
label.grid(row=i, column=0, padx=10, sticky=tk.W)
entry.grid(row=i, column=1, padx=10, pady=5)
self.submit.grid(row=len(fields), column=1, sticky=tk.E,
padx=10, pady=10)
def print_info(self):
for label, entry in self.widgets:
print("{} = {}".format(label.cget("text"), "=", entry.get()))
if __name__ == "__main__":
app = App()
app.mainloop()
您可以在每个输入上输入不同的文本,然后单击“打印信息”按钮以验证每个元组是否包含相应的标签和条目。
每个列表理解都会迭代列表字段的字符串。标签使用每个项目作为显示的文本,条目只需要引用父容器。下划线是一种常见的习惯用法,表示忽略变量值。
从 Python3 开始,zip
返回一个迭代器而不是一个列表,因此我们使用 list 函数进行聚合。因此,widgets
属性包含可以安全迭代多次的元组列表:
fields = ["First name", "Last name", "Phone", "Email"]
labels = [tk.Label(self, text=f) for f in fields]
entries = [tk.Entry(self) for _ in fields]
self.widgets = list(zip(labels, entries))
现在,我们必须在每个小部件元组上调用几何体管理器。通过enumerate
函数,我们可以跟踪每次迭代的索引,并将其作为行号传递:
for i, (label, entry) in enumerate(self.widgets):
label.grid(row=i, column=0, padx=10, sticky=tk.W)
entry.grid(row=i, column=1, padx=10, pady=5)
请注意,我们使用了for i, (label, entry) in ...
语法,因为我们必须解包使用enumerate
生成的元组,然后解包widgets
属性的每个元组。
在print_info()
回调中,我们迭代小部件以打印每个标签文本及其相应的条目值。为了检索标签“text
,我们使用了cget()
方法,它允许您通过名称获取小部件选项的值。
在 Tkinter 中,几何体管理器会占用所有必要的空间,以便在其父容器中容纳所有小部件。但是,如果容器具有固定大小或超过屏幕大小,则会有一个用户看不到的区域。
滚动条小部件不会自动添加到 Tkinter 中,因此您必须像其他类型的小部件一样创建和布局它们。另一个需要考虑的问题是,只有少数小部件类具有使它们能够连接到滚动条的配置选项。
为了解决这个问题,您将学习利用画布小部件的灵活性,使任何容器都可以滚动。
为了演示如何组合使用Canvas
和Scrollbar
类来创建可调整大小和可滚动的框架,我们将构建一个应用程序,通过加载图像动态更改其大小。
单击“加载图像”按钮时,它会删除自身并将图像加载到大于可滚动区域的Canvas
中。在本例中,我们使用了预定义图像,但您可以通过文件对话框修改此程序以选择任何其他 GIF 图像:
这将启用水平和垂直滚动条,如果调整主窗口的大小,这些滚动条将自动进行调整:
当我们在单独的一章中深入讨论 Canvas 小部件的功能时,这个应用程序将介绍它的标准滚动界面和create_window()
方法。请注意,此脚本要求将文件python.gif
放在同一目录中:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.scroll_x = tk.Scrollbar(self, orient=tk.HORIZONTAL)
self.scroll_y = tk.Scrollbar(self, orient=tk.VERTICAL)
self.canvas = tk.Canvas(self, width=300, height=100,
xscrollcommand=self.scroll_x.set,
yscrollcommand=self.scroll_y.set)
self.scroll_x.config(command=self.canvas.xview)
self.scroll_y.config(command=self.canvas.yview)
self.frame = tk.Frame(self.canvas)
self.btn = tk.Button(self.frame, text="Load image",
command=self.load_image)
self.btn.pack()
self.canvas.create_window((0, 0), window=self.frame,
anchor=tk.NW)
self.canvas.grid(row=0, column=0, sticky="nswe")
self.scroll_x.grid(row=1, column=0, sticky="we")
self.scroll_y.grid(row=0, column=1, sticky="ns")
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.bind("<Configure>", self.resize)
self.update_idletasks()
self.minsize(self.winfo_width(), self.winfo_height())
def resize(self, event):
region = self.canvas.bbox(tk.ALL)
self.canvas.configure(scrollregion=region)
def load_image(self):
self.btn.destroy()
self.image = tk.PhotoImage(file="python.gif")
tk.Label(self.frame, image=self.image).pack()
if __name__ == "__main__":
app = App()
app.mainloop()
我们应用程序的第一行创建滚动条,并使用xscrollcommand
和yscrollcommand
选项将它们连接到Canvas
对象,这两个选项分别引用scroll_x
和scroll_y
的set()
方法,这是负责移动滚动条滑块的方法。
定义Canvas
后,还需要配置每个滚动条的command
选项:
self.scroll_x = tk.Scrollbar(self, orient=tk.HORIZONTAL)
self.scroll_y = tk.Scrollbar(self, orient=tk.VERTICAL)
self.canvas = tk.Canvas(self, width=300, height=100,
xscrollcommand=self.scroll_x.set,
yscrollcommand=self.scroll_y.set)
self.scroll_x.config(command=self.canvas.xview)
self.scroll_y.config(command=self.canvas.yview)
也可以先创建Canvas
并在稍后实例化滚动条时配置其选项。
下一步是使用create_window()
方法将框架添加到可滚动的Canvas
。它采用的第一个参数是放置通过window
选项传递的小部件的位置。由于Canvas
小部件的x和y轴从左上角开始,我们将框架放置在(0, 0)
位置,并将其与anchor=tk.NW
对齐(西北):
self.frame = tk.Frame(self.canvas)
# ...
self.canvas.create_window((0, 0), window=self.frame, anchor=tk.NW)
然后,我们将使用rowconfigure()
和columnconfigure()
方法调整第一行和第一列的大小。weight
选项表示分配额外空间的相对权重,但在我们的例子中,没有更多的行或列需要调整大小。
绑定到<Configure>
事件将有助于我们在调整主窗口大小时正确重新配置canvas
。处理此类事件遵循与我们在上一章中看到的处理鼠标和键盘事件相同的原则:
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.bind("<Configure>", self.resize)
最后,我们将使用当前宽度和高度设置主窗口的最小大小,可以使用winfo_width()
和winfo_height()
方法检索。
为了获得容器的真实大小,我们必须通过调用update_idletasks()
强制几何体管理器首先绘制所有子部件。此方法在所有小部件类中都可用,并强制 Tkinter 处理所有挂起的空闲事件,例如重绘和几何体重新计算:
self.update_idletasks()
self.minsize(self.winfo_width(), self.winfo_height())
resize
方法处理窗口大小调整事件并更新scrollregion
选项,该选项定义可滚动的canvas
区域。为了方便地重新计算,您可以使用带有ALL
常量的bbox()
方法。这将返回整个画布小部件的边界框:
def resize(self, event):
region = self.canvas.bbox(tk.ALL)
self.canvas.configure(scrollregion=region)
当我们启动应用程序时,Tkinter 将自动触发几个<Configure>
事件,因此无需在__init__
方法末尾调用self.resize()
。
只有少数小部件类支持标准滚动选项:Listbox
、Text
和Canvas
允许xscrollcommand
和yscrollcommand
,而条目小部件只允许xscrollcommand
。我们已经了解了如何将此模式应用于canvas
,因为它可以用作通用解决方案,但您可以遵循类似的结构,使这些小部件中的任何一个都可以滚动和调整大小。
需要指出的另一个细节是,我们没有调用任何几何体管理器来绘制框架,因为create_window()
方法为我们完成了这项工作。为了更好地组织我们的应用程序类,我们可以将属于框架及其内部小部件的所有功能移动到专用的Frame
子类中。
- 处理鼠标和键盘事件配方
- 使用框架对小部件进行分组