QListWidget::sortItems()だけじゃない!Qtで実現する高度なリストソートの代替手法

2025-05-31

QListWidget::sortItems() とは?

QListWidget::sortItems() は、Qtの QListWidget クラスが提供する便利な関数で、リストウィジェット内のすべてのアイテムを特定の順序でソート(並び替え)するために使用されます。

QListWidget は、複数の項目をリスト形式で表示し、選択したり操作したりできるUIコンポーネントです。このウィジェットに追加された各項目は QListWidgetItem オブジェクトとして管理されます。

関数のシグネチャ

void QListWidget::sortItems(Qt::SortOrder order = Qt::AscendingOrder)
  • order: ソートの順序を指定する引数です。Qt::SortOrder 型で指定します。
    • Qt::AscendingOrder (デフォルト): 昇順(AからZ、小さい数字から大きい数字へ)でソートします。
    • Qt::DescendingOrder: 降順(ZからA、大きい数字から小さい数字へ)でソートします。

動作の仕組み

sortItems() メソッドが呼び出されると、QListWidget は内部的に保持している QListWidgetItem のリストをソートします。

ソートのカスタマイズ

もし、デフォルトのテキストベースのソートではなく、独自の基準でソートを行いたい場合は、以下の方法があります。

  1. QListWidgetItem::operator< をオーバーロードする: QListWidgetItem を継承したカスタムクラスを作成し、その中で比較演算子 operator< をオーバーロードします。sortItems() は、このオーバーロードされた演算子を使用してアイテムを比較し、ソートを行います。これにより、アイテムに紐付けられたカスタムデータや、テキスト以外の属性に基づいてソートロジックを定義できます。

    class CustomListWidgetItem : public QListWidgetItem
    {
    public:
        CustomListWidgetItem(const QString &text, int customValue)
            : QListWidgetItem(text), m_customValue(customValue) {}
    
        bool operator<(const QListWidgetItem &other) const override {
            // 例: テキストの長さでソートする
            return text().length() < other.text().length();
            // 例: カスタムの数値でソートする
            // return m_customValue < static_cast<const CustomListWidgetItem&>(other).m_customValue;
        }
    
    private:
        int m_customValue;
    };
    
    // 使用例
    QListWidget* listWidget = new QListWidget();
    listWidget->addItem(new CustomListWidgetItem("Banana", 2));
    listWidget->addItem(new CustomListWidgetItem("Apple", 1));
    listWidget->addItem(new CustomListWidgetItem("Cherry", 3));
    listWidget->sortItems(Qt::AscendingOrder); // CustomListWidgetItem::operator< が使われる
    
  2. QListView とカスタムモデルを使用する: QListWidget は便利なクラスですが、より高度なデータ管理やソートの柔軟性が必要な場合は、QListView とカスタムのデータモデル(QAbstractListModel から派生)を組み合わせるのが推奨されます。このアプローチでは、データと表示を完全に分離できるため、ソートロジックをモデル内で自由に実装でき、QSortFilterProxyModel を介してフィルタリングなども容易に行えます。

  • setSortingEnabled(true) を呼び出すと、新しいアイテムが追加されるたびに自動的にソートされるようになります。これを無効にするには setSortingEnabled(false) を使用します。
  • ソート後、リストウィジェットの表示が自動的に更新されます。
  • sortItems() は、QListWidget の内部的なソート処理を実行します。


QListWidget::sortItems() は便利な機能ですが、意図しない挙動やソート結果になることがあります。ここでは、よくある問題とその解決策を説明します。

数字が正しくソートされない (文字列比較の問題)

問題
数字を含むアイテムをソートすると、"1", "10", "2" のように、文字列として辞書順にソートされてしまうことがあります。これは、QListWidgetItem のデフォルトのソートがアイテムのテキスト(QString)に基づいて行われるためです。"10" は文字コード的に "2" の前に来るため、このような結果になります。


期待するソート: 1, 2, 10, 100 実際のソート: 1, 10, 100, 2

解決策

  • QSortFilterProxyModel を使用する (より複雑なケース)
    QListWidget の代わりに QListView と、カスタムモデル (QAbstractListModel を継承) および QSortFilterProxyModel を組み合わせることで、より柔軟なソートロジックを実装できます。QSortFilterProxyModel を使用すると、元のモデルのデータを変更せずにソートやフィルタリングを適用できます。これは、大量のデータや複雑なソート基準が必要な場合に特に有効です。

  • QListWidgetItem::setData(Qt::DisplayRole, ...) を利用する
    QListWidgetItem には様々な役割 (Role) のデータを設定できます。Qt::DisplayRole 以外のロールに数値データを格納し、ソート時にそのロールを使用するようにすることも可能ですが、QListWidget::sortItems() はデフォルトで Qt::DisplayRole(テキスト)を使用するため、上記の operator< オーバーロードがより直接的です。

  • QListWidgetItem::operator< をオーバーロードする (推奨)
    数値としてソートしたい場合は、QListWidgetItem を継承したカスタムクラスを作成し、operator< をオーバーロードして数値比較を実装します。

    #include <QListWidget>
    #include <QListWidgetItem>
    #include <QString>
    #include <QDebug> // デバッグ用
    
    // カスタムQListWidgetItemクラス
    class NumericListWidgetItem : public QListWidgetItem
    {
    public:
        NumericListWidgetItem(const QString &text)
            : QListWidgetItem(text)
        {
            // アイテムのテキストが数値であることを前提として、数値として保存
            bool ok;
            m_value = text.toInt(&ok);
            if (!ok) {
                m_value = 0; // 変換失敗時はデフォルト値
            }
        }
    
        // 比較演算子をオーバーロード
        bool operator<(const QListWidgetItem &other) const override
        {
            // otherがNumericListWidgetItem型であることを確認(安全のため)
            const NumericListWidgetItem *otherNumeric = dynamic_cast<const NumericListWidgetItem *>(&other);
            if (otherNumeric) {
                return m_value < otherNumeric->m_value;
            }
            // 型が異なる場合はデフォルトのテキスト比較に戻すか、エラー処理を行う
            return text() < other.text();
        }
    
    private:
        int m_value; // 数値としてソートするための値
    };
    
    // 使用例
    void setupListWidget(QListWidget* listWidget) {
        listWidget->addItem(new NumericListWidgetItem("10"));
        listWidget->addItem(new NumericListWidgetItem("2"));
        listWidget->addItem(new NumericListWidgetItem("100"));
        listWidget->addItem(new NumericListWidgetItem("1"));
    
        qDebug() << "ソート前:";
        for (int i = 0; i < listWidget->count(); ++i) {
            qDebug() << listWidget->item(i)->text();
        }
    
        listWidget->sortItems(Qt::AscendingOrder);
    
        qDebug() << "ソート後 (数値ソート):";
        for (int i = 0; i < listWidget->count(); ++i) {
            qDebug() << listWidget->item(i)->text();
        }
    }
    

