RealPython 中文系列教程(七十四)

原文:RealPython

协议:CC BY-NC-SA 4.0

用 Tkinter 进行 Python GUI 编程

原文:https://realpython.com/python-gui-tkinter/

Python 有很多 GUI 框架,但是 Tkinter 是唯一内置到 Python 标准库中的框架。Tkinter 有几个优点。它是跨平台,所以同样的代码可以在 Windows、macOS 和 Linux 上运行。视觉元素是使用本地操作系统元素呈现的,因此用 Tkinter 构建的应用程序看起来就像它们属于运行它们的平台。

尽管 Tkinter 被认为是事实上的 Python GUI 框架,但它也不是没有批评。一个值得注意的批评是用 Tkinter 构建的 GUI 看起来过时了。如果你想要一个闪亮、现代的界面,那么 Tkinter 可能不是你要找的。

然而,与其他框架相比,Tkinter 是轻量级的,使用起来相对容易。这使得它成为用 Python 构建 GUI 应用程序的一个令人信服的选择,特别是对于不需要现代光泽的应用程序,当务之急是快速构建功能性的和跨平台的应用程序。

在本教程中,您将学习如何:

  • 使用一个 Hello,World 应用程序开始使用 Tkinter
  • 使用小部件,例如按钮和文本框
  • 使用几何图形管理器控制您的应用布局
  • 通过将按钮点击与 Python 函数相关联,使您的应用程序具有交互性

**注:**本教程改编自 Python 基础知识:Python 实用入门 3 的“图形用户界面”一章。

该书使用 Python 内置的 IDLE 编辑器来创建和编辑 Python 文件,并与 Python shell 进行交互。在本教程中,对 IDLE 的引用已经被删除,取而代之的是更通用的语言。

本教程中的大部分内容保持不变,从您选择的编辑器和环境中运行示例代码应该没有问题。

一旦您通过完成每一节末尾的练习掌握了这些技能,您就可以通过构建两个应用程序将所有内容联系起来。第一个是温度转换器,第二个是文本编辑器。是时候开始学习如何用 Tkinter 构建一个应用程序了!

免费奖励: 掌握 Python 的 5 个想法,这是一个面向 Python 开发者的免费课程,向您展示将 Python 技能提升到下一个水平所需的路线图和心态。

***参加测验:***通过我们的交互式“使用 Tkinter 进行 Python GUI 编程”测验来测试您的知识。完成后,您将收到一个分数,以便您可以跟踪一段时间内的学习进度:

*参加测验

用 Tkinter 构建您的第一个 Python GUI 应用程序

Tkinter GUI 的基本元素是窗口。窗口是所有其他 GUI 元素所在的容器。这些其他 GUI 元素,比如文本框、标签和按钮,被称为小部件。窗口中包含小部件。

首先,创建一个包含单个小部件的窗口。启动一个新的 Python shell 会话,然后继续!

**注意:**本教程中的代码示例都已经在 Windows、macOS 和 Ubuntu Linux 20.04 上用 Python 3.10 版进行了测试。

如果你已经从 python.org 的用官方安装程序为的 Windows的 macOS 安装了 Python ,那么运行示例代码应该没有问题。您可以放心地跳过本笔记的其余部分,继续学习教程!

如果你还没有用官方安装程序安装 Python,或者你的系统还没有官方发行版,那么这里有一些开始使用的技巧。

带有自制软件的 macOS 上的 Python:

Homebrew 上可用的用于 macOS 的 Python 发行版没有捆绑 Tkinter 所需的 Tcl/Tk 依赖项。而是使用默认的系统版本。此版本可能已过时,并阻止您导入 Tkinter 模块。为了避免这个问题,使用官方 macOS 安装程序

Ubuntu Linux 20.04:

为了节省内存空间,Ubuntu Linux 20.04 上预装的 Python 解释器的默认版本不支持 Tkinter。但是,如果您想继续使用与您的操作系统捆绑在一起的 Python 解释器,请安装以下软件包:

$ sudo apt-get install python3-tk

这将安装 Python GUI Tkinter 模块。

其他 Linux 版本:

如果您无法在自己的 Linux 上安装 Python,那么您可以从源代码中用正确版本的 Tcl/Tk 构建 Python。为了一步一步地完成这个过程,请查看 Python 3 安装&安装指南。你也可以尝试使用 pyenv 来管理多个 Python 版本。

打开 Python shell 后,您需要做的第一件事是导入 Python GUI Tkinter 模块:

>>> import tkinter as tk

一个窗口是 Tkinter 的Tk类的一个实例。继续创建一个新窗口,并将其分配给变量 window:

>>> window = tk.Tk()

当您执行上述代码时,屏幕上会弹出一个新窗口。它的外观取决于您的操作系统:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在本教程的其余部分,你会看到 Windows 屏幕截图。

Remove ads

添加微件

现在您有了一个窗口,您可以添加一个小部件。使用tk.Label类给窗口添加一些文本。用文本"Hello, Tkinter"创建一个Label小部件,并将其分配给一个名为greeting的变量:

>>> greeting = tk.Label(text="Hello, Tkinter")

您之前创建的窗口不会改变。您刚刚创建了一个Label小部件,但是还没有将它添加到窗口中。有几种方法可以将小部件添加到窗口中。现在,您可以使用Label小部件的.pack()方法:

>>> greeting.pack()

窗口现在看起来像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当您将一个小部件打包到一个窗口中时,Tkinter 会将窗口调整到尽可能小的大小,同时仍然完全包含该小部件。现在执行以下命令:

>>> window.mainloop()

似乎什么也没发生,但是请注意,shell 中没有出现新的提示。

window.mainloop()告诉 Python 运行 Tkinter 事件循环。这个方法监听事件,比如按钮点击或按键,并且阻止任何跟随它的代码运行,直到你关闭调用这个方法的窗口。继续并关闭您创建的窗口,您将看到 shell 中显示一个新的提示。

**警告:**当您在 Python REPL 中使用 Tkinter 时,会在执行每一行时应用对 windows 的更新。这是而不是从 Python 文件执行 Tkinter 程序的情况!

如果在 Python 文件中的程序末尾没有包含window.mainloop(),那么 Tkinter 应用程序将永远不会运行,也不会显示任何内容。或者,您可以在 Python REPL 中通过在每个步骤后调用window.update()来逐步构建您的用户界面,以反映变更。

用 Tkinter 创建一个窗口只需要几行代码。但是空白窗口不是很有用!在下一节中,您将了解 Tkinter 中可用的一些小部件,以及如何定制它们来满足您的应用程序的需求。

检查你的理解能力

展开下面的代码块,检查您的理解情况:

编写一个完整的 Python 脚本,用文本"Python rocks!"创建一个 Tkinter 窗口。

窗口应该是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在试试这个练习。

您可以展开下面的代码块来查看解决方案:

这里有一个可能的解决方案:

import tkinter as tk

window = tk.Tk()
label = tk.Label(text="Python rocks!")
label.pack()

window.mainloop()

请记住,您的代码可能看起来不同。

当你准备好了,你可以进入下一部分。

使用微件

小部件是 Python GUI 框架 Tkinter 的基础。它们是用户与你的程序交互的元素。Tkinter 中的每个小部件都是由一个类定义的。以下是一些可用的小部件:

小部件类描述
Label用于在屏幕上显示文本的小部件
Button可以包含文本并可以在单击时执行操作的按钮
Entry只允许单行文本的文本输入小部件
Text允许多行文本输入的文本输入小部件
Frame用于分组相关部件或在部件之间提供填充的矩形区域

在接下来的小节中,您将看到如何使用其中的每一个,但是请记住,Tkinter 有比这里列出的更多的小部件。当你考虑到一整套全新的主题窗口小部件时,窗口小部件的选择变得更加复杂。在本教程的剩余部分,你将只使用 Tkinter 的经典部件

如果您想了解关于这两种小部件类型的更多信息,您可以展开下面的可折叠部分:

值得注意的是,Tkinter 中目前有两大类小部件:

  1. 经典 widgets:tkinter包中的,例如tkinter.Label
  2. 主题控件:ttk子模块中可用的,例如tkinter.ttk.Label

Tkinter 的经典窗口小部件是高度可定制和简单明了的,但在今天的大多数平台上,它们往往显得过时或有些陌生。如果您想利用给定操作系统的用户熟悉的具有本机外观和感觉的小部件,那么您可能想要查看主题小部件。

大多数主题部件都是传统部件的替代物,但是看起来更现代。你也可以使用一些全新的小部件,比如进度条,这是 Tkinter 之前没有的。同时,您需要继续使用一些没有主题替代的经典小部件。

注意:tkinter.ttk模块中的主题小部件默认使用操作系统的本地外观。然而,你可以改变他们的主题来定制视觉外观,比如亮暗模式。主题是可重用的样式定义的集合,您可以将其视为 Tkinter 的级联样式表(CSS)

使新的小部件主题化意味着将它们的大部分风格信息提取到单独的对象中。一方面,这种关注点的分离是库设计中期望的属性,但另一方面,它引入了一个额外的抽象层,这使得主题化的小部件比传统的小部件更难设计。

在 Tkinter 中使用常规和主题小部件时,通常会为 Tkinter 包和模块声明以下别名:

>>> import tkinter as tk
>>> import tkinter.ttk as ttk

像这样的别名允许你显式地引用tk.Labelttk.Label,例如,根据你的需要在一个程序中:

>>> tk.Label()
<tkinter.Label object .!label>

>>> ttk.Label()
<tkinter.ttk.Label object .!label2>

然而,有时您可能会发现使用通配符导入(*)来自动覆盖所有带有主题的遗留小部件会更方便,比如:

>>> from tkinter import *
>>> from tkinter.ttk import *

>>> Label()
<tkinter.ttk.Label object .!label>

>>> Text()
<tkinter.Text object .!text>

现在,您不必在小部件的类名前面加上相应的 Python 模块。只要有主题窗口小部件,你就会一直创建它,否则你就会退回到经典窗口小部件。上述两个 import 语句必须按照指定的顺序放置才能生效。因此,通配符导入被认为是一种不好的做法,除非有意识地使用,否则通常应该避免。

要查看 Tkinter 小部件的完整列表,请查看 TkDocs 教程中的基本小部件更多小部件。尽管它描述了 Tcl/Tk 8.5 中引入的主题小部件,但其中的大部分信息也应该适用于经典小部件。

有趣的事实: Tkinter 字面上代表“Tk 接口”,因为它是一个 Python 绑定或者是一个编程接口到 Tcl 脚本语言中的 Tk 库。

现在,仔细看看Label小部件。

Remove ads

Label小工具显示文本和图像

Label 控件用于显示文本图像。用户不能编辑由Label小部件显示的文本。这只是为了展示的目的。正如您在本教程开头的例子中看到的,您可以通过实例化Label类并向text参数传递一个字符串来创建一个Label小部件:

label = tk.Label(text="Hello, Tkinter")

Label小工具使用默认系统文本颜色和默认系统文本背景颜色显示文本。它们通常分别是黑色和白色,但是如果您在操作系统中更改了这些设置,您可能会看到不同的颜色。

您可以使用foregroundbackground参数控制Label文本和背景颜色:

label = tk.Label(
    text="Hello, Tkinter",
    foreground="white",  # Set the text color to white
    background="black"  # Set the background color to black
)

有许多有效的颜色名称,包括:

  • "red"
  • "orange"
  • "yellow"
  • "green"
  • "blue"
  • "purple"

许多 HTML 颜色名称都使用 Tkinter。要获得完整的参考,包括当前系统主题控制的特定于 macOS 和 Windows 的系统颜色,请查看颜色手册页

您也可以使用十六进制 RGB 值指定颜色:

label = tk.Label(text="Hello, Tkinter", background="#34A2FE")

这将标签背景设置为漂亮的浅蓝色。十六进制的 RGB 值比命名的颜色更神秘,但也更灵活。幸运的是,有工具可以让获取十六进制颜色代码变得相对容易。

如果你不想一直键入foregroundbackground,那么你可以使用简写的fgbg参数来设置前景和背景颜色:

label = tk.Label(text="Hello, Tkinter", fg="white", bg="black")

您也可以使用widthheight参数控制标签的宽度和高度:

label = tk.Label(
    text="Hello, Tkinter",
    fg="white",
    bg="black",
    width=10,
    height=10
)

以下是该标签在窗口中的外观:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

虽然窗口的宽度和高度都被设置为10,但是窗口中的标签并不是方形的,这似乎有点奇怪。这是因为宽度和高度是以文本单位测量的。在默认系统字体中,一个水平文本单位由字符0或数字零的宽度决定。类似地,一个垂直文本单位由字符0的高度决定。

**注意:**对于宽度和高度的测量,Tkinter 使用文本单位,而不是英寸、厘米或像素,以确保应用程序跨平台的一致行为。

用字符的宽度来度量单位意味着小部件的大小是相对于用户机器上的默认字体而言的。这确保了无论应用程序在哪里运行,文本都能恰当地适合标签和按钮。

标签对于显示一些文本很有用,但是它们不能帮助你从用户那里得到输入。接下来您将了解的三个小部件都用于获取用户输入。

Remove ads

显示带有Button小部件的可点击按钮

Button 小部件用来显示可点击按钮。您可以将它们配置为在被点击时调用一个函数。您将在下一节讲述如何通过点击按钮来调用函数。现在,让我们看看如何创建和设计一个按钮。

ButtonLabel小部件有很多相似之处。在很多方面,按钮只是一个你可以点击的标签!用于创建和样式化Label的相同关键字参数将适用于Button小部件。例如,下面的代码创建一个蓝色背景黄色文本的按钮。它还将宽度和高度分别设置为255文本单位:

button = tk.Button(
    text="Click me!",
    width=25,
    height=5,
    bg="blue",
    fg="yellow",
)

下面是该按钮在窗口中的外观:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

相当漂亮!您可以使用接下来的两个小部件来收集用户输入的文本。

使用Entry小部件获取用户输入

当您需要从用户那里获取一些文本时,比如姓名或电子邮件地址,使用一个 Entry 小部件。它将显示一个小文本框,用户可以在其中键入一些文本。创建和设计Entry小部件的工作方式与LabelButton小部件非常相似。例如,下面的代码创建了一个小部件,它具有蓝色背景、一些黄色文本和宽度为50的文本单位:

entry = tk.Entry(fg="yellow", bg="blue", width=50)

然而,关于Entry小部件有趣的一点不是如何设计它们的样式。而是如何使用它们从用户那里获得输入。您可以使用Entry小部件执行三个主要操作:

  1. .get()检索文本
  2. .delete()删除文本
  3. .insert()插入文本

理解Entry小部件的最好方法是创建一个并与之交互。打开一个 Python shell,按照本节中的示例进行操作。首先,导入tkinter并创建一个新窗口:

>>> import tkinter as tk
>>> window = tk.Tk()

