我正在开发一个小部件。它有一个拨盘和一个滑块,并且都修改相同的数字,尽管我希望滑块比拨盘具有更高的变化率。可以将其视为微调的表盘。例如,滑块以 0.1 为单位变化,但转盘以 0.01 为单位变化。 另外,我希望只要向一个方向转动拨盘即可增加或减少数字的值。因此,如果我转动 5 整圈,数字将增加 50 个单位(考虑到表盘“轴”中有 100 步)。
我成功实现了大部分,但是有一个问题,我不知道如何解决。
表盘按我想要的方式工作。它无边无际地转动。它的变化速度也比滑块慢。问题是这个速率只有在我顺时针旋转(增加数字)时才有效。
刻度盘以 0.01 增加,滑块以 0.1 增加。我可以通过操作拨盘从 1 到 1.1(1.01、1.02 ...),滑块只会移动一次,但是如果我从 1.1 返回拨盘,它将直接更改为 1,然后更改为 0.9(跳过之间的数字)。也就是说只要我顺时针转动表盘的速率就会不同。
这是代码
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout, QDial, QSlider
from PyQt5.QtCore import Qt
class DialWidget(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self._label_number = 0.000
self.label = QLabel(str(self._label_number), self)
self.label.setAlignment(Qt.AlignCenter)
self.dial = QDial(self)
self.dial.setNotchesVisible(True)
self.dial.setWrapping(True)
self.dial.setMinimum(0)
self.dial.setMaximum(100) # 3600 for full revolutions (0-3600 = 0-360 degrees)
self.dial.valueChanged.connect(self.dial_value_changed)
self.slider = QSlider(Qt.Horizontal, self)
self.slider.setMinimum(0)
self.slider.setMaximum(80)
self.slider.valueChanged.connect(self.slider_value_changed)
self._slider_value = self.slider.value()
layout = QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.dial)
layout.addWidget(self.slider)
self.dial_value = self.dial.value()
self.setLayout(layout)
@property
def label_number(self):
return self._label_number
@label_number.setter
def label_number(self, number):
print(f' Inside label_number.setter')
if number < 0 or number > 8:
pass
else:
print(f' Changing label_number {self._label_number} --> {number}')
self._label_number = number
#print(f' Changing label text {self.label.text()} --> {str(round(number, 4))}')
self.label.setText(str(round(number, 4)))
print(f' Changing slider value: {self.slider.value()} --> {number * 10.000} ')
print(f'self.slider.setValue({number * 10.000})')
self.slider_number = number * 10.000
self.slider.setValue(self.slider_number)
print(f' Exiting label_number.setter')
def dial_value_changed(self):
print(f'Inside dial_value_changed')
# Get the current value of the dial
dial_delta = self.dial.value() - self.dial_value
print(f'Delta number of the dial: {dial_delta}')
if dial_delta == 1:
print(f'dn = {1/100}')
new_number = self.label_number + 1/100
self.label_number = new_number
elif dial_delta == -1:
print(f'dn = {1/100}')
new_number = self.label_number - 1/100
self.label_number = new_number
elif dial_delta == -100:
print(f'dn = {1/100}')
new_number = self.label_number + 1/100
self.label_number = new_number
elif dial_delta == 99:
print(f'dn = {1/100}')
new_number = self.label_number - 1/100
self.label_number = new_number
#print(f'Setting self.dial_value to {self.dial.value()}')
self.dial_value = self.dial.value()
print(f'Exiting dial_value_changed')
print()
def slider_value_changed(self, value):
print(f' Inside slider_value_changed')
print(f' {self.slider.value()}')
print(f' Value sent {value}')
#print(f' Changing self.label_number {self.label_number} --> {value / 10.0}')
self.label_number = value / 10.000
print(f' Exiting slider_value_changed')
if __name__ == '__main__':
app = QApplication(sys.argv)
window = DialWidget()
window.setWindowTitle('Dial Widget')
window.show()
sys.exit(app.exec_())
我添加了打印以更好地遵循终端中的代码。我发现问题出在
self.slider.valueChanged.connect(self.slider_value_changed)
行。该信号仅发送整数(因为滑块的位置以整数表示)。
然后我尝试通过添加一个额外的属性来解决这个问题,该属性包含滑块的位置或编号,但作为浮点数。当我尝试执行此操作时,转盘在两个方向上都可以正常工作,但是如果我按下滑块,滑块将不会移动。
class DialWidget(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self._label_number = 0.000
self.label = QLabel(str(self._label_number), self)
self.label.setAlignment(Qt.AlignCenter)
self.dial = QDial(self)
self.dial.setNotchesVisible(True)
self.dial.setWrapping(True)
self.dial.setMinimum(0)
self.dial.setMaximum(100) # 3600 for full revolutions (0-3600 = 0-360 degrees)
self.dial.valueChanged.connect(self.dial_value_changed)
self.slider = QSlider(Qt.Horizontal, self)
self.slider.setMinimum(0)
self.slider.setMaximum(80)
self.slider.valueChanged.connect(self.slider_value_changed)
self._slider_number = self.slider.value()
layout = QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.dial)
layout.addWidget(self.slider)
self.dial_value = self.dial.value()
self.setLayout(layout)
@property
def label_number(self):
return self._label_number
@label_number.setter
def label_number(self, number):
print(f' Inside label_number.setter')
if number < 0 or number > 8:
pass
else:
print(f' Changing label_number {self._label_number} --> {number}')
self._label_number = number
#print(f' Changing label text {self.label.text()} --> {str(round(number, 4))}')
self.label.setText(str(round(number, 4)))
print(f' Changing slider value: {self.slider.value()} --> {number * 10.000} ')
print(f'self.slider.setValue({number * 10.000})')
self.slider_number = number * 10.000
self.slider.setValue(self.slider_number)
#self.slider.setValue(number * 10.000)
print(f' Exiting label_number.setter')
@property
def slider_number(self):
return self._slider_number
@slider_number.setter
def slider_number(self, number):
self._slider_number = number
def dial_value_changed(self):
print(f'Inside dial_value_changed')
# Get the current value of the dial
dial_delta = self.dial.value() - self.dial_value
print(f'Delta number of the dial: {dial_delta}')
if dial_delta == 1:
print(f'dn = {1/100}')
new_number = self.label_number + 1/100
self.label_number = new_number
elif dial_delta == -1:
print(f'dn = {1/100}')
new_number = self.label_number - 1/100
self.label_number = new_number
elif dial_delta == -100:
print(f'dn = {1/100}')
new_number = self.label_number + 1/100
self.label_number = new_number
elif dial_delta == 99:
print(f'dn = {1/100}')
new_number = self.label_number - 1/100
self.label_number = new_number
#print(f'Setting self.dial_value to {self.dial.value()}')
self.dial_value = self.dial.value()
print(f'Exiting dial_value_changed')
print()
def slider_value_changed(self, value):
print(f' Inside slider_value_changed')
print(f' {self.slider.value()}')
print(f' Value sent {value}')
#print(f' Changing self.label_number {self.label_number} --> {value / 10.0}')
value = self.slider_number
self.label_number = value / 10.000
#self.slider.setValue(self.slider_value)
print(f' Exiting slider_value_changed')
这是我尝试过的。我添加了 slider_number 作为属性。 我一整天都在努力解决这个问题。我非常感谢另一个大脑来帮助我。
QDial 存在与用户体验方面相关的重要问题,最重要的是,当使用鼠标时,它将二维向量映射到单维(线性)空间。
使用 wrapping 时,这个问题更加明显(并且有问题),因为没有直接的方法可以知道鼠标“移动”是顺时针还是逆时针旋转。
这意味着没有直接的方法可以知道滑块值是否应该保持相同,或者当旋转经过表盘的最小值和最大值之间的边缘时它最终应该添加/减去。
一个可能的解决方案是使用自定义信号对 QDial 进行子类化,并根据先前的值和触发的操作发出它。
actionTriggered
信号:
当信号发出时,sliderPosition已经根据动作进行了调整,但值还没有传播(意味着valueChanged()信号还没有发出),视觉显示还没有更新。
技巧是将该信号连接到一个函数,该函数跟踪当前滑块位置(在值实际更改之前)以及触发它的操作,然后连接到标准
valueChanged
信号并使用可以根据先前的值和相关操作来更改的值。
只要“越过”边缘,新值就会在整个表盘范围内增加或减少。
有些动作可以让我们知道方向,所以很容易知道结果是否:如果之前的值为 100,新的值为 0,动作为
SliderSingleStepAdd
,那么我们就知道应该将滑块增加 1,等等
鼠标移动需要仅通过前一个值来获取方向,因此我们必须假设通过获取值之间的差值,但使用全范围的模:
oldValue = 90
newValue = 10
diff = (newValue - oldValue) % 100 # 20
if diff < 50: # clockwise
if newValue < oldValue:
# increment the slider by 1
newValue += 100
elif newValue > oldValue: # counterclockwise is implied
# decrement by 1
newValue -= 100
# emit signal with newValue
然后,在主小部件中我们连接自定义信号:如果该值小于 0,那么我们知道必须从滑块值中减去 1,如果它大于 或 等于 100,则加 1。
请注意,为了避免递归并提供适当的范围边界,我们还应该防止滑块的
valueChanged
信号传播,以便我们最终可以在最终值达到最小值或最大值时将拨盘调整到0
.
最后,由于换行会导致最大值和最小值之间的鼠标/键盘范围非常窄(且不可靠),因此最好防止转盘实际达到最大值:每当位置等于 100 时,我们就根据情况进行调整之前的位置或动作(如上所述),因此如果它大于 50,它将变为 0,否则它将变为 99。这已在连接到
actionTriggered
的函数中完成,因为此时该值已没有调整过。
class Dial(QDial):
AddActions = (
QAbstractSlider.SliderSingleStepAdd,
QAbstractSlider.SliderPageStepAdd,
)
SubActions = (
QAbstractSlider.SliderSingleStepSub,
QAbstractSlider.SliderPageStepSub,
)
lastAction = QAbstractSlider.SliderNoAction
customValueChanged = pyqtSignal(int)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setNotchesVisible(True)
self.setWrapping(True)
self.setRange(0, 100)
self.lastValue = self.value()
self.actionTriggered.connect(self.checkActionTriggered)
self.valueChanged.connect(self.emitValueChanged)
def checkActionTriggered(self, action):
self.lastAction = action
self.lastValue = self.value()
# prevent the dial to get to 100
if self.sliderPosition() == 100:
if action in self.AddActions:
self.setSliderPosition(0)
elif action in self.SubActions:
self.setSliderPosition(99)
elif action == self.SliderMove:
if self.lastValue > 50:
self.setSliderPosition(0)
else:
self.setSliderPosition(99)
def emitValueChanged(self, value):
if self.lastAction in self.AddActions:
if value < self.lastValue:
value += 100
elif self.lastAction in self.SubActions:
if value > self.lastValue:
value -= 100
elif self.lastAction == self.SliderMove:
if self.lastValue == self.minimum() and value > 50:
value -= 100
else:
diff = (value - self.lastValue) % 100
if diff <= 50:
if value < self.lastValue:
value += 100
elif value > self.lastValue:
value -= 100
self.customValueChanged.emit(value)
class DialWidget(QWidget):
def __init__(self, minimum=0, maximum=80):
super().__init__()
self.label = QLabel()
self.label.setAlignment(Qt.AlignCenter)
self.dial = Dial()
self.slider = QSlider(Qt.Horizontal)
self.slider.setMinimum(minimum)
self.slider.setMaximum(maximum)
layout = QVBoxLayout(self)
layout.addWidget(self.label)
layout.addWidget(self.dial)
layout.addWidget(self.slider)
self.dial.customValueChanged.connect(self.dial_value_changed)
self.slider.valueChanged.connect(self.slider_value_changed)
self.updateLabel()
@property
def value(self):
return self.slider.value() + self.dial.value() * .01
@value.setter
def value(self, value):
value = max(self.slider.minimum(), min(value, self.slider.maximum()))
if self.value != value:
with QSignalBlocker(self.slider):
intVal, decimal = divmod(value, 1.)
self.slider.setValue(int(intVal))
self.dial.setValue(int(decimal * 100))
self.updateLabel()
def dial_value_changed(self, value):
if value < 0:
if self.slider.value() > self.slider.minimum():
with QSignalBlocker(self.slider):
self.slider.setValue(self.slider.value() - 1)
else:
self.dial.setValue(0)
elif value >= 100:
with QSignalBlocker(self.slider):
self.slider.setValue(self.slider.value() + 1)
if self.slider.value() == self.slider.maximum():
self.dial.setValue(0)
elif self.slider.value() == self.slider.maximum():
self.dial.setValue(0)
self.updateLabel()
def slider_value_changed(self, value):
if value == self.slider.minimum() or value == self.slider.maximum():
self.dial.setValue(0)
self.updateLabel()
def updateLabel(self):
self.label.setText(str(round(self.value, 2)))