PDA

View Full Version : Using QTreeWidget and setItemWidget performance issue?



neosettler
7th December 2014, 21:11
Hello QT Jedi masters,

I’ve been biting the bullet on making a custom QTreeWidget and QTreeWidgetItem to access and modify 3D actors parameters values. It looks like this:
10786

The concept works pretty well overall except that setItemWidget takes an awful lot of time to process. To put things in perspective, to populate a QTreeWidget with about 50 QTreeWidgetItems, it takes over 5 seconds to generate while setData takes only a fraction of a second. My project is fairly complex so it would be quite difficult to extract a code snippet but I was wondering if anyone could bring some light on this issue?

Digging further, most information I can find is about using delegates as an alternative. Unfortunately, I cannot find any source that reproduces something similar to my current way of doing it. The example provided by QT on delegates overrides paintEvent and I can’t grasp why I would have to go down that road since I’m using generic widgets.

Any help and/or ideas on this would be greatly appreciated,
neo

wysota
7th December 2014, 22:31
The example provided by QT on delegates overrides paintEvent and I can’t grasp why I would have to go down that road since I’m using generic widgets.
Because you want to have the look of widgets without having widgets.

neosettler
8th December 2014, 05:00
Hello Master, thank you for the input.

I agree that it would make sense for several thousand entries but why would one want to use proxies and not the real widgets for a few dozen rows? I’d like every widgets to have instant focus on mouse over for instance. Since we have more than one widget per row, I don’t see how delegates could be a good approach.

What’s about using setItemWidget that makes it not recommended and so process hungry to populating a QTreeWidget? Is there any way to have setItemWidget behave faster?

wysota
8th December 2014, 08:49
I agree that it would make sense for several thousand entries but why would one want to use proxies and not the real widgets for a few dozen rows? I’d like every widgets to have instant focus on mouse over for instance. Since we have more than one widget per row, I don’t see how delegates could be a good approach.
The model-view architecture in Qt was not designed to have widgets for each cell in the view.


What’s about using setItemWidget that makes it not recommended and so process hungry to populating a QTreeWidget? Is there any way to have setItemWidget behave faster?
There are two downsides of using setItemWidget. First of all it is not tied to your model in any way thus it seems questionable whether this fits into model-view approach. Second of all it requires a lot of work with positioning the widgets. It may be that the view configuration you have (e.g. resizing columns to contents) triggers some slow path in the code.

neosettler
8th December 2014, 17:14
Hello again Master,


First of all it is not tied to your model in any way thus it seems questionable whether this fits into model-view approach.

Ok, fair enough. Taking into account that we need the widgets themselves and not proxies, what alternative do we have?


Second of all it requires a lot of work with positioning the widgets. It may be that the view configuration you have (e.g. resizing columns to contents) triggers some slow path in the code.

While this is possible, I really doubt that positioning is the fault here... and resizing or not to content doesn't change much. I'd be more incline to believe there is something fishy under the hood and I'm eager to find out what it is.

wysota
8th December 2014, 17:34
Why do you say you need real widgets? Based on the image you posted it seems to me you can achieve all that without widgets without any big effort.

neosettler
8th December 2014, 21:24
Thanks again for helping me and probably others with this issue.


Because you want to have the look of widgets without having widgets.

When using delegates, based on the previous quote and the spinboxdelegate example, we must activate (ei: double clicked) the tree item delegate to be able to edit the widgets tied to it inside a QTreeWidget. Point in case, the widgets doesn't really exist until the delegate is activated. I'd like to skip the part entirely and show the “real” widgets at all time.

Note: The image provided here is already a working implementation (not Photoshoped) and it does the job very well using setItemWidget. It’s just ridiculously long to populate the QTreeWidget.


Based on the image you posted it seems to me you can achieve all that without widgets without any big effort.

hum... Are you having delegates in mind or some other recipe from the ancient? If that is the case, I'm clearly missing something and I surrender to your teaching, Master!

wysota
9th December 2014, 08:37
When using delegates, based on the previous quote and the spinboxdelegate example, we must activate (ei: double clicked) the tree item delegate to be able to edit the widgets tied to it inside a QTreeWidget. Point in case, the widgets doesn't really exist until the delegate is activated. I'd like to skip the part entirely and show the “real” widgets at all time.
You missed my point. I'm saying you can do the same entirely without widgets.


Note: The image provided here is already a working implementation (not Photoshoped) and it does the job very well using setItemWidget. It’s just ridiculously long to populate the QTreeWidget.
Then maybe you should try optimizing elsewhere. Did you profile your app?


hum... Are you having delegates in mind or some other recipe from the ancient? If that is the case, I'm clearly missing something and I surrender to your teaching, Master!

The delegate acts like a widget, it draws things and handles events for items. The difference is that you don't have an API that let's you easily compose the look of an item from simpler pieces. However your case looks simple enough that it shouldn't be a problem.

neosettler
11th December 2014, 03:18
You missed my point. I'm saying you can do the same entirely without widgets.

I'm sorry, without widgets? I'm severely confused, could you elaborate on this?

I did not profile the app but as a reminder from the OP: "To put things in perspective, to populate a QTreeWidget with about 50 QTreeWidgetItems, it takes over 5 seconds to generate while setData takes only a fraction of a second." This means that by commenting/uncommenting those two lines, using setData over setItemWidget is few hundred time faster.

item->setData(1, DisplayRole, QVariant::fromValue(new QParameter()));
or
QTreeWidget->setItemWidget(item, 1, new QParameter());

I read in another thread that this could be related to styles on windows but whatever styles I'm using, it's still super slow.


The difference is that you don't have an API that let's you easily compose the look of an item from simpler pieces.

Ok, I'm clearly missing something big and I'm wondering where I should start to fill the blanks. I'm still digging.

wysota
11th December 2014, 08:23
I'm sorry, without widgets? I'm severely confused, could you elaborate on this?
The delegate can draw everything that you now draw by putting a widget in the item. It can also handle events you need for the item which is now done by your widget.


I did not profile the app but as a reminder from the OP: "To put things in perspective, to populate a QTreeWidget with about 50 QTreeWidgetItems, it takes over 5 seconds to generate while setData takes only a fraction of a second." This means that by commenting/uncommenting those two lines, using setData over setItemWidget is few hundred time faster.
You don't know how much overhead is added by the widget itself.

alketi
11th December 2014, 17:32
wysota, I think the OP was saying that the issue with using a Delegate is that it only appears when the cell is in edit mode (double-clicked for example). And he wants the item to always appear.

Is that not the Delegate behavior? How do you make a Delegate appear when the tree/table cell is not being edited?

wysota
11th December 2014, 18:28
wysota, I think the OP was saying that the issue with using a Delegate is that it only appears when the cell is in edit mode (double-clicked for example). And he wants the item to always appear.

Is that not the Delegate behavior?

No.


How do you make a Delegate appear when the tree/table cell is not being edited?
The delegate does not "appear". It is an object which renders an item. It has no visual aspect by itself.

wysota
14th December 2014, 18:13
Ok, I found some time to implement an example which replicates (more or less) the OP image without using widgets. The code contains minor calculation errors but these are easy to fix. I added a number of comments to clarify what is going on in the code. It should be easy to extend the example to test larger amounts of properties and various other types of properties. The code should probably be refactored so that different property types are handled by different physical entities similar to how widgets behave.


#include <QApplication>
#include <QAbstractTableModel>
#include <QTreeView>
#include <QPainter>
#include <QMouseEvent>
#include <QtDebug>

// simple flat model
class Model : public QAbstractTableModel {
Q_OBJECT
public:
enum Roles {
ConnectedRole = Qt::UserRole+1
};

Model(QObject *parent = 0) : QAbstractTableModel(parent) {}

int rowCount(const QModelIndex &parent = QModelIndex()) const {
if(parent.isValid()) return 0;
return m_keys.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const {
return 2;
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const {
if(!index.isValid()) return QVariant();
if(index.column() == 0) {
if(role == Qt::DisplayRole || role == Qt::EditRole) return m_keys.at(index.row());
}
if(index.column() == 1) {
switch(role) {
case Qt::DisplayRole:
case Qt::EditRole: return m_values.at(index.row());
case ConnectedRole: return m_connected.at(index.row());
default:
return QVariant();
}
}
return QVariant();
}
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) {
if(index.column() != 1) return false;
if(role == ConnectedRole) {
if(m_connected[index.row()] == value.toBool()) return false;
m_connected[index.row()] = value.toBool();
emit dataChanged(index, index);
return true;
}
if(role == Qt::EditRole) {
m_values[index.row()] = value;
emit dataChanged(index, index);
return true;
}
return false;
}

void appendRow(const QString &key, const QVariant &value, bool conn = false) {
beginInsertRows(QModelIndex(), m_keys.size(), m_keys.size());
m_keys << key;
m_values << value;
m_connected << conn;
endInsertRows();
}

private:
QStringList m_keys; // store keys
QVariantList m_values; // store values
QList<bool> m_connected; // store info about which item is "connected" (whatever that would mean)
};

class Delegate : public QAbstractItemDelegate {
public:
Delegate(QObject *parent = 0) : QAbstractItemDelegate(parent) {}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
paintConnected(painter, option, index); // every item has a "connected" value, let's paint it

switch(type(index)) {
case QVariant::Bool: drawBoolValue(painter, option, index); break;
case QVariant::Double: drawDoubleValue(painter, option, index); break;
default: break;
}

}
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
return QSize(0,20);
}
bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)
{
// bools are handled in MouseRelease, dragging has to be handled in MouseMove
if(event->type() == QEvent::MouseButtonRelease) {
QMouseEvent *me = static_cast<QMouseEvent*>(event);
if(checkAndHandleConnected(me, model, option, index)) return true;
switch(type(index)) {
case QVariant::Bool: checkAndHandleBoolValue(me, model, option, index); break;
default: break;
}
return true;
}
if(event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseMove) {
QMouseEvent *me = static_cast<QMouseEvent*>(event);
switch(type(index)) {
case QVariant::Double: if(checkAndHandleDoubleValue(me, model, option, index)) return true; break;
default: break;
}
return false;
}
return QAbstractItemDelegate::editorEvent(event, model, option, index);
}
protected:
void paintConnected(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
QRect connectedRect = QRect(option.rect.topLeft(), QSize(21, option.rect.height()));
painter->fillRect(connectedRect, Qt::gray);
// if connected, draw a white circle
if(index.data(Model::ConnectedRole).toBool()) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
painter->setPen(Qt::NoPen);
painter->setBrush(Qt::white);
painter->drawEllipse(connectedRect.center(), 3, 3);
painter->restore();
}
}
bool checkAndHandleConnected(QMouseEvent *me, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) {
QRect connectedRect = QRect(option.rect.topLeft(), QSize(21, option.rect.height()));
if(connectedRect.contains(me->pos())) {
model->setData(index, !index.data(Model::ConnectedRole).toBool(), Model::ConnectedRole);
return true;
}
return false;
}
void drawBoolValue(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
QRect boolRect = QRect(option.rect.topLeft(), QSize(21, option.rect.height())).translated(21, 0);
painter->fillRect(boolRect, Qt::darkGray);
if(index.data(Qt::EditRole).toBool()) {
QRect crossRect = QRect(boolRect.center()-QPoint(4,4), boolRect.center()+QPoint(5,5));
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
QPen p;
p.setWidth(2);
p.setColor(Qt::white);
painter->setPen(p);
painter->drawLine(crossRect.topLeft(), crossRect.bottomRight());
painter->drawLine(crossRect.topRight(), crossRect.bottomLeft());
painter->restore();
}
}
bool checkAndHandleBoolValue(QMouseEvent *me, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) {
QRect boolRect = QRect(option.rect.topLeft(), QSize(21, option.rect.height())).translated(21, 0);
if(boolRect.contains(me->pos())) {
model->setData(index, !index.data(Qt::EditRole).toBool(), Qt::EditRole);
return true;
}
return false;
}

