PDA

View Full Version : Word wrap in QListView



jiveaxe
2nd August 2007, 17:51
Hi everybody,
I was trying to create a listView with word wrap function for text in items (rather than using horizontal scrollbar).

I know that is necessary using delegates but the QT Manual wasn't of much help to me:

SpinBoxDelegate example (http://doc.trolltech.com/4.3/itemviews-spinboxdelegate.html) shows how insert a spinbox in a tableView but the spinBox is visible only when editing and not in view mode. It doesn't use paint().
StarDelegate example (http://doc.trolltech.com/4.3/itemviews-stardelegate.html) doesn't use a common widget but a paintDevice. It reimplement paint() (obviously).


Does somebody know an example/tutorial for delegate a common widget in view mode?

Delegating a QLabel for my purpose is correct or is recommended a different widget? Have I to reimplement paint() function?

I don't still have a sample code (a decent code :p) to post now, so if some patient man want submit some hints would be very appreciated.

Best regards

marcel
2nd August 2007, 18:13
You will have to do some something similar to the star delegate, only simpler.
No editor is needed.
You need the to override the paint for the delegate, and using the painter passed to you, paint the item text.
Here is where you do the wrapping.
Based on the item width, you can insert "\n"s in the text, or you can use a QTextLayout.
With the text layout you would have to draw the text on a pixmap, and display this pixmap in the delegate.

Regards

jiveaxe
2nd August 2007, 18:52
Ok marcel, I will follow your suggestion: modify stardelegate example and use qtextlayout.
Eventually I will post the resulting code.

Bye

jiveaxe
3rd August 2007, 16:04
Well, I have some new and some code to post.

Following marcel's suggestion I have modified the stardelegate example removing the editor and related parts. I have changed the parameter passed to StarRating (from int, the rating, to QString, the text to wordwrap) and used the code from this post (http://www.qtcentre.org/forum/f-qt-programming-2/t-qstandarditem-subpart-of-the-text-as-bold-5548.html); in this way the tableWidget contains a rich text column and long text is wrapped but it overlaps rows below: the cell height is not updated to enclose the multi-line text (hoping i was clear!).

Then i have made a new copy of the source code and substituted QTableWidget with QListWidget; after some editing i have obtained a working application but without wordwrap (it used a horizontal scrollbar if text is too long).

My questions are:
1) how disable scrollbar in qlistwidget and fix the width forcing word wrap?
2) how change row height according wordwrapping?

The application, like Star Delegate example, has 2 classes:
- TextDelegate.
- TextPainter.

Here the code (sorry for the long post):

textdelegate.h

#ifndef TEXTDELEGATE_H
#define TEXTDELEGATE_H

#include <QItemDelegate>

class TextDelegate : public QItemDelegate
{
Q_OBJECT

public:
TextDelegate(QWidget *parent = 0) : QItemDelegate(parent) {}

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

#endif

textdelegate.cpp

#include <QtGui>

#include "textdelegate.h"
#include "textpainter.h"

void TextDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (qVariantCanConvert<TextPainter>(index.data())) {
TextPainter textPainter = qVariantValue<TextPainter>(index.data());

if (option.state & QStyle::State_Selected)
painter->fillRect(option.rect, option.palette.highlight());

textPainter.paint(painter, option, TextPainter::ReadOnly);
} else {
QItemDelegate::paint(painter, option, index);
}
}

QSize TextDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (qVariantCanConvert<TextPainter>(index.data())) {
TextPainter textPainter = qVariantValue<TextPainter>(index.data());
return textPainter.sizeHint();
} else {
return QItemDelegate::sizeHint(option, index);
}
}

textpainter.h

#ifndef TEXTPAINTER_H
#define TEXTPAINTER_H

#include <QMetaType>
#include <QString>

class TextPainter
{
public:
enum EditMode { Editable, ReadOnly };

TextPainter(QString text = "");

void paint(QPainter *painter, const QStyleOptionViewItem &option, EditMode mode) const;
QSize sizeHint() const;

private:

QString itemText;
};

Q_DECLARE_METATYPE(TextPainter)

#endif

textpainter.cpp

#include <QtGui>

#include "textpainter.h"

const int PaintingScaleFactor = 20;

TextPainter::TextPainter(QString text)
{
itemText = text;
}

QSize TextPainter::sizeHint() const
{
return PaintingScaleFactor * QSize(itemText.length()/2-2, 1);
}

void TextPainter::paint(QPainter *painter, const QStyleOptionViewItem &option, EditMode mode) const
{
if (mode == Editable) {
painter->setBrush(option.palette.highlight());
} else {
painter->setBrush(option.palette.foreground());
}


painter->save();
QTextDocument doc;
doc.setHtml(itemText);
QAbstractTextDocumentLayout::PaintContext context;
doc.setPageSize( option.rect.size());
painter->translate(option.rect.x(), option.rect.y()-20);
doc.documentLayout()->draw(painter, context);
painter->restore();
}

jiveaxe
4th August 2007, 16:48
I forgot main.cpp (maybe someone needs it):


#include <QApplication>
#include <QListWidget>
#include <QStringList>

#include "textdelegate.h"
#include "textpainter.h"


int main(int argc, char *argv[])
{
QApplication app( argc, argv );

QListWidget listWidget;
listWidget.setItemDelegate(new TextDelegate);
listWidget.setAlternatingRowColors(true);
listWidget.setResizeMode(QListWidget::Adjust);

QStringList list;
list << "The <b>QListWidget</b> class provides an item-based list widget."
<< "The <b>QListWidgetItem</b> class provides an item for use with the QListWidget item view class."
<< "The <b>QObject</b> class is the base class of all Qt objects."
<< "Qt's <b>drag and drop</b> infrastructure is fully supported by the model/view framework. Items in lists, tables, and trees can be dragged within the views, and data can be imported and exported as MIME-encoded data.";

for (int row = 0; row < list.size(); ++row) {
QListWidgetItem *item = new QListWidgetItem;
item->setData(0, qVariantFromValue(TextPainter(list.at(row))));
listWidget.insertItem(row, item);
}

listWidget.setWindowTitle(QObject::tr("Text Delegate"));
listWidget.show();


return app.exec();
}

Bye

marcel
4th August 2007, 18:27
I did not test the code yet, but I don't think it is a good idea to use the text layout like that.
Anyway, keeping the code, I think you can compute the wrapped text bounding rect separately from the text layout.
You should use QFontMetrics, just like in the other post, and compute the size hint height according to how many lines of text you have and the text height(also given by font metrics ).


Regards

jiveaxe
4th August 2007, 18:37
How can I get the number of lines?

Thanks

jiveaxe
4th August 2007, 18:44
Have found numLines() in Q3MultiLineEdit; maybe switching from QTextDocument to it can help. What do you say?

Bye

Edit: Forgot this post, Q3MultiLineEdit is for QT3.

marcel
4th August 2007, 18:48
Look how it is done in the other post!
Generally, if the item rect is rW, and the unwrapped text width returned by font metrics tW.
You have:


float f = tW/rW;
int approxLineCount = int(f) + 1;

approxLineCount tells you how many lines of text you approximately have. In the worst case it can give you one extra line, but never less than what you really have.

To get the required height for the item, just multiply approxLineCount to the text height returned by the font metrics.

Regards

jiveaxe
4th August 2007, 18:51
Ok marcel; I try.

Bye

jiveaxe
5th August 2007, 09:15
I have done some progresses, but have a problem retrieving the "item rect" (rW of marcel's last post); I tryed to get it with option.rect.width() but its value is zero and the application fault computing f. Here the new sizeHint:


QSize TextPainter::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QFontMetrics fontMetrics = option.fontMetrics;
QRect rect = fontMetrics.boundingRect(index.data().toString());
float f = rect.width() / option.rect.width();
int approxLineCount = int(f) + 1;
return QSize(option.rect.width(), approxLineCount * fontMetrics.height());
}

Regards

marcel
5th August 2007, 12:39
You get a 0 width several times before the widget is actually displayed on the screen and the layout(if any) is set up.

So you should test this and do nothing if you get 0.
After the widget is properly initialized by the layout, any subsequent paint events will call your delegate's paint event and pass the correct item rect.

Regards

jiveaxe
5th August 2007, 13:51
But the program crashes with "Floating point exception" during execution. While if I substitute option.rect.width() with a constant value it works but row's height is obviously fixed.

Bye

marcel
5th August 2007, 14:08
I meant to test when the rect.width() is 0, and do nothing.
Something like:


