菜单处理#

本章将介绍如何在 Tk 中处理菜单栏和弹出菜单。为了打造完善的应用程序,这些是你需要特别注意的部分。如果你想让你的应用程序与用户平台上的其他应用程序保持一致,菜单需要特别关注。

说到这,推荐的方式来确定你正在运行的平台是:

root.tk.call('tk', 'windowingsystem')    # 返回 x11, win32 或 aqua

备注

Tkinter 没有提供这个调用的直接等价物。然而,通过使用 call() 方法(在任何 Tkinter 小部件上都可用),可以直接执行任意基于 Tcl 的 Tk 命令。在这里,我们调用了 Tcl/Tk 命令 tk windowingsystem

小技巧

这比检查全局变量如 tcl_platformsys.platform 更有用;过去使用这些方法的检查应该被重新审视。虽然平台和窗口系统之间曾经有很强的相关性,但今天这种关系不那么明显了。例如,如果你的平台被识别为 Unix,那可能意味着在 X11 下的 Linux、在 Aqua 下的 macOS,甚至是在 X11 下的 macOS。

菜单栏#

在这一部分,我们将探讨菜单栏:如何创建它们,里面放什么,如何使用等等。

正确设计一个菜单栏及其一组菜单超出了本教程的范围。然而,如果你是为了除了自己之外的其他人创建一个应用程序,这里有一些建议。首先,如果你发现自己有太多的菜单、非常长的菜单或深度嵌套的菜单,你可能需要重新考虑你的用户界面是如何组织的。其次,许多人使用菜单来探索程序能做什么,特别是在他们刚开始学习时,因此请确保主要功能可以通过菜单访问。最后,对于你目标的每个平台,熟悉应用程序如何使用菜单。查阅该平台的人类接口指南,以获取关于设计、术语、快捷键等更多细节的完整信息。这是一个你可能需要在每个平台上进行定制的区域。

备注

您可能会注意到,在一些最近的Linux发行版上,活动状态下的应用程序会将菜单显示在屏幕顶部,而不是在窗口本身内。目前,Tk还不支持这种风格的菜单。

菜单小部件与层级结构#

在Tk中,菜单被实现为小部件,就像按钮和输入框一样。每个菜单小部件由许多不同的菜单项组成。这些项具有各种属性,例如显示的文本、键盘快捷键以及要调用的命令。

菜单是按层级结构排列的。菜单栏本身就是一个菜单小部件。它有几个项目(“文件”、“编辑”等),每个都是包含更多项目的子菜单。这些项目可以包括像“文件”菜单中的“打开…”命令,也可以包括其他项目之间的分隔符。它甚至可以有打开自己子菜单的项目(所谓的级联菜单)。正如你在Tk中已经看到的其他东西所期望的那样,每当你有子菜单时,它必须作为其父菜单的子元素创建。

菜单是经典Tk小部件的一部分;在主题化的Tk小部件集中没有菜单小部件。

在您开始之前#

在您着手创建菜单之前,务必在应用程序的某个位置添加以下代码行。

root.option_add('*tearOff', FALSE)

若未进行修改,您在 Windows 和 X11 上的每个菜单都会以类似虚线的形式开始,并允许您“撕下”菜单,使其在自己的窗口中显示。您应该从应用程序中删除这种可撕下菜单,因为它们不属于任何现代用户界面风格的一部分。

图1 没有设置  的图示

备注

这是对 Motif 风格的 X11 的一种怀旧,Tk 的原始外观和感觉正是基于此。除非您的应用程序仅设计用于运行在地下室里那台积满灰尘的旧机器上,否则应该去除它们。期待未来的 Tk 版本能移除这种误导性地向后兼容性致敬的功能。

在讨论古代历史的同时,option_add 使用选项数据库。这提供了一种通过基于文本的配置文件来定制X11用户界面某些方面的标准化方式,但今天已不再使用。较旧的Tk程序可能会内部使用 option 命令来将样式配置选项与小部件创建代码分开。这种方法早于主题化的Tk样式,而如今应使用主题化Tk样式来完成这一目的。然而,使用过时的选项数据库来自动移除过时的可撕下菜单,不知怎的,似乎颇为合适。

先载入可能用到的包:

from tkinter import ttk, Tk, Listbox, Menu
from tkinter import messagebox
from tkinter import IntVar, StringVar, BooleanVar

root = Tk()
root.geometry('200x100')
root.option_add('*tearOff', False)

创建菜单栏#

在 Tk 中,菜单栏是与单个窗口关联的;每个顶级窗口最多可以有一个菜单栏。这一点在 Windows 和许多 X11 系统上视觉上很明显,其中菜单是每个窗口的一部分,位于标题栏正下方。

然而,在 macOS 上,屏幕顶部有单一的菜单栏,由每个窗口共享。就你的 Tk 程序而言,每个窗口仍然有自己的菜单栏。当你在不同窗口之间切换时,Tk 确保显示正确的菜单栏。如果你没有为特定窗口指定菜单栏,Tk 会使用与根窗口关联的菜单栏;现在你可能已经注意到,当你的 Tk 应用程序启动时,这会自动为你创建。

因为在 macOS 上所有窗口都有一个菜单栏,所以定义菜单栏很重要,要么为每个窗口定义,要么为根窗口定义备用菜单栏。否则,你最终会得到“内置”的菜单栏,它包含仅用于直接输入解释器的菜单。

为了给窗口创建一个菜单栏,首先创建一个菜单小部件。然后,使用窗口的菜单配置选项将菜单小部件附加到窗口。

win = Toplevel(root)
# 创建菜单栏
menubar = Menu(win) 
win['menu'] = menubar # 或者 win.config(menu=menubar)

备注

你可以为多个窗口使用相同的菜单栏。换句话说,你可以将同一个菜单栏指定为几个顶级窗口的菜单配置选项。这在 Windows 和 X11 上特别有用,你可能希望一个窗口包含一个菜单,但不一定需要在应用程序中处理不同的菜单。然而,如果菜单项的内容或状态依赖于活动窗口中的情况,你必须自己管理这一点。

小技巧

这是真正的古代历史了,但菜单栏过去是通过创建一个包含菜单项的框架小部件并将其像任何其他小部件一样打包到窗口顶部来实现的。希望你没有任何代码或文档仍在这样做。

添加菜单#

我们现在有了菜单栏,但没有一些菜单放进去它是相当无用的。因此,将再次为菜单栏创建菜单小部件,每个都是菜单栏的子项。然后我们将它们全部添加到菜单栏中。

menubar = Menu(parent)
menu_file = Menu(menubar)
menu_edit = Menu(menubar)
menubar.add_cascade(menu=menu_file, label='File')
menubar.add_cascade(menu=menu_edit, label='Edit')

备注

add_cascade 方法添加菜单项,本身是菜单(子菜单)。

添加菜单项#

现在我们的菜单栏中已经有了几个菜单,我们可以向每个菜单添加一些项目。

命令项#

在 Tk 中,常规菜单项被称为 command 项。稍后会看到其他类型的菜单项。请注意,菜单项是菜单的一部分;不必为每个菜单项(子菜单除外)创建单独的菜单小部件。

menu_file.add_command(label='New', command=newFile)
menu_file.add_command(label='Open...', command=openFile)
menu_file.add_command(label='Close', command=closeFile)

备注

省略号(“…”)在 macOS 上是特殊字符,比三个连续的点更紧密地排列。Tk 会自动为你替换这个字符。

每个菜单项都与一些配置选项相关联,类似于小部件的配置选项。每种类型的菜单项都有一组不同的可用选项。级联菜单项有一个用于指定子菜单的 menu 选项,命令菜单项有一个用于指定选择该项时调用的命令的 command 选项。两者都有 label 选项来指定显示项目的文本。

子菜单#

已经看到了使用 cascade 菜单项将菜单添加到菜单栏的方法。毫不奇怪,如果你想在现有的菜单中添加子菜单,你也是用完全相同的方式使用 cascade 菜单项。例如,你可以用它来构建“最近文件”子菜单。

menu_recent = Menu(menu_file)
menu_file.add_cascade(menu=menu_recent, label='Open Recent')
for f in recent_files:
    menu_recent.add_command(label=os.path.basename(f), command=lambda f=f: openFile(f))

分隔符#

第三种类型的菜单项是 separator,它产生你通常在不同类型的菜单项之间看到的分隔线。

menu_file.add_separator()

复选框和单选按钮项#

最后,还有 checkbuttonradiobutton 菜单项,它们的行为类似于复选框和小部件。这些菜单项与一个变量相关联。根据它的值,一个指示器(即复选标记或选中的单选按钮)可能会显示在其标签旁边。

check = StringVar()
menu_file.add_checkbutton(label='Check', variable=check, onvalue=1, offvalue=0)
radio = StringVar()
menu_file.add_radiobutton(label='One', variable=radio, value=1)
menu_file.add_radiobutton(label='Two', variable=radio, value=2)

当用户选择一个未选中的复选框项时,它会将关联的变量设置为 onvalue 中的值。选择一个已经选中的项会将其设置为 offvalue 中的值。选择一个单选按钮项会将关联的变量设置为 value 中的值。这两种类型的项也会对任何你对关联变量所做的更改做出反应。

像命令项一样,复选框和单选按钮菜单项支持 command 配置选项,该选项在选择菜单项时调用。在回调被调用之前,关联的变量和菜单项的状态会被更新。

备注

单选按钮菜单项不是Windows或macOS人机界面指南的一部分。在这些平台上,该项的指示器是一个复选标记,就像复选框项一样。语义仍然有效。这是一种在多个项目之间进行选择的好方法,因为它会显示其中一个被选中(被勾选)。

操作菜单项#

除了在菜单末尾添加项目外,您还可以通过 insert index type ?option value...? 方法在菜单中间插入项目;这里 index 是您希望在其之前插入的项目的位置(0…n-1)。您也可以使用 delete index ?endidx? 方法删除一个或多个菜单项。

menu_recent.delete(0, 'end')

与Tk中的大多数内容一样,您可以在任何时候查看或更改项目的选项值。项目是通过索引引用的。通常,这是一个数字(0…n-1),表示项目中的位置。您还可以指定菜单项的标签(或者,实际上,是一个“glob-style”模式来匹配项目的标签)。

print(menu_file.entrycget(0, 'label'))  # 获取菜单顶部条目的标签
print(menu_file.entryconfigure(0))      # 显示某个项目的选项

状态#

您可以禁用菜单项,使用户无法选择它。这可以通过将 state 选项设置为 disabled 值来实现。使用 normal 值重新启用该项目。

菜单应始终反映应用程序的当前状态。如果菜单项当前不相关(例如,只有在应用程序中选择了某些内容时,“复制”项才适用),则应将其禁用。当应用程序状态发生变化,使该项适用时,请确保将其启用。

menu_file.entryconfigure('Close', state=DISABLED)

有时,您可能有一些菜单项的名称会随着应用程序状态的变化而变化,而不是禁用菜单项。例如,一个网络浏览器可能会有一个菜单项,在书签窗格隐藏或显示时在“显示书签”和“隐藏书签”之间切换。

menu_bookmarks.entryconfigure(3, label="Hide Bookmarks")

备注

随着程序变得越来越复杂,很容易错过启用或禁用某些项目。一种策略是将所有菜单状态更改集中在一个例程中。每当应用程序状态发生变化时,它应该调用此例程。它应该检查当前状态并相应地更新菜单。相同的代码还可以处理工具栏、状态栏或其他用户界面组件。

快捷键#

accelerator 选项用于指示与菜单项对应的键盘等效键。这不会实际创建加速器,而只是在菜单项旁边显示它。您仍然需要自己创建加速器的事件绑定。

备注

记住,事件绑定可以设置在单个小部件上,特定类型的所有小部件上,包含您感兴趣的小部件的顶级窗口上,或者整个应用程序上。由于菜单栏与单个窗口相关联,因此菜单项的事件绑定通常会在关联的顶级窗口上。

快捷键非常具体于平台,不仅在于哪些键用于什么操作,还取决于用于菜单加速器的修饰键(例如,在macOS上是“Command”键,在Windows和X11上通常是“Control”键)。有效的加速器选项示例包括Command-N、Shift+Ctrl+X和Command-Option-B。常用的修饰符包括Control、Ctrl、Option、Opt、Alt、Shift、"Command, Cmd, and Meta。

备注

在macOS上,修饰符名称会自动映射到出现在菜单中的不同修饰符图标,即Shift ⇒ ⇧,Command ⇒ ⌘,Control ⇒ ⌃,Option ⇒ ⌥。

m_edit.entryconfigure('Paste', accelerator='Command+V')

下划线#

所有平台都支持通过箭头键遍历菜单栏。在Windows和X11上,您还可以使用其他键跳转到特定的菜单或菜单项。触发这些跳转的键在菜单项标签中用下划线表示。要为菜单项添加一个这样的下划线,请使用该项的 underline 配置选项。其值应该是您希望加下划线的字符的索引(从0到字符串长度减1)。与加速器键不同,菜单将监视按键,因此不需要单独的事件绑定。

m.add_command(label='Path Browser', underline=5)  # 下划线 "B"

