QTableWidget and QStyledItemDelegate
- 
The information in one column of a table widget is quite long (full file path), so I am trying to use a styled item delegate to force it to elide in the middle. // // The filename is quite long (full path), so use a subclass // of QStyledItemDelegate to handle the rendering for column one // the table with the text set to elide in the middle. // elidedTextDelegate = new ElidedTextDelegate(this); imageList->setItemDelegateForColumn(static_cast<int>(ImageListColumns::File), elidedTextDelegate);Header information for the delegate: class ElidedTextDelegate : public QStyledItemDelegate { Q_OBJECT public: using QStyledItemDelegate::QStyledItemDelegate; protected: void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; //inline QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const //{ // QSize result = QStyledItemDelegate::sizeHint(option, index); // result.setHeight(result.height() * 1.2); // return result; //} QString calculateElidedText(const QString& text, const QTextOption& textOption, const QFont& font, const QRect& textRect, const Qt::Alignment valign, Qt::TextElideMode textElideMode, [[maybe_unused]] int flags, bool lastVisibleLineShouldBeElided, QPointF* paintStartPosition) const; };Implementation: static QSizeF viewItemTextLayout(QTextLayout& textLayout, int lineWidth, int maxHeight = -1, int* lastVisibleLine = nullptr) { if (lastVisibleLine) *lastVisibleLine = -1; qreal height = 0; qreal widthUsed = 0; textLayout.beginLayout(); int i = 0; while (true) { QTextLine line = textLayout.createLine(); if (!line.isValid()) break; line.setLineWidth(lineWidth); line.setPosition(QPointF(0, height)); height += line.height(); widthUsed = qMax(widthUsed, line.naturalTextWidth()); // we assume that the height of the next line is the same as the current one if (maxHeight > 0 && lastVisibleLine && height + line.height() > maxHeight) { const QTextLine nextLine = textLayout.createLine(); *lastVisibleLine = nextLine.isValid() ? i : -1; break; } ++i; } textLayout.endLayout(); return QSizeF(widthUsed, height); } QString ElidedTextDelegate::calculateElidedText(const ::QString& text, const QTextOption& textOption, const QFont& font, const QRect& textRect, const Qt::Alignment valign, Qt::TextElideMode textElideMode, [[maybe_unused]] int flags, bool lastVisibleLineShouldBeElided, QPointF* paintStartPosition) const { QTextLayout textLayout(text, font); textLayout.setTextOption(textOption); // In AlignVCenter mode when more than one line is displayed and the height only allows // some of the lines it makes no sense to display those. From a users perspective it makes // more sense to see the start of the text instead something inbetween. const bool vAlignmentOptimization = paintStartPosition && valign.testFlag(Qt::AlignVCenter); int lastVisibleLine = -1; viewItemTextLayout(textLayout, textRect.width(), vAlignmentOptimization ? textRect.height() : -1, &lastVisibleLine); const QRectF boundingRect = textLayout.boundingRect(); // don't care about LTR/RTL here, only need the height const QRect layoutRect = QStyle::alignedRect(Qt::LayoutDirectionAuto, valign, boundingRect.size().toSize(), textRect); if (paintStartPosition) *paintStartPosition = QPointF(textRect.x(), layoutRect.top()); QString ret; qreal height = 0; const int lineCount = textLayout.lineCount(); for (int i = 0; i < lineCount; ++i) { const QTextLine line = textLayout.lineAt(i); height += line.height(); // above visible rect if (height + layoutRect.top() <= textRect.top()) { if (paintStartPosition) paintStartPosition->ry() += line.height(); continue; } const int start = line.textStart(); const int length = line.textLength(); const bool drawElided = line.naturalTextWidth() > textRect.width(); bool elideLastVisibleLine = lastVisibleLine == i; if (!drawElided && i + 1 < lineCount && lastVisibleLineShouldBeElided) { const QTextLine nextLine = textLayout.lineAt(i + 1); const int nextHeight = height + nextLine.height() / 2; // elide when less than the next half line is visible if (nextHeight + layoutRect.top() > textRect.height() + textRect.top()) elideLastVisibleLine = true; } QString text1 = textLayout.text().mid(start, length); if (drawElided || elideLastVisibleLine) { if (elideLastVisibleLine) { if (text1.endsWith(QChar::LineSeparator)) text1.chop(1); text1 += QChar(0x2026); } QFontMetrics fontMetrics(font); ret += fontMetrics.elidedText(text1, textElideMode, textRect.width()); // no newline for the last line (last visible or real) // sometimes drawElided is true but no eliding is done so the text ends // with QChar::LineSeparator - don't add another one. This happened with // arabic text in the testcase for QTBUG-72805 if (i < lineCount - 1 && !ret.endsWith(QChar::LineSeparator)) ret += QChar::LineSeparator; } else { ret += text1; } // below visible text, can stop if ((height + layoutRect.top() >= textRect.bottom()) || (lastVisibleLine >= 0 && lastVisibleLine == i)) break; } return ret; } void ElidedTextDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { Q_ASSERT(index.isValid()); QStyleOptionViewItem opt{ option }; initStyleOption(&opt, index); QStyle* style = opt.widget ? opt.widget->style() : QApplication::style(); opt.textElideMode = Qt::ElideMiddle; painter->save(); QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &opt); // // Draw the text as elided text // QFontMetrics fontMetrics(opt.font); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr) + 1; textRect = textRect.adjusted(textMargin, 0, -textMargin, 0); // remove width padding const bool wrapText = opt.features & QStyleOptionViewItem::WrapText; QTextOption textOption; textOption.setWrapMode(wrapText ? QTextOption::WordWrap : QTextOption::ManualWrap); textOption.setTextDirection(opt.direction); textOption.setAlignment(QStyle::visualAlignment(opt.direction, opt.displayAlignment)); QPointF paintPosition; const QString newText = calculateElidedText(opt.text, textOption, opt.font, textRect, opt.displayAlignment, opt.textElideMode, 0, true, &paintPosition); QTextLayout textLayout(newText, opt.font); textLayout.setTextOption(textOption); viewItemTextLayout(textLayout, textRect.width()); textLayout.draw(painter, paintPosition); painter->restore(); }My problem is it is not eliding in the middle, but the end, and I'm also seeing this in the debug log: QPainter::begin: Paint device returned engine == 0, type: 2 QPainter::setRenderHint: Painter must be active to set rendering hints QPainter::setRenderHint: Painter must be active to set rendering hintswhich don't appear to be from my code! Clearly I've messed it up, but how!? Thanks 
 David
