QTreeView collapse()の代替手段:Qtでツリー表示を柔軟に制御するテクニック

2025-05-16

QTreeView は、階層的なデータをツリー構造で表示するためのQtウィジェットです。ファイルシステムのエクスプローラーのように、フォルダを開いたり閉じたりするような操作が可能です。

void QTreeView::collapse(const QModelIndex &index) は、QTreeView クラスのpublic slot(スロット)です。この関数は、引数で指定された QModelIndex に対応するアイテム(ノード)を「折りたたむ」ために使用されます。

具体的には、以下のことを行います。

  • アイコンの変更
    通常、展開されているアイテムの横には、展開/折りたたみを切り替えるためのアイコン(例えば、三角形の矢印やプラス記号など)が表示されます。collapse() が呼び出されると、このアイコンが折りたたまれた状態を示すものに変わります。
  • 子の非表示化
    指定された index のアイテムが展開されていた場合、その直下のすべての子アイテムを非表示にします。その子アイテムがさらに子を持つ場合でも、それらもすべて非表示になります。

使用例

例えば、ユーザーがツリービューの特定のノードをクリックしたときに、そのノードが展開されていれば折りたたみ、折りたたまれていれば展開するといった機能を実装する場合に利用できます。

// model は QAbstractItemModel の派生クラスのインスタンス
// treeView は QTreeView のインスタンス

// 特定のインデックスを持つアイテムを折りたたむ
QModelIndex itemIndex = model->index(0, 0); // 最初のトップレベルアイテムのインデックス
treeView->collapse(itemIndex);

// または、現在選択されているアイテムを折りたたむ場合
QModelIndex currentIndex = treeView->currentIndex();
if (currentIndex.isValid()) {
    treeView->collapse(currentIndex);
}
  • void QTreeView::collapsed(const QModelIndex &index) (シグナル): アイテムが折りたたまれたときに発行されるシグナルです。
  • void QTreeView::expandAll(): ツリービュー内のすべての折りたたまれたアイテムを展開します。
  • void QTreeView::collapseAll(): ツリービュー内のすべての展開されたアイテムを折りたたみます。
  • void QTreeView::expand(const QModelIndex &index): 指定されたアイテムを展開します。


QModelIndex の無効性 (Invalid QModelIndex)

問題
collapse() に渡す QModelIndex が無効な場合、何も起こらないか、意図しない動作をすることがあります。無効なインデックスとは、モデル内に存在しない、または現在表示されていないアイテムを指す場合です。

原因

  • QSortFilterProxyModel などを使用している場合、元のモデルのインデックスとプロキシモデルのインデックスを混同している。
  • 存在しない行や列のインデックスを指定している。
  • モデルのデータが変更されたにもかかわらず、古い QModelIndex を使用している。

トラブルシューティング

  • QSortFilterProxyModel を使用している場合は、mapFromSource()mapToSource() を適切に使用して、モデル間のインデックス変換が正しく行われていることを確認してください。
  • QAbstractItemModel::index()QModelIndex::child() などでインデックスを取得する際に、返り値が有効であることを確認するデバッグログを追加します。
  • QModelIndex::isValid() を使用して、collapse() を呼び出す前にインデックスが有効であることを確認してください。

モデルの変更通知の不足

問題
モデルのデータが変更されたにもかかわらず、QTreeView がその変更を認識せず、ツリービューの表示が更新されないことがあります。この場合、collapse() を呼び出しても見た目に変化がないことがあります。

原因

  • QTreeView がモデルのシグナルに正しく接続されていない。
  • QAbstractItemModel の派生クラスを使用している場合、データの変更があったときに適切なシグナル(例: dataChanged()rowsInserted()rowsRemoved() など)を発行していない。

トラブルシューティング

  • QTreeView がモデルに正しく設定されているか (setModel()) 確認します。
  • カスタムモデルを使用している場合は、データが変更されたときに必ず適切なシグナルを発行するようにしてください。特に、beginInsertRows() / endInsertRows()beginRemoveRows() / endRemoveRows() の呼び出し忘れはよくある間違いです。

パフォーマンスの問題(多数のアイテムを一度に操作する場合)

問題
非常に多くのアイテムを持つツリービューで、ループ内で collapse()expand() を繰り返し呼び出すと、アプリケーションが一時的にフリーズしたり、動作が遅くなったりすることがあります。

原因

  • collapse() 呼び出しごとに QTreeView が再描画処理を行うため、多数のアイテムに対して実行するとオーバーヘッドが大きくなる。

トラブルシューティング

  • collapseAll() / expandAll() の利用
    すべてのアイテムを折りたたみ/展開したい場合は、collapseAll()expandAll() の使用を検討してください。これらは内部的に最適化されている可能性があります。
  • 遅延処理
    必要に応じて、QTimer を使用して、数フレームごとに少しずつ collapse() を実行するように処理を分割することも検討できます。
  • QTreeView::setUpdatesEnabled(false) の利用
    多数の collapse() / expand() 操作を行う前に treeView->setUpdatesEnabled(false) を呼び出し、操作が完了した後に treeView->setUpdatesEnabled(true) を呼び出すことで、中間的な再描画を抑制し、パフォーマンスを向上させることができます。

展開/折りたたみアイコンの表示問題

問題
アイテムが子を持っているにもかかわらず、展開/折りたたみを示すアイコン(デコレーション)が表示されないことがあります。これにより、ユーザーはアイテムが折りたたみ可能であると認識できません。

原因

  • QTreeView::itemsExpandable プロパティが false に設定されている(アイテムが展開可能でない場合)。
  • QTreeView::rootIsDecorated プロパティが false に設定されている(ルートアイテムのデコレーションを表示しない場合)。
  • モデルの hasChildren() メソッドが正しく true を返していない。

トラブルシューティング

  • QTreeView::setRootIsDecorated(true)QTreeView::setItemsExpandable(true) が設定されていることを確認します。
  • カスタムモデルを使用している場合、子を持つべきアイテムに対して QAbstractItemModel::hasChildren()true を返すことを確認します。

問題
モデルの構造(行の挿入や削除など)が頻繁に変更される場合、以前に取得した QModelIndex が無効になることがあります。その無効なインデックスを使って collapse() を呼び出すと、予期しない動作になるか、何も起こりません。

原因

  • QModelIndex は、モデル内のデータ構造への一時的なポインタのようなものです。モデルの構造が変わると、同じインデックスが別のアイテムを指すようになったり、無効になったりします。
  • 必要な時にインデックスを再取得
    collapse() を呼び出す直前に、モデルから最新の有効な QModelIndex を再取得するようにします。
  • QPersistentModelIndex の使用
    モデルの構造変更後もインデックスを維持したい場合は、QPersistentModelIndex を使用します。これは、モデルが構造変更の通知(シグナル)を発行することで、自身のインデックスを自動的に更新する機能を持っています。ただし、QPersistentModelIndex も大量に作成するとパフォーマンスに影響を与える可能性があるため、必要な場合のみ使用してください。


例1: 特定のアイテムをプログラムで折りたたむ

この例では、QStandardItemModel を使用して簡単なツリー構造を作成し、特定のアイテムをプログラムで折りたたみます。

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug> // デバッグ出力用

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

    // QStandardItemModel の作成
    QStandardItemModel model;

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

    // 親アイテム (フォルダ1)
    QStandardItem *folder1 = new QStandardItem("Folder 1");
    rootItem->appendRow(folder1);

    // 子アイテム (ファイル1.1, ファイル1.2)
    folder1->appendRow(new QStandardItem("File 1.1"));
    folder1->appendRow(new QStandardItem("File 1.2"));

    // 親アイテム (フォルダ2)
    QStandardItem *folder2 = new QStandardItem("Folder 2");
    rootItem->appendRow(folder2);

    // 子アイテム (ファイル2.1)
    QStandardItem *file2_1 = new QStandardItem("File 2.1");
    folder2->appendRow(file2_1);

    // 孫アイテム (サブフォルダA)
    QStandardItem *subFolderA = new QStandardItem("SubFolder A");
    file2_1->appendRow(subFolderA);
    subFolderA->appendRow(new QStandardItem("SubFile A.1"));

    // QTreeView の作成とモデルの設定
    QTreeView treeView;
    treeView.setModel(&model);

    // 初期状態ではすべて展開しておく
    treeView.expandAll();
    qDebug() << "Initial state: All expanded.";

    // 特定のアイテムを折りたたむボタン
    QPushButton *collapseButton = new QPushButton("Collapse 'Folder 2'");
    QObject::connect(collapseButton, &QPushButton::clicked, [&]() {
        // "Folder 2" のインデックスを取得
        // QStandardItemModel の場合、テキストで検索するのが簡単
        QList<QStandardItem*> items = model.findItems("Folder 2", Qt::MatchExactly, 0);
        if (!items.isEmpty()) {
            QStandardItem *itemToCollapse = items.first();
            QModelIndex indexToCollapse = model.indexFromItem(itemToCollapse);

            if (indexToCollapse.isValid()) {
                treeView.collapse(indexToCollapse);
                qDebug() << "'Folder 2' collapsed.";
            } else {
                qDebug() << "Invalid index for 'Folder 2'.";
            }
        } else {
            qDebug() << "'Folder 2' not found.";
        }
    });

    // レイアウトの設定
    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(&treeView);
    layout->addWidget(collapseButton);

    QWidget window;
    window.setLayout(layout);
    window.setWindowTitle("QTreeView Collapse Example 1");
    window.resize(400, 300);
    window.show();

    return a.exec();
}

解説

  1. QStandardItemModel を使用して階層的なデータを作成します。Folder 1Folder 2 という親アイテム、そしてその中に子アイテム、さらに孫アイテムも作成しています。
  2. QTreeView を作成し、そのモデルとして model を設定します。
  3. treeView.expandAll() で、最初はすべてのアイテムを展開した状態で表示します。
  4. QPushButton をクリックすると、"Folder 2" というテキストを持つアイテムをモデルから検索し、その QStandardItem から QModelIndex を取得します。
  5. 取得した QModelIndextreeView.collapse() に渡すことで、"Folder 2" アイテムが折りたたまれ、その子孫アイテムが非表示になります。

例2: ユーザーがクリックしたアイテムを展開/折りたたむトグル機能

この例では、ユーザーがツリービューのアイテムをクリックしたときに、そのアイテムが展開されていれば折りたたみ、折りたたまれていれば展開する機能(トグル)を実装します。

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug>

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

    QStandardItemModel model;
    QStandardItem *rootItem = model.invisibleRootItem();

    QStandardItem *folder1 = new QStandardItem("Folder A");
    rootItem->appendRow(folder1);
    folder1->appendRow(new QStandardItem("File A.1"));
    folder1->appendRow(new QStandardItem("File A.2"));

    QStandardItem *folder2 = new QStandardItem("Folder B");
    rootItem->appendRow(folder2);
    folder2->appendRow(new QStandardItem("File B.1"));
    QStandardItem *subFolderB = new QStandardItem("SubFolder B.1");
    folder2->appendRow(subFolderB);
    subFolderB->appendRow(new QStandardItem("SubFile B.1.1"));

    QTreeView treeView;
    treeView.setModel(&model);
    treeView.expandAll(); // 初期状態ではすべて展開

    // QTreeView の clicked シグナルにスロットを接続
    QObject::connect(&treeView, &QTreeView::clicked, [&](const QModelIndex &index) {
        if (!index.isValid()) {
            return; // 無効なインデックスは無視
        }

        // アイテムが展開されているかどうかをチェック
        if (treeView.isExpanded(index)) {
            treeView.collapse(index);
            qDebug() << "Collapsed:" << model.data(index).toString();
        } else {
            treeView.expand(index);
            qDebug() << "Expanded:" << model.data(index).toString();
        }
    });

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(&treeView);

    QWidget window;
    window.setLayout(layout);
    window.setWindowTitle("QTreeView Toggle Collapse/Expand Example");
    window.resize(400, 300);
    window.show();

    return a.exec();
}

解説

  1. 前述と同様に、ツリービューとモデルを設定します。
  2. QTreeView::clicked(const QModelIndex &index) シグナルを独自のスロットに接続します。このシグナルは、ツリービュー内のアイテムがクリックされたときに、そのアイテムの QModelIndex を引数として発行されます。
  3. スロット内で、treeView.isExpanded(index) を使用して、クリックされたアイテムが現在展開されているかどうかを確認します。
  4. もし展開されていれば treeView.collapse(index) を呼び出して折りたたみ、そうでなければ treeView.expand(index) を呼び出して展開します。

大量のアイテムをプログラムで折りたたみ/展開する場合、setUpdatesEnabled(false) を使用して再描画を一時的に停止することで、パフォーマンスを向上させることができます。

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug>
#include <QElapsedTimer> // 処理時間計測用

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

    QStandardItemModel model;
    QStandardItem *rootItem = model.invisibleRootItem();

    // 大量のデータを生成
    const int numFolders = 1000;
    const int numFilesPerFolder = 5;

    for (int i = 0; i < numFolders; ++i) {
        QStandardItem *folder = new QStandardItem(QString("Folder %1").arg(i + 1));
        rootItem->appendRow(folder);
        for (int j = 0; j < numFilesPerFolder; ++j) {
            folder->appendRow(new QStandardItem(QString("File %1.%2").arg(i + 1).arg(j + 1)));
        }
    }

    QTreeView treeView;
    treeView.setModel(&model);
    treeView.expandAll(); // すべて展開

    QPushButton *collapseAllButton = new QPushButton("Collapse All (Optimized)");
    QObject::connect(collapseAllButton, &QPushButton::clicked, [&]() {
        QElapsedTimer timer;
        timer.start();

        // 描画更新を一時的に無効化
        treeView.setUpdatesEnabled(false);

        // すべてのトップレベルアイテムを折りたたむ
        for (int i = 0; i < model.rowCount(QModelIndex()); ++i) {
            QModelIndex index = model.index(i, 0, QModelIndex());
            if (index.isValid()) {
                treeView.collapse(index);
            }
        }

        // 描画更新を再度有効化
        treeView.setUpdatesEnabled(true);

        qDebug() << "Collapsed all items in" << timer.elapsed() << "ms (optimized).";
    });

    QPushButton *collapseAllUnoptimizedButton = new QPushButton("Collapse All (Unoptimized)");
    QObject::connect(collapseAllUnoptimizedButton, &QPushButton::clicked, [&]() {
        treeView.expandAll(); // 一度展開し直す
        QElapsedTimer timer;
        timer.start();

        // 描画更新を無効化せずにすべて折りたたむ
        for (int i = 0; i < model.rowCount(QModelIndex()); ++i) {
            QModelIndex index = model.index(i, 0, QModelIndex());
            if (index.isValid()) {
                treeView.collapse(index);
            }
        }
        qDebug() << "Collapsed all items in" << timer.elapsed() << "ms (unoptimized).";
    });

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(&treeView);
    layout->addWidget(collapseAllButton);
    layout->addWidget(collapseAllUnoptimizedButton);

    QWidget window;
    window.setLayout(layout);
    window.setWindowTitle("QTreeView Performance Example");
    window.resize(600, 400);
    window.show();

    return a.exec();
}

解説

  1. 大量のフォルダとファイルを生成し、ツリービューに表示します。
  2. "Collapse All (Optimized)" ボタンは、treeView.setUpdatesEnabled(false) で更新を一時停止してから、ループで各トップレベルアイテムを折りたたみます。その後、treeView.setUpdatesEnabled(true) で更新を再開します。これにより、すべての操作が完了した後に一度だけ再描画が行われるため、処理が高速になります。
  3. "Collapse All (Unoptimized)" ボタンは、setUpdatesEnabled() を使用せずに、ループ内で直接 collapse() を呼び出します。これにより、各 collapse() 呼び出しごとに再描画が行われ、処理が遅くなることを確認できます。

この例を実行すると、最適化された方法が非常に高速であることがわかるはずです。



QTreeView::collapseAll() を使用する

これは collapse() の最も直接的な代替であり、すべての展開されたアイテムを一度に折りたたむ場合に非常に便利です。

目的
ツリービュー全体を初期状態(ルートアイテムのみ表示)に戻したい場合や、すべての詳細を非表示にしたい場合。

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>

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

    QStandardItemModel model;
    // ... (モデルの構築は前述の例と同様)
    QStandardItem *rootItem = model.invisibleRootItem();
    QStandardItem *folder1 = new QStandardItem("Folder 1");
    rootItem->appendRow(folder1);
    folder1->appendRow(new QStandardItem("File 1.1"));
    folder1->appendRow(new QStandardItem("File 1.2"));
    QStandardItem *folder2 = new QStandardItem("Folder 2");
    rootItem->appendRow(folder2);
    folder2->appendRow(new QStandardItem("File 2.1"));
    QStandardItem *subFolderA = new QStandardItem("SubFolder A");
    folder2->appendRow(subFolderA);
    subFolderA->appendRow(new QStandardItem("SubFile A.1"));

    QTreeView treeView;
    treeView.setModel(&model);
    treeView.expandAll(); // 最初はすべて展開

    QPushButton *collapseAllButton = new QPushButton("Collapse All");
    QObject::connect(collapseAllButton, &QPushButton::clicked, [&]() {
        treeView.collapseAll(); // すべてのアイテムを折りたたむ
        qDebug() << "All items collapsed.";
    });

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(&treeView);
    layout->addWidget(collapseAllButton);

    QWidget window;
    window.setLayout(layout);
    window.setWindowTitle("QTreeView collapseAll() Example");
    window.resize(400, 300);
    window.show();

    return a.exec();
}

解説

  • 個々のアイテムをループして collapse() を呼び出すよりも効率的です。
  • treeView.collapseAll() を呼び出すだけで、ツリービュー内のすべての展開されたノードが折りたたまれ、ルートアイテム(またはrootIsDecoratedfalseの場合はルートアイテムの子)のみが表示されます。

QAbstractItemModel::hasChildren() を利用して表示を制御する(間接的な方法)

これは collapse() の直接的な代替ではありませんが、そもそも「展開可能ではない」アイテムとして表示することで、ユーザーが展開しようとしても何も起こらないように制御できます。

目的
特定の条件に基づいて、アイテムが子を持つかのように見せかけるが、実際には展開できないようにしたい場合。あるいは、子がない場合は展開アイコンを表示したくない場合。

// カスタムモデルの hasChildren() メソッドの例
bool MyCustomModel::hasChildren(const QModelIndex &parent) const {
    // 特定の条件に基づいて、子が存在しないかのように振る舞う
    if (parent.isValid() && data(parent, Qt::DisplayRole).toString() == "Pseudo-Folder") {
        return false; // "Pseudo-Folder" という名前のアイテムは子を持たないと報告する
    }
    // 通常のロジックで子があるかを判断
    return getItem(parent)->childCount() > 0;
}

解説

  • これは、UI上での「展開/折りたたみ」の可否をモデル側で制御するアプローチです。
  • もし hasChildren()false を返せば、たとえ実際には子データを持っていても、QTreeView はそのアイテムを展開可能なものとして扱いません。 これにより、ユーザーがクリックしても展開されず、collapse() を呼び出す必要もありません。
  • QTreeView はモデルの hasChildren() メソッドを呼び出して、アイテムの横に展開/折りたたみアイコン(デコレーター)を表示するかどうかを判断します。

QTreeView::setItemsExpandable(bool expandable) を使用する

ツリービュー内のすべてのアイテムの展開/折りたたみ機能を一括で有効/無効にする方法です。

目的
ツリービュー全体として、展開・折りたたみ機能を一時的に無効にしたい場合。

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>

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

    QStandardItemModel model;
    // ... (モデルの構築は前述の例と同様)
    QStandardItem *rootItem = model.invisibleRootItem();
    QStandardItem *folder1 = new QStandardItem("Folder 1");
    rootItem->appendRow(folder1);
    folder1->appendRow(new QStandardItem("File 1.1"));
    folder1->appendRow(new QStandardItem("File 1.2"));
    QStandardItem *folder2 = new QStandardItem("Folder 2");
    rootItem->appendRow(folder2);
    folder2->appendRow(new QStandardItem("File 2.1"));
    QStandardItem *subFolderA = new QStandardItem("SubFolder A");
    folder2->appendRow(subFolderA);
    subFolderA->appendRow(new QStandardItem("SubFile A.1"));

    QTreeView treeView;
    treeView.setModel(&model);
    treeView.expandAll(); // 最初はすべて展開

    QPushButton *toggleExpandableButton = new QPushButton("Toggle Expandable");
    QObject::connect(toggleExpandableButton, &QPushButton::clicked, [&]() {
        bool current = treeView.itemsExpandable();
        treeView.setItemsExpandable(!current); // 展開機能を切り替える
        qDebug() << "Items expandable set to:" << !current;
    });

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(&treeView);
    layout->addWidget(toggleExpandableButton);

    QWidget window;
    window.setLayout(layout);
    window.setWindowTitle("QTreeView setItemsExpandable() Example");
    window.resize(400, 300);
    window.show();

    return a.exec();
}

解説

  • collapse() とは異なり、これは個々のアイテムの状態を変更するのではなく、ツリービュー全体の振る舞いを制御します。
  • treeView.setItemsExpandable(false) を呼び出すと、ユーザーはツリービュー上のどのアイテムも展開できなくなります(アイコンも消えることが多い)。

これは collapse() の本来の目的とは異なりますが、特定の行を完全にツリービューから見えなくするという意味では、似たような結果をもたらすことがあります。ただし、これはツリー構造を維持しつつ「折りたたむ」というよりは、「行を隠す」という全く別の操作です。

目的
ツリーの階層構造を意識せず、特定の行を完全に表示から除外したい場合。

// これは QTreeView::collapse() の代替としては推奨されません
// 特定の行を完全に隠す場合にのみ考慮
// treeView.hideRow(rowIndex, parentIndex);
// treeView.showRow(rowIndex, parentIndex);
  • これは、モデルのデータ自体を変更せずにビューの表示を操作する際に使えますが、ツリー構造の「折りたたみ」とは概念が異なるため、注意が必要です。通常、ツリービューの階層的な表示制御には collapse() / expand() を使うべきです。
  • hideRow() は、指定された QModelIndex の行とそのすべての子孫行をツリービューから完全に非表示にします。collapse() とは異なり、折りたたみアイコンの状態が変わるのではなく、その行自体が消えたように見えます。