Qt QTreeView で行を非表示にする方法:setRowHidden() の使い方と具体例

2025-05-27

QTreeView::setRowHidden() とは?

QTreeView::setRowHidden() は、Qtのウィジェットである QTreeView の特定行を表示/非表示に切り替えるための関数です。QTreeView は、階層的なデータをツリー構造で表示するために使用されます。

この関数は、ツリービュー内で特定の行を一時的に隠したり、再度表示したりする際に非常に便利です。例えば、ユーザーの検索条件に基づいて関連する行だけを表示し、それ以外の行を非表示にしたい場合などに利用できます。

関数のシグネチャ

void QTreeView::setRowHidden(int row, const QModelIndex &parent, bool hide)

各引数の説明

  • bool hide: true を指定すると、指定された行は非表示になります。 false を指定すると、指定された行は表示状態に戻ります。

  • const QModelIndex &parent: 非表示にする行の親となるアイテムの QModelIndex を指定します。

    • もし、最上位の行(ルートアイテムの直下の子)を対象とする場合は、空の QModelIndex (QModelIndex()) を渡します。
    • 特定の子アイテムの下にある行を対象とする場合は、その親アイテムの QModelIndex を指定します。
  • int row: 非表示にする(または表示に戻す)行のインデックスを指定します。これは、parent によって指定された親の下にある子要素の何番目の行かを示すゼロベースのインデックスです。

使用例

例えば、QStandardItemModel を使用してツリービューにデータを表示しているとします。その中で、特定の条件に合わない行を非表示にする例を考えます。

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    // モデルの作成
    QStandardItemModel model(0, 1); // 0行1列
    model.setHeaderData(0, Qt::Horizontal, "アイテム");

    // ルートアイテムの追加
    QStandardItem *rootItem = model.invisibleRootItem();

    // トップレベルのアイテムを追加
    QStandardItem *item1 = new QStandardItem("フルーツ");
    QStandardItem *item2 = new QStandardItem("野菜");
    QStandardItem *item3 = new QStandardItem("飲み物");

    rootItem->appendRow(item1);
    rootItem->appendRow(item2);
    rootItem->appendRow(item3);

    // フルーツの下に子アイテムを追加
    item1->appendRow(new QStandardItem("リンゴ"));
    item1->appendRow(new QStandardItem("バナナ"));
    item1->appendRow(new QStandardItem("オレンジ"));

    // 野菜の下に子アイテムを追加
    item2->appendRow(new QStandardItem("トマト"));
    QStandardItem *potatoItem = new QStandardItem("ジャガイモ");
    item2->appendRow(potatoItem);
    item2->appendRow(new QStandardItem("ニンジン"));

    // 飲み物の下に子アイテムを追加
    item3->appendRow(new QStandardItem("水"));
    item3->appendRow(new QStandardItem("ジュース"));

    // QTreeViewの作成とモデルの設定
    QTreeView view;
    view.setModel(&model);
    view.expandAll(); // すべてのアイテムを展開

    // 例1: トップレベルの「野菜」行を非表示にする
    // item2はrootItemの2番目の子(インデックス1)なので、row = 1
    view.setRowHidden(1, QModelIndex(), true); // QModelIndex() はルートの親

    // 例2: 「フルーツ」の下にある「バナナ」を非表示にする
    // 「バナナ」はitem1の2番目の子(インデックス1)
    // parentはitem1のモデルインデックス
    view.setRowHidden(1, item1->index(), true);

    // 例3: 後で「ジャガイモ」行を表示に戻す
    // 「ジャガイモ」はitem2の2番目の子(インデックス1)
    // parentはitem2のモデルインデックス
    // view.setRowHidden(1, item2->index(), false); // 必要に応じてコメントを外して試してください

    view.setWindowTitle("QTreeView::setRowHidden() の例");
    view.show();

    return a.exec();
}

