Qt QTreeView: 列の移動を検知!columnMoved()シグナル徹底解説

2025-05-27

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

これは、QTreeViewのヘッダーセクション(通常、各列のタイトルが表示される部分)で、ユーザーが列をドラッグして位置を変更したときに発行されます。

もう少し詳しく説明すると:

  • シグナル (Signal): Qtの「シグナル&スロット」メカニズムの一部です。あるイベント(ここでは列の移動)が発生したときに、そのイベントを周囲に知らせるためのものです。シグナルは、そのイベントを処理する別の関数(スロット)に接続することができます。
  • QTreeView: Qtのウィジェットの一つで、階層的なデータをツリー形式で表示するために使われます。ファイルシステムのエクスプローラーのような表示を想像するとわかりやすいでしょう。

具体的な使用例

例えば、アプリケーションでユーザーが列の順序を変更したときに、その新しい順序を保存しておきたい場合などにこのシグナルを利用します。

// .h ファイル
class MyTreeView : public QTreeView
{
    Q_OBJECT
public:
    MyTreeView(QWidget *parent = nullptr);

private slots:
    void handleColumnMoved();
};

// .cpp ファイル
MyTreeView::MyTreeView(QWidget *parent)
    : QTreeView(parent)
{
    // columnMoved() シグナルを handleColumnMoved() スロットに接続
    connect(this, &QTreeView::columnMoved, this, &MyTreeView::handleColumnMoved);
}

void MyTreeView::handleColumnMoved()
{
    // 列が移動したときにここが呼び出される
    qDebug() << "列が移動しました!";

    // 必要であれば、現在の列の順序を取得して処理を行う
    // 例: header()->logicalIndex(int visualIndex) などを使って現在の列の順序を取得
}


シグナルが発されない、または期待通りに発されない

  • トラブルシューティング
    columnMoved()シグナルを受け取ったスロット内で、モデルの列の順序を実際に変更する処理(例えば、カスタムモデル内でbeginMoveColumns() / endMoveColumns()を呼び出す)が必要です。

  • 原因
    モデルの変更が適切に行われていない。QTreeViewは基となるQAbstractItemModel(またはその派生クラス)によってデータを管理しています。columnMoved()シグナルはビューの表示上の移動を示すものであり、モデルの列の順序を自動的に変更するわけではありません。

  • トラブルシューティング

    QHeaderView* header = treeView->header();
    if (header) {
        header->setSectionsMovable(true); // Qt 5以降
        // header->setMovable(true); // Qt 4の場合 (非推奨)
    }
    

    setSectionsMovable(true)を設定することで、ユーザーが列をドラッグして移動できるようになり、columnMoved()シグナルが発行されるようになります。

  • 原因
    QHeaderViewQTreeViewのヘッダー部分)のsetMovable(true)が設定されていない。デフォルトでは列の移動は無効になっている場合があります。

移動後の列のインデックスの取得ミス

  • トラブルシューティング
    QTreeView::columnMoved()シグナルを受け取ったスロット内で、QHeaderViewのメソッドを使用して現在の列の順序を取得する必要があります。

    • QHeaderView::logicalIndex(int visualIndex): 表示上のインデックス(ユーザーが見ている順序)から論理インデックス(元のモデル上のインデックス)を取得します。
    • QHeaderView::visualIndex(int logicalIndex): 論理インデックスから表示上のインデックスを取得します。
    • これらのメソッドを使って、列が移動した後の新しい順序を把握し、それに応じてデータを更新するなどの処理を行います。

    例:

    void MyTreeView::handleColumnMoved()
    {
        QHeaderView* header = this->header();
        if (header) {
            // 現在の表示上の列の順序を取得
            QList<int> currentLogicalOrder;
            for (int i = 0; i < header->count(); ++i) {
                currentLogicalOrder.append(header->logicalIndex(i));
            }
            qDebug() << "新しい論理的な列の順序:" << currentLogicalOrder;
    
            // この順序を保存したり、モデルに適用したりする
        }
    }
    
  • 原因
    columnMoved()シグナルには、移動前の列のインデックスや移動後の列のインデックスといった具体的な情報が直接含まれていません。このシグナルを受け取っただけでは、どの列がどこに移動したのかを把握することはできません。

モデルの列の順序とビューの列の順序の不一致

  • トラブルシューティング
    • カスタムモデルを使用している場合は、beginMoveColumns()endMoveColumns()を適切に呼び出すことで、ビューとモデルの状態を同期させます。
    • モデルの内部で列の順序を管理し、ビューからの変更通知に基づいてその順序を更新するロジックを実装します。
  • 原因
    columnMoved()シグナルはビューの表示上の変化を通知しますが、モデルのデータ構造自体は変更しません。もしユーザーの操作に合わせてモデルの列の順序も変更したい場合、シグナルを受け取ったスロット内で明示的にモデルを更新する必要があります。これを怠ると、ビューの表示とモデルのデータが一致しなくなり、後続のデータアクセスや保存で問題が生じます。

列の移動に関するUIの制約

  • トラブルシューティング
    • QHeaderView::setSectionMovable(int logicalIndex, bool movable)を使用して、特定の列の移動を個別に制御できます。
    • 例: 最初の列を移動不可にする
      QHeaderView* header = treeView->header();
      if (header) {
          header->setSectionsMovable(true); // 全体を移動可能に
          header->setSectionMovable(0, false); // 最初の列(論理インデックス0)は移動不可に
      }
      
  • 原因
    特定の列(例えば、常に先頭に表示させたい列)を移動させたくない場合、ユーザーがドラッグできてしまうとUXが悪化する可能性があります。
  • 原因
    columnMoved()シグナルが発火するたびに、重い処理(例: データベースへの保存、大規模なモデルの再構築)を実行すると、アプリケーションのパフォーマンスが低下する可能性があります。


例1: 列が移動したことをデバッグ出力で確認する

これは最も基本的な例で、シグナルが正しく接続され、発火しているかを確認するのに役立ちます。

main.cpp

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

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

    // QStandardItemModel を作成
    QStandardItemModel model(3, 4); // 3行4列のモデル
    model.setHeaderData(0, Qt::Horizontal, "列A");
    model.setHeaderData(1, Qt::Horizontal, "列B");
    model.setHeaderData(2, Qt::Horizontal, "列C");
    model.setHeaderData(3, Qt::Horizontal, "列D");

    // モデルにデータを設定 (省略)
    for (int row = 0; row < 3; ++row) {
        for (int col = 0; col < 4; ++col) {
            model.setItem(row, col, new QStandardItem(QString("データ(%0,%1)").arg(row).arg(col)));
        }
    }

    // QTreeView を作成
    QTreeView treeView;
    treeView.setModel(&model);

    // ヘッダービューを取得し、列を移動可能に設定
    QHeaderView *header = treeView.header();
    if (header) {
        header->setSectionsMovable(true); // これが重要!
    }

    // columnMoved() シグナルをラムダ式で接続
    QObject::connect(&treeView, &QTreeView::columnMoved, [&]() {
        qDebug() << "QTreeView::columnMoved() シグナルが発火しました!";

        // 現在の列の論理的な順序(モデル上のインデックス)を取得
        qDebug() << "現在の列の順序 (論理インデックス):";
        for (int i = 0; i < header->count(); ++i) {
            qDebug() << "  ビジュアルインデックス" << i << ": 論理インデックス" << header->logicalIndex(i);
        }
    });

    treeView.setWindowTitle("QTreeView columnMoved() 例");
    treeView.show();

    return a.exec();
}

解説

  1. QApplicationQTreeViewQStandardItemModel を含めます。
  2. QStandardItemModel を作成し、ヘッダーを設定します。
  3. QTreeView を作成し、モデルを設定します。
  4. treeView.header()->setSectionsMovable(true); が非常に重要です。これを設定しないと、ユーザーは列をドラッグして移動させることができず、columnMoved() シグナルも発火しません。
  5. QObject::connect を使用して、treeViewcolumnMoved() シグナルをラムダ式(またはスロット関数)に接続します。
  6. ラムダ式の中では、単にデバッグメッセージを出力します。さらに、QHeaderView::logicalIndex(int visualIndex) を使って、現在の列の表示上の順序(ビジュアルインデックス)から、対応するモデル上の元のインデックス(論理インデックス)を取得しています。これにより、列がどのように並べ替えられたかをプログラム的に把握できます。

このコードを実行し、QTreeView のヘッダーで列をドラッグすると、デバッグ出力にメッセージが表示されるのが確認できます。

例2: カスタムモデルで列の順序を永続化する

より実践的なシナリオでは、ユーザーが変更した列の順序を保存し、次回アプリケーション起動時に復元したい場合があります。この場合、QAbstractItemModel を継承したカスタムモデルを使用し、その中で列の移動を処理するのが一般的です。

この例では、簡略化のためモデル内部での列の並べ替えは行わず、デバッグ出力で新しい順序を確認するに留めますが、実際のアプリケーションではこの情報を設定ファイルなどに保存します。

MyCustomModel.h

#ifndef MYCUSTOMMODEL_H
#define MYCUSTOMMODEL_H

#include <QAbstractTableModel>
#include <QVariant>
#include <QList>

class MyCustomModel : public QAbstractTableModel
{
    Q_OBJECT
public:
    explicit MyCustomModel(QObject *parent = nullptr);

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;

    // モデル内の列の論理的な順序を更新するメソッド (簡易版)
    void updateColumnOrder(const QList<int>& newOrder);

private:
    QVector<QVector<QVariant>> m_data;
    QStringList m_headers;
    QList<int> m_currentLogicalOrder; // モデルが保持する現在の列の論理的な順序
};

#endif // MYCUSTOMMODEL_H

MyCustomModel.cpp

#include "MyCustomModel.h"
#include <QDebug>

MyCustomModel::MyCustomModel(QObject *parent)
    : QAbstractTableModel(parent)
{
    // ダミーデータを設定
    m_data.resize(3); // 3行
    m_headers << "First Name" << "Last Name" << "Age" << "City"; // 4列
    m_currentLogicalOrder << 0 << 1 << 2 << 3; // 初期順序

    m_data[0] << "Alice" << "Smith" << 30 << "New York";
    m_data[1] << "Bob" << "Johnson" << 24 << "London";
    m_data[2] << "Charlie" << "Brown" << 45 << "Paris";
}

int MyCustomModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;
    return m_data.size();
}

int MyCustomModel::columnCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;
    return m_headers.size(); // 論理的な列数
}

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

    if (role == Qt::DisplayRole) {
        // 論理インデックスを使ってデータを取得
        int logicalColumn = m_currentLogicalOrder.at(index.column()); // ビューの列インデックスからモデルのデータインデックスへ変換
        return m_data[index.row()][logicalColumn];
    }
    return QVariant();
}

QVariant MyCustomModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
        // 論理インデックスを使ってヘッダーを取得
        int logicalColumn = m_currentLogicalOrder.at(section);
        return m_headers.at(logicalColumn);
    }
    return QVariant();
}

void MyCustomModel::updateColumnOrder(const QList<int>& newOrder)
{
    // 実際にはここでは beginMoveColumns / endMoveColumns を使うべきですが、
    // 簡易例のため、ここではモデル内の順序を直接更新するだけです。
    // QTreeView::columnMoved() シグナルはビューの変更を通知するもので、
    // モデルの移動処理は別途行われる必要があります。
    m_currentLogicalOrder = newOrder;
    // layoutChanged() を発行してビューにモデルのレイアウト変更を通知することも検討する
    // emit layoutChanged(); // これを呼ぶとビューが再描画される
    qDebug() << "モデルの列順序が更新されました (論理インデックス):" << m_currentLogicalOrder;
}

main.cpp

#include <QApplication>
#include <QTreeView>
#include <QHeaderView>
#include "MyCustomModel.h" // 作成したカスタムモデル

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

    MyCustomModel *model = new MyCustomModel();

    QTreeView treeView;
    treeView.setModel(model);

    QHeaderView *header = treeView.header();
    if (header) {
        header->setSectionsMovable(true); // 列の移動を許可
    }

    // columnMoved() シグナルを接続
    QObject::connect(&treeView, &QTreeView::columnMoved, [&](int /*logicalIndex*/, int /*oldVisualIndex*/, int /*newVisualIndex*/) {
        // columnMoved シグナルには、論理インデックスと移動前後のビジュアルインデックスが引数として渡される
        // これらの引数は Qt 5.0 以降で追加されました。
        // Qt 4.x の場合は引数なしのシグナルになります。

        qDebug() << "QTreeView::columnMoved() シグナルが発火しました!";

        QList<int> newLogicalOrder;
        for (int i = 0; i < header->count(); ++i) {
            newLogicalOrder.append(header->logicalIndex(i));
        }
        qDebug() << "ビューの新しい列の順序 (論理インデックス):" << newLogicalOrder;

        // モデルに新しい列の順序を通知
        model->updateColumnOrder(newLogicalOrder);

        // ここで newLogicalOrder を設定ファイルやデータベースに保存するなどの処理を行う
    });

    treeView.setWindowTitle("QTreeView columnMoved() カスタムモデル例");
    treeView.show();

    return a.exec();
}

解説

  1. QAbstractTableModel を継承した MyCustomModel を作成します。このモデルは、m_currentLogicalOrder というリストで列の論理的な順序を内部的に管理します。
  2. data()headerData() メソッド内で、ビューから渡されるインデックス(ビジュアルインデックス)を、m_currentLogicalOrder を使ってモデルが保持する論理的なデータインデックスに変換しています。これにより、ビューで列の順序が変更されても、正しいデータが取得されるようになります。
  3. main.cppQTreeView::columnMoved() シグナルを接続します。
  4. シグナルが発火すると、スロット内で QHeaderView::logicalIndex() を再度使用して、ユーザーが移動させた後のビュー上の列の新しい論理的な順序を取得します。
  5. 取得した新しい順序を model->updateColumnOrder(newLogicalOrder); を通じてモデルに通知します。この例では updateColumnOrder が単純に内部リストを更新するだけですが、実際にはモデルがデータを再配置したり、beginMoveColumns() / endMoveColumns() を呼び出してビューに通知したりする複雑なロジックが含まれることがあります。

QTreeView::columnMoved() シグナルは、ユーザーがツリービューの列の表示順序を変更したときに発行されます。このシグナル自体は、どの列がどのように移動したかという具体的な情報(移動元や移動先のインデックスなど)を持ちません。そのため、このシグナルを受け取ったスロットでは、QHeaderViewQTreeViewのヘッダー部分)のメソッドを使って現在の列の順序を再取得する必要があります。

ここでは、最も一般的な使用例として、列の移動を検知し、その新しい順序をコンソールに出力するシンプルな例と、**カスタムモデルと連携して列の順序を永続化する(またはモデルの順序も変更する)**場合の概念的な例を紹介します。

例1: 列の移動を検知して新しい順序を出力する(シンプルな例)

この例では、QTreeViewが作成され、列の移動が許可され、columnMoved()シグナルがスロットに接続されます。スロットでは、移動後の列の論理的な順序(元のインデックス)を取得して表示します。

main.cpp

#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QHeaderView>
#include <QDebug>

// QTreeViewを継承して、シグナルを処理するクラス
class MyTreeView : public QTreeView
{
    Q_OBJECT // シグナルとスロットを使用するために必要

public:
    explicit MyTreeView(QWidget *parent = nullptr)
        : QTreeView(parent)
    {
        // ヘッダービューを取得し、列の移動を有効にする
        QHeaderView *header = this->header();
        if (header) {
            header->setSectionsMovable(true); // Qt 5 以降
            // header->setMovable(true); // Qt 4.x の場合はこちらを使用 (非推奨)
        }

        // columnMoved() シグナルをカスタムスロットに接続
        // QTreeViewはcolumnMoved()シグナルを直接持たないため、
        // 実際にはそのヘッダービューのsectionMoved()シグナルに接続します。
        // QTreeView::columnMoved()はprotected slotであり、QHeaderView::sectionMoved()が内部的に呼び出されます。
        // ここでは、QHeaderView::sectionMoved()を直接接続するのがより適切です。
        connect(header, &QHeaderView::sectionMoved, this, &MyTreeView::handleSectionMoved);
    }

private slots:
    // 列が移動したときに呼び出されるスロット
    void handleSectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex)
    {
        Q_UNUSED(logicalIndex); // この例では使用しないので警告を抑制
        Q_UNUSED(oldVisualIndex); // この例では使用しないので警告を抑制

        qDebug() << "列が移動しました!";
        qDebug() << "移動後のビジュアルインデックス (論理インデックス):" << newVisualIndex;

        // 現在の列の表示順序(論理インデックスのリスト)を取得
        QHeaderView *header = this->header();
        if (header) {
            QStringList currentOrder;
            for (int i = 0; i < header->count(); ++i) {
                // visualIndex (表示上の位置) から logicalIndex (元のモデル上の位置) を取得
                int logicalIdx = header->logicalIndex(i);
                currentOrder << QString("Col%1").arg(logicalIdx); // 例として "Col0", "Col1" のように表示
            }
            qDebug() << "現在の列の論理的な表示順序:" << currentOrder.join(", ");
        }
    }
};

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

    // モデルの作成
    QStandardItemModel model(3, 4); // 3行4列のモデル
    for (int row = 0; row < 3; ++row) {
        for (int col = 0; col < 4; ++col) {
            QStandardItem *item = new QStandardItem(QString("R%1C%2").arg(row).arg(col));
            model.setItem(row, col, item);
        }
    }

    // ヘッダーデータのセット
    model.setHeaderData(0, Qt::Horizontal, "列A");
    model.setHeaderData(1, Qt::Horizontal, "列B");
    model.setHeaderData(2, Qt::Horizontal, "列C");
    model.setHeaderData(3, Qt::Horizontal, "列D");


    // TreeView のインスタンス化
    MyTreeView treeView;
    treeView.setModel(&model);

    treeView.setWindowTitle("QTreeView Column Move Example");
    treeView.resize(600, 300);
    treeView.show();

    return app.exec();
}

#include "main.moc" // moc ファイルのインクルード (Q_OBJECTを使用する場合、通常はこのようにします)

解説

  1. MyTreeView クラス
    QTreeViewを継承し、列の移動を処理するためのロジックをカプセル化しています。
  2. QHeaderView::setSectionsMovable(true)
    これが非常に重要です。この設定がないと、ユーザーは列をドラッグして移動させることができません。
  3. connect(header, &QHeaderView::sectionMoved, ...)
    QTreeViewcolumnMoved()シグナルは実際にはQHeaderViewが発行するsectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex)シグナルと関連しています。QTreeViewは内部的にこのシグナルを受け取って自身のcolumnMoved()プロテクテッドスロットを呼び出します。ユーザーが直接扱う場合は、QHeaderView::sectionMoved()に接続するのがより直感的で、移動前後の具体的なインデックスも取得できます。
    • logicalIndex: 移動した列の元の(モデル上の)インデックスです。
    • oldVisualIndex: 移動前の表示上のインデックスです。
    • newVisualIndex: 移動後の表示上のインデックスです。
  4. handleSectionMoved() スロット
    このスロットが列の移動時に呼び出されます。ここでは、QHeaderView::logicalIndex(int visualIndex) を使って、現在の表示順序に基づいて各列の元の論理インデックスを取得し、コンソールに出力しています。

この例は、ユーザーの列の移動に合わせて、基盤となるカスタムモデルの列の順序も変更する場合の概念的なコードです。これはより複雑ですが、実用的なアプリケーションではよく必要になります。

MyModel.h (カスタムモデルのヘッダーファイル)

#ifndef MYMODEL_H
#define MYMODEL_H

#include <QAbstractTableModel> // 列の移動を考慮する場合、QAbstractTableModel がより適切

class MyModel : public QAbstractTableModel
{
    Q_OBJECT

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

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;

    // 列の移動をサポートするために必要
    bool moveColumns(const QModelIndex &sourceParent, int sourceColumn, int count,
                     const QModelIndex &destinationParent, int destinationChild) override;

    // 現在の列の順序を取得
    QList<int> getColumnOrder() const;

private:
    QVector<QStringList> m_data; // 実際のデータ
    QList<int> m_columnOrder;    // 列の論理的な順序を管理するリスト
};

#endif // MYMODEL_H

MyModel.cpp (カスタムモデルの実装ファイル)

#include "MyModel.h"
#include <QDebug>

MyModel::MyModel(QObject *parent)
    : QAbstractTableModel(parent)
{
    // ダミーデータを初期化
    m_data.resize(3); // 3行
    for (int i = 0; i < m_data.size(); ++i) {
        m_data[i].resize(4); // 4列
        for (int j = 0; j < m_data[i].size(); ++j) {
            m_data[i][j] = QString("R%1C%2").arg(i).arg(j);
        }
    }

    // 初期列順序: 0, 1, 2, 3
    for (int i = 0; i < columnCount(); ++i) {
        m_columnOrder.append(i);
    }
}

int MyModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;
    return m_data.size();
}

int MyModel::columnCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;
    return m_columnOrder.size(); // 管理している列の数
}

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

    if (role == Qt::DisplayRole) {
        int row = index.row();
        int visualColumn = index.column(); // ビューからの列インデックス
        int logicalColumn = m_columnOrder.at(visualColumn); // 論理インデックスに変換

        if (row >= 0 && row < m_data.size() &&
            logicalColumn >= 0 && logicalColumn < m_data[row].size()) {
            return m_data[row][logicalColumn];
        }
    }
    return QVariant();
}

QVariant MyModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
        // ヘッダーも論理インデックスに基づいて表示する
        int logicalColumn = m_columnOrder.at(section);
        switch (logicalColumn) {
            case 0: return "製品名";
            case 1: return "数量";
            case 2: return "単価";
            case 3: return "備考";
            default: return QVariant();
        }
    }
    return QAbstractTableModel::headerData(section, orientation, role);
}

// 列の移動を処理する重要なメソッド
bool MyModel::moveColumns(const QModelIndex &sourceParent, int sourceColumn, int count,
                          const QModelIndex &destinationParent, int destinationChild)
{
    if (sourceParent.isValid() || destinationParent.isValid()) // ツリーモデルの場合は親インデックスも考慮
        return false; // シンプルなテーブルモデルでは親は無効

    if (sourceColumn < 0 || sourceColumn >= m_columnOrder.size() ||
        destinationChild < 0 || destinationChild > m_columnOrder.size() ||
        count <= 0) {
        return false;
    }

    // Qt のモデル/ビューフレームワークのルールに従い、
    // 変更が開始される前に beginMoveColumns() を呼び出す
    if (!beginMoveColumns(sourceParent, sourceColumn, sourceColumn + count - 1,
                          destinationParent, destinationChild)) {
        return false;
    }

    // 実際の列の順序の変更ロジック
    // m_columnOrder リスト内の要素を移動
    QList<int> movedColumns;
    for (int i = 0; i < count; ++i) {
        movedColumns.append(m_columnOrder.takeAt(sourceColumn));
    }

    int destIndex = destinationChild;
    if (sourceColumn < destinationChild) { // 後ろに移動する場合
        destIndex -= count; // 移動した分だけ挿入位置を調整
    }

    for (int i = 0; i < count; ++i) {
        m_columnOrder.insert(destIndex + i, movedColumns.at(i));
    }

    // 変更が完了したことをビューに通知
    endMoveColumns();

    qDebug() << "モデルの列順が変更されました:" << m_columnOrder;

    return true;
}

QList<int> MyModel::getColumnOrder() const
{
    return m_columnOrder;
}

main.cpp (更新版)

#include <QApplication>
#include <QTreeView>
#include <QHeaderView>
#include <QDebug>
#include "MyModel.h" // 作成したカスタムモデルをインクルード

// QTreeViewを継承して、シグナルを処理するクラス
class MyTreeView : public QTreeView
{
    Q_OBJECT

public:
    explicit MyTreeView(MyModel *model, QWidget *parent = nullptr)
        : QTreeView(parent)
        , m_myModel(model)
    {
        setModel(m_myModel); // モデルをセット

        QHeaderView *header = this->header();
        if (header) {
            header->setSectionsMovable(true); // 列の移動を有効に

            // QHeaderView::sectionMoved シグナルを接続
            // このシグナルから移動元と移動先のビジュアルインデックスが取得できる
            connect(header, &QHeaderView::sectionMoved, this, &MyTreeView::handleSectionMoved);
        }
    }

private slots:
    void handleSectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex)
    {
        qDebug() << "QTreeView: 列が移動しました!";
        qDebug() << "  論理インデックス:" << logicalIndex;
        qDebug() << "  古い表示インデックス:" << oldVisualIndex;
        qDebug() << "  新しい表示インデックス:" << newVisualIndex;

        // モデルに対して列の移動を指示
        // QTreeViewは内部的にQAbstractItemModel::moveColumns()を呼び出すため、
        // このシグナルを受け取って再度moveColumns()を呼び出す必要は通常ありません。
        // ただし、モデルがmoveColumns()をサポートしていない場合や、
        // 移動後に特定の永続化処理を行う場合などには、ここでカスタムロジックを追加できます。

        // 例として、モデルの現在の列順序を出力してみる
        qDebug() << "モデルから取得した現在の列の論理的な順序:" << m_myModel->getColumnOrder();
    }

private:
    MyModel *m_myModel;
};

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

    MyModel myModel; // カスタムモデルのインスタンスを作成

    MyTreeView treeView(&myModel); // カスタムモデルを渡す
    treeView.setWindowTitle("QTreeView Column Move with Custom Model Example");
    treeView.resize(600, 300);
    treeView.show();

    return app.exec();
}

#include "main.moc" // moc ファイルのインクルード

