事件处理和拾取#

Matplotlib 可以与许多用户界面工具包(wxpython、tkinter、qt、gtk 和 macOS)一起使用,为了支持诸如图形的交互式平移和缩放等功能,对于开发人员来说,拥有一个通过按键和鼠标移动与图形交互的 “GUI 中性” API 非常有帮助,这样我们就不必在不同的用户界面上重复大量的代码。尽管事件处理 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'

MouseEvent

按下鼠标按钮

'button_release_event'

MouseEvent

MouseEvent

释放鼠标按钮

'close_event'

CloseEvent

关闭图形

'draw_event'

DrawEvent

已绘制画布(但屏幕小部件尚未更新)

'key_press_event'

KeyEvent

按下按键

'key_press_event'

'key_release_event'

KeyEvent

MouseEvent

释放按键

'motion_notify_event'

MouseEvent

鼠标移动

'pick_event'

PickEvent

选中画布中的艺术家

'resize_event'

MouseEvent

ResizeEvent

调整图形画布大小

'scroll_event'

MouseEvent

滚动鼠标滚轮

'scroll_event'

'figure_enter_event'

LocationEvent

'scroll_event'

鼠标进入新的图形

'figure_leave_event'

'scroll_event'

LocationEvent

注意

鼠标离开图形

'axes_enter_event'

LocationEvent

鼠标进入新的坐标轴

'axes_leave_event'

LocationEvent

鼠标离开坐标轴

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

按下的键

Tkinter

Tkinter

Tkinter

Tkinter

Tkinter

Qt

macosx

WebAgg

WebAgg

WebAgg

WebAgg

WebAgg

WebAgg

GTK

WxPython

WxPython

WxPython

WxPython

WxPython

WxPython

Shift+2

shift, @

shift, @

shift, @

shift, @

shift, @

shift, @

shift, shift+2

Shift+F1

Shift+F1

Shift+F1

Shift+F1

Shift+F1

Shift+F1

shift, shift+f1

Shift

shift

Shift+F1

Shift

shift

Control

control

control

control

control

control

control

Alt

alt

AltGr

AltGr

alt

alt

AltGr

iso_level3_shift

iso_level3_shift

iso_level3_shift

iso_level3_shift

iso_level3_shift

iso_level3_shift

iso_level3_shift

nothing

CapsLock

CapsLock

CapsLock

CapsLock

CapsLock

CapsLock

caps_lock

CapsLock+a

caps_lock, A

caps_lock, A

CapsLock+a

CapsLock+a

caps_lock, A

caps_lock, a

a

a

Shift+a

a

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+meta

control, ctrl+shift, ctrl+meta

control, ctrl+shift, ctrl+meta

control, ctrl+shift, ctrl+meta

control, ctrl+shift, ctrl+alt+shift

control, ctrl+shift, ctrl+alt

control, ctrl+shift, ctrl+alt

Ctrl+Shift+a

control, ctrl+shift, ctrl+alt

control, ctrl+shift, ctrl+alt

control, ctrl+shift, ctrl+alt

control, ctrl+shift, ctrl+a

control, ctrl+shift, ctrl+A

F1

f1

Ctrl+F1

control, ctrl+f1

control, nothing

Matplotlib 默认附加了一些用于交互的按键回调;它们在导航键盘快捷键部分中进行了文档说明。

事件属性#

所有 Matplotlib 事件都继承自基类 matplotlib.backend_bases.Event,它存储以下属性

name

事件名称

canvas

生成事件的 FigureCanvas 实例

guiEvent

触发 Matplotlib 事件的 GUI 事件

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

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()

x, y

鼠标 x 和 y 位置,以像素为单位,从画布的左侧和底部开始

inaxes

鼠标所在的 Axes 实例(如果有);否则为 None

xdata, ydata

如果鼠标位于坐标轴上方,则鼠标 x 和 y 位置(以数据坐标为单位)

让我们看一个简单的画布示例,其中每次按下鼠标时都会创建一个简单的线段

提示:你需要存储矩形的原始 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

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

布尔值

如果为 True,则启用拾取,并且如果鼠标事件发生在艺术家上方,则该艺术家将触发拾取事件。

可调用对象

如果 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。有关鼠标事件的有用属性列表,请参见 事件属性

artist

生成拾取事件的 Artist

此外,某些艺术家(如 Line2DPatchCollection)可能会附加其他元数据,例如满足拾取器标准的数据索引(例如,在指定的 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 标记图。将 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()