この例では、最初に「野菜」のトップレベルの行を非表示にし、次に「フルーツ」の子である「バナナ」を非表示にしています。

  • 非表示行の選択: 非表示になった行はユーザーから見えなくなりますが、モデル上は存在し続けます。プログラム的には、その行のデータにアクセスすることは可能です。
  • フィルター: 特定の条件に基づいてデータをフィルタリングする場合は、QSortFilterProxyModel を使用する方がより強力で柔軟な方法です。setRowHidden はシンプルなケースや一時的な表示/非表示に適しています。
  • パフォーマンス: 大量の行を頻繁に表示/非表示にする場合、パフォーマンスに影響が出る可能性があります。
  • QAbstractItemModel::index() の活用: QModelIndex を取得する際は、QModelIndex model()->index(row, column, parent) メソッドや、QStandardItem::index() メソッドなどを利用できます。特に QStandardItemModel を使っている場合は、QStandardItem::index() が便利です。


QTreeView::setRowHidden() は非常に便利な機能ですが、正しく使用しないと意図した通りに動作しないことがあります。ここでは、よくある問題とその解決策を説明します。

行が非表示にならない

エラーの原因
  • ビューが適切に設定されていない
    QTreeView が正しくモデルに接続されていなかったり、表示されていない場合。
  • モデルの更新の問題
    モデルの変更通知がビューに伝わっていない場合。ただし、setRowHidden() はビュー側で直接操作するため、通常はモデルの変更通知とは直接関係ありません。
  • 行インデックスの誤り
    指定した row インデックスが、parent の下の子アイテムの範囲外である場合。
  • parent 引数の誤り
    最も一般的な間違いです。対象の行の親となる QModelIndex を正しく指定していない場合、非表示になりません。特に、ルート直下の子アイテムを非表示にする場合は、QModelIndex() (デフォルトコンストラクタで生成された空のインデックス) を parent として渡す必要があります。
トラブルシューティング
  • ビューが可視状態か確認する
    view.show() が呼び出されているか、ビューがレイアウト内に配置されているかを確認します。
  • 行インデックスを確認する
    デバッグ出力 (qDebug()) を使って、非表示にしたい行の正確なインデックスと、その親のインデックスが正しいかを確認してください。
  • parent を確認する
    • 最上位の行を非表示にする場合: view.setRowHidden(rowIndex, QModelIndex(), true);
    • 特定の子アイテムの下の行を非表示にする場合: その親アイテムの QModelIndex を正確に取得して渡します。例えば QStandardItem* parentItem; view.setRowHidden(rowIndex, parentItem->index(), true);

非表示にしたはずの行が、スクロールしたり展開したりすると表示される

これは通常のエラーではなく、setRowHidden() の動作によるものです。setRowHidden() は、あくまで「ビュー」の表示を制御するものであり、モデルからデータを削除するわけではありません。したがって、ビューの状態が変化したときに、非表示に設定された行が再描画されて一時的に表示される、または他の操作によって状態がリセットされることがあります。

  • QSortFilterProxyModel の利用を検討する
    データのフィルタリングや永続的な非表示を目的とする場合、QSortFilterProxyModel を使用するのがより堅牢で推奨される方法です。これは、ビューのモデルとしてプロキシモデルを設定し、プロキシモデルがソースモデルから取得するデータをフィルタリングします。これにより、ビューの再描画によって非表示の行が誤って表示される問題を防ぐことができます。

    QSortFilterProxyModel の基本的な使い方

    1. QSortFilterProxyModel のインスタンスを作成します。
    2. proxyModel->setSourceModel(yourSourceModel); で、実際のデータを持つモデル(QStandardItemModel など)を設定します。
    3. view.setModel(proxyModel); で、QTreeView にプロキシモデルを設定します。
    4. プロキシモデルの filterAcceptsRow() メソッドをオーバーライドして、どの行を表示し、どの行を非表示にするかのロジックを実装します。
    5. フィルタリング条件が変更されたら、proxyModel->invalidateFilter(); を呼び出してビューを更新します。
  • setRowHidden() を再適用する
    何らかのビューの状態変更(例えば expandAll()collapseAll()、スクロールなど)が発生した後に、再度 setRowHidden() を呼び出す必要がある場合があります。

