0

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.

Jones659
  • 49
  • 1
  • 7
  • If you need, for whatever reason, something special, the proper way is to use `event.time`. However the convenient and in almost in any case better solution is already stated by Bryan. – Thingamabobs Dec 27 '22 at 18:04

1 Answers1

1

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.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Hi Bryan, thanks for your reply, I had no clue that "<>" existed, this is cool because it actually pings when an item in the treeview is selected, so for example clicking on empty space, or on the column headers, doesn't cause it to ping. By the way, I was able to get the described result by just replacing ButtonPress, and leaving the "after" in there - it does feel a bit like dark magic at the moment, despite reading the explanation you linked. – Jones659 Dec 27 '22 at 22:59
  • Have to keep commentary short due to char restrictions here, ugh - but I have one follow up question: Do you happen to know if it's possible to combine using TreeviewSelect for when items are selected, and something like ButtonPress-1 for when a user clicks on a blank space inside the treeview. I want to add the feature of treeview selection being cleared when user clicks on an empty space inside the treeview - "TreeviewSelect" pings only when an item is selected, the old "ButtonPress-1" pings whether an item or a blank space is selected, is there a way to make this work? Sorry if off topic! – Jones659 Dec 27 '22 at 23:07
  • @Jones659: yes, what you ask is possible. The treeview method `identify` can tell what you what was clicked on, or if you clicked on an empty part of the tree. – Bryan Oakley Dec 28 '22 at 01:39
  • Hi Bryan, thanks again for replying - I will investigate and test out this method when I return from work today, it might be the puzzle piece I was missing all along – Jones659 Dec 28 '22 at 07:53
  • Hi again, I have looked into the "identify" method, and man, this just goes to show how little I know. I have found another reply of yours to someone else, and been able to use the "identify" to return the exact result of what was clicked in the treeview. The best I could do currently is bind buttonrelease-1 of the treeview to a function which does '''print('Results =', tree.identify("item", e.x, e.y))''', it's awesome but no luck with using treeviewselect, lambda, or after - you have been too helpful already, but do you know how I could incorporate treeviewselect with identify? – Jones659 Dec 29 '22 at 02:03
  • @Jones659: you shouldn't use `identify` with `<>` since that may be triggered by something other than a click. You should use it when creating a binding on a click, since you'll have the x,y coordinate of where the user clicked. – Bryan Oakley Dec 29 '22 at 02:32
  • Hi Bryan, shortly after posting my latest reply, I was able to get simple "ButtonPress-1" working with the above mentioned... and you're right, looks like it does it's thing and doesn't need TreeviewSelect - I should be able to take it from there, I think. Many many thanks for your patience with me in these comments - this is obviously outside of the scope of the originally asked question, it's just i feel so stupid posting questions under the ttk tag so frequently. You've really helped me on this - even if it's just to help give a personal project some professional "feeling" to it. Thanks! – Jones659 Dec 29 '22 at 08:12