PyQt infinite spinbox

In the past I lacked an infinite spinbox, as any default qt spinbox will require a min and max value (or default to one).

So messing around with the QAbstractSpinbox I managed to create an InfiniteSpinBox, which I finally made bug free today while extending it with more advanced functionality such as scrolling the digit the mouse is on and selecting all when the text receives focus (generally when typing in a spinbox the user will always want to type an entirely new value).

I am going to inherit the QAbstractSpinBox, and because we normally have double and integer spinboxes, I will give the inherited class a default type of float, but allow the user to set the type in the constructor or later on. Also I need a settable step size, would like to set a default value and QAbstractSpinBox can accept a default parent as well.

The init will inherit and parent, will store the step size and type as well as store the default value and set it as text to display. I am calling upon helper functions numberToStr and strToNumber which I will describe below as well.

from PyQt4 import QtGui, QtCore
import sys


class InfiniteSpinBox(QtGui.QAbstractSpinBox):
    def __init__(self, in_parent=None, in_value=0, in_type=float):
        QtGui.QAbstractSpinBox.__init__(self, in_parent)
        self.singlestep = 1
        self.type = in_type
        self.value = self.type(in_value)
        self.setText( numberToStr(in_value) )
        self.basevalue = self.value

The next thing to do is handle typing.
The key press events are forwarded to the contained QLineEdit automatically so we simply call the parents key press event and then parse the text manually using a new function I named updateValue. This simply converts the typed text to a valid number and then sets that number as text again (in the event a user types characters)

    def keyPressEvent(self, in_event):
        QtGui.QAbstractSpinBox.keyPressEvent(self, in_event)
        self.updateValue()
        
    def keyReleaseEvent(self, in_event):
        QtGui.QAbstractSpinBox.keyReleaseEvent(self, in_event)
        self.updateValue()
        
    def updateValue(self):
        value = strToNumber(self.text(), self.type)
        if value is not None:
            self.value = value
            return
        elif self.text() != '':
            self.lineEdit().setText( numberToStr(self.value) )

Another way of editing the value is by scrolling
The parent class calls stepBy automatically so all we need to do is fill in that function to use the step size and increment the value by the number of steps initiated by scrolling.

The parent class also depends on stepEnabled, which should return StepUpEnabled if the value is not the maximum value and which should return StepDownEnabled if the value is not the minimum value. In the case of the infinite spinbox, obviously it should always return both flags because we are never at the min or max value.

    def stepBy(self, in_step):
        self.value += self.singlestep*in_step
        self.setText( numberToStr(self.value, self.type) )

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

Then all that remains is a bunch of setter functions to be consistent with other qt classes.

    def setSingleStep(self, in_step):
        self.singlestep = in_step
        
    def setType(self, in_type):
        self.type = in_type

    def setValue(self, in_value):
        self.value = self.type(in_value)
        self.setText( numberToStr(self.value, self.type) )

    def setText(self, in_text):
        self.lineEdit().setText( str(in_text) )
        self.updateValue()

And at last the numberToStr and strToNumber functions.

numberToStr is reasonably easy, it will convert a number to a string and remove any decimals in the event the type is int or long. Also it will print pretty numbers by clamping the maximum decimals to six.

strToNumber validates the number to be composed of digits and only one point and then typecasts it to the given type, ditching decimals in the event of long or float again.

def numberToStr(in_number, in_type=float):
    out_string = str(in_number)
    out_string = out_string.split('.')
    if in_type in (long, int):
        return out_string[0]
    
    if len(out_string) > 1 and out_string[1]:
        if len(out_string[1]) > 6:
            out_string[1] = out_string[1][0:6]
        return '%s.%s'%(out_string[0],out_string[1])
    
    return '%s.0'%out_string[0]


def strToNumber(in_str, in_type=float):
    segs = str(in_str).split('.')
    if len(segs) in (1,2) and segs[0].isdigit():
        if len(segs) == 1 or not segs[1] or segs[1].isdigit():
            return in_type(in_str)
    return None

Leave a Reply

Your email address will not be published. Required fields are marked *