【Qtプログラミング】QTableWidget::visualColumn()でよくあるエラーと解決策

2025-05-27

論理的な列番号 (logicalColumn) とは? これは、テーブルのデータ構造における列のインデックスです。通常、0から始まる数値で、データの並び順に対応しています。

表示上の列番号 (visual column) とは? これは、ユーザーが現在見ているテーブルの列の順序におけるインデックスです。ユーザーが列をドラッグ&ドロップして並び替えたり、列を非表示にしたりした場合、論理的な列番号と表示上の列番号は異なる場合があります。



想定と異なる結果が返される (Misinterpretation of Results)

問題
visualColumn()が返した値が、期待していた表示上の列番号と異なる。

原因

  • 無効な論理列番号(Invalid Logical Column)
    存在しない論理列番号(例えば、columnCount()を超える値や負の値)を引数に渡した場合、予期せぬ結果(-1やアサーションエラー)が発生する可能性があります。
  • 列の非表示(Hidden Columns)
    setColumnHidden(logicalColumn, true)などで列が非表示になっている場合、その列には表示上の列番号が存在しません。visualColumn()は非表示の列に対しては-1を返すことがあります(Qtのバージョンや挙動による)。
  • 列の並び替え(Sorting/Moving Columns)
    ユーザーが手動で列の並び替えを行った場合、またはプログラム的にhorizontalHeader()->moveSection()などで列の順序を変更した場合、論理的な列番号と表示上の列番号は一致しなくなります。visualColumn(logicalColumn)は、その時点での表示上の順序を返します。

トラブルシューティング

  • デバッグ出力で確認
    qDebug()などを使って、visualColumn()が返した値と、渡した論理列番号、およびテーブルの実際の表示順序を比較します。
  • columnCount()で範囲チェックを行う
    visualColumn()を呼び出す前に、引数として渡す論理列番号が0からcolumnCount() - 1の範囲内にあることを確認します。
  • 常にvisualColumn()を使用する場面を理解する
    ユーザーインターフェース(表示)に関連する操作(例: クリックされた列の取得、ドラッグ&ドロップのターゲット列の特定)ではvisualColumn()を使用し、データ処理(例: 特定のデータの取得・設定)では論理的な列番号(例: QTableWidgetItem::column()や、直接指定したインデックス)を使用することを明確に区別します。

QTableWidgetItemとの混同 (Confusion with QTableWidgetItem)

問題
QTableWidgetItemのメソッドと混同し、意図しない列番号を使用している。

原因
QTableWidgetItemにはcolumn()というメソッドがありますが、これはそのアイテムが元々属する論理的な列番号を返します。テーブルの並び替えが行われた場合でも、QTableWidgetItem::column()が返す値は変わりません。一方、QTableWidget::visualColumn()は、現在表示されている位置を返します。



例1: 論理列番号から表示列番号への変換

この例では、テーブルの初期設定を行い、その後、特定の論理列が現在画面上のどの位置に表示されているかを確認します。

#include <QtWidgets/QApplication>
#include <QtWidgets/QTableWidget>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QDebug> // デバッグ出力用

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

    QTableWidget tableWidget(3, 4); // 3行4列のテーブルを作成
    tableWidget.setWindowTitle("QTableWidget::visualColumn() Example 1");

    // ヘッダーラベルを設定(論理列番号に対応)
    QStringList headerLabels;
    headerLabels << "ID" << "名前" << "年齢" << "都市";
    tableWidget.setHorizontalHeaderLabels(headerLabels);

    // データ設定
    tableWidget.setItem(0, 0, new QTableWidgetItem("101"));
    tableWidget.setItem(0, 1, new QTableWidgetItem("Alice"));
    tableWidget.setItem(0, 2, new QTableWidgetItem("30"));
    tableWidget.setItem(0, 3, new QTableWidgetItem("Tokyo"));

    tableWidget.setItem(1, 0, new QTableWidgetItem("102"));
    tableWidget.setItem(1, 1, new QTableWidgetItem("Bob"));
    tableWidget.setItem(1, 2, new QTableWidgetItem("25"));
    tableWidget.setItem(1, 3, new QTableWidgetItem("Osaka"));

    tableWidget.setItem(2, 0, new QTableWidgetItem("103"));
    tableWidget.setItem(2, 1, new QTableWidgetItem("Charlie"));
    tableWidget.setItem(2, 2, new QTableWidgetItem("35"));
    tableWidget.setItem(2, 3, new QTableWidgetItem("Kyoto"));

    // ユーザーに列の並び替えを許可する
    tableWidget.horizontalHeader()->setSectionsMovable(true);

    tableWidget.show();

    // デバッグ出力
    qDebug() << "初期状態:";
    for (int i = 0; i < tableWidget.columnCount(); ++i) {
        qDebug() << "論理列" << i << "(" << headerLabels[i] << ")"
                 << " -> 表示列: " << tableWidget.visualColumn(i);
    }

    // ユーザーが手動で列を並び替えたと仮定
    // 例えば、論理列1("名前")を論理列0("ID")の前に移動したとします。
    // プログラム的に並び替える場合は horizontalHeader()->moveSection() を使います。
    // ユーザーが実際に操作しないと visualColumn の値は変わりません。
    // 以下の行は、プログラムで並び替える場合の例です。
    // tableWidget.horizontalHeader()->moveSection(1, 0); // 論理列1を論理列0の位置へ

    // ここでユーザーが列を並び替えるのを待つか、
    // moveSection() を呼び出してプログラム的に変更をシミュレートします。
    // この例では、ユーザーが手動で操作した後の状態を想定しています。

    // 例えば、ユーザーが "年齢" 列(論理列2)を一番左(表示列0)に移動した場合をシミュレート
    qDebug() << "\n'年齢' 列を一番左に移動 (ユーザー操作または moveSection(2, 0) 後) の状態:";
    // QHeaderView::moveSection は直接呼べないため、実際の動作をシミュレートするためには
    // ユーザー操作が必要です。ここでは、仮にユーザーが「年齢」を移動したと仮定し、
    // その後の visualColumn の値を確認する意図です。
    // あるいは、以下のように moveSection を使用してプログラミングで並び替えをシミュレートできます。
    tableWidget.horizontalHeader()->moveSection(2, 0); // 論理列2(年齢)を論理列0の位置へ

    qDebug() << "論理列0 (ID) -> 表示列: " << tableWidget.visualColumn(0); // 1
    qDebug() << "論理列1 (名前) -> 表示列: " << tableWidget.visualColumn(1); // 2
    qDebug() << "論理列2 (年齢) -> 表示列: " << tableWidget.visualColumn(2); // 0
    qDebug() << "論理列3 (都市) -> 表示列: " << tableWidget.visualColumn(3); // 3

    return a.exec();
}

解説

  1. QTableWidget を作成し、ヘッダーラベルとデータを設定します。
  2. tableWidget.horizontalHeader()->setSectionsMovable(true); を呼び出すことで、ユーザーが列のヘッダーをドラッグ&ドロップして列の順序を並び替えることができるようになります。
  3. 初期状態では、論理列番号と表示列番号は一致します。visualColumn(0)0 を、visualColumn(1)1 を返します。
  4. コード内で tableWidget.horizontalHeader()->moveSection(2, 0); を実行すると、論理列2("年齢")が一番左(表示列0)に移動します。この後、
    • tableWidget.visualColumn(0) (元の "ID") は 1 を返します。
    • tableWidget.visualColumn(1) (元の "名前") は 2 を返します。
    • tableWidget.visualColumn(2) (元の "年齢") は 0 を返します。

例2: ヘッダークリックイベントでの使用 (QHeaderView::sectionClicked)

この例では、テーブルのヘッダーがクリックされたときに、クリックされた列の論理番号と、それが現在表示上の何番目の列であるかをログ出力します。

