作为一种更多用途的编程语言之一,Python以其“电池包含”哲学而闻名,其中包括其标准库中丰富的模块集;Tkinter是用于构建桌面应用程序的库。Tkinter是建立在TkGUI工具包之上的,是快速GUI开发的常见选择,复杂的应用程序可以从该库的全部功能中受益。本书涵盖了Tkinter和PythonGUI开发的所有问题和解决方案。
通过本书,您将深入了解Tkinter类,并知道如何使用它们构建高效和丰富的GUI应用程序。
这本书的目标读者是熟悉Python语言基础知识(语法、数据结构和面向对象编程)的开发人员,希望学习GUI开发常见挑战的有效解决方案,并希望发现Tkinter可以提供的有趣功能,以构建复杂的应用程序。
您不需要有Tkinter或其他GUI开发库的先前经验,因为本书的第一部分将通过介绍性用例教授库的基础知识。
第一章,开始使用Tkinter,介绍了Tkinter程序的结构,并向您展示如何执行最常见的任务,例如创建小部件和处理用户事件。
第二章,窗口布局,演示了如何使用几何管理器放置小部件并改进大型应用程序的布局。
第三章,自定义小部件,深入探讨了Tkinter小部件的配置和外观自定义。
第四章,对话框和菜单,教会您如何通过菜单和对话框改进Tkinter应用程序的导航。
第五章,面向对象编程和MVC,教会您如何在Tkinter应用程序中有效应用设计模式。
第七章,画布和图形,探索了画布小部件以及您可以添加到画布的项目类型以及如何操作它们。
第八章,主题小部件,教会您如何使用Tk主题小部件集扩展Tkinter应用程序。
要开始并运行,用户需要安装以下技术:
您可以按照以下步骤下载代码文件:
文件下载后,请确保使用最新版本的软件解压或提取文件夹。
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟URL,用户输入和Twitter句柄。这是一个例子:"delete()方法接受两个参数,指示应删除的字符范围。"
代码块设置如下:
defshow_caption(self,event):caption=tk.Label(self,...)caption.place(in_=event.widget,x=event.x,y=event.y)#...粗体:表示一个新术语,一个重要单词,或者您在屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中以这种方式出现。这是一个例子:"第一个将被标记为选择文件。"
警告或重要提示会以这种方式出现。提示和技巧会以这种方式出现。
在本章中,我们将涵盖以下内容:
由于其清晰的语法和广泛的库和工具生态系统,Python已经成为一种流行的通用编程语言。从Web开发到自然语言处理(NLP),您可以轻松找到一个符合您应用领域需求的开源库,最后,您总是可以使用Python标准库中包含的任何模块。
标准库遵循“电池包含”哲学,这意味着它包含了大量的实用程序:正则表达式、数学函数、网络等。该库的标准图形用户界面(GUI)包是Tkinter,它是Tcl/Tk的一个薄的面向对象的层。
从Python3开始,Tkinter模块被重命名为tkinter(小写的t)。它也影响到tkinter.ttk和tkinter.tix扩展。我们将在本书的最后一章深入探讨tkinter.ttk模块,因为tkinter.tix模块已经正式弃用。
在本章中,我们将探索tkinter模块的一些基本类的几种模式以及所有小部件子类共有的一些方法。
使用Tkinter制作应用程序的主要优势之一是,使用几行脚本非常容易设置基本GUI。随着程序变得更加复杂,逻辑上分离每个部分变得更加困难,因此有组织的结构将帮助我们保持代码整洁。
我们将以以下程序为例:
fromtkinterimport*root=Tk()btn=Button(root,text="Clickme!")btn.config(command=lambda:print("Hello,Tkinter!"))btn.pack(padx=120,pady=30)root.title("MyTkinterapp")root.mainloop()它创建一个带有按钮的主窗口,每次点击按钮时都会在控制台中打印Hello,Tkinter!。按钮在水平轴上以120px的填充和垂直轴上以30px的填充放置。最后一条语句启动主循环,处理用户事件并更新GUI,直到主窗口被销毁:
您可以执行该程序并验证它是否按预期工作。但是,所有我们的变量都是在全局命名空间中定义的,添加的小部件越多,理清它们的使用部分就变得越困难。
在生产代码中,强烈不建议使用通配符导入(from...import*),因为它们会污染全局命名空间——我们只是在这里使用它们来说明一个常见的反模式,这在在线示例中经常见到。
这些可维护性问题可以通过基本的面向对象编程技术来解决,在所有类型的Python程序中都被认为是良好的实践。
为了改进我们简单程序的模块化,我们将定义一个包装我们全局变量的类:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.btn=tk.Button(self,text="Clickme!",command=self.say_hello)self.btn.pack(padx=120,pady=30)defsay_hello(self):print("Hello,Tkinter!")if__name__=="__main__":app=App()app.title("MyTkinterapp")app.mainloop()现在,每个变量都被封装在特定的范围内,包括command函数,它被移动为一个单独的方法。
首先,我们用import...as语法替换了通配符导入,以便更好地控制我们的全局命名空间。
然后,我们将我们的App类定义为Tk子类,现在通过tk命名空间引用。为了正确初始化基类,我们将使用内置的super()函数调用Tk类的__init__方法。这对应以下行:
classApp(tk.Tk):def__init__(self):super().__init__()#...现在,我们有了对App实例的引用,使用self变量,所以我们将把所有的按钮小部件作为我们类的属性添加。
虽然对于这样一个简单的程序来说可能看起来有点过度,但这种重构将帮助我们理清每个部分,按钮实例化与单击时执行的回调分开,应用程序引导被移动到if__name__=="__main__"块中,这是可执行Python脚本中的常见做法。
我们将遵循这个约定通过所有的代码示例,所以您可以将这个模板作为任何更大应用程序的起点。
在我们的示例中,我们对Tk类进行了子类化,但通常也会对其他小部件类进行子类化。我们这样做是为了重现在重构代码之前的相同语句。
然而,在更大的程序中,比如有多个窗口的程序中,可能更方便地对Frame或Toplevel进行子类化。这是因为Tkinter应用程序应该只有一个Tk实例,如果在创建Tk实例之前实例化小部件,系统会自动创建一个Tk实例。
请记住,这个决定不会影响我们的App类的结构,因为所有的小部件类都有一个mainloop方法,它在内部启动Tk主循环。
按钮小部件表示GUI应用程序中可点击的项目。它们通常使用文本或指示单击时将执行的操作的图像。Tkinter允许您使用Button小部件类的一些标准选项轻松配置此功能。
以下包含一个带有图像的按钮,单击后会被禁用,并带有不同类型可用的relief的按钮列表:
importtkinterastkRELIEFS=[tk.SUNKEN,tk.RAISED,tk.GROOVE,tk.RIDGE,tk.FLAT]classButtonsApp(tk.Tk):def__init__(self):super().__init__()self.img=tk.PhotoImage(file="python.gif")self.btn=tk.Button(self,text="Buttonwithimage",image=self.img,compound=tk.LEFT,command=self.disable_btn)self.btns=[self.create_btn(r)forrinRELIEFS]self.btn.pack()forbtninself.btns:btn.pack(padx=10,pady=10,side=tk.LEFT)defcreate_btn(self,relief):returntk.Button(self,text=relief,relief=relief)defdisable_btn(self):self.btn.config(state=tk.DISABLED)if__name__=="__main__":app=ButtonsApp()app.mainloop()这个程序的目的是显示在创建按钮小部件时可以使用的几个配置选项。
在执行上述代码后,您将得到以下输出:
Button实例化的最基本方法是使用text选项设置按钮标签和引用在按钮被点击时要调用的函数的command选项。
在我们的示例中,我们还通过image选项添加了PhotoImage,它优先于text字符串。compound选项用于在同一个按钮中组合图像和文本,确定图像放置的位置。它接受以下常量作为有效值:CENTER、BOTTOM、LEFT、RIGHT和TOP。
第二行按钮是用列表推导式创建的,使用了RELIEF值的列表。每个按钮的标签对应于常量的名称,因此您可以注意到每个按钮外观上的差异。
为了避免这种情况,始终记住在窗口仍然存在时保留对每个PhotoImage对象的引用。
Entry小部件表示以单行显示的文本输入。它与Label和Button类一样,是Tkinter类中最常用的类之一。
importtkinterastkclassLoginApp(tk.Tk):def__init__(self):super().__init__()self.username=tk.Entry(self)self.password=tk.Entry(self,show="*")self.login_btn=tk.Button(self,text="Login",command=self.print_login)self.clear_btn=tk.Button(self,text="Clear",command=self.clear_form)self.username.pack()self.password.pack()self.login_btn.pack(fill=tk.BOTH)self.clear_btn.pack(fill=tk.BOTH)defprint_login(self):print("Username:{}".format(self.username.get()))print("Password:{}".format(self.password.get()))defclear_form(self):self.username.delete(0,tk.END)self.password.delete(0,tk.END)self.username.focus_set()if__name__=="__main__":app=LoginApp()app.mainloop()Login按钮在控制台中打印值,而Clear按钮删除两个输入框的内容,并将焦点返回到username的输入框:
使用父窗口或框架作为第一个参数实例化Entry小部件,并使用一组可选关键字参数来配置其他选项。我们没有为对应username字段的条目指定任何选项。为了保持密码的机密性,我们使用字符串"*"指定show参数,它将显示每个键入的字符为星号。
使用get()方法,我们将检索当前文本作为字符串。这在print_login方法中用于在标准输出中显示条目的内容。
delete()方法接受两个参数,指示应删除的字符范围。请记住,索引从位置0开始,并且不包括范围末尾的字符。如果只传递一个参数,它将删除该位置的字符。
在clear_form()方法中,我们从索引0删除到常量END,这意味着整个内容被删除。最后,我们将焦点设置为username条目。
可以使用insert()方法以编程方式修改Entry小部件的内容,该方法接受两个参数:
使用delete()和insert()的组合可以实现重置条目内容为默认值的常见模式:
entry.delete(0,tk.END)entry.insert(0,"defaultvalue")另一种模式是在文本光标的当前位置追加文本。在这里,您可以使用INSERT常量,而不必计算数值索引:
entry.insert(tk.INSERT,"cursorhere")与Button类一样,Entry类还接受relief和state选项来修改其边框样式和状态。请注意,在状态为"disabled"或"readonly"时,对delete()和insert()的调用将被忽略。
Tk变量允许您的应用程序在输入更改其值时得到通知。Tkinter中有四个变量类:BooleanVar、DoubleVar、IntVar和StringVar。每个类都包装了相应Python类型的值,该值应与附加到变量的输入小部件的类型匹配。
如果您希望根据某些输入小部件的当前状态自动更新应用程序的某些部分,则此功能特别有用。
在以下示例中,我们将使用textvariable选项将StringVar实例与我们的条目关联;此变量跟踪写操作,并使用show_message()方法作为回调:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.var=tk.StringVar()self.var.trace("w",self.show_message)self.entry=tk.Entry(self,textvariable=self.var)self.btn=tk.Button(self,text="Clear",command=lambda:self.var.set(""))self.label=tk.Label(self)self.entry.pack()self.btn.pack()self.label.pack()defshow_message(self,*args):value=self.var.get()text="Hello,{}!".format(value)ifvalueelse""self.label.config(text=text)if__name__=="__main__":app=App()app.mainloop()当您在Entry小部件中输入内容时,标签将使用由Tk变量值组成的消息更新其文本。例如,如果您输入单词Phara,标签将显示Hello,Phara!。如果输入为空,标签将不显示任何文本。为了向您展示如何以编程方式修改变量的内容,我们添加了一个按钮,当您单击它时清除条目:
我们的应用程序构造函数的前几行实例化了StringVar并将回调附加到写入模式。有效的模式值如下:
Tk包装器的get()方法返回变量的当前值,set()方法更新其值。它们还通知相应的观察者,因此通过GUI修改输入内容或单击“清除”按钮都将触发对show_message()方法的调用。
对于Entry小部件,Tk变量是可选的,但对于其他小部件类(例如Checkbutton和Radiobutton类)来说,它们是必要的,以便正确工作。
通常,文本输入代表遵循某些验证规则的字段,例如具有最大长度或匹配特定格式。一些应用程序允许在这些字段中键入任何类型的内容,并在提交整个表单时触发验证。
在某些情况下,我们希望阻止用户将无效内容输入文本字段。我们将看看如何使用Entry小部件的验证选项来实现此行为。
以下应用程序显示了如何使用正则表达式验证输入:
importreimporttkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.pattern=re.compile("^\w{0,10}$")self.label=tk.Label(self,text="Enteryourusername")vcmd=(self.register(self.validate_username),"%i","%P")self.entry=tk.Entry(self,validate="key",validatecommand=vcmd,invalidcommand=self.print_error)self.label.pack()self.entry.pack(anchor=tk.W,padx=10,pady=10)defvalidate_username(self,index,username):print("Modificationatindex"+index)returnself.pattern.match(username)isnotNonedefprint_error(self):print("Invalidusernamecharacter")if__name__=="__main__":app=App()app.mainloop()如果您运行此脚本并在Entry小部件中键入非字母数字字符,则它将保持相同的内容并打印错误消息。当您尝试键入超过10个有效字符时,也会发生这种情况,因为正则表达式还限制了内容的长度。
将validate选项设置为"key",我们将激活在任何内容修改时触发的输入验证。默认情况下,该值为"none",这意味着没有验证。
其他可能的值是"focusin"和"focusout",分别在小部件获得或失去焦点时进行验证,或者简单地使用"focus"在两种情况下进行验证。或者,我们可以使用"all"值在所有情况下进行验证。
validatecommand函数在每次触发验证时调用,如果新内容有效,则应返回true,否则返回false。
由于我们需要更多信息来确定内容是否有效,我们使用Widget类的register方法创建了一个围绕Python函数的Tcl包装器。然后,您可以为将传递给Python函数的每个参数添加百分比替换。最后,我们将这些值分组为Python元组。这对应于我们示例中的以下行:
vcmd=(self.register(self.validate_username),"%i","%P")一般来说,您可以使用以下任何一个替换:
invalidcommand选项接受一个在validatecommand返回false时调用的函数。这个选项也可以应用相同的百分比替换,但在我们的示例中,我们直接传递了我们类的print_error()方法。
Tcl/Tk文档建议不要混合validatecommand和textvariable选项,因为将无效值设置为Tk变量将关闭验证。如果validatecommand函数不返回布尔值,也会发生同样的情况。
以前的食谱介绍了如何处理文本输入;我们可能希望强制某些输入只包含数字值。这是Spinbox和Scale类的用例——这两个小部件允许用户从范围或有效选项列表中选择数值,但它们在显示和配置方式上有几个不同之处。
此程序具有用于从0到5选择整数值的Spinbox和Scale:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.spinbox=tk.Spinbox(self,from_=0,to=5)self.scale=tk.Scale(self,from_=0,to=5,orient=tk.HORIZONTAL)self.btn=tk.Button(self,text="Printvalues",command=self.print_values)self.spinbox.pack()self.scale.pack()self.btn.pack()defprint_values(self):print("Spinbox:{}".format(self.spinbox.get()))print("Scale:{}".format(self.scale.get()))if__name__=="__main__":app=App()app.mainloop()在上面的代码中,出于调试目的,我们添加了一个按钮,当您单击它时,它会打印每个小部件的值:
这两个类都接受from_和to选项,以指示有效值的范围——由于from选项最初是在Tcl/Tk中定义的,但它在Python中是一个保留关键字,因此需要添加下划线。
Scale类的一个方便功能是resolution选项,它设置了舍入的精度。例如,分辨率为0.2将允许用户选择值0.0、0.2、0.4等。此选项的默认值为1,因此小部件将所有值舍入到最接近的整数。
与往常一样,可以使用get()方法检索每个小部件的值。一个重要的区别是,Spinbox将数字作为字符串返回,而Scale返回一个整数值,如果舍入接受小数值,则返回一个浮点值。
Spinbox类具有与Entry小部件类似的配置,例如textvariable和validate选项。您可以将所有这些模式应用于旋转框,主要区别在于它限制为数值。
使用Radiobutton小部件,您可以让用户在多个选项中进行选择。这种模式适用于相对较少的互斥选择。
您可以使用Tkinter变量连接多个Radiobutton实例,以便当您单击未选择的选项时,它将取消选择先前选择的任何其他选项。
在下面的程序中,我们为Red,Green和Blue选项创建了三个单选按钮。每次单击单选按钮时,它都会打印相应颜色的小写名称:
importtkinterastkCOLORS=[("Red","red"),("Green","green"),("Blue","blue")]classChoiceApp(tk.Tk):def__init__(self):super().__init__()self.var=tk.StringVar()self.var.set("red")self.buttons=[self.create_radio(c)forcinCOLORS]forbuttoninself.buttons:button.pack(anchor=tk.W,padx=10,pady=5)defcreate_radio(self,option):text,value=optionreturntk.Radiobutton(self,text=text,value=value,command=self.print_option,variable=self.var)defprint_option(self):print(self.var.get())if__name__=="__main__":app=ChoiceApp()app.mainloop()如果您运行此脚本,它将显示已选择红色单选按钮的应用程序:
为了避免重复Radiobutton初始化的代码,我们定义了一个实用方法,该方法从列表推导中调用。我们解压了COLORS列表的每个元组的值,然后将这些局部变量作为选项传递给Radiobutton。请记住,尽可能尝试不要重复自己。
由于StringVar在所有Radiobutton实例之间共享,它们会自动连接,并且我们强制用户只能选择一个选项。
我们在程序中设置了默认值为"red";但是,如果我们省略此行,且StringVar的值与任何单选按钮的值都不匹配会发生什么?它将匹配tristatevalue选项的默认值,即空字符串。这会导致小部件显示在特殊的“三态”或不确定模式下。虽然可以使用config()方法修改此选项,但最好的做法是设置一个明智的默认值,以便变量以有效状态初始化。
通常使用复选框和选项列表实现两个选择之间的选择,其中每个选择与其余选择无关。正如我们将在下一个示例中看到的,这些概念可以使用Checkbutton小部件来实现。
以下应用程序显示了如何创建Checkbutton,它必须连接到IntVar变量才能检查按钮状态:
importtkinterastkclassSwitchApp(tk.Tk):def__init__(self):super().__init__()self.var=tk.IntVar()self.cb=tk.Checkbutton(self,text="Active",variable=self.var,command=self.print_value)self.cb.pack()defprint_value(self):print(self.var.get())if__name__=="__main__":app=SwitchApp()app.mainloop()在上面的代码中,我们只是在每次单击小部件时打印小部件的值:
与Button小部件一样,Checkbutton也接受command和text选项。
使用onvalue和offvalue选项,我们可以指定按钮打开和关闭时使用的值。我们使用整数变量,因为默认情况下这些值分别为1和0;但是,您也可以将它们设置为任何其他整数值。
对于Checkbuttons,也可以使用其他变量类型:
var=tk.StringVar()var.set("OFF")checkbutton_active=tk.Checkbutton(master,text="Active",variable=self.var,onvalue="ON",offvalue="OFF",command=update_value)唯一的限制是要将onvalue和offvalue与Tkinter变量的类型匹配;在这种情况下,由于"ON"和"OFF"是字符串,因此变量应该是StringVar。否则,当尝试设置不同类型的相应值时,Tcl解释器将引发错误。
Listbox小部件包含用户可以使用鼠标或键盘选择的文本项。这种选择可以是单个的或多个的,这取决于小部件的配置。
以下程序创建了一个星期几的列表选择。有一个按钮来打印实际选择,以及一个按钮列表来更改选择模式:
importtkinterastkDAYS=["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"]MODES=[tk.SINGLE,tk.BROWSE,tk.MULTIPLE,tk.EXTENDED]classListApp(tk.Tk):def__init__(self):super().__init__()self.list=tk.Listbox(self)self.list.insert(0,*DAYS)self.print_btn=tk.Button(self,text="Printselection",command=self.print_selection)self.btns=[self.create_btn(m)forminMODES]self.list.pack()self.print_btn.pack(fill=tk.BOTH)forbtninself.btns:btn.pack(side=tk.LEFT)defcreate_btn(self,mode):cmd=lambda:self.list.config(selectmode=mode)returntk.Button(self,command=cmd,text=mode.capitalize())defprint_selection(self):selection=self.list.curselection()print([self.list.get(i)foriinselection])if__name__=="__main__":app=ListApp()app.mainloop()您可以尝试更改选择模式并打印所选项目:
我们创建一个空的Listbox对象,并使用insert()方法添加所有文本项。0索引表示应在列表的开头添加项目。在下面的代码片段中,我们解包了DAYS列表,但是可以使用END常量将单独的项目附加到末尾:
self.list.insert(tk.END,"Newitem")使用curselection()方法检索当前选择。它返回所选项目的索引,以便将它们转换为相应的文本项目,我们为每个索引调用了get()方法。最后,为了调试目的,列表将被打印在标准输出中。
在我们的示例中,selectmode选项可以通过编程方式进行更改,以探索不同的行为,如下所示:
如果文本项的数量足够大,可能需要添加垂直滚动条。您可以使用yscrollcommand选项轻松连接它。在我们的示例中,我们可以将两个小部件都包装在一个框架中,以保持相同的布局。记得在打包滚动条时指定fill选项,以便在y轴上填充可用空间。
def__init__(self):self.frame=tk.Frame(self)self.scroll=tk.Scrollbar(self.frame,orient=tk.VERTICAL)self.list=tk.Listbox(self.frame,yscrollcommand=self.scroll.set)self.scroll.config(command=self.list.yview)#...self.frame.pack()self.list.pack(side=tk.LEFT)self.scroll.pack(side=tk.LEFT,fill=tk.Y)同样,对于水平轴,还有一个xscrollcommand选项。
能够对事件做出反应是GUI应用程序开发中最基本但最重要的主题之一,因为它决定了用户如何与程序进行交互。
按键盘上的键和用鼠标点击项目是一些常见的事件类型,在一些Tkinter类中会自动处理。例如,这种行为已经在Button小部件类的command选项上实现,它调用指定的回调函数。
有些事件可以在没有用户交互的情况下触发,例如从一个小部件到另一个小部件的程序性输入焦点更改。
您可以使用bind方法将事件绑定到小部件。以下示例将一些鼠标事件绑定到Frame实例:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()frame=tk.Frame(self,bg="green",height=100,width=100)frame.bind("
以下示例包含一个带有一对绑定的Entry小部件;一个用于在输入框获得焦点时触发的事件,另一个用于所有按键事件:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()entry=tk.Entry(self)entry.bind("
bind方法在widget类中定义,并接受三个参数,一个事件sequence,一个callback函数和一个可选的add字符串:
widget.bind(sequence,callback,add='')sequence字符串使用
首先,修饰符是可选的,允许您指定事件的一般类型的其他组合:
事件类型确定事件的一般类型:
详细信息也是可选的,用于指示鼠标按钮或键:
callback函数接受一个事件参数。对于鼠标事件,它具有以下属性:
对于键盘事件,它包含这些属性:
在这两种情况下,事件都有widget属性,引用生成事件的实例,以及type,指定事件类型。
我们强烈建议您为callback函数定义方法,因为您还将拥有对类实例的引用,因此您可以轻松访问每个widget属性。
最后,add参数可以是'',以替换callback函数(如果有先前的绑定),或者是'+',以添加回调并保留旧的回调。
除了这里描述的事件类型之外,还有其他类型,在某些情况下可能会有用,比如当小部件被销毁时生成的
Tk实例与普通小部件不同,它的配置方式也不同,因此我们将探讨一些基本方法,允许我们自定义它的显示方式。
这段代码创建了一个带有自定义标题和图标的主窗口。它的宽度为400像素,高度为200像素,与屏幕左上角的每个轴向的间隔为10像素:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.title("MyTkinterapp")self.iconbitmap("python.ico")self.geometry("400x200+10+10")if__name__=="__main__":app=App()app.mainloop()该程序假定您在脚本所在的目录中有一个名为python.ico的有效ICO文件。
Tk类的title()和iconbitmap()方法非常自描述——第一个设置窗口标题,而第二个接受与窗口关联的图标的路径。
geometry()方法使用遵循以下模式的字符串配置窗口的大小:
{width}x{height}+{offset_x}+{offset_y}
如果您向应用程序添加更多的辅助窗口,这些方法也适用于Toplevel类。
如果您想使应用程序全屏,将对geometry()方法的调用替换为self.state("zoomed")。
在本章中,我们将介绍以下食谱:
小部件确定用户可以在GUI应用程序中执行的操作;但是,我们应该注意它们的放置和我们与该安排建立的关系。有效的布局帮助用户识别每个图形元素的含义和优先级,以便他们可以快速理解如何与我们的程序交互。
布局还确定了用户期望在整个应用程序中一致找到的视觉外观,例如始终将确认按钮放在屏幕右下角。尽管这些信息对我们作为开发人员来说可能是显而易见的,但如果我们不按照自然顺序引导他们通过应用程序,最终用户可能会感到不知所措。
本章将深入探讨Tkinter提供的不同机制,用于布置和分组小部件以及控制其他属性,例如它们的大小或间距。
框架的另一个常见模式是封装应用程序功能的一部分,以便您可以创建一个抽象,隐藏子部件的实现细节。
我们将看到一个示例,涵盖了从Frame类继承并公开包含小部件上的某些信息的组件的两种情况。
我们将构建一个应用程序,其中包含两个列表,第一个列表中有一系列项目,第二个列表最初为空。两个列表都是可滚动的,并且您可以使用两个中央按钮在它们之间移动项目:
我们将定义一个Frame子类来表示可滚动列表,然后创建该类的两个实例。两个按钮也将直接添加到主窗口:
这些方法用于父类中将项目从一个列表转移到另一个列表:
defmove(self,frame_from,frame_to):value=frame_from.pop_selection()ifvalue: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()方法来实现这一点,这意味着我们使用了Pack几何管理器。
这是Tkinter中三种可用的几何管理器之一,非常适合简单的布局,例如当您想要将所有小部件放在彼此上方或并排时。
假设我们想在应用程序中实现以下布局:
它由三行组成,最后一行有三个小部件并排放置。在这种情况下,Pack布局管理器可以轻松地按预期添加小部件,而无需额外的框架。
我们将使用五个具有不同文本和背景颜色的Label小部件来帮助我们识别每个矩形区域:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()label_a=tk.Label(self,text="LabelA",bg="yellow")label_b=tk.Label(self,text="LabelB",bg="orange")label_c=tk.Label(self,text="LabelC",bg="red")label_d=tk.Label(self,text="LabelD",bg="green")label_e=tk.Label(self,text="LabelE",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字典中添加了一些选项,以便清楚地确定每个区域的大小:
首先,我们将两个标签打包到屏幕顶部。虽然tk.TOP常量是side选项的默认值,但我们明确设置它以清楚地区分它与我们使用tk.LEFT值的调用。
然后,我们使用side选项设置为tk.LEFT来打包下面的三个标签,这会使它们并排放置:
指定label_e上的side实际上并不重要,只要它是我们添加到容器中的最后一个小部件即可。
请记住,这就是在使用Pack布局管理器时顺序如此重要的原因。为了防止复杂布局中出现意外结果,通常将小部件与框架分组,这样当您将所有小部件打包到一个框架中时,就不会干扰其他小部件的排列。
在这些情况下,我们强烈建议您使用网格布局管理器,因为它允许您直接调用几何管理器设置每个小部件的位置,并且避免了额外框架的需要。
除了tk.TOP和tk.LEFT,您还可以将tk.BOTTOM和tk.RIGHT常量传递给side选项。它们执行相反的堆叠,正如它们的名称所暗示的那样;但是,这可能是反直觉的,因为我们遵循的自然顺序是从上到下,从左到右。
例如,如果我们在最后三个小部件中用tk.RIGHT替换tk.LEFT的值,它们从左到右的顺序将是label_e,label_d和label_c。
网格布局管理器被认为是三种布局管理器中最通用的。它直接重新组合了通常用于用户界面设计的网格概念,即一个二维表格,分为行和列,其中每个单元格代表小部件的可用空间。
我们将演示如何使用网格布局管理器来实现以下布局:
这可以表示为一个3x3的表格,其中第二列和第三列的小部件跨越两行,底部行的小部件跨越三列。
与前面的食谱一样,我们将使用五个具有不同背景的标签来说明单元格的分布:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()label_a=tk.Label(self,text="LabelA",bg="yellow")label_b=tk.Label(self,text="LabelB",bg="orange")label_c=tk.Label(self,text="LabelC",bg="red")label_d=tk.Label(self,text="LabelD",bg="green")label_e=tk.Label(self,text="LabelE",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几何管理器相比,可以更改对每个小部件的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将小部件放置在单元格的右下角(东南)。
由于这些常量代表它们对应的小写字母,我们用"nswe"字符串简写了tk.N+tk.S+tk.W+tk.E表达式。这意味着小部件应该在水平和垂直方向上都扩展,类似于Pack几何管理器的fill=tk.BOTH选项。
如果sticky选项没有传递值,则小部件将在单元格内居中。
Place几何管理器允许您以绝对或相对于另一个小部件的位置和大小。
在三种几何管理器中,它是最不常用的一种。另一方面,它可以适应一些复杂的情况,例如您想自由定位一个小部件或重叠一个先前放置的小部件。
为了演示如何使用Place几何管理器,我们将通过混合绝对位置和相对位置和大小来复制以下布局:
我们将显示的标签具有不同的背景,并按从左到右和从上到下的顺序定义:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()label_a=tk.Label(self,text="LabelA",bg="yellow")label_b=tk.Label(self,text="LabelB",bg="orange")label_c=tk.Label(self,text="LabelC",bg="red")label_d=tk.Label(self,text="LabelD",bg="green")label_e=tk.Label(self,text="LabelE",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的值为0.5表示父容器宽度的一半,rely和relheight的值为0.5表示父容器高度的一半。
第四个标签通过将其作为in_参数放置在label_c上(请注意,Tkinter在其后缀中添加了下划线,因为in是一个保留关键字)。使用in_时,您可能会注意到对齐不是几何上精确的。在我们的示例中,我们必须在每个方向上添加2个像素的偏移量,以完全重叠label_c的右下角。
最后,第五个标签使用绝对定位和相对大小。正如您可能已经注意到的那样,这些尺寸可以很容易地切换,因为我们假设父容器为200x200像素;但是,如果调整主窗口的大小,只有相对权重才能按预期工作。您可以通过调整窗口大小来测试此行为。
Place几何管理器的另一个重要优势是它可以与Pack或Grid一起使用。
例如,假设您希望在右键单击小部件时动态显示标题。您可以使用Label小部件表示此标题,并将其放置在单击小部件的相对位置:
defshow_caption(self,event):caption=tk.Label(self,...)caption.place(in_=event.widget,x=event.x,y=event.y)#...作为一般建议,我们建议您在Tkinter应用程序中尽可能多地使用其他几何管理器,并且仅在需要自定义定位的专门情况下使用此几何管理器。
LabelFrame类可用于对多个输入小部件进行分组,指示它们表示的逻辑实体的标签。它通常用于表单,与Frame小部件非常相似。
我们将构建一个带有一对LabelFrame实例的表单,每个实例都有其相应的子输入小部件:
由于此示例的目的是显示最终布局,我们将添加一些小部件,而不将它们的引用保留为属性:
label=tk.Label(master,text="Info",...)frame=tk.LabelFrame(master,labelwidget=label)#...frame.pack()这将允许您进行任何类型的自定义,例如添加图像。请注意,我们没有为标签使用任何几何管理器,因为当您放置框架时,它会被管理。
网格几何管理器在简单和高级布局中都很容易使用,也是与小部件列表结合使用的强大机制。
我们将看看如何通过列表推导和zip和enumerate内置函数,可以减少行数并仅用几行调用几何管理器方法。
我们将构建一个应用程序,其中包含四个Entry小部件,每个小部件都有相应的标签,指示输入的含义。我们还将添加一个按钮来打印所有条目的值:
我们将使用小部件列表而不是创建和分配每个小部件到单独的属性。由于我们将在这些列表上进行迭代时跟踪索引,因此我们可以轻松地使用适当的column选项调用grid()方法。
我们将使用zip函数聚合标签和输入列表。按钮将单独创建和显示,因为它与其余小部件没有共享任何选项:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()fields=["Firstname","Lastname","Phone","Email"]labels=[tk.Label(self,text=f)forfinfields]entries=[tk.Entry(self)for_infields]self.widgets=list(zip(labels,entries))self.submit=tk.Button(self,text="Printinfo",command=self.print_info)fori,(label,entry)inenumerate(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)defprint_info(self):forlabel,entryinself.widgets:print("{}={}".format(label.cget("text"),"=",entry.get()))if__name__=="__main__":app=App()app.mainloop()您可以在每个输入上输入不同的文本,并单击“打印信息”按钮以验证每个元组包含相应的标签和输入。
每个列表推导式都会迭代字段列表的字符串。标签使用每个项目作为显示的文本,输入只需要父容器的引用——下划线是一个常见的习惯用法,表示变量值被忽略。
从Python3开始,zip返回一个迭代器而不是列表,因此我们使用列表函数消耗聚合。结果,widgets属性包含一个可以安全多次迭代的元组列表:
fields=["Firstname","Lastname","Phone","Email"]labels=[tk.Label(self,text=f)forfinfields]entries=[tk.Entry(self)for_infields]self.widgets=list(zip(labels,entries))现在,我们必须在每个小部件元组上调用几何管理器。使用enumerate函数,我们可以跟踪每次迭代的索引并将其作为行号传递:
fori,(label,entry)inenumerate(self.widgets):label.grid(row=i,column=0,padx=10,sticky=tk.W)entry.grid(row=i,column=1,padx=10,pady=5)请注意,我们使用了fori,(label,entry)in...语法,因为我们必须解压使用enumerate生成的元组,然后解压widgets属性的每个元组。
在print_info()回调中,我们迭代小部件以打印每个标签文本及其相应的输入值。要检索标签的text,我们使用了cget()方法,它允许您通过名称获取小部件选项的值。
在Tkinter中,几何管理器会占用所有必要的空间,以适应其父容器中的所有小部件。但是,如果容器具有固定大小或超出屏幕大小,将会有一部分区域对用户不可见。
在Tkinter中,滚动条小部件不会自动添加,因此您必须像其他类型的小部件一样创建和布置它们。另一个考虑因素是,只有少数小部件类具有配置选项,使其能够连接到滚动条。
为了解决这个问题,您将学习如何利用Canvas小部件的灵活性使任何容器可滚动。
为了演示Canvas和Scrollbar类的组合,创建一个可调整大小和可滚动的框架,我们将构建一个通过加载图像动态更改大小的应用程序。
当单击“加载图像”按钮时,它会将自身移除,并将一个大于可滚动区域的图像加载到Canvas中-例如,我们使用了一个预定义的图像,但您可以修改此程序以使用文件对话框选择任何其他GIF图像:
这将启用水平和垂直滚动条,如果主窗口被调整大小,它们会自动调整自己:
当我们将在单独的章节中深入了解Canvas小部件的功能时,本应用程序将介绍其标准滚动界面和create_window()方法。请注意,此脚本需要将文件python.gif放置在相同的目录中:
还需要在定义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选项指示相对权重以分配额外的空间,但在我们的情况下,没有更多的行或列需要调整大小。
绑定到
self.rowconfigure(0,weight=1)self.columnconfigure(0,weight=1)self.bind("
为了获得容器的真实大小,我们必须通过调用update_idletasks()强制几何管理器首先绘制所有子小部件。这个方法在所有小部件类中都可用,并强制Tkinter处理所有待处理的空闲事件,如重绘和几何重新计算:
self.update_idletasks()self.minsize(self.winfo_width(),self.winfo_height())resize方法处理窗口调整大小事件,并更新scrollregion选项,该选项定义了可以滚动的canvas区域。为了轻松地重新计算它,您可以使用bbox()方法和ALL常量。这将返回整个Canvas小部件的边界框:
defresize(self,event):region=self.canvas.bbox(tk.ALL)self.canvas.configure(scrollregion=region)当我们启动应用程序时,Tkinter将自动触发多个
只有少数小部件类支持标准滚动选项:Listbox、Text和Canvas允许xscrollcommand和yscrollcommand,而输入小部件只允许xscrollcommand。我们已经看到如何将此模式应用于canvas,因为它可以用作通用解决方案,但您可以遵循类似的结构使这些小部件中的任何一个可滚动和可调整大小。
还有一点要指出的是,我们没有调用任何几何管理器来绘制框架,因为create_window()方法会为我们完成这项工作。为了更好地组织我们的应用程序类,我们可以将属于框架及其内部小部件的所有功能移动到专用的Frame子类中。
在本章中,我们将涵盖以下示例:
默认情况下,Tkinter小部件将显示本机外观和感觉。虽然这种标准外观可能足够快速原型设计,但我们可能希望自定义一些小部件属性,如字体、颜色和背景。
这种自定义不仅影响小部件本身,还影响其内部项目。我们将深入研究文本小部件,它与画布小部件一样是最多功能的Tkinter类之一。文本小部件表示具有格式化内容的多行文本区域,具有几种方法,使得可以格式化字符或行并添加特定事件绑定。
在以前的示例中,我们使用颜色名称(如白色、蓝色或黄色)来设置小部件的颜色。这些值作为字符串传递给foreground和background选项,这些选项修改了小部件的文本和背景颜色。
颜色名称内部映射到RGB值(一种通过红、绿和蓝强度的组合来表示颜色的加法模型),这种转换基于一个因平台而异的表。因此,如果要在不同平台上一致显示相同的颜色,可以将RGB值传递给小部件选项。
以下应用程序显示了如何动态更改显示固定文本的标签的foreground和background选项:
颜色以RGB格式指定,并由用户使用本机颜色选择对话框选择。以下屏幕截图显示了Windows10上的此对话框的外观:
像往常一样,我们将使用标准按钮触发小部件配置——每个选项一个按钮。与以前的示例的主要区别是,可以直接使用tkinter.colorchooser模块的askcolor对话框直接选择值:
fromfunctoolsimportpartialimporttkinterastkfromtkinter.colorchooserimportaskcolorclassApp(tk.Tk):def__init__(self):super().__init__()self.title("Colorsdemo")text="Thequickbrownfoxjumpsoverthelazydog"self.label=tk.Label(self,text=text)self.fg_btn=tk.Button(self,text="Setforegroundcolor",command=partial(self.set_color,"fg"))self.bg_btn=tk.Button(self,text="Setbackgroundcolor",command=partial(self.set_color,"bg"))self.label.pack(padx=20,pady=20)self.fg_btn.pack(side=tk.LEFT,fill=tk.BOTH,expand=True)self.bg_btn.pack(side=tk.LEFT,fill=tk.BOTH,expand=True)defset_color(self,option):color=askcolor()[1]print("Chosencolor:",color)self.label.config(**{option:color})if__name__=="__main__":app=App()app.mainloop()如果要查看所选颜色的RGB值,在对话框确认时会在控制台上打印出来,如果关闭而没有选择颜色,则不会显示任何值。
正如您可能已经注意到的,两个按钮都使用了部分函数作为回调。这是functools模块中的一个实用程序,它创建一个新的可调用对象,其行为类似于原始函数,但带有一些固定的参数。例如,考虑以下语句:
tk.Button(self,command=partial(self.set_color,"fg"),...)前面的语句执行与以下语句相同的操作:
tk.Button(self,command=lambda:self.set_color("fg"),...)我们这样做是为了同时重用我们的set_color()方法和引入functools模块。这些技术在更复杂的场景中非常有用,特别是当您想要组合多个函数并且非常清楚地知道一些参数已经预定义时。
要记住的一个小细节是,我们用fg和bg分别缩写了foreground和background。在这个语句中,这些字符串使用**进行解包,用于配置小部件:
defset_color(self,option):color=askcolor()[1]print("Chosencolor:",color)self.label.config(**{option:color})#sameas(fg=color)or(bg=color)askcolor返回一个包含两个项目的元组,表示所选颜色——第一个是表示RGB值的整数元组,第二个是十六进制代码作为字符串。由于第一个表示不能直接传递给小部件选项,我们使用了十六进制格式。
如果要将颜色名称转换为RGB格式,可以在先前创建的小部件上使用winfo_rgb()方法。由于它返回一个整数元组,表示16位RGB值的整数从0到65535,您可以通过向右移动8位将其转换为更常见的#RRGGBB十六进制表示:
rgb=widget.winfo_rgb("lightblue")red,green,blue=[x>>8forxinrgb]print("#{:02x}{:02x}{:02x}".format(red,green,blue))在前面的代码中,我们使用{:02x}将每个整数格式化为两个十六进制数字。
在Tkinter中,可以自定义用于向用户显示文本的小部件的字体,例如按钮、标签和输入框。默认情况下,字体是特定于系统的,但可以使用font选项进行更改。
以下应用程序允许用户动态更改具有静态文本的标签的字体系列和大小。尝试不同的值以查看字体配置的结果:
我们将有两个小部件来修改字体配置:一个下拉选项,其中包含字体系列名称,以及一个输入字体大小的微调框:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.title("Fontsdemo")text="Thequickbrownfoxjumpsoverthelazydog"self.label=tk.Label(self,text=text)self.family=tk.StringVar()self.family.trace("w",self.set_font)families=("Times","Courier","Helvetica")self.option=tk.OptionMenu(self,self.family,*families)self.size=tk.StringVar()self.size.trace("w",self.set_font)self.spinbox=tk.Spinbox(self,from_=8,to=18,textvariable=self.size)self.family.set(families[0])self.size.set("10")self.label.pack(padx=20,pady=20)self.option.pack(side=tk.LEFT,fill=tk.BOTH,expand=True)self.spinbox.pack(side=tk.LEFT,fill=tk.BOTH,expand=True)defset_font(self,*args):family=self.family.get()size=self.size.get()self.label.config(font=(family,size))if__name__=="__main__":app=App()app.mainloop()请注意,我们已为与每个输入连接的Tkinter变量设置了一些默认值。
FAMILIES元组包含Tk保证在所有平台上支持的三种字体系列:Times(TimesNewRoman)、Courier和Helvetica。它们可以通过与self.family变量连接的OptionMenu小部件进行切换。
类似的方法用于使用Spinbox设置字体大小。这两个变量触发了更改font标签的方法:
defset_font(self,*args):family=self.family.get()size=self.size.get()self.label.config(font=(family,size))传递给font选项的元组还可以定义以下一个或多个字体样式:粗体、罗马体、斜体、下划线和删除线:
widget1.config(font=("Times","20","bold"))widget2.config(font=("Helvetica","16","italicunderline"))您可以使用tkinter.font模块的families()方法检索可用字体系列的完整列表。由于您需要首先实例化root窗口,因此可以使用以下脚本:
importtkinterastkfromtkinterimportfontroot=tk.Tk()print(font.families())如果您使用的字体系列未包含在可用系列列表中,Tkinter不会抛出任何错误,而是会尝试匹配类似的字体。
tkinter.font模块包括一个Font类,可以在多个小部件上重复使用。修改font实例的主要优势是它会影响与font选项共享它的所有小部件。
使用Font类的工作方式与使用字体描述符非常相似。例如,此代码段创建一个18像素的Courier粗体字体:
fromtkinterimportfontcourier_18=font.Font(family="Courier",size=18,weight=font.BOLD)要检索或更改选项值,您可以像往常一样使用cget和configure方法:
family=courier_18.cget("family")courier_18.configure(underline=1)另请参阅使用选项数据库Tkinter定义了一个称为选项数据库的概念,这是一种用于自定义应用程序外观的机制,而无需为每个小部件指定它。它允许您将一些小部件选项与单个小部件配置分离开来,根据小部件层次结构提供标准化的默认值。
在此配方中,我们将构建一个具有不同样式的多个小部件的应用程序,这些样式将在选项数据库中定义:
在我们的示例中,我们将通过option_add()方法向数据库添加一些选项,该方法可以从所有小部件类访问:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.title("Optionsdemo")self.option_add("*font","helvetica10")self.option_add("*header.font","helvetica18bold")self.option_add("*subtitle.font","helvetica14italic")self.option_add("*Button.foreground","blue")self.option_add("*Button.background","white")self.option_add("*Button.activeBackground","gray")self.option_add("*Button.activeForeground","black")self.create_label(name="header",text="Thisistheheader")self.create_label(name="subtitle",text="Thisisthesubtitle")self.create_label(text="Thisisaparagraph")self.create_label(text="Thisisanotherparagraph")self.create_button(text="Seemore")defcreate_label(self,**options):tk.Label(self,**options).pack(padx=20,pady=5,anchor=tk.W)defcreate_button(self,**options):tk.Button(self,**options).pack(padx=5,pady=5,anchor=tk.E)if__name__=="__main__":app=App()app.mainloop()因此,Tkinter将使用选项数据库中定义的默认值,而不是与其他选项一起配置字体、前景和背景。
让我们从解释对option_add的每个调用开始。第一次调用添加了一个选项,将font属性设置为所有小部件——通配符代表任何应用程序名称:
self.option_add("*font","helvetica10")下一个调用将匹配限制为具有header名称的元素——规则越具体,优先级越高。稍后在使用name="header"实例化标签时指定此名称:
self.option_add("*header.font","helvetica18bold")对于self.option_add("*subtitle.font","helvetica14italic"),也是一样的,所以每个选项都匹配到不同命名的小部件实例。
下一个选项使用Button类名而不是实例名。这样,您可以引用给定类的所有小部件以提供一些公共默认值:
self.option_add("*Button.foreground","blue")self.option_add("*Button.background","white")self.option_add("*Button.activeBackground","gray")self.option_add("*Button.activeForeground","black")正如我们之前提到的,选项数据库使用小部件层次结构来确定适用于每个实例的选项,因此,如果我们有嵌套的容器,它们也可以用于限制优先级选项。
这些配置选项不适用于现有小部件,只适用于修改选项数据库后创建的小部件。因此,我们始终建议在应用程序开头调用option_add()。
这些是一些示例,每个示例比前一个更具体:
不仅可以通过编程方式添加选项,还可以使用以下格式在单独的文本文件中定义它们:
*font:helvetica10*header.font:helvetica18bold*subtitle.font:helvetica14italic*Button.foreground:blue*Button.background:white*Button.activeBackground:gray*Button.activeForeground:black这个文件应该使用option_readfile()方法加载到应用程序中,并替换所有对option_add()的调用。在我们的示例中,假设文件名为my_options_file,并且它放在与我们的脚本相同的目录中:
def__init__(self):super().__init__()self.title("Optionsdemo")self.option_readfile("my_options_file")#...如果文件不存在或其格式无效,Tkinter将引发TclError。
Tkinter允许您在悬停在小部件上时自定义光标图标。这种行为有时是默认启用的,比如显示I型光标的Entry小部件。
鼠标指针图标可以使用cursor选项更改。在我们的示例中,我们使用watch值来显示本机繁忙光标,question_arrow来显示带有问号的常规箭头:
如果一个小部件没有指定cursor选项,它将采用父容器中定义的值。因此,我们可以通过在root窗口级别设置它来轻松地将其应用于所有小部件。这是通过在perform_action()方法中调用set_watch_cursor()来完成的:
defperform_action(self):self.config(cursor="watch")#...这里的例外是Help按钮,它明确将光标设置为question_arrow。此选项也可以在实例化小部件时直接设置:
self.btn_help=tk.Button(self,text="Help",cursor="question_arrow")还有更多...请注意,如果在调用预定方法之前单击Start!按钮并将鼠标放在Help按钮上,光标将显示为help而不是watch。这是因为如果小部件的cursor选项已设置,它将优先于父容器中定义的cursor。
为了避免这种情况,我们可以保存当前的cursor值并将其更改为watch,然后稍后恢复它。执行此操作的函数可以通过迭代winfo_children()列表在子小部件中递归调用:
defperform_action(self):self.set_watch_cursor(self)#...defend_action(self):self.restore_cursor(self)#...defset_watch_cursor(self,widget):widget._old_cursor=widget.cget("cursor")widget.config(cursor="watch")forwinwidget.winfo_children():self.set_watch_cursor(w)defrestore_cursor(self,widget):widget.config(cursor=widget._old_cursor)forwinwidget.winfo_children():self.restore_cursor(w)在前面的代码中,我们为每个小部件添加了_old_cursor属性,因此如果您遵循类似的方法,请记住在set_watch_cursor()之前不能调用restore_cursor()。
Text小部件提供了与其他小部件类相比更高级的功能。它显示可编辑文本的多行,可以按行和列进行索引。此外,您可以使用标签引用文本范围,这些标签可以定义自定义外观和行为。
以下应用程序展示了Text小部件的基本用法,您可以动态插入和删除文本,并检索所选内容:
除了Text小部件,我们的应用程序还包含三个按钮,这些按钮调用方法来清除整个文本内容,在当前光标位置插入"Hello,world"字符串,并打印用鼠标或键盘进行的当前选择:
delete(start,end)方法从start索引到end索引删除内容。如果省略第二个参数,它只删除start位置的字符。
在我们的示例中,我们通过从1.0索引(第一行的第0列)调用此方法到tk.END索引(指向最后一个字符)来删除所有文本:
defclear_text(self):self.text.delete("1.0",tk.END)insert(index,text)方法在index位置插入给定的文本。在这里,我们使用INSERT索引调用它,该索引对应于插入光标的位置:
definsert_text(self):self.text.insert(tk.INSERT,"Hello,world")tag_ranges(tag)方法返回一个元组,其中包含给定tag的所有范围的第一个和最后一个索引。我们使用特殊的tk.SEL标签来引用当前选择。如果没有选择,这个调用会返回一个空元组。这与get(start,end)方法结合使用,该方法返回给定范围内的文本:
defprint_selection(self):selection=self.text.tag_ranges(tk.SEL)ifselection:content=self.text.get(*selection)print(content)由于SEL标签只对应一个范围,我们可以安全地解包它来调用get方法。
在本示例中,您将学习如何配置Text小部件中标记的字符范围的行为。
所有的概念都与适用于常规小部件的概念相同,比如事件序列或配置选项,这些概念在之前的示例中已经涵盖过了。主要的区别是,我们需要使用文本索引来识别标记的内容,而不是使用对象引用。
为了说明如何使用文本标记,我们将创建一个模拟插入超链接的Text小部件。点击时,此链接将使用默认浏览器打开所选的URL。
例如,如果用户输入以下内容,python.org文本可以被标记为超链接:
对于此应用程序,我们将定义一个名为"link"的标记,它表示可点击的超链接。此标记将被添加到当前选择中,鼠标点击将触发打开浏览器中的链接的事件:
def__init__(self):#...self.text.tag_config("link",foreground="blue",underline=1)self.text.tag_bind("link","
position="@{},{}+1c".format(event.x,event.y)index=self.text.index(position)prevrange=self.text.tag_prevrange("link",index)请注意,与点击的索引对应的位置是"@x,y",但我们将其移动到下一个字符。我们这样做是因为tag_prevrange返回给定索引的前一个范围,因此如果我们点击第一个字符,它将不返回当前范围。
最后,我们将从范围中检索文本,并使用webbrowser模块的open函数在默认浏览器中打开它:
url=self.text.get(*prevrange)webbrowser.open(url)还有更多...由于webbrowser.open函数不检查URL是否有效,可以通过包含基本的超链接验证来改进此应用程序。例如,您可以使用urlparse函数来验证URL是否具有网络位置:
fromurllib.parseimporturlparsedefvalidate_hyperlink(self,url):returnurlparse(url).netloc尽管这个解决方案并不打算处理一些特殊情况,但它可能作为丢弃大多数无效URL的第一步。
一般来说,您可以使用标签来创建复杂的基于文本的程序,比如带有语法高亮的IDE。事实上,IDLE——默认的Python实现中捆绑的——就是基于Tkinter的。