首页 Qt 学习之路 2 Qt 学习之路 2(50):自定义可编辑模型

Qt 学习之路 2(50):自定义可编辑模型

17 4K

上一章我们了解了如何自定义只读模型。顾名思义,只读模型只能够用于展示只读数据,用户不能对其进行修改。如果允许用户修改数据,则应该提供可编辑的模型。可编辑模型与只读模型非常相似,至少在展示数据方面几乎是完全一样的,所不同的是可编辑模型需要提供用户编辑数据后,应当如何将数据保存到实际存储值中。

我们还是利用上一章的CurrencyModel,在此基础上进行修改。相同的代码这里不再赘述,我们只列出增加以及修改的代码。相比只读模型,可编辑模型需要增加以下两个函数的实现:

Qt::ItemFlags flags(const QModelIndex &index) const;
bool setData(const QModelIndex &index, const QVariant &value,
             int role = Qt::EditRole);

还记得之前我们曾经介绍过,在 Qt 的 model/view 模型中,我们使用委托 delegate 来实现数据的编辑。在实际创建编辑器之前,委托需要检测这个数据项是不是允许编辑。模型必须让委托知道这一点,这是通过返回模型中每个数据项的标记 flag 来实现的,也就是这个 flags() 函数。这本例中,只有行和列的索引不一致的时候,我们才允许修改(因为对角线上面的值恒为 1.0000,不应该对其进行修改):

Qt::ItemFlags CurrencyModel::flags(const QModelIndex &index) const
{
    Qt::ItemFlags flags = QAbstractItemModel::flags(index);
    if (index.row() != index.column()) {
        flags |= Qt::ItemIsEditable;
    }
    return flags;
}

注意,我们并不是在判断了index.row() != index.column()之后直接返回Qt::ItemIsEditable,而是返回QAbstractItemModel::flags(index) | Qt::ItemIsEditable。这是因为我们不希望丢弃原来已经存在的那些标记。

我们不需要知道在实际编辑的过程中,委托究竟做了什么,只需要提供一种方式,告诉 Qt 如何将委托获得的用户输入的新的数据保存到模型中。这一步骤是通过setData()函数实现的:

bool CurrencyModel::setData(const QModelIndex &index,
                            const QVariant &value, int role)
{
    if (index.isValid()
            && index.row() != index.column()
            && role == Qt::EditRole) {
        QString columnCurrency = headerData(index.column(),
                                            Qt::Horizontal, Qt::DisplayRole)
                                     .toString();
        QString rowCurrency = headerData(index.row(),
                                         Qt::Vertical, Qt::DisplayRole)
                                  .toString();
        currencyMap.insert(columnCurrency,
                           value.toDouble() * currencyMap.value(rowCurrency));
        emit dataChanged(index, index);
        return true;
    }
    return false;
}

回忆一下我们的业务逻辑:我们的底层数据结构中保存的是各个币种相对美元的汇率,显示的时候,我们使用列与行的比值获取两两之间的汇率。例如,当我们修改currencyMap["CNY"]/currencyMap["HKD"]的值时,我们认为人民币相对美元的汇率发生了变化,而港币相对美元的汇率保持不变,因此新的数值应当是用户新输入的值与原来currencyMap["HKD"]的乘积。这正是上面的代码所实现的逻辑。另外注意,在实际修改之前,我们需要检查 index 是否有效,以及从业务来说,行列是否不等,最后还要检查角色是不是Qt::EditRole。这里为方便起见,我们使用了Qt::EditRole,也就是编辑时的角色。但是,对于布尔类型,我们也可以选择使用设置Qt::ItemIsUserCheckable标记的Qt::CheckStateRole,此时,Qt 会显示一个选择框而不是输入框。注意这里的底层数据都是一样的,只不过显示方式的区别。当数据重新设置是,模型必须通知视图,数据发生了变化。这要求我们必须发出dataChanged()信号。由于我们只有一个数据发生了改变,因此这个信号的两个参数是一致的(dataChanged()的两个参数是发生改变的数据区域的左上角和右下角的索引值,由于我们只改变了一个单元格,所以二者是相同的)。最后,如果数据修改成功就返回 true,否则返回 false。

当我们完成以上工作时,还需要修改一下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 || role == Qt::EditRole) {
        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();
}

我们的修改很简单:仅仅是增加了role == Qt::EditRole这么一行判断。这意味着,当是EditRole时,Qt 会提供一个默认值。我们可以试着删除这个判断来看看其产生的效果。

最后运行一下程序,修改一下数据就会发现,当我们修改过一个单元格后,Qt 会自动刷新所有受影响的数据的值。这也正是 model / view 模型的强大之处:对数据模型的修改会直接反映到视图上。

