拓展知识

工作中为什么使用自定义模型
使用 QStandardItemModel 虽然方便,但当你的数据量较大或者需要更精细控制数据行为时,性能和灵活性可能会受到限制。通过创建自定义模型,你可以:
●优化性能:仅实现你需要的方法,减少不必要的开销。
●更好地控制数据:定义专门的数据结构,更符合业务需求。
●扩展功能:实现特殊的数据处理或数据验证逻辑。

自定义模型注意事项

在 Qt 中,如果你有一个自定义的模型,并且需要手动管理数据的插入和删除,那么你确实需要在相应的方法中调用 beginInsertRowsendInsertRows 以插入新行,调用 beginRemoveRowsendRemoveRows 以删除行。这些函数的作用是通知视图(如 QTableViewQListView)即将发生的数据变化,确保视图能够正确更新。

为什么系统模型不需要手动调用这些方法?

当使用 Qt 提供的系统模型(如 QStringListModelQStandardItemModel 等)时,这些模型已经实现了内部的管理机制。它们在添加、删除或更新数据时会自动调用 beginInsertRowsendInsertRowsbeginRemoveRowsendRemoveRows。因此,用户不需要手动干预,只需调用相应的方法(如 insertRow()removeRow() 等),系统模型会自动处理这些通知。

在 Qt 中,自定义模型类时,通常需要继承 QAbstractItemModel 类,并实现一些纯虚函数,以定义数据模型的行为。根据 Qt 的文档,子类化模型需要实现的函数可以分为以下几类:

自定义模型需要实现的核心函数

当你创建一个自定义模型,通常会继承自 QAbstractTableModelQAbstractListModel。对于表格数据(如姓名、年龄、职业),QAbstractTableModel 是更合适的选择。以下是创建自定义模型时通常需要实现的核心函数:

1 必须实现的纯虚函数

这些函数是 QAbstractTableModel 的纯虚函数,必须在自定义模型中实现:

  • rowCount: 返回模型中的行数。
  • columnCount: 返回模型中的列数。
  • data: 根据给定的 QModelIndex 和角色(例如显示、编辑)返回对应的数据。

2 推荐实现的函数

为了使模型支持编辑、插入、删除等功能,还应实现以下函数:

  • setData: 允许模型的数据被修改。
  • flags: 设置每个单元格的属性(如是否可编辑)。
  • headerData: 设置行/列的头部标签。
  • insertRows: 实现插入新行的逻辑。
  • removeRows: 实现删除行的逻辑。

3 可选实现的函数

根据需要,特别是当数据结构发生大规模变化时,可以实现:

  • resetModel(使用 beginResetModelendResetModel): 当模型的数据发生重大变化时使用,如整体更换数据源。

自定义模型实现案例

我们可以通过自定义模型,将之前的信息表重新实现

1 创建Person.h用来存储用户信息

class Person
{
public:
    Person();
    QString _name;
    int _age;
    QString _profession;
};

Person.cpp中实现构造函数

Person::Person():_name(""),_age(0),_profession("")
{

}

Person::Person(QString name, int age, QString profession)
    :_name(name),_age(age),_profession(profession)
{

}

2 创建自定义模型类

因为我们是用QTableView展示数据,那配套的数据是QAbstractTableModel(表格数据)最为合适,所以我们定义的模型类要继承这个模型类

我们右键QT项目选择新建模型类PeopleModel, 先定义PeopleModel类并声明成员函数

class PeopleModel : public QAbstractTableModel
{
    Q_OBJECT
public:
    explicit PeopleModel(QObject* parent = nullptr);
    //必须实现的纯虚函数
    //返回行数
   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;
   // 设置数据
   bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
   // 返回索引项对应的标记
   Qt::ItemFlags flags(const QModelIndex &index) const override;
   // 返回头部各列数据
   QVariant headerData(int section, Qt::Orientation orientation,
                         int role = Qt::DisplayRole) const override;
   //插入和删除行会调用如下函数
   bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
   bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
   //重置整个模型数据
   void resetData(const QList<Person> &newPeople);
private:
    QList<Person> _people;
};

