Solved many troubles with a simple Delegate
-
Hi all,
I have been working on a simple MVC example to fit my needs but I'm facing many troubles at many levels and I just started with a very simple case. I have been reading the documentation and many posts here but it seems that I still miss a lot of understanding. I'm still a newbie so please bear with me.
My model is a string and integer pair where the integer may have different ranges. The delegate is applied to the integer column only. I want a QSlider and a QSpinBox to change the ints.
To go straight to the point, let me post my simple code and list all the troubles I'm facing.QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & option, const QModelIndex & index ) const { QWidget* editor = new QWidget(parent); QSlider *slider = new QSlider(editor); slider->setOrientation(Qt::Orientation::Horizontal); slider->setMinimumHeight(20); slider->setMinimumWidth(100); // Todo: // find a way to get properties from QModelIndex // so that we can set dynamic ranges... slider->setMinimum(0); slider->setMaximum(100); QSpinBox *spinbox = new QSpinBox(editor); spinbox->setMinimumWidth(20); spinbox->setFrame(false); spinbox->setMinimum(0); spinbox->setMaximum(100); spinbox->selectAll(); //<- not working! connect(slider, SIGNAL(valueChanged(int)), spinbox, SLOT(setValue(int))); connect(spinbox, SIGNAL(valueChanged(int)), slider, SLOT(setValue(int))); connect(spinbox, SIGNAL(valueChanged(int)), this, SLOT(onSpinboxValueChanged(int))); QHBoxLayout *layout = new QHBoxLayout; layout->addWidget(slider); layout->addWidget(spinbox); editor->setLayout(layout); return editor; } void SpinBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { int value = index.model()->data(index, Qt::EditRole).toInt(); for (auto widget : editor->findChildren<QSlider*>()) { widget->setValue(value); } for (auto widget : editor->findChildren<QSpinBox*>()) { widget->setValue(value); } } void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { int value = index.model()->data(index, Qt::EditRole).toInt(); for (auto widget : editor->findChildren<QSlider*>()) { value = widget->value(); } for (auto widget : editor->findChildren<QSpinBox*>()) { value = widget->value(); } model->setData(index, value, Qt::EditRole); }
This is how it looks like:
Problems:
1- In the image you can immediately see the widget is drawn on top of the table cell... That's not good at all.
2- The QSpinbox is not selected automatically on creation. Tried several codes, non worked.
3- I don't know how to change the range(min/max) of the Qslider depending on QModelIndex ranges. If anyone could please point to a doc/example explaining how to do that.
4- the widget adds some margin. Tried to editor->setContentsMargins(QMargins(0, 0, 0, 0)), no luck.
5- do not know how to resize table cell to widget size. I don't even know if that code must be on the delegate class. Otherwise where?Thank you for any hints.
Jordi -
I modified the SpinBoxDelegate example from Qt to tackle some of these issues for you. Here is the pic:
Here's the fixes --
When I create the editor widget, I set the auto fill background. This will fix the fact that you can see the cell through your editor in your example.
auto *widget = new QWidget(parent); widget->setAutoFillBackground(true);
To fix the margin and position the widget properly I set the margin to 0 on the layout like so:
auto *layout = new QHBoxLayout(); layout->addWidget(slider); layout->addWidget(spinbox); layout->setMargin(0); widget->setLayout(layout);
That solves #1 and #4 for you.
#5 happens automatically for me and if the table widget window isn't big enough it gives me a horizontal scrollbar which is how it should work. What are you looking for beyond that?
#3 I don't quite understand what you want here... If you explain it further me or someone else can tell you how to get the info you want from the model.
#2 I haven't tackled this yet. My initial attempt in setting focus didn't work. I will play with it some more if I have time later on. Maybe someone else has a quick tip here.
Edit: forgot to attach full code... here is the modified delegate.cpp file from the example:
/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the examples of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** BSD License Usage ** Alternatively, you may use this file under the terms of the BSD license ** as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of The Qt Company Ltd nor the names of its ** contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ /* delegate.cpp A delegate that allows the user to change integer values from the model using a spin box widget. */ #include "delegate.h" #include <QSpinBox> #include <QHBoxLayout> //! [0] SpinBoxDelegate::SpinBoxDelegate(QObject *parent) : QStyledItemDelegate(parent) { } //! [0] //! [1] QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &/* option */, const QModelIndex &/* index */) const { /*QSpinBox *editor = new QSpinBox(parent); editor->setFrame(false); editor->setMinimum(0); editor->setMaximum(100); return editor;*/ auto *widget = new QWidget(parent); widget->setAutoFillBackground(true); auto *slider = new QSlider(widget); slider->setOrientation(Qt::Orientation::Horizontal); slider->setMinimumHeight(20); slider->setMinimumWidth(100); slider->setMinimum(0); slider->setMaximum(100); auto *spinbox = new QSpinBox(widget); spinbox->setMinimumWidth(20); spinbox->setFrame(false); spinbox->setMinimum(0); spinbox->setMaximum(100); connect(slider, SIGNAL(valueChanged(int)), spinbox, SLOT(setValue(int))); connect(spinbox, SIGNAL(valueChanged(int)), slider, SLOT(setValue(int))); auto *layout = new QHBoxLayout(); layout->addWidget(slider); layout->addWidget(spinbox); layout->setMargin(0); widget->setLayout(layout); return widget; } //! [1] //! [2] void SpinBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { int value = index.model()->data(index, Qt::EditRole).toInt(); auto *spinBox = static_cast<QSpinBox *>(editor->layout()->itemAt(1)->widget()); //QSpinBox *spinBox = static_cast<QSpinBox*>(editor); spinBox->setValue(value); } //! [2] //! [3] void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { auto *spinBox = static_cast<QSpinBox *>(editor->layout()->itemAt(1)->widget()); //QSpinBox *spinBox = static_cast<QSpinBox*>(editor); spinBox->interpretText(); int value = spinBox->value(); model->setData(index, value, Qt::EditRole); } //! [3] //! [4] void SpinBoxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/* index */) const { editor->setGeometry(option.rect); } //! [4]
-
Solved #2 with a quick focus event check. Here is the custom widget code and the changes to the delegate:
MyWidget.h
#pragma once #include <QWidget> class MyWidget : public QWidget { Q_OBJECT public: MyWidget(QWidget *parent = nullptr); protected: void focusInEvent(QFocusEvent *event); };
MyWidget.cpp:
#include <QSpinBox> #include "MyWidget.h" #include <QLayout> MyWidget::MyWidget(QWidget *parent) : QWidget(parent) { } void MyWidget::focusInEvent(QFocusEvent *event) { auto *spinbox = static_cast<QSpinBox *>(layout()->itemAt(1)->widget()); Q_ASSERT(spinbox); spinbox->setFocus(); spinbox->selectAll(); QWidget::focusInEvent(event); }
And then in the delegate.cpp just change
new Widget
tonew MyWidget
like so:auto *widget = new MyWidget(parent);
So that just leaves #3 which I don't understand what you're after there..
-
Thank you so much.
This answer reduces the problems to #2 and #3.
The #2 is a "nice to have" kind of issue, but it is definitely important in the long run.For #3 what I mean is that each row of the table may refer to values of different ranges. For example, 0 to 100, 0 to 360, or -1 to 1, etc... So this is basically an general MVC programming question. How to specialize each slider depending on the properties specific to a certain table index? How to insert those properties in the model and how to read them from the delegate so that i can set them to the corresponding QSlider.
Thank you so much for your help.
JordiEDIT:
Well, #2 is fixed now. Thanks! -
@wasawi One way to do this is to call
setData()
on your model with the information you want to get later, i.e. the range. You can put anything in the data for the row and retrieve it withdata()
later on.So you could save a struct/class/QList or a tuple or something with the range you want. Pull that out when you create the editor and set your range. The
createEditor
function passed the QModelIndex so you can easily grab thedata
from that. -
#3 is very easy to solve, in
setEditorData
you already set the value of the widgets so should be trivial to set the range based on the data coming fromindex
You probably also want to reimplement
updateEditorGeometry
so that you can handle resizing while the editor is open -
@ambershark and @VRonin Thank you for your answers.
I didn't know quite well how to extend my model to get other data than the index data itself. Not sure if what i did is the best solution. Please let me know. This is what I did.
added a custom role
enum MyRoles { maxRange = Qt::UserRole };
in model::data() added a case for that role
if (role == MyRoles::maxRange) { return myData.getMax(index.row()); }
added a new funtion in myData class to provide maxRange
QVariant getMax(int row) const { auto max = paramGroup[row].getMax(); return QVariant(max); }
And finally in my delegate i set the max range in both createEditor() and setEditorData()
int maxRange = index.model()->data(index, MyRoles::maxRange).toInt(); slider->setMaximum(maxRange); spinbox->setMaximum(maxRange);
Any improvements are welcome.
Since everything now already works I will mark it as Solved.
Thank you for your patience!
J -
@wasawi More work than was needed but by doing it that way it helps you learn a lot about the nature of data() and UserRole.
You could have just done:
model->setData(index, QStringList() << "10" << "100"); // and then later to pull it out auto range = model->data(index).toStringList(); slider->setMinimum(range.at(0).toInt()); slider->setMaximum(range.at(1).toInt());
Code may not be 100% right since I did it from my head with no auto complete or docs, but it can be that easy for your simple use case. Your way is the correct way if you want more complex user data in your model. It allows you to have multiple UserRole/UserRole+1,+2, etc. It allows data to be easily found based off the enum and returned, etc.
Bottom line, great job. :)