Solved Communication between parent/grandparent and children/grandchildren object - the proper way?
-
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?
-
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
-
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