Comment dissocier correctement l'interface utilisateur de la logique sur les applications Pyqt / Qt?


20

J'ai lu beaucoup de choses sur ce sujet dans le passé et j'ai regardé des discussions intéressantes comme celle-ci de l' oncle Bob . Pourtant, je trouve toujours assez difficile d'architecturer correctement mes applications de bureau et de distinguer quelles devraient être les responsabilités du côté de l' interface utilisateur et celles du côté logique .

Un très bref résumé des bonnes pratiques ressemble à ceci. Vous devez concevoir votre logique découplée de l'interface utilisateur, de sorte que vous puissiez utiliser (théoriquement) votre bibliothèque quel que soit le type de framework backend / UI. Cela signifie essentiellement que l'interface utilisateur doit être aussi fictive que possible et que le traitement lourd doit être effectué du côté logique. Autrement dit, je pourrais littéralement utiliser ma jolie bibliothèque avec une application console, une application web ou une application de bureau.

De plus, l'oncle Bob suggère des discussions différentes sur la technologie à utiliser qui vous donnera beaucoup d'avantages (bonnes interfaces), ce concept de report vous permet d'avoir des entités bien testées très découplées, qui sonnent bien mais qui sont toujours délicates.

Donc, je sais que cette question est une question assez large qui a été discutée à plusieurs reprises sur Internet et dans de nombreux bons livres. Donc, pour en tirer quelque chose de bien, je posterai un très petit exemple factice essayant d'utiliser MCV sur pyqt:

import sys
import os
import random

from PyQt5 import QtWidgets
from PyQt5 import QtGui
from PyQt5 import QtCore

random.seed(1)


class Model(QtCore.QObject):

    item_added = QtCore.pyqtSignal(int)
    item_removed = QtCore.pyqtSignal(int)

    def __init__(self):
        super().__init__()
        self.items = {}

    def add_item(self):
        guid = random.randint(0, 10000)
        new_item = {
            "pos": [random.randint(50, 100), random.randint(50, 100)]
        }
        self.items[guid] = new_item
        self.item_added.emit(guid)

    def remove_item(self):
        list_keys = list(self.items.keys())

        if len(list_keys) == 0:
            self.item_removed.emit(-1)
            return

        guid = random.choice(list_keys)
        self.item_removed.emit(guid)
        del self.items[guid]


class View1():

    def __init__(self, main_window):
        self.main_window = main_window

        view = QtWidgets.QGraphicsView()
        self.scene = QtWidgets.QGraphicsScene(None)
        self.scene.addText("Hello, world!")

        view.setScene(self.scene)
        view.setStyleSheet("background-color: red;")

        main_window.setCentralWidget(view)


class View2():

    add_item = QtCore.pyqtSignal(int)
    remove_item = QtCore.pyqtSignal(int)

    def __init__(self, main_window):
        self.main_window = main_window

        button_add = QtWidgets.QPushButton("Add")
        button_remove = QtWidgets.QPushButton("Remove")
        vbl = QtWidgets.QVBoxLayout()
        vbl.addWidget(button_add)
        vbl.addWidget(button_remove)
        view = QtWidgets.QWidget()
        view.setLayout(vbl)

        view_dock = QtWidgets.QDockWidget('View2', main_window)
        view_dock.setWidget(view)

        main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, view_dock)

        model = main_window.model
        button_add.clicked.connect(model.add_item)
        button_remove.clicked.connect(model.remove_item)


class Controller():

    def __init__(self, main_window):
        self.main_window = main_window

    def on_item_added(self, guid):
        view1 = self.main_window.view1
        model = self.main_window.model

        print("item guid={0} added".format(guid))
        item = model.items[guid]
        x, y = item["pos"]
        graphics_item = QtWidgets.QGraphicsEllipseItem(x, y, 60, 40)
        item["graphics_item"] = graphics_item
        view1.scene.addItem(graphics_item)

    def on_item_removed(self, guid):
        if guid < 0:
            print("global cache of items is empty")
        else:
            view1 = self.main_window.view1
            model = self.main_window.model

            item = model.items[guid]
            x, y = item["pos"]
            graphics_item = item["graphics_item"]
            view1.scene.removeItem(graphics_item)
            print("item guid={0} removed".format(guid))


