web-dev-qa-db-fra.com

Conception MVC avec Qt Designer et PyQt / PySide

Débutant en Python provenant de Java (+ SWT/Windowbuilder) et j'ai du mal à trouver comment coder correctement une grande application de bureau en Python/Qt4 (QtDesigner)/PySide.

Je voudrais garder toute logique d'affichage dans une classe de contrôleur en dehors du fichier .ui (et c'est la conversion .py). Premièrement comme alors la logique est indépendante du framework GUI et deuxièmement, le fichier .ui et le fichier .py résultant sont écrasés sur toutes les modifications!.

Seuls les exemples que j'ai trouvés ajoutent du code d'action à un MainWindow.py monolithique (généré à partir de l'interface utilisateur) ou à MyForm.py (également généré à partir de .ui). Je ne vois aucun moyen de lier une classe de contrôleur POPO à des actions dans QtDesigner.

Quelqu'un peut-il m'orienter vers des flux de travail pour créer une application à grande échelle à l'aide de QtDesigner dans une méthodologie MVC/P évolutive?

25
Don Smythe

Tout d'abord, sachez que Qt utilise déjà le concept de vues et de modèles, mais ce n'est pas vraiment ce que vous recherchez. En bref, c'est un moyen de lier automatiquement un widget (par exemple, QListView) à une source de données (par exemple, QStringListModel) afin que les modifications apportées aux données du modèle apparaissent automatiquement dans le widget et vice versa. C'est une fonctionnalité utile, mais c'est différent d'une conception MVC à l'échelle d'une application, bien que les deux puissent être utilisés ensemble et offrent des raccourcis évidents. La conception du MVC à l'échelle de l'application doit cependant être programmée manuellement.

Voici un exemple d'application MVC qui a une vue, un contrôleur et un modèle uniques. La vue dispose de 3 widgets qui écoutent et réagissent chacun indépendamment aux modifications des données dans le modèle. La zone de sélection numérique et le bouton peuvent tous deux manipuler des données dans le modèle via le contrôleur.

mvc_app

La structure du fichier est organisée comme suit:

project/
    mvc_app.py              # main application with App class
    mvc_app_rc.py           # auto-generated resources file (using pyrcc.exe or equivalent)
    controllers/
        main_ctrl.py        # main controller with MainController class
        other_ctrl.py
    model/
        model.py            # model with Model class
    resources/
        mvc_app.qrc         # Qt resources file
        main_view.ui        # Qt designer files
        other_view.ui
        img/
            icon.png
    views/
        main_view.py        # main view with MainView class
        main_view_ui.py     # auto-generated ui file (using pyuic.exe or equivalent)
        other_view.py
        other_view_ui.py

Application

mvc_app.py Serait responsable de l'instanciation de chacune des vues, des contrôleurs et des modèles et de la transmission des références entre eux. Cela peut être assez minime:

import sys
from PyQt5.QtWidgets import QApplication
from model.model import Model
from controllers.main_ctrl import MainController
from views.main_view import MainView


class App(QApplication):
    def __init__(self, sys_argv):
        super(App, self).__init__(sys_argv)
        self.model = Model()
        self.main_controller = MainController(self.model)
        self.main_view = MainView(self.model, self.main_controller)
        self.main_view.show()


if __name__ == '__main__':
    app = App(sys.argv)
    sys.exit(app.exec_())

Vues

Utilisez Qt Designer pour créer les fichiers de disposition .ui dans la mesure où vous attribuez des noms de variables aux widgets et ajustez leurs propriétés de base. Ne vous embêtez pas à ajouter des signaux ou des emplacements car il est généralement plus facile de simplement les connecter aux fonctions de la classe view.

Les fichiers de disposition .ui sont convertis en fichiers de disposition .py lorsqu'ils sont traités avec pyuic ou pyside-uic. Les fichiers de vue .py peuvent ensuite importer les classes générées automatiquement pertinentes à partir des fichiers de disposition .py.

La ou les classes d'affichage doivent contenir le code minimal requis pour se connecter aux signaux provenant des widgets de votre mise en page. Les événements de vue peuvent appeler et transmettre des informations de base à une méthode de la classe de vue et à une méthode d'une classe de contrôleur, où toute logique devrait se trouver. Cela ressemblerait à quelque chose comme:

from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtCore import pyqtSlot
from views.main_view_ui import Ui_MainWindow


class MainView(QMainWindow):
    def __init__(self, model, main_controller):
        super().__init__()

        self._model = model
        self._main_controller = main_controller
        self._ui = Ui_MainWindow()
        self._ui.setupUi(self)

        # connect widgets to controller
        self._ui.spinBox_amount.valueChanged.connect(self._main_controller.change_amount)
        self._ui.pushButton_reset.clicked.connect(lambda: self._main_controller.change_amount(0))

        # listen for model event signals
        self._model.amount_changed.connect(self.on_amount_changed)
        self._model.even_odd_changed.connect(self.on_even_odd_changed)
        self._model.enable_reset_changed.connect(self.on_enable_reset_changed)

        # set a default value
        self._main_controller.change_amount(42)

    @pyqtSlot(int)
    def on_amount_changed(self, value):
        self._ui.spinBox_amount.setValue(value)

    @pyqtSlot(str)
    def on_even_odd_changed(self, value):
        self._ui.label_even_odd.setText(value)

    @pyqtSlot(bool)
    def on_enable_reset_changed(self, value):
        self._ui.pushButton_reset.setEnabled(value)

La vue ne fait pas grand-chose à part les événements de widget de lien vers la fonction de contrôleur appropriée et écoute les changements dans le modèle, qui sont émis sous forme de signaux Qt.

Contrôleurs

La ou les classes de contrôleur exécutent n'importe quelle logique, puis définissent les données dans le modèle. Un exemple:

from PyQt5.QtCore import QObject, pyqtSlot


class MainController(QObject):
    def __init__(self, model):
        super().__init__()

        self._model = model

    @pyqtSlot(int)
    def change_amount(self, value):
        self._model.amount = value

        # calculate even or odd
        self._model.even_odd = 'odd' if value % 2 else 'even'

        # calculate button enabled state
        self._model.enable_reset = True if value else False

La fonction change_amount Prend la nouvelle valeur du widget, exécute la logique et définit les attributs sur le modèle.

Modèle

La classe de modèle stocke les données et l'état du programme et une logique minimale pour annoncer les modifications apportées à ces données. Ce modèle ne doit pas être confondu avec le modèle Qt ( voir http://qt-project.org/doc/qt-4.8/model-view-programming.html ) car il n'est pas vraiment le même chose.

Le modèle pourrait ressembler à:

from PyQt5.QtCore import QObject, pyqtSignal


class Model(QObject):
    amount_changed = pyqtSignal(int)
    even_odd_changed = pyqtSignal(str)
    enable_reset_changed = pyqtSignal(bool)

    @property
    def amount(self):
        return self._amount

    @amount.setter
    def amount(self, value):
        self._amount = value
        self.amount_changed.emit(value)

    @property
    def even_odd(self):
        return self._even_odd

    @even_odd.setter
    def even_odd(self, value):
        self._even_odd = value
        self.even_odd_changed.emit(value)

    @property
    def enable_reset(self):
        return self._enable_reset

    @enable_reset.setter
    def enable_reset(self, value):
        self._enable_reset = value
        self.enable_reset_changed.emit(value)

    def __init__(self):
        super().__init__()

        self._amount = 0
        self._even_odd = ''
        self._enable_reset = False

Les écritures sur le modèle émettent automatiquement des signaux vers toutes les vues d'écoute via le code dans les fonctions décorées setter. Alternativement, le contrôleur peut déclencher manuellement le signal chaque fois qu'il le décide.

Dans le cas où les types de modèles Qt (par exemple QStringListModel) ont été connectés à un widget, la vue contenant ce widget n'a pas du tout besoin d'être mise à jour; cela se produit automatiquement via le framework Qt.

Fichier source de l'interface utilisateur

Pour terminer, l'exemple de fichier main_view.ui Est inclus ici:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>93</width>
    <height>86</height>
   </rect>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout">
    <item>
     <widget class="QSpinBox" name="spinBox_amount"/>
    </item>
    <item>
     <widget class="QLabel" name="label_even_odd"/>
    </item>
    <item>
     <widget class="QPushButton" name="pushButton_reset">
      <property name="enabled">
       <bool>false</bool>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

Il est converti en main_view_ui.py En appelant:

pyuic5 main_view.ui -o ..\views\main_view_ui.py

Le fichier de ressources mvc_app.qrc Est converti en mvc_app_rc.py En appelant:

pyrcc5 mvc_app.qrc -o ..\mvc_app_rc.py

Liens intéressants

Pourquoi Qt utilise-t-il mal la terminologie du modèle/vue?

53
101