カスタムソート (オーバーロードした operator<) が機能しない

問題
QListWidgetItem を継承し、operator< をオーバーロードしたにもかかわらず、sortItems() が意図通りにソートしてくれない。

原因と解決策

  • 異なる型のアイテムが混在している
    QListWidget に、カスタムの QListWidgetItem と標準の QListWidgetItem が混在している場合、ソートの挙動が予測不能になることがあります。sortItems() は、比較を行う際に QListWidgetItem::operator< を呼び出すため、異なる型のアイテム間で比較を行う際に問題が生じます。

    解決策
    QListWidget に追加するアイテムは、すべて同じカスタム QListWidgetItem の型に統一することを強く推奨します。もし異なる型のアイテムを比較する必要がある場合は、オーバーロードした operator< の中で dynamic_cast などを用いて型安全な比較を行う必要があります。

  • 正しいシグネチャでオーバーロードしていない
    QListWidgetItem::operator< のシグネチャは virtual bool operator<(const QListWidgetItem &other) const です。このシグネチャと完全に一致するようにオーバーロードしないと、ベースクラスの比較演算子が呼び出されてしまいます。C++11以降では override キーワードを使用すると、オーバーロードが正しく行われているかコンパイラがチェックしてくれます。

    誤った例

    bool operator<(const MyCustomItem &other) const { // MyCustomItem型で比較
        // ...
    }
    

    正しい例

    // MyCustomItem クラス内で
    bool operator<(const QListWidgetItem &other) const override {
        // other が MyCustomItem 型であることを確認してから比較
        const MyCustomItem *otherMyCustom = dynamic_cast<const MyCustomItem *>(&other);
        if (otherMyCustom) {
            // MyCustomItem の特定のメンバ変数で比較
            return this->myCustomValue < otherMyCustom->myCustomValue;
        }
        // そうでない場合は、デフォルトのテキスト比較に戻すか、エラーをログに出す
        return QListWidgetItem::operator<(other);
    }
    

ロケールによるソート順の問題

原因
QListWidget::sortItems() は、デフォルトで QString::localeAwareCompare() を使用して文字列を比較します。これはシステムの現在のロケール設定に依存するため、環境によってソート順が異なる可能性があります。

解決策

  • カスタムの QListWidgetItem::operator< で独自の比較ロジックを実装する
    operator< の中で、QString::compare() メソッドのオーバーロード版(QString::compare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs, Qt::LocaleAwareCompare lc))を使用し、Qt::CaseSensitivity (大文字小文字の区別) や Qt::LocaleAwareCompare (ロケール依存の比較) を細かく制御することができます。

    bool operator<(const QListWidgetItem &other) const override {
        // 大文字小文字を区別しない、ロケールに依存した比較
        return QString::compare(text(), other.text(), Qt::CaseInsensitive, Qt::LocaleAwareCompare) < 0;
    }
    

setSortingEnabled(true) を使用している場合の挙動

問題
listWidget->setSortingEnabled(true) を設定していると、アイテムを追加したり編集したりするたびに自動的にソートが走ります。これにより、パフォーマンスの問題が発生したり、一時的に意図しない順序になったりする可能性があります。

解決策

  • 一時的にソートを無効にする
    複数のアイテムを一括で追加したり、多くの変更を加える場合は、一時的にソートを無効にし、処理が完了した後に再度有効にして sortItems() を呼び出すのが効果的です。

    listWidget->setSortingEnabled(false); // ソートを一時的に無効化
    
    // 複数のアイテムを追加・変更する処理
    for (int i = 0; i < 1000; ++i) {
        listWidget->addItem(QString("Item %1").arg(i));
    }
    
    listWidget->setSortingEnabled(true);  // ソートを有効化
    listWidget->sortItems();              // 明示的にソートを呼び出す
    

QListWidgetItem をヒープではなくスタックに作成している

問題
QListWidgetItem をスタック(ローカル変数)に作成し、そのポインタを QListWidget::addItem() に渡すと、関数スコープを抜けた瞬間にオブジェクトが破棄され、QListWidget が不正なポインタを参照してクラッシュしたり、予期せぬ挙動を示したりします。

例 (誤り)

void addMyItem(QListWidget* listWidget) {
    QListWidgetItem item("My Item"); // スタックに作成
    listWidget->addItem(&item);      // 誤り!
} // item がここで破棄される

解決策
QListWidgetItem は必ずヒープに作成し、new でインスタンス化し、その所有権を QListWidget に渡すようにします。QListWidget は追加された QListWidgetItem の所有権を持つため、QListWidget が破棄される際に自動的にこれらのアイテムも削除されます。

void addMyItem(QListWidget* listWidget) {
    QListWidgetItem *item = new QListWidgetItem("My Item"); // ヒープに作成
    listWidget->addItem(item); // 正しい!
}


ここでは、QListWidget::sortItems() の基本的な使い方から、カスタムソートの実現方法まで、いくつかの例を挙げます。

