Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Communication between parent/grandparent and children/grandchildren object - the proper way?



  • Hello,
    I am trying to communicate from a child of a child of a child to up the grand grand parent and then some child to redraw some widget content and I am wondering what is the best method to do so, and how to implement it? Until now I was using self.parent().parent().parent()... but it seems horrible to maintain etc... So I was thinking of signals, is that the right choice, if so how ? :D

    here is a short version of the objects that I have:

    #!/usr/bin/env python3
    import sys
    import signal
    
    from PyQt5.QtCore import QCoreApplication
    from PyQt5.QtGui import QIcon
    from PyQt5.QtWidgets import (
        QAction,
        QApplication,
        QComboBox,
        QDialog,
        QFrame,
        QGridLayout,
        QGroupBox,
        QLabel,
        QMainWindow,
        QMenu,
        QScrollArea,
        QSystemTrayIcon,
        QVBoxLayout,
        QWidget,
        qApp,
    )
    
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            icon = QIcon('./do/statics/images/icon.ico')
            self.setWindowIcon(icon)
            SystemTrayIcon(icon=icon, parent=self)
            self.content_widget = ContentWidget(parent=self)
    
    
    class SystemTrayIcon(QSystemTrayIcon):
        def __init__(self, icon, parent=None):
            super().__init__(icon=icon, parent=parent)
            self.dialog = SystemTrayIconSettingsDialog(parent=self.parent())
            self.dialog.hide()
            menu = QMenu()
            some_action = QAction('&Some Action', parent=menu)
            some_action.triggered.connect(self.some_action)
            menu.addAction(some_action)
            self.setContextMenu(menu)
            self.show()
    
        def some_action(self):
            # emit a signal to change some widget
            self.dialog.show()
    
    
    class SystemTrayIconSettingsDialog(QDialog):
        def __init__(self, parent):
            super().__init__(parent=parent)
            layout = QVBoxLayout()
            group_box = QGroupBox('General Options', parent=self)
            layout.addWidget(group_box)
            self.setLayout(layout)
    
            combobox = QComboBox(parent=group_box)
            combobox.addItems(['option1', 'option2'])
            combobox.currentTextChanged.connect(self.combobox_text_changed)
    
            group_box_layout = QGridLayout()
            group_box_layout.addWidget(combobox, 0, 0)
            group_box.setLayout(group_box_layout)
    
        def combobox_text_changed(self, text):
            # 1. save the value in QSettings
            # 2. Communicate to mainWindow to redraw the widget_I_want_to_change
            # from ContentWidget's scroll_area
            pass
    
    
    class ContentWidget(QFrame):
        def __init__(self,parent):
            super().__init__(parent)
            layout = QVBoxLayout()
            self.setMinimumHeight(140)
            self.setLayout(layout)
            scroll_area = QScrollArea(parent=self)
            scroll_area.setMinimumHeight(100)
            scroll_area.setWidgetResizable(True)
            self.layout().addWidget(scroll_area)
            self.container = ScrollAreaContainer(parent=scroll_area)
            scroll_area.setWidget(container)
            for i in range(10):
                # Those are the one I woud like to change
                widget_I_want_to_change = QLabel('test_%s' % i)
                self.container.layout().addWidget(widget_I_want_to_change)
    
    
    class ScrollAreaContainer(QWidget):
        def __init__(self, parent):
            super().__init__(parent=parent)
            layout = QVBoxLayout()
            self.setLayout(layout)
    
    
    def exit_app(p1=None, p2=None):
        qApp.exit()
        sys.exit()
    
    
    if __name__ == '__main__':
        signal.signal(signal.SIGINT, exit_app)
        QCoreApplication.setOrganizationDomain('test')
        QCoreApplication.setApplicationName('test')
        app = QApplication(sys.argv)
        app.setQuitOnLastWindowClosed(True)
        main_window = MainWindow()
        main_window.show()
        sys.exit(app.exec_())
    
    

    So in the combobox_text_changed I would like to get an (or all) item(s) from the ScrollAreaContainer (which are QLabels) and change their text to let's say original_text+value_of_combox - just as example.
    I could do the following in the combobox_text_changed:

    for i in reversed(range(self.parent().container.layout().count())):
            widget = self.scroll_area.content_widget.container.layout().itemAt(i).widget()
            widget.setText('balblalb')
    

    But it is really ugly and not maintainable

    I believe the signals are a better solution but I would really like to avoid to chain them, as, as I understand I would need to emit and catch that signal in each upper level objects and then forward to each children until I reach my object, this would also not be really nice.

    What would be the best pattern to use here please ?


  • Lifetime Qt Champion

    No, you should not.

    Take for example QDockWidget, it provides a toggle action that allows to create buttons, menus, etc and switch its visibility.

    As I already said, child, sub-child, etc shall not care about their parents. The parents knows what to do with their children, not the other way around. Hence the importance of defining proper APIs for your widgets.



  • @Tyskie said in Communication between parent/grandparent and children/grandchildren object - the proper way?:

    I believe the signals are a better solution but I would really like to avoid to chain them, as, as I understand I would need to emit and catch that signal in each upper level objects and then forward to each children until I reach my object, this would also not be really nice.

    I would be using signals/slots. It's not true to say " I would need to emit and catch that signal in each upper level objects and then forward to each children until I reach my object". That does apply to your "direct" code with self.parent().parent().parent(), but there is no reason why a (great-)grandparent cannot connect to a (great-)grandchild signal, or vice versa. The point of signals/slots is that they allow communication between non-parental-child objects, so sounds like you might want to make use of them.



  • @JonB hey, oh yeah I want to use them for sure! That was simply my understanding that I would have to chain them.
    I would be glad if you could show me how to use signal without chaining if you can!


  • Lifetime Qt Champion

    Hi,

    Can you explain a bit more what you need to do ?

    Typically child widgets shall not care about their ancestors. It's rather the ancestors that knows what they want to do with their children.

    You can emit signals and chain them up several layers if needed. It's called signal forwarding.

    What is usually a good thing to do is to give a widget a proper API to do something. What then happens inside that widget is of no concern to the parent widget.



  • @SGaist Hi,
    The application I am doing is an overlay, that should stay on top of all windows and display some information etc..
    So I decided to go with 1 mainwindow that stay hidden as the parent of all (systemtray, the overlay widget, a qframe, etc).

    From the systemtray I have an action that shows a QDialog with settings of the overlay and its children, like border color of element in the overlay, text color etc.

    What I need to do is that from the QDialog settings (QCombobox, checkbox etc) element's events I want to change live those settings. So it would mean get that object and apply new css/properties, to do so I would need to get back to the mainwindow, and finally access the overlay qframe and them looking for those children.

    Hoping it is clear :D

    About the signal forwarding, would it ok to have a dedicated object too comunicate between parent-child? like ParentChildCommunicator class that would have all pyqtSignal? What I fail to see is really what should own what and what pattern to use


  • Lifetime Qt Champion

    One way to do it is to have a controller class that allows you to decouple objects from settings GUI.



  • @SGaist that sounds great, would you know where I can find some example maybe?



  • I tried to represent it as close as possible from what I have - it is probably far from perfect and maybe totally wrong? (maybe that is why I struggle :D ) but here is the speudo schema of the app structure, each card is a Class, hopefully it makes sense
    schema.png

    So first of all I noticed that I dont need a QMainWindow, as I only need a systemtrayicon, a Qframe - that serves my overlay (maybe this could be a QMainWindow instead of QFrame?) - and QDialog for the settings.
    I also added an AppController to hold them all in one object in order to have a clean entrypoint script to start the app.

    My goal is to do the red arrows, change the design of the 3 widgets: user nickname, user avatar, and users container on any settings change from the combobox events.

    What/How would you achieve that ?

    PS: I am a still a newbie so be forgiving :D

    Thanks for the help !

    Edit 1: also I do not use QTCreator


  • Lifetime Qt Champion

    You can have a "settings controller" that will handle the QSettings load and store part.

    If will provide the properties that the other objects will connect to.

    Setup your various elements and once done load the settings through the controller that will emit all the required signals so your GUI will automatically get setup based on the settings.



  • @SGaist Does it mean that I would need to instanciate all my objects with that 'settings controller' ?


  • Lifetime Qt Champion

    Not at all.

    You have one controller and you connect your various widgets to it.



  • I am not sure that I understand how to connect them with that controller and where should my controller be instancied, would I need to add all the widget as property of that controller?
    like

    class SomeController:
        def __init__(self):
            self.widget_1 = ...
            self.widget_2 = ...
            self.widget_3 = ...
           etc
    

    and have it instanciated in the AppController?
    Do you maybe know some article where I could see/learn the controller pattern thingy? I am not sure how to arrange the classes


  • Lifetime Qt Champion

    No, the controller does not care about any widgets.

    It will be instanciated at the same level as the widgets. There you use the usual signals and slots to put things together.



  • what if a child widget is instanciated in a parent widget? like the UserContainer which is instanciated in the ScrollArea in my example? do I need to create 2 instance of that controller? one for each widget? sorry I totally lost


  • Lifetime Qt Champion

    No, the controller is a central component.

    Typically, your UserWidget will be connected to the controller and will update its child widgets.



  • @SGaist thanks for all the efforts and help!
    After some trial and errors. I think I start to see the big picture (hopefully !). I ended up, for now, having a QApplication which instanciate a model and controller with the model passed as argument and providing them to the view. Like so:

    class App(QApplication):
        def __init__(self):
            super().__init__([])
            preload_resources() # qrc resource compiled
            self.model = MainModel()
            self.main_controller = MainController(self.model)
            self.system_tray_view = SystemTrayView(
                controller=self.main_controller,
                model=self.model
            )
            self.main_view = MainView(
                controller=self.main_controller,
                model=self.model,
            )
    

    Then in the MainView I have (this is where my brain start to melt :D):

    class MainView(QMainWindow):
        def __init__(self, controller, model):
            super().__init__()
            self._controller = controller
            self._model = model
            self._ui = MainViewUi()
            self._ui.setupUi(self) # this is adding all the children widgets
            self._model.users_changed.connect(self.on_users_changed)
    
        def on_users_changed(self, value):
            logging.debug('redrawing users %s', value)
    

    example of the MainViewUi

    class MainViewUi():
        def setupUi(self, main_window):
            self.centralwidget = QFrame(parent=main_window)
            centralwidget_layout = QVBoxLayout()
            self.scroll_area = CentralWidgetScrollArea(parent=self.centralwidget)
            self.scroll_area_header = CentralWidgetScrollAreaHeaderWidget(
                parent=self.centralwidget
            )
            # ... more widgets to add to the scroll area, 
            # and more widgets to add to those widgets..
            # see question 2.
            centralwidget_layout.addWidget(self.scroll_area_header)
            centralwidget_layout.addWidget(self.scroll_area)
            self.centralwidget.setLayout(centralwidget_layout)
            main_window.setCentralWidget(self.centralwidget)
    

    as for the model and controller, nothing fancy, the controller has some pyqtSlot and the model some signal + property/setter that emit to those signal.

    But ! I have still another 3 questions on somethings that I am wondering...

    1. First of all is this way of building the app making sense? Am I using controller and model as you tried to explain me?
    2. I think the way that I create the children is wrong (for instance the scroll_area . I should use probably the same pattern as the MainView with a - for example - CentralWidgetScrollAreaHeaderWidgetUi class to set the CentralWidgetScrollAreaHeaderWidget UI.
    3. About accessing model and controller in the children, should I pass the model/controller to the children too? like:
    self.scroll_area = CentralWidgetScrollArea(
        parent=self.centralwidget, 
        model=main_window._model,
        controller=main_window._controller
    )
    

    if not how could I access those otherwise?


  • Lifetime Qt Champion

    There's really only rare cases where subclassing QApplication makes sense. Your application does not belong to such a case.

    What is that model you have ?

    As an example:

    QApplication app(sys.argv)
    controller = MainController()
    
    # widgets creation
    systray = MySystray()
    settings_widget = SettingsWidget()
    
    # connections
    systray.settingsRequested.connect(settings_widget.open)
    settings_widget.userNameChanged.connect(controller.setUserName)
    
    controller.somethingChanged.connect(systray.showMessage)
    controller.userNameChanged.connect(settings_widget.setUserName)
    
    controller.loadSettings()
    sys.exit(app.exec())
    


  • @SGaist said in Communication between parent/grandparent and children/grandchildren object - the proper way?:

    There's really only rare cases where subclassing QApplication makes sense.

    I am using click for the entrypoint and I like to have short lisible code, then I can have a clean start script:

    [...]
    from my_package import App
    
    @click.command()
    @click.option('--debug', is_flag=True, default=False)
    def main(debug=False):
        if debug:
            logger = logging.getLogger()
            logger.setLevel(logging.DEBUG)
    
        app = App(
            organization_domain=ORGANIZATION_DOMAIN,
            application_name=APPLICATION_NAME
        )
        sys.exit(app.exec())
    

    @SGaist said in Communication between parent/grandparent and children/grandchildren object - the proper way?:

    What is that model you have ?

    this is my Controller and Model:

    class MainController(QObject):
        def __init__(self, model):
            super().__init__()
            self._model = model
    
        @pyqtSlot()
        def quit_app(self):
            qApp.quit()
    
        @pyqtSlot()
        def show_settings_dialog(self):
            # not sure what to put there yet as I need to start a QDialog from somewhere
            pass
    
        @pyqtSlot()
        def toggle_main_window(self):
            # not sure what to put there yet too as I need to access MainWindow to hide it
            pass
    
    class MainModel(QObject):
        users_changed = pyqtSignal(list)
    
        def __init__(self):
            super().__init__()
            self._users = []
    
        @property
        def users(self):
            return self._users
    
        def add_user(self, value):
            logging.debug('adding user %s', value)
            self._users.append(value)
            self.users_changed.emit(self._users)
    
        def delete_user(self, value):
            logging.debug('deleting user %s', value)
            del self._users[value]
            self.users_changed.emit(self._users)
    
    

  • Lifetime Qt Champion

    I understand that you want short code (readable is a must in any case) but you are offloading that the wrong way.

    As I said, subclassing QApplication is not the good option.

    Also, your controller should not be in charge of managing widget.

    You should approach that the following way:

    • GUI
    • Controller
    • Data/Device

    Your controller is a central point but does not care what is connected to it. It just control some stuff. The fact that a widget is shown is on the GUI layer.

    Since you have a model, are you trying to implement the MVC pattern ?



  • Since you have a model, are you trying to implement the MVC pattern ?

    Yes I think I do, even if the app is really small, I want to learn. And I am also pretty sure it would solve my original issue with parent/child interaction on the UI without all those injections.

    Your controller is a central point but does not care what is connected to it. It just control some stuff. The fact that a widget is shown is on the GUI layer.

    So in order to show or hide the MainWindow, I would have keep the state of my window visibility in the mode. Then on the event from a button to show I should emit() to the model signal and the MainWindow will have the .connect() on that model signal to watch the change of visibility. did I got it right this time?


  • Lifetime Qt Champion

    Usually the model is purely about data not application state.

    For switching the main window visibility you would have for example a checkbox associated with the visible property of the main window.



  • Yeah but if this checkbox is in a child of a child, then I would have to use the parent().parent() to get to main_window object and i am back to my problem


  • Lifetime Qt Champion

    No, you should not.

    Take for example QDockWidget, it provides a toggle action that allows to create buttons, menus, etc and switch its visibility.

    As I already said, child, sub-child, etc shall not care about their parents. The parents knows what to do with their children, not the other way around. Hence the importance of defining proper APIs for your widgets.



  • omg thanks I think I get the thing now, I could just have signal in my widget... and connect parent to them or from controller what ever needs to emit that, I guess :D


Log in to reply