《QTreeView+QAbstractItemModel自定义模型》:系列教程之三

  • Post author:
  • Post category:其他


本文属于

《QTreeView使用系列教程》

之一,欢迎查看其它文章。



1、了解常用的model类



通过对上一节的阅读,我们知道只要具备model+view就可以显示数据。



那么有哪些model类呢,从下图中我们可以看到






Qt


中模型类的层次结构




QStandardItemModel


:可以作为QListView、QTableView、QTreeView的标准model。




QAbstractListModel


:需要使用QListView显示数据,并配合自定义model时,我们从此类继承。




QAbstractTableModel


:需要使用QTableView显示数据时,并配合自定义model时,我们从此类继承。




QAbstractItemModel


:需要使用QTreeView显示数据时,并配合自定义model时,我们从此类继承。



此处我们只关注可以用作QTreeView之model的类QAbstractItemModel与QStandardItemModel。




2、QStandardItemModel的使用



首先我们来看看如果用QStandardItemModel作为model时,我们的代码:

QTreeView* view = new QTreeView();
QStandardItemModel* model = new QStandardItemModel();
for (int row = 0; row < 4; ++row) {
     QStandardItem *item = new QStandardItem(QString("%1").arg(row) );
     model->appendRow( item );
} 
view->setModel(model);



用法比较简单,QStandardItemModel可以使用QStandardItem,通过不断添加子节点,从而构建出list、table、tree结构的数据。



使用QStandardItemModel表示数据集具有以下优点:



  • 实现代码简单


  • 该类使用QStandardItem存放数据项,用户不必定义任何数据结构来存放数据项;


  • QStandardItem使用自关联关系,能够表达列表、表格、树甚至更复杂的数据结构,能够涵盖各种各样的数据集;


  • QStandardItem本身存放着多个『角色,数据子项』,视图类、委托类或者其他用户定义的类能够方便地依据角色访问各个数据子项。



缺点:



  • 当数据集中的数据项很多时,施加在数据集上的某些操作的执行效率会很低。


  • 数据太大时,占用内存巨大,性能低下




性能比较,可参考此文末尾的demo代码:


https://blog.csdn.net/dpsying/article/details/80456263




3、QAbstractItemModel自定义model



(1)原理知识铺垫



  • 我们以实现如下树形显示为例,进行自定义model。





我们要将数据显示到QTreeView中,按照Model/View框架介绍,需要定义2个类TreeModel和TreeItem,TreeModel继承于QAbstractItemModel,用于向View提供数据;



TreeItem用于定义我们的

数据节点

,然后被model获取数据。



QTreeView与TreeItem交互过程大致如下:





  • 注意:在树中,我们一般默认认为,只有column为0的单元格才能添加下级单元格,也就是说树中的每一行单元格只能与Column为0的单元格建立父子关系。

所以我们可以简单的认为树,就是一行一行单元格组成的表格,只不过在每一行通过其首个单元格,建立了父子关系。




此处我们的一个

TreeItem

代表一行若干单元格,我们需要将多个

TreeItem

建立父子关系,就能够正确表示出树显示所需的数据结构。





  • 而TreeItem的数据是从其他地方获取来的,所以我们先定义树中显示的原始数据结构,如下:

// person信息
typedef struct Person_t{
    QString name;   // 姓名
    QString sex;    // 性别
    int age;     // 年龄
    QString phone;  // 电话号码
    Person_t()
    {
        age = 0;
    }
} Person;

// 省份信息
typedef struct Province_t{
    QString name;
    QVector<Person*> people;
} Province;





(2)定义TreeItem类



  • 提供建立树形结构的功能



通过addChild可以添加TreeItem子节点,并保存该子节点在父节点的序号。

void TreeItem::addChild(TreeItem *item)
{
    item->setRow(_children.size());
    _children.append(item);
}



另外提供释放子节点内存,用于删除根节点时,自动逐级释放所有TreeItem内存。

void TreeItem::removeChildren()
{
    qDeleteAll(_children);
    _children.clear();
}



返回父节点

TreeItem *parent() { return _parent; }



返回子节点数量

int childCount() const { return _children.count(); }




  • 提供获取列数据,以及获取TreeItem子节点功能



