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).
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)
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.
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 have a model / view schema.
I’m developing a general multipurpose delegates, and I’d want to use some custom widgets I have.
The first I have tested does not work:
void A_delegates::paint(QPainter* painter, const QStyleOptionViewItem & option,
const QModelIndex& index) const
QRect the_rect(option.rect);
A_file_chooser file_chooser;
file_chooser.setGeometry(QRect(QPoint(0,0), the_rect.size()));
file_chooser.render(painter,the_rect.topLeft());
I see an empty cell ?
I have read the doc searching something usefull about this but without success.
Can anybody help me ? Thanks.
( the file chooser is as simple as a label plus a button. I can see it in other scenarios)
You're trying fit file chooser to the cell and force it to render into your widget. But file chooser isn't child of anything, is isn't initialized yet, so it will just skip the render. In paint function of the delegate you must use given painter to draw something. Do you want static button image in the cell? It can't be clicked! You need real file chooser in the cell, added as a child to get it working.
I'm writing an image viewer as a custom Qt widget (see: https://github.com/dov/Qviv) and I now got stuck on the question of how to make my widget notify a parent QScrollArea of changes in the view port, and thus to tell it to move the scrollbars. E.g. if the image viewer changes the zoom factor as the result of a keypress then the scrollbars need to change their page size.
One way of doing it would be to have the widget explicitly check if the parent is a QScrollArea and then make an explicit call to its methods to notify it on any changes.
Of course I also need to connect the changes of the ScrollArea to the internal view of the image, but that is a different question. And I need to cut the infinite recursion where the widget reports changes to the scrollbar that report changes to the widget etc.
Edit 20:15 Wednesday (GMT/UTC) trying to clarify to Vjo and myself what I need.
What I am trying to achieve is the equivalent of a Gtk widget that has been assigned a pair of GtkAdjustment's that are connected to a horizontal and vertical scrollbar. In my widget GtkImageViewer, that QvivImageViewer is based on, whenever I change the view due to some internal event (e.g. a keypress) I update the GtkAdjustment's. The scrollbars are connected to such changes and are update accordingly. GtkImageViewer also listens to the GtkAdjustment changes, and thus if the user scrolls the scrollbars, the GtkImageViewer is updated with this information and can change its view. My question is whether there is anything similar to GtkAdjustment in Qt that you can connect to for changes, and update in which case the update will be propagated to all the listeners?
Thus I don't expect the ScrollArea to be part of QvivImageViewer, but if the user has placed QvivImageViewer within a ScrollArea, I want bidirectional communication with it so that the scrollbars reflect the internal state of the widget.
The simplest is to send the QResizeEvent event from your widget object to the QScrollArea object.
I finally downloaded the Qt sources and investigated how QTextEdit does it. What I found is that QTextEdit inherits the QAbstractScrollArea on its own, and thus the scroll area and the scrollbars are part of the widget. This is different from Gtk, which uses a higher level of abstraction, through its GtkAdjustment's that are used to signal changes between the scrollbars and the widget. The Qt model is simpler and this is the way that I will implement it in my widget.
It's been a while, but I ran across this same issue.
You can inherit QAbstractScrollArea if you'd like, but QScrollArea will work as well.
Your custom inner widget (i.e. the one that you are scrolling), should do the following when its size changes:
void MyCustomControl::resize_me() {
// recompute internal data such that sizeHint() returns the new size
...
updateGeometry();
adjustSize();
}
QSize MyCustomControl::sizeHint() {
return ... ; // Return my internally computed size.
}
I was missing the adjustSize() call, and without it the QScrollArea will ignore size changes of the internal widget.