1

I'm trying to make an Application with PyQt5, Python 3.7.3 using a Raspberry pi4B and a 5 inch touch screen. The thing is that I need to make a QDial, but I want it to make more than one revolution if it goes from min range to max range. For example, if the Qdial has range from 0 to 500, I want it to make 100 points per revolution, so you have to do a full rotation 5 times to go from the min value to the max value.

This is what I've tried: `

from PyQt5.QtWidgets import *
import sys

class Window(QWidget):
    def __init__(self):
        QWidget.__init__(self)
        layout = QGridLayout()
        self.setLayout(layout)
        self.dial = QDial()
        self.dial.setMinimum(0)
        self.dial.setMaximum(100)
        self.dial.setValue(40)
        self.dial.valueChanged.connect(self.sliderMoved)
        self.dial.setWrapping(True)
        self.text=QLabel()
        layout.addWidget(self.dial)
        layout.addWidget(self.text)
        self.isHigher=False

    def sliderMoved(self):
        print("Dial value = %i" % (self.dial.value()))
        self.text.setText(str(self.dial.value()))
        if(self.dial.value()==100 and self.isHigher==False):
            self.higher_range()
            self.isHigher=True
        if(self.dial.value()==100 and self.isHigher==True):
            self.lower_range()
            self.isHigher=False



    def higher_range(self):
        self.dial.setRange(100,200)
        self.dial.setValue(105)

    def lower_range(self):
        self.dial.setRange(0,100)
        self.dial.setValue(95)



        

app = QApplication(sys.argv)
screen = Window()
screen.show()
sys.exit(app.exec_())

