2016-02-14 15 views
5

Sto creando un programma di dizionario che visualizza le definizioni di parola in una sottoclasse QTableView a 3 colonne, mentre l'utente le digita, prendendo i dati da una sottoclasse QAbstractTableModel. Qualcosa del genere:Come creare un QTableView veloce con celle formattate in HTML e selezionabili?

Table and user input screenshot

voglio aggiungere vari formattazione al testo, sto usando QAbstractItemView::setIndexWidget per aggiungere un QLabel ad ogni cella come dati è disponibile in:

WordView.h

#include <QTableView> 

class QLabel; 

class WordView : public QTableView { 
    Q_OBJECT 

public: 
    explicit WordView(QWidget *parent = 0); 

    void rowsInserted(const QModelIndex &parent, int start, int end); 

private: 
    void insertLabels(int row); 
    void removeLabels(int row); 
}; 

WordView.cpp

#include <QLabel> 
#include "WordView.h" 

WordView::WordView(QWidget *parent) : 
    QTableView(parent) 
{} 

void WordView::rowsInserted(const QModelIndex &parent, int start, int end) { 
    QTableView::rowsInserted(parent, start, end); 

    for (int row = start; row <= end; ++row) { 
     insertLabels(row); 
    } 
} 

void WordView::insertLabels(int row) { 
    for (int i = 0; i < 3; ++i) { 
     auto label = new QLabel(this); 
     label->setTextFormat(Qt::RichText); 
     label->setAutoFillBackground(true); 
     QModelIndex ix = model()->index(row, i); 
     label->setText(model()->data(ix, Qt::DisplayRole).toString()); // this has HTML 
     label->setWordWrap(true); 
     setIndexWidget(ix, label); // this calls QAbstractItemView::dataChanged 
    } 
} 

Tuttavia, questo è molto lento - ci vogliono circa 1 secondo per aggiornare 100 righe (rimuovere tutto, quindi aggiungere 100 nuove) in questo modo. Con QTableView originale ha funzionato velocemente, ma non avevo la formattazione e la possibilità di aggiungere collegamenti (riferimenti incrociati nel dizionario). Come rendere questo molto più veloce? O quale altro widget posso usare per visualizzare quei dati?

miei requisiti sono:

  • Aggiunta/rimozione di circa 1000 righe in ~ 0.2s, dove circa il 30 saranno visibili in una sola volta
  • cliccabile, più link interni (<a>?) In ogni cella (ad esempio QLabel ha che, QItemDelegate avrebbe potuto essere veloce, ma non so come ottenere informazioni che collegano ho cliccato lì)
  • formattazione che permette diverse dimensioni dei caratteri e colori, a capo automatico, altezze diverse cellule
  • Io non sono veramente morto-set su QTableView, tutto ciò che si presenta come un tavolo scorrevole e sembra coerente con la grafica Qt è bene

Note:

  • Ho provato a fare una sola etichetta con HTML <table> invece, ma non era molto più veloce. Sembra che QLabel non sia la strada da percorrere.
  • Dati nel campione per gentile concessione del progetto JMdict.

risposta

5

Ho risolto il problema mettendo insieme poche risposte e guardando gli interni di Qt.

Una soluzione che funziona molto veloce per il contenuto HTML statico con collegamenti in QTableView è folows:

  • sottoclasse QTableView e gestire gli eventi del mouse là;
  • Sottoclasse QStyledItemDelegate e dipingi il codice HTML lì (contrariamente alla risposta di RazrFalcon, è molto veloce, poiché solo una piccola quantità di celle è visibile alla volta e solo quelle hanno il metodo paint() chiamato);
  • Nella sottoclasse QStyledItemDelegate creare una funzione che indovina quale collegamento è stato selezionato da QAbstractTextDocumentLayout::anchorAt(). Non è possibile creare da soli QAbstractTextDocumentLayout, ma è possibile ottenerlo da QTextDocument::documentLayout() e, in base al codice sorgente Qt, è garantito che non sia nullo.
  • In sottoclasse QTableView modificare QCursor forma del puntatore di conseguenza per se è in bilico su un collegamento