QSize TextPainter::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if(option.rect.width() == 0 || option.rect.height() == 0)
return QSize(0, 0);
QFontMetrics fontMetrics = option.fontMetrics;
QRect rect = fontMetrics.boundingRect(index.data().toString());
float f = rect.width() / option.rect.width();
int approxLineCount = int(f) + 1;
return QSize(option.rect.width(), approxLineCount * fontMetrics.height());
}


As I told you, the width should be 0 only at the beginning of the application,. before the widgets are actually laid out.

Regards

jiveaxe
5th August 2007, 14:38
I applied your code: the application runs but the widget is empty! So


if(option.rect.width() == 0 || option.rect.height() == 0)

is always true?

Bye

P.S. i have inserted code in and after the if statement for output text in console and the text after the if is never shown, while the one in the condition always.

jiveaxe
5th August 2007, 15:04
I have noticed that even index.data().toString() returns an empty string. What's wrong?

marcel
5th August 2007, 16:54
See the attached archive. I have created a small example.
Note that the list view resize mode is set to QListView::Adjust.
This means that the items are laid out at every resize.
I noticed this causes some flicker. I haven't been able to solve it, maybe you can.
However, this flag is needed to update the delegates size and force an item repaint at the real, current size.
F.e. when you have an item spreading on 3 lines of text, and you resize the view such that the item will fit in one line of text, then you need a relayout of the items in order to shrink the painted area.

Also, the wrapping will work at work breaks. If you have a very large word, it won't be wrapped.
To do this, you have to modify the sizeHint function and insert line breaks in the text in the positions where it should be wrapped.

EDIT: as a small fix, replcae the Qt::TextWordWrap flag from the delegate's paint method with Qt::TextWrapAnywhere. This will solve the last problem I've mentioned.

Regards

jiveaxe
5th August 2007, 18:05
Thank you very much, marcel; it works just like I want, and with a better polished code; i prefer the version with the Qt::TextWordWrap flag (i plan to fix the widget width, so no flicker effect). Now I have to study all the code of this discussion and apply it to my application (which include even a qtableview; but now shouldn't be hard managing this widget for word wrapping).

Best regards

jiveaxe
29th August 2007, 16:24
Hi,
have used the code of marcel creating a qtrewidget with word-wrap function and it works nice. The next step should be display html text instead of plain text like now. I have used the following code in overridden paint():


painter->save();
QTextDocument doc;
doc.setHtml(index.data().toString());
QAbstractTextDocumentLayout::PaintContext context;
doc.setPageSize(option.rect.size());
painter->translate(option.rect.x(), option.rect.y());
doc.documentLayout()->draw(painter, context);
painter->restore();

( instead of painter->drawText() ) but with no luck: the treeWidget is illegible.

Any hint?

Some code:
delegate.cpp

void delegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex & index) const
{
painter->save();
QTextDocument doc;
doc.setHtml(index.data().toString());
QAbstractTextDocumentLayout::PaintContext context;
doc.setPageSize(option.rect.size());
painter->translate(option.rect.x(), option.rect.y());
doc.documentLayout()->draw(painter, context);
painter->restore();
// painter->drawText(option.rect, Qt::AlignLeft | Qt::TextWordWrap, index.data().toString());
}

QSize delegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QTreeWidget *p = (QTreeWidget*)parent();
QFontMetrics fm(p->font());

float rw = float(p->viewport()->size().width());
float tw = fm.width(index.data().toString());
float ratio = tw/rw;
int lines = int(ratio) + 1;
return QSize(rw,lines*fm.height());
}

Thanks

jiveaxe
1st September 2007, 13:42
Hi,
I'm planning use two different delegate to draw items so I choose QListWidget. For providing support to the two delegates I created a new class, named ListWidgetItemDelegate (like StarDelegate example in the manual). Now from the MainDialog I can decide which delegate using. All works fine. I then decided to modify one of the delegate for supporting the marcel's word-wrap solution:


QSize SectionItem::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QListWidget *p = (QListWidget*)parent();
QString text = index.data().toString();
QFontMetrics fm(p->font());
float rw = float(p->viewport()->size().width());
float tw = fm.width(text);
float ratio = tw/rw;
int lines = int(ratio) + 1;
return QSize(rw,lines*fm.height());
}

but i get an error during compiling because parent is not declared in the scope. Now from maindialog passing by ListWidgetItemDelegate to SectionItem I'm little lost and i don't know which row modify for passing the right parent.

Here the code that have used hoping someone, read marcel, can help me:

listwidgetitemdelegate.cpp

