本页内容

自定义排序/筛选模型示例

“自定义排序/过滤模型”示例演示了如何通过继承 `QSortFilterProxyModel ` 类来实现高级排序和过滤功能。

自定义排序/筛选模型示例的屏幕截图

QSortFilterProxyModel 类支持对在另一个模型与视图之间传递的数据进行排序和过滤。

该模型通过将源模型提供的索引映射到对应不同位置的新索引,从而转换源模型的结构,供视图使用。这种方法允许从视图的角度对给定的源模型进行重组,而无需对底层数据进行任何转换,也不会在内存中复制数据。

“自定义排序/过滤模型”示例由两个类组成:

  • MySortFilterProxyModel 类提供了一个自定义代理模型。
  • Window 类提供主应用程序窗口,并利用自定义代理模型对标准项模型进行排序和过滤。

我们将首先查看MySortFilterProxyModel 类,了解自定义代理模型的实现方式;然后查看Window 类,了解该模型的使用方法;最后简要介绍main() 函数。

MySortFilterProxyModel 类定义

MySortFilterProxyModel 类继承自QSortFilterProxyModel 类。

由于QAbstractProxyModel 及其子类都从QAbstractItemModel 派生而来,因此关于普通模型子类的许多建议同样适用于代理模型。

另一方面,值得注意的是,QSortFilterProxyModel 类中许多函数的默认实现都是通过调用相关源模型中的等效函数来实现的。对于行为更复杂的源模型,可能需要重写这一简单的代理机制。 在此示例中,我们继承自QSortFilterProxyModel 类,以确保我们的过滤器能够识别有效的日期范围,并控制排序行为。

class MySortFilterProxyModel : public QSortFilterProxyModel
{
    Q_OBJECT

public:
    MySortFilterProxyModel(QObject *parent = nullptr);

    QDate filterMinimumDate() const { return minDate; }
    void setFilterMinimumDate(QDate date);

    QDate filterMaximumDate() const { return maxDate; }
    void setFilterMaximumDate(QDate date);

protected:
    bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
    bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;

private:
    bool dateInRange(QDate date) const;

    QDate minDate;
    QDate maxDate;
};

我们希望能够通过指定特定时间段来过滤数据。为此,我们实现了自定义的setFilterMinimumDate() 和setFilterMaximumDate() 函数,以及相应的filterMinimumDate() 和filterMaximumDate() 函数。 我们重写了QSortFilterProxyModel 的filterAcceptsRow()方法,使其仅接受日期有效的行;并重写了QSortFilterProxyModel::lessThan()方法,以便能够按发件人的电子邮件地址进行排序。最后,我们实现了dateInRange() 便捷方法,用于判断日期是否有效。

MySortFilterProxyModel 类的实现

MySortFilterProxyModel 的构造函数非常简单,它将父类参数传递给基类的构造函数:

MySortFilterProxyModel::MySortFilterProxyModel(QObject *parent)
    : QSortFilterProxyModel(parent)
{
}

MySortFilterProxyModel 实现中最有趣的部分是对QSortFilterProxyModel 中filterAcceptsRow()和lessThan()函数的重写。让我们先来看看我们自定义的lessThan() 函数。

bool MySortFilterProxyModel::lessThan(const QModelIndex &left,
                                      const QModelIndex &right) const
{
    QVariant leftData = sourceModel()->data(left);
    QVariant rightData = sourceModel()->data(right);

我们希望按发件人的电子邮箱地址对其进行排序。在排序时,lessThan() 函数被用作 < 运算符。默认实现支持包括QDateTime 和 String 在内的多种类型集合,但为了能够按电子邮箱地址对发件人进行排序,我们必须首先从给定的字符串中识别出该地址:

    if (leftData.userType() == QMetaType::QDateTime)
        return leftData.toDateTime() < rightData.toDateTime();

    static const QRegularExpression emailPattern(u"[\\w\\.]*@[\\w\\.]*"_s);

    QString leftString = leftData.toString();
    if (left.column() == 1) {
        const QRegularExpressionMatch match = emailPattern.match(leftString);
        if (match.hasMatch())
            leftString = match.captured(0);
    }
    QString rightString = rightData.toString();
    if (right.column() == 1) {
        const QRegularExpressionMatch match = emailPattern.match(rightString);
        if (match.hasMatch())
            rightString = match.captured(0);
    }

    return QString::localeAwareCompare(leftString, rightString) < 0;
}

