QT模型视图MVC系列教程(3)---自定义模型,以QAbstractTableModel为例

MVC中的M就是模型Model,QT中所有的模型都继承自:QAbstractItemModel

查阅手册可知,它的子类有3个:

分别是列表模型、代理模型、表格模型。

要实现自定义模型,可以继承QAbstractItemModel以及任何一个后代类(含子类、孙子类。。。等),按照QT的设计惯例,名字里带抽象字样Abstract的类,都不能直接使用,必须继承并自行实现某些特定函数;对于QT自带的不带Abstract字样的Model类,可以不用继承重写,直接new出来使用即可,这种情形较为简单。所以本文只演示继承重写Abstract的情形,只要掌握了这种情形,那么不带Abstract的情形就更不在话下了。

以QAbstractTableModel为例,想使用它,就必须先看手册:

以上就是其标准用法了,这里再简单描述一下:QAbstractTableModel提供了二维数据模型的标准接口,可用于列表视图(不推荐)或者表格视图(推荐),虽然表格模型与列表视图是可以绑定的,但更好的方式继承列表模型QAbstractListModel。

子类化基本模型QAbstractItemModel时,我们继承后必须至少实现5个函数(见上述英文文档):index(), parent(), rowCount(), columnCount() data()。然而,子类化抽象表格模型QAbstractTableModel时,至少只需实现3个即可:rowCount(), columnCount(), and data(),因为抽象表格模型QAbstractTableModel,已经帮我们实现了两个函数index(), parent(),显然这比直接继承QAbstractItemModel来自定义模型要轻松一丁点。一般来说,最好把表头数据也重写一下:headerData()。

以上工作可以满足只读模型的最低需求了,如果想要该模型支持读写,那么还必须实现一下setData()和flags()这两个。

下面我们来实现一下rowCount()、columnCount()、data() 、headerData()、 setData()、flags()这6个函数,以实现完整支持读写的自定义模型。

这几个函数应该怎么写:

  •  
    [pure virtual] int QAbstractItemModel::rowCount(const QModelIndex &parent = QModelIndex()) const
    功能:返回指定索引下的行数,也即返回该索引有几个儿子。索引的行数这个概念,对于列表、表格、树,定义是不同的,可参考我的本系列教程另一篇博文《QModelIndex详解》
    参数:parent为指定的索引

  • [pure virtual] int QAbstractItemModel::columnCount(const QModelIndex &parent = QModelIndex()) const
    功能: 返回指定索引下的列数
  • data(index,role)
    功能:读取指定index处的role类型的数据。一般View在渲染时,会自动调用该函数来获取数据。
    说明:数据类型要根据role做转换,例如role=Qt::DisplayRole时,应当返回QString(返回时QT还会自动再将QString再次转换为QVariant)。其余role应当返回什么类型,请参考我的本系列教程另一篇博文《角色role的使用》
  • headerData(n,orient,role) 返回表头第n个名字,到底要返回行表头还是列表头,取决于orient参数
  • bool QAbstractItemModel::setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole)
    功能:将index处的内容修改保存下来,并返回是否能修改成功。一般视图编辑完成后后自动调用该函数,以实现数据保存。
    说明:视图送进来的数据,是用QVariant包装过的,我们需要把这个QVariant转换为Model可以保存的实际数据类型。
  • flags(index)返回index处的数据特性:是否使能、是否允许编辑、是否允许选中等等

这里再次特意强调一点,Model只是用来管理数据Data的映射,真正的Data不一定非得处于Model内部(成员变量),Data可以是txt文件、数据库文件、操作系统内核数据(如系统文件目录)。不过为了便于展示自定义model的原理,我就把Data放在Model内部了,以成员变量的形式来存储Data。

为方便读者理解以下代码,这里先描述一下该代码的功能:MyTableModel中提供了一个表格数据模型,每一行是一个人的信息:名字、出生日期,共3条记录,也即这是一个3行2列的数据模型。

