【Qt6】列表模型——抽象基类
列表模型(Item Model),老周没有翻译为“项目模型”,因为 Project 和 Item 都可以翻译为“项目”,容易出现歧义。干脆叫列表模型。这个模型也确实是为数据列表准备的,它以 MVC 的概念为基础,在原始数据和用户界面视图之间搭建桥梁,使两者可以传递数据(提取、修改)。
Qt 里面使用列表控制比较复杂,需要先创建模型(Model)。当然,也有像 QListWidget 类这样已经封装好,开箱即食的,这个后面再扯,现在咱们的重点是弄清楚 Item Model 是啥玩意儿。
这里所说的 Item Model 并不是真正的数据,应该说算是个控制器。当用户界面要显示数据时,模型负责从原始数据那里提取值,再把值传到界面上呈现;如果用户界面要修改数据,通过输入框(QLineEdit等)输入/修改内容,然后传给模型,模型负责修改原始数据。这么看来,视图和原始数据不是直接通信的,模型就成了“中间商”。这个“中间商”可以不赚差价(按原始数据的样子呈现),也可能赚差价(把原始数据加工一下再让你看)。
列表模型有一个抽象基类,叫 QAbstractItemModel;对应地,视图组件也有一个抽象基类,叫 QAbstractItemView。另外,在模型和视图之间还有一个“代理人”,抽象基类叫 QAbstractItemDelegate,它干吗的呢?这是专业经纪人,负责门面工作。比如,在视图组件里呈现数据时用什么字体,什么颜色来绘制文本,用什么方式从模型提取数据等;在编辑数据时,有什么控件来输入文本。以及在编辑结束后,输入的内容怎么传给模型等。日常使用时咱们用到 QAbstractItemDelegate 不多,除非你自己想为数据项绘制 UI,或用自定义的编辑组件。如果只是改改外观什么的,还不如用 QSS 方便。
行了,不扯太远了,咱们只要知道这几个基类之间的关系就行了。咱们的重点还是放在 QAbstractItemModel 类上面。
QAbstractItemModel 有几个纯虚函数是必须在派生类中重写的:
1、index 方法,声明如下:
virtual QModelIndex index(
int row,
int column,
const QModelIndex &parent = QModelIndex()) const = 0;
列表模型中的索引,专门用一个叫 QModelIndex 的类表示。index 方法是根据传入的参数,返回 QModelIndex 对象。之所以要用 QModelIndex 类来表示列表项的索引,是因为它是由几个值组成的:
a、行号;
b、列号;
c、父索引。
Qt 中的列表模型用的是二维表结构,即由行和列组成,就像这样:
问:D在哪里?
答:row = 1,column = 0。
每个项又可以包含父级节点和子级节点,但上面的二维表只有一层,没有父级,所以它的 parent = QModelIndex()。用默认构造函数创建的 QModelIndex 表示无效索引,即行号是 -1,列号是 -1,无父无子。
综上所言,D 的索引就是:row = 1,col = 0,parent = QModelIndex()。
这个模型真正可怕的地方在于,每个索引都有父、子级。于是你可以构想下面这么恐怖的列表:
Root是一个无效的索引,可以认为是顶层的”父级“。A、B、C、D、E、F 的父级都是 Root,行列号由0开始编排,A在第一行第一列,所以 row=0,col=0,parent=Root。E有子节点,即 M、N、O、P,然后MNOP的行号和列号也要从 0 重新计算,即 N 的索引是 row=0, col=1, parent=E。最后,Q 这厮又有子节点,是一个只有一行的列表:R、S、T。于是,RST的行列号也重新计算。即 R 的索引是 row=0, col=0, parent=Q。
不过,实际使用时,一般不需要构建这么神的数据结构,而且这玩意放到用户界面上还不知道怎么显示好呢。毕竟,咱们在界面上常见的视图也就以下三种:
1)、多行,只有一列,这就相当于像数组这样的数据了。用 QListView 组件来呈现;
2)、一级二维表,由行、列组成,由 QTableView 组件呈现;
3)、多级节点,典型的就是 QTreeView 组件了。Qt 的 TreeView 比 .NET 的控件多了一个特点——可以在显示多级节点的同时显示表格。但要注意的是,只有首列才支持父子节点。所以,对于 QTreeView 视图,构建这样的数据也足够了:
2、parent 方法。它的声明如下:
virtual QModelIndex parent(const QModelIndex &child) const = 0;
返回 child 节点的父级节点,对于只有一层的列表,返回 QModelIndex() 即可。
3、rowCount 方法。声明如下:
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const = 0;
返回原始数据有总共有多少行。
4、columnCount 方法。它的声明如下:
virtual int columnCount(const QModelIndex &parent = QModelIndex()) const = 0;
该方法返回原始数据有多少列,如果是数组之类的,返回 1。
5、data 方法。声明如下:
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0;
这是个重要的成员,它要根据 index 参数指定的索引,返回数据项的值。这里要说一下叫”数据角色“的概念。说通俗不易懂一点就是返回的值的用途。比如,role 参数的默认值指定了 DisplayRole,意思就是你返回的值是要显示在用户界面上的,就是你想让用户看到的文本。Qt::ItemDataRole 枚举定义了一系列数据角色。
enum ItemDataRole {
DisplayRole = 0, // 显示在界面的内容
DecorationRole = 1, // 和文本一起显示的图标,类型一般是QIcon
EditRole = 2, // 当编辑数据时,返回给用户看的值
ToolTipRole = 3, // 显示在工具提示中的文本
StatusTipRole = 4, // 显示在状态栏中的文本
WhatsThisRole = 5, // 帮助信息,显示在”这是啥?“提示中
// Metadata
FontRole = 6, // 呈现数据时用啥字体
TextAlignmentRole = 7, // 文本的对齐方式
BackgroundRole = 8, // 返回画刷对象,用来绘制列表项的背景
ForegroundRole = 9, // 文本的颜色
CheckStateRole = 10, // 如果界面上显示了 checkbox,那么返回checkbox的状态(选中?未选中?未知?)
// Accessibility
AccessibleTextRole = 11, // 简练的辅助信息。用于像”讲述人“这些辅助工具
AccessibleDescriptionRole = 12, //详细辅助信息,用于像”讲述人“类似的辅助工具
// More general purpose
SizeHintRole = 13, // 返回该列表项希望显示的大小(宽多少,高多少)
InitialSortOrderRole = 14, // 数据第一次呈现时用的排序方式(升序?降序?)
// 下面五个不知道是什么鬼
// Internal UiLib roles. Start worrying whe
high.
DisplayPropertyRole = 27,
DecorationPropertyRole = 28,
ToolTipPropertyRole = 29,
StatusTipPropertyRole = 30,
WhatsThisPropertyRole = 31,
// Reserved,保留用来给开发者自定义角色。自定义角色从这个数值开始
UserRole = 0x0100
};
重写 data 方法实现了数据的只读模式,若数据支持编辑,必须重写 setData 方法,把内容写入原始数据。
如果要实现添加、删除数据项,还要重写以下方法:
6、insertRows:在行号为 row 处连续插入 count 行数据。
virtual bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex());
7、removeRows:从行号为 row 处开始,连续删除 count 行。
virtual bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());
8、insertColumns:从列号为 column 处起,连接插入 count 列。
virtual bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex());
9、removeColumns:从列号为 column 处开始,连续删除 count 列。
virtual bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex());
===============================================================
下面咱们做一个简单的模型练练手。该模型的原始数据是一个整数列表(QList<int>)。先实现只读功能,即需要重写 parent、index、rowCount、columnCount 和 data 方法。头文件的声明如下:
#ifndef MODELS_H
#define MODELS_H #include <QAbstractItemModel>
#include <QObject>
#include <QList> class MyItemModel: public QAbstractItemModel
{
Q_OBJECT public:
// 下面两个是构造函数
explicit MyItemModel(QObject* parent = nullptr);
explicit MyItemModel(const QList<int> &list, QObject* parent = nullptr); // 返回父级
QModelIndex parent(const QModelIndex & child) const override;
// 返回索引
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
// 返回行数和列数
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
// 获取数据
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; // 下面这两个方法用来获取或设置数据源
QList<int> intList() const;
void setIntList(const QList<int> &list); private:
QList<int> m_list; // 私有的
}; #endif
私有字段 m_list 用于引用原始数据,可以通过构造函数的 list 参数设置,也可以通过 setIntList 方法设置。
下面代码实现构造函数,主要是初始化私有成员。
MyItemModel::MyItemModel(QObject *parent)
: m_list(QList<int>()), QAbstractItemModel(parent)
{
} MyItemModel::MyItemModel(const QList<int> &list, QObject *parent)
: QAbstractItemModel(parent), m_list(list)
{
}
下面代码实现 parent 方法。由于是整数列表,数据只有一层,所以直接返回 QModelIndex() 即可。
QModelIndex MyItemModel::parent(const QModelIndex &child) const
{
// 简单的列表不需要父子层次
// 使用无参构造函数表示无效索引
return QModelIndex();
}
接着是实现返回数据列表的行数和列数。
int MyItemModel::rowCount(const QModelIndex &parent) const
{
// 一样的道理,不能有父级数据
if (parent.isValid() ){
return 0;
}
// 返回QList中元素个数,每个元素代表一行
return m_list.size();
} int MyItemModel::columnCount(const QModelIndex &parent) const
{
if(parent.isValid())
return 0;
// 咱们这个模型永远只有一列
return 1;
}
实现 index 方法,为数据项创建索引。
QModelIndex MyItemModel::index(int row, int column, const QModelIndex &parent) const
{
// 因为此列表不存在爷爷/孙子/父/子关系
// 所以如果索引是有效的,说明它不对
// 咱们这个列表是没有父级的
if(parent.isValid())
return QModelIndex(); // 有效索引不是咱们想要的,返回无效索引
// 如果索引无效,说明是顶层数据,是咱们想要的
return createIndex(row, column);
}
QModelIndex 无法直接访问其成员,要产生索引请调用 createIndex 方法。
实现 data 方法。返回数据,这里咱们实现了正常显示的文本和作为工具提示用的文本。
QVariant MyItemModel::data(const QModelIndex &index, int role) const
{
// 注意 role 这个参数,返回前必须判断
if(role == Qt::DisplayRole)
{
// DisplayRole 说明获取的数据是用在界面呈现上的
// 咱们只考虑行号,列号嘛,反正只有一列
int idx = index.row();
return m_list.at(idx);
}
// 可以提供工具提示
if(role == Qt::ToolTipRole)
{
int i = index.row();
int val = m_list.at(i);
return QString("这是整数值:%1").arg(val);
}
// 如果是其他role,就返回一个默认的QVariant给它
return QVariant();
}
其他未用到的数据角色返回空的 QVariant 就可以。data 方法返回的值,是对应着二维表中某个单元格的,所以,你希望在那个单元格中显示什么就返回什么。
最后实现的两个方法是用来获取或设置数据源的(即原始数据)。
QList<int> MyItemModel::intList() const
{
return m_list;
} void MyItemModel::setIntList(const QList<int> &list)
{
m_list = list;
}
至此,一个简单的模型就有了,当然,没有实现 setData 方法,它只能读数据,不支持编辑。现在我们可以拿来用了。
int main(int argc, char** argv)
{
QApplication app(argc, argv);
// 创建视图实例
QListView lv;
lv.setWindowTitle("简单模型");
// 准备点数据
QList<int> theList;
theList << 100 << 300 << 4500 << 600 << 1200;
// 实例化模型
MyItemModel *model;
model = new MyItemModel(theList);
//model->setIntList(theList);
// 为视图设置模型
lv.setModel(model);
// 显示窗口
lv.show(); // 主循环
return QApplication::exec();
}
QListView 作为视图组件,适合显示简单的列表。调用视图的 setModel 方法就可以关联指定的模型对象了。如上述代码中,咱们自定义的 MyItemModel 在设置了原始数据后,就可以传递给 setModel 方法以提供数据。
结果如下图所示:
把鼠标移到某个项上,还能看到工具提示呢。
咱们给 MyItemModel 加上 setData 方法的重写,使它支持编辑功能。
// 头文件
bool setData(const QModelIndex &index, const QVariant &value, int
role = Qt::EditRole) override; Qt::ItemFlags flags(const QModelIndex &index) const override;
// 实现代码
bool MyItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
// 设置数据时数据角色通常是编辑
if(role == Qt::EditRole)
{
// 因为只有一列,我们不用关心列号,只取行号
int row = index.row();
if(value.canConvert<int>() == false)
{
// 不是int值,玩不下去了
return false;
}
// 更新数据
m_list.replace(row, value.toInt());
// 发出信号
QList<int> roles = { Qt::DisplayRole, Qt::EditRole, Qt::ToolTipRole };
emit dataChanged(index, index, roles);
// 输出一下,主要是检查list有没有顺利修改
qDebug() << m_list;
return true;
}
return false;
} Qt::ItemFlags MyItemModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags oldFlags = QAbstractItemModel::flags(index);
return oldFlags | Qt::ItemIsEditable;
}
先说说为什么要同时重写 flags 方法,此方法返回 ItemFlag 枚举的值(值可以合并)。如果想让视图组件知道此模型允许编辑,那么返回的 ItemFlags 必须包含 ItemEditable 值。
现在说 setData 方法。首先,role 参数得是 EditRole 才表明用户界面已进入编辑状态,并且这个值是在编辑状态下传送过来的。canConvert 方法是检查一下传过来的是不是 int 值,这里如果是 QListView 组件默认处理的话,一般不会搞错类型。
咱们的原始数据就是存放在 QList<int> 对象中的,所以只调用 replace 方法把某个索引处的值替换下就可以了;如果数据来自文件,就得写入文件以保存。
在数据更新后,记得发送一个 dataChanged 信号,通知所有连接到此信号的对象,数据已变更,赶紧刷新提取最更的值。dataChanged 信号需要三个参数:
void dataChanged(
const QModelIndex &topLeft,
const QModelIndex &bottomRight,
const QList<int> &roles = QList<int>());
topLeft 参数和 bottomRight 参数是两个索引,它们描述了被修动数据的区域,用左上角和右下角的索引来表示。本示例中,每次只修改一个行,所以,左上角和右下角的索引都是被修改项的索引。roles 参数告诉程序:哪些角色的数要更新一下。一般 EditRole 和 DisplayRole 的要更新,这样可让应用程序知道去刷新数据。模型只用在 QListView 视图中,所以就算不发出 dataChanged 信号,组件也能自动刷新。但如果模型同时应用在多个视图中,并且有其他代码连接了 dataChanged 信号,那就得发出这个信号了。
setData 方法返回 bool 值,true 表示成功,false 表示失败。
修改后,只要双击列表项,就会出现文本框,然后你可以输入新的值,输完后按“回车”键,或者移开焦点(如点击其他空白地方),就会触发更新。
但是,你会发现一个问题:进入编辑状态时,文本框里都是空的。如下图:
这不合理,应该显示原有的值让用户修改。造成编辑状态下初始值空白的原因是咱们前面的 data 方法。因为咱们在返回值的时候,只判断了在 DisplayRole 角色下才返回,当视图进入编辑状态后,调用 data 方法获取数据时,role 参数的值是 EditRole,这就导致获取到空值。
回去修改一下 data 方法的代码。
QVariant MyItemModel::data(const QModelIndex &index, int role) const
{
// 注意 role 这个参数,返回前必须判断
if(role == Qt::DisplayRole || role == Qt::EditRole)
{
……
}
……
}
现在,双击列表项或按【F2】键进入编辑状态,文本框中的初始值就不会空白了。
好了,关于怎么继承列表模型的公共基类的话题,咱们就扯到这儿了。
【Qt6】列表模型——抽象基类的更多相关文章
- Djiango-建立模型抽象基类
创建一个抽象模型基类 ‘ 然后 ’base_model.py from django.db import models from datetime import date class BaseMode ...
- PythonI/O进阶学习笔记_3.2面向对象编程_python的继承(多继承/super/MRO/抽象基类/mixin模式)
前言: 本篇相关内容分为3篇多态.继承.封装,这篇为第二篇 继承. 本篇内容围绕 python基础教程这段: 在面向对象编程中,术语对象大致意味着一系列数据(属性)以及一套访问和操作这些数据的方法.使 ...
- 流畅python学习笔记:第十一章:抽象基类
__getitem__实现可迭代对象.要将一个对象变成一个可迭代的对象,通常都要实现__iter__.但是如果没有__iter__的话,实现了__getitem__也可以实现迭代.我们还是用第一章扑克 ...
- 【Python】【元编程】【从协议到抽象基类】
"""class Vector2d: typecode = 'd' def __init__(self,x,y): self.__x = float(x) self.__ ...
- python(七):元类与抽象基类
一.实例创建 在创建实例时,调用__new__方法和__init__方法,这两个方法在没有定义时,是自动调用了object来实现的.python3默认创建的类是继承了object. class A(o ...
- OOP2(虚函数/抽象基类/访问控制与继承)
通常情况下,如果我们不适用某个函数,则无需为该函数提供定义.但我们必须为每个虚函数都提供定义而不管它是否被用到了,这因为连编译器也无法确定到底会适用哪个虚函数 对虚函数的调用可能在运行时才被解析: 当 ...
- python cookbook第三版学习笔记十六:抽象基类
假设一个工程中有多个类,每个类都通过__init__来初始化参数.但是可能有很多高度重复且样式相同的__init__.为了减少代码.我们可以将初始化数据结构的步骤归纳到一个单独的__init__函数中 ...
- 流畅的python学习笔记:第十一章:抽象基类
__getitem__实现可迭代对象.要将一个对象变成一个可迭代的对象,通常都要实现__iter__.但是如果没有__iter__的话,实现了__getitem__也可以实现迭代.我们还是用第一章扑克 ...
- Fluent_Python_Part4面向对象,11-iface-abc,协议(接口),抽象基类
第四部分第11章,接口:从协议到抽象基类(重点讲抽象基类) 接口就是实现特定角色的方法集合. 严格来说,协议是非正式的接口(只由文档约束),正式接口会施加限制(抽象基类对接口一致性的强制). 在Pyt ...
- Python中的抽象基类
1.说在前头 "抽象基类"这个词可能听着比较"深奥",其实"基类"就是"父类","抽象"就是&quo ...
随机推荐
- C# EFCore 根据Oracle/SqlServer数据库表生成实体类和DbContext
官方文档: https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/scaffolding?tabs=dotnet-core-cli 本文以 ...
- 瞄准程序员招聘痛点,ShowMeBug让面试代码操作可“回放”
程序员虽然是建设互联网的职业之一,但他们的招聘工作的线上化却有不少难题. 疫情加速了市场对远程办公.远程面试.远程教学等模式的接受程度,但程序员招聘涉及到代码能力测试,甚至不同企业有不同的产品代码基础 ...
- NixOS 与 Nix Flakes 新手入门
独立博客阅读: https://thiscute.world/posts/nixos-and-flake-basics/ 长文警告️ 本文的目标 NixOS 版本为 22.11,Nix 版本为 2.1 ...
- vue2中v-if 或者 v-show 使用数组中的值判断不生效
知识点来源:博客园==> 外号蓝大胖// 对象this.$set(obj, key, value)/vue.set(obj, key, value)// 数组this.$set(arr, ind ...
- 快速把PDF文档里的表格粘贴到excel的方法
1 打开需要复制的PDf文件,点一下页面上方的"选择文本"按钮(如下图中手图标左边的箭头),以便选中文本 2 ctrl c 需要复制的表格,到excel中ctrl v.这时候所有类 ...
- 助力长城汽车数据管道平台连接“数据孤岛”,加强数据一元化,Apache DolphinScheduler 的角色定位
讲师简介 长城汽车-IDC-数据中台部-刘永飞 高级工程师 我是长城汽车 IDC-数据中台部的刘永飞,给大家分享一下我们自研的一个数据同步工具平台,以及在使用这个工具过程中遇到的问题.今天的分享主要有 ...
- Docusaurus之markdown文档的vscode代码片段
需求 我是使用Docusaurus建立的个人站点,在写文档是总是要在开头配置作者.日期等等,用过Docusaurus的都应该知道. 因为每次新建一个md文档都需要重新配置,很麻烦,于是我就想能不能新建 ...
- C++面试八股文:static_cast了解一下?
某日二师兄参加XXX科技公司的C++工程师开发岗位第20面: 面试官:C++中支持哪些类型转换? 二师兄:C++支持C风格的类型转换,并在C++11引入新的关键字规范了类型转换. 二师兄:C++11引 ...
- 如何制作 GitHub 个人主页
人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站.然而,如果你使用GitHub来分享你的代码并参与开源项目,那么你的GitHub个人主 ...
- 聊聊Asp.net Core中如何做服务的熔断与降级
概念解析 啥是熔断 而对于微服务来说,熔断就是我们常说的"保险丝",意为当服务出现某些状况时,切断服务,从而防止应用程序不断地尝试执行可能会失败的操作造成系统的"雪崩&q ...