After hours of work, I'm able to paint a widget on QListView. However, the painting is done through a QPixmap. The widget appears, and I can see a progress bar. However, it's a little "pixelated" (due to using QPixmap). Is it possible to paint directly as a normal widget? That's my question.
The following is what I do:
void FileQueueItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QPaintDevice* original_pdev_ptr = painter->device();
FileQueueListItem* itemWidget = reinterpret_cast<FileQueueListItem*>(index.data(Qt::UserRole).value<void*>());
itemWidget->setGeometry(option.rect);
painter->end();
QPixmap pixmap(itemWidget->size());
if (option.state & QStyle::State_Selected)
pixmap.fill(option.palette.highlight().color());
else
pixmap.fill(option.palette.background().color());
itemWidget->render(&pixmap,QPoint(),QRegion(),QWidget::RenderFlag::DrawChildren);
painter->begin(original_pdev_ptr);
painter->drawPixmap(option.rect, pixmap);
}
I learned how to do what I did with the hints from here. There, the painting is done directly on QListView, which is what I'm looking to achieve. What am I doing wrong for the following attempt not to work:
void FileQueueItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
std::cout<<"Painting..."<<std::endl;
QPaintDevice* original_pdev_ptr = painter->device();
FileQueueListItem* itemWidget = reinterpret_cast<FileQueueListItem*>(index.data(Qt::UserRole).value<void*>());
itemWidget->setGeometry(option.rect);
painter->end();
if (option.state & QStyle::State_Selected)
painter->fillRect(option.rect, option.palette.highlight());
else
painter->fillRect(option.rect, option.palette.background());
itemWidget->render(painter->device(),
QPoint(option.rect.x(), option.rect.y()),
QRegion(0, 0, option.rect.width(), option.rect.height()),
QWidget::RenderFlag::DrawChildren);
painter->begin(original_pdev_ptr);
}
The list just remains empty, and nothing happens. Though the selection can be seen, but the widget doesn't show up.
Let's make a few things clear:
You're not supposed to create widgets and put them in a model. There's a very good reason for this. Widgets are involved in the Qt event loop, which means that having too many widgets will significantly slow down your program.
Widgets are not simply a bunch of controls (which seems to be how you see them). They take part in the event loop, which is why you should not have a widget that's a part of a data model.
If you're using a multithreaded program and you have our model separated from the view, memory management will become a nightmare. Qt will never tolerate trying to construct or delete any widgets from other threads (which makes sense, since detaching threads from the event loop is not generally thread-safe).
Given this information, what's the right way to do what you're trying to do? Sadly the only correct way is to draw the controls yourself. If your widget is simple, that's easy to do. If your widget is complicated, you're gonna need lots of math to calculate the positions of every widget.
In the Qt Torrent Example, you'll see how a progress bar is drawn. All you have to do to draw your controls, is calculate the position, and use the rect member variable as the containing rectangle of the controls, and then draw them (of course, after setting their values). The function paint() has an option.rect parameter in it, which is the rectangle of the whole item. All you have to do, is use some math to calculate the positions inside this rect for every widget.
PS: NEVER USE ABSOLUTE VALUES FOR THE POSITIONS. You will never get it right, especially for different DPIs.
That will draw the controls without widgets, and will guarantee the speed you need even for thousands of elements.
Related
I have a custom list, and on the view (with the QStyledItemDelegate) I want display many things, including a text edit
(think about an online shopping cart where you have the items (photos and infos of them) and next to them you can change the quantity, but within a text edit, and not a spinbox).
This text edit should be able to communicate with the model. Currently I can only draw an empty textEdit, but I don't know how to connect it properly to the editorEvent ( and createEditor, setEditorData).
void CustomDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &opt,
const QModelIndex &idx) const
{
// My other painting stuff (labels, shapes...)
QStyleOptionFrame panelFrame;
QLineEdit lineEdit;
panelFrame.initFrom(&lineEdit);
panelFrame.rect = rectLE;
panelFrame.state |= QStyle::State_Sunken;
QApplication::style()->drawPrimitive(QStyle::PE_PanelLineEdit, &panelFrame, painter);
}
QWidget *CustomDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
auto editor = new QLineEdit(parent);
editor->setText("test");
return editor;
}
void CustomDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
auto lineEdit = dynamic_cast<QLineEdit*>(editor);
if(lineEdit)
{
lineEdit->setText("test2");
}
}
As a the result I can only see an empty lineEdit and can't really interact with it.
If I would have multpiple lineEdits inside one modelIndex delegate, how could I differentiate them in the setEditorData and createEditor?
Thanks
By default, editor in QAbstractItemView is created only by edit triggers, those are set to it. Refer to docs
To make a view interactive in sense of rendered rows as regular widgets you can use a workaround.
I've used this approach with QListView and QStyledItemDelegate.
When i faced with same problem, i've tracked a viewport's mouse move event, to get a QModelIndex under the mouse cursor and if it was valid and different from previous value, i close an editor (if one was opened) and open a new one using methods QAbstractItemView::closePersistentEditor and QAbstractItemView::openPersistentEditor.
In my QStyledItemDelegate derived class i've overrided createEditor, setEditorData and updateEditorGeometry methods.
In createEditor just create a widget, in updateEditorGeometry setGeometry to editor, in setEditorData get required data to render from QModelIndex and set it to the editor widget.
The same widget class i've used to render all rows in view in paint method.
I forgot several things in my implementation:
To "interact" with the lineEdit the Qt::ItemIsEditable flag must be set in the QAbstractListModel::flags(), otherwise the Editor functions in the Delegate won't get called.
To reimplement updateEditorGeometry() where you specify the lineEdit's position.
To reimplement setModelData() where you can communicate with the model.
To draw the text in the Delegate's paint() function after drawPrimitive (drawPrimitive just draws the frame, but you want to draw the text you wrote in lineEdit as well. You can get it from the model)
In the proprietary code base I'm currently working with, there is a custom list view (derived form QListView).
Currently it has an issue when lots of items (>10000) make the main thread freeze.
Every item in the view is a custom widget designed in QtDesigner.
To render every row, we use setIndexWidget, which is called on QAbstractItemModel::rowsInserted signal. For every inserted row, from first to last custom widget is set for every index.
I tried to port this code to use QStyledItemDelegate, because disconnecting item widget from actual model seems to solve slow rendering.
Qt5 in that case can render items in view lazily, on demand. We will not need to create every widget for view before displaying the list.
I achieved initial results, using a class which is derived from QStyledItemDelegate. I create a list item widget in constructor and then override paint event like this.
void paint(QPainter *painter, const QStyleOptionViewItem &option,const QModelIndex &index) const override {
auto baseWid = getBaseWidget(); // Get's list item widget pointer
setSubEditorData(baseWid,index); // Set's it's state to display current item
baseWid->resize(option.rect.size());
QPixmap pixmap(option.rect.size());
baseWid->render(&pixmap);
painter->drawPixmap(option.rect, pixmap);
}
This is sufficient for static content, but my widget has checkboxes and can be selected.
I don't really understand how to make it interactive, while preserving benefits delegates provide (rendering on demand and so on).
My question is how to make delegate handle user events? Like mouse clicks, selection changes.
Qt5 examples covering delegates are too simple, I don't understand how to draw with delegate a custom widget.
The best workaround I tried to use involves dynamic switching between the static rendered QPixmap and real widget when mouse moves or delegate receives event.
First I override QAbstractItemDelegate::editorEvent, so when delegate receives any event it switches to real widget for that QModelIndex.
Switching to real widget is done using QAbstractItemView::openPersistentEditor. After that call QAbstractItemDelegate::createEditor is automatically invoked to obtain widget.
bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override
{
auto view = qobject_cast<NyView*>(parent());
view->openPersistentEditor(index);
return Base::editorEvent(event, model, option, index);
}
In my view I also enabled every described way to open editor.
this->setEditTriggers(QAbstractItemView::EditTrigger::AllEditTriggers);
this->setMouseTracking(true);
connect(this, &MyView::entered, this, &MyView::openPersistentEditor);
QAbstractItemView::entered signal is emitted when mouse is hovered on widget.
At user perspective nothing changes, before they can interact with list item it's already dynamically replaces with real widget.
Some garbage collection strategy seems to be necessary too, because if user hovers over many widgets they remain in memory even if they didn't interact with it for long time. For deleted rows editors are automatically destroyed, but this may be insufficient.
Open source Qt5 based software (Telegram Desktop) renders large list without using widgets. They render list manually overriding QWidget::paint method and implementing virtualization (drawing only what is seen on screen).
I have quite the headache.
Let's consider the following:
I have a QListView with a custom delegate derived from QStyledItemDelegate.
In the delegate's paint() event I use a custom widget that I render() in the tableview. It's just a statick rendering and it's fine because I only need to show something without interacting.
My custom widget contains a QTableWidget imbedded in a vertical layout and some other labels that I fill with data in the sizehint() of the delegate. I then "force update" the layout of the custom widget with this technique: Qt: How to force a hidden widget to calculate its layout? - see forceUpdate() code.
Everything seems really fine, except one thing: the tablewidget of my custom widget seems to grow vertically when needed (when I add rows in it), BUT the rows are not rendered!!! Shrinking is ok though, setting a very big height for the custom widget somehow fixes the problem but it's not elegant and just reports the problem.
As it is just rendered and therefore not interactive, I don't want scrollbars but I need the QTableWidget to shrink/grow to show the added data. No more, no less.
The custom widget's GUI is made with the designer, everything is set to be dynamically growing and shrinking. Where's the catch? Has anyone seen such a behaviour? If yes, what is the magic parameters combination?
Some code for the eyes:
QSize ResultsRunDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const {
// updating custom widget's data
item_widget->UpdateDisplay(index.row()+1);
forceUpdate(item_widget); //updating the layout
return item_widget->sizeHint(); }
void ResultsRunDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QPaintDevice* originalPaintDev = painter->device();
if (option.state & QStyle::State_Selected)
painter->fillRect(option.rect, option.palette.highlight());
painter->end();
forceUpdate(item_widget);
item_widget->render(painter->device(), QPoint(option.rect.x(), option.rect.y()), QRegion(0, 0, item_widget->rect().width()/*item_widget->sizeHint().width()*/, /*item_widget->rect().height()*/item_widget->sizeHint().height()), QWidget::DrawChildren);
painter->begin(originalPaintDev);
}
Any help would be greatly appreciated. Thx in advance!
I hope the question title is good, comments welcome.
the sizeHint() of a QTableView does not depend on its contents unfortunately. After the layout is calculated, you can get the ideal dimensions of the table from
int width = view->verticalHeader()->width() + view->horizontalHeader()->width() + view->frameWidth()*2;
int height= view->horizontalHeader()->height() + view->verticalHeader()->height() + view->frameWidth()*2;
and resize your widget accordingly.
I do have 2 QDeclarativeItems.
void BackgroundLayer::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
{
painter->drawImage( QRectF(dx1, dy1, dx2-dx1, dy2-dy1), shownImage, QRectF(sx1, sy1, sx2-sx1, sy2-sy1) );
}
void ForegroundLayer::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
{
QPen pen(Qt::red, 3, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
painter->setPen(pen);
painter->drawLine(p1, p2);
}
QML
Rectangle {
width: 1920
height: 1080
BackgroundLayer{
id: background_layer
anchors.fill: parent
}
ForegroundLayer {
id: foreground_layer
anchors.fill: parent
}
}
Drawing on ForegroundLayer triggers BackgroundLayer paint event, causing it to repaint the whole image. As a result, drawing works slow. Is it possible to avoid this and repaint the image only when it's really needed?
Why do you expect any other kind of behavior? Qt doesn't keep the images of every declarative item for you, it'd be prohibitively expensive in terms of memory. You have the option of enabling this, though: perhaps you should. See the cacheMode documentation.
When any item needs to be updated, everything underneath and intersecting the update rectangle has to be repainted too, in the Z order from bottom to top. If there are any widgets underneath the QGraphicsView and if the view itself is translucent, then these widgets will have to be repainted as well.
If you have knowledge exactly of what area needs to be updated, you should use that knowledge: call QGraphicsItem::update(const QRectF &) to indicate the bounds of what needs updating. Otherwise, with a null rectangle, the update region spans the whole item.
Also ensure that the QGraphicsView's updateMode is set to MinimalViewportUpdate.
Under the covers, all QGraphicsItem instances and all QWidget instances all paint on an internal QImage that is then blitted or swapped into the underlying native window. They paint in back-to-front Z order, and the only widgets or items that are skipped are those that are completely contained under an opaque widget or item.
Short answer : Just use a QPixmap converted once from shownImage
void BackgroundLayer::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
{
painter->drawPixmap( QRectF(dx1, dy1, dx2-dx1, dy2-dy1), shownPixmap, QRectF(sx1, sy1, sx2-sx1, sy2-sy1) );
}
Explanation :
painting pixmaps on screen is much faster than images. The other choices may not work or are way too complex.
Paint events propagate from the top widgets to their children, recursively.
Basically Qt is given a rectangle to paint, and every widget inside this rectangle will receive a paint event.
I am sure what you want to achieve might be doable in some specific cases with widget attribute hacking, but I fail to see how you can do it here without having old paint artifacts from the ForegroundLayer.
Let say you have two lines AB et CD.
After the first call to paint you only want to see line AB
After the p1, p2 have been updated you only want to see line CD
In order to prevent you from seeing the line AB when painting CD, Qt has to clear the entire background in the rectangle being painted. If for some reason BackgroundLayer doesn't paint, the background image will disappear.
I'm currently implementing a custom delegate, in part of which I need a QSpinBox to be drawn in the paint(..) method.
void Sy_floatingPointPD::paint( QPainter* painter,
const QStyleOptionViewItem& option,
const QModelIndex& index ) const
{
painter->save();
// Paint check box.
QStyleOptionSpinBox spOpt;
spOpt.palette = option.palette;
spOpt.rect = option.rect;
spOpt.state = option.state;
spOpt.frame = true;
spOpt.stepEnabled = QAbstractSpinBox::StepUpEnabled |
QAbstractSpinBox::StepDownEnabled;
style->drawComplexControl( QStyle::CC_SpinBox, &spOpt, painter );
painter->restore();
}
Unfortunately it appears as:
As you can see the step buttons are drawn massive and only the down arrow appears. Interestingly the width of the buttons mirrors that of the first table column, despite option.rect being the size of the cell (which is correct, which is presumably why the frame is drawn correctly).
Any ideas what information I'm not giving QStyle?
Jens over at the qt-project forums answered this question, so I'll link to it here.
In short, there is a design flaw in the spin box drawing (at least in QGtkStyle) whereby it only uses the size of the option.rect, ignoring it's position. Although this perfectly reasonable in a 'normal' painting scenario because it maps to the widget origin, it fails when rendered in an item view due to the cell offset.
To solve this, move option.rect to the widget origin (i.e. move it's top left corner to (0,0)), and then translate the QPainter to take into account the cell offset.