#include <QtWidgets/QApplication>
#include <QtWidgets/QTableWidget>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QDebug>
#include <QtWidgets/QPushButton> // 追加
#include <QtWidgets/QVBoxLayout> // 追加
#include <QtWidgets/QWidget>     // 追加

class MyTableWindow : public QWidget
{
    Q_OBJECT

public:
    MyTableWindow(QWidget *parent = nullptr) : QWidget(parent)
    {
        tableWidget = new QTableWidget(3, 4, this);
        tableWidget->setWindowTitle("QTableWidget::visualColumn() Example 2");

        QStringList headerLabels;
        headerLabels << "ID" << "名前" << "年齢" << "都市";
        tableWidget->setHorizontalHeaderLabels(headerLabels);

        tableWidget->setItem(0, 0, new QTableWidgetItem("101"));
        tableWidget->setItem(0, 1, new QTableWidgetItem("Alice"));
        tableWidget->setItem(0, 2, new QTableWidgetItem("30"));
        tableWidget->setItem(0, 3, new QTableWidgetItem("Tokyo"));

        tableWidget->setItem(1, 0, new QTableWidgetItem("102"));
        tableWidget->setItem(1, 1, new QTableWidgetItem("Bob"));
        tableWidget.setItem(1, 2, new QTableWidgetItem("25"));
        tableWidget.setItem(1, 3, new QTableWidgetItem("Osaka"));

        tableWidget.setItem(2, 0, new QTableWidgetItem("103"));
        tableWidget.setItem(2, 1, new QTableWidgetItem("Charlie"));
        tableWidget.setItem(2, 2, new QTableWidgetItem("35"));
        tableWidget.setItem(2, 3, new QTableWidgetItem("Kyoto"));

        // ユーザーに列の並び替えを許可する
        tableWidget->horizontalHeader()->setSectionsMovable(true);

        // ヘッダーがクリックされたシグナルをスロットに接続
        connect(tableWidget->horizontalHeader(), &QHeaderView::sectionClicked,
                this, &MyTableWindow::onHeaderSectionClicked);

        // レイアウト
        QVBoxLayout *layout = new QVBoxLayout(this);
        layout->addWidget(tableWidget);
        setLayout(layout);
    }

private slots:
    void onHeaderSectionClicked(int logicalIndex)
    {
        qDebug() << "ヘッダーがクリックされました!";
        qDebug() << "クリックされた論理列番号 (logicalIndex): " << logicalIndex;
        qDebug() << "この論理列の現在の表示列番号 (visualColumn): " << tableWidget->visualColumn(logicalIndex);

        // 論理列番号に対応するヘッダーテキストを取得
        qDebug() << "ヘッダーテキスト: " << tableWidget->horizontalHeaderItem(logicalIndex)->text();
    }

private:
    QTableWidget *tableWidget;
};

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

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MyTableWindow window;
    window.resize(600, 400); // ウィンドウサイズ設定
    window.show();
    return a.exec();
}

解説

  1. QHeaderView::sectionClicked シグナルは、クリックされたヘッダーセクションの論理的なインデックスを引数として渡します。
  2. onHeaderSectionClicked スロット内で、この論理インデックスをtableWidget->visualColumn(logicalIndex)に渡すことで、クリックされた列が現在画面上のどの位置に表示されているかを取得できます。
  3. ユーザーが列を並び替えた後でも、logicalIndexは元のデータの列番号を指し続けますが、visualColumn()は常に現在の表示位置を反映します。

例3: 表示列番号から論理列番号への逆変換 (QTableWidget::logicalColumn())

visualColumn()の逆の操作として、表示されている列番号から元の論理列番号を取得するにはQTableWidget::logicalColumn()を使用します。これは、例えばitemAt()などで画面上の座標からアイテムを取得し、そのアイテムがどの表示列にあるかを知った後で、元のデータ列にアクセスしたい場合に便利です。

#include <QtWidgets/QApplication>
#include <QtWidgets/QTableWidget>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QDebug>
#include <QtWidgets/QVBoxLayout>
#include <QtWidgets/QWidget>

class MyTableWindowReverse : public QWidget
{
    Q_OBJECT

public:
    MyTableWindowReverse(QWidget *parent = nullptr) : QWidget(parent)
    {
        tableWidget = new QTableWidget(3, 4, this);
        tableWidget->setWindowTitle("QTableWidget::logicalColumn() Example");

        QStringList headerLabels;
        headerLabels << "ID" << "名前" << "年齢" << "都市";
        tableWidget->setHorizontalHeaderLabels(headerLabels);

        tableWidget->setItem(0, 0, new QTableWidgetItem("101"));
        tableWidget->setItem(0, 1, new QTableWidgetItem("Alice"));
        tableWidget->setItem(0, 2, new QTableWidgetItem("30"));
        tableWidget->setItem(0, 3, new QTableWidgetItem("Tokyo"));

        tableWidget->setItem(1, 0, new QTableWidgetItem("102"));
        tableWidget.setItem(1, 1, new QTableWidgetItem("Bob"));
        tableWidget.setItem(1, 2, new QTableWidgetItem("25"));
        tableWidget.setItem(1, 3, new QTableWidgetItem("Osaka"));

        tableWidget.setItem(2, 0, new QTableWidgetItem("103"));
        tableWidget.setItem(2, 1, new QTableWidgetItem("Charlie"));
        tableWidget.setItem(2, 2, new QTableWidgetItem("35"));
        tableWidget.setItem(2, 3, new QTableWidgetItem("Kyoto"));

        tableWidget->horizontalHeader()->setSectionsMovable(true);

        // 論理列2(年齢)を表示列0に移動
        tableWidget->horizontalHeader()->moveSection(2, 0);

        // セルがクリックされたときのシグナルを接続
        connect(tableWidget, &QTableWidget::cellClicked,
                this, &MyTableWindowReverse::onCellClicked);

        QVBoxLayout *layout = new QVBoxLayout(this);
        layout->addWidget(tableWidget);
        setLayout(layout);
    }

private slots:
    void onCellClicked(int row, int visualColumn)
    {
        qDebug() << "セルがクリックされました!";
        qDebug() << "クリックされた行: " << row;
        qDebug() << "クリックされた表示列番号 (visualColumn): " << visualColumn;

        // 表示列番号から元の論理列番号を取得
        int logicalColumn = tableWidget->logicalColumn(visualColumn);
        qDebug() << "対応する論理列番号 (logicalColumn): " << logicalColumn;

        // 論理列番号を使ってアイテムのテキストを取得(元のデータ)
        if (logicalColumn != -1) {
            QTableWidgetItem *item = tableWidget->item(row, logicalColumn);
            if (item) {
                qDebug() << "アイテムのテキスト (論理列から): " << item->text();
            }
        }
    }

private:
    QTableWidget *tableWidget;
};

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

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MyTableWindowReverse window;
    window.resize(600, 400);
    window.show();
    return a.exec();
}
  1. 初期化時に、プログラム的に列の並び替えを行います(moveSection(2, 0)で「年齢」列を先頭に移動)。
  2. QTableWidget::cellClicked シグナルは、クリックされたセルの行番号表示列番号を引数として渡します。
  3. onCellClicked スロット内で、受け取ったvisualColumntableWidget->logicalColumn(visualColumn)に渡すことで、元の論理列番号を取得します。
  4. この論理列番号を使ってtableWidget->item(row, logicalColumn)を呼び出すことで、並び替えられた状態であっても正確なデータにアクセスできます。


QHeaderView のメソッドを使用する

QTableWidget は内部的に QHeaderView を使用して列のヘッダーを管理しています。QHeaderView にも同様の目的のメソッドがあり、より柔軟な制御が必要な場合に利用できます。