void drawDoubleValue(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
// paint the background first
QRect doubleRect = option.rect.adjusted(21, 0,0,0);
painter->fillRect(doubleRect, Qt::darkGray);
// consists of a "slider" and the value itself

// let the slider be of constant width of 50 (for simplicity)
QRect sliderRect = doubleRect;
sliderRect.setWidth(50);
sliderRect.adjust(0,0,0,-1);
painter->save();
QPen p;
p.setWidth(1);
p.setColor(Qt::black);
painter->setPen(p);
painter->setBrush(Qt::gray);
painter->drawRect(sliderRect);
// I don't know the logic, so I'll assume the value to be between 0 and 1
double val = index.data(Qt::EditRole).toDouble();
QRect valueRect = sliderRect.adjusted(1, 1, -1, -1);
// 2/48 = val/widthToPaint => widthToPaint = val * 48 / 2
double widthToPaint = val * 24.0;
valueRect.setWidth(widthToPaint);
painter->fillRect(valueRect, Qt::lightGray);
painter->restore();
// paint the value text with precision 6, right adjusted
painter->setPen(Qt::white);
QRect textRect = doubleRect;
textRect.setLeft(sliderRect.right()+1);
// leave some margin
textRect.adjust(1, 1, -1, -1);
painter->drawText(textRect, Qt::AlignRight|Qt::AlignVCenter, QString::number(val, 'f', 6));
}
bool checkAndHandleDoubleValue(QMouseEvent *me, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) {
QRect doubleRect = option.rect.adjusted(21, 0,0,0);
QRect sliderRect = doubleRect;
sliderRect.setWidth(50);
sliderRect.adjust(1,1,1,-2);
if(!sliderRect.contains(me->pos())) return false;
// we've hit the slider
int width = me->pos().x()-sliderRect.left();
// 2/48 = val/width => val = 2*width/48
double val = 2*width/48.0;
model->setData(index, val, Qt::EditRole);
return true;
}


Model *model(const QModelIndex &index) const {
return const_cast<Model*>(qobject_cast<const Model*>(index.model()));
}
QVariant::Type type(const QModelIndex &index) const {
return index.data(Qt::DisplayRole).type();
}
};

#include "main.moc"

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QTreeView w;
Model m;
m.appendRow("x", 1.0);
m.appendRow("y", 1.0);
m.appendRow("z", 1.0);
m.appendRow("Selectability", true, true);

w.setModel(&m);
w.setItemDelegateForColumn(1, new Delegate(&w));
w.show();

return a.exec();
}

Obligatory screenshot:
10805

neosettler
22nd December 2014, 04:29
Hi wysota, my apologies for the late reply and thanks a million for your example!

My brain is still making cycle redundancy checks on repaint the widgets instead of using the widget themselves but I'll be looking into your proposal closely!