例 1: 基本的なソート(テキストによる昇順・降順)

最も基本的な使い方です。QListWidgetItem のテキストに基づいてソートします。

main.cpp

#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QPushButton>
#include <QListWidget>

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

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

    QListWidget *listWidget = new QListWidget(centralWidget);
    listWidget->addItem("Banana");
    listWidget->addItem("Apple");
    listWidget->addItem("Cherry");
    listWidget->addItem("Date");
    listWidget->addItem("Apricot"); // "Apple" と "Apricot" の順序を確認

    QPushButton *ascButton = new QPushButton("昇順ソート (A-Z)", centralWidget);
    QObject::connect(ascButton, &QPushButton::clicked, [listWidget]() {
        listWidget->sortItems(Qt::AscendingOrder);
    });

    QPushButton *descButton = new QPushButton("降順ソート (Z-A)", centralWidget);
    QObject::connect(descButton, &QPushButton::clicked, [listWidget]() {
        listWidget->sortItems(Qt::DescendingOrder);
    });

    layout->addWidget(listWidget);
    layout->addWidget(ascButton);
    layout->addWidget(descButton);

    window.setCentralWidget(centralWidget);
    window.setWindowTitle("QListWidget::sortItems() 基本例");
    window.resize(300, 400);
    window.show();

    return a.exec();
}

解説

  • QListWidget はデフォルトで QString::localeAwareCompare() を使用して文字列を比較するため、ロケールに応じた正しい順序で並び替えられます。
  • 「降順ソート」ボタンをクリックすると、listWidget->sortItems(Qt::DescendingOrder); が呼び出され、アルファベット逆順(ZからA)にソートされます。
  • 「昇順ソート」ボタンをクリックすると、listWidget->sortItems(Qt::AscendingOrder); が呼び出され、アルファベット順(AからZ)にソートされます。
  • QListWidget にいくつかの文字列アイテムを追加します。

例 2: 数値のソート(QListWidgetItem::operator< のオーバーロード)

QListWidgetItem のテキストが数値の場合、デフォルトのソートでは文字列として比較されてしまいます ("1", "10", "2")。これを数値として正しくソートするために、QListWidgetItem を継承して operator< をオーバーロードする例です。

numericlistwidgetitem.h

#ifndef NUMERICLISTWIDGETITEM_H
#define NUMERICLISTWIDGETITEM_H

#include <QListWidgetItem>
#include <QString>

class NumericListWidgetItem : public QListWidgetItem
{
public:
    // コンストラクタ: テキストと、ソート用の数値を受け取る
    NumericListWidgetItem(const QString &text, int value)
        : QListWidgetItem(text), m_value(value)
    {}

    // QListWidgetItem::operator< をオーバーロード
    // これがソート時に呼ばれる比較関数になります。
    bool operator<(const QListWidgetItem &other) const override
    {
        // other が NumericListWidgetItem 型であることを確認
        // dynamic_cast を使用して安全にダウンキャストします。
        const NumericListWidgetItem *otherNumeric = dynamic_cast<const NumericListWidgetItem *>(&other);
        if (otherNumeric) {
            // もし other も NumericListWidgetItem なら、保持している数値で比較
            return m_value < otherNumeric->m_value;
        }
        // そうでない場合(例えば、標準のQListWidgetItemと比較する場合など)は、
        // デフォルトのテキスト比較に戻すか、適切なエラー処理を行います。
        // ここでは、基底クラスの比較演算子を呼び出しています。
        return QListWidgetItem::operator<(other);
    }

private:
    int m_value; // ソートに使用する実際の数値
};

#endif // NUMERICLISTWIDGETITEM_H

main.cpp

#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QPushButton>
#include <QListWidget>
#include <QDebug> // デバッグ出力用

#include "numericlistwidgetitem.h" // 作成したカスタムアイテムをインクルード

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

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

    QListWidget *listWidget = new QListWidget(centralWidget);

    // NumericListWidgetItem を使用してアイテムを追加
    listWidget->addItem(new NumericListWidgetItem("Value: 10", 10));
    listWidget->addItem(new NumericListWidgetItem("Value: 2", 2));
    listWidget->addItem(new NumericListWidgetItem("Value: 100", 100));
    listWidget->addItem(new NumericListWidgetItem("Value: 1", 1));
    listWidget->addItem(new NumericListWidgetItem("Value: 20", 20));

    qDebug() << "初期状態のアイテム:";
    for (int i = 0; i < listWidget->count(); ++i) {
        qDebug() << listWidget->item(i)->text();
    }

    QPushButton *sortButton = new QPushButton("数値でソート", centralWidget);
    QObject::connect(sortButton, &QPushButton::clicked, [listWidget]() {
        qDebug() << "\nソート実行...";
        listWidget->sortItems(Qt::AscendingOrder); // オーバーロードされた operator< が使われる

        qDebug() << "ソート後のアイテム:";
        for (int i = 0; i < listWidget->count(); ++i) {
            qDebug() << listWidget->item(i)->text();
        }
    });

    layout->addWidget(listWidget);
    layout->addWidget(sortButton);

    window.setCentralWidget(centralWidget);
    window.setWindowTitle("QListWidget::sortItems() 数値ソート例");
    window.resize(300, 400);
    window.show();

    return a.exec();
}

解説

  • main.cpp では、NumericListWidgetItem のインスタンスを作成して QListWidget に追加し、ソートボタンをクリックすると数値順にソートされることを確認できます。
  • dynamic_cast を使用して、比較対象の otherNumericListWidgetItem であることを確認することで、型安全な比較を実現しています。
  • 最も重要なのは bool operator<(const QListWidgetItem &other) const override のオーバーロードです。この中で、m_value を使って数値比較を行います。
  • コンストラクタで、表示テキストとソートに使う実際の数値(m_value)を受け取ります。
  • NumericListWidgetItem クラスを作成し、QListWidgetItem を継承します。