现在创建一个Label和一个Entry小部件:

>>> label = tk.Label(text="Name")
>>> entry = tk.Entry()

Label描述了什么样的文本应该放在Entry小部件中。它没有在Entry上强加任何类型的要求,但是它告诉用户你的程序期望他们在那里放什么。您需要.pack()将小部件放到窗口中,以便它们可见:

>>> label.pack()
>>> entry.pack()

看起来是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请注意,Tkinter 会自动将标签置于窗口中Entry小部件的中央。这是.pack()的一个特性,您将在后面的章节中了解更多。

用鼠标在Entry小部件内点击并输入Real Python:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在您已经在Entry小部件中输入了一些文本,但是这些文本还没有发送到您的程序中。您可以使用.get()来检索文本,并将其分配给一个名为name的变量:

>>> name = entry.get()
>>> name
'Real Python'

您也可以删除文本。这个.delete()方法接受一个整数参数,告诉 Python 要删除哪个字符。例如,下面的代码块显示了.delete(0)如何从Entry中删除第一个字符:

>>> entry.delete(0)

小部件中剩余的文本现在是eal Python:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意,就像 Python 字符串对象一样,Entry小部件中的文本从0开始索引。

如果您需要从一个Entry中删除几个字符,那么将第二个整数参数传递给.delete(),指示删除应该停止的字符的索引。例如,以下代码删除了Entry中的前四个字母:

>>> entry.delete(0, 4)

剩下的文本现在读作Python:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Entry.delete()的工作原理和切弦一样。第一个参数决定了起始索引,删除会一直继续,但不会包括作为第二个参数传递的索引。使用特殊常量tk.END作为.delete()的第二个参数,删除Entry中的所有文本:

>>> entry.delete(0, tk.END)

您现在会看到一个空白文本框:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

另一方面,您也可以将文本插入到Entry小部件中:

>>> entry.insert(0, "Python")

窗口现在看起来像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第一个参数告诉.insert()在哪里插入文本。如果Entry中没有文本,那么新的文本将总是被插入到小部件的开头,不管您作为第一个参数传递什么值。例如,像上面所做的那样,用100而不是0作为第一个参数调用.insert(),将会生成相同的输出。

如果Entry已经包含一些文本,那么.insert()将在指定位置插入新文本,并将所有现有文本向右移动:

>>> entry.insert(0, "Real ")

小部件文本现在显示为Real Python:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

小工具很适合从用户那里获取少量文本,但是因为它们只显示在一行中,所以它们不适合收集大量文本。这就是Text小部件的用武之地!

Remove ads

使用Text小部件获取多行用户输入

Text 小工具用于输入文本,就像Entry小工具一样。不同之处在于Text小部件可能包含多行文本。使用Text小部件,用户可以输入一整段甚至几页的文本!就像使用Entry小部件一样,您可以使用Text小部件执行三个主要操作:

  1. .get()检索文本
  2. .delete()删除文本
  3. 插入文本.insert()

尽管方法名与Entry方法相同,但它们的工作方式略有不同。是时候动手创建一个Text小部件,看看它能做什么了。

**注意:**上一节的窗口是否仍然打开着?

如果是这样,您可以通过执行以下命令来关闭它:

>>> window.destroy()

也可以通过点击关闭按钮手动关闭。

在 Python shell 中,创建一个新的空白窗口,并在其中装入一个Text()小部件:

>>> window = tk.Tk()
>>> text_box = tk.Text()
>>> text_box.pack()

默认情况下,文本框比Entry小部件大得多。下面是上面创建的窗口的样子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

单击窗口内的任意位置激活文本框。键入单词Hello。然后按下 Enter ,在第二行输入World。窗口现在应该看起来像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

就像使用Entry小部件一样,您可以使用.get()Text小部件中检索文本。然而,不带参数调用.get()不会像调用Entry小部件那样返回文本框中的完整文本。它引发一个异常:

>>> text_box.get()
Traceback (most recent call last):
  ...
TypeError: get() missing 1 required positional argument: 'index1'

Text.get()至少需要一个参数。用单个索引调用.get()返回单个字符。要检索几个字符,需要传递一个开始索引和一个结束索引Text小部件中的索引与Entry小部件中的不同。由于Text窗口小部件可以有几行文本,所以一个索引必须包含两条信息:

  1. 一个字符的行号
  2. 字符在那一行上的位置

行号以1开头,字符位置以0开头。要创建一个索引,需要创建一个形式为"<line>.<char>"的字符串,用行号替换<line>,用字符号替换<char>。例如,"1.0"代表第一行的第一个字符,"2.3"代表第二行的第四个字符。

使用索引"1.0"从您之前创建的文本框中获取第一个字母:

>>> text_box.get("1.0")
'H'

单词Hello有五个字母,o的字符数是4,因为字符数从0开始,单词Hello从文本框的第一个位置开始。就像 Python 字符串切片一样,为了从文本框中获取整个单词Hello,结束索引必须比要读取的最后一个字符的索引大 1。

因此,要从文本框中获取单词Hello,使用"1.0"作为第一个索引,使用"1.5"作为第二个索引:

>>> text_box.get("1.0", "1.5")
'Hello'

要在文本框的第二行找到单词World,请将每个索引中的行号更改为2:

>>> text_box.get("2.0", "2.5")
'World'

要获取文本框中的所有文本,请在"1.0"中设置起始索引,并对第二个索引使用特殊的tk.END常量:

>>> text_box.get("1.0", tk.END)
'Hello\nWorld\n'

请注意,.get()返回的文本包含任何换行符。从这个例子中还可以看到,Text小部件中的每一行末尾都有一个换行符,包括文本框中的最后一行文本。

.delete()用于从文本框中删除字符。它的工作原理就像.delete()对于Entry小部件一样。使用.delete()有两种方法:

  1. 用一个单参数
  2. 两个自变量

使用单参数版本,您将待删除的单个字符的索引传递给.delete()。例如,以下代码从文本框中删除第一个字符H:

>>> text_box.delete("1.0")

窗口中的第一行文本现在显示为ello:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于双参数版本,您传递两个索引来删除从第一个索引开始到第二个索引的字符范围,但不包括第二个索引。

例如,要删除文本框第一行剩余的ello,请使用索引"1.0""1.4":

>>> text_box.delete("1.0", "1.4")

注意,文本从第一行开始就消失了。这就在第二行的单词World后面留下了一个空行:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

即使看不到,第一行还是有个人物。是换行符!您可以使用.get()验证这一点:

>>> text_box.get("1.0")
'\n'

如果删除该字符,文本框的其余内容将上移一行:

>>> text_box.delete("1.0")

现在,World位于文本框的第一行:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

尝试清除文本框中的其余文本。将"1.0"设置为开始索引,并将tk.END用于第二个索引:

>>> text_box.delete("1.0", tk.END)

文本框现在是空的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

您可以使用.insert()将文本插入文本框:

>>> text_box.insert("1.0", "Hello")

这将在文本框的开头插入单词Hello,使用与.get()相同的"<line>.<column>"格式来指定插入位置:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

看看如果你试图在第二行插入单词World会发生什么:

>>> text_box.insert("2.0", "World")

不是在第二行插入文本,而是在第一行的末尾插入文本:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果您想在新行上插入文本,那么您需要在要插入的字符串中手动插入一个换行符:

>>> text_box.insert("2.0", "\nWorld")

现在World在文本框的第二行:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

.insert()会做两件事之一:

  1. 在指定位置插入文本,如果该位置或其后已经有文本。
  2. 如果字符数大于文本框中最后一个字符的索引,则将文本追加到指定行。

试图跟踪最后一个字符的索引通常是不切实际的。在Text小部件末尾插入文本的最佳方式是将tk.END传递给.insert()的第一个参数:

>>> text_box.insert(tk.END, "Put me at the end!")

如果您想将文本放在新的一行,请不要忘记在文本的开头包含换行符(\n):

>>> text_box.insert(tk.END, "\nPut me on a new line!")

LabelButtonEntryText小部件只是 Tkinter 中可用的几个小部件。还有其他几个,包括复选框、单选按钮、滚动条和进度条的小部件。有关所有可用小部件的更多信息,请参见附加资源部分的附加小部件列表。

Remove ads

使用Frame小部件将小部件分配给框架

在本教程中,您将只使用五个小部件:

  1. Label
  2. Button
  3. Entry
  4. Text
  5. Frame

这是到目前为止你已经看到的四个插件和Frame插件。 Frame 小部件对于组织应用程序中小部件的布局很重要。

在您详细了解小部件的视觉呈现之前,请仔细看看Frame小部件是如何工作的,以及如何为它们分配其他小部件。以下脚本创建一个空白的Frame小部件,并将其分配给主应用程序窗口:

import tkinter as tk

window = tk.Tk()
frame = tk.Frame()
frame.pack()

window.mainloop()

frame.pack()将框架装入窗口,使窗口尽可能小以包含框架。当您运行上面的脚本时,您会得到一些非常无趣的输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一个空的Frame窗口小部件实际上是不可见的。框架最好被认为是其他部件的容器。您可以通过设置小部件的master属性将小部件分配给框架:

frame = tk.Frame()
label = tk.Label(master=frame)

为了感受一下这是如何工作的,编写一个脚本来创建两个名为frame_aframe_bFrame小部件。在这个脚本中,frame_a包含一个带有文本"I'm in Frame A"的标签,frame_b包含标签"I'm in Frame B"。有一种方法可以做到这一点:

import tkinter as tk

window = tk.Tk()

frame_a = tk.Frame()
frame_b = tk.Frame()

label_a = tk.Label(master=frame_a, text="I'm in Frame A")
label_a.pack()

label_b = tk.Label(master=frame_b, text="I'm in Frame B")
label_b.pack()

frame_a.pack()
frame_b.pack()

window.mainloop()

注意frame_a是在frame_b之前装入窗口的。打开的窗口显示frame_b标签上方的frame_a标签:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在看看当您交换frame_a.pack()frame_b.pack()的顺序时会发生什么:

import tkinter as tk

window = tk.Tk()

frame_a = tk.Frame()
label_a = tk.Label(master=frame_a, text="I'm in Frame A")
label_a.pack()

frame_b = tk.Frame()
label_b = tk.Label(master=frame_b, text="I'm in Frame B")
label_b.pack()

# Swap the order of `frame_a` and `frame_b`
frame_b.pack() frame_a.pack() 
window.mainloop()

输出如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在label_b在上面。由于label_b被分配给frame_b,所以frame_b被定位到哪里,它就移动到哪里。

您已经了解的所有四种小部件类型— LabelButtonEntryText—都有一个在实例化它们时设置的master属性。这样,您可以控制将小部件分配给哪个Frame。小部件非常适合以逻辑方式组织其他小部件。相关的窗口小部件可以被分配到同一个框架中,这样,如果框架在窗口中移动,那么相关的窗口小部件就保持在一起。

**注意:**如果在创建新的小部件实例时省略了master参数,那么默认情况下,它将被放置在顶层窗口中。

除了对你的小部件进行逻辑分组,Frame小部件还可以给你的应用程序的视觉呈现添加一点亮点。继续阅读,了解如何为Frame小部件创建各种边框。

Remove ads

用浮雕调整框架外观

Frame小部件可以配置一个relief属性,在框架周围创建一个边框。您可以将relief设置为以下任意值:

  • tk.FLAT : 无边框效果(默认值)
  • tk.SUNKEN : 产生凹陷效果
  • tk.RAISED : 产生凸起效果
  • tk.GROOVE : 创建凹槽边框效果
  • tk.RIDGE : 产生脊状效果

要应用边框效果,必须将borderwidth属性设置为大于1的值。该属性以像素为单位调整边框的宽度。感受每种效果的最佳方式是自己去看。下面的脚本将五个Frame小部件打包到一个窗口中,每个小部件的relief参数都有不同的值:

 1import tkinter as tk
 2
 3border_effects = {
 4    "flat": tk.FLAT,
 5    "sunken": tk.SUNKEN,
 6    "raised": tk.RAISED,
 7    "groove": tk.GROOVE,
 8    "ridge": tk.RIDGE,
 9}
10
11window = tk.Tk()
12
13for relief_name, relief in border_effects.items():
14    frame = tk.Frame(master=window, relief=relief, borderwidth=5)
15    frame.pack(side=tk.LEFT)
16    label = tk.Label(master=frame, text=relief_name)
17    label.pack()
18
19window.mainloop()

以下是该脚本的详细内容:

  • 第 3 行到第 9 行创建一个字典,其关键字是 Tkinter 中可用的不同浮雕效果的名称。这些值是相应的 Tkinter 对象。这个字典被分配给border_effects变量。

  • 第 13 行开始一个 for循环来循环遍历border_effects字典中的每个条目。

  • 第 14 行创建一个新的Frame小部件,并将其分配给window对象。将relief属性设置为border_effects字典中相应的浮雕,将border属性设置为5,效果可见。

  • 15 号线使用.pack()Frame打包到窗口中。side关键字参数告诉 Tkinter 在哪个方向打包frame对象。在下一节中,您将看到更多关于这是如何工作的内容。

  • 第 16 行和第 17 行创建一个Label小部件来显示浮雕的名称,并将其打包到您刚刚创建的frame对象中。

上述脚本生成的窗口如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在此图像中,您可以看到以下效果:

  • tk.FLAT 创建看似平面的帧。
  • tk.SUNKEN 添加边框,使框架看起来像凹进了窗口。
  • tk.RAISED 给框架一个边框,让它看起来突出于屏幕。
  • tk.GROOVE 在原本平坦的框架周围添加一个看起来像凹槽的边框。
  • tk.RIDGE 给人一种边框边缘凸起的感觉。

这些效果给你的 Python GUI Tkinter 应用程序增加了一点视觉吸引力。

了解小部件命名约定

当你创建一个小部件时,你可以给它起任何你喜欢的名字,只要它是一个有效的 Python 标识符。在分配给小部件实例的变量名中包含小部件类的名称通常是一个好主意。例如,如果使用一个Label小部件来显示用户名,那么您可以将这个小部件命名为label_user_name。一个用于收集用户年龄的Entry小部件可能被称为entry_age

**注意:**有时候,你可以定义一个新的小部件,而不用把它赋给一个变量。您将在同一行代码中直接调用它的.pack()方法:

>>> tk.Label(text="Hello, Tkinter").pack()

当您以后不打算引用小部件的实例时,这可能会有所帮助。由于自动内存管理,Python 通常会垃圾收集这种未分配的对象,但是 Tkinter 通过在内部注册每个新的小部件来防止这种情况。

当您在变量名中包含小部件类名时,您可以帮助自己和任何需要阅读您的代码的人理解变量名所指的小部件类型。然而,使用 widget 类的全名会导致很长的变量名,所以您可能希望采用一种简称来引用每种 widget 类型。在本教程的其余部分,您将使用以下简写前缀来命名小部件:

小部件类变量名前缀例子
Labellbllbl_name
Buttonbtnbtn_submit
Entryentent_age
Texttxttxt_notes
Framefrmfrm_address

在本节中,您学习了如何创建窗口、使用小部件以及使用框架。此时,您可以创建一些显示消息的普通窗口,但是您还没有创建一个完整的应用程序。在下一节中,您将学习如何使用 Tkinter 强大的几何图形管理器来控制应用程序的布局。

检查你的理解能力

展开下面的代码块,做一个练习来检查您的理解:

编写一个完整的脚本,显示一个 40 个文本单位宽、白底黑字的Entry小部件。使用.insert()在小部件中显示文本What is your name?

输出窗口应该如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在试试这个练习。

您可以展开下面的代码块来查看解决方案:

有几种方法可以解决这个问题。这里有一个解决方案,使用bgfg参数来设置Entry小部件的背景和前景色:

import tkinter as tk

window = tk.Tk()

entry = tk.Entry(width=40, bg="white", fg="black")
entry.pack()

entry.insert(0, "What is your name?")

window.mainloop()

这个解决方案很棒,因为它明确地为Entry小部件设置了背景和前景色。

在大多数系统中,Entry小部件的默认背景色是白色,默认前景色是黑色。因此,您可能能够生成省略了bgfg参数的同一个窗口:

import tkinter as tk

window = tk.Tk()

entry = tk.Entry(width=40)
entry.pack()

entry.insert(0, "What is your name?")

window.mainloop()

请记住,您的代码可能看起来不同。

当你准备好了,你可以进入下一部分。

Remove ads

用几何图形管理器控制布局

到目前为止,您已经使用.pack()向窗口和Frame窗口添加了小部件,但是您还没有了解这个方法到底是做什么的。让我们把事情弄清楚!Tkinter 中的应用布局由几何图形管理器控制。虽然.pack()是几何管理者的一个例子,但它不是唯一的一个。Tkinter 还有另外两个:

  • .place()
  • .grid()

应用程序中的每个窗口或Frame只能使用一个几何管理器。但是,不同的框架可以使用不同的几何管理器,即使它们使用另一个几何管理器被指定给框架或窗口。先来仔细看看.pack()

.pack()几何图形管理器

.pack()几何图形管理器使用打包算法将小部件以指定的顺序放置在Frame或窗口中。对于给定的小部件,打包算法有两个主要步骤:

  1. 计算一个名为 parcel 的矩形区域,其高度(或宽度)刚好足以容纳小部件,并用空白空间填充窗口中剩余的宽度(或高度)。
  2. 除非指定了不同的位置,否则将微件置于宗地中心。

很强大,但是很难想象。感受.pack()的最好方法是看一些例子。看看当你把三个.pack()小部件变成一个Frame时会发生什么:

import tkinter as tk

window = tk.Tk()

frame1 = tk.Frame(master=window, width=100, height=100, bg="red")
frame1.pack()

frame2 = tk.Frame(master=window, width=50, height=50, bg="yellow")
frame2.pack()

frame3 = tk.Frame(master=window, width=25, height=25, bg="blue")
frame3.pack()

window.mainloop()

默认情况下,.pack()将每个Frame放置在前一个Frame的下方,按照它们被分配到窗口的顺序:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个Frame被放置在最高的可用位置。所以,红色的Frame放在窗口的顶部。然后黄色的Frame放在红色的下面,蓝色的Frame放在黄色的下面。

有三个不可见的包裹,每个包裹包含三个Frame部件中的一个。每个包裹和窗户一样宽,和它所装的Frame一样高。因为在为每个Frame,调用.pack()时没有指定锚点,所以它们都在它们的包裹内居中。这就是为什么每个Frame都在窗口中央的原因。

.pack()接受一些关键字参数,以便更精确地配置小部件的位置。例如,您可以设置 fill 关键字参数来指定帧应该填充的方向。选项有tk.X填充水平方向、tk.Y填充垂直方向、tk.BOTH填充两个方向。下面是如何堆叠三个框架,使每个框架水平填充整个窗口:

import tkinter as tk

window = tk.Tk()

frame1 = tk.Frame(master=window, height=100, bg="red")
frame1.pack(fill=tk.X)

frame2 = tk.Frame(master=window, height=50, bg="yellow")
frame2.pack(fill=tk.X)

frame3 = tk.Frame(master=window, height=25, bg="blue")
frame3.pack(fill=tk.X)

window.mainloop()

注意,width没有在任何Frame小部件上设置。width不再必要,因为每一帧都设置.pack()为水平填充,覆盖你可能设置的任何宽度。

该脚本生成的窗口如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

.pack()填充窗口的一个好处是,填充对窗口大小的响应**。尝试扩大前一个脚本生成的窗口,看看这是如何工作的。当您加宽窗口时,三个Frame小部件的宽度会增加以填满窗口:**

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

不过,请注意,Frame小部件不会在垂直方向上扩展。

.pack()side 关键字参数指定小工具应放置在窗口的哪一侧。以下是可用的选项:

  • tk.TOP
  • tk.BOTTOM
  • tk.LEFT
  • tk.RIGHT

如果你不设置side,那么.pack()将自动使用tk.TOP并在窗口顶部放置新的窗口小部件,或者在窗口的最顶端没有被小部件占据的部分。例如,以下脚本从左到右并排放置三个框架,并扩展每个框架以垂直填充窗口:

import tkinter as tk

window = tk.Tk()

frame1 = tk.Frame(master=window, width=200, height=100, bg="red")
frame1.pack(fill=tk.Y, side=tk.LEFT)

frame2 = tk.Frame(master=window, width=100, bg="yellow")
frame2.pack(fill=tk.Y, side=tk.LEFT)

frame3 = tk.Frame(master=window, width=50, bg="blue")
frame3.pack(fill=tk.Y, side=tk.LEFT)

window.mainloop()

这一次,您必须在至少一个框架上指定height关键字参数,以强制窗口具有一定的高度。

生成的窗口如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

就像您设置fill=tk.X在水平调整窗口大小时使框架响应一样,您可以设置fill=tk.Y在垂直调整窗口大小时使框架响应:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了使布局真正具有响应性,您可以使用widthheight属性来设置框架的初始大小。然后,将.pack()fill关键字参数设置为tk.BOTH,将expand关键字参数设置为True:

import tkinter as tk

window = tk.Tk()

frame1 = tk.Frame(master=window, width=200, height=100, bg="red")
frame1.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)

frame2 = tk.Frame(master=window, width=100, bg="yellow")
frame2.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)

frame3 = tk.Frame(master=window, width=50, bg="blue")
frame3.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)

window.mainloop()

当您运行上面的脚本时,您将看到一个窗口,该窗口最初看起来与您在前面的示例中生成的窗口相同。不同之处在于,现在您可以随意调整窗口大小,框架会相应地扩展并填充窗口:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

相当酷!

Remove ads

.place()几何图形管理器

你可以使用.place()来控制窗口中的精确位置或者Frame。您必须提供两个关键字参数,xy,它们指定小部件左上角的 x 和 y 坐标。xy都是以像素为单位,而不是文本单位。

记住原点,其中xy都是0,是Frame或窗口的左上角。因此,您可以将.place()y参数视为距离窗口顶部的像素数,将x参数视为距离窗口左边缘的像素数。

下面是一个关于.place()几何图形管理器如何工作的例子:

 1import tkinter as tk
 2
 3window = tk.Tk()
 4
 5frame = tk.Frame(master=window, width=150, height=150)
 6frame.pack()
 7
 8label1 = tk.Label(master=frame, text="I'm at (0, 0)", bg="red")
 9label1.place(x=0, y=0)
10
11label2 = tk.Label(master=frame, text="I'm at (75, 75)", bg="yellow")
12label2.place(x=75, y=75)
13
14window.mainloop()

下面是这段代码的工作原理:

  • 第 5 行和第 6 行创建一个名为frame的新的Frame小部件,测量150像素宽和150像素高,并用.pack()将其打包到窗口中。
  • 第 8 行和第 9 行创建一个名为label1的红色背景的新Label,并将其放置在frame1的位置(0,0)。
  • 第 11 行和第 12 行创建第二个Label,名为label2,背景为黄色,并将其放置在frame1的位置(75,75)。

下面是代码生成的窗口:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请注意,如果您在使用不同字体大小和样式的不同操作系统上运行此代码,那么第二个标签可能会被窗口边缘部分遮挡。这就是为什么.place()不常使用的原因。除此之外,它还有两个主要缺点:

  1. 使用.place()很难管理布局。如果您的应用程序有很多小部件,这一点尤其正确。
  2. .place()创建的布局没有响应。它们不会随着窗口大小的改变而改变。

跨平台 GUI 开发的一个主要挑战是让布局无论在哪个平台上看起来都好看,而对于做出响应性和跨平台的布局来说,.place()不是一个好的选择。

这并不是说你不应该使用.place()!在某些情况下,这可能正是你所需要的。例如,如果您正在为地图创建 GUI 界面,那么.place()可能是确保小部件在地图上以正确的距离放置的最佳选择。

.pack()通常是比.place()更好的选择,但即使是.pack()也有一些缺点。窗口小部件的位置取决于调用.pack()的顺序,因此在没有完全理解控制布局的代码的情况下,很难修改现有的应用程序。.grid()几何图形管理器解决了很多这样的问题,你将在下一节看到。

.grid()几何图形管理器

您可能最常使用的几何管理器是.grid(),它以一种更容易理解和维护的格式提供了.pack()的所有功能。

.grid()的工作原理是将一个窗口或Frame分割成行和列。通过调用.grid()并将行和列索引分别传递给rowcolumn关键字参数,可以指定小部件的位置。行索引和列索引都从0开始,因此1的行索引和2的列索引告诉.grid()将小部件放在第二行的第三列。

以下脚本创建了一个 3 × 3 的框架网格,其中包含了Label小部件:

import tkinter as tk

window = tk.Tk()

for i in range(3):
    for j in range(3):
        frame = tk.Frame(
            master=window,
            relief=tk.RAISED,
            borderwidth=1
        )
        frame.grid(row=i, column=j)
        label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
        label.pack()

window.mainloop()

下面是生成的窗口的样子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在本例中,您使用了两个几何体管理器。每个框架通过.grid()几何图形管理器连接到window:

import tkinter as tk

window = tk.Tk()

for i in range(3):
    for j in range(3):
        frame = tk.Frame(
            master=window,
            relief=tk.RAISED,
            borderwidth=1
        )
 frame.grid(row=i, column=j)        label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
        label.pack()

window.mainloop()

每个label通过.pack()连接到其主Frame:

import tkinter as tk

window = tk.Tk()

for i in range(3):
    for j in range(3):
        frame = tk.Frame(
            master=window,
            relief=tk.RAISED,
            borderwidth=1
        )
        frame.grid(row=i, column=j)
        label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
 label.pack() 
window.mainloop()

这里要意识到的重要一点是,即使在每个Frame对象上调用了.grid(),几何管理器也适用于window对象。类似地,每个frame的布局由.pack()几何图形管理器控制。

上例中的框架紧密相邻放置。要在每个框架周围添加一些空间,可以设置网格中每个单元格的填充。填充只是一些空白空间,围绕着一个小部件,在视觉上把它的内容分开。

两种类型的衬垫是外部内部衬垫。外部填充在网格单元的外部增加了一些空间。它由.grid()的两个关键字参数控制:

  1. padx 在水平方向添加填充。
  2. pady 在垂直方向添加填充。

padxpady都是以像素度量的,而不是文本单位,因此将它们设置为相同的值将在两个方向上创建相同的填充量。尝试在前面示例中的框架外部添加一些填充:

import tkinter as tk

window = tk.Tk()

for i in range(3):
    for j in range(3):
        frame = tk.Frame(
            master=window,
            relief=tk.RAISED,
            borderwidth=1
        )
 frame.grid(row=i, column=j, padx=5, pady=5)        label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
        label.pack()

window.mainloop()

这是生成的窗口:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

.pack()也有padxpady参数。以下代码与前面的代码几乎相同,除了您在每个标签周围的xy方向添加了五个像素的额外填充:

import tkinter as tk

window = tk.Tk()

for i in range(3):
    for j in range(3):
        frame = tk.Frame(
            master=window,
            relief=tk.RAISED,
            borderwidth=1
        )
        frame.grid(row=i, column=j, padx=5, pady=5)
        label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
 label.pack(padx=5, pady=5) 
window.mainloop()

Label小部件周围的额外填充给网格中的每个单元格在Frame边框和标签中的文本之间留有一点空间:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

看起来很不错!但是,如果你尝试向任何方向扩展窗口,你会发现布局没有很好的响应:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当窗口扩展时,整个网格停留在左上角。

通过在window对象上使用.columnconfigure().rowconfigure(),你可以调整网格的行和列在窗口调整大小时的增长方式。记住,网格是附属于window的,即使你在每个Frame小部件上调用.grid().columnconfigure().rowconfigure()都有三个基本参数:

  1. Index: 要配置的网格列或行的索引,或者同时配置多行或多列的索引列表
  2. Weight: 一个名为weight的关键字参数,它确定该列或行相对于其他列和行应该如何响应窗口大小调整
  3. **最小尺寸:**一个名为minsize的关键字参数,以像素为单位设置行高或列宽的最小尺寸

默认情况下,weight被设置为0,这意味着当窗口调整大小时,列或行不会扩展。如果每一列或每一行都被赋予一个1的权重,那么它们都以相同的速度增长。如果一列的权重为1,另一列的权重为2,那么第二列的膨胀速度是第一列的两倍。调整前面的脚本以更好地处理窗口大小调整:

import tkinter as tk

window = tk.Tk()

for i in range(3):
 window.columnconfigure(i, weight=1, minsize=75) window.rowconfigure(i, weight=1, minsize=50) 
    for j in range(0, 3):
        frame = tk.Frame(
            master=window,
            relief=tk.RAISED,
            borderwidth=1
        )
        frame.grid(row=i, column=j, padx=5, pady=5)
        label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
        label.pack(padx=5, pady=5)

window.mainloop()

.columnconfigure().rowconfigure()被放置在外部for回路的主体中。您可以在for循环之外显式配置每一列和每一行,但是这需要编写额外的六行代码。

在循环的每次迭代中,第i列和行被配置为具有权重1。这确保了无论何时调整窗口大小时,行和列都以相同的速率扩展。每列的minsize参数被设置为75,每行的50。这确保了Label小部件总是显示它的文本而不截断任何字符,即使窗口非常小。

结果是网格布局随着窗口大小的调整而平滑地扩展和收缩:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

亲自尝试一下,感受一下它是如何工作的!摆弄一下weightminsize参数,看看它们如何影响网格。

默认情况下,小部件在其网格单元中居中。例如,下面的代码创建了两个Label小部件,并将它们放在一个一列两行的网格中:

import tkinter as tk