Di seguito è riportato un completo, implementazione di lavoro di QTableView e QStyledItemDelegate sottoclassi che dipingono il codice HTML e inviano segnali su Link hover/attivazione. Il delegato e il modello devono ancora essere impostato dall'esterno, come segue:

wordTable->setModel(&myModel); 
auto wordItemDelegate = new WordItemDelegate(this); 
wordTable->setItemDelegate(wordItemDelegate); // or just choose specific columns/rows 

WordView.h

class WordView : public QTableView { 
    Q_OBJECT 

public: 
    explicit WordView(QWidget *parent = 0); 

signals: 
    void linkActivated(QString link); 
    void linkHovered(QString link); 
    void linkUnhovered(); 

protected: 
    void mousePressEvent(QMouseEvent *event); 
    void mouseMoveEvent(QMouseEvent *event); 
    void mouseReleaseEvent(QMouseEvent *event); 

private: 
    QString anchorAt(const QPoint &pos) const; 

private: 
    QString _mousePressAnchor; 
    QString _lastHoveredAnchor; 
}; 

WordView.cpp

#include <QApplication> 
#include <QCursor> 
#include <QMouseEvent> 
#include "WordItemDelegate.h" 
#include "WordView.h" 

WordView::WordView(QWidget *parent) : 
    QTableView(parent) 
{ 
    // needed for the hover functionality 
    setMouseTracking(true); 
} 

void WordView::mousePressEvent(QMouseEvent *event) { 
    QTableView::mousePressEvent(event); 

    auto anchor = anchorAt(event->pos()); 
    _mousePressAnchor = anchor; 
} 

void WordView::mouseMoveEvent(QMouseEvent *event) { 
    auto anchor = anchorAt(event->pos()); 

    if (_mousePressAnchor != anchor) { 
     _mousePressAnchor.clear(); 
    } 

    if (_lastHoveredAnchor != anchor) { 
     _lastHoveredAnchor = anchor; 
     if (!_lastHoveredAnchor.isEmpty()) { 
      QApplication::setOverrideCursor(QCursor(Qt::PointingHandCursor)); 
      emit linkHovered(_lastHoveredAnchor); 
     } else { 
      QApplication::restoreOverrideCursor(); 
      emit linkUnhovered(); 
     } 
    } 
} 

void WordView::mouseReleaseEvent(QMouseEvent *event) { 
    if (!_mousePressAnchor.isEmpty()) { 
     auto anchor = anchorAt(event->pos()); 

     if (anchor == _mousePressAnchor) { 
      emit linkActivated(_mousePressAnchor); 
     } 

     _mousePressAnchor.clear(); 
    } 

    QTableView::mouseReleaseEvent(event); 
} 

QString WordView::anchorAt(const QPoint &pos) const { 
    auto index = indexAt(pos); 
    if (index.isValid()) { 
     auto delegate = itemDelegate(index); 
     auto wordDelegate = qobject_cast<WordItemDelegate *>(delegate); 
     if (wordDelegate != 0) { 
      auto itemRect = visualRect(index); 
      auto relativeClickPosition = pos - itemRect.topLeft(); 

      auto html = model()->data(index, Qt::DisplayRole).toString(); 

      return wordDelegate->anchorAt(html, relativeClickPosition); 
     } 
    } 

    return QString(); 
} 

WordItemDelegate.h

#include <QStyledItemDelegate> 

class WordItemDelegate : public QStyledItemDelegate { 
    Q_OBJECT 

public: 
    explicit WordItemDelegate(QObject *parent = 0); 

    QString anchorAt(QString html, const QPoint &point) const; 

protected: 
    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; 
    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; 
}; 

WordItemDelegate.cpp

#include <QPainter> 
#include <QTextDocument> 
#include <QAbstractTextDocumentLayout> 
#include "WordItemDelegate.h" 

WordItemDelegate::WordItemDelegate(QObject *parent) : 
    QStyledItemDelegate(parent) 
{} 

QString WordItemDelegate::anchorAt(QString html, const QPoint &point) const { 
    QTextDocument doc; 
    doc.setHtml(html); 

    auto textLayout = doc.documentLayout(); 
    Q_ASSERT(textLayout != 0); 
    return textLayout->anchorAt(point); 
} 

void WordItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { 
    auto options = option; 
    initStyleOption(&options, index); 

    painter->save(); 

    QTextDocument doc; 
    doc.setHtml(options.text); 

    options.text = ""; 
    options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &option, painter); 

    painter->translate(options.rect.left(), options.rect.top()); 
    QRect clip(0, 0, options.rect.width(), options.rect.height()); 
    doc.drawContents(painter, clip); 

    painter->restore(); 
} 

QSize WordItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { 
    QStyleOptionViewItemV4 options = option; 
    initStyleOption(&options, index); 

    QTextDocument doc; 
    doc.setHtml(options.text); 
    doc.setTextWidth(options.rect.width()); 
    return QSize(doc.idealWidth(), doc.size().height()); 
} 

noti che questa soluzione è veloce solo perché un piccolo sottoinsieme di righe è reso in una volta, e quindi non molti QTextDocument s sono resi contemporaneamente. La regolazione automatica di tutte le altezze delle righe o delle colonne contemporaneamente sarà comunque lenta. Se hai bisogno di questa funzionalità, puoi fare in modo che il delegato informi la visualizzazione che ha dipinto qualcosa e quindi, se non lo ha fatto, regola la larghezza e l'altezza della vista. Combinalo con QAbstractItemView::rowsAboutToBeRemoved per rimuovere le informazioni memorizzate nella cache e hai una soluzione funzionante. Se sei pignolo riguardo alla dimensione e alla posizione della barra di scorrimento, puoi calcolare l'altezza media in base a pochi elementi campione in QAbstractItemView::rowsInserted e ridimensionare il resto di conseguenza senza sizeHint.

Riferimenti: risposta

+0

Risposta perfetta per quello di cui avevo bisogno. Grazie. –

2

Nel tuo caso QLabel (ri) verniciatura è lenta, non QTableView. D'altra parte, QTableView non supporta affatto il testo formattato.

Probabilmente, l'unico modo, è creare il proprio delegato, QStyledItemDelegate, e creare la propria elaborazione di disegni e clic.

PS: sì, è possibile utilizzare QTextDocument per il rendering di html all'interno del delegato, ma sarà anche lento.

+0

Ma dopo che ho elementi di rendering in un QStyledItemDelegate, non sono cliccabili. Come rilevare dove viene eseguito il rendering di un collegamento, in modo che possa gestirlo da solo? – Xilexio

+0

Ancora, da solo. Probabilmente, dovresti semplificare la formattazione suddividendola in blocchi. Quindi quando dipingi il tuo oggetto - calcoli quei blocchi, es. Esegui il QRect e memorizzalo come variabile di classe, quindi, nell'evento click, puoi facilmente rilevare la posizione del clic e i suoi dati. Ma è solo quello che dovrei fare. Forse ci sono soluzioni più facili. – RazrFalcon

+0

Immagino che ci proverò se non avrò risposte migliori. Il problema è che gestire un word-wrapping e visualizzare testi di dimensioni diverse uno dopo l'altro sarà un bel po 'di lavoro. E ho bisogno di questo per capire dove ho messo manualmente quel link. Oppure potrei fare uno scribacchino come controllare se il colore è stato cliccato se il link ha un unico. – Xilexio

2

Uso una soluzione leggermente migliorata basata sul codice Xilexio.Vi sono 3 differenze fondamentali:

  • Allineamento verticale, quindi se metti il ​​testo in una cella più in alto del testo sarà allineato al centro e non allineato in alto.
  • Il testo verrà spostato a destra se la cella contiene un'icona in modo che l'icona non venga visualizzata sopra il testo.
  • Lo stile del widget per le celle evidenziate verrà seguito, quindi si seleziona questa cella, i colori si comporteranno come le altre celle senza il delegato.

Ecco il mio codice della funzione paint() (il resto del codice rimane lo stesso).

QStyleOptionViewItemV4 options = option; 
initStyleOption(&options, index); 

painter->save(); 

QTextDocument doc; 
doc.setHtml(options.text); 

options.text = ""; 
options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter); 

QSize iconSize = options.icon.actualSize(options.rect.size); 
// right shit the icon 
painter->translate(options.rect.left() + iconSize.width(), options.rect.top()); 
QRect clip(0, 0, options.rect.width() + iconSize.width(), options.rect.height()); 

