PDA

View Full Version : Drawing a widget in QItemDelegate's paint method



darkadept
20th August 2007, 16:02
I know this question was asked before in this (http://www.qtcentre.org/forum/f-qt-programming-2/t-how-to-show-progess-in-a-qtreeview-item--2330.html) thread but I want to add to it.

I want to render a custom widget in a QListView. The above thread shows how to do this using QStyle to paint a progress bar manually. That is a great help! But what if i want a more complex widget (with a layout, buttons, text, and an icon) instead of just a single progress bar?

The ways of doing this that I can think of are:

design the widget in Designer, load my data into it and render it to a pixmap and then paint the pixmap during the QItemDelegate's paint method. The problem here is that the widget would not be interactive (can't click the buttons etc).
create a widget for each item in the list. For long lists this would mean too many widgets. How would you draw a widget during QItemDelegate's paint method anyways?
use QStyle's drawControl method. But how do you make it adhere to a layout? Do I need to have pixel absolute offsets for each control, this would suck because then resizing the list doesn't help. Also how to I communicate the size to the sizeHint method? This seems to be the right way to go except for the problems I mentioned.


Am I on the right track here or has anyone done this before?

wysota
20th August 2007, 16:37
What do you want to use this complex widget for? Maybe it'll be easier not to use QListView but a QScrollArea and place real widgets there? Do you have a model that you want to handle in the list view?

darkadept
20th August 2007, 17:32
I have a list of plugins that I'm storing and displaying like this:
note: (don't fixate on the fact that they're plugins because I want to use this same technique for all sorts of types of data)


QList<Plugin*> myPlugins;
populateWithSampleData(myPlugins);

foreach(Plugin *plugin, myPlugins) {
QListWidgetItem *item = new QListWidgetItem;
item->setData(0, qVariantFromValue(Plugin(*plugin)));
listWidget->addItem(item);
}

PluginDelegate *pluginDelegate = new PluginDelegate;
listWidget->setItemDelegate(pluginDelegate);


note: (eventually I'll use a custom model and a QListView instead of QListWidget)

I want to provide a widget that looks a bit like the firefox plugin list or like ktorrent's plugin list but add a qpushbutton on it as well. I've included a screenshot of ktorrent's plugin list. Instead of having the Load buttons on the right hand side I would like to put the buttons directly on the list in each list item.
note: (i don't necessarily want the buttons there in my final program but want to know if it's even possible to do in Qt.)

In the code I have written already my delegate is subclassing QItemDelegate and looks like this:


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

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

//I was testing the drawing of controls with QStyle
/*
QStyleOptionProgressBar opt;
opt.rect = option.rect;
opt.minimum = 0;
opt.maximum = 100;
opt.progress = 60;
QApplication::style()->drawControl(QStyle::CE_ProgressBar, &opt, painter, 0);
*/

} else {
QItemDelegate::paint(painter, option, index);
}
}

QSize PluginDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const
{
//This just calls the base class method for now...
return QItemDelegate::sizeHint(option, index);

}


To me it makes sense to use the delegate system as I would be able to reuse it in different views.

Maybe I need to hack together a control that takes parts from QScrollArea and QListView but that seems a little too extreme.

Surely I can't be the only one that needs this functionality and I'm a little surprised that Qt can't do this as gracefully as I first thought. Of course, I'm sure all of us Qt programmers could bemoan that thought multiple times during a day! :D

wysota
20th August 2007, 18:24
I don't see any widgets in the screenshot you provided. All you need is to draw the icon and the text in proper places using paint() method of the delegate and maybe abusing QTextDocument to render rich text (if you need it).

Qt can do it very gracefully, you just have to implement the painting :)

wysota
20th August 2007, 18:43
Here you go - a very simple implementation of what you seek.

BTW. You should not allow casting data from your model to "Plugin". Use QModelIndex::internalPointer() instead. And handle default roles like DisplayRole and DecorationRole.

darkadept
21st August 2007, 04:53
I've definitely taken what you've posted and ran with it. I realize now that you can't just have a real QPushButton sitting on a ListView and make it work but you can draw a "fake" one just fine. You can, of course, have an interactive widget as an editor.

I also found out that you can easily draw rich text using a QTextDocument and it's drawContents() method. But I'm not sure how efficient it is to create one more QTextDocument's inside the paint() and sizeHint() methods. Is QTextDocument too "heavy" to use inside these methods? The sizeHint() method seems to fire quite a lot. I took a peek into what Qt does inside these methods of the QItemDelegate class and saw a lot of code that makes me think I should be OK.

I've been working on the sizeHint method and found that you can not determine the current width of the list (or cell, etc). In otherwords you can't dynamically resize your text horizontally dependent on the width of the window. Does anyone happen to know if you actually can get the listView's width?

I've also modified my list to use Qt's role system. I tried reading up on QModelIndex::internalPointer() but I'm not quite sure how that works. Should I be attaching each Plugin object to each QModelIndex? This is what I currently have:


class Plugin {
public:
Plugin(QString name, QString desc, QString author, const QPixmap &icon) {
//snipped constructor code ...
};
~Plugin() {};

QString pluginName;
QString pluginDescription;
QString pluginAuthor;
QPixmap pluginIcon;
};

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

QListWidget *listWidget = new QListWidget;

QList<Plugin *> pluginList;
pluginList << new Plugin("plugin1", "plugin1 description", "mr ed", QPixmap("icon.png"));
pluginList << new Plugin("plugin2", "plugin2 description", "mrs ed", QPixmap("icon2.png"));

foreach(Plugin *plugin, pluginList) {
QListWidgetItem *item = new QListWidgetItem;
item->setData(Qt::DisplayRole, plugin->pluginName);
item->setData(Qt::DecorationRole, plugin->pluginIcon);
item->setData(Qt::UserRole + 1, plugin->pluginDescription);
item->setData(Qt::UserRole + 2, plugin->pluginAuthor);
listWidget->addItem(item);
}

PluginDelegate *pluginDelegate = new PluginDelegate;
listWidget->setItemDelegate(pluginDelegate);

listWidget->show();

return app.exec();
}


Then when I'm in my paint() and sizeHint() methods I can access index.data(Qt::UserRole + 1) to get at my custom data, right? Is this what they intended the role system for or should I be passing my data via a different method?

Also, one more quick stupid question, in the example above will I need to manually delete my Plugin *objects or does the listWidget take care of the itself? My Plugin object is not a QObject at the moment.

wysota
21st August 2007, 08:22
I've definitely taken what you've posted and ran with it. I realize now that you can't just have a real QPushButton sitting on a ListView and make it work but you can draw a "fake" one just fine.
You can have a real widget, but it's not worth it - it's too heavy right now (maybe when 4.4 is released this situation will change).


I also found out that you can easily draw rich text using a QTextDocument and it's drawContents() method. But I'm not sure how efficient it is to create one more QTextDocument's inside the paint() and sizeHint() methods. Is QTextDocument too "heavy" to use inside these methods?
It's a bit heavy, but Trolltech uses that approach in Qt in some places, so unless you plan to have thousand plugins, you can use that approach. But first think if you need rich text here at all. I don't think you do.


I've been working on the sizeHint method and found that you can not determine the current width of the list (or cell, etc). In otherwords you can't dynamically resize your text horizontally dependent on the width of the window.
Does anyone happen to know if you actually can get the listView's width?
Take a look at QStyleOptionViewItem::rect that is passed to the sizeHint() method. It contains the rectangle of the view occupied by the item.


I've also modified my list to use Qt's role system. I tried reading up on QModelIndex::internalPointer() but I'm not quite sure how that works. Should I be attaching each Plugin object to each QModelIndex? This is what I currently have
Looks fine.

About the internal pointer - when you implement your own model, you often have an internal data structure there (like a list) where you store pointers to objects representing items of the model. By using internalPointer() you can return such a pointer to the outside world. A similar approach is to use internalId() - then you return an id (like an index of a list).


Then when I'm in my paint() and sizeHint() methods I can access index.data(Qt::UserRole + 1) to get at my custom data, right?
Correct.

Is this what they intended the role system for or should I be passing my data via a different method?
No, that's exactly how you should access your data.


Also, one more quick stupid question, in the example above will I need to manually delete my Plugin *objects or does the listWidget take care of the itself? My Plugin object is not a QObject at the moment.
You have to take care of it in the model destructor (when you decide to use the model approach) or the widget destructor (when you decide to stick with convinience classes).

BTW. I suggest to use Qt::ToolTipRole for the description - you'll get a tooltip that shows the description for free. Also remember that not the whole description might be visible at all times (for example when the item is not high or wide enough to display the whole text) - then you'll have to elide the text.

darkadept
21st August 2007, 16:45
Ok i've rewritten my delegate to use standard painter->drawText() instead of QTextDocument. It's good to know i can use both though.


Take a look at QStyleOptionViewItem::rect that is passed to the sizeHint() method. It contains the rectangle of the view occupied by the item.

That was my first thought too but when I qDebug inside the sizeHint() method i get "QRect(0,0 0x0)". I've tried various settings on QListWidget but it never changes. That same parameter works fine in the paint() method.
Hmm, i just noticed i was using Qt 4.2.2 so i'm going to upgrade to Qt 4.3.1 and see if anything changes.

I'm still not entirely sure how to use QModelIndex::internalPointer(). But once I get my paint() and sizeHint() methods working I'll switch to QListView and a custom model and worry about it then. :p


I've got to say that QT Centre kicks butt and major kudos to you wysota! You've helped me out so incredibly much! thanks!

darkadept
21st August 2007, 20:02
Alright, I guess what I was going for originally would look something like the screencap in this post:

http://www.ereslibre.es/?p=61

I know that's with KDE4.x but I was wondering if there was an easy way to implement it.

Also, I have Qt 4.3.1 installed now and I'm still getting zero's in my sizeHint() option.rect parameter.

darkadept
21st August 2007, 22:33
I'm beginning to think that there is a bug with the QStyleOptionViewItem parameter in QAbstractItemDelegate's sizeHint() method.

How can I calculate the sizeHint if I don't know in what rect I'm working in? This makes drawing wrapped text impossible. I want to dynamically adjust the height depending on how much text can fit on screen.

Ahh well.

marcel
21st August 2007, 22:55
It is possible to implement dynamic word-wrap. I have done it for a QListView.

See this post: http://www.qtcentre.org/forum/f-qt-programming-2/t-word-wrap-in-qlistview-8371.html

Regards

darkadept
22nd August 2007, 18:05
Your code works wonderfully! Thanks.

But isn't assuming that the parent of the delegate is a QListView a bad thing? So I modified your sizeHint() method to use QAbstractItemView instead of QListView.



QAbstractItemView *p = (QAbstractItemView*)parent();
QString text = index.data(Qt::DisplayRole).toString();
QFontMetrics fm(option.fontMetrics);

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 yes, the gaping flaw is that by using viewport()->size() this delegate will only work when the item consumes the entire width of the view. In a QAbstractTableView we will have problems. I guess I'll tackle one thing at a time.

Thanks a ton for the help!

marcel
22nd August 2007, 18:11
But isn't assuming that the parent of the delegate is a QListView a bad thing?

I made it for that particular case.
I didn't think someone else might use it.

Regards

marcel
22nd August 2007, 18:16
BTW:


I'm beginning to think that there is a bug with the QStyleOptionViewItem parameter in QAbstractItemDelegate's sizeHint() method.

How can I calculate the sizeHint if I don't know in what rect I'm working in? This makes drawing wrapped text impossible. I want to dynamically adjust the height depending on how much text can fit on screen.


That QRect is actually based on the sizeHint you return.
The framework first calls sizeHint for the delegate and then paints it.

Regards

darkadept
22nd August 2007, 20:14
That QRect is actually based on the sizeHint you return.
The framework first calls sizeHint for the delegate and then paints it.


Following that frame of thought, then making wrapping text work in a QTreeView and QTableView is different then in the QListView.
I haven't tested this yet but I think you would do something like this:



QSize PluginDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const
{
int width;

if (qobject_cast<QColumnView *>(parent()) != 0) {
QColumnView *v = qobject_cast<QColumnView *>(parent());
width = v->columnWidths().at(index.column());
} else if (qobject_cast<QHeaderView *>(parent()) != 0) {
//i'm going to leave this out for now because I don't know how to get the width of a QHeaderView cell
width = 100; //dumb value
} else if (qobject_cast<QTableView *>(parent()) != 0) {
QTableView *v = qobject_cast<QTableView *>(parent());
width = v->columnWidth(index.column());
} else if (qobject_cast<QTreeView *>(parent()) !=0) {
QTreeView *v = qobject_cast<QTreeView *>(parent());
width = v->columnWidth(index.column());
} else if (qobject_cast<QListView *>(parent()) != 0) {
QListView *v = qobject_cast<QListView*>(parent());
width = p->viewport()->size().width();
} else {
//specify default value if we are using a non-standard view
width = 400;
}

// ... calculate text wrapping and cell/row height based on width.

// ... future feature: specify max # of text lines and then elide the text if it's too long. (see QFontMetrics::elidedText() method)

}


I'm sure there is a way to optimize that whole thing too, of course.

Now if you know for sure that your delegate will only ever be used in a QListView then don't do the above.

andytork
10th January 2009, 11:46
Alright, I guess what I was going for originally would look something like the screencap in this post:

http://www.ereslibre.es/?p=61

I know that's with KDE4.x but I was wondering if there was an easy way to implement it.

Also, I have Qt 4.3.1 installed now and I'm still getting zero's in my sizeHint() option.rect parameter.


Did anyone ever come up with similar list to the above. I am trying to produce something similar and finding it very hard

Thanks
Andy

shrike.pt
5th August 2009, 12:52
I'm reviving this thread, since I need a solution to exactly this kind of problem.
I need to create a list of items with a lot of functionality and a "Look" which is quite different to a standard ListView.
Each item has multiple images, text labels with multiple formats, and multiple "action" areas, with mouse-over "hot" re-draw funtionality.
Also, anyone knows for sure what the lifetime of each item in a QListView is ? The list of items can be quite large, so I'm hopping it only keps in memory the items currently in view and requests re-draw/re-create of the item when it comes into view.
To get an ideia of what i need, go have a look at any decently powerful, fully-functional tweeter client.

shrike.pt
11th August 2009, 05:15
Ok, I think I've got it.
In case anyone else needs to do this:
http://www.qtforum.org/article/25345/listview-within-a-listviewitem.html

Basically create a delegate inheriting QAbstractItemDelegate, with a reference to a widget which "looks" and "acts" like you want your nodes to do.
Override editorEvent in your delegate and pass the event to your widget, remembering to set it's data first and to adjust mouse position for the widget.

Then just use the sizeHint event to set data ( it will be called first every time your itemview needs something from your node) and render the widget to the painter in the paint event.

Presto, virtual itemview having only one "real" widget no matter how many nodes. Tested with 50k items, works beautifully.