原文:https://tkdocs.com/tutorial/concepts.html#widgets
几何管理知识概念
有了第一个示例,您现在对 Tk 程序的外观以及为使其工作所需编写的代码类型有了基本的了解。在本章中,我们将退后一步,看看您需要了解的三个广泛的概念来理解 Tk:小部件、几何管理和事件处理。
小部件
小部件是您在屏幕上看到的所有东西。在我们的示例中,我们有一个按钮、一个输入框、几个标签和一个框架。其他是复选框、树图、滚动条、文本区域等。小部件通常称为“控件”。您有时还会看到它们被称为“窗口”,尤其是在 Tk 的文档中。
这是一个示例,显示了一些 Tk 的小部件,我们将在稍后单独介绍。
几个 Tk 小部件。
小部件类
小部件是对象,代表按钮、框架等的类的实例。当您想要创建一个小部件时,您需要做的第一件事是确定您要实例化的小部件的特定类。本教程和小部件综述将对此有所帮助。
小部件层次结构
除了小部件类之外,您还需要另一条信息来创建它:它的parent。小部件不会漂浮在空间中。相反,它们包含在其他东西中,例如窗口。在 Tk 中,所有小部件都是小部件(或窗口)层次结构的一部分,在层次结构的顶部有一个根。
在我们的度量转换示例中,我们有一个作为根窗口的子窗口而创建的框架,并且该框架将所有其他控件作为子窗口。根窗口是 框架的容器,因此是框架的父窗口。该示例的完整层次结构如下所示:
指标转换示例的小部件层次结构。
此层次结构可以任意深,因此您可能在根窗口内的另一个框架中的某个框架中有一个按钮。甚至应用程序中的新窗口(通常称为toplevel)也是同一层次结构的一部分。该窗口及其所有内容构成了整个小部件层次结构的子树。
更实质的应用程序的层次结构。叶节点(按钮、标签等)被省略。
创建小部件
每个单独的小部件都是一个 Python 对象。实例化小部件时,您必须将其父级作为参数传递给小部件类。唯一的例外是“根”窗口,这是顶层窗口将包含一切。这是您实例化时自动创建的Tk
。它没有父级。例如:
root = Tk()
content = ttk.Frame(root)
button = ttk.Button(content)
是否将小部件对象保存在变量中完全取决于您,这取决于您以后是否需要引用它。因为对象在被插入到小部件层次结构中,即使您不保留对它的引用,它也不会被垃圾收集。
如何管理小部件?你会看到每个小部件都有一个特定的路径名;您还将在 Tk 参考文档中看到此路径名。Tkinter 在幕后为您选择和管理所有这些路径名,因此您永远不必担心它们。您可以通过调用str(widget)
获取部件路径名。
配置选项
所有小部件都有几个配置选项。这些控制小部件的显示方式或行为方式。
当然,小部件的可用选项取决于小部件类。不同的小部件类之间有很多一致性,因此几乎相同的选项往往被命名为相同的。例如,按钮和标签都有一个text
选项来调整小部件显示的文本,而滚动条则没有text
选项,因为它不需要。类似地,按钮有一个command
选项告诉它按下时要做什么,而标签只包含静态文本,则没有。
可以在首次创建小部件时通过指定可选参数的名称和值来设置配置选项。稍后,您可以检索这些选项的当前值,除了极少数例外,您可以随时更改它们。
如果您不确定小部件支持哪些配置选项,您可以要求小部件描述它们。这为您提供了所有选项的长列表。
通过以下与解释器的交互对话可以最好地说明这一点。
>>> from tkinter import *
>>> from tkinter import ttk
>>> root = Tk()
# create a button, passing two options:
>>> button = ttk.Button(root, text="Hello", command="buttonpressed")
>>> button.grid()
# check the current value of the text option:
>>> button['text']
'Hello'
# change the value of the text option:
>>> button['text'] = 'goodbye'
# another way to do the same thing:
>>> button.configure(text='goodbye')
# check the current value of the text option:
>>> button['text']
'goodbye'
# get all information about the text option:
>>> button.configure('text')
('text', 'text', 'Text', '', 'goodbye')
get information on all options for this widget:
>>> button.configure()
{'cursor' : ('cursor', 'cursor', 'Cursor', '', ''),
'style' : ('style', 'style', 'Style', '', ''),
'default' : ('default', 'default', 'Default', <index object at 0x00DFFD10>, <index object at 0x00DFFD10>),
'text' : ('text', 'text', 'Text', '', 'goodbye'),
'image' : ('image', 'image', 'Image', '', ''),
'class' : ('class', '', '', '', ''),
'padding' : ('padding', 'padding', 'Pad', '', ''),
'width' : ('width', 'width', 'Width', '', ''),
'state' : ('state', 'state', 'State', <index object at 0x0167FA20>, <index object at 0x0167FA20>),
'command' : ('command', 'command' , 'Command', '', 'buttonpressed'),
'textvariable': ('textvariable', 'textVariable', 'Variable', '', ''),
'compound' : ('compound', 'compound', 'Compound', <index object at 0x0167FA08>, <index object at 0x0167FA08>),
'underline' : ('underline', 'underline', 'Underline', -1, -1),
'takefocus' : ('takefocus', 'takeFocus', 'TakeFocus', '', 'ttk::takefocus')}
如您所见,对于每个选项,Tk 将显示选项的名称及其当前值(以及您通常不需要担心的其他三个属性)。
每个配置选项提供五个数据信息。最有用的是第一个,它是选项的名称,第五个,它是选项的当前值。第四个是选项的默认值,或者换句话说,如果你不改变它的值。另外两个与称为选项数据库的东西有关。我们在讨论菜单时会涉及到它,但它不是现代应用程序中使用的东西。第二项是数据库中选项的名称,第三项是它的类。
检查小部件
Tk 公开了有关您的应用程序可以利用的每个小部件的信息宝库。其中大部分可通过winfo
获得;winfo
有关完整详细信息,请参阅命令参考。
这个简短的示例遍历小部件层次结构,使用每个小部件的winfo_children
方法来识别需要检查的任何子小部件。对于每个小部件,我们打印一些基本信息,包括它的类(按钮、框架等)、它的宽度和高度,以及它相对于它的父级的位置。
def print_hierarchy(w, depth=0):
print(' '*depth + w.winfo_class() +
' w=' + str(w.winfo_width()) +
' h=' + str(w.winfo_height()) +
' x=' + str(w.winfo_x()) +
' y=' + str(w.winfo_y()))
for i in w.winfo_children():
print_hierarchy(i, depth+1)
print_hierarchy(root)
以下是一些最有用的方法:
名称 | 介绍 |
---|---|
winfo_class : | 标识小部件类型的类,例如TButton 主题按钮 |
winfo_children : | 作为层次结构中小部件的直接子代的小部件列表 |
winfo_parent : | 层次结构中小部件的父级 |
winfo_toplevel : | 包含此小部件的顶级窗口 |
winfo_width, winfo_height : | 小部件的当前宽度和高度;直到出现在屏幕上才准确 |
winfo_reqwidth, winfo_reqheight : | 几何管理器的小部件请求的宽度和高度(稍后会详细介绍) |
winfo_x, winfo_y : | 小部件左上角相对于其父级的位置 |
winfo_rootx, winfo_rooty : | 小部件左上角相对于整个屏幕的位置 |
winfo_vieweable : | 小部件是显示还是隐藏(层次结构中的所有祖先都必须可见才能可见) |
几何管理
如果您一直以交互方式运行代码,您可能已经注意到,仅通过创建小部件,它们并没有出现在屏幕上。将小部件放置在屏幕上并准确放置是一个单独的步骤,称为几何管理。
在示例中,定位每个小部件是由grid
命令完成的。我们指定了希望每个小部件进入的列和行,如何在网格内对齐事物,等等。网格是几何管理器的一种 (在 Tk 中有几个,网格是最有用的)。现在,我们将大致了解几何管理;我们将在后面的章节中讨论网格。
几何经理是确定这些小部件将被放置在哪里。结果证明这是一个复杂的优化问题,一个好的几何管理器依赖于非常复杂的算法。一个好的几何管理器提供了让程序员满意的灵活性、强大功能和易用性。它还可以轻松创建美观的用户界面布局。grid
毫无疑问是Tk中最好的之一。
我们将在后面的章节中更详细地介绍,但grid
在 Tk 流行几年后才被引入。在此之前,pack
最常用的旧几何管理器。它非常强大,但使用起来要困难得多,并且很难创建在今天看起来很有吸引力的布局。不幸的是,大部分示例 Tk 代码和文档都使用了pack
而不是grid
。广泛使用pack
是许多 Tk 用户界面看起来很糟糕的主要原因之一。使用 grid
开始新代码,并尽可能升级旧代码。
问题
几何管理器的问题是获取程序创建的所有不同小部件,以及程序关于每个小部件应该在窗口中的位置的指令(明确地,或者更常见的是,相对于其他小部件),然后实际将它们定位在窗口中.
这样做时,几何管理器必须平衡多个约束。考虑这些情况:
- 小部件可以具有自然大小,例如,标签的自然宽度将取决于它显示的文本和用于显示它的字体。
- 如果包含所有不同小部件的应用程序窗口不足以容纳它们怎么办?几何管理器必须决定缩小哪些小部件以适应其大小,缩小多少等等。
- 如果应用程序窗口大于所有小部件的自然大小,那么额外的空间如何使用?每个小部件之间是否放置了额外的空间,如果是,该空间是如何分布的?它是否用于使某些小部件比它们通常想要的更大,例如加大文本输入框以填充更宽的窗口?哪些小部件应该增长?
- 如果应用程序窗口被调整大小,其中每个小部件的大小和位置如何变化?某些区域(例如,文本输入区域)会扩大或缩小而其他部分保持相同大小,还是区域分布不同?某些小部件是否具有您想要避免低于的最小尺寸?最大尺寸?窗口本身是否有最小或最大尺寸?
- 用户界面不同部分的小部件如何相互对齐?他们之间应该留多少空间?这是呈现干净布局并遵守特定于平台的用户界面指南所必需的。
- 对于一个复杂的用户界面,它可能有许多框架嵌套在窗口中的其他框架中(等),如何完成上述所有操作,权衡整个用户界面不同部分的冲突需求?
几何管理怎么运作
在Tk的几何管理依赖于概念主与从部件。master 是一个小部件,通常是顶级应用程序窗口或框架,其中包含的其他小部件称为slave
部件。您可以想象一个几何管理器控制主小部件并决定如何在其中显示所有从属小部件。
Tk 核心将慢慢采用一套更具包容性的术语。例如,在有意义的地方,“父”和“子”将比“主”和“从”更受欢迎。为了保持向后兼容性,当前的术语不会消失。这是未来需要注意的事情。有关更多详细信息,请参阅 提示 #581。
您的程序告诉几何管理器要在主站内管理哪些从站,即通过调用grid
. 您的程序还提供了有关如何显示每个从站的提示,例如,通过column
和row
选项。您还可以向几何管理器提供其他内容。例如,如果窗口中有额外的可用空间,我们使用columnconfigure
和rowconfigure
来指示我们想要扩展的列和行。值得注意的是,所有这些参数和提示都特定于grid
; 其他几何管理器会使用不同的几何管理器。
几何管理器获取有关主站中从站的所有信息,以及有关主站有多大的信息。然后它询问每个从属部件的自然尺寸,即理想情况下它希望显示多大。几何管理器的内部算法计算每个从站将被分配的区域(如果有的话!)。然后,从设备负责在该特定矩形内呈现自己。当然,任何时候 master 的大小发生变化(例如,因为顶层窗口被调整了大小),slave 的自然大小发生了变化(例如,因为我们改变了标签中的文本),或者任何几何图形管理器参数改变(例如,像row
、column
、 或sticky
)我们重复整个事情。
这一切也是递归的。在我们的示例中,我们在顶级应用程序窗口中有一个内容框架,然后在内容框架中有几个其他小部件。因此,我们必须管理两个不同母版的几何图形。在外层,顶层窗口是主窗口,内容框架是它的从窗口。在内部级别,内容框架是主,而其他每个小部件都是从。请注意,同一个小部件,例如内容框架,既可以是主部件,也可以是从属部件!正如我们之前看到的,这个小部件层次结构可以嵌套得更深。
虽然每个母版只能由一个几何管理器(例如grid
)管理,但不同的母版可以有不同的几何管理器。虽然grid
大多数时候是正确的选择,但其他人可能对用户界面的某一部分中使用的特定布局有意义。其他 Tk 几何管理器包括pack
和place
,这让所有布局决策完全由您决定。一些复杂的小部件喜欢canvas
和text
嵌入其他小部件,使它们成为事实上的几何管理器。
最后,我们一直假设从属部件是其主部件在部件层次结构中的直接子代。虽然通常是这种情况,而且大多数情况下没有充分的理由以任何其他方式这样做,但也有可能(有一些限制)解决这个问题。
事件处理
与大多数其他用户界面工具包一样,Tk 运行一个从操作系统接收事件的事件循环。这些是按钮按下、击键、鼠标移动、窗口大小调整等。
通常,Tk 会为您管理此事件循环。它将找出事件适用于哪个小部件(用户是否单击了此按钮?如果按下某个键,哪个文本框具有焦点?),并相应地调度它。单个小部件知道如何响应事件;例如,当鼠标移到按钮上时,按钮可能会改变颜色,并在鼠标离开时恢复颜色。
在事件驱动的应用程序中,事件循环不被阻塞是至关重要的。事件循环应该连续运行,通常每秒执行几十个步骤。在每一步,它都会处理一个事件。如果您的程序正在执行长时间操作,则可能会阻塞事件循环。在这种情况下,不会处理任何事件,不会进行绘图,并且看起来好像您的应用程序被冻结了。有很多方法可以避免这种情况发生,主要与应用程序的结构有关。我们将在后面的章节中更详细地讨论这一点。
命令回调
您通常希望您的程序以特定方式处理某些事件,例如,按下按钮时执行某些操作。对于那些最常定制的事件(按下按钮时没有发生任何事情的按钮有什么用?),小部件将允许您将回调指定为小部件配置选项。我们在带有command
按钮选项的示例中看到了这一点。
def calculate(*args):
...
ttk.Button(mainframe, text="Calculate", command=calculate)
Tk 中的回调往往比使用编译语言的用户界面工具包更简单(其中回调必须是具有特定参数集的过程或具有特定签名的对象方法)。相反,回调只是解释器评估的一段普通代码。虽然它可以像您想要的那样复杂,但大多数时候,您只希望您的回调调用其他过程。
绑定到事件
对于没有关联bind
事件命令回调的特定于小部件,您可以使用 Tk来捕获任何事件,然后(与回调一样)执行任意一段代码。
这是一个(愚蠢的)示例,显示了响应不同事件的标签。当事件发生时,标签中会显示该事件的描述。
from tkinter import *
from tkinter import ttk
root = Tk()
l =ttk.Label(root, text="Starting...")
l.grid()
l.bind('<Enter>', lambda e: l.configure(text='Moved mouse inside'))
l.bind('<Leave>', lambda e: l.configure(text='Moved mouse outside'))
l.bind('<ButtonPress-1>', lambda e: l.configure(text='Clicked left mouse button'))
l.bind('<3>', lambda e: l.configure(text='Clicked right mouse button'))
l.bind('<Double-1>', lambda e: l.configure(text='Double clicked'))
l.bind('<B3-Motion>', lambda e: l.configure(text='right button drag to %d,%d' % (e.x, e.y)))
root.mainloop()
前两个绑定非常简单,只是观察简单的事件。一个<Enter>
事件是指移动鼠标经过顶部窗口小部件,而<Leave>
产生的事件是鼠标移动到不同的部件时。
下一个绑定查找鼠标单击<ButtonPress-1>
事件。此处,<ButtonPress>
是实际事件,但-1
是指定鼠标左(主)按钮的事件信息。绑定只会在<ButtonPress>
生成涉及鼠标主按钮的事件时触发。如果单击了另一个鼠标按钮,则此绑定将忽略它。
下一个绑定查找<3>
事件。这实际上是<ButtonPress-3>
. 它将响应单击鼠标右键时生成的事件。下一个绑定<Double-1>
(<Double-ButtonPress-1>
的简写)添加了另一个修饰符 , Double
将响应双击鼠标左键。
最后一个绑定还使用了一个修饰符:捕获鼠标移动 ( Motion
),但仅限于按住鼠标右键 ( B3
) 时。此绑定还显示了如何使用事件参数的示例。许多事件,例如鼠标点击或移动,都带有附加信息,例如鼠标的当前位置。Tk 通过使用百分比替换来提供对 Tcl 回调脚本中这些参数的访问。这些百分比替换让您可以捕获它们,以便在您的脚本中使用它们。
Tkinter 抽象掉了这些百分比替换,而是将所有事件参数封装在一个事件对象中。 上面,我们使用x
和y
字段来检索鼠标位置。我们稍后会在另一个上下文中看到百分比替换,即输入框小部件验证。
为什么用lambda
?Tkinter 期望你提供一个函数作为事件回调,它的第一个参数是一个事件对象,表示触发回调的事件。有时不值得为一次性琐碎回调定义常规命名函数,例如在本例中。相反,我们只是通过lambda
. 在实际应用中,您几乎总是会使用常规函数或者对象的方法,例如我们的英尺到米示例中的calculate
函数。
正如我们在此处以及在整个 Tk 文档中所描述的,Tk 中的事件被尖括号包围,例如<Enter>
.
一个事件的多个绑定
我们刚刚看到了如何为单个小部件设置事件绑定。当该小部件接收到匹配事件时,将触发绑定。但这并不是你能做的全部。
您的绑定不仅可以捕获单个事件,还可以捕获短的事件序列。<Double-1>
结合触发了当鼠标两次点击发生在很短的时间。你可以做同样的事情来捕捉连续按下的两个键,例如,<KeyPress-A><KeyPress-B>
或者简单地<ab>
。
您还可以在顶级窗口上设置事件绑定。当该窗口中的任何位置发生匹配事件时,将触发绑定。在我们的示例中,我们为主应用程序顶层窗口上的 Return 键设置了一个绑定。如果在顶级窗口中的任何小部件具有焦点时按下 Return 键,则该绑定将触发。
不太常见的是,您可以创建事件绑定,当匹配事件发生在应用程序的任何地方时触发,甚至可以为给定类的任何小部件(例如,所有按钮)接收的事件创建事件绑定。
一个事件可以触发多个绑定。这使事件处理程序保持简洁且范围有限,这意味着更多的模块化代码。例如,Tk 中每个小部件类的行为本身都是用脚本级事件绑定定义的。这些与应用程序中的事件绑定是分开的。还可以更改或删除事件绑定。可以修改它们以更改特定类或应用程序部分的小部件的事件处理。您可以重新排序、扩展或更改将为每个小部件触发的事件绑定的顺序;bindtags
如果您好奇,请参阅命令参考。
可用事件
下面描述了最常用的事件,以及它们生成时的情况。有些是在某些平台上生成的,而不是在其他平台上生成的。有关所有不同事件名称、修饰符和每个可用的不同事件参数的完整描述,最好查看 bind
命令参考。
-
<Activate>
:窗口已变为活动状态。
-
<Deactivate>
:窗口已停用。
-
<MouseWheel>
:鼠标滚轮已移动。
-
<KeyPress>
:键盘上的键被按下。
-
<KeyRelease>
:释放键盘上的键。
-
<ButtonPress>
:已按下鼠标按钮。
-
<ButtonRelease>
:鼠标按钮已被释放。
-
<Motion>
:鼠标已移动。
-
<Configure>
:小部件已更改大小或位置。
-
<Destroy>
:小部件正在被销毁。
-
<FocusIn>
:小部件已获得键盘焦点。
-
<FocusOut>
:小部件失去了键盘焦点。
-
<Enter>
:鼠标指针进入小部件。
-
<Leave>
:鼠标指针离开小部件。
鼠标事件的事件详细信息是按下的按钮,例如1
、2
、 或3
。对于键盘事件,它是特定的键,例如A
, 9
, space
, plus
, comma
, equal
。可以在keysyms
命令参考中找到完整列表 。
事件修饰符包括,例如B1
或Button1
表示按住鼠标主键, Double
或Triple
用于同一事件的序列。对于当键盘上的按键内嵌按住键修饰符Control
,Shift
,Alt
,Option
,和Command
。
虚拟活动
到目前为止,我们看到的事件是低级操作系统事件,例如鼠标单击和窗口大小调整。许多小部件还生成称为虚拟事件的更高级别或语义事件。这些由事件名称周围的双尖括号表示,例如,<<foo>>
。
例如,一个列表框小部件将<<ListboxSelect>>
在其选择更改时生成一个虚拟事件。无论用户单击某个项目、使用箭头键移动到该项目,还是其他方式,都会生成相同的虚拟事件。虚拟事件避免了设置多个可能特定于平台的事件绑定来捕获常见更改的问题。小部件的可用虚拟事件将在小部件类的文档中列出。
Tk 还为不同平台以不同方式触发的常见操作定义了虚拟事件。这些包括<<Cut>>
、<<Copy>>
和<<Paste>>
。
您可以定义自己的虚拟事件,这可以特定于您的应用程序。当您在整个应用程序中使用虚拟事件时,这可能是将特定于平台的详细信息隔离在单个模块中的有用方法。您自己的代码可以生成虚拟事件,其工作方式与 Tk 生成的虚拟事件完全相同。
root.event_generate("<<MyOwnEvent>>")
event generate . "<<MyOwnEvent>>"
Tk.event_generate(root, "<MyOwnEvent>")
Tkx::event_generate($mw, "<<MyOwnEvent>>");