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
QScienceSpinBox.cpp
#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;
}