图像#

您还可以在菜单项中使用图像,无论是在菜单项标签旁边还是在替换标签的情况下。为此,请使用 imagecompound 选项,它们的工作方式与标签小部件相同。image 的值必须是 Tk 图像对象,而 compound 可以有 bottomcenterleftrighttopnone 的值。

虚拟事件菜单#

菜单的平台惯例建议大多数应用程序中应提供标准菜单和项。例如,大多数应用程序都有一个“编辑”菜单,菜单项包括“复制”、“粘贴”等。Tk小部件如entry或text会在选择这些菜单项时做出适当的反应。但是,如果你正在构建自己的菜单,如何实现这一功能呢?你会给“复制”菜单项分配什么命令?

Tk通过虚拟事件来处理这个问题。正如你在Tk概念章节中回忆的,这些是高级应用程序事件,而不是低级操作系统事件。Tk的小部件会监视特定的事件。当你构建菜单时,你可以生成这些事件,而不是直接调用回调函数。你的应用程序也可以创建事件绑定来监视这些事件。

一些开发人员为他们菜单中的每个项目创建虚拟事件,而不是直接在代码中调用例程。这是将用户界面代码与应用程序的其他部分分离的一种方式。记住,即使你这样做,你仍然需要代码来根据应用程序状态的变化启用和禁用菜单项、调整它们的标签等。

这里有一个最小示例,展示了我们如何在“编辑”菜单中添加两个项目,标准的“粘贴”项目和一个特定于应用程序的“查找…”项目,该项目将打开一个对话框以查找或搜索某物。我们将包括一个entry小部件,以便我们可以检查“粘贴”是否工作。

from tkinter import *
from tkinter import ttk, messagebox

root = Tk()
ttk.Entry(root).grid()
m = Menu(root)
m_edit = Menu(m)
m.add_cascade(menu=m_edit, label="Edit")
m_edit.add_command(label="Paste", command=lambda: root.focus_get().event_generate("<<Paste>>"))
m_edit.add_command(label="Find...", command=lambda: root.event_generate("<<OpenFindDialog>>"))
root['menu'] = m

def launchFindDialog(*args):
    messagebox.showinfo(message="I hope you find what you're looking for!")
    
root.bind("<<OpenFindDialog>>", launchFindDialog)
root.mainloop()

小技巧

生成虚拟事件时,你需要指定该事件应该被发送到哪个小部件。我们希望“粘贴”事件被发送到具有键盘焦点的小部件(通常通过一个焦点环来指示)。你可以使用焦点命令来确定哪个小部件拥有键盘焦点。尝试一下,在窗口首次打开时(此时没有焦点)选择“粘贴”选项,然后在点击输入框后再次尝试(使其成为焦点)。注意观察输入框是如何处理 <<Paste>> 事件的。无需创建事件绑定。

<<OpenFindDialog>> 事件被发送到根窗口,这是我们创建事件绑定的地方。如果我们有多个顶层窗口,我们会将其发送到特定的窗口。

Tk预定义了以下虚拟事件:<<Clear>>, <<Copy>>, <<Cut>>, <<Paste>>, <<PasteSelection>>, <<PrevWindow>>, <<Redo>><<Undo>

事件参考

平台菜单#

每个平台在每个菜单栏中都有一些特殊的菜单,这些菜单由Tk特别处理。

macOS#

你可能已经注意到,在macOS上运行的Tk提供了它自己的默认菜单栏。这包括一个以正在运行的程序命名的菜单(在这种情况下,是你的编程语言的shell,例如,“Wish”,“Python”等),一个文件菜单,以及标准的编辑、窗口和帮助菜单,所有这些都配备了各种菜单项。

你可以在自己的程序中覆盖这个菜单栏,但要获得你想要的结果,你需要遵循一些特定的步骤(在某些情况下,需要按特定顺序)。

备注

从Tk 8.5.13开始,由于底层Tk代码从过时的Carbon API迁移到Cocoa,macOS上特殊菜单的处理方式发生了变化。如果你看到重复的菜单名称、缺少的项、不是你放的东西等,请仔细阅读这部分内容。

首先需要知道的是,如果你没有为窗口(或其父窗口,例如根窗口)指定菜单栏,你最终会得到Tk提供的默认菜单栏,除非你自己在搞乱,否则几乎肯定不是你想要的。

应用程序菜单#

