Qt QTreeViewの「折りたたみ」を極める:collapsed()シグナルのすべて

2025-05-27

Qt プログラミングにおける void QTreeView::collapsed() は、QTreeView ウィジェットが提供するシグナルの一つです。

QTreeView とは?

まず、QTreeView について簡単に説明します。QTreeView は、Qt のモデル/ビューアーキテクチャの一部であり、階層的なデータをツリー構造として表示するためのウィジェットです。例えば、ファイルエクスプローラーのようなツリー表示を実装する際に使用されます。ツリーの各項目(ノード)は、子項目を持つことができ、それらを「展開(expanded)」したり「折りたたむ(collapsed)」ことができます。

void QTreeView::collapsed() シグナルの意味

void QTreeView::collapsed(const QModelIndex &index) シグナルは、以下の状況で発生します。

  • 引数 index: このシグナルには const QModelIndex &index という引数が渡されます。これは、折りたたまれた項目に対応するモデルインデックスを示します。QModelIndex は、モデル内のデータ項目を一意に識別するためのものです。
  • 何が起こるか: ツリー内の特定の項目が折りたたまれた(非表示になった)ときに、このシグナルが発行されます。

なぜこのシグナルが重要なのか?

この collapsed() シグナルは、ツリーの表示状態の変化に応じて、アプリケーションが特定の処理を行う必要がある場合に非常に役立ちます。例えば:

  • カスタム動作: ツリーの折りたたみ時に、特定のカスタムアニメーションを実行したり、ユーザーに通知したりする。
  • UI の更新: ツリー表示以外の UI 要素が、ツリーの折りたたみ状態に応じて更新される必要がある場合(例:関連する詳細パネルを非表示にする)。
  • データの変更: 項目が折りたたまれた際に、関連するデータをメモリから解放したり、一時的な状態をリセットしたりする。
  • ログ記録: どの項目が折りたたまれたかをログに記録する。

コードでの使用は、C++ のシグナル/スロット接続のメカニズムに従います。

// 例えば、QTreeView のインスタンスがあるとして
QTreeView *treeView = new QTreeView(this);

// 独自のスロット(関数)を定義します
// このスロットは、項目が折りたたまれたときに呼び出されます
void MyClass::handleItemCollapsed(const QModelIndex &index)
{
    qDebug() << "項目が折りたたまれました: " << index.data().toString();
    // ここで、折りたたまれた項目に応じた処理を行います
}

// シグナルとスロットを接続します
connect(treeView, &QTreeView::collapsed, this, &MyClass::handleItemCollapsed);

この例では、treeViewcollapsed シグナルが発行されると、MyClass クラスの handleItemCollapsed スロットが呼び出され、折りたたまれた項目の情報 (QModelIndex) が渡されます。



シグナルが期待通りに発火しない/接続されていない

エラーの症状:

  • アプリケーションがクラッシュするわけではないが、期待する動作が起きない。
  • 項目を折りたたんでも、接続したスロットが呼び出されない。

考えられる原因とトラブルシューティング:

  • 親アイテムの有無:
    • 折りたたむ対象のアイテムに子アイテムが存在しない場合、collapsed() シグナルは発火しません。これは仕様通りの動作です。
  • オブジェクトの寿命:
    • QTreeView オブジェクトや、シグナルを受け取るスロットを持つオブジェクトが、シグナルが発火する前に削除されていないか確認してください。ポインタが無効になっているとクラッシュの原因にもなります。
  • シグナルとスロットの接続ミス:
    • connect() 関数の引数が正しいか確認してください。特に、シグナルのシグネチャとスロットのシグネチャが一致しているか(引数の型と数)が重要です。
      // 良い例
      connect(treeView, &QTreeView::collapsed, this, &MyClass::handleItemCollapsed);
      
      // 悪い例 (引数が不一致)
      // MyClass::handleItemCollapsed が引数なしの場合など
      // connect(treeView, &QTreeView::collapsed, this, &MyClass::handleItemCollapsed);
      
    • connect が失敗した場合、デバッグ出力に警告が表示されることがあります。アプリケーション出力ウィンドウをチェックしてください。

QModelIndex が無効、または期待するデータではない

エラーの症状:

  • collapsed() スロット内で index を使用してデータにアクセスしようとすると、無効なデータが返される、またはクラッシュする。

考えられる原因とトラブルシューティング:

  • internalPointer() の使用ミス:
    • カスタムモデルを使用している場合、QModelIndex::internalPointer() を介して独自の内部データ構造にアクセスしていることが多いです。このポインタが誤って解放されたり、古いポインタを参照したりしていないか確認してください。
  • QModelIndex::isValid() の確認:
    • スロット内で QModelIndex::isValid() を常にチェックして、有効なインデックスであるかを確認する習慣をつけましょう。 <!-- end list -->
    void MyClass::handleItemCollapsed(const QModelIndex &index)
    {
        if (index.isValid()) {
            qDebug() << "項目が折りたたまれました: " << index.data().toString();
            // 正常な処理
        } else {
            qWarning() << "無効な QModelIndex が collaped() シグナルで渡されました。";
        }
    }
    
  • モデルの不整合:
    • QTreeViewQAbstractItemModel またはその派生クラス(QStandardItemModel やカスタムモデルなど)を使用しています。モデルのデータがツリービューの表示と同期していない場合、QModelIndex が指すデータが期待と異なることがあります。
    • 特に、モデルの変更(行の追加、削除、移動など)を beginInsertRows() / endInsertRows()beginRemoveRows() / endRemoveRows() といった適切な通知メカニズムを使用して行っていない場合、ビューとモデル間で不整合が生じやすいです。これが原因で、collapsed() シグナルで渡される QModelIndex が、実際には意図しないアイテムを指している可能性があります。

パフォーマンスの問題

エラーの症状:

  • 項目を折りたたむ際に UI が一時的にフリーズしたり、動作が遅くなったりする。

考えられる原因とトラブルシューティング:

  • 過剰な UI 更新:
    • collapsed() シグナルを受け取るたびに、ツリービュー全体を再描画したり、他の複数のウィジェットを不必要に更新したりしていないか確認します。
    • 解決策: 必要な部分のみを更新するように最適化します。
  • スロットでの重い処理:
    • collapsed() シグナルに接続されたスロット内で、時間のかかる処理(ファイル I/O、大量のデータ処理、ネットワークリクエストなど)を行っている場合、UI スレッドがブロックされ、フリーズが発生します。
    • 解決策:
      • 重い処理は別スレッド(QtConcurrentQThreadPool を使用)で実行し、結果が出たら再度 UI スレッドにシグナルで通知して UI を更新します。
      • 処理を分割し、QTimer を利用して少しずつ実行するなど、非同期処理を検討します。

QTreeView の特定の設定による影響

エラーの症状:

  • collapsed() シグナル自体は発火するが、ツリーの表示が期待通りに変化しない。

考えられる原因とトラブルシューティング:

  • カスタムデリゲート:
    • QAbstractItemDelegate を継承したカスタムデリゲートを使用している場合、描画ロジックに問題があると、折りたたみ状態が正しく反映されないことがあります。デリゲートの paint() メソッドが正しく QStyleOptionViewItem の状態(state & QStyle::State_Children など)を考慮しているか確認します。
  • animated プロパティ:
    • QTreeView::setAnimated(bool animated) プロパティが true に設定されている場合、折りたたみ/展開にアニメーションが適用されます。アニメーション中は即座に表示が更新されないことがあります。
    • もし即座の更新が必要なら、setAnimated(false) を設定することも検討しますが、ユーザーエクスペリエンスが低下する可能性があります。
  • Qt フォーラム/Stack Overflow: 同じ問題に遭遇した人がいないか、Qt フォーラムや Stack Overflow で検索してみます。
  • Qt ドキュメントの参照: QTreeView::collapsed() や関連するクラス(QAbstractItemModel, QModelIndex など)の公式ドキュメントを再確認し、仕様や推奨される使い方を確認します。
  • 最小再現コードの作成: 問題が発生した場合、その現象を最小限のコードで再現できるか試します。これにより、問題の原因を絞り込みやすくなります。
  • デバッグ出力の活用: qDebug() を使って、シグナルがいつ発火したか、QModelIndex の内容は何かなどを詳細にログ出力します。


基本的な使用例:コンソールにログを出力する

これは最も基本的な例で、アイテムが折りたたまれたときにその情報をコンソールに出力します。

main.cpp

#include <QApplication>
#include <QMainWindow>
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug> // qDebug() を使用するために必要

// カスタムウィンドウクラスの定義
class MyWindow : public QMainWindow
{
    Q_OBJECT // シグナルとスロットを使用するために必要

public:
    MyWindow(QWidget *parent = nullptr) : QMainWindow(parent)
    {
        // QStandardItemModel の作成
        QStandardItemModel *model = new QStandardItemModel(0, 1, this);
        model->setHeaderData(0, Qt::Horizontal, "ツリー項目");

        // アイテムの追加
        QStandardItem *parentItem1 = new QStandardItem("親項目 1");
        parentItem1->appendRow(new QStandardItem("子項目 1-1"));
        parentItem1->appendRow(new QStandardItem("子項目 1-2"));
        QStandardItem *grandChildItem = new QStandardItem("孫項目 1-2-1");
        parentItem1->child(1)->appendRow(grandChildItem); // 子項目 1-2 の下に孫項目を追加

        QStandardItem *parentItem2 = new QStandardItem("親項目 2");
        parentItem2->appendRow(new QStandardItem("子項目 2-1"));

        model->appendRow(parentItem1);
        model->appendRow(parentItem2);

        // QTreeView の作成
        QTreeView *treeView = new QTreeView(this);
        treeView->setModel(model);
        treeView->expandAll(); // 最初はすべて展開しておく

        // collapsed() シグナルをスロットに接続
        // アイテムが折りたたまれたときに handleCollapsed() が呼び出される
        connect(treeView, &QTreeView::collapsed, this, &MyWindow::handleCollapsed);

        // レイアウトの設定
        QWidget *centralWidget = new QWidget(this);
        QVBoxLayout *layout = new QVBoxLayout(centralWidget);
        layout->addWidget(treeView);
        setCentralWidget(centralWidget);

        setWindowTitle("QTreeView::collapsed() 例");
        resize(400, 300);
    }

private slots:
    // collapsed() シグナルを受け取るスロット
    void handleCollapsed(const QModelIndex &index)
    {
        if (index.isValid()) {
            // 折りたたまれたアイテムのデータを取得して出力
            qDebug() << "項目が折りたたまれました: " << index.data().toString();
            qDebug() << "行: " << index.row() << ", 列: " << index.column();

            // 親アイテムが存在する場合、その情報も出力
            if (index.parent().isValid()) {
                qDebug() << "親項目: " << index.parent().data().toString();
            }
        } else {
            qDebug() << "無効なインデックスが collapsed() シグナルから渡されました。";
        }
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MyWindow w;
    w.show();
    return a.exec();
}

#include "main.moc" // moc ファイルのインクルード(Q_OBJECTを使用する場合に必要)

解説:

  • handleCollapsed スロットでは、引数として渡された QModelIndex を使って、折りたたまれた項目に関する情報を取得し、qDebug() でコンソールに出力しています。
  • connect(treeView, &QTreeView::collapsed, this, &MyWindow::handleCollapsed); で、QTreeViewcollapsed シグナルと MyWindow クラスの handleCollapsed スロットを接続しています。
  • treeView->expandAll(); で、最初はすべての項目を展開した状態にします。
  • QTreeView を作成し、モデルを設定します。
  • QStandardItemModel を使用してツリーに表示するデータを準備します。

UI の状態を連動させる例:ステータスバーにメッセージを表示

項目が折りたたまれたときに、その情報に基づいてUI(例えばステータスバー)を更新する例です。

main.cpp

#include <QApplication>
#include <QMainWindow>
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug>
#include <QStatusBar> // QStatusBar を使用するために必要

class MyWindow : public QMainWindow
{
    Q_OBJECT

public:
    MyWindow(QWidget *parent = nullptr) : QMainWindow(parent)
    {
        QStandardItemModel *model = new QStandardItemModel(0, 1, this);
        model->setHeaderData(0, Qt::Horizontal, "ツリー項目");

        QStandardItem *parentItem1 = new QStandardItem("ディレクトリ A");
        parentItem1->appendRow(new QStandardItem("ファイル A-1.txt"));
        parentItem1->appendRow(new QStandardItem("ファイル A-2.txt"));
        QStandardItem *subDir = new QStandardItem("サブディレクトリ A-3");
        subDir->appendRow(new QStandardItem("ファイル A-3-1.doc"));
        parentItem1->appendRow(subDir);

        QStandardItem *parentItem2 = new QStandardItem("ディレクトリ B");
        parentItem2->appendRow(new QStandardItem("ファイル B-1.jpg"));

        model->appendRow(parentItem1);
        model->appendRow(parentItem2);

        QTreeView *treeView = new QTreeView(this);
        treeView->setModel(model);
        treeView->expandAll();

        // collapsed() シグナルをスロットに接続
        connect(treeView, &QTreeView::collapsed, this, &MyWindow::updateStatusBarOnCollapsed);

        // ステータスバーの作成
        statusBar()->showMessage("ツリービューを操作してください");

        QWidget *centralWidget = new QWidget(this);
        QVBoxLayout *layout = new QVBoxLayout(centralWidget);
        layout->addWidget(treeView);
        setCentralWidget(centralWidget);

        setWindowTitle("QTreeView::collapsed() とステータスバー");
        resize(400, 300);
    }

private slots:
    void updateStatusBarOnCollapsed(const QModelIndex &index)
    {
        if (index.isValid()) {
            QString itemName = index.data().toString();
            statusBar()->showMessage(QString("'%1' が折りたたまれました。").arg(itemName));
            qDebug() << "'" << itemName << "' が折りたたまれました。";
        } else {
            statusBar()->showMessage("無効な項目が折りたたまれました。");
            qWarning() << "無効な QModelIndex が collaped() シグナルから渡されました。";
        }
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MyWindow w;
    w.show();
    return a.exec();
}

#include "main.moc"

解説:

  • updateStatusBarOnCollapsed スロット内で、折りたたまれたアイテムの名前をステータスバーに表示するように変更しています。これにより、ユーザーはどのアイテムが折りたたまれたかを視覚的に確認できます。
  • QMainWindowstatusBar() メソッドでステータスバーを取得し、初期メッセージを設定します。
  • 前の例と同様にツリービューを作成します。

実際のアプリケーションでは、ツリーの展開/折りたたみ状態を保存し、次回起動時に復元したい場合があります。これは collapsed() シグナルと expanded() シグナルを組み合わせて行われます。

概念的なコード:

#include <QApplication>
#include <QMainWindow>
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug>
#include <QSettings> // 設定を保存・ロードするために使用

// ... (MyWindow クラスの定義は前述の例とほぼ同じ)

class MyWindow : public QMainWindow
{
    Q_OBJECT

public:
    MyWindow(QWidget *parent = nullptr) : QMainWindow(parent)
    {
        // ... (QStandardItemModel の作成とアイテムの追加は省略)

        QTreeView *treeView = new QTreeView(this);
        treeView->setModel(model);

        // collapsed() と expanded() シグナルを接続
        connect(treeView, &QTreeView::collapsed, this, &MyWindow::saveExpandedState);
        connect(treeView, &QTreeView::expanded, this, &MyWindow::saveExpandedState);

        // ... (レイアウトの設定は省略)

        loadExpandedState(treeView, model); // 起動時に状態をロード

        setWindowTitle("QTreeView 折りたたみ状態の保存");
        resize(400, 300);
    }

private:
    void saveExpandedState(const QModelIndex &index)
    {
        QSettings settings("MyCompany", "MyApplication");
        // ツリーのすべての展開状態を QSettings に保存するロジック
        // 例: 各展開されたアイテムのパスをリストとして保存
        QStringList expandedPaths;
        // treeView->isExpanded(index) で状態を確認し、パスを構築・追加
        // (この部分は再帰的な処理が必要で、ここでは簡略化)
        // 例: /Parent1/Child2 のようなパスを保存
        QModelIndex current = index;
        QString path;
        while (current.isValid()) {
            path = "/" + current.data().toString() + path;
            current = current.parent();
        }
        if (treeView->isExpanded(index)) {
             expandedPaths.append(path);
        } else {
             expandedPaths.removeAll(path); // 折りたたまれたらリストから削除
        }
        settings.setValue("ExpandedPaths", expandedPaths);
        qDebug() << "展開状態を保存: " << expandedPaths;
    }

    void loadExpandedState(QTreeView *treeView, QStandardItemModel *model)
    {
        QSettings settings("MyCompany", "MyApplication");
        QStringList expandedPaths = settings.value("ExpandedPaths").toStringList();
        qDebug() << "展開状態をロード: " << expandedPaths;

        // 保存されたパスに基づいてツリーを再構築・展開するロジック
        // (これも再帰的な処理やモデルの走査が必要で、ここでは簡略化)
        // 例えば、モデルのすべてのアイテムを走査し、パスが expandedPaths に含まれていれば展開する
        // treeView->setExpanded(modelIndex, true);
    }
};
// ... main() 関数は省略

解説:

  • loadExpandedState メソッドでは、アプリケーションの起動時に QSettings から保存された展開状態を読み込み、それに基づいて QTreeView::setExpanded() メソッドを使って各アイテムをプログラム的に展開または折りたたみます。
  • saveExpandedState スロットでは、折りたたみ/展開が発生するたびに現在のツリーの展開状態を調べ、その情報を QSettings に保存します。具体的な実装としては、各アイテムの一意なパス(例: /ルート/親/子)を識別子として、展開されているアイテムのパスのリストを保存する方法が考えられます。
  • QSettings クラスを使用して、アプリケーションの設定を永続化します(レジストリやINIファイルなど)。

注意点:

  • QModelIndex は永続的ではないため、アプリケーションの再起動時に同じ QModelIndex が同じアイテムを指す保証はありません。そのため、アイテムの識別には QPersistentModelIndex を使用するか、アイテムのデータやパスなど、永続的な情報で識別する必要があります。上記の例では簡単のために文字列パスを使用しています。
  • 実際の保存・復元処理は、ツリーの深さやアイテム数が多い場合に効率的なアルゴリズムを考慮する必要があります。単純にすべてのアイテムの状態を保存すると、データ量が増えすぎたり、起動時に時間がかかったりする可能性があります。


QTreeView のメソッドを直接呼び出す場合

QTreeView::collapsed() シグナルは、ユーザーの操作(クリックやキーボード操作)や、プログラムによる QTreeView::collapse() メソッドの呼び出しによって発生します。もし、ツリーの折りたたみをプログラム的に制御している場合、シグナルを待つのではなく、直接関連するメソッドの呼び出し後に処理を実行することができます。

QTreeView::collapsed() シグナルの代替とは言えませんが、シグナルが発火する「原因」となるメソッドを直接利用するケースです。


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

class MyWindow : public QMainWindow
{
    Q_OBJECT

public:
    MyWindow(QWidget *parent = nullptr) : QMainWindow(parent)
    {
        model = new QStandardItemModel(0, 1, this);
        model->setHeaderData(0, Qt::Horizontal, "ツリー項目");

        QStandardItem *parentItem1 = new QStandardItem("親項目 A");
        parentItem1->appendRow(new QStandardItem("子項目 A-1"));
        parentItem1->appendRow(new QStandardItem("子項目 A-2"));
        parentItem1->appendRow(new QStandardItem("子項目 A-3"));

        QStandardItem *parentItem2 = new QStandardItem("親項目 B");
        parentItem2->appendRow(new QStandardItem("子項目 B-1"));

        model->appendRow(parentItem1);
        model->appendRow(parentItem2);

        treeView = new QTreeView(this);
        treeView->setModel(model);
        treeView->expandAll(); // 最初はすべて展開

        // 親項目Aを折りたたむボタン
        QPushButton *collapseButton = new QPushButton("「親項目 A」を折りたたむ", this);
        connect(collapseButton, &QPushButton::clicked, this, &MyWindow::collapseParentA);

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

        QWidget *centralWidget = new QWidget(this);
        centralWidget->setLayout(layout);
        setCentralWidget(centralWidget);

        setWindowTitle("QTreeView::collapse() メソッド直接呼び出し例");
        resize(400, 300);
    }

private slots:
    void collapseParentA()
    {
        // "親項目 A" の QModelIndex を取得
        QModelIndex parentAIndex = model->index(0, 0); // 最初のトップレベル項目が「親項目 A」

        if (parentAIndex.isValid()) {
            treeView->collapse(parentAIndex); // 直接折りたたむメソッドを呼び出す
            qDebug() << "「親項目 A」をプログラム的に折りたたみました。";
            // ここで、折りたたみ後の追加処理を直接記述できる
            // 例えば、関連するUI要素の更新など
            updateUiAfterCollapse(parentAIndex);
        }
    }

