网格几何管理器#

Grid 允许你在列和行中布局小部件。本章展示了你可以如何调整网格以获得对用户界面的完全控制。

GridTk 中可用的几种几何管理器之一,但它在功能、灵活性和易用性方面的综合优势使其成为通用的最佳选择。它的约束模型与当今依赖小部件对齐的布局方式非常契合。Tk 中还有其他几何管理器:Pack 也非常强大但较难使用和理解,而 Place 则让你完全掌控每个元素的位置。即使像窗格窗口、笔记本、画布和文本等稍后我们会探讨的小部件,也可以充当几何管理器。

列和行#

使用 Grid,为小部件分配 "column" 号和 "row" 号,以指示它们之间的相对位置。因此,同一列中的所有小部件将位于彼此之上或之下,而同一行中的所有小部件将位于彼此的左侧或右侧。

列号和行号必须是正整数(即,0, 1, 2, …)。你不必从 0 开始,也可以在列号和行号之间留出间隙(例如,列 121011122021)。如果你计划以后在用户界面中间添加更多小部件,这会很有用。

每列的宽度将根据列内包含的小部件的宽度而变化。每行的高度也是如此。这意味着在草拟你的用户界面并将其划分为行和列时,你不必担心每列或每行的宽度是否相等。

跨越多个单元格#

小部件可以占据 Grid 格中的多个单元格;为此,在将小部件放入 Grid 时使用 columnspanrowspan 选项。这些选项类似于 HTML 表格中的 "colspan""rowspan" 属性。

以下是创建用户界面的示例,其中包含多个小部件,有些小部件占据多个单元格。

from tkinter import ttk, Tk
from tkinter import BooleanVar
# 创建根 widget(根节点)
root = Tk()
# 创建子节点
content = ttk.Frame(root)
frame = ttk.Frame(content, borderwidth=5,
                  relief="sunken", width=200, height=100)
namelbl = ttk.Label(content, text="Name")
name = ttk.Entry(content)

# 创建跟踪变量
onevar = BooleanVar()
twovar = BooleanVar()
threevar = BooleanVar()
onevar.set(True)
twovar.set(False)
threevar.set(True)

# 设置按钮节点
one = ttk.Checkbutton(content, text="One", variable=onevar)
two = ttk.Checkbutton(content, text="Two", variable=twovar)
three = ttk.Checkbutton(content, text="Three", variable=threevar)
ok = ttk.Button(content, text="Okay")
cancel = ttk.Button(content, text="Cancel")

# 设置几何管理器
content.grid(column=0, row=0)
frame.grid(column=0, row=0, columnspan=3, rowspan=2)
namelbl.grid(column=3, row=0, columnspan=2)
name.grid(column=3, row=1, columnspan=2)
one.grid(column=0, row=3)
two.grid(column=1, row=3)
three.grid(column=2, row=3)
ok.grid(column=3, row=3)
cancel.grid(column=4, row=3)

# 启动主循环
root.mainloop()

显示结果为:

Grid 的例子

单元格内的布局#

列的宽度(以及行的高度)取决于其中包含的所有小部件。这意味着某些小部件可能会小于它们所在的单元格。如果是这样,它们应该被放置在单元格内的什么位置?

默认情况下,如果单元格大于其中包含的小部件,该小部件将被水平和垂直居中放置。主容器的背景色将显示在小部件周围的空白处。在下图中,右上角的小部件比分配给它的单元格要小。主容器的(白色)背景填充了其余的单元格空间。

单元格内的布局和“sticky”选项。

“sticky”选项可以改变这种默认行为。它的值是包含 0 个或更多指南针方向(nsew)的字符串,指定小部件应该“粘附”在单元格的哪些边缘。例如,值为 n(北)将把小部件挤到顶部,任何多余的垂直空间将在底部;小部件仍然会在水平方向上居中。值为 nw(西北)意味着小部件会粘附在左上角,底部和右侧有额外的空间。

小技巧