而且,通过该例读者可以发现,表格模型,并不一定要求原始数据真的是二维表,只要Model能够把原始数据映射成二维表即可,在本例中,每一列数据都是一个QStringList,根本不是二维结构,但本类通过重写data(index,role)函数,把这些不是二维结构的数据,映射成了二维,详情可阅读以下代码。

#include <QObject>
#include <QAbstractTableModel>
#include <QStringList>

class MyTableModel : public QAbstractTableModel
{
public:
    MyTableModel(QObject *parent = 0);
    int rowCount(const QModelIndex &parent = QModelIndex()) const;
    int columnCount(const QModelIndex &parent = QModelIndex()) const;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;//读取index处的数据
    QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
    Qt::ItemFlags flags(const QModelIndex &index) const;
private:
    QStringList name;//第1列的内容
    QStringList birth;//第2列的内容
    QStringList headName;

};
#include "myTableModel.h"
#include <QDebug>


MyTableModel::MyTableModel(QObject *parent)
    : QAbstractTableModel(parent)
{
    name << "zhangSan" << "liSi" << "wangWu";
    birth << "1970.01.01" << "1980.02.02" << "1990.03.03";
    headName << "name" << "birth";
}

int MyTableModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return name.size();
}

int MyTableModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return 2;
}

QVariant MyTableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if(orientation != Qt::Horizontal)
        return QVariant("only support horizontal");

    if(role != Qt::DisplayRole)
        return QVariant();

    if(section >= headName.size())
        return QVariant("NoName");

    return headName.at(section);
}

bool MyTableModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    Q_UNUSED(role)
    int col = index.column();
    int row = index.row();
    if(0 == col)
    {
        name[row] = value.toString();
    }

    if(1 == col)
    {
        birth[row] = value.toString();
    }
    qDebug() << name << birth;
    //数据已写完,下面通知视图类对象来更新界面
    const QVector<int> roles;//哪些角色被更新
    roles << role;
    emit dataChanged(index, index, roles);//通知view哪些范围内的哪些角色数据被更新了
    return true;

}

Qt::ItemFlags MyTableModel::flags(const QModelIndex &index) const
{
    if(index.row() > 3 || index.column() > 2)
        return Qt::NoItemFlags;

    if(0 == index.column())
    {
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
    }
    if(1 == index.column())
    {
        return Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable;
    }

    return Qt::NoItemFlags;
}

QVariant MyTableModel::data(const QModelIndex &index, int role) const
{
    if(!index.isValid())
        return QVariant();
    
    //以下两种代码的区别,参见代码后面的动图
    //代码1:只返回展示数据。这时用户双击单元格时,单元格会变空,交互体验很差。
    //if(role != Qt::DisplayRole)
    //代码2:展示数据和编辑数据均返回Model的当前值。这时用户双击单元格,会显示model的当前值
    if(role != Qt::DisplayRole && role != Qt::EditRole)
        return QVariant();

    int row = index.row();
    int col = index.column();
    if(row >= name.size() || col >= 3)
        return QVariant("indexError");

    if(0 == col)
    {
        return name.at(row);
    }

    if(1 == col)
    {
        return birth.at(row);
    }    

    return QVariant("indexError");
}
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    MyTableModel *model = new MyTableModel(this);
    ui->tableView->setModel(model);
}

运行效果如下:而且由于我们在上述代码的flags(index)函数中没有给第1列设置“可选中”标志,所以用鼠标点击第1列是无法选中的(不会变蓝色),而点击第2列是可以选中并进入编辑状态的。

下图1是没有给EditRole返回空值,一旦双击进入编辑态,单元格就被清空了,图2是给EditRole返回当前Model的值。
     

下面再继续做一些测试,

因为在本例中我们的有效的真实数据的列数只有2列,如果我们把上述代码的列数函数columnCount()强制返回3,让行数强制返回5,会有什么效果?

先看之前的代码,视图打算显示第3列数据时,会通过前文的data(index)函数向模型索要数据,在这个函数中,我写的是:所请求的行或列索引超出了合法范围,则返回: QVariant("indexError");

对于列表头数据,我写的是:如果index不合法则返回: QVariant("NoName")。

