首页 Qt 学习之路 2 Qt 学习之路 2(46):视图和委托

Qt 学习之路 2(46):视图和委托

65 11

前面我们介绍了模型的概念。下面则是另外一个基本元素:视图。在 model/view 架构中,视图是数据从模型到最终用户的途径。数据通过视图向用户进行显示。此时,这种显示方式不必须同模型的存储结构相一致。实际上,很多情况下,数据的显示同底层数据的存储是完全不同的。

我们使用QAbstractItemModel提供标准的模型接口,使用 QAbstractItemView提供标准的视图接口,而结合这两者,就可以将数据同表现层分离,在视图中利用前面所说的模型索引。视图管理来自模型的数据的布局:既可以直接渲染数据本身,也可以通过委托渲染和编辑数据。

视图不仅仅用于展示数据,还用于在数据项之间的导航以及数据项的选择。另外,视图也需要支持很多基本的用户界面的特性,例如右键菜单以及拖放。视图可以提供数据编辑功能,也可以将这种编辑功能交由某个委托完成。视图可以脱离模型创建,但是在其进行显示之前,必须存在一个模型。也就是说,视图的显示是完全基于模型的,这是不能脱离模型存在的。对于用户的选择,多个视图可以相互独立,也可以进行共享。

某些视图,例如QTableViewQTreeView,不仅显示数据,还会显示列头或者表头。这些是由QHeaderView视图类提供的。在《QFileSystemModel》一章的最后,我们曾经提到过这个问题。表头通常访问视图所包含的同一模型。它们使用QAbstractItemModel::headerData()函数从模型中获取数据,然后将其以标签 label 的形式显示出来。我们可以通过继承QHeaderView类,实现某些更特殊的功能。

正如前面的章节介绍的,我们通常会为视图提供一个模型。拿前面我们曾经见过的一个例子来看:

QStringList data;
data << "0" << "1" << "2";
model = new QStringListModel(this);
model->setStringList(data);

listView = new QListView(this);
listView->setModel(model);

QPushButton *btnShow = new QPushButton(tr("Show Model"), this);
connect(btnShow, SIGNAL(clicked()),
        this, SLOT(showModel()));
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(btnShow);

QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(listView);
layout->addLayout(buttonLayout);
setLayout(layout);

运行一下程序,这个界面十分简单:

NumberList 示例

跟我们前面的演示几乎一模一样。现在我们有一个问题:如果我们双击某一行,列表会允许我们进行编辑。但是,我们没办法控制用户只能输入数字——当然,我们可以在提交数据时进行检测,这也是一种办法,不过,更友好的方法是,根本不允许用户输入非法字符。为了达到这一目的,我们使用了委托。下面,我们增加一个委托:

class SpinBoxDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    SpinBoxDelegate(QObject *parent = 0) : QStyledItemDelegate(parent) {}

    QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
                          const QModelIndex &index) const;

    void setEditorData(QWidget *editor, const QModelIndex &index) const;
    void setModelData(QWidget *editor, QAbstractItemModel *model,
                      const QModelIndex &index) const;

    void updateEditorGeometry(QWidget *editor,
                              const QStyleOptionViewItem &option,
                              const QModelIndex &index) const;
};

正如前面所说,委托就是供视图实现某种高级的编辑功能。不同于经典的 Model-View-Controller(MVC)模式,model/view 没有将用户交互部分完全分离。一般地,视图将数据向用户进行展示并且处理通用的输入。但是,对于某些特殊要求(比如这里的要求必须输入数字),则交予委托完成。这些组件提供输入功能,同时也能渲染某些特殊数据项。委托的接口由QAbstractItemDelegate定义。在这个类中,委托通过paint()sizeHint()两个函数渲染用户内容(也就是说,你必须自己将渲染器绘制出来)。为使用方便,从 4.4 开始,Qt 提供了另外的基于组件的子类:QItemDelegateQStyledItemDelegate。默认的委托是QStyledItemDelegate。二者的区别在于绘制和向视图提供编辑器的方式。QStyledItemDelegate使用当前样式绘制,并且能够使用 Qt Style Sheet(我们会在后面的章节对 QSS 进行介绍),因此我们推荐在自定义委托时,使用QStyledItemDelegate作为基类。不过,除非自定义委托需要自己进行绘制,否则,二者的代码其实是一样的。

