Как правильно отделить интерфейс от логики в приложениях Pyqt / Qt?

20

Я много читал об этом в прошлом и смотрел некоторые интересные разговоры, подобные этой, от дяди Боба . Тем не менее, я всегда нахожу довольно сложным правильно спроектировать мои настольные приложения и различать, какие должны быть обязанности на стороне пользовательского интерфейса, а какие - на стороне логики .

Очень краткое изложение хороших практик - что-то вроде этого. Вы должны спроектировать свою логику, отделенную от пользовательского интерфейса, чтобы вы могли (теоретически) использовать вашу библиотеку независимо от того, какой тип инфраструктуры бэкэнда / пользовательского интерфейса. По сути, это означает, что пользовательский интерфейс должен быть настолько пустым, насколько это возможно, а тяжелая обработка должна выполняться со стороны логики. Иначе говоря, я мог бы буквально использовать свою симпатичную библиотеку с консольным приложением, веб-приложением или настольным приложением.

Кроме того, дядя Боб предлагает различные дискуссии о том, какая технология использовать даст вам много преимуществ (хорошие интерфейсы), эта концепция отсрочки позволяет вам иметь хорошо открепленные хорошо протестированные объекты, что звучит замечательно, но все еще сложно.

Итак, я знаю, что этот вопрос - довольно широкий вопрос, который много раз обсуждался во всем Интернете, а также в тоннах хороших книг. Поэтому, чтобы получить что-то хорошее из этого, я опубликую очень небольшой пример с попыткой использовать MCV на 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_())

Приведенный выше фрагмент содержит множество недостатков, более очевидным из которых является то, что модель связана с каркасом пользовательского интерфейса (QObject, pyqt сигналы). Я знаю, что пример действительно глупый, и вы могли бы закодировать его в несколько строк, используя одно окно QMainWindow, но моя цель - понять, как правильно спроектировать большое приложение pyqt.

ВОПРОС

Как бы вы правильно спроектировали большое приложение PyQt, используя MVC, следуя хорошей общей практике?

ССЫЛКИ

Я сделал подобный вопрос к этому здесь

BPL
источник

Ответы:

1

Я пришел из (в первую очередь) из WPF / ASP.NET фона и пытаюсь сделать приложение PyQT для MVC прямо сейчас, и этот вопрос преследует меня. Я поделюсь тем, что делаю, и мне было бы любопытно получить какие-либо конструктивные комментарии или критику.

Вот небольшая диаграмма 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
      ...

Мое приложение имеет много (LOT) элементов пользовательского интерфейса и виджетов, которые должны быть легко изменены многими программистами. Код «view» состоит из QMainWindow с QTreeWidget, содержащим элементы, которые отображаются QStackedWidget справа (представьте себе представление Master-Detail).

Поскольку элементы могут быть добавлены и удалены динамически из QTreeWidget, и я хотел бы поддерживать функциональность отмены повторения, я решил создать модель, которая отслеживает текущие / предыдущие состояния. Команды пользовательского интерфейса передают информацию в модель (добавление или удаление виджета, обновление информации в виджете) контроллером. Единственный раз, когда контроллер передает информацию в пользовательский интерфейс, это проверка, обработка событий и загрузка файла / отменить и повторить.

Сама модель состоит из словаря идентификатора элемента пользовательского интерфейса со значением, которое оно удерживало последним (и нескольких дополнительных фрагментов информации). Я сохраняю список предыдущих словарей и могу вернуться к предыдущему, если кто-нибудь нажмет отмену. В конце концов модель выгружается на диск как файл определенного формата.

Я буду честен - я нашел это довольно сложно для разработки. PyQT не чувствует себя хорошо отделяемым от модели, и я не мог найти программ с открытым исходным кодом, пытающихся сделать что-то похожее на это. Любопытно, как другие люди подошли к этому.

PS: я понимаю, что QML - это вариант для создания MVC, и он казался привлекательным, пока я не осознал, насколько сильно Javascript был задействован - и тот факт, что он все еще довольно незрелый с точки зрения переноса на PyQT (или просто период). Сложные факторы, связанные с отсутствием хороших инструментов отладки (достаточно сложно только с PyQT), и необходимость в том, чтобы другие программисты легко модифицировали этот код, не зная, что JS его исправил.

Филлис Диллер
источник
0

Я хотел создать приложение. Я начал писать отдельные функции, которые выполняли крошечные задачи (что-то искать в БД, что-то вычислять, искать пользователя с автозаполнением). Отображается на терминале. Затем поместите эти методы в файл, main.py..

Затем я хотел добавить пользовательский интерфейс. Я осмотрел различные инструменты и остановился на Qt. Я использовал Creator для создания пользовательского интерфейса, а затем pyuic4для создания UI.py.

Во main.py, я импортировал UI. Затем добавлены методы, которые запускаются событиями пользовательского интерфейса поверх основной функциональности (буквально сверху: «основной» код находится внизу файла и не имеет ничего общего с пользовательским интерфейсом, вы можете использовать его из оболочки, если хотите). к).

Вот пример метода, display_suppliersкоторый отображает список поставщиков (поля: имя, учетная запись) в таблице. (Я вырезал это из остальной части кода просто для иллюстрации структуры).

Когда пользователь вводит текстовое поле HSGsupplierNameEdit, текст меняется, и каждый раз, когда это происходит, вызывается этот метод, поэтому таблица изменяется в зависимости от типа пользователя.

Он получает поставщиков из метода, get_suppliers(opchoice)который называется независимым от пользовательского интерфейса и также работает с консоли.

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

Я не знаю много о лучших практиках и подобных вещах, но это то, что имело для меня смысл и, между прочим, облегчило мне возвращение в приложение после перерыва и желание сделать из него веб-приложение, используя web2py или веб-приложение2. Тот факт, что код, который на самом деле выполняет эту работу, является независимым и в нижней части позволяет легко просто захватить его, а затем просто изменить способ отображения результатов (элементы HTML или элементы рабочего стола).

Джугуртха Хаджар
источник
0

... много недостатков, тем более очевидным является то, что модель связана с каркасом пользовательского интерфейса (QObject, pyqt сигналы).

Так что не делай этого!

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)

Это было тривиальное изменение, которое полностью отделило вашу модель от Qt. Теперь вы даже можете переместить его в другой модуль.

Бесполезный
источник