在 Tkinter 中,你也可以将其指定为包含 NSEW 的列表。

指定两个相对的边缘,比如 we(西、东),意味着小部件将被拉伸。在这种情况下,它将粘附在单元格的左右边缘。因此,小部件会比其“理想”尺寸更宽。

如果你希望小部件扩展以填满整个单元格,请使用 sticky 值为 nsew(北、南、东、西),这意味着它会粘附在每一侧。这在上图的左下角的小部件中有所展示。

大多数小部件都有一些选项,可以控制它们在超出所需大小时如何显示。例如,标签小部件有一个锚点选项,可以控制标签文本在小部件边界内的位置。上图中的左下角标签使用了默认的锚点(w,即左侧,垂直居中)。

处理调整大小时的网格布局#

即使你查看了下面的代码并在我们的示例中添加了额外的粘性选项,你仍然会看到同样的情况。看起来 sticky 可能会告诉 Tk 如何处理单元格的行或列是否调整大小,但实际上并没有说如果有任何额外的空间可用,行或列应该调整大小。让我们修正这个问题。

网格中的每个列和行都有与之关联的权重选项。这告诉网格如果有额外的空间需要填充时,列或行应该扩展多少。默认情况下,每列或每行的权重为 0,意味着它不会扩展以填补任何额外空间。

为了使用户界面能够调整大小,需要为我们希望扩展的列和行指定正的权重。你必须至少为列和行提供权重。这是通过使用 gridcolumnconfigurerowconfigure 方法来完成的。这个权重是相对的。如果两列具有相同的权重,它们将以相同的速率扩展。在我们的示例中,我们将给最左边的三列(持有复选按钮)赋予权重3,而最右边的两列赋予权重1。对于右边列每增长一像素,左边列将增长三像素。因此,当窗口变得更大时,大部分额外的空间将归于左侧。

填充网格#

通常,每一列或每一行都会直接相邻,这样小部件就会紧挨着彼此。有时这是你想要的(比如一个列表框和它的滚动条),但通常你希望在小部件之间有一些空间。在 Tk 中,这被称为填充,有几种方法可以选择添加它。

我们已经实际上看到了一种方法,那就是使用小部件自己的选项来增加周围的额外空间。并非所有小部件都有这个功能,但有一个是框架;这是有用的,因为框架最常被用作其他小部件的主容器。框架的填充选项让你可以在框架内部指定一些额外的填充,无论是四个边相同数量还是每个边不同。

第二种方法是在添加小部件时使用 padxpady 网格选项。正如你所期待的,padx 在左右两边增加一些额外的空间,而 pady 在顶部和底部增加额外的空间。选项的单个值会在左右(或上下)两边放置相同的填充,而两个值的列表让你可以在左右(或上下)两边放置不同的量。请注意,这种额外的填充是在包含小部件的网格单元内。

如果你想在整个行或列周围添加填充,columnconfigurerowconfigure 方法接受 pad 选项,这将为你完成这项工作。

让我们在我们的示例中添加额外的粘性、调整大小和填充行为(新增部分用粗体显示)。

from tkinter import *
from tkinter import ttk

root = Tk()

content = ttk.Frame(root, padding=(3,3,12,12))
frame = ttk.Frame(content, borderwidth=5, relief="ridge", width=200, height=100)
namelbl = ttk.Label(content, text="Name")
name = ttk.Entry(content)

onevar = BooleanVar()
twovar = BooleanVar()
threevar = BooleanVar()

onevar.set(True)
twovar.set(False)
threevar.set(True)

one = ttk.Checkbutton(content, text="One", variable=onevar, onvalue=True)
two = ttk.Checkbutton(content, text="Two", variable=twovar, onvalue=True)
three = ttk.Checkbutton(content, text="Three", variable=threevar, onvalue=True)
ok = ttk.Button(content, text="Okay")
cancel = ttk.Button(content, text="Cancel")

