PyQt中如何自定义QDial刻度线?

3

目前我有一个自定义旋转的 QDial()小部件,它的指针向上指向 0 位置,而不是默认的 180 位置。

为了改变刻度间距,使用setNotchTarget() 来间隔出刻度,但这会创建一个均匀分布的刻度(左图)。 我想要创建一个只有三个可调节刻度的自定义表盘(右图)。

enter image description here enter image description here

中心刻度将永远不会移动,并且始终位于 0 的北方位。但是其他两个刻度可以调整,并且应该平均分配到两侧。例如,如果刻度设置为 70,则会将左/右刻度与中心相差35单位。同样,如果刻度更改为 120,则会使两个刻度之间的间距为60

enter image description here enter image description here

我该如何做到这一点? 如果使用QDial()不可能实现,那么哪种其他小部件可以实现这一点? 我正在使用 PyQt4和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_())
1个回答

6

显示测试代码结果的图片

首先,Qt的刻度盘很混乱。它们是不错的小部件,但它们大多数情况下都是为简单用例而开发的。

如果你需要“特殊”行为,你需要覆盖一些重要方法。这个例子显然需要覆盖paintEvent,但最重要的部分是鼠标事件和滚轮事件。跟踪键盘事件需要将单步和页步设置为值范围,并“覆盖”原始的valueChanged信号,以确保发出的值范围始终在-1和1之间。你可以通过添加专门的函数来改变这些值。
理论上,QDial窗口小部件应始终使用240|-60度角,但这可能会在未来发生变化,因此我决定启用包装以将度数保持为“内部”值。请记住,您可能还需要提供自己的value()属性实现。

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_())

编辑:我更新了代码,实现了键盘导航并避免了不必要的多个信号发射。


谢谢!这正是我正在寻找的。 - nathancy
不用客气。只是有一件事:根据你的问题,你使用的是PyQt4,它不使用QtWidgets(除非你使用了“Qt.py”包装器,但这是你的选择,不应反映在示例中)。我在示例中错误地输入了PyQt5,但问题仍然存在:你是在使用PyQt4还是5? - musicamante
我目前正在使用PyQt4,过渡会容易吗? - nathancy
抱歉有点烦人,但是请在编辑他人答案的代码时更加小心:在检查了您的编辑后,除了Qt4命名问题之外,我还注意到您在每次for循环中都分配了self.dial(使这些引用无用),并在信号中使用它(使槽的工作不一致,因为它总是指向最后创建的QDial)。话虽如此,我又编辑了答案,并且除了命名和一些小修复/拼写错误之外,我还实现了键盘支持和更好的信号处理,这需要比我想象的更多的关注。 :) - musicamante
抱歉!感谢您的帮助! - nathancy
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接