事件处理与拾取#
Matplotlib 与多个用户界面工具包(wxpython、tkinter、qt、gtk 和 macOS)兼容,为了支持诸如交互式平移和缩放图形等功能,开发人员拥有一个通过按键和鼠标移动与图形交互的“GUI 中性”API 是非常有帮助的,这样我们就不必在不同的用户界面中重复大量代码。尽管事件处理 API 是 GUI 中性的,但它基于 GTK 模型,这是 Matplotlib 支持的第一个用户界面。触发的事件相对于 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(一个整数),可以通过以下方式断开回调:
fig.canvas.mpl_disconnect(cid)
备注
画布仅保留对用作回调的实例方法的弱引用。因此,您需要保留对拥有这些方法的实例的引用。否则,实例将被垃圾回收,回调将消失。
这不会影响用作回调的自由函数。
以下是你可以连接的事件、事件发生时返回给你的类实例,以及事件描述:
事件名称 |
类 |
描述 |
|---|---|---|
'button_press_event' |
鼠标按钮被按下 |
|
'button_release_event' |
鼠标按钮被释放 |
|
'close_event' |
figure 已关闭 |
|
'draw_event' |
画布已被绘制(但屏幕小部件尚未更新) |
|
'key_press_event' |
按下键 |
|
'key_release_event' |
key 已释放 |
|
'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 |
控制 |
控制 |
控制 |
控制 |
控制 |
控制 |
控制 |
Alt |
替代文本 |
替代文本 |
替代文本 |
替代文本 |
替代文本 |
替代文本 |
AltGr |
iso_level3_shift |
无 |
替代文本 |
iso_level3_shift |
无 |
|
CapsLock |
caps_lock |
caps_lock |
caps_lock |
caps_lock |
caps_lock |
caps_lock |
大写锁定+a |
caps_lock, A |
caps_lock, 一个 |
caps_lock, 一个 |
caps_lock, A |
caps_lock, A |
caps_lock, 一个 |
a |
一个 |
一个 |
一个 |
一个 |
一个 |
一个 |
Shift+a |
shift, A |
shift, A |
shift, A |
shift, A |
shift, A |
shift, A |
大写锁定+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 |
控制, 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 |
控制, ctrl+f1 |
控制, ctrl+f1 |
控制,什么都没有 |
控制, ctrl+f1 |
控制, ctrl+f1 |
控制, 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 属性外,它还具有:
button按下的按钮:None,
MouseButton, 'up', 或 'down'(up 和 down 用于滚动事件)key按下的键:无,任意字符,'shift','win',或 'control'
可拖动矩形练习#
编写一个可拖动的矩形类,该类使用 Rectangle 实例初始化,但在拖动时会移动其 xy 位置。
提示:你需要存储矩形的原始 xy 位置,该位置存储为 rect.xy,并连接到按下、移动和释放鼠标事件。当鼠标按下时,检查点击是否发生在你的矩形上(参见 Rectangle.contains),如果是,则存储矩形的 xy 和鼠标点击在数据坐标中的位置。在移动事件回调中,计算鼠标移动的 deltax 和 deltay,并将这些增量添加到你存储的矩形原点,然后重绘图形。在按钮释放事件中,只需将你存储的所有按钮按下数据重置为 None。
以下是解决方案:
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()
额外加分:使用blitting使动画绘图更快更流畅。
额外学分解决方案:
# 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 属性可以使用多种类型进行设置:
None此艺术家的选取功能已禁用(默认)。
boolean如果为 True,则将启用拾取功能,并且如果鼠标事件在艺术家上方,艺术家将触发拾取事件。
callable如果 picker 是一个可调用对象,它是一个用户提供的函数,用于确定艺术家是否被鼠标事件击中。签名是
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生成拾取事件的
MouseEvent。 有关鼠标事件上有用的属性列表,请参见 event-attributes。artist生成拾取事件的
Artist。
此外,某些艺术家如 Line2D 和 PatchCollection 可能会附加额外的元数据,例如满足选择器标准的数据的索引(例如,在线中所有在指定 pickradius 容差内的点)。
简单的拾取示例#
在下面的示例中,我们启用了对线条的选择,并设置了一个以点为单位的选择半径容差。当选择事件在距离线条容差距离内时,将调用 onpick 回调函数,并且该函数具有在选择距离容差内的数据顶点的索引。我们的 onpick 回调函数只是打印位于选择位置下的数据。不同的 Matplotlib 艺术家可以将不同的数据附加到 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()