首页 Qt 学习之路 2 Qt 学习之路 2(49):自定义只读模型

Qt 学习之路 2(49):自定义只读模型

21 3K

model/view 模型将数据与视图分割开来,也就是说,我们可以为不同的视图,QListViewQTableViewQTreeView提供一个数据模型,这样我们可以从不同角度来展示数据的方方面面。但是,面对变化万千的需求,Qt 预定义的几个模型是远远不能满足需要的。因此,我们还必须自定义模型。

类似QAbstractView类之于自定义视图,QAbstractItemModel 为自定义模型提供了一个足够灵活的接口。它能够支持数据源的层次结构,能够对数据进行增删改操作,还能够支持拖放。不过,有时候一个灵活的类往往显得过于复杂,所以,Qt 又提供了QAbstarctListModelQAbstractTableModel两个类来简化非层次数据模型的开发。顾名思义,这两个类更适合于结合列表和表格使用。

本节,我们正式开始对自定义模型进行介绍。

在开始自定义模型之前,我们首先需要思考这样一个问题:我们的数据结构适合于哪种视图的显示方式?是列表,还是表格,还是树?如果我们的数据仅仅用于列表或表格的显示,那么QAbstractListModel或者QAbstractTableModel 已经足够,它们为我们实现了很多默认函数。但是,如果我们的数据具有层次结构,并且必须向用户显示这种层次,我们只能选择QAbstractItemModel。不管底层数据结构是怎样的格式,最好都要直接考虑适应于标准的QAbstractItemModel的接口,这样就可以让更多视图能够轻松访问到这个模型。

现在,我们开始自定义一个模型。这个例子修改自《C++ GUI Programming with Qt4, 2nd Edition》。首先描述一下需求。我们想要实现的是一个货币汇率表,就像银行营业厅墙上挂着的那种电子公告牌。当然,你可以选择QTableWidget。的确,直接使用QTableWidget确实很方便。但是,试想一个包含了 100 种货币的汇率表。显然,这是一个二维表,并且对于每一种货币,都需要给出相对于其他 100 种货币的汇率(我们把自己对自己的汇率也包含在内,只不过这个汇率永远是 1.0000)。现在,按照我们的设计,这张表要有 100 x 100 = 10000 个数据项。我们希望减少存储空间,有没有更好的方式?于是我们想,如果我们的数据不是直接向用户显示的数据,而是这种货币相对于美元的汇率,那么其它货币的汇率都可以根据这个汇率计算出来了。比如,我存储人民币相对美元的汇率,日元相对美元的汇率,那么人民币相对日元的汇率只要作一下比就可以得到了。这种数据结构就没有必要存储 10000 个数据项,只要存储 100 个就够了(实际情况中这可能是不现实的,因为两次运算会带来更大的误差,但这不在我们现在的考虑范畴中)。

于是我们设计了CurrencyModel类。它底层使用QMap<QString, double>数据结构进行存储,QString类型的键是货币名字,double类型的值是这种货币相对美元的汇率。(这里提一点,实际应用中,永远不要使用 double 处理金额敏感的数据!因为 double 是不精确的,不过这一点显然不在我们的考虑中。)

首先从头文件开始看起:

class CurrencyModel : public QAbstractTableModel
{
public:
    CurrencyModel(QObject *parent = 0);
    void setCurrencyMap(const QMap<QString, double> &map);
    int rowCount(const QModelIndex &parent) const;
    int columnCount(const QModelIndex &parent) const;
    QVariant data(const QModelIndex &index, int role) const;
    QVariant headerData(int section, Qt::Orientation orientation, int role) const;
private:
    QString currencyAt(int offset) const;
    QMap<QString, double> currencyMap;
};

这段代码平淡无奇,我们继承了QAbstractTableModel类,然后重写了所要求的几个函数。构造函数同样如此:

CurrencyModel::CurrencyModel(QObject *parent)
    : QAbstractTableModel(parent)
{
}

rowCount()columnCount()用于返回行和列的数目。记得我们保存的是每种货币相对美元的汇率,而需要显示的是它们两两之间的汇率,因此这两个函数都应该返回这个 map 的项数:

int CurrencyModel::rowCount(const QModelIndex & parent) const
{
    return currencyMap.count();
}

int CurrencyModel::columnCount(const QModelIndex & parent) const
{
    return currencyMap.count();
}

headerData()用于返回列名:

QVariant CurrencyModel::headerData(int section, Qt::Orientation, int role) const
{
    if (role != Qt::DisplayRole) {
        return QVariant();
    }
    return currencyAt(section);
}

我们在前面的章节中介绍过有关角色的概念。这里我们首先判断这个角色是不是用于显示的,如果是,则调用currencyAt()函数返回第 section 列的名字;如果不是则返回一个空白的QVariant对象。currencyAt()函数定义如下:

QString CurrencyModel::currencyAt(int offset) const
{
    return (currencyMap.begin() + offset).key();
}

