Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Tkinter ttk Treeview binding responds to past events!

100 views
Skip to first unread message

John O'Hagan

unread,
Sep 11, 2023, 2:45:11 PM9/11/23
to
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

Mirko

unread,
Sep 11, 2023, 4:27:07 PM9/11/23
to
Am 11.09.23 um 14:30 schrieb John O'Hagan via Python-list:
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

Rob Cliffe

unread,
Sep 11, 2023, 7:30:44 PM9/11/23
to
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

John O'Hagan

unread,
Sep 12, 2023, 10:04:27 AM9/12/23
to
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

MRAB

unread,
Sep 12, 2023, 11:00:13 AM9/12/23
to
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.

Mirko

unread,
Sep 12, 2023, 2:51:51 PM9/12/23
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

MRAB

unread,
Sep 12, 2023, 3:59:04 PM9/12/23
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]

Rob Cliffe

unread,
Sep 12, 2023, 6:10:28 PM9/12/23
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

John O'Hagan

unread,
Sep 12, 2023, 7:41:34 PM9/12/23
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!
I like this solution better - it's much more obvious to me what it's
doing.

Regards

John


MRAB

unread,
Sep 12, 2023, 8:33:51 PM9/12/23
to
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'.

John O'Hagan

unread,
Sep 12, 2023, 9:51:18 PM9/12/23
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:
> > >

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

0 new messages