如何正确地将UI与Pyqt / Qt应用上的逻辑分离?


20

过去,我已经阅读了很多有关该主题的文章,并观看了Bob叔叔的有趣的演讲。但是,我始终很难正确地构建桌面应用程序,并区分在UI端和逻辑端应该承担哪些责任。

良好做法的简短摘要就是这样的。您应该设计与UI分离的逻辑,以便无论哪种后端/ UI框架,都可以(理论上)使用库。基本上,这意味着UI应该尽可能地虚设,繁重的处理应该在逻辑端进行。否则,我可以在控制台应用程序,Web应用程序或桌面应用程序上真正使用我的漂亮库。

此外,鲍伯叔叔建议对使用哪种技术会给您带来很多好处(良好的界面)的不同讨论,这种递延概念使您可以将经过良好测试的实体高度去耦,这听起来不错,但仍然很棘手。

因此,我知道这个问题是一个相当广泛的问题,已经在整个互联网上讨论了很多次,还以大量好书进行了讨论。因此,为了从中获得好处,我将发布一个非常小的虚拟示例,尝试在pyqt上使用MCV:

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_())

上面的代码段包含许多缺陷,最明显的是该模型与UI框架耦合(QObject,pyqt信号)。我知道该示例确实是虚拟的,您可以使用单个QMainWindow在几行代码上进行编码,但我的目的是了解如何正确构建更大的pyqt应用程序。

您将如何遵循良好的常规做法,使用MVC适当地设计大型PyQt应用程序?

参考资料

我在这里提出了类似的问题

Answers:


1

我来自(主要是)WPF / ASP.NET背景,现在正试图制作一个MVC风格的PyQT应用程序,这个问题困扰着我。我将分享我的工作,我很想收到任何建设性的意见或批评。

这是一些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
      ...

我的应用程序具有很多UI元素和小部件,许多程序员需要轻松地对其进行修改。“视图”代码由一个QMainWindow和一个QTreeWidget组成,其中的QTreeWidget包含由右侧的QStackedWidget显示的项目(请考虑主从视图)。

由于可以动态地从QTreeWidget添加和删除项目,并且我想支持撤消重做功能,因此我选择创建一个模型来跟踪当前/先前状态。UI命令由控制器将信息传递到模型(添加或删除窗口小部件,更新窗口小部件中的信息)。控制器唯一将信息传递到UI的时间是关于验证,事件处理以及加载文件/撤消和重做。

模型本身由UI元素ID的字典及其最后保存的值(以及一些其他信息)组成。我保留了先前词典的列表,如果有人点击撤消,可以将其恢复为先前的词典。最终,模型以某种文件格式转储到磁盘上。

老实说-我发现这很难设计。PyQT觉得它不适合与模型分离,因此我找不到真正的开源程序来尝试做类似的事情。很好奇其他人如何处理这个问题。

PS:我意识到QML是进行MVC的一种选择,在我意识到涉及到Javascript之前,它似乎很有吸引力-而且就移植到PyQT(或只是一个时期)而言,它还很不成熟。没有出色的调试工具的复杂因素(仅使用PyQT就足够困难),以及其他不了解JS的程序员都需要轻松修改此代码。


0

我想构建一个应用程序。我开始编写完成微小任务的单个函数(在db中查找某些内容,计算出一些内容,并寻找具有自动完成功能的用户)。显示在终端上。然后将这些方法放入文件main.py..

然后,我想添加一个UI。我环顾了不同的工具,并选择了Qt。我使用Creator创建了UI,然后pyuic4生成了UI.py

main.py,我进口了UI。然后在核心功能之上添加了由UI事件触发的方法(从字面上看:“核心”代码位于文件的底部,与UI无关,如果需要,可以从外壳程序使用它)至)。

这是display_suppliers在表格上显示供应商列表(字段:名称,客户)的方法示例。(我从其余代码中删除了此代码,只是为了说明结构)。

当用户在文本字段中键入时HSGsupplierNameEdit,文本会更改,并且每次更改时都会调用此方法,因此表会随着用户键入而更改。

它从一种称为get_suppliers(opchoice)UI 的方法获取供应商,该方法独立于UI,也可以从控制台使用。

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从中创建一个Web应用程序或webapp2。实际上执行操作的代码是独立的,并且在底部使可以轻松地抓住它,然后更改结果的显示方式(html元素还是桌面元素)变得容易。


0

...有很多缺陷,最明显的是模型与UI框架耦合(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完全脱钩了。您现在甚至可以将其移动到其他模块中。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.