在本章中,我们将介绍以下配方:
- 使用颜色
- 设置小部件字体
- 使用选项数据库
- 更改光标图标
- 介绍文本小部件
- 向文本小部件添加标记
默认情况下,Tkinter 小部件将以本机外观显示。虽然这种标准外观对于快速原型设计来说已经足够了,但我们可能需要定制一些小部件属性,例如字体、颜色和背景。
这种定制不仅影响小部件本身,还影响其内部项。我们将深入研究文本小部件,它与画布小部件一起是最通用的 Tkinter 类之一。文本小部件表示具有格式化内容的多行文本区域,有几种方法可以格式化字符或行并添加特定于事件的事件绑定。
在前面的配方中,我们使用颜色名称(如白色、蓝色或黄色)设置小部件的颜色。这些值作为字符串传递给foreground
和background
选项,它们修改小部件的文本和背景颜色。
颜色名称在内部映射到RGB值(一种通过红、绿、蓝强度组合表示颜色的相加模型),此转换基于平台相关的表。因此,如果希望在不同的平台上一致地显示相同的颜色,可以将 RGB 值传递给小部件选项。
以下应用程序演示如何动态更改显示固定文本的标签的foreground
和background
选项:
颜色以 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
模块。这些技术在更复杂的场景中非常有用,特别是当您想要组合多个函数时,并且很明显,一些参数已经预定义。
需要记住的一个小细节是,我们的foreground
和background
分别缺少fg
和bg
。在本语句中配置小部件时,使用**
解包这些字符串:
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
(泰晤士报新罗马)、Courier
和Helvetica
。它们可以通过连接到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)
要检索或更改选项值,您可以像往常一样使用cget
和configure
方法:
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 将使用选项数据库中定义的默认值,而不是使用其他选项配置字体foreground
和background
。
让我们先解释一下对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 的。
- 更改光标图标配方
- 介绍文本小部件配方