Solved 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 ? :Dhere 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 theScrollAreaContainer
(which are QLabels) and change their text to let's sayoriginal_text+value_of_combox
- just as example.
I could do the following in thecombobox_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 ?
-
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! -
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 allpyqtSignal
? What I fail to see is really what should own what and what pattern to use -
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
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
-
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' ?
-
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?
likeclass 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 -
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
-
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...
- First of all is this way of building the app making sense? Am I using controller and model as you tried to explain me?
- I think the way that I create the children is wrong (for instance the
scroll_area
. I should use probably the same pattern as theMainView
with a - for example -CentralWidgetScrollAreaHeaderWidgetUi
class to set theCentralWidgetScrollAreaHeaderWidget
UI. - 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?
-
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)
-
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?