简体   繁体   中英

Custom QDial notch ticks with PyQt

Currently I have this custom rotated QDial() widget with the dial handle pointing upward at the 0 position instead of the default 180 value position.

To change the tick spacing, setNotchTarget() is used to space the notches but this creates an even distribution of ticks (left). I want to create a custom dial with only three adjustable ticks (right).

在此处输入图片说明 在此处输入图片说明

The center tick will never move and will always be at the north position at 0 . But the other two ticks can be adjustable and should be evenly spaced. So for instance, if the tick was set at 70 , it would place the left/right ticks 35 units from the center. Similarly, if the tick was changed to 120 , it would space the ticks by 60 .

在此处输入图片说明 在此处输入图片说明

How can I do this? If this is not possible using QDial() , what other widget would be capable of doing this? I'm using PyQt4 and 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_())

该图显示了测试代码结果

First of all. Qt's dials are a mess. They are nice widgets, but they've been mostly developed for simple use cases.

If you need "special" behavior, you'll need to override some important methods. This example obviously requires paintEvent overriding, but the most important parts are the mouse events and wheel events. Tracking keyboard events required to set single and page step to the value range, and to "overwrite" the original valueChanged signal to ensure that the emitted value range is always between -1 and 1. You can obviously change those values by adding a dedicated function.
Theoretically, QDial widgets should always use 240|-60 degrees angles, but that might change in the future, so I decided to enable the wrapping to keep degrees as an "internal" value. Keep in mind that you'll probably need to provide your own value() property implementation also.

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 : I updated the code to implement keyboard navigation and avoid unnecessary multiple signal emissions.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM