Как сделать так, чтобы представление элементов отображало форматированный (html) текст в Qt
Предположим, что в моей модели есть элементы со следующей строкой для Qt::DisplayRole
<span>blah-blah <b>some text</b> other blah</span>
Я хочу, чтобы QTreeView (фактически любой вид элемента) отображал его как форматированный текст. Вместо этого представления элементов отображают его как чистый текст по умолчанию. Как добиться желаемого рендеринга?
На самом деле, это модель результатов поиска. Пользователь вводит текст, в этом тексте выполняется поиск некоторого документа, и пользователю предоставляются результаты поиска, где искомые слова должны быть более жирными, чем окружающий текст.
6 ответов
Мой ответ в основном вдохновлен ответом @serge_gubenko. Тем не менее, было сделано несколько улучшений, так что код, наконец, полезен в моем приложении.
class HtmlDelegate : public QStyledItemDelegate
{
protected:
void paint ( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const;
QSize sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const;
};
void HtmlDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionViewItemV4 optionV4 = option;
initStyleOption(&optionV4, index);
QStyle *style = optionV4.widget? optionV4.widget->style() : QApplication::style();
QTextDocument doc;
doc.setHtml(optionV4.text);
/// Painting item without text
optionV4.text = QString();
style->drawControl(QStyle::CE_ItemViewItem, &optionV4, painter);
QAbstractTextDocumentLayout::PaintContext ctx;
// Highlighting text if item is selected
if (optionV4.state & QStyle::State_Selected)
ctx.palette.setColor(QPalette::Text, optionV4.palette.color(QPalette::Active, QPalette::HighlightedText));
QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &optionV4);
painter->save();
painter->translate(textRect.topLeft());
painter->setClipRect(textRect.translated(-textRect.topLeft()));
doc.documentLayout()->draw(painter, ctx);
painter->restore();
}
QSize HtmlDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionViewItemV4 optionV4 = option;
initStyleOption(&optionV4, index);
QTextDocument doc;
doc.setHtml(optionV4.text);
doc.setTextWidth(optionV4.rect.width());
return QSize(doc.idealWidth(), doc.size().height());
}
Я предполагаю, что вы можете использовать метод setItemDelegate древовидной структуры для настройки собственного рисователя для элементов древовидной структуры. В методе рисования делегата вы можете использовать QTextDocument для загрузки текста элемента в виде HTML и его рендеринга. Пожалуйста, проверьте, подойдет ли вам приведенный ниже пример:
инициализация в виде дерева:
...
// create simple model for a tree view
QStandardItemModel *model = new QStandardItemModel();
QModelIndex parentItem;
for (int i = 0; i < 4; ++i)
{
parentItem = model->index(0, 0, parentItem);
model->insertRows(0, 1, parentItem);
model->insertColumns(0, 1, parentItem);
QModelIndex index = model->index(0, 0, parentItem);
model->setData(index, "<span>blah-blah <b>some text</b> other blah</span>");
}
// create custom delegate
HTMLDelegate* delegate = new HTMLDelegate();
// set model and delegate to the treeview object
ui->treeView->setModel(model);
ui->treeView->setItemDelegate(delegate);
...
пользовательская реализация делегата
class HTMLDelegate : public QStyledItemDelegate
{
protected:
void paint ( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const;
QSize sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const;
};
void HTMLDelegate::paint(QPainter* painter, const QStyleOptionViewItem & option, const QModelIndex &index) const
{
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);
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 HTMLDelegate::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());
}
надеюсь, что это помогает, привет
update0: изменения в HTMLDelegate для отображения значков и различного цвета пера для выбранных элементов
void HTMLDelegate::paint(QPainter* painter, const QStyleOptionViewItem & option, const QModelIndex &index) const
{
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);
// shift text right to make icon visible
QSize iconSize = options.icon.actualSize(options.rect.size());
painter->translate(options.rect.left()+iconSize.width(), options.rect.top());
QRect clip(0, 0, options.rect.width()+iconSize.width(), options.rect.height());
//doc.drawContents(painter, clip);
painter->setClipRect(clip);
QAbstractTextDocumentLayout::PaintContext ctx;
// set text color to red for selected item
if (option.state & QStyle::State_Selected)
ctx.palette.setColor(QPalette::Text, QColor("red"));
ctx.clip = clip;
doc.documentLayout()->draw(painter, ctx);
painter->restore();
}
Вот преобразование PyQt комбинации вышеуказанных ответов, которые работали для меня. Я ожидаю, что это будет работать практически одинаково для PySide.
from PyQt4 import QtCore, QtGui
class HTMLDelegate(QtGui.QStyledItemDelegate):
def paint(self, painter, option, index):
options = QtGui.QStyleOptionViewItemV4(option)
self.initStyleOption(options,index)
style = QtGui.QApplication.style() if options.widget is None else options.widget.style()
doc = QtGui.QTextDocument()
doc.setHtml(options.text)
options.text = ""
style.drawControl(QtGui.QStyle.CE_ItemViewItem, options, painter);
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
# Highlighting text if item is selected
#if (optionV4.state & QStyle::State_Selected)
#ctx.palette.setColor(QPalette::Text, optionV4.palette.color(QPalette::Active, QPalette::HighlightedText));
textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, options)
painter.save()
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint(self, option, index):
options = QtGui.QStyleOptionViewItemV4(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())
Написание еще одного ответа о том, как это можно сделать на C++. Разница в ответах, представленных до сих пор, заключается в том, что это для Qt5, а не для Qt4. Однако наиболее важно, однако, в предыдущих ответах не учитывалось, что делегат элемента должен иметь возможность выровнять текст, как указано (например, в
Итак, без лишних слов, вот мой код для
void RichTextItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &inOption,
const QModelIndex &index) const {
QStyleOptionViewItem option = inOption;
initStyleOption(&option, index);
if (option.text.isEmpty()) {
// This is nothing this function is supposed to handle
QStyledItemDelegate::paint(painter, inOption, index);
return;
}
QStyle *style = option.widget ? option.widget->style() : QApplication::style();
QTextOption textOption;
textOption.setWrapMode(option.features & QStyleOptionViewItem::WrapText ? QTextOption::WordWrap
: QTextOption::ManualWrap);
textOption.setTextDirection(option.direction);
QTextDocument doc;
doc.setDefaultTextOption(textOption);
doc.setHtml(option.text);
doc.setDefaultFont(option.font);
doc.setDocumentMargin(0);
doc.setTextWidth(option.rect.width());
doc.adjustSize();
if (doc.size().width() > option.rect.width()) {
// Elide text
QTextCursor cursor(&doc);
cursor.movePosition(QTextCursor::End);
const QString elidedPostfix = "...";
QFontMetrics metric(option.font);
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
int postfixWidth = metric.horizontalAdvance(elidedPostfix);
#else
int postfixWidth = metric.width(elidedPostfix);
#endif
while (doc.size().width() > option.rect.width() - postfixWidth) {
cursor.deletePreviousChar();
doc.adjustSize();
}
cursor.insertText(elidedPostfix);
}
// Painting item without text (this takes care of painting e.g. the highlighted for selected
// or hovered over items in an ItemView)
option.text = QString();
style->drawControl(QStyle::CE_ItemViewItem, &option, painter, inOption.widget);
// Figure out where to render the text in order to follow the requested alignment
QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &option);
QSize documentSize(doc.size().width(), doc.size().height()); // Convert QSizeF to QSize
QRect layoutRect = QStyle::alignedRect(Qt::LayoutDirectionAuto, option.displayAlignment, documentSize, textRect);
painter->save();
// Translate the painter to the origin of the layout rectangle in order for the text to be
// rendered at the correct position
painter->translate(layoutRect.topLeft());
doc.drawContents(painter, textRect.translated(-textRect.topLeft()));
painter->restore();
}
QSize RichTextItemDelegate::sizeHint(const QStyleOptionViewItem &inOption, const QModelIndex &index) const {
QStyleOptionViewItem option = inOption;
initStyleOption(&option, index);
if (option.text.isEmpty()) {
// This is nothing this function is supposed to handle
return QStyledItemDelegate::sizeHint(inOption, index);
}
QTextDocument doc;
doc.setHtml(option.text);
doc.setTextWidth(option.rect.width());
doc.setDefaultFont(option.font);
doc.setDocumentMargin(0);
return QSize(doc.idealWidth(), doc.size().height());
}
Этот в PySide. Вместо того, чтобы много рисовать, я передаю QPainter в QLabel и заставляю его рисовать сам. Подсветка кода заимствована из других ответов.
from PySide import QtGui
class TaskDelegate(QtGui.QItemDelegate):
#http://doc.qt.nokia.com/4.7/qitemdelegate.html#drawDisplay
#http://doc.qt.nokia.com/4.7/qwidget.html#render
def drawDisplay(self, painter, option, rect, text):
label = QtGui.QLabel(text)
if option.state & QtGui.QStyle.State_Selected:
p = option.palette
p.setColor(QtGui.QPalette.WindowText, p.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
label.setPalette(p)
label.render(painter, rect.topLeft(), renderFlags=QtGui.QWidget.DrawChildren)
Просто небольшое обновление из ответа jbmohler для PyQt5: некоторые классы, по-видимому, были перенесены на
QtWidgets
Это выходит далеко за рамки моей зарплаты (т.е. знания болтов и гаек, стоящих за PyQt5).
Я разделяю мнение, выраженное в комментарии Сесила Карри к вопросу. Сейчас 2021 год, и нам, похоже, еще предстоит бороться с такого рода взломами. Смешной. На сегодняшний день Qt5 впечатлил меня, по сравнению, например, с JavaFX. Этот недостаток вызывает разочарование.
class HTMLDelegate( QtWidgets.QStyledItemDelegate ):
def __init__( self ):
super().__init__()
# probably better not to create new QTextDocuments every ms
self.doc = QtGui.QTextDocument()
def paint(self, painter, option, index):
options = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(options, index)
painter.save()
self.doc.setTextWidth(options.rect.width())
self.doc.setHtml(options.text)
self.doc.setDefaultFont(options.font)
options.text = ''
options.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, options, painter)
painter.translate(options.rect.left(), options.rect.top())
clip = QtCore.QRectF(0, 0, options.rect.width(), options.rect.height())
painter.setClipRect(clip)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
ctx.clip = clip
self.doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint( self, option, index ):
options = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(option, index)
self.doc.setHtml(option.text)
self.doc.setTextWidth(option.rect.width())
return QtCore.QSize(self.doc.idealWidth(), self.doc.size().height())