painter->setClipRect(clip); 
QAbstractTextDocumentLayout::PaintContext ctx; 

// Adjust color palette if the cell is selected 
if (option.state & QStyle::State_Selected) 
    ctx.palette.setColor(QPalette::Text, option.palette.color(QPalette::Active, QPalette::HighlightedText)); 
ctx.clip = clip; 

// Vertical Center alignment instead of the default top alignment 
painter->translate(0, 0.5*(options.rect.height() - doc.size().height())); 

doc.documentLayout()->draw(painter, ctx); 
painter->restore(); 
0

Molte grazie per questi esempi di codice, mi ha aiutato a implementare functionalaity simile nella mia applicazione. Sto lavorando con Python 3 e QT5 e vorrei condividere il mio codice Python, è un caso che potrebbe essere utile implementarlo in Python.

Si noti che se si utilizza QT Designer per la progettazione dell'interfaccia utente, è possibile utilizzare "promuovi" per modificare un normale widget "QTableView" per utilizzare automaticamente il widget personalizzato quando si converte il codice XML in Python con "pyuic5".

codice come segue:

from PyQt5 import QtCore, QtWidgets, QtGui 

class CustomTableView(QtWidgets.QTableView): 

    link_activated = QtCore.pyqtSignal(str) 

    def __init__(self, parent=None): 
     self.parent = parent 
     super().__init__(parent) 

     self.setMouseTracking(True) 
     self._mousePressAnchor = '' 
     self._lastHoveredAnchor = '' 

    def mousePressEvent(self, event): 
     anchor = self.anchorAt(event.pos()) 
     self._mousePressAnchor = anchor 

    def mouseMoveEvent(self, event): 
     anchor = self.anchorAt(event.pos()) 
     if self._mousePressAnchor != anchor: 
      self._mousePressAnchor = '' 

     if self._lastHoveredAnchor != anchor: 
      self._lastHoveredAnchor = anchor 
      if self._lastHoveredAnchor: 
       QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 
      else: 
       QtWidgets.QApplication.restoreOverrideCursor() 

    def mouseReleaseEvent(self, event): 
     if self._mousePressAnchor: 
      anchor = self.anchorAt(event.pos()) 
      if anchor == self._mousePressAnchor: 
       self.link_activated.emit(anchor) 
      self._mousePressAnchor = '' 

    def anchorAt(self, pos): 
     index = self.indexAt(pos) 
     if index.isValid(): 
      delegate = self.itemDelegate(index) 
      if delegate: 
       itemRect = self.visualRect(index) 
       relativeClickPosition = pos - itemRect.topLeft() 
       html = self.model().data(index, QtCore.Qt.DisplayRole) 
       return delegate.anchorAt(html, relativeClickPosition) 
     return '' 


class CustomDelegate(QtWidgets.QStyledItemDelegate): 

    def anchorAt(self, html, point): 
     doc = QtGui.QTextDocument() 
     doc.setHtml(html) 
     textLayout = doc.documentLayout() 
     return textLayout.anchorAt(point) 

    def paint(self, painter, option, index): 
     options = QtWidgets.QStyleOptionViewItem(option) 
     self.initStyleOption(options, index) 

     if options.widget: 
      style = options.widget.style() 
     else: 
      style = QtWidgets.QApplication.style() 

     doc = QtGui.QTextDocument() 
     doc.setHtml(options.text) 
     options.text = '' 

     style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, options, painter) 
     ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() 

     textRect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, options) 

     painter.save() 

     painter.translate(textRect.topLeft()) 
     painter.setClipRect(textRect.translated(-textRect.topLeft())) 
     painter.translate(0, 0.5*(options.rect.height() - doc.size().height())) 
     doc.documentLayout().draw(painter, ctx) 

     painter.restore() 

    def sizeHint(self, option, index): 
     options = QtWidgets.QStyleOptionViewItem(option) 
     self.initStyleOption(options, index) 

     doc = QtGui.QTextDocument() 
     doc.setHtml(options.text) 
     doc.setTextWidth(options.rect.width()) 

     return QtCore.QSize(doc.idealWidth(), doc.size().height()) 
Problemi correlati