class MainWindow(QtWidgets.QMainWindow):

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

        # (M)odel ===> Model/Library containing should be UI agnostic, right now it's not
        self.model = Model()

        # (V)iew      ===> Coupled to UI
        self.view1 = View1(self)
        self.view2 = View2(self)

        # (C)ontroller ==> Coupled to UI
        self.controller = Controller(self)

        self.attach_views_to_model()

    def attach_views_to_model(self):
        self.model.item_added.connect(self.controller.on_item_added)
        self.model.item_removed.connect(self.controller.on_item_removed)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)

    form = MainWindow()
    form.setMinimumSize(800, 600)
    form.show()
    sys.exit(app.exec_())

L'extrait ci-dessus contient de nombreux défauts, le plus évident étant le modèle couplé au cadre de l'interface utilisateur (QObject, signaux pyqt). Je sais que l'exemple est vraiment factice et vous pouvez le coder sur quelques lignes en utilisant un seul QMainWindow mais mon but est de comprendre comment architecturer correctement une application pyqt plus grande.

QUESTION

Comment concevriez-vous correctement une grande application PyQt utilisant MVC en suivant les bonnes pratiques générales?

LES RÉFÉRENCES

J'ai posé une question similaire à celle-ci ici

Réponses:


1

Je viens d'un arrière-plan (principalement) WPF / ASP.NET et j'essaie de créer une application PyQT MVC-ish en ce moment et cette question me hante. Je partagerai ce que je fais et je serais curieux d'avoir des commentaires constructifs ou des critiques.

Voici un petit diagramme ASCII:

View                          Controller             Model
---------------
| QMainWindow |   ---------> controller.py <----   Dictionary containing:
---------------   Add, remove from View                |
       |                                               |
    QWidget       Restore elements from Model       UIElementId + data
       |                                               |
    QWidget                                         UIElementId + data
       |                                               |
    QWidget                                         UIElementId + data
      ...

Mon application a beaucoup (BEAUCOUP) d'éléments d'interface utilisateur et de widgets qui doivent être facilement modifiés par un certain nombre de programmeurs. Le code "view" se compose d'un QMainWindow avec un QTreeWidget contenant des éléments qui sont affichés par un QStackedWidget à droite (pensez à la vue Master-Detail).

