カスタムソート/フィルターモデルの例

カスタムソート/フィルタモデルの例は、高度なソートとフィルタリングを実行するためにQSortFilterProxyModel をサブクラス化する方法を説明します。

Screenshot of the Custom Sort/Filter Model Example

QSortFilterProxyModel クラスは、別のモデルとビューの間で渡されるデータのソートとフィルタリングのサポートを提供します。

このモデルは、ビューが使用するために、それが供給するモデルのインデックスを、異なる場所に対応する新しいインデックスにマッピングすることによって、ソースモデルの構造を変換します。このアプローチにより、基礎となるデータに対する変換を必要とせず、メモリ内のデータを複製することなく、ビューに関する限り、与えられたソースモデルを再構築することができます。

カスタム・ソート/フィルタ・モデルの例は、2 つのクラスで構成されています:

  • 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() 関数を実装します。また、QSortFilterProxyModelfilterAcceptsRow() 関数を再実装して、有効な日付の行のみを受け付けるようにし、QSortFilterProxyModel::lessThan() 関数を再実装して、メールアドレスで送信者をソートできるようにする。最後に、日付が有効かどうかを判定するために使用するdateInRange() 便利関数を実装します。

MySortFilterProxyModelクラスの実装

MySortFilterProxyModel コンストラクタは些細なもので、ベースクラスのコンストラクタに親パラメータを渡します:

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

MySortFilterProxyModel の実装で最も興味深いのは、QSortFilterProxyModelfilterAcceptsRow() と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();
    } else {
        static const QRegularExpression emailPattern("[\\w\\.]*@[\\w\\.]*");

        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() は真を返します。マッチの結果は、QRegularExpressionMatchcaptured() 関数で取得できます。マッチ全体のインデックスは 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()関数は、指定された行がモデルに含まれるべき場合に真を返すことが期待される。この例では、件名または差出人のいずれかが指定された正規表現を含み、日付が有効な場合に行を受け付けます。

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

日付が有効かどうかを判断するために、dateInRange() 関数を使用しています。

指定した期間を指定してデータをフィルタリングできるようにするために、 最小日付と最大日付を取得したり設定したりする関数も実装しています:

void MySortFilterProxyModel::setFilterMinimumDate(QDate date)
{
    minDate = date;
    invalidateFilter();
}

void MySortFilterProxyModel::setFilterMaximumDate(QDate date)
{
    maxDate = date;
    invalidateFilter();
}

取得関数である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;

    QGroupBox *sourceGroupBox;
    QGroupBox *proxyGroupBox;
    QTreeView *sourceView;
    QTreeView *proxyView;
    QLabel *filterPatternLabel;
    QLabel *fromLabel;
    QLabel *toLabel;
    FilterWidget *filterWidget;
    QDateEdit *fromDateEdit;
    QDateEdit *toDateEdit;
};

textFilterChanged()dateFilterChanged() の2つのプライベート・スロットを実装し、ユーザーがフィルター・パターン、大文字小文字の区別、日付のいずれかを変更した場合に対応する。さらに、モデル/ビューのリレーションを設定するために、publicなsetSourceModel() コンビニエンス関数を実装します。

ウィンドウクラスの実装

この例では、main () 関数(後で説明します)でソースモデルを作成し、設定することにしました。そのため、メイン・アプリケーション・ウィンドウを構築する際には、ソース・モデルが既に存在すると仮定し、カスタム・プロキシ・モデルのインスタンスを作成することから始めます:

Window::Window()
{
    proxyModel = new MySortFilterProxyModel(this);

プロキシ・モデルを動的にソートおよびフィルタリングするかどうかを保持するdynamicSortFilter プロパティを設定します。このプロパティを true に設定することで、ソース・モデルのコンテンツが変更されるたびにモデルがソートおよびフィルタリングされるようにします。

メイン・アプリケーション・ウィンドウには、ソース・モデルとプロキシ・モデルの両方のビューが表示されます。ソース・ビューは非常にシンプルです:

sourceView = new QTreeView;
sourceView->setRootIsDecorated(false);
sourceView->setAlternatingRowColors(true);

QTreeView クラスは、ツリー・ビューのデフォルトのモデル/ビュー実装を提供します。私たちのビューは、アプリケーションのソース・モデルのアイテムのツリー表現を実装しています。

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

QTreeView クラスは、ツリー・ビューのデフォルト・モデル/ビュー実装を提供します。私たちのビューは、アプリケーションのソース・モデル内のアイテムのツリー表現を実装します。ビューウィジェットは、対応するグループボックス上に設置するレイアウトに追加します。

一方、プロキシモデル・ビューには、ソース・モデルのデータ構造を変換する様々な側面を制御する複数のウィジェットが含まれています:

filterWidget = new FilterWidget;
filterWidget->setText(tr("Grace|Sports"));
connect(filterWidget, &FilterWidget::filterChanged, this, &Window::textFilterChanged);

filterPatternLabel = new QLabel(tr("&Filter pattern:"));
filterPatternLabel->setBuddy(filterWidget);

fromDateEdit = new QDateEdit;
fromDateEdit->setDate(QDate(1970, 01, 01));
fromLabel = new QLabel(tr("F&rom:"));
fromLabel->setBuddy(fromDateEdit);

toDateEdit = new QDateEdit;
toDateEdit->setDate(QDate(2099, 12, 31));
toLabel = new QLabel(tr("&To:"));
toLabel->setBuddy(toDateEdit);

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

ユーザがフィルタリングオプションの1つを変更するたびに、明示的にフィルタを再適用しなければならないことに注意してください。これは、様々なエディタをプロキシモデルを更新する関数に接続することで行われます。

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

QGridLayout *proxyLayout = new QGridLayout;
proxyLayout->addWidget(proxyView, 0, 0, 1, 3);
proxyLayout->addWidget(filterPatternLabel, 1, 0);
proxyLayout->addWidget(filterWidget, 1, 1);
proxyLayout->addWidget(fromLabel, 3, 0);
proxyLayout->addWidget(fromDateEdit, 3, 1, 1, 2);
proxyLayout->addWidget(toLabel, 4, 0);
proxyLayout->addWidget(toDateEdit, 4, 1, 1, 2);

proxyGroupBox = new QGroupBox(tr("Sorted/Filtered Model"));
proxyGroupBox->setLayout(proxyLayout);

ソートはビューによって処理されます。私たちがしなければならないことは、QTreeView::sortingEnabled プロパティ(デフォルトでは false)を設定することで、プロキシビューのソートを有効にすることだけです。次に、すべてのフィルタリングウィジェットとプロキシビューを、対応するグループボックスに設置するレイアウトに追加します。

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

setWindowTitle(tr("Custom Sort/Filter Model"));
resize(500, 450);
}

最後に、2つのグループ・ボックスをメイン・アプリケーション・ウィジェットにインストールした別のレイアウトに配置した後、アプリケーション・ウィンドウをカスタマイズします。

前述のように、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 プロパティは、ソース・モデルのコンテンツをフィルタリングするために使用される正規表現を保持します。QSortFilterProxyModelsetFilterRegularExpression() 関数を呼び出すと、モデルも更新されることに注意してください。

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(createMailModel(&window));
    window.show();
    return app.exec();
}

createMailModel() 関数は、コンストラクタを簡略化するために用意された便利な関数です。この関数は、メールのコレクションを記述するモデルを作成して返すだけです。モデルはQStandardItemModel クラスのインスタンスです。つまり、標準的な Qt データ型のリポジトリとして一般的に使用される、カスタムデータを保存するための汎用モデルです。各メールの説明はaddMail() を使ってモデルに追加されます。詳細はitemviews/customsortfiltermodel/main.cpp を参照してください。

サンプルプロジェクト @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。