Skip to content

Latest commit

 

History

History
622 lines (438 loc) · 24.1 KB

File metadata and controls

622 lines (438 loc) · 24.1 KB

三、自定义小部件

在本章中,我们将介绍以下配方:

  • 使用颜色
  • 设置小部件字体
  • 使用选项数据库
  • 更改光标图标
  • 介绍文本小部件
  • 向文本小部件添加标记

介绍

默认情况下,Tkinter 小部件将以本机外观显示。虽然这种标准外观对于快速原型设计来说已经足够了,但我们可能需要定制一些小部件属性,例如字体、颜色和背景。

这种定制不仅影响小部件本身,还影响其内部项。我们将深入研究文本小部件,它与画布小部件一起是最通用的 Tkinter 类之一。文本小部件表示具有格式化内容的多行文本区域,有几种方法可以格式化字符或行并添加特定于事件的事件绑定。

使用颜色

在前面的配方中,我们使用颜色名称(如白色、蓝色或黄色)设置小部件的颜色。这些值作为字符串传递给foregroundbackground选项,它们修改小部件的文本和背景颜色。

颜色名称在内部映射到RGB值(一种通过红、绿、蓝强度组合表示颜色的相加模型),此转换基于平台相关的表。因此,如果希望在不同的平台上一致地显示相同的颜色,可以将 RGB 值传递给小部件选项。

准备

以下应用程序演示如何动态更改显示固定文本的标签的foregroundbackground选项:

颜色以 RGB 格式指定,并由用户使用本机颜色选择器对话框选择。以下屏幕截图显示了此对话框在 Windows 10 上的外观:

怎么做。。。

与往常一样,我们将使用标准按钮触发小部件配置,每个选项对应一个按钮。与前面示例的主要区别在于,可以使用tkinter.colorchooser模块中的askcolor对话框直接选择值:

from functools import partial

import tkinter as tk
from tkinter.colorchooser import askcolor

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Colors demo")
        text = "The quick brown fox jumps over the lazy dog"
        self.label = tk.Label(self, text=text)
        self.fg_btn = tk.Button(self, text="Set foreground color",
                                command=partial(self.set_color, "fg")) 
        self.bg_btn = tk.Button(self, text="Set background color",
                                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)

    def set_color(self, option):
        color = askcolor()[1]
        print("Chosen color:", 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模块。这些技术在更复杂的场景中非常有用,特别是当您想要组合多个函数时,并且很明显,一些参数已经预定义。

需要记住的一个小细节是,我们的foregroundbackground分别缺少fgbg。在本语句中配置小部件时,使用**解包这些字符串:

def set_color(self, option):
    color = askcolor()[1]
    print("Chosen color:", color)
    self.label.config(**{option: color}) # same as (fg=color)
                      or (bg=color)

askcolor返回一个元组,其中包含两个表示所选颜色的项。第一个是表示 RGB 值的整数元组,第二个是字符串形式的十六进制代码。因为第一个表示不能直接传递给小部件选项,所以我们使用十六进制格式。

还有更多。。。

如果要将颜色名称转换为 RGB 格式,可以在先前创建的小部件上使用winfo_rgb()方法。由于它返回一个从 0 到 65535 的整数元组来表示一个 16 位 RGB 值,因此您可以通过将 8 位向右移位,将其转换为更常见的*【RRGGBB】*十六进制表示:

rgb = widget.winfo_rgb("lightblue")
red, green, blue = [x>>8 for x in rgb]
print("#{:02x}{:02x}{:02x}".format(red, green, blue))

在前面的代码中,我们使用{:02x}将每个整数格式化为两个十六进制数。

设置小部件字体

在 Tkinter 中,可以自定义向用户显示文本的小部件中使用的字体,例如按钮、标签和条目。默认情况下,字体是特定于系统的,但您可以使用font选项进行更改。

准备

以下应用程序允许用户使用静态文本动态更改标签的字体系列和大小。请尝试使用不同的值查看字体配置的结果:

怎么做。。。

我们将有两个小部件来修改字体配置:一个带有字体系列名称的下拉选项和一个输入字体大小的微调框:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Fonts demo")
        text = "The quick brown fox jumps over the lazy dog"
        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)

    def set_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(泰晤士报新罗马)、CourierHelvetica。它们可以通过连接到self.family变量的OptionMenu小部件进行切换。