例 3: setSortingEnabled(true) の使用と一時的な無効化

setSortingEnabled(true) を設定すると、アイテムが追加されるたびに自動的にソートされます。大量のアイテムを追加する際など、一時的にこの機能を無効にする方法も示します。

main.cpp

#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QPushButton>
#include <QListWidget>
#include <QTimer> // デモンストレーション用

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

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

    QListWidget *listWidget = new QListWidget(centralWidget);
    listWidget->setSortingEnabled(true); // ソートを常に有効にする

    QPushButton *addSingleButton = new QPushButton("単一アイテム追加 (自動ソート)", centralWidget);
    QObject::connect(addSingleButton, &QPushButton::clicked, [listWidget]() {
        QString newItem = QInputDialog::getText(nullptr, "アイテム追加", "追加するテキスト:");
        if (!newItem.isEmpty()) {
            listWidget->addItem(newItem); // setSortingEnabled(true) なので、追加後自動でソートされる
        }
    });

    QPushButton *addBatchButton = new QPushButton("複数アイテム一括追加 (最適化)", centralWidget);
    QObject::connect(addBatchButton, &QPushButton::clicked, [listWidget]() {
        // 大量のアイテムを追加する前に、ソートを一時的に無効にする
        listWidget->setSortingEnabled(false);

        for (int i = 0; i < 100; ++i) {
            listWidget->addItem(QString("Item %1").arg(rand() % 1000)); // ランダムな数字のアイテム
        }

        // 全てのアイテムを追加し終わったら、ソートを再度有効にし、明示的にソートを呼び出す
        listWidget->setSortingEnabled(true);
        listWidget->sortItems(); // これにより、追加されたすべてのアイテムが一度にソートされる
    });

    layout->addWidget(listWidget);
    layout->addWidget(addSingleButton);
    layout->addWidget(addBatchButton);

    window.setCentralWidget(centralWidget);
    window.setWindowTitle("QListWidget::sortItems() 自動ソートと最適化");
    window.resize(400, 500);
    window.show();

    return a.exec();
}

解説

  • 「複数アイテム一括追加」ボタンでは、パフォーマンス最適化のために setSortingEnabled(false) を呼び出し、一括でアイテムを追加した後で setSortingEnabled(true) に戻し、最後に sortItems() を手動で呼び出してソートします。これにより、アイテム一つ一つを追加するたびにソート処理が走るのを防ぎ、処理速度を向上させることができます。
  • 「単一アイテム追加」ボタンでは、この自動ソートの挙動を確認できます。
  • listWidget->setSortingEnabled(true); を呼び出すと、以降 addItem() などでアイテムが追加されるたびに自動的にソートが実行されます。

QListWidget::sortItems() は、リストウィジェット内のアイテムをソートするためのメソッドです。ここでは、基本的な使い方から、カスタムソートまで、いくつかの例を挙げます。

例 1: 基本的な文字列ソート (昇順/降順)

この例では、QListWidget に文字列アイテムを追加し、ボタンをクリックすることで昇順または降順にソートします。

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

class MyListWidgetApp : public QWidget
{
    Q_OBJECT // シグナルとスロットを使用するために必要

public:
    MyListWidgetApp(QWidget *parent = nullptr) : QWidget(parent)
    {
        // UI要素の作成
        listWidget = new QListWidget(this);
        QPushButton *sortAscButton = new QPushButton("昇順でソート", this);
        QPushButton *sortDescButton = new QPushButton("降順でソート", this);
        QPushButton *addItemsButton = new QPushButton("アイテムを追加", this);

        // アイテムの追加
        addInitialItems();

        // レイアウトの設定
        QVBoxLayout *layout = new QVBoxLayout(this);
        layout->addWidget(listWidget);
        layout->addWidget(sortAscButton);
        layout->addWidget(sortDescButton);
        layout->addWidget(addItemsButton);

        // シグナルとスロットの接続
        connect(sortAscButton, &QPushButton::clicked, this, &MyListWidgetApp::sortAscending);
        connect(sortDescButton, &QPushButton::clicked, this, &MyListWidgetApp::sortDescending);
        connect(addItemsButton, &QPushButton::clicked, this, &MyListWidgetApp::addMoreItems);

        setWindowTitle("QListWidget ソート例");
        resize(300, 400);
    }

private slots:
    void sortAscending()
    {
        qDebug() << "昇順でソートします...";
        listWidget->sortItems(Qt::AscendingOrder);
        printItemsOrder("昇順ソート後");
    }

    void sortDescending()
    {
        qDebug() << "降順でソートします...";
        listWidget->sortItems(Qt::DescendingOrder);
        printItemsOrder("降順ソート後");
    }

    void addMoreItems()
    {
        static int count = 0;
        listWidget->addItem(QString("New Item %1").arg(count++));
        listWidget->addItem(QString("Zebra %1").arg(count++));
        listWidget->addItem(QString("Apple %1").arg(count++));
        qDebug() << "新しいアイテムを追加しました。";
        // setSortingEnabled(true) が設定されていない場合、自動ソートはされない
        // 必要に応じて、追加後に再度 sortItems() を呼び出す
        // listWidget->sortItems();
    }

private:
    void addInitialItems()
    {
        listWidget->addItem("Banana");
        listWidget->addItem("Apple");
        listWidget->addItem("Cherry");
        listWidget->addItem("Date");
        listWidget->addItem("grape"); // 小文字のテスト
        listWidget->addItem("Orange");
        listWidget->addItem("Elderberry");
        listWidget->addItem("fig"); // 小文字のテスト
        printItemsOrder("初期状態");
    }

    void printItemsOrder(const QString &title)
    {
        qDebug() << "---" << title << "---";
        for (int i = 0; i < listWidget->count(); ++i) {
            qDebug() << listWidget->item(i)->text();
        }
        qDebug() << "--------------------";
    }

    QListWidget *listWidget;
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MyListWidgetApp window;
    window.show();
    return app.exec();
}

