Skip to content

Latest commit

 

History

History
829 lines (584 loc) · 32.1 KB

File metadata and controls

829 lines (584 loc) · 32.1 KB

四、对话框和菜单

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

  • 显示警报对话框
  • 请求用户确认
  • 选择文件和目录
  • 将数据保存到文件中
  • 创建菜单栏
  • 在菜单中使用变量
  • 显示上下文菜单
  • 打开辅助窗口
  • 在窗口之间传递变量
  • 处理窗口删除

介绍

几乎每个重要的 GUI 应用程序都由多个视图组成。在浏览器中,这是通过从一个 HTML 页面导航到另一个 HTML 页面来实现的,而在桌面应用程序中,它由用户可以交互的多个窗口和对话框来表示。

到目前为止,我们已经学习了如何只创建一个根窗口,它与 Tcl 解释器相关联。然而,Tkinter 允许我们在同一个应用程序下创建多个顶级窗口,它还包括带有内置对话框的特定模块。

另一种在应用程序中导航的方法是使用菜单,菜单通常显示在桌面应用程序的标题栏下。在 Tkinter 中,这些菜单由一个小部件类表示;稍后我们将深入探讨它的方法以及如何将它与应用程序的其余部分集成。

显示警报对话框

对话框的一个常见用例是通知用户应用程序中发生的事件,例如记录已保存或无法打开文件。现在我们来看看 Tkinter 中显示信息对话框的一些基本功能。

准备

我们的程序将有三个按钮,每一个按钮都用一个静态标题和消息显示一个不同的对话框。此类对话框只有一个按钮用于确认和关闭对话框:

运行上述示例时,请注意,每个对话框都会播放平台定义的相应声音,并且按钮标签会转换为您的语言:

怎么做。。。

前面准备部分提到的三个对话框是通过tkinter.messagebox模块的showinfoshowwarningshowerror功能打开的:

import tkinter as tk
import tkinter.messagebox as mb

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        btn_info = tk.Button(self, text="Show Info",
                             command=self.show_info)
        btn_warn = tk.Button(self, text="Show Warning",
                             command=self.show_warning)
        btn_error = tk.Button(self, text="Show Error",
                              command=self.show_error)

        opts = {'padx': 40, 'pady': 5, 'expand': True, 'fill': tk.BOTH}
        btn_info.pack(**opts)
        btn_warn.pack(**opts)
        btn_error.pack(**opts)

    def show_info(self):
        msg = "Your user preferences have been saved"
        mb.showinfo("Information", msg)

    def show_warning(self):
        msg = "Temporary files have not been correctly removed"
        mb.showwarning("Warning", msg)

    def show_error(self):
        msg = "The application has encountered an unknown error"
        mb.showerror("Error", msg)

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

它是如何工作的。。。

首先,我们导入了别名较短的mb模块tkinter.messagebox。这个模块在 Python2 中被命名为tkMessageBox,所以这个语法也帮助我们在一条语句中隔离兼容性问题。

根据通知用户的信息类型,通常使用每个对话框:

  • showinfo:操作成功完成
  • showwarning:操作已完成,但出现了不符合预期的情况
  • showerror:由于错误,操作失败

这三个函数接收两个字符串作为输入参数:第一个字符串显示在标题栏上,第二个字符串对应于对话框显示的消息。

通过添加新行字符\n,对话框消息也可以跨多行生成。

请求用户确认

Tkinter 中包含的其他类型的对话框是用于请求用户确认的对话框,例如,当我们要保存文件并准备覆盖具有相同名称的现有对话框时显示的对话框。

这些对话框与前面的对话框不同,因为函数返回的值取决于用户单击的确认按钮。这样,我们可以与程序交互以指示是继续还是取消操作。

准备

在本配方中,我们将介绍tkinter.messagebox模块中定义的其余对话框功能。每个按钮都标有单击时打开的对话框类型:

由于这些对话框之间存在一些差异,您可以尝试使用它们,看看哪一个更适合您在各种情况下的需要:

怎么做。。。