    void updateUiAfterCollapse(const QModelIndex &index)
    {
        if (index.isValid()) {
            qDebug() << "プログラム的な折りたたみ後処理: " << index.data().toString();
            // 例: 別のウィジェットの状態を更新
            // someOtherWidget->setEnabled(false);
        }
    }

private:
    QStandardItemModel *model;
    QTreeView *treeView;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MyWindow w;
    w.show();
    return a.exec();
}

#include "main.moc"

この方法の利点:

  • シグナル/スロット接続のオーバーヘッドがない。
  • 折りたたみ処理とそれに続くアクションがコード上で密接に結びついており、フローが追いやすい。

この方法の限界:

  • ユーザーが手動でツリーを折りたたんだ場合には、この方法で記述したコードは実行されません。あくまでプログラム的に collapse() メソッドを呼び出した場合にのみ適用されます。したがって、ユーザー操作にも対応する場合は、やはり collapsed() シグナルを使うべきです。

QTreeView::isExpanded() をポーリングする(非推奨)

これはほとんどのケースで非効率的であり、推奨されませんが、特定のシナリオ(例えば、ごく限られた、制御されたタイミングでのみ状態を確認したい場合)では考えられます。

考え方: タイマーなどを使って定期的に QTreeView::isExpanded(const QModelIndex &index) を呼び出し、各項目の展開状態が変化したかどうかを自分で監視します。

例(概念):

// これはあくまで概念的なものであり、一般的には推奨されません
// (UI更新のオーバーヘッドが大きく、効率的ではないため)
#include <QApplication>
#include <QMainWindow>
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug>
#include <QTimer> // ポーリングに必要

class MyWindow : public QMainWindow
{
    Q_OBJECT

public:
    MyWindow(QWidget *parent = nullptr) : QMainWindow(parent)
    {
        // ... (QStandardItemModel と QTreeView の初期化は省略)
        model = new QStandardItemModel(0, 1, this);
        // ... アイテムの追加

        treeView = new QTreeView(this);
        treeView->setModel(model);
        treeView->expandAll();

        // 監視したい特定のインデックス
        targetIndex = model->index(0, 0); // 例えば「親項目 A」

        // ポーリングタイマーの設定
        QTimer *timer = new QTimer(this);
        connect(timer, &QTimer::timeout, this, &MyWindow::checkCollapsedState);
        timer->start(200); // 200ミリ秒ごとにチェック (非常に非効率!)

        // ... (レイアウトの設定は省略)

        setWindowTitle("QTreeView::isExpanded() ポーリング例");
        resize(400, 300);
    }

private slots:
    void checkCollapsedState()
    {
        if (!targetIndex.isValid()) return;

        // 前回の状態と比較
        bool currentExpanded = treeView->isExpanded(targetIndex);
        if (lastExpandedState.contains(targetIndex) && lastExpandedState[targetIndex] != currentExpanded) {
            if (!currentExpanded) {
                qDebug() << "ポーリングにより、「" << targetIndex.data().toString() << "」が折りたたまれたことを検出しました。";
                // 折りたたみ後の処理
            } else {
                qDebug() << "ポーリングにより、「" << targetIndex.data().toString() << "」が展開されたことを検出しました。";
                // 展開後の処理
            }
        }
        lastExpandedState[targetIndex] = currentExpanded; // 状態を更新
    }

private:
    QStandardItemModel *model;
    QTreeView *treeView;
    QModelIndex targetIndex;
    QMap<QModelIndex, bool> lastExpandedState; // QModelIndex をキーにするのは注意が必要 (永続的ではないため)
};

// ... main() 関数は省略

この方法の問題点:

  • QModelIndex の不安定性: QModelIndex はモデルの内部構造が変更されると無効になる可能性があるため、長時間保持してポーリングに使用することは危険です。
  • 反応の遅延: 状態の変化を検出するまでにタイマーの間隔分の遅延が発生します。
  • 非効率性: 定期的に全項目(または監視対象の項目)の状態をチェックするため、CPUリソースを無駄に消費します。特にツリーが大規模な場合、パフォーマンスに大きな影響を与えます。

QTreeView の動作をより細かく制御したい場合、QTreeView をサブクラス化し、特定のイベントハンドラをオーバーライドすることが考えられます。ただし、collapsed() シグナルに直接対応する「折りたたみイベント」のようなものはありません。QTreeViewQAbstractItemView を継承しており、通常は描画やマウスイベントを処理します。

  • mousePressEvent(QMouseEvent *event) のオーバーライド: ツリーの展開/折りたたみインジケーター(通常は小さな矢印)がクリックされたことを検知し、そのインデックスを取得して処理を行うことができます。しかし、これはユーザーがキーボードで操作した場合や、プログラム的に折りたたまれた場合には対応できません。また、インジケーターのクリック位置を正確に判断する必要があり、実装が複雑になります。

    // MyTreeView.h
    #include <QTreeView>
    #include <QMouseEvent>
    #include <QDebug>
    #include <QStyleOptionViewItem> // QStyleOptionViewItem を含める
    
    class MyTreeView : public QTreeView
    {
        Q_OBJECT
    public:
        MyTreeView(QWidget *parent = nullptr) : QTreeView(parent) {}
    
    protected:
        void mousePressEvent(QMouseEvent *event) override
        {
            QModelIndex clickedIndex = indexAt(event->pos());
            if (clickedIndex.isValid()) {
                // クリック位置が展開/折りたたみインジケーターの範囲内にあるか判断
                // これは複雑で、QStyle クラスの計算や QRect の操作が必要になる
                // 例として非常に簡略化
                QRect visualRect = this->visualRect(clickedIndex);
                // 仮のインジケーター領域の判定 (正確ではありません)
                if (event->pos().x() < visualRect.x() + indentation()) {
                    // インジケーター領域がクリックされた可能性
                    if (isExpanded(clickedIndex)) {
                        qDebug() << "カスタム: 項目が折りたたまれる直前 (インジケータークリック)";
                    } else {
                        qDebug() << "カスタム: 項目が展開される直前 (インジケータークリック)";
                    }
                }
            }
            QTreeView::mousePressEvent(event); // 基底クラスの処理を呼び出すことを忘れない
        }
    };
    

この方法の問題点:

  • 網羅性の欠如: マウス操作以外の方法(キーボード、プログラムによる呼び出し)で項目が折りたたまれた場合には、このイベントハンドラは発火しません。
  • 複雑性: 展開/折りたたみインジケーターの正確なクリックを検出するには、QStyleQStyleOptionViewItem の詳細な知識が必要で、実装が非常に複雑になります。
  • プログラム制御の場合: ツリーの折りたたみを完全にプログラムで制御している場合、QTreeView::collapse() メソッドを呼び出した直後に、それに続く処理を直接記述することができます。これはシグナルの代替というよりは、シグナルを必要としない特定のケースです。
  • 最も推奨される方法: ほとんどのケースで、QTreeView::collapsed() シグナルと QTreeView::expanded() シグナルを使用するのが最もクリーンで信頼性が高く、効率的な方法です。Qt のシグナル/スロットメカニズムは、まさにこのようなイベントドリブンのプログラミングのために設計されています。