`

But this doesn't work, It keeps changing from 95 to 105 and viceversa.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241

2 Answers2

1

QDial is a pretty peculiar control. While it's still supported, it's poorly implemented, and I believe it's by choice: due to its nature, it's really hard to add more features. I had quite an amount of experience with it, and I know it's not an easy element to deal with.

One of its issues is that it represents a monodimensional range but, visually and UI speaking, it is a bidimensional object.

What you're trying to achieve is possible, but consider that an UI element should always display its state in a clear way and have a corresponding proper behavior; that's the only way UI can tell the user the state. Physical dials don't have this issue: you also have a tactile response that tells you when the gear reaches its end.

From my experience I could tell you that you should avoid it as much as possible: it seems a nice and intuitive widget, but in reality it's very difficult to get a proper result that is actually intuitive to the user. There are some instances for which it makes sense to use it (in my case, representation of a physical knob of an electronic musical instrument). I suggest you to do some research on skeumorphism and UX aspects.

That said, this is a possible raw implementation. I've overridden some aspects (most importantly, the valueChanged signal, for naming consistency), but for a proper implementation you should do much more work (and testing).

The trick is to set the range based on the number of "revolutions": if the maximum is 500 and 5 revolutions are chosen, then the dial will have an actual maximum of 100. Then, whenever the value changes, we check whether previous value was below or above the minimum/maximum of the actual range, and change the revolution count accordingly.

Two important notes:

  • since QDial inherits from QAbstractSlider, it has a range(minimum, maximum + 1), and since the division could have some rest, the "last" revolution will have a different range;
  • I didn't implement the wheel event, as that requires further inspection and choosing the appropriate behavior depending on the "previous" value and revolution;
class SpecialDial(QDial):
    _cycleValueChange = pyqtSignal(int)
    def __init__(self, minimum=0, maximum=100, cycleCount=2):
        super().__init__()
        assert cycleCount > 1, 'cycles must be 2 or more'
        self.setWrapping(True)
        self.cycle = 0
        self.cycleCount = cycleCount
        self._minimum = minimum
        self._maximum = maximum
        self._normalMaximum = (maximum - minimum) // cycleCount
        self._lastMaximum = self._normalMaximum + (maximum - minimum) % self._normalMaximum
        self._previousValue = super().value()
        self._valueChanged = self.valueChanged
        self.valueChanged = self._cycleValueChange
        self._valueChanged.connect(self.adjustValueChanged)

        self.setRange(0, self._normalMaximum)

    def value(self):
        return super().value() + self._normalMaximum * self.cycle

    def minimum(self):
        return self._minimum

    def maximum(self):
        return self._maximum()

    def dialMinimum(self):
        return super().minimum()

    def dialMaximum(self):
        return super().maximum()

    def adjustValueChanged(self, value):
        if value < self._previousValue:
            if (value < self.dialMaximum() * .3 and self._previousValue > self.dialMaximum() * .6 and 
                self.cycle + 1 < self.cycleCount):
                    self.cycle += 1
                    if self.cycle == self.cycleCount - 1:
                        self.setMaximum(self._lastMaximum)
        elif (value > self.dialMaximum() * .6 and self._previousValue < self.dialMaximum() * .3 and
            self.cycle > 0):
                self.cycle -= 1
                if self.cycle == 0:
                    self.setMaximum(self._normalMaximum)
        new = self.value()
        if self._previousValue != new:
            self._previousValue = value
            self.valueChanged.emit(self.value())

    def setValue(self, value):
        value = max(self._minimum, min(self._maximum, value))
        if value == self.value():
            return
        block = self.blockSignals(True)
        self.cycle, value = divmod(value, self._normalMaximum)
        if self.dialMaximum() == self._normalMaximum and self.cycle == self.cycleCount - 1:
            self.setMaximum(self._lastMaximum)
        elif self.dialMaximum() == self._lastMaximum and self.cycle < self.cycleCount - 1:
            self.setMaximum(self._normalMaximum)
        super().setValue(value)
        self.blockSignals(block)
        self._previousValue = self.value()
        self.valueChanged.emit(self._previousValue)

    def keyPressEvent(self, event):
        key = event.key()
        if key in (Qt.Key_Right, Qt.Key_Up):
            step = self.singleStep()
        elif key in (Qt.Key_Left, Qt.Key_Down):
            step = -self.singleStep()
        elif key == Qt.Key_PageUp:
            step = self.pageStep()
        elif key == Qt.Key_PageDown:
            step = -self.pageStep()
        elif key in (Qt.Key_Home, Qt.Key_End):
            if key == Qt.Key_Home or self.invertedControls():
                if super().value() > 0:
                    self.cycle = 0
                    block = self.blockSignals(True)
                    super().setValue(0)
                    self.blockSignals(block)
                    self.valueChanged.emit(self.value())
            else:
                if self.cycle != self.cycleCount - 1:
                    self.setMaximum(self._lastMaximum)
                    self.cycle = self.cycleCount - 1
                if super().value() != self._lastMaximum:
                    block = self.blockSignals(True)
                    super().setValue(self._lastMaximum)
                    self.blockSignals(block)
                    self.valueChanged.emit(self.value())
            return
        else:
            super().keyPressEvent(event)
            return
        if self.invertedControls():
            step *= -1

        current = self.value()
        new = max(self._minimum, min(self._maximum, current + step))
        if current != new:
            super().setValue(super().value() + (new - current))


class Window(QWidget):
    def __init__(self):
        QWidget.__init__(self)
        layout = QGridLayout()
        self.setLayout(layout)
        self.dial = SpecialDial()
        self.dial.valueChanged.connect(self.sliderMoved)
        self.text=QLabel()
        layout.addWidget(self.dial)
        layout.addWidget(self.text)

    def sliderMoved(self):
        self.text.setText(str(self.dial.value()))

I strongly suggest you to take your time to:

  • consider is this is really what you want, since, as said, this kind of control can be very tricky from the UX perspective;
  • carefully read the code and understand its logics;
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thank you so much for your answer, this actually helped me a lot. I think I will be able to implement this on the future, but as you said, I ended up using another widget instead of the Qdial. – manoleitor97 May 07 '21 at 11:42
  • Amazing answer musicamante. Kudos – Martin Feb 21 '22 at 13:33
1

I was trying to do the same and I found a solution. It is based on calculating the change of the dial number. If the dial number goes from the maximum to the minimum then it is considered as only increasing one unit.

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout, QDial
from PyQt5.QtCore import Qt


class DialWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.label_number = 0
        self.label = QLabel(str(self.label_number), self)
        self.label.setAlignment(Qt.AlignCenter)

        self.dial = QDial(self)
        self.dial.setNotchesVisible(True)
        self.dial.setWrapping(True)
        self.dial.setMinimum(0)
        self.dial.setMaximum(100)  # 3600 for full revolutions (0-3600 = 0-360 degrees)

        self.dial.valueChanged.connect(self.dial_value_changed)

        layout = QVBoxLayout()
        layout.addWidget(self.label)
        layout.addWidget(self.dial)

        self.dial_value = self.dial.value()
        self.setLayout(layout)

        self.total_revolutions = 0

    def get_label_number(self):
        return self.label_number
    
    def set_label_number(self, number):
        self.label_number = number
        self.label.setText(str(self.label_number))

    def dial_value_changed(self):

        dial_delta = self.dial.value() - self.dial_value
        if dial_delta == 1:
            new_number = self.get_label_number() + 1
            self.set_label_number(new_number)
        elif dial_delta == -1:
            new_number =  self.get_label_number() - 1
            self.set_label_number(new_number)
        elif dial_delta == -100:
            new_number =  self.get_label_number() + 1
            self.set_label_number(new_number)
        elif dial_delta == 99:
            new_number =  self.get_label_number() - 1
            self.set_label_number(new_number)

        self.dial_value = self.dial.value()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = DialWidget()
    window.setWindowTitle('Dial Widget')
    window.show()
    sys.exit(app.exec_())

This dial will need 5 full turns to increase the value from 0 to 500. It has problems with the speed at which you turn, though, and there is no maximum value, but I'm sure you could add it somehow.

bajotupie
  • 165
  • 1
  • 2
  • 11