I'm trying to create a custom item delegate for a list of objects, but when I run my app the list view is blank. I know my model is populated, and it displays as expected with the standard default delegate. I can also tell that my custom delegate is being rendered in some sense, since space is taken up in the list, and the tooltips and statustips that I set work, but the contents are not visible (the listview appears to be blank white).
My code is based on this example: https://doc.qt.io/archives/qt-5.12/qtwidgets-itemviews-stardelegate-example.html
Qt 5.12 is the version that I'm stuck with, and I'm also stuck using visual studio without the normal Qt debugging tools, do to factors outside of my control.
Anyway, I create the widgets that should be displayed in the constructor, with default values for testing:
EntityListDelegate::EntityListDelegate(QWidget* parent /*= nullptr*/) : QStyledItemDelegate(parent)
{
ui = new QWidget(parent);
ui->setMinimumSize(QSize(200, 40));
QHBoxLayout* hLayout = new QHBoxLayout(ui);
iconLabel = new QLabel(ui);
iconLabel->setMinimumSize(QSize(20, 20));
iconLabel->setMaximumSize(QSize(40, 40));
iconLabel->setPixmap(QPixmap(":/icons/Placeholder.png"));
hLayout->addWidget(iconLabel);
QVBoxLayout* vLayout = new QVBoxLayout(ui);
nameLabel = new QLabel(ui);
nameLabel->setMinimumSize(QSize(150, 20));
nameLabel->setText("Unknown entity");
vLayout->addWidget(nameLabel);
typeLabel = new QLabel(ui);
typeLabel->setMinimumSize(QSize(150, 16));
typeLabel->setText("Unknown type");
vLayout->addWidget(typeLabel);
hLayout->addLayout(vLayout);
ui->setLayout(hLayout);
}
Note that each item should have a thick black border, which is not being rendered. (Edit: I changed the QFrame back to a QWidget)
I also implemented paint, where the real values should get set based on info in the model:
void EntityListDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
nameLabel->setText(index.data(Qt::DisplayRole).toString());
switch (qvariant_cast<int>(index.data(UserRoles::Type)))
{
case EntityType::Foo:
{
iconLabel->setPixmap(QPixmap(":/icons/foo.png"));
typeLabel->setText("Foo");
break;
}
case EntityType::Bar:
{
iconLabel->setPixmap(QPixmap(":/icons/bar.png"));
typeLabel->setText("Bar");
break;
}
case EntityType::Baz:
{
iconLabel->setPixmap(QPixmap(":/icons/baz.png"));
typeLabel->setText("Baz");
break;
}
default:
break;
}
}
The other functions are just stubs, since the user shouldn't be able to edit this data.
Am I missing some step in my setup?
Update: I tried making the pointer to the delegate that gets passed into setItemDelegate() a member of the class that owns the the ListView. I now get one item on the list that renders correctly, in the top position in the list. I can sometimes get it to display another item by clicking where that item should be, but it's still in the same location.
The answer to the question as asked is that if you're trying to alter the default display, not the editor, you have to reimplement paint and basically draw the whole thing manually. This is such a massive PITA as to make QStyledItemDelegate functionally unusable for this use case, IMO. Use QtQuick if you can, but that's not an option in my case.
My actual solution, for the curious, was to use QSortFilterProxyModel and override data() to return "EntityName\nEntityType". That gets me ~80% of what I was trying to achieve, and hopefully I can get a little closer with styling when I get to that part.
Related
I have a QListView with a lot of items that are of various heights. I implement a custom delegate for painting items and set layout mode to Batched.
However, when the model is assigned, the list view requests sizeHint for every item in the model upfront, ignoring the Batched setting and thus ruining the performance because to calculate the size, the delegate has to layout a lot of text (which is not fast).
Probably it does this to calculate the scrollbar position, but I reckoned that when the number of items is large, the scrollbar position can be based on item indices only, not taking into consideration item heights. However it seems that this is not how QListView works.
I also tried to use canFetchMore/fetchMore in the model, but this leads to bad user experience - the scrollbar position is no longer accurate, and the list jumps around when more items are loaded, it was not smooth at all.
So, the question is:
Is there a way to prevent QListView from calling sizeHint for invisible items?
If the only way is to use canFetchMore/fetchMore, how to get smooth scrolling and stable and accurate scrollbar?
Thanks a lot!
UPD: Here is the minimal example that reproduces this behavior:
https://github.com/ajenter/qt_hugelistview
Note the huge startup delay and the debug messages showing that sizeHint of all 5000 items is requested upfront.
Well it seems that I've found a solution, so I'll share it here for sake of anyone who has the same problem and googles this thread.
First of all, I've found that this is actually a bug in Qt registered back in 2011 and still open:
https://bugreports.qt.io/browse/QTBUG-16592
I've added my vote to it (and you should, too!).
Then decided to try out using QTableView instead of QListView - and, surpise, I managed to make it work, or so it seems.
Unlike QListView, QTableView only resizes rows upon explicit request, by calling resizeRowToContents(rowNum). So the trick is to call it in a just-in-time fashion for rows that become visible in the viewport.
Here's what I did:
Inherit from QTableView (let's call it MyTableView)
Replace QListView with MyTableView and initialize it like this in the constructor. This assigns custom item delegate, hides table headers and applies "by row" selection mode:
MyTableView::MyTableView(QWidget* parent) : QTableView(parent)
{
setSelectionBehavior(QAbstractItemView::SelectRows);
horizontalHeader()->setStretchLastSection(true);
horizontalHeader()->hide();
verticalHeader()->hide();
setItemDelegateForColumn(0, new CustomDelegate(&table)); // for custom-drawn items
}
In MyTableView, add a QItemSelection private field and a public function that calculates real heights of rows, but only those that are currently visible:
QItemSelection _itemsWithKnownHeight; // private member of MyTableView
void MyTableView::updateVisibleRowHeights()
{
const QRect viewportRect = table.viewport()->rect();
QModelIndex topRowIndex = table.indexAt(QPoint(viewportRect.x() + 5, viewportRect.y() + 5));
QModelIndex bottomRowIndex = table.indexAt(QPoint(viewportRect.x() + 5, viewportRect.y() + viewportRect.height() - 5));
qDebug() << "top row: " << topRowIndex.row() << ", bottom row: " << bottomRowIndex.row();
for (auto i = topRowIndex.row() ; i < bottomRowIndex.row() + 1; ++i)
{
auto index = model()->index(i, 0);
if (!_itemsWithKnownHeights.contains(index))
{
resizeRowToContents(i);
_itemsWithKnownHeights.select(index, index);
qDebug() << "Marked row #" << i << " as resized";
}
}
}
Note: if item heights depend on control's width, you need to override resizeEvent(), clear _itemsWithKnownHeights, and call updateVisibleRowsHeight() again.
Call updateVisibleRowHeights() after assigning a model to MyTableView instance, so that initial view is correct:
table.setModel(&myModel);
table.updateVisibleRowHeights();
In fact it should be done in some MyTableView's method that reacts to model changes, but I'll leave it as an exercise.
Now all that's left is to have something call updateRowHeights whenever table's vertical scroll position changes. So we need to add the following to MyTableView's constructor:
connect(verticalScrollBar(), &QScrollBar::valueChanged, [this](int) {
updateRowHeights();
});
Done - it works really fast even with model of 100,000 items! And startup is instantenious!
A basic proof-of-concept example of this technique (using pure QTableView instead of subclass) can be found here:
https://github.com/ajenter/qt_hugelistview/blob/tableview-experiment/src/main.cpp
Warning: this technique is not battle proven yet and may contain some yet unknown issues. Use at own risk!
TL;DR: I'm having a grow/shrink probably using embedded forms inside a MainWindow. I'm unsure what to try next.
Okay, I have another sizing problem.
This is a sample app of what I'm trying to do:
When I click on the various toolbar options, I intend to change the central widget contents accordingly. Maybe I should just use a tab widget, but I wanted to do it this way.
In the simplest form, with a widget layout like this:
I set the central widget's layout to Horizontal, and the Inner Widget to FormLayout then set the inner widget's expand rules to expand any expandable fields. As I resize the window, the simple line edit expands and contracts as desired.
When I click the bus icon in the toolbar, I swap out the contents of the central widget with a separate panel. That panel has a widget with a form layout, and is also set to expand and collapse. Here are the layout rules for the second panel:
My trigger code does this:
currentCenter = ui->innerWidget; // In the constructor
currentCenter->hide();
if (v1Form == nullptr) {
v1Form = new V1Form(ui->centralWidget);
}
v1Form->show();
currentCenter = v1Form;
I have tried various orders to this, and I tried using setCentralWidget(). In all cases, the new central area remains a fixed size, even though the original one expands and collapses.
What is working: I can readily change the inner contains for different forms. That's working great. (It took a while to figure it out.)
-or- I can make simple popup forms that grow and shrink properly.
What is not working is grow/shrink when I embed my form inside my central widget or if I use setCentralWidget.
I'm not sure what else to try.
Maybe I should just use a tab widget, but I wanted to do it this way.
You should definitely use a QTabWidget as your central widget. It is designed specifically for your use case, and it will greatly simplify your code.
My trigger code does this:
currentCenter = ui->innerWidget; // In the constructor
currentCenter->hide();
if (v1Form == nullptr) {
v1Form = new V1Form(ui->centralWidget);
}
v1Form->show();
currentCenter = v1Form;
With a QTabWidget, your trigger code can be simplified to:
ui->innerTabWidget->setIndex(1).
You don't need to dynamically construct a V1Form. Simply use Qt Designer to create multiple pages in your QTabWidget and implement all your subpanel widgets within your MainWindow.ui.
(Nonetheless, if you want to implement each subpanel in its own separate *.ui file, you can still promote each page in your QTabWidget to your custom widget.)
What is not working is grow/shrink when I embed my form inside my central widget or if I use setCentralWidget.
To address your original symptoms: Your widgets don't grow/shrink because you didn't put them inside a layout that is part of your main window.
I found a solution doing it the way I started. I had to add one line of code:
void MainWindow::switchForm(QWidget *widget) {
if (centralForm != widget) {
if (centralForm != nullptr) {
centralForm->hide();
centralForm = nullptr;
}
if (widget != nullptr) {
centralForm = widget;
centralForm->show();
ui->centralwidget->layout()->addWidget(centralForm);
}
}
}
void MainWindow::on_actionSetup_triggered()
{
if (setupForm == nullptr) {
setupForm = new SetupForm(ui->centralwidget);
}
switchForm(setupForm);
}
The missing line -- adding my new form to the layout:
ui->centralwidget->layout()->addWidget(centralForm);
I am doing a small game in Qt and try to use WASD to move my protagonist(QGraphicsPixmapItem). The map is quite big so I use a QComboBox to change the scale of the scene.
The game looks like this:
The simple game
How I built the protagonist:
protagonist = new MyProtagonist();
protagonist->setFlag(QGraphicsItem::ItemIsFocusable);
protagonist->setFocus();
scene->addItem(protagonist);
How I built the combox:
sceneScaleCombo = new QComboBox;
QStringList scales;
scales << tr("1%")<<tr("10%") << tr("20%") <<tr("50%") << tr("100%") <<tr("200%");
sceneScaleCombo->addItems(scales);
sceneScaleCombo->setCurrentIndex(4);
connect(sceneScaleCombo,SIGNAL(currentIndexChanged(QString)),this,SLOT(sceneScaleChanged(QString)));
void MainWindow::sceneScaleChanged(const QString &scale)
{
double newScale = scale.left(scale.indexOf(tr("%"))).toDouble() / 100;
QMatrix oldMatrix = view->matrix();
view->resetMatrix();
view->translate(oldMatrix.dx(),oldMatrix.dy());
view->scale(newScale,newScale);
protagonist->setFocus();
}
Everything worked well at the beginning. However, after I clicking the combobox, my protagonist cannot be controlled by keyboard any more. I need to click my protagonist to make it focused again.
Is there any way to set it focused automatically?
Thanks a lot.
By clicking QComboBox the QGraphicsView looses the focus. You attempt to return it back by calling setFocus(); on the QGraphicsPixmapItem. This approach doesn't work as you expect, because it sets the focus item inside the view, but the view itself still does not have focus. To fix that in MainWindow::sceneScaleChanged instead of protagonist->setFocus(); write view->setFocus();.
I have to draw a custom control in QTableView. This control must looks like FileChooser.
FileChooser http://www.vision.ee.ethz.ch/computing/sepp-irix/qt-3.0-mo/filechooser.png
QStyleOptionButton button_option;
button_option.state |= QStyle::State_Enabled | QStyle::State_Off;
button_option.rect = PushButtonRect(option); //calculate button rect
button_option.text = "...";
QApplication::style()->drawControl(
QStyle::CE_PushButton,
&button_option,
painter);
The code above draws QStyle::CE_PushButton - that looks like QButton, - but there is no QStyle::CE_LineEdit in Qt library. How can I draw QLineEdit?
In order to draw custom widgets in a Table View, you need to create a custom QItemDelegate subclass and override at least the createEditor method, where you can create any kind of widget which is displayed when double-clicking into the table cell. This item delegate can be assigned to the respective column in your table view.
You would then need to create a separate class e.g. CustomFileChooser which inherits from QWidget and consists of a Line Edit and Button.
Your createEditor method would then return such an object.
You may also have to override setEditorData (which shall assign the current model value to the editor widget which was created) and setModelData (which is called when the changes are committed).
This way, the line edit and button would only be visible after double-clicking into the table cell. If you want it to be always visible, you will have to override drawDisplay() as well.
I found an answer by myself. You may display a custom editor (ordinary widget) permanently using:
void QAbstractItemView::openPersistentEditor ( const QModelIndex & index )
First you need to understand that a button is a control Element and thus you can find it under CE but when you need a lineEdit it is not a control element.
In order to paint a lineEdit, I shall quote from the qt documentation,
"QStyleOptionFrameV2 inherits QStyleOptionFrame which is used for drawing several built-in Qt widgets, including QFrame, QGroupBox, QLineEdit, and QMenu."
Yes, only a sample code that might work will help you understand it clearly!
The code should somehow look like this
QStyleOptionFrameV2 *panelFrame = new QStyleOptionFrameV2;
QLineEdit *search = new QLineEdit;
panelFrame->initFrom(search);
panelFrame->rect = QRect(x,y,w,h);//Indeed the location and the size
panelFrame->lineWidth = QApplication::style->pixelMetric(QStyle::PM_DefaultFrameWidth, panelFrame, search);
panelFrame->state |= QStyle::State_Sunken;
QApplication::style()->drawPrimitive(QStyle::PE_PanelLineEdit, panelFrame, painter);
I am using QT to create a chat messenger client. To display the list of online users, I'm using a QListWidget, as created like this:
listWidget = new QListWidget(horizontalLayoutWidget);
listWidget->setObjectName("userList");
QSizePolicy sizePolicy1(QSizePolicy::Preferred, QSizePolicy::Expanding);
sizePolicy1.setHorizontalStretch(0);
sizePolicy1.setVerticalStretch(0);
sizePolicy1.setHeightForWidth(listWidget->sizePolicy().hasHeightForWidth());
listWidget->setSizePolicy(sizePolicy1);
listWidget->setMinimumSize(QSize(30, 0));
listWidget->setMaximumSize(QSize(150, 16777215));
listWidget->setBaseSize(QSize(100, 0));
listWidget->setContextMenuPolicy(Qt::CustomContextMenu);
Users are shown by constantly refreshing the list, like this: (Note: There are different channels, with different userlists, so refreshing it is the most efficient thing to do, as far as I know.)
void FMessenger::refreshUserlist()
{
if (currentPanel == 0)
return;
listWidget = this->findChild<QListWidget *>(QString("userList"));
listWidget->clear();
QList<FCharacter*> charList = currentPanel->charList();
QListWidgetItem* charitem = 0;
FCharacter* character;
foreach(character, charList)
{
charitem = new QListWidgetItem(character->name());
// charitem->setIcon(QIcon(":/Images/status.png"));
listWidget->addItem(charitem);
}
}
This has always worked perfectly. The line that I commented out is the one I have problems with: my current goal is to be able to display a user's online status with an image, which represents whether they are busy, away, available, etc. Using setIcon() does absolutely nothing though, apparently; the items still show up as they used to, without icons.
I'm aware that this is probably not the way this function needs to be used, but I have found little documentation about it online, and absolutely no useful examples of implementations. My question is, can anybody help me with fixing this problem?
This is how you may conduct your debugging:
Try the constructor that has both icon and text as arguments.
Try to use that icon in another context to ensure it is displayable (construct a QIcon with same argument and use it elsewhere, e.g. QLabel!).
Use icon() from the QListWidgetItem to receive back the icon and then look at that QIcon.
Create a new QListWidget, change nothing, and ordinarily add some stock items in your MainWidget's constructor. See if the icons show up there.