Обработка событий мыши и клавиатуры на графиках Matplotlib

Немного рекламы

Содержание

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

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

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

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

mpl_connect(self, 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.

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

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

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

import pylab
import numpy as np


def _print_event(event, attr_list):
    print()
    print('**** {} ****'.format(event.name))
    print('    ' + str(type(event)))
    for attr in attr_list:
        title = 'event.' + attr
        value = getattr(event, attr)
        line = '    {title:20}: {value}'.format(title=title, value=value)
        print(line)


def onMouseEvent(event):
    # type: (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):
    # type: (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 = pylab.figure()
    pylab.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)

    pylab.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         : 6.419354838709678
    event.ydata         : 0.45833333333333326
    event.x             : 279
    event.y             : 328.0
    event.inaxes        : Axes(0.125,0.1;0.775x0.8)
    event.step          : 0
    event.guiEvent      : <tkinter.Event object at 0x7f7a9334df98>

**** button_release_event ****
    <class 'matplotlib.backend_bases.MouseEvent'>
    event.name          : button_release_event
    event.dblclick      : False
    event.button        : 1
    event.key           : None
    event.xdata         : 6.419354838709678
    event.ydata         : 0.45833333333333326
    event.x             : 279
    event.y             : 328.0
    event.inaxes        : Axes(0.125,0.1;0.775x0.8)
    event.step          : 0
    event.guiEvent      : <tkinter.Event object at 0x7f7a9334d860>

**** scroll_event ****
    <class 'matplotlib.backend_bases.MouseEvent'>
    event.name          : scroll_event
    event.dblclick      : False
    event.button        : down
    event.key           : None
    event.xdata         : 3.3548387096774195
    event.ydata         : 0.53125
    event.x             : 184
    event.y             : 342.0
    event.inaxes        : Axes(0.125,0.1;0.775x0.8)
    event.step          : -1
    event.guiEvent      : <tkinter.Event object at 0x7fd034aee860>

**** key_press_event ****
    <class 'matplotlib.backend_bases.KeyEvent'>
    event.name          : key_press_event
    event.key           : control
    event.xdata         : 3.3548387096774195
    event.ydata         : 0.53125
    event.x             : 184
    event.y             : 342.0
    event.inaxes        : Axes(0.125,0.1;0.775x0.8)
    event.guiEvent      : <tkinter.Event object at 0x7fd034aeea58>

**** button_press_event ****
    <class 'matplotlib.backend_bases.MouseEvent'>
    event.name          : button_press_event
    event.dblclick      : False
    event.button        : 3
    event.key           : control
    event.xdata         : 3.3548387096774195
    event.ydata         : 0.53125
    event.x             : 184
    event.y             : 342.0
    event.inaxes        : Axes(0.125,0.1;0.775x0.8)
    event.step          : 0
    event.guiEvent      : <tkinter.Event object at 0x7fd034aeeb70>

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

Обработка событий клавиатуры. Класс 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 описывают положение курсора мыши в пикселях относительно окна фигуры (окна с графиком) в момент возникновения события. Причем, координата (0, 0) соответствует точке левого нижнего угла области фигуры (не осей!).

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

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

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

**** button_press_event ****
    ...
    event.key           : control
    ...

**** button_press_event ****
    ...
    event.key           : ctrl+shift
    ...

**** button_press_event ****
    ...
    event.key           : h
    ...

**** button_press_event ****
    ...
    event.key           : ctrl+b
    ...
  • Свойство event.step - это целое значение, отличное от 0 при обработке события "scroll_event". Оно показывает направление вращения колесика мыши. Теоретически это значение может быть различным, но оно будет положительным при вращении колесика мыши от себя (вверх) и отрицательным при вращении колесика по направлению к себе (вниз). Хотя во всех экспериментах, которые я проводил у себя на компьютере, это значение принимало значения -1 или 1, но согласно документации это не обязательно.

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

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

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

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

# -*- 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)

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

    pylab.show()

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

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

# -*- 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)

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

    pylab.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() проживет совсем не долго и будет уничтожен сборщиком мусора.

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

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

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

import pylab
import numpy as np


marker = None


def onMouseClick(event):
    # type: (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 = u'({:.3f}; {:.3f})'.format(x, y)

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

    # Обновим график
    pylab.show()


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

    # Создадим окно с графиком
    fig = pylab.figure()

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

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

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

    pylab.show()

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

Другие статьи про Matplotlib

Немного рекламы

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

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



 17.12.2016 - 12:00

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

Jenyay 17.12.2016 - 16:22

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

Ant 12.07.2017 - 01:56

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


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