大量の行を非表示にするとパフォーマンスが低下する

setRowHidden() はビューごとに直接行の表示状態を管理するため、非常に多くの行を個別に操作する場合、オーバーヘッドが発生し、パフォーマンスが低下する可能性があります。

  • Viewport Updateの最適化
    view->setUpdatesEnabled(false); で更新を一時停止し、変更後に view->setUpdatesEnabled(true); および view->viewport()->update(); を呼び出すことで、描画の回数を減らすことができます。
  • 遅延処理
    もし setRowHidden() を使用する必要がある場合でも、すべての行を一度に処理するのではなく、QTimer::singleShot などを使用して処理を細かく分割し、UIがフリーズしないように考慮することができます。
  • QSortFilterProxyModel の利用
    上記のように、大量の行をフィルタリングする場合は、QSortFilterProxyModel が最も効率的な解決策です。モデルレベルでフィルタリングを行うため、ビューの再描画にかかるコストを削減できます。

QModelIndex が無効になる

モデルの構造が変更された(行が追加/削除された、親が変更されたなど)後に、以前に取得した QModelIndex が無効になることがあります。無効な QModelIndexsetRowHidden() に渡しても、期待通りに動作しません。

  • QPersistentModelIndex の使用
    モデルの変更後も有効なインデックスが必要な場合は、QPersistentModelIndex を使用することを検討してください。これは、モデルの変更(行の追加/削除など)に追従して、自身を自動的に更新する QModelIndex のラッパーです。ただし、これも万能ではなく、モデルの構造が大幅に変更される場合は注意が必要です。
  • 常に最新の QModelIndex を取得する
    モデルが変更される可能性のある操作(行の追加、削除、移動など)の後には、対象の行の QModelIndex を再取得するようにしてください。


QTreeView::setRowHidden() は、QTreeView ウィジェットの特定の行を表示したり非表示にしたりするために使用されます。ここでは、いくつかの具体的なシナリオでの使用例を紹介します。

準備: 基本的なツリービューの設定

まず、QTreeViewQStandardItemModel を使った基本的なセットアップが必要です。

#include <QApplication>
#include <QMainWindow>
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QVBoxLayout>
#include <QWidget>
#include <QPushButton> // ボタンを追加するために必要
#include <QLineEdit>   // 検索フィールドを追加するために必要
#include <QLabel>      // ラベルを追加するために必要

// ヘルパー関数: モデルにデータを追加
void addItems(QStandardItemModel* model) {
    QStandardItem *rootItem = model->invisibleRootItem();

    // トップレベルアイテム
    QStandardItem *fruitItem = new QStandardItem("フルーツ");
    QStandardItem *vegetableItem = new QStandardItem("野菜");
    QStandardItem *drinkItem = new QStandardItem("飲み物");

    rootItem->appendRow(fruitItem);
    rootItem->appendRow(vegetableItem);
    rootItem->appendRow(drinkItem);

    // フルーツの子アイテム
    fruitItem->appendRow(new QStandardItem("リンゴ"));
    fruitItem->appendRow(new QStandardItem("バナナ"));
    fruitItem->appendRow(new QStandardItem("オレンジ"));
    fruitItem->appendRow(new QStandardItem("ブドウ"));

    // 野菜の子アイテム
    vegetableItem->appendRow(new QStandardItem("トマト"));
    vegetableItem->appendRow(new QStandardItem("ジャガイモ"));
    vegetableItem->appendRow(new QStandardItem("ニンジン"));
    vegetableItem->appendRow(new QStandardItem("キュウリ"));

    // 飲み物の子アイテム
    drinkItem->appendRow(new QStandardItem("水"));
    drinkItem->appendRow(new QStandardItem("ジュース"));
    drinkItem->appendRow(new QStandardItem("コーヒー"));
}

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    QMainWindow window;
    window.setWindowTitle("QTreeView::setRowHidden() の例");

    // モデルの作成
    QStandardItemModel model(0, 1);
    model.setHeaderData(0, Qt::Horizontal, "アイテム名");
    addItems(&model); // ヘルパー関数でデータを追加

    // QTreeView の作成とモデルの設定
    QTreeView treeView;
    treeView.setModel(&model);
    treeView.expandAll(); // 最初はすべて展開しておく

    // レイアウトの準備
    QWidget *centralWidget = new QWidget(&window);
    QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
    mainLayout->addWidget(&treeView);

    window.setCentralWidget(centralWidget);
    window.resize(400, 300);
    window.show();

    return a.exec();
}

