使用Qt Designer和PyQt / PySide的MVC设计

37 投票
1 回答
31887 浏览
提问于 2025-05-01 03:24

我是一名刚接触Python的新手,之前用过Java(还有SWT/Windowbuilder),现在在用Python/Qt4(QtDesigner)和PySide开发一个大型桌面应用时遇到了一些困难。

我希望把视图逻辑放在一个控制器类中,而不是放在.ui文件(以及它转换后的.py文件)里。这样做的原因有两个:首先,这样逻辑就和图形界面框架无关了;其次,.ui文件和生成的.py文件在任何修改后都会被覆盖!

我找到的例子大多是在一个庞大的MainWindow.py(从.ui生成)或MyForm.py(同样是从.ui生成)中添加操作代码。我看不出有什么方法可以把一个POPO控制器类和QtDesigner中的操作连接起来。

有没有人能告诉我如何使用QtDesigner创建一个大型应用的工作流程,特别是采用可扩展的MVC/P方法?

暂无标签

1 个回答

107

首先要知道,Qt已经有了视图和模型的概念,但这并不是你真正需要的。简单来说,Qt的MVC概念是一种自动将一个小部件(比如QListView)和一个数据源(比如QStringListModel)连接起来的方法,这样模型中的数据一旦改变,界面上就会自动更新,反之亦然。这是一个非常有用的功能,但它和应用程序级别的MVC设计模式是不同的。当然,这两者可以一起使用,这样会有一些明显的捷径。不过,应用程序级别的MVC设计还是需要手动编程的。

这里有一个MVC应用的例子,它包含一个视图、一个控制器和一个模型。这个视图有三个小部件,它们各自独立地监听并响应模型中数据的变化。旋转框和按钮都可以通过控制器来操作模型中的数据。

mvc_app

文件结构大致是这样的:

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视图文件可以导入相关的自动生成的类。

视图类应该包含连接小部件信号的最基本代码。视图事件可以调用并传递基本信息到视图类中的一个方法,然后再传递到控制器类中的一个方法,逻辑应该在这里处理。大概是这样的:

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的模型(参考这里)是不同的,不要混淆了。

模型的代码可能是这样的:

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框架自动完成。

UI源文件

为了完整性,这里包含了示例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

有趣的链接

为什么Qt在误用模型/视图术语?

撰写回答