window = tk.Tk()
window.columnconfigure(0, minsize=250)
window.rowconfigure([0, 1], minsize=100)

label1 = tk.Label(text="A")
label1.grid(row=0, column=0)

label2 = tk.Label(text="B")
label2.grid(row=1, column=0)

window.mainloop()

每个网格单元的宽度为250像素,高度为100像素。标签放置在每个单元格的中央,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

您可以使用sticky参数更改网格单元内每个标签的位置,该参数接受包含一个或多个以下字母的字符串:

  • "n""N" 向单元格中上部对齐
  • "e""E" 向单元格的右中央对齐
  • "s""S" 向单元格的下中部对齐
  • "w""W" 向单元格的左侧居中对齐

字母"n""s""e""w"来自北、南、东、西四个主要方向。在前面的代码中,将两个标签上的sticky设置为"n"会将每个标签定位在其网格单元的顶部中心:

import tkinter as tk

window = tk.Tk()
window.columnconfigure(0, minsize=250)
window.rowconfigure([0, 1], minsize=100)

label1 = tk.Label(text="A")
label1.grid(row=0, column=0, sticky="n") 
label2 = tk.Label(text="B")
label2.grid(row=1, column=0, sticky="n") 
window.mainloop()

以下是输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

您可以在单个字符串中组合多个字母,以将每个标签放置在其网格单元格的角上:

import tkinter as tk

window = tk.Tk()
window.columnconfigure(0, minsize=250)
window.rowconfigure([0, 1], minsize=100)

label1 = tk.Label(text="A")
label1.grid(row=0, column=0, sticky="ne") 
label2 = tk.Label(text="B")
label2.grid(row=1, column=0, sticky="sw") 
window.mainloop()

在这个例子中,label1sticky参数被设置为"ne",这将标签放置在其网格单元的右上角。通过"sw"sticky,使label2位于左下角。这是它在窗口中的样子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当用sticky定位一个小部件时,小部件本身的大小刚好可以容纳任何文本和其他内容。它不会填满整个网格单元。为了填充网格,您可以指定"ns"强制小部件在垂直方向填充单元格,或者指定"ew"在水平方向填充单元格。要填充整个单元格,将sticky设置为"nsew"。以下示例说明了这些选项中的每一个:

import tkinter as tk

window = tk.Tk()

window.rowconfigure(0, minsize=50)
window.columnconfigure([0, 1, 2, 3], minsize=50)

label1 = tk.Label(text="1", bg="black", fg="white")
label2 = tk.Label(text="2", bg="black", fg="white")
label3 = tk.Label(text="3", bg="black", fg="white")
label4 = tk.Label(text="4", bg="black", fg="white")

label1.grid(row=0, column=0)
label2.grid(row=0, column=1, sticky="ew")
label3.grid(row=0, column=2, sticky="ns")
label4.grid(row=0, column=3, sticky="nsew")

window.mainloop()

下面是输出的样子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面的例子说明了.grid()几何图形管理器的sticky参数可以用来实现与.pack()几何图形管理器的fill参数相同的效果。下表总结了stickyfill参数之间的对应关系:

.grid().pack()
sticky="ns"fill=tk.Y
sticky="ew"fill=tk.X
sticky="nsew"fill=tk.BOTH

.grid()是一个强大的几何图形管理器。它通常比.pack()更容易理解,也比.place()更灵活。当你创建新的 Tkinter 应用时,你应该考虑使用.grid()作为你的主要几何管理器。

注意: .grid()提供了比你在这里看到的更多的灵活性。例如,您可以将单元格配置为跨越多行和多列。更多信息,请查看 TkDocs 教程网格几何管理器章节

现在您已经掌握了 Python GUI 框架 Tkinter 的几何管理器的基础,下一步是将动作分配给按钮,使您的应用程序变得生动。

Remove ads

检查你的理解能力

展开下面的代码块,做一个练习来检查您的理解:

下面是用 Tkinter 制作的地址条目表单的图像:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

编写一个完整的脚本来重新创建窗口。你可以使用任何你喜欢的几何图形管理器。

您可以展开下面的代码块来查看解决方案:

有许多不同的方法来解决这个问题。如果您的解决方案生成的窗口与练习语句中的窗口相同,那么恭喜您!您已经成功完成了练习!下面,你可以看看两个使用.grid()几何图形管理器的解决方案。

一种解决方案是用每个字段所需的设置创建一个LabelEntry小部件:

import tkinter as tk

# Create a new window with the title "Address Entry Form"
window = tk.Tk()
window.title("Address Entry Form")

# Create a new frame `frm_form` to contain the Label
# and Entry widgets for entering address information
frm_form = tk.Frame(relief=tk.SUNKEN, borderwidth=3)
# Pack the frame into the window
frm_form.pack()

# Create the Label and Entry widgets for "First Name"
lbl_first_name = tk.Label(master=frm_form, text="First Name:")
ent_first_name = tk.Entry(master=frm_form, width=50)
# Use the grid geometry manager to place the Label and
# Entry widgets in the first and second columns of the
# first row of the grid
lbl_first_name.grid(row=0, column=0, sticky="e")
ent_first_name.grid(row=0, column=1)

# Create the Label and Entry widgets for "Last Name"
lbl_last_name = tk.Label(master=frm_form, text="Last Name:")
ent_last_name = tk.Entry(master=frm_form, width=50)
# Place the widgets in the second row of the grid
lbl_last_name.grid(row=1, column=0, sticky="e")
ent_last_name.grid(row=1, column=1)

# Create the Label and Entry widgets for "Address Line 1"
lbl_address1 = tk.Label(master=frm_form, text="Address Line 1:")
ent_address1 = tk.Entry(master=frm_form, width=50)
# Place the widgets in the third row of the grid
lbl_address1.grid(row=2, column=0, sticky="e")
ent_address1.grid(row=2, column=1)

# Create the Label and Entry widgets for "Address Line 2"
lbl_address2 = tk.Label(master=frm_form, text="Address Line 2:")
ent_address2 = tk.Entry(master=frm_form, width=50)
# Place the widgets in the fourth row of the grid
lbl_address2.grid(row=3, column=0, sticky=tk.E)
ent_address2.grid(row=3, column=1)

# Create the Label and Entry widgets for "City"
lbl_city = tk.Label(master=frm_form, text="City:")
ent_city = tk.Entry(master=frm_form, width=50)
# Place the widgets in the fifth row of the grid
lbl_city.grid(row=4, column=0, sticky=tk.E)
ent_city.grid(row=4, column=1)

# Create the Label and Entry widgets for "State/Province"
lbl_state = tk.Label(master=frm_form, text="State/Province:")
ent_state = tk.Entry(master=frm_form, width=50)
# Place the widgets in the sixth row of the grid
lbl_state.grid(row=5, column=0, sticky=tk.E)
ent_state.grid(row=5, column=1)

# Create the Label and Entry widgets for "Postal Code"
lbl_postal_code = tk.Label(master=frm_form, text="Postal Code:")
ent_postal_code = tk.Entry(master=frm_form, width=50)
# Place the widgets in the seventh row of the grid
lbl_postal_code.grid(row=6, column=0, sticky=tk.E)
ent_postal_code.grid(row=6, column=1)

# Create the Label and Entry widgets for "Country"
lbl_country = tk.Label(master=frm_form, text="Country:")
ent_country = tk.Entry(master=frm_form, width=50)
# Place the widgets in the eight row of the grid
lbl_country.grid(row=7, column=0, sticky=tk.E)
ent_country.grid(row=7, column=1)

# Create a new frame `frm_buttons` to contain the
# Submit and Clear buttons. This frame fills the
# whole window in the horizontal direction and has
# 5 pixels of horizontal and vertical padding.
frm_buttons = tk.Frame()
frm_buttons.pack(fill=tk.X, ipadx=5, ipady=5)

# Create the "Submit" button and pack it to the
# right side of `frm_buttons`
btn_submit = tk.Button(master=frm_buttons, text="Submit")
btn_submit.pack(side=tk.RIGHT, padx=10, ipadx=10)

# Create the "Clear" button and pack it to the
# right side of `frm_buttons`
btn_clear = tk.Button(master=frm_buttons, text="Clear")
btn_clear.pack(side=tk.RIGHT, ipadx=10)

# Start the application
window.mainloop()

这个解决方案没什么问题。有点长,但是一切都很露骨。如果你想改变一些东西,那么很清楚地看到具体在哪里这样做。

也就是说,通过认识到每个Entry具有相同的宽度,并且对于每个Label您所需要的只是文本,可以大大缩短解决方案:

import tkinter as tk

# Create a new window with the title "Address Entry Form"
window = tk.Tk()
window.title("Address Entry Form")

# Create a new frame `frm_form` to contain the Label
# and Entry widgets for entering address information
frm_form = tk.Frame(relief=tk.SUNKEN, borderwidth=3)
# Pack the frame into the window
frm_form.pack()

# List of field labels
labels = [
    "First Name:",
    "Last Name:",
    "Address Line 1:",
    "Address Line 2:",
    "City:",
    "State/Province:",
    "Postal Code:",
    "Country:",
]

# Loop over the list of field labels
for idx, text in enumerate(labels):
    # Create a Label widget with the text from the labels list
    label = tk.Label(master=frm_form, text=text)
    # Create an Entry widget
    entry = tk.Entry(master=frm_form, width=50)
    # Use the grid geometry manager to place the Label and
    # Entry widgets in the row whose index is idx
    label.grid(row=idx, column=0, sticky="e")
    entry.grid(row=idx, column=1)

# Create a new frame `frm_buttons` to contain the
# Submit and Clear buttons. This frame fills the
# whole window in the horizontal direction and has
# 5 pixels of horizontal and vertical padding.
frm_buttons = tk.Frame()
frm_buttons.pack(fill=tk.X, ipadx=5, ipady=5)

# Create the "Submit" button and pack it to the
# right side of `frm_buttons`
btn_submit = tk.Button(master=frm_buttons, text="Submit")
btn_submit.pack(side=tk.RIGHT, padx=10, ipadx=10)

# Create the "Clear" button and pack it to the
# right side of `frm_buttons`
btn_clear = tk.Button(master=frm_buttons, text="Clear")
btn_clear.pack(side=tk.RIGHT, ipadx=10)

# Start the application
window.mainloop()

在这个解决方案中,一个列表用于存储表单中每个标签的字符串。它们按照每个表单域应该出现的顺序存储。然后, enumerate()labels列表中的每个值获取索引和字符串。

当你准备好了,你可以进入下一部分。

让您的应用程序具有交互性

到目前为止,您已经非常了解如何使用 Tkinter 创建一个窗口,添加一些小部件,以及控制应用程序布局。这很好,但是应用程序不应该只是看起来很好——它们实际上需要做一些事情!在本节中,您将学习如何通过在特定的事件发生时执行动作来激活您的应用程序。

使用事件和事件处理程序

创建 Tkinter 应用程序时,必须调用window.mainloop()来启动事件循环。在事件循环期间,您的应用程序检查事件是否已经发生。如果是,那么它将执行一些代码作为响应。

Tkinter 为您提供了事件循环,因此您不必自己编写任何检查事件的代码。但是,您必须编写将被执行以响应事件的代码。在 Tkinter 中,为应用程序中使用的事件编写名为事件处理程序的函数。

注意:一个事件是在事件循环期间发生的任何可能触发应用程序中某些行为的动作,比如当一个键或鼠标按钮被按下时。

当一个事件发生时,一个事件对象被发出,这意味着一个代表该事件的类的实例被创建。您不需要担心自己实例化这些类。Tkinter 将自动为您创建事件类的实例。

为了更好地理解 Tkinter 的事件循环是如何工作的,您将编写自己的事件循环。这样,您可以看到 Tkinter 的事件循环如何适合您的应用程序,以及哪些部分需要您自己编写。

假设有一个包含事件对象的名为events的列表。每当程序中发生一个事件时,一个新的事件对象会自动追加到events中。您不需要实现这种更新机制。在这个概念性的例子中,它会自动发生。使用无限循环,您可以不断地检查events中是否有任何事件对象:

# Assume that this list gets updated automatically
events = []

# Run the event loop
while True:
    # If the event list is empty, then no events have occurred
    # and you can skip to the next iteration of the loop
    if events == []:
        continue

    # If execution reaches this point, then there is at least one
    # event object in the event list
    event = events[0]

现在,您创建的事件循环不会对event做任何事情。让我们改变这一点。假设您的应用程序需要响应按键。您需要检查event是否是由用户按下键盘上的一个键生成的,如果是,则将event传递给按键的事件处理函数。

假设event有一个设置为字符串"keypress".type属性(如果该事件是一个按键事件对象)和一个包含被按下按键的字符的.char属性。创建一个新的handle_keypress()函数并更新你的事件循环代码:

events = []

# Create an event handler def handle_keypress(event):
 """Print the character associated to the key pressed""" print(event.char) 
while True:
    if events == []:
        continue

    event = events[0]

    # If event is a keypress event object
    if event.type == "keypress":
        # Call the keypress event handler
        handle_keypress(event)

当您调用window.mainloop()时,类似上面的循环会自动运行。这个方法负责循环的两个部分:

  1. 它维护一个已经发生的事件的列表。
  2. 每当一个新事件被添加到列表中时,它就运行一个事件处理程序

更新您的事件循环以使用window.mainloop()而不是您自己的事件循环:

import tkinter as tk 
# Create a window object window = tk.Tk() 
# Create an event handler
def handle_keypress(event):
    """Print the character associated to the key pressed"""
    print(event.char)

# Run the event loop window.mainloop()

为你做了很多,但是上面的代码缺少了一些东西。Tkinter 怎么知道什么时候用handle_keypress()?Tkinter 小部件有一个名为.bind()的方法就是为了这个目的。

Remove ads

使用.bind()

要在小部件上发生事件时调用事件处理程序,请使用.bind()。事件处理程序被称为绑定到事件,因为它在每次事件发生时被调用。您将继续上一节的按键示例,并使用.bind()handle_keypress()绑定到按键事件:

import tkinter as tk

window = tk.Tk()

def handle_keypress(event):
    """Print the character associated to the key pressed"""
    print(event.char)

# Bind keypress event to handle_keypress() window.bind("<Key>", handle_keypress) 
window.mainloop()

这里,使用window.bind()handle_keypress()事件处理程序绑定到一个"<Key>"事件。当应用程序运行时,只要按下一个键,你的程序就会打印出所按下的键的字符。

**注意:**上述程序的输出是而不是打印在 Tkinter 应用程序窗口中。它被打印到标准输出流(stdout)

如果你在空闲状态下运行程序,那么你会在交互窗口中看到输出。如果您从终端运行程序,那么您应该在终端上看到输出。

.bind()总是需要至少两个参数:

  1. 一个由形式为"<event_name>"的字符串表示的事件,其中event_name可以是 Tkinter 的任何事件
  2. 一个事件处理程序,它是事件发生时要调用的函数的名称

