首页 Qt 学习之路 2 Qt 学习之路 2(41):model/view 架构

Qt 学习之路 2(41):model/view 架构

54 4.4K

有时,我们的系统需要显示大量数据,比如从数据库中读取数据,以自己的方式显示在自己的应用程序的界面中。早期的 Qt 要实现这个功能,需要定义一个组件,在这个组件中保存一个数据对象,比如一个列表。我们对这个列表进行查找、插入等的操作,或者把修改的地方写回,然后刷新组件进行显示。这个思路很简单,也很清晰,但是对于大型程序,这种设计就显得苍白无力。比如,在一个大型系统中,你的数据可能很大,全部存入一个组件的数据对象中,效率会很低,并且这样的设计也很难在不同组件之间共享数据。如果你要几个组件共享一个数据对象,要么你就要用存取函数公开这个数据对象,要么你就必须把这个数据对象放进不同的组件分别进行维护。

Smalltalk 语言发明了一种崭新的实现,用来解决这个问题,这就是著名的 MVC 模型。对这个模型无需多言。MVC 是  Model-View-Controller 的简写,即模型-视图-控制器。在 MVC 中,模型负责获取需要显示的数据,并且存储这些数据的修改。每种数据类型都有它自己对应的模型,但是这些模型提供一个相同的 API,用于隐藏内部实现。视图用于将模型数据显示给用户。对于数量很大的数据,或许只显示一小部分,这样就能很好的提高性能。控制器是模型和视图之间的媒介,将用户的动作解析成对数据的操作,比如查找数据或者修改数据,然后转发给模型执行,最后再将模型中需要被显示的数据直接转发给视图进行显示。MVC 的核心思想是分层,不同的层应用不同的功能。

Qt 4 开始,引入了类似的 model/view 架构来处理数据和面向最终用户的显示之间的关系。当 MVC 的 V 和 C 结合在一起,我们就得到了 model/view 架构。这种架构依然将数据和界面分离,但是框架更为简单。同样,这种架构也允许使用不同界面显示同一数据,也能够在不改变数据的情况下添加新的显示界面。为了处理用户输入,我们还引入了委托(delegate)。引入委托的好处是,我们能够自定义数据项的渲染和编辑。

Model View 概览
Model View 概览

如上图所示,模型与数据源进行交互,为框架中其它组件提供接口。这种交互的本质在于数据源的类型以及模型的实现方式。视图从模型获取模型索引,这种索引就是数据项的引用。通过将这个模型索引反向传给模型,视图又可以从数据源获取数据。在标准视图中,委托渲染数据项;在需要编辑数据时,委托使用直接模型索引直接与模型进行交互。

总的来说,model/view 架构将传统的 MV 模型分为三部分:模型、视图和委托。每一个组件都由一个抽象类定义,这个抽象类提供了基本的公共接口以及一些默认实现。模型、视图和委托则使用信号槽进行交互:

  • 来自模型的信号通知视图,其底层维护的数据发生了改变;
  • 来自视图的信号提供了有关用户与界面进行交互的信息;
  • 来自委托的信号在用户编辑数据项时使用,用于告知模型和视图编辑器的状态。

所有的模型都是QAbstractItemModel的子类。这个类定义了供视图和委托访问数据的接口。模型并不存储数据本身。这意味着,你可以将数据存储在一个数据结构中、另外的类中、文件中、数据库中,或者其他你所能想到的东西中。我们将在后面再详细讨论这些内容。

QAbstractItemModel提供的接口足够灵活,足以应付以表格、列表和树的形式显示的数据。但是,如果你需要为列表或者表格设计另外的模型,直接继承QAbstractListModelQAbstractTableModel类可能更好一些,因为这两个类已经实现了很多通用函数。关于这部分内容,我们也会在后文中详述。

Qt 内置了许多标准模型:

  • QStringListModel:存储简单的字符串列表。
  • QStandardItemModel:可以用于树结构的存储,提供了层次数据。
  • QFileSystemModel:本地系统的文件和目录信息。
  • QSqlQueryModelQSqlTableModelQSqlRelationalTableModel:存取数据库数据。

正如上面所说,如果这些标准模型不能满足你的需要,就必须继承QAbstractItemModelQAbstractListModel或者QAbstractTableModel,创建自己的模型类。

Qt 还提供了一系列预定义好的视图:QListView用于显示列表,QTableView用于显示表格,QTreeView用于显示层次数据。这些类都是QAbstractItemView的子类。这意味着,如果你要创建新的视图类,则可以继承QAbstractItemView