上記のコードは、基本的なツリービューを表示します。ここから、setRowHidden() の具体的な使用例を見ていきます。

例1: 特定のトップレベルの行を非表示にする

この例では、「野菜」というトップレベルの行を非表示にします。

// main関数の中に追加
// 例1: トップレベルの「野菜」行を非表示にする
// "野菜" はルートアイテム (QModelIndex()) の下にある2番目の行 (インデックス1)
// QModelIndex() はルートの親を示す
treeView.setRowHidden(1, QModelIndex(), true);

// 「フルーツ」の行も非表示にする場合 (インデックス0)
// treeView.setRowHidden(0, QModelIndex(), true);

例2: 特定の子アイテムの行を非表示にする

この例では、「野菜」の下にある「ジャガイモ」の行を非表示にします。

// main関数の中に追加
// 例2: 「野菜」の下にある「ジャガイモ」を非表示にする
// まず「野菜」アイテムの QModelIndex を取得する必要がある
QStandardItem *vegetableItem = static_cast<QStandardItem*>(model.item(1)); // 2番目のトップレベルアイテム

if (vegetableItem) {
    // 「ジャガイモ」は「野菜」アイテムの2番目の子 (インデックス1)
    treeView.setRowHidden(1, vegetableItem->index(), true);
}

// 例: 「フルーツ」の下にある「バナナ」を非表示にする場合
// QStandardItem *fruitItem = static_cast<QStandardItem*>(model.item(0)); // 1番目のトップレベルアイテム
// if (fruitItem) {
//     treeView.setRowHidden(1, fruitItem->index(), true); // 「バナナ」は「フルーツ」の2番目の子 (インデックス1)
// }

例3: ボタンクリックで行の表示/非表示を切り替える

ユーザーがボタンをクリックすることで、特定の行の表示状態を切り替える例です。

// main関数内のQTreeViewとモデル設定の後に追加

QPushButton *toggleButton = new QPushButton("「バナナ」の表示を切り替え");
mainLayout->addWidget(toggleButton);

// 「フルーツ」アイテムへのポインタを保持
QStandardItem *fruitItemForToggle = static_cast<QStandardItem*>(model.item(0));
Q_ASSERT(fruitItemForToggle); // エラーチェック

QObject::connect(toggleButton, &QPushButton::clicked, [&]() {
    // 「バナナ」のインデックス (fruitItemForToggle の2番目の子)
    int bananaRowIndex = 1;
    QModelIndex bananaParentIndex = fruitItemForToggle->index();

    // 現在の表示状態を取得し、反転させる
    bool isCurrentlyHidden = treeView.isRowHidden(bananaRowIndex, bananaParentIndex);
    treeView.setRowHidden(bananaRowIndex, bananaParentIndex, !isCurrentlyHidden);

    if (isCurrentlyHidden) {
        qDebug() << "「バナナ」を表示しました。";
    } else {
        qDebug() << "「バナナ」を非表示にしました。";
    }
});

例4: 検索機能で一致する行のみを表示する

