Search code examples
python-2.7user-interfacematplotlibpyqt4

PyQt4 "mpl_connect" construct doesn't work through classes


I'm a new user of this stackoverflow community. I'm struggling from days with some issue in my code that I can't really figure out. In my following code I'm trying to define a custom selector tool for my GUI. I've reproduced the "bug" and the main structure of my program). I'm trying to connect a matplotlib signal with the "mpl_connect" structor, but unfortunately the signal of button clicked,released and mouse motion and the relative methods seems to be inactive for the canvas class. Here is the not-working code.

    import sys
    from PyQt4 import QtGui,QtCore
    from matplotlib.figure import Figure
    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
    from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar

    class Example(QtGui.QMainWindow):
        def __init__(self, parent=None):
            super(Example, self).__init__(parent)
            self.setupUi(self)

        def setupUi(self,parent):
            self.canvas=MyCanvas(self)
            self.toolbar=MyToolBar(self.canvas,self)
            self.addToolBar(QtCore.Qt.BottomToolBarArea,parent.toolbar)

    class MyCanvas(FigureCanvas):
        def __init__(self,parent):
            self.fig=Figure() 
            FigureCanvas.__init__(self, self.fig)
            FigureCanvas.setSizePolicy(self,
                                   QtGui.QSizePolicy.Expanding,
                                   QtGui.QSizePolicy.Expanding)
            self.setParent(parent)

    class MyToolBar(QtGui.QToolBar):
        def __init__(self,canvas,parent):
            super(MyToolBar,self).__init__(parent)
            #
            self.PICK_act=QtGui.QAction("PUSH ME!!",parent,checkable=True)
            self.PICK_act.toggled.connect(lambda: self.pickData(canvas))
            #
            self.addAction(self.PICK_act)
            self.addSeparator()
            ## Creating the matplotlib toolbar
            self.mpl_tool=NavigationToolbar(canvas,parent)
            ## Merge the two toolbar 
            self.addWidget(self.mpl_tool)

        def pickData(self,canvas):
            P=Picker(canvas)     
            if self.PICK_act.isChecked():
                print "CHECKED"
                P._activation(True)
            else:
                print "NOT CHECKED" 
                P._activation(False)
                
    class Picker(object):
        def __init__(self,canvas):
            self.index=None
            self.is_pressed=None     
            # To define the event of PickData_ACTION
            self.canvas=canvas 
            ###
            self.selPressEvent=None      
            self.selReleaseEvent=None
            self.selMoveEvent=None           
        
        def _activation(self,condition):
            if condition==True:
                self.selPressEvent=self.canvas.mpl_connect('button_press_event',self.onpress)
                self.selReleaseEvent=self.canvas.mpl_connect('button_release_event',self.onrelease)
                self.selMoveEvent=self.canvas.mpl_connect('motion_notify_event',self.onmotion)
                print "Picker ON"
                return True
            else:
                self.canvas.mpl_disconnect(self.selPressEvent)
                self.canvas.mpl_disconnect(self.selReleaseEvent)
                self.canvas.mpl_disconnect(self.selMoveEvent)
                print "Picker OFF"
                return False

        def onpress(self,event):
            print "..clicked"
            self.x0 = event.xdata
            self.y0 = event.ydata
            self.is_pressed=True
                
        def onrelease(self,event):
            print "...released"
            self.x1 = event.xdata
            self.y1 = event.ydata
            self.is_pressed=False

        def onmotion(self,event):
            if self.is_pressed==True:
                print "moving"
            
    # =========================================================================        
    def run():
        App=QtGui.QApplication(sys.argv)
        GUI=Example()
        GUI.show()
        sys.exit(App.exec_())
    #
    run()    

