QSpinBox with Unsigned Int for Hex Input

8

There are a variety of questions written here about QSpinBox's limitation of using an int as its datatype. Often people want to display larger numbers. In my case, I want to be able to show an unsigned 32bit integer in hexadecimal. This means I'd like my range to be [0x0, 0xFFFFFFFF]. The largest a normal QSpinBox can go is 0x7FFFFFFF. Answering my own question here, the solution I came up with is to simply force the int to be treated like an unsigned int, by reimplementing the relevant display and validation functions.

c++
qt
hex
unsigned-integer
qspinbox
asked on Stack Overflow Oct 27, 2014 by (unknown user)

5 Answers

8

The result is pretty simple, and it works well. Sharing here in case anyone else can benefit from this. It has a 32bit mode and a 16bit mode.

Example of HexSpinBox

class HexSpinBox : public QSpinBox
{
public:
    HexSpinBox(bool only16Bits, QWidget *parent = 0) : QSpinBox(parent), m_only16Bits(only16Bits)
    {
        setPrefix("0x");
        setDisplayIntegerBase(16);
        if (only16Bits)
            setRange(0, 0xFFFF);
        else
            setRange(INT_MIN, INT_MAX);
    }
    unsigned int hexValue() const
    {
        return u(value());
    }
    void setHexValue(unsigned int value)
    {
        setValue(i(value));
    }
protected:
    QString textFromValue(int value) const
    {
        return QString::number(u(value), 16).toUpper();
    }
    int valueFromText(const QString &text) const
    {
        return i(text.toUInt(0, 16));
    }
    QValidator::State validate(QString &input, int &pos) const
    {
        QString copy(input);
        if (copy.startsWith("0x"))
            copy.remove(0, 2);
        pos -= copy.size() - copy.trimmed().size();
        copy = copy.trimmed();
        if (copy.isEmpty())
            return QValidator::Intermediate;
        input = QString("0x") + copy.toUpper();
        bool okay;
        unsigned int val = copy.toUInt(&okay, 16);
        if (!okay || (m_only16Bits && val > 0xFFFF))
            return QValidator::Invalid;
        return QValidator::Acceptable;
    }

private:
    bool m_only16Bits;
    inline unsigned int u(int i) const
    {
        return *reinterpret_cast<unsigned int *>(&i);
    }
    inline int i(unsigned int u) const
    {
        return *reinterpret_cast<int *>(&u);
    }

};
answered on Stack Overflow Oct 27, 2014 by (unknown user)
2

If you don't need full 32 bits you can do it very simply like this:

#pragma once

#include <QSpinBox>

class PaddedSpinBox : public QSpinBox
{
public:
    PaddedSpinBox(QWidget *parent = 0) : QSpinBox(parent)
    {
    }
protected:
    QString textFromValue(int value) const override
    {
        // Pad to the width of maximum().
        int width = QString::number(maximum(), displayIntegerBase()).size();
        return QString("%1").arg(value, width, displayIntegerBase(), QChar('0')).toUpper();
    }
};

In the form designer (or whatever) then you just set:

  • prefix: 0x
  • displayIntegerBase: 16
  • maximum: 255 (or whatever)

If you need full 32 bits you will have to employ casting tricks, or maybe just use a line edit.

answered on Stack Overflow Aug 11, 2017 by Timmmm
1

I came up with the same problem but using PyQt so I could not avoid the range checking that Qt was doing in C under the hood.

The workaround was to use a QDoulbeSpinbox and to cast the value to an int in textFromValue.

Here is my code (it also implements a right click menu to change the display base):

from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from future_builtins import *


import re
import sys
from PyQt4.QtCore import (QRegExp, Qt)
from PyQt4.QtGui import (QApplication, QRegExpValidator, QDoubleSpinBox)
from PyQt4.QtCore import pyqtSlot,SIGNAL,SLOT
from PyQt4 import QtCore, QtGui

