PyQt和MVC模式

36

我正在尝试使用PyQt设计MVC模式。 我希望将所有程序拆分为3个部分:

  1. 抽象自所有Qt类的类(模型)
  2. 从模型提供数据给Qt应用程序的类(控制器)
  3. 具有定义方法SignalsToSlots的Qt应用程序,该方法将信号与控制器连接。

这是最优的吗?在PyQt开发中推荐使用什么方案?

4个回答

47

首先,你需要使用Qt4 designer设计GUI并使用pyuic4生成Python GUI。这将成为您的视图,您永远不要手动编辑这些Python文件。一定要使用设计师进行更改,这样可以确保您的视图与您的模型和控件分离。

对于控件元素,请创建一个从基本GUI小部件(例如QMainWindow)继承的中央类。然后,该对象将包含一个名为ui的成员,它是您刚生成的视图对象。

以下是来自教程的示例:

更新2013年:这里有关于PyQt和MVC模型的更多最新教程: PyQt MVC教程系列

import sys
from PyQt4 import QtCore, QtGui
from edytor import Ui_notepad

class StartQT4(QtGui.QMainWindow):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.ui = Ui_notepad()
        self.ui.setupUi(self)


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    myapp = StartQT4()
    myapp.show()
    sys.exit(app.exec_())

上述示例的关键点在于控制器包含UI,而不是直接继承UI。控制器将负责管理GUI的信号插槽连接,并为您的数据模型提供接口。
为了描述模型部分,我们需要一个例子,假设您的项目是创建电影收藏数据库。模型将包括代表单个电影的内部对象,以及代表电影列表的对象。您的控件将从视图中输入的数据并捕获信号,然后在要求模型更新自身之前对其进行验证。这一部分非常重要,如果可能的话,控制器不应直接访问模型,而应要求模型访问自身。
以下是此交互的一个小示例(未经测试,可能有些错别字):
class Movie():
    def __init__(self,title=None,year=None,genre=None):
        self.title=title
        self.year=year
        self.genre=genre
    def update(self,title=None,year=None,genre=None):
        self.title=title
        self.year=year
        self.genre=genre
    def to_xml(self,title=None,date=None,genre=None):
        pass #not implementing this for an example!

#when the controller tries to update it should use update function
movie1.update("Manos Hands Of Fate",1966,"Awesome")
#don't set by direct access, your controller shouldn't get that deep
movie1.title="Bad Idea" #do not want!

在MVC中,集中访问非常重要。例如用户可以通过双击屏幕上的标题或点击标题字段旁边的编辑按钮来更改标题,这两个界面都应该使用相同的更改方法。这并不意味着每个界面都调用movie.update_title(title)。我是指两个信号应在控制器中使用相同的方法。
尽可能使视图和控制器之间的关系为多对1。也就是说,如果你有5种方法可以更改GUI中的某个内容,请在控制器中只使用1种方法处理此操作。如果各个插座不兼容,则为每种方法创建一个单独的方法,然后再调用一个单一的方法。如果你为5种视图样式解决了5次问题,那么将视图与控件分开其实没有任何意义。而且现在,由于控制器中只有一种方式去完成某项任务,所以控制器和模型之间存在很好的1对1的关系。
至于将您的模型完全与Qt分开,这并不是必需的,而且可能会让您的生活更加困难。在模型中使用诸如QString之类的东西可能很方便,如果在另一个应用程序中,您不想要Gui的开销,但想要模型,那么只需导入QtCore即可。希望这可以帮到您!

3
+1,“不要通过直接访问设置,您的控制器不应该这么深入。”为什么不提到“self._title=title; self._year=year; ...”的可能性? 为了避免控制器被设计得太复杂,建议不要直接访问属性进行设置。即使可以使用self._title=title; self._year=year; ...,也建议遵循最佳实践,通过类中专门的方法来设置属性值。 - mlvljr
为什么手动编辑视图会导致它与模型和控制器不分离? - Kemeia
它不会引起它,但不这样做确实会防止它。 - cmaynard

