Spinbutton doesn't work

Hi,

Does anyone have any idea why SpinButton won’t work in the code below when down button is pressed when time value is 00:00? I expect it to be set to 23:55 when I press the down button, but it doesn’t work.

Also, the up button won’t work either, when the time value reaches 03:45. Strange.

import wx
from wx.lib.embeddedimage import PyEmbeddedImage
from wx.lib.masked import TimeCtrl
from datetime import datetime, timedelta

class MyTimeCtrl(TimeCtrl):
    def __init__(self, the_parent, time_val):
        TimeCtrl.__init__(self, parent=the_parent, id=-1, value=time_val, fmt24hr=True)
        self.SetParameters(display_seconds=False)

class Example(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(Example, self).__init__(*args, **kwargs)
        self.InitUI()

    def InitUI(self):
        date_val = datetime(2023, 10, 1)
        self.pnl = wx.Panel(self)
        spin = wx.SpinButton(self.pnl, -1, wx.DefaultPosition, (-1,20), wx.SP_VERTICAL, name="spin")
        spin.Bind(wx.EVT_SPIN_UP, self.on_time_up)
        spin.Bind(wx.EVT_SPIN_DOWN, self.on_time_down)
        self.time_ctrl = MyTimeCtrl(self.pnl, time_val=date_val.strftime("%H:%M"))

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.time_ctrl, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)
        sizer.Add(spin, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)
        self.pnl.SetSizer(sizer)

    def on_time_up(self, event):
        the_val = self.time_ctrl.GetValue()
        hoursxx = int(the_val[:2])
        minutesxx = int(the_val[3:])
        st_val = datetime(2023, 5, 10, hoursxx, minutesxx) + timedelta(minutes=5)
        self.time_ctrl.SetValue(st_val.strftime("%H:%M"))
        self.time_ctrl.Refresh()

    def on_time_down(self, event):
        the_val = self.time_ctrl.GetValue()
        hoursxx = int(the_val[:2])
        minutesxx = int(the_val[3:])
        st_val = datetime(2023, 5, 10, hoursxx, minutesxx) - timedelta(minutes=5)
        self.time_ctrl.SetValue(st_val.strftime("%H:%M"))
        self.time_ctrl.Refresh()

def main():
    app = wx.App()
    ex = Example(None)
    ex.Show()
    app.MainLoop()

if __name__ == '__main__':
    main()

Hi Steve,

If you call print(spin.GetRange()) after creating the SpinButton, you will see that its range defaults to 0 to 100. When you convert that range to your time values you will see why it doesn’t go beyond those times.

You need to call spin.SetRange(min, max) to set the range to whatever values your application requires.

**Edit: the above appears to be true unless you make the buttons autorepeat, in which case the event handlers are not triggered for every value in the range, meaning the range of times is more limited.

Also, if the user employs a mixture of single clicks and autorepeats, plus and minus, the value in the SpinButton will get out of sync with the displayed value. It’s probably better to call the SpinButton’s GetValue() method or event.GetInt() and use the value to calculate the time to be displayed.

Here is a rough version:

import wx
from wx.lib.masked import TimeCtrl
from datetime import datetime, timedelta

class MyTimeCtrl(TimeCtrl):
    def __init__(self, the_parent, time_val):
        TimeCtrl.__init__(self, parent=the_parent, id=-1, value=time_val, fmt24hr=True)
        self.SetParameters(display_seconds=False)

class Example(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(Example, self).__init__(*args, **kwargs)
        self.InitUI()

    def InitUI(self):
        date_val = datetime(2023, 10, 1)
        self.pnl = wx.Panel(self)
        spin = wx.SpinButton(self.pnl, -1, wx.DefaultPosition, (-1,20), wx.SP_VERTICAL, name="spin")
        max_val = 24*60//5 # +24hrs
        min_val = -max_val # -24hrs
        spin.SetRange(min_val, max_val)
        spin.Bind(wx.EVT_SPIN, self.OnSpin)
        self.time_ctrl = MyTimeCtrl(self.pnl, time_val=date_val.strftime("%H:%M"))

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.time_ctrl, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)
        sizer.Add(spin, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)
        self.pnl.SetSizer(sizer)


    def OnSpin(self, event):
        offset = event.GetInt()
        # print(offset)
        st_val = datetime(2023, 5, 10) + timedelta(minutes=5*offset)
        self.time_ctrl.SetValue(st_val.strftime("%H:%M"))
        self.time_ctrl.Refresh()


def main():
    app = wx.App()
    ex = Example(None)
    ex.Show()
    app.MainLoop()

if __name__ == '__main__':
    main()
1 Like

There is a problem with my suggestion above, which occurs if the user changes the value in the TimeCtrl using the keyboard, as that won’t update the value in the SpinButton. This would mean that the next time they used the SpinButton, the current value in the TimeCtrl would be ignored and it would jump to the time calculated from the updated value in the SpinButton.

One heavy-handed solution to that would be to pass style=wx.TE_READONLY to the TimeCtrl’s constructor. That should prevent the user from changing the time using the keyboard.

Or, perhaps an EVT_TIMEUPDATE handler for the TimeCtrl could calculate the correct value to pass to the SpinButton’s SetValue() method?

Using the keyboard to change the value in a TimeCtrl does seem to be rather flaky on linux. Tabbing between the hours and minutes and using the up/down keys seem to work, but typing other characters can confuse it, which then causes it to raise IndexErrors.

1 Like

Hi Richard,

Thank you for the explanation. I solved the problem by setting a wider range for the spin button. In this case, when user reaches 23:55 value, if spin button up is pressed again, the spin button’s value will not be out of range.

        max_val = (24*60//5)*2 # +48hrs
        min_val = -max_val # -48hrs
        spin.SetRange(min_val, max_val)