# Regex adapted from Mark Pilgrim's "Dive Into Python" book
class QHexSpinBox(QDoubleSpinBox):

    def __init__(self, parent=None):
        super(QHexSpinBox, self).__init__(parent)
        self.mode = 'dec'
        self.setContextMenuPolicy(Qt.CustomContextMenu);

        regex = QRegExp("[x0-9A-Fa-f]{1,8}")
        regex.setCaseSensitivity(Qt.CaseInsensitive)
        self.hexvalidator = QRegExpValidator(regex, self)
        regex = QRegExp("[0-9]{1,10}")
        regex.setCaseSensitivity(Qt.CaseInsensitive)
        self.decvalidator = QRegExpValidator(regex, self)
        regex = QRegExp("[b0-1]{1,64}")
        regex.setCaseSensitivity(Qt.CaseInsensitive)
        self.binvalidator = QRegExpValidator(regex, self)
        self.setRange(1, 999999)

        self.connect(self,SIGNAL("customContextMenuRequested(QPoint)"),
                       self,SLOT("contextMenuRequested(QPoint)"))

    @pyqtSlot(QtCore.QPoint)
    def contextMenuRequested(self,point):

        menu = QtGui.QMenu()

        hex = menu.addAction("Hex")
        dec = menu.addAction("Dec")
        bin = menu.addAction("Bin")

        self.connect(hex,SIGNAL("triggered()"),
                     self,SLOT("hex()"))
        self.connect(dec,SIGNAL("triggered()"),
                     self,SLOT("dec()"))
        self.connect(bin,SIGNAL("triggered()"),
                     self,SLOT("bin()"))
        menu.exec_(self.mapToGlobal(point))

    @pyqtSlot()
    def hex(self):
        self.mode = 'hex'
        self.setValue(self.value())

    @pyqtSlot()
    def dec(self):
        self.mode = 'dec'
        self.setValue(self.value())

    @pyqtSlot()
    def bin(self):
        self.mode = 'bin'
        self.setValue(self.value())

    def validate(self, text, pos):
        if self.mode == 'hex':
            return self.hexvalidator.validate(text, pos)
        if self.mode == 'dec':
            return self.decvalidator.validate(text, pos)
        if self.mode == 'bin':
            return self.binvalidator.validate(text, pos)


    def valueFromText(self, text):
        if self.mode == 'hex':
            return int(unicode(text), 16)
        elif self.mode == 'dec':
            return int(unicode(text))
        elif self.mode == 'bin':
            return int(unicode(text), 2)

    def textFromValue(self, value):
        value = int(value)
        if self.mode == 'hex':
            return hex(value)
        elif self.mode == 'dec':
            return str(value)
        elif self.mode =='bin':
            return "0b{0:b}".format(value)
answered on Stack Overflow Jan 21, 2016 by Techniquab
1

I know this is an old answer but came here from google. Here is my solution with pyside 1.2.4 based somewhat off of Techniquab's solution but doesn't have the integer overflow issue:

from PySide import QtCore, QtGui
from numpy import base_repr
from PySide.QtGui import QRegExpValidator

class QBaseSpinBox(QtGui.QAbstractSpinBox):
    valueChanged = QtCore.Signal(int)
    _value = 0
    default_value = 0
    base = 10
    def __init__(self, parent=None):
        self.setRange(None, None)
        QtGui.QAbstractSpinBox.__init__(self, parent)
        self.set_base(self.base)
        self.lineEdit().setValidator(QRegExpValidator(self))
        self.default_value = self.value()

        self.lineEdit().textChanged.connect(self.textChanged)

        self.lineEdit().setContextMenuPolicy(QtCore.Qt.CustomContextMenu);
        self.lineEdit().customContextMenuRequested.connect(self.contextMenuRequested)

    @QtCore.Slot()
    def contextMenuRequested(self, point):
        menu = self.lineEdit().createStandardContextMenu() #QtGui.QMenu()

        actionDefault = menu.addAction("&Set Default Value of %s" % self.textFromValue(self.default_value),
                                       shortcut=QtCore.Qt.CTRL | QtCore.Qt.Key_D) #QtGui.QKeySequence("Ctrl+D")))     
        menu.insertSeparator(actionDefault)

        actionDefault.triggered.connect(self.menuActionDefault_triggered)
        menu.exec_(self.mapToGlobal(point))

    @QtCore.Slot()
    def menuActionDefault_triggered(self):
        self.setValue(self.default_value)

    def value(self):
        return self._value

    def setValue(self, value):
        if self.validate(value) == QtGui.QValidator.Invalid:
            self.setValue(self._value)
            return
        changed = False
        if self._value != value:
            changed = True
        self._value = value

        self.lineEdit().setText(self.textFromValue(value))
        if changed:
            self.valueChanged.emit(self._value)

    @QtCore.Slot()
    def stepBy(self, value):
        self.setValue(self._value + value)
        QtGui.QAbstractSpinBox.stepBy(self, self._value)

    def stepEnabled(self):
        return QtGui.QAbstractSpinBox.StepDownEnabled | QtGui.QAbstractSpinBox.StepUpEnabled

    @QtCore.Slot()
    def textChanged(self, text):
        try:
            self.setValue(int(text, self.base))
        except:
            self.setValue(self._value)

    def setRange(self, _min, _max):
        self.minimum = _min if _min != None else 0
        self.maximum = _max if _max != None else 0xFFFFFFFFFFFFFFFF

    def validate(self, input):
        if not input:
            return QtGui.QValidator.Intermediate
        try:
            try:
                value = int(input, self.base)
            except TypeError:
                value = input
            if not (self.minimum <= input <= self.maximum):
                raise Exception()
        except Exception as ex:
            return QtGui.QValidator.Invalid
        return QtGui.QValidator.Acceptable

    def valueFromText(self, text):
        return int(text, self.base)

    def textFromValue(self, value):
        return base_repr(value, self.base).upper()

    def set_default_value(self, value):
        self.default_value = int(value)
        #self.setValue(self.default_value)
        self.set_base(self.base) # Redo the tooltip

    def set_base(self, base):
        self.base = base
        min = self.textFromValue(self.minimum)
        max = self.textFromValue(self.maximum)
        default = self.textFromValue(self.default_value)
        self.lineEdit().setToolTip("Base %d\nRange: %s-%s\nDefault Value: %s" % (self.base, min, max, default))