继承QStyledItemDelegate需要实现以下几个函数:

  • createEditor():返回一个组件。该组件会被作为用户编辑数据时所使用的编辑器,从模型中接受数据,返回用户修改的数据。
  • setEditorData():提供上述组件在显示时所需要的默认值。
  • updateEditorGeometry():确保上述组件作为编辑器时能够完整地显示出来。
  • setModelData():返回给模型用户修改过的数据。

下面依次看看各函数的实现:

QWidget *SpinBoxDelegate::createEditor(QWidget *parent,
                                       const QStyleOptionViewItem & /* option */,
                                       const QModelIndex & /* index */) const
{
    QSpinBox *editor = new QSpinBox(parent);
    editor->setMinimum(0);
    editor->setMaximum(100);
    return editor;
}

createEditor()函数中,parent 参数会作为新的编辑器的父组件。

void SpinBoxDelegate::setEditorData(QWidget *editor,
                                    const QModelIndex &index) const
{
    int value = index.model()->data(index, Qt::EditRole).toInt();
    QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
    spinBox->setValue(value);
}

setEditorData()函数从模型中获取需要编辑的数据(具有Qt::EditRole角色)。由于我们知道它就是一个整型,因此可以放心地调用toInt()函数。editor 就是所生成的编辑器实例,我们将其强制转换成QSpinBox实例,设置其数据作为默认值。

void SpinBoxDelegate::setModelData(QWidget *editor,
                                   QAbstractItemModel *model,
                                   const QModelIndex &index) const
{
    QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
    spinBox->interpretText();
    int value = spinBox->value();
    model->setData(index, value, Qt::EditRole);
}

在用户编辑完数据后,委托会调用setModelData()函数将新的数据保存到模型中。因此,在这里我们首先获取QSpinBox实例,得到用户输入值,然后设置到模型相应的位置。标准的QStyledItemDelegate类会在完成编辑时发出closeEditor()信号,视图会保证编辑器已经关闭,但是并不会销毁,因此需要另外对内存进行管理。由于我们的处理很简单,无需发出closeEditor()信号,但是在复杂的实现中,记得可以在这里发出这个信号。针对数据的任何操作都必须提交给QAbstractItemModel,这使得委托独立于特定的视图。当然,在真实应用中,我们需要检测用户的输入是否合法,是否能够存入模型。

void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
                                           const QStyleOptionViewItem &option,
                                           const QModelIndex &index) const
{
    editor->setGeometry(option.rect);
}

最后,由于我们的编辑器只有一个数字输入框,所以只是简单将这个输入框的大小设置为单元格的大小(由option.rect提供)。如果是复杂的编辑器,我们需要根据单元格参数(由option提供)、数据(由index提供)结合编辑器(由editor提供)计算编辑器的显示位置和大小。

现在,我们的委托已经编写完毕。接下来需要将这个委托设置为QListView所使用的委托:

listView->setItemDelegate(new SpinBoxDelegate(listView));

值得注意的是,new 操作符并不会真的创建编辑器实例。相反,只有在真正需要时,Qt 才会生成一个编辑器实例。这保证了程序运行时的性能。

然后我们运行下程序:

NumberList 编辑示例

65 评论

似水流年 2013年3月11日 - 23:40

豆子老兄,问个问题哈,在QGraphics框架下画折线图,怎么实现这条折线的灵活移动?

回复
豆子 2013年3月12日 - 15:18

如果要添加动画功能,可以看看 Qt 的动画框架

回复
路过 2013年3月21日 - 22:19

哥,您的网站打开好慢啊!我都着急了!我联通的网。

回复
豆子 2013年3月22日 - 11:28

你是什么地区的?我这边测试速度还是可以的

回复
路过 2013年3月22日 - 12:20

上海啊,我公司的网电信的,家里联通的,打开都很慢,需要好久好久才能打开,甚至是等了好久之后,打开失败。

回复
豆子 2013年3月25日 - 09:19

