QListViewが各アイテムのsizeHintを呼び出さないようにするにはどうすればよいですか?

Nov 20 2020

さまざまな高さのアイテムがたくさんあるQListViewがあります。アイテムをペイントするためのカスタムデリゲートを実装し、レイアウトモードをバッチに設定します。

ただし、モデルが割り当てられると、リストビューはモデル内のすべてのアイテムに対してsizeHintを事前に要求し、バッチ設定を無視するため、サイズを計算するためにデリゲートは大量のテキストをレイアウトする必要があるため、パフォーマンスが低下します(これは高速ではありません) )。

おそらくこれはスクロールバーの位置を計算するために行われますが、アイテムの数が多い場合、スクロールバーの位置はアイテムの高さを考慮せずにアイテムのインデックスのみに基づくことができると思いました。ただし、これはQListViewの動作方法ではないようです。

また、モデルでcanFetchMore / fetchMoreを使用しようとしましたが、これによりユーザーエクスペリエンスが低下します。スクロールバーの位置が正確でなくなり、さらにアイテムが読み込まれるとリストがジャンプし、まったくスムーズになりませんでした。

だから、問題は:

  1. QListViewが非表示のアイテムに対してsizeHintを呼び出さないようにする方法はありますか?
  2. canFetchMore / fetchMoreを使用することが唯一の方法である場合、スムーズなスクロールと安定した正確なスクロールバーを取得するにはどうすればよいですか?

どうもありがとう!

UPD:この動作を再現する最小限の例を次に示します。https://github.com/ajenter/qt_hugelistview

起動の大幅な遅延と、5000項目すべてのsizeHintが事前に要求されていることを示すデバッグメッセージに注意してください。

回答

2 AlexJenter Nov 23 2020 at 20:05

解決策を見つけたようですので、同じ問題を抱えていてこのスレッドをグーグルで検索している人のために、ここで共有します。

まず第一に、これは実際には2011年に登録されたQtのバグであり、まだ開いていることがわかりました。 https://bugreports.qt.io/browse/QTBUG-16592

私はそれに投票を追加しました(そしてあなたもそうすべきです!)。次に、QListViewの代わりにQTableViewを使用してみることにしました。驚いたことに、なんとか機能させることができたようです。

QListViewとは異なり、QTableViewは、resizeRowToContents(rowNum)を呼び出すことにより、明示的な要求があった場合にのみ行のサイズを変更します。したがって、トリックは、ビューポートに表示されるようになる行に対してジャストインタイム方式で呼び出すことです。

これが私がしたことです:

  1. QTableViewから継承します(MyTableViewと呼びましょう)

  2. QListViewをMyTableViewに置き換え、コンストラクターでこのように初期化します。これにより、カスタムアイテムデリゲートが割り当てられ、テーブルヘッダーが非表示になり、「行ごと」の選択モードが適用されます。

   MyTableView::MyTableView(QWidget* parent) : QTableView(parent)
    {
       setSelectionBehavior(QAbstractItemView::SelectRows);
       horizontalHeader()->setStretchLastSection(true); 
       horizontalHeader()->hide();
       verticalHeader()->hide();
       setItemDelegateForColumn(0, new CustomDelegate(&table)); // for custom-drawn items
    }
  1. MyTableViewで、QItemSelectionプライベートフィールドと行の実際の高さを計算するパブリック関数を追加しますが、現在表示されているものだけです。
QItemSelection _itemsWithKnownHeight; // private member of MyTableView

void MyTableView::updateVisibleRowHeights()
{
    const QRect viewportRect = table.viewport()->rect();

    QModelIndex topRowIndex = table.indexAt(QPoint(viewportRect.x() + 5, viewportRect.y() + 5));
    QModelIndex bottomRowIndex = table.indexAt(QPoint(viewportRect.x() + 5, viewportRect.y() + viewportRect.height() - 5));
    qDebug() << "top row: " << topRowIndex.row() << ", bottom row: " << bottomRowIndex.row();

    for (auto i = topRowIndex.row() ; i < bottomRowIndex.row() + 1; ++i)
    {
        auto index = model()->index(i, 0);
        if (!_itemsWithKnownHeights.contains(index))
        {
            resizeRowToContents(i);
            _itemsWithKnownHeights.select(index, index);
            qDebug() << "Marked row #" << i << " as resized";
        }
    }
}
  1. 注:アイテムの高さがコントロールの幅に依存する場合は、オーバーライドresizeEvent()してクリアし_itemsWithKnownHeights、updateVisibleRowsHeight()を再度呼び出す必要があります。

  2. モデルをMyTableViewインスタンスに割り当てた後、updateVisibleRowHeights()を呼び出して、初期ビューが正しくなるようにします。

   table.setModel(&myModel);
   table.updateVisibleRowHeights();

実際、モデルの変更に反応するMyTableViewのメソッドで実行する必要がありますが、演習として残しておきます。

  1. あとは、テーブルの垂直スクロール位置が変わるたびにupdateRowHeightsを呼び出すだけです。したがって、MyTableViewのコンストラクターに以下を追加する必要があります。
connect(verticalScrollBar(), &QScrollBar::valueChanged, [this](int) {
        updateRowHeights();
    });

完了-10万アイテムのモデルでも非常に高速に動作します!そして、スタートアップは瞬時です!

この手法の基本的な概念実証の例(サブクラスの代わりに純粋なQTableViewを使用)は、次の場所にあります。 https://github.com/ajenter/qt_hugelistview/blob/tableview-experiment/src/main.cpp

警告:このテクニックはまだ戦闘で証明されておらず、まだ未知の問題が含まれている可能性があります。自己責任で使用してください!