#include "listwidgetitemdelegate.h"
#include "headeritem.h"
#include "sectionitem.h"

void ListWidgetItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (qVariantCanConvert<HeaderItem>(index.data()))
{
HeaderItem headerItem = qVariantValue<HeaderItem>(index.data());
headerItem.paint(painter, option, index);
}
else if (qVariantCanConvert<SectionItem>(index.data()))
{
SectionItem sectionItem = qVariantValue<SectionItem>(index.data());
sectionItem.paint(painter, option, index);
}
else
{
QItemDelegate::paint(painter, option, index);
}
}

QSize ListWidgetItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (qVariantCanConvert<SectionItem>(index.data())) {
SectionItem sectionItem = qVariantValue<SectionItem>(index.data());
return sectionItem.sizeHint(option, index);
} else {
return QItemDelegate::sizeHint(option, index);
}
}


sectionitem.cpp

#include "sectionitem.h"

SectionItem::SectionItem(QString text)
{
itemText = text;
}

SectionItem::~SectionItem()
{
}

void SectionItem::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex & index) const
{
painter->save();
painter->setPen(QPen(Qt::darkGreen));
painter->drawText(option.rect, Qt::AlignLeft | Qt::TextWordWrap, itemText);
painter->restore();
}

QSize SectionItem::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QListWidget *p = (QListWidget*)parent();
QFontMetrics fm(p->font());

float rw = float(p->viewport()->size().width());
float tw = fm.width(itemText);
float ratio = tw/rw;
int lines = int(ratio) + 1;
return QSize(rw,lines*fm.height());
}

maindialog.cpp

...
listWidget = new QListWidget;
listWidget->setItemDelegate(new ListWidgetItemDelegate);
listWidget->setAlternatingRowColors(true);
listWidget->setResizeMode(QListWidget::Adjust);

QStringList list;
list << "New Section"
<< "The <b>QListWidgetItem</b> class provides an item for use with the QListWidget item view class."
<< "The <b>QObject</b> class is the base class of all Qt objects."
<< "Qt's <b>drag and drop</b> infrastructure is fully supported by the model/view framework. Items in lists, tables, and trees can be dragged within the views, and data can be imported and exported as MIME-encoded data. ciao da giuseppe";

QListWidgetItem *item1 = new QListWidgetItem;
item1->setData(0, qVariantFromValue(HeaderItem(list.at(0))));
listWidget->insertItem(0, item1);

for(int i = 1; i < list.size(); i++)
{
QListWidgetItem *item = new QListWidgetItem;
item->setData(0, qVariantFromValue(SectionItem(list.at(i))));
listWidget->insertItem(i, item);
}
...

Regards

marcel
1st September 2007, 13:54
In ListWidgetItemDelegate::sizeHint you can pass parent() to SectionItem::sizeHint(). All you have to do is to add another parameter to SectionItem's sizeHint method.
SectionItem is not a QAbstractItemDelegate, isn't it?

Regards

jiveaxe
1st September 2007, 13:59
No marcel, SectionItem is not a QAbstractItemDelegate. Now I edit the code and then tell you the result.

Thanks

marcel
1st September 2007, 14:04
Yes. Just make sure you pass the parent of ListWidgetItemDelegate.

Regards

jiveaxe
1st September 2007, 14:39
I'm making some sort of mistake because i got errors during compiling; here the modifications:

listwidgetitemdelegate.cpp

...
return sectionItem.sizeHint(parent, option, index);
...

sectionitem.h

