• Tkinter ttk Treeview binding responds to past events!

    From John O'Hagan@21:1/5 to All on Mon Sep 11 22:30:21 2023
    I was surprised that the code below prints 'called' three times.


    from tkinter import *
    from tkinter.ttk import *

    root=Tk()

    def callback(*e):
        print('called')

    tree = Treeview(root)
    tree.pack()

    iid = tree.insert('', 0, text='test')

    tree.selection_set(iid)
    tree.selection_remove(iid)
    tree.selection_set(iid)

    tree.bind('<<TreeviewSelect>>', callback)

    mainloop()

    In other words, selection events that occurred _before_ the callback
    function was bound to the Treeview selections are triggering the
    function upon binding. AFAIK, no other tk widget/binding combination
    behaves this way (although I haven't tried all of them).

    This was a problem because I wanted to reset the contents of the
    Treeview without triggering a relatively expensive bound function, but
    found that temporarily unbinding didn't prevent the calls.

    I've worked around this by using a regular button-click binding for
    selection instead, but I'm curious if anyone can cast any light on
    this.

    Cheers

    John

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Mirko@21:1/5 to All on Mon Sep 11 22:25:45 2023
    Am 11.09.23 um 14:30 schrieb John O'Hagan via Python-list:
    I was surprised that the code below prints 'called' three times.


    from tkinter import *
    from tkinter.ttk import *

    root=Tk()

    def callback(*e):
        print('called')

    tree = Treeview(root)
    tree.pack()

    iid = tree.insert('', 0, text='test')

    tree.selection_set(iid)
    tree.selection_remove(iid)
    tree.selection_set(iid)

    tree.bind('<<TreeviewSelect>>', callback)

    mainloop()

    In other words, selection events that occurred _before_ the callback
    function was bound to the Treeview selections are triggering the
    function upon binding. AFAIK, no other tk widget/binding combination
    behaves this way (although I haven't tried all of them).

    This was a problem because I wanted to reset the contents of the
    Treeview without triggering a relatively expensive bound function, but
    found that temporarily unbinding didn't prevent the calls.

    I've worked around this by using a regular button-click binding for
    selection instead, but I'm curious if anyone can cast any light on
    this.

    Cheers

    John


    AFAIK (it's been quite some time, since I used Tk/Tkinter):

    These selection events are not triggered upon binding, but after the
    mainloop has startet. Tk's eventloop is queue-driven, so the tree.selection_{set,remove}() calls just place the events on the
    queue. After that, you setup a callback and when the mainloop
    starts, it processes the events from the queue, executing the
    registered callback.

    I seem to remember, that I solved a similar issue by deferring the
    callback installation using root.after().


    from tkinter import *
    from tkinter.ttk import *

    root=Tk()

    def callback(*e):
    print('called')

    tree = Treeview(root)
    tree.pack()

    iid = tree.insert('', 0, text='test')

    tree.selection_set(iid)
    tree.selection_remove(iid)
    tree.selection_set(iid)

    root.after(100, lambda: tree.bind('<<TreeviewSelect>>', callback))

    mainloop()



    This does not print "called" at all after startup (but still selects
    the entry), because the callback has not been installed when the
    mainloop starts. But any subsequent interaction with the list
    (clicking) will print it (since the callback is then setup).

    HTH

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Rob Cliffe@21:1/5 to Mirko via Python-list on Mon Sep 11 22:58:59 2023
    On 11/09/2023 21:25, Mirko via Python-list wrote:
    Am 11.09.23 um 14:30 schrieb John O'Hagan via Python-list:
    I was surprised that the code below prints 'called' three times.


    from tkinter import *
    from tkinter.ttk import *

    root=Tk()

    def callback(*e):
         print('called')

    tree = Treeview(root)
    tree.pack()

    iid = tree.insert('', 0, text='test')

    tree.selection_set(iid)
    tree.selection_remove(iid)
    tree.selection_set(iid)

    tree.bind('<<TreeviewSelect>>', callback)

    mainloop()

    In other words, selection events that occurred _before_ the callback
    function was bound to the Treeview selections are triggering the
    function upon binding. AFAIK, no other tk widget/binding combination
    behaves this way (although I haven't tried all of them).

    This was a problem because I wanted to reset the contents of the
    Treeview without triggering a relatively expensive bound function, but
    found that temporarily unbinding didn't prevent the calls.

    I've worked around this by using a regular button-click binding for
    selection instead, but I'm curious if anyone can cast any light on
    this.

    Cheers

    John


    AFAIK (it's been quite some time, since I used Tk/Tkinter):

    These selection events are not triggered upon binding, but after the
    mainloop has startet. Tk's eventloop is queue-driven, so the tree.selection_{set,remove}() calls just place the events on the
    queue. After that, you setup a callback and when the mainloop starts,
    it processes the events from the queue, executing the registered
    callback.

    I seem to remember, that I solved a similar issue by deferring the
    callback installation using root.after().


    from tkinter import *
    from tkinter.ttk import *

    root=Tk()

    def callback(*e):
        print('called')

    tree = Treeview(root)
    tree.pack()

    iid = tree.insert('', 0, text='test')

    tree.selection_set(iid)
    tree.selection_remove(iid)
    tree.selection_set(iid)

    root.after(100, lambda: tree.bind('<<TreeviewSelect>>', callback))

    mainloop()



    This does not print "called" at all after startup (but still selects
    the entry), because the callback has not been installed when the
    mainloop starts. But any subsequent interaction with the list
    (clicking) will print it (since the callback is then setup).

    HTH
    Indeed.  And you don't need to specify a delay of 100 milliseconds. 0
    will work (I'm guessing that's because queued actions are performed in
    the order that they were queued).
    I have also found that after() is a cure for some ills, though I avoid
    using it more than I have to because it feels ... a bit fragile, perhaps.
    E.g. suppose the mouse is clicked on a widget and tk responds by giving
    that widget the focus, but I don't want that to happen.
    I can't AFAIK prevent the focus change, but I can immediately cancel it with
        X.after(0, SomeOtherWidget.focus_set)
    where X is any convenient object with the "after" method (just about any widget, or the root).
    Best wishes
    Rob Cliffe

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From John O'Hagan@21:1/5 to Mirko via Python-list on Tue Sep 12 15:43:11 2023
    On Mon, 2023-09-11 at 22:25 +0200, Mirko via Python-list wrote:
    Am 11.09.23 um 14:30 schrieb John O'Hagan via Python-list:
    I was surprised that the code below prints 'called' three times.


    from tkinter import *
    from tkinter.ttk import *

    root=Tk()

    def callback(*e):
         print('called')

    tree = Treeview(root)
    tree.pack()

    iid = tree.insert('', 0, text='test')

    tree.selection_set(iid)
    tree.selection_remove(iid)
    tree.selection_set(iid)

    tree.bind('<<TreeviewSelect>>', callback)

    mainloop()

    In other words, selection events that occurred _before_ the
    callback
    function was bound to the Treeview selections are triggering the
    function upon binding. AFAIK, no other tk widget/binding
    combination
    behaves this way (although I haven't tried all of them).

    This was a problem because I wanted to reset the contents of the
    Treeview without triggering a relatively expensive bound function,
    but
    found that temporarily unbinding didn't prevent the calls.

    I've worked around this by using a regular button-click binding for selection instead, but I'm curious if anyone can cast any light on
    this.

    Cheers

    John


    AFAIK (it's been quite some time, since I used Tk/Tkinter):

    These selection events are not triggered upon binding, but after the mainloop has startet. Tk's eventloop is queue-driven, so the tree.selection_{set,remove}() calls just place the events on the
    queue. After that, you setup a callback and when the mainloop
    starts, it processes the events from the queue, executing the
    registered callback.

    I seem to remember, that I solved a similar issue by deferring the
    callback installation using root.after().


    from tkinter import *
    from tkinter.ttk import *

    root=Tk()

    def callback(*e):
         print('called')

    tree = Treeview(root)
    tree.pack()

    iid = tree.insert('', 0, text='test')

    tree.selection_set(iid)
    tree.selection_remove(iid)
    tree.selection_set(iid)

    root.after(100, lambda: tree.bind('<<TreeviewSelect>>', callback))

    mainloop()



    This does not print "called" at all after startup (but still selects
    the entry), because the callback has not been installed when the
    mainloop starts. But any subsequent interaction with the list
    (clicking) will print it (since the callback is then setup).

    HTH


    Thanks for your reply. However, please see the example below, which is
    more like my actual use-case. The selection events take place when a
    button is pressed, after the mainloop has started but before the
    binding. This also prints 'called' three times. 

    from tkinter import *
    from tkinter.ttk import *

    class Test:

    def __init__(self):
    root=Tk()
    self.tree = Treeview(root)
    self.tree.pack()
    self.iid = self.tree.insert('', 0, text='test')
    Button(root, command=self.temp_unbind).pack()
    mainloop()

    def callback(self, *e):
    print('called')

    def temp_unbind(self):
    self.tree.unbind('<<TreeviewSelect>>')
    self.tree.selection_set(self.iid)
    self.tree.selection_remove(self.iid)
    self.tree.selection_set(self.iid)
    self.tree.bind('<<TreeviewSelect>>', self.callback)
    #self.tree.after(0, lambda: self.tree.bind('<<TreeviewSelect>>',
    self.callback))

    c=Test()

    It seems the events are still queued, and then processed by a later
    bind?

    However, your solution still works, i.e. replacing the bind call with
    the commented line. This works even with a delay of 0, as suggested in
    Rob Cliffe's reply. Does the call to after clear the event queue
    somehow?

    My issue is solved, but I'm still curious about what is happening here.

    Regards

    John

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From MRAB@21:1/5 to John O'Hagan via Python-list on Tue Sep 12 15:59:42 2023
    On 2023-09-12 06:43, John O'Hagan via Python-list wrote:
    On Mon, 2023-09-11 at 22:25 +0200, Mirko via Python-list wrote:
    Am 11.09.23 um 14:30 schrieb John O'Hagan via Python-list:
    I was surprised that the code below prints 'called' three times.


    from tkinter import *
    from tkinter.ttk import *

    root=Tk()

    def callback(*e):
         print('called')

    tree = Treeview(root)
    tree.pack()

    iid = tree.insert('', 0, text='test')

    tree.selection_set(iid)
    tree.selection_remove(iid)
    tree.selection_set(iid)

    tree.bind('<<TreeviewSelect>>', callback)

    mainloop()

    In other words, selection events that occurred _before_ the
    callback
    function was bound to the Treeview selections are triggering the
    function upon binding. AFAIK, no other tk widget/binding
    combination
    behaves this way (although I haven't tried all of them).

    This was a problem because I wanted to reset the contents of the
    Treeview without triggering a relatively expensive bound function,
    but
    found that temporarily unbinding didn't prevent the calls.

    I've worked around this by using a regular button-click binding for
    selection instead, but I'm curious if anyone can cast any light on
    this.

    Cheers

    John


    AFAIK (it's been quite some time, since I used Tk/Tkinter):

    These selection events are not triggered upon binding, but after the
    mainloop has startet. Tk's eventloop is queue-driven, so the
    tree.selection_{set,remove}() calls just place the events on the
    queue. After that, you setup a callback and when the mainloop
    starts, it processes the events from the queue, executing the
    registered callback.

    I seem to remember, that I solved a similar issue by deferring the
    callback installation using root.after().


    from tkinter import *
    from tkinter.ttk import *

    root=Tk()

    def callback(*e):
         print('called')

    tree = Treeview(root)
    tree.pack()

    iid = tree.insert('', 0, text='test')

    tree.selection_set(iid)
    tree.selection_remove(iid)
    tree.selection_set(iid)

    root.after(100, lambda: tree.bind('<<TreeviewSelect>>', callback))

    mainloop()



    This does not print "called" at all after startup (but still selects
    the entry), because the callback has not been installed when the
    mainloop starts. But any subsequent interaction with the list
    (clicking) will print it (since the callback is then setup).

    HTH


    Thanks for your reply. However, please see the example below, which is
    more like my actual use-case. The selection events take place when a
    button is pressed, after the mainloop has started but before the
    binding. This also prints 'called' three times.

    from tkinter import *
    from tkinter.ttk import *

    class Test:

    def __init__(self):
    root=Tk()
    self.tree = Treeview(root)
    self.tree.pack()
    self.iid = self.tree.insert('', 0, text='test')
    Button(root, command=self.temp_unbind).pack()
    mainloop()

    def callback(self, *e):
    print('called')

    def temp_unbind(self):
    self.tree.unbind('<<TreeviewSelect>>')
    self.tree.selection_set(self.iid)
    self.tree.selection_remove(self.iid)
    self.tree.selection_set(self.iid)
    self.tree.bind('<<TreeviewSelect>>', self.callback)
    #self.tree.after(0, lambda: self.tree.bind('<<TreeviewSelect>>',
    self.callback))

    c=Test()

    It seems the events are still queued, and then processed by a later
    bind?

    However, your solution still works, i.e. replacing the bind call with
    the commented line. This works even with a delay of 0, as suggested in
    Rob Cliffe's reply. Does the call to after clear the event queue
    somehow?

    My issue is solved, but I'm still curious about what is happening here.

    Yes, it's still queuing the events.
    When an event occurs, it's queued.

    So, you unbound and then re-bound the callback in temp_unbind?

    Doesn't matter.

    All that matters is that on returning from temp_unbind to the main event
    loop, there are events queued and there's a callback registered, so the callback is invoked.

    Using the .after trick queues an event that will re-bind the callback
    _after_ the previous events have been handled.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Mirko@21:1/5 to All on Tue Sep 12 20:51:24 2023
    Am 12.09.23 um 07:43 schrieb John O'Hagan via Python-list:

    My issue is solved, but I'm still curious about what is happening here.

    MRAB already said it: When you enter the callback function, Tk's
    mainloop waits for it to return. So what's happening is:

    1. Tk's mainloop pauses
    2. temp_unbind() is called
    3. TreeviewSelect is unbound
    4. events are queued
    5. TreeviewSelect is bound again
    6. temp_unbind() returns
    7. Tk's mainloop continues with the state:
    - TreeviewSelect is bound
    - events are queued

    Am 11.09.23 um 23:58 schrieb Rob Cliffe:

    Indeed. And you don't need to specify a delay of 100 milliseconds. 0 will work (I'm guessing that's because queued actions are performed in the order that they were queued).

    Ah, nice, didn't know that!

    I have also found that after() is a cure for some ills, though I
    avoid using it more than I have to because it feels ... a bit
    fragile, perhaps.
    Yeah. Though for me it was the delay which made it seem fragile.
    With a 0 delay, this looks much more reliable.


    FWIW, here's a version without after(), solving this purely on the
    python side, not by temporarily unbinding the event, but by
    selectively doing nothing in the callback function.

    from tkinter import *
    from tkinter.ttk import *

    class Test:
    def __init__(self):
    self.inhibit = False
    root=Tk()
    self.tree = Treeview(root)
    self.tree.pack()
    self.iid = self.tree.insert('', 0, text='test')
    Button(root, command=self.temp_inhibit).pack()
    mainloop()

    def callback(self, *e):
    if not self.inhibit:
    print('called')

    def temp_inhibit(self):
    self.inhibit = True
    self.tree.selection_set(self.iid)
    self.tree.selection_remove(self.iid)
    self.tree.selection_set(self.iid)
    self.inhibit = False
    self.callback()

    c=Test()


    HTH and regards

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From MRAB@21:1/5 to Mirko via Python-list on Tue Sep 12 20:55:33 2023
    On 2023-09-12 19:51, Mirko via Python-list wrote:
    Am 12.09.23 um 07:43 schrieb John O'Hagan via Python-list:

    My issue is solved, but I'm still curious about what is happening here.

    MRAB already said it: When you enter the callback function, Tk's
    mainloop waits for it to return. So what's happening is:

    1. Tk's mainloop pauses
    2. temp_unbind() is called
    3. TreeviewSelect is unbound
    4. events are queued
    5. TreeviewSelect is bound again
    6. temp_unbind() returns
    7. Tk's mainloop continues with the state:
    - TreeviewSelect is bound
    - events are queued

    Am 11.09.23 um 23:58 schrieb Rob Cliffe:

    Indeed. And you don't need to specify a delay of 100 milliseconds. 0 will work (I'm guessing that's because queued actions are performed in the order that they were queued).

    Ah, nice, didn't know that!

    Well, strictly speaking, it's the order in which they were queued except
    for .after, which will be postponed if you specify a positive delay.

    [snip]

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Rob Cliffe@21:1/5 to Mirko via Python-list on Tue Sep 12 23:09:23 2023
    On 12/09/2023 19:51, Mirko via Python-list wrote:


    I have also found that after() is a cure for some ills, though I
    avoid using it more than I have to because it feels ... a bit
    fragile, perhaps.
    Yeah. Though for me it was the delay which made it seem fragile. With
    a 0 delay, this looks much more reliable.

    At one point I found myself writing, or thinking of writing, this sort
    of code
        after(1, DoSomeThing)
        after(2, Do SomeThingElse)
        after(3, DoAThirdThing)
        ...
    but this just felt wrong (is it reliable if some Things take more than a millisecond?  It may well be; I don't know), and error-prone if I want
    to add some more Things.
    Rob Cliffe

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From John O'Hagan@21:1/5 to Mirko via Python-list on Wed Sep 13 09:40:31 2023
    On Tue, 2023-09-12 at 20:51 +0200, Mirko via Python-list wrote:
    Am 12.09.23 um 07:43 schrieb John O'Hagan via Python-list:

    My issue is solved, but I'm still curious about what is happening
    here.

    MRAB already said it: When you enter the callback function, Tk's
    mainloop waits for it to return. So what's happening is:

    1. Tk's mainloop pauses
    2. temp_unbind() is called
    3. TreeviewSelect is unbound
    4. events are queued
    5. TreeviewSelect is bound again
    6. temp_unbind() returns
    7. Tk's mainloop continues with the state:
    - TreeviewSelect is bound
    - events are queued

    [. . .]

    Thanks (also to others who have explained), now I get it!


    FWIW, here's a version without after(), solving this purely on the 
    python side, not by temporarily unbinding the event, but by
    selectively doing nothing in the callback function.

    from tkinter import *
    from tkinter.ttk import *

    class Test:
         def __init__(self):
             self.inhibit = False
             root=Tk()
             self.tree = Treeview(root)
             self.tree.pack()
             self.iid = self.tree.insert('', 0, text='test')          Button(root, command=self.temp_inhibit).pack()          mainloop()

         def callback(self, *e):
             if not self.inhibit:
                 print('called')

         def temp_inhibit(self):
             self.inhibit = True
             self.tree.selection_set(self.iid)
             self.tree.selection_remove(self.iid)
             self.tree.selection_set(self.iid)
             self.inhibit = False
             self.callback()

    c=Test()



    I like this solution better - it's much more obvious to me what it's
    doing.

    Regards

    John

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From MRAB@21:1/5 to John O'Hagan via Python-list on Wed Sep 13 01:33:27 2023
    On 2023-09-13 00:40, John O'Hagan via Python-list wrote:
    On Tue, 2023-09-12 at 20:51 +0200, Mirko via Python-list wrote:
    Am 12.09.23 um 07:43 schrieb John O'Hagan via Python-list:

    My issue is solved, but I'm still curious about what is happening
    here.

    MRAB already said it: When you enter the callback function, Tk's
    mainloop waits for it to return. So what's happening is:

    1. Tk's mainloop pauses
    2. temp_unbind() is called
    3. TreeviewSelect is unbound
    4. events are queued
    5. TreeviewSelect is bound again
    6. temp_unbind() returns
    7. Tk's mainloop continues with the state:
    - TreeviewSelect is bound
    - events are queued

    [. . .]

    Thanks (also to others who have explained), now I get it!


    FWIW, here's a version without after(), solving this purely on the
    python side, not by temporarily unbinding the event, but by
    selectively doing nothing in the callback function.

    from tkinter import *
    from tkinter.ttk import *

    class Test:
         def __init__(self):
             self.inhibit = False
             root=Tk()
             self.tree = Treeview(root)
             self.tree.pack()
             self.iid = self.tree.insert('', 0, text='test')
             Button(root, command=self.temp_inhibit).pack()
             mainloop()

         def callback(self, *e):
             if not self.inhibit:
                 print('called')

         def temp_inhibit(self):
             self.inhibit = True
             self.tree.selection_set(self.iid)
             self.tree.selection_remove(self.iid)
             self.tree.selection_set(self.iid)
             self.inhibit = False
             self.callback()

    c=Test()



    I like this solution better - it's much more obvious to me what it's
    doing.

    That code is not binding at all, it's just calling 'temp_inhibit' when
    the button is clicked.

    You can remove all uses of self.inhibit and rename 'temp_inhibit' to
    something more meaningful, like 'delete_item'.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From John O'Hagan@21:1/5 to MRAB via Python-list on Wed Sep 13 11:50:18 2023
    On Wed, 2023-09-13 at 01:33 +0100, MRAB via Python-list wrote:
    On 2023-09-13 00:40, John O'Hagan via Python-list wrote:
    On Tue, 2023-09-12 at 20:51 +0200, Mirko via Python-list wrote:
    Am 12.09.23 um 07:43 schrieb John O'Hagan via Python-list:


    [...]




    FWIW, here's a version without after(), solving this purely on
    the
    python side, not by temporarily unbinding the event, but by
    selectively doing nothing in the callback function.

    from tkinter import *
    from tkinter.ttk import *

    class Test:
         def __init__(self):
             self.inhibit = False
             root=Tk()
             self.tree = Treeview(root)
             self.tree.pack()
             self.iid = self.tree.insert('', 0, text='test')          Button(root, command=self.temp_inhibit).pack()          mainloop()

         def callback(self, *e):
             if not self.inhibit:
                 print('called')

         def temp_inhibit(self):
             self.inhibit = True
             self.tree.selection_set(self.iid)
             self.tree.selection_remove(self.iid)
             self.tree.selection_set(self.iid)
             self.inhibit = False
             self.callback()

    c=Test()



    I like this solution better - it's much more obvious to me what
    it's
    doing.

    That code is not binding at all, it's just calling 'temp_inhibit'
    when
    the button is clicked.

    You can remove all uses of self.inhibit and rename 'temp_inhibit' to something more meaningful, like 'delete_item'.

    You're right of course, not as obvious as I thought!

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)