事件处理和拾取#

Matplotlib 与多个用户界面工具包(wxpythontkinterqtgtk 和 macOS)配合使用。为了支持诸如图表的交互式平移和缩放等功能,开发者们需要一个“与GUI无关”的 API 来通过按键和鼠标移动与图表进行互动,这样我们就不必在不同的用户界面之间重复编写大量代码。虽然事件处理 API 是与 GUI 无关的,但它基于 GTK 模型,这是 Matplotlib 最初支持的用户界面。触发的事件也比标准 GUI 事件更丰富,包括事件发生在哪个坐标轴中的信息。这些事件还理解 Matplotlib 坐标系,并以像素和数据坐标报告事件位置。

事件连接#

为了接收事件,你需要编写回调函数,然后将你的函数连接到事件管理器,这是 FigureCanvasBase 的一部分。下面是一个简单示例,打印鼠标点击的位置和按下的按钮:

# %matplotlib tk
import numpy as np
from matplotlib import  pyplot as plt
fig, ax = plt.subplots()
ax.plot(np.random.rand(10))

def onclick(event):
    print(
        f"{'double' if event.dblclick else 'single'} "
        f"click: button={event.button:d}, x={event.x:g}, y={event.y:g}, "
        f"xdata={event.xdata:g}, ydata={event.ydata:g}"
    )

cid = fig.canvas.mpl_connect('button_press_event', onclick)
../../../../_images/84a1ad3cef998c17436217cec776f09781e04ddb61665790602034b0f686de68.png

mpl_connect() 方法返回连接 ID (一个整数),可以通过该 ID 来断开回调函数的连接。

fig.canvas.mpl_disconnect(cid)

备注

画布仅对用作回调的实例方法保留弱引用。因此,你需要保留对拥有这些方法的实例的引用。否则,该实例将被垃圾回收,回调也将消失。

这不影响用作回调的独立函数。

以下是你可以连接的事件、事件触发时传回给你的类实例以及事件描述:

好的,下面是您需要的中文译文:

事件名称

类名

描述

'button_press_event'

MouseEvent

鼠标按钮被按下

'button_release_event'

MouseEvent

鼠标按钮被释放

'close_event'

CloseEvent

图形关闭

'draw_event'

DrawEvent

画布已被绘制(但屏幕窗口尚未更新)

'key_press_event'

KeyEvent

按键被按下

'key_release_event'

KeyEvent

按键被释放

'motion_notify_event'

MouseEvent

鼠标移动

'pick_event'

PickEvent

画布中的图形被选中

'resize_event'

ResizeEvent

图形画布大小调整

'scroll_event'

MouseEvent

鼠标滚轮滚动

'figure_enter_event'

LocationEvent

鼠标进入新的图形

'figure_leave_event'

LocationEvent

鼠标离开图形

'axes_enter_event'

LocationEvent

鼠标进入新的坐标轴

'axes_leave_event'

LocationEvent

鼠标离开坐标轴

当连接到 'key_press_event''key_release_event' 事件时,你可能会遇到 Matplotlib 使用的不同用户界面工具包之间的不一致性。这是由于用户界面工具包的不一致性/限制所致。下表显示了你可能从不同用户界面工具包接收到的一些基本键(使用 QWERTY 键盘布局)的例子,其中逗号分隔不同的键:

Key(s) Pressed

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, nothing

control, ctrl+f1

control, ctrl+f1

control, ctrl+f1

Matplotlib 默认附加了一些按键回调以实现交互性;这些内容在 Navigation keyboard shortcuts 部分中有文档记录。

事件属性#

所有 Matplotlib 事件都继承自基类 Event,该基类存储以下属性:

name

事件名称

canvas

生成事件的 FigureCanvas 实例

guiEvent

触发 Matplotlib 事件的 GUI 事件

最常见的事件是按键按下/释放事件和鼠标按下/释放及移动事件。处理这些事件的 KeyEventMouseEvent 类都是从 LocationEvent 派生的,它具有以下属性:

x, y

鼠标在画布左下角的像素位置

inaxes

如果鼠标在某个轴上,则为鼠标所在的 Axes 实例;否则为 None

xdata, ydata

如果鼠标在某个轴上,则为鼠标在数据坐标中的位置

下面是简单的例子,每次鼠标按下时,都会在画布上创建简单的线段:

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()
../../../../_images/2a86e202928ec603a268d69392106758692e6dc3edb0d0f89105bcb229fb318b.png

刚刚使用的 MouseEventLocationEvent,因此我们可以使用 (event.x, event.y)(event.xdata, event.ydata) 访问数据和像素坐标。除了 LocationEvent 属性之外,它还具有以下属性:

button

按下的按钮:NoneMouseButton'up''down'(用于滚动事件的上和下)

key

按下的键:None、任何字符、'shift''win''control'

可拖动矩形练习#

编写一个可拖动矩形类,该类使用 Rectangle 实例初始化,但在拖动时会移动其 xy 位置。

提示:您需要存储矩形的原始xy位置,该位置存储为rect.xy,并连接到按下、移动和释放鼠标事件。当鼠标按下时,检查点击是否发生在您的矩形上(请参见 contains),如果是,则在数据坐标中存储矩形的 xy 和鼠标点击的位置。在移动事件回调中,计算鼠标移动的 deltaxdeltay,将这些增量添加到您存储的矩形原点,然后重绘图形。在按钮释放事件上,只需将您存储的所有按钮按下数据重置为 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()
../../../../_images/de9996d67c4052cb718d080be74f52b7f83abdaac81bcda965d940c4c07a4a28.png

加分项:使用双缓冲技术使动画绘制更快更流畅。

加分项解决方案:

# 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()
../../../../_images/1bdd0f94bbc8e5cac98de921094b4c469d8822d72f2cb009e6c6119e5f0ae828.png

鼠标进入和离开#

如果你想在鼠标进入或离开一个图形或坐标轴时收到通知,你可以连接到图形/坐标轴的进入/离开事件。以下是一个简单示例,该示例会改变鼠标悬停时的坐标轴和图形背景的颜色:

"""
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()
../../../../_images/f7cfae9c4aa84b16f3f03c6ff10d17a2646dfaafd36d20e58e917dd7a74ca77b.png ../../../../_images/f7cfae9c4aa84b16f3f03c6ff10d17a2646dfaafd36d20e58e917dd7a74ca77b.png

对象拾取#

你可以通过设置 Artist(如 Line2DTextPatchPolygonAxesImage等)的 picker 属性来启用拾取功能。

picker 属性可以使用以下类型进行设置:

None

禁用此艺术家的拾取功能(默认)。 boolean

如果为True,则启用拾取功能,当鼠标事件在艺术家上时,艺术家将触发拾取事件。 callable

如果picker是一个可调用的函数,那么它是一个用户提供的函数,用于确定鼠标事件是否击中了艺术家。 签名是 hit, props = picker(artist, mouseevent) 来确定命中测试。 如果鼠标事件在艺术家上,返回hit = Trueprops是一个字典,其属性成为 PickEvent 上的附加属性。

此外,还可以将艺术家的pickradius属性设置为点数(每英寸72点)的容差值,以确定鼠标距离多远仍能触发鼠标事件。

在通过设置picker属性启用艺术家的拾取功能后,你需要连接到图形画布的 pick_event 处理器,以便在鼠标按下事件上获得拾取回调。 处理器通常看起来像这样::: python     def pick_handler(event):         mouseevent = event.mouseevent         artist = event.artist         # 现在用这个做一些事情...     传递给回调的 PickEvent 始终具有以下属性:

mouseevent

生成拾取事件的 MouseEvent。 有关鼠标事件上有用的属性列表,请参阅event-attributes_。 artist

生成拾取事件的 Artist

此外,某些艺术家(如Line2DPatchCollection)可能会附加额外的元数据,例如满足拾取条件的数据索引(例如,线中所有在指定pickradius容差内的点)。

简单拾取示例#

在下面的示例中,我们在线上启用拾取功能并设置拾取半径容差(以点为单位)。当拾取事件距离线的距离在容差范围内时,onpick 回调函数将被调用,并且具有数据顶点索引,这些顶点在拾取距离容差范围内。我们的onpick回调函数只是打印拾取位置下的数据。不同的 Matplotlib Artist 可以将不同的数据附加到 PickEvent 上。例如,Line2D 附加了 ind 属性,它们是拾取点下的线数据的索引。

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()
../../../../_images/41939c74bbdef0523fa29097393a0a71567f8f433a777a0950dc8fdea60b518f.png

拾取练习#

创建一个包含100个长度为1000的高斯随机数数组的数据集,并计算每个数组的样本均值和标准差(提示:NumPy数组有mean和std方法)。然后绘制100个均值与100个标准差的xy标记图。将plot命令创建的线连接到pick事件,并绘制生成点击点的数据的原始时间序列。如果多个点在点击点的容差范围内,可以使用多个子图来绘制多个时间序列。

"""
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()
../../../../_images/55ac95f9eb9cf3d9270da8f55b8dbd93ee8e5e1db560dad0f096ecbe8933a91a.png