QT编程(17): Qt 实现自定义列表模型

一、核心原理

Qt中自定义列表模型,核心是继承 QAbstractListModel(单列表)或 QAbstractTableModel(表格列表),重写其纯虚函数,实现数据的存储、访问、更新等逻辑。

QAbstractListModel 是所有列表模型的抽象基类,它定义了模型与视图(如QListView、QListWidget)交互的标准接口,我们只需重写关键虚函数,就能实现自定义的数据逻辑,无需关心视图的渲染细节,符合Qt的"模型-视图-控制器(MVC)"设计模式。

核心逻辑:模型负责管理数据(如存储自定义结构体、数组等),视图通过模型提供的接口读取/修改数据,当数据发生变化时,模型通过信号通知视图更新,实现数据与视图的解耦。

二、必备重写函数(核心)

继承QAbstractListModel后,必须重写以下4个纯虚函数,否则无法实例化模型:

  1. rowCount(const QModelIndex &parent = QModelIndex()) const:返回列表的行数(数据总数),parent默认为空(列表模型无父子关系)。

  2. data(const QModelIndex &index, int role = Qt::DisplayRole) const:根据索引(index)和角色(role)返回对应的数据,是模型与视图交互的核心接口。

  3. setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole):根据索引和角色修改数据,修改成功后需发送 dataChanged 信号通知视图。

  4. flags(const QModelIndex &index) const:返回索引对应的item的状态(如是否可编辑、可选中、可勾选等)。

可选重写函数(根据需求添加):

  • headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const:设置列表的表头(如QListView的水平/垂直表头)。

  • insertRows(int row, int count, const QModelIndex &parent = QModelIndex()):插入行(添加数据),需先调用 beginInsertRows,后调用 endInsertRows,避免视图错乱。

  • removeRows(int row, int count, const QModelIndex &parent = QModelIndex()):删除行(移除数据),需先调用 beginRemoveRows,后调用 endRemoveRows

三、完整实现步骤(示例:自定义用户信息列表)

步骤1:定义自定义数据结构体(存储列表项数据)

首先定义一个结构体,存储每一行(每一个列表项)的数据,示例为用户信息(姓名、年龄、性别):

cpp 复制代码
#include <QAbstractListModel>
#include <QString>
#include <QList>

// 自定义数据结构体
struct UserInfo {
    QString name;   // 姓名
    int age;        // 年龄
    QString gender; // 性别
};

// 声明自定义列表模型
class CustomListModel : public QAbstractListModel {
    Q_OBJECT
public:
    // 构造函数,传入父对象
    explicit CustomListModel(QObject *parent = nullptr);

步骤2:重写纯虚函数(核心实现)

在CustomListModel类中,重写必备的4个纯虚函数,以及常用的可选函数,实现数据的管理与交互:

cpp 复制代码
private:
    QList<UserInfo> m_userList; // 存储所有用户数据(模型的核心数据容器)

public:
    // 1. 返回行数(数据总数)
    int rowCount(const QModelIndex &parent = QModelIndex()) const override {
        Q_UNUSED(parent); // 列表模型无父子关系,忽略parent
        return m_userList.size();
    }

    // 2. 返回指定索引、指定角色的数据
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
        // 校验索引有效性(行号不能超出范围)
        if (!index.isValid() || index.row() < 0 || index.row() >= m_userList.size()) {
            return QVariant();
        }

        // 获取当前行的用户数据
        const UserInfo &user = m_userList.at(index.row());

        // 根据角色返回对应数据(Qt内置角色 + 自定义角色)
        switch (role) {
            case Qt::DisplayRole: // 视图默认显示的文本(可自定义格式)
                return QString("%1(%2岁,%3)").arg(user.name).arg(user.age).arg(user.gender);
            case Qt::EditRole: // 编辑模式下的原始数据(可选,用于编辑时获取完整信息)
                return QVariant::fromValue(user);
            // 自定义角色(用于获取单个字段,如单独获取姓名、年龄)
            case 1001: // 自定义角色:姓名
                return user.name;
            case 1002: // 自定义角色:年龄
                return user.age;
            case 1003: // 自定义角色:性别
                return user.gender;
            default:
                return QVariant();
        }
    }

    // 3. 修改指定索引、指定角色的数据
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
        if (!index.isValid() || index.row() < 0 || index.row() >= m_userList.size()) {
            return false; // 索引无效,修改失败
        }

        UserInfo &user = m_userList[index.row()]; // 获取当前行的可修改引用
        bool changed = false;

        // 根据角色修改对应数据
        switch (role) {
            case Qt::EditRole: // 编辑模式下,直接设置完整的UserInfo对象
                if (value.canConvert<UserInfo>()) {
                    user = value.value<UserInfo>();
                    changed = true;
                }
                break;
            case 1001:
                user.name = value.toString();
                changed = true;
                break;
            case 1002:
                user.age = value.toInt();
                changed = true;
                break;
            case 1003:
                user.gender = value.toString();
                changed = true;
                break;
            default:
                break;
        }

        // 若数据修改成功,发送信号通知视图更新该索引对应的内容
        if (changed) {
            emit dataChanged(index, index); // 两个index相同,表示只更新当前行
            return true;
        }
        return false;
    }

    // 4. 返回item的状态(可编辑、可选中)
    Qt::ItemFlags flags(const QModelIndex &index) const override {
        if (!index.isValid()) {
            return Qt::NoItemFlags;
        }
        // 可选中、可编辑、可启用(默认常用配置)
        return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled;
    }

    // 可选:插入行(添加数据)
    bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
        Q_UNUSED(parent);
        // 校验插入位置(row不能小于0,不能大于当前行数)
        if (row < 0 || row > rowCount()) {
            return false;
        }

        beginInsertRows(parent, row, row + count - 1); // 通知视图:开始插入行
        // 插入count个空的UserInfo对象(可根据需求初始化)
        for (int i = 0; i < count; ++i) {
            m_userList.insert(row + i, UserInfo());
        }
        endInsertRows(); // 通知视图:插入完成
        return true;
    }

    // 可选:删除行(移除数据)
    bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
        Q_UNUSED(parent);
        if (row < 0 || row + count > rowCount()) {
            return false;
        }

        beginRemoveRows(parent, row, row + count - 1); // 通知视图:开始删除行
        for (int i = 0; i < count; ++i) {
            m_userList.removeAt(row);
        }
        endRemoveRows(); // 通知视图:删除完成
        return true;
    }

    // 可选:添加单个用户数据(对外提供接口,方便外部调用)
    void addUser(const UserInfo &user) {
        insertRows(rowCount(), 1); // 在最后一行插入1行
        QModelIndex index = createIndex(rowCount() - 1, 0); // 创建最后一行的索引
        setData(index, QVariant::fromValue(user), Qt::EditRole); // 设置数据
    }

    // 可选:获取单个用户数据(对外提供接口)
    UserInfo getUser(int row) const {
        if (row < 0 || row >= m_userList.size()) {
            return UserInfo();
        }
        return m_userList.at(row);
    }
};

步骤3:实现构造函数(初始化数据)

在.cpp文件中实现构造函数,可初始化一些测试数据,方便调试:

cpp 复制代码
#include "CustomListModel.h"

CustomListModel::CustomListModel(QObject *parent) : QAbstractListModel(parent) {
    // 初始化测试数据(可选)
    m_userList << UserInfo{"张三", 25, "男"}
               << UserInfo{"李四", 28, "女"}
               << UserInfo{"王五", 30, "男"};
}

步骤4:模型与视图绑定(使用自定义模型)

在UI界面中,使用QListView(或QListWidget)绑定自定义模型,实现数据的显示与交互,示例在MainWindow中使用:

cpp 复制代码
#include "MainWindow.h"
#include "CustomListModel.h"
#include <QListView>
#include <QVBoxLayout>

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
    // 1. 创建自定义模型
    CustomListModel *model = new CustomListModel(this);

    // 2. 创建视图(QListView)
    QListView *listView = new QListView(this);

    // 3. 绑定模型与视图
    listView->setModel(model);

    // 4. 可选:设置视图为可编辑(双击item可编辑)
    listView->setEditTriggers(QAbstractItemView::DoubleClicked);

    // 5. 布局设置
    QWidget *centralWidget = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout(centralWidget);
    layout->addWidget(listView);
    setCentralWidget(centralWidget);

    // 示例:添加新数据(对外接口调用)
    model->addUser(UserInfo{"赵六", 22, "女"});

    // 示例:修改数据(通过索引修改)
    QModelIndex index = model->createIndex(0, 0); // 第0行索引
    model->setData(index, "张三(修改后)", 1001); // 修改姓名(自定义角色1001)

    // 示例:删除数据(删除第1行)
    model->removeRows(1, 1);
}

四、关键注意事项

  1. 索引有效性校验:所有操作(data、setData等)前,必须校验index是否有效(isValid())、行号是否在合理范围,否则会导致程序崩溃。

  2. 信号发送:数据修改、插入、删除后,必须发送对应的信号(dataChanged、beginInsertRows/endInsertRows等),否则视图不会更新。

  3. 角色使用:Qt内置角色(如DisplayRole、EditRole)用于视图默认交互,自定义角色用于获取/修改单个字段,角色值建议大于1000(避免与Qt内置角色冲突)。

  4. 数据容器:模型内部建议使用QList、QVector等容器存储数据,方便插入、删除、访问,避免使用普通数组(不易管理大小)。

  5. 线程安全:若模型数据在子线程中修改,需通过信号槽机制通知主线程的模型更新,避免跨线程直接操作模型数据(Qt视图仅支持主线程更新)。

五、扩展功能(可选)

  • 自定义委托(QStyledItemDelegate):配合模型使用,实现列表项的自定义渲染(如设置颜色、字体、图标)、自定义编辑控件(如下拉框、日期选择器)。

  • 排序功能:重写 sort(int column, Qt::SortOrder order) 函数,实现数据排序(需结合数据容器的排序接口)。

  • 过滤功能:结合QSortFilterProxyModel,实现列表数据的过滤(如按姓名、年龄筛选)。

六、常见问题排查

  1. 视图不显示数据:检查rowCount是否返回正确的行数、data函数是否返回有效QVariant、模型与视图是否正确绑定。

  2. 数据修改后视图不更新:检查setData函数是否发送dataChanged信号、修改后是否返回true。

  3. 插入/删除行后视图错乱:检查是否调用了beginInsertRows/endInsertRows、beginRemoveRows/endRemoveRows,且参数(row范围)正确。

  4. 编辑功能无效:检查flags函数是否返回Qt::ItemIsEditable,setData函数是否正确处理EditRole。

相关推荐
ms_27_data_develop2 小时前
Java枚举类、异常、常用类
java·开发语言
add45a2 小时前
C++编译期数据结构
开发语言·c++·算法
岁岁种桃花儿2 小时前
AI超级智能开发系列从入门到上天第四篇:AI应用方案设计
java·服务器·开发语言
Amnesia0_02 小时前
C++中的IO流
开发语言·c++
2401_891482172 小时前
C++模块化编程指南
开发语言·c++·算法
暮冬-  Gentle°3 小时前
自定义类型转换机制
开发语言·c++·算法
2301_816651223 小时前
嵌入式C++低功耗设计
开发语言·c++·算法
czlczl200209253 小时前
JVM创建对象过程
java·开发语言
qq_416018723 小时前
分布式缓存一致性
开发语言·c++·算法