2 votes

Des tics d'encoche QDial personnalisés avec PyQt

Actuellement, j'ai une rotation personnalisée QDial() widget avec la poignée du cadran pointant vers le haut à la 0 au lieu de la position par défaut 180 position de valeur.

Pour modifier l'espacement des coches, setNotchTarget() est utilisé pour espacer les encoches mais cela crée une distribution uniforme des encoches (à gauche). Je veux créer un cadran personnalisé avec seulement trois tiques réglables (à droite).

enter image description here enter image description here

La coche centrale ne bougera jamais et sera toujours à la position nord à 0 . Mais les deux autres ticks peuvent être réglés et doivent être régulièrement espacés. Ainsi, par exemple, si le tic-tac a été réglé sur 70 il placerait les ticks gauche/droite. 35 unités du centre. De même, si le tick est changé en 120 il espacerait les ticks de 60 .

enter image description here enter image description here

Comment puis-je faire cela ? Si cela n'est pas possible en utilisant QDial() Quel autre widget serait capable de faire cela ? J'utilise PyQt4 et Python 3.7.

import sys
from PyQt4 import QtGui, QtCore

class Dial(QtGui.QWidget):
    def __init__(self, rotation=0, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.dial = QtGui.QDial()
        self.dial.setMinimumHeight(160)
        self.dial.setNotchesVisible(True)
        # self.dial.setNotchTarget(90)
        self.dial.setMaximum(360)
        self.dial.setWrapping(True)

        self.label = QtGui.QLabel('0')
        self.dial.valueChanged.connect(self.label.setNum)

        self.view = QtGui.QGraphicsView(self)
        self.scene = QtGui.QGraphicsScene(self)
        self.view.setScene(self.scene)
        self.graphics_item = self.scene.addWidget(self.dial)
        self.graphics_item.rotate(rotation)

        # Make the QGraphicsView invisible
        self.view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.view.setFixedHeight(self.dial.height())
        self.view.setFixedWidth(self.dial.width())
        self.view.setStyleSheet("border: 0px")

        self.layout = QtGui.QVBoxLayout()
        self.layout.addWidget(self.view)
        self.layout.addWidget(self.label)
        self.setLayout(self.layout)

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    dialexample = Dial(rotation=180)
    dialexample.show()
    sys.exit(app.exec_())

6voto

musicamante Points 477

Image showing test code results

Tout d'abord. Les cadrans de Qt sont un gâchis. Ce sont de jolis widgets, mais ils ont été développés pour des cas d'utilisation simples.

Si vous avez besoin d'un comportement "spécial", vous devrez remplacer certaines méthodes importantes. Dans cet exemple, il est évident que paintEvent mais les parties les plus importantes sont les événements de la souris et de la roue. Le suivi des événements clavier nécessaires pour définir le pas simple et le pas de page dans la plage de valeurs, et pour "écraser" la valeur d'origine de l'élément valueChanged pour s'assurer que la valeur émise est toujours comprise entre -1 et 1. Vous pouvez évidemment modifier ces valeurs en ajoutant une fonction dédiée.
En théorie, les widgets QDial devrait utilise toujours des angles de 240|-60 degrés, mais cela pourrait changer dans le futur, j'ai donc décidé d'activer le wrapping pour garder les degrés comme valeur "interne". Gardez à l'esprit que vous devrez probablement fournir vos propres valeurs de value() est également mise en œuvre.

from PyQt4 import QtCore, QtGui
from math import sin, cos, atan2, degrees, radians
import sys

class Dial(QtGui.QDial):
    MinValue, MidValue, MaxValue = -1, 0, 1
    __valueChanged = QtCore.pyqtSignal(int)

    def __init__(self, valueRange=120):
        QtGui.QDial.__init__(self)
        self.setWrapping(True)
        self.setRange(0, 359)
        self.valueChanged.connect(self.emitSanitizedValue)
        self.valueChanged = self.__valueChanged
        self.valueRange = valueRange
        self.__midValue = valueRange / 2
        self.setPageStep(valueRange)
        self.setSingleStep(valueRange)
        QtGui.QDial.setValue(self, 180)
        self.oldValue = None
        # uncomment this if you want to emit the changed value only when releasing the slider
        # self.setTracking(False)
        self.notchSize = 5
        self.notchPen = QtGui.QPen(QtCore.Qt.black, 2)
        self.actionTriggered.connect(self.checkAction)

    def emitSanitizedValue(self, value):
        if value < 180:
            self.valueChanged.emit(self.MinValue)
        elif value > 180:
            self.valueChanged.emit(self.MaxValue)
        else:
            self.valueChanged.emit(self.MidValue)

    def checkAction(self, action):
        value = self.sliderPosition()
        if action in (self.SliderSingleStepAdd, self.SliderPageStepAdd) and value < 180:
            value = 180 + self.valueRange
        elif action in (self.SliderSingleStepSub, self.SliderPageStepSub) and value > 180:
            value = 180 - self.valueRange
        elif value < 180:
            value = 180 - self.valueRange
        elif value > 180:
            value = 180 + self.valueRange
        else:
            value = 180
        self.setSliderPosition(value)

    def valueFromPosition(self, pos):
        y = self.height() / 2. - pos.y()
        x = pos.x() - self.width() / 2.
        angle = degrees(atan2(y, x))
        if angle > 90 + self.__midValue or angle < -90:
            value = self.MinValue
            final = 180 - self.valueRange
        elif angle >= 90 - self.__midValue:
            value = self.MidValue
            final = 180
        else:
            value = self.MaxValue
            final = 180 + self.valueRange
        self.blockSignals(True)
        QtGui.QDial.setValue(self, final)
        self.blockSignals(False)
        return value

    def value(self):
        rawValue = QtGui.QDial.value(self)
        if rawValue < 180:
            return self.MinValue
        elif rawValue > 180:
            return self.MaxValue
        return self.MidValue

    def setValue(self, value):
        if value < 0:
            QtGui.QDial.setValue(self, 180 - self.valueRange)
        elif value > 0:
            QtGui.QDial.setValue(self, 180 + self.valueRange)
        else:
            QtGui.QDial.setValue(self, 180)

    def mousePressEvent(self, event):
        self.oldValue = self.value()
        value = self.valueFromPosition(event.pos())
        if self.hasTracking() and self.oldValue != value:
            self.oldValue = value
            self.valueChanged.emit(value)

    def mouseMoveEvent(self, event):
        value = self.valueFromPosition(event.pos())
        if self.hasTracking() and self.oldValue != value:
            self.oldValue = value
            self.valueChanged.emit(value)

    def mouseReleaseEvent(self, event):
        value = self.valueFromPosition(event.pos())
        if self.oldValue != value:
            self.valueChanged.emit(value)

    def wheelEvent(self, event):
        delta = event.delta()
        oldValue = QtGui.QDial.value(self)
        if oldValue < 180:
            if delta < 0:
                outValue = self.MinValue
                value = 180 - self.valueRange
            else:
                outValue = self.MidValue
                value = 180
        elif oldValue == 180:
            if delta < 0:
                outValue = self.MinValue
                value = 180 - self.valueRange
            else:
                outValue = self.MaxValue
                value = 180 + self.valueRange
        else:
            if delta < 0:
                outValue = self.MidValue
                value = 180
            else:
                outValue = self.MaxValue
                value = 180 + self.valueRange
        self.blockSignals(True)
        QtGui.QDial.setValue(self, value)
        self.blockSignals(False)
        if oldValue != value:
            self.valueChanged.emit(outValue)

    def paintEvent(self, event):
        QtGui.QDial.paintEvent(self, event)
        qp = QtGui.QPainter(self)
        qp.setRenderHints(qp.Antialiasing)
        qp.translate(.5, .5)
        rad = radians(self.valueRange)
        qp.setPen(self.notchPen)
        c = -cos(rad)
        s = sin(rad)
        # use minimal size to ensure that the circle used for notches
        # is always adapted to the actual dial size if the widget has
        # width/height ratio very different from 1.0
        maxSize = min(self.width() / 2, self.height() / 2)
        minSize = maxSize - self.notchSize
        center = self.rect().center()
        qp.drawLine(center.x(), center.y() -minSize, center.x(), center.y() - maxSize)
        qp.drawLine(center.x() + s * minSize, center.y() + c * minSize, center.x() + s * maxSize, center.y() + c * maxSize)
        qp.drawLine(center.x() - s * minSize, center.y() + c * minSize, center.x() - s * maxSize, center.y() + c * maxSize)

class Test(QtGui.QWidget):
    def __init__(self, *sizes):
        QtGui.QWidget.__init__(self)
        layout = QtGui.QGridLayout()
        self.setLayout(layout)
        if not sizes:
            sizes = 70, 90, 120
        self.dials = []
        for col, size in enumerate(sizes):
            label = QtGui.QLabel(str(size))
            label.setAlignment(QtCore.Qt.AlignCenter)
            dial = Dial(size)
            self.dials.append(dial)
            dial.valueChanged.connect(lambda value, dial=col: self.dialChanged(dial, value))
            layout.addWidget(label, 0, col)
            layout.addWidget(dial, 1, col)

    def dialChanged(self, dial, value):
        print('dial {} changed to {}'.format(dial, value))

    def setDialValue(self, dial, value):
        self.dials[dial].setValue(value)

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    dialexample = Test(70, 90, 120)
    # Change values here
    dialexample.setDialValue(1, 1)
    dialexample.show()
    sys.exit(app.exec_())

EDIT : J'ai mis à jour le code pour implémenter la navigation au clavier et éviter les émissions de signaux multiples inutiles.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X