Qt 之 QHeaderView 自定义排序(终极版)

作者: 一去、二三里
个人微信号: iwaleon
微信公众号: 高效程序员

本节主要解决自定义排序衍生的第二个问题-将整形显示为字符串,而排序依然正常。

下面我们介绍三种方案:

  1. 委托绘制
  2. 用户数据
  3. 辅助列

很多人也许会有疑虑,平时都用delegate来绘制各种按钮、图标、图形等操作,它还能排序?当然,它本身是不会排序的,但他的高级用法之一就是-辅助排序。

委托绘制

效果

这里写图片描述

QStyledItemDelegate

我们可以通过设置显示的文本,然后调用QStyle的drawControl来进行ViewItem的绘制。绘制之后,数据源中的数据依然是qint64的,而我们看到的是绘制之后的文本-QString类型,这样QSortFilterProxyModel默认排序(根据源数据排序)就可以满足我们的要求了。

void SortDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QStyleOptionViewItem viewOption(option);
    initStyleOption(&viewOption, index);
    if (option.state.testFlag(QStyle::State_HasFocus))
        viewOption.state = viewOption.state ^ QStyle::State_HasFocus;

    // 进行大小转换
    if (index.column() == FILE_SIZE_COLUMN)
    {
        qint64 bytes = index.data().toLongLong();
        viewOption.text = bytesToGBMBKB(bytes) ;
        QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &viewOption, painter, viewOption.widget);
    }
    else
    {
        QStyledItemDelegate::paint(painter, viewOption, index);
    }
}

眼见不一定为实

通过效果图我们也可以很明显的看出来,其实内部的数据并不是界面显示的字符串,而是原始的qint64类型的数据。

pTableView->setMouseTracking(true);
connect(pTableView, SIGNAL(entered(QModelIndex)), this, SLOT(showToolTip(QModelIndex)));

void MainWindow::showToolTip(const QModelIndex &index)
{
    if (!index.isValid())
        return;

    int nColumn = index.column();
    if ((nColumn == FILE_SIZE_COLUMN))
        QToolTip::showText(QCursor::pos(), index.data().toString());
}

用户数据

QAbstractTableModel

显示在界面的数据为DisplayRole中的数据,我们可以看到已经通过bytesToGBMBKB转化为字符串,这时我们可以通过设置UserRole添加用户数据将源数据存储起来。

// 表格项数据
QVariant TableModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    int nRow = index.row();
    int nColumn = index.column();
    FileRecord record = m_recordList.at(nRow);

    switch (role)
    {
    case Qt::TextColorRole:
        return QColor(Qt::white);
    case Qt::TextAlignmentRole:
        return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
    case Qt::DisplayRole:
    {
        if (nColumn == FILE_NAME_COLUMN)
        {
            return record.strFileName;
        }
        else if (nColumn == DATE_TIME_COLUMN)
        {
            return record.dateTime;
        }
        else if (nColumn == FILE_SIZE_COLUMN)
        {
            return bytesToGBMBKB(record.nSize);
        }

        return "";
    }
    case Qt::UserRole:
    {
	    // 新增代码
        if (nColumn == FILE_SIZE_COLUMN)
            return record.nSize;
    }
    default:
        return QVariant();
    }

    return QVariant();
}

QSortFilterProxyModel

根据用户源数据进行排序。

bool SortFilterProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
    if (!source_left.isValid() || !source_right.isValid())
        return false;

    if ((source_left.column() == FILE_SIZE_COLUMN) && (source_right.column() == FILE_SIZE_COLUMN))
    {
        // 这里我们所取得数据是用户源数据
        QVariant leftData = sourceModel()->data(source_left, Qt::UserRole);
        QVariant rightData = sourceModel()->data(source_right, Qt::UserRole);

        if (leftData.canConvert<qint64>() && rightData.canConvert<qint64>())
        {
            return leftData.toLongLong() < rightData.toLongLong();
        }
    }

    return QSortFilterProxyModel::lessThan(source_left, source_right);
}

辅助列

效果

这里写图片描述

QAbstractTableModel

设置辅助数据

#define FILE_NAME_COLUMN 0          // 文件名
#define DATE_TIME_COLUMN 1          // 修改日期
#define FILE_SIZE_COLUMN 2          // 文件大小
#define FILE_SIZE_HIDDEN_COLUMN 3   // 文件大小隐藏列,显示为字节

// 列数
int TableModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);

    return 4;
}

// 设置表格项数据
bool TableModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (!index.isValid())
        return false;

    int nColumn = index.column();
    FileRecord record = m_recordList.at(index.row());
    switch (role)
    {
    case Qt::DisplayRole:
    {
        if (nColumn == FILE_NAME_COLUMN)
        {
            record.strFileName = value.toString();
        }
        else if (nColumn == DATE_TIME_COLUMN)
        {
            record.dateTime = value.toDateTime();
        }
        // 新增代码
        else if ((nColumn == FILE_SIZE_COLUMN) || (nColumn == FILE_SIZE_HIDDEN_COLUMN))
        {
            record.nSize = value.toLongLong();
        }

        m_recordList.replace(index.row(), record);
        emit dataChanged(index, index);

        // 新增代码
        if ((nColumn == FILE_SIZE_COLUMN) || (nColumn == FILE_SIZE_HIDDEN_COLUMN))
        {
            int nSizeColumn = (nColumn == FILE_SIZE_COLUMN) ? FILE_SIZE_HIDDEN_COLUMN : FILE_SIZE_COLUMN;
            QModelIndex sizeIndex = this->index(index.row(), nSizeColumn);
            emit dataChanged(sizeIndex, sizeIndex);
        }

        return true;
    }
    default:
        return false;
    }
    return false;
}

// 表格项数据
QVariant TableModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    int nRow = index.row();
    int nColumn = index.column();
    FileRecord record = m_recordList.at(nRow);

    switch (role)
    {
    case Qt::TextColorRole:
        return QColor(Qt::white);
    case Qt::TextAlignmentRole:
        return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
    case Qt::DisplayRole:
    {
        if (nColumn == FILE_NAME_COLUMN)
        {
            return record.strFileName;
        }
        else if (nColumn == DATE_TIME_COLUMN)
        {
            return record.dateTime;
        }
        else if (nColumn == FILE_SIZE_COLUMN)
        {
            return bytesToGBMBKB(record.nSize);
        }
        // 新增代码
        else if (nColumn == FILE_SIZE_HIDDEN_COLUMN)
        {
            return record.nSize;
        }

        return "";
    }
    default:
        return QVariant();
    }

    return QVariant();
}

// 表头数据
QVariant TableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    switch (role)
    {
    case Qt::TextAlignmentRole:
        return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
    case Qt::DisplayRole:
    {
        if (orientation == Qt::Horizontal)
        {
            if (section == FILE_NAME_COLUMN)
                return QStringLiteral("名称");

            if (section == DATE_TIME_COLUMN)
                return QStringLiteral("修改日期");

            if (section == FILE_SIZE_COLUMN)
                return QStringLiteral("大小");

            // 新增代码
            if (section == FILE_SIZE_HIDDEN_COLUMN)
                return QStringLiteral("大小(字节)");
        }
    }
    default:
        return QVariant();
    }

    return QVariant();
}

QSortFilterProxyModel

这里对第三列进行排序,因为第三列的数据是字符串(当然,也可以反转换),所以使用的辅助列数据,获取字节大小后进行对比。

bool SortFilterProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
    if (!source_left.isValid() || !source_right.isValid())
        return false;

    if ((source_left.column() == FILE_SIZE_COLUMN) && (source_right.column() == FILE_SIZE_COLUMN))
    {
        // 获取辅助列索引
        QModelIndex sizeLeftIndex = sourceModel()->index(source_left.row(), FILE_SIZE_HIDDEN_COLUMN);
        QModelIndex sizeRightIndex = sourceModel()->index(source_right.row(), FILE_SIZE_HIDDEN_COLUMN);

        QVariant leftData = sourceModel()->data(sizeLeftIndex);
        QVariant rightData = sourceModel()->data(sizeRightIndex);

        if (leftData.canConvert<qint64>() && rightData.canConvert<qint64>())
        {
            return leftData.toLongLong() < rightData.toLongLong();
        }
    }

    return QSortFilterProxyModel::lessThan(source_left, source_right);
}

隐藏辅助列

一般来说,辅助列(数据)只对我们处理数据有帮助,而不直接显示在界面上,所以我们可以将其隐藏pTableView->setColumnHidden(FILE_SIZE_HIDDEN_COLUMN, true);

总结

小小一个排序居然也有这么多门道,真是条条大路通罗马,通过这几节的分享,想必大家对排序有了更深入的了解,更多的知识请参考官方文档。

<think>我们正在讨论如何为QTreeView实现自定义排序。根据引用,Qt提供了两种排序方式:一种是使用内置的sortByColumn()和setSortingEnabled(),另一种是自定义排序逻辑。当默认的按字符串排序不满足需求时,我们需要自定义排序规则。 引用[1]提到,默认排序是对item的内容(字符串)进行排序。如果我们需要根据其他规则(如数值、自定义数据类型等)排序,就需要自定义排序。 引用[4]展示了如何通过重写排序函数来实现自定义排序。在该例子中,对TreeItem的_children列表使用qSort进行排序排序依据是自定义的比较函数lessThan_sex(根据性别排序)。 在Qt中,自定义排序主要有两种方式: 1. 在模型(Model)中重写`sort`函数。 2. 使用代理模型(如QSortFilterProxyModel)并重写`lessThan`函数。 下面分别介绍这两种方法: ### 方法1:在自定义模型中重写sort函数(适用于QAbstractItemModel派生的模型) 如果你使用的是QStandardItemModel,你可以通过继承并重写sort函数来实现自定义排序。 步骤: 1. 创建一个继承自QStandardItemModel(或QAbstractItemModel)的自定义模型类。 2. 重写`sort`函数,在该函数中实现自定义排序逻辑。 示例代码: ```cpp class CustomSortModel : public QStandardItemModel { public: void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override { // 在这里实现自定义排序逻辑 // 例如,根据列的不同采用不同的排序规则 if (column == 0) { // 第一列按字符串排序 QStandardItemModel::sort(column, order); } else if (column == 1) { // 第二列按数值排序 // 自定义排序逻辑 // 获取根节点的所有子项(注意:根节点是QModelIndex()) int rowCount = this->rowCount(); QList<QStandardItem*> items; for (int i = 0; i < rowCount; ++i) { items.append(this->item(i, column)); } // 使用std::sort进行排序 std::sort(items.begin(), items.end(), [order](QStandardItem* a, QStandardItem* b) { // 这里假设第二列存储的是数字 bool lessThan = a->text().toInt() < b->text().toInt(); return (order == Qt::AscendingOrder) ? lessThan : !lessThan; }); // 重新排列行(注意:这里只处理了同一级节点,树状结构需要递归处理?) // 注意:重新排列行需要小心,因为树状结构可能有子节点。但QStandardItemModel的sort函数默认只对顶层节点排序,子节点排序需要递归处理。 // 实际上,我们通常只对当前层级排序,而Qt的默认行为也是只对当前层级排序(即点击表头时只对顶层节点排序,子节点保持原顺序)。 // 因此,这里我们只对顶层节点排序。 // 重新组织行顺序 QList<QList<QStandardItem*>> rows; for (QStandardItem* it : items) { int row = it->row(); QList<QStandardItem*> rowItems = takeRow(row); // 取出这一行 rows.append(rowItems); } // 将排好序的行重新插入 for (const QList<QStandardItem*>& row : rows) { appendRow(row); } } } }; ``` 但是,上面的方法比较复杂,而且只处理了顶层节点。实际上,Qt的模型视图框架中,排序函数应该能够处理树形结构,即每个父节点下的子节点分别排序。 更常见的做法是使用代理模型进行排序,这样不需要改变原始模型。 ### 方法2:使用QSortFilterProxyModel并重写lessThan函数(推荐) 步骤: 1. 创建一个继承自QSortFilterProxyModel的类。 2. 重写`lessThan`函数,在该函数中定义两个模型索引的比较规则。 示例代码: ```cpp class CustomSortProxyModel : public QSortFilterProxyModel { protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { // 获取数据 QVariant leftData = sourceModel()->data(left); QVariant rightData = sourceModel()->data(right); // 根据列的不同采用不同的比较规则 if (left.column() == 1) { // 假设第二列是数值 return leftData.toInt() < rightData.toInt(); } else if (left.column() == 2) { // 第三列可能是自定义类型 // 这里举例:按照字符串长度排序 return leftData.toString().length() < rightData.toString().length(); } // 默认按字符串排序 return leftData.toString() < rightData.toString(); } }; ``` 使用方式: ```cpp // 原始模型 QStandardItemModel *model = new QStandardItemModel(this); // 填充模型... // 创建代理模型 CustomSortProxyModel *proxyModel = new CustomSortProxyModel(this); proxyModel->setSourceModel(model); // 将代理模型设置到视图 ui->treeView->setModel(proxyModel); // 启用点击表头排序 ui->treeView->setSortingEnabled(true); ``` 这样,当用户点击表头时,就会调用我们自定义的`lessThan`函数进行排序。 ### 注意事项: 1. 树形结构的排序:`lessThan`函数比较的是同一父节点下的两个子节点。代理模型会自动处理树形结构,即每个父节点下的子节点分别排序。 2. 性能:对于大型树,排序可能会比较慢。如果数据量很大,可以考虑在数据模型中实现排序(比如在数据库层面排序),或者使用多线程。 3. 自定义数据角色:如果数据不是存储在DisplayRole,比如在UserRole中,那么我们需要在`lessThan`函数中通过指定角色来获取数据。 ```cpp QVariant leftData = sourceModel()->data(left, Qt::UserRole+1); ``` 4. 引用[4]中提到的qSortQt5.15中已被弃用,建议使用std::sort。 ### 总结 在Qt中实现QTreeView的自定义排序,推荐使用QSortFilterProxyModel并重写lessThan方法,这种方法分离了数据和视图,且不破坏原始模型,更加灵活。 如果原始模型是QStandardItemModel,并且你希望在该模型中直接实现排序,可以重写sort函数,但要注意处理树形结构的层级。 根据引用[3]和引用[4],我们还可以通过自定义QHeaderView来实现更复杂的排序交互,但通常重写lessThan已经足够。 最后,记得在视图上启用排序:`ui->treeView->setSortingEnabled(true);`。 §§相关问题§§ 1. 如何在自定义排序中处理树形结构的层级关系? 2. 当使用代理模型进行排序时,如何保持展开状态? 3. 如何实现多列联合排序(例如先按列A排,再按列B排)? 4. 自定义排序时如何避免性能问题(例如超过10万行数据)?</think>### Qt QTreeView 自定义排序数据实现方法 在 Qt 中实现 QTreeView 的自定义排序,主要有两种核心方法:通过模型类重写排序函数或使用代理模型。以下是详细实现方案: --- #### 方法 1:在自定义模型中重写 sort() 函数(推荐) 适用于从 QAbstractItemModel 派生的自定义模型: ```cpp class CustomTreeModel : public QStandardItemModel { public: void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override { // 调用基类方法处理默认排序 QStandardItemModel::sort(column, order); // 自定义排序逻辑 if (column == 2) { // 对特定列进行自定义排序 // 获取根节点 for (int i = 0; i < rowCount(); ++i) { QStandardItem* rootItem = item(i); sortChildren(rootItem, column, order); } } } private: void sortChildren(QStandardItem* parent, int column, Qt::SortOrder order) { if (!parent || parent->rowCount() == 0) return; // 获取所有子节点 QList<QStandardItem*> children; for (int row = 0; row < parent->rowCount(); ++row) { children.append(parent->child(row, column)); } // 自定义排序规则(示例:按数值排序) std::sort(children.begin(), children.end(), [order](QStandardItem* a, QStandardItem* b) { bool lessThan = a->data(Qt::UserRole).toInt() < b->data(Qt::UserRole).toInt(); return (order == Qt::AscendingOrder) ? lessThan : !lessThan; }); // 重新排列子节点 QList<QList<QStandardItem*>> sortedRows; for (QStandardItem* item : children) { sortedRows.append(parent->takeRow(item->row())); } for (const auto& row : sortedRows) { parent->appendRow(row); } // 递归排序子节点的子节点 for (int row = 0; row < parent->rowCount(); ++row) { sortChildren(parent->child(row), column, order); } } }; // 使用示例 CustomTreeModel* model = new CustomTreeModel; ui->treeView->setModel(model); ui->treeView->setSortingEnabled(true); // 启用表头排序 ``` **关键点说明**: 1. 重写 `sort()` 函数实现自定义排序逻辑 2. 使用 `Qt::UserRole` 存储自定义排序数据 3. 递归处理子节点确保整个树结构排序正确 4. 通过 `std::sort` 配合 lambda 表达式实现灵活排序规则[^4] --- #### 方法 2:使用 QSortFilterProxyModel 实现排序 适用于不修改原始模型的情况: ```cpp class CustomSortProxy : public QSortFilterProxyModel { protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { // 获取自定义排序数据(存储在UserRole) QVariant leftData = sourceModel()->data(left, Qt::UserRole); QVariant rightData = sourceModel()->data(right, Qt::UserRole); // 自定义比较逻辑(示例:按优先级排序) if (leftData.isValid() && rightData.isValid()) { return leftData.toInt() < rightData.toInt(); } // 默认文本排序 return QSortFilterProxyModel::lessThan(left, right); } }; // 使用示例 QStandardItemModel* sourceModel = new QStandardItemModel; CustomSortProxy* proxyModel = new CustomSortProxy; proxyModel->setSourceModel(sourceModel); ui->treeView->setModel(proxyModel); ui->treeView->setSortingEnabled(true); ``` **优势**: - 无需修改原始模型 - 自动处理树形结构层级关系 - 支持动态排序(数据变化时自动重新排序)[^1] --- #### 自定义排序数据存储 在模型中存储自定义排序数据: ```cpp // 创建带自定义数据的项 QStandardItem* item = new QStandardItem("Item Text"); // 存储排序用的数值(UserRole) item->setData(42, Qt::UserRole); // 存储优先级标记(UserRole+1) item->setData(true, Qt::UserRole + 1); ``` --- #### 排序触发方式 1. **程序化排序**: ```cpp ui->treeView->sortByColumn(2, Qt::DescendingOrder); ``` 2. **用户点击表头排序**: ```cpp // 启用表头排序功能 ui->treeView->setSortingEnabled(true); // 设置表头可点击 ui->treeView->header()->setSectionsClickable(true); ``` --- #### 性能优化技巧 1. 对于大型数据集: ```cpp model->layoutAboutToBeChanged(); // 开始批量更新 // 执行排序操作... model->layoutChanged(); // 结束批量更新 ``` 2. 使用 `beginResetModel()`/`endResetModel()` 减少界面刷新 3. 对超过 10,000 个节点的树结构,考虑分页加载或延迟排序 --- #### 示例场景:按文件类型和大小排序 ```cpp bool lessThan(const QModelIndex &left, const QModelIndex &right) const { // 获取文件类型(UserRole) QString leftType = sourceModel()->data(left, Qt::UserRole).toString(); QString rightType = sourceModel()->data(right, Qt::UserRole).toString(); // 获取文件大小(UserRole+1) int leftSize = sourceModel()->data(left, Qt::UserRole+1).toInt(); int rightSize = sourceModel()->data(right, Qt::UserRole+1).toInt(); // 先按类型排序,类型相同则按大小排序 if (leftType != rightType) { return leftType < rightType; } return leftSize < rightSize; } ``` --- #### 注意事项 1. **树形结构排序**:确保排序逻辑正确处理父子关系,子节点应随父节点一起移动 2. **数据一致性**:当自定义排序数据变化时调用 `dataChanged()` 信号通知视图更新 3. **代理模型**:使用 `QSortFilterProxyModel` 时,原始索引需通过 `mapToSource()`/`mapFromSource()` 转换[^3] 4. **性能监控**:对超大型数据集实现进度指示器,避免界面卡顿
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一去丶二三里

有收获,再打赏!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值