【问题标题】:Custom QDial notch ticks with PyQt使用 PyQt 自定义 QDial 刻度
【发布时间】:2019-11-28 21:02:29
【问题描述】:

目前我有这个自定义旋转的QDial() 小部件,拨号手柄向上指向0 位置,而不是默认的180 值位置。

要更改刻度间距,setNotchTarget() 用于分隔凹槽,但这会创建均匀分布的刻度(左)。我想创建一个只有 三个 可调节刻度的自定义表盘(右)。

中心刻度永远不会移动,并且始终位于0 的北方位置。但其他两个刻度可以调整,并且应该均匀分布。例如,如果刻度设置为70,它会将左/右刻度从中心放置35 个单位。同样,如果刻度更改为120,它会将刻度间隔60

我该怎么做?如果使用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_())

【问题讨论】:

    标签: python pyqt pyqt4 qgraphicsview qdial


    【解决方案1】:

    首先。 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_())
    

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

    【讨论】:

    • 谢谢!这正是我想要的
    • 不客气。只是一件事:根据您在 PyQt4 下的问题,它不使用 QtWidgets(除非您使用 Qt.py 包装器,但这是您的选择,不应反映在示例中)。我在示例中错误地输入了 PyQt5,但问题仍然存在:您使用的是 PyQt4 还是 5?
    • 我目前正在使用 PyQt4,过渡会容易吗?
    • 这取决于你的代码的复杂性,有一些工具和脚本可以帮助你。但这不是重点:您问的是一个问题,说您正在使用 PyQt4,即使差异“很小”,您也已根据 PyQt5 模块命名更改了我的代码,使其不兼容;由于存在一些不兼容性,它也会出现问题:例如,在 Qt4 中,QWheelEvent 有 delta(),它是一个 int,而 Qt5 不再获得 delta,但两者都有angleDelta() 和 pixelDelta() 是 QPoints。
    • 我明白了,我会想办法的:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-07-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-11
    • 2017-07-21
    • 1970-01-01
    相关资源
    最近更新 更多