Wie kann ich die Benutzeroberfläche bei Pyqt / Qt-Apps richtig von der Logik entkoppeln?


20

Ich habe in der Vergangenheit viel über dieses Thema gelesen und mir einige interessante Vorträge wie diesen von Onkel Bob angeschaut . Trotzdem finde ich es immer ziemlich schwierig, meine Desktop-Anwendungen richtig zu erstellen und zu unterscheiden, welche Aufgaben auf der Benutzeroberfläche und welche auf der Logikseite liegen sollten.

Eine sehr kurze Zusammenfassung der guten Praktiken ist in etwa so. Sie sollten Ihre Logik von der Benutzeroberfläche entkoppelt entwerfen, damit Sie (theoretisch) Ihre Bibliothek verwenden können, unabhängig von der Art des Back-End / UI-Frameworks. Dies bedeutet im Grunde, dass die Benutzeroberfläche so einfach wie möglich sein sollte und die umfangreiche Verarbeitung auf der logischen Seite erfolgen sollte. Anders gesagt, ich könnte meine schöne Bibliothek buchstäblich mit einer Konsolenanwendung, einer Webanwendung oder einer Desktopanwendung verwenden.

Onkel Bob schlägt außerdem vor, dass unterschiedliche Diskussionen darüber, welche Technologie verwendet werden soll, viele Vorteile mit sich bringen (gute Schnittstellen). Dieses Konzept des Aufschiebens ermöglicht es Ihnen, gut getestete Entitäten hochgradig zu entkoppeln, was großartig klingt, aber dennoch schwierig ist.

Ich weiß also, dass diese Frage eine ziemlich breite Frage ist, die viele Male im gesamten Internet und auch in Tonnen von guten Büchern diskutiert wurde. Um etwas Gutes daraus zu machen, werde ich ein kleines Dummy-Beispiel veröffentlichen, in dem versucht wird, MCV auf pyqt zu verwenden:

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_())

Das obige Snippet enthält viele Fehler, wobei das Modell deutlicher an das UI-Framework (QObject, Pyqt-Signale) gekoppelt ist. Ich weiß, dass das Beispiel wirklich Dummy ist und Sie es mit einem einzigen QMainWindow in wenigen Zeilen codieren können, aber mein Ziel ist es, zu verstehen, wie man eine größere PyQT-Anwendung richtig entwirft.

FRAGE

Wie würden Sie eine große PyQt-Anwendung mithilfe von MVC nach bewährten allgemeinen Vorgehensweisen ordnungsgemäß erstellen?

VERWEISE

Ich habe eine ähnliche Frage an das macht hier

Antworten:


1

Ich komme aus einem (hauptsächlich) WPF / ASP.NET-Hintergrund und versuche gerade, eine MVC-artige PyQT-App zu erstellen, und genau diese Frage verfolgt mich. Ich teile mit, was ich tue, und bin gespannt auf konstruktive Kommentare oder Kritik.

Hier ist ein kleines ASCII-Diagramm:

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
      ...

Meine Anwendung verfügt über eine Vielzahl von UI-Elementen und Widgets, die von einer Reihe von Programmierern problemlos geändert werden müssen. Der Code "view" besteht aus einem QMainWindow mit einem QTreeWidget, das Elemente enthält, die von einem QStackedWidget auf der rechten Seite angezeigt werden (siehe Master-Detail-Ansicht).

Da Elemente dynamisch zu QTreeWidget hinzugefügt und daraus entfernt werden können und die Funktion zum Rückgängigmachen von Wiederherstellungen unterstützt werden soll, habe ich mich für die Erstellung eines Modells entschieden, das den aktuellen / vorherigen Status verfolgt. Die UI-Befehle leiten Informationen vom Controller an das Modell weiter (Hinzufügen oder Entfernen eines Widgets, Aktualisieren der Informationen in einem Widget). Die einzige Zeit, in der der Controller Informationen an die Benutzeroberfläche weitergibt, ist die Überprüfung, Ereignisbehandlung und das Laden einer Datei / Rückgängigmachen und Wiederherstellen.

