Como desacoplar a interface do usuário da lógica nos aplicativos Pyqt / Qt corretamente?


20

Eu já li bastante sobre esse assunto no passado e assisti a algumas palestras interessantes como essa do tio Bob . Mesmo assim, sempre acho muito difícil arquitetar corretamente meus aplicativos de área de trabalho e distinguir quais devem ser as responsabilidades no lado da interface do usuário e quais no lado lógico .

Um breve resumo das boas práticas é algo assim. Você deve projetar sua lógica dissociada da interface do usuário, para poder usar (teoricamente) sua biblioteca, independentemente do tipo de estrutura de back-end / interface do usuário. O que isso significa é basicamente que a interface do usuário deve ser o mais fictícia possível e o processamento pesado deve ser feito no lado lógico. Dito o contrário, eu poderia literalmente usar minha boa biblioteca com um aplicativo de console, um aplicativo da Web ou um desktop.

Além disso, o tio Bob sugere discussões diferentes sobre qual tecnologia usar lhe trará muitos benefícios (boas interfaces); esse conceito de adiamento permite que você tenha entidades bem testadas e altamente dissociadas, o que soa muito bem, mas ainda é complicado.

Então, eu sei que essa pergunta é uma pergunta bastante ampla, que foi discutida muitas vezes em toda a Internet e também em toneladas de bons livros. Então, para obter algo de bom, postarei um pequeno exemplo fictício tentando usar o MCV no 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_())

O snippet acima contém muitas falhas, sendo o mais óbvio o modelo sendo acoplado à estrutura da interface do usuário (QObject, sinais pyqt). Eu sei que o exemplo é realmente fictício e você pode codificá-lo em poucas linhas usando um único QMainWindow, mas meu objetivo é entender como arquitetar corretamente um aplicativo pyqt maior.

QUESTÃO

Como você arquitetaria adequadamente um grande aplicativo PyQt usando o MVC seguindo boas práticas gerais?

REFERÊNCIAS

Eu fiz uma pergunta semelhante a isso aqui

Respostas:


1

Estou vindo de um background (principalmente) do WPF / ASP.NET e estou tentando criar um aplicativo PyQT MVC-ish agora e essa pergunta está me assustando. Vou compartilhar o que estou fazendo e ficaria curioso para receber comentários ou críticas construtivas.

Aqui está um pequeno 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
      ...

Meu aplicativo possui muitos elementos e widgets de interface do usuário que precisam ser facilmente modificados por vários programadores. O código "view" consiste em um QMainWindow com um QTreeWidget contendo itens que são exibidos por um QStackedWidget à direita (pense na exibição Master-Detail).

Como os itens podem ser adicionados e removidos dinamicamente do QTreeWidget, e eu gostaria de oferecer suporte à funcionalidade de desfazer refazer, optei por criar um modelo que monitore os estados atuais / anteriores. Os comandos da interface do usuário transmitem informações ao modelo (adicionando ou removendo um widget, atualizando as informações em um widget) pelo controlador. O único momento em que o controlador passa as informações para a interface do usuário é na validação, manipulação de eventos e carregamento de um arquivo / desfazer e refazer.

O modelo em si é composto por um dicionário do ID do elemento da UI com o valor que ele manteve pela última vez (e algumas informações adicionais). Eu mantenho uma lista de dicionários anteriores e posso reverter para um anterior se alguém clicar em desfazer. Eventualmente, o modelo é despejado no disco como um determinado formato de arquivo.

Serei honesto - achei isso muito difícil de projetar. O PyQT não parece bom em se divorciar do modelo, e eu realmente não consegui encontrar nenhum programa de código aberto tentando fazer algo parecido com isso. Curioso como outras pessoas abordaram isso.

PS: Eu sei que o QML é uma opção para fazer MVC, e parecia atraente até eu perceber o quanto de JavaScript estava envolvido - e o fato de ainda ser bastante imaturo em termos de ser portado para o PyQT (ou apenas por um período). Os fatores complicadores de não haver grandes ferramentas de depuração (bastante difíceis apenas com o PyQT) e a necessidade de outros programadores modificarem esse código facilmente e que não sabem que o JS o nixou.


0

Eu queria criar um aplicativo. Comecei a escrever funções individuais que executavam pequenas tarefas (procure algo no banco de dados, calcule algo, procure um usuário com preenchimento automático). Exibido no terminal. Em seguida, coloque esses métodos em um arquivo main.py.

Então eu queria adicionar uma interface do usuário. Olhei em volta de ferramentas diferentes e me conformei com o Qt. Usei o Creator para criar a interface do usuário e depois pyuic4para gerar UI.py.

Em main.py, eu importei UI. Em seguida, foram adicionados os métodos que são acionados pelos eventos da interface do usuário na parte superior da funcionalidade principal (literalmente na parte superior: o código "core" fica na parte inferior do arquivo e não tem nada a ver com a interface do usuário, você pode usá-lo no shell, se desejar para).

Aqui está um exemplo de método display_suppliersque exibe uma lista de fornecedores (campos: nome, conta) em uma tabela. (Eu cortei isso do resto do código apenas para ilustrar a estrutura).

À medida que o usuário digita no campo de texto HSGsupplierNameEdit, o texto muda e, sempre que muda, esse método é chamado para que a Tabela seja alterada conforme o usuário digita.

Ele obtém os fornecedores de um método chamado get_suppliers(opchoice)independente da interface do usuário e funciona também no 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

Eu não sei muito sobre práticas recomendadas e coisas assim, mas foi isso que fez sentido para mim e, aliás, facilitou o retorno ao aplicativo após um hiato e o desejo de fazer um aplicativo Web usando o web2py ou webapp2. O fato de o código que realmente faz as coisas ser independente e na parte inferior facilita a captura e a alteração da forma como os resultados são exibidos (elementos html versus elementos da área de trabalho).


0

... muitas falhas, o mais óbvio é o modelo sendo acoplado à estrutura da interface do usuário (QObject, sinais pyqt).

Então não faça isso!

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)

Essa foi uma mudança trivial, que separou completamente seu modelo do Qt. Você pode até movê-lo para um módulo diferente agora.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.