Using QListView with a custom item layout has been a very popular demand by many developers. The problem with using a custom delegate was having some basic features like word wrap depending on the allowed list size.
Here is a screenshot from a project prototype I've been working on. The text will wrap around to fit in multiple lines depending on text length and list view width.
My custom delegate is about using two important methods: QFontMetrics::boundingRect() and QPainter::drawText() .
The first method will return the minimum Rect needed to draw the given text using the given origin and font. The second method draws the given text within the given Rect. Note that the flag Qt::TextWordWrap is given to both methods when called.
The code is simple:
======
[listviewdelegate.h]
[listviewdelegate.cpp]
Using it is pretty simple:
[mainwindow.cpp]
Here is a screenshot from a project prototype I've been working on. The text will wrap around to fit in multiple lines depending on text length and list view width.
My custom delegate is about using two important methods: QFontMetrics::boundingRect() and QPainter::drawText() .
The first method will return the minimum Rect needed to draw the given text using the given origin and font. The second method draws the given text within the given Rect. Note that the flag Qt::TextWordWrap is given to both methods when called.
The code is simple:
- Get system default font.
- Process the font to suite your needs. In my case: I have a normal font and another bold one for the header.
- Create a QFontMetrics instance for each font.
- Use the QFontMetrics instances to calculate the Rect needed to draw that text given the needed flags and option.rect.width() that indicates the allowed width inside the list view.
- Calculate the total area needed to return it in the sizeHint() method of your list view delegate. For example: I return the total heights of the two calculated rects -header and subheader- and the item width from option.rect.width(). And I prefer to add some extra padding.
- Almost the same calculations apply in the paint() method of the list view delegate. Then the calculations are used in drawText().
======
[listviewdelegate.h]
#ifndef LISTVIEWDELEGATE_H #define LISTVIEWDELEGATE_H #include <QStyledItemDelegate> #include <QLabel> #include <QPainter> #include <QApplication> class ListViewDelegate : public QStyledItemDelegate { public: enum datarole { HeaderRole = Qt::UserRole + 100, SubheaderRole}; ListViewDelegate(); ~ListViewDelegate(); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index ) const; static QSize iconSize; static int padding; }; #endif // LISTVIEWDELEGATE_H======
[listviewdelegate.cpp]
#include "listviewdelegate.h" QSize ListViewDelegate::iconSize = QSize(60, 60); int ListViewDelegate::padding = 5; ListViewDelegate::ListViewDelegate() { } ListViewDelegate::~ListViewDelegate() { } QSize ListViewDelegate::sizeHint(const QStyleOptionViewItem & option , const QModelIndex & index) const { if(!index.isValid()) return QSize(); QString headerText = index.data(HeaderRole).toString(); QString subheaderText = index.data(SubheaderRole).toString(); QFont headerFont = QApplication::font(); headerFont.setBold(true); QFont subheaderFont = QApplication::font(); QFontMetrics headerFm(headerFont); QFontMetrics subheaderFm(subheaderFont); /* No need for x,y here. we only need to calculate the height given the width. * Note that the given height is 0. That is because boundingRect() will return * the suitable height if the given geometry does not fit. And this is exactly * what we want. */ QRect headerRect = headerFm.boundingRect(0, 0, option.rect.width() - iconSize.width(), 0, Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap, headerText); QRect subheaderRect = subheaderFm.boundingRect(0, 0, option.rect.width() - iconSize.width(), 0, Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap, subheaderText); QSize size(option.rect.width(), headerRect.height() + subheaderRect.height() + 3*padding); /* Keep the minimum height needed in mind. */ if(size.height()<iconSize.height()) size.setHeight(iconSize.height()); return size; } void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if(!index.isValid()) return; painter->save(); if (option.state & QStyle::State_Selected) painter->fillRect(option.rect, option.palette.highlight()); QString headerText = index.data(HeaderRole).toString(); QString subheaderText = index.data(SubheaderRole).toString(); QFont headerFont = QApplication::font(); headerFont.setBold(true); QFont subheaderFont = QApplication::font(); QFontMetrics headerFm(headerFont); QFontMetrics subheaderFm(subheaderFont); /* * The x,y coords are not (0,0) but values given by 'option'. So, calculate the * rects again given the x,y,w. * Note that the given height is 0. That is because boundingRect() will return * the suitable height if the given geometry does not fit. And this is exactly * what we want. */ QRect headerRect = headerFm.boundingRect(option.rect.left() + iconSize.width(), option.rect.top() + padding, option.rect.width() - iconSize.width(), 0, Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap, headerText); QRect subheaderRect = subheaderFm.boundingRect(headerRect.left(), headerRect.bottom()+padding, option.rect.width() - iconSize.width(), 0, Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap, subheaderText); painter->setPen(Qt::black); painter->setFont(headerFont); painter->drawText(headerRect, Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap, headerText); painter->setFont(subheaderFont); painter->drawText(subheaderRect, Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap, subheaderText); painter->restore(); }===========
Using it is pretty simple:
[mainwindow.cpp]
QStandardItemModel *model; model = new QStandardItemModel(); ListViewDelegate *listdelegate; listdelegate = new ListViewDelegate(); ui->listView->setItemDelegate(listdelegate); ui->listView->setModel(model); QStandardItem *item = new QStandardItem(); item->setData("Some Header",ListViewDelegate::HeaderRole); item->setData("Some long description text.",ListViewDelegate::SubheaderRole); model->appendRow(item);
Hi,
ReplyDeleteThank you for the tutorial. Can you give tutorial to insert new row to the ListView by push button?
Thanks
Sure, it is a small piece of code. I can post it today or tomorrow.
DeleteHi brother,
DeleteI was reviewing my code to discover I was doing it in one line, like:
model->appendRow(item);
So, depending on your application, you just create whatever QStandardItem you want (and maybe save some data in database) then just add the item with that single line.
In my case: I used to call a controller that manages saving my new item to the database then the controller emits a signal with the saved item and I handle it in my view like:
connect(EntryController::getInstance(), SIGNAL(entryCreated(QStandardItem*)),
SLOT(entryCreated(QStandardItem*)));
and in the slot I add the item to the model.
Thanks!
DeleteHi buddy, I have a question regarding your post.
ReplyDeleteI follow the path you are suggesting here but I'm unable to wrap the content. it still consumes (and even exceeds) the rect.width(). Can it be related with a listview property?
Yes it can. It happened with me while testing my code. Maybe you can check your listview properties for any changed values and reset them to defaults for testing.
DeleteI can't remember the property that caused my problem, but I think it was a property for word wrap.
Thanks for code sharing, it is very useful to me.
ReplyDeleteI'm glad I could help. Thank you for your comment.
DeleteThanks for the code! One note: You should probably also be using if your text can be higher than the height of the viewport: `setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)` Otherwise you won't be able to view certain portions of the text.
ReplyDeleteGood job. Thanks
ReplyDelete