answered on Stack Overflow Feb 21, 2017 by David
0

Thanks @ZX2C4 for the answer. I some modified the class HexSpinBox:

  1. you may set prefix.
  2. you may set max range (in case INT_MAX < maxRange < UINT_MAX there are bugs).
  3. you may disable fill fields 0.
  4. width of fields count auto.

hexspinbox.h

#ifndef HEXSPINBOX_H
#define HEXSPINBOX_H

#include <QSpinBox>

class HexSpinBox : public QSpinBox
{
    Q_OBJECT
public:
    HexSpinBox(QWidget *parent = nullptr);
    unsigned int hexValue() const { return u(value()); }
    void setHexValue(unsigned int value) { setValue(i(value)); }
    void setRange(unsigned int max);
    bool fillField() const { return m_fillField; }
    void setFillField(bool fillFieldWidth) { m_fillField = fillFieldWidth; }

protected:
    QString textFromValue(int value) const;
    int valueFromText(const QString &text) const;
    QValidator::State validate(QString &input, int &pos) const;

private:
    unsigned int m_maxRange = UINT_MAX;
    bool m_fillField = true;
    inline unsigned int u(int i) const { return *reinterpret_cast<unsigned int *>(&i); }
    inline int i(unsigned int u) const { return *reinterpret_cast<int *>(&u); }
};

#endif // HEXSPINBOX_H

hexspinbox.cpp

#include "hexspinbox.h"

HexSpinBox::HexSpinBox(QWidget *parent) : QSpinBox(parent), m_maxRange(maximum())
{
    setDisplayIntegerBase(16);
}

void HexSpinBox::setRange(unsigned int max)
{
    m_maxRange = max;
    if (m_maxRange <= INT_MAX) {
        QSpinBox::setRange(0, int(m_maxRange));
    } else {
        QSpinBox::setRange(INT_MIN, INT_MAX);
    }
}

QString HexSpinBox::textFromValue(int value) const
{
    int fillField = 0;
    if (m_fillField) {
        uint m = m_maxRange;
        while (m) {
            m >>= 4;
            ++fillField;
        }
    }
    return QString("%1").arg(u(value), fillField, 16, QLatin1Char('0')).toUpper();
}

int HexSpinBox::valueFromText(const QString &text) const
{
    return i(text.toUInt(nullptr, 16));
}

QValidator::State HexSpinBox::validate(QString &input, int &pos) const
{
    QString copy(input);
    QString pref = prefix();
    if (copy.startsWith(pref))
        copy.remove(pref);
    pos -= copy.size() - copy.trimmed().size();
    copy = copy.trimmed();
    if (copy.isEmpty())
        return QValidator::Intermediate;
    input = pref + copy.toUpper();
    bool okay;
    unsigned int val = copy.toUInt(&okay, 16);
    if (!okay || val > m_maxRange)
        return QValidator::Invalid;
    return QValidator::Acceptable;
}

You can use the class for the range [0x0, 0xFFFFFFFF] :

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow) {
    ui->setupUi(this);
    ui->hexspinbox->setRange(UINT_MAX); // or 0xFF =)
    ui->hexspinbox->setPrefix("0x");
}
answered on Stack Overflow Jul 3, 2019 by Kto To • edited Jul 3, 2019 by Kto To

User contributions licensed under CC BY-SA 3.0