我在这里测试的,http://www.webkaka.com/webCheck.aspx?url=www.devbean.net&sitesearch=0,上海电信速度是正常的,不能说很慢的啊

回复
路过 2013年3月25日 - 21:14

哥,我去你给的网址看了下,在公司看的。上海电信的全部测试超时。

豆子 2013年3月25日 - 22:33

这个我也很奇怪,我使用南京电信、南京移动的网络全部正常,网站测速也是正常

twyok 2013年3月27日 - 11:24

豆子老兄:
QStyledItemDelegate(QObject *parent = 0) : QStyledItemDelegate(parent) {}
这应该修改成:
SpinBoxDelegate (QObject *parent = 0):QStyledItemDelegate(parent) {}

回复
豆子 2013年3月27日 - 12:45

多谢指出哦!

回复
twyok 2013年3月27日 - 13:57

不用谢,加油

回复
Const_Lin 2014年3月18日 - 22:40

文中好像还没改....

回复
Const_Lin 2014年3月18日 - 22:41

分号那里。。

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

哪段代码没有修改?

回复
GUEST 2013年4月22日 - 09:55

interpretText(); 是干嘛用的?

回复
豆子 2013年4月22日 - 11:06

这是 QSpinBox 的一个函数,说明 QSpinBox 需要解释输入框中的值。

回复
wuboaini 2013年4月23日 - 23:13

请问如果要 在tableView中设置 第一行输入的字符不能相同。数据保存在数据库中。这设置应该写在哪个函数中?

回复
豆子 2013年4月24日 - 09:13

如果你是说类似主键的设置,要么考虑在保存时检测数据库异常,这是交给数据库去保证;要么自己在保存时循环一遍,这是交给程序去保证;要么重写编辑器的 change() 或者失去焦点事件,在事件处理中循环检测。

回复
疯子 2013年9月6日 - 17:27

请问有什么方法能使列表每一项的高度增加?我感觉是在视图中处理,但是没有找到具体方法。

回复
豆子 2013年9月6日 - 17:38

你需要继承 QStyledItemDelegate,重写它的 sizeHint() 函数

回复
疯子 2013年9月7日 - 11:39

已重写了,只是return 了个QSize,达到预期的效果,只是这个函数的两个参数有什么作用。

回复
达达 2013年9月26日 - 17:17

豆子,我想问下,如果我要在listView通过代理来createEditor()里面创建多个组件,这个createEditor()函数怎么实现:
比如:listView的一个Item里面,即有lineEdit,还有lalbel等。。。

回复
豆子 2013年9月27日 - 14:26

你可以把所需要的组件放在一个 QWidget 上,将这个 QWidget 返回

回复
NashLegend 2013年9月29日 - 15:19

如何在QTableView里面添加自定义的控件呢,不是createEditor,就是普通的显示,看起来应该是在paint()函数里面写,但是不知道如何绘制自定义的控件

回复
豆子 2013年9月30日 - 09:02

用于显示的组件可以通过自定义 delegate 实现。例如继承 QItemDelegate 或者 QStyledItemDelegate,重写其 paint() 函数。比如你想在里面添加一个按钮,就可以在 paint() 函数新建一个 QPushButton,然后用 QApplication::style()->drawControl() 把这个组件绘制出来。

回复
realymylove 2013年11月27日 - 15:41

楼主能否解释下这句:“SpinBoxDelegate(QObject *parent = 0) : QStyledItemDelegate(parent) {}”?我把他改为“SpinBoxDelegate(QObject *parent = 0);”后编译通过。

回复
豆子 2013年11月27日 - 21:22

这句其实是一个空的构造函数,只不过这个构造函数的实现与声明写在了一起。按照你的代码,只是一个声明,应该也有这么一个实现才对,否则在调用构造函数时会有错误的

回复
realymylove 2013年11月28日 - 10:46

明白了,谢谢指点!另外,确实如豆子所言,我在程序中将SpinBoxDelegate类分为头文件和源文件,在头文件中生命构造函数,在源文件中构造了一个空的构造函数。

回复
豆纸 2016年3月21日 - 20:12

这个编译不会通过啊 把声明和实现放一起