正如我们在前面的示例中所做的,我们将使用import ... as语法导入tkinter.messagebox,并使用titlemessage调用每个函数:

import tkinter as tk
import tkinter.messagebox as mb

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.create_button(mb.askyesno, "Ask Yes/No",
                           "Returns True or False")
        self.create_button(mb.askquestion, "Ask a question",
                           "Returns 'yes' or 'no'")
        self.create_button(mb.askokcancel, "Ask Ok/Cancel",
                           "Returns True or False")
        self.create_button(mb.askretrycancel, "Ask Retry/Cancel",
                           "Returns True or False")
        self.create_button(mb.askyesnocancel, "Ask Yes/No/Cancel",
                           "Returns True, False or None")

    def create_button(self, dialog, title, message):
        command = lambda: print(dialog(title, message))
        btn = tk.Button(self, text=title, command=command)
        btn.pack(padx=40, pady=5, expand=True, fill=tk.BOTH)

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

它是如何工作的。。。

为了避免重复按钮实例化和回调方法的代码,我们定义了一个create_button方法,在需要添加所有按钮及其对话框时重复使用它。这些命令只是打印作为参数传递的dialog函数的结果,这样我们就可以看到返回的值,具体取决于单击的按钮,以回答对话框。

选择文件和目录

文件对话框允许用户从文件系统中选择一个或多个文件。在 Tkinter 中,这些函数在tkinter.filedialog模块中声明,该模块还包括用于选择目录的对话框。它还允许您自定义新对话框的行为,例如按扩展名筛选文件或选择对话框显示的初始目录。

准备

我们的应用程序将包含两个按钮。第一个将标记为“选择文件”,它将显示一个选择文件的对话框。默认情况下,仅显示扩展名为.txt的文件:

第二个按钮是选择目录,它将打开一个类似的对话框来选择目录:

这两个按钮都将打印所选文件或目录的完整路径,如果取消对话框,则不会执行任何操作。

怎么做。。。

我们应用程序的第一个按钮将触发对askopenfilename函数的调用,而第二个按钮将调用askdirectory函数:

import tkinter as tk
import tkinter.filedialog as fd

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        btn_file = tk.Button(self, text="Choose file",
                             command=self.choose_file)
        btn_dir = tk.Button(self, text="Choose directory",
                             command=self.choose_directory)
        btn_file.pack(padx=60, pady=10)
        btn_dir.pack(padx=60, pady=10)

    def choose_file(self):
        filetypes = (("Plain text files", "*.txt"),
                     ("Images", "*.jpg *.gif *.png"),
                     ("All files", "*"))
        filename = fd.askopenfilename(title="Open file", 
                   initialdir="/", filetypes=filetypes)
        if filename:
            print(filename)

    def choose_directory(self):
        directory = fd.askdirectory(title="Open directory", 
                                    initialdir="/")
        if directory:
            print(directory)

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

由于可以取消这些对话框,因此我们添加了条件语句,以在将对话框打印到控制台之前检查对话框函数是否返回非空字符串。在任何必须使用此路径执行操作(例如读取或复制文件或更改权限)的应用程序中,我们都需要此验证。

它是如何工作的。。。

我们使用askopenfilename函数创建第一个对话框,该函数返回一个字符串,表示所选文件的完整路径。它接受以下可选参数:

  • title:对话框标题栏中显示的标题。
  • initialdir:初始目录。
  • filetypes:两个字符串的元组序列。第一个是以人类可读的格式指示文件类型的标签,而第二个是匹配文件名的模式。
  • multiple:布尔值,表示用户是否可以选择多个文件。
  • defaultextension:如果未明确给出扩展名,则将其添加到文件名中。

