Як правильно від'єднати інтерфейс користувача від логіки на додатках 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, дотримуючись належних загальних практик?

ЛІТЕРАТУРА

Я поставив подібне запитання до цього тут

Відповіді:


1

Я приходжу з (в першу чергу) фону WPF / ASP.NET і намагаюся зробити додаток PyQT MVC-ish прямо зараз, і саме це питання переслідує мене. Я поділюсь тим, що роблю, і мені буде цікаво отримати будь-які конструктивні коментарі чи критику.

Ось невелика діаграма 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
      ...

У моєму додатку багато (багато) елементів інтерфейсу та віджетів, які потрібно легко модифікувати кількома програмістами. Код "view" складається з QMainWindow з QTreeWidget, що містить елементи, які відображаються QStackedWidget праворуч (подумайте перегляд Master-Detail).

Оскільки елементи можна додавати та видаляти динамічно з QTreeWidget, і я хотів би підтримати функцію скасування повторного повторення, я вирішив створити модель, яка відслідковує поточні / попередні стани. Команди інтерфейсу передають інформацію в модель (додавання або видалення віджета, оновлення інформації у віджеті) контролером. Єдиний раз, коли контролер передає інформацію до інтерфейсу користувача, відбувається перевірка, обробка подій та завантаження файлу / скасування та повтор.

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

Я чесно скажу - мені це було важко спроектувати. PyQT не відчуває, що він добре піддається розлученню з моделлю, і мені не вдалося знайти жодної програми з відкритим кодом, яка намагалася зробити щось подібне до цього. Цікаво, як інші люди підійшли до цього.

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


0

Я хотів створити додаток. Я почав писати окремі функції, які виконували крихітні завдання (шукати щось у db, обчислювати щось, шукати користувача з автозаповненням). Відображається на терміналі. Потім помістіть ці методи у файл, main.py..

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

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

Ось приклад методу, 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 або webapp2. Факт коду, який насправді робить матеріал, є незалежним, і внизу полегшує його просто захоплення, а потім просто змінити спосіб відображення результатів (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. Ви можете навіть перемістити його в інший модуль зараз.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.