Как обрабатывать события мыши и клавиатуры на графиках Matplotlib | jenyay.net

Как обрабатывать события мыши и клавиатуры на графиках Matplotlib

Дата публикации: 16.12.2016
Дата последней правки: 25.03.2023

Содержание

Немного теории

Иногда при использовании библиотеки Matplotlib требуется добавить возможность пользователю взаимодействовать с нарисованным графиком - выделять интересующие области, перетаскивать объекты на графике и т.п. Если требуется создать сложный интерфейс, то Matplotlib легко интегрируется в приложения, написанные, например, на wxPython (см. статью Как рисовать графики с помощью Matplotlib при использовании wxPython. Для более простых задач можно обойтись без сторонних библиотек, т.к. Matplotlib позволяет обрабатывать различные события, связанные с действиями мышью и нажатиями клавиш.

Подписка на события происходит через базовый класс matplotlib.backend_bases.FigureCanvasBase, который включает в себя методы для подписки на события и отписки от них. Класс matplotlib.figure.Figure содержит в себе свойство canvas, через которое можно получить экземпляр класса, производный от FigureCanvasBase (тип конкретного класса зависит от настроек рендеринга графиков).

Класс FigureCanvasBase содержит метод mpl_connect, который позволяет подписаться на нужное событие. Он описывается следующей сигнатурой:

mpl_connect(s, func)

Здесь s - строка, описывающая событие, на которое надо подписаться, а func - это вызываемый объект (скорее всего это будет функция), который будет вызван в случае возникновения данного события.

В качестве первого параметра s функции mpl_connect() могут быть переданы следующие строки (все они перечислены на этой странице доккументации):

  • "button_press_event" - нажатие кнопки мыши.
  • "button_release_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" - мышь покинула пределы осей (графика).
  • "close_event" - закрытие фигуры.

Обработчик события - это функция (или другой вызываемый объект), которая должна принимать один параметр, тип которого зависит от обрабатываемого события, но в любом случае это будет класс, производный от matplotlib.backend_bases.Event. Для событий, связанных с мышью это будет экземпляр класса MouseEvent, а для событий, связанных с клавиатурой - KeyEvent.

В качестве результата функция mpl_connect возвращает целое число, являющееся идентификатором, по которому позже можно будет отписаться от события с помощью метода mpl_disconnect.

Пример программы

Для демонстрации обработки некоторых событий мыши и клавиатуры был написан следующий скрипт, который выводит информацию о случившемся событии в консоль:

import matplotlib.backend_bases
import matplotlib.pyplot as plt
import numpy as np


def _print_event(event: matplotlib.backend_bases.Event, attr_list: list[str]) -> None:
    print()
    print('**** {} ****'.format(event.name))
    print('    ' + str(type(event)))
    for attr in attr_list:
        title = 'event.' + attr
        value = getattr(event, attr)
        line = f'    {title:20}: {value}'
        print(line)


def onMouseEvent(event: matplotlib.backend_bases.MouseEvent) -> None:
    '''
    Обработчик событий, связанных с мышью
    '''

    attr_list = ['name',
                 'dblclick', 'button', 'key',
                 'xdata', 'ydata',
                 'x', 'y',
                 'inaxes',
                 'step',
                 'guiEvent']
    _print_event(event, attr_list)


def onKeyEvent(event: matplotlib.backend_bases.KeyEvent) -> None:
    '''
    Обработчик событий, связанных с клавиатурой
    '''

    attr_list = ['name',
                 'key',
                 'xdata', 'ydata',
                 'x', 'y',
                 'inaxes',
                 'guiEvent']
    _print_event(event, attr_list)


if __name__ == '__main__':
    # Расчитываем функцию
    x = np.arange(0, 5 * np.pi, 0.01)
    y = np.sin(x) * np.cos(3 * x)

    # Нарисовать график
    fig, axes = plt.subplots()
    axes.plot(x, y)

    # События, связанные с мышью
    button_press_event_id = fig.canvas.mpl_connect('button_press_event',
                                                   onMouseEvent)

    button_release_event_id = fig.canvas.mpl_connect('button_release_event',
                                                     onMouseEvent)

    scroll_event_id = fig.canvas.mpl_connect('scroll_event',
                                             onMouseEvent)

    # События, связанные с клавишами
    key_press_event_id = fig.canvas.mpl_connect('key_press_event',
                                                onKeyEvent)

    key_release_event_id = fig.canvas.mpl_connect('key_release_event',
                                                  onKeyEvent)

    plt.show()

    # Отпишемся от событий
    fig.canvas.mpl_disconnect(button_press_event_id)
    fig.canvas.mpl_disconnect(button_release_event_id)
    fig.canvas.mpl_disconnect(scroll_event_id)
    fig.canvas.mpl_disconnect(key_press_event_id)
    fig.canvas.mpl_disconnect(key_release_event_id)

Этот скрипт подписывается на события "button_press_event", "button_release_event", "scroll_event", "key_press_event", "key_release_event" и выводит информацию о наиболее важных свойствах возникших событий.

Ниже приведен пример обработки некоторых событий:

**** button_press_event ****
    <class 'matplotlib.backend_bases.MouseEvent'>
    event.name          : button_press_event
    event.dblclick      : False
    event.button        : 1
    event.key           : None
    event.xdata         : 2.28782615169402
    event.ydata         : 0.10954656191644085
    event.x             : 168
    event.y             : 258
    event.inaxes        : Axes(0.125,0.11;0.775x0.77)
    event.step          : 0
    event.guiEvent      : <Gdk.EventButton object at 0x7f5bd407a570 (void at 0x558315e7be60)>

**** button_release_event ****
    <class 'matplotlib.backend_bases.MouseEvent'>
    event.name          : button_release_event
    event.dblclick      : False
    event.button        : 1
    event.key           : None
    event.xdata         : 2.28782615169402
    event.ydata         : 0.10954656191644085
    event.x             : 168
    event.y             : 258
    event.inaxes        : Axes(0.125,0.11;0.775x0.77)
    event.step          : 0
    event.guiEvent      : <Gdk.EventButton object at 0x7f5bd407a570 (void at 0x558315f1f180)>

**** scroll_event ****
    <class 'matplotlib.backend_bases.MouseEvent'>
    event.name          : scroll_event
    event.dblclick      : False
    event.button        : down
    event.key           : None
    event.xdata         : 12.365181968442853
    event.ydata         : -0.587766714481353
    event.x             : 457
    event.y             : 125
    event.inaxes        : Axes(0.125,0.11;0.775x0.77)
    event.step          : -1
    event.guiEvent      : <Gdk.EventScroll object at 0x7f5d9c5c9670 (void at 0x56329299d970)>

**** key_press_event ****
    <class 'matplotlib.backend_bases.KeyEvent'>
    event.name          : key_press_event
    event.key           : control
    event.xdata         : 12.68977822580645
    event.ydata         : 0.6097740392119746
    event.x             : 467
    event.y             : 354
    event.inaxes        : Axes(0.125,0.11;0.775x0.77)
    event.guiEvent      : <Gdk.EventKey object at 0x7f33369ef290 (void at 0x55f99e208700)>

**** key_release_event ****
    <class 'matplotlib.backend_bases.KeyEvent'>
    event.name          : key_release_event
    event.key           : control
    event.xdata         : 12.68977822580645
    event.ydata         : 0.6097740392119746
    event.x             : 467
    event.y             : 354
    event.inaxes        : Axes(0.125,0.11;0.775x0.77)
    event.guiEvent      : <Gdk.EventKey object at 0x7f33369ef290 (void at 0x55f99e2ac680)>

Разберем некоторые свойства классов событий подробнее.

Обработка событий клавиатуры. Класс KeyEvent

Сначала рассмотрим наиболее важные свойства класса KeyEvent, экземпляр которого посылается обработчику событий, связанных с клавиатурой. Эти же свойства будут содержаться и в классе MouseEvent, о котором будет говориться в следующем разделе.

  • Свойство event.name содержит строку, соответствующую той, что мы передавали в метод mpl_connect, когда подписывались на данное событие. Использовать это свойство можно, например, если вы хотите подписать на несколько событий один и тот же обработчик, а уже внутри него разбираться, какое событие пришло.
  • Свойство event.key описывает нажатую или отпущенную клавишу на клавиатуре. Это свойство является строкой, которая описывает клавишу или комбинацию клавиш. Значением этого свойства могут быть строки вида "d" или "D" (если нажата комбинация Shift+d), 'control', 'shift', 'ctrl+d' и т.д.
  • Свойства event.xdata и event.ydata описывают положение курсора мыши в момент возникновения события графика в окне (относительно осей графика). Если в момент возникновения события курсор мыши находился за пределами какого-либо графика, то эти значения будут равны None.
  • Чтобы узнать в пределах каких именно осей находился курсор мыши в момент возникновения события, предназначено свойство event.inaxes, которое возвращает объект класса, производного от matplotlib.axes.Axes, если событие произошло в тот момент, когда курсор находился над графиком. В противном случае значение свойства event.inaxes равно None.
  • Свойства event.x и event.y описывают положение курсора мыши в момент возникновения события. Координаты рассчитываются в пикселях относительно левого нижнего угла графика за пределами осей. Эта точка показана на следующем рисунке:

Если в момент возникновения события курсор мыши находился за пределами окна, то значения этих координат могут быть отрицательными или быть больше размера окна.

Обработка событий мыши. Класс MouseEvent

Как уже было сказано выше, класс MouseEvent включает в себя свойства класса KeyEvent, а кроме того, в нем добавлены новые свойства. Рассмотрим их.

  • Тип свойства event.button зависит от типа события. Если мы обрабатываем событие "button_press_event" или "button_release_event", то это свойство содержит целое число, описывающее кнопку мыши, которую нажали. Значение этого свойства равно 1, если нажали левую кнопку мыши, 2, если нажали среднюю кнопку мыши, и 3, если нажали правую кнопку мыши. Если у мышки есть дополнительные кнопки, то при их нажатии значение параметра event.button будет больше 3. Например, на моей мышке при нажатии боковых кнопок это значение равно 8 и 9. Если мы обрабатываем событие "scroll_event", связанное с вращением колесика мыши, то свойство event.button - это строка, равная "up", если колесико мыши вращаем от себя и "down", если колесико вращаем к себе.
  • Свойство event.dblclick - содержит булево значение, которое равно True, если был произведен двойной клик мышью, и False, если был произведен одинарный клик. Не знаю, с чем это связано, но мне не удалось добиться, чтобы это значение стало равно True, как бы я усердно ни кликал.
  • Свойство event.key аналогично одноименному свойству из класса KeyEvent. Оно равно None, если в момент нажатия кнопки мыши не была зажата ни одна клавиша на клавиатуре, в противном случае это свойство содержит строку, описывающую зажатые клавиши. Ниже показаны несколько примеров с различными комбинациями клавиш:
  • Свойство event.step - это целое значение, отличное от 0 при обработке события "scroll_event". Оно показывает направление вращения колесика мыши. Теоретически это значение может быть различным, но оно будет положительным при вращении колесика мыши от себя (вверх) и отрицательным при вращении колесика по направлению к себе (вниз). Хотя во всех экспериментах, которые я проводил у себя на компьютере, это значение принимало значения -1 или 1, но согласно документации это не обязательно.

Обработчики событий и сборщик мусора

В документации по обработке событий есть важное примечание, которое гласит, что класс Canvas сохраняет только слабые ссылки на вызываемый объект, поэтому, если, например, в качестве обработчика события передан метод объекта, на которого не осталось других ссылок, то он будет уничтожен сборщиком мусора, и обработчик не будет вызван.

Давайте убедимся в этом на практике.

Изменим предыдущий пример, создав класс, внутри которого будет метод-обработчик события. Для наглядности кода упростим действия, которые происходят внутри обработчика события.

import matplotlib.backend_bases
import matplotlib.pyplot as plt
import numpy as np


class Handler:
    def mouseHandler(self, event: matplotlib.backend_bases.MouseEvent) -> None:
        print(event.name)


if __name__ == '__main__':
    x = np.arange(0, 5 * np.pi, 0.01)
    y = np.sin(x) * np.cos(3 * x)

    fig, axes = plt.subplots()
    axes.plot(x, y)

    # Создадим класс обработчика событий
    handler = Handler()
    fig.canvas.mpl_connect('button_press_event', handler.mouseHandler)

    plt.show()

Этот код работает, при нажатии кнопки мыши в окне графика в консоль выводится строка "button_press_event".

Теперь после вызова метода mpl_connect присвоим переменной handler значение None, чтобы уменьшить количество ссылок на созданный экземпляр класса Handler. Если класс канвы действительно сохраняет лишь слабую ссылку на вызываемый метод, то количество ссылок на этот экземпляр класса Handler станет равно 0, и сборщик мусора уничтожит этот объект.

import matplotlib.backend_bases
import matplotlib.pyplot as plt
import numpy as np


class Handler:
    def mouseHandler(self, event: matplotlib.backend_bases.MouseEvent) -> None:
        print(event.name)


if __name__ == '__main__':
    x = np.arange(0, 5 * np.pi, 0.01)
    y = np.sin(x) * np.cos(3 * x)

    fig, axes = plt.subplots()
    axes.plot(x, y)

    # Создадим класс обработчика событий
    handler = Handler()
    fig.canvas.mpl_connect('button_press_event', handler.mouseHandler)

    # !!!
    handler = None

    plt.show()

Запускаем код и убеждаемся, что теперь при клике на график в консоль ничего не выводится.

Последний пример можно было бы записать таким образом (в нем не создается переменная handler):

# -*- coding: UTF-8 -*-

import pylab
import numpy as np


class Handler(object):
    def mouseHandler(self, event):
        # type: (matplotlib.backend_bases.MouseEvent) -> None
        print(event.name)


if __name__ == '__main__':
    x = np.arange(0, 5 * np.pi, 0.01)
    y = np.sin(x) * np.cos(3 * x)

    fig = pylab.figure()
    pylab.plot(x, y)

    fig.canvas.mpl_connect('button_press_event', Handler().mouseHandler)

    pylab.show()

Он тоже не будет работать из-за того, что экземпляр класса, созданный с помощью команды Handler() проживет совсем не долго и будет уничтожен сборщиком мусора.

Более практичный пример

И в завершение статьи рассмотрим более жизненный пример, который показывает, как можно использовать те данные о событиях, которые мы рассмотрели. Пример достаточно простой, он рисует два графика в разных осях, а затем, если пользователь щелкает мышью в пределах одного или второго графика, то в точке клика выводится текст с координатами, где произошел клик (при этом старая надпись, если она существует, удаляется).

from typing import Optional

import matplotlib.backend_bases
import matplotlib.pyplot as plt
import matplotlib.text
import numpy as np


marker: Optional[matplotlib.text.Text] = None


def onMouseClick(event: matplotlib.backend_bases.MouseEvent) -> None:
    axes = event.inaxes

    # Если кликнули вне какого-либо графика, то не будем ничего делать
    if axes is None:
        return

    global marker
    # Если маркер с текстом уже был создан, то удалим его
    if marker is not None:
        marker.remove()

    # В качестве текущих выберем оси, внутри которых кликнули мышью
    # pylab.sca(axes)

    # Координаты клика в системе координат осей
    x = event.xdata
    y = event.ydata
    text = f'({x:.3f}; {y:.3f})'

    # Выведем текст в точку, куда кликнули
    marker = axes.text(x, y, text)

    # Обновим график
    axes.figure.canvas.draw()


if __name__ == '__main__':
    # Расчитываем функции
    x = np.arange(0, 5 * np.pi, 0.01)
    y1 = np.sin(x) * np.cos(2 * x)
    y2 = np.sin(x) * np.cos(5 * x)

    # Создадим окно с графиком
    fig, (axes1, axes2) = plt.subplots(nrows=2)

    # Нарисуем первый график
    axes1.plot(x, y1)
    axes1.grid()

    # Нарисуем второй график
    axes2.plot(x, y2)
    axes2.grid()

    # Подписка на событие
    fig.canvas.mpl_connect('button_press_event', onMouseClick)

    plt.show()

Пожалуй, единственное, на что здесь стоит обратить внимание, - это необходимость обновления графика c помощью вызова axes.figure.canvas.draw() после добавления нового объекта с текстом. Про то, как выводить текст на графике, и с какими особенностями можно при этом столкнуться, написано в статье Как выводить текст и настраивать его внешний вид.

Похожие статьи

Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
4.5 stars

Рейтинг 4.5/5. Всего 12 голос(а, ов)



 17.12.2016 - 12:00

Здравствуйте!
Я правильно понимаю, что события будут работать только если график в отдельном окошке будет показываться? Т.е., если в Jupyter Notebook он будет просто выводиться в виде png картинки, то события работать не будут?

Jenyay 17.12.2016 - 16:22

Здравствуйте. Да, все правильно.

Ant 12.07.2017 - 01:56

Спасибо за статью. В обработчике события вместо pylab.show() надо pylab.draw().


Подписаться на комментарии
Автор:
Тема:
 Ваш комментарий
 
 
Введите код 493