QScienceSpinBox
Since I started using Qt over a year ago I have been asking and looking for a Qt widget that can handle numbers in a scientific notation. Unfortunately Qt Software does not offer such a solution nor is it trivial to implement. With the hints I got in some public web forums I implementet the solution provided here.
This widget is derived from QDoubleSpinBox. It uses a decimal value of 1000 (that is more decimal points than a double can handle) and implements a new decimal value for the presentation in scientific notation. The Validator is realised by setting the LineEdit to a QDoubleValidator::ScientificNotation
. However the most important part is the reimplementation of textFromValue
and valueFromText
. This unfortunately requires to copy the whole validation code of QDoubleSpinBox, which can not be borrowed and represents the major part of the code.
If someone can show a shrinked but still functional equivalent version that would be great. In the end I think that it would be better if such a solution would be included into a Qt release, especially because in its current form I use so much of their code.
Sample Project
Source Code
QScienceSpinBox.h#ifndef __QScienceSpinBox_H__ #define __QScienceSpinBox_H__ #include <QtGui/QDoubleSpinBox> #include <QtGui/QDoubleValidator> #include <QtGui/QLineEdit> #include <QtCore/QVariant> #include <QtCore/QDebug> #include <QtCore/QString> static bool isIntermediateValueHelper(qint64 num, qint64 minimum, qint64 maximum, qint64 *match = 0); class QScienceSpinBox : public QDoubleSpinBox { Q_OBJECT public: QScienceSpinBox(QWidget * parent = 0); int decimals() const; void setDecimals(int value); QString textFromValue ( double value ) const; double valueFromText ( const QString & text ) const; private: int dispDecimals; QChar delimiter, thousand; QDoubleValidator * v; private: void initLocalValues(QWidget *parent); bool isIntermediateValue(const QString &str) const; QVariant validateAndInterpret(QString &input, int &pos, QValidator::State &state) const; QValidator::State validate(QString &text, int &pos) const; void fixup(QString &input) const; QString stripped(const QString &t, int *pos) const; double round(double value) const; void stepBy(int steps); public slots: void stepDown(); void stepUp(); }; #endif |
#include "QScienceSpinBox.h" #include <limits> //#define QSPINBOX_QSBDEBUG #ifdef QSPINBOX_QSBDEBUG # define QSBDEBUG qDebug #else # define QSBDEBUG if (false) qDebug #endif QScienceSpinBox::QScienceSpinBox(QWidget * parent) : QDoubleSpinBox(parent) { initLocalValues(parent); setDecimals(8); QDoubleSpinBox::setDecimals(1000); // set Range to maximum possible values double doubleMax = std::numeric_limits<double>::max(); setRange(-doubleMax, doubleMax); v = new QDoubleValidator(this); v->setDecimals(1000); // (standard anyway) v->setNotation(QDoubleValidator::ScientificNotation); this->lineEdit()->setValidator(v); } void QScienceSpinBox::initLocalValues(QWidget *parent) { const QString str = (parent ? parent->locale() : QLocale()).toString(4567.1); if (str.size() == 6) { delimiter = str.at(4); thousand = QChar((ushort)0); } else if (str.size() == 7) { thousand = str.at(1); delimiter = str.at(5); } Q_ASSERT(!delimiter.isNull()); } int QScienceSpinBox::decimals() const { return dispDecimals; } void QScienceSpinBox::setDecimals(int value) { dispDecimals = value; } // overwritten virtual function of QAbstractSpinBox void QScienceSpinBox::stepBy(int steps) { if (steps < 0) stepDown(); else stepUp(); } void QScienceSpinBox::stepDown() { QSBDEBUG() << "stepDown()"; setValue(value()/10.0); } void QScienceSpinBox::stepUp() { QSBDEBUG() << "stepUp()"; setValue(value()*10.0); } /*! * text to be displayed in spinbox */ QString QScienceSpinBox::textFromValue(double value) const { // convert to string -> Using exponetial display with internal decimals QString str = locale().toString(value, 'e', dispDecimals); // remove thousand sign if (qAbs(value) >= 1000.0) { str.remove(thousand); } return str; } double QScienceSpinBox::valueFromText(const QString &text) const { QString copy = text; int pos = this->lineEdit()->cursorPosition(); QValidator::State state = QValidator::Acceptable; return validateAndInterpret(copy, pos, state).toDouble(); } // this function is never used...? double QScienceSpinBox::round(double value) const { const QString strDbl = locale().toString(value, 'g', dispDecimals); return locale().toDouble(strDbl); } // overwritten virtual function of QAbstractSpinBox QValidator::State QScienceSpinBox::validate(QString &text, int &pos) const { QValidator::State state; validateAndInterpret(text, pos, state); return state; } // overwritten virtual function of QAbstractSpinBox void QScienceSpinBox::fixup(QString &input) const { input.remove(thousand); } // reimplemented function, copied from QDoubleSpinBoxPrivate::isIntermediateValue bool QScienceSpinBox::isIntermediateValue(const QString &str) const { QSBDEBUG() << "input is" << str << minimum() << maximum(); qint64 dec = 1; for (int i=0; i < decimals(); ++i) dec *= 10; const QLatin1Char dot('.'); /*! * determine minimum possible values on left and right of Decimal-char */ // I know QString::number() uses CLocale so I use dot const QString minstr = QString::number(minimum(), 'f', QDoubleSpinBox::decimals()); qint64 min_left = minstr.left(minstr.indexOf(dot)).toLongLong(); qint64 min_right = minstr.mid(minstr.indexOf(dot) + 1).toLongLong(); const QString maxstr = QString::number(maximum(), 'f', QDoubleSpinBox::decimals()); qint64 max_left = maxstr.left(maxstr.indexOf(dot)).toLongLong(); qint64 max_right = maxstr.mid(maxstr.indexOf(dot) + 1).toLongLong(); /*! * determine left and right long values (left and right of delimiter) */ const int dotindex = str.indexOf(delimiter); const bool negative = maximum() < 0; qint64 left = 0, right = 0; bool doleft = true; bool doright = true; // no separator -> everthing in left if (dotindex == -1) { left = str.toLongLong(); doright = false; } // separator on left or contains '+' else if (dotindex == 0 || (dotindex == 1 && str.at(0) == QLatin1Char('+'))) { // '+' at negative max if (negative) { QSBDEBUG() << __FILE__ << __LINE__ << "returns false"; return false; } doleft = false; right = str.mid(dotindex + 1).toLongLong(); } // contains '-' else if (dotindex == 1 && str.at(0) == QLatin1Char('-')) { // '-' at positiv max if (!negative) { QSBDEBUG() << __FILE__ << __LINE__ << "returns false"; return false; } doleft = false; right = str.mid(dotindex + 1).toLongLong(); } else { left = str.left(dotindex).toLongLong(); if (dotindex == str.size() - 1) { // nothing right of Separator doright = false; } else { right = str.mid(dotindex + 1).toLongLong(); } } // left > 0, with max < 0 and no '-' if ((left >= 0 && max_left < 0 && !str.startsWith(QLatin1Char('-'))) // left > 0, with min > 0 || (left < 0 && min_left >= 0)) { QSBDEBUG("returns false"); return false; } qint64 match = min_left; if (doleft && !isIntermediateValueHelper(left, min_left, max_left, &match)) { QSBDEBUG() << __FILE__ << __LINE__ << "returns false"; return false; } if (doright) { QSBDEBUG("match %lld min_left %lld max_left %lld", match, min_left, max_left); if (!doleft) { if (min_left == max_left) { const bool ret = isIntermediateValueHelper(qAbs(left), negative ? max_right : min_right, negative ? min_right : max_right); QSBDEBUG() << __FILE__ << __LINE__ << "returns" << ret; return ret; } else if (qAbs(max_left - min_left) == 1) { const bool ret = isIntermediateValueHelper(qAbs(left), min_right, negative ? 0 : dec) || isIntermediateValueHelper(qAbs(left), negative ? dec : 0, max_right); QSBDEBUG() << __FILE__ << __LINE__ << "returns" << ret; return ret; } else { const bool ret = isIntermediateValueHelper(qAbs(left), 0, dec); QSBDEBUG() << __FILE__ << __LINE__ << "returns" << ret; return ret; } } if (match != min_left) { min_right = negative ? dec : 0; } if (match != max_left) { max_right = negative ? 0 : dec; } qint64 tmpl = negative ? max_right : min_right; qint64 tmpr = negative ? min_right : max_right; const bool ret = isIntermediateValueHelper(right, tmpl, tmpr); QSBDEBUG() << __FILE__ << __LINE__ << "returns" << ret; return ret; } QSBDEBUG() << __FILE__ << __LINE__ << "returns true"; return true; } /*! \internal Multi purpose function that parses input, sets state to the appropriate state and returns the value it will be interpreted as. */ // reimplemented function, copied from QDoubleSpinBoxPrivate::validateAndInterpret QVariant QScienceSpinBox::validateAndInterpret( QString &input, int &pos, QValidator::State &state) const { /*! return 'cachedText' if * input = cachedText, or input Empty */ static QString cachedText; static QValidator::State cachedState; static QVariant cachedValue; if (cachedText == input && !input.isEmpty()) { state = cachedState; QSBDEBUG() << "cachedText was" << "'" << cachedText << "'" << "state was " << state << " and value was " << cachedValue; return cachedValue; } const double max = maximum(); const double min = minimum(); // removes prefix & suffix QString copy = stripped(input, &pos); QSBDEBUG() << "input" << input << "copy" << copy; int len = copy.size(); double num = min; const bool plus = max >= 0; const bool minus = min <= 0; // Test possible 'Intermediate' reasons switch (len) { case 0: // Length 0 is always 'Intermediate', except for min=max if (max != min) { state = QValidator::Intermediate; } else { state = QValidator::Invalid; } goto end; case 1: // if only char is '+' or '-' if (copy.at(0) == delimiter || (plus && copy.at(0) == QLatin1Char('+')) || (minus && copy.at(0) == QLatin1Char('-'))) { state = QValidator::Intermediate; goto end; } break; case 2: // if only chars are '+' or '-' followed by Comma seperator (delimiter) if (copy.at(1) == delimiter && ((plus && copy.at(0) == QLatin1Char('+')) || (minus && copy.at(0) == QLatin1Char('-')))) { state = QValidator::Intermediate; goto end; } break; default: break; } // end switch // First char must not be thousand-char if (copy.at(0) == thousand) { QSBDEBUG() << __FILE__ << __LINE__<< "state is set to Invalid"; state = QValidator::Invalid; goto end; } // Test possible 'Invalid' reasons else if (len > 1) { const int dec = copy.indexOf(delimiter); // position of delimiter // if decimal separator (delimiter) exists if (dec != -1) { // not two delimiters after one other (meaning something like ',,') if (dec + 1 < copy.size() && copy.at(dec + 1) == delimiter && pos == dec + 1) { copy.remove(dec + 1, 1); // typing a delimiter when you are on the delimiter } // should be treated as typing right arrow // too many decimal points if (copy.size() - dec > QDoubleSpinBox::decimals() + 1) { QSBDEBUG() << __FILE__ << __LINE__<< "state is set to Invalid"; state = QValidator::Invalid; goto end; } // after decimal separator no thousand char for (int i=dec + 1; i<copy.size(); ++i) { if (copy.at(i).isSpace() || copy.at(i) == thousand) { QSBDEBUG() << __FILE__ << __LINE__<< "state is set to Invalid"; state = QValidator::Invalid; goto end; } } // if no decimal separator exists } else { const QChar &last = copy.at(len - 1); const QChar &secondLast = copy.at(len - 2); // group of two thousand or space chars is invalid if ((last == thousand || last.isSpace()) && (secondLast == thousand || secondLast.isSpace())) { state = QValidator::Invalid; QSBDEBUG() << __FILE__ << __LINE__<< "state is set to Invalid"; goto end; } // two space chars is invalid else if (last.isSpace() && (!thousand.isSpace() || secondLast.isSpace())) { state = QValidator::Invalid; QSBDEBUG() << __FILE__ << __LINE__<< "state is set to Invalid"; goto end; } } } // end if (len > 1) // block of remaining test before 'end' mark { bool ok = false; bool notAcceptable = false; // convert 'copy' to double, and check if that was 'ok' QLocale loc(locale()); num = loc.toDouble(copy, &ok); QSBDEBUG() << __FILE__ << __LINE__ << loc << copy << num << ok; // conversion to double did fail if (!ok) { // maybe thousand char was responsable if (thousand.isPrint()) { // if no thousand sign is possible, then // something else is responable -> Invalid if (max < 1000 && min > -1000 && copy.contains(thousand)) { state = QValidator::Invalid; QSBDEBUG() << __FILE__ << __LINE__<< "state is set to Invalid"; goto end; } // two thousand-chars after one other are not valid const int len = copy.size(); for (int i=0; i<len- 1; ++i) { if (copy.at(i) == thousand && copy.at(i + 1) == thousand) { QSBDEBUG() << __FILE__ << __LINE__<< "state is set to Invalid"; state = QValidator::Invalid; goto end; } } // remove thousand-chars const int s = copy.size(); copy.remove(thousand); pos = qMax(0, pos - (s - copy.size())); num = loc.toDouble(copy, &ok); QSBDEBUG() << thousand << num << copy << ok; // if conversion still not valid, then reason unknown -> Invalid if (!ok) { state = QValidator::Invalid; QSBDEBUG() << __FILE__ << __LINE__<< "state is set to Invalid"; goto end; } notAcceptable = true; // -> state = Intermediate } // endif: (thousand.isPrint()) } // no thousand sign, but still invalid for unknown reason if (!ok) { state = QValidator::Invalid; QSBDEBUG() << __FILE__ << __LINE__<< "state is set to Invalid"; } // number valid and within valid range else if (num >= min && num <= max) { if (notAcceptable) { state = QValidator::Intermediate; // conversion to num initially failed } else { state = QValidator::Acceptable; } QSBDEBUG() << __FILE__ << __LINE__<< "state is set to " << (state == QValidator::Intermediate ? "Intermediate" : "Acceptable"); } // when max and min is the same the only non-Invalid input is max (or min) else if (max == min) { state = QValidator::Invalid; QSBDEBUG() << __FILE__ << __LINE__<< "state is set to Invalid"; } else { // value out of valid range (coves only special cases) if ((num >= 0 && num > max) || (num < 0 && num < min)) { state = QValidator::Invalid; QSBDEBUG() << __FILE__ << __LINE__<< "state is set to Invalid"; } else { // invalid range, further test with 'isIntermediateValue' if (isIntermediateValue(copy)) { state = QValidator::Intermediate; } else { state = QValidator::Invalid; } QSBDEBUG() << __FILE__ << __LINE__<< "state is set to " << (state == QValidator::Intermediate ? "Intermediate" : "Acceptable"); } } } end: // if something went wrong, set num to something valid if (state != QValidator::Acceptable) { num = max > 0 ? min : max; } // save (private) cache values cachedText = prefix() + copy + suffix(); cachedState = state; cachedValue = QVariant(num); // return resulting valid num return QVariant(num); } /*! \internal Strips any prefix/suffix from \a text. */ // reimplemented function, copied from QAbstractSpinBoxPrivate::stripped QString QScienceSpinBox::stripped(const QString &t, int *pos) const { QString text = t; QString prefixtext = prefix(); QString suffixtext = suffix(); if (specialValueText().size() == 0 || text != specialValueText()) { int from = 0; int size = text.size(); bool changed = false; if (prefixtext.size() && text.startsWith(prefixtext)) { from += prefixtext.size(); size -= from; changed = true; } if (suffixtext.size() && text.endsWith(suffixtext)) { size -= suffixtext.size(); changed = true; } if (changed) text = text.mid(from, size); } const int s = text.size(); text = text.trimmed(); if (pos) (*pos) -= (s - text.size()); return text; } // reimplemented function, copied from qspinbox.cpp static bool isIntermediateValueHelper(qint64 num, qint64 min, qint64 max, qint64 *match) { QSBDEBUG("%lld %lld %lld", num, min, max); if (num >= min && num <= max) { if (match) *match = num; QSBDEBUG("returns true 0"); return true; } qint64 tmp = num; int numDigits = 0; int digits[10]; if (tmp == 0) { numDigits = 1; digits[0] = 0; } else { tmp = qAbs(num); for (int i=0; tmp > 0; ++i) { digits[numDigits++] = tmp % 10; tmp /= 10; } } int failures = 0; qint64 number; for (number=max; number>=min; --number) { tmp = qAbs(number); for (int i=0; tmp > 0;) { if (digits[i] == (tmp % 10)) { if (++i == numDigits) { if (match) *match = number; QSBDEBUG("returns true 1"); return true; } } tmp /= 10; } if (failures++ == 500000) { //upper bound if (match) *match = num; QSBDEBUG("returns true 2"); return true; } } QSBDEBUG("returns false"); return false; } |
I created a simpler implementation of a spin box for scientific notation using PySide on my blog: http://jdreaver.com/posts/2014-07-28-scientific-notation-spin-box-pyside.html
Hi all
I know my post that I wrote/write a technical spinbox is ancient. Just in case anyone is still interested: I posted the code on https://www.gitorious.org/techspinbox.
Cheers
Al_
I added all my code to a google code repository.
You can check it out with
svn checkout http://qsciencespinbox.googlecode.com/svn/trunk/ qsciencespinbox
I only updated the pro file, the widget code is them as 3 years ago with all its features and bugs...
Please can you add this to a public code repository e.g. Google Code?
I dont have any improved code at the moment but he problem seems to lie with the fact that entering the value and using the up and down arrows follow different validation paths.
To fully validate I think that the validate() method has to be over-ridden for both the QDoubleSpinBox and the QLineEdit, contained within the spin box.
Honestly, I have not been using this code for a longer time. So if you find that something is wrong (bug) or can be improved feel free to do so. I would be very glad to get the improved code back though. If more people are interested one could also think of putting this into a public repository…
Matthias,
Really good work, however, I have implemented this and the validation is not applied when the up and down arrow of the spin box is clicked…
Is this expected behaviour?
Jonathan
Al, do you have a website or a download of your ‘QTechSpinBox’? Google only finds my website.
After trying a few options, I inherited QTechSpinBox from QAbstractSpinBox. I have now an alpha version of QTechSpinBox ready, together with a very small sample project to demonstrate its use. I will mail you source and exe for WinXP separately. I would very much appreciate getting your feedback. And below the description as I entered it as comment into the source code.
Regarsd, A_
Description of QTechSpinBox:
QTechSpinBox allows user to set double values in exponential notation
If no exponential notation is needed, QDoubleSpoinBox from Qt can be used;
if scientific notation, but neither units nor technical notation is needed,
QScienceSpinBox from Matthias Pospiech (http://www.matthiaspospiech.de).
QTechSpinBox allows the user to choose a value by clicking the up/down
buttons or pressing up/down on the keyboard to increase/decrease
the value currently displayed. The user can also type the value in
manually.
The displayed value can be appended with arbitrary strings indicating
the unit. If three digits are allowed in front of the decimal sign, the
unit will be displayed with unit modifiers (milli, kilo, micro, Mega and so on).
The value retrieved by value() factors the unit modifier in. If a textual
representation is needed, the value can be formatted using textFromValue().
When entering data manually, all entries acceptable as output (i.e., number of
pre- and postdigits, unit) are allowed. In addition, exponents and unit modifier
(with unit, unless empty) may be combined. E.g., if unit is m (meter) and
‘3e12 nm’ or ‘3e12 n’ is entered, it will be formatted to ‘3 km’ (if
predigits == 3 and modifierunits == true) or ‘3e3 m’ (otherwise).
Ambiguity resolution: units and unit modifier might be ambiguous (e.g., ‘P’
could be first letter of Pa (pascal, unit for pressure) or unit modifier for
Penta (10^15). In such cases it is interpreted as pascal. This only applies if
the baseunit starts with one of the letters used as unit prefix. In these cases,
at least the first letter of the base unit needs to be included (e.g., PP for
pentapascal, full unit would be PPa).
Every time the value changes QTechSpinBox emits valueChanged()
signals. If only the textual representation changes, valueChanged(string)
is emitted. The current value can be fetched with value() and set
with setValue().
Clicking the up/down buttons or using the keyboard accelerator’s
up and down arrows will increase or decrease the current value by
factors of size singleStep(). If you want to change this behaviour you
can reimplement the virtual function stepBy(). The minimum and
maximum value and the step size can be changed with setMinimum(),
setMaximum() and setSingleStep().
ok, understood.
Greek letters are relevant for QTechSpinBox, too (for micro). Seems to be really straight forward as QString is Unicode based. I just tried it: in a QLineEdit I can type ‘micro’W and whatever greek letters I want. Or do I miss a complication downstream? Bear with me, I am new to Qt (the last 12 years I used Borland).
^2 and ^3 (i.e., superscript 2 and 3) also happen to be regular characters (appear like superscripts as the ‘2’ and ‘3’ are printed tiny and well above the baseline of the letters). ‘micro’W^2 and ‘micro’m/s^2 look nice on my screen (using Win XP Pro). I do not have a suggestion for ^4 and above, nor for negative superscripts.
by fractions I meant that units such as m/s (meters per second) or W/s/m^2 are looking not nice. Also it is not possible (or at least I do not know how) to display greek letters.
Ok, so this leaves a small bit for me to contribute, on top of the major work done by you. 🙂
What do you mean by ‘display fractions’?
While trying to implement the ‘technical notation’ feature as QTechSpinBox : public QScienceSpinBox, I also plan to adapt the behaviour of stepDown() and stepUp(), unless you have soemthing like this already in planning for QScienceSpinBox (please let me know). I like to honor QDoubleSpinBox::singleStep by using this value as multiplication factor (instead of hard-coded 10). Also, if value==0, I like stepUp() to set value=1 (as multiplication of zero does nothing, an unexpected behaviour for end-users).
Using unit modifiers such as kilo or nano (if a base unit is given) is a nice idea. I will have a look into.
I have played with such code, but never got it finished. I was thinking about a widget that would automatically rescale to micro, milli and such. I wanted it to have a unit, such that it would display mJ, µJ, pJ or ps, fs (femto seconds) and so forth. the biggest problem about units is, that there is not nice way to display fractions inside a Spinbox.
Thanks for providing the scientific spinbox to the community. I wanted to implement something very similar, then read the discussion on the Qt forum and realized that is much more complex than I thought. I will need a small adaptation of your code; prior to inheriting my own class from QScienceSpinBox, I want to confirm that you do not have this already coded: technical notation instead of scientific. I.e., the exponent is only 0, 3, 6 , -3 and so on (matching kilo, Mega, Giga, and for my use more likely milli, micro, nano, pico).
Well, in any case you deal with number such as 147.05e-235, you will be happy, that it is not converted to 0.0 (which it would be with a normal QDoubleSpinBox).
I will add a note for licencing it under GPL.
Hiya; I tried the app and I love how you show the customizability of the spinbox. I’m trying to find out what is a good useage (or usecase) for this widget. Can you tell me when its a good widget?
Also, would be nice if you add that this is all licensed under the GPL in your zip; to make it clear for everyone downloading it. Just a README or LICENCE file or somesuch.