Search code examples
matplotlibplotwxpythonmouseevent

matplotlib pick_event returns incorrect pressed keys


I am writing a program that incorporates a matplotlib plot into a wxPython GUI. I would like to be able to differentiate between simple click, OPTION-click, COMMAND-click etc. on some of the plot elements (in order to do different things depending on which key was pressed while clicking on the plot element).

My code for this is as follows:

  • binding the matplotlib "pick_event" to the handler:

    self.figure.canvas.mpl_connect('pick_event', self.onClick)
    
  • in the handler, checking which/if any key was pressed

    def onClick(self, event):
        """handle clicking on different objects in the plotarea"""
        ## get relevant event data
        ## ... other stuff here ...
        pressedKey = None
        pressedKey = event.mouseevent.key    ## key pressed while clicking
        print "You pressed key '%s' while clicking!" % pressedKey
        ## ... more stuff here ...
        if pressedKey == "alt+alt":
            self.onOptionClick(...)
    
  • ...and then go on to other functions

My problem, however, is that what is returned by matplotlib as the pressed key is simply - wrong.

For example, when I open my program, plot my data, click on points (without any keys pressed), I keep getting "You pressed key 'ctrl+control' while clicking!". If I option-click on a data point, this then changes to a permanent "You pressed key 'alt+alt' while clicking!", regardless of whether I was pressing the option key or not. Only once I have command-clicked on a data point does it return the correct "You pressed key 'None' while clicking!" for simple clicks.

(Not to mention that the pressedKey returns are pretty unintuitive: Why "alt+alt" if I was simply pressing one "Alt/Option" key? Why "ctrl+control' for command?)

It is very important for the correct functioning of my program to be able to differentiate between different types of clicks.


UPDATE #1:

Oh dear. This is getting more and more confusing. My sample code below is doing fine, my main program still isn't. How could that be possible? Also, my little example below did alternate between "None" responses and "" responses for simple clicks. (I can't reproduce it; currently it's only giving me "None" responses - i.e. "You pressed key 'None' while clicking!")

Here's the example code:

#!/bin/usr/env python

import wx
import matplotlib as mpl
mpl.use('WXAgg')
from matplotlib.figure import Figure as mplFigure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as mplCanvas

class PlotPanel(wx.Panel):

    def __init__(self, parent):
        wx.Panel.__init__(self, parent)

        self.figure = mplFigure(figsize=(9, 6))
        self.ax = self.figure.add_subplot(111)
        self.ax.plot([1, 2, 3, 4], [2, 3, 5, 8], marker="o", markersize=20, picker=10, linestyle="None")
        self.canvas = mplCanvas(self, -1, self.figure)

        self.figure.canvas.mpl_connect('pick_event', self.onClick)

    def onClick(self, event):
        pressedKey = None
        pressedKey = event.mouseevent.key                           ## key pressed while clicking
        print "You pressed key '%s' while clicking!" % pressedKey

class MainFrame(wx.Frame):

    def __init__(self):
        wx.Frame.__init__(self, None, -1, "matplotlib pick_event problem")
        self.plotarea = PlotPanel(self)
        self.mainSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.mainSizer.Add(self.plotarea, 1, wx.EXPAND)
        self.SetSizer(self.mainSizer)
        self.mainSizer.Fit(self)

if __name__ == "__main__":

    app = wx.App(False)
    mainFrame = MainFrame()
    mainFrame.Show()
    app.MainLoop()

UPDATE #2:

OK, so the problem seems to be that my handlers sometimes open up other windows, and that the key release event gets "lost" in those windows. That is, matplotlib never gets to know that the key in question was released, and so on the next click, it is still under the impression that the key is pressed, even when it is not. If you change the handler above to

    def onClick(self, event):
        pressedKey = None
        pressedKey = event.mouseevent.key                           ## key pressed while clicking
        wx.MessageBox("You pressed key '%s' while clicking!" % pressedKey)

it actually reproduces the problem.

So I guess my question now becomes: How do I (manually) tell matplotlib that the key was released? "event.Skip()" is not working; python is telling me

"PickEvent instance has no attribute 'Skip'"

Solution

  • The easiest solution here is to forgo mouseevent.key and use wx.GetKeyState function:

    def onClick(self, event):
        print event
        keys = ""
        if wx.GetKeyState(wx.WXK_CONTROL):
            keys += "ctrl "
        if wx.GetKeyState(wx.WXK_ALT):
            keys += "alt "
        wx.MessageBox("You pressed key '%s' while clicking!" % keys)
    

    If this won't work for you, it's probably best to track the up and down key presses on your own. BUT, to do that, you need to do it in EVERY window that can get the focus while you're doing the tracking, which is a significant pain.

    Here's an example:

    class PlotPanel(wx.Panel):
    
        def __init__(self, parent):
            wx.Panel.__init__(self, parent)
    
            self.figure = mplFigure(figsize=(9, 6))
            self.ax = self.figure.add_subplot(111)
            self.ax.plot([1, 2, 3, 4], [2, 3, 5, 8], marker="o", markersize=20, picker=10, linestyle="None")
            self.canvas = mplCanvas(self, -1, self.figure)
    
            self.figure.canvas.mpl_connect('pick_event', self.onClick)
            self.canvas.Bind(wx.EVT_KEY_DOWN, self._on_key_down)
            self.canvas.Bind(wx.EVT_KEY_UP, self._on_key_up)
    
            self.states = {"cmd":False, "ctrl":False, "shift":False}
    
        def onClick(self, event):
            print event
            #print "You pressed key '%s' while clicking!" % pressedKey
            print "Pressed keys:", [k for k in self.states if self.states[k]]
            dlg = TestDialog(self)
            dlg.ShowModal()
    
        def _on_key_down(self, evt):
            self._set_state(evt)
            evt.Skip()
    
        def _on_key_up(self, evt):
            self._set_state(evt)
            evt.Skip()
    
        def _set_state(self, evt):
            self.states["cmd"] = evt.CmdDown()
            self.states["ctrl"] = evt.ControlDown()
            self.states["shift"] = evt.ShiftDown()
    
    class MainFrame(wx.Frame):
    
        def __init__(self):
            wx.Frame.__init__(self, None, -1, "matplotlib pick_event problem")
            self.plotarea = PlotPanel(self)
            self.mainSizer = wx.BoxSizer(wx.HORIZONTAL)
            self.mainSizer.Add(self.plotarea, 1, wx.EXPAND)
            self.SetSizer(self.mainSizer)
            self.mainSizer.Fit(self)
    
    class TestDialog(wx.Dialog):
    
        def __init__(self, parent):     
    
            pre = wx.PreDialog()
            pre.SetExtraStyle(wx.DIALOG_EX_CONTEXTHELP)
            pre.Create(parent, -1, "sample dialog", size=(200, 100), style=wx.CAPTION|wx.RESIZE_BORDER)
            self.PostCreate(pre)
    
            self.parent = parent
            self.Bind(wx.EVT_KEY_DOWN, self.parent._on_key_down)
            self.Bind(wx.EVT_KEY_UP, self.parent._on_key_up)
    
            btn = wx.Button(self, -1, "OK")
            btn.Bind(wx.EVT_BUTTON, self._OnClick)
    
        def _OnClick(self, evt):
            self.EndModal(wx.ID_OK)
    

    In general, I find the matplotlib's wx canvas to be very useful, but it's also not a complete solution for all corner cases.