#include "main.moc" // mocファイルを含める

解説

  • デフォルトでは、文字列は辞書順にソートされます。"Apple", "Banana", "Cherry" のようになりますが、"grape""fig" のような小文字は、"Zebra" のような大文字の後に来る場合があります(これはロケール設定に依存します)。
  • sortAscending()sortDescending() スロットで、listWidget->sortItems(Qt::AscendingOrder) または listWidget->sortItems(Qt::DescendingOrder) を呼び出すことで、アイテムがそれぞれ昇順または降順にソートされます。
  • QListWidgetaddItem() で文字列を追加します。

例 2: 数値のカスタムソート (QListWidgetItem::operator< のオーバーロード)

前述の通り、デフォルトのソートは文字列比較であるため、数値が正しくソートされない問題があります。この問題を解決するには、QListWidgetItem を継承したカスタムクラスを作成し、operator< をオーバーロードします。

#include <QApplication>
#include <QWidget>
#include <QVBoxLayout>
#include <QPushButton>
#include <QListWidget>
#include <QListWidgetItem>
#include <QString>
#include <QDebug>

// 数値を保持し、数値として比較するカスタムQListWidgetItem
class NumericListWidgetItem : public QListWidgetItem
{
public:
    // コンストラクタでテキストと数値を設定
    NumericListWidgetItem(int value)
        : QListWidgetItem(QString::number(value)), m_value(value) {}

    // テキストと数値を明示的に設定するコンストラクタ(テキストが数値と異なる場合)
    NumericListWidgetItem(const QString& text, int value)
        : QListWidgetItem(text), m_value(value) {}

    // 比較演算子をオーバーロード
    // QListWidgetItem::operator< のシグネチャと一致させる
    bool operator<(const QListWidgetItem &other) const override
    {
        // other が NumericListWidgetItem 型であることを確認
        // dynamic_cast が安全なキャストを提供
        const NumericListWidgetItem *otherNumeric = dynamic_cast<const NumericListWidgetItem *>(&other);

        if (otherNumeric) {
            // NumericListWidgetItem の場合は、m_value (数値) で比較
            return m_value < otherNumeric->m_value;
        } else {
            // 異なる型のアイテムとの比較の場合は、QListWidgetItem のデフォルト比較にフォールバック
            // または、ここで適切なエラー処理やログ出力を行う
            qWarning() << "Warning: Comparing NumericListWidgetItem with a non-NumericListWidgetItem.";
            return text() < other.text(); // デフォルトのテキスト比較
        }
    }

private:
    int m_value; // ソートに使用する実際の数値
};

class MyNumericSortApp : public QWidget
{
    Q_OBJECT

public:
    MyNumericSortApp(QWidget *parent = nullptr) : QWidget(parent)
    {
        listWidget = new QListWidget(this);
        QPushButton *sortButton = new QPushButton("数値でソート", this);

        // 数値としてソートしたいアイテムを追加
        listWidget->addItem(new NumericListWidgetItem(100));
        listWidget->addItem(new NumericListWidgetItem(5));
        listWidget->addItem(new NumericListWidgetItem(20));
        listWidget->addItem(new NumericListWidgetItem(1));
        listWidget->addItem(new NumericListWidgetItem("Value: 15", 15)); // テキストと実際の値が異なる例

        printItemsOrder("初期状態");

        QVBoxLayout *layout = new QVBoxLayout(this);
        layout->addWidget(listWidget);
        layout->addWidget(sortButton);

        connect(sortButton, &QPushButton::clicked, this, &MyNumericSortApp::sortNumeric);

        setWindowTitle("数値ソート例");
        resize(300, 300);
    }

private slots:
    void sortNumeric()
    {
        qDebug() << "数値でソートします...";
        listWidget->sortItems(Qt::AscendingOrder); // オーバーロードされた operator< が使われる
        printItemsOrder("数値ソート後");
    }

private:
    void printItemsOrder(const QString &title)
    {
        qDebug() << "---" << title << "---";
        for (int i = 0; i < listWidget->count(); ++i) {
            qDebug() << listWidget->item(i)->text();
        }
        qDebug() << "--------------------";
    }

    QListWidget *listWidget;
};

int main(int argc, char *argv[])
{
    QApplication app(argc);
    MyNumericSortApp window;
    window.show();
    return app.exec();
}

#include "main.moc" // mocファイルを含める

解説

  • QListWidget::sortItems() を呼び出すと、QListWidgetItem の比較が必要な場面で、自動的にこのオーバーロードされた operator< が使用され、数値に基づいたソートが行われます。
  • dynamic_cast を使用して、otherNumericListWidgetItem 型に安全にキャストできるかを確認します。これにより、異なる型のアイテムが混在している場合に予期せぬ動作を防ぎます。
  • operator<const QListWidgetItem &other を引数としてオーバーロードしています。このシグネチャが非常に重要です。
  • コンストラクタで、表示するテキストだけでなく、ソートに使用する実際の数値 (m_value) を保持します。
  • NumericListWidgetItem クラスが QListWidgetItem を継承しています。

例 3: 自動ソートの有効化/無効化

QListWidget::setSortingEnabled(bool) を使うと、アイテムの追加や削除時に自動的にソートさせるかどうかを制御できます。

#include <QApplication>
#include <QWidget>
#include <QVBoxLayout>
#include <QPushButton>
#include <QListWidget>
#include <QCheckBox>
#include <QDebug>

