How to override 'paint' function of custom delegate class to draw QSpinBox - c++

I have added a custom delegator to the QTableView. When I double click on an item I see the editor widget which is a 'QSpinBox' and I am able to edit the value fine. This editor widget disappears once the focus is lost and I understand that. What I want is QSpinBox to be there all the time. Looking at the Qt example here I know I need to override the paint function of QAbstractItemDelegate class to draw the QSpinBox but I don't know how to that. In general, I want to know how any of the Qt widgets can be drawn inside a paint function.
For reference, I am having following test code:
#include <QtWidgets/QApplication>
#include <QtGui>
#include <QTableview>
#include <QLayout>
#include <QColor>
#include <QStyledItemDelegate>
#include <QSpinbox>
class SpinBoxDeligate : public QStyledItemDelegate {
public:
QWidget * createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
auto w = new QSpinBox(parent);
w->setFrame(false);
w->setMinimum(0);
w->setMaximum(100);
return w;
}
void setEditorData(QWidget *editor, const QModelIndex &index) const override {
static_cast<QSpinBox*>(editor)->setValue(index.data(Qt::EditRole).toInt());
}
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override {
model->setData(index, static_cast<QSpinBox*>(editor)->value(), Qt::EditRole);
}
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
// What to replace below line with to have a QSpinBox
QStyledItemDelegate::paint(painter, option, index);
}
};
int main(int argc, char** argv)
{
QApplication app(argc, argv);
QStandardItemModel model(3, 1);
for (int r = 0; r < 3; ++r)
{
auto text = QString("%0").arg(r);
QStandardItem* item = new QStandardItem(text);
item->setFlags(Qt::ItemIsUserCheckable
| Qt::ItemIsEnabled
| Qt::ItemIsEditable
);
item->setData(Qt::Unchecked, Qt::CheckStateRole);
item->setData(text, Qt::ToolTipRole);
item->setData(QSize(100, 30), Qt::SizeHintRole);
item->setData(QIcon(":/QtMVC/Desert.jpg"), Qt::DecorationRole);
model.setItem(r, 0, item);
}
QTableView* table = new QTableView();
table->setModel(&model);
table->setItemDelegate(new SpinBoxDeligate());
QWidget w;
QVBoxLayout* containerLayout = new QVBoxLayout();
w.setLayout(containerLayout);
containerLayout->addWidget(table);
w.show();
return app.exec();
}

A possible solution for your background problem is to use paint() and for that you could create a QSpinBox and use grab to take an image, but before that you should calculate the geometry so that it does not cover the QCheckBox, as you see it is a tedious job, another way is using QStyle but it is still much more code.
A simple solution is to keep the editor open with the openPersistentEditor() method.
int main(int argc, char** argv)
{
QApplication app(argc, argv);
QStandardItemModel model(3, 1);
QTableView* table = new QTableView();
table->setModel(&model);
table->setItemDelegate(new SpinBoxDeligate());
for (int r = 0; r < 3; ++r)
{
auto text = QString("%0").arg(r);
QStandardItem* item = new QStandardItem(text);
item->setFlags(Qt::ItemIsUserCheckable
| Qt::ItemIsEnabled
| Qt::ItemIsEditable
);
item->setData(Qt::Unchecked, Qt::CheckStateRole);
item->setData(text, Qt::ToolTipRole);
item->setData(QSize(100, 30), Qt::SizeHintRole);
item->setData(QIcon(":/QtMVC/Desert.jpg"), Qt::DecorationRole);
model.setItem(r, 0, item);
table->openPersistentEditor(model.indexFromItem(item));
}
QWidget w;
QVBoxLayout* containerLayout = new QVBoxLayout();
w.setLayout(containerLayout);
containerLayout->addWidget(table);
w.show();
return app.exec();
}

I can think of two possible solutions:
It is possible to insert widgets into table cells. I know this is not what you're trying to do but it can be a better solution for your problem. Check setIndexWidget.
If you really want to render QSpinBox, you should use the render method of QWidget. To try, in your paint method create a QSpinBox and call it's render method passing it the QPainter pointer. After you get it working this way, you can improve your design by possibly holding a 'template' QSpinBox instance in your QTableView and use it to render different QSpinBox values inside cells where required

Related

QListView set Custom Editor via QStyledItemDelegate::createEditor

I want to show custom widget in each QListView cells (3 labels width different fonts and 2 tool buttons). The widget must handle mouse events for correct handling of the hover events and button clicks. (Therefore I cannot just draw it in QStyledItemDelegate::paint()).
Here is what I want each row in a list view looks like:
The main idea: QAbstractItemView::openPersistentEditor().
#include <QApplication>
#include <QWidget>
#include <QHBoxLayout>
#include <QLabel>
#include <QToolButton>
#include <QVBoxLayout>
#include <QDateTime>
#include <QListView>
#include <QStringListModel>
#include <QStyledItemDelegate>
class Form : public QWidget
{
//Q_OBJECT
public:
explicit Form(QWidget *parent = nullptr)
:QWidget(parent)
{
verticalLayout = new QVBoxLayout(this);
horizontalLayout = new QHBoxLayout();
labelTitle = new QLabel(this);
labelTitle->setFont(QFont("Calibri", 12, QFont::Bold));
horizontalLayout->addWidget(labelTitle);
toolButtonEdit = new QToolButton(this);
toolButtonEdit->setText("E");
horizontalLayout->addWidget(toolButtonEdit);
toolButtonRemove = new QToolButton(this);
toolButtonRemove->setText("R");
horizontalLayout->addWidget(toolButtonRemove);
verticalLayout->addLayout(horizontalLayout);
labelDate = new QLabel(this);
labelDate->setFont(QFont("Calibri", 8));
verticalLayout->addWidget(labelDate);
labelText = new QLabel(this);
labelText->setFont(QFont("Calibri", 10));
verticalLayout->addWidget(labelText);
verticalLayout->setStretch(2, 1);
setMinimumSize(QSize(300, 50));
}
public:
QVBoxLayout *verticalLayout;
QHBoxLayout *horizontalLayout;
QLabel *labelTitle;
QToolButton *toolButtonEdit;
QToolButton *toolButtonRemove;
QLabel *labelDate;
QLabel *labelText;
};
class MyDelegate : public QStyledItemDelegate
{
public:
QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
auto editor = new Form(parent);
return editor;
}
void setEditorData(QWidget *ed, const QModelIndex &index) const override
{
QVariant var = index.model()->data(index, Qt::DisplayRole);
if (Form *editor = dynamic_cast<Form*>(ed))
{
editor->labelTitle->setText("SYMBOL");
editor->labelDate->setText("date-time");
editor->labelText->setText(var.toString());
}
}
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem& option, const QModelIndex &)const override
{
editor->setGeometry(option.rect);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Form form(nullptr);
form.labelTitle->setText("TITLE");
form.labelDate->setText(QDateTime::currentDateTime().toString());
form.labelText->setText("text body");
form.show();
auto model = new QStringListModel;
model->setStringList(QStringList()
<< "text body 1"
<< "text body 2"
<< "text body 3");
auto view = new QListView(nullptr);
view->setModel(model);
view->setItemDelegate(new MyDelegate);
int rowCount = model->rowCount();
for (int row = 0; row < rowCount; ++row)
{
QModelIndex index = model->index(row, 0);
view->openPersistentEditor(index);
}
view->show();
return a.exec();
}
Here is how the list view actually looks:
What how can one set such a custom widget to show view cells?
Note that while you are defining your own delegate MyDelegate you never actually use it (i.e. by calling QAbstractItemView::setItemDelegate(). Therefore you see the default delegate (a simple QLineEdit for data of type QString) when calling openPersistentEditor().

Is there a way to calculate the height of the QStandardItem?

I have created an expandable ListView which extends from the QListView, everything works well when I just want to show the Header data (Item which is not expanded) because I gave it a hard-coded height which is 64, the details appear when expanding the item. But the problem is I do not know the exact height of the details because the details can one line or more, I want to fit the Item height according to the item content.
Here the code which is handling click listener when the item expanding or collapsing:
LogListItemDelegate *delegate = static_cast<LogListItemDelegate *>(itemDelegate());
QStandardItem *item = static_cast<QStandardItemModel *>(model())->itemFromIndex(index);
bool expand = delegate->isExpandable() && mapFromGlobal(QCursor::pos()).x() >= visualRect(index).width() - 48;
bool expanded = index.data(LogListItemDelegate::DT_Expanded).toBool();
// here the height returned is header height, no containing the details which it is in expanding mode
int height = item->sizeHint().height();
if (!expanded) {
item->setData(true, LogListItemDelegate::DT_Expanded);
item->setSizeHint(QSize(0, 150)); // 150 here must be dynamically calculated
} else {
item->setData(false, LogListItemDelegate::DT_Expanded);
item->setSizeHint(QSize(0, 64)); // 64 is the header height, no prolem
}
Now the question is: How to calculate the height when item expanded?
Result:
Edit:
It is when I want to add the message to the list
void LogListView::addMessage(const QJsonObject &msg, const bool append)
{
static int id = 1; // unique id for log items
auto *item = new QStandardItem();
item->setEditable(false);
item->setData(QString("%1").arg(id++, 5, 10, QChar('0')), LogListItemDelegate::DT_Id);
item->setData(msg["icon"], LogListItemDelegate::DT_ICON);
item->setData(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"), LogListItemDelegate::DT_Timestamp);
item->setData(msg["title"], LogListItemDelegate::DT_Title);
item->setData(msg["subtitle"], LogListItemDelegate::DT_Subtitle);
item->setData(msg["details"], LogListItemDelegate::DT_Details);
item->setData(false, LogListItemDelegate::DT_Expanded);
// here I am unable to calculate the height, because the details does not have a specific height to set here,
// so when append the item to the list it is unvisible. If set the height 64, it is the exact height of the item without details, which is good
//item->setSizeHint(QSize(0, 64));
static_cast<QStandardItemModel *>(model())->appendRow(item);
scrollToBottom();
}
It is the code in sizeHint()
QSize LogListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
bool expanded = index.data(DT_Expanded).toBool();
QFont fntDetials = option.font;
fntDetials.setPointSize(12);
QRect r = option.rect;
QFontMetrics fm(fntDetials);
QString details = index.data(DT_Details).toString();
QRect br = fm.boundingRect(r, Qt::TextWordWrap, details);
return QSize(option.rect.width(), br.height()+64);
}
Unfortunately not working..., I think Qt can look the Android ListView and its recycle functionality to solve the ListView problem, in this way, I think it is very very painful.
If you want to set a custom size you should use the sizeHint method of QStyledItemDelegate, for example:
#include <QApplication>
#include <QStyledItemDelegate>
#include <QListView>
#include <QStandardItemModel>
class HeightDelegate: public QStyledItemDelegate
{
public:
using QStyledItemDelegate::QStyledItemDelegate;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override{
QSize s = QStyledItemDelegate::sizeHint(option, index);
// some calculation
int h = (index.row()+1)*20;
s.setHeight(h);
return s;
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QListView w;
QStandardItemModel model;
HeightDelegate delegate;
w.setItemDelegate(&delegate);
w.setModel(&model);
for(int i=0; i<8; i++){
QStandardItem *it = new QStandardItem(QString::number(i));
it->setBackground(QBrush(QColor(qrand()%255, qrand()%255, qrand()%255)));
model.appendRow(it);
}
w.show();
return a.exec();
}

QListView items also showing the delegate editor even though the delegate is only added to the QTableView

I have a QListView and a QTableView both have the same QStandardItemModel. I have added a custom delegator to the QTableView. Now when I go to my QTableView and double-click an item I see the delegate editor widget, now if I go to my QListView and double-click the same item I see the delegate editor widget there as well. Point to be noted is that I see the editor widget only for those items in QListView which have been double-clicked in QTableView already. Whats going on here? Why do QListView items also showing the delegate editor widget even though the delegate is only added to the QTableView?
For reference, I am having below code:
#include <QtWidgets/QApplication>
#include <QtGui>
#include <QCombobox>
#include <QListview>
#include <QTableview>
#include <QLayout>
#include <QColor>
#include <QStyledItemDelegate>
#include <QSpinbox>
class SpinBoxDeligate : public QStyledItemDelegate {
public:
QWidget * createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
auto w = new QSpinBox(parent);
w->setFrame(false);
w->setMinimum(0);
w->setMaximum(100);
return w;
}
void setEditorData(QWidget *editor, const QModelIndex &index) const override {
static_cast<QSpinBox*>(editor)->setValue(index.data(Qt::EditRole).toInt());
}
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override {
model->setData(index, static_cast<QSpinBox*>(editor)->value(), Qt::EditRole);
}
};
int main(int argc, char** argv)
{
QApplication app(argc, argv);
QStandardItemModel model(3, 1);
for (int r = 0; r < 3; ++r)
{
auto text = QString("%0").arg(r);
QStandardItem* item = new QStandardItem(text);
item->setFlags(Qt::ItemIsUserCheckable
| Qt::ItemIsEnabled
| Qt::ItemIsEditable
);
item->setData(Qt::Unchecked, Qt::CheckStateRole);
item->setData(text, Qt::ToolTipRole);
item->setData(QSize(100, 30), Qt::SizeHintRole);
item->setData(QIcon(":/QtMVC/Desert.jpg"), Qt::DecorationRole);
model.setItem(r, 0, item);
}
QComboBox* combo = new QComboBox();
combo->setModel(&model);
QListView* list = new QListView();
list->setModel(&model);
QTableView* table = new QTableView();
table->setModel(&model);
table->setItemDelegate(new SpinBoxDeligate());
QWidget w;
QVBoxLayout* containerLayout = new QVBoxLayout();
w.setLayout(containerLayout);
containerLayout->addWidget(combo);
containerLayout->addWidget(list);
containerLayout->addWidget(table);
w.show();
return app.exec();
}
The problem is really simple, if the data saved in the model are numbers the delegate is a QSpinBox by default, ie the delegate you see is the QListView is not the SpinBoxDeligate, but the delegate by default.
And why is it generated if you do not keep a number?
It's because the SpinBoxDeligate saves the data as a number.
So the solution is to save the data obtained by the SpinBoxDeligate as text:
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override {
model->setData(index, static_cast<QSpinBox*>(editor)->text(), Qt::EditRole);
}

QStyledItemDelegate: commit QComboBox value to model on click

I am setting a QStyledItemDelegate on my model for a particular field, and returning a QComboBox from QStyledItemDelegate::createEditor
QComboBox* createEditor(QWidget* parent)
{
QComboBox* cb = new QComboBox(parent);
cb->addItem("UNDEFINED");
cb->addItem("TEST");
cb->addItem("OSE");
cb->addItem("TSE");
return cb;
}
void setEditorData(QWidget* editor, const QModelIndex& index)
{
QComboBox* cb = qobject_cast<QComboBox*>(editor);
if (!cb)
throw std::logic_error("editor is not a combo box");
QString value = index.data(Qt::EditRole).toString();
int idx = cb->findText(value);
if (idx >= 0)
cb->setCurrentIndex(idx);
cb->showPopup();
}
This is working fine, and when I select the field in question I am shown a combo box.
When I select an option from the drop-down list, the combobox closes and the item is displayed with a drop-down icon next to it:
At this point I would like the QStyledItemDelegate::setModelData function to be called, so that selecting an item in the list commits the data to the model.
However, I am required to first press Enter to commit the data (whereby the drop-down icon disappears)
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index)
{
QComboBox* cb = qobject_cast<QComboBox*>(editor);
if (!cb)
throw std::logic_error("editor is not a combo box");
model->setData(index, cb->currentText(), Qt::EditRole);
}
Question:
How can I configure my QComboBox to automatically commit the data when the user selects an item in the list and the combobox list closes, rather than requiring the additional press of Enter?
You have to issue the signal commitData and closeEditor when an item is selected as shown in the following example:
#include <QApplication>
#include <QStandardItemModel>
#include <QListView>
#include <QStyledItemDelegate>
#include <QComboBox>
class ComboBoxDelegate: public QStyledItemDelegate{
public:
using QStyledItemDelegate::QStyledItemDelegate;
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const{
Q_UNUSED(option)
Q_UNUSED(index)
QComboBox* editor = new QComboBox(parent);
connect(editor, QOverload<int>::of(&QComboBox::activated),
this, &ComboBoxDelegate::commitAndCloseEditor);
editor->addItems({"UNDEFINED", "TEST", "OSE", "TSE"});
return editor;
}
void setEditorData(QWidget *editor, const QModelIndex &index) const{
QComboBox* cb = qobject_cast<QComboBox*>(editor);
if (!cb)
throw std::logic_error("editor is not a combo box");
QString value = index.data(Qt::EditRole).toString();
int idx = cb->findText(value);
if (idx >= 0)
cb->setCurrentIndex(idx);
cb->showPopup();
}
private:
void commitAndCloseEditor(){
QComboBox *editor = qobject_cast<QComboBox *>(sender());
emit commitData(editor);
emit closeEditor(editor);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QListView view;
QStandardItemModel model;
for(int i=0; i<10; i++){
model.appendRow(new QStandardItem("UNDEFINED"));
}
view.setItemDelegate(new ComboBoxDelegate(&view));
view.setModel(&model);
view.show();
return a.exec();
}

How do I animate the border of a list item in a QListView?

I am using a QListView with a custom delegate that extends from QStyledItemDelegate. I reimplemented the paint method to custom paint each item in the list. In the paint method, I am drawing a border around selected items in the list view.
I want to be able to animate the item border as I select an item. For example, if the intended item border is 5 pixels, I want to have it "animate in" from 0 pixels to 5 pixels when the item is selected.
My original idea was to hook up a timer to go off every 50 milliseconds and have the delegate paint every time the timer goes off until the full border width has been painted. However, the delegate's reimplemented paint method is const, so I can't save or update a border width member variable during each pass through of the paint method.
What is the best way of accomplishing this?
A possible solution is to create a role that manages the border size of the item, and update it using a QVariantAnimation:
#include <QApplication>
#include <QListView>
#include <QPainter>
#include <QStandardItemModel>
#include <QStyledItemDelegate>
#include <QVariantAnimation>
int BorderSizeRole = Qt::UserRole+1;
class AnimationDelegate: public QStyledItemDelegate{
public:
using QStyledItemDelegate::QStyledItemDelegate;
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const{
QStyledItemDelegate::paint(painter, option, index);
bool ok;
int borderSize = index.data(BorderSizeRole).toInt(&ok);
if(borderSize >0 && ok){
painter->save();
QPen pen(QBrush(Qt::red), borderSize);
painter->setPen(pen);
painter->drawRect(option.rect);
painter->restore();
}
}
};
class CustomAnimation: public QVariantAnimation{
QPersistentModelIndex m_index;
QAbstractItemModel *m_model;
public:
CustomAnimation(QAbstractItemModel *m_model, QPersistentModelIndex index, QObject *parent=nullptr)
: QVariantAnimation(parent),
m_index(index),
m_model(m_model)
{
setStartValue(0);
setEndValue(5);
setDuration(50*5);
connect(this, &CustomAnimation::valueChanged, this, &CustomAnimation::on_valueChanged);
// delete animation
start(QAbstractAnimation::DeleteWhenStopped);
}
private:
Q_SLOT void on_valueChanged(const QVariant & value){
if(m_model)
m_model->setData(m_index, value, BorderSizeRole);
else
stop();
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QListView view;
view.setItemDelegate(new AnimationDelegate(&view));
QStandardItemModel model;
for(int i=0; i<10; i++){
QStandardItem *item = new QStandardItem(QString("item %1").arg(i));
item->setData(-1, BorderSizeRole);
model.appendRow(item);
}
view.setModel(&model);
QObject::connect(view.selectionModel(), &QItemSelectionModel::selectionChanged,
[&model](const QItemSelection &selected, const QItemSelection & deselected){
for(const QModelIndex & index: selected.indexes()){
new CustomAnimation(&model, QPersistentModelIndex(index));
}
// remove border
for(const QModelIndex & index: deselected.indexes()){
model.setData(index, -1, BorderSizeRole);
}
});
view.show();
return a.exec();
}