在第一章中,我们涵盖了标准Tkinter小部件的几个配方。但是,我们跳过了Canvas小部件,因为它提供了丰富的图形功能,并且值得单独的章节来深入了解其常见用例。
画布是一个矩形区域,您不仅可以在其中显示文本和几何形状,如线条、矩形或椭圆,还可以嵌套其他Tkinter小部件。这些对象称为画布项目,每个项目都有一个唯一的标识符,允许我们在它们最初显示在画布上之前对它们进行操作。
我们将使用Canvas类的方法进行交互示例,这将帮助我们识别可能转换为我们想要构建的应用程序的常见模式。
要在画布上绘制图形项目,我们需要使用坐标系统指定它们的位置。由于画布是二维空间,点将通过它们在水平和垂直轴上的坐标来表示——通常分别标记为x和y。
通过一个简单的应用程序,我们可以很容易地说明如何定位这些点与坐标系统原点的关系,该原点位于画布区域的左上角。
以下程序包含一个空画布和一个标签,显示光标在画布上的位置;您可以移动光标以查看其所处的位置,清晰地反映了鼠标指针移动的方向,x和y坐标是如何增加或减少的:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.title("Basiccanvas")self.canvas=tk.Canvas(self,bg="white")self.label=tk.Label(self)self.canvas.bind("
def__init__(self):#...self.canvas=tk.Canvas(self,bg="white")self.label=tk.Label(self)self.canvas.bind("
正如您可能已经注意到的在前面的屏幕截图中,这些坐标直接映射到传递给处理程序的event实例的x和y属性:
defmouse_motion(self,event):x,y=event.x,event.ytext="Mouseposition:({},{})".format(x,y)self.label.config(text=text)这是因为这些属性是相对于事件绑定到的小部件计算的,在这种情况下是
画布表面还可以显示具有其坐标中的负值的项目。根据项目的大小,它们可能部分显示在画布的顶部或左边界上。
类似地,如果项目放置在任一坐标大于画布大小的点上,它可能部分落在底部或右边界之外。
您可以使用画布执行的最基本的操作之一是从一个点到另一个点绘制线段。虽然可以使用其他方法直接绘制多边形,但Canvas类的create_line方法具有足够的选项来理解显示项目的基础知识。
在这个示例中,我们将构建一个应用程序,允许我们通过单击画布来绘制线条。每条线都将通过首先单击确定线条起点的点,然后第二次设置线条终点来显示。
我们还可以指定一些外观选项,如颜色和宽度:
我们的App类将负责创建一个空画布并处理鼠标点击事件。
线选项的信息将从LineForm类中检索。将此组件分离到不同的类中的方法有助于我们抽象其实现细节,并专注于如何使用Canvas小部件。
为了简洁起见,我们在以下片段中省略了LineForm类的实现:
importtkinterastkclassLineForm(tk.LabelFrame):#...classApp(tk.Tk):def__init__(self):super().__init__()self.title("Basiccanvas")self.line_start=Noneself.form=LineForm(self)self.canvas=tk.Canvas(self,bg="white")self.canvas.bind("
由于我们想要处理画布上的鼠标点击,我们将draw()方法绑定到这种类型的事件。我们还将定义line_start字段
跟踪每条新线的起点:
def__init__(self):#...self.line_start=Noneself.form=LineForm(self)self.canvas=tk.Canvas(self,bg="white")self.canvas.bind("
defdraw(self,event):x,y=event.x,event.yifnotself.line_start:self.line_start=(x,y)else:#...如果line_start已经有一个值,我们将检索原点,并将其与当前事件的坐标一起传递以绘制线条:
defdraw(self,event):x,y=event.x,event.yifnotself.line_start:#...else:x_origin,y_origin=self.line_startself.line_start=Noneline=(x_origin,y_origin,x,y)self.canvas.create_line(*line)text="Linedrawnfrom({},{})to({},{})".format(*line)canvas.create_line()方法需要四个参数,前两个是线条起点的水平和垂直坐标,最后两个是与线条终点对应的坐标。
如果我们想在画布上写一些文本,我们不需要使用额外的小部件,比如Label。Canvas类包括create_text方法来显示一个可以像任何其他类型的画布项一样操作的字符串。
还可以使用与我们可以指定的相同的格式选项来为常规Tkinter小部件的文本添加样式,例如颜色、字体系列和大小。
在这个例子中,我们将连接一个Entry小部件与文本画布项的内容。虽然输入将具有标准外观,但画布上的文本将具有自定义样式:
文本项将首先使用canvas.create_text()方法显示,还有一些额外的选项来使用Consolas字体和蓝色。
文本项的动态行为将使用StringVar实现。通过跟踪这个Tkinter变量,我们可以修改项目的内容:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.title("Canvastextitems")self.geometry("300x100")self.var=tk.StringVar()self.entry=tk.Entry(self,textvariable=self.var)self.canvas=tk.Canvas(self,bg="white")self.entry.pack(pady=5)self.canvas.pack()self.update()w,h=self.canvas.winfo_width(),self.canvas.winfo_height()options={"font":"courier","fill":"blue","activefill":"red"}self.text_id=self.canvas.create_text((w/2,h/2),**options)self.var.trace("w",self.write_text)defwrite_text(self,*args):self.canvas.itemconfig(self.text_id,text=self.var.get())if__name__=="__main__":app=App()app.mainloop()您可以通过在输入框中键入任意文本并注意它如何自动更新画布上的文本来尝试此程序。
首先,我们使用其StringVar变量和Canvas小部件初始化Entry实例:
self.var=tk.StringVar()self.entry=tk.Entry(self,textvariable=self.var)self.canvas=tk.Canvas(self,bg="white")然后,我们通过调用Pack几何管理器的方法来放置小部件。请注意调用根窗口上的update()的重要性,因为我们希望强制Tkinter处理所有未决的更改,在这种情况下在__init__方法继续执行之前渲染小部件:
self.entry.pack(pady=5)self.canvas.pack()self.update()我们这样做是因为下一步将是计算画布的尺寸,直到几何管理器显示小部件,它才会有其宽度和高度的真实值。
之后,我们可以安全地检索画布的尺寸。由于我们想要将文本项与画布的中心对齐,我们将宽度和高度的值除以二。
这些坐标确定了项目的位置,并与样式选项一起传递给create_text()方法。text关键字参数在这里是一个常用选项,但我们将省略它,因为当StringVar更改其值时,它将被动态设置:
w,h=self.canvas.winfo_width(),self.canvas.winfo_height()options={"font":"courier","fill":"blue","activefill":"red"}self.text_id=self.canvas.create_text((w/2,h/2),**options)self.var.trace("w",self.write_text)create_text()返回的标识符存储在text_id字段中。它将在write_text()方法中用于引用该项,该方法由var实例的写操作的跟踪机制调用。
要更新write_text()处理程序中的text选项,我们使用canvas.itemconfig()方法调用项目标识符作为第一个参数,然后是配置选项。
在我们的情况下,我们使用了我们在初始化App实例时存储的text_id字段和通过其get()方法获取的StringVar的内容:
defwrite_text(self,*args):self.canvas.itemconfig(self.text_id,text=self.var.get())我们定义了write_text()方法,以便它可以接收可变数量的参数,即使我们不需要它们,因为Tkinter变量的trace()方法将它们传递给回调函数。
canvas.create_text()方法有许多其他选项,可以自定义创建的画布项目。
anchor选项允许我们控制相对于作为其canvas.create_text()的第一个参数传递的位置放置项目的位置。默认情况下,此选项值为tk.CENTER,这意味着文本小部件居中于这些坐标上。
如果要将文本放在画布的左上角,可以通过传递(0,0)位置并将anchor选项设置为tk.NW来这样做,将原点对齐到文本放置在其中的矩形区域的西北角:
#...options={"font":"courier","fill":"blue","activefill":"red","anchor":tk.NW}self.text_id=self.canvas.create_text((0,0),**options)上述代码片段将给我们以下结果:
默认情况下,文本项目的内容将显示在单行中。width选项允许我们定义最大行宽,用于换行超过该宽度的行:
#...options={"font":"courier","fill":"blue","activefill":"red","width":70}self.text_id=self.canvas.create_text((w/2,h/2),**options)现在,当我们在输入框中写入Hello,world!时,超过行宽的文本部分将显示在新行中:
在本示例中,我们将介绍三种标准画布项目:矩形、椭圆和弧。它们都显示在一个边界框内,因此只需要两个点来设置它们的位置:框的左上角和右下角。
以下应用程序允许用户通过三个按钮选择其类型在画布上自由绘制一些项目-每个按钮选择相应的形状。
项目的位置是通过首先在画布上单击来设置项目将包含在其中的框的左上角,然后再单击来设置此框的左下角并使用一些预定义选项绘制项目来确定的:
我们的应用程序存储当前选择的项目类型,可以使用放置在画布下方框架上的三个按钮之一选择。
使用主鼠标按钮单击画布会触发处理程序,该处理程序存储新项目的第一个角的位置,然后再次单击,它会读取所选形状的值以有条件地绘制相应的项目:
我们使用functools模块中的partial函数来定义每个回调命令。由于这样,我们可以将Button实例和循环的当前形状作为每个按钮的回调的参数冻结:
forshapeinself.shapes:btn=tk.Button(frame,text=shape.capitalize())btn.config(command=partial(self.set_selection,btn,shape))btn.pack(side=tk.LEFT,expand=True,fill=tk.BOTH)set_selection()回调标记了单击的按钮,并将选择存储在shape字段中,使用SUNKENrelief。
其他小部件兄弟姐妹通过导航到父级(在当前小部件的master字段中可用)并使用winfo_children()方法检索所有子小部件来配置标准的relief(RAISED):
defset_selection(self,widget,shape):forwinwidget.master.winfo_children():w.config(relief=tk.RAISED)widget.config(relief=tk.SUNKEN)self.shape=shapedraw_item()处理程序将每对事件的第一次单击的坐标存储起来,以便在再次单击画布时绘制项目-就像我们在绘制线条和箭头示例中所做的那样。
根据shape字段的值,调用以下方法之一来显示相应的项目类型:
Canvas类包括检索接近画布坐标的项目标识符的方法。
这非常有用,因为它可以避免我们存储对画布项目的每个引用,然后计算它们的当前位置以检测哪些项目在特定区域内或最接近特定点。
以下应用程序创建了一个带有四个矩形的画布,并更改了最接近鼠标指针的矩形的颜色:
为了找到最接近指针的项目,我们将鼠标事件坐标传递给canvas.find_closest()方法,该方法检索最接近给定位置的项目的标识符。
一旦画布中至少有一个项目,我们可以安全地假定该方法将始终返回有效的项目标识符:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.title("Findingcanvasitems")self.current=Noneself.canvas=tk.Canvas(self,bg="white")self.canvas.bind("
self.current=Noneself.canvas=tk.Canvas(self,bg="white")self.canvas.bind("
self.update()w=self.canvas.winfo_width()h=self.canvas.winfo_height()positions=[(60,60),(w-60,60),(60,h-60),(w-60,h-60)]forx,yinpositions:self.canvas.create_rectangle(x-10,y-10,x+10,y+10,fill="blue")mouse_motion()处理程序将当前项目的颜色设置回blue,并保存新项目的项目标识符,该项目更接近事件坐标。最后,设置此项目的fill颜色为yellow:
defmouse_motion(self,event):self.canvas.itemconfig(self.current,fill="blue")self.current=self.canvas.find_closest(event.x,event.y)self.canvas.itemconfig(self.current,fill="yellow")当首次调用mouse_motion()时,current字段仍为None时不会出现错误,因为它也是itemconfig()的有效输入参数;它只是不会在画布上执行任何操作。
一旦放置,画布项目就可以移动到特定的偏移量,而无需指定绝对坐标。
移动画布项目时,通常需要计算其当前位置,例如确定它们是否放置在具体的画布区域内,并限制它们的移动,使其始终保持在该区域内。
我们的示例将包括一个简单的带有矩形项目的画布,可以使用箭头键在水平和垂直方向上移动。
为了防止此项目移出屏幕,我们将限制其在画布尺寸内的移动:
def__init__(self):#...self.pressed_keys={}self.bind("
App实例初始化的最后一句是调用process_movements(),它开始处理画布项目的移动。
该方法计算项目应该在每个轴上偏移的量。根据pressed_keys字典的内容,speed值将添加或减去坐标的每个分量:
defprocess_movements(self):off_x,off_y=0,0speed=3if'Right'inself.pressed_keys:off_x+=speedif'Left'inself.pressed_keys:off_x-=speedif'Down'inself.pressed_keys:off_y+=speedif'Up'inself.pressed_keys:off_y-=speed之后,通过调用canvas.coords()并解压形成边界框的一对点来检索当前项目位置到四个变量。
通过将左上角的x和y分量加上其宽度和高度的一半来计算项目的中心。这个结果,再加上每个轴上的偏移量,对应于项目移动后的最终位置:
x0,y0,x1,y1=self.canvas.coords(self.item)pos_x=x0+(x1-x0)/2+off_xpos_y=y0+(y1-y0)/2+off_y然后,我们检查最终项目位置是否在画布区域内。为此,我们利用Python对链接比较运算符的支持:
if0<=pos_x<=self.widthand0<=pos_y<=self.height:self.canvas.move(self.item,off_x,off_y)最后,该方法通过调用self.after(10,self.process_movements)以10毫秒的延迟安排自身。因此,我们实现了在Tkinter的主循环中具有“自定义主循环”的效果。
您可能会想知道为什么我们没有调用after_idle()而是调用after()来安排process_movements()方法。
这似乎是一个有效的方法,因为除了重新绘制我们的画布和处理键盘输入之外,没有其他事件需要处理,因此在process_movements()之间如果没有待处理的GUI事件,就不需要添加延迟。
通过引入最小固定延迟,我们给具有不同功能的机器一个机会,以便以类似的方式行事。
作为前面食谱的延续,我们可以检测矩形项目是否与另一个项目重叠。实际上,假设我们正在使用包含在矩形框中的形状,可以使用Canvas类的find_overlapping()方法来实现这一点。
该应用程序通过向画布添加四个绿色矩形并突出显示通过使用箭头键移动的蓝色矩形触摸的矩形,扩展了前一个应用程序:
由于此脚本与前一个脚本有许多相似之处,我们标记了创建四个矩形并调用canvas.find_overlapping()方法的代码部分:
在计算任何重叠之前,除了用户可以控制的项目之外,所有画布项目的填充颜色都更改为绿色。这些项目的标识符由canvas.find_all()方法检索:
defprocess_movements(self):all_items=self.canvas.find_all()foriteminfilter(lambdai:i!=self.item,all_items):self.canvas.itemconfig(item,fill="green")现在项目颜色已重置,我们调用canvas.find_overlapping()以获取当前与移动项目发生碰撞的所有项目。同样,用户控制的项目在循环中被排除,重叠项目的颜色(如果有)被更改为黄色:
defprocess_movements(self):#...x0,y0,x1,y1=self.canvas.coords(self.item)items=self.canvas.find_overlapping(x0,y0,x1,y1)foriteminfilter(lambdai:i!=self.item,items):self.canvas.itemconfig(item,fill="yellow")该方法继续执行,通过计算偏移量移动蓝色矩形,并再次安排process_movements()自身。
如果要检测移动项目完全与另一个项目重叠,而不是部分重叠,可以将对canvas.find_overlapping()的调用替换为使用相同参数的canvas.find_enclosed()。
除了在画布上添加和修改项目,还可以通过Canvas类的delete()方法删除它们。虽然这种方法的使用非常简单,但在下一个示例中我们将看到一些有用的模式。
对于此示例,我们将构建一个应用程序,在画布上随机显示几个圆。单击圆后,每个圆都会自行删除,窗口包含一个按钮来清除所有项目和另一个按钮来重新开始:
为了在画布上不规则地放置项目,我们将使用random模块的randint函数生成坐标。项目颜色也将通过调用choice并使用预定义的颜色列表来随机选择。
一旦生成,项目可以通过单击触发on_click处理程序或按下Clearitems按钮来删除,后者执行clear_all回调。这些方法内部使用适当的参数调用canvas.delete():
在on_click()处理程序中,我们可以看到如何通过其标识符删除项目的示例:
defon_click(self,event):item=self.canvas.find_withtag(tk.CURRENT)self.canvas.delete(item)请注意,如果我们单击空点,canvas.find_withtag(tk.CURRENT)将返回None,但当传递给canvas.delete()时不会引发任何错误。这是因为None参数不会匹配任何项目标识符或标记,因此,即使它不执行任何操作,它也是有效的值。
在clear_items()回调中,我们可以找到另一个删除项目的示例。在这里,我们使用ALL标记而不是传递项目标识符来匹配所有项目并将其从画布中删除:
defclear_items(self):self.canvas.delete(tk.ALL)您可能已经注意到,ALL标记可以直接使用,无需添加到每个画布项目。
到目前为止,我们已经看到了如何将事件绑定到小部件;但是,也可以为画布项目这样做。这有助于我们编写更具体和更简单的事件处理程序,而不是在Canvas实例上绑定我们想要处理的所有事件类型,然后根据受影响的项目确定要应用的操作。
以下应用程序显示了如何在画布项目上实现拖放功能。这是一个常见的功能,用于说明这种方法如何简化我们的程序。
我们将创建两个可以使用鼠标拖放的项目——一个矩形和一个椭圆。不同的形状帮助我们注意到单击事件如何正确应用于相应的项目,即使项目重叠放置:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.title("Draganddrop")self.dnd_item=Noneself.canvas=tk.Canvas(self,bg="white")self.canvas.pack()self.canvas.create_rectangle(30,30,60,60,fill="green",tags="draggable")self.canvas.create_oval(120,120,150,150,fill="red",tags="draggable")self.canvas.tag_bind("draggable","
即使方法被命名为tag_bind();传递项目标识符而不是标记也是有效的:
self.canvas.tag_bind("draggable","
button_press()方法是在单击项目时调用的处理程序。通常,检索单击的项目的常见模式是调用canvas.find_withtag(tk.CURRENT)。
此项目标识符和click事件的x和y坐标存储在dnd_item字段中。稍后将使用这些值来与鼠标运动同步移动项目:
defbutton_press(self,event):item=self.canvas.find_withtag(tk.CURRENT)self.dnd_item=(item,event.x,event.y)button_motion()方法在按住主按钮时处理鼠标运动事件。
为了设置项目应该移动的距离,我们计算当前事件位置与先前存储的坐标之间的差异。这些值传递给canvas.move()方法,并再次保存在dnd_item字段中:
defbutton_motion(self,event):x,y=event.x,event.yitem,x0,y0=self.dnd_itemself.canvas.move(item,x-x0,y-y0)self.dnd_item=(item,x,y)还有一些变体的拖放功能,还实现了
然而,这并非必要,因为一旦发生这种类型的事件,直到再次单击项目,
可以通过添加一些基本验证来改进此示例,例如验证用户不能将项目放在画布可见区域之外。
要实现这一点,您可以使用我们在以前的配方中介绍的模式来计算画布的宽度和高度,并通过链接比较运算符来验证项目的最终位置是否在有效范围内。您可以使用以下代码段中显示的结构作为模板:
final_x,final_y=pos_x+off_x,pos_y+off_yif0<=final_x<=canvas_widthand0<=final_y<=canvas_height:canvas.move(item,off_x,off_y)另请参阅将画布渲染成PostScript文件Canvas类通过其postscript()方法本地支持使用PostScript语言保存其内容。这会存储画布项目的图形表示,如线条、矩形、多边形、椭圆和弧,但不会对嵌入式小部件和图像进行存储。
我们将修改一个之前的配方,动态生成这种简单项目的功能,以添加将画布的表示保存到PostScript文件的功能。
我们将从绘制线条和箭头配方中获取代码示例,以添加一个按钮,将画布内容打印到PostScript文件中:
importtkinterastkclassApp(tk.Tk):def__init__(self):super().__init__()self.title("Basiccanvas")self.line_start=Noneself.form=LineForm(self)self.render_btn=tk.Button(self,text="Rendercanvas",command=self.render_canvas)self.canvas=tk.Canvas(self,bg="white")self.canvas.bind("
它在canvas实例上调用postscript()方法,并使用file和colormode参数。这些选项指定了写入PostScript和输出颜色信息的目标文件的路径,可以是"color"表示全彩输出,"gray"表示转换为灰度等效,"mono"表示将所有颜色转换为黑色或白色:
由于PostScript文件不像其他文件格式那样流行,您可能希望将生成的文件从PostScript转换为更熟悉的格式,如PDF。
为了做到这一点,您需要一个第三方软件,比如Ghostscript,它是根据GNU的Affero通用公共许可证(AGPL)分发的。Ghostscript的解释器和渲染器实用程序可以从您的程序中调用,自动将PostScript结果转换为PDF。
然后,修改您的Tkinter应用程序,调用ps2pdf程序作为子进程,并在执行完毕时删除output.ps文件,如下所示:
importosimportsubprocessimporttkinterastkclassApp(tk.Tk):#...defrender_canvas(self):output_filename="output.ps"self.canvas.postscript(file=output_filename,colormode="color")process=subprocess.run(["ps2pdf",output_filename,"output.pdf"],shell=True)os.remove(output_filename)第八章:主题小部件在本章中,我们将涵盖以下内容:
Tk主题小部件是Tk小部件的一个单独集合,具有本机外观和感觉,并且它们的样式可以使用特定的API进行高度定制。
这些类在tkinter.ttk模块中定义。除了定义新的小部件,如Treeview和Notebook,这个模块还重新定义了经典Tk小部件的实现,如Button、Label和Frame。
在本章中,我们将不仅涵盖如何将应用程序Tk小部件更改为主题小部件,还将涵盖如何对其进行样式设置和使用新的小部件类。
主题Tk小部件集是在Tk8.5中引入的,这不应该是一个问题,因为Python3.6安装程序可以让您包含Tcl/Tk解释器的8.6版本。
但是,您可以通过在命令行中运行python-mtkinter来验证任何平台,这将启动以下程序,输出Tcl/Tk版本:
作为使用主题Tkinter类的第一种方法,我们将看看如何从这个不同的模块中使用相同的小部件(按钮、标签、输入框等),在我们的应用程序中保持相同的行为。
尽管这不会充分发挥其样式能力,但我们可以轻松欣赏到带来主题小部件本机外观和感觉的视觉变化。
在下面的屏幕截图中,您可以注意到带有主题小部件的GUI和使用标准Tkinter小部件的相同窗口之间的差异:
我们将构建第一个窗口中显示的应用程序,但我们还将学习如何轻松地在两种样式之间切换。
请注意,这高度依赖于平台。在这种情况下,主题变化对应于Windows10上主题小部件的外观。
要开始使用主题小部件,您只需要导入tkinter.ttk模块,并像往常一样在您的Tkinter应用程序中使用那里定义的小部件:
importtkinterastkimporttkinter.ttkasttkclassApp(tk.Tk):greetings=("Hello","Ciao","Hola")def__init__(self):super().__init__()self.title("Tkthemedwidgets")var=tk.StringVar()var.set(self.greetings[0])label_frame=ttk.LabelFrame(self,text="Chooseagreeting")forgreetinginself.greetings:radio=ttk.Radiobutton(label_frame,text=greeting,variable=var,value=greeting)radio.pack()frame=ttk.Frame(self)label=ttk.Label(frame,text="Enteryourname")entry=ttk.Entry(frame)command=lambda:print("{},{}!".format(var.get(),entry.get()))button=ttk.Button(frame,text="Greet",command=command)label.grid(row=0,column=0,padx=5,pady=5)entry.grid(row=0,column=1,padx=5,pady=5)button.grid(row=1,column=0,columnspan=2,pady=5)label_frame.pack(side=tk.LEFT,padx=10,pady=10)frame.pack(side=tk.LEFT,padx=10,pady=10)if__name__=="__main__":app=App()app.mainloop()如果您想要使用常规的Tkinter小部件运行相同的程序,请将所有ttk.出现替换为tk.。
开始使用主题小部件的常见方法是使用import...as语法导入tkinter.ttk模块。因此,我们可以轻松地用tk名称标识标准小部件,用ttk名称标识主题小部件:
importtkinterastkimporttkinter.ttkasttk正如您可能已经注意到的,在前面的代码中,将tkinter模块中的小部件替换为tkinter.ttk中的等效小部件就像更改别名一样简单:
importtkinterastkimporttkinter.ttkasttk#...entry_1=tk.Entry(root)entry_2=ttk.Entry(root)在我们的示例中,我们为ttk.Frame、ttk.Label、ttk.Entry、ttk.LabelFrame和ttk.Radiobutton小部件这样做。这些类接受的基本选项几乎与它们的标准Tkinter等效类相同;事实上,它们实际上是它们的子类。
然而,这个翻译很简单,因为我们没有移植任何样式选项,比如foreground或background。在主题小部件中,这些关键字通过ttk.Style类分别使用,我们将在另一个食谱中介绍。
下拉列表是一种简洁的方式,通过垂直显示数值列表来选择数值,只有在需要时才显示。这也是让用户输入列表中不存在的另一个选项的常见方式。
这个功能结合在ttk.Combobox类中,它采用您平台下拉菜单的本机外观和感觉。
我们的下一个应用程序将包括一个简单的下拉输入框,带有一对按钮来确认选择或清除其内容。
如果选择了预定义的值之一或单击了提交按钮,则当前Combobox值将以以下方式打印在标准输出中:
我们的应用程序在初始化期间创建了一个ttk.Combobox实例,传递了一个预定义的数值序列,可以在下拉列表中进行选择:
importtkinterastkimporttkinter.ttkasttkclassApp(tk.Tk):def__init__(self):super().__init__()self.title("TtkCombobox")colors=("Purple","Yellow","Red","Blue")self.label=ttk.Label(self,text="Pleaseselectacolor")self.combo=ttk.Combobox(self,values=colors)btn_submit=ttk.Button(self,text="Submit",command=self.display_color)btn_clear=ttk.Button(self,text="Clear",command=self.clear_color)self.combo.bind("<
我们绑定了"<
self.label=ttk.Label(self,text="Pleaseselectacolor")self.combo=ttk.Combobox(self,values=colors)btn_submit=ttk.Button(self,text="Submit",command=self.display_color)btn_clear=ttk.Button(self,text="Clear",command=self.clear_color)self.combo.bind("<
我们定义display_color()使用*语法接受可选参数列表。这是因为当通过事件绑定调用它时,会传递一个事件给它,但当从按钮回调中调用它时,不会接收任何参数。
在这个方法中,我们通过其get()方法检索当前Combobox值,并将其打印在标准输出中:
defdisplay_color(self,*args):color=self.combo.get()print("Yourselectionis",color)最后,clear_color()通过调用其set()方法并传递空字符串来清除Combobox的内容:
defclear_color(self):self.combo.set("")通过这些方法,我们已经探讨了如何与Combobox实例的当前选择进行交互。
ttk.Combobox类扩展了ttk.Entry,后者又扩展了tkinter模块中的Entry类。
这意味着如果需要,我们也可以使用我们已经介绍的Entry类的方法:
combobox.insert(0,"Addthisatthebeginning:")前面的代码比combobox.set("Addthisatthebeginning:"+combobox.get())更简单。
在这个示例中,我们将介绍ttk.Treeview类,这是一个多功能的小部件,可以让我们以表格和分层结构显示信息。
添加到ttk.Treeview类的每个项目都分成一个或多个列,其中第一列可能包含文本和图标,并用于指示项目是否可以展开并显示更多嵌套项目。其余的列包含我们想要为每一行显示的值。
ttk.Treeview类的第一行由标题组成,通过其名称标识每一列,并可以选择性地隐藏。
使用ttk.Treeview,我们将对存储在CSV文件中的联系人列表的信息进行制表,类似于我们在第五章中所做的面向对象编程和MVC:
我们将创建一个ttk.Treeview小部件,其中包含三列,分别用于每个联系人的字段:一个用于姓,另一个用于名,最后一个用于电子邮件地址。
联系人是使用csv模块从CSV文件中加载的,然后我们为"<
要创建一个具有多列的ttk.Treeview,我们需要使用columns选项指定每个列的标识符。然后,我们可以通过调用heading()方法来配置标题文本。
我们使用标识符#1、#2和#3,因为第一列始终使用#0标识符生成,其中包含可展开的图标和文本。
我们还将"headings"值传递给show选项,以指示我们要隐藏#0列,因为不会有嵌套项目。
show选项的有效值如下:
之后,我们将垂直滚动条附加到我们的ttk.Treeview小部件:
columns=("#1","#2","#3")self.tree=ttk.Treeview(self,show="headings",columns=columns)self.tree.heading("#1",text="Lastname")self.tree.heading("#2",text="Firstname")self.tree.heading("#3",text="Email")ysb=ttk.Scrollbar(self,orient=tk.VERTICAL,command=self.tree.yview)self.tree.configure(yscroll=ysb.set)要将联系人加载到表中,我们使用csv模块的reader()函数处理文件,并在每次迭代中读取的行添加到ttk.Treeview中。
这是通过调用insert()方法来完成的,该方法接收父节点和放置项目的位置。
由于所有联系人都显示为顶级项目,因此我们将空字符串作为第一个参数传递,并将END常量传递以指示每个新项目插入到最后位置。
您可以选择为insert()方法提供一些关键字参数。在这里,我们指定了values选项,该选项接受在Treeview的每一列中显示的值序列:
withopen("contacts.csv",newline="")asf:forcontactincsv.reader(f):self.tree.insert("",tk.END,values=contact)self.tree.bind("<
defprint_selection(self,event):forselectioninself.tree.selection():item=self.tree.item(selection)last_name,first_name,email=item["values"][0:3]text="Selection:{},{}<{}>"print(text.format(last_name,first_name,email))还有更多...到目前为止,我们已经涵盖了ttk.Treeview类的一些基本方面,因为我们将其用作常规表。但是,还可以通过更高级的功能扩展我们现有的应用程序。
ttk.Treeview项目可用标签,因此可以为contacts表的特定行绑定事件序列。
假设我们希望在双击时打开一个新窗口以给联系人写电子邮件;但是,这仅适用于填写了电子邮件字段的记录。
我们可以通过在插入项目时有条件地向其添加标签,然后在小部件实例上使用"
columns=("Lastname","Firstname","Email")tree=ttk.Treeview(self,show="headings",columns=columns)forcontactincsv.reader(f):email=contact[2]tags=("dbl-click",)ifemailelse()self.tree.insert("",tk.END,values=contact,tags=tags)tree.tag_bind("dbl-click","
虽然ttk.Treeview可以用作常规表,但它也可能包含分层结构。这显示为树,其中的项目可以展开以查看层次结构的更多节点。
这对于显示递归调用的结果和多层嵌套项目非常有用。在此食谱中,我们将研究适合这种结构的常见场景。
为了说明如何在ttk.Treeview小部件中递归添加项目,我们将创建一个基本的文件系统浏览器。可展开的节点将表示文件夹,一旦打开,它们将显示它们包含的文件和文件夹:
树将最初由populate_node()方法填充,该方法列出当前目录中的条目。如果条目是目录,则还会添加一个空子项以显示它作为可展开节点。
打开表示目录的节点时,它会通过再次调用populate_node()来延迟加载目录的内容。这次,不是将项目添加为顶级节点,而是将它们嵌套在打开的节点内部:
在这个例子中,我们将使用os模块,它是Python标准库的一部分,提供了执行操作系统调用的便携方式。
os模块的第一个用途是将树的初始路径转换为绝对路径,以及初始化nodes字典,它将存储可展开项和它们表示的目录路径之间的对应关系:
importosimporttkinterastkimporttkinter.ttkasttkclassApp(tk.Tk):def__init__(self,path):#...abspath=os.path.abspath(path)self.nodes={}例如,os.path.abspath(".")将返回你从脚本运行的路径的绝对版本。我们更喜欢这种方法而不是使用相对路径,因为这样可以避免在应用程序中处理路径时出现混淆。
现在,我们使用垂直和水平滚动条初始化ttk.Treeview实例。图标标题的text将是我们之前计算的绝对路径:
self.tree=ttk.Treeview(self)self.tree.heading("#0",text=abspath,anchor=tk.W)ysb=ttk.Scrollbar(self,orient=tk.VERTICAL,command=self.tree.yview)xsb=ttk.Scrollbar(self,orient=tk.HORIZONTAL,command=self.tree.xview)self.tree.configure(yscroll=ysb.set,xscroll=xsb.set)然后,我们使用Grid布局管理器放置小部件,并使ttk.Treeview实例在水平和垂直方向上自动调整大小。
之后,我们绑定了"<
self.tree.bind("<
在populate_node()方法中,我们通过调用os.listdir()列出目录条目的名称。对于每个条目名称,我们执行以下操作:
defpopulate_node(self,parent,abspath):forentryinos.listdir(abspath):entry_path=os.path.join(abspath,entry)node=self.tree.insert(parent,tk.END,text=entry,open=False)ifos.path.isdir(entry_path):self.nodes[node]=entry_pathself.tree.insert(node,tk.END)当单击可展开项时,open_node()处理程序通过调用ttk.Treeview实例的focus()方法检索所选项。
此项标识符用于获取先前添加到nodes属性的绝对路径。为了避免在字典中节点不存在时引发KeyError,我们使用了它的pop()方法,它将第二个参数作为默认值返回——在我们的例子中是False。
如果节点存在,我们清除可展开节点的“虚假”项。调用self.tree.get_children(item)返回item的子项的标识符,然后通过调用self.tree.delete(children)来删除它们。
一旦清除了该项,我们通过使用item作为父项调用populate_node()来添加“真实”的子项:
defopen_node(self,event):item=self.tree.focus()abspath=self.nodes.pop(item,False)ifabspath:children=self.tree.get_children(item)self.tree.delete(children)self.populate_node(item,abspath)显示带有Notebook的选项卡窗格ttk.Notebook类是ttk模块中引入的另一种新的小部件类型。它允许您在同一窗口区域中添加许多应用程序视图,让您通过单击与每个视图关联的选项卡来选择应该显示的视图。
选项卡面板是重用GUI相同部分的好方法,如果多个区域的内容不需要同时显示。
以下应用程序显示了一些按类别分隔的待办事项列表,列表显示为只读数据,以简化示例:
我们使用固定大小实例化ttk.Notebook,然后循环遍历具有一些预定义数据的字典,这些数据将用于创建选项卡并向每个区域添加一些标签:
importtkinterastkimporttkinter.ttkasttkclassApp(tk.Tk):def__init__(self):super().__init__()self.title("TtkNotebook")todos={"Home":["Dothelaundry","Gogroceryshopping"],"Work":["InstallPython","LearnTkinter","Replyemails"],"Vacations":["Relax!"]}self.notebook=ttk.Notebook(self,width=250,height=100)self.label=ttk.Label(self)forkey,valueintodos.items():frame=ttk.Frame(self.notebook)self.notebook.add(frame,text=key,underline=0,sticky=tk.NE+tk.SW)fortextinvalue:ttk.Label(frame,text=text).pack(anchor=tk.W)self.notebook.pack()self.label.pack(anchor=tk.W)self.notebook.enable_traversal()self.notebook.bind("<
我们的ttk.Notebook小部件具有特定的宽度和高度,以及外部填充。
self.notebook=ttk.Notebook(self,width=250,height=100,padding=10)forkey,valueintodos.items():frame=ttk.Frame(self.notebook)self.notebook.add(frame,text=key,underline=0,sticky=tk.NE+tk.SW)fortextinvalue:ttk.Label(frame,text=text).pack(anchor=tk.W)之后,我们在ttk.Notebook小部件上调用enable_traversal()。这允许用户使用Ctrl+Shift+Tab和Ctrl+Tab在选项卡面板之间来回切换选项卡。
它还可以通过按下Alt和下划线字符来切换到特定的选项卡,即Alt+H代表Home选项卡,Alt+W代表Work选项卡,Alt+V代表Vacation选项卡。
当选项卡选择更改时,生成"<
self.notebook.pack()self.label.pack(anchor=tk.W)self.notebook.enable_traversal()self.notebook.bind("<
defselect_tab(self,event):tab_id=self.notebook.select()tab_name=self.notebook.tab(tab_id,"text")self.label.config(text=f"Yourcurrentselectionis:{tab_name}")还有更多...如果您想要检索ttk.Notebook当前显示的子窗口,您不需要使用任何额外的数据结构来将选项卡索引与小部件窗口进行映射。
Tkinter的nametowidget()方法可从所有小部件类中使用,因此您可以轻松获取与小部件名称对应的小部件对象:
defselect_tab(self,event):tab_id=self.notebook.select()frame=self.nametowidget(tab_id)#Dosomethingwiththeframe应用Ttk样式正如我们在本章的第一个配方中提到的,主题小部件具有特定的API来自定义它们的外观。我们不能直接设置选项,例如前景色或内部填充,因为这些值是通过ttk.Style类设置的。
在这个配方中,我们将介绍如何修改第一个配方中的小部件以添加一些样式选项。
为了添加一些默认设置,我们只需要一个ttk.Style对象,它提供以下方法:
例如,我们已经标记了以下两种情况的示例:
importtkinterastkimporttkinter.ttkastkclassApp(tk.Tk):def__init__(self):super().__init__()self.title("Tkthemedwidgets")style=ttk.Style(self)style.configure("TLabel",padding=10)style.map("TButton",foreground=[("pressed","grey"),("active","white")],background=[("pressed","white"),("active","grey")])#...现在,每个ttk.Label显示为10的填充,ttk.Button具有动态样式:当状态为pressed时,灰色前景和白色背景,当状态为active时,白色前景和灰色背景。
为我们的应用程序构建ttk.Style非常简单——我们只需要使用我们的父小部件作为它的第一个参数创建一个实例。
然后,我们可以为我们的主题小部件设置默认样式选项,使用大写的T加上小部件名称:TButton代表ttk.Button,TLabel代表ttk.Label,依此类推。然而,也有一些例外,因此建议您在Python解释器上调用小部件实例的winfo_class()方法来检查类名。
我们还可以添加前缀来标识我们不想默认使用的样式,但明确地将其设置为某些特定的小部件:
style.configure("My.TLabel",padding=10)#...label=ttk.Label(master,text="Sometext",style="My.TLabel")创建日期选择器小部件如果我们想让用户在我们的应用程序中输入日期,我们可以添加一个文本输入,强制他们编写一个带有有效日期格式的字符串。另一种解决方案是添加几个数字输入,用于日期、月份和年份,但这也需要一些验证。
与其他GUI框架不同,Tkinter不包括一个专门用于此目的的类;然而,我们可以选择应用我们对主题小部件的知识来构建一个日历小部件。
在这个配方中,我们将逐步解释使用Ttk小部件和功能制作日期选择器小部件的实现:
除了tkinter模块,我们还需要标准库中的calendar和datetime模块。它们将帮助我们对小部件中保存的数据进行建模和交互。
小部件标题显示了一对箭头,根据Ttk样式选项来前后移动当前月份。小部件的主体由一个ttk.Treeview表格组成,其中包含一个Canvas实例来突出显示所选日期单元格:
def__init__(self,master=None,**kw):now=datetime.datetime.now()fwday=kw.pop('firstweekday',calendar.MONDAY)year=kw.pop('year',now.year)month=kw.pop('month',now.month)sel_bg=kw.pop('selectbackground','#ecffc4')sel_fg=kw.pop('selectforeground','#05640e')super().__init__(master,**kw)然后,我们定义一些属性来存储日期信息:
小部件的可视部分在create_header()和create_table()方法中内部实例化,稍后我们将对其进行介绍。
我们还使用了一个tkfont.Font实例来帮助我们测量字体大小。
一旦这些属性被初始化,通过调用build_calendar()方法来安排日历的可视部分:
self.selected=Noneself.date=datetime.date(year,month,1)self.cal=calendar.TextCalendar(fwday)self.font=tkfont.Font(self)self.header=self.create_header()self.table=self.create_table()self.canvas=self.create_canvas(sel_bg,sel_fg)self.build_calendar()create_header()方法使用ttk.Style来显示箭头以前后移动月份。它返回显示当前月份名称的标签:
defcreate_header(self):left_arrow={'children':[('Button.leftarrow',None)]}right_arrow={'children':[('Button.rightarrow',None)]}style=ttk.Style(self)style.layout('L.TButton',[('Button.focus',left_arrow)])style.layout('R.TButton',[('Button.focus',right_arrow)])hframe=ttk.Frame(self)lbtn=ttk.Button(hframe,style='L.TButton',command=lambda:self.move_month(-1))rbtn=ttk.Button(hframe,style='R.TButton',command=lambda:self.move_month(1))label=ttk.Label(hframe,width=15,anchor='center')#...returnlabelmove_month()回调隐藏了用画布字段突出显示的当前选择,并将指定的offset添加到当前月份以设置date属性为上一个或下一个月份。然后,日历再次重绘,显示新月份的日期:
defmove_month(self,offset):self.canvas.place_forget()month=self.date.month-1+offsetyear=self.date.year+month//12month=month%12+1self.date=datetime.date(year,month,1)self.build_calendar()日历主体是在create_table()中使用ttk.Treeview小部件创建的,它在一行中显示当前月份的每周:
defcreate_table(self):cols=self.cal.formatweekheader(3).split()table=ttk.Treeview(self,show='',selectmode='none',height=7,columns=cols)table.bind('
defcreate_canvas(self,bg,fg):canvas=tk.Canvas(self.table,background=bg,borderwidth=0,highlightthickness=0)canvas.text=canvas.create_text(0,0,fill=fg,anchor=tk.W)handler=lambda_:canvas.place_forget()canvas.bind('
这种行为对每个月的第一周和最后一周很重要,因为这通常是我们找到这些空白位置的地方:
defbuild_calendar(self):year,month=self.date.year,self.date.monthmonth_name=self.cal.formatmonthname(year,month,0)month_weeks=self.cal.monthdayscalendar(year,month)self.header.config(text=month_name.title())items=self.table.get_children()[1:]forweek,iteminzip_longest(month_weeks,items):week=weekifweekelse[]fmt_week=['%02d'%dayifdayelse''fordayinweek]self.table.item(item,values=fmt_week)当您单击表项时,pressed()事件处理程序如果该项存在则设置选择,并重新显示画布以突出显示选择:
defpressed(self,event):x,y,widget=event.x,event.y,event.widgetitem=widget.identify_row(y)column=widget.identify_column(x)items=self.table.get_children()[1:]ifnotcolumnornotiteminitems:#clickedteheaderoroutsidethecolumnsreturnindex=int(column[1])-1values=widget.item(item)['values']text=values[index]iflen(values)elseNonebbox=widget.bbox(item,column)ifbboxandtext:self.selected='%02d'%textself.show_selection(bbox)show_selection()方法将画布放置在包含选择的边界框上,测量文本大小以使其适合其上方:
defshow_selection(self,bbox):canvas,text=self.canvas,self.selectedx,y,width,height=bboxtextw=self.font.measure(text)canvas.configure(width=width,height=height)canvas.coords(canvas.text,width-textw,height/2-1)canvas.itemconfigure(canvas.text,text=text)canvas.place(x=x,y=y)最后,selection属性使得可以将所选日期作为datetime.date对象获取。在我们的示例中没有直接使用它,但它是与TtkCalendar类一起使用的API的一部分: