python-3.x 在PyQt5中耦合滑块和拨号小部件,解决不一致的结果

2nc8po8w  于 2023-08-08  发布在  Python
关注(0)|答案(1)|浏览(121)

我正在做一个小部件。它有一个表盘和一个滑块,都修改相同的数字,虽然我希望滑块有一个更高的变化率比表盘。把它想象成微调的刻度盘。例如,滑块的单位为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作为属性。我一整天都在试图解决这个问题。我会非常感激另一个大脑来帮助我。

编辑(可能的解决方案):

我设法解决了问题,我有断开slider.valueChangedslider_value_changed时,拨号是行动(dial_value_changed),并连接它再次一旦我退出了功能。这是因为当slider.valueChanged被调用两次时会出现问题,其中一次是当移动拨号盘时数字以0.1个单位变化时,调用set_number(),然后再次调用slider.valueChanged

def dial_value_changed(self):
        dial_delta = self.dial.value() - self.dial_value
        self.slider.valueChanged.disconnect()
        if dial_delta == 1:
            new_number = self.number + 1/10000
            self.set_number(new_number)
        elif dial_delta == -1:
            new_number = self.number - 1/10000
            self.set_number(new_number)
        elif dial_delta == -100:
            new_number = self.number + 1/10000
            self.set_number(new_number)
        elif dial_delta == 99:
            new_number = self.number - 1/10000
            self.set_number(new_number)
        self.dial_value = self.dial.value()
        self.slider.valueChanged.connect(self.slider_value_changed)

ki1q1bka

ki1q1bka1#

QDial在用户体验方面存在重要问题,最重要的是,当使用鼠标时,它会将二维向量Map到一维(线性)空间。
当使用 wrapping 时,这个问题甚至更加明显(并且是有问题的),因为没有直接的方法来知道鼠标“移动”是打算顺时针旋转还是逆时针旋转。请记住,鼠标移动事件不是连续的:它们只是一个 * 表面上的 * 运动,但实际上鼠标“跳跃”到点,给出了 * 运动的 * 错觉 *。
这必然意味着没有直接的方式来知道滑块值是否应该保持相同,或者当旋转通过刻度盘的最小值和最大值之间的边缘时,滑块值是否应该最终加/减。
我们不能仅仅依赖于前一个值和当前值之间的差异;较小的新值可能并不总是意味着刻度盘已逆时针移动:如果值是例如50和40,则该假设是有效的,但是如果您只是将刻度盘从99移动到2呢?
一个可能的解决方案是使用自定义信号子类化QDial,并基于先前的值 * 和 * 触发的动作发出它。
由于actionTriggered信号,这是可能的:
当发出信号时,sliderPosition已经根据动作进行了调整,但是值还没有被传播(意味着valueChanged()信号还没有发出),并且视觉显示还没有被更新。
诀窍是将该信号连接到一个跟踪当前滑块位置(在值实际更改之前)和触发它的操作的函数,然后连接到标准valueChanged信号并使用可以根据以前的值和相关操作更改的值发出自定义信号。
每当边缘“交叉”时,新值将增加或减去整个刻度盘范围。
一些动作让我们知道方向,所以很容易知道结果是否:如果前一个值是100,新的是0,动作是SliderSingleStepAdd,那么我们知道我们应该将滑块增加1,等等。
鼠标移动需要通过前一个值来获得方向,因此我们必须假设通过获得值之间的差异,但使用全范围的模。那么我们必须做一个假设:如果该差值小于该范围的一半,则意味着该Angular 是顺时针的。

diff = (newValue - oldValue) % 100
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

字符串
以上将给予以下结果:

  • 对于oldValue = 90newValue = 10diff将是20,因此旋转是顺时针的,并且由于newValue小于oldValue,我们将其递增100,因此滑块将增加1;
  • 分别使用1090diff80,因此旋转是逆时针的,并且由于newValue大于oldValue,我们将其递减100,因此滑块将减少1;

请注意,由于开头提到的问题,当“旋转”没有“穿过”边缘时,这仍然可能产生意外或不可靠的结果:例如,如果当前刻度盘值是25,并且在75之后单击,则实际上可能导致逆时针旋转。
然后,在主小部件中,我们连接自定义信号:如果值小于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)))

相关问题