如果不了解QVariant类,可以简单认为这个类型相当于 Java 里面的 Object,它把 Qt 提供的大部分数据类型封装起来,起到一个类型擦除的作用。比如我们的单元格的数据可以是 string,可以是 int,也可以是一个颜色值,这么多类型怎么使用一个函数返回呢?回忆一下,返回值并不用于区分一个函数。于是,Qt 提供了QVariant类型。你可以把很多类型存放进去,到需要使用的时候使用一系列的 to 函数取出来即可。比如把 int 包装成一个 QVariant,使用的时候要用QVariant::toInt()重新取出来。这非常类似于 union,但是 union 的问题是,无法保持没有默认构造函数的类型,于是 Qt 提供了QVariant作为 union 的一种模拟。

setCurrencyMap()函数则是用于设置底层的实际数据。由于我们不可能将这种数据硬编码,所以我们必须为模型提供一个用于设置的函数:

void CurrencyModel::setCurrencyMap(const QMap<QString, double> &map)
{
    beginResetModel();
    currencyMap = map;
    endResetModel();
}

我们当然可以直接设置 currencyMap,但是我们依然添加了beginResetModel()endResetModel()两个函数调用。这将告诉关心这个模型的其它类,现在要重置内部数据,大家要做好准备。这是一种契约式的编程方式。

接下来便是最复杂的data()函数:

QVariant CurrencyModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid()) {
        return QVariant();
    }

    if (role == Qt::TextAlignmentRole) {
        return int(Qt::AlignRight | Qt::AlignVCenter);
    } else if (role == Qt::DisplayRole) {
        QString rowCurrency = currencyAt(index.row());
        QString columnCurrency = currencyAt(index.column());
        if (currencyMap.value(rowCurrency) == 0.0) {
            return "####";
        }
        double amount = currencyMap.value(columnCurrency)
                            / currencyMap.value(rowCurrency);
        return QString("%1").arg(amount, 0, 'f', 4);
    }
    return QVariant();
}

data()函数返回一个单元格的数据。它有两个参数:第一个是QModelIndex,也就是单元格的位置;第二个是role,也就是这个数据的角色。这个函数的返回值是QVariant类型。我们首先判断传入的index是不是合法,如果不合法直接返回一个空白的QVariant。然后如果roleQt::TextAlignmentRole,也就是文本的对齐方式,返回int(Qt::AlignRight | Qt::AlignVCenter);如果是Qt::DisplayRole,就按照我们前面所说的逻辑进行计算,然后以字符串的格式返回。这时候你就会发现,其实我们在 if…else… 里面返回的不是一种数据类型:if 里面返回的是 int,而 else 里面是QString,这就是QVariant的作用了。

为了看看实际效果,我们可以使用这样的main()函数代码:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QMap<QString, double> data;
    data["USD"] = 1.0000;
    data["CNY"] = 0.1628;
    data["GBP"] = 1.5361;
    data["EUR"] = 1.2992;
    data["HKD"] = 0.1289;

    QTableView view;
    CurrencyModel *model = new CurrencyModel(&view);
    model->setCurrencyMap(data);
    view.setModel(model);
    view.resize(400, 300);
    view.show();

    return a.exec();
}

这是我们的实际运行效果:

自定义只读模型
自定义只读模型

21 评论

gameboy 2013年6月28日 - 18:07

return (currencyMap.begin() + offset).key();
这个看不明白,QMap相关的方法有没有介绍啊,一头雾水

回复
豆子 2013年6月28日 - 23:54

begin() 返回指向第一个元素的遍历器,类似于索引 0,加上 offset,相当于取索引为 offset 的键值对,key() 函数是取出该键值对的值。文章中大部分示例代码都没有函数说明,这些说明可以在文档中找到,所以没有必要面面俱到了,可以查看文档了哦。

回复
welliam 2014年3月26日 - 10:09

视图、模型、委托这一大堆东西觉得蛮复杂,有些地方还是不清楚。我再看看文档和Demo吧,把问题总结出来再来请教。

回复
豆子 2014年3月26日 - 13:25

是的,基本是最复杂的部分,但是思路很清晰,慢慢了解下就好了。

回复
DS离心泵 2015年12月11日 - 11:00

有个很菜的问题:这里Map数据不是随便输入的么?怎么出来的显示是按照顺序排列的,我没发现有排序的语句啊!

回复
豆子 2015年12月14日 - 21:21

QMap 文档里面有明确的说明:With QMap, the items are always sorted by key.

回复
DS离心泵 2015年12月14日 - 21:32

是的~我去查了哈搞清楚老~忘记来改落:)

回复
踏雪飞鸿 2015年12月18日 - 17:10

豆子老师,可以咨询个问题么,我通过Model-View实现对数据显示,现在有一个问题,刷新数据的时候,无法选中行。 我的数据时一行一行的增加的,目前的刷新函数用的是 { beginResetModel(); endResetModel();},这个应该是全部刷新,我试过emit dataChanged(),可是一直没有成功,我改如何进行局部刷新呢,求赐教

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

