[英]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. 目前,我有这个自定义的旋转
QDial()
小部件,其拨盘手柄向上指向0
位置,而不是默认的180
值位置。
To change the tick spacing, setNotchTarget()
is used to space the notches but this creates an even distribution of ticks (left). 要更改刻度线间距,可以使用
setNotchTarget()
来分隔刻度线,但这会创建刻度线的均匀分布(左图)。 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
. 中心刻度永远不会移动,并且始终位于
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. 因此,例如,如果刻度线设置为
70
,它将把左/右刻度线放置在距中心35
单位的位置。 Similarly, if the tick was changed to 120
, it would space the ticks by 60
. 类似地,如果刻度线更改为
120
,它将使刻度线间隔60
。
How can I do this? 我怎样才能做到这一点? If this is not possible using
QDial()
, what other widget would be capable of doing this? 如果使用
QDial()
无法做到这一点,还有哪些其他小部件可以做到这一点? I'm using PyQt4 and Python 3.7 我正在使用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_())
First of all. 首先。 Qt's dials are a mess.
Qt的拨盘很乱。 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. 这个例子显然需要
paintEvent
重写,但是最重要的部分是鼠标事件和wheel事件。 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. 跟踪将单步和页面步设置为值范围并“覆盖”原始
valueChanged
信号所需的键盘事件,以确保发出的值范围始终在-1和1之间。您显然可以通过添加专用功能来更改这些值。
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. 从理论上讲,QDial小部件应始终使用240 | -60度的角度,但是将来可能会发生变化,因此我决定启用包装以将度数保持为“内部”值。 Keep in mind that you'll probably need to provide your own
value()
property implementation also. 请记住,您可能还需要提供自己的
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_())
EDIT : I updated the code to implement keyboard navigation and avoid unnecessary multiple signal emissions. 编辑 :我更新了代码以实现键盘导航,并避免了不必要的多重信号发射。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.