- 
The information in one column of a table widget is quite long (full file path), so I am trying to use a styled item delegate to force it to elide in the middle. // // The filename is quite long (full path), so use a subclass // of QStyledItemDelegate to handle the rendering for column one // the table with the text set to elide in the middle. // elidedTextDelegate = new ElidedTextDelegate(this); imageList->setItemDelegateForColumn(static_cast<int>(ImageListColumns::File), elidedTextDelegate);Header information for the delegate: class ElidedTextDelegate : public QStyledItemDelegate { Q_OBJECT public: using QStyledItemDelegate::QStyledItemDelegate; protected: void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; //inline QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const //{ // QSize result = QStyledItemDelegate::sizeHint(option, index); // result.setHeight(result.height() * 1.2); // return result; //} QString calculateElidedText(const QString& text, const QTextOption& textOption, const QFont& font, const QRect& textRect, const Qt::Alignment valign, Qt::TextElideMode textElideMode, [[maybe_unused]] int flags, bool lastVisibleLineShouldBeElided, QPointF* paintStartPosition) const; };Implementation: static QSizeF viewItemTextLayout(QTextLayout& textLayout, int lineWidth, int maxHeight = -1, int* lastVisibleLine = nullptr) { if (lastVisibleLine) *lastVisibleLine = -1; qreal height = 0; qreal widthUsed = 0; textLayout.beginLayout(); int i = 0; while (true) { QTextLine line = textLayout.createLine(); if (!line.isValid()) break; line.setLineWidth(lineWidth); line.setPosition(QPointF(0, height)); height += line.height(); widthUsed = qMax(widthUsed, line.naturalTextWidth()); // we assume that the height of the next line is the same as the current one if (maxHeight > 0 && lastVisibleLine && height + line.height() > maxHeight) { const QTextLine nextLine = textLayout.createLine(); *lastVisibleLine = nextLine.isValid() ? i : -1; break; } ++i; } textLayout.endLayout(); return QSizeF(widthUsed, height); } QString ElidedTextDelegate::calculateElidedText(const ::QString& text, const QTextOption& textOption, const QFont& font, const QRect& textRect, const Qt::Alignment valign, Qt::TextElideMode textElideMode, [[maybe_unused]] int flags, bool lastVisibleLineShouldBeElided, QPointF* paintStartPosition) const { QTextLayout textLayout(text, font); textLayout.setTextOption(textOption); // In AlignVCenter mode when more than one line is displayed and the height only allows // some of the lines it makes no sense to display those. From a users perspective it makes // more sense to see the start of the text instead something inbetween. const bool vAlignmentOptimization = paintStartPosition && valign.testFlag(Qt::AlignVCenter); int lastVisibleLine = -1; viewItemTextLayout(textLayout, textRect.width(), vAlignmentOptimization ? textRect.height() : -1, &lastVisibleLine); const QRectF boundingRect = textLayout.boundingRect(); // don't care about LTR/RTL here, only need the height const QRect layoutRect = QStyle::alignedRect(Qt::LayoutDirectionAuto, valign, boundingRect.size().toSize(), textRect); if (paintStartPosition) *paintStartPosition = QPointF(textRect.x(), layoutRect.top()); QString ret; qreal height = 0; const int lineCount = textLayout.lineCount(); for (int i = 0; i < lineCount; ++i) { const QTextLine line = textLayout.lineAt(i); height += line.height(); // above visible rect if (height + layoutRect.top() <= textRect.top()) { if (paintStartPosition) paintStartPosition->ry() += line.height(); continue; } const int start = line.textStart(); const int length = line.textLength(); const bool drawElided = line.naturalTextWidth() > textRect.width(); bool elideLastVisibleLine = lastVisibleLine == i; if (!drawElided && i + 1 < lineCount && lastVisibleLineShouldBeElided) { const QTextLine nextLine = textLayout.lineAt(i + 1); const int nextHeight = height + nextLine.height() / 2; // elide when less than the next half line is visible if (nextHeight + layoutRect.top() > textRect.height() + textRect.top()) elideLastVisibleLine = true; } QString text1 = textLayout.text().mid(start, length); if (drawElided || elideLastVisibleLine) { if (elideLastVisibleLine) { if (text1.endsWith(QChar::LineSeparator)) text1.chop(1); text1 += QChar(0x2026); } QFontMetrics fontMetrics(font); ret += fontMetrics.elidedText(text1, textElideMode, textRect.width()); // no newline for the last line (last visible or real) // sometimes drawElided is true but no eliding is done so the text ends // with QChar::LineSeparator - don't add another one. This happened with // arabic text in the testcase for QTBUG-72805 if (i < lineCount - 1 && !ret.endsWith(QChar::LineSeparator)) ret += QChar::LineSeparator; } else { ret += text1; } // below visible text, can stop if ((height + layoutRect.top() >= textRect.bottom()) || (lastVisibleLine >= 0 && lastVisibleLine == i)) break; } return ret; } void ElidedTextDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { Q_ASSERT(index.isValid()); QStyleOptionViewItem opt{ option }; initStyleOption(&opt, index); QStyle* style = opt.widget ? opt.widget->style() : QApplication::style(); opt.textElideMode = Qt::ElideMiddle; painter->save(); QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &opt); // // Draw the text as elided text // QFontMetrics fontMetrics(opt.font); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr) + 1; textRect = textRect.adjusted(textMargin, 0, -textMargin, 0); // remove width padding const bool wrapText = opt.features & QStyleOptionViewItem::WrapText; QTextOption textOption; textOption.setWrapMode(wrapText ? QTextOption::WordWrap : QTextOption::ManualWrap); textOption.setTextDirection(opt.direction); textOption.setAlignment(QStyle::visualAlignment(opt.direction, opt.displayAlignment)); QPointF paintPosition; const QString newText = calculateElidedText(opt.text, textOption, opt.font, textRect, opt.displayAlignment, opt.textElideMode, 0, true, &paintPosition); QTextLayout textLayout(newText, opt.font); textLayout.setTextOption(textOption); viewItemTextLayout(textLayout, textRect.width()); textLayout.draw(painter, paintPosition); painter->restore(); }My problem is it is not eliding in the middle, but the end, and I'm also seeing this in the debug log: QPainter::begin: Paint device returned engine == 0, type: 2 QPainter::setRenderHint: Painter must be active to set rendering hints QPainter::setRenderHint: Painter must be active to set rendering hintswhich don't appear to be from my code! Clearly I've messed it up, but how!? Thanks 
 David@Perdrix I also tried: class ElidedTextDelegate : public QStyledItemDelegate { Q_OBJECT public: using QStyledItemDelegate::QStyledItemDelegate; protected: void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override { QStyledItemDelegate::initStyleOption(option, index); option->textElideMode = Qt::ElideNone; } };but that didn't seem to help either :( D. 
- 
@Perdrix I also tried: class ElidedTextDelegate : public QStyledItemDelegate { Q_OBJECT public: using QStyledItemDelegate::QStyledItemDelegate; protected: void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override { QStyledItemDelegate::initStyleOption(option, index); option->textElideMode = Qt::ElideNone; } };but that didn't seem to help either :( D. 
- 
@JoeCFD One thing is sure - the debug errors are nothing to do with that styled item delegate! I decided in the end to just display the filename in the table view so didn't need the elision in the middle (though it would be nice to know how it is supposed to be done). How to track down those debug messages: QPainter::begin: Paint device returned engine == 0, type: 2 QPainter::setRenderHint: Painter must be active to set rendering hints QPainter::setRenderHint: Painter must be active to set rendering hints
- 
@JoeCFD One thing is sure - the debug errors are nothing to do with that styled item delegate! I decided in the end to just display the filename in the table view so didn't need the elision in the middle (though it would be nice to know how it is supposed to be done). How to track down those debug messages: QPainter::begin: Paint device returned engine == 0, type: 2 QPainter::setRenderHint: Painter must be active to set rendering hints QPainter::setRenderHint: Painter must be active to set rendering hints
 