QLineEdit を使った検索フィールドを設け、入力されたテキストに一致する行だけを表示し、他を非表示にする例です。これは setRowHidden() の一般的なユースケースです。

// main関数内のQTreeViewとモデル設定の後に追加

QLabel *searchLabel = new QLabel("検索:");
QLineEdit *searchLineEdit = new QLineEdit();
QPushButton *searchButton = new QPushButton("検索");
QPushButton *showAllButton = new QPushButton("すべて表示");

QHBoxLayout *searchLayout = new QHBoxLayout();
searchLayout->addWidget(searchLabel);
searchLayout->addWidget(searchLineEdit);
searchLayout->addWidget(searchButton);
searchLayout->addWidget(showAllButton);
mainLayout->addLayout(searchLayout); // メインレイアウトに追加

// 検索ロジック
auto filterRows = [&](const QString& searchText) {
    // まず、すべての行を表示状態に戻す(重要!)
    // QTreeView を直接操作するので、階層を再帰的に巡回する必要がある
    std::function<void(const QModelIndex&)> processNode =
        [&](const QModelIndex& parentIndex) {
        int rowCount = model.rowCount(parentIndex);
        for (int i = 0; i < rowCount; ++i) {
            QModelIndex currentIndex = model.index(i, 0, parentIndex); // 各行の最初の列を使用
            treeView.setRowHidden(i, parentIndex, false); // まず表示状態に戻す
            if (model.hasChildren(currentIndex)) {
                processNode(currentIndex); // 子を持つ場合は再帰的に処理
            }
        }
    };
    processNode(QModelIndex()); // ルートから開始

    if (searchText.isEmpty()) {
        // 検索文字列が空の場合は、すべて表示するだけで終了
        return;
    }

    // 検索文字列に一致しない行を非表示にする
    std::function<bool(const QModelIndex&)> hideNonMatching =
        [&](const QModelIndex& parentIndex) -> bool {
        bool anyChildVisible = false;
        int rowCount = model.rowCount(parentIndex);
        for (int i = 0; i < rowCount; ++i) {
            QModelIndex currentIndex = model.index(i, 0, parentIndex);
            QString itemText = model.data(currentIndex, Qt::DisplayRole).toString();

            bool currentItemMatches = itemText.contains(searchText, Qt::CaseInsensitive);
            bool childrenHaveVisibleItems = false;

            if (model.hasChildren(currentIndex)) {
                // 子アイテムがマッチするか、子孫にマッチするアイテムがあるかチェック
                childrenHaveVisibleItems = hideNonMatching(currentIndex);
            }

            // 現在のアイテム自体がマッチするか、その子孫にマッチするアイテムがある場合、表示
            if (currentItemMatches || childrenHaveVisibleItems) {
                treeView.setRowHidden(i, parentIndex, false); // 表示する
                anyChildVisible = true; // 親は少なくとも1つの表示可能な子を持つ
            } else {
                treeView.setRowHidden(i, parentIndex, true); // 非表示にする
            }
        }
        return anyChildVisible;
    };
    hideNonMatching(QModelIndex()); // ルートから開始

    // 非表示にした後に必要に応じて展開を調整
    // 例: 検索結果が見つかったパスを展開する
    // これはもう少し複雑になるため、ここでは省略します。
    // 一般的には、hideNonMatching関数の結果に基づいて親をexpandする必要があります。
    treeView.expandAll(); // 簡単な例として、すべて展開し直す
};

QObject::connect(searchButton, &QPushButton::clicked, [&]() {
    filterRows(searchLineEdit->text());
});

QObject::connect(searchLineEdit, &QLineEdit::returnPressed, [&]() {
    filterRows(searchLineEdit->text());
});