事件处理程序被绑定到调用.bind()的小部件上。当调用事件处理程序时,事件对象被传递给事件处理程序函数。

在上面的例子中,事件处理程序被绑定到窗口本身,但是您可以将事件处理程序绑定到应用程序中的任何小部件。例如,您可以将一个事件处理程序绑定到一个Button小部件,每当按钮被按下时,它就会执行一些操作:

def handle_click(event):
    print("The button was clicked!")

button = tk.Button(text="Click me!")

button.bind("<Button-1>", handle_click)

在这个例子中,button小部件上的"<Button-1>"事件被绑定到handle_click事件处理程序。当鼠标在小部件上时,只要按下鼠标左键,就会发生"<Button-1>"事件。鼠标点击还有其他事件,包括鼠标中键的"<Button-2>"和鼠标右键的"<Button-3>"

**注:**常用事件列表请参见 Tkinter 8.5 参考事件类型部分。

您可以使用.bind()将任何事件处理程序绑定到任何类型的小部件,但是有一种更直接的方法可以使用Button小部件的command属性将事件处理程序绑定到按钮点击。

使用command

每个Button小部件都有一个command属性,您可以将其分配给一个函数。每当按下按钮时,该功能就被执行。

看一个例子。首先,您将创建一个窗口,其中有一个保存数值的Label小部件。你将在标签的左右两边放置按钮。左键用于减少Label中的数值,右键用于增加数值。以下是窗口的代码:

 1import tkinter as tk
 2
 3window = tk.Tk()
 4
 5window.rowconfigure(0, minsize=50, weight=1)
 6window.columnconfigure([0, 1, 2], minsize=50, weight=1)
 7
 8btn_decrease = tk.Button(master=window, text="-")
 9btn_decrease.grid(row=0, column=0, sticky="nsew")
10
11lbl_value = tk.Label(master=window, text="0")
12lbl_value.grid(row=0, column=1)
13
14btn_increase = tk.Button(master=window, text="+")
15btn_increase.grid(row=0, column=2, sticky="nsew")
16
17window.mainloop()

窗口看起来像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

定义好应用程序布局后,您可以通过给按钮一些命令来赋予它生命。从左边的按钮开始。当按下此按钮时,标签中的值应减少 1。为了做到这一点,你首先需要得到两个问题的答案:

  1. 如何获取Label中的文本?
  2. 如何更新Label中的文字?

Label小工具不像EntryText小工具那样有.get()。但是,您可以通过使用字典样式的下标符号访问text属性来从标签中检索文本:

label = tk.Label(text="Hello")

# Retrieve a label's text
text = label["text"]

# Set new text for the label
label["text"] = "Good bye"

现在您已经知道如何获取和设置标签的文本,编写一个将lbl_value中的值增加 1 的increase()函数:

 1import tkinter as tk
 2
 3def increase():
 4    value = int(lbl_value["text"])
 5    lbl_value["text"] = f"{value + 1}"
 6
 7# ...

increase()lbl_value获取文本并用int()将其转换成整数。然后,它将这个值增加 1,并将标签的text属性设置为这个新值。

您还需要decrease()value_label中的值减一:

 5# ...
 6
 7def decrease():
 8    value = int(lbl_value["text"])
 9    lbl_value["text"] = f"{value - 1}"
10
11# ...

increase()decrease()放在代码中的import语句之后。

要将按钮连接到功能,请将功能分配给按钮的command属性。您可以在实例化按钮时做到这一点。例如,将实例化按钮的两行代码更新为:

14# ...
15
16btn_decrease = tk.Button(master=window, text="-", command=decrease) 17btn_decrease.grid(row=0, column=0, sticky="nsew")
18
19lbl_value = tk.Label(master=window, text="0")
20lbl_value.grid(row=0, column=1)
21
22btn_increase = tk.Button(master=window, text="+", command=increase) 23btn_increase.grid(row=0, column=2, sticky="nsew")
24
25window.mainloop()

这就是将按钮绑定到increase()decrease()并使程序正常运行所需做的全部工作。尝试保存您的更改并运行应用程序!点按窗口中央的按钮来增大和减小值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以下是完整的应用程序代码供您参考:

import tkinter as tk

def increase():
    value = int(lbl_value["text"])
    lbl_value["text"] = f"{value + 1}"

def decrease():
    value = int(lbl_value["text"])
    lbl_value["text"] = f"{value - 1}"

window = tk.Tk()

window.rowconfigure(0, minsize=50, weight=1)
window.columnconfigure([0, 1, 2], minsize=50, weight=1)

btn_decrease = tk.Button(master=window, text="-", command=decrease)
btn_decrease.grid(row=0, column=0, sticky="nsew")

lbl_value = tk.Label(master=window, text="0")
lbl_value.grid(row=0, column=1)

btn_increase = tk.Button(master=window, text="+", command=increase)
btn_increase.grid(row=0, column=2, sticky="nsew")

window.mainloop()

这个应用程序不是特别有用,但是你在这里学到的技能适用于你将要制作的每个应用程序:

  • 使用小部件创建用户界面的组件。
  • 使用几何图形管理器控制应用的布局。
  • 编写与各种组件交互的事件处理程序来捕获和转换用户输入。

在接下来的两节中,您将构建更有用的应用程序。首先,您将构建一个温度转换器,将温度值从华氏温度转换为摄氏温度。之后,您将构建一个可以打开、编辑和保存文本文件的文本编辑器!

检查你的理解能力

展开下面的代码块,做一个练习来检查您的理解:

写一个模拟滚动六面骰子的程序。应该有一个带有文本Roll的按钮。当用户点击按钮时,应该显示一个从16的随机整数。

**提示:**您可以使用 random 模块中的randint()生成一个随机数。如果你不熟悉random模块,那么查看在 Python 中生成随机数据(指南)了解更多信息。

应用程序窗口应该如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在试试这个练习。

您可以展开下面的代码块来查看解决方案:

这里有一个可能的解决方案:

import random
import tkinter as tk

def roll():
    lbl_result["text"] = str(random.randint(1, 6))

window = tk.Tk()
window.columnconfigure(0, minsize=150)
window.rowconfigure([0, 1], minsize=50)

btn_roll = tk.Button(text="Roll", command=roll)
lbl_result = tk.Label()

btn_roll.grid(row=0, column=0, sticky="nsew")
lbl_result.grid(row=1, column=0)

window.mainloop()

请记住,您的代码可能看起来不同。

当你准备好了,你可以进入下一部分。

构建温度转换器(示例应用程序)

在本节中,您将构建一个温度转换器应用程序,它允许用户以华氏度为单位输入温度,并按下按钮将该温度转换为摄氏度。您将一步一步地浏览代码。您还可以在本节末尾找到完整的源代码,以供参考。

**注意:**为了充分利用这一部分,请跟随一个 Python shell

在你开始编码之前,你首先要设计应用程序。你需要三个要素:

  1. Entry : 一个名为ent_temperature的控件,用于输入华氏温度值
  2. Label : 显示摄氏温度结果的名为lbl_result的小部件
  3. Button : 一个名为btn_convert的小部件,它从Entry小部件中读取值,将其从华氏温度转换为摄氏温度,并将Label小部件的文本设置为单击时的结果

您可以将它们排列在一个网格中,每个小部件占一行一列。这让你得到一个最低限度工作的应用程序,但它不是非常用户友好的。所有东西都需要有标签

您将直接在包含华氏符号(℧)的ent_temperature小部件的右侧放置一个标签,以便用户知道值ent_temperature应该是华氏温度。为此,将标签文本设置为"\N{DEGREE FAHRENHEIT}",它使用 Python 命名的 Unicode 字符支持来显示华氏符号。

您可以通过将文本设置为值"\N{RIGHTWARDS BLACK ARROW}"来给btn_convert增加一点魅力,它会显示一个指向右边的黑色箭头。您还将确保lbl_result的标签文本"\N{DEGREE CELSIUS}"后面总是有摄氏符号(℃),以表明结果是以摄氏度为单位的。这是最终窗口的样子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在你已经知道了你需要什么样的小部件,以及这个窗口看起来会是什么样子,你可以开始编写代码了!首先,导入tkinter并创建一个新窗口:

 1import tkinter as tk
 2
 3window = tk.Tk()
 4window.title("Temperature Converter")
 5window.resizable(width=False, height=False)

window.title()设置一个现有窗口的标题,而window.resizable()的两个参数都设置为False使窗口具有固定的大小。当您最终运行该应用程序时,窗口的标题栏中将会显示文本温度转换器。接下来,创建标签为lbl_tempent_temperature小部件,并将二者分配给名为frm_entryFrame小部件:

 5# ...
 6
 7frm_entry = tk.Frame(master=window)
 8ent_temperature = tk.Entry(master=frm_entry, width=10)
 9lbl_temp = tk.Label(master=frm_entry, text="\N{DEGREE FAHRENHEIT}")

用户将在ent_temperature中输入华氏温度值,lbl_temp用于给ent_temperature标注华氏温度符号。frm_entry集装箱将ent_temperaturelbl_temp组合在一起。

您希望将lbl_temp直接放在ent_temperature的右边。您可以使用具有一行两列的.grid()几何图形管理器在frm_entry中对它们进行布局:

 9# ...
10
11ent_temperature.grid(row=0, column=0, sticky="e")
12lbl_temp.grid(row=0, column=1, sticky="w")

您已经为ent_temperaturesticky参数设置为"e",这样它总是贴在它的网格单元的最右边。您还可以将lbl_tempsticky设置为"w",使其保持在网格单元的最左边。这确保了lbl_temp总是直接位于ent_temperature的右侧。

现在,使btn_convertlbl_result转换输入到ent_temperature中的温度并显示结果:

12# ...
13
14btn_convert = tk.Button(
15    master=window,
16    text="\N{RIGHTWARDS BLACK ARROW}"
17)
18lbl_result = tk.Label(master=window, text="\N{DEGREE CELSIUS}")

frm_entry一样,btn_convertlbl_result都被分配给window。这三个小部件共同构成了主应用程序网格中的三个单元。现在使用.grid()继续并布置它们:

18# ...
19
20frm_entry.grid(row=0, column=0, padx=10)
21btn_convert.grid(row=0, column=1, pady=10)
22lbl_result.grid(row=0, column=2, padx=10)

最后,运行应用程序:

22# ...
23
24window.mainloop()

看起来棒极了!但是这个按钮还不能做任何事情。在脚本文件的顶部,就在import行的下面,添加一个名为fahrenheit_to_celsius()的函数:

 1import tkinter as tk
 2
 3def fahrenheit_to_celsius():
 4    """Convert the value for Fahrenheit to Celsius and insert the
 5 result into lbl_result.
 6 """
 7    fahrenheit = ent_temperature.get()
 8    celsius = (5 / 9) * (float(fahrenheit) - 32)
 9    lbl_result["text"] = f"{round(celsius, 2)}  \N{DEGREE CELSIUS}"
10
11# ...

该函数从ent_temperature中读取数值,将其从华氏温度转换为摄氏温度,然后在lbl_result中显示结果。

现在转到定义btn_convert的那一行,将其command参数设置为fahrenheit_to_celsius:

20# ...
21
22btn_convert = tk.Button(
23    master=window,
24    text="\N{RIGHTWARDS BLACK ARROW}",
25    command=fahrenheit_to_celsius  # <--- Add this line 26)
27
28# ...

就是这样!您只用了 26 行代码就创建了一个功能齐全的温度转换器应用程序!很酷,对吧?

您可以展开下面的代码块来查看完整的脚本:

以下是完整的脚本供您参考:

import tkinter as tk

def fahrenheit_to_celsius():
    """Convert the value for Fahrenheit to Celsius and insert the
 result into lbl_result.
 """
    fahrenheit = ent_temperature.get()
    celsius = (5 / 9) * (float(fahrenheit) - 32)
    lbl_result["text"] = f"{round(celsius, 2)}  \N{DEGREE CELSIUS}"

# Set up the window
window = tk.Tk()
window.title("Temperature Converter")
window.resizable(width=False, height=False)

# Create the Fahrenheit entry frame with an Entry
# widget and label in it
frm_entry = tk.Frame(master=window)
ent_temperature = tk.Entry(master=frm_entry, width=10)
lbl_temp = tk.Label(master=frm_entry, text="\N{DEGREE FAHRENHEIT}")

# Layout the temperature Entry and Label in frm_entry
# using the .grid() geometry manager
ent_temperature.grid(row=0, column=0, sticky="e")
lbl_temp.grid(row=0, column=1, sticky="w")

# Create the conversion Button and result display Label
btn_convert = tk.Button(
    master=window,
    text="\N{RIGHTWARDS BLACK ARROW}",
    command=fahrenheit_to_celsius
)
lbl_result = tk.Label(master=window, text="\N{DEGREE CELSIUS}")

# Set up the layout using the .grid() geometry manager
frm_entry.grid(row=0, column=0, padx=10)
btn_convert.grid(row=0, column=1, pady=10)
lbl_result.grid(row=0, column=2, padx=10)

# Run the application
window.mainloop()

是时候让事情更上一层楼了!请继续阅读,了解如何构建文本编辑器。

构建文本编辑器(示例应用程序)

在本节中,您将构建一个能够创建、打开、编辑和保存文本文件的文本编辑器应用程序。该应用程序有三个基本要素:

  1. 名为btn_openButton小部件,用于打开文件进行编辑
  2. 用于保存文件的名为btn_saveButton小部件
  3. 名为txt_editTextBox小部件,用于创建和编辑文本文件

这三个小部件的排列方式是,两个按钮在窗口的左侧,文本框在右侧。整个窗口的最小高度应该是 800 像素,txt_edit的最小宽度应该是 800 像素。整个布局应该是响应性的,这样如果窗口被调整大小,那么txt_edit也被调整大小。然而,容纳按钮的框架的宽度不应该改变。

这是窗户外观的草图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

您可以使用.grid()几何图形管理器实现所需的布局。布局包含单行和两列:

  1. 按钮左边的窄栏
  2. 文本框右侧的宽栏

要设置窗口和txt_edit的最小尺寸,您可以将窗口方法.rowconfigure().columnconfigure()minsize参数设置为800。为了处理调整大小,您可以将这些方法的weight参数设置为1

为了将两个按钮放在同一列中,您需要创建一个名为frm_buttonsFrame小部件。根据草图,这两个按钮应该垂直堆叠在这个框架内,顶部是btn_open。你可以用.grid().pack()几何图形管理器来完成。现在,您将坚持使用.grid(),因为它更容易使用。

既然有了计划,就可以开始编写应用程序了。第一步是创建您需要的所有小部件:

 1import tkinter as tk
 2
 3window = tk.Tk()
 4window.title("Simple Text Editor")
 5
 6window.rowconfigure(0, minsize=800, weight=1)
 7window.columnconfigure(1, minsize=800, weight=1)
 8
 9txt_edit = tk.Text(window)
10frm_buttons = tk.Frame(window, relief=tk.RAISED, bd=2)
11btn_open = tk.Button(frm_buttons, text="Open")
12btn_save = tk.Button(frm_buttons, text="Save As...")

下面是这段代码的分类:

  • 1 号线进口tkinter
  • 第 3 行和第 4 行创建一个标题为"Simple Text Editor"的新窗口。
  • 第 6 行和第 7 行设置行和列配置。
  • 第 9 行到第 12 行创建文本框、框架、打开和保存按钮所需的四个部件。

仔细看看第 6 行。.rowconfigure()minsize参数设置为800weight设置为1:

window.rowconfigure(0, minsize=800, weight=1)

第一个参数是0,它将第一行的高度设置为800像素,并确保行的高度与窗口的高度成比例增长。应用程序布局中只有一行,因此这些设置适用于整个窗口。

让我们再仔细看看第 7 行。这里,您使用.columnconfigure()将索引为1的列的widthweight属性分别设置为8001:

window.columnconfigure(1, minsize=800, weight=1)

记住,行和列的索引是从零开始的,所以这些设置只适用于第二列。通过只配置第二列,当调整窗口大小时,文本框将自然地扩展和收缩,而包含按钮的列将保持固定的宽度。

现在,您可以处理应用程序布局了。首先,使用.grid()几何管理器将两个按钮分配给frm_buttons帧:

12# ...
13
14btn_open.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
15btn_save.grid(row=1, column=0, sticky="ew", padx=5)

这两行代码frm_buttons帧中创建了一个具有两行一列的网格,因为btn_openbtn_save都将其master属性设置为frm_buttonsbtn_open放在第一行,btn_save放在第二行,这样btn_open就出现在布局图中btn_save的上方,就像你在草图中计划的那样。

btn_openbtn_save都将它们的sticky属性设置为"ew",这迫使按钮的向两个方向水平扩展并填充整个框架。这确保了两个按钮大小相同。

通过将padxpady参数设置为5,在每个按钮周围放置五个像素的填充。只有btn_open有垂直填充。因为它在顶部,垂直填充从窗口顶部向下偏移按钮一点,并确保它和btn_save之间有一个小间隙。

现在frm_buttons已经布置好了,可以开始为窗口的其余部分设置网格布局了:

15# ...
16
17frm_buttons.grid(row=0, column=0, sticky="ns")
18txt_edit.grid(row=0, column=1, sticky="nsew")

这两行代码window创建了一个一行两列的网格。您将frm_buttons放在第一列,将txt_edit放在第二列,这样frm_buttons就会出现在窗口布局中txt_edit的左边。

frm_buttonssticky参数设置为"ns",强制整个框架垂直扩展并填充其列的整个高度。txt_edit填充其整个网格单元,因为您将其sticky参数设置为"nsew",这迫使其向每个方向扩展。

现在应用程序布局已经完成,将window.mainloop()添加到程序底部,保存并运行文件:

18# ...
19
20window.mainloop()

将显示以下窗口:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

看起来棒极了!但是它现在还不能做任何事情,所以您需要开始编写按钮的命令。btn_open需要显示一个文件打开对话框,允许用户选择一个文件。然后需要打开文件并将txt_edit的文本设置为文件的内容。这里有一个open_file()函数可以做到这一点:

 1import tkinter as tk
 2
 3def open_file():
 4    """Open a file for editing."""
 5    filepath = askopenfilename(
 6        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
 7    )
 8    if not filepath:
 9        return
10    txt_edit.delete("1.0", tk.END)
11    with open(filepath, mode="r", encoding="utf-8") as input_file:
12        text = input_file.read()
13        txt_edit.insert(tk.END, text)
14    window.title(f"Simple Text Editor - {filepath}")
15
16# ...

下面是这个函数的分解:

  • 第 5 行到第 7 行使用tkinter.filedialog模块的askopenfilename()对话框显示一个文件打开对话框,并将选择的文件路径保存到filepath
  • 第 8 行和第 9 行检查用户是否关闭对话框或点击取消按钮。如果是,那么filepath将是None,并且函数将return而不执行任何代码来读取文件和设置txt_edit的文本。
  • 第 10 行使用.delete()清除txt_edit的当前内容。
  • 第 11 行和第 12 行在将text存储为字符串之前,打开所选文件及其内容.read()
  • 第 13 行使用.insert()将字符串text分配给txt_edit
  • 第 14 行设置窗口的标题,使其包含打开文件的路径。

现在你可以更新程序,这样每当点击时btn_open就会调用open_file()。你需要做一些事情来更新程序。首先,通过将下面的导入添加到程序的顶部,从tkinter.filedialog导入askopenfilename():

 1import tkinter as tk
 2from tkinter.filedialog import askopenfilename 3
 4# ...

接下来,将btn_opncommand属性设置为open_file:

 1import tkinter as tk
 2from tkinter.filedialog import askopenfilename
 3
 4def open_file():
 5    """Open a file for editing."""
 6    filepath = askopenfilename(
 7        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
 8    )
 9    if not filepath:
10        return
11    txt_edit.delete("1.0", tk.END)
12    with open(filepath, mode="r", encoding="utf-8") as input_file:
13        text = input_file.read()
14        txt_edit.insert(tk.END, text)
15    window.title(f"Simple Text Editor - {filepath}")
16
17window = tk.Tk()
18window.title("Simple Text Editor")
19
20window.rowconfigure(0, minsize=800, weight=1)
21window.columnconfigure(1, minsize=800, weight=1)
22
23txt_edit = tk.Text(window)
24frm_buttons = tk.Frame(window, relief=tk.RAISED, bd=2)
25btn_open = tk.Button(frm_buttons, text="Open", command=open_file) 26btn_save = tk.Button(frm_buttons, text="Save As...")
27
28# ...

保存文件并运行它以检查一切是否正常。然后尝试打开一个文本文件!

随着btn_open的运行,是时候为btn_save运行函数了。这需要打开一个保存文件对话框,以便用户可以选择他们想要保存文件的位置。为此,您将使用tkinter.filedialog模块中的asksaveasfilename()对话框。该函数还需要提取当前在txt_edit中的文本,并将其写入选定位置的文件中。这里有一个函数可以做到这一点:

15# ...
16
17def save_file():
18    """Save the current file as a new file."""
19    filepath = asksaveasfilename(
20        defaultextension=".txt",
21        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")],
22    )
23    if not filepath:
24        return
25    with open(filepath, mode="w", encoding="utf-8") as output_file:
26        text = txt_edit.get("1.0", tk.END)
27        output_file.write(text)
28    window.title(f"Simple Text Editor - {filepath}")
29
30# ...

下面是这段代码的工作原理:

  • 第 19 行到第 22 行使用asksaveasfilename()对话框从用户处获取所需的保存位置。选择的文件路径存储在filepath变量中。
  • 第 23 和 24 行检查用户是否关闭对话框或点击取消按钮。如果是,那么filepath将是None,并且函数将返回,而不执行任何代码来将文本保存到文件中。
  • 在选定的文件路径创建一个新文件。
  • 第 26 行.get()方法从txt_edit中提取文本并赋给变量text
  • 第 27 行将text写入输出文件。
  • 第 28 行更新窗口标题,这样新的文件路径显示在窗口标题中。

现在你可以更新程序,这样当它被点击时,btn_save会调用save_file()。同样,为了更新程序,您需要做一些事情。首先,通过更新脚本顶部的导入,从tkinter.filedialog导入asksaveasfilename(),如下所示:

 1import tkinter as tk
 2from tkinter.filedialog import askopenfilename, asksaveasfilename 3
 4# ...

最后,将btn_savecommand属性设置为save_file:

28# ...
29
30window = tk.Tk()
31window.title("Simple Text Editor")
32
33window.rowconfigure(0, minsize=800, weight=1)
34window.columnconfigure(1, minsize=800, weight=1)
35
36txt_edit = tk.Text(window)
37frm_buttons = tk.Frame(window, relief=tk.RAISED, bd=2)
38btn_open = tk.Button(frm_buttons, text="Open", command=open_file)
39btn_save = tk.Button(frm_buttons, text="Save As...", command=save_file) 40
41btn_open.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
42btn_save.grid(row=1, column=0, sticky="ew", padx=5)
43
44frm_buttons.grid(row=0, column=0, sticky="ns")
45txt_edit.grid(row=0, column=1, sticky="nsew")
46
47window.mainloop()

保存文件并运行它。您现在已经有了一个最小但功能齐全的文本编辑器!

您可以展开下面的代码块来查看完整的脚本:

以下是完整的脚本供您参考:

import tkinter as tk
from tkinter.filedialog import askopenfilename, asksaveasfilename

def open_file():
    """Open a file for editing."""
    filepath = askopenfilename(
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
    )
    if not filepath:
        return
    txt_edit.delete("1.0", tk.END)
    with open(filepath, mode="r", encoding="utf-8") as input_file:
        text = input_file.read()
        txt_edit.insert(tk.END, text)
    window.title(f"Simple Text Editor - {filepath}")

def save_file():
    """Save the current file as a new file."""
    filepath = asksaveasfilename(
        defaultextension=".txt",
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")],
    )
    if not filepath:
        return
    with open(filepath, mode="w", encoding="utf-8") as output_file:
        text = txt_edit.get("1.0", tk.END)
        output_file.write(text)
    window.title(f"Simple Text Editor - {filepath}")

window = tk.Tk()
window.title("Simple Text Editor")

window.rowconfigure(0, minsize=800, weight=1)
window.columnconfigure(1, minsize=800, weight=1)

txt_edit = tk.Text(window)
frm_buttons = tk.Frame(window, relief=tk.RAISED, bd=2)
btn_open = tk.Button(frm_buttons, text="Open", command=open_file)
btn_save = tk.Button(frm_buttons, text="Save As...", command=save_file)

btn_open.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
btn_save.grid(row=1, column=0, sticky="ew", padx=5)

frm_buttons.grid(row=0, column=0, sticky="ns")
txt_edit.grid(row=0, column=1, sticky="nsew")

window.mainloop()

现在,您已经用 Python 构建了两个 GUI 应用程序,并应用了您在本教程中学到的许多技能。这是一个不小的成就,所以花些时间为你所做的感到高兴。您现在已经准备好自己处理一些应用程序了!

结论

在本教程中,您学习了如何开始 Python GUI 编程。对于 Python GUI 框架来说,Tkinter 是一个令人信服的选择,因为它内置于 Python 标准库中,并且使用该框架制作应用程序相对来说不太费力。

在本教程中,您已经学习了几个重要的 Tkinter 概念:

  • 如何使用小工具
  • 如何用几何图形管理器控制你的应用布局
  • 如何让您的应用程序具有交互性
  • 如何使用五个基本的 Tkinterwidget:LabelButtonEntryTextFrame

现在您已经掌握了使用 Tkinter 进行 Python GUI 编程的基础,下一步是构建一些您自己的应用程序。你会创造什么?在下面的评论中分享你有趣的项目吧!

额外资源

在本教程中,您仅仅触及了使用 Tkinter 创建 Python GUI 应用程序的基础。还有许多其他主题没有在这里介绍。在本节中,您将找到一些可帮助您继续旅程的最佳资源。

t 内部引用

这里有一些官方资源可供参考:

  • 官方 Python Tkinter 参考文档以中等深度介绍了 Python 的 Tkinter 模块。它是为更高级的 Python 开发人员编写的,并不是初学者的最佳资源。
  • Tkinter 8.5 参考:Python 的 GUI 是一个广泛的参考,涵盖了 Tkinter 模块的大部分。它是详尽的,但它是以参考风格编写的,没有评论或例子。
  • Tk 命令参考是 Tk 库中命令的权威指南。它是为 Tcl 语言编写的,但它回答了许多关于为什么 Tkinter 中的事情会这样工作的问题。

附加部件

在本教程中,您学习了LabelButtonEntryTextFrame小部件。Tkinter 中还有其他几个小部件,它们对于构建真实世界的应用程序都是必不可少的。以下是一些继续学习小部件的资源:

  • TkDocs Tkinter 教程是一个相当全面的 Tk 指南,Tkinter 使用的底层代码库。用 Python、Ruby、Perl 和 Tcl 给出了例子。除了这两个部分中介绍的以外,您还可以找到几个小部件示例:
  • 官方 Python 文档涵盖了额外的小部件:

应用分布

一旦你用 Tkinter 创建了一个应用程序,你可能想把它分发给你的同事和朋友。这里有一些教程可以帮助你完成这个过程:

其他 GUI 框架

Tkinter 不是 Python GUI 框架的唯一选择。如果 Tkinter 不能满足您项目的需求,那么这里有一些其他的框架可以考虑:

***参加测验:***通过我们的交互式“使用 Tkinter 进行 Python GUI 编程”测验来测试您的知识。完成后,您将收到一个分数,以便您可以跟踪一段时间内的学习进度:

参加测验***********

如何用 wxPython 构建 Python GUI 应用程序

原文:https://realpython.com/python-gui-with-wxpython/

有许多图形用户界面(GUI)工具包可用于 Python 编程语言。三巨头分别是 Tkinter ,wxPython, PyQt 。这些工具包都可以在 Windows、macOS 和 Linux 上工作,PyQt 还具有在移动设备上工作的能力。

图形用户界面是一个应用程序,它有按钮、窗口和许多其他小部件,用户可以用它们来与应用程序进行交互。web 浏览器就是一个很好的例子。它有按钮、标签和一个主窗口,所有的内容都在这里加载。

在本文中,您将学习如何使用 wxPython GUI 工具包用 Python 构建一个图形用户界面。

以下是涵盖的主题:

  • wxPython 入门
  • 图形用户界面的定义
  • 创建框架应用程序
  • 创建工作应用程序

我们开始学习吧!

免费下载: 从 Python 技巧中获取一个示例章节:这本书用简单的例子向您展示了 Python 的最佳实践,您可以立即应用它来编写更漂亮的+Python 代码。

wxPython 入门

wxPython GUI toolkit 是一个 Python 包装器,围绕着一个名为 wxWidgets 的 C++库。wxPython 的首次发布是在 1998 年,所以 wxPython 已经存在了相当长的时间。wxPython 与其他工具包(如 PyQtTkinter )的主要区别在于,wxPython 尽可能使用原生平台上的实际部件。这使得 wxPython 应用程序看起来是运行它的操作系统的原生程序。

PyQt 和 Tkinter 都是自己绘制小部件,这就是为什么它们不总是匹配原生小部件,尽管 PyQt 非常接近。

这并不是说 wxPython 不支持定制小部件。事实上,wxPython 工具包包含了许多定制的小部件,以及数十个核心小部件。wxPython 下载页面有一个名为额外文件的部分值得一看。

这里有一个 wxPython 演示包的下载。这是一个不错的小应用程序,演示了 wxPython 中包含的绝大多数小部件。该演示允许开发人员在一个选项卡中查看代码,并在另一个选项卡中运行它。您甚至可以编辑并重新运行演示中的代码,以查看您的更改如何影响应用程序。

Remove ads

安装 wxPython

本文将使用最新的 wxPython,即 wxPython 4 ,也称为 Phoenix release。wxPython 3 和 wxPython 2 版本只为 Python 2 打造。当 wxPython 的主要维护者 Robin Dunn 创建 wxPython 4 版本时,他弃用了大量别名,清理了大量代码,使 wxPython 更加 Python 化,更易于维护。

如果您要从旧版本的 wxPython 迁移到 wxPython 4 (Phoenix ),您需要参考以下链接:

wxPython 4 包兼容 Python 2.7 和 Python 3。

您现在可以使用pip来安装 wxPython 4,这在 wxPython 的遗留版本中是不可能的。您可以执行以下操作将它安装到您的计算机上:

$ pip install wxpython

**注意:**在 Mac OS X 上,你需要安装一个编译器,比如 XCode ,这样安装才能成功完成。Linux 可能还需要您安装一些依赖项,然后pip安装程序才能正常工作。

例如,我需要在 Xubuntu 上安装 freeglut3-devlibgstreamer-plugins-base 0.10-devlibwebkitgtk-3.0-dev 来安装它。

幸运的是,pip显示的错误消息有助于找出缺少的内容,如果您想在 Linux 上安装 wxPython,可以使用 wxPython Github 页面上的先决条件部分来帮助您找到所需的信息。

在 GTK2 和 GTK3 版本的 Extras Linux 部分,您可以找到一些适用于最流行的 Linux 版本的 Python wheels。要安装其中一个轮子,您可以使用以下命令:

$ pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04/ wxPython

确保您已经修改了上面的命令以匹配您的 Linux 版本。

图形用户界面的定义

正如在引言中提到的,图形用户界面(GUI)是绘制在屏幕上供用户交互的界面。

用户界面有一些通用组件:

  • 主窗口
  • 菜单
  • 工具栏
  • 小跟班
  • 文本输入
  • 标签

所有这些项目统称为小部件。wxPython 还支持许多其他常见的小部件和许多自定义小部件。开发人员将获取这些小部件,并将它们逻辑地排列在一个窗口上,供用户进行交互。

事件循环

图形用户界面通过等待用户做一些事情来工作。这件事被称为一个事件。当您的应用程序处于焦点时,或者当用户使用鼠标按下按钮或其他小部件时,当用户键入某些内容时,就会发生事件。

在幕后,GUI 工具包正在运行一个被称为事件循环的无限循环。事件循环只是等待事件发生,然后根据开发人员编写的应用程序来处理这些事件。当应用程序没有捕捉到一个事件时,它实际上忽略了它的发生。

当你在编写一个图形用户界面时,你会想记住你需要把每个部件挂接到事件处理程序上,这样你的应用程序就会做一些事情。

在处理事件循环时,有一点需要特别注意:它们可能会被阻塞。当您阻塞一个事件循环时,GUI 将变得无响应,并对用户显示为冻结。

在 GUI 中启动的任何进程,如果花费的时间超过四分之一秒,应该作为一个单独的线程或进程启动。这将防止您的 GUI 冻结,并为用户提供更好的用户体验。

wxPython 框架具有特殊的线程安全方法,您可以使用这些方法与您的应用程序进行通信,让它知道线程已经完成或者给它一个更新。

让我们创建一个框架应用程序来演示事件是如何工作的。

Remove ads

创建一个框架应用程序

GUI 环境中的应用程序框架是一个带有小部件的用户界面,这些小部件没有任何事件处理程序。这些对原型制作很有用。在花费大量时间在后端逻辑上之前,您基本上只需要创建 GUI,并将其提交给您的利益相关者签字认可。

让我们从用 wxPython 创建一个Hello World应用程序开始:

import wx

app = wx.App()
frame = wx.Frame(parent=None, title='Hello World')
frame.Show()
app.MainLoop()

注意: Mac 用户可能会得到以下消息:*这个程序需要访问屏幕。请使用 python 的框架构建运行,并且仅当您登录到 Mac 的主显示屏时运行。*如果您看到这条消息,并且您没有在 virtualenv 中运行,那么您需要使用 pythonw 而不是 python 来运行您的应用程序。如果您在 virtualenv 中运行 wxPython,那么请参见 wxPython wiki 获取解决方案。

在这个例子中,您有两个部分:wx.Appwx.Framewx.App是 wxPython 的应用程序对象,是运行 GUI 所必需的。这个wx.App启动了一个叫做.MainLoop()的东西。这是您在上一节中了解到的事件循环。

拼图的另一块是wx.Frame,它将为用户创建一个交互窗口。在这种情况下,您告诉 wxPython 该帧没有父帧,它的标题是Hello World。下面是运行代码时的样子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

**注意:**在 Mac 或 Windows 上运行时,应用程序看起来会有所不同。

默认情况下,wx.Frame会在顶部包含最小化、最大化和退出按钮。但是,您通常不会以这种方式创建应用程序。大多数 wxPython 代码将要求您子类化wx.Frame和其他小部件,以便您可以获得该工具包的全部功能。

让我们花点时间将您的代码重写为一个类:

import wx

class MyFrame(wx.Frame):    
    def __init__(self):
        super().__init__(parent=None, title='Hello World')
        self.Show()

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    app.MainLoop()

您可以将此代码用作应用程序的模板。然而,这个应用程序做的不多,所以让我们花一点时间来了解一些您可以添加的其他小部件。

小部件

wxPython 工具包有一百多个小部件可供选择。这使您可以创建丰富的应用程序,但是要想知道使用哪个小部件也是一件令人生畏的事情。这就是为什么 wxPython 演示是有用的,因为它有一个搜索过滤器,您可以使用它来帮助您找到可能适用于您的项目的小部件。

大多数 GUI 应用程序允许用户输入一些文本并按下按钮。让我们继续添加这些小部件:

import wx

class MyFrame(wx.Frame):    
    def __init__(self):
        super().__init__(parent=None, title='Hello World')
        panel = wx.Panel(self)

        self.text_ctrl = wx.TextCtrl(panel, pos=(5, 5))
        my_btn = wx.Button(panel, label='Press Me', pos=(5, 55))

        self.Show()

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    app.MainLoop()

当您运行这段代码时,您的应用程序应该如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

您需要添加的第一个小部件叫做wx.Panel。这个小部件不是必需的,但是推荐使用。在 Windows 上,您实际上需要使用一个面板,以便框架的背景颜色是正确的灰色阴影。在 Windows 上,如果没有面板,选项卡遍历将被禁用。

当您将面板小部件添加到框架中,并且面板是框架的唯一子级时,它将自动扩展以用自身填充框架。

下一步是给面板添加一个wx.TextCtrl。几乎所有部件的第一个参数是部件应该放在哪个父部件上。在这种情况下,您希望文本控件和按钮位于面板的顶部,因此它是您指定的父控件。

您还需要告诉 wxPython 在哪里放置小部件,这可以通过用pos参数传入一个位置来完成。在 wxPython 中,原点位置是(0,0),即父对象的左上角。因此,对于文本控件,您告诉 wxPython,您希望将其左上角定位在距离左侧(x)5 个像素和距离顶部(y)5 个像素的位置。

然后将按钮添加到面板中,并给它一个标签。为了防止小部件重叠,您需要将按钮位置的 y 坐标设置为 55。

Remove ads

绝对定位

当您为小部件的位置提供精确坐标时,您使用的技术称为绝对定位。大多数 GUI 工具包都提供了这种功能,但实际上并不推荐。

随着应用程序变得越来越复杂,跟踪所有小部件的位置变得越来越困难,如果你不得不四处移动小部件。重置所有这些位置变成了一场噩梦。

幸运的是,所有现代 GUI 工具包都为此提供了一个解决方案,这也是您接下来将要学习的内容。

分级器(动态分级)

wxPython 工具包包括sizer,用于创建动态布局。它们为您管理小部件的位置,并在您调整应用程序窗口大小时调整它们。其他 GUI 工具包将 sizers 称为布局,这就是 PyQt 所做的。

以下是您最常看到的主要类型的筛分机:

  • wx.BoxSizer
  • wx.GridSizer
  • wx.FlexGridSizer

让我们给你的例子添加一个wx.BoxSizer,看看我们是否能让它工作得更好一点:

import wx

class MyFrame(wx.Frame):    
    def __init__(self):
        super().__init__(parent=None, title='Hello World')
        panel = wx.Panel(self)        
        my_sizer = wx.BoxSizer(wx.VERTICAL)        
        self.text_ctrl = wx.TextCtrl(panel)
        my_sizer.Add(self.text_ctrl, 0, wx.ALL | wx.EXPAND, 5)        
        my_btn = wx.Button(panel, label='Press Me')
        my_sizer.Add(my_btn, 0, wx.ALL | wx.CENTER, 5)        
        panel.SetSizer(my_sizer)        
        self.Show()

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    app.MainLoop()

这里您创建了一个wx.BoxSizer的实例并传递给它wx.VERTICAL,这是小部件被添加到 sizer 的方向。

在这种情况下,小部件将垂直添加,这意味着它们将从上到下一次添加一个。您也可以将 BoxSizer 的方向设置为wx.HORIZONTAL。当你这样做时,小部件将从左到右添加。

要向 sizer 添加一个小部件,您将使用.Add()。它最多接受五个参数:

  • window(微件)
  • proportion
  • flag
  • border
  • userData

window参数是要添加的小部件,而proportion设置这个小部件相对于 sizer 中的其他小部件应该占用多少空间。默认情况下,它是零,这告诉 wxPython 保持小部件的默认比例。

第三个参数是flag。如果您愿意,您实际上可以传入多个标志,只要您用管道字符:|分隔它们。wxPython 工具包使用一系列按位“或”运算,使用|来添加标志。

在本例中,您添加了带有wx.ALLwx.EXPAND标志的文本控件。wx.ALL标志告诉 wxPython 您想要在小部件的所有边上添加边框,而wx.EXPAND让小部件在 sizer 中尽可能地扩展。

最后,有一个border参数,它告诉 wxPython 在小部件周围需要多少像素的边框。userData参数仅在您想要对小部件的大小做一些复杂的事情时使用,实际上在实践中很少见到。

将按钮添加到 sizer 遵循完全相同的步骤。然而,为了让事情变得更有趣一点,我把wx.CENTERwx.EXPAND标志去掉,这样按钮就会在屏幕上居中。

当您运行此版本的代码时,您的应用程序应该如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果您想了解关于 sizers 的更多信息,wxPython 文档有一个关于这个主题的很好的页面。

Remove ads

添加事件

虽然你的应用程序看起来更有趣,但它实际上并没有做什么。例如,如果你按下按钮,什么都不会发生。

让我们给按钮一个任务:

import wx

class MyFrame(wx.Frame):    
    def __init__(self):
        super().__init__(parent=None, title='Hello World')
        panel = wx.Panel(self)        
        my_sizer = wx.BoxSizer(wx.VERTICAL)        
        self.text_ctrl = wx.TextCtrl(panel)
        my_sizer.Add(self.text_ctrl, 0, wx.ALL | wx.EXPAND, 5)        
        my_btn = wx.Button(panel, label='Press Me')
        my_btn.Bind(wx.EVT_BUTTON, self.on_press)
        my_sizer.Add(my_btn, 0, wx.ALL | wx.CENTER, 5)        
        panel.SetSizer(my_sizer)        
        self.Show()

    def on_press(self, event):
        value = self.text_ctrl.GetValue()
        if not value:
            print("You didn't enter anything!")
        else:
            print(f'You typed: "{value}"')

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    app.MainLoop()

wxPython 中的小部件允许您将事件绑定附加到它们,以便它们可以响应某些类型的事件。

**注意:**上面的代码块使用了 f 字符串。你可以在 Python 3 的 f-Strings:一个改进的字符串格式化语法(指南)中读到所有关于它们的内容。

当用户按下按钮时,您希望按钮做一些事情。您可以通过调用按钮的.Bind()方法来实现这一点。.Bind()接受您想要绑定的事件、事件发生时要调用的处理程序、一个可选的源和几个可选的 id。

在本例中,您将按钮对象绑定到wx.EVT_BUTTON事件,并告诉它在该事件被触发时调用on_press()

当用户执行您绑定到的事件时,事件被“激发”。在这种情况下,您设置的事件是按钮按下事件wx.EVT_BUTTON

.on_press()接受第二个参数,您可以称之为event。这是惯例。如果你愿意,你可以叫它别的名字。然而,这里的 event 参数指的是这样一个事实:当调用这个方法时,它的第二个参数应该是某种类型的 event 对象。

.on_press()中,您可以通过调用它的GetValue()方法来获取文本控件的内容。然后,根据文本控件的内容,将一个字符串打印到 stdout。

现在你已经有了基本的方法,让我们学习如何创建一个有用的应用程序!

创建工作应用程序

创造新事物的第一步是弄清楚你想创造什么。在这种情况下,我冒昧地为你做了那个决定。你将学习如何创建一个 MP3 标签编辑器!创建新东西的下一步是找出哪些包可以帮助你完成任务。

如果你在谷歌上搜索Python mp3 tagging,你会发现你有几个选项:

  • mp3-tagger
  • eyeD3
  • mutagen

我试用了几个,并决定 eyeD3 有一个不错的 API,你可以使用它而不会陷入 MP3 的 ID3 规范。你可以用pip安装 eyeD3 ,像这样:

$ pip install eyed3

在 macOS 上安装这个包时,可能需要使用brew安装libmagic。Windows 和 Linux 用户安装 eyeD3 应该没有任何问题。

设计用户界面

当设计一个界面的时候,最好只是勾画出你认为用户界面应该是什么样子。

您需要能够做到以下几点:

  • 打开一个或多个 MP3 文件
  • 显示当前的 MP3 标签
  • 编辑 MP3 标签

大多数用户界面使用菜单或按钮来打开文件或文件夹。你可以用一个文件菜单来做这个。因为您可能希望看到多个 MP3 文件的标签,所以您需要找到一个小部件来很好地做到这一点。

带有列和行的表格是理想的,因为这样你就可以为 MP3 标签标记列。wxPython 工具包中有几个小部件可以解决这个问题,其中最重要的两个部件如下:

  • wx.grid.Grid
  • wx.ListCtrl

在这种情况下,您应该使用wx.ListCtrl,因为Grid小部件太过了,而且坦率地说,它也要复杂得多。最后,您需要一个按钮来编辑所选 MP3 的标签。

现在你知道你想要什么了,你可以把它画出来:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面的插图让我们了解了应用程序应该是什么样子。现在你知道你想做什么了,是时候编码了!

Remove ads

创建用户界面

在编写新的应用程序时,有许多不同的方法。比如需要遵循模型-视图-控制器的设计模式吗?你是怎么分班的?每个文件一个类?有很多这样的问题,随着你对 GUI 设计越来越有经验,你会知道你想如何回答它们。

在您的情况下,您实际上只需要两个类:

  • 一门wx.Panel
  • 一门wx.Frame

你也可以主张创建一个控制器类型的模块,但是对于这样的东西,你真的不需要它。也可以将每个类放入自己的模块中,但是为了保持简洁,您将为所有代码创建一个 Python 文件。

让我们从导入和面板类开始:

import eyed3
import glob
import wx

class Mp3Panel(wx.Panel):    
    def __init__(self, parent):
        super().__init__(parent)
        main_sizer = wx.BoxSizer(wx.VERTICAL)
        self.row_obj_dict = {}

        self.list_ctrl = wx.ListCtrl(
            self, size=(-1, 100), 
            style=wx.LC_REPORT | wx.BORDER_SUNKEN
        )
        self.list_ctrl.InsertColumn(0, 'Artist', width=140)
        self.list_ctrl.InsertColumn(1, 'Album', width=140)
        self.list_ctrl.InsertColumn(2, 'Title', width=200)
        main_sizer.Add(self.list_ctrl, 0, wx.ALL | wx.EXPAND, 5)        
        edit_button = wx.Button(self, label='Edit')
        edit_button.Bind(wx.EVT_BUTTON, self.on_edit)
        main_sizer.Add(edit_button, 0, wx.ALL | wx.CENTER, 5)        
        self.SetSizer(main_sizer)

    def on_edit(self, event):
        print('in on_edit')

    def update_mp3_listing(self, folder_path):
        print(folder_path)

这里,您为您的用户界面导入了eyed3包、Python 的glob包和wx包。接下来,您子类化wx.Panel并创建您的用户界面。你需要一本字典来存储你的 MP3 数据,你可以把它命名为row_obj_dict

然后创建一个wx.ListCtrl,并将其设置为带有凹陷边框(wx.BORDER_SUNKEN)的报告模式(wx.LC_REPORT)。根据传入的样式标志,列表控件可以采用其他几种形式,但报告标志是最常用的。

为了使ListCtrl拥有正确的标题,您需要为每个列标题调用.InsertColumn()。然后提供列的索引、标签以及列的宽度(以像素为单位)。

最后一步是添加您的Edit按钮、事件处理程序和方法。您可以创建事件的绑定,并让它调用的方法暂时为空。

现在,您应该为框架编写代码:

class Mp3Frame(wx.Frame):    
    def __init__(self):
        super().__init__(parent=None,
                         title='Mp3 Tag Editor')
        self.panel = Mp3Panel(self)
        self.Show()

if __name__ == '__main__':
    app = wx.App(False)
    frame = Mp3Frame()
    app.MainLoop()

这个类比第一个简单得多,因为您需要做的只是设置框架的标题并实例化面板类Mp3Panel。完成后,您的用户界面应该如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

用户界面看起来几乎没错,但是你没有一个文件菜单。这使得不可能添加 MP3 到应用程序和编辑他们的标签!

让我们现在就解决这个问题。

制作一个有效的应用程序

让您的应用程序工作的第一步是更新应用程序,使它有一个文件菜单,因为这样您就可以将 MP3 文件添加到您的作品中。菜单几乎总是添加到wx.Frame类中,所以这是您需要修改的类。

**注意:**一些应用程序已经不再有菜单了。第一个这样做的是微软 Office,他们添加了功能区栏。wxPython 工具包有一个定制的小部件,可以用来在wx.lib.agw.ribbon中创建功能区。

另一种最近放弃菜单的应用是网络浏览器,比如谷歌 Chrome 和 Mozilla Firefox。他们现在只使用工具栏。

让我们学习如何在应用程序中添加菜单栏:

class Mp3Frame(wx.Frame):

    def __init__(self):
        wx.Frame.__init__(self, parent=None, 
                          title='Mp3 Tag Editor')
        self.panel = Mp3Panel(self)
        self.create_menu()
        self.Show()

    def create_menu(self):
        menu_bar = wx.MenuBar()
        file_menu = wx.Menu()
        open_folder_menu_item = file_menu.Append(
            wx.ID_ANY, 'Open Folder', 
            'Open a folder with MP3s'
        )
        menu_bar.Append(file_menu, '&File')
        self.Bind(
            event=wx.EVT_MENU, 
            handler=self.on_open_folder,
            source=open_folder_menu_item,
        )
        self.SetMenuBar(menu_bar)

    def on_open_folder(self, event):
        title = "Choose a directory:"
        dlg = wx.DirDialog(self, title, 
                           style=wx.DD_DEFAULT_STYLE)
        if dlg.ShowModal() == wx.ID_OK:
            self.panel.update_mp3_listing(dlg.GetPath())
        dlg.Destroy()

这里,您在类的构造函数中添加了对.create_menu()的调用。然后在.create_menu()本身中,您将创建一个wx.MenuBar实例和一个wx.Menu实例。

要将菜单项添加到菜单中,您可以调用菜单实例的.Append(),并向其传递以下内容:

  • 唯一的标识符
  • 新菜单项的标签
  • 帮助字符串

接下来,您需要将菜单添加到菜单栏,因此您需要调用菜单栏的.Append()。它接受菜单实例和菜单标签。这个标签有点奇怪,因为你称它为&File而不是File。“与”号告诉 wxPython 创建一个键盘快捷键 Alt + F ,只需使用键盘就可以打开File菜单。

**注意:**如果你想在你的应用程序中添加键盘快捷键,那么你需要使用wx.AcceleratorTable的一个实例来创建它们。你可以在 wxPython 文档中阅读更多关于 Accerator 表的内容。

要创建事件绑定,您需要调用self.Bind(),它将帧绑定到wx.EVT_MENU。当您为菜单事件使用self.Bind()时,您不仅需要告诉 wxPython 使用哪个handler,还需要告诉 wxPython 将处理程序绑定到哪个source

最后,您必须调用框架的.SetMenuBar()并向其传递 menubar 实例,以便向用户显示。

现在,您已经将菜单添加到了框架中,让我们来看一下菜单项的事件处理程序,如下所示:

def on_open_folder(self, event):
    title = "Choose a directory:"
    dlg = wx.DirDialog(self, title, style=wx.DD_DEFAULT_STYLE)
    if dlg.ShowModal() == wx.ID_OK:
        self.panel.update_mp3_listing(dlg.GetPath())
    dlg.Destroy()

既然你想让用户选择一个包含 MP3 的文件夹,你就应该使用 wxPython 的wx.DirDialogwx.DirDialog只允许用户打开目录。

您可以设置对话框的标题和各种样式标志。要显示该对话框,您需要调用.ShowModal()。这将导致对话框有模式地显示,这意味着当对话框显示时,用户将不能与您的主应用程序交互。

如果用户按下对话框的 OK 按钮,可以通过对话框的.GetPath()得到用户的路径选择。您需要将该路径传递给 panel 类,这可以通过调用 panel 的.update_mp3_listing()来实现。

最后,您需要关闭对话框。要关闭对话框,推荐的方法是调用它的.Destroy()

对话框确实有一个.Close()方法,但它基本上只是隐藏对话框,当你关闭应用程序时它不会自我销毁,这可能会导致奇怪的问题,比如你的应用程序现在正常关闭。更简单的方法是在对话框上调用.Destroy()来防止这个问题。

现在让我们更新你的Mp3Panel类。你可以从更新.update_mp3_listing()开始:

def update_mp3_listing(self, folder_path):
    self.current_folder_path = folder_path
    self.list_ctrl.ClearAll()

    self.list_ctrl.InsertColumn(0, 'Artist', width=140)
    self.list_ctrl.InsertColumn(1, 'Album', width=140)
    self.list_ctrl.InsertColumn(2, 'Title', width=200)
    self.list_ctrl.InsertColumn(3, 'Year', width=200)

    mp3s = glob.glob(folder_path + '/*.mp3')
    mp3_objects = []
    index = 0
    for mp3 in mp3s:
        mp3_object = eyed3.load(mp3)
        self.list_ctrl.InsertItem(index, 
            mp3_object.tag.artist)
        self.list_ctrl.SetItem(index, 1, 
            mp3_object.tag.album)
        self.list_ctrl.SetItem(index, 2, 
            mp3_object.tag.title)
        mp3_objects.append(mp3_object)
        self.row_obj_dict[index] = mp3_object
        index += 1

在这里,您将当前目录设置为指定的文件夹,然后清除列表控件。这使得列表控件保持新鲜,并且只显示你当前正在处理的 MP3。这也意味着您需要再次重新插入所有列。

接下来,您将需要获取传入的文件夹,并使用 Python 的glob 模块来搜索 MP3 文件。

然后你可以循环播放 MP3 并把它们转换成eyed3对象。你可以通过调用eyed3.load()来实现。假设 MP3 已经有了适当的标签,那么您可以将 MP3 的艺术家、专辑和标题添加到列表控件中。

有趣的是,向列表控件对象添加新行的方法是对第一列调用.InsertItem(),对所有后续列调用SetItem()

最后一步是将 MP3 对象保存到 Python 字典row_obj_dict

现在您需要更新.on_edit()事件处理程序,以便编辑 MP3 的标签:

def on_edit(self, event):
    selection = self.list_ctrl.GetFocusedItem()
    if selection >= 0:
        mp3 = self.row_obj_dict[selection]
        dlg = EditDialog(mp3)
        dlg.ShowModal()
        self.update_mp3_listing(self.current_folder_path)
        dlg.Destroy()

您需要做的第一件事是通过调用列表控件的.GetFocusedItem()来获取用户的选择。

如果用户没有在列表控件中选择任何内容,它将返回-1。假设用户选择了某个内容,您将希望从字典中提取 MP3 对象并打开一个 MP3 标签编辑器对话框。这将是一个自定义对话框,您将使用它来编辑 MP3 文件的艺术家、专辑和标题标签。

像往常一样,显示对话框。当对话框关闭时,将执行.on_edit()中的最后两行。这两行将更新列表控件,使其显示用户刚刚编辑的当前 MP3 标签信息,并销毁对话框。

创建编辑对话框

拼图的最后一块是创建一个 MP3 标签编辑对话框。为了简洁,我们将跳过这个界面的草图,因为它是一系列包含标签和文本控件的行。文本控件中应该预先填充了现有的标记信息。您可以通过创建wx.StaticText的实例来为文本控件创建标签。

当你需要创建一个自定义对话框时,wx.Dialog类就是你的朋友。您可以使用它来设计编辑器:

class EditDialog(wx.Dialog):    
    def __init__(self, mp3):
        title = f'Editing "{mp3.tag.title}"'
        super().__init__(parent=None, title=title)        
        self.mp3 = mp3        
        self.main_sizer = wx.BoxSizer(wx.VERTICAL)        
        self.artist = wx.TextCtrl(
            self, value=self.mp3.tag.artist)
        self.add_widgets('Artist', self.artist)        
        self.album = wx.TextCtrl(
            self, value=self.mp3.tag.album)
        self.add_widgets('Album', self.album)        
        self.title = wx.TextCtrl(
            self, value=self.mp3.tag.title)
        self.add_widgets('Title', self.title)        
        btn_sizer = wx.BoxSizer()
        save_btn = wx.Button(self, label='Save')
        save_btn.Bind(wx.EVT_BUTTON, self.on_save)        
        btn_sizer.Add(save_btn, 0, wx.ALL, 5)
        btn_sizer.Add(wx.Button(
            self, id=wx.ID_CANCEL), 0, wx.ALL, 5)
        self.main_sizer.Add(btn_sizer, 0, wx.CENTER)        
        self.SetSizer(self.main_sizer)

这里你想从子类化wx.Dialog开始,并根据你正在编辑的 MP3 的标题给它一个自定义的标题。

接下来,您可以创建您想要使用的 sizer 和小部件。为了使事情变得简单,您可以创建一个名为.add_widgets()的助手方法,用于将wx.StaticText小部件添加为带有文本控件实例的行。这里唯一的另一个小部件是保存按钮。

接下来让我们编写add_widgets方法:

 def add_widgets(self, label_text, text_ctrl):
        row_sizer = wx.BoxSizer(wx.HORIZONTAL)
        label = wx.StaticText(self, label=label_text,
                              size=(50, -1))
        row_sizer.Add(label, 0, wx.ALL, 5)
        row_sizer.Add(text_ctrl, 1, wx.ALL | wx.EXPAND, 5)
        self.main_sizer.Add(row_sizer, 0, wx.EXPAND)

add_widgets()获取标签的文本和文本控件实例。然后它创建一个水平方向的BoxSizer

接下来,您将使用传入的文本为标签参数创建一个wx.StaticText的实例。您还将设置它的大小为50像素宽,默认高度用-1设置。因为您希望标签在文本控件之前,所以您将首先向 BoxSizer 添加 StaticText 小部件,然后添加文本控件。

最后,您希望将水平尺寸标注添加到顶级垂直尺寸标注中。通过将 sizers 相互嵌套,您可以设计复杂的应用程序。

现在您需要创建on_save()事件处理程序,以便保存您的更改:

 def on_save(self, event):
        self.mp3.tag.artist = self.artist.GetValue()
        self.mp3.tag.album = self.album.GetValue()
        self.mp3.tag.title = self.title.GetValue()
        self.mp3.tag.save()
        self.Close()

在这里,您将标签设置为文本控件的内容,然后调用eyed3对象的.save()。最后,你调用对话框的.Close()。你打电话的原因。这里的Close()而不是.Destroy()是你已经在你的 panel 子类的.on_edit()中调用了.Destroy()

现在你的申请完成了!

Remove ads

结论

在本文中,您学到了很多关于 wxPython 的知识。您已经熟悉了使用 wxPython 创建 GUI 应用程序的基础知识。

您现在对以下内容有了更多的了解:

  • 如何使用 wxPython 的一些小部件
  • wxPython 中事件的工作方式
  • 绝对定位与 sizers 相比如何
  • 如何创建一个框架应用程序

最后,您学习了如何创建一个工作应用程序,一个 MP3 标签编辑器。您可以使用在本文中学到的知识来继续增强这个应用程序,或者自己创建一个令人惊叹的应用程序。

wxPython GUI toolkit 是健壮的,并且充满了有趣的小部件,可以用来构建跨平台的应用程序。你只受限于你的想象力。

延伸阅读

如果您想了解更多关于 wxPython 的信息,可以查看以下链接:

想了解更多关于 Python 的其他功能,你可能想看看我能用 Python 做什么?如果你想了解更多关于 Python 的super(),那么用 Python super() 增强你的类可能正适合你。

如果您想更深入地研究它,您还可以下载您在本文中创建的 MP3 标签编辑器应用程序的代码。******

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值