Como dissociar UI da lógica em aplicativos Pyqt / Qt corretamente?

15

Eu li bastante sobre esse assunto no passado e assisti algumas palestras interessantes como essa de tio Bob . Ainda assim, sempre acho difícil arquitetar adequadamente meus aplicativos de desktop e distinguir quais devem ser as responsabilidades no lado da UI e quais no lado de lógica .

Um breve resumo das boas práticas é algo assim. Você deve projetar sua lógica desacoplada da interface do usuário, de modo que você possa usar (teoricamente) sua biblioteca, independentemente do tipo de estrutura de backend / interface do usuário. O que isto significa é basicamente que a interface do usuário deve ser tão fictícia quanto possível e o processamento pesado deve ser feito no lado lógico. Dito de outra forma, eu poderia literalmente usar minha bela biblioteca com um aplicativo de console, um aplicativo da web ou um desktop.

Além disso, o tio Bob sugere discussões divergentes sobre qual tecnologia usar, o que lhe dará muitos benefícios (boas interfaces), esse conceito de adiamento permite que você tenha entidades altamente testadas e altamente dissociadas, que soam ótimas, mas ainda assim são complicadas.

Então, eu sei que esta questão é bastante ampla e tem sido discutida muitas vezes em toda a internet e também em toneladas de bons livros. Então, para obter algo de bom, vou postar um pequeno exemplo de simulação 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 fragmento acima contém muitas falhas, sendo mais óbvio o modelo sendo acoplado à estrutura da interface do usuário (sinais QObject, pyqt). Eu sei que o exemplo é realmente falso e você poderia codificá-lo em poucas linhas usando um único QMainWindow, mas meu objetivo é entender como arquitetar adequadamente um aplicativo pyqt maior.

PERGUNTA

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

REFERÊNCIAS

Fiz uma pergunta semelhante a esta aqui

    
por BPL 16.09.2016 / 17:28
fonte

3 respostas

1

Eu estou vindo de um plano de fundo (principalmente) do WPF / ASP.NET e tentando fazer um aplicativo PyQT MVC-ish agora e essa pergunta está me assombrando. Compartilharei 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 tem muito (LOT) de elementos de UI e widgets que precisam ser facilmente modificados por um número de programadores. O código "view" consiste em um QMainWindow com um QTreeWidget contendo itens que são exibidos por um QStackedWidget à direita (pense na visualização Master-Detail).

Como os itens podem ser adicionados e removidos dinamicamente do QTreeWidget e gostaríamos de oferecer suporte à funcionalidade desfazer-refazer, optei por criar um modelo que acompanhe os estados atual / anterior. Os comandos de interface do usuário transmitem informações para o 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 até a interface do usuário é na validação, na manipulação de eventos e no carregamento de um arquivo / desfazer & refazer.

O modelo em si é composto de um dicionário do ID do elemento da UI com o valor que ele reteve 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.

Eu serei honesto - achei isso muito difícil de projetar. PyQT não se sente bem em se divorciar do modelo, e eu realmente não encontrei 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é que percebi o quanto de Javascript estava envolvido - e o fato de ainda ser bastante imaturo em termos de ser portado para PyQT (ou apenas período). Os fatores complicadores de nenhuma grande ferramenta de depuração (difícil o suficiente com apenas PyQT) e a necessidade de outros programadores modificarem este código facilmente que não sabem que JS o anulou.

    
por 29.03.2017 / 18:23
fonte
0

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

Então, eu queria adicionar uma interface do usuário. Eu olhei ao redor de diferentes ferramentas e me decidi por Qt. Eu usei o Creator para criar a interface do usuário e, em seguida, pyuic4 para gerar UI.py .

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

Veja um exemplo do método display_suppliers que 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, a cada vez, esse método é chamado, de modo que a tabela muda conforme o usuário digita.

Ele obtém os fornecedores de um método chamado get_suppliers(opchoice) , que é independente da interface do usuário e também funciona 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 as práticas recomendadas e coisas desse tipo, mas isso é o que fez sentido para mim e, por acaso, facilitou o retorno ao aplicativo depois de um hiato e a necessidade de fazer um aplicativo da web usando web2py ou webapp2. O fato de o código que realmente faz o material é independente e na parte inferior torna fácil apenas agarrá-lo e, em seguida, basta alterar como os resultados são exibidos (elementos html vs elementos de desktop).

    
por 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).

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 desvinculou completamente seu modelo do Qt. Você pode até mesmo movê-lo para um módulo diferente agora.

    
por 19.09.2016 / 17:20
fonte