Jak prawidłowo oddzielić interfejs użytkownika od logiki w aplikacjach Pyqt / Qt?


20

W przeszłości dużo czytałem na ten temat i oglądałem ciekawe rozmowy, takie jak ta wuja Boba . Mimo to zawsze trudno mi właściwie zaprojektować aplikacje komputerowe i rozróżnić, które powinny być obowiązki po stronie interfejsu użytkownika, a które po stronie logiki .

Bardzo krótkie podsumowanie dobrych praktyk jest coś takiego. Powinieneś zaprojektować swoją logikę oddzieloną od interfejsu użytkownika, aby w ten sposób można było (teoretycznie) korzystać z biblioteki bez względu na rodzaj struktury zaplecza / interfejsu użytkownika. Oznacza to, że interfejs użytkownika powinien być możliwie jak najbardziej sztuczny, a ciężkie przetwarzanie powinno odbywać się po stronie logiki. Inaczej mówiąc, mógłbym dosłownie używać mojej ładnej biblioteki z aplikacją konsoli, aplikacją internetową lub komputerową.

Ponadto wujek Bob sugeruje różne dyskusje na temat tego, która technologia da wiele korzyści (dobre interfejsy), ta koncepcja odroczenia pozwala mieć wysoce oddzielone, dobrze przetestowane byty, które brzmią świetnie, ale nadal są trudne.

Wiem więc, że to pytanie jest dość szerokim pytaniem, które było dyskutowane wiele razy w całym Internecie, a także w tonach dobrych książek. Aby wyciągnąć z tego coś dobrego, opublikuję bardzo mały przykładowy próbę użycia MCV na 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_())

Powyższy fragment zawiera wiele wad, tym bardziej oczywistym jest to, że model jest sprzężony ze strukturą interfejsu użytkownika (QObject, sygnały pyqt). Wiem, że przykład jest naprawdę fałszywy i można go zakodować w kilku wierszach przy użyciu pojedynczego QMainWindow, ale moim celem jest zrozumienie, jak poprawnie zaprojektować większą aplikację pyqt.

PYTANIE

Jak właściwie zaprojektowałbyś dużą aplikację PyQt przy użyciu MVC, przestrzegając dobrych ogólnych praktyk?

BIBLIOGRAFIA

Zrobiłem podobne pytanie to tutaj

Odpowiedzi:


1

Pochodzę z (przede wszystkim) tła WPF / ASP.NET i próbuję teraz stworzyć aplikację PyQT w stylu MVC i to samo pytanie mnie prześladuje. Podzielę się tym, co robię i byłbym ciekawy wszelkich konstruktywnych komentarzy lub krytyki.

Oto mały schemat 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
      ...

Moja aplikacja ma dużo (LOT) elementów interfejsu użytkownika i widżetów, które muszą być łatwo modyfikowane przez wielu programistów. Kod „widoku” składa się z QMainWindow z QTreeWidget zawierającym elementy, które są wyświetlane przez QStackedWidget po prawej stronie (pomyśl widok Master-Detail).

Ponieważ elementy można dynamicznie dodawać i usuwać z QTreeWidget, a chciałbym wesprzeć funkcjonalność cofania i ponawiania, zdecydowałem się stworzyć model, który będzie śledził bieżące / poprzednie stany. Polecenia interfejsu użytkownika przekazują informacje do modelu (dodawanie lub usuwanie widżetu, aktualizowanie informacji w widgecie) przez kontroler. Jedyny czas, w którym kontroler przekazuje informacje do interfejsu użytkownika, dotyczy sprawdzania poprawności, obsługi zdarzeń i ładowania pliku / cofania i ponawiania.

Sam model składa się ze słownika identyfikatora elementu interfejsu użytkownika z wartością, którą ostatnio utrzymywał (oraz kilku dodatkowych informacji). Prowadzę listę wcześniejszych słowników i mogę przywrócić poprzedni, jeśli ktoś cofnie operację. Ostatecznie model zostaje zrzucony na dysk jako określony format pliku.

Będę szczery - trudno mi to zaprojektować. PyQT nie wydaje się, że dobrze nadaje się do bycia oderwanym od modelu, a tak naprawdę nie mogłem znaleźć żadnych programów open source próbujących zrobić coś całkiem podobnego do tego. Ciekawe, jak podeszli do tego inni ludzie.

PS: Zdaję sobie sprawę, że QML jest opcją do robienia MVC i wydawało się to atrakcyjne, dopóki nie zorientowałem się, ile JavaScript jest zaangażowana - i fakt, że nadal jest dość niedojrzały, jeśli chodzi o przeniesienie do PyQT (lub po prostu kropka). Czynniki komplikujące brak świetnych narzędzi do debugowania (wystarczająco trudne przy pomocy PyQT) i potrzeba łatwego modyfikowania tego kodu przez innych programistów, którzy nie znają JS.


0

Chciałem zbudować aplikację. Zacząłem pisać poszczególne funkcje, które wykonywały drobne zadania (poszukaj czegoś w bazie danych, oblicz coś, poszukaj użytkownika z autouzupełnianiem). Wyświetlany na terminalu. Następnie umieść te metody w pliku main.py...

Potem chciałem dodać interfejs użytkownika. Rozejrzałem się po różnych narzędziach i zdecydowałem na Qt. Użyłem Creatora do zbudowania interfejsu użytkownika, a następnie pyuic4do wygenerowania UI.py.

W main.pyzaimportowałem UI. Następnie dodano metody uruchamiane przez zdarzenia interfejsu użytkownika nad podstawową funkcjonalnością (dosłownie na górze: kod „core” znajduje się na dole pliku i nie ma nic wspólnego z interfejsem użytkownika, możesz użyć go z powłoki, jeśli chcesz do).

Oto przykład metody, display_suppliersktóra wyświetla listę dostawców (pola: nazwa, konto) w tabeli. (Wytnąłem to z reszty kodu, aby zilustrować strukturę).

Gdy użytkownik pisze w polu tekstowym HSGsupplierNameEdit, tekst się zmienia i za każdym razem ta metoda jest wywoływana, więc Tabela zmienia się wraz z typem użytkownika.

Dostaje dostawców z metody o nazwie get_suppliers(opchoice)niezależnej od interfejsu użytkownika i działającej również z konsoli.

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

Nie znam się na najlepszych praktykach i tego typu rzeczach, ale to właśnie miało dla mnie sens i nawiasem mówiąc, ułatwiło mi powrót do aplikacji po przerwie i chęć stworzenia z niej aplikacji internetowej za pomocą web2py lub webapp2. Fakt, że kod, który faktycznie robi te rzeczy, jest niezależny i na dole ułatwia po prostu złapanie go, a następnie zmianę sposobu wyświetlania wyników (elementy HTML vs. Elementy pulpitu).


0

... wiele wad, tym bardziej oczywistym jest model sprzężony ze strukturą interfejsu użytkownika (obiekt QO, sygnały pyqt).

Więc nie rób tego!

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)

To była trywialna zmiana, która całkowicie oddzieliła twój model od Qt. Możesz teraz nawet przenieść go do innego modułu.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.