这个我不是很确定,按照 model/view 模型,底层数据改变,视图会自动刷新,不知道你的实现是怎样的?

回复
DS离心泵 2015年12月21日 - 09:01

不知道有没有理解错~我估计你用的是整个数据集~
这个Model/View虽然很方便,但毕竟是个通用的东西~他没有对数据分批处理功能~
要使之实用需要自己控制数据~分批提供分批显示~这也就是实现分页的算法~
前提是你的数据得自己分页~Model/View本身不提供分页功能~

回复
王翔 2016年3月27日 - 10:39

豆子老师 ,是不是每次刷新数据 都要setCurrencyMap(data);调用这个接口去刷新数据 ,还是只要改变 map ,currencyMap这个变量就自动改变?

回复
蜗牛 2016年4月7日 - 11:10

豆子老师你好,我现在有个需求,我想要设置指定单元格进行修改,需要怎么写

回复
豆子 2016年4月11日 - 21:42

可以在修改时判断单元格的位置是不是允许修改

回复
蜗牛 2016年4月13日 - 10:03

这个已经解决,但是又遇到新的问题,找了两天实在无法解决,请您帮忙看看:
pyqt4 怎么获取tablewidget中comboBox控件的值?
for x in range(rows):
#画出表格行数
#给算法列添加下拉框
comboBoxObj=QtGui.QComboBox(self.tableWidget)
comboBoxObj.addItems(comboBoxList)
self.tableWidget.setCellWidget(x,7,comboBoxObj)
这是写下拉框的程序,已经写入成功了,我到tablewidget中进行选择对应的项,然后读取comboBox里的值,
但是读取的时候发现有问题 我打印表名和comboBox这列的对象
print tableNameSave,self.tableWidget.cellWidget(row,7)
结果如下:
XPADC_CORP_ISSUE
XPADC_CORP_ISSUE
XPADC_CORP_ISSUE
XPADC_CORP_ISSUE
XPADC_CORP_ISSUE
XPADC_CORP_ISSUE
XPADC_CORP_ISSUE
XPADC_CORP_ISSUE None
XPADC_CORP_ISSUE None
XPADC_CORP_ISSUE None
XPADC_CORP_ISSUE None
XPADC_CORP_ISSUE None
XPADC_CORP_ISSUE None
为什么有一部分没有显示comboBox对象,而且有comboBox对象时用currentText()也获取不到内容

回复
蜗牛 2016年4月13日 - 10:04

XPADC_CORP_ISSUE
XPADC_CORP_ISSUE
XPADC_CORP_ISSUE
XPADC_CORP_ISSUE None
XPADC_CORP_ISSUE None
XPADC_CORP_ISSUE None

回复
蜗牛 2016年4月13日 - 10:05

XPADC_CORP_ISSUE PyQt4.QtGui.QComboBox object at 0x000000000417CA68
XPADC_CORP_ISSUE PyQt4.QtGui.QComboBox object at 0x000000000417CA68
XPADC_CORP_ISSUE PyQt4.QtGui.QComboBox object at 0x000000000417CA68
XPADC_CORP_ISSUE PyQt4.QtGui.QComboBox object at 0x000000000417CA68
XPADC_CORP_ISSUE
XPADC_CORP_ISSUE
XPADC_CORP_ISSUE None
XPADC_CORP_ISSUE None
XPADC_CORP_ISSUE None
居然显示不了

AryGone 2016年4月12日 - 22:13

麻烦问下豆子老师
文中int rowCount(const QModelIndex &parent) const;
与文档里int rowCount( const QModelIndex & parent = QModelIndex() ) const 有何区别么,文档里后边这个const QModelIndex & parent = QModelIndex() 有何意思啊

回复
豆子 2016年4月22日 - 16:33

文档中的 parent = QModelIndex() 是函数声明时的默认参数,当显示不传入这个参数时,会自动传入一个默认参数。由于子类不需要这个默认参数,所以没有加上。稳妥起见,你可以按照文档中的声明来写的。

回复
Sa 2018年3月20日 - 17:44

M/V模型中Model应该只是数据的提供者,但是当请求TextAlignmentRole时,model决定了view如何align,似乎违反了m/v的原则。并且如果一个model支持多个view,每个view有自己的alignment,如何支持呐?

回复
豆子 2018年3月25日 - 09:55

对于支持多个 view 的情况,可以考虑使用 model 代理,也就是再加一层 model,或者在 delegate 层面解决显示的问题。的确,model 决定 view 的显示有些不符合设计原则,Qt 给出这样的设计,或许是考虑到基于界面显示一致性的问题,这种显示方式一般不会在不同的 view 而有很大的区别。

回复
PengPeng 2020年4月11日 - 16:49

大哥,有个问题难受我好几天了,我使用自定义数据模型和自定义delegate,每次QList append完数据之后,列表就会自动跳到第100条数据,过程中也没有调用其他方法,我用的是qt5.14版本的,大佬能不能给个解题思路呀

回复

回复 蜗牛 取消回复

关于我

devbean

devbean

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

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