3 实现PeopleModel构造函数

PeopleModel::PeopleModel(QObject *parent)
    :QAbstractTableModel(parent)
{
    // 初始化一些示例数据
    _people.append(Person{"张三", 25, "工程师"});
    _people.append(Person{"李四", 30, "设计师"});
    _people.append(Person{"王五", 28, "教师"});
    _people.append(Person{"赵六", 22, "学生"});
}

4 返回行数和列数

int PeopleModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    //返回QList中元素的个数
    return _people.count();
}

int PeopleModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    //返回列数
    return 3;
}

5 返回index对应的表格模型数据

//返回index对应的数据
QVariant PeopleModel::data(const QModelIndex &index, int role) const
{
    //先判断索引是否有效
    if(!index.isValid()){
        return QVariant();
    }

    //判断行号是不是超过最大行,因为index的row是从0开始的
    if(index.row() >= _people.count() || index.row() < 0){
        return QVariant();
    }

    //获取索引对应的行号,再根据行号获取对应人物数据
    auto & person = _people[index.row()];

    // 如果role不是表现角色并且不是可编辑的,就直接返回无效数据
    if(role != Qt::DisplayRole && role != Qt::EditRole){
        return QVariant();
    }

    //判断列数
    auto const & column = index.column();
    //第0列则返回用户名字
    if(column == 0){
        return person._name;
    }

    //第1列则返回用户年龄
    if(column == 1){
        return person._age;
    }

    //第2列则返回用户专业
    if(column == 2){
        return person._profession;
    }

    //都不满足则返回空
    return QVariant();
}

6 设置用户数据

//设置用户数据
bool PeopleModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    //如果索引无效,或者角色不可编辑则直接返回false,表示无需设置
    if(!index.isValid() || role != Qt::EditRole){
        return false;
    }

    //进一步判断index.row是否越界
    if(index.row() >= _people.count() || index.row() < 0){
        return false;
    }

    auto & person = _people[index.row()];

    //最后判断列数设置对应字段
    const auto & column = index.column();
    //列数索引大于等于3则直接返回
    if(column >= 3){
        return false;
    }

    //设置名字
    if(column == 0){
        person._name = value.toString();
        //通知tableview视图刷新
        //三个参数分别表示和要刷新区域的左上角的索引,右下角的索引,以及刷新的角色列表
        emit dataChanged(index,index,{role});
        return true;
    }


    //设置年龄
    if(column == 1){
        person._age = value.toInt();
        //通知tableview视图刷新
        //三个参数分别表示和要刷新区域的左上角的索引,右下角的索引,以及刷新的角色列表
        emit dataChanged(index,index,{role});
        return true;
    }


    //设置专业
    if(column == 2){
        person._profession = value.toString();
        //通知tableview视图刷新
        //三个参数分别表示和要刷新区域的左上角的索引,右下角的索引,以及刷新的角色列表
        emit dataChanged(index,index,{role});
        return true;
    }

    return false;

}

7 返回项的角色

//返回项的标记
Qt::ItemFlags PeopleModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
          return Qt::NoItemFlags;

      return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
}

如果索引无效,则返回NoItemFlags表示无效的项,否则返回可编辑,可被选择以及有效标记。

8 返回表头数据

根据表头返回某个列的名字

QVariant PeopleModel::headerData(int section, Qt::Orientation orientation,
                                 int role) const
{
    if (role != Qt::DisplayRole)
        return QVariant();

    if (orientation == Qt::Horizontal) {
        switch (section) {
        case 0:
            return tr("姓名");
        case 1:
            return tr("年龄");
        case 2:
            return tr("职业");
        default:
            return QVariant();
        }
    }else if (orientation == Qt::Vertical){

        return QString("Row %1").arg(section + 1);
    }
    return QVariant();
}

9 实现插入接口

insertRows会在model调用insertRow的时候触发,所以更新模型数据的时候要实现