回复
qiangmingliu@hotmail.com 2014年3月11日 - 22:54

QApplication::style()->drawControl() ,请我是用那个属性,来绘制自定义widget,piant 函数里面,new 可能有内存泄漏,,能否有简约的代码展示一下,谢谢

回复
豆子 2014年3月13日 - 09:24

你可以看看这个函数的文档。第一个参数就是指定绘制哪一个组件。这里使用一个枚举代替的,不需要将这个组件 new 出来。

回复
tony 2014年3月24日 - 13:37

你好,默认的委托双击才可以显示控件进行编辑,我现在想要单击Item就能生成委托进行比编辑,应该怎样实现呢?谢谢

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

试着重写下鼠标事件呢?单击编辑就不好实现选择,一般很少单击编辑的。

回复
tony 2014年3月24日 - 16:59

好的,谢谢,我试一下重写鼠标事件。
现在我是想实现一个属性管理器,根据不用属性类型在QTableView中插入不同的控件,想做成QtCreator属性管理器那种单击就能呈现控件进行选择的形式。

回复
linlq 2016年10月24日 - 11:36

tony您好,请问您重写鼠标事件完成了吗?我也想实现单击显示控件的功能。

回复
welliam 2014年3月29日 - 09:27

豆子,请问个问题。
用 QTreeWidget::setItemWidget 把widget加入到TreeWidget里,假如此widget显示大小是100X100,那TreeWidget里这整个行高度也变成了100(每行里还有其他文字内容)。在选中此行时,有没有办法让加入widget的这列不显示背景色,而其他列的背景色只显示指定的高度?

回复
welliam 2014年3月29日 - 15:47

哈哈,插入widget的那列不显示背景色搞定了!

另一个问题:一行被选中时的背景色怎么控制显示范围 ?比如一行高为100,但是我只想让背景色显示在0到20的区间。 这个我想是用Delegate,但是找不到方法。

回复
豆子 2014年3月29日 - 16:18

具体没有试过,不过你可以试试在 paintEvent 函数中绘制。利用第二个 option 参数,可以知道当前是不是选中状态,如果是的话就自己绘制一个选中的样式。

回复
welliam 2014年3月29日 - 16:49

刚试了一下,paint里用 option.state & QStyle::State_Selected 判断选中状态,然后自己用painter画一个矩形当做被选中的样式。
可是有个问题,画出来的图形会把原来的文字信息挡住。(不能发截图,看我手工画一个)
这是1行4列的显示
------------------------------——————
这里是 | name | age | male |
插入的| | | |
widget | | | |
-----------------------------——————

如果像前面说那样画一个图形的话,会把上面的文字挡住。
------------------------------——————
这里是 | ***** | age | male |
插入的| | | |
widget | | | |
-----------------------------——————

回复
豆子 2014年3月31日 - 16:28

被挡住可能是绘制调用顺序的问题。改变一下父类实现的调用顺序看看,不行的话可以自己再重新绘制一遍这个数据。

welliam 2014年4月1日 - 09:36

果然是调用顺序的问题。。。
唉,最基本的没想起来,光往复杂的去想了,还是基础不牢靠啊

回复
Tony 2014年4月2日 - 10:06

您好,我想问一下,怎么在QTreeView中在一个parent下的不同child item插入不同的委托?谢谢~~

回复
welliam 2014年4月2日 - 10:21

这个应该是用setItemDelegateForColumn、setItemDelegateForRow()直接设置委托吧,
https://www.devbean.net/2013/02/qt-study-road-2-model/ 可以看看这个,根据parent、row、cow可以确定一个item的。

我的一点想法,你可以试试。

回复
Tony 2014年4月2日 - 10:44

好的,去看一下,谢谢~

回复
Tony 2014年4月2日 - 11:18

etItemDelegateForColumn()和setItemDelegateForRow()只有一个row和delegate参数,加入我现在要对第一个根节点的第一个孩子设置delegate,这时row参数还是0,会使得所有row为0的都设置成了此delegate,怎样定位到单独的孩子呢?谢谢

回复
乔卫 2014年8月5日 - 11:49

