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

这两个类是QAbstractListModel 和QAbstractTableModel。

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

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

// 头文件#ifndef LIST_H#define LIST_H#include #include class CustListModel: public QAbstractListModel{    Q_OBJECTpublic:    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 中指定的元素,用 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 方法获得被选项的列表。

看看效果。

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

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

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

咱们演练一下。

// 头文件#ifndef DEMO_H#define DEMO_H#include #include #include #include class MyWindow : public QWidget{    Q_OBJECTpublic:    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 #include 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();}

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

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#include#include #include class MyTableView : public QTableView{    Q_OBJECTpublic:    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 #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();}

结果如下图:

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

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

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