采用类似的方法将字体大小设置为Spinbox。这两个变量都会触发更改font标签的方法:

def set_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", "italic underline"))

您可以通过tkinter.font模块中的families()方法检索平台可用字体系列的完整列表。由于需要先实例化root窗口,因此可以使用以下脚本:

import tkinter as tk
from tkinter import font

root = tk.Tk()
print(font.families())

如果您使用的字体系列未包含在可用系列列表中,但会尝试匹配类似的字体,Tkinter 不会抛出任何错误。

还有更多。。。

tkinter.font模块包括一个Font类,可在多个小部件上重用。修改font实例的主要优点是,它会影响与font选项共享该实例的所有小部件。

使用Font类与使用字体描述符非常相似。例如,此代码段创建了一个 18 像素Courier粗体字体:

from tkinter import font
courier_18 = font.Font(family="Courier", size=18, weight=font.BOLD)

要检索或更改选项值,您可以像往常一样使用cgetconfigure方法:

family = courier_18.cget("family")
courier_18.configure(underline=1)

另见

  • 使用选项数据库配方

使用选项数据库

Tkinter 定义了一个称为选项数据库的概念,该机制用于定制应用程序的外观,而无需为每个小部件指定它。它允许您将一些小部件选项与单个小部件配置分离,提供基于小部件层次结构的标准化默认值。

准备

在此配方中,我们将使用几个具有不同样式的小部件构建一个应用程序,这些小部件将在选项数据库中定义:

怎么做。。。

在我们的示例中,我们将通过option_add()方法向数据库添加一些选项,该方法可从所有小部件类访问:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Options demo")
        self.option_add("*font", "helvetica 10")
        self.option_add("*header.font", "helvetica 18 bold")
        self.option_add("*subtitle.font", "helvetica 14 italic")
        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="This is the header")
        self.create_label(name="subtitle", text="This is the subtitle")
        self.create_label(text="This is a paragraph")
        self.create_label(text="This is another paragraph")
        self.create_button(text="See more")

    def create_label(self, **options):
        tk.Label(self, **options).pack(padx=20, pady=5, anchor=tk.W)

    def create_button(self, **options):
        tk.Button(self, **options).pack(padx=5, pady=5, anchor=tk.E)

if __name__ == "__main__":
    app = App()
    app.mainloop()

因此,Tkinter 将使用选项数据库中定义的默认值,而不是使用其他选项配置字体foregroundbackground

它是如何工作的。。。

让我们先解释一下对option_add的每次呼叫。第一次调用添加了一个选项,该选项为所有 widget 设置font属性,通配符表示任何应用程序名称:

self.option_add("*font", "helvetica 10")

下一个调用将匹配限制为具有header名称的元素。规则越具体,其优先级越高。稍后在用name="header"实例化标签时指定此名称:

self.option_add("*header.font", "helvetica 18 bold")

这同样适用于self.option_add("*subtitle.font", "helvetica 14 italic"),因此每个选项都与不同的命名小部件实例相匹配。

下一个选项使用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")

正如我们前面提到的,选项数据库使用小部件层次结构来确定应用于每个实例的选项,因此如果我们有嵌套容器,它们也可以用来限制优先的选项。

These configuration options are not applied to existing widgets, only to the ones created after modifying the options database. Therefore, we always recommend calling option_add() at the beginning of your applications.

以下是一些示例,其中每个示例都比前面的示例更具体:

  • *Frame*background:匹配一帧内所有窗口小部件的背景
  • *Frame.background:匹配所有帧的背景
  • *Frame.myButton.background:匹配名为myButton的小部件的背景
  • *myFrame.myButton.background:匹配名为myFrame的容器中名为myButton的小部件的背景

还有更多。。。

也可以使用以下格式在单独的文本文件中定义选项,而不是以编程方式添加选项:

*font: helvetica 10
*header.font: helvetica 18 bold
*subtitle.font: helvetica 14 italic
*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("Options demo")
        self.option_readfile("my_options_file")
        # ...

如果文件不存在或格式无效,Tkinter 将发出TclError

另见

  • 处理颜色配方
  • 设置控件字体配方

更改光标图标

Tkinter 允许您在将光标悬停在小部件上时自定义光标图标。此行为有时在默认情况下启用,例如显示 I-beam 指针的条目小部件。

准备

以下应用程序显示了如何在执行长时间运行的操作时显示忙碌的光标,以及带有问号的光标(通常在帮助菜单中使用):

怎么做。。。

可以使用cursor选项更改鼠标指针图标。在我们的示例中,我们使用watch值显示本机忙碌光标,question_arrow显示带问号的常规箭头:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Cursors demo")
        self.resizable(0, 0)
        self.label = tk.Label(self, text="Click the button to start")
        self.btn_launch = tk.Button(self, text="Start!",
                                    command=self.perform_action)
        self.btn_help = tk.Button(self, text="Help",
                                  cursor="question_arrow")

        btn_opts = {"side": tk.LEFT, "expand":True, "fill": tk.X,
                    "ipadx": 30, "padx": 20, "pady": 5}
        self.label.pack(pady=10)
        self.btn_launch.pack(**btn_opts)
        self.btn_help.pack(**btn_opts)

    def perform_action(self):
        self.config(cursor="watch")
        self.btn_launch.config(state=tk.DISABLED)
        self.btn_help.config(state=tk.DISABLED)
        self.label.config(text="Working...")
        self.after(3000, self.end_action)

    def end_action(self):
        self.config(cursor="arrow")
        self.btn_launch.config(state=tk.NORMAL)
        self.btn_help.config(state=tk.NORMAL)
        self.label.config(text="Done!")

if __name__ == "__main__":
    app = App()
    app.mainloop()

您可以在的 Tcl/Tk 官方文档中查看有效的cursor值和系统特定值的完整列表 https://www.tcl.tk/man/tcl/TkCmd/cursors.htm

它是如何工作的。。。

如果一个小部件没有指定cursor选项,它将接受父容器中定义的值。因此,我们可以通过将其设置在root窗口级别,轻松地将其应用于所有小部件。这是通过在perform_action()方法中调用set_watch_cursor()来完成的:

def perform_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()列表,可以在子窗口小部件中递归调用执行此操作的函数:

def perform_action(self):
    self.set_watch_cursor(self)
    # ...

def end_action(self):
 self.restore_cursor(self)
    # ...

def set_watch_cursor(self, widget):
    widget._old_cursor = widget.cget("cursor")
    widget.config(cursor="watch")
    for w in widget.winfo_children():
        self.set_watch_cursor(w)

def restore_cursor(self, widget):
    widget.config(cursor=widget._old_cursor)
    for w in widget.winfo_children():
        self.restore_cursor(w)

在前面的代码中,我们为每个小部件添加了_old_cursor属性,因此如果您采用类似的方法,请记住您不能在set_watch_cursor()之前调用restore_cursor()

介绍文本小部件

与其他小部件类相比,文本小部件提供了高级功能。它显示多行可编辑文本,可按行和列编制索引。此外,您可以使用标记引用文本范围,标记可以定义自定义的外观和行为。

准备

以下应用程序显示了文本小部件的基本用法,您可以在其中动态插入和删除文本并检索所选内容:

怎么做。。。

除了Text小部件外,我们的应用程序还包含三个按钮,它们调用清除整个文本内容的方法,在当前光标位置插入"Hello, world"字符串,并打印用鼠标或键盘进行的当前选择:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Text demo")
        self.resizable(0, 0)
        self.text = tk.Text(self, width=50, height=10)
        self.btn_clear = tk.Button(self, text="Clear text",
                                   command=self.clear_text)
        self.btn_insert = tk.Button(self, text="Insert text",
                                    command=self.insert_text)
        self.btn_print = tk.Button(self, text="Print selection",
                                   command=self.print_selection)
        self.text.pack()
        self.btn_clear.pack(side=tk.LEFT, expand=True, pady=10)
        self.btn_insert.pack(side=tk.LEFT, expand=True, pady=10)
        self.btn_print.pack(side=tk.LEFT, expand=True, pady=10)

    def clear_text(self):
        self.text.delete("1.0", tk.END)

    def insert_text(self):
        self.text.insert(tk.INSERT, "Hello, world")

    def print_selection(self):
        selection = self.text.tag_ranges(tk.SEL)
        if selection:
            content = self.text.get(*selection)
            print(content)

if __name__ == "__main__":
    app = App()
    app.mainloop()

它是如何工作的。。。