好像不行,我对单独一列进行委托,但是运行后发现还是每列都被委托了。

回复
sherry 2014年5月7日 - 18:06

豆子,您好。请教一个问题。setModelData()这个函数中没有发closeEditor这个信号,是不是spinBox就不会关闭?这样会不会造成内存泄漏?真正实现时是不是要在这个函数的最后发closeEditor信号?

回复
豆子 2014年5月8日 - 09:02

文档并没有说明需要自己发出这个信号。个人感觉应该是编辑器数据提交的时候会发出这个信号。

回复
xie 2015年8月13日 - 10:56

请教一个问题:怎么把QListView右边的scrollbar移动到左边?

回复
Kian 2015年12月8日 - 14:11

文中提到:
标准的QStyledItemDelegate类会在完成编辑时发出closeEditor()信号,视图会保证编辑器已经关闭并且销毁,因此无需对内存进行管理。
但是在QAbstractItemView::setItemDelegateForColumn的官方文档中有提到:
Any existing column delegate for column will be removed, but not deleted. QAbstractItemView does not take ownership of delegate.
所以我想请问下这两种说法到底哪一种说法是正确的?目前在处理多个model共用一个view的情况下,我是采用QHash存储委托的指针,然后手动delete,实际上运行也没有报错。

回复
豆子 2015年12月8日 - 17:06

应该以文档为准,这里应该是视图将委托移除但并不销毁,这样的话你的处理就是正确的。

回复
cc 2015年12月27日 - 11:31

为什么设置了delegate后,我的程序跑出来没有显示spinbox,双击每一行后还会crash,求指教,难道是因为用的qt5的原因?

回复
豆子 2015年12月29日 - 16:01

这个应该与 Qt 5 没有关系,可能是内存哪里有问题。崩溃问题大多源于内存。

回复
cc 2015年12月29日 - 16:03

已经解决了,谢谢回复哦

回复
周宇全 2016年1月24日 - 17:12

豆子哥最近一直在看您的教程,感觉写的都很好但是...因为是没什么开发经验的学生所以对那些代码结构什么的还不是很清楚,有没有完整的样例代码呢?如果有希望发一份供我学习~

回复
豆子 2016年1月25日 - 09:47

文中的代码都是全的,只不过限于篇幅的介绍,有些头文件之类没有列出来,有些代码片段需要结合前面几篇补充完整,你可以试一下看看

回复
sailing 2016年6月14日 - 11:55

豆兄:把这一句
listView->setItemDelegate(new SpinBoxDelegate(listView));
放在
listView->setModel(model);
之后,
提示错误
error: expected type-specifier before 'SpinBoxDelegate'
请问是什么意思?

回复
sailing 2016年6月14日 - 13:12

原来是没有包含头文件,谢谢。

回复
乘风life 2016年7月16日 - 21:01

豆大大,你好,非常感谢你为大家提供的无私奉献,我觉得你的文章写到非常好,给我提供了很大的帮助。我想问一下本章节中那四个被重新实现的函数是何时被调用的,看起来好像是双击的时候,不过还是感觉迷糊,希望豆大大解惑!

回复
豆子 2016年7月17日 - 19:34

回调函数的调用是由 Qt 决定的,你只需要明白每个函数是做什么用的就好了

回复
Qingyun 2016年12月7日 - 21:08

豆大您好,本节那四个函数是回调函数是怎么知道的呢?qt里的其他函数如何判断是不是回调函数,又是谁的回调函数呢?望解答

回复
豆子 2016年12月10日 - 16:18

回调函数是由 Qt 确定的,可以阅读文档得知,其本身并没有明显的特征

回复
ester 2017年7月5日 - 14:27

收益不菲,感谢大神

回复
Tudou 2019年9月18日 - 10:24

豆子您好,博客文章是使用的markdown写的吗。不知道源文件还在不在。 如果可以的话,建议在GitHub上备份一份源文件。我这边无网时候比较多。希望可以离线下载源文件来学习。

回复
豆子 2019年9月19日 - 15:28

现在不是 markdown 的,全部是在 wordpress 上面写的,所以好像也不大方便下载

回复

发表评论

关于我

devbean

devbean

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

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