我们使用QRegularExpression 来定义要查找的地址模式。match() 函数返回一个QRegularExpressionMatch 对象,其中包含匹配结果。如果匹配成功,hasMatch() 将返回 true。 可以通过QRegularExpressionMatch 的captured()函数获取匹配结果。整个匹配结果的索引为0,而括号内的子表达式的索引从1开始(不包括非捕获括号)。

bool MySortFilterProxyModel::filterAcceptsRow(int sourceRow,
                                              const QModelIndex &sourceParent) const
{
    QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent);
    QModelIndex index1 = sourceModel()->index(sourceRow, 1, sourceParent);
    QModelIndex index2 = sourceModel()->index(sourceRow, 2, sourceParent);

    return (sourceModel()->data(index0).toString().contains(filterRegularExpression())
            || sourceModel()->data(index1).toString().contains(filterRegularExpression()))
            && dateInRange(sourceModel()->data(index2).toDate());
}

另一方面,filterAcceptsRow() 函数在给定行应被纳入模型时,预期返回 true。在我们的示例中,如果主题或发件人中包含给定的正则表达式,且日期有效,则该行被接受。

bool MySortFilterProxyModel::dateInRange(QDate date) const
{
    return (!minDate.isValid() || date > minDate)
            && (!maxDate.isValid() || date < maxDate);
}

我们使用自定义的 `dateInRange() ` 函数来判断日期是否有效。

为了能够通过指定特定时间段来过滤数据,我们还实现了用于获取和设置最小及最大日期的函数:

void MySortFilterProxyModel::setFilterMinimumDate(QDate date)
{
    beginFilterChange();
    minDate = date;
    endFilterChange(QSortFilterProxyModel::Direction::Rows);
}

void MySortFilterProxyModel::setFilterMaximumDate(QDate date)
{
    beginFilterChange();
    maxDate = date;
    endFilterChange(QSortFilterProxyModel::Direction::Rows);
}

获取函数filterMinimumDate() 和filterMaximumDate() 非常简单,作为内联函数在头文件中实现。

至此,我们的自定义代理模型已完成。接下来让我们看看如何在应用程序中使用它。

窗口类定义

CustomFilter 类继承自QWidget ,并提供了本示例的主应用程序窗口:

class Window : public QWidget
{
    Q_OBJECT

public:
    Window();

    void setSourceModel(QAbstractItemModel *model);

private slots:
    void textFilterChanged();
    void dateFilterChanged();

private:
    MySortFilterProxyModel *proxyModel;

    QTreeView *sourceView;
    QTreeView *proxyView;
    FilterWidget *filterWidget;
    QDateEdit *fromDateEdit;
    QDateEdit *toDateEdit;
};

我们实现了两个私有槽:textFilterChanged() 和dateFilterChanged() ,用于响应用户更改筛选模式、区分大小写或任何日期的情况。此外,我们还实现了一个公共的setSourceModel() 便利函数,用于建立模型与视图之间的关系。

窗口类的实现

在此示例中,我们选择在 `main ()` 函数中创建并设置源模型(稍后将详细说明)。因此,在构建主应用程序窗口时,我们假设源模型已存在,并首先创建自定义代理模型的实例:

Window::Window()
    : proxyModel(new MySortFilterProxyModel(this)),

我们设置了dynamicSortFilter 属性,该属性用于控制代理模型是否进行动态排序和过滤。将此属性设置为true,可确保每当源模型的内容发生变化时,代理模型都会进行排序和过滤。

主应用程序窗口同时显示源模型和代理模型的视图。源视图非常简单:

sourceView->setRootIsDecorated(false);
sourceView->setAlternatingRowColors(true);

QTreeView 类提供了一种树形视图的默认模型/视图实现。我们的视图实现了应用程序源模型中项目的树形表示。

auto *sourceGroupBox = new QGroupBox(tr("Original Model"));
auto *sourceLayout = new QHBoxLayout(sourceGroupBox);
sourceLayout->addWidget(sourceView);

