1

【Qt6】列表模型——便捷类型 - 东邪独孤

 8 months ago
source link: https://www.cnblogs.com/tcjiaan/p/17707380.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【Qt6】列表模型——便捷类型

前一篇水文中,老周演示了 QAbstractItemModel 抽象类的继承方法。其实,在 Qt 的库里面,QAbstractItemModel 类也派生了两个基类,能让开发者继承起来【稍稍】轻松一些。

这两个类是 QAbstractListModel 和 QAbstractTableModel。

  • QAbstractListModel类专门用来实现一维列表结构模型的。它实现了 index、parent 等方法,并且把 columnCount 方法变成了私有成员(一维列表不需要它)。继承时直接实现 rowCount、data、setData 这几个方法即可;
  • QAbstractTableModel类专门用来实现二维表结构的模型。它实现了 index、parent 等方法。继承时咱们要实现 rowCount、columnCount、data、setData 等方法。

虽然它帮咱们实现了一些成员,但实际上也省不了多功夫的。下面咱们用 QAbstractListModel 举例,和上篇中的一样,操作一个 QList<int> 数据。毕竟一维的比较简单,演示出来大伙都容易懂。

// 头文件
#ifndef LIST_H
#define LIST_H

#include <QAbstractListModel>
#include <QList>

class CustListModel: public QAbstractListModel
{
    Q_OBJECT

public:
    explicit CustListModel(QObject* parent = nullptr);
    ~CustListModel();

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);

    bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
    bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;

    Qt::ItemFlags flags(const QModelIndex &index) const override;
private:
    // 私有,存数据用
    QList<int> m_list;
};

#endif

先弄 rowCount 方法,返回总行数,这个简单一点。

int CustListModel::rowCount(const QModelIndex &parent) const
{
    if(parent.isValid())
    {
        return 0;
    }
    return m_list.size();
}

实现 data 方法,获取数据时用。

QVariant CustListModel::data(const QModelIndex &index, int role) const
{
    if( role == Qt::DisplayRole || role == Qt::EditRole)
    {
        if(!index.isValid())
        {
            return QVariant();
        }
        // 获取索引
        int i = index.row();
        // 返回指定索引处的值
        return m_list.at(i);
    }
    return QVariant();
}

要返回 QList<T> 中指定的元素,用 at 方法,传递索引给它即可。

实现 setData 方法,编辑结束后用于更新数据的。

bool CustListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if(!index.isValid())
        return false;
    if(role == Qt::EditRole || role == Qt::DisplayRole)
    {
        // 解包数据
        bool ok;
        int val = value.toInt(&ok);
        if(ok)
        {
            // 设置值
            m_list.replace(index.row(), val);

              // 发出信号
              emit dataChanged(index,index,{role});

return true;
        }
    }
    return false;
}

要修改某个索引处的值,用 replace 方法替换。

实现 flags 方法,表明该模型的列表项支持交互、编辑、被选择。

Qt::ItemFlags CustListModel::flags(const QModelIndex &index) const
{
    return Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable;
}

ItemIsEnabled 表明列表项是活动的,用户可操作的;ItemIsEditable 表明列表项可编辑;ItemIsSelectable 表示列表项可以选择。

下面这两个方法是有点麻烦的,这里先介绍一下。

bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex());

row 参数表示要插入元素的索引,count 表示插入元素个数,插入元素后,原来的元素向后移动。例如:A、B、C、D,现在我要在B处插入两个元素。那么 row = 1,count = 2,B 处放入 E、F,B、C、D 向后移,变成 A、E、F、B、C、D。

然后,删除元素的方法也类似。

bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());

row 是要删除的索引,count 是连续要删掉的元素个数。

insertRows 和 removeRows 方法的返回值含义相同:成功返回 true,失败返回 false。

好,现在上代码。

bool CustListModel::insertRows(int row, int count, const QModelIndex &parent)
{
    if(parent.isValid())
        return false;
    // 注意这里!!
    beginInsertRows(parent, row, row + count - 1);
    // 开始插入
    m_list.insert(row, count, 0);
    // 注意这里!!
    endInsertRows();
    return true;
}

bool CustListModel::removeRows(int row, int count, const QModelIndex &parent)
{
    if(parent.isValid())
        return false;
    // 注意这里!!!
    beginRemoveRows(parent, row, row + count - 1);
    // 删除
    m_list.remove(row, count);
    // 注意这里!!!
    endRemoveRows();
    return true;
}

在插入数据前必须调用 beginInsertRows 方法。注意这个方法的参数含义和 insertRows 有些不一样。

void beginInsertRows(const QModelIndex &parent, int first, int last)

insertRows 方法是指定开始索引 ,然后连续加入多少个元素,而 beginInsertRows 方法是你插入元素后,它们在列表中的开始索引和结束索引。如 A、B、C,在B处插入两个元素。开始索引是 1,结束索引是 2,即结束索引的计算方法是: endIndex = startIndex + count -1。插入元素结束后必须调用 endInsertRows 方法。这一对方法的作用是让用户界面上的视图(如 QListView、QTableView 等)能及时做出响应。

同理,在删除元素时,beginRemoveRows 和 endRemoveRows 方法也必须调用。beginRemoveRows 方法的参数与 beginInsertRows 方法一样,也是索引的起止值。即要删除元素的起始索引,和被删除的最后一个元素的索引。如1、2、3、4,要删除2、3、4,那么起始索引是 1,结束索引 3。

模型已完工,下面做个界面试一试。

int main(int argc, char** argv)
{
    QApplication myapp(argc, argv);
    // 窗口
    QWidget *window = new QWidget;
    // 布局
    QGridLayout *layout = new QGridLayout;
    window->setLayout(layout);
    // 列表视图
    QListView* view = new QListView(window);
    // 实例化模型
    CustListModel* model = new CustListModel(window);
    // 设置到视图中
    view->setModel(model);
    layout->addWidget(view, 0, 0, 3, 1);

    // 按钮
    QPushButton* btnAdd=new QPushButton("新增", window);
    QPushButton* btnDel = new QPushButton("移除", window);
    QPushButton* btnShowAll = new QPushButton("显示列表", window);
    layout->addWidget(btnAdd, 0, 1);
    layout->addWidget(btnDel, 1, 1);
    layout->addWidget(btnShowAll, 2, 1);
    layout->setColumnStretch(0, 1);
    // 连接clicked信号
    QObject::connect(
        btnAdd,
        &QPushButton::clicked,
        [&view, &model, &window]()
        {
            bool res;
            int val = QInputDialog::getInt(
                window,             //父窗口
                "输入",              //窗口标题
                "请输入整数:",       //提示文本
                0,                   //默认值
                0,                  //最小值
                1000,               //最大值
                1,                  //步长值
                &res                 //表示操作结果
            );
            if(!res)
                return;
            // 索引为count
            int i = model->rowCount();
            // 添加一项
            model->insertRow(i);
            // 获取新索引
            QModelIndex newIndex = model->index(i);
            // 设置此项的值
            model->setData(newIndex, QVariant(val), Qt::DisplayRole);
        }
    );
    QObject::connect(
        btnDel,
        &QPushButton::clicked,
        [&window, &view, &model]()
        {
            // 当前项
            QModelIndex curri = view->currentIndex();
            if(!curri.isValid())
                return;
            // 删除
            model->removeRow(curri.row());
        }
    );
    QObject::connect(
        btnShowAll,
        &QPushButton::clicked,
        [&model, &window]()
        {
            // 获取总数
            int c = model->rowCount();
            QString msg;
            for(int x = 0; x < c; x++)
            {
                // 获取值
                QVariant value = model->data(model->index(x), Qt::DisplayRole);
                if(value.isValid())
                {
                    int n = qvariant_cast<int>(value);
                    msg += QString(" %1").arg(n);
                }
            }
            QMessageBox::information(window,"提示",msg);
        }
    );

    // 标题
    window->setWindowTitle("小型列表");
    // 显示
    window->show();

    return QApplication::exec();
}

QListView 组件用来显示模型中的数据。三个按钮分别用来添加、删除和显示列表项。最后一个按钮显示模型中的所有数据,它的作用是验证模型是否工作正常。

“新增”按钮被点击后,先通过 QInputDialog.getInt 静态方法,弹出一个输入内容的对话框,然后把输入的值添加到模型中。模型中添加元素用的就是 insertRow 方法——只插入一个元素,它调用了咱们上面实现的 insertRows 方法,只是把 count 参数设置为 1。这里我实现的是新元素总是追加在列表最后,即新元素的行号等于 rowCount。

删除元素时,QListView 组件的 currentIndex 方法返回当前选中项的索引。在单选模式下,99.9% 被选中的项是等同于当前项的。多选模式下就不好说了,所以,如果需要,可以访问 selectionModel 方法,再通过 selectedIndexes 方法获得被选项的列表。

看看效果。

367389-20230917000113583-1708964766.gif

QAbstractTableModel 类的用法也是差不多的,只不过要实现行和列的读写。

这两个类哪怕帮咱们实现了一些代码,但用起来还是麻烦,要是有不需要继承、开箱就用的类就会好多了。Qt 还真的提供了这样的类型。

首先说的是 QStringListModel,这个就是针对 QStringList 类的——字符串列表。QStringList 其实就是 QList<QString>。这个 QStringListModel 模型就是以字符串列表为数据源的。不需要继承(除非你需要自定义),直接可用。

咱们演练一下。

// 头文件
#ifndef DEMO_H
#define DEMO_H

#include <QWidget>
#include <QPushButton>
#include <QListView>
#include <QVBoxLayout>

class MyWindow : public QWidget
{
    Q_OBJECT

public:
    explicit MyWindow(QWidget* parent = nullptr);
    ~MyWindow();

private:
    QVBoxLayout* m_layout;
    QPushButton* m_btn;
    QListView* m_view;
    // 用于连接clicked信号
    void onClicked();
};

#endif

三个组件:按钮被点击后加载数据;QListView 是视图组件,显示数据;QVBoxLayout 是布局用的,界面元素沿垂直方向排列(纵向)。公共成员就是构造函数和析构函数。析构是空白的,实现构造函数即可。

#include "../include/demo.h"
#include <QStringList>
#include <QStringListModel>

MyWindow::MyWindow(QWidget * parent)
    : QWidget::QWidget(parent)
{
    m_layout = new QVBoxLayout();
    setLayout(m_layout);
    // 按钮
    m_btn = new QPushButton("加载数据", this);
    m_layout->addWidget(m_btn);
    // 视图
    m_view = new QListView(this);
    m_layout->addWidget(m_view, 1);
    // 连接信号
    connect(m_btn, &QPushButton::clicked, this, &MyWindow::onClicked);
}

MyWindow::~MyWindow()
{
}

下面是重点,实现 onClicked 成员方法,构建数据并在视图中显示。

void MyWindow::onClicked()
{
    // 创建字符串列表
    QStringList list;
    list << "手榴弹" << "地雷" << "鱼雷" << "燃烧弹";
    // 创建模型实例
    QStringListModel *model = new QStringListModel(list, this);
    // 设置给视图
    m_view->setModel(model);
}

代码不复杂。QStringList 和我们熟悉的 cout 一样,可以用“<<”运算符写入字符串。每一次运算就写入一个元素,故上述代码向列表填充了四个元素。接着实例化 QStringListModel 类,传给 setModel 方法就可以与视图关联。这里实例化模型类最好使用指针类型,可以将 this 传给构造函数,这样,当前窗口会管理它的生命周期,你不用担心堆内存没有释放。

main 函数就没什么了,初始化应用程序,显示窗口就完事了。

int main(int argc, char** argv)
{
    QApplication app(argc, argv);
    MyWindow* win = new MyWindow();
    win->setWindowTitle("示例");
    win->resize(320, 275);
    win->show();

    return QApplication::exec();
}

运行代码后,点一下按钮,就能看到字符串列表了。

367389-20230917214033084-832956871.png

QStringListModel 只针对字符串列表,对于复杂一些的列表项,还是不够用的。所以咱们要请出下一位表演者—— QStandardItemModel。字面翻译:标准列表模型。这个模型就不仅仅可以用字符串列表当源了,它可以设置更复杂的数据。比如图标、显示文本、背景色、文本颜色等。

为了便于使用,标准列表模型中的“项”由 QStandardItem 封装。你可以先创建 QStandardItem 实例,设置好各种数据后,再添加进 QStandardItemModel 中。在设置子项,你可以不用 setData,而改用 setItem 方法。setItem 方法调用时只要指出行号、列号(如果是一维列表,调用另一个重载可以忽略列号)即可。

对于行标题、列标题,还有更方便的 setHorizontalHeaderLabels(水平)和 setVerticalHeaderLabels(垂直)方法,只要提供一个字符串列表,就能设置行列标题了。

现在,咱们了解一下 QStandardItem 这厮怎么用,这个很重要,用好标准模型得靠它。它的构造函数传参允许你指定显示文本、图标,或者总共多少行多少列。毕竟它有四个重载:

QStandardItem();
explicit QStandardItem(const QString &text);
QStandardItem(const QIcon &icon, const QString &text);
explicit QStandardItem(int rows, int columns = 1);

当然,可以调用无参数构造函数,然后再调用 set**** 方法来设置各项数据,比如:

  1. setText:设置要显示的文本,就是你想让用户看到的文本;
  2. setIcon:设置图标。不管是 QTableView 视图还是 QTreeView 视图,其实只有第一列才会显示图标。说直接点就是其他非第一列的项,你设置了图标也是呵呵;
  3. setToolTip:设置工具提示。就是当鼠标悬浮在上面时显示的文本;
  4. setWhatsThis:设置 What the FK,不,是 What is This 帮助信息;
  5. setFont:设置显示该项所使用的字体;
  6. setTextAlignment:设置显示该项时,文本的对齐方式;
  7. setBackground:设置该项的背景颜色;
  8. setForeground:前景色,就是设置文本的颜色;
  9. setStatusTip:显示在状态栏上的提示信息(这个好像不常用);
  10. setDragEnabled、setDropEnabled:该项支不支持拖放操作;
  11. setAutoTristate:树形视图时用得上,比如父节点被 check 后,是否所有子节点也跟着 check;
  12. setCheckable:该项是否显示一个 CheckBox 框,让用户可以在那里勾来勾去;
  13. setSelectable:该项允许用户选择吗;
  14. setEnabled:若是 true,用户可以操作该项,如选中它;如为 false,此项为禁用状态;
  15. setEditable:该项允许编辑吗。

你要是觉得 setEnabled、setEditable、setSelectable 这些方法麻烦,可以用 setFlags 方法一次性解决,用 Or 运算组合的 Qt::ItemFlags 枚举来设置。

当然,还有些成员我没列出,看着那么多成员方法好像很复杂,其实我们不一定全调用一遍的,看需要,不需要的可以不管的。

下面咱们也弄个例子。这个例子,我直接从 QTableView 类派生。

// 头文件

#ifndef CUST_H
#define CUST_H

#include<qwidget.h>
#include<qstandarditemmodel.h>
#include <qtableview.h>
#include <qcolor.h>

class MyTableView : public QTableView
{
    Q_OBJECT

public:
    explicit MyTableView(QWidget* parent = nullptr);
    ~MyTableView();

private:
    QStandardItemModel *model;
};

#endif

私有成员是模型对象,然后,咱们实际要做的是实现构造函数,实例化模型,再往里面塞数据。

MyTableView::MyTableView(QWidget *parent)
    : QTableView(parent)
{
    // 实例化模型类
    model = new QStandardItemModel(this);
    // 五行三列
    // model->setRowCount(5);
    // model->setColumnCount(3);
    // 设置列标题
    model->setHorizontalHeaderLabels({"学号", "姓名", "分数"});
    // 好,我们开始准备数据
    // 第一行
    QStandardItem *cell00 = new QStandardItem(QIcon("a.png"), "2572");
    QStandardItem *cell01 = new QStandardItem("小李");
    QStandardItem *cell02 = new QStandardItem("58");
    // 不合格,让它显示黄色
    cell02->setBackground(QColor("yellow"));
    cell02->setForeground(QColor("red"));
    // 设置到模型中
    model->setItem(0, 0, cell00);
    model->setItem(0, 1, cell01);
    model->setItem(0, 2, cell02);
    // 第二行
    QStandardItem *cell10 = new QStandardItem(QIcon("a.png"), "2055");
    QStandardItem *cell11 = new QStandardItem("小陈");
    QStandardItem *cell12 = new QStandardItem("85");
    model->setItem(1, 0, cell10);
    model->setItem(1, 1, cell11);
    model->setItem(1, 2, cell12);
    // 第三行
    QStandardItem *cell20 = new QStandardItem(QIcon("a.png"), "1069");
    QStandardItem *cell21 = new QStandardItem("小杜");
    QStandardItem *cell22 = new QStandardItem("70");
    model->setItem(2, 0, cell20);
    model->setItem(2, 1, cell21);
    model->setItem(2, 2, cell22);
    // 第四行
    QStandardItem *cell30 = new QStandardItem(QIcon("a.png"), "2469");
    QStandardItem *cell31 = new QStandardItem("小王");
    QStandardItem *cell32 = new QStandardItem("100");
    // 满分,给他点奖励
    cell32->setBackground(QColor("blue"));
    cell32->setForeground(QColor("white"));
    model->setItem(3, 0, cell30);
    model->setItem(3, 1, cell31);
    model->setItem(3, 2, cell32);
    // 第五行
    QStandardItem *cell40 = new QStandardItem(QIcon("a.png"), "6394");
    QStandardItem *cell41 = new QStandardItem("小张");
    QStandardItem *cell42 = new QStandardItem("89");
    model->setItem(4, 0, cell40);
    model->setItem(4, 1, cell41);
    model->setItem(4, 2, cell42);

    // 设置模型
    setModel(model);
}

MyTableView::~MyTableView()
{
}

setRowCount、setColumnCount 方法可以不调用,模型会根据你放的数据调整。有两点得注意:

1、这里每个 QStandardItem 代表的是一个单元格的数据,而不是一行;

2、QStandardItem 类型的变量要声明为指针类型,并且要在堆上分配;不能用栈分配,会显示空白(提前析构了)。

然后,main 函数就更简单了。

#include <QApplication>
#include "cust.h"

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);
    MyTableView *win = new MyTableView();
    win->setWindowTitle("月考结果");
    win->resize(300, 260);
    win->show();

    return QApplication::exec();
}

结果如下图:

367389-20230917230420047-2552882.png

 第一行和第四行,咱们修改过背景色和文本颜色。

// 不合格,让它显示黄色
cell02->setBackground(QColor("yellow"));
cell02->setForeground(QColor("red"));
……
// 满分,给他点奖励
cell32->setBackground(QColor("blue"));
cell32->setForeground(QColor("white"));

好了,今天就聊到这儿,QStandardItemModel 还有其他耍法,尤其是在树形结构上。咱们下一期继续扯。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK