Callback timing with ttk treeview (handling user treeview interaction)

Question:

Update: this question has been answered, at the bottom is the summary!

I have a ttk treeview populated with items, I have some buttons next to the treeview, and the state of those buttons is determined by what is highlighted within the treeview (for example, if no items are currently highlighted, all option buttons are disabled, if there is one or more items selected/highlighted, and their state is enabled, the "disable" button will become clickable), it’s quite simple.

The task of querying the treeview to find out what is selected, and assigning the state of each button accordingly, is handled by a "checker" function. Because items in the treeview can only become highlighted or un-highlighted by user input (such as a mouse click), this "checker" function only needs to run once after every user input inside the treeview area.

So, I bound <ButtonPress-1> inside of the treeview, to run the "checker" function. The issue I originally ran into, is that the user would click inside the treeview, but the function would run before the "selection" was updated inside the treeview, so the checker function wouldn’t actually see that a user had clicked on a treeview item the first time around.

In a previous question ("ButtonPress" not working whilst "ButtonRelease" does), this issue was fixed by replacing my "bind" line with this paraphrased line of code:

treeview.bind('<ButtonPress-1>', lambda e: window.after(1, checker_function))

The above line ensured that after an LMB click occured, the treeview selection would update, then the checker function would run, and update the state of the buttons in accordance to the treeview selection. Everything worked "instantly", it was really cool.

Now there has arisen a a need to pass additional context to the checker function, namely, what it was called by. I chose to do this by using a string, and then modifying the checker function, so that it does different things depending on what string it receives.

I will show two bits of code, one demonstrating how things should work, and one showing what happens when I try to pass things to the function. The checker function is called "disablebutton_statecheck".

First code (working)

import tkinter as tk
from tkinter import *
from tkinter import ttk

def disablebutton_statecheck(event=None):                      
    print("triggered statecheck")
    print(list(tree.selection()))
    print(len(tree.selection()))
    if len(tree.selection()) > 0:
        button_6.state(["!disabled"])
    else:
        button_6.state(["disabled"])
    
        

window = Tk()
window.title("test window")
window.geometry("600x600+0+0")
section_1 = Frame(window, width = 600, height = 600, bg="#faf")
section_1.place(x=0, y=0)

tabControl_1 = ttk.Notebook(section_1, height = 470, width = 395)
tab1 = ttk.Frame(tabControl_1)

tabControl_1.add(tab1, text='Devices')
tabControl_1.place(y=10,x=25)

tree = ttk.Treeview(tab1, height=7)
tree.place(x=30, y=95)
tree.bind('<Motion>', 'break')

tree["columns"] = ("one", "two")
tree['show'] = 'headings'
tree.column("one", width=100, anchor='c', stretch=False)
tree.column("two", width=100, anchor='c', stretch=False)
tree.heading("one", text="Account")
tree.heading("two", text="Type")
tree.insert("",'end', text="L1",values=("Test","Test12345"))

button_6 = ttk.Button(tab1, text="disable", width=17)               
button_6.place(x=50, y= 50)                                        

tree.bind('<ButtonPress-1>', lambda e: window.after(1, disablebutton_statecheck))             
disablebutton_statecheck()                                          

window.resizable(False,False)
window.mainloop()

When this runs, the following is printed to the shell:

triggered statecheck
[]
0

When the function is triggered, it prints as such, whilst the 2nd line shows the selected items in the treeview, and the 3rd line tells you how many items are selected.

With the program running, click once on the only item in the treeview using left click of a mouse, the following is then printed to shell:

triggered statecheck
['I001']
1

This is all correct!

Now, I have modified the code, to receive extra context from wherever it is called, and now, it’s not working.

Here is the modified code, every line which is new or different has been noted:

import tkinter as tk
from tkinter import *
from tkinter import ttk

def disablebutton_statecheck(y):                                    ###amended                   
    print("triggered statecheck")
    print(list(tree.selection()))
    print(len(tree.selection()))
    if len(tree.selection()) > 0:
        button_6.state(["!disabled"])
    else:
        button_6.state(["disabled"])
    if y == "system":                                               ###new
        print("this command was run by the system")                 ###new
    elif y == "click":                                              ###new
        print("this command came from user mouse click")            ###new
    
        

window = Tk()
window.title("test window")
window.geometry("600x600+0+0")
section_1 = Frame(window, width = 600, height = 600, bg="#faf")
section_1.place(x=0, y=0)

tabControl_1 = ttk.Notebook(section_1, height = 470, width = 395)
tab1 = ttk.Frame(tabControl_1)

tabControl_1.add(tab1, text='Devices')
tabControl_1.place(y=10,x=25)

tree = ttk.Treeview(tab1, height=7)
tree.place(x=30, y=95)
tree.bind('<Motion>', 'break')

tree["columns"] = ("one", "two")
tree['show'] = 'headings'
tree.column("one", width=100, anchor='c', stretch=False)
tree.column("two", width=100, anchor='c', stretch=False)
tree.heading("one", text="Account")
tree.heading("two", text="Type")
tree.insert("",'end', text="L1",values=("Test","Test12345"))


button_6 = ttk.Button(tab1, text="disable", width=17)               
button_6.place(x=50, y= 50)                                        

tree.bind('<ButtonPress-1>', lambda e: window.after(1, disablebutton_statecheck("click")))  ###amended        
disablebutton_statecheck("system")                                                          ###amended                                       

window.resizable(False,False)
window.mainloop()

When the program runs, the correct thing is printed to the shell:

triggered statecheck
[]
0
this command was run by the system

But if you click once on the item in the treeview, the shell gives strange result:

triggered statecheck
[]
0
this command came from user mouse click

What the heck? That bracket should have contained ‘I001’, and the zero should have been a one. You need to click a second time on the treeview item, to receive the correct result:

triggered statecheck
['I001']
1
this command came from user mouse click

Can someone explain what’s going on here, and how I can fix it? Thanks in advance, apologies this post is so damn long.

—————

Update: I set out wanting to be able to pass info to a function that triggers immediately after ttk treeview interaction takes place – I wanted "up" and "down" keys, and "left mouse click", to pass different data to the function respectively, so that my program could deal with each of these interactions correctly.

Well, multiple binds, and the passing of context, are not needed in this scenario, using TreeviewSelect like so:

treeview.bind('<<TreeviewSelect>>', lambda event: function_name)

(for context, the function that was triggered by the above line then used mytable.selection() to get the selected items in the treeview)

This actually allows one bind to be required for the up key, down key, and left mouse button click inside the treeview widget. It also doesn’t trigger if you click on a column title or on a blank space. Many thanks to Bryan Oakley for enlightening me.

Asked By: Jones659

||

Answers:

When you add a binding to a widget, that binding happens before the bindings on the widget class. It is the bindings on the widget class that cause the selection to change.

Instead of using after, a better solution is to bind to "<<TreeviewSelect>>". This will fire after the selection changes regardless of how it changes.

tree.bind('<<TreeviewSelect>>', lambda event: disablebutton_statecheck("click"))

For a description of how bindings work, see this answer to the question Basic query regarding bindtags in tkinter. This answer uses the Entry widget as an example, but the mechanism is the same for all widgets.

Answered By: Bryan Oakley
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.