Qt QTreeView の「行削除」を極める - rowsRemoved() エラーと解決策

2025-05-16

Qtプログラミングにおける void QTreeView::rowsRemoved() は、QTreeView クラスが発するシグナルです。

これは、モデルから行が削除されたことをビュー(QTreeView)に通知するために使用されます。

Qtのモデル/ビューアーキテクチャでは、データ(モデル)と表示(ビュー)が分離されています。QTreeView はツリー構造のデータを表示するためのビューウィジェットです。データそのものは QAbstractItemModel を継承したモデルクラスが管理します。

rowsRemoved() シグナルは、具体的には以下の情報を伴って発せられます。

  • end: 削除された行の最後の行番号です。
  • start: 削除された行の最初の行番号です。
  • parent: 行が削除された親アイテムの QModelIndex です。もしトップレベルの行が削除された場合は、無効な QModelIndex (つまり QModelIndex() ) になります。

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

QTreeView は、表示しているデータが変更されたことを知る必要があります。モデル内のデータが直接変更されても、ビューは自動的にその変更を検知することはできません。そこで、モデルが行の削除を行った際に、この rowsRemoved() シグナルを発行することで、ビューに「データが変更されたので、表示を更新してください」と伝えるのです。

典型的な使用例

モデル側で removeRows() などのメソッドを呼び出して行を削除する場合、通常は以下のような手順を踏みます。

  1. beginRemoveRows() を呼び出す: これにより、モデルがビューに行削除の準備をすることを通知します。
  2. 実際に行データを削除する: モデルの内部データ構造から該当する行を削除します。
  3. endRemoveRows() を呼び出す: これにより、行の削除が完了したことをビューに通知します。この endRemoveRows() の呼び出しの中で、モデルは自動的に rowsRemoved() シグナルを発行します。

QTreeView はこの rowsRemoved() シグナルを受け取ると、内部的に表示を更新し、削除された行を画面から消します。



void QTreeView::rowsRemoved() に関連する一般的なエラー

    • 原因
      モデルが beginRemoveRows()endRemoveRows() のペアを正しく呼び出していない場合、QTreeView は行が削除されたことを知りません。結果として、古い表示のままになり、削除された行が画面上に残り続けます。
    • 具体的な状況
      • removeRows() メソッド内で beginRemoveRows() または endRemoveRows() のどちらか、あるいは両方が呼び出されていない。
      • beginRemoveRows()endRemoveRows() の引数(parent, start, end)が、実際に削除された行の範囲と一致しない。
      • beginRemoveRows() を呼び出した後に、実際にデータが削除される前に endRemoveRows() を呼び出してしまっている。
  1. クラッシュ (不正なメモリアクセス、セグメンテーションフォールト)

    • 原因
      モデルとビューの同期が取れていない状態で、ビューが不正な QModelIndex を参照しようとしたり、既に削除されたデータにアクセスしようとしたりすると発生します。
    • 具体的な状況
      • beginRemoveRows() を呼び出す前にモデルからデータが削除されている。
      • endRemoveRows() を呼び出した後に、ビューが削除されたインデックス(QModelIndex)を使って何か操作を行おうとする。QModelIndex はデータが削除されると無効になります。
      • モデルが QModelIndex を生成する際に、internalPointer()internalId() で不正なポインタを返している。削除処理中にこれが原因で、ビューが不正なメモリアドレスにアクセスしようとする。
      • 特に、選択範囲やカレントアイテムが削除対象の行に含まれていた場合、これらのインデックスが無効になるため、注意が必要です。
  2. 表示の乱れ、奇妙な動作

    • 原因
      rowsRemoved() シグナルが不適切に発行されたり、モデルの状態とビューの状態が一時的に不一致になったりすることで発生します。
    • 具体的な状況
      • rowsRemoved() シグナルが複数回発行される(通常は endRemoveRows() 内で一度だけ発行される)。
      • beginRemoveRows()endRemoveRows() の間に、行の挿入など、他の構造変更が行われる。
      • beginRemoveRows() で指定した範囲と endRemoveRows() で指定した範囲が異なる。
  1. beginRemoveRows()endRemoveRows() のペアを確認する

    • 行を削除するモデルのメソッド(例: removeRows(), removeRow())で、必ず beginRemoveRows()endRemoveRows() がセットで呼び出されているかを確認します。
    • 呼び出し順序が正しいか(beginRemoveRows() -> 実際のデータ削除 -> endRemoveRows())を確認します。
    • 例:
      bool MyModel::removeRows(int row, int count, const QModelIndex &parent)
      {
          if (row < 0 || row + count > rowCount(parent))
              return false;
      
      

    beginRemoveRows(parent, row, row + count - 1); // 削除開始を通知 // ここで実際にモデルの内部データを行削除する // 例: m_data.removeAt(row, count); endRemoveRows(); // 削除完了を通知 (この中でrowsRemoved()が発行される) return true; } ```

  2. QModelIndex の有効性を確認する

    • QModelIndex は、そのインデックスが指すデータが削除されると無効になります。削除処理の前後で、QModelIndex::isValid() を使ってインデックスが有効かどうかを確認する習慣をつけましょう。
    • 特に、削除されたアイテムの選択状態をクリアしたり、カレントインデックスを更新したりする必要があります。
    • 例:
      // 選択されているアイテムのリスト
      QModelIndexList selectedIndexes = treeView->selectionModel()->selectedIndexes();
      
      // 削除処理の前に選択状態をクリアする
      treeView->selectionModel()->clearSelection();
      
      // あるいは、削除対象のインデックスと一致しないインデックスのみを保持する
      // ... 削除処理 ...
      
      // 削除後、もし必要であれば、残りの有効なインデックスに基づいて選択を再構築する
      
  3. デバッガを使用する

    • クラッシュが発生した場合、デバッガを使ってスタックトレースを確認し、どのコードパスで問題が発生しているかを特定します。QModelIndex の不正な使用や、既に解放されたメモリへのアクセスが原因であることが多いです。
    • QDebug を使って、beginRemoveRows()endRemoveRows() が正しい引数で呼び出されているか、モデルのデータ構造が期待通りに変化しているかなどを出力して確認します。
  4. シンプルなモデルでテストする

    • カスタムモデルが複雑な場合、問題の切り分けが難しくなります。一時的に QStandardItemModel のようなシンプルなモデルを使用して、rowsRemoved() の動作が期待通りかを確認し、問題がカスタムモデルの実装にあるのか、ビュー側の設定にあるのかを特定します。


Qt の void QTreeView::rowsRemoved() シグナルは、通常、モデル側でデータが削除されたときに自動的に発行されます。開発者がこのシグナルを直接接続して何か処理を行うことは稀で、むしろモデルがこのシグナルを正しく発行するように実装することが重要です。

  1. モデル側
    QAbstractItemModel を継承したカスタムモデルで、rowsRemoved() シグナルがどのように発行されるかを示す例。
  2. ビュー側
    QTreeViewrowsRemoved() シグナルを受け取った際に、内部的にどのように表示を更新するか(直接的なコード記述は不要だが、概念的に理解するため)。

モデル側: カスタムモデルで rowsRemoved() シグナルを発行する例

QTreeView は、QAbstractItemModel のサブクラスに接続されています。モデルがデータを削除する際には、beginRemoveRows()endRemoveRows() を呼び出す必要があります。endRemoveRows() が呼び出されたときに、QAbstractItemModel は自動的に rowsRemoved() シグナルを発行します。

以下の例では、簡単なカスタムモデル MyModel を作成し、行を削除するメソッド removeRows を実装しています。

#include <QAbstractItemModel>
#include <QModelIndex>
#include <QVariant>
#include <QDebug>
#include <QApplication>
#include <QTreeView>
#include <QPushButton>
#include <QVBoxLayout>
#include <QStandardItemModel> // QStandardItemModel の動作と比較のため

// MyModel.h
class MyModel : public QAbstractItemModel
{
    Q_OBJECT

public:
    explicit MyModel(QObject *parent = nullptr);

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    Qt::ItemFlags flags(const QModelIndex &index) const override;
    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
    QModelIndex parent(const QModelIndex &index) const override;
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;

    // 行を削除するカスタムメソッド
    bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;

    // 簡単なテストデータ用
    void setupTestData(int numRows = 5);

private:
    QVector<QStringList> m_data; // 簡単なデータ格納用
};

// MyModel.cpp
MyModel::MyModel(QObject *parent)
    : QAbstractItemModel(parent)
{
}

QVariant MyModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    if (role == Qt::DisplayRole) {
        if (index.row() < m_data.size() && index.column() < m_data.at(index.row()).size()) {
            return m_data.at(index.row()).at(index.column());
        }
    }
    return QVariant();
}

Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::NoItemFlags;
    return QAbstractItemModel::flags(index);
}

QModelIndex MyModel::index(int row, int column, const QModelIndex &parent) const
{
    if (!hasIndex(row, column, parent))
        return QModelIndex();

    if (!parent.isValid()) { // トップレベルアイテム
        return createIndex(row, column, nullptr); // ポインタはここではダミー
    }
    return QModelIndex(); // この例では子アイテムは持たない
}

QModelIndex MyModel::parent(const QModelIndex &index) const
{
    Q_UNUSED(index);
    return QModelIndex(); // この例では常にトップレベル
}

int MyModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0; // この例では子アイテムは持たない
    return m_data.size();
}

int MyModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return (m_data.isEmpty() ? 0 : m_data.first().size()); // 最初の行のカラム数を使う
}

bool MyModel::removeRows(int row, int count, const QModelIndex &parent)
{
    if (parent.isValid()) // この例では子アイテムの削除はサポートしない
        return false;

    if (row < 0 || row + count > m_data.size())
        return false;

    // ★重要: 行削除の開始をビューに通知する
    // この呼び出しがなければ、ビューはデータの削除を知らない
    beginRemoveRows(parent, row, row + count - 1);

    // 実際にデータを削除する
    for (int i = 0; i < count; ++i) {
        m_data.removeAt(row); // 指定された行からデータを削除
    }

    // ★重要: 行削除の完了をビューに通知する
    // この呼び出しの中で、QAbstractItemModel::rowsRemoved() シグナルが自動的に発行される
    endRemoveRows();

    qDebug() << "Rows removed:" << row << "to" << row + count - 1;
    return true;
}

void MyModel::setupTestData(int numRows)
{
    beginResetModel(); // モデルのリセットを通知(初期データのセットアップ時など)
    m_data.clear();
    for (int i = 0; i < numRows; ++i) {
        m_data.append({QString("Item %1_0").arg(i), QString("Item %1_1").arg(i)});
    }
    endResetModel(); // モデルのリセット完了を通知
}


// main.cpp
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QWidget window;
    QVBoxLayout *layout = new QVBoxLayout(&window);

    MyModel *model = new MyModel(&window);
    model->setupTestData(7); // 7行のテストデータを設定

    QTreeView *treeView = new QTreeView(&window);
    treeView->setModel(model);
    layout->addWidget(treeView);

    QPushButton *removeButton = new QPushButton("選択行を削除", &window);
    layout->addWidget(removeButton);

    // ボタンがクリックされたら、選択されている行をモデルから削除
    QObject::connect(removeButton, &QPushButton::clicked, [&]() {
        QModelIndexList selectedIndexes = treeView->selectionModel()->selectedRows();
        if (!selectedIndexes.isEmpty()) {
            // 選択されている最初の行を取得
            int rowToRemove = selectedIndexes.first().row();
            // 単一行の削除を例示
            model->removeRows(rowToRemove, 1);
        } else {
            qDebug() << "行が選択されていません。";
        }
    });

    window.setWindowTitle("QTreeView rowsRemoved() Example");
    window.show();

    return app.exec();
}

解説

  • QTreeView は、この rowsRemoved() シグナルを受け取ると、その情報に基づいて自身の表示を更新します。開発者が明示的に treeView->update()treeView->viewport()->update() を呼び出す必要はありません。
  • endRemoveRows(): 実際にデータがモデルから削除された後にこのメソッドを呼び出します。この呼び出しの中で、QAbstractItemModel の基底クラスが rowsRemoved(const QModelIndex &parent, int start, int end) シグナルを自動的に発行します。
  • beginRemoveRows(parent, row, row + count - 1): これにより、モデルはビューに、指定された parent の下の row から row + count - 1 までの行が削除されようとしていることを通知します。この通知によって、ビューは内部的に削除される行に関連するキャッシュや選択状態などを更新する準備ができます。
  • MyModel::removeRows() メソッド内で、データの削除の前後で beginRemoveRows()endRemoveRows() を呼び出しています。

QTreeView の内部には、QAbstractItemModel から発せられる rowsRemoved() シグナルに接続するためのスロットが実装されています。この接続は QTreeView::setModel() が呼び出されたときに自動的に行われます。

以下は、QTreeView がどのようにシグナルを受け取って反応するかを示す概念的なコードであり、実際に開発者が書く必要はありません。

// これは QTreeView の内部実装の概念的な抜粋です
// 開発者が直接記述するコードではありません

class QTreeViewPrivate : public QObject
{
    // ... 内部状態 ...
public slots:
    void _q_rowsRemoved(const QModelIndex &parent, int first, int last)
    {
        Q_D(QTreeView);
        qDebug() << "QTreeView: rowsRemoved() シグナルを受信しました。";
        qDebug() << "  Parent:" << parent;
        qDebug() << "  First Row:" << first;
        qDebug() << "  Last Row:" << last;

        // 内部的にツリービューの表示を更新するロジック
        // - 削除された行の描画領域を無効化する
        // - スクロールバーの位置を調整する
        // - 選択モデルから無効なインデックスを削除する
        // - 内部のレイアウト情報を再計算する
        d->updateGeometries(); // ジオメトリの更新
        d->viewport()->update(); // ビューポートの再描画を要求

        // 必要に応じて、選択モデルのアイテムを削除または調整する
        // d->selectionModel->rowsRemoved(parent, first, last);

        // ... その他、内部的な整合性を保つための処理 ...
    }
};

// QTreeView::setModel() 内で、次のような接続が行われる(概念)
// QObject::connect(model, &QAbstractItemModel::rowsRemoved,
//                  this, &QTreeViewPrivate::_q_rowsRemoved); // 実際の接続はもう少し複雑です
  • 開発者が QTreeView::rowsRemoved() シグナルを直接 connect して何かを行うことは非常に稀です。なぜなら、そのシグナルはビューが自身の表示を更新するために内部的に使用するものだからです。もし、行削除時に特別なロギングや他のビューへの通知などを行いたい場合は、モデルの rowsRemoved シグナル(QAbstractItemModel::rowsRemoved)を接続するべきです。
  • QTreeView::rowsRemoved() は、QTreeView クラスのプロテクテッドスロットです。これは、ビューの内部でモデルからのシグナルを受け取るために使われることを意味します。一般的に、開発者がこのスロットを直接呼び出すことはありません。


しかし、「代替」という言葉が、rowsRemoved() シグナルを利用しない、あるいは異なる状況でビューにデータ変更を伝える方法を指すのであれば、いくつかの概念的なアプローチが考えられます。

大前提
QAbstractItemModel の仕様に則り、データの構造変更(行の追加、削除、移動)は必ず begin...Rows()end...Rows() のペアで通知されるべきです。これが最も堅牢でパフォーマンスの高い方法であり、ビュー(QTreeView など)はこれらのシグナルを適切に処理するように設計されています。

QAbstractItemModel::modelReset() シグナルを使用する

これは rowsRemoved() の直接的な代替ではありませんが、モデル全体の構造が完全に変更されたことをビューに伝えるためのシグナルです。

  • 使用方法
    // MyModel::clearAllData() のようなメソッド内で
    beginResetModel(); // モデルのリセット開始を通知
    m_data.clear();    // 実際のデータクリア
    endResetModel();   // モデルのリセット完了を通知
    
    endResetModel() が呼び出されると、QAbstractItemModel::modelReset() シグナルが自動的に発行されます。
  • デメリット
    • ビューがその内部状態(選択状態、展開状態、スクロール位置など)をすべて失う可能性があります。これはユーザーエクスペリエンスを損なう可能性があります。
    • ビューはデータを最初から全て再フェッチし、再構築するため、多くの変更がない場合はパフォーマンスが低下する可能性があります。
  • いつ使うか
    • モデルのデータが完全にクリアされ、再構築される場合。
    • 非常に多数の行が追加/削除され、個々の rowsInserted()rowsRemoved() シグナルを大量に発行するよりも、一度にビュー全体をリセットする方が効率的だと判断される場合。
    • モデルの階層構造そのものが大きく変わる場合(例:ツリーのルートが変更される)。

QAbstractItemModel::dataChanged() シグナルを使用する

これは行の追加や削除ではなく、既存のアイテムのデータ内容が変更されたことを通知するためのシグナルです。

  • 使用方法
    // モデル内でデータが変更された後
    QModelIndex topLeft = index(changedRow, changedColumnStart);
    QModelIndex bottomRight = index(changedRow, changedColumnEnd);
    emit dataChanged(topLeft, bottomRight);
    
  • rowsRemoved() との関連
    • rowsRemoved() とは目的が異なります。rowsRemoved() は「構造の変更(行がなくなった)」を通知し、dataChanged() は「内容の変更(行は存在するがデータが変わった)」を通知します。
  • いつ使うか
    • 行が削除されるのではなく、行内の特定のセルの値が変更された場合。
    • 例えば、行が非表示になったり(しかしデータ自体は存在)、表示されるテキストが変わったりする場合。

これはQtのモデル/ビューアーキテクチャの意図に反するため、強く非推奨です。

  • デメリット
    • ビューはモデルの構造変更を正確に把握できないため、内部的なキャッシュやレイアウト情報が古くなり、表示の乱れ、パフォーマンスの問題、クラッシュの原因となる可能性が非常に高いです。
    • 選択状態や展開状態などが適切に管理されません。
    • Qtの設計思想に反しており、将来的な互換性やメンテナンス性が損なわれます。
  • 方法
    • モデルからデータが削除された後に、QTreeView オブジェクトに対して viewport()->update()repaint() を呼び出す。
    • または、QTreeView::reset() を呼び出す(これは内部的に modelReset() と似た動作をする可能性があります)。
  • dataChanged() は、行の削除ではなく、既存のデータの変更を通知するために使用します。
  • modelReset() は、モデル全体が再構築されるような大規模な変更の場合に検討する選択肢です。
  • 行の削除には、必ず beginRemoveRows()endRemoveRows() をモデル内で使用してください。 これが QTreeView::rowsRemoved() シグナルが発行される唯一の適切な方法であり、ビューが正しく更新されることを保証します。