class AutoSortApp : public QWidget
{
    Q_OBJECT

public:
    AutoSortApp(QWidget *parent = nullptr) : QWidget(parent)
    {
        listWidget = new QListWidget(this);
        QPushButton *addItemButton = new QPushButton("アイテムを追加", this);
        QCheckBox *autoSortCheckBox = new QCheckBox("自動ソートを有効にする", this);
        QPushButton *manualSortButton = new QPushButton("手動でソート", this);

        // 初期アイテム
        listWidget->addItem("Orange");
        listWidget->addItem("Apple");
        listWidget->addItem("Banana");

        // デフォルトでは自動ソートは無効
        listWidget->setSortingEnabled(false);
        autoSortCheckBox->setChecked(false);

        QVBoxLayout *layout = new QVBoxLayout(this);
        layout->addWidget(listWidget);
        layout->addWidget(addItemButton);
        layout->addWidget(autoSortCheckBox);
        layout->addWidget(manualSortButton);

        connect(addItemButton, &QPushButton::clicked, this, &AutoSortApp::addNewItem);
        connect(autoSortCheckBox, &QCheckBox::toggled, listWidget, &QListWidget::setSortingEnabled);
        connect(manualSortButton, &QPushButton::clicked, this, &AutoSortApp::manualSort);

        setWindowTitle("自動ソートの制御");
        resize(300, 300);
    }

private slots:
    void addNewItem()
    {
        static int count = 0;
        QString newItemText = QString("Item %1").arg(QChar('A' + (count % 26))) + QString::number(count);
        listWidget->addItem(newItemText);
        qDebug() << "追加: " << newItemText;
        printItemsOrder("アイテム追加後");
        count++;
    }

    void manualSort()
    {
        qDebug() << "手動でソートします...";
        listWidget->sortItems(Qt::AscendingOrder);
        printItemsOrder("手動ソート後");
    }

private:
    void printItemsOrder(const QString &title)
    {
        qDebug() << "---" << title << "---";
        for (int i = 0; i < listWidget->count(); ++i) {
            qDebug() << listWidget->item(i)->text();
        }
        qDebug() << "--------------------";
    }

    QListWidget *listWidget;
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    AutoSortApp window;
    window.show();
    return app.exec();
}

#include "main.moc"
  • addNewItem() スロットでアイテムを追加します。
    • 自動ソートが有効な場合、アイテム追加後にリストは自動的にソートされます。
    • 自動ソートが無効な場合、アイテムは追加された位置にそのまま残り、manualSort() ボタンを押して初めてソートされます。
  • QCheckBoxtoggled シグナルを QListWidget::setSortingEnabled スロットに接続することで、チェックボックスの状態に応じて自動ソートの有効/無効を切り替えます。
  • listWidget->setSortingEnabled(false) で、初期状態では自動ソートを無効にしています。


QListWidget::sortItems() はシンプルで使いやすいですが、以下のような場合に限界があります。

  • モデル/ビューアーキテクチャに則った設計が必要な場合
    データの表示とロジックを厳密に分離したい場合。
  • 大規模なデータセットを扱う場合
    パフォーマンスが問題になる場合。
  • フィルタリングも同時に行いたい場合
    ソートとフィルタリングを連携させたい場合。
  • ソート基準が複雑な場合
    複数の列(QTableWidget のように)や、表示テキスト以外の複雑なデータに基づいてソートしたい場合。

これらのシナリオでは、Qt のモデル/ビューアーキテクチャを活用することが推奨されます。

QListView とカスタムモデル (QAbstractListModel) を組み合わせる

これは、QListWidget の最も強力で柔軟な代替手段です。QListWidget は内部的に独自のシンプルなモデル(QStringListModel のようなもの)を使用していますが、QListView は開発者が自由にデータモデルを定義できます。

特徴

  • パフォーマンス
    大規模なデータセットでも効率的に動作するよう設計されています。
  • 追加機能の統合
    フィルタリング、ドラッグ&ドロップ、異なるビューでのデータ表示などが容易になります。
  • 柔軟なソート
    モデル内でソートロジックを自由に実装できます。
  • データの分離
    データ(モデル)と表示(ビュー)が完全に分離されるため、データ操作のロジックがクリーンになります。

基本的な手順

    • rowCount(): リストの行数を返します。
    • data(): 特定のインデックスと役割(Qt::DisplayRole など)に対応するデータを返します。
    • flags(): アイテムのフラグ(選択可能か、編集可能かなど)を返します。
    • ソート機能の実装
      • sort(int column, Qt::SortOrder order = Qt::AscendingOrder) をオーバーライドし、モデルの内部データをソートします。beginResetModel()endResetModel() または layoutAboutToBeChanged()layoutChanged() を呼び出して、ビューにデータの変更を通知する必要があります。
  1. QListView のインスタンスを作成します。

  2. 作成したカスタムモデルのインスタンスを QListView::setModel() で設定します。

例(簡易版 - ソートロジックはモデル内で実装)

#include <QApplication>
#include <QWidget>
#include <QVBoxLayout>
#include <QPushButton>
#include <QListView>
#include <QAbstractListModel>
#include <QList>
#include <QPair> // データ格納用
#include <QDebug>
#include <algorithm> // std::sort 用

// カスタムデータ構造(テキストと数値)
struct ListItemData {
    QString text;
    int value;

    // ソートのための比較演算子 (任意 - モデル内で直接比較してもOK)
    bool operator<(const ListItemData& other) const {
        return value < other.value;
    }
};

// カスタムリストモデル
class MyListModel : public QAbstractListModel
{
    Q_OBJECT
public:
    explicit MyListModel(QObject *parent = nullptr) : QAbstractListModel(parent) {}

    // 行数
    int rowCount(const QModelIndex &parent = QModelIndex()) const override {
        if (parent.isValid())
            return 0;
        return m_data.count();
    }

    // データ取得
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
        if (!index.isValid())
            return QVariant();

        if (index.row() >= m_data.size())
            return QVariant();