每个菜单栏都以系统范围的苹果图标菜单开始。在其右侧是当前最前面的应用程序的菜单。它总是以正在运行的二进制文件命名。当你将菜单栏附加到窗口时,如果它还没有包含一个特命名的 .apple 菜单(见下文),Tk将提供其默认的应用程序菜单。它包括一个“关于Tcl & Tk”项目,接着是标准菜单项:首选项、服务子菜单、隐藏/显示项目和退出。同样,这不是你想要的。

如果你提供自己的 .apple 菜单,当菜单栏附加到窗口时,Tk将在你添加的任何项目之后添加标准项目(首选项及以后的项目)。完美!你在菜单栏附加到窗口后添加的项目将出现在退出项目之后,再次强调,这不是你想要的。

备注

我们在这里处理的应用程序菜单与苹果菜单(带有苹果图标的那个,就在应用程序菜单的左侧)不同。尽管如此,我们实际上指的是应用程序菜单,尽管Tk仍然将其称为“苹果”菜单。这是从OS X之前的时期遗留下来的,当时这类项目确实进入了实际的苹果菜单,而且没有单独的应用程序菜单。

换句话说,在你的程序中,确保你:

  • 为每个窗口或根窗口创建一个菜单栏。不要将菜单栏附加到窗口上!

  • 向菜单栏添加一个名为 .apple 的菜单。它将用作应用程序菜单。

  • 该菜单将自动命名为与应用程序二进制文件相同的名称;如果你想更改此名称,请重命名(或复制)用于运行脚本的二进制文件。

  • 添加你想在应用程序菜单顶部出现的项目,即一个“关于你的应用”项目,后面跟着一个分隔符。

  • 完成所有这些后,你可以通过窗口的菜单配置选项将菜单栏附加到你窗口上。

win = Toplevel(root)
menubar = Menu(win)
appmenu = Menu(menubar, name='apple')
menubar.add_cascade(menu=appmenu)
appmenu.add_command(label='About My Application')
appmenu.add_separator()
win['menu'] = menubar

虽然通常Tkinter会为我们选择一个小部件路径名,但我们不得不在使用 name 选项创建应用程序菜单时显式提供一个(apple)。

处理首选项菜单项#

如你所见,应用程序菜单总是包含一个“首选项…”菜单项。如果你的应用程序有首选项对话框,这个菜单项应该打开它。如果没有,这个菜单项应该是禁用的,默认情况下就是如此。

为了连接你的首选项对话框,你需要定义一个名为 ::tk::mac::ShowPreferences 的Tcl过程。当选择首选项菜单项时,将调用此过程;如果未定义该过程,则菜单项将被禁用。

def showMyPreferencesDialog():
    ....
	
root.createcommand('tk::mac::ShowPreferences', showMyPreferencesDialog)

提供帮助菜单#

像应用程序菜单一样,你在自定义菜单栏中添加的任何帮助菜单在macOS上都会受到特殊对待。就像需要特殊名称(.apple)的应用程序菜单一样,帮助菜单也必须被命名为.help。帮助菜单也应该在菜单栏附加到窗口之前添加。

帮助菜单将包括标准的macOS搜索框来搜索帮助,以及一个名为“yourapp Help”的项目。与应用程序菜单的名称一样,这来自你的程序的可执行文件,不能更改。类似于如何处理首选项对话框,要响应这个帮助项,你需要定义一个名为 ::tk::mac::ShowHelp 的Tcl过程。如果未定义此过程,它不会禁用菜单项。相反,当选择帮助项时,它将生成错误。

备注

如果你不想包含帮助,不要向菜单栏添加帮助菜单,那么将不会显示任何帮助。

与X11和早期版本的Tk在macOS上不同,帮助菜单不会自动放在菜单栏的末尾,因此请确保它是最后添加的菜单。

你还可以在帮助菜单中添加其他项。这些将出现在应用程序帮助项之后。

helpmenu = Menu(menubar, name='help')
menubar.add_cascade(menu=helpmenu, label='Help')
root.createcommand('tk::mac::ShowHelp', ...)

提供窗口菜单#

在macOS上,“窗口”菜单包含最小化、放大、全部前置等项目。它还包含当前打开的窗口列表。在该列表之前,有时会提供其他应用程序特定的项目。

通过提供一个名为 .window 的菜单,这个标准的窗口菜单将被添加。Tk会自动保持它与你的所有顶层窗口同步,而无需你编写任何额外代码。你也可以向此菜单添加任何应用程序特定的命令。这些会出现在你的窗口列表之前。

