【问题标题】:MVC design with Qt Designer and PyQt / PySide使用 Qt Designer 和 PyQt / PySide 进行 MVC 设计
【发布时间】:2014-12-29 04:30:11
【问题描述】:

来自 Java (+SWT/Windowbuilder) 的 Python 新手,并且难以弄清楚如何在 Python/Qt4(QtDesigner)/PySide 中正确编码大型桌面应用程序。

我想将任何视图逻辑保留在 .ui 文件之外的控制器类中(它是 .py 转换)。首先,逻辑独立于 GUI 框架,其次,任何更改都会覆盖 .ui 和生成的 .py 文件!。

仅我发现的示例将操作代码添加到整体 MainWindow.py(从 ui 生成)或 MyForm.py(也从 .ui 生成)。我看不到任何将 POPO 控制器类链接到 QtDesigner 中的操作的方法。

谁能告诉我使用 QtDesigner 在可扩展的 MVC/P 方法中创建大型应用程序的工作流程?

【问题讨论】:

    标签: python model-view-controller pyqt pyside qt-designer


    【解决方案1】:

    首先,请注意 Qt 已经使用了视图和模型的概念,但这并不是您真正想要的。简而言之,Qt MCV 概念是一种将小部件(例如 QListView)自动链接到数据源(例如 QStringListModel)的方法,以便模型中数据的更改自动出现在小部件中,反之亦然。这是一个(非常)有用的特性,但它与应用程序规模的 MVC 设计模式不同。当然,这两个概念可以一起使用,并且确实提供了一些明显的捷径。然而,应用规模的 MVC 设计必须手动编程。

    这是一个示例 MVC 应用程序,它具有单个视图、控制器和模型。该视图有 3 个小部件,每个小部件独立地侦听模型中数据的更改并对它们做出反应。旋转框和按钮都可以通过控制器来操作模型中的数据。

    文件结构是这样排列的:

    project/
        mvc_app.py              # main application with App class
        mvc_app_rc.py           # auto-generated resources file (using pyrcc.exe or equivalent)
        controllers/
            main_ctrl.py        # main controller with MainController class
            other_ctrl.py
        model/
            model.py            # model with Model class
        resources/
            mvc_app.qrc         # Qt resources file
            main_view.ui        # Qt designer files
            other_view.ui
            img/
                icon.png
        views/
            main_view.py        # main view with MainView class
            main_view_ui.py     # auto-generated ui file (using pyuic.exe or equivalent)
            other_view.py
            other_view_ui.py
    

    应用

    mvc_app.py 将负责实例化每个视图、控制器和模型并在它们之间传递引用。这可能非常小:

    import sys
    from PyQt5.QtWidgets import QApplication
    from model.model import Model
    from controllers.main_ctrl import MainController
    from views.main_view import MainView
    
    
    class App(QApplication):
        def __init__(self, sys_argv):
            super(App, self).__init__(sys_argv)
            self.model = Model()
            self.main_controller = MainController(self.model)
            self.main_view = MainView(self.model, self.main_controller)
            self.main_view.show()
    
    
    if __name__ == '__main__':
        app = App(sys.argv)
        sys.exit(app.exec_())
    

    观看次数

    使用 Qt 设计器创建 .ui 布局文件,以便为小部件分配变量名称并调整它们的基本属性。不要费心添加信号或槽,因为通常更容易将它们连接到视图类中的函数。

    .ui 布局文件在使用 pyuic 或 pyside-uic 处理时转换为 .py 布局文件。 .py 视图文件然后可以从 .py 布局文件中导入相关的自动生成的类。

    视图类应包含连接到来自布局中小部件的信号所需的最少代码。视图事件可以调用并将基本信息传递给视图类中的方法和控制器类中的方法,任何逻辑都应该在其中。它看起来像:

    from PyQt5.QtWidgets import QMainWindow
    from PyQt5.QtCore import pyqtSlot
    from views.main_view_ui import Ui_MainWindow
    
    
    class MainView(QMainWindow):
        def __init__(self, model, main_controller):
            super().__init__()
    
            self._model = model
            self._main_controller = main_controller
            self._ui = Ui_MainWindow()
            self._ui.setupUi(self)
    
            # connect widgets to controller
            self._ui.spinBox_amount.valueChanged.connect(self._main_controller.change_amount)
            self._ui.pushButton_reset.clicked.connect(lambda: self._main_controller.change_amount(0))
    
            # listen for model event signals
            self._model.amount_changed.connect(self.on_amount_changed)
            self._model.even_odd_changed.connect(self.on_even_odd_changed)
            self._model.enable_reset_changed.connect(self.on_enable_reset_changed)
    
            # set a default value
            self._main_controller.change_amount(42)
    
        @pyqtSlot(int)
        def on_amount_changed(self, value):
            self._ui.spinBox_amount.setValue(value)
    
        @pyqtSlot(str)
        def on_even_odd_changed(self, value):
            self._ui.label_even_odd.setText(value)
    
        @pyqtSlot(bool)
        def on_enable_reset_changed(self, value):
            self._ui.pushButton_reset.setEnabled(value)
    

    除了将小部件事件链接到相关控制器功能之外,视图并没有做太多事情,并监听模型中的变化,这些变化作为 Qt 信号发出。

    控制器

    控制器类执行任何逻辑,然后在模型中设置数据。一个例子:

    from PyQt5.QtCore import QObject, pyqtSlot
    
    
    class MainController(QObject):
        def __init__(self, model):
            super().__init__()
    
            self._model = model
    
        @pyqtSlot(int)
        def change_amount(self, value):
            self._model.amount = value
    
            # calculate even or odd
            self._model.even_odd = 'odd' if value % 2 else 'even'
    
            # calculate button enabled state
            self._model.enable_reset = True if value else False
    

    change_amount 函数从小部件中获取新值、​​执行逻辑并在模型上设置属性。

    型号

    模型类存储程序数据和状态以及一些用于宣布对这些数据进行更改的最小逻辑。此模型不应与 Qt 模型 (see http://qt-project.org/doc/qt-4.8/model-view-programming.html) 混淆,因为它不是一回事。

    模型可能如下所示:

    from PyQt5.QtCore import QObject, pyqtSignal
    
    
    class Model(QObject):
        amount_changed = pyqtSignal(int)
        even_odd_changed = pyqtSignal(str)
        enable_reset_changed = pyqtSignal(bool)
    
        @property
        def amount(self):
            return self._amount
    
        @amount.setter
        def amount(self, value):
            self._amount = value
            self.amount_changed.emit(value)
    
        @property
        def even_odd(self):
            return self._even_odd
    
        @even_odd.setter
        def even_odd(self, value):
            self._even_odd = value
            self.even_odd_changed.emit(value)
    
        @property
        def enable_reset(self):
            return self._enable_reset
    
        @enable_reset.setter
        def enable_reset(self, value):
            self._enable_reset = value
            self.enable_reset_changed.emit(value)
    
        def __init__(self):
            super().__init__()
    
            self._amount = 0
            self._even_odd = ''
            self._enable_reset = False
    

    写入模型会通过setter 修饰函数中的代码自动向任何侦听视图发出信号。或者,控制器可以在决定时手动触发信号。

    如果 Qt 模型类型(例如 QStringListModel)已与小部件连接,则包含该小部件的视图根本不需要更新;这通过 Qt 框架自动发生。

    界面源文件

    为了完成,此处包含示例main_view.ui 文件:

    <?xml version="1.0" encoding="UTF-8"?>
    <ui version="4.0">
     <class>MainWindow</class>
     <widget class="QMainWindow" name="MainWindow">
      <property name="geometry">
       <rect>
        <x>0</x>
        <y>0</y>
        <width>93</width>
        <height>86</height>
       </rect>
      </property>
      <widget class="QWidget" name="centralwidget">
       <layout class="QVBoxLayout">
        <item>
         <widget class="QSpinBox" name="spinBox_amount"/>
        </item>
        <item>
         <widget class="QLabel" name="label_even_odd"/>
        </item>
        <item>
         <widget class="QPushButton" name="pushButton_reset">
          <property name="enabled">
           <bool>false</bool>
          </property>
         </widget>
        </item>
       </layout>
      </widget>
     </widget>
     <resources/>
     <connections/>
    </ui>
    

    通过调用将其转换为main_view_ui.py

    pyuic5 main_view.ui -o ..\views\main_view_ui.py
    

    资源文件mvc_app.qrc通过调用转换为mvc_app_rc.py

    pyrcc5 mvc_app.qrc -o ..\mvc_app_rc.py
    

    有趣的链接

    Why Qt is misusing model/view terminology?

    【讨论】:

    • 好的,我现在开始看到事情是如何结合在一起的。我认为它的设置方式 - main_view.py 是将模型链接到 ui_main_view.py 并向 main_cont.py 发出信号的粘合层。这是我无法理解如何使用 qtDesigner。感谢您的详细解释。
    • 添加了一些代码示例和一个可以自动创建大部分代码的代码生成器的链接。
    • 只是不要创建Ui_MainWindow() 对象,并在视图中手动构建小部件(如许多教程中所示)。
    • 我没有使用状态机 API,但我不明白为什么不能一起使用它们。
    • @thinwybk 这有点复杂,你能问一个新问题吗?
    猜你喜欢
    • 2013-04-09
    • 2012-08-11
    • 2013-05-19
    • 1970-01-01
    • 2016-11-04
    • 1970-01-01
    • 2016-07-27
    • 2015-04-13
    • 2017-08-11
    相关资源
    最近更新 更多