我们的Text小部件最初是空的,宽度为 50 个字符,高度为 10 行。除了允许用户输入任何类型的文本外,我们还将深入研究每个按钮使用的方法,以便更好地了解如何与此小部件交互。

delete(start, end)方法将内容从start索引移除到end索引。如果省略第二个参数,则仅删除start位置的字符。

在我们的示例中,我们通过调用此方法将所有文本从1.0索引(第一行第 0 列)删除到tk.END索引,后者表示最后一个字符:

def clear_text(self):
    self.text.delete("1.0", tk.END)

insert(index, text)方法在index位置插入给定文本。这里,我们用INSERT索引来称呼它,它对应于插入光标的位置:

def insert_text(self):
    self.text.insert(tk.INSERT, "Hello, world")

tag_ranges(tag)方法返回一个元组,其中包含给定tag的所有范围的第一个和最后一个索引。我们使用了特殊的tk.SEL标记来引用当前选择。如果没有选择,此调用将返回一个空元组。这与返回给定范围内文本的get(start, end)方法相结合:

def print_selection(self):
    selection = self.text.tag_ranges(tk.SEL)
    if selection:
        content = self.text.get(*selection)
        print(content)

由于SEL标记只对应一个范围,我们可以安全地将其解包以调用get方法。

向文本小部件添加标记

在本食谱中,您将学习如何在Text小部件中配置标记字符范围的行为。

所有这些概念都与那些应用于常规小部件的概念相同,如事件序列或配置选项,它们已经在前面的食谱中介绍过。主要区别在于我们需要使用文本索引来识别标记的内容,而不是使用对象引用。

准备

为了说明如何使用文本标记,我们将创建一个模拟超链接插入的Text小部件。单击时,此链接将使用默认浏览器打开选定的 URL。

例如,如果用户输入以下内容,python.org 文本可以标记为超链接:

怎么做。。。

对于这个应用程序,我们将定义一个名为"link"的标记,它表示一个可点击的超链接。此标记将使用按钮添加到当前选择中,鼠标单击将触发事件以在浏览器中打开链接:

import tkinter as tk
import webbrowser

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Text tags demo")
        self.text = tk.Text(self, width=50, height=10)
        self.btn_link = tk.Button(self, text="Add hyperlink",
                                  command=self.add_hyperlink)

        self.text.tag_config("link", foreground="blue", underline=1)
        self.text.tag_bind("link", "<Button-1>", self.open_link)
        self.text.tag_bind("link", "<Enter>",
                           lambda _: self.text.config(cursor="hand2"))
        self.text.tag_bind("link", "<Leave>",
                           lambda e: self.text.config(cursor=""))

        self.text.pack()
        self.btn_link.pack(expand=True)

    def add_hyperlink(self):
        selection = self.text.tag_ranges(tk.SEL)
        if selection:
            self.text.tag_add("link", *selection)

    def open_link(self, event):
        position = "@{},{} + 1c".format(event.x, event.y)
        index = self.text.index(position)
        prevrange = self.text.tag_prevrange("link", index)
        url = self.text.get(*prevrange)
        webbrowser.open(url)

if __name__ == "__main__":
    app = App()
    app.mainloop()

它是如何工作的。。。

首先,我们将通过配置颜色和下划线样式来初始化标记。我们添加事件绑定以使用浏览器打开单击的文本,并在将鼠标放置在标记文本上时更改光标外观:

def __init__(self):
    # ...
    self.text.tag_config("link", foreground="blue", underline=1)
    self.text.tag_bind("link", "<Button-1>", self.open_link)
    self.text.tag_bind("link", "<Enter>",
                       lambda e: self.text.config(cursor="hand2"))
    self.text.tag_bind("link", "<Leave>",
                       lambda e: self.text.config(cursor=""))

open_link方法中,我们使用Text类的index方法将点击的位置转换为对应的行和列:

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 是否具有网络位置:

from urllib.parse import urlparse

def validate_hyperlink(self, url):
    return urlparse(url).netloc

尽管此解决方案不打算处理某些特殊情况,但它可以作为丢弃大多数无效 URL 的第一种方法。

通常,可以使用标记创建复杂的基于文本的程序,例如带有语法高亮显示的 IDE。事实上,默认 Python 实现中捆绑的 IDLE 是基于 Tkinter 的。

另见

  • 更改光标图标配方
  • 介绍文本小部件配方