QAbstractItemDelegate
则是所有委托的抽象基类。自 Qt 4.4 依赖,默认的委托实现是QStyledItemDelegate。但是,QStyledItemDelegateQItemDelegate都可以作为视图的编辑器,二者的区别在于,QStyledItemDelegate使用当前样式进行绘制。在实现自定义委托时,推荐使用QStyledItemDelegate作为基类,或者结合 Qt style sheets。

如果你觉得 model/view 模型过于复杂,或者有很多功能是用不到的,Qt 还有一系列方便使用的类。这些类都是继承自标准的视图类,并且继承了标准模型。这些类并不是为其他类继承而准备的,只是为了使用方便。它们包括QListWidgetQTreeWidgetQTableWidget。这些类远不如视图类灵活,不能使用另外的模型,因此只适用于简单的情形。

54 评论

abc 2013年1月25日 - 14:47

你写的这些文章好像是参考书上的,或者直接从书上抄下来的

回复
豆子 2013年1月25日 - 16:38

是的,很多内容是文档里面的。理论上的东西大部分是雷同的,所以很多都是翻译自文档。

回复
xuefu 2013年5月6日 - 21:56

首先,感谢站主的无私奉献。。。顺便提个建议。。。字体和行距有点小啊,看着很伤眼。。。

回复
豆子 2013年5月7日 - 09:06

嗯嗯,感谢哦~ 等我调一下 CSS

回复
2016年12月16日 - 11:41

感谢楼主为初学者的付出

回复
老牛拉货车 2019年8月13日 - 18:03

语言表达很好,翻译通俗易懂。感谢作者。

回复
米米 2013年2月1日 - 10:14

你真是站着说话不腰疼。贴主辛苦归纳和自己的见解都在里面。这样都能很好的帮助初学者。你有本事也也归纳看看。别看了别人的还说别人。小人也

回复
msccreater 2013年2月21日 - 21:58

不多加评论,默默看文章学习

回复
大灰狼嘎嘎 2014年2月20日 - 13:51

豆哥问个问题:
在Qt Help的例程中,窗口部件(Wdiget)类型的类成员变量往往创建在堆上,而像QTCpSocket、QThread类型的成员变量,为什么往往建在栈上?

回复
大灰狼嘎嘎 2014年2月20日 - 14:49

换句话说,类成员变量什么时候创建在栈上,什么时候创建在堆上?

回复
豆子 2014年2月20日 - 15:02

这个应该没有统一的规定,只能说看情况。由于 Qt 的半自动的内存管理机制,即便在堆上创建,只有要 parent 属性,也不用担心内存泄露,所以,很多 widgets 都会建议直接在堆上创建对象。这个还是主要看经验,没有更好的办法。

回复
大灰狼嘎嘎 2014年2月20日 - 15:12

那什么情况下,类成员建在栈上?为什么?

回复
豆子 2014年2月20日 - 17:23

一般集合类,比如 QList、QSet,数剧类,比如 QString、QVariant 等,都直接建在栈上。一方面因为集合类大部分有运算符重载,使用指针的话就不能直接调用重载的运算符;对于数据类,大部分数据是不可变的,因此使用对象来保证其不可变性(指针的话需要添加额外的 const 修饰),并且也有运算符方面的原因吧。

回复
大灰狼嘎嘎 2014年2月20日 - 19:05

我想一下,是不是除了 窗口部件,其他类成员一般都建立在栈上?
而窗口部件之所以建立在堆上,大概是为了防止栈溢出把。。

回复
大灰狼嘎嘎 2014年2月20日 - 19:06

不是栈溢出。。或许叫系统栈耗尽比较合适。。

回复
大灰狼嘎嘎 2014年2月20日 - 20:52

也不对。。
又想了一下,应该说建在堆上或建在栈上由两个因素决定:
1. 占用的内存。像图形界面这种占内存较大的放在堆中,防止栈耗尽。
2. 生命周期。如果希望成员变量的生命周期和父对象一样长的话,就创建在栈上,或者new object(this); 如果希望该成员变量生命周期和父对象不一致的话,就 new object;

豆哥觉得对不?

豆子 2014年2月21日 - 09:46

首先我们要清楚一个问题:类成员变量和函数内部的变量是不一样的。如果单纯拿出语句:Object o;,你不能确定是在堆上分配,还是在栈上分配。Object o; 语句的含义是,创建一个具有自动存储特性的对象。所谓“自动存储”,意思是这个对象创建的位置取决于其声明所在的上下文:如果这行代码出现在函数中,则会在栈上分配空间。但是,这行代码也可以出现在类成员,或者函数或类的外部(比如全局变量)。这种情况,例如:

struct Foo {
Object o;
};
Foo* foo = new Foo;

注意,foo 指针是在堆上面分配创建的,foo->o 同样在堆上创建。这是因为我们说过的,Object o; 语句的含义是自动存储,也就是由上下文决定。

真正复杂的是上面说的 Object o; 语句。对于带有 new 或者 malloc 的语句则没有异议,肯定是在堆上分配。

理解了这一点我们才可以继续说明。在选择的机器上面,栈和堆基本没有性能上的区别,或许曾经有,但那时很多年之前的事情。二者的区别在于生命周期。在栈上的对象,脱离了当前上下文后,会自动销毁。也就是说,如果你需要一个生命周期长于当前上下文的对象,只能选择在堆上分配。

有的开发者则建议,尽量在栈上分配,按需在堆上分配。也就是尽量选择栈,原因也很简单,因为完全不要你关心内存管理问题。但是当你需要一个对象更长的生命周期时,毫无选择,只能使用堆。

另外还有一点,不要认为你所做的就是内部真实实现的。比如:

void func () {
std::vector v;
}

这是 STL 的链表类。我们在栈上分配空间,但是,这个类其实是使用的堆存储其数据。也就是说,我们只是把这个类在栈上创建,而这个类管理的数据依然在堆上。毫无疑问,这个类的数据才是真正占用空间的。所以,你所说的,自己在堆上分配占用内存的对象,有时候还要被其内部实现左右。

总的来说,关于是在堆上还是在栈上创建对象,依赖于这个对象所需要的生命周期,与其它无关。

菊花三弄 2016年6月12日 - 10:52

struct A
{
A()
{
b = new B();
)

~A()
{
delete b;
}

B *b;
}

void main()
{
A a;
}

这样你就不用担心内存泄漏了,或者多用stl的智能指针

小熊博士 2019年8月18日 - 20:50

哇~这一段的讨论和豆子老师的讲解,真是好强大,好清晰

qiangmingliu@hotmail.com 2014年3月8日 - 20:35

你好,如果在Model /View 下在item绘制一个子类化的widget ,里面加了,按钮标签,如何TablevieW一显示就全部显示其Widget ,目前是选中才显示

回复
豆子 2014年3月10日 - 10:54

怀疑你是使用了 createEditor 函数而不是 paint 函数。前者是编辑器,在编辑时才会调用;后者是渲染器,渲染时使用。

回复
qiangmingliu@hotmail.com 2014年3月16日 - 21:39

void QPushDelegate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const
{

QPushButton *pButton = new QPushButton();
pButton->setText("hello");
QApplication::style()->drawControl(QStyle::CE_ItemViewItem,&option,painter,pButton);
}

QSize QPushDelegate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const
{
return QSize(100,40);
}
请问一下,这些代码,可以简约的在ListWidget 上显示全部是PushButton吗

回复
qiangmingliu@hotmail.com 2014年3月16日 - 21:40

这些继承QAbstractItemDelegate类的

回复
qiangmingliu@hotmail.com 2014年3月16日 - 21:58

class QPushDelegate : public QStyledItemDelegate
{
Q_OBJECT

public:

void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;

QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index)const ;
};
//! [0]

回复
豆子 2014年3月17日 - 09:55

如果你使用的是 QTableWidget,它直接就有 setCellWidget() 函数,可以设置每个单元格的组件;如果是 QTableView,则需要设置一个 delegate。不过你的 paint 里面的代码有问题,所以无法绘制。可以参考类似下面的代码:

void ButtonDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QStyleOptionButton button;
button.rect = option.rect;
button.text = tr("hello, world");
button.state = QStyle::State_Enabled;

QApplication::style()->drawControl(QStyle::CE_PushButton, &button, painter);
}

回复
qiangmingliu@hotmail.com 2014年3月17日 - 11:24

我本意是想绘制一个自定义widget,想简单的这样写,如果那样,这写法有问题吗

回复
豆子 2014年3月17日 - 11:28

你是指重写 QStyledItemDelegate 的方法?这种写法没有问题,而且也是 QTableView 唯一的方法。

回复
qiangmingliu@hotmail.com 2014年3月17日 - 11:49

对,但是我昨天试是没有成成功,不知是哪里出了问题

回复
qiangmingliu@hotmail.com 2014年3月17日 - 21:40

QPushButton *pButton = new QPushButton();
pButton->setText("hello");
pButton->setGeometry(option.rect);
pButton->setStyleSheet("background-color: blue");
pButton->show();
QApplication::style()->drawControl(QStyle::CE_CustomBase,&option,painter,pButton);

这些代码显示出来了,但没有父窗口,显示没有达到理想的效果

回复
豆子 2014年3月18日 - 08:55

不明白什么是“没有父窗口”?而且你这样写不是内存泄露么?这与我给你的示例代码是不一样的,你按照我给你的示例修改下看看。

