虽然程序可以完美地使用黑色、白色和灰色的纯文本,但对颜色、字体和图像的微妙使用可以增强即使是最实用的应用的视觉吸引力和可用性。您的数据输入应用也不例外。您的老板和用户已经向您提出了几个问题,这些问题似乎需要使用 Tkinter 的样式功能。您的老板告诉您,公司总部要求所有内部软件都要突出显示公司徽标,而数据输入人员提到了应用可读性和整体外观的各种问题。
在本章中,我们将学习 Tkinter 的一些功能,这些功能将帮助我们解决这些问题:
- 我们将学习如何向 Tkinter GUI 添加图像
- 我们将学习如何调整 Tkinter 小部件中的字体和颜色,包括直接调整和使用标记调整
- 我们将学习如何使用样式和主题调整 Ttk 小部件的外观
我们要处理的第一个要求是添加公司徽标。作为公司政策的结果,您的应用应该嵌入公司徽标,并且您被要求尽可能使您的应用符合要求。
要将此图像添加到我们的应用中,您需要了解 Tkinter 的PhotoImage类。
几个 Tkinter 小部件,包括Label和Button可以接受image参数,这允许它们显示图像。在这些情况下,我们不能简单地设置图像文件的路径;相反,我们必须创建一个PhotoImage对象。
制作一个PhotoImage对象相当简单:
myimage = tk.PhotoImage(file='my_image.png')通常使用关键字参数file调用PhotoImage,该参数指向文件路径。或者,您可以使用data参数指向包含图像数据的bytes对象。
在接受image参数的任何地方都可以使用PhotoImage,例如Label:
mylabel = tk.Label(root, image=myimage)需要注意的是,应用必须保留对PhotoImage对象的引用,该引用将在图像显示期间保持在范围内;否则,图像将不会显示。
考虑下面的例子:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
smile = tk.PhotoImage(file='smile.gif')
tk.Label(self, image=smile).pack()
App().mainloop()如果运行此示例,您将注意到没有显示任何图像。这是因为一旦__init__()退出,smile变量就会被销毁;没有参考PhotoImage对象,图像消失,即使我们已经将其打包到布局中。要解决这个问题,您需要将image对象存储在一个实例变量中,例如self.smile,该变量将在方法返回后继续存在。
Tkinter 中的图像支持仅限于 GIF、PGM、PPM 和(从 8.6 版起)PNG 文件。要使用其他文件格式,如 JPEG 或 BMP,您需要使用图像处理库(如 Pillow)将其转换为 Tkinter 能够理解的格式。
At the time of writing, Python 3 for macOS ships with Tkinter 8.5. To use PNG on macOS, you'll need to upgrade to Tkinter 8.6 or later, or use Pillow. Please see https://www.python.org/download/mac/tcltk/ for more information about Tcl/Tk and macOS. Pillow is not in the Python standard library. To install it, follow the instructions at http://python-pillow.org.
根据我们对PhotoImage的了解,在我们的程序中添加公司徽标应该很简单;然而,我们必须解决如何确定图像文件路径的问题。路径可以是绝对的,也可以是相对于工作目录的,但是我们不知道在另一个系统上会是什么。幸运的是,有办法解决这个问题。
在abq_data_entry目录下,创建一个名为images的新目录,并在其中放置一个大小合适的 PNG 文件,我们可以在应用中使用它(该图像的纵横比为 8x5,因此在本例中,我们使用32x20。为了获得图像的绝对路径,我们将依赖 Python 中名为__file__的内置变量。在任何 Python 脚本中,__file__变量将包含当前脚本文件的绝对路径,我们可以使用它来定位图像文件。
例如,从我们的application.py文件中,我们可以使用以下代码找到我们的图像:
from os import path
image_path = path.join(path.dirname(__file__),
'images/abq_logo_32x20.png')在本例中,我们首先通过调用path.dirname(__file__)找到包含application.py文件的目录。这给了我们一个到abq_data_entry的绝对路径,从中我们知道到图像的相对路径。我们可以将这两条路径连接起来,并拥有到映像的绝对路径,无论程序安装在文件系统的何处。
这种方法工作得很好,但是考虑到我们可能希望从我们的应用中的各种模块中访问图像,并且必须导入 HORT T0,并且在多个文件中重复这个逻辑是不太理想的。一种更简洁的方法是将images文件夹视为 Python 包,并在其中创建指向图像路径的常量。
首先在images文件夹中创建一个__init__.py文件,并添加以下代码:
from os import path
IMAGE_DIRECTORY = path.dirname(__file__)
ABQ_LOGO_32 = path.join(IMAGE_DIRECTORY, 'abq_logo-32x20.png')
ABQ_LOGO_64 = path.join(IMAGE_DIRECTORY, 'abq_logo-64x40.png')现在,我们的application.py模块可以简单地做到:
from .images import ABQ_LOGO_32Application.__init__()然后可以使用ABQ_LOGO_32中的路径创建PhotoImage对象:
self.logo = tk.PhotoImage(file=ABQ_LOGO_32)
tk.Label(self, image=self.logo).grid(row=0)创建PhotoImage对象后,我们使用Label显示它。如果您运行应用,您应该会看到徽标显示在顶部。
我们还可以添加徽标作为窗口图标,这比保留默认的 Tkinter 徽标更有意义。这样,徽标将显示在窗口装饰和操作系统的任务栏中。
作为 Tk 的一个子类,我们的Application对象有一个名为iconbitmap的方法,该方法应该在给定图标文件路径的情况下,适当地设置图标。不幸的是,这种方法对给定的文件类型相当挑剔,并且不能跨平台很好地工作。我们可以使用PhotoImage和特殊的 Tkcall()方法来解决这个问题。
The call method allows us to directly call Tcl/Tk commands, and can be useful to access Tk capabilities that Tkinter wraps poorly or not at all.
代码如下所示:
self.taskbar_icon = tk.PhotoImage(file=ABQ_LOGO_64)
self.call('wm', 'iconphoto', self._w, self.taskbar_icon)第一行创建了另一个PhotoImage对象,引用了更大版本的徽标。接下来,我们执行self.call(),传入 Tcl/Tk 命令的各个令牌。在本例中,我们调用wm iconphoto命令。self._w返回Application对象的 Tcl/Tk 名称;最后,我们传入我们创建的PhotoImage对象。
Hopefully, you won't need to use call often, but if you do, you can find documentation about Tcl/Tk commands at: https://www.tcl.tk/doc/ .
运行应用并注意图标是如何更改的。
Tkinter 基本上有两个样式系统:旧的 Tkinter widgets 系统和新的 Ttk 系统。因为我们仍然需要同时使用 Tkinter 和 Ttk 小部件,所以我们必须研究这两个系统。让我们先看看老的 TKTETER 系统,并在我们的应用中应用 TKITER 小部件的一些样式。
基本 Tkinter 小部件允许您更改两种颜色:前景,主要表示文本和边框;以及背景,表示小部件的其余部分。可以使用foreground和background参数或其别名fg和bg设置这些参数。
本例显示了Label上颜色的使用:
l = tk.Label(text='Hot Dog!', fg='yellow', bg='red')颜色的值可以是颜色名称字符串或 CSS 样式的 RGB 十六进制字符串。
例如,此代码产生相同的效果:
l2 = tk.Label(text='Also Hot Dog!',
foreground='#FFFF00',
background='#FF0000')Tkinter 识别的命名颜色超过 700 种,大致相当于 Linux 和 Unix 上使用的 X11 显示服务器识别的颜色,或者 web 设计师使用的 CSS 命名颜色。有关完整列表,请参见https://www.tcl.tk/man/tcl8.6/TkCmd/colors.htm 。
您从数据输入人员处收到的一个请求是增加数据输入表单各部分之间的视觉分隔。我们的LabelFrame小部件是简单的 Tkinter 小部件(而不是 Ttk),因此我们可以通过给各部分添加彩色背景来完成这一点。
经过一番思考和讨论,您决定对各部分进行颜色编码,如下所示:
- 记录信息将使用
khaki,表示用于纸质记录的经典马尼拉文件夹 - 环境信息将使用
lightblue,水和空气的符号 - 植物信息将具有
lightgreen背景,象征植物 Notes已经足够独特,因此将保持不变
打开views.py并编辑DataRecordForm.__init__()中的LabelFrame调用:
recordinfo = tk.LabelFrame(
self, text="Record Information",
bg="khaki", padx=10, pady=10)
...
environmentinfo = tk.LabelFrame(
self, text="Environment Data",
bg='lightblue', padx=10, pady=10)
...
plantinfo = tk.LabelFrame(
self, text="Plant Data",
bg="lightgreen", padx=10, pady=10)请注意,我们在这里也添加了一些填充,以使小部件周围的颜色更可见,并在表单中创建更多的分隔。
我们应该在Notes小部件周围添加类似的填充:
self.inputs['Notes'].grid(sticky="w", row=4, column=0,
padx=10, pady=10)在本例中,我们将填充添加到grid调用中,以便整个LabelInput都被转移。
结果(至少在 Debian Linux 上)如下所示:
虽然还不是视觉杰作,但我们在表单部分之间有一些分隔和颜色编码。
前台和后台对于按钮等简单的小部件来说已经足够了,但更复杂的 Tkinter 小部件,如Text小部件或 TtkTreeview依赖于标签系统。Tkinter 中的标记是小部件内容的命名区域,可以应用颜色和字体设置。为了了解它是如何工作的,让我们构建一个粗糙但漂亮的 Python shell。
我们将首先创建一个Text小部件:
import tkinter as tk
text = tk.Text(width=50, height=20, bg='black', fg='lightgreen')
text.pack()在这里,我们使用了fg和bg参数来设置一个由黑变绿的主题,这在程序员中很流行。不过,让我们为提示符和解释器输出配置不同的颜色,而不是只有绿色文本。
为此,我们将定义一些标记:
text.tag_configure('prompt', foreground='magenta')
text.tag_configure('output', foreground='yellow')tag_configure方法允许我们在Text小部件上创建和配置标记。我们为 shell 提示符创建了一个名为'prompt'的命令,为 Python 输出创建了另一个名为'output'的命令。
对于应用了给定标记的insert文本,我们执行以下操作:
text.insert('end', '>>> ', ('prompt',))您可能还记得,Text.insert方法将索引和字符串作为其前两个参数。注意第三个参数:这是一个标记的元组,我们想用它来标记插入的文本。即使只使用一个标记,该值也必须是元组。
如果您在代码末尾添加text.mainloop()并运行它,您将看到一个带有洋红色提示的黑色文本输入窗口,但如果您键入文本,它将显示为绿色。到现在为止,一直都还不错;现在,让我们让它执行一些 Python。
在mainloop()调用之前创建一个函数:
def on_return(*args):
cmd = text.get('prompt.last', 'end').strip()从Text小部件检索文本时,我们需要为要检索的文本提供开始和结束索引。请注意,我们在索引中使用了标记名。prompt.last告诉 Tkinter 从标记为prompt的区域结束后开始获取文本。
接下来执行cmd:
if cmd:
try:
output = str(eval(cmd))
except Exception as e:
output = str(e)如果我们的cmd实际上包含任何内容,我们将尝试使用eval执行它,然后将响应值的字符串存储为output。如果它抛出异常,我们将以字符串形式获取异常,并将其设置为output。
然后,我们将展示我们的output:
text.insert('end', '\n' + output, ('output',))我们插入我们的output文本,以换行符开头,并标记为output。
我们将通过向用户返回一个prompt来完成此功能:
text.insert('end', '\n>>> ', ('prompt',))
return 'break'我们还返回字符串break,告诉 Tkinter 忽略触发回调的原始事件。因为我们将从Return/Enter击键触发此操作,所以我们希望在完成后忽略该击键。如果没有,则在函数返回后将执行击键,并且用户将在提示下在线。
最后,我们需要将函数绑定到Return键:
text.bind('<Return>', on_return)请注意,Enter/Return键的事件始终是<Return>,即使是在非苹果硬件上(该键通常标记为Enter)。
您的应用应该如下所示:
虽然这个外壳不会很快取代闲置的,但它看起来确实很不错,你不觉得吗?
尽管Treeview是一个 Ttk 小部件,但它使用标记控制单个行的样式。我们可以用它来回答您从数据输入人员那里收到的另一个请求:他们希望记录列表突出显示在当前会话期间更新和插入的记录。
我们需要做的第一件事是让Application对象跟踪会话期间更新或插入的行。
在Application.__init__()中,我们将创建以下实例变量:
self.inserted_rows.clear()
self.updated_rows.clear()保存记录时,我们需要使用其行号更新这些列表中的一个或另一个。我们将在Application.on_save()中执行此操作,在保存记录后,但在重新填充记录列表之前。
首先,我们将检查更新的记录:
if rownum is not None:
self.updated_rows.append(rownum)更新的rownum没有None值,因此如果是这种情况,我们会将其附加到列表中。如果一个记录不断更新,我们的列表中会有重复的记录,但这对我们的运营规模来说并没有任何影响。
现在,我们需要处理插入:
else:
rownum = len(self.data_model.get_all_records()) - 1
self.inserted_rows.append(rownum)插入的记录比较麻烦,因为我们没有现成的行号可供记录。但是,我们知道插入总是附加到文件的末尾,因此它应该比文件中的行数少一行。
我们插入和更新的记录将保留到程序会话结束(当用户退出程序时),但如果用户选择新文件,我们需要手动删除这些记录。
我们可以通过清除on_file_select()中的列表来处理:
if filename:
...
self.inserted_rows = []
self.updated_rows = []现在,我们的控制器知道插入和更新的记录。然而,我们的记录列表没有;我们需要解决这个问题。
在Application.__init__()中找到RecordList调用,并将变量添加到其参数中:
self.recordlist = v.RecordList(
self, self.callbacks,
self.inserted_rows,
self.updated_rows)现在,我们需要回到views.py并告诉RecordList如何处理这些信息。
我们将首先更新其参数列表并将列表保存到实例变量:
def __init__(self, parent, callbacks,
inserted, updated,
*args, **kwargs):
self.inserted = inserted
self.updated = updated接下来,我们需要用适当的颜色配置标签。我们的数据输入人员认为,lightgreen将是插入记录的合理颜色,lightblue将是更新记录的合理颜色。
在self.treeview配置后的__init__()中增加此代码:
self.treeview.tag_configure('inserted', background='lightgreen')
self.treeview.tag_configure('updated', background='lightblue')正如我们之前对Text小部件所做的那样,我们调用tag_configure将背景颜色设置与标记名连接起来。请注意,您在这里不限于一个配置设置;我们可以想象为同一个呼叫添加foreground、font或其他配置设置。
要将标记添加到TreeView行,我们需要更新populate方法。
在for循环中,在插入行之前,我们将添加以下代码:
if self.inserted and rownum in self.inserted:
tag = 'inserted'
elif self.updated and rownum in self.updated:
tag = 'updated'
else:
tag = ''如果inserted列表存在并且我们的rownum在其中,我们希望tag等于'inserted';如果updated列表存在并且我们的rownum在其中,我们希望它是'updated'。否则,我们将其留空。
现在,我们的treeview.insert调用只需要修改这个tag值:
self.treeview.insert('', 'end', iid=str(rownum),
text=str(rownum), values=values,
tag=tag)运行应用并尝试插入和更新一些记录。
你应该得到这样的东西:
在 Tkinter 中指定小部件字体有三种方法。
最简单的方法是只使用字符串格式:
tk.Label(text="Direct font format",
font="Times 20 italic bold")字符串格式为Font-family``size``styles,其中styles可以是任何有效的文本样式关键字组合。
这些词语包括:
bold用于黑体文字,或normal用于正常重量italic表示斜体,或roman表示规则斜体underline用于带下划线的文本overstrike用于删除的文本
除字体系列之外的所有内容都是可选的,不过如果要指定任何样式关键字,则需要指定一个size。样式关键字的顺序无关紧要,但权重和倾斜关键字是互斥的(即不能有bold``normal或italic roman。
字符串方法的一个缺点是它不能处理名称中带有空格的字体。
要处理这些问题,可以对字体使用元组格式:
tk.Label(
text="Tuple font format",
font=('Droid sans', 15, 'overstrike'))此格式与字符串格式完全相同,只是不同的组件作为元组中的项写入。size组件可以是整数或包含数字的字符串,根据值的来源提供一定的灵活性。
这种方法适用于在启动时设置少量字体更改,但对于需要动态操作字体设置的情况,Tkinter 有一个名为**命名字体的功能。**这种方法使用一个Font类,该类可以分配给小部件,然后动态更改。
要使用Font,必须从tkinter.font模块导入:
from tkinter.font import Font现在,我们可以创建一个自定义Font对象并将其分配给小部件:
labelfont = Font(family='Courier', size=30,
weight='bold', slant='roman',
underline=False, overstrike=False)
tk.Label(text='Using the Font class', font=labelfont).pack()如您所见,Font构造函数参数关联字符串和元组字体规范中使用的关键字。
分配此字体后,我们可以在运行时动态更改其各个方面:
def toggle_overstrike():
labelfont['overstrike'] = not labelfont['overstrike']
tk.Button(text='Toggle Overstrike', command=toggle_overstrike).pack()在本例中,我们提供了一个Button,它将打开和关闭overstrike属性。
Tk 附带了几个已配置的命名字体;我们可以使用tkinter.font模块中的nametofont函数从它们创建 PythonFont对象。
此表显示了 Tkinter 中包含的一些命名字体:
| 字体名称 | 默认为 | 用于 |
| TkCaptionFont | 系统标题字体 | 窗口和对话框标题栏 |
| TkDefaultFont | 系统默认字体 | 未另行规定的项目 |
| TkFixedFont | 系统固定宽度字体 | 没有什么 |
| TkHeadingFont | 系统标题字体 | 列表和表格中的列标题 |
| TkIconFont | 系统图标字体 | 图标说明 |
| TkMenuFont | 系统菜单字体 | 菜单标签 |
| TkSmallCaptionFont | 系统名称 | 子窗口、工具对话框 |
| TkTextFont | 系统输入字体 | 输入小部件:Entry、Spinbox 等 |
| TkTooltipFont | 系统工具提示字体 | 工具提示 |
如果您对 Tkinter 在您的操作系统上使用的字体感到好奇,您可以使用tkinter.font.names()函数检索它们的列表。
要更改应用的整体外观,我们可以覆盖这些命名字体,这些更改将应用于所有没有字体集的小部件。
例如:
import tkinter as tk
from tkinter.font import nametofont
default_font = nametofont('TkDefaultFont')
default_font.config(family='Helvetica', size=32)
tk.Label(text='Feeling Groovy').pack()在本例中,我们使用nametofont函数检索TkDefaultFont的对象,这是 Tkinter 应用的默认命名字体类。检索后,我们可以设置其字体family和size,使用TkDefaultFont更改所有小部件的这些值。
然后,Label显示该调整的结果:
我们的一些数据输入用户抱怨应用的字体太小,不容易阅读,但其他人不喜欢增加字体,因为这样会使应用在屏幕上显得太大。为了适应所有用户,我们可以添加一个配置选项,允许他们设置首选字体大小。
首先,我们需要在设置模型中添加一个'font size'选项。
打开models.py追加SettingsModel.variables字典如下:
variables = {
...
'font size': {'type': 'int', 'value': 9}接下来,我们将在选项菜单中添加一组单选按钮,以便用户可以设置值。
打开views.py并在选项菜单添加到主菜单之前开始创建菜单:
font_size_menu = tk.Menu(self, tearoff=False)
for size in range(6, 17, 1):
font_size_menu.add_radiobutton(
label=size, value=size,
variable=settings['font size'])
options_menu.add_cascade(label='Font size',
menu=font_size_menu)这看起来应该很熟悉,因为我们在学习 TkinterMenu小部件时创建了一个几乎相同的字体大小菜单。我们允许从6到16的字体,这应该为我们的用户提供足够的范围。
在Application类中,让我们创建一个将字体设置应用于应用字体的方法:
def set_font(self, *args):我们包括*args,因为set_font将被调用为trace回调,所以我们需要捕获发送进来的任何参数,即使我们不使用它们。
接下来,我们将得到当前的'font size'值:
font_size = self.settings['font size'].get()我们需要更改几个命名字体,而不仅仅是TkDefaultFont。对于我们的应用,TkDefaultFont、TkTextFont和TkMenuFont应该足够了。
我们将循环这些,检索类并设置每个类的大小:
font_names = ('TkDefaultFont', 'TkMenuFont', 'TkTextFont')
for font_name in font_names:
tk_font = nametofont(font_name)
tk_font.config(size=font_size)我们需要做的最后一件事是确保调用此回调。
在load_settings()调用Application.__init__()之后,添加以下内容:
self.set_font()
self.settings['font size'].trace('w', self.set_font)我们调用set_font()一次以激活任何保存的font size设置,然后设置trace以在值更改时运行它。
运行应用并尝试字体菜单。它应该是这样的:
Ttk 小部件相对于标准 Tkinter 小部件而言,在功能和灵活性方面有了重大改进,可以对其进行样式化。这种灵活性使 Ttk 小部件能够跨平台模拟本机 UI 控件,但这是有代价的:Ttk 样式令人困惑,文档记录不完整,有时不一致。
为了理解 Ttk 样式,让我们从一些词汇开始,从最基本的元素到最复杂的元素:
- Ttk 以元素开头。元素是小部件的一部分,例如边框、箭头或可以键入文本的字段。
- 元素使用布局组合成一个完整的小部件(例如
Combobox或Treeview)。 - 样式是定义颜色和字体设置的属性集合:
- 每个样式都有一个名称,通常是 T,加上小部件的名称,例如
TButton或TEntry。这有一些例外。 - 布局中的每个元素都引用一个或多个样式特性来定义其外观。
- 每个样式都有一个名称,通常是 T,加上小部件的名称,例如
- 小部件有许多状态,这些状态是可以打开或关闭的标志:
- 样式可以配置一个将属性值连接到状态或状态组合的映射
- 一系列样式被称为主题。Tkinter 在不同的平台上发布不同的主题:
- 一个主题不仅可以定义不同的样式,还可以定义不同的布局。例如,默认 macOS 主题上的
ttk.Button可能包含不同的元素集,与 Windows 中使用默认主题的ttk.Button应用样式设置不同。
- 一个主题不仅可以定义不同的样式,还可以定义不同的布局。例如,默认 macOS 主题上的
如果你在这一点上感到困惑,那没关系。让我们深入剖析一下这些想法,以便更好地了解这些想法。
为了更好地了解 Ttk 小部件是如何构建的,请在 IDLE 中打开一个 shell 并导入tkinter、ttk和pprint:
>>> import tkinter as tk
>>> from tkinter import ttk
>>> from pprint import pprint现在,创建一个根窗口Combobox和Style对象:
>>> root = tk.Tk()
>>> cb = ttk.Combobox(root)
>>> cb.pack()
>>> style = ttk.Style()Style对象可能有点命名错误;它并不指向单一的样式,而是为我们提供了一个句柄来检查和更改当前主题的样式、布局和地图。
为了检查我们的Combobox,我们首先使用winfo_class()方法得到它的stylename:
>>> cb_stylename = cb.winfo_class()
>>> print(cb_stylename)
TCombobox然后我们可以使用Style.layout()方法检查Combobox的布局:
>>> cb_layout = style.layout(cb_stylename)
>>> pprint(cb_layout)
[('Combobox.field',
{'children': [('Combobox.downarrow',
{'side': 'right', 'sticky': 'ns'}),
('Combobox.padding',
{'children': [('Combobox.textarea',
{'sticky': 'nswe'})],
'expand': '1',
'sticky': 'nswe'})],
'sticky': 'nswe'})]通过将样式名(在本例中为TCombobox)传递给style.layout()方法,我们得到了一个布局规范,该规范显示了用于构造此小部件的元素的层次结构。
在本例中,元素为"Combobox.field"、"Combobox.downarrow"、"Combobox.padding"和"Combobox.textarea"。如您所见,每个元素都有相关的定位属性,类似于您将传递到pack()中的内容。
layout方法还可以通过传递新的布局规范来替换样式的布局。不幸的是,这需要替换整个布局规范,而不能只调整或替换单个元素。
要查看样式如何与元素连接,我们可以使用style.element_options()方法。此方法获取元素名称并返回可用于更改它的选项列表。
例如:
>>> pprint(style.element_options('Combobox.downarrow'))
('background', 'relief', 'borderwidth', 'arrowcolor', 'arrowsize')这告诉我们,Combobox的downarrow元素使用这些样式属性来确定其外观。要更改这些属性,我们必须使用style.configure()方法。
让我们将箭头的颜色更改为red:
>>> style.configure('TCombobox', arrowcolor='red')您应该看到arrowcolor已更改为red。这就是为静态更改配置小部件所需的全部知识,但动态更改又如何呢?
要进行动态更改,我们需要了解小部件的状态。
我们可以使用state方法检查或更改我们Combobox的状态:
>>> print(cb.state())
()
>>> cb.state(['active', 'invalid'])
('!active', '!invalid')
>>> print(cb.state())
('active', 'invalid')不带参数的Combobox.state()将返回当前设置状态标志的元组;当与参数(必须是字符串序列)一起使用时,它将设置相应的状态标志。
要关闭国旗,请在国旗名称前加上!:
>>> cb.state(['!invalid'])
('invalid',)
>>> print(cb.state())
('active',)When you call state() with an argument to change the value, the return value is a tuple containing a set of states that would, if applied, undo the state change you just set. This might be useful in a situation where you want to temporarily set a widget's state, then return it to its previous (unknown) state.
您不能只为state()使用任何字符串;它们必须是以下之一:
activedisabledfocuspressedselectedbackgroundreadonlyalternateinvalid
不同的小部件如何使用这些状态取决于小部件;默认情况下,并非每个state()都配置为具有效果。
小部件状态通过使用映射与小部件样式交互。我们使用style.map()方法检查或设置每个样式的地图。
查看TCombobox的默认映射:
>>> pprint(style.map(cb_stylename))
{'arrowcolor': [('disabled', '#a3a3a3')],
'fieldbackground': [('readonly', '#d9d9d9'),
('disabled', '#d9d9d9')]}如您所见,TCombobox默认情况下有arrowcolor和fieldbackground属性的样式映射。每个样式映射是一个元组列表,每个元组是一个或多个状态标志,后跟一个设置值。当所有状态标志与小部件的当前状态匹配时,该值生效。
设置disabled标志时,默认贴图将箭头颜色变为浅灰色;设置disabled或readonly标志时,默认贴图将字段背景变为不同的浅灰色。
我们可以使用相同的方法设置自己的样式映射:
>>> style.map('TCombobox', arrowcolor=[('!invalid', 'blue'), ('invalid', 'focus', 'red')])
{}
>>> pprint(style.map('TCombobox'))
{'arrowcolor': [('!invalid', 'blue'), ('invalid', 'focus', 'red')],
'fieldbackground': [('readonly', '#d9d9d9'), ('disabled', '#d9d9d9')]}在这里,我们已经将arrowcolor属性配置为blue当invalid标志未设置时,以及invalid和focus标志同时设置时的red。注意,虽然我们对map的调用完全重写了arrowcolor样式的映射,但fieldbackground映射没有受到影响。这意味着您可以单独替换样式映射,但不能简单地附加到给定属性的现有映射。
到目前为止,我们一直在使用TCombobox样式,这是所有Combobox小部件的默认样式。我们所做的任何更改都会影响应用中的每个Combobox。我们还可以通过在现有样式名称前加上名称和点来创建从现有样式派生的自定义样式。
例如:
>>> style.configure('Blue.TCombobox', fieldbackground='blue')
>>> cb.configure(style='Blue.TCombobox')Blue.TCombobox继承TCombobox的所有属性(包括我们之前配置的blue``downarrow,但可以使用自己的设置添加或覆盖它们,这些设置不会影响TCombobox。这允许您为某些小部件创建自定义样式,而不会影响相同类型的其他小部件。
我们可以通过改变主题一次改变所有 Ttk 小部件的外观。请记住,主题是样式的集合,因此通过更改主题,我们将替换所有内置样式和布局。
不同的主题在不同的平台上发布;要查看平台上可用的主题,请使用theme_names()方法:
>>> style.theme_names()
('clam', 'alt', 'default', 'classic')(这些是 Debian Linux 上可用的主题;您的主题可能有所不同。)
要查询当前主题或设置新主题,请使用theme_use()方法:
>>> style.theme_use()
'default'
>>> style.theme_use('alt')请注意,更改主题时,上一个样式是如何消失的。但是,如果切换回默认设置,您将看到更改被保留。
我们可以利用样式知识处理的第一件事是表单小部件。由于LabelInput小部件保留其默认的单调颜色,因此我们对表单的着色相当难看且不完整。我们需要对这些小部件中的每一个进行样式设置,以匹配其LabelInput的颜色。
在views.py文件中,在DataRecordForm.__init__()方法的开头附近添加:
style = ttk.Style()我们正在创建Style对象,以便开始使用 Ttk 样式。我们需要什么款式?
- 每个部分都需要 Ttk
Label小部件的样式,因为RecordInfo、EnvironmentInfo和Plant Info中的小部件需要不同的颜色。 - 我们需要设计 Ttk
Checkbutton的样式,因为它使用自己的内置标签,而不是单独的标签小部件。因为现在只有一种,所以我们只需要一种款式。
让我们创建这些样式:
style.configure('RecordInfo.TLabel', background='khaki')
style.configure(
'EnvironmentInfo.TLabel',
background='lightblue')
style.configure(
'EnvironmentInfo.TCheckbutton',
background='lightblue')
style.configure('PlantInfo.TLabel', background='lightgreen')如您所见,我们正在基于TLabel创建一个自定义样式,但每个部分都有前缀。对于每种样式,我们只是适当地设置background颜色。
现在,将此样式添加到每个小部件的繁琐任务来了:
self.inputs['Date'] = w.LabelInput(
recordinfo, "Date",
field_spec=fields['Date'],
label_args={'style': 'RecordInfo.TLabel'})在每个LabelInput调用中,您需要添加一个label_args参数,将style设置为该节的适当TLabel样式。对所有小部件进行检查并执行此操作。
对于Checkbutton,您需要以不同的方式进行操作:
self.inputs['Equipment Fault'] = w.LabelInput(
environmentinfo, "Equipment Fault",
field_spec=fields['Equipment Fault'],
label_args={'style': 'EnvironmentInfo.TLabel'},
input_args={'style': 'EnvironmentInfo.TCheckbutton'})在这里,我们设置了input_args,因为样式应用于Checkbutton而不是标签(留下label_args;我们将在一分钟内需要它)。
如果您在此时运行该程序,您将看到显著的改进,但还没有完全实现;错误标签仍然是旧的默认颜色。
为了解决这个问题,我们只需要编辑LabelInput小部件,将label_args也用于错误标签。
打开widgets.py并修复LabelInput.__init__()中的self.error_label赋值:
self.error_label = ttk.Label(self, textvariable=self.error,
**label_args)现在,您的应用应该具有一致的颜色,并且看起来更具吸引力:
我们的数据输入人员抱怨说,我们领域中的错误样式并不是非常明显。目前,我们只是将foreground颜色设置为red。
这有两个问题:
- 对于空字段,没有实际颜色
red - 我们的色盲用户无法区分
red和正常文本颜色
我们将使用我们的样式知识来改进错误样式,并使无效字段更加明显。
不过,在我们这样做之前,您可能需要修复我们的一个小部件的一个小问题。
如果您使用的是 Python 3.6 或更早版本,Spinbox小部件仅在tkinter中可用,而在ttk中不可用。我们需要修复此问题,以便我们的错误样式能够保持一致。
At the time of writing this book, the author has submitted a patch to Python 3.7 to include the Ttk Spinbox. If you're using Python 3.7 or later, you can just use ttk::spinbox and skip this section.
因为Spinbox已经在 Tcl/Tk Ttk 库中,所以为它创建 Python 类非常简单。
在widgets.py顶部附近添加此代码:
class TtkSpinbox(ttk.Entry):
def __init__(self, parent=None, **kwargs):
super().__init__(parent, 'ttk::spinbox', **kwargs)这就是为此应用创建 TtkSpinbox所需的全部内容。我们只是将ttk.Entry子类化,但更改了__init__语句中使用的 Ttk 小部件。如果我们需要Entry所缺乏的Spinbox方法,我们需要提供这些方法;对于这个应用,我们不需要其他任何东西。
现在,我们只需要更新我们的ValidatedSpinbox类来继承TtkSpinbox而不是tk.Spinbox:
class ValidatedSpinbox(ValidatedMixin, TtkSpinbox):现在我们正在使用所有 Ttk 小部件,我们可以用一些动态样式更新我们的ValidatedMixin类。
我们将在__init__()方法中首先创建一个Style对象:
style = ttk.Style()因为这是一个 mixin 类,所以我们不知道要混合的小部件的原始样式名称,所以我们必须获取它。
请记住,我们可以通过winfo_class()实现这一点:
widget_class = self.winfo_class()
validated_style = 'ValidatedInput.' + widget_class在获得 widget 类之后,我们通过在它前面加上ValidatedInput来创建一个派生样式。为了在错误外观和非错误外观之间切换输入外观,我们将创建一个样式映射,该样式映射使用invalid状态标志的状态进行切换。
您可以通过呼叫style.map()来完成此操作:
style.map(
validated_style,
foreground=[('invalid', 'white'), ('!invalid', 'black')],
fieldbackground=[('invalid', 'darkred'), ('!invalid', 'white')]
)我们仍然使用红色,因为它是一种已确定的“错误颜色”,但这次我们将场从亮时的暗反转为亮时的暗。这将帮助我们的色盲用户区分错误,即使它们是红色的。
最后,我们需要更新对self.config的调用,包括将小部件的样式设置为新的验证样式:
self.config(
style=validated_style,
validate='all',
...Ttk 小部件自动设置其invalid标志,作为内置验证系统的一部分。目前,我们有一个名为_toggle_error()的方法,在验证开始或失败时调用该方法,并将错误状态设置为 on 和 off。我们可以完全删除该方法以及对它的所有引用。
如果您现在尝试该应用,您将看到出现错误的字段现在变成深红色,并显示白色文本:
一般来说,任何给定平台上的默认 Ttk 主题可能是该平台上使用的最佳主题,但外观是主观的,有时我们可能会觉得 Tkinter 搞错了。有一种调整主题的方法可能有助于平滑一些粗糙的边缘,并使一些用户对应用的外观感到更舒适。
正如我们已经看到的,查询可用主题和设置新主题相当简单。让我们创建一个配置选项来更改应用的主题。
主题不是用户需要经常更改的东西,正如我们所看到的,更改主题可以撤消我们对小部件所做的样式更改。有鉴于此,我们将安全地设计主题转换器,使其需要重新启动程序才能进行实际更改。
我们将首先在SettingsModel中添加一个主题选项:
variables = {
...
'theme': {'type': 'str', 'value': 'default'}
}每个平台都有一个别名为default的theme,因此这是一个安全合理的默认值。
接下来,我们的Application.__init__()方法将需要检查该值并相应地设置theme。
在调用load_settings()之后添加此代码:
style = ttk.Style()
theme = self.settings.get('theme').get()
if theme in style.theme_names():
style.theme_use(theme)我们创建一个Style对象,查询主题名称的设置,然后(假设保存的theme在可用主题中)相应地设置theme。
现在剩下的就是创建 UI。
在views.py文件中,我们将为options_menu创建一个新的子菜单:
style = ttk.Style()
themes_menu = tk.Menu(self, tearoff=False)
for theme in style.theme_names():
themes_menu.add_radiobutton(
label=theme, value=theme,
variable=settings['theme']
)
options_menu.add_cascade(label='Theme', menu=themes_menu)在这里,我们只需循环使用可用的主题,并为每个主题添加一个Radiobutton,将其绑定到settings['theme']变量。
用户可能不太清楚更改主题需要重新启动,所以让我们确保让他们知道。
我们将在变量中添加一个trace:
settings['theme'].trace('w', self.on_theme_change)on_theme_change方法只会显示一个警告对话框,通知用户需要重新启动才能实现更改。
将其添加到MainMenu类的末尾:
def on_theme_change(self, *args):
"""Popup a message about theme changes"""
message = "Change requires restart"
detail = (
"Theme changes do not take effect"
" until application restart")
messagebox.showwarning(
title='Warning',
message=message,
detail=detail)现在,您可以运行应用并尝试更改theme。哪个主题在您的平台上看起来最好?
You might find that some themes on your platform break the widget styling in the form. Remember that themes don't just change default colors and fonts, they change the layout and contents of the widget elements themselves. Sometimes, a style setting doesn't carry across different themes due to this change of property names.
在本章中,我们对应用的外观和感觉进行了全面检查,以改进其美观性和可用性。您学习了如何使用 Tkinter 小部件的颜色和字体设置,以及 Ttk 样式的复杂世界。