QTreeView 类提供树视图的默认模型/视图实现;我们的视图则实现了应用程序源模型中各项内容的树形表示。我们将视图控件添加到一个布局中,并将其安装在相应的组框上。

另一方面,代理模型视图包含若干控件,用于控制源模型数据结构转换的各个方面:

filterWidget->setText(tr("Grace|Sports"));
fromDateEdit->setDate(QDate(1970, 01, 01));
toDateEdit->setDate(QDate(2099, 12, 31));

请注意,每当用户更改其中一个过滤选项时,我们必须显式地重新应用过滤器。这是通过将各个编辑器连接到更新代理模型的函数来实现的。

connect(filterWidget, &FilterWidget::filterChanged,
        this, &Window::textFilterChanged);
connect(filterWidget, &QLineEdit::textChanged,
        this, &Window::textFilterChanged);
connect(fromDateEdit, &QDateTimeEdit::dateChanged,
        this, &Window::dateFilterChanged);
connect(toDateEdit, &QDateTimeEdit::dateChanged,
        this, &Window::dateFilterChanged);

排序将由视图处理。我们只需通过设置QTreeView::sortingEnabled 属性(默认值为false)来启用代理视图的排序功能。然后,我们将所有过滤控件和代理视图添加到一个布局中,并将该布局安装在相应的组框上。

proxyView->setRootIsDecorated(false);
proxyView->setAlternatingRowColors(true);
proxyView->setModel(proxyModel);
proxyView->setSortingEnabled(true);
proxyView->sortByColumn(1, Qt::AscendingOrder);

auto *proxyGroupBox = new QGroupBox(tr("Sorted/Filtered Model"));
auto *proxyLayout = new QVBoxLayout(proxyGroupBox);
proxyLayout->addWidget(proxyView);
auto *formLayout = new QFormLayout;
proxyLayout->addLayout(formLayout);
formLayout->addRow(tr("&Filter pattern:"), filterWidget);
formLayout->addRow(tr("F&rom:"), fromDateEdit);
formLayout->addRow(tr("&To:"), toDateEdit);

最后,将这两个组框放入另一个布局中,并将其安装在主应用程序控件上,从而完成应用程序窗口的自定义。

    auto *mainLayout = new QVBoxLayout(this);
    mainLayout->addWidget(sourceGroupBox);
    mainLayout->addWidget(proxyGroupBox);

    setWindowTitle(tr("Custom Sort/Filter Model"));
    auto screenGeometry = screen()->geometry();
    resize(screenGeometry.width() / 2, screenGeometry.height() * 2 / 3);
}

如上所述,我们在main ()函数中创建源模型,并调用Window::setSourceModel() 函数使应用程序使用该模型:

void Window::setSourceModel(QAbstractItemModel *model)
{
    proxyModel->setSourceModel(model);
    sourceView->setModel(model);

    for (int i = 0; i < proxyModel->columnCount(); ++i)
        proxyView->resizeColumnToContents(i);
    for (int i = 0; i < model->columnCount(); ++i)
        sourceView->resizeColumnToContents(i);
}

QSortFilterProxyModel::setSourceModel() 函数使代理模型处理给定模型中的数据,在本例中即为邮件模型。视图控件从QAbstractItemModel 类继承的setModel() 方法,用于设置视图要呈现的模型。请注意,后者还会创建并设置一个新的选择模型。

void Window::textFilterChanged()
{
    FilterWidget::PatternSyntax s = filterWidget->patternSyntax();
    QString pattern = filterWidget->text();
    switch (s) {
    case FilterWidget::Wildcard:
        pattern = QRegularExpression::wildcardToRegularExpression(pattern);
        break;
    case FilterWidget::FixedString:
        pattern = QRegularExpression::escape(pattern);
        break;
    default:
        break;
    }

    QRegularExpression::PatternOptions options = QRegularExpression::NoPatternOption;
    if (filterWidget->caseSensitivity() == Qt::CaseInsensitive)
        options |= QRegularExpression::CaseInsensitiveOption;
    QRegularExpression regularExpression(pattern, options);
    proxyModel->setFilterRegularExpression(regularExpression);
}

每当用户更改过滤器模式或大小写敏感性时,都会调用textFilterChanged() 函数。