content.grid(column=0, row=0, sticky=(N, S, E, W))
frame.grid(column=0, row=0, columnspan=3, rowspan=2, sticky=(N, S, E, W))
namelbl.grid(column=3, row=0, columnspan=2, sticky=(N, W), padx=5)
name.grid(column=3, row=1, columnspan=2, sticky=(N,E,W), pady=5, padx=5)
one.grid(column=0, row=3)
two.grid(column=1, row=3)
three.grid(column=2, row=3)
ok.grid(column=3, row=3)
cancel.grid(column=4, row=3)

root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
content.columnconfigure(0, weight=3)
content.columnconfigure(1, weight=3)
content.columnconfigure(2, weight=3)
content.columnconfigure(3, weight=1)
content.columnconfigure(4, weight=1)
content.rowconfigure(1, weight=1)

root.mainloop()

查询和更改网格选项#

像小部件本身一样,很容易内省各种网格选项并进行更改。 初次网格化小部件时设置选项只是一种方便,您当然可以随时更改它们。

“slaves”(从属)方法将告诉您已在主对象内网格化的所有小部件,或仅在特定列或行内的那些小部件。 “info” 方法将为您提供小部件的所有网格选项及其值的列表。最后,“configure”(配置)方法使您可以更改小部件上的一个或多个网格选项。

这些在此交互式会话中进行了说明:

查询和更改网格选项

内部填充#

你已经看到padxpady 网格选项如何在小部件外部添加额外的空间。还有一种较少使用的填充类型,称为“内部填充”,由网格选项 ipaddxipaddy 控制。

这种差异可能很微妙。假设你有 20x20 的框架,并在每边指定了5像素的常规(外部)填充。框架将从几何管理器请求 20x20 的矩形(其自然大小)。通常,这就是它被授予的,所以它会获得 20x20 的矩形用于框架,周围有 5 像素的边框。

对于内部填充,几何管理器在确定其自然大小时,实际上会将额外的填充添加到小部件中,就好像小部件请求了 30x30 的矩形。如果框架被居中或附着在边或角落(使用 sticky),我们将以 20x20 的框架结束,周围有额外的空间。然而,如果框架被设置为伸展(即,sticky 值为 we, ns, 或 nwes),它将填充额外的空间,导致没有边框的 30x30 的框架。

忘记和移除#

gridforget 方法将子部件从小部件当前所在的网格中移除。它接受一个或多个奴隶小部件作为参数。这并不会完全销毁小部件,而是将其从屏幕上移除,就好像它从未被放置在网格中一样。你可以稍后再次将其放入网格,尽管你最初分配的任何网格选项都将丢失。

gridremove 方法工作方式相同,不同之处在于如果你稍后再次将其放入网格,网格选项将被记住。

嵌套布局#

随着你的用户界面变得更加复杂,组织所有小部件的网格也会变得越来越复杂。这可能会使更改和维护你的程序非常困难。

幸运的是,你不必用单个网格管理整个用户界面。如果你的用户界面中有一块区域相对独立于其他区域,创建一个新的框架并将该区域内的小部件放入该框架中。例如,如果你正在构建一个具有多个调色板、工具栏等的图形编辑器,这些区域中的每个都可能是一个放入自己框架中的候选者。

理论上,这些带有自己的网格的框架可以任意深度地嵌套,尽管在实践中,这通常不会超过几个层级。这对于模块化你的程序非常有帮助。例如,如果你有一个绘图工具的调色板,你可以在一个单独的函数或类中创建整个东西。它将负责创建所有组件小部件,将它们放在一起,设置事件绑定等。那个调色板内部如何工作的详细信息可以包含在那一段代码中。你的主程序只需要知道包含你的调色板的单个框架小部件。

我们的例子只是展示了这一点:一个内容框架被放入主窗口,然后所有其他小部件都被放入内容框架中。

随着你自己的程序增长,你可能会遇到这样的情况:改变界面某一部分的布局需要对另一部分的布局进行代码更改。这可能是一个重新考虑你如何使用网格以及是否将组件拆分到单独的框架中的线索。