Search code examples
pythonpyqt4signals-slots

Avoiding a circular import when one module has the signal and another the slot and arguments need to be passed


mouseClickEvent is in ViewBoxCustom.py and is correctly triggered when you click the scene. compute_spc_map is in SPCanalyse.py and ViewBoxCustom.py doesn't import it as that would make a cyclic import. I know it can be done, but I want to avoid it.

snippet from ViewBoxCustom

from SPCanalyse import MainWindow #This import should not exist
def mouseClickEvent(self, ev):
    elif ev.button() == QtCore.Qt.LeftButton:
        ev.accept()
        # The following line does not work because MainWindow is **NOT** imported 
        MainWindow.compute_spc_map(ev.pos().x(), ev.pos().y())

snippet from SPCanalyse. SPCanalyse imports ViewBoxCustom so as to access functions and generate app functionality

from ViewBoxCustom import MultiRoiViewBox # MultiRoiViewBox contains mouseClickEvent
def compute_spc_map(self, x, y):
    if not self.preprocessed_frames == None:
        self.image = fj.get_correlation_map(y, x, self.preprocessed_frames)

I cant just place compute_spc_map in ViewBoxCustom because preprocessed_frames is a variable generated and used in SPCanalyse

I thought it might work to connect the mouseClickEvent in ViewBoxCustom with compute_spc_map in SPCanalyse doing the following in SPCanalyse

from ViewBoxCustom import MultiRoiViewBox
self.vb = MultiRoiViewBox(lockAspect=True,enableMenu=True)
self.vb.mouseClickEvent.connect(self.compute_spc_map)

sadly mouseClickEvent has no attribute 'connect'


Solution

  • It looks like you're trying to call a method on a parent widget from a child widget (ie. ViewBoxCustom is a child of MainWindow). You generally don't want to do this as it limits the flexibility of the child widget and can lead to circular dependencies like you have here when there's no reason the child should have to depend on the parent.

    A good design pattern to use here is for the child to emit a Signal, which the parent connects to, and uses it to trigger the calling of a function (as opposed to the child widget calling the function directly).

    In your case:

    class ViewBoxCustom(...)
        clicked = QtCore.pyqtSignal(int, int)
    
        def mouseClickEvent(self, ev):
            if ev.button() == QtCore.Qt.LeftButton:
                ev.accept()
                self.clicked.emit(ev.pos().x(), ev.pos().y())
    
    
    class MainWindow(QtGui.QMainWindow):
    
        def __init__(...)
            ...
            self.vbc = ViewBoxCustom(...)
            self.vbc.clicked.connect(self.on_vbc_clicked)
    
        @QtCore.pyqtSlot(int, int)
        def on_vbc_clicked(self, x, y):
            self.compute_spec_map(x, y)
    

    There's no reason for ViewBoxCustom to ever import or know anything about MainWindow.