bool PeopleModel::insertRows(int row, int count, const QModelIndex &parent)
{
    Q_UNUSED(parent);
    if (row < 0 || row > _people.size())
        return false;

    beginInsertRows(QModelIndex(), row, row + count - 1);
    for (int i = 0; i < count; ++i) {
        _people.insert(row, Person{"", 0, ""});
    }
    endInsertRows();
    return true;
}

10 实现删除接口

removeRows会在model调用removeRow的时候触发,所以更新模型的时候要实现

bool PeopleModel::removeRows(int row, int count, const QModelIndex &parent)
{
    Q_UNUSED(parent);
    if (row < 0 || (row + count) > _people.size())
        return false;

    beginRemoveRows(QModelIndex(), row, row + count - 1);
    for (int i = 0; i < count; ++i) {
        _people.removeAt(row);
    }
    endRemoveRows();
    return true;
}

11 重置数据

更新模型数据的时候会调用此函数

void PeopleModel::resetData(const QList<Person> &newPeople)
{
    beginResetModel();
    _people = newPeople;
    endResetModel();
}

到此模型更新完毕,为了方便演示,我们将原来的模型改为我们自定义的模型

12 更改模型为自定义模型

在MainWindow的声明中将model改为PeopleModel*类型

PeopleModel* model;

然后将MainWindow的构造函数中构造这个自定义模型 , 并将之前对模型数据的初始化删除

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow),
    tableView(new QTableView(this)),
    model(new PeopleModel(this)),
    insertButton(new QPushButton("插入行",this)),
    deleteButton(new QPushButton("删除行",this)){
        
    }

13 新增重置按钮

MainWindow声明处增加重置按钮,添加新的槽函数

//重置数据按钮
QPushButton *resetButton;
private slots:
    //重置数据
    void resetModelData();

MainWindow的构造函数中添加重置按钮

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow),
    tableView(new QTableView(this)),
    model(new PeopleModel(this)),
    insertButton(new QPushButton("插入行",this)),
    deleteButton(new QPushButton("删除行",this)),
    resetButton(new QPushButton("重置数据", this)){
        //...
    }

并且MainWindow构造函数中将重置按钮放入布局,且链接信号和槽

//添加重置按钮
buttonLayout->addWidget(resetButton);
connect(resetButton, &QPushButton::clicked, this, &MainWindow::resetModelData);

14 重新实现槽函数

我们希望在insertRow()调用后让新插入的行的第一个单元格处于编辑状态

删除和之前没区别,获取选中的行然后删除,从后往前删

最后是重置数据直接调用模型的resetData就可以了。

void MainWindow::insertRow()
{
    // 在末尾插入一行
    int row = model->rowCount();
    if (model->insertRow(row)) {
        // 自动选择新插入的行并开始编辑第一个单元格(姓名)
        QModelIndex index = model->index(row, 0);
        tableView->setCurrentIndex(index);
        tableView->edit(index);
    } else {
        QMessageBox::warning(this, "插入失败", "无法插入新行!");
    }
}

//删除行
void MainWindow::deleteRow()
{
    // 获取选中的行
    QModelIndexList selected = tableView->selectionModel()->selectedRows();
    if (selected.isEmpty()) {
        QMessageBox::information(this, "提示", "请先选择要删除的行!");
        return;
    }

    // 删除从后往前
        QList<int> rows;
        for(int i = selected.size()-1; i >= 0; --i){
            rows.append(selected[i].row());
        }

        std::sort(rows.begin(), rows.end(), std::greater<int>());

        for(int i = 0; i < rows.size(); i++){
            if(!model->removeRow(rows[i])){
                QMessageBox::warning(this, "删除失败", "无法删除所选择的行!");
            }
        }

}

void MainWindow::resetModelData()
{
    // 示例:重置模型数据为一个新的列表
    QList<Person> newPeople = {
        {"孙七", 35, "产品经理"},
        {"周八", 27, "测试工程师"},
        {"吴九", 29, "销售经理"}
    };
    model->resetData(newPeople);
}

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