Das Modell selbst besteht aus einem Wörterbuch der UI-Element-ID mit dem Wert, den es zuletzt besaß (und einigen zusätzlichen Informationen). Ich behalte eine Liste früherer Wörterbücher und kann auf ein vorheriges zurückgreifen, wenn jemand auf "Rückgängig" klickt. Schließlich wird das Modell als bestimmtes Dateiformat auf die Festplatte kopiert.

Ich werde ehrlich sein - ich fand das ziemlich schwer zu entwerfen. PyQT hat nicht das Gefühl, dass es sich gut dazu eignet, von dem Modell getrennt zu werden, und ich konnte keine Open-Source-Programme finden, die versuchen, etwas Ähnliches zu tun. Neugierig, wie andere Leute das angegangen sind.

PS: Mir ist klar, dass QML eine Option für MVC ist, und es schien attraktiv, bis mir klar wurde, wie viel Javascript involviert war - und die Tatsache, dass es noch ziemlich unausgereift ist, wenn es auf PyQT (oder nur auf Perioden) portiert wird. Die erschwerenden Faktoren, dass es keine großartigen Debugging-Tools gibt (die nur mit PyQT schwer genug sind), und die Notwendigkeit, dass andere Programmierer diesen Code leicht modifizieren können, die nicht wissen, dass JS ihn nicht unterstützt.


0

Ich wollte eine Anwendung erstellen. Ich fing an, einzelne Funktionen zu schreiben, die kleine Aufgaben erledigten (etwas in der Datenbank suchen, etwas berechnen, einen Benutzer mit Autovervollständigung suchen). Wird am Terminal angezeigt. Dann legen Sie diese Methoden in eine Datei, main.py..

Dann wollte ich eine Benutzeroberfläche hinzufügen. Ich sah mich in verschiedenen Werkzeugen um und entschied mich für Qt. Ich habe Creator verwendet, um die Benutzeroberfläche zu erstellen und dann pyuic4zu generieren UI.py.

In main.pyimportierte ich UI. Anschließend wurden die von UI-Ereignissen ausgelösten Methoden über der Kernfunktionalität hinzugefügt (wörtlich oben: "Kern" -Code befindet sich am Ende der Datei und hat nichts mit der Benutzeroberfläche zu tun. Sie können ihn von der Shell aus verwenden, wenn Sie möchten zu).

Hier ist ein Beispiel für eine Methode display_suppliers, mit der eine Liste der Lieferanten (Felder: Name, Konto) in einer Tabelle angezeigt wird. (Ich habe dies aus dem Rest des Codes herausgeschnitten, um die Struktur zu veranschaulichen.)

Während der Benutzer in das Textfeld eingibt HSGsupplierNameEdit, ändert sich der Text. Bei jedem Aufruf dieser Methode ändert sich die Tabelle entsprechend der Benutzertypen.

Die Lieferanten werden von einer Methode abgerufen, get_suppliers(opchoice)die von der Benutzeroberfläche unabhängig ist und auch von der Konsole aus funktioniert.

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

Ich weiß nicht viel über Best Practices und ähnliches, aber das hat mir Sinn gemacht und es mir im Übrigen erleichtert, nach einer Pause wieder zur App zurückzukehren und mit web2py eine Webanwendung daraus zu machen oder webapp2. Die Tatsache, dass der Code, der das Zeug tatsächlich ausführt, unabhängig ist und sich unten befindet, macht es einfach, es einfach zu greifen und dann einfach zu ändern, wie die Ergebnisse angezeigt werden (HTML-Elemente vs. Desktop-Elemente).


0

... viele Fehler, wobei das Modell offensichtlicher an das UI-Framework (QObject, PyQT-Signale) gekoppelt ist.

Also mach das nicht!

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)

Das war eine triviale Änderung, die Ihr Modell vollständig von Qt entkoppelt hat. Sie können es jetzt sogar in ein anderes Modul verschieben.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.