9
是的,PyQt 使用 Model/View 概念(官方上没有“Controller”部分),但您可能对 PyQt 中这个概念有些扭曲的理解。
这里有两个部分:
1. 模型,是从适当的 PyQt 基础抽象模型类(如 QAbstractItemModel、QAbstractTableModel、QAbstractListModel 等)派生出来的。这些模型可以直接与数据源交互(文件、数据库),也可以代理您自己编写的 PyQt-不可知模型。 2. 视图,是在 Qt 库中实现的,通常不需要任何修改(例如:QTreeView、QTableView 等)。甚至一些更简单的控件,如 QComboBox,也可以作为 PyQt 模型的视图。
您应用程序的所有其他部分,响应信号等,都可以被视为“控制器”。
PyQt 还提供了一组预定义的“通用”模型,如果您只需要模型的简单功能,可以对其进行子类化或直接使用,例如 QStringListModel、QStandardItemModel 等。还有可以直接与数据库交互的模型,例如 QSqlTableModel。

2
请注意,Qt中的Model/View框架仅限于递归表格(尽管视图主要支持列表、行、表格和递归行(树))。如果您的数据具有不同的结构,可以在PyQt/Qt中构建通用的MVC应用程序,而不使用Qt的ModelView框架。(在我看来,总是将东西强行放入Qt的ModelView并不是最好的想法...) - Macke
事实上,我仍然不太明白没有控制器的Qt模型/视图框架的真正含义。 - Drake Guan
它可以更好地实现代码重用。当您在不同的视图中使用完全相同的模型类甚至实例时(例如,在一个对话框窗口中使用QTreeView,在另一个窗口中使用QComboBox),这是非常常见的。 - abbot
@freitass:是的,我认为你过于简化了。为了更好地理解,尝试将相同基于Qt的模型类连接到多个视图类,其中一些通过中间模型类(如QSortFilterProxyModel)进行连接。 - abbot
1
@freitass:在这个意义上,Qt模型是非常普通的模型,无论它们是不是代理你底层的“真正”的模型类(可以想一下你的代码中子类化的QAbstractItemModel和QStandardItemModel或QStringListModel)。我会说Qt视图并不是“纯粹”的视图,它们是视图和控制器的混合。另外,我想指出,这种纯度/术语的讨论对于实际软件开发来说是相当无关紧要的,只是你所偏好的抽象级别的问题而已 ;) - abbot
显示剩余3条评论

7
这里有一份官方详细指南,介绍了Qt架构如何为应用程序提供模型-视图设计。

http://doc.qt.io/qt-5/model-view-programming.html

在Qt中,视图和控制器被合并在一起,因此可以使用Model-View框架设计应用程序。
模型与数据源通信,为架构中的其他组件提供接口。通信的性质取决于数据源的类型以及模型的实现方式。视图从模型获取模型索引;这些是对数据项的引用。通过向模型提供模型索引,视图可以从数据源检索数据项。在标准视图中,委托渲染数据项。当编辑项目时,委托直接使用模型索引与模型通信。

...

模型、视图和代理之间使用信号和槽进行通信


给出权威、非常有帮助的文章来源,而不仅仅是说出自己的观点,这是一个加分项。我很感激Abbot和Maynard的回复,但这篇文章真正帮助我理解了Qt如何处理这个问题,所以我不会与框架对抗。 - labyrinth

0
我通常使用peewee来创建模型,这里是一个例子:
from peewee import *


from pathlib import Path
import datetime
import os

root_path = Path(__file__).resolve().parent
db_path = os.path.join(root_path, 'db', 'db.sqlite3')
db = SqliteDatabase(db_path)

class BaseModel(Model):
    class Meta:
        database = db

class Categorie(BaseModel) :
    name = CharField(max_length=64, unique=True, null=False)

class Product(BaseModel) :
    code = CharField(max_length=20, unique=True, null=False)
    name = CharField(max_length=128, unique=True, null=False)
    mark = CharField(max_length=128, unique=True, null=False)
    model = CharField(max_length=128, unique=True, null=False)
    category = ForeignKeyField(Categorie, null=False)
    image_path = CharField(max_length=1024, null=True)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接