对于数据的flag,我写的是:如果index不合法则返回: 无效标志。

看看运行效果:

int MyTableModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return 5;
    //return name.size();//正常
}

int MyTableModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return 3;
    //return 2;//正常
}

运行效果如下,与前文的分析完全一致。

  • 13
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这里是一个简单的示例代码,实现了Qt5中的MVC模型视图代理,并展示了包含ico图片的第7列,同时初始化了第3、8、9列的数据为空。当收到当前页的某行数据时,该行变为蓝色。 ```cpp // Model class MyModel : public QAbstractTableModel { public: MyModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {} int rowCount(const QModelIndex &parent = QModelIndex()) const override { return m_data.size(); } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 9; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if (!index.isValid() || role != Qt::DisplayRole && role != Qt::DecorationRole) return QVariant(); const auto &rowData = m_data[index.row()]; if (index.column() == 6) { if (role == Qt::DecorationRole) return rowData.icon; } else { return rowData.values[index.column()]; } return QVariant(); } QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override { if (orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant(); switch (section) { case 0: return "#"; case 1: return "Name"; case 2: return "Age"; case 3: return "Address"; case 4: return "Phone"; case 5: return "Email"; case 6: return "Icon"; case 7: return "Note1"; case 8: return "Note2"; default: return QVariant(); } } void setPageData(const QVector<RowData> &newData) { m_data = newData; } void setRowColor(const QModelIndex &index, const QColor &color) { Q_EMIT dataChanged(index, index, {Qt::BackgroundRole}); m_rowColors[index.row()] = color; } QColor getRowColor(int row) const { if (m_rowColors.contains(row)) return m_rowColors[row]; return QColor(); } private: QVector<RowData> m_data; QMap<int, QColor> m_rowColors; }; // Delegate class MyDelegate : public QStyledItemDelegate { public: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() == 6) { const auto &icon = index.data(Qt::DecorationRole).value<QIcon>(); if (!icon.isNull()) { const auto rect = option.rect; const auto iconSize = icon.actualSize(rect.size()); const auto x = rect.center().x() - iconSize.width() / 2; const auto y = rect.center().y() - iconSize.height() / 2; painter->drawPixmap(x, y, icon.pixmap(iconSize)); return; } } QStyledItemDelegate::paint(painter, option, index); } QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() == 6) { const auto &icon = index.data(Qt::DecorationRole).value<QIcon>(); if (!icon.isNull()) { return icon.actualSize(option.rect.size()); } } return QStyledItemDelegate::sizeHint(option, index); } }; // View class MyTableView : public QTableView { public: MyTableView(QWidget *parent = nullptr) : QTableView(parent) { const auto model = new MyModel(this); setModel(model); const auto delegate = new MyDelegate(this); setItemDelegate(delegate); setShowGrid(false); setAlternatingRowColors(true); setEditTriggers(QAbstractItemView::NoEditTriggers); // 初始化数据 QVector<RowData> data; for (int i = 0; i < 100; ++i) { auto rowData = RowData{i + 1, QString("name%1").arg(i+1), i+20, "address", "phone", "email"}; if (i % 2 == 0) rowData.icon = QIcon(":/icons/person.png"); data.append(rowData); } model->setPageData(data); resizeColumnsToContents(); setColumnWidth(6, 50); // 当收到数据时,设置第3、8、9列的值,并且设置当前行颜色为蓝色 connect(this, &MyTableView::clicked, [=](const QModelIndex &index) { if (!index.isValid()) return; auto model = qobject_cast<MyModel *>(this->model()); auto data = model->getPageData(); if (index.row() >= data.size()) return; data[index.row()].values[2] = 30; data[index.row()].values[7] = "note1"; data[index.row()].values[8] = "note2"; model->setPageData(data); model->setRowColor(index, Qt::blue); }); } }; // Usage int main(int argc, char *argv[]) { QApplication app(argc, argv); MyTableView view; view.show(); return app.exec(); } ``` 注意:这里的代码只是一个示例,实际使用时需要根据自己的需求进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值