Mailing List Archive

Tkinter ttk Treeview binding responds to past events!
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
--
https://mail.python.org/mailman/listinfo/python-list
Re: Tkinter ttk Treeview binding responds to past events! [ In reply to ]
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
--
https://mail.python.org/mailman/listinfo/python-list
Re: Tkinter ttk Treeview binding responds to past events! [ In reply to ]
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

--
https://mail.python.org/mailman/listinfo/python-list
Re: Tkinter ttk Treeview binding responds to past events! [ In reply to ]
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

--
https://mail.python.org/mailman/listinfo/python-list
Re: Tkinter ttk Treeview binding responds to past events! [ In reply to ]
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.
--
https://mail.python.org/mailman/listinfo/python-list
Re: Tkinter ttk Treeview binding responds to past events! [ In reply to ]
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
--
https://mail.python.org/mailman/listinfo/python-list
Re: Tkinter ttk Treeview binding responds to past events! [ In reply to ]
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]

--
https://mail.python.org/mailman/listinfo/python-list
Re: Tkinter ttk Treeview binding responds to past events! [ In reply to ]
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
--
https://mail.python.org/mailman/listinfo/python-list
Re: Tkinter ttk Treeview binding responds to past events! [ In reply to ]
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


--
https://mail.python.org/mailman/listinfo/python-list
Re: Tkinter ttk Treeview binding responds to past events! [ In reply to ]
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'.

--
https://mail.python.org/mailman/listinfo/python-list
Re: Tkinter ttk Treeview binding responds to past events! [ In reply to ]
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!

--
https://mail.python.org/mailman/listinfo/python-list