在我们的示例中,我们将初始目录设置为根文件夹和自定义标题。在我们的文件类型元组中,我们有以下三个有效的选择:使用.txt扩展名保存的文本文件;带有.jpg.gif.png扩展名的图像;以及匹配所有文件的通配符("*"

请注意,这些模式不一定与文件中包含的数据格式匹配,因为可以使用不同的扩展名重命名文件:

filetypes = (("Plain text files", "*.txt"),
             ("Images", "*.jpg *.gif *.png"),
             ("All files", "*"))
filename = fd.askopenfilename(title="Open file", initialdir="/",
                              filetypes=filetypes)

askdirectory函数还采用titleinitialdir参数,并使用mustexist布尔选项指示用户是否必须选择现有目录:

directory = fd.askdirectory(title="Open directory", initialdir="/")

还有更多。。。

tkinter.filedialog模块包括这些函数的一些变体,允许您直接检索文件对象。

例如,askopenfile返回所选文件对应的文件对象,而不必使用askopenfilename返回的路径调用open。在调用文件方法之前,我们仍然需要检查对话框是否未被取消:

import tkinter.filedialog as fd

filetypes = (("Plain text files", "*.txt"),)
my_file = fd.askopenfile(title="Open file", filetypes=filetypes)
if my_file:
    print(my_file.readlines())
    my_file.close()

将数据保存到文件中

除了选择现有文件和目录外,还可以使用 Tkinter 对话框创建新文件。它们可以用来持久化应用程序生成的数据,让用户选择新文件的名称和位置。

准备

我们将使用“保存文件”对话框将文本小部件的内容写入纯文本文件:

怎么做。。。

要打开保存文件的对话框,我们从tkinter.filedialog模块调用asksaveasfile函数。它在内部创建一个文件对象,该文件对象采用'w'模式进行写入,如果对话框被取消,则为None

import tkinter as tk
import tkinter.filedialog as fd

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.text = tk.Text(self, height=10, width=50)
        self.btn_save = tk.Button(self, text="Save",
                                  command=self.save_file)

        self.text.pack()
        self.btn_save.pack(pady=10, ipadx=5)

    def save_file(self):
        contents = self.text.get(1.0, tk.END)
        new_file = fd.asksaveasfile(title="Save file",
                                    defaultextension=".txt",
                                    filetypes=(("Text files", 
                                                "*.txt"),))
        if new_file:
            new_file.write(contents)
            new_file.close()

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

它是如何工作的。。。

asksaveasfile函数接受与askopenfile函数相同的可选参数,但也允许您默认使用defaultextension选项添加文件扩展名。

为了防止用户意外覆盖以前的文件,如果您试图使用与现有文件相同的名称保存新文件,此对话框将自动发出警告。

使用 file 对象,我们可以编写文本小部件的内容,请始终记住关闭文件以释放对象占用的资源:

contents = self.text.get(1.0, tk.END)
new_file.write(contents)
new_file.close()

还有更多。。。

在前面的配方中,我们看到有一个相当于askopenfilename的函数,它返回一个文件对象,而不是一个名为askopenfile的字符串。

为了保存文件,还有一个asksaveasfilename函数返回所选文件的路径。如果要在打开文件进行写入之前修改路径或执行任何验证,可以使用此函数。

另见

  • 选择文件和目录配方

创建菜单栏

复杂的 GUI 通常使用菜单栏来组织应用程序中可用的操作和导航。此模式还用于对密切相关的操作进行分组,例如大多数文本编辑器中包含的文件菜单。

Tkinter 本机支持这些菜单,这些菜单以目标桌面环境的外观显示。因此,您不必使用框架或标签来模拟它们,因为这样会丢失 Tkinter 中已经内置的跨平台功能。

准备

首先,我们将通过一个嵌套的下拉菜单向根窗口添加一个菜单栏。在 Windows 10 上,显示如下:

怎么做。。。

Tkinter 有一个Menu小部件类,可用于多种菜单,包括顶部菜单栏。与任何其他小部件类一样,菜单以父容器作为第一个参数和一些可选配置选项进行实例化:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        menu = tk.Menu(self)
        file_menu = tk.Menu(menu, tearoff=0)

        file_menu.add_command(label="New file")
        file_menu.add_command(label="Open")
        file_menu.add_separator()
        file_menu.add_command(label="Save")
        file_menu.add_command(label="Save as...")

        menu.add_cascade(label="File", menu=file_menu)
        menu.add_command(label="About")
        menu.add_command(label="Quit", command=self.destroy)
        self.config(menu=menu)

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

如果您运行前面的脚本,您可以看到File条目显示二级菜单,您可以通过点击Quit菜单按钮关闭应用程序。

它是如何工作的。。。

首先,我们实例化每个菜单,指示父容器。tearoff选项默认设置为1,表示可以通过点击菜单上边框的虚线来分离菜单。此行为不适用于顶部菜单栏,但如果我们想停用此功能,必须将此选项设置为0

    def __init__(self):
        super().__init__()
        menu = tk.Menu(self)
        file_menu = tk.Menu(menu, tearoff=0)

菜单项的排列顺序与添加顺序相同,使用add_commandadd_separatoradd_cascade方法:

menu.add_cascade(label="File", menu=file_menu)
menu.add_command(label="About")
menu.add_command(label="Quit", command=self.destroy)

通常,add_command是通过command选项调用的,这是单击条目时调用的回调。没有传递给回调函数的参数,与按钮小部件的command选项完全相同。

出于说明目的,我们仅将此选项添加到Quit条目中,以销毁Tk实例并关闭应用程序。

最后,我们通过调用self.config(menu=menu)将菜单附加到顶层窗口。请注意,每个顶级窗口只能配置一个菜单栏。

在菜单中使用变量

除了调用命令和嵌套子菜单外,还可以将 Tkinter 变量连接到菜单项。

准备

我们将在选项子菜单中添加一个复选按钮条目和三个单选按钮条目,并用分隔符分隔。将有两个底层 Tkinter 变量来存储选定的值,因此我们可以从应用程序的其他方法轻松检索它们:

怎么做。。。

这些类型的条目是通过Menu小部件类的add_checkbuttonadd_radiobutton方法添加的。与常规单选按钮一样,所有按钮都连接到同一 Tkinter 变量,但每个按钮都设置不同的值:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.checked = tk.BooleanVar()
        self.checked.trace("w", self.mark_checked)
        self.radio = tk.StringVar()
        self.radio.set("1")
        self.radio.trace("w", self.mark_radio)

        menu = tk.Menu(self)
        submenu = tk.Menu(menu, tearoff=0)

        submenu.add_checkbutton(label="Checkbutton", onvalue=True,
                                offvalue=False, variable=self.checked)
        submenu.add_separator()
        submenu.add_radiobutton(label="Radio 1", value="1",
                                variable=self.radio)
        submenu.add_radiobutton(label="Radio 2", value="2",
                                variable=self.radio)
        submenu.add_radiobutton(label="Radio 3", value="3",
                                variable=self.radio)

        menu.add_cascade(label="Options", menu=submenu)
        menu.add_command(label="Quit", command=self.destroy)
        self.config(menu=menu)

    def mark_checked(self, *args):
        print(self.checked.get())

    def mark_radio(self, *args):
        print(self.radio.get())

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

此外,我们正在跟踪变量更改,以便在运行此应用程序时可以看到控制台上打印的值。

它是如何工作的。。。

要将布尔变量连接到Checkbutton条目,我们首先定义BooleanVar,然后使用variable选项调用add_checkbutton来创建条目。

记住,onvalueoffvalue选项应该与 Tkinter 变量的类型相匹配,就像我们对常规 RadioButton 和 CheckButton 小部件所做的那样:

self.checked = tk.BooleanVar()
self.checked.trace("w", self.mark_checked)
# ...
submenu.add_checkbutton(label="Checkbutton", onvalue=True,
                        offvalue=False, variable=self.checked)

使用add_radiobutton方法以类似的方式创建Radiobutton条目,并且只有一个value选项可在单击收音机时设置 Tkinter 变量。由于StringVar最初持有空字符串值,我们将其设置为第一个单选值,以便显示为选中状态:

self.radio = tk.StringVar()
self.radio.set("1")
self.radio.trace("w", self.mark_radio)
# ...        
submenu.add_radiobutton(label="Radio 1", value="1",
                        variable=self.radio)
submenu.add_radiobutton(label="Radio 2", value="2",
                        variable=self.radio)
submenu.add_radiobutton(label="Radio 3", value="3",
                        variable=self.radio)

这两个变量都使用mark_checkedmark_radio方法跟踪变化,这两种方法只是将变量值打印到控制台中。

显示上下文菜单

Tkinter 菜单不一定必须位于菜单栏上,但它们实际上可以自由放置在任何坐标上。这些类型的菜单称为上下文菜单,通常在用户右键单击某个项目时显示。

在 GUI 应用程序中广泛使用上下文菜单;例如,文件浏览器显示它们以提供对所选文件的可用操作,因此用户可以直观地了解如何与它们交互。

准备

我们将为文本小部件构建上下文菜单,以显示文本编辑器的一些常见操作,例如剪切、复制、粘贴和删除:

怎么做。。。

您可以使用post方法显式放置菜单实例,而不是将顶级容器配置为顶级菜单栏。

菜单项中的所有命令都调用使用文本实例检索当前选择或插入位置的方法:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.menu = tk.Menu(self, tearoff=0)
        self.menu.add_command(label="Cut", command=self.cut_text)
        self.menu.add_command(label="Copy", command=self.copy_text)
        self.menu.add_command(label="Paste", command=self.paste_text)
        self.menu.add_command(label="Delete", command=self.delete_text)

        self.text = tk.Text(self, height=10, width=50)
        self.text.bind("<Button-3>", self.show_popup)
        self.text.pack()

    def show_popup(self, event):
        self.menu.post(event.x_root, event.y_root)

    def cut_text(self):
        self.copy_text()
        self.delete_text()

    def copy_text(self):
        selection = self.text.tag_ranges(tk.SEL)
        if selection:
            self.clipboard_clear()
            self.clipboard_append(self.text.get(*selection))

    def paste_text(self):
        self.text.insert(tk.INSERT, self.clipboard_get())

    def delete_text(self):
        selection = self.text.tag_ranges(tk.SEL)
        if selection:
            self.text.delete(*selection)

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

它是如何工作的。。。

我们将右键单击事件绑定到文本实例的show_popup处理程序,该处理程序显示菜单,其左上角位于单击位置。每次触发此事件时,都会再次显示相同的菜单实例:

def show_popup(self, event):
    self.menu.post(event.x_root, event.y_root)

以下方法可用于所有小部件类与剪贴板交互:

  • clipboard_clear():从剪贴板中清除数据
  • clipboard_append(string):将字符串追加到剪贴板
  • clipboard_get():从剪贴板返回数据

复制操作的回调方法获取当前选择并将其添加到剪贴板:

    def copy_text(self):
        selection = self.text.tag_ranges(tk.SEL)
        if selection:
            self.clipboard_clear()
 self.clipboard_append(self.text.get(*selection))

粘贴操作将剪贴板内容插入到由INSERT索引定义的插入光标位置。我们必须将其包装在一个try...except块中,因为如果剪贴板为空,调用clipboard_get将引发一个TclError

    def paste_text(self):
        try:
 self.text.insert(tk.INSERT, self.clipboard_get())
        except tk.TclError:
            pass

删除操作不会与剪贴板交互,但会删除当前选择的内容:

    def delete_text(self):
        selection = self.text.tag_ranges(tk.SEL)
        if selection:
            self.text.delete(*selection)

由于 cut 操作是 copy 和 delete 的组合,因此我们重用了这些方法来编写其回调函数。

还有更多。。。

postcommand选项允许您在每次使用post方法显示菜单时重新配置菜单。为了说明如何使用此选项,如果文本小部件中没有当前选择,我们将禁用剪切、复制和删除条目,如果剪贴板中没有内容,则禁用粘贴条目。

与其他回调函数一样,我们传递对类的方法的引用以添加此配置选项:

def __init__(self):
    super().__init__()
    self.menu = tk.Menu(self, tearoff=0, 
    postcommand=self.enable_selection)

然后,我们检查SEL范围是否存在,以确定条目的状态应该是ACTIVE还是DISABLED。该值被传递给entryconfig方法,该方法将要配置的条目的索引作为其第一个参数,并且要更新的选项列表请记住,菜单条目是0索引的:

def enable_selection(self):
    state_selection = tk.ACTIVE if self.text.tag_ranges(tk.SEL) 
                      else tk.DISABLED
    state_clipboard = tk.ACTIVE
    try:
        self.clipboard_get()
    except tk.TclError:
        state_clipboard = tk.DISABLED

    self.menu.entryconfig(0, state=state_selection) # Cut
    self.menu.entryconfig(1, state=state_selection) # Copy
    self.menu.entryconfig(2, state=state_clipboard) # Paste
    self.menu.entryconfig(3, state=state_selection) # Delete

例如,如果没有选择或剪贴板上没有内容,则所有条目都应灰显:

通过entryconfig,还可以配置许多其他选项,例如标签、字体和背景。参见https://www.tcl.tk/man/tcl8.6/TkCmd/menu.htm#M48 获取可用进入选项的完整参考。

打开辅助窗口

Tk实例代表 GUI 的主窗口,当它被销毁,应用程序退出,事件 mainloop 完成时。

然而,还有另一个 Tkinter 类在我们的应用程序中创建额外的顶级窗口,称为Toplevel。您可以使用这个类来显示任何类型的窗口,从自定义对话框到向导窗体。

准备

我们将首先创建一个简单的窗口,当单击主窗口的按钮时打开该窗口。它将包含一个按钮,用于关闭它并将焦点返回到主窗口:

怎么做。。。

Toplevel小部件类创建了一个新的顶级窗口,它与Tk实例一样充当父容器。与Tk类不同,您可以根据需要实例化任意多个顶级窗口:

import tkinter as tk

class Window(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        self.label = tk.Label(self, text="This is another window")
        self.button = tk.Button(self, text="Close", 
                                command=self.destroy)

        self.label.pack(padx=20, pady=20)
        self.button.pack(pady=5, ipadx=2, ipady=2)

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.btn = tk.Button(self, text="Open new window",
                             command=self.open_window)
        self.btn.pack(padx=50, pady=20)

    def open_window(self):
        window = Window(self)
        window.grab_set()

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

它是如何工作的。。。

我们定义一个Toplevel子类来表示我们的自定义窗口,它与父窗口的关系在其__init__方法中定义。小部件像往常一样添加到此窗口,因为我们遵循与子类Tk相同的约定:

class Window(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)

窗口是通过创建一个新实例打开的,但是为了让它接收所有事件,我们必须调用它的grab_set方法。这将阻止用户与主窗口交互,直到此窗口关闭:

def open_window(self):
    window = Window(self)
 window.grab_set()

处理窗口删除

在某些情况下,您可能希望在用户关闭顶级窗口之前执行操作,例如,以防止丢失未保存的工作。Tkinter 允许您拦截此类事件以有条件地破坏窗口。

准备

我们将重用前面配方中的App类,并修改Window类,使其显示确认关闭窗口的对话框:

怎么做。。。

在 Tkinter 中,我们可以通过注册WM_DELETE_WINDOW协议的处理函数来检测窗口何时即将关闭。在大多数桌面环境中,单击标题栏的 X 按钮可以触发此操作:

import tkinter as tk
import tkinter.messagebox as mb

class Window(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        self.protocol("WM_DELETE_WINDOW", self.confirm_delete)

        self.label = tk.Label(self, text="This is another window")
        self.button = tk.Button(self, text="Close", 
                                command=self.destroy)

        self.label.pack(padx=20, pady=20)
        self.button.pack(pady=5, ipadx=2, ipady=2)

    def confirm_delete(self):
        message = "Are you sure you want to close this window?"
        if mb.askyesno(message=message, parent=self):
            self.destroy()

我们的 handler 方法显示一个对话框来确认窗口删除。在更复杂的程序中,此逻辑通常通过附加验证进行扩展。

它是如何工作的。。。

bind()方法用于注册小部件事件的处理程序,而protocol方法用于窗口管理器协议。

当顶级窗口即将关闭时,WM_DELETE_WINDOW处理程序被调用,默认情况下,Tk销毁接收到它的窗口。由于我们通过注册confirm_delete处理程序来覆盖此行为,因此如果对话框被确认,它需要显式销毁窗口。

另一个有用的协议是WM_TAKE_FOCUS,当窗口获取焦点时调用它。

还有更多。。。

请记住,为了在显示对话框时保持第二个窗口的焦点,我们必须将对顶层实例的引用parent选项传递给对话框函数:

if mb.askyesno(message=message, parent=self):
    self.destroy()

否则,该对话框将根窗口作为其父窗口,您将看到它在第二个窗口上弹出。这些怪癖可能会让您的用户感到困惑,因此正确设置每个顶级实例或对话框的父窗口是一个很好的做法。

在窗口之间传递变量

在程序执行期间,两个不同的窗口可能需要共享信息。虽然这些数据可能会保存到磁盘上,并从使用它的窗口读取,但在某些情况下,在内存中处理这些数据更简单,只需将这些信息作为变量传递即可。

准备

主窗口将包含三个单选按钮,用于选择要创建的用户类型,次窗口将打开表单以填写用户数据:

怎么做。。。

为了保存用户数据,我们使用表示每个用户实例的字段创建了namedtuplecollections模块的此函数接收类型名和字段名序列,并返回一个元组子类,以创建具有给定字段的轻量级对象:

import tkinter as tk
from collections import namedtuple

User = namedtuple("User", ["username", "password", "user_type"])

class UserForm(tk.Toplevel):
    def __init__(self, parent, user_type):
        super().__init__(parent)
        self.username = tk.StringVar()
        self.password = tk.StringVar()
        self.user_type = user_type

        label = tk.Label(self, text="Create a new " + 
                         user_type.lower())
        entry_name = tk.Entry(self, textvariable=self.username)
        entry_pass = tk.Entry(self, textvariable=self.password, 
                              show="*")
        btn = tk.Button(self, text="Submit", command=self.destroy)

        label.grid(row=0, columnspan=2)
        tk.Label(self, text="Username:").grid(row=1, column=0)
        tk.Label(self, text="Password:").grid(row=2, column=0)
        entry_name.grid(row=1, column=1)
        entry_pass.grid(row=2, column=1)
        btn.grid(row=3, columnspan=2)

    def open(self):
        self.grab_set()
        self.wait_window()
        username = self.username.get()
        password = self.password.get()
        return User(username, password, self.user_type)

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        user_types = ("Administrator", "Supervisor", "Regular user")
        self.user_type = tk.StringVar()
        self.user_type.set(user_types[0])

        label = tk.Label(self, text="Please, select the type of user")
        radios = [tk.Radiobutton(self, text=t, value=t, \
                  variable=self.user_type) for t in user_types]
        btn = tk.Button(self, text="Create user", 
                        command=self.open_window)

        label.pack(padx=10, pady=10)
        for radio in radios:
            radio.pack(padx=10, anchor=tk.W)
        btn.pack(pady=10)

    def open_window(self):
        window = UserForm(self, self.user_type.get())
        user = window.open()
        print(user)

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

当执行流返回主窗口时,用户数据将打印到控制台。

它是如何工作的。。。

这个配方的大部分代码已经在其他配方中介绍过了,主要区别在于UserForm类的open()方法,我们将调用移到了grab_set()。然而,wait_window()方法实际上是停止执行并阻止我们在表单修改之前返回数据的方法:

    def open(self):
 self.grab_set()
 self.wait_window()
        username = self.username.get()
        password = self.password.get()
        return User(username, password, self.user_type)

值得注意的是,wait_window()进入一个本地事件循环,该循环在窗口被破坏时结束。虽然可以传递要等待删除的小部件,但可以忽略它以隐式引用调用此方法的实例。

UserForm实例被销毁时,open()方法继续执行,返回App类中现在可以使用的User对象:

    def open_window(self):
        window = UserForm(self, self.user_type.get())
        user = window.open()
        print(user)