[SOLVED] How to do a custom range for a Spinbox
-
Is it possible to create a custom range for a spinbox? Additionally, that range could be changed at run time. The range would have a initial value (design time) of -20 to 25. Those values are for a spinner that determines zoom ratio for a loaded image. The reason for the custom range is that I cannot have zero in the range of values - either the initial range or any calculated range thereafter. If re-implementing the class, how then would you keep the spinner from selecting zero in the range from minimum to maximum? It would have to, for instance go from -1 to 1 or vice-versa.
If created a custom spinner class and in designer promoted the spinner to use it, using the validate method doesn't help in "skipping" zero when clicking up/down within the range unless I'm missing something.
Thanks in advance
-
Hi astodolski,
You can simply reimplement the setValue slot, to have to obtain the correct behavior.
You could try something like this (not tested)
@
void MySpinBox::setValue(int newValue)
{
if (newValue == 0)
newValue = (value() == 1) ? -1 : 1;QSpinBox::setValue(newValue);
}
@You would better reuse the existing range/value than creating your own, to leverage existing logic. And this may be one way of achieving this.
Here we simply transform the target value if it was zero, but use the base class implementation to handle range/value/etc.
Regards
-
[quote author="Adrien Leravat" date="1377088782"]Hi astodolski,
You can simply reimplement the setValue slot, to have to obtain the correct behavior.
You could try something like this (not tested)
@
void MySpinBox::setValue(int newValue)
{
if (newValue == 0)
newValue = (value() == 1) ? -1 : 1;QSpinBox::setValue(newValue);
}
@You would better reuse the existing range/value than creating your own, to leverage existing logic. And this may be one way of achieving this.
Here we simply transform the target value if it was zero, but use the base class implementation to handle range/value/etc.
Regards[/quote]
Thanks for the post. Trying to see why the slot never gets a hit:
Header
@
#ifndef SCALESPINBOX_H
#define SCALESPINBOX_H#include <QSpinBox>
#include <QKeyEvent>
#include <QRegExpValidator>class QRegExpValidator;
class ScaleSpinBox : public QSpinBox
{
Q_OBJECT
public:
explicit ScaleSpinBox(QWidget *parent = 0);signals:
public slots:
virtual void setValue(int val);protected:
virtual QValidator::State validate(QString &input, int &pos) const;
int valueFromText(const QString &text) const;
QString textFromValue(int value) const;
virtual void fixup(QString &str) const;private:
QRegExpValidator *validator;};
#endif // SCALESPINBOX_H
@
Class code
@
#include "scalespinbox.h"ScaleSpinBox::ScaleSpinBox(QWidget *parent) : QSpinBox(parent)
{
validator = new QRegExpValidator(QRegExp("[-1-3]"), this);
}void ScaleSpinBox::setValue(int val)
{
if(val == 0)
val = (value() == 1)? -1 : 1;QSpinBox::setValue(val);
}
QValidator::State ScaleSpinBox::validate(QString &input, int &pos) const
{
return validator->validate(input, pos);
}int ScaleSpinBox::valueFromText(const QString &text) const
{
bool ok;
return text.toInt(&ok, 10);
}QString ScaleSpinBox::textFromValue(int value) const
{
return QString::number(value, 10);
}void ScaleSpinBox::fixup(QString &str) const
{
bool ok;
int value = str.toInt(&ok, 10);if(ok) { if (value == 0) { value = -1; str = QString::number(value); } } else { QSpinBox::fixup(str); }
}
@
Perhaps because I use the spinner as-is without calling setValue().
-
Well I would think it is used by all input methods to modifly the value of the spinbox, and so that you have nothing to do but reimplementing it. But I'm not sure it is declared virtual in QSpinBox. May be an idea to check if yes or not.
Actually your code should work too, without the setValue. I was not aware of the "fixup" method role. I'm not sure you need to reimplement textFromValue and valueFromText though. You seem to have kept the initial displaying of values.
Are the validate and fixup correctly called ?
-
Validate and fixup get called "as needed" I need to come up with the proper way to look back at the prior value and perhaps place that in the fixup method. I though that re-implementing setValue was the solution.
-
Ok, I thought it didn't worked.
If "setValue" isn't called, then a little trick is to make ScaleSpinBox some active proxy of the QSpinBox:
- connect the valueChanged signal to some slot in ScaleSpinBox
- In that slot, test the new value against the previous, locally stored in your ScaleSpinBox class, and if necessary, call setValue with your corrected value
- update previous value
- emit a new signal (like scaleValueChanged(int)), with the corrected value
This is not a solution as good as reimplementing the setValue method, as the component will have the "0" value before corrected value is set, but by binding on the scaleValueChanged signal, you will always receive a appropriate value.
-
I'm not sure I understand. Would you mind editing the code I posted as an illustration of your idea?
-
Sure,
@
#ifndef SCALESPINBOX_H
#define SCALESPINBOX_H#include <QSpinBox>
class ScaleSpinBox : public QSpinBox
{
Q_OBJECT
public:
explicit ScaleSpinBox(QWidget *parent = 0);signals:
void scaleValueChanged(int);private slots:
void onValueChanged(int);private:
int m_nPreviousValue;
};#endif // SCALESPINBOX_H
@@
#include "scalespinbox.h"ScaleSpinBox::ScaleSpinBox(QWidget *parent) : QSpinBox(parent)
{
connect(this, SIGNAL(valueChanged(int)), SLOT(onValueChanged(int)));
}void ScaleSpinBox::onValueChanged(int val)
{
if (val != m_nPreviousValue)
{
if(val == 0)
{
val = (m_nPreviousValue == 1) ? -1 : 1;
setValue(val);
}m_nPreviousValue = val; emit scaleValueChanged(val); }
}
@ -
Thanks for that. How does the previous value (m_nPreviousValue) get assigned?
-
Line 14 ;). But it must be initialized to something like -INF in the cosntructor
-
[quote author="Adrien Leravat" date="1377179330"]it must be initialized to something like -INF in the cosntructor[/quote]
I saw the need to do that after my question. However I get a nasty crash.
"HEAP CORRUPTION DETECTED"
This only occured in Qt Creator. VS2010 didn't dsiplay the error - Hmmm
-
When defining it to -INF ? Well any other mecanism to go through it the first time will do else, like a flag
-
[quote author="Adrien Leravat" date="1377183155"]When defining it to -INF ? Well any other mecanism to go through it the first time will do else, like a flag[/quote]
No actually I set it to INT_MIN in the xtor. When I took out the code to do the validate and the actual validator object, there were no issues.
It seems though that by doing an additional signal that my calling method defines a signal/slot mechanism for the slot to be called when the value is changed. I think what is happening is that the slot in the caller gets called twice.
For example:
The caller has in its xtor:
@
connect(ui->sbScaleImage, SIGNAL(valueChanged(int)), this, SLOT(zoomImage(int)));
@The slot zoomImage gets called when the up/down arrows are clicked in the UI. The slot gets called now 2x
-
Yes you have to connect to the new signal (if it wasn't a typo). The original one will be emitted twice because the internal value changes twice. But the signal scaleValueChanged (note that this is a different signal) should be emitted only once if I made no mistake.
Edit: In my code, the m_nPreviousValue variable should be updated BEFORE calling setValue, as the emit will be synchronous. My bad, wrote it too quickly.
@
if (val != m_nPreviousValue)
{
if(val == 0)
{
val = (m_nPreviousValue == 1) ? -1 : 1;
m_nPreviousValue = val;
setValue(val);
}
else
{
m_nPreviousValue = val;
}emit scaleValueChanged(val); }
@
-
It appears that the slot zoomImage(int ctlValue) gets called twice when passing through zero be it up or down. One call of the slot every other time. :( To be clear, this is the slot which is part of the UI. I don't want to confuse things. I DID make the corrections to the code you posted.
EDIT: Changing to the new signal scaleValueChanged(int) doesn't prevent the spinner from selecting zero.
-
Any progress with your issue ?
A step by step debugging in the slot would clearly highly any issue in the logic. Maybe I missed something in your explanation.
-
Hi,
Well the slot in the UI works. That is, the slot that gets connected to the spinbox valueChanged signal in the UI class. However, the issue is that it gets called 2x depending on the signal that is used in the ScaleSpinBox class. If I
use this constructor
@
ScaleSpinBox::ScaleSpinBox(QWidget *parent) : QSpinBox(parent)
{
connect(this, SIGNAL(scaleValueChanged(int)), SLOT(onValueChanged(int)));
m_nPreviousValue = INT_MIN;}
@Then the slot associated with the UI gets called ONCE. However the control is NOT prevented from going from 1 to zero. In fact
@ScaleSpinBox::onValueChanged(int val)@ never gets called.If I were to change to this constructor:
@
ScaleSpinBox::ScaleSpinBox(QWidget *parent) : QSpinBox(parent)
{
connect(this, SIGNAL(valueChanged(int)), SLOT(onValueChanged(int)));
m_nPreviousValue = INT_MIN;
}
@the slot in the UI gets called 2x.
-
Ok so the problem to me is that your UI is connected to the "valueChanged" signal. Actually it should be connected only to "scaleValueChanged". The "valueChanged" is used only by your ScaneSpinBox class, but should not be used elsewhere.
It bowls down to:
- Connecting the "valueChanged" signal to the "onValueChanged" slot of the ScaleSpinBox class
- Connecting your UI to the "scaleValueChanged" signal, the one emitted by the ScaleSpinBox class, and only this one
-
That solved it, thanks much
-
Glad to here ! :)
Don't forget the "solved" tag in addition the title ;)