既然TreeItem代表的是一行数据,那么必定需要提供获取某列数据函数。


QVariant data(int column) const;



也必定需要提供获取TreeItem下某子节点函数(某一行)。


TreeItem *child(int row) { return _children.value(row); }




  • 关键:提供设置数据源地址功能



保存数据源地址,以便TreeItem可以访问原始数据;通常情况下,原始数据与TreeItem一一对应。

void setPtr(void* p) { _ptr = p; }
void* ptr() const { return _ptr; }



由于建立TreeItem对象树时,

Province


和Person地址会被setPtr()保存到TreeItem上,所以为了便于按类型取数据,在

setPtr()时需要setType()保存数据属于哪种类型。

enum Type
{
    UNKNOWN = -1,
    PROVINCE,
    PERSON
};
Type getType() const { return _type; }
void setType(const Type &value) { _type = value; }



到此,我们可以建立TreeItem树,并获取任意行、列的数据。已经满足了TreeModel获取任意数据的要求。



下一步,我们来定义TreeModel类。




(3)定义TreeModel类



我们需要继承自QAbstractItemModel,让我们来看看它有哪些接口。



QAbstractItemModel类中定义如下:

Q_INVOKABLE virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const = 0;
Q_INVOKABLE virtual QModelIndex parent(const QModelIndex &child) const = 0;
Q_INVOKABLE virtual QModelIndex sibling(int row, int column, const QModelIndex &idx) const;
Q_INVOKABLE virtual int rowCount(const QModelIndex &parent = QModelIndex()) const = 0;
Q_INVOKABLE virtual int columnCount(const QModelIndex &parent = QModelIndex()) const = 0;
Q_INVOKABLE virtual bool hasChildren(const QModelIndex &parent = QModelIndex()) const;
Q_INVOKABLE virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0;
Q_INVOKABLE virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
Q_INVOKABLE virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;



其中共5个纯虚函数,index()、parent()、rowCount()、columnCount()和data(),这是我们必须要实现的;另外一般我们还是需要显示表头的,所以还需要实现headerData()。QTreeView显示树时,会自动调用TreeModel,来获取显示一个树所需要的一些信息;我们重写这些函数的目的就是为了向QTreeView提供这些信息的。



接下来我们解释下重写各个函数的作用。





  • QVariant



    headerData


    (

    int


    section

    ,

    Qt

    ::

    Orientation


    orientation

    ,

    int


    role

    )

    const


    override

    ;



表示获取表头数据,第section列;orientation方向,一般为水平方向;DisplayRole角色的表头数据,DisplayRole表示是用于界面显示的数据。

QStringList headers;
headers << QString("名称/姓名")
        << QString("性别")
        << QString("年龄")
        << QString("电话");
QVariant TreeModel::headerData(int section, Qt::Orientation orientation,int role) const
{
    if (orientation == Qt::Horizontal)
    {
        if(role == Qt::DisplayRole)
        {
            return _headers.at(section);
        }
    }
    return QVariant();
}





  • int



    rowCount


    (

    const


    QModelIndex

    &

    parent

    )

    const


    override

    ;



获取索引parent下有多少行。View会遍历每个单元格索引,若不是第一列单元格索引,则不会有子节点,所以直接返回行数为0;



若是第一列单元格索引,那么该单元格是否为空(空表示根节点),则需要返回根节点下行数,反之则返回parent下行数。

int TreeModel::rowCount(const QModelIndex &parent) const
{
    if (parent.column() > 0)
        return 0;

    TreeItem* item = itemFromIndex(parent);
    return item->childCount();
}





  • int



    columnCount


    (

    const


    QModelIndex

    &

    parent

    )

    const


    override

    ;



返回索引parent下有多少列

int TreeModel::columnCount(const QModelIndex &parent) const
{
    return _headers.size();
}





  • QModelIndex



    index


    (

    int


    row

    ,

    int


    column

    ,

    const


    QModelIndex

    &

    parent

    )

    const


    override

    ;



在parent节点下,第row行,第column列位置上创建索引;将TreeItem指针保存至该索引。

QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{
    if (!hasIndex(row, column, parent))
        return QModelIndex();

    TreeItem *parentItem = itemFromIndex(parent);
    TreeItem *item = parentItem->child(row);
    if (item)
        return createIndex(row, column, item);
    else
        return QModelIndex();
}





  • QVariant



    data


    (

    const


    QModelIndex

    &

    index

    ,

    int


    role

    )

    const


    override

    ;



获取index.row行,index.column列数据;通过itemFromIndex()获取保存在索引index中的TreeItem指针。

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

    TreeItem *item = itemFromIndex(index);
    if (role == Qt::DisplayRole)
    {
        return item->data(index.column());
    }
    return QVariant();
}





  • QModelIndex



    parent


    (

    const


    QModelIndex

    &

    index

    )

    const


    override

    ;



创建index的父索引,若父节点为根节点,则返回QModelIndex(),默认根节点索引为空。

QModelIndex TreeModel::parent(const QModelIndex &index) const
{
    if (!index.isValid())
        return QModelIndex();

    TreeItem *item = itemFromIndex(index);
    TreeItem *parentItem = item->parent();

    if (parentItem == _rootItem)
        return QModelIndex();
    return createIndex(parentItem->row(), 0, parentItem);
}






TreeModel类一般不需要怎么修改,都大同小异,实际使用时,根据需要微调就可以。

4、测试TreeModel



初始化原始数据:

QVector<Province*> MainWindow::initData()
{
    // 初始化数据,5个省,每个省5人
    QVector<Province*> proList;
    int provinceCount = 5;
    int personCount = 5;
    for(int i = 0; i < provinceCount; i++)
    {
        Province* pro = new Province();
        pro->name = QString("Province%1").arg(i);
        for(int j = 0; j < personCount; j++)
        {
            Person* per = new Person();
            per->name = QString("name%1").arg(j);
            per->sex = "man";
            per->age = 25;
            per->phone = "123456789";
            pro->people.append(per);
        }
        proList.append(pro);
    }
    return proList;
}




构建TreeItem对象树,设置model:

void MainWindow::setModel(const QVector<Province *> &proList)
{
    QStringList headers;
    headers << QString("名称/姓名")
            << QString("性别")
            << QString("年龄")
            << QString("电话");

    TreeModel* model = new TreeModel(headers, treeView);
    TreeItem* root = model->root();
    foreach (auto pro, proList)
    {
        TreeItem* province = new TreeItem(root);
        province->setPtr(pro); // 保存数据指针
        province->setType(TreeItem::PROVINCE); // 设置节点类型为PROVINCE
        root->addChild(province);

        foreach (auto per, pro->people)
        {
            TreeItem* person = new TreeItem(province);
            person->setPtr(per);    // 保存数据指针
            person->setType(TreeItem::PERSON);  // 设置节点类型为PERSON
            province->addChild(person);
        }
    }

    treeView->setModel(model);
}



再贴一遍运行结果:






5、QStandardItemModel与自定义model如何选择



在一个项目中开了很多线程,此时QTreeView+QStandardItemModel更新任务信息,在更新QTreeView中一行共7列数据,也就是7个单元格数据,居然花了40ms。。。



似乎QStandardItemModel效率欠佳,当然也可能是系统压力较大的原因。



自己大概整理了下这2种model在不同情况下的使用建议:

model选择

QStandardItemModel

自定义model

开发难度

简单

稍高

显示大量数据

不建议

建议

显示固定少量数据

建议

不建议

需要更新数据

不建议

建议




对于数据量小且不需要更新的场景,我们使用QStandardItemModel来实现比较简单,没有自定义model那么多代码逻辑。



在数据量小,但是需要更新情况下,我们采用自定义model来实现,即使数据量小,更新数据其实也是比较慢的,它会占用较多UI线程时间,如果其他线程业务繁重,就会影响UI线程性能,导致界面卡顿。



在数据量大情况下,无论更新与否,我们都采用自定义model来实现。


若对你有帮助,欢迎点赞、收藏、评论,你的支持就是我的最大动力!!!

同时,阿超为大家准备了丰富的学习资料,欢迎关注公众号“

超哥学编程

”,即可领取。

本文涉及工程代码,公众号回复:

34CustomModel

,即可下载


在这里插入图片描述



版权声明:本文为u011832525原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。