QDial, like any other QAbstractSlider descendant, only supports integer values.
There are at least three solutions to achieve floating numbers support, depending on what you need.
The idea is that you use the integer based value of the dial as an index to get the actual floating value, by using mathematical operations or index based functions.
Fixed value list
Create a list of all possible values, which can also use uneven "gaps" between them (eg. 0.02, 0.08, 0.16, 2.0
), set the maximum value at len(valueList) - 1
, then use the value of the valueChanged
signal argument as an index for that list.
Pros: very easy implementation for "one-way" dials (the dial can only update the value, not the other way around);
Cons: issues setting the "value" of the dial if the provided value is not in the list (see code below); "tick" positions don't take into account the actual values and can be visually inconsistent whenever "uneven" steps are used: if values are 0.0, 0.01, 0.02, 0.1, 0.2
, the middle position will be 0.02
, not the more intuitive 0.1
;
Step value based
You have to set a step that will be used as a range between the minimum and the maximum (such as 0.02
), resulting in values being, for example, 0.02, 0.04, 0.06, [...], 0.18, 0.2
.
In the example at the bottom I assume that the step allows the dial to exactly reach the maximum; if it doesn't, you'll have to add another step at the end (by increasing the QDial.maximum()
by one).
The actual floating value is computed by multiplying the current integer value by the step, and adding the result to the minimum float value.
This approach is similar to this answer, which correctly implements all methods, but has the limit of a fixed step based on decimal units if used on QDials (meaning you can't have 0.02, 0.04, 0.06
, but only 0.02, 0.03, 0.04, 0.05, etc
), meaning that its implementation should be fixed accordingly.
NB: while values converted to strings are usually rounded, remember that floating point values have limitations, meaning that, in this specific example, 0.2 is actually (about) 0.20000000000000004.
pros: the step allows simplified data entries, reducing the data range;
cons: no matter how many digits the step has, the actual value will probably be "imprecise" due to floating limitations; the range (maximum - minimum) has to be a multiple of the step value;
Step count (precision) based
In this case you set how many steps you'll need between minumum and maximum values, allowing you to specify the precision of its values. The resulting value is taken from the ratio between the current dial value and the dial maximum (the number of steps), multiplied by the range between the minimum and the maximum, and added to the minimum like before.
pros: higher precision;
conts: getting values with zero or very few decimals is very hard, if not impossible;
SetValue()
issues
If you need to set the value of the dial, you'll always encounter some kind of problem, mostly due to the aforementioned floating limitations, but not only.
Also, different strategies have to be applied according to the different implementations, with the most hard being the "fixed value list" case.
The problem is that the actual QDial index is integer based, meaning that if you want to set a "value" of 0.9, it could be interpreted as 0 or 1, according to the implementation.
In the following example I'm also using a QDoubleSpinBox to show the current value, and that is able to set back the dial value when the spinbox value is changed by the user; while the step based dials work almost without flaws, the fixed list case clearly shows that limitation: if you try to slowly move the mouse wheel on the first spin box, you'll see that the dial is not always updated properly, expecially when setting a value that is less than the current one.
from bisect import bisect_left
from PyQt4 import QtCore, QtGui
class FixedValuesDial(QtGui.QDial):
floatValueChanged = QtCore.pyqtSignal(float)
def __init__(self, valueList):
super(FixedValuesDial, self).__init__()
self.valueList = valueList
self.setMaximum(len(valueList) - 1)
self.valueChanged.connect(self.computeFloat)
self.currentFloatValue = self.value()
def computeFloat(self, value):
self.currentFloatValue = self.valueList[value]
self.floatValueChanged.emit(self.currentFloatValue)
def setFloatValue(self, value):
try:
# set the index, assuming the value is in the valueList
self.setValue(self.valueList.index(value))
except:
# find the most closest index, based on the value
index = bisect_left(self.valueList, value)
if 0 < index < len(self.valueList):
before = self.valueList[index - 1]
after = self.valueList[index]
# bisect_left returns the position where the value would be
# added, assuming valueList is sorted; the resulting index is the
# one preceding the one with a value greater or equal to the
# provided value, so if the difference between the next value and
# the current is greater than the difference between the previous
# and the current, the index is closest to the previous
if after - value > value - before:
index -= 1
# note that the value -the new index- is actually updated only *if*
# the new index is different from the current, otherwise there will
# no valueChanged signal emission
self.setValue(index)
class StepDial(QtGui.QDial):
floatValueChanged = QtCore.pyqtSignal(float)
def __init__(self, minimum, maximum, step):
super(StepDial, self).__init__()
self.scaleValues = []
self.minimumFloat = minimum
self.step = step
self.setMaximum((maximum - minimum) // step)
self.valueChanged.connect(self.computeFloat)
def computeFloat(self, value):
self.floatValueChanged.emit(value * self.step + self.minimumFloat)
def setFloatValue(self, value):
# compute the index by subtracting minimum from the value, divided by the
# step value, then round provide a *rounded* value, otherwise values like
# 0.9999[...] will be interpreted as 0
index = (value - self.minimumFloat) / self.step
self.setValue(int(round(index)))
class FloatDial(QtGui.QDial):
floatValueChanged = QtCore.pyqtSignal(float)
def __init__(self, minimum, maximum, stepCount=1001):
super(FloatDial, self).__init__()
self.minimumFloat = minimum
self.maximumFloat = maximum
self.floatRange = maximum - minimum
# since QDial (as all QAbstractSlider subclasses), includes its maximum()
# in its value range; to get the expected step count, we subtract 1 from
# it: maximum = minimum + (stepCount - 1)
# also, since the default minimum() == 0, the maximum is stepCount - 1.
# Note that QDial is usually symmetrical, using 240 degrees
# counterclockwise (in cartesian coordinates, as in 3-o'clock) for the
# minimum, and -60 degrees for its maximum; with this implementation we
# assume all of that, and get a correct "middle" value, but this requires
# an *odd* stepCount number so that the odd "middle" index value is
# actually what it should be.
self.stepCount = stepCount
self.setMaximum(stepCount - 1)
self.valueChanged.connect(self.computeFloat)
def computeFloat(self, value):
ratio = float(value) / self.maximum()
self.floatValueChanged.emit(self.floatRange * ratio + self.minimumFloat)
def setFloatValue(self, value):
# compute the "step", based on the stepCount then use the same concept
# as in the StepDial.setFloatValue function
step = (self.maximumFloat - self.minimumFloat) / self.stepCount
index = (value - self.minimumFloat) // step
self.setValue(int(round(index)))
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
layout = QtGui.QGridLayout()
self.setLayout(layout)
layout.addWidget(QtGui.QLabel('List based'), 0, 0)
self.listDial = FixedValuesDial([0.02, 0.03, 0.05, 0.08, 0.1, 0.15, 0.2])
layout.addWidget(self.listDial, 1, 0)
self.listDial.floatValueChanged.connect(self.updateListValue)
self.listSpin = QtGui.QDoubleSpinBox(
minimum=0.02, maximum=0.2, singleStep=0.01)
layout.addWidget(self.listSpin, 2, 0)
self.listSpin.valueChanged.connect(self.listDial.setFloatValue)
layout.addWidget(QtGui.QLabel('Step precision (0.02)'), 0, 1)
self.stepDial = StepDial(0.02, 0.2, 0.02)
layout.addWidget(self.stepDial, 1, 1)
self.stepDial.floatValueChanged.connect(self.updateStepDisplay)
self.stepSpin = QtGui.QDoubleSpinBox(
minimum=0.02, maximum=0.2, singleStep=0.02)
layout.addWidget(self.stepSpin, 2, 1)
self.stepSpin.valueChanged.connect(self.stepDial.setFloatValue)
layout.addWidget(QtGui.QLabel('Step count (21 steps)'), 0, 2)
# see the FloatDial implementation above to understand the reason of odd
# numbered steps
self.floatDial = FloatDial(0.02, 0.2, 21)
layout.addWidget(self.floatDial, 1, 2)
self.floatDial.floatValueChanged.connect(self.updateFloatValue)
self.floatSpin = QtGui.QDoubleSpinBox(
minimum=0.02, maximum=0.2, decimals=5, singleStep=0.001)
layout.addWidget(self.floatSpin, 2, 2)
self.floatSpin.valueChanged.connect(self.floatDial.setFloatValue)
def updateStepDisplay(self, value):
# prevent the spinbox sending valuechanged while updating its value,
# otherwise you might face an infinite recursion caused by the spinbox
# trying to update the dial, which will correct the value and possibly
# send the floatValueChanged back again to it; obviously, this applies
# to the following slots
self.stepSpin.blockSignals(True)
self.stepSpin.setValue(value)
self.stepSpin.blockSignals(False)
def updateFloatValue(self, value):
self.floatSpin.blockSignals(True)
self.floatSpin.setValue(value)
self.floatSpin.blockSignals(False)
def updateListValue(self, value):
self.listSpin.blockSignals(True)
self.listSpin.setValue(value)
self.listSpin.blockSignals(False)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())