QObject::connect(showAllButton, &QPushButton::clicked, [&]() {
    filterRows(""); // 空文字列を渡してすべて表示
    searchLineEdit->clear(); // 検索フィールドをクリア
});
  • フィルタリングの代替
    複雑なフィルタリングや、モデルレベルでのデータのフィルタリングを目的とする場合、QSortFilterProxyModel を使用する方がより推奨されるアプローチです。QSortFilterProxyModel は、ソースモデルをラップし、ビューに表示するデータをフィルタリングする機能を提供します。これにより、大規模なデータセットでもパフォーマンスが向上し、より柔軟なフィルタリングロジックを実装できます。setRowHidden() は、シンプルな一時的な表示/非表示の切り替えに適しています。
  • 再帰的な処理
    階層構造全体を処理する場合(例: 検索機能)、setRowHidden() を再帰的に呼び出すロジックが必要になります。
  • parent 引数
    setRowHidden()parent 引数は非常に重要です。最上位のアイテムの場合は QModelIndex() を渡し、子アイテムの場合はその親の QModelIndex を渡す必要があります。


主に以下の2つの方法が setRowHidden() の代替として考えられます。

  1. QSortFilterProxyModel の使用
    これは、データのフィルタリングやソートを行うためのQtの標準的なアプローチであり、最も推奨される代替方法です。

  2. カスタム QAbstractItemModel の実装
    より高度なケースで、モデル自体がフィルタリングロジックを持つように設計する場合。

QSortFilterProxyModel の使用(最も推奨される方法)

QSortFilterProxyModel は、元のデータモデル(ソースモデル)とビュー(QTreeView など)の間に位置する「プロキシ」モデルです。このプロキシモデルは、ソースモデルからデータを受け取り、フィルタリングやソートを適用した後に、その結果をビューに提示します。

利点

  • 再利用性
    フィルタリングロジックをプロキシモデル内にカプセル化できるため、複数のビューで同じフィルタリングを適用できます。
  • ソート機能との統合
    フィルタリングと同時にソートも簡単に実現できます。
  • パフォーマンス
    大量のデータを扱う場合、ビューの再描画ごとに setRowHidden() を繰り返し呼び出すよりも効率的です。
  • 堅牢なフィルタリング
    setRowHidden() のようにビューの状態に依存せず、モデルレベルでフィルタリングが行われるため、スクロールや展開・折りたたみによって非表示にしたはずの行が一時的に表示される、といった問題が起こりません。
  • MVC(Model-View-Controller)原則の遵守
    データ(モデル)、表示(ビュー)、操作(コントローラー)が明確に分離され、コードの保守性が向上します。

使用方法

  1. QSortFilterProxyModel のインスタンスを作成します。
  2. setSourceModel() メソッドで、実際にデータを持つモデル(例: QStandardItemModel)を設定します。
  3. QTreeViewsetSourceModel() で設定したモデルではなく、この QSortFilterProxyModel のインスタンスを設定します。
  4. フィルタリングロジックを実装するために、filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const メソッドをオーバーライドします。このメソッドは、指定された行を表示するかどうかを true/false で返します。
  5. フィルタリング条件が変更されたら、invalidateFilter() を呼び出してプロキシモデルにフィルタを再適用するよう指示します。

コード例

#include <QApplication>
#include <QMainWindow>
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QVBoxLayout>
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QSortFilterProxyModel> // これが必要

// カスタムのプロキシモデルを定義
class MyFilterProxyModel : public QSortFilterProxyModel {
public:
    explicit MyFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {}