17 评论

hiitiger 2013年6月18日 - 18:04

QWidget体系自带的model view不是很好用。可以用Qt的model自己写个model/view框架

回复
HaborHuang 2014年2月16日 - 16:24

楼主你好,我想问一下
Qt::ItemFlags flags = QAbstractItemModel::flags(index);
是什么意思?除了初始化flags之外还有别的用以吗?如果不这样初始化flags是否可以呢?

回复
豆子 2014年2月16日 - 19:13

主要是为了保持原始的标记位的数据,因为后面需要使用这个数据做与、或运算等,所以有必要保留原始值。其他并没有什么特别的用意。

回复
HaborHuang 2014年2月16日 - 19:14

谢谢楼主!

回复
dreamychi 2015年11月19日 - 15:51

上一章我们了解了如何自定义只读模型。顾名思义,只读模型只能够用于展示只读数据,用户不能对其进行修改。如果允许用户修改数据,则应该提供可编译的模型。可编辑模型与只读模型非常相似,至少在展示数据方面几乎是完全一样的,所不同的是可编译模型需要提供用户编辑数据后

这里是不是 可编译模型 → 可编辑模型?

回复
豆子 2015年11月20日 - 09:29

应该是这样的,感谢指出!

回复
LinWM 2015年12月17日 - 16:30

整个编辑的过程大致是这样的:模型flags()给每个数据项做标记,用户双击触发委托(也不一定是双击触发,视情况而定),委托查询该项是否可编辑,若是,则创建委托界面,用户编辑完数据后,模型通过setData()获取委托传过来的修改后的数据并进行保存。不知道豆子老兄是不是这样理解的?

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

我感觉是的

回复
hqwhqwhq 2016年2月24日 - 21:05

豆子老师,问一个比较菜的问题:我是不是要单独写一个委托类来完成可编辑的功能啊?

回复
hqwhqwhq 2016年2月24日 - 21:09

知道哪里错了。。。

回复
豆纸 2016年3月26日 - 10:40

豆子老师
你有没有C++ GUI Programming with Qt 4, 2nd Edition 这本书的pdf 版
可以 发给我吗?

回复
李广鹏 2016年4月21日 - 16:31

老师你好,我想问一下,当模型中的原数据如果设置为public,当在model对象外改变原数据的值时,view如何自动跟着变,现在的情况是,必须点击一下tableView,显示的数据才会变,太不灵活了

回复
Bill Du 2016年7月14日 - 15:52

楼主,我是一个初初初初学者,请教:
我需要用model/view写出一个列表,这个列表显示的内容并非只显示一个文本(要显示多个文本和图标),我已经按你教的写好了一个自定义的 ListModel, 能把文字显示到 QListView 上。但不会让显示自定义的控件。网上找了很多,虽然关键字是 QListView,但内容却是 QListWidget。我不用 QListWidget 是因为我的内容是一直会变的,好像 Widget 的内容变起来不方便(是不是这样?)。请问你可以讲霁怎么写显示到 QListView 的自定义 Item 吗?我找了一个说是 ItemDelegate 的paint 方法的,但没有更详细的内容。你的帖子中也有 ItemDelegate 的东西,但我没有学到。请教我怎么在 paint 中画上控件,或教我什么是正确的方法。(没有在用 QML)谢谢谢谢

回复
小熊博士 2019年8月30日 - 10:16

豆子老师,请教一个疑惑,setCurrencyMap为了达到类似锁操作,添加了BeginResetModel()和EndResetModel(), 然后在setData函数里面更新currencyMap时,并没有做任何保护操作,是不是Model/View框架自己实现了在调用setData时候增加类似锁操作呢?

回复
小熊博士 2019年8月30日 - 10:22

豆子老师,请教另一个疑惑,我尝试把data函数里判断role == Qt::EditRole去掉, 数据任然可编辑。 然后又把setData里面的emit dataChanged()去掉,编辑回车后,相关项数据跟着也改变了。这是为什么呀? 是不是Qt默认setData返回true, 默认emit。 然后如果ItemFlags 包含Qt::ItemIsEditable时候,data默认回显示role==QtEditRole的角色数据。

回复
小熊博士 2019年8月30日 - 10:22

补充一下Qt版本5.12.2

回复
dll 2021年3月9日 - 20:08

话说,setData里面重新计算值的时候,表达式
currencyMap.insert(columnCurrency, value.toDouble() * currencyMap.value(rowCurrency));
不出意外的话把columnCurrency和rowCurrency弄反了。。。

回复

发表评论

关于我

devbean

devbean

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

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