windowmenu = Menu(menubar, name='window')
menubar.add_cascade(menu=windowmenu, label='Window')

其他菜单处理程序#

您已经了解到,处理某些标准菜单项需要您定义Tcl回调过程,例如 tk::mac::ShowPreferencestk::mac::ShowHelp

还有其他几个您可以定义的回调。例如,您可以拦截“退出”菜单项,提示用户在退出前保存他们的更改。以下是完整的列表:

  • tk::mac::ShowPreferences: 当选择“首选项…”菜单项时调用。

  • tk::mac::ShowHelp: 用于显示应用程序的主要在线帮助。

  • tk::mac::Quit: 当选择“退出”菜单项或用户尝试关闭系统等时调用。

  • tk::mac::OnHide: 当您的应用程序被隐藏时调用。

  • tk::mac::OnShow: 当您的应用程序在被隐藏后再次显示时调用。

  • tk::mac::OpenApplication: 当您的应用程序首次打开时调用。

  • tk::mac::ReopenApplication: 当用户“重新打开”已经在运行的应用程序时调用(例如,在Dock上点击它)。

  • tk::mac::OpenDocument: 当Finder希望应用程序打开一个或多个文档(例如,那些被拖放到其上的文件)时调用。该过程会传递一个要打开的文件路径名列表。 tk::mac::PrintDocument: 与 OpenDocument 类似,但应打印文档而不是打开它们。

更多信息,请参阅 tk_mac 命令参考。

Windows#

在Windows操作系统中,每个窗口的左上角都有一个“系统”菜单,其中包含一个小图标代表您的应用程序。该菜单包括诸如“关闭”、“最小化”等选项。在使用Tk时,如果您创建一个系统菜单,您可以在标准项下方添加新的项目。

sysmenu = Menu(menubar, name='system')
menubar.add_cascade(menu=sysmenu)

备注

虽然Tkinter通常会为我们选择组件路径名,但在这种情况下,我们必须显式提供一个名为“system”的路径名;这是Tk识别其为系统菜单的信号。

X11#

在X11系统中,如果您创建了一个帮助菜单,Tk会确保它始终是菜单栏中的最后一个菜单。

menu_help = Menu(menubar, name='help')
menubar.add_cascade(menu=menu_help, label='Help')

备注

同样,尽管Tkinter通常会自动选择一个组件路径名,但在这里我们需要明确地指定一个名为“help”的路径名;这是Tk将其识别为帮助菜单的关键提示。

上下文菜单#

上下文菜单(“弹出”菜单)通常通过在应用程序中的对象上点击鼠标右键来调用。一个菜单会在鼠标光标的位置弹出。然后,用户可以从菜单中选择一个项目(或点击菜单外部来不选择任何项目就将其关闭)。

为了创建一个上下文菜单,我们将使用与创建菜单栏菜单相同的命令。通常情况下,我们会创建一个包含几个命令项以及可能的一些级联菜单项及其关联菜单的菜单。

要激活菜单,用户将执行一个上下文菜单点击。我们必须创建一个事件绑定来捕获该点击。然而,这在不同平台上可能意味着不同的事情。在Windows和X11上,这可能是点击鼠标右键(第三个鼠标按钮)。在macOS上,它可能是按住控制键点击左键(或唯一的按钮)或在多按钮鼠标上右键点击。与Windows和X11不同的是,macOS将此称为第二个鼠标按钮,而不是第三个,因此这是我们的程序中将会看到的事件。

备注

大多数早期使用弹出菜单的程序都认为只需要担心“按钮3”。

除了捕获正确的上下文菜单事件外,我们还需要捕获鼠标的位置。事实证明,我们需要相对于整个屏幕(全局坐标)而不是局部于你点击的窗口或小部件(局部坐标)来做这件事。Tk的事件绑定系统中的 %X%Y 替换将为我们捕获这些信息。

最后一步是通过 post 方法告诉菜单在特定位置弹出。这里有一个整个过程的例子,使用应用程序主窗口上的弹出菜单。

from tkinter import *
root = Tk()
menu = Menu(root)
for i in ('One', 'Two', 'Three'):
    menu.add_command(label=i)
if (root.tk.call('tk', 'windowingsystem')=='aqua'):
    root.bind('<2>', lambda e: menu.post(e.x_root, e.y_root))
    root.bind('<Control-1>', lambda e: menu.post(e.x_root, e.y_root))
else:
    root.bind('<3>', lambda e: menu.post(e.x_root, e.y_root))
root.mainloop()