我们首先获取首选语法(使用 FilterWidget::PatternSyntax 枚举来解释给定模式的含义),然后确定首选的大小写敏感性。基于这些首选项和当前的过滤模式,我们设置代理模型的filterRegularExpression 属性。filterRegularExpression 属性保存用于过滤源模型内容的正则表达式。 请注意,调用QSortFilterProxyModel 的setFilterRegularExpression()函数也会更新模型。

void Window::dateFilterChanged()
{
    proxyModel->setFilterMinimumDate(fromDateEdit->date());
    proxyModel->setFilterMaximumDate(toDateEdit->date());
}

每当用户修改有效日期范围时,都会调用dateFilterChanged() 函数。我们从用户界面获取新的日期,并调用相应的函数(由我们的自定义代理模型提供)来设置代理模型的最小日期和最大日期。如上所述,调用这些函数也会更新模型。

Main() 函数

在此示例中,我们通过在 `main ()` 函数中创建模型,将应用程序与源模型分离。首先创建应用程序,然后创建源模型:

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    Window window;
    window.setSourceModel(new MailModel(&window));
    window.show();
    return QApplication::exec();
}

我们创建了一个名为MailModel 的类,该类继承自QRangeModel ,并重写了 QAbstractItemView::headerData() 方法以返回正确的章节标题。该类通过一个MailHeader 结构体数组进行初始化。MailHeader 被声明为Q_GADGET ,其属性将由QRangeModel 使用:

struct MailHeader
{
private:
    Q_GADGET
    Q_PROPERTY(QString subject MEMBER subject)
    Q_PROPERTY(QString sender MEMBER sender)
    Q_PROPERTY(QDateTime date MEMBER date)

public:
    QString subject;
    QString sender;
    QDateTime date;
};
class MailModel : public QRangeModel
{
public:
    explicit MailModel(QObject *parent = nullptr) : QRangeModel(mails, parent) {}

    QVariant headerData(int section, Qt::Orientation orientation,
                        int role = Qt::DisplayRole) const override
    {
        if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
            switch (section) {
            case 0:
                return Window::tr("Subject");
            case 1:
                return Window::tr("Sender");
            case 2:
                return Window::tr("Date");
            default:
                break;
            }
        }
        return QRangeModel::headerData(section, orientation, role);
    }

private:
    static const std::array<MailHeader, 10> mails;
};

const std::array<MailHeader, 10> MailModel::mails = {
    MailHeader{ u"RE: Sports"_s, u"Petra Schmidt <petras@nospam.com>"_s,
                QDateTime(QDate(2007, 01, 05), QTime(12, 01)) },
    MailHeader{ u"AW: Sports"_s, u"Rolf Newschweinstein <rolfn@nospam.com>"_s,
                QDateTime(QDate(2007, 01, 05), QTime(12, 00)) },
    MailHeader{ u"Sports"_s, u"Linda Smith <linda.smith@nospam.com>"_s,
                QDateTime(QDate(2007, 01, 05), QTime(11, 33)) },
    MailHeader{ u"Re: Accounts"_s, u"Andy <andy@nospam.com>"_s,
                QDateTime(QDate(2007, 01, 03), QTime(14, 26)) },
    MailHeader{ u"Re: Accounts"_s, u"Joe Bloggs <joe@bloggs.com>"_s,
                QDateTime(QDate(2007, 01, 03), QTime(14, 18)) },
    MailHeader{ u"Re: Expenses"_s, u"Andy <andy@nospam.com>"_s,
                QDateTime(QDate(2007, 01, 02), QTime(16, 05)) },
    MailHeader{ u"Expenses"_s, u"Joe Bloggs <joe@bloggs.com>"_s,
                QDateTime(QDate(2006, 12, 25), QTime(11, 39)) },
    MailHeader{ u"Accounts"_s, u"pascale@nospam.com"_s,
                QDateTime(QDate(2006, 12, 31), QTime(12, 50)) },
    MailHeader{ u"Radically new concept"_s, u"Grace K. <grace@software-inc.com>"_s,
                QDateTime(QDate(2006, 12, 22), QTime(9, 44)) },
    MailHeader{ u"Happy New Year!"_s, u"Grace K. <grace@software-inc.com>"_s,
                QDateTime(QDate(2006, 12, 31), QTime(17, 03)) }
};

示例项目 @ code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.