    // フィルタリング文字列を設定
    void setFilterString(const QString &text) {
        m_filterString = text;
        invalidateFilter(); // フィルタを再適用する
    }

protected:
    // このメソッドをオーバーライドしてフィルタリングロジックを実装
    bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override {
        // 親モデルのデータを取得
        QModelIndex index = sourceModel()->index(sourceRow, filterKeyColumn(), sourceParent);
        QString itemText = sourceModel()->data(index, Qt::DisplayRole).toString();

        // 検索文字列が空の場合はすべて表示
        if (m_filterString.isEmpty()) {
            return true;
        }

        // 現在のアイテムがフィルタ文字列を含む場合、表示
        if (itemText.contains(m_filterString, Qt::CaseInsensitive)) {
            return true;
        }

        // 子要素の中に表示すべきアイテムがあるかチェック(階層フィルタリング)
        if (sourceModel()->hasChildren(index)) {
            int childCount = sourceModel()->rowCount(index);
            for (int i = 0; i < childCount; ++i) {
                if (filterAcceptsRow(i, index)) { // 再帰的に子をチェック
                    return true; // 子がマッチすれば親も表示
                }
            }
        }
        return false; // 現在のアイテムも子もマッチしない場合は非表示
    }

private:
    QString m_filterString;
};

// ヘルパー関数: モデルにデータを追加 (前述の例と同じ)
void addItems(QStandardItemModel* model) {
    QStandardItem *rootItem = model->invisibleRootItem();
    QStandardItem *fruitItem = new QStandardItem("フルーツ");
    QStandardItem *vegetableItem = new QStandardItem("野菜");
    QStandardItem *drinkItem = new QStandardItem("飲み物");
    rootItem->appendRow(fruitItem);
    rootItem->appendRow(vegetableItem);
    rootItem->appendRow(drinkItem);

    fruitItem->appendRow(new QStandardItem("リンゴ"));
    fruitItem->appendRow(new QStandardItem("バナナ"));
    fruitItem->appendRow(new QStandardItem("オレンジ"));
    fruitItem->appendRow(new QStandardItem("ブドウ"));

    vegetableItem->appendRow(new QStandardItem("トマト"));
    vegetableItem->appendRow(new QStandardItem("ジャガイモ"));
    vegetableItem->appendRow(new QStandardItem("ニンジン"));
    vegetableItem->appendRow(new QStandardItem("キュウリ"));

    drinkItem->appendRow(new QStandardItem("水"));
    drinkItem->appendRow(new QStandardItem("ジュース"));
    drinkItem->appendRow(new QStandardItem("コーヒー"));
}

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    QMainWindow window;
    window.setWindowTitle("QSortFilterProxyModel の例");

    // ソースモデルの作成
    QStandardItemModel sourceModel(0, 1);
    sourceModel.setHeaderData(0, Qt::Horizontal, "アイテム名");
    addItems(&sourceModel);

    // プロキシモデルの作成とソースモデルの設定
    MyFilterProxyModel proxyModel;
    proxyModel.setSourceModel(&sourceModel);
    // filterKeyColumn() はデフォルトで0なので、最初の列をフィルタ対象とする
    // proxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive); // 大文字小文字を区別しない

    // QTreeView の作成とプロキシモデルの設定
    QTreeView treeView;
    treeView.setModel(&proxyModel);
    treeView.expandAll();

    // 検索UIの追加
    QLineEdit *searchLineEdit = new QLineEdit();
    QPushButton *searchButton = new QPushButton("検索");
    QPushButton *showAllButton = new QPushButton("すべて表示");

    QHBoxLayout *searchLayout = new QHBoxLayout();
    searchLayout->addWidget(new QLabel("検索:"));
    searchLayout->addWidget(searchLineEdit);
    searchLayout->addWidget(searchButton);
    searchLayout->addWidget(showAllButton);

    QVBoxLayout *mainLayout = new QVBoxLayout();
    mainLayout->addLayout(searchLayout);
    mainLayout->addWidget(&treeView);

    QWidget *centralWidget = new QWidget(&window);
    centralWidget->setLayout(mainLayout);
    window.setCentralWidget(centralWidget);
    window.resize(400, 300);
    window.show();

    // 接続: 検索ボタンとQLineEditの変更
    QObject::connect(searchButton, &QPushButton::clicked, [&]() {
        proxyModel.setFilterString(searchLineEdit->text());
        treeView.expandAll(); // フィルタリング後にツリーを展開
    });
    QObject::connect(searchLineEdit, &QLineEdit::returnPressed, [&]() {
        proxyModel.setFilterString(searchLineEdit->text());
        treeView.expandAll();
    });
    QObject::connect(showAllButton, &QPushButton::clicked, [&]() {
        proxyModel.setFilterString(""); // 空文字列でフィルタを解除
        searchLineEdit->clear();
        treeView.expandAll();
    });

    return a.exec();
}

カスタム QAbstractItemModel の実装

非常に特殊なデータ構造やフィルタリング要件がある場合、または QSortFilterProxyModel が提供する以上の制御が必要な場合は、QAbstractItemModel を直接サブクラス化し、モデル自体がフィルタリングロジックを持つように設計できます。

利点

  • 最高のパフォーマンス(理論上)
    モデルの内部構造に合わせて最適化されたフィルタリングを実装できます。
  • 完全な制御
    データモデルの動作を完全にカスタマイズできます。

欠点

  • 再利用性の低下
    特定のフィルタリングロジックがモデルに組み込まれるため、他のビューや異なるフィルタリング要件で再利用することが難しくなる場合があります。
  • 複雑性
    QAbstractItemModel を完全に実装することは、特に階層モデルの場合、非常に複雑で時間がかかります。data(), index(), parent(), rowCount(), columnCount() などの多数の仮想関数を正しく実装する必要があります。
  1. QAbstractItemModel を継承する新しいクラスを作成します。
  2. data(), index(), parent(), rowCount(), columnCount() などの純粋仮想関数を実装します。
  3. フィルタリングロジックを、これらの関数、特に rowCount()data() の中で、特定の条件に基づいてデータを「提供しない」形で実装します。
  4. フィルタリング条件が変更されたら、beginResetModel()endResetModel() を呼び出して、ビューにモデルが完全にリセットされたことを通知します。

コード例(概念のみ - 完全な実装は非常に複雑になります)

// これは完全なコードではありません。概念を示すためのものです。
class CustomFilteredModel : public QAbstractItemModel {
    Q_OBJECT
public:
    explicit CustomFilteredModel(QObject *parent = nullptr) : QAbstractItemModel(parent) {}

    void setFilterString(const QString &text) {
        // モデルのリセットを開始
        beginResetModel();
        m_filterString = text;
        // モデルのリセットを終了
        endResetModel();
        // モデルが変更されたことをビューに通知し、再描画を促す
    }

    // QAbstractItemModel の純粋仮想関数を実装
    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override {
        // フィルタリングロジックに基づいて有効なインデックスを返す
        // 非表示にするべきアイテムは、この段階でインデックスを返さないか、
        // あるいはrowCount()で数を減らすなどの工夫が必要
        // 実際には、内部データ構造でフィルタリングされたリストを保持する必要がある
        return QModelIndex(); // ダミー
    }

    QModelIndex parent(const QModelIndex &child) const override {
        // 親インデックスを返す
        return QModelIndex(); // ダミー
    }

    int rowCount(const QModelIndex &parent = QModelIndex()) const override {
        // フィルタリングされた後の行数を返す
        // ここでフィルタリングロジックを適用し、表示する行数だけを数える
        return 0; // ダミー
    }

    int columnCount(const QModelIndex &parent = QModelIndex()) const override {
        return 1; // 1列のみ
    }

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
        // フィルタリングされた後のデータを提供する
        // 非表示にするべきアイテムのデータは提供しない
        return QVariant(); // ダミー
    }

private:
    QString m_filterString;
    // 内部的に、フィルタリングされたデータを保持するリストや構造体が必要になる
    // 例: QList<QStandardItem*> のようなもの
};

// main関数では、このカスタムモデルをQTreeViewに設定する
// QTreeView view;
// CustomFilteredModel *model = new CustomFilteredModel(&view);
// view.setModel(model);
// model->setFilterString("検索文字列");