If instead I define the same methods inside the "MyCanvas" class, they'll work. The working code is this:

    import sys
    from PyQt4 import QtGui,QtCore
    from matplotlib.figure import Figure
    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
    from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar

    class Example(QtGui.QMainWindow):
        def __init__(self, parent=None):
            super(Example, self).__init__(parent)
            self.setupUi(self)

        def setupUi(self,parent):
            self.canvas=MyCanvas(self)
            self.toolbar=MyToolBar(self.canvas,self)
            self.addToolBar(QtCore.Qt.BottomToolBarArea,parent.toolbar)

    class MyCanvas(FigureCanvas):
        def __init__(self,parent):
            self.fig=Figure() 
            FigureCanvas.__init__(self, self.fig)
            FigureCanvas.setSizePolicy(self,
                                   QtGui.QSizePolicy.Expanding,
                                   QtGui.QSizePolicy.Expanding)
            self.setParent(parent)
            ###
            self.selPressEvent=None      
            self.selReleaseEvent=None
            self.selMoveEvent=None          

        def onpress(self,event):
            print "MyCanvas::onpress ---> clicked"
            
        def onrelease(self,event):
            print "MyCanvas::onrelease ---> release"

        def onmotion(self,event):
            print "MyCanvas::onmotion ---> motion"                
            
    class MyToolBar(QtGui.QToolBar):
        def __init__(self,canvas,parent):
            super(MyToolBar,self).__init__(parent)
            #
            self.PICK_act=QtGui.QAction("PUSH ME!!",parent,checkable=True)
            self.PICK_act.toggled.connect(lambda: self.pickData(canvas))
            #
            self.addAction(self.PICK_act)
            self.addSeparator()
            ## Creating the matplotlib toolbar
            self.mpl_tool=NavigationToolbar(canvas,parent)
            ## Merge the two toolbar 
            self.addWidget(self.mpl_tool)

        def pickData(self,canvas):
            P=Picker(canvas)     
            if self.PICK_act.isChecked():
                print "CHECKED"
                P._activation(True)
            else:
                print "NOT CHECKED" 
                P._activation(False)
                
    class Picker(object):
        def __init__(self,canvas):
            self.index=None
            self.is_pressed=None     
            # To define the event of PickData_ACTION
            self.canvas=canvas
           
        
        def _activation(self,condition):
            if condition==True:
                self.canvas.selPressEvent=self.canvas.mpl_connect('button_press_event',self.canvas.onpress)
                self.canvas.selReleaseEvent=self.canvas.mpl_connect('button_release_event',self.canvas.onrelease)
                self.canvas.selMoveEvent=self.canvas.mpl_connect('motion_notify_event',self.canvas.onmotion)
                print "Picker ON"
                return True
            else:
                self.canvas.mpl_disconnect(self.canvas.selPressEvent)
                self.canvas.mpl_disconnect(self.canvas.selReleaseEvent)
                self.canvas.mpl_disconnect(self.canvas.selMoveEvent)
                print "Picker OFF"
                return False

        def onpress(self,event):
            print "..clicked"
            self.x0 = event.xdata
            self.y0 = event.ydata
            self.is_pressed=True
                
        def onrelease(self,event):
            print "...released"
            self.x1 = event.xdata
            self.y1 = event.ydata
            self.is_pressed=False

        def onmotion(self,event):
            if self.is_pressed==True:
                print "moving"
            
    # =========================================================================        
    def run():
        App=QtGui.QApplication(sys.argv)
        GUI=Example()
        GUI.show()
        sys.exit(App.exec_())
    #
    run()
        

I need to keep the "Picker" and the relative methods separated from "MyCanvas" class for the sake of clarity. But I really can't understand what's going wrong in my first code: the signal seems to be emitted but not received. No error reported in STDERR. I think I've correctly respect the scoping of classes/methods.


Solution

  • The problem is that the mpl callback registry only holds weak references to the callbacks it is handed (the logic being that if you are not careful about holding onto the tokens you get back from mpl_connect you could end up with objects that you have no way to get a ref to, but will never be gc'd because mpl is holding a hard reference). So what is happening is that you are hooking up a methods from your Picker object to the callback system, it is going out of scope, getting gc'd and then when mpl goes to run the callbacks, it discovers the object is gone and automatically removes the defunct functions.

    All you have to do is hold onto a ref to the Picker object in your toolbar class.

    This should probably be more clearly documented upstream.