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


20

He leído mucho sobre este tema en el pasado y vi algunas charlas interesantes como esta del tío Bob . Aún así, siempre me resulta bastante difícil diseñar correctamente mis aplicaciones de escritorio y distinguir cuáles deberían ser las responsabilidades en el lado de la interfaz de usuario y cuáles en el lado de la lógica .

Un resumen muy breve de buenas prácticas es algo como esto. Debe diseñar su lógica desacoplada de la interfaz de usuario, de modo que pueda usar (teóricamente) su biblioteca sin importar qué tipo de marco de fondo / interfaz de usuario. Lo que esto significa es que básicamente la interfaz de usuario debe ser lo más ficticia posible y el procesamiento pesado debe hacerse en el lado lógico. Dicho de otro modo, 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 diferentes debates sobre qué tecnología usar le brindará muchos beneficios (buenas interfaces), este concepto de aplazamiento le permite tener entidades altamente probadas y muy desacopladas, lo que suena muy bien pero sigue siendo complicado.

Entonces, sé que esta pregunta es bastante amplia y se ha discutido muchas veces a través de Internet y también en toneladas de buenos libros. Entonces, para obtener algo bueno, publicaré un pequeño ejemplo ficticio que intenta 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 anterior contiene muchos defectos, el más obvio es el modelo que se acopla 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 un solo QMainWindow, pero mi propósito es entender cómo diseñar correctamente una aplicación pyqt más grande.

PREGUNTA

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

Referencias

He hecho una pregunta similar a esta aquí.

Respuestas:


1

Vengo de un fondo (principalmente) de WPF / ASP.NET e intento hacer una aplicación PyQT MVC-ish en este momento y esta misma pregunta me está atormentando. Compartiré lo que estoy haciendo y sentiría 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 (MUCHO) de elementos de interfaz de usuario y widgets que varios programadores deben modificar fácilmente. El código de "vista" consiste en una QMainWindow con un QTreeWidget que contiene elementos que se muestran mediante un QStackedWidget a la derecha (piense en la vista Master-Detail).

Debido a que los elementos se pueden agregar y eliminar dinámicamente del QTreeWidget, y me gustaría admitir la funcionalidad de deshacer y rehacer, opté por crear un modelo que realiza 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 el controlador. La única vez que el controlador pasa información a la interfaz de usuario es en validación, manejo de eventos y cargando un archivo / deshacer y rehacer.

El modelo en sí está compuesto por un diccionario de la ID del elemento de la interfaz de usuario con el valor que tenía por última vez (y algunas piezas de información adicionales). Mantengo una lista de diccionarios anteriores y puedo volver a uno anterior si alguien presiona deshacer. Finalmente, el modelo se volca al disco como un formato de archivo determinado.

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

PD: Me doy cuenta de que QML es una opción para hacer MVC, y me pareció atractivo hasta que me di cuenta de cuánto Javascript estaba involucrado, y el hecho de que todavía es bastante inmaduro en términos de ser portado a PyQT (o solo punto). Los factores complicados de no tener excelentes herramientas de depuración (lo suficientemente difícil con solo PyQT) y la necesidad de que otros programadores modifiquen fácilmente este código que no conocen JS lo rechazaron.


0

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

Entonces quería agregar una interfaz de usuario. Miré alrededor de diferentes herramientas y me decidí por Qt. Usé Creator para construir la interfaz de usuario, luego pyuic4para generar UI.py.

En main.py, importé UI. Luego se agregaron los métodos que se desencadenan por los eventos de la interfaz de usuario en la parte superior de la funcionalidad central (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).

Aquí hay un ejemplo de método display_suppliersque 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, este método se llama para que la Tabla cambie a medida que el usuario escribe.

Obtiene 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 sobre las mejores prácticas y cosas así, pero esto es lo que tenía sentido para mí y, por cierto, me facilitó volver a la aplicación después de un paréntesis y querer hacer una aplicación web con 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 fácil agarrarlo y luego cambiar la forma en que se muestran los resultados (elementos html frente a elementos de escritorio).


0

... muchos defectos, el más obvio es el modelo acoplado al marco de la interfaz de usuario (QObject, señales pyqt).

¡Entonces 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)

Ese fue un cambio trivial, que desacopló por completo su modelo de Qt. Incluso puede moverlo a un módulo diferente ahora.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.