Étant donné que des éléments peuvent être ajoutés et supprimés dynamiquement de QTreeWidget et que j'aimerais prendre en charge la fonctionnalité d'annulation-redo, j'ai choisi de créer un modèle qui garde la trace des états actuels / antérieurs. Les commandes de l'interface utilisateur transmettent des informations au modèle (ajout ou suppression d'un widget, mise à jour des informations dans un widget) par le contrôleur. La seule fois où le contrôleur transmet des informations à l'interface utilisateur est sur la validation, la gestion des événements et le chargement d'un fichier / annuler et rétablir.

Le modèle lui-même est composé d'un dictionnaire de l'ID d'élément d'interface utilisateur avec la dernière valeur qu'il contenait (et de quelques informations supplémentaires). Je garde une liste des dictionnaires antérieurs et je peux revenir à un précédent si quelqu'un frappe annuler. Finalement, le modèle est vidé sur le disque sous un certain format de fichier.

Je vais être honnête - j'ai trouvé cela assez difficile à concevoir. PyQT n'a pas l'impression qu'il se prête bien à être séparé du modèle, et je n'ai pas vraiment pu trouver de programmes open source essayant de faire quelque chose de similaire. Curieux de voir comment d'autres personnes ont abordé cette question.

PS: Je réalise que QML est une option pour faire du MVC, et cela semblait attrayant jusqu'à ce que je réalise à quel point Javascript était impliqué - et le fait qu'il est encore assez immature en termes de portage sur PyQT (ou tout simplement période). Les facteurs compliquant l'absence d'excellents outils de débogage (assez dur avec juste PyQT) et la nécessité pour d'autres programmeurs de modifier facilement ce code qui ne connaissent pas JS.


0

Je voulais créer une application. J'ai commencé à écrire des fonctions individuelles qui faisaient de petites tâches (chercher quelque chose dans la base de données, calculer quelque chose, chercher un utilisateur avec la saisie semi-automatique). Affiché sur le terminal. Ensuite, mettez ces méthodes dans un fichier,main.py ..

Ensuite, je voulais ajouter une interface utilisateur. J'ai regardé différents outils et me suis contenté de Qt. J'ai utilisé Creator pour créer l'interface utilisateur, puis pyuic4pour générerUI.py .

Dans main.py, j'ai importéUI . Ensuite, ajouté les méthodes déclenchées par les événements d'interface utilisateur en plus de la fonctionnalité de base (littéralement en haut: le code "de base" est en bas du fichier et n'a rien à voir avec l'interface utilisateur, vous pouvez l'utiliser à partir du shell si vous le souhaitez à).

Voici un exemple de méthode display_suppliers qui affiche une liste de fournisseurs (champs: nom, compte) sur une table. (J'ai coupé cela du reste du code juste pour illustrer la structure).

Au fur et à mesure que l'utilisateur tape dans le champ de texte HSGsupplierNameEdit, le texte change et chaque fois qu'il le fait, cette méthode est appelée afin que la table change en tant que types d'utilisateur.

Il obtient les fournisseurs d'une méthode appelée get_suppliers(opchoice)qui est indépendante de l'interface utilisateur et fonctionne également à partir de la console.

from PyQt4 import QtCore, QtGui
import UI

class Treasury(QtGui.QMainWindow):

    def __init__(self, parent=None):
        self.ui = UI.Ui_MainWindow()
        self.ui.setupUi(self)
        self.ui.HSGsuppliersTable.resizeColumnsToContents()
        self.ui.HSGsupplierNameEdit.textChanged.connect(self.display_suppliers)

    @QtCore.pyqtSlot()
    def display_suppliers(self):

        """
            Display list of HSG suppliers in a Table.
        """
        # TODO: Refactor this code and make it generic
        #       to display a list on chosen Table.


        self.suppliers_virement = self.get_suppliers(self.OP_VIREMENT)
        name = unicode(self.ui.HSGsupplierNameEdit.text(), 'utf_8')
        # Small hack for auto-modifying list.
        filtered = [sup for sup in self.suppliers_virement if name.upper() in sup[0]]

        row_count = len(filtered)
        self.ui.HSGsuppliersTable.setRowCount(row_count)

        # supplier[0] is the supplier's name.
        # supplier[1] is the supplier's account number.

        for index, supplier in enumerate(filtered):
            self.ui.HSGsuppliersTable.setItem(
                index,
                0,
                QtGui.QTableWidgetItem(supplier[0])
            )

            self.ui.HSGsuppliersTable.setItem(
                index,
                1,
                QtGui.QTableWidgetItem(self.get_supplier_bank(supplier[1]))
            )

            self.ui.HSGsuppliersTable.setItem(
                index,
                2,
                QtGui.QTableWidgetItem(supplier[1])
            )

            self.ui.HSGsuppliersTable.resizeColumnsToContents()
            self.ui.HSGsuppliersTable.horizontalHeader().setStretchLastSection(True)


    def get_suppliers(self, opchoice):
        '''
            Return a list of suppliers who are 
            relevant to the chosen operation. 

        '''
        db, cur = self.init_db(SUPPLIERS_DB)
        cur.execute('SELECT * FROM suppliers WHERE operation = ?', (opchoice,))
        data = cur.fetchall()
        db.close()
        return data

Je ne connais pas grand-chose aux meilleures pratiques et à des choses comme ça, mais c'est ce qui était logique pour moi et, par ailleurs, il m'a été plus facile de revenir à l'application après une interruption et de vouloir en faire une application Web à l'aide de web2py ou webapp2. Le fait que le code qui fait réellement le truc est indépendant et en bas, il est facile de le saisir, puis de simplement changer la façon dont les résultats sont affichés (éléments html vs éléments de bureau).


0

... beaucoup de défauts, le plus évident étant le modèle couplé au framework UI (QObject, signaux pyqt).

Alors ne fais pas ça!

class Model(object):
    def __init__(self):
        self.items = {}
        self.add_callbacks = []
        self.del_callbacks = []

    # just use regular callbacks, caller can provide a lambda or whatever
    # to make the desired Qt call
    def emit_add(self, guid):
        for cb in self.add_callbacks:
            cb(guid)

Ce fut un changement trivial, qui a complètement découplé votre modèle de Qt. Vous pouvez même le déplacer dans un autre module maintenant.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.