¿Cómo desacoplar la IU de la lógica en las aplicaciones Pyqt / Qt correctamente?

15

He leído mucho sobre este tema en el pasado y he visto algunas charlas interesantes como esta de tío Bob's . Sin embargo, siempre me resulta bastante difícil diseñar correctamente mis aplicaciones de escritorio y distinguir cuáles deben ser las responsabilidades en el lado UI y cuáles en el lado lógico .

Un breve resumen de buenas prácticas es algo como esto. Debe diseñar su lógica desacoplada de la interfaz de usuario, de modo que pueda utilizar (teóricamente) su biblioteca sin importar qué tipo de backend / UI framework. Lo que esto significa es que, básicamente, la interfaz de usuario debe ser lo más simulada posible y el procesamiento pesado debe realizarse en el lado lógico. De lo contrario, literalmente podría usar mi bonita biblioteca con una aplicación de consola, una aplicación web o una de escritorio.

Además, el tío Bob sugiere que las diferentes discusiones sobre qué tecnología usar le dará muchos beneficios (buenas interfaces), este concepto de aplazamiento le permite tener entidades bien probadas altamente desacopladas, eso suena bien, pero aún así es complicado.

Por lo tanto, sé que esta pregunta es una pregunta bastante amplia que se ha discutido muchas veces en todo el Internet y también en toneladas de buenos libros. Así que para obtener algo bueno, publicaré un pequeño ejemplo ficticio tratando de usar MCV en 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_())

El fragmento de código anterior contiene muchas fallas, mientras más obvio es el modelo que se está acoplando al marco de la interfaz de usuario (QObject, señales pyqt). Sé que el ejemplo es realmente ficticio y podría codificarlo en pocas líneas usando una única QMainWindow, pero mi propósito es entender cómo diseñar adecuadamente una aplicación pyqt más grande.

PREGUNTA

¿Cómo podría diseñar correctamente una gran aplicación PyQt usando MVC siguiendo buenas prácticas generales?

REFERENCES

He hecho una pregunta similar a esta aquí

    
pregunta BPL 16.09.2016 - 17:28
fuente

3 respuestas

1

Vengo de un fondo (principalmente) WPF / ASP.NET e intento crear una aplicación PyQT MVC-ish en este momento y esta misma pregunta me persigue. Compartiré lo que estoy haciendo y tendré curiosidad por recibir comentarios constructivos o críticas.

Aquí hay un pequeño diagrama 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
      ...

Mi aplicación tiene una gran cantidad (LOT) de elementos de UI y widgets que necesitan ser modificados fácilmente por varios programadores. El código de "vista" consiste en un QMainWindow con un QTreeWidget que contiene elementos que se muestran en un QStackedWidget a la derecha (piense en la vista Maestro-Detalle).

Como los elementos pueden agregarse y eliminarse dinámicamente de QTreeWidget, y me gustaría admitir la funcionalidad de deshacer y rehacer, opté por crear un modelo que realice un seguimiento de los estados actuales / anteriores. Los comandos de la interfaz de usuario pasan información al modelo (agregando o eliminando un widget, actualizando la información en un widget) por parte del controlador. La única vez que el controlador pasa la información a la interfaz de usuario es en la validación, el manejo de eventos y la carga de un archivo / deshacer & rehacer.

El modelo en sí está compuesto por un diccionario con el ID del elemento de la interfaz de usuario con el último valor que posee (y algunos datos adicionales). Mantengo una lista de diccionarios anteriores y puedo volver a uno anterior si alguien hace clic en Deshacer. Finalmente, el modelo se descarga en el disco como un determinado formato de archivo.

Seré honesto, esto me resultó bastante difícil de diseñar. PyQT no siente que se adapte bien a estar divorciado del modelo, y realmente no pude encontrar ningún programa de código abierto que intentara hacer algo similar a esto. Curioso cómo otras personas se han acercado a esto.

PS: Me doy cuenta de que QML es una opción para hacer MVC, y me pareció atractivo hasta que me di cuenta de la cantidad de Javascript involucrada, y el hecho de que todavía es bastante inmaduro en términos de portabilidad a PyQT (o solo período). Los factores complicados de no tener grandes herramientas de depuración (lo suficientemente duro como solo con PyQT) y la necesidad de que otros programadores modifiquen este código fácilmente y no lo saben, Jp lo rechazó.

    
respondido por el phyllis diller 29.03.2017 - 18:23
fuente
0

Quería construir una aplicación. Comencé a escribir funciones individuales que hacían pequeñas tareas (buscar algo en la base de datos, calcular algo, buscar un usuario con autocompletar). Mostrado en el terminal. Luego ponga estos métodos en un archivo, main.py ..

Luego quise agregar una interfaz de usuario. Miré a mi alrededor diferentes herramientas y me conformé con Qt. Usé Creator para construir la interfaz de usuario, luego pyuic4 para generar UI.py .

En main.py , importé UI . Luego, agregue los métodos que son activados por los eventos de la interfaz de usuario en la parte superior de la funcionalidad del núcleo (literalmente en la parte superior: el código "central" se encuentra en la parte inferior del archivo y no tiene nada que ver con la interfaz de usuario; puede usarlo desde el shell si lo desea). a).

Este es un ejemplo del método display_suppliers que muestra una lista de proveedores (campos: nombre, cuenta) en una tabla. (Corté esto del resto del código solo para ilustrar la estructura).

A medida que el usuario escribe en el campo de texto HSGsupplierNameEdit , el texto cambia y cada vez que lo hace, se llama a este método para que la Tabla cambie a medida que el usuario escribe.

Obtiene a los proveedores de un método llamado get_suppliers(opchoice) que es independiente de la interfaz de usuario y también funciona desde la consola.

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

No sé mucho acerca de las mejores prácticas y cosas por el estilo, pero esto es lo que tenía sentido para mí y, por cierto, me facilitó volver a la aplicación después de una pausa y querer hacer una aplicación web a partir de utilizando web2py o webapp2. El hecho de que el código que realmente hace las cosas es independiente y en la parte inferior hace que sea más fácil agarrarlo y luego cambiar la forma en que se muestran los resultados (elementos html vs elementos del escritorio).

    
respondido por el Jugurtha Hadjar 19.09.2016 - 15:30
fuente
0
  

... muchas fallas, cuanto más obvio es el modelo que se está acoplando al marco de UI (QObject, señales pyqt).

¡Así que no hagas esto!

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)

Eso fue un cambio trivial, que desacopla completamente tu modelo de Qt. Incluso puedes moverlo a un módulo diferente ahora.

    
respondido por el Useless 19.09.2016 - 17:20
fuente

Lea otras preguntas en las etiquetas