如何在微调器中允许无限整数值?

2022-01-15 00:00:00 python kivy

问题描述

我需要一个 Spinner 小部件,其中用户可以选择 integer 具有特定步骤且没有下限或上限的值(我的意思是,它们至少应该在十亿范围内,所以没有机会记住整个序列).

I need a Spinner widget in which the user can select integer values with a certain step and without lower or upper limits (I mean, they should be at least in the billion range, so no chance of memorizing the whole sequence).

我看到了 kivy 的 Spinner 小部件但我不认为像 Spinner(values=itertool.count()) 这样的事情会奏效.它也仅限于字符串值.

I saw kivy's Spinner widget but I don't think doing something like Spinner(values=itertool.count()) would work. Also it is limited to string values.

有没有什么简单的方法可以获取类似于 QSpinBox 的东西Qt的?

Is there any simple way of obtaining something similar to QSpinBox of the Qt?


解决方案

目前看来 kivy 没有提供类似于 SpinnerSpinBox 的任何东西,但是你想调用它.可以使用的小部件是 Slider 但它看起来很糟糕,如果你想允许一个非常大的范围但只有一小步,它就不是那么有用了.

It seems like kivy at the moment does not provide anything similar to a Spinner or SpinBox or however you want to call it. A widget that might be used instead is the Slider but it looks awful and it's not so useful if you want to allow a very big range but with a small step.

因此,我编写了自己的 SpinBox 实现:

Therefore I wrote my own implementation of a SpinBox:

class SpinBox(BoxLayout):
    """A widget to show and take numeric inputs from the user.

    :param min_value: Minimum of the range of values.
    :type min_value: int, float
    :param max_value: Maximum of the range of values.
    :type max_value: int, float
    :param step: Step of the selection
    :type step: int, float
    :param value: Initial value selected
    :type value: int, float
    :param editable: Determine if the SpinBox is editable or not
    :type editable: bool
    """

    min_value = NumericProperty(float('-inf'))
    max_value = NumericProperty(float('+inf'))
    step = NumericProperty(1)
    value = NumericProperty(0)
    range = ReferenceListProperty(min_value, max_value, step)

    def __init__(self, btn_size_hint_x=0.2, **kwargs):
        super(SpinBox, self).__init__(orientation='horizontal', **kwargs)

        self.value_label = Label(text=str(self.value))
        self.inc_button = TimedButton(text='+')
        self.dec_button = TimedButton(text='-')

        self.inc_button.bind(on_press=self.on_increment_value)
        self.inc_button.bind(on_time_slice=self.on_increment_value)
        self.dec_button.bind(on_press=self.on_decrement_value)
        self.dec_button.bind(on_time_slice=self.on_decrement_value)

        self.buttons_vbox = BoxLayout(orientation='vertical',
                                      size_hint_x=btn_size_hint_x)
        self.buttons_vbox.add_widget(self.inc_button)
        self.buttons_vbox.add_widget(self.dec_button)

        self.add_widget(self.value_label)
        self.add_widget(self.buttons_vbox)

    def on_increment_value(self, btn_instance):
        if float(self.value) + float(self.step) <= self.max_value:
            self.value += self.step

    def on_decrement_value(self, btn_instance):
        if float(self.value) - float(self.step) >= self.min_value:
            self.value -= self.step

    def on_value(self, instance, value):
        instance.value_label.text = str(value)

实际上我使用的代码略有不同,因为我认为子类化布局来实现小部件很丑,因此我将 Widget 子类化并添加了一个水平 BoxLayout 作为Widget 的唯一子项,然后我 binded 每个大小和位置更改以更新此子项的大小和位置(请参阅 这个问题我为什么必须这样做).

Actually the code I use is slightly different because I think it is ugly to subclass a layout to implement a widget and thus I subclassed Widget and added a horizontal BoxLayout as only children of the Widget, then I binded every size and position change to update the size and position of this child(see this question for why I had to do that).

TimedButtonButton 的子类,它允许长按,并且当长按时,每隔一定时间发出一个 on_time_slice 事件毫秒数(因此用户将能够按住按钮进行连续增量).如果需要,您可以简单地使用普通的 Button,删除 bindon_time_slice 事件.

The TimedButton is a subclass of Button that allows long-presses and, when long-pressed, emits a on_time_slice event every a certain amount of millisecond(thus the user will be able to hold the button to do a continuous increment). You can simply use a normal Button if you want, removing the binds to on_time_slice event.

TimedButton源代码是这样的:

class TimedButton(Button):
    """A simple ``Button`` subclass that produces an event at regular intervals
    when pressed.

    This class, when long-pressed, emits an ``on_time_slice`` event every
    ``time_slice`` milliseconds.

    :param long_press_interval: Defines the minimum time required to consider
                                the press a long-press.
    :type long_press_interval: int
    :param time_slice: The number of milliseconds of each slice.
    :type time_slice: int
    """

    def __init__(self, long_press_interval=550, time_slice=225, **kwargs):
        super(TimedButton, self).__init__(**kwargs)

        self.long_press_interval = long_press_interval
        self.time_slice = time_slice

        self._touch_start = None
        self._long_press_callback = None
        self._slice_callback = None

        self.register_event_type('on_time_slice')
        self.register_event_type('on_long_press')


    def on_state(self, instance, value):
        if value == 'down':
            start_time = time.time()
            self._touch_start = start_time

            def callback(dt):
                self._check_long_press(dt)

            Clock.schedule_once(callback, self.long_press_interval / 1000.0)
            self._long_press_callback = callback
        else:
            end_time = time.time()
            delta = (end_time - (self._touch_start or 0)) * 1000
            Clock.unschedule(self._slice_callback)
            # Fixes the bug of multiple presses causing fast increase
            Clock.unschedule(self._long_press_callback)
            if (self._long_press_callback is not None and
                delta > self.long_press_interval):
                self.dispatch('on_long_press')
            self._touch_start = None
            self._long_press_callback = self._slice_callback = None

    def _check_long_press(self, dt):
        delta = dt * 1000
        if delta > self.long_press_interval and self.state == 'down':
            self.dispatch('on_long_press')
            self._long_press_callback = None

            def slice_callback(dt):
                self.dispatch('on_time_slice')
                return self.state == 'down'

            Clock.schedule_interval(slice_callback, self.time_slice / 1000.0)

            self._slice_callback = slice_callback


    def on_long_press(self):
        pass

    def on_time_slice(self):
        pass

请注意,我必须绑定 state 属性,而不是使用 on_touch_downon_touch_up 因为它们提供了一些 奇怪的行为,即使在工作"时,也会无缘无故地发生一些奇怪的事情(例如点击递减按钮导致on_increment 被调用,即使 bindings 正确).

Note that I had to bind the state property instead of using on_touch_down and on_touch_up because they give some strange behaviour, and even when "working" there were some strange things happening by no reason(e.g. clicking the decrement button caused on_increment to be called even though the bindings where correct).

更新了 TimedButton 类,修复了一个小错误(之前的实现在快速单击多次然后按住按钮会产生太多 on_time_slice 事件:当状态变为 'normal'

Updated the TimedButton class fixing a little bug(the previous implementation when clicked rapidly multiple times and then holding down the button would yield too many on_time_slice events: I'd forgot to "unschedule" the _long_press_callback when the state goes 'normal'

相关文章