回复
qiangmingliu@hotmail.com 2014年3月17日 - 22:28

if (index.isValid())
{

pButton->setText("hello");
pButton->setGeometry(option.rect);
pButton->setStyleSheet("background-color: blue");

QApplication::style()->drawControl(QStyle::CE_ItemViewItem,&option,painter,pButton);
}
只显示第五行

回复
qiangmingliu@hotmail.com 2014年3月18日 - 21:48

按你所说的确实能全部显示,但如果是自定义widget,那该如何入手,麻烦您讲下思路

回复
豆子 2014年3月19日 - 09:21

自定义 widget 也是类似的,只不过你要调用这个组件的 paint() 函数,把自己画出来。具体可以参考文档中的 Star Delegate Example 这个例子。

回复
liyongnan 2014年4月3日 - 16:26

豆子高手,您好!
QStandardItemModel *goodsModel = new QStandardItemModel(this);
QModelIndex index00,index22,index33;
...
index00 = goodsModel->index(0, 0, QModelIndex());
...

goodsModel->insertRow(0,index00);
index22 = goodsModel->index(0, 0,index00);
goodsModel->setData(index22,"1234567");
goodsModel->insertRow(0,index22);
index33 = goodsModel->index(0, 0,index22);
goodsModel->setData(index22,"98765");

结果 “1234567”这行可以插进去,并在treeview中显示出来了,但是紧接着的 子项“98765” 没有插进去, 是什么原因啊?
请多多指教,谢谢!

回复
liyongnan 2014年4月3日 - 16:31

补充一点: 怎么在一个已知 index下建立第一个子项呢? 好像用 insertRow 不行。

回复
豆子 2014年4月3日 - 16:43

两个 setData 都是设置的 index22,这是你需要的吗?

回复
liyongnan 2014年4月3日 - 16:51

index33 = goodsModel->index(0, 0,index22);
goodsModel->setData(index22,”98765″);
应该是:
index33 = goodsModel->index(0, 0,index22);
goodsModel->setData(index33,”98765″);

回复
liyongnan 2014年4月3日 - 16:56

goodsModel->insertRow(0,index22); 这一句没起作用。
如果起作用的话,treeview里相应位置会多有一个空白行

回复
豆子 2014年4月3日 - 17:29

你是想实现什么功能?同时插入两个子项,还是插入一个带有子项的子项?

liyongnan 2014年4月3日 - 17:41

您好! 是插入一个子项1,紧接着在刚插入的子项1里面再插入一个 孙子项2

回复
liyongnan 2014年4月3日 - 17:43

也就是您说的 插入一个带有子项的子项?

回复
liyongnan 2014年4月3日 - 18:34

是的,也就是您说的 插入一个带有子项的子项

回复
豆子 2014年4月4日 - 08:48

在 goodsModel->insertRow(0, index22); 之后添加一个 goodsModel->insertColumn(0, index22);。子项是作为第二列来实现的。

回复
liyongnan 2014年4月4日 - 09:57

谢谢豆子高手!

liyongnan 2014年4月4日 - 10:04

豆子高手, Qt 编辑源程序在用 google 输入法输入汉字后,光标就不见了, 有什么解决办法呢?

豆子 2014年4月4日 - 10:52

这个可能的原因很多,很大的可能是 Qt 与底层 OS 配合的问题,目前不大可能通过修改应用程序代码解决。

zhudx6512 2015年8月25日 - 21:27

豆子,你好

请问数据能直接储存在model里面吗?比如一个QList里。然后程序关闭打开数据都保持在那里

回复
豆子 2015年8月26日 - 12:53

model 是在内存中的,如果要实现你说的功能,必须把 model 中的数据持久化到硬盘上面。可以考虑使用文件系统或者数据库都可以实现。

回复
田园 2015年9月26日 - 11:08

感谢楼主的无私奉献

回复
ccppaa 2016年7月20日 - 11:40

请问大神,treeview的用法是什么样的,,想做一个分类查询,在左边把分类列出来,是用treeview比较合适吗,

回复
豆子 2016年7月20日 - 21:31

使用树是可以的,具体可以看看文档;另外也可以使用多个 list 来模拟层次的感觉

回复
canid 2016年9月19日 - 10:05

没有例子吗

回复
Jeff Xiang 2021年9月18日 - 15:48

真的很有用,把MVC模型深入浅出的讲清楚了,看完就至少知道想用什么模块功能去哪里找了,笔芯!

回复
Luo 2021年11月5日 - 14:35

当 MVC 的 V 和 C 结合在一起,我们就得到了 model/view 架构。不是m和v结合的么?

回复

回复 大灰狼嘎嘎 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2