...
QSize sizeHint(QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const;
...

sectionitem.cpp

QSize SectionItem::sizeHint(QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
{...}

For clearness here the code for the header files:
listwidgetitemdelegate.h

class ListWidgetItemDelegate : public QItemDelegate
{
Q_OBJECT

public:
ListWidgetItemDelegate(QWidget *parent = 0) : QItemDelegate(parent) {}

void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;

QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
};

sectionitem.h

class SectionItem
{
public:

SectionItem(QString text = "");
~SectionItem();

void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const;
QSize sizeHint(QObject *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const;

private:
QString itemText;
};

Q_DECLARE_METATYPE(SectionItem)

Bye

marcel
1st September 2007, 14:44
Who is parent? The one that you pass to SectionItem::sizeHint().
It should be (QListWidget*)parent().

And in maindialog.cpp, it should be:


listWidget->setItemDelegate(new ListWidgetItemDelegate(listWidget));

jiveaxe
1st September 2007, 14:59
Ok marcel, now works! Here the modifications I made:

maindialog.cpp

listWidget->setItemDelegate(new ListWidgetItemDelegate(listWidget));

listwidgetitemdelegate.cpp

...
return sectionItem.sizeHint(parent(), option, index);
...

sectionitem.h

QSize sizeHint(QObject *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const;

sectionitem.cpp

QSize SectionItem::sizeHint(QObject *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QListWidget *p = (QListWidget*)parent;
...
}

Thanks a lot

jiveaxe
1st September 2007, 15:14
I would like rendering html text instead of plain text; changing SectionItem::paint like this:


void SectionItem::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex & index) const
{
painter->save();
QTextDocument doc;
doc.setHtml(itemText);
QAbstractTextDocumentLayout::PaintContext context;
doc.setPageSize(option.rect.size());
painter->translate(option.rect.x(), option.rect.y());
doc.documentLayout()->draw(painter, context);
painter->restore();
// painter->drawText(option.rect, Qt::AlignLeft | Qt::TextWordWrap, index.data().toString());
}

It renders html text but the last row of each phrase (when wrapped) is always drawn in the following item and a blank row remains in own item (see the attached image for better explanation).

Any hint?

Cheers

jiveaxe
1st September 2007, 18:16
I have found a possible solution even if for some widths it renders not optimally. Attached are 2 images, one with correct draw and one with the unwanted behavior.

Here the new code for SectionItem::paint


void SectionItem::paint(QObject *parent, QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex & index) const
{
QListWidget *p = (QListWidget*)parent;

painter->save();
QTextDocument doc;
doc.setHtml(itemText);
QAbstractTextDocumentLayout::PaintContext context;
doc.setPageSize(QSize(option.rect.size().width(), option.rect.size().height() + p->viewport()->size().height()));
painter->translate(option.rect.x(), option.rect.y());
doc.documentLayout()->draw(painter, context);
painter->restore();
}

Regards

marcel
1st September 2007, 18:42
It might have to do with the HTML itself. Do you have any <p> tags or any other tag that enforces spacings around it?

Can you paste the html?

BTW: don't tell me that you too are working on (yet another) IDE.

Regards

jiveaxe
1st September 2007, 18:47
There aren't <p> tags in the html, only <b> and </b>, but here the code:


QStringList list;
list << "New Section"
<< "The <b>QListWidgetItem</b> class provides an item for use with the QListWidget item view class. The <b>QListWidgetItem</b> class provides an item for use with the QListWidget item view class. The <b>QListWidgetItem</b> class provides an item for use with the QListWidget item view class."
<< "The <b>QObject</b> class is the base class of all Qt objects."
<< "Qt's <b>drag and drop</b> infrastructure is fully supported by the model/view framework. Items in lists, tables, and trees can be dragged within the views, and data can be imported and exported as MIME-encoded data.";

...and not, I am not working on a new ide.

Bye

marcel
1st September 2007, 20:02
I can't say what's wrong.
Maybe if you can post a minimal example or event the whole code, so I can be able to test it.

Regards

jiveaxe
1st September 2007, 21:13
Attached there is the code.

Thanks for your time

marcel
1st September 2007, 21:55
void SectionItem::paint(QObject *parent, QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex & index) const
{
QListWidget *p = (QListWidget*)parent;

painter->save();
QTextDocument doc;
doc.setHtml(itemText);
doc.setTextWidth(option.rect.size().width());
QAbstractTextDocumentLayout::PaintContext context;
painter->translate(option.rect.x(), option.rect.y());
doc.documentLayout()->draw(painter, context);
painter->restore();
}


QSize SectionItem::sizeHint(QObject *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QListWidget *p = (QListWidget*)parent;
QFontMetrics fm(p->font());

float rw = float(p->viewport()->size().width());
float tw = fm.width(itemText);
float ratio = tw/rw;
int lines = 0;
if(ratio - int(ratio) < 0.1f)
lines = int(ratio);
else
lines = int(ratio) + 1;
return QSize(rw,lines*fm.height());
}


Problem 1: don't give the height to the text document. you already set it with the size hint.

Problem 2: ocassionaly, due to rounding errors, you got 1 extra line of text that remained empty( the +1 in the sizeHint).

Regards

jiveaxe
1st September 2007, 22:06
Thank for your tips, marcel, helpful as usual; I have corrected the code.

Regards