TkInter, slider: how to trigger the event only when the iteraction is complete?
Asked Answered
F

3

14

I'm using the slider to update my visualization, but the command updateValue is sent everytime I move the slider thumb, even for intermediate values.

Instead I want to trigger it only when I release the mouse button and the interaction is complete.

self.slider = tk.Scale(self.leftFrame, from_=0, to=256, orient=tk.HORIZONTAL, command=updateValue)

How can I trigger the function only once, when the interaction is ended ?

Fuddyduddy answered 19/10, 2010 at 7:57 Comment(0)
S
9

You can't.

What you can do instead is have your command delay any real work for a short period of time using 'after'. Each time your command is called, cancel any pending work and reschedule the work. Depending on what your actual requirements are, a half second delay might be sufficient.

Another choice is to not use the built-in command feature and instead use custom bindings. This can be a lot of work to get exactly right, but if you really need fine grained control you can do it. Don't forget that one can interact with the widget using the keyboard in addition to the mouse.

Here's a short example showing how to schedule the work to be done in half a second:

import Tkinter as tk

#create window & frames
class App:
    def __init__(self):
        self.root = tk.Tk()
        self._job = None
        self.slider = tk.Scale(self.root, from_=0, to=256, 
                               orient="horizontal", 
                               command=self.updateValue)
        self.slider.pack()
        self.root.mainloop()

    def updateValue(self, event):
        if self._job:
            self.root.after_cancel(self._job)
        self._job = self.root.after(500, self._do_something)

    def _do_something(self):
        self._job = None
        print "new value:", self.slider.get()

app=App()
Silda answered 19/10, 2010 at 12:53 Comment(3)
Ok, what's the best way to have a 0.5 delay ?Fuddyduddy
Thanks, I get this error: AttributeError: 'str' object has no attribute '_job'Fuddyduddy
@Patric: I can't debug your code for you, especially without seeing it. Think about what the error message is telling you, this is not a hard problem. You are trying to access the attribute "_job" from some object. That object has no such attribute. Thus, either you're using the wrong object, or your object is somehow broken.Silda
S
57

This is quite an ancient question now, but in case anyone stumbles upon this particular problem just use the bind() function and the "ButtonRelease-1" event like so:

import Tkinter as tk

class App:
    def __init__(self):
        self.root = tk.Tk()

        self.slider = tk.Scale(self.root, from_=0, to=256, 
                               orient="horizontal")
        self.slider.bind("<ButtonRelease-1>", self.updateValue)
        self.slider.pack()
        self.root.mainloop()

    def updateValue(self, event):
        print self.slider.get()

app=App()

Hope this helps anyone!

Selfsuggestion answered 6/6, 2013 at 19:45 Comment(2)
I know this is a reply to an encient question, but it was exactly the answer I was looking for. Thank you.Spondaic
Beware! If Scale.takefocus is True, then people will be able to move the slider using just keyboard arrow keys. But nothing will ever happen because you are only catching mouse click. To remedy, catch the key presses too. (You can bind multiple events for a single callback)Nikola
S
9

You can't.

What you can do instead is have your command delay any real work for a short period of time using 'after'. Each time your command is called, cancel any pending work and reschedule the work. Depending on what your actual requirements are, a half second delay might be sufficient.

Another choice is to not use the built-in command feature and instead use custom bindings. This can be a lot of work to get exactly right, but if you really need fine grained control you can do it. Don't forget that one can interact with the widget using the keyboard in addition to the mouse.

Here's a short example showing how to schedule the work to be done in half a second:

import Tkinter as tk

#create window & frames
class App:
    def __init__(self):
        self.root = tk.Tk()
        self._job = None
        self.slider = tk.Scale(self.root, from_=0, to=256, 
                               orient="horizontal", 
                               command=self.updateValue)
        self.slider.pack()
        self.root.mainloop()

    def updateValue(self, event):
        if self._job:
            self.root.after_cancel(self._job)
        self._job = self.root.after(500, self._do_something)

    def _do_something(self):
        self._job = None
        print "new value:", self.slider.get()

app=App()
Silda answered 19/10, 2010 at 12:53 Comment(3)
Ok, what's the best way to have a 0.5 delay ?Fuddyduddy
Thanks, I get this error: AttributeError: 'str' object has no attribute '_job'Fuddyduddy
@Patric: I can't debug your code for you, especially without seeing it. Think about what the error message is telling you, this is not a hard problem. You are trying to access the attribute "_job" from some object. That object has no such attribute. Thus, either you're using the wrong object, or your object is somehow broken.Silda
W
0

debounced tk scale with label and value

tk_scale_debounced.py

import tkinter as tk
from tkinter import ttk

class tk_scale_debounced(ttk.Frame):

    """
    scale with after_change event

    aka: debounced scale

    example:

    def after_change(key, value):
        print(key, value)
    root = tk.Tk()
    s = tk_scale_debounced(root, "some label", after_change, "x", from_=-10, to=10)
    s.pack()
    """

    # current value
    value = None

    # debounce timer for keyboard input
    _change_key_timer = None

    def __init__(self, parent, label, after_change, key=None, **scale_kwargs):

        """
        example scale_kwargs:

        from_=-10, to=10, orient='horizontal'
        """

        super().__init__(parent)

        self._after_change = after_change
        self._label = label
        self._key = key or label

        self.value = tk.DoubleVar()

        self.columnconfigure(0, weight=2)
        self.columnconfigure(1, weight=1)
        self.columnconfigure(2, weight=100)

        # label
        self._scale_label = ttk.Label(self, text=self._label)
        self._scale_label.grid(column=0, row=0, sticky='w')

        # value
        self._value_label = ttk.Label(self, text=self._format_value())
        self._value_label.grid(column=1, row=0, sticky='w')

        #  scale
        self._scale = ttk.Scale(self, command=self._scale_change_live, variable=self.value, **scale_kwargs)
        self._scale.grid(column=2, row=0, columnspan=4, sticky='we')

        # mouse
        self._scale.bind("<ButtonRelease-1>", self._scale_change_done)
        # keyboard
        self._scale.bind("<KeyRelease>", self._scale_change_key)

    def _format_value(self):
        return '{: .2f}'.format(self.value.get())

    def _scale_change_live(self, event):
        self._value_label.configure(text=self._format_value())

    def _scale_change_done(self, event=None):
        self._after_change(self._key, self.value.get())

    def _scale_change_key(self, event):
        if self._change_key_timer:
            self.after_cancel(self._change_key_timer)
        t = 1000
        self._change_key_timer = self.after(t, self._scale_change_done)
Wamble answered 29/3 at 9:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.