解説

  1. MyModel クラス
    • QAbstractTableModelを継承しています。ツリービューですが、列の概念はテーブルモデルに近いため、例としてこれを使用します。
    • m_data: 実際のデータを保持します。
    • m_columnOrder: これが重要で、ビューの表示上の列順序とモデルの論理的な列順序を対応させるためのリストです。例えば、m_columnOrder = {2, 0, 3, 1} であれば、ビューの0列目にはモデルの2列目のデータが、ビューの1列目にはモデルの0列目のデータが表示されることを意味します。
    • data()headerData(): これらは m_columnOrder を利用して、ビューからの要求(表示上の列インデックス)をモデルの実際の論理的な列インデックスに変換しています。
    • moveColumns() のオーバーライド
      QAbstractItemModelmoveColumns()をオーバーライドし、ビューからの列移動要求を処理します。
      • beginMoveColumns()endMoveColumns(): これらはQtのモデル/ビューフレームワークで非常に重要です。データの変更(移動)を行う前にbeginMoveColumns()を呼び出し、完了後にendMoveColumns()を呼び出すことで、ビューはモデルの変更を正しく認識し、自身を更新します。
      • 実際の列順序の更新
        m_columnOrder リスト内で、指定された範囲の要素を新しい位置に移動させるロジックを実装します。これは、モデルがビューの要求に応じてその内部状態を更新する部分です。
  2. MyTreeView クラス (更新版)
    • コンストラクタでMyModelのインスタンスを受け取り、setModel()で設定します。
    • handleSectionMoved() スロットは、QHeaderView::sectionMovedシグナルから移動元の論理インデックス、古い表示インデックス、新しい表示インデックスを受け取ります。
    • 重要
      QTreeView(とQTableView)は、ユーザーがヘッダーをドラッグして列を移動させたときに、内部的に接続されているモデルのmoveColumns()メソッドを自動的に呼び出そうとします。したがって、QTreeView::columnMoved()シグナルやQHeaderView::sectionMoved()シグナルを受け取ったスロットの中で、再度自分でmodel()->moveColumns(...)を呼び出す必要は通常ありません。モデルのmoveColumns()が正しく実装されていれば、ビューは自動的にモデルの変更を反映します。この例のhandleSectionMovedスロットは、列移動の事実を確認したり、移動後のモデルの順序をデバッグ出力したりするためのものです。
    • もし、モデルのmoveColumns()false を返す(移動を許可しない)場合でも、ビューの表示は一時的に移動しますが、その後元の位置に戻る可能性があります。

コンパイルと実行の注意点

これらの例は、Qt のデスクトップアプリケーションとしてコンパイルします。

  1. .pro ファイルに QT += widgets を追加します。
  2. main.cpp の最後に #include "main.moc" を追加し、MyModel.h も同様に moc ファイルが必要な場合は対応します。
  3. QMake を使用してプロジェクトファイルを生成し、ビルドします。

<!-- end list -->

qmake -project
qmake
make # または nmake, jom など


このシグナル自体が主要な機能であり、その目的は「列が移動したことをアプリケーションに通知する」ことです。したがって、直接的な「代替方法」というよりは、columnMoved() シグナルがカバーしない、あるいは異なるアプローチで列の順序変更を扱う方法について説明するのが適切でしょう。

主に以下のシナリオが考えられます。

columnMoved() を使用しない場合(または使用できない場合)

a. プログラムによる列の順序変更 (User Interactionなし)

ユーザーの操作ではなく、プログラム的に列の順序を変更したい場合は、columnMoved() シグナルは不要です。代わりに、QHeaderView のメソッドを使用します。

  • カスタムモデルでの moveColumns() の実装
    もし、ビューの表示順序だけでなく、基盤となるモデル自体の列の順序も変更したい場合は、QAbstractItemModel またはその派生クラス(例: QAbstractTableModel)で moveColumns() メソッドをオーバーライドして実装する必要があります。 QTreeView は、ユーザーが列をドラッグして移動させたときに、内部的にこの moveColumns() メソッドを呼び出そうとします。したがって、モデルがこのメソッドを適切に実装していれば、ビューとモデルの列順序を同期させることができます。 この方法は、columnMoved() シグナルの代替というよりは、columnMoved() シグナルを受け取るべきイベント(ユーザーによる列移動)の際にモデル側で何が起こるべきか、という側面です。

    特徴

    • モデルのデータ構造自体を変更します。
    • beginMoveColumns() / endMoveColumns() を適切に呼び出す必要があります。
    • 複雑なデータモデルを扱う場合に必要となります。
  • QHeaderView::moveSection(int from, int to)
    最も直接的な方法です。指定したヘッダーセクション(列)を、from の位置から to の位置へ移動させます。これは、ビューの表示順序のみを変更し、基盤となるモデルのデータ順序は変更しません。 このメソッドを呼び出すと、QHeaderView::sectionMoved() シグナルが発行され、結果的にQTreeView::columnMoved() シグナルも発行されます。

    // treeView のヘッダーを取得
    QHeaderView *header = treeView->header();
    if (header) {
        // 論理インデックス0の列を、ビジュアルインデックス2の位置に移動
        header->moveSection(header->visualIndex(0), 2);
    }
    

columnMoved() シグナル自体は情報量が少ないため、関連する他のシグナルやメソッドを組み合わせることで、より詳細な制御や永続化が可能になります。

a. QHeaderView::sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) シグナルの直接利用

これは、QTreeView::columnMoved() シグナルの事実上の直接のソースです。QTreeView::columnMoved() は保護されたスロットであり、QHeaderView からの発火を内部的に処理します。ユーザーが直接接続する場合は、QHeaderView::sectionMoved() を使用する方が推奨されます。

特徴

  • この情報を使って、アプリケーションの設定ファイルに列の順序を保存したり、データベースに書き込んだりするなどの永続化処理を実装できます。
  • これにより、どの列がどこからどこへ移動したかを正確に把握できます。
  • 移動した列の論理インデックス、移動前のビジュアルインデックス、移動後の新しいビジュアルインデックスという、具体的な情報が引数として提供されます。


// QTreeView のインスタンスがあると仮定
QTreeView *treeView = new QTreeView(this);
// ... モデルの設定など ...

// QHeaderView を取得
QHeaderView *header = treeView->header();
if (header) {
    header->setSectionsMovable(true); // 列の移動を有効に

    // sectionMoved シグナルを接続
    connect(header, &QHeaderView::sectionMoved, [header](int logicalIndex, int oldVisualIndex, int newVisualIndex) {
        qDebug() << "Section moved:";
        qDebug() << "  Logical Index:" << logicalIndex;
        qDebug() << "  Old Visual Index:" << oldVisualIndex;
        qDebug() << "  New Visual Index:" << newVisualIndex;

        // 例: 現在の全列の順序を再取得
        QStringList currentOrder;
        for (int i = 0; i < header->count(); ++i) {
            currentOrder << QString("Col%1").arg(header->logicalIndex(i));
        }
        qDebug() << "Current Logical Order:" << currentOrder.join(", ");

        // ここで設定ファイルに保存するなどの処理を実装
    });
}

b. 列の順序の永続化 (Settings)

ユーザーが変更した列の順序をアプリケーションを閉じても保持したい場合、QSettings クラスなどを使って列の順序を保存・復元するのが一般的な代替(または付加的な)方法です。

方法

  1. QHeaderView::sectionMoved() シグナルを受け取った際に、現在のヘッダーの論理インデックスの配列(またはリスト)を取得します。
    • QHeaderView::logicalIndex(int visualIndex) をループで呼び出して、すべての列の論理インデックスを取得します。
  2. 取得した順序を QSettings に保存します(例: QSettings::setValue("columnOrder", QVariant(yourOrderList));)。
  3. アプリケーションの起動時、または QTreeView の初期化時に、保存された設定を QSettings から読み込みます。
  4. 読み込んだ順序に基づいて、QHeaderView::moveSection() を使用してプログラム的に列の順序を復元します。

例 (概念)

// 保存:
void MyWidget::saveColumnOrder()
{
    QSettings settings;
    QHeaderView *header = treeView->header();
    if (header) {
        QList<int> logicalOrder;
        for (int i = 0; i < header->count(); ++i) {
            logicalOrder.append(header->logicalIndex(i));
        }
        settings.setValue("myTreeView/columnOrder", QVariant::fromValue(logicalOrder)); // QList<int>を保存
    }
}

// 復元:
void MyWidget::restoreColumnOrder()
{
    QSettings settings;
    QHeaderView *header = treeView->header();
    if (header) {
        QList<int> savedOrder = settings.value("myTreeView/columnOrder").value<QList<int>>();
        if (!savedOrder.isEmpty() && savedOrder.size() == header->count()) {
            // 現在のビジュアルインデックスを論理インデックスに対応させておく
            QMap<int, int> currentLogicalToVisual;
            for (int i = 0; i < header->count(); ++i) {
                currentLogicalToVisual[header->logicalIndex(i)] = i;
            }

            // 保存された順序に基づいて、列を移動
            for (int visualIndex = 0; visualIndex < savedOrder.size(); ++visualIndex) {
                int targetLogicalIndex = savedOrder.at(visualIndex);
                int currentVisualIndex = currentLogicalToVisual.value(targetLogicalIndex);

                // もし現在のビジュアルインデックスがすでに正しい位置にあれば移動不要
                if (currentVisualIndex != visualIndex) {
                    header->moveSection(currentVisualIndex, visualIndex);
                    // moveSectionを呼び出すとvisualIndexが変更されるため、currentLogicalToVisualを更新する必要がある場合がある
                    // または、一連の移動後に最終的な状態を再構築するロジックにする
                }
            }
        }
    }
}

QTreeView::columnMoved() シグナルは、列の移動というイベントが発生したことを通知する基本的なメカニズムです。

  • 列の順序をアプリケーション間で永続化したい場合は、QHeaderView の状態を**QSettings などで保存・復元する**ロジックを追加します。
  • モデル自体の列順序を変更したい場合は、カスタムモデルで moveColumns() を実装する必要があります。
  • プログラムから列の順序を変更したい場合は、QHeaderView::moveSection() を使用します。
  • 最も直接的な代替/補完は、QHeaderView::sectionMoved() シグナルを直接利用することです。これにより、より詳細な移動情報が得られます。