事件处理和拾取#
Matplotlib 可与多种用户界面工具包(wxpython、tkinter、qt、gtk 和 macOS)配合使用,为了支持诸如交互式平移和缩放图形等功能,对于开发人员来说,通过按键和鼠标移动与图形进行交互的 API 能够“GUI 中立”将非常有帮助,这样我们不必在不同的用户界面中重复大量代码。尽管事件处理 API 是 GUI 中立的,但它基于 GTK 模型,这是 Matplotlib 支持的第一个用户界面。触发的事件也比标准 GUI 事件更丰富,包括诸如事件发生在哪个 Axes
中等信息。这些事件还了解 Matplotlib 坐标系,并以像素和数据坐标报告事件位置。
事件连接#
要接收事件,您需要编写一个回调函数,然后将您的函数连接到事件管理器,该管理器是 FigureCanvasBase
的一部分。以下是一个简单的示例,它打印鼠标点击的位置和按下的按钮
fig, ax = plt.subplots()
ax.plot(np.random.rand(10))
def onclick(event):
print('%s click: button=%d, x=%d, y=%d, xdata=%f, ydata=%f' %
('double' if event.dblclick else 'single', event.button,
event.x, event.y, event.xdata, event.ydata))
cid = fig.canvas.mpl_connect('button_press_event', onclick)
FigureCanvasBase.mpl_connect
方法返回一个连接 ID(一个整数),该 ID 可用于通过以下方式断开回调
fig.canvas.mpl_disconnect(cid)
注意
画布仅保留对用作回调的实例方法的弱引用。因此,您需要保留对拥有此类方法的实例的引用。否则,实例将被垃圾回收,回调也将消失。
这不会影响用作回调的自由函数。
以下是可以连接到的事件、事件发生时发送给您的类实例以及事件描述
事件名称 |
类 |
描述 |
---|---|---|
'button_press_event' |
鼠标按钮按下 |
|
'button_release_event' |
鼠标按钮释放 |
|
'close_event' |
图像关闭 |
|
'draw_event' |
画布已绘制(但屏幕窗口尚未更新) |
|
'key_press_event' |
按键按下 |
|
'key_release_event' |
按键释放 |
|
'motion_notify_event' |
鼠标移动 |
|
'pick_event' |
画布中的艺术家被选中 |
|
'resize_event' |
图像画布调整大小 |
|
'scroll_event' |
鼠标滚轮滚动 |
|
'figure_enter_event' |
鼠标进入新图像 |
|
'figure_leave_event' |
鼠标离开图像 |
|
'axes_enter_event' |
鼠标进入新坐标轴 |
|
'axes_leave_event' |
鼠标离开坐标轴 |
注意
在连接到“key_press_event”和“key_release_event”事件时,您可能会遇到 Matplotlib 使用的不同用户界面工具包之间的不一致。这是由于用户界面工具包的不一致/限制造成的。下表显示了一些基本示例,说明您可能期望从不同的用户界面工具包收到哪些键(使用 QWERTY 键盘布局),其中逗号分隔不同的键
按下的键 |
Tkinter |
Qt |
macosx |
WebAgg |
GTK |
WxPython |
---|---|---|---|---|---|---|
Shift+2 |
shift, @ |
shift, @ |
shift, @ |
shift, @ |
shift, @ |
shift, shift+2 |
Shift+F1 |
shift, shift+f1 |
shift, shift+f1 |
shift, shift+f1 |
shift, shift+f1 |
shift, shift+f1 |
shift, shift+f1 |
Shift |
shift |
shift |
shift |
shift |
shift |
shift |
Control |
control |
control |
control |
control |
control |
control |
Alt |
alt |
alt |
alt |
alt |
alt |
alt |
AltGr |
iso_level3_shift |
nothing |
alt |
iso_level3_shift |
nothing |
|
CapsLock |
caps_lock |
caps_lock |
caps_lock |
caps_lock |
caps_lock |
caps_lock |
CapsLock+a |
caps_lock, A |
caps_lock, a |
caps_lock, a |
caps_lock, A |
caps_lock, A |
caps_lock, a |
a |
a |
a |
a |
a |
a |
a |
Shift+a |
shift, A |
shift, A |
shift, A |
shift, A |
shift, A |
shift, A |
CapsLock+Shift+a |
caps_lock, shift, a |
caps_lock, shift, A |
caps_lock, shift, A |
caps_lock, shift, a |
caps_lock, shift, a |
caps_lock, shift, A |
Ctrl+Shift+Alt |
control, ctrl+shift, ctrl+meta |
control, ctrl+shift, ctrl+meta |
control, ctrl+shift, ctrl+alt+shift |
control, ctrl+shift, ctrl+meta |
control, ctrl+shift, ctrl+meta |
control、ctrl+shift、ctrl+alt |
Ctrl+Shift+a |
control、ctrl+shift、ctrl+a |
control、ctrl+shift、ctrl+A |
control、ctrl+shift、ctrl+A |
control、ctrl+shift、ctrl+A |
control、ctrl+shift、ctrl+A |
control、ctrl+shift、ctrl+A |
F1 |
f1 |
f1 |
f1 |
f1 |
f1 |
f1 |
Ctrl+F1 |
control、ctrl+f1 |
control、ctrl+f1 |
control、无 |
control、ctrl+f1 |
control、ctrl+f1 |
control、ctrl+f1 |
Matplotlib 默认附加一些按键回调以实现交互性;它们在 导航键盘快捷键 部分中进行了说明。
事件属性#
所有 Matplotlib 事件均从基类 matplotlib.backend_bases.Event
继承,该基类存储以下属性
name
事件名称
canvas
生成事件的 FigureCanvas 实例
guiEvent
触发 Matplotlib 事件的 GUI 事件
最常见的事件是事件处理的基础,即按键按下/释放事件以及鼠标按下/释放和移动事件。处理这些事件的 KeyEvent
和 MouseEvent
类均派生自 LocationEvent,后者具有以下属性
x
、y
鼠标的 x 和 y 位置,以画布左下角为基准,单位为像素
inaxes
鼠标所在的
Axes
实例(如果有);否则为 Nonexdata
、ydata
鼠标的 x 和 y 位置,以数据坐标为基准(如果鼠标位于某个坐标轴上)
我们来看一个简单的画布示例,其中每次按下鼠标时都会创建一个简单的线段
from matplotlib import pyplot as plt
class LineBuilder:
def __init__(self, line):
self.line = line
self.xs = list(line.get_xdata())
self.ys = list(line.get_ydata())
self.cid = line.figure.canvas.mpl_connect('button_press_event', self)
def __call__(self, event):
print('click', event)
if event.inaxes!=self.line.axes: return
self.xs.append(event.xdata)
self.ys.append(event.ydata)
self.line.set_data(self.xs, self.ys)
self.line.figure.canvas.draw()
fig, ax = plt.subplots()
ax.set_title('click to build line segments')
line, = ax.plot([0], [0]) # empty line
linebuilder = LineBuilder(line)
plt.show()
我们刚刚使用的 MouseEvent
是 LocationEvent
,因此我们可以通过 (event.x, event.y)
和 (event.xdata, event.ydata)
访问数据和像素坐标。除了 LocationEvent
属性外,它还具有
按钮
按下的按钮:无、
MouseButton
、“向上”或“向下”(向上和向下用于滚动事件)键
按下的键:无、任何字符、“shift”、“win”或“control”
可拖动矩形练习#
编写可拖动矩形类,该类使用 Rectangle
实例进行初始化,但拖动时会移动其 xy
位置。提示:您需要存储矩形的原始 xy
位置,该位置存储为 rect.xy,并连接到按下、移动和释放鼠标事件。当按下鼠标时,检查点击是否发生在矩形上(参见 Rectangle.contains
),如果发生,存储矩形 xy 和鼠标点击在数据坐标中的位置。在移动事件回调中,计算鼠标移动的 deltax 和 deltay,并将这些增量添加到您存储的矩形原点。然后重新绘制图形。在按钮释放事件中,只需将您存储的所有按钮按下数据重置为无。
以下是解决方案
import numpy as np
import matplotlib.pyplot as plt
class DraggableRectangle:
def __init__(self, rect):
self.rect = rect
self.press = None
def connect(self):
"""Connect to all the events we need."""
self.cidpress = self.rect.figure.canvas.mpl_connect(
'button_press_event', self.on_press)
self.cidrelease = self.rect.figure.canvas.mpl_connect(
'button_release_event', self.on_release)
self.cidmotion = self.rect.figure.canvas.mpl_connect(
'motion_notify_event', self.on_motion)
def on_press(self, event):
"""Check whether mouse is over us; if so, store some data."""
if event.inaxes != self.rect.axes:
return
contains, attrd = self.rect.contains(event)
if not contains:
return
print('event contains', self.rect.xy)
self.press = self.rect.xy, (event.xdata, event.ydata)
def on_motion(self, event):
"""Move the rectangle if the mouse is over us."""
if self.press is None or event.inaxes != self.rect.axes:
return
(x0, y0), (xpress, ypress) = self.press
dx = event.xdata - xpress
dy = event.ydata - ypress
# print(f'x0={x0}, xpress={xpress}, event.xdata={event.xdata}, '
# f'dx={dx}, x0+dx={x0+dx}')
self.rect.set_x(x0+dx)
self.rect.set_y(y0+dy)
self.rect.figure.canvas.draw()
def on_release(self, event):
"""Clear button press information."""
self.press = None
self.rect.figure.canvas.draw()
def disconnect(self):
"""Disconnect all callbacks."""
self.rect.figure.canvas.mpl_disconnect(self.cidpress)
self.rect.figure.canvas.mpl_disconnect(self.cidrelease)
self.rect.figure.canvas.mpl_disconnect(self.cidmotion)
fig, ax = plt.subplots()
rects = ax.bar(range(10), 20*np.random.rand(10))
drs = []
for rect in rects:
dr = DraggableRectangle(rect)
dr.connect()
drs.append(dr)
plt.show()
附加积分:使用光栅化技术使动画绘制更快、更流畅。
附加积分解决方案
# Draggable rectangle with blitting.
import numpy as np
import matplotlib.pyplot as plt
class DraggableRectangle:
lock = None # only one can be animated at a time
def __init__(self, rect):
self.rect = rect
self.press = None
self.background = None
def connect(self):
"""Connect to all the events we need."""
self.cidpress = self.rect.figure.canvas.mpl_connect(
'button_press_event', self.on_press)
self.cidrelease = self.rect.figure.canvas.mpl_connect(
'button_release_event', self.on_release)
self.cidmotion = self.rect.figure.canvas.mpl_connect(
'motion_notify_event', self.on_motion)
def on_press(self, event):
"""Check whether mouse is over us; if so, store some data."""
if (event.inaxes != self.rect.axes
or DraggableRectangle.lock is not None):
return
contains, attrd = self.rect.contains(event)
if not contains:
return
print('event contains', self.rect.xy)
self.press = self.rect.xy, (event.xdata, event.ydata)
DraggableRectangle.lock = self
# draw everything but the selected rectangle and store the pixel buffer
canvas = self.rect.figure.canvas
axes = self.rect.axes
self.rect.set_animated(True)
canvas.draw()
self.background = canvas.copy_from_bbox(self.rect.axes.bbox)
# now redraw just the rectangle
axes.draw_artist(self.rect)
# and blit just the redrawn area
canvas.blit(axes.bbox)
def on_motion(self, event):
"""Move the rectangle if the mouse is over us."""
if (event.inaxes != self.rect.axes
or DraggableRectangle.lock is not self):
return
(x0, y0), (xpress, ypress) = self.press
dx = event.xdata - xpress
dy = event.ydata - ypress
self.rect.set_x(x0+dx)
self.rect.set_y(y0+dy)
canvas = self.rect.figure.canvas
axes = self.rect.axes
# restore the background region
canvas.restore_region(self.background)
# redraw just the current rectangle
axes.draw_artist(self.rect)
# blit just the redrawn area
canvas.blit(axes.bbox)
def on_release(self, event):
"""Clear button press information."""
if DraggableRectangle.lock is not self:
return
self.press = None
DraggableRectangle.lock = None
# turn off the rect animation property and reset the background
self.rect.set_animated(False)
self.background = None
# redraw the full figure
self.rect.figure.canvas.draw()
def disconnect(self):
"""Disconnect all callbacks."""
self.rect.figure.canvas.mpl_disconnect(self.cidpress)
self.rect.figure.canvas.mpl_disconnect(self.cidrelease)
self.rect.figure.canvas.mpl_disconnect(self.cidmotion)
fig, ax = plt.subplots()
rects = ax.bar(range(10), 20*np.random.rand(10))
drs = []
for rect in rects:
dr = DraggableRectangle(rect)
dr.connect()
drs.append(dr)
plt.show()
鼠标进入和离开#
如果您希望在鼠标进入或离开图形或轴时收到通知,则可以连接到图形/轴进入/离开事件。这是一个简单的示例,它会更改鼠标所在的轴和图形背景的颜色
"""
Illustrate the figure and axes enter and leave events by changing the
frame colors on enter and leave
"""
import matplotlib.pyplot as plt
def enter_axes(event):
print('enter_axes', event.inaxes)
event.inaxes.patch.set_facecolor('yellow')
event.canvas.draw()
def leave_axes(event):
print('leave_axes', event.inaxes)
event.inaxes.patch.set_facecolor('white')
event.canvas.draw()
def enter_figure(event):
print('enter_figure', event.canvas.figure)
event.canvas.figure.patch.set_facecolor('red')
event.canvas.draw()
def leave_figure(event):
print('leave_figure', event.canvas.figure)
event.canvas.figure.patch.set_facecolor('grey')
event.canvas.draw()
fig1, axs = plt.subplots(2)
fig1.suptitle('mouse hover over figure or axes to trigger events')
fig1.canvas.mpl_connect('figure_enter_event', enter_figure)
fig1.canvas.mpl_connect('figure_leave_event', leave_figure)
fig1.canvas.mpl_connect('axes_enter_event', enter_axes)
fig1.canvas.mpl_connect('axes_leave_event', leave_axes)
fig2, axs = plt.subplots(2)
fig2.suptitle('mouse hover over figure or axes to trigger events')
fig2.canvas.mpl_connect('figure_enter_event', enter_figure)
fig2.canvas.mpl_connect('figure_leave_event', leave_figure)
fig2.canvas.mpl_connect('axes_enter_event', enter_axes)
fig2.canvas.mpl_connect('axes_leave_event', leave_axes)
plt.show()
对象拾取#
您可以通过设置 Artist
(例如 Line2D
、Text
、Patch
、Polygon
、AxesImage
等)的 picker
属性来启用拾取。
picker
属性可以使用各种类型进行设置
无
禁用此艺术家的拾取(默认)。
布尔值
如果为 True,则会启用拾取,并且如果鼠标事件位于艺术家上方,艺术家将触发拾取事件。
可调用对象
如果拾取器是可调用对象,则它是由用户提供的函数,用于确定鼠标事件是否击中了艺术家。签名为
hit, props = picker(artist, mouseevent)
以确定命中测试。如果鼠标事件位于艺术家上方,则返回hit = True
;props
是一个属性字典,这些属性成为PickEvent
的附加属性。
艺术家的 pickradius
属性还可以设置为公差值(以点为单位,每英寸有 72 点),该值确定鼠标可以移动多远且仍会触发鼠标事件。
通过设置 picker
属性为艺术家启用拾取后,您需要将处理程序连接到图形画布 pick_event 以在鼠标按下事件上获取拾取回调。处理程序通常如下所示
def pick_handler(event):
mouseevent = event.mouseevent
artist = event.artist
# now do something with this...
传递给回调的 PickEvent
始终具有以下属性
鼠标事件
生成拾取事件的
MouseEvent
。有关鼠标事件的有用属性列表,请参见 事件属性。艺术家
生成拾取事件的
Artist
。
此外,某些艺术家(如 Line2D
和 PatchCollection
)可能会附加其他元数据,例如符合拾取器条件的数据索引(例如,线中所有在指定的 pickradius
容差范围内的点)。
简单的拾取示例#
在下面的示例中,我们在线上启用拾取并以点为单位设置拾取半径容差。onpick
回调函数将在拾取事件在距离线容差范围内的位置时调用,并具有在拾取距离容差范围内的 data 顶点的索引。我们的 onpick
回调函数只是打印拾取位置下的数据。不同的 Matplotlib Artist 可以将不同的数据附加到 PickEvent。例如,Line2D
附加 ind 属性,该属性是拾取点下行数据的索引。有关 PickEvent
的详细信息,请参见 Line2D.pick
行属性。
import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.set_title('click on points')
line, = ax.plot(np.random.rand(100), 'o',
picker=True, pickradius=5) # 5 points tolerance
def onpick(event):
thisline = event.artist
xdata = thisline.get_xdata()
ydata = thisline.get_ydata()
ind = event.ind
points = tuple(zip(xdata[ind], ydata[ind]))
print('onpick points:', points)
fig.canvas.mpl_connect('pick_event', onpick)
plt.show()
拾取练习#
创建一个包含 100 个数组的数据集,每个数组包含 1000 个高斯随机数,并计算每个数组的样本均值和标准差(提示:NumPy 数组具有 mean 和 std 方法),并绘制 100 个均值与 100 个标准差的 xy 标记图。将绘图命令创建的线连接到拾取事件,并绘制生成已单击点的数据的原始时间序列。如果有多个点在已单击点的容差范围内,则可以使用多个子图来绘制多个时间序列。
练习解决方案
"""
Compute the mean and stddev of 100 data sets and plot mean vs. stddev.
When you click on one of the (mean, stddev) points, plot the raw dataset
that generated that point.
"""
import numpy as np
import matplotlib.pyplot as plt
X = np.random.rand(100, 1000)
xs = np.mean(X, axis=1)
ys = np.std(X, axis=1)
fig, ax = plt.subplots()
ax.set_title('click on point to plot time series')
line, = ax.plot(xs, ys, 'o', picker=True, pickradius=5) # 5 points tolerance
def onpick(event):
if event.artist != line:
return
n = len(event.ind)
if not n:
return
fig, axs = plt.subplots(n, squeeze=False)
for dataind, ax in zip(event.ind, axs.flat):
ax.plot(X[dataind])
ax.text(0.05, 0.9,
f"$\\mu$={xs[dataind]:1.3f}\n$\\sigma$={ys[dataind]:1.3f}",
transform=ax.transAxes, verticalalignment='top')
ax.set_ylim(-0.5, 1.5)
fig.show()
return True
fig.canvas.mpl_connect('pick_event', onpick)
plt.show()