Come disaccoppiare correttamente l'interfaccia utente dalla logica sulle app Pyqt / Qt?


20

Ho letto parecchio su questo argomento in passato e ho visto alcuni discorsi interessanti come questo di zio Bob . Tuttavia, trovo sempre piuttosto difficile progettare correttamente le mie applicazioni desktop e distinguere quali dovrebbero essere le responsabilità sul lato dell'interfaccia utente e quali quelle sul lato logico .

Un breve riassunto delle buone pratiche è qualcosa di simile. Dovresti progettare la tua logica disaccoppiata dall'interfaccia utente, in modo da poter usare (teoricamente) la tua libreria indipendentemente dal tipo di backend / framework dell'interfaccia utente. Ciò significa fondamentalmente che l'interfaccia utente dovrebbe essere il più fittizia possibile e l'elaborazione pesante dovrebbe essere eseguita dal lato logico. Detto altrimenti, potrei letteralmente usare la mia bella libreria con un'applicazione console, un'applicazione web o desktop.

Inoltre, lo zio Bob suggerisce discussioni discordanti su quale tecnologia utilizzare ti darà molti vantaggi (buone interfacce), questo concetto di differimento ti consente di avere entità ben collaudate, che sembrano grandi ma sono ancora difficili.

Quindi, so che questa domanda è una domanda piuttosto ampia che è stata discussa molte volte su tutta Internet e anche in tonnellate di buoni libri. Quindi, per ottenere qualcosa di buono, posterò un piccolo esempio fittizio che prova a usare MCV su 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_())

Lo snippet di cui sopra contiene molti difetti, tanto più evidente è il modello associato al framework dell'interfaccia utente (QObject, segnali pyqt). So che l'esempio è davvero fittizio e potresti codificarlo su poche righe usando una singola QMainWindow ma il mio scopo è capire come progettare correttamente un'applicazione pyqt più grande.

DOMANDA

Come progettereste correttamente una grande applicazione PyQt usando MVC seguendo le buone pratiche generali?

RIFERIMENTI

Ho fatto una domanda simile a questa qui

Risposte:


1

Vengo da uno (principalmente) sfondo WPF / ASP.NET e sto provando a creare un'app PyQT MVC in questo momento e questa domanda mi sta perseguitando. Condividerò quello che sto facendo e sarei curioso di ricevere commenti o critiche costruttive.

Ecco un piccolo diagramma 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
      ...

La mia applicazione ha molti elementi LOT (UI) e widget che devono essere facilmente modificati da un numero di programmatori. Il codice "view" è costituito da una QMainWindow con un QTreeWidget contenente gli elementi che vengono visualizzati da un QStackedWidget sulla destra (si pensi alla vista Master-Detail).

Poiché gli elementi possono essere aggiunti e rimossi in modo dinamico da QTreeWidget e vorrei supportare la funzionalità di annullamento della ripetizione, ho optato per creare un modello che tenga traccia degli stati attuali / precedenti. I comandi dell'interfaccia utente passano le informazioni al modello (aggiungendo o rimuovendo un widget, aggiornando le informazioni in un widget) dal controller. L'unica volta in cui il controller trasmette informazioni all'interfaccia utente è in corso la convalida, la gestione degli eventi e il caricamento di un file / annullamento e ripetizione.

Il modello stesso è composto da un dizionario dell'ID elemento dell'interfaccia utente con il valore che ha conservato per ultimo (e alcune informazioni aggiuntive). Tengo un elenco di dizionari precedenti e posso tornare a uno precedente se qualcuno colpisce annulla. Alla fine il modello viene scaricato su disco come un determinato formato di file.

Sarò onesto: l'ho trovato piuttosto difficile da progettare. PyQT non si sente come se si prestasse bene ad essere divorziato dal modello, e non riuscivo davvero a trovare nessun programma open source che provasse a fare qualcosa di abbastanza simile a questo. Curioso di come le altre persone si siano avvicinate a questo.

PS: mi rendo conto che QML è un'opzione per fare MVC, e mi è sembrato attraente fino a quando ho capito quanto Javascript fosse coinvolto - e il fatto è ancora abbastanza immaturo in termini di porting su PyQT (o solo periodo). I fattori complicanti di nessun grande strumento di debug (abbastanza difficile solo con PyQT) e la necessità per altri programmatori di modificare facilmente questo codice che non sanno che JS lo ha annullato.


0

Volevo creare un'applicazione. Ho iniziato a scrivere singole funzioni che facevano piccoli compiti (cercare qualcosa nel db, calcolare qualcosa, cercare un utente con il completamento automatico). Visualizzato sul terminale. Quindi inserisci questi metodi in un file, main.py..

Quindi volevo aggiungere un'interfaccia utente. Mi sono guardato intorno diversi strumenti e ho optato per Qt. Ho usato Creator per creare l'interfaccia utente, quindi pyuic4per generare UI.py.

In main.py, ho importato UI. Quindi aggiunti i metodi che sono attivati ​​dagli eventi dell'interfaccia utente in cima alla funzionalità principale (letteralmente in alto: il codice "core" si trova nella parte inferiore del file e non ha nulla a che fare con l'interfaccia utente, è possibile utilizzarlo dalla shell se si desidera per).

Ecco un esempio di metodo display_suppliersche visualizza un elenco di fornitori (campi: nome, account) su una tabella. (Ho tagliato questo dal resto del codice solo per illustrare la struttura).

Man mano che l'utente digita nel campo di testo HSGsupplierNameEdit, il testo cambia e ogni volta che lo fa, questo metodo viene chiamato in modo che la Tabella cambi mentre l'utente digita.

Ottiene i fornitori da un metodo chiamato get_suppliers(opchoice)che è indipendente dall'interfaccia utente e funziona anche dalla 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

Non so molto sulle migliori pratiche e cose del genere, ma questo è ciò che aveva senso per me e per inciso mi ha reso più facile tornare all'app dopo una pausa e voler farne un'applicazione web usando web2py o webapp2. Il fatto che il codice che fa effettivamente le cose sia indipendente e in fondo rende facile afferrarlo e poi cambiare semplicemente la modalità di visualizzazione dei risultati (elementi html rispetto agli elementi desktop).


0

... molti difetti, tanto più evidente è il modello accoppiato al framework dell'interfaccia utente (QObject, segnali pyqt).

Quindi non farlo!

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)

È stato un cambiamento banale, che ha completamente disaccoppiato il tuo modello da Qt. Ora puoi persino spostarlo in un modulo diverso.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.