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

15

Ho letto molto su questo argomento in passato e ho guardato alcuni interessanti talk come questo da Uncle Bob's . Tuttavia, trovo sempre molto difficile architettare correttamente le mie applicazioni desktop e distinguere quali dovrebbero essere le responsabilità sul lato UI e quali sul lato logic .

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

Inoltre, lo zio Bob suggerisce discussioni divergenti su quale tecnologia usare per darti molti vantaggi (buone interfacce), questo concetto di differimento ti permette di avere entità ben collaudate e altamente disaccoppiate, che suona alla grande ma è comunque complicato.

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

Il frammento di cui sopra contiene molti difetti, il più ovvio è il modello che è accoppiato al framework UI (QObject, segnali pyqt). So che l'esempio è davvero fittizio e potresti codificarlo su poche righe usando una sola QMainWindow ma il mio scopo è capire come progettare correttamente un'applicazione pyqt più grande.

DOMANDA

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

RIFERIMENTI

Ho fatto una domanda simile a questo qui

    
posta BPL 16.09.2016 - 17:28
fonte

3 risposte

1

Vengo da uno sfondo (principalmente) WPF / ASP.NET e sto tentando di creare un'applicazione PyQT MVC-ish in questo momento e questa stessa domanda mi perseguita. 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 (LOT) elementi dell'interfaccia utente e widget che devono essere facilmente modificati da un numero di programmatori. Il codice "view" è costituito da una QMainWindow con un QTreeWidget contenente elementi che vengono visualizzati da QStackedWidget sulla destra (si pensi alla vista Master-Detail).

Poiché gli articoli possono essere aggiunti e rimossi dinamicamente da QTreeWidget e mi piacerebbe supportare la funzionalità di annullamento del ripristino, ho scelto di creare un modello che tenga traccia degli stati attuali / precedenti. I comandi dell'interfaccia passano le informazioni attraverso il modello (aggiungendo o rimuovendo un widget, aggiornando le informazioni in un widget) dal controller. L'unica volta che il controller passa le informazioni all'interfaccia utente è in fase di convalida, gestione degli eventi e caricamento di un file / annullamento & rifare.

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

Sarò onesto - ho trovato questo piuttosto difficile da progettare. PyQT non si sente come se si prestasse bene ad essere divorziato dal modello, e non riuscivo a trovare programmi open source che cercassero di fare qualcosa del tutto simile a questo. Curioso come altre persone si sono avvicinate a questo.

PS: Mi rendo conto che QML è un'opzione per fare MVC, e mi è sembrato attraente fino a quando non ho realizzato quanto Javascript fosse implicato - e il fatto è ancora piuttosto immaturo in termini di porting su PyQT (o solo periodo). I fattori complicanti di non avere grandi strumenti 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 nixato.

    
risposta data 29.03.2017 - 18:23
fonte
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. Ho guardato attorno a diversi strumenti e ho optato per Qt. Ho utilizzato Creator per creare l'interfaccia utente, quindi pyuic4 per generare UI.py .

In main.py , ho importato UI . Quindi aggiunto i metodi attivati dagli eventi dell'interfaccia utente in cima alle funzionalità di base (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 a).

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

Mentre 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 ne so molto delle best practice e cose del genere, ma questo era ciò che per me aveva senso e, tra l'altro, mi ha reso più facile tornare all'app dopo una pausa e volendo creare un'applicazione web da usando web2py o webapp2. Il fatto che il codice faccia effettivamente il roba sia indipendente e in fondo rende facile afferrarlo e quindi cambiare semplicemente il modo in cui i risultati vengono visualizzati (elementi html vs elementi del desktop).

    
risposta data 19.09.2016 - 15:30
fonte
0

... a lot of flaws, the more obvious being the model being coupled to the UI framework (QObject, pyqt signals).

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. Puoi persino spostarlo in un modulo diverso ora.

    
risposta data 19.09.2016 - 17:20
fonte

Leggi altre domande sui tag