Como evitar que QListView chame sizeHint de cada item?

Nov 20 2020

Eu tenho um QListView com muitos itens de várias alturas. Implemento um delegado personalizado para itens de pintura e defino o modo de layout como Lote.

No entanto, quando o modelo é atribuído, a exibição de lista solicita sizeHint para cada item no modelo antecipadamente, ignorando a configuração em lote e, assim, arruinando o desempenho porque, para calcular o tamanho, o delegado precisa fazer o layout de muito texto (o que não é rápido )

Provavelmente ele faz isso para calcular a posição da barra de rolagem, mas calculei que quando o número de itens é grande, a posição da barra de rolagem pode ser baseada apenas nos índices dos itens, sem levar em consideração as alturas dos itens. No entanto, parece que não é assim que QListView funciona.

Eu também tentei usar canFetchMore / fetchMore no modelo, mas isso leva a uma experiência ruim do usuário - a posição da barra de rolagem não é mais precisa e a lista pula quando mais itens são carregados, não foi nada uniforme.

Então, a questão é:

  1. Existe uma maneira de impedir que QListView chame sizeHint para itens invisíveis?
  2. Se a única maneira é usar canFetchMore / fetchMore, como obter uma rolagem suave e uma barra de rolagem estável e precisa?

Muito obrigado!

UPD: aqui está o exemplo mínimo que reproduz esse comportamento:https://github.com/ajenter/qt_hugelistview

Observe o grande atraso na inicialização e as mensagens de depuração mostrando que sizeHint de todos os 5000 itens é solicitado antecipadamente.

Respostas

2 AlexJenter Nov 23 2020 at 20:05

Bem, parece que encontrei uma solução, então vou compartilhá-la aqui para o bem de qualquer pessoa que tenha o mesmo problema e pesquise este tópico no Google.

Em primeiro lugar, descobri que este é realmente um bug no Qt registrado em 2011 e ainda aberto: https://bugreports.qt.io/browse/QTBUG-16592

Eu adicionei meu voto a ele (e você também deveria!). Então decidi tentar usar QTableView em vez de QListView - e, surpreendentemente, consegui fazer funcionar, ou assim parece.

Ao contrário de QListView, QTableView apenas redimensiona linhas mediante solicitação explícita, chamando resizeRowToContents (rowNum). Portanto, o truque é chamá-lo de maneira just-in-time para as linhas que se tornam visíveis na janela de visualização.

Aqui está o que eu fiz:

  1. Herdar de QTableView (vamos chamá-lo de MyTableView)

  2. Substitua QListView por MyTableView e inicialize-o assim no construtor. Isso atribui delegado de item personalizado, oculta cabeçalhos de tabela e aplica o modo de seleção "por linha":

   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. Em MyTableView, adicione um campo privado QItemSelection e uma função pública que calcula alturas reais de linhas, mas apenas aquelas que estão visíveis no momento:
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. Observação: se as alturas dos itens dependem da largura do controle, você precisa substituir resizeEvent(), limpar _itemsWithKnownHeightse chamar updateVisibleRowsHeight () novamente.

  2. Chame updateVisibleRowHeights () após atribuir um modelo à instância MyTableView, para que a visualização inicial esteja correta:

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

Na verdade, isso deve ser feito em algum método de MyTableView que reage às alterações do modelo, mas deixarei como um exercício.

  1. Agora, tudo o que resta é ter algo chamado updateRowHeights sempre que a posição de rolagem vertical da tabela muda. Portanto, precisamos adicionar o seguinte ao construtor de MyTableView:
connect(verticalScrollBar(), &QScrollBar::valueChanged, [this](int) {
        updateRowHeights();
    });

Pronto - funciona muito rápido mesmo com um modelo de 100.000 itens! E a inicialização é instantânea!

Um exemplo básico de prova de conceito desta técnica (usando QTableView puro em vez de subclasse) pode ser encontrado aqui: https://github.com/ajenter/qt_hugelistview/blob/tableview-experiment/src/main.cpp

Aviso: esta técnica ainda não foi comprovada em batalha e pode conter alguns problemas ainda desconhecidos. Use por sua conta e risco!