        const ListItemData& item = m_data.at(index.row());
        if (role == Qt::DisplayRole) {
            return item.text;
        } else if (role == Qt::UserRole) { // カスタムデータ(ソート用など)
            return item.value;
        }
        return QVariant();
    }

    // アイテムフラグ
    Qt::ItemFlags flags(const QModelIndex &index) const override {
        if (!index.isValid())
            return Qt::NoItemFlags;
        return QAbstractListModel::flags(index) | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
    }

    // データ追加
    void addItem(const QString& text, int value) {
        beginInsertRows(QModelIndex(), m_data.count(), m_data.count());
        m_data.append({text, value});
        endInsertRows();
    }

    // ソートの実装 (QListView::setModel() 後に listView->sortByColumn(0, Qt::AscendingOrder) で呼び出される)
    void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override {
        Q_UNUSED(column); // 単一列なのでcolumnは無視

        // ビューにソート開始を通知
        beginResetModel(); // または beginInsertRows/beginRemoveRows/beginMoveRows を組み合わせて、より効率的な更新を通知
                           // シンプルなソートでは beginResetModel が最も簡単

        if (order == Qt::AscendingOrder) {
            std::sort(m_data.begin(), m_data.end(), [](const ListItemData& a, const ListItemData& b) {
                return a.value < b.value; // 数値で昇順ソート
            });
        } else {
            std::sort(m_data.begin(), m_data.end(), [](const ListItemData& a, const ListItemData& b) {
                return a.value > b.value; // 数値で降順ソート
            });
        }

        // ビューにソート終了を通知
        endResetModel();
        qDebug() << "Model sorted.";
    }

private:
    QList<ListItemData> m_data;
};

class MyListViewApp : public QWidget
{
    Q_OBJECT
public:
    MyListViewApp(QWidget *parent = nullptr) : QWidget(parent) {
        model = new MyListModel(this);
        listView = new QListView(this);
        listView->setModel(model);

        // 初期データ追加
        model->addItem("Apple (Value: 10)", 10);
        model->addItem("Orange (Value: 5)", 5);
        model->addItem("Banana (Value: 20)", 20);
        model->addItem("Grape (Value: 1)", 1);

        QPushButton *sortAscButton = new QPushButton("昇順ソート", this);
        QPushButton *sortDescButton = new QPushButton("降順ソート", this);

        QVBoxLayout *layout = new QVBoxLayout(this);
        layout->addWidget(listView);
        layout->addWidget(sortAscButton);
        layout->addWidget(sortDescButton);

        connect(sortAscButton, &QPushButton::clicked, this, [this](){
            listView->sortByColumn(0, Qt::AscendingOrder); // モデルの sort() が呼び出される
        });
        connect(sortDescButton, &QPushButton::clicked, this, [this](){
            listView->sortByColumn(0, Qt::DescendingOrder); // モデルの sort() が呼び出される
        });

        setWindowTitle("QListView カスタムモデルソート");
        resize(300, 400);
    }

private:
    MyListModel *model;
    QListView *listView;
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    MyListViewApp window;
    window.show();
    return app.exec();
}

#include "main.moc"

利点

  • 再利用性
    同じモデルを複数の異なるビュー(例: QTableViewQTreeView)で再利用できます。
  • データ構造の柔軟性
    モデルの内部データ構造を自由に定義できます。
  • 完全に制御可能
    ソートロジックを細かく制御できます。

欠点

  • QListWidget に比べてコード量が増え、学習曲線が少し高くなります。

QListView と QSortFilterProxyModel を組み合わせる

この方法は、既存のモデルのデータを変更せずに、ソートやフィルタリングを適用したい場合に最適です。元のモデル(ソースモデル)とビューの間にプロキシモデルを挟む形になります。

特徴

  • 既存モデルの活用
    既存の QAbstractItemModel 派生クラス(例: QStringListModel)をそのまま利用できます。
  • 組み合わせ可能
    ソートとフィルタリングを同時に適用できます。
  • 非破壊的なソート/フィルタリング
    元のデータモデルは変更されません。

基本的な手順

  1. ソースモデル (QAbstractListModel 派生クラス、例: QStringListModel) を作成します。
  2. QSortFilterProxyModel のインスタンスを作成します。
  3. プロキシモデルにソースモデルを設定します (proxyModel->setSourceModel(sourceModel))。
  4. QListView にプロキシモデルを設定します (listView->setModel(proxyModel))。
  5. プロキシモデルのソート基準を設定します (proxyModel->setSortRole(Qt::UserRole) など)。
  6. listView->sortByColumn() を呼び出すか、プロキシモデルの sort() メソッドを直接呼び出します。

例(数値ソートに QSortFilterProxyModel を使用)

#include <QApplication>
#include <QWidget>
#include <QVBoxLayout>
#include <QPushButton>
#include <QListView>
#include <QStringListModel> // ソースモデルとして使用
#include <QSortFilterProxyModel>
#include <QDebug>

class MyProxySortApp : public QWidget
{
    Q_OBJECT
public:
    MyProxySortApp(QWidget *parent = nullptr) : QWidget(parent) {
        // 1. ソースモデルを作成
        sourceModel = new QStringListModel(this);
        QStringList data;
        data << "Orange (5)" << "Apple (10)" << "Banana (2)" << "Grape (15)" << "Kiwi (8)";
        sourceModel->setStringList(data);

        // 2. プロキシモデルを作成
        proxyModel = new QSortFilterProxyModel(this);
        proxyModel->setSourceModel(sourceModel);
        // 数値ソートのために、カスタムのソートロールを設定(ここではデフォルトのDisplayRoleを使用)
        // または、proxyModel->setSortRole(Qt::UserRole); で、data(index, Qt::UserRole) で取得する値をソート基準にする。
        // この例では、文字列から数値をパースしてソートするカスタム比較をオーバーライドする。

        // カスタムソート比較関数の設定
        proxyModel->setSortLocaleAware(true); // ロケールアウェアな比較を有効にする (デフォルト)

        // QSortFilterProxyModel はデフォルトで Qt::DisplayRole のテキストをソートする
        // 数値としてソートするには、ソート基準となるデータをQVariantとして持たせる
        // または、QListWidgetItem の例のように、model->data() の返り値をカスタマイズする
        // しかし、QStringListModel ではそれができないため、カスタムの比較関数をオーバーライドする。
        // これには QSortFilterProxyModel を継承して lessThan() をオーバーライドする必要がある。

        // より簡単な方法として、QStringListModel にあらかじめ数値データを埋め込む。
        // もし、"Apple (10)" のような文字列を数値としてソートしたいなら、
        // QSortFilterProxyModel を継承し、lessThan() をオーバーライドする必要がある。
        // 簡単化のため、ここではテキストが数値であると仮定するか、
        // より複雑なデータの例 (QListWidgetItemの例と同じ) でソートする。

        // 例外的に、QSortFilterProxyModel を継承して lessThan をオーバーライド
        class CustomProxyModel : public QSortFilterProxyModel {
        protected:
            bool lessThan(const QModelIndex &left, const QModelIndex &right) const override {
                QVariant leftData = sourceModel()->data(left);
                QVariant rightData = sourceModel()->data(right);

                // "Apple (10)" から "(10)" を抽出し、数値に変換して比較
                QRegExp rx("\\((\\d+)\\)"); // (数字) を抽出する正規表現
                int leftValue = 0;
                int rightValue = 0;

                if (leftData.toString().contains(rx)) {
                    leftValue = rx.cap(1).toInt();
                }
                if (rightData.toString().contains(rx)) {
                    rightValue = rx.cap(1).toInt();
                }
                return leftValue < rightValue;
            }
        };
        CustomProxyModel* customProxyModel = new CustomProxyModel();
        customProxyModel->setSourceModel(sourceModel);
        proxyModel = customProxyModel; // proxyModelをカスタムプロキシモデルに置き換える

        // 3. ビューを作成し、プロキシモデルを設定
        listView = new QListView(this);
        listView->setModel(proxyModel);
        listView->setSortingEnabled(true); // ビューでソートを有効にする (ヘッダークリックなど)

        QPushButton *sortAscButton = new QPushButton("昇順ソート", this);
        QPushButton *sortDescButton = new QPushButton("降順ソート", this);

        QVBoxLayout *layout = new QVBoxLayout(this);
        layout->addWidget(listView);
        layout->addWidget(sortAscButton);
        layout->addWidget(sortDescButton);

        connect(sortAscButton, &QPushButton::clicked, this, [this](){
            listView->sortByColumn(0, Qt::AscendingOrder); // proxyModel の sort() が呼び出される
            qDebug() << "昇順ソートしました。";
        });
        connect(sortDescButton, &QPushButton::clicked, this, [this](){
            listView->sortByColumn(0, Qt::DescendingOrder); // proxyModel の sort() が呼び出される
            qDebug() << "降順ソートしました。";
        });

        setWindowTitle("QSortFilterProxyModel ソート例");
        resize(300, 400);
    }

private:
    QStringListModel *sourceModel;
    QSortFilterProxyModel *proxyModel; // CustomProxyModel のポインタとして使用
    QListView *listView;
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    MyProxySortApp window;
    window.show();
    return app.exec();
}

#include "main.moc"

利点

  • 再利用性
    既存のモデルをソート/フィルタリング可能にできます。
  • ソートとフィルタリングの統合
    同じプロキシモデルで両方を管理できます。
  • データ非破壊
    元のデータモデルは変更されません。

欠点

  • カスタムのソートロジックが複雑な場合、QSortFilterProxyModel を継承して lessThan() メソッドをオーバーライドする必要がある。
  • モデル/ビューアーキテクチャの理解が必要。

手動でのソートと再描画

これは最も原始的な方法で、QListWidget::sortItems() やモデル/ビューを使用せず、自力でアイテムをソートして QListWidget に入れ直す方法です。通常は推奨されませんが、特定のニッチなケースや非常に小規模なデータセットで、他の方法が複雑すぎる場合に検討されることがあります。

基本的な手順

  1. QListWidget からすべての QListWidgetItem を取得します。
  2. これらのアイテムを QListstd::vector などに格納します。
  3. カスタムの比較関数を使って、これらのアイテムをソートします(std::sort などを使用)。
  4. QListWidget::clear() で既存のアイテムをすべて削除します。
  5. ソートされた順序で、新しいアイテムを QListWidget に再追加します。

例 (概念のみ、実際にはあまり使わない)

// ... (QListWidgetのセットアップなど)

void MyListWidgetApp::manualSort() {
    QList<QListWidgetItem*> items;
    for (int i = 0; i < listWidget->count(); ++i) {
        items.append(listWidget->item(i));
    }

    // QListWidget からアイテムをデタッチ (所有権を移動)
    // これをしないと clear() でアイテムが削除されてしまう
    listWidget->clear();

    // カスタムソートロジック
    std::sort(items.begin(), items.end(), [](QListWidgetItem* a, QListWidgetItem* b) {
        // 例: テキストの逆順でソート
        return a->text() > b->text();
    });

    // ソートされた順序でアイテムを再追加
    for (QListWidgetItem* item : qAsConst(items)) {
        listWidget->addItem(item); // QListWidget がアイテムの所有権を取り戻す
    }
}

利点

  • モデル/ビューアーキテクチャの知識が不要。

欠点

  • 保守性
    コードが複雑になり、保守が難しくなります。
  • 所有権の管理
    アイテムの所有権管理が複雑になり、メモリリークのリスクがあります。
  • イベントの発生
    アイテムの削除と追加がそれぞれ多くのイベントを発生させます。
  • 非効率的
    大規模なデータではパフォーマンスが悪化します(特に clear()addItem() の繰り返し)。
  • 既存モデルのデータを変更せずにソート/フィルタリング、複数のビューでの表示
    QListViewQSortFilterProxyModel
  • 大規模なデータセット、複雑なソート/フィルタリング、モデル/ビューアーキテクチャへの準拠
    QListView とカスタムモデル (QAbstractListModel)
  • より高度なソート、数値ソート、カスタムデータでのソート、小規模〜中規模のデータセット
    QListWidgetItem::operator< のオーバーロード
  • 最もシンプルで基本的なソート
    QListWidget::sortItems()