Daniel Huckson
Daniel Huckson

Reputation: 1217

How to limit the number of events generated by widget

I'm using a spinbox to control the size of fonts in real time, for a zooming effect. A spinbox widget can generate a lot of events. As long as you hold down a direction key on the keyboard or click and hold down one the direction widget arrow icons, events get generated by the spinbox.
The problem is I'm getting to many events and this is making the zoom effect hang. I have setup a demo showing this using two different spinboxes, a tk.Spinbox and a ttk.Spinbox. With th tk.Spinbox you can limit the rate using "repeatdelay and repeatinterval" well this does work it only works when you click one of the arrow buttons in the spinbox. If you press the up or down keys though the "repeatdelay and repeatinterval" has no effect. As for the ttk.Spinbox it doesn't except the parameters "repeatdelay and repeatinterval" so it has no effect on it. How can I limit the repeat rate for both type of spinboxes?

import tkinter as tk
import tkinter.ttk as ttk


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.rowconfigure(990, weight=1)
        self.columnconfigure(0, weight=1)
        self.title('Timed Events Demo')
        self.geometry('420x200+20+20')

        tk_spn = tk.Spinbox(
            self,
            value=0,
            from_=0, to=1000,
            repeatdelay=500,
            repeatinterval=500,
            values=list(range(0, 1000))
        )
        tk_spn.grid(row=0, pady=5)

        tk_spn = ttk.Spinbox(
            self,
            from_=0, to=1000,
            value=0,
            values=list(range(0, 1000))
        )
        tk_spn.grid(row=1, pady=5)

        self.cnt = 0

        def test(e):
            print(self.cnt, e)

        tk_spn.bind('<<Increment>>', test)


def main():
    app = App()
    app.mainloop()


if __name__ == '__main__':
    main()

Upvotes: 1

Views: 673

Answers (1)

j_4321
j_4321

Reputation: 16169

I used similar solutions for both kinds of spinboxes but the implementation is a bit different since they don't use the same events. The idea is to create a Spinbox class with an _increment_lock attribute which is set to True when the Spinbox is incremented and after a delay is set back to False. Then, the events that increment the spinbox are bound to a method that checks _increment_lock before actually performing the increment. The principle is the same for the decrement.

For the tk.Spinbox, I used the bindings to the <Up> and <Down> arrows to implement the above solution while I used the bindings to <<Increment>> and <<Decrement>>.

Here is the code:

import tkinter as tk
import tkinter.ttk as ttk


class MySpinbox(tk.Spinbox):
    def __init__(self, master=None, delay=500, **kwargs):
        kwargs.setdefault('repeatdelay', delay)
        kwargs.setdefault('repeatinterval', delay)
        tk.Spinbox.__init__(self, master, **kwargs)
        self.delay = delay  # repeatdelay in ms
        self.bind('<Up>', self._on_increment)
        self.bind('<Down>', self._on_decrement)
        self._increment_lock = False
        self._decrement_lock = False

    def _unlock_increment(self):
        self._increment_lock = False

    def _on_increment(self, event):
        if self._increment_lock:
            return "break"  # stop the increment
        else:
            self._increment_lock = True
            self.after(self.delay, self._unlock_increment)

    def _unlock_decrement(self):
        self._decrement_lock = False

    def _on_decrement(self, event):
        if self._decrement_lock:
            return "break"  # stop the increment
        else:
            self._decrement_lock = True
            self.after(self.delay, self._unlock_decrement)


class MyTtkSpinbox(ttk.Spinbox):
    def __init__(self, master=None, delay=500, **kwargs):
        ttk.Spinbox.__init__(self, master, **kwargs)
        self.delay = delay  # repeatdelay in ms
        self.bind('<<Increment>>', self._on_increment)
        self.bind('<<Decrement>>', self._on_decrement)
        self._increment_lock = False
        self._decrement_lock = False

    def _unlock_increment(self):
        self._increment_lock = False

    def _on_increment(self, event):
        if self._increment_lock:
            return "break"  # stop the increment
        else:
            # generate a virtual event corresponding to when the spinbox
            # is actually incremented
            self.event_generate('<<ActualIncrement>>')
            self._increment_lock = True
            self.after(self.delay, self._unlock_increment)

    def _unlock_decrement(self):
        self._decrement_lock = False

    def _on_decrement(self, event):
        if self._decrement_lock:
            return "break"  # stop the increment
        else:
            # generate a virtual event corresponding to when the spinbox
            # is actually decremented
            self.event_generate('<<ActualDecrement>>')
            self._decrement_lock = True
            self.after(self.delay, self._unlock_decrement)


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.rowconfigure(990, weight=1)
        self.columnconfigure(0, weight=1)
        self.title('Timed Events Demo')
        self.geometry('420x200+20+20')

        tk_spn1 = MySpinbox(self, value=0, values=list(range(0, 1000)))
        tk_spn1.grid(row=0, pady=5)

        tk_spn2 = MyTtkSpinbox(self, from_=0, to=1000)
        tk_spn2.grid(row=1, pady=5)

        def test(e):
            print(e)

        tk_spn2.bind('<<ActualIncrement>>', test)


if __name__ == '__main__':
    app = App()
    app.mainloop()

Upvotes: 1

Related Questions