Search code examples
pythontkintertreeview

tkinter treeview open state is not up to date when callback is triggered


I am currently working on an tkinter application that uses a treeview element to display data in a hierarchy. In part of this application, I have the need to update data for items. Unfortunately, getting this data is expensive (in time and processing), so I now only update items that are currently visible.

The problem I am having is that when I bind a (left click) event to an item and then query the treeview for open items to try and determine what is visible, previously opened items are returned, but the currently opened item is not (until a subsequent item is clicked).

I saw this question about querying (and setting) the open state, but it doesn't seem to cover my needs.

I have the following simplified code, which demonstrates the problem:

## import required libraries
import tkinter
import tkinter.ttk
import random
import string

## event handler for when items are left clicked
def item_clicked(event):        
    tree = event.widget
    
    ## we assume event.widget is the treeview    
    current_item = tree.item(tree.identify('item', event.x, event.y))
    print('Clicked ' + str(current_item['text']))
    
    ## for each itewm in the treeview
    for element in tree.get_children():    
        ## if it has elements under it..
        if len(tree.get_children(element)) > 0:
            ## get the element name
            element_name = tree.item(element)['text']
            ## check if it is open
            if tree.item(element)['open'] is True:
                print('Parent ' + element_name + ' is open')
            else:
                print('Parent ' + element_name + ' is closed')
 
## make the window and treeview
root = tkinter.Tk()
root.title('Treeview test')
tree = tkinter.ttk.Treeview(root, selectmode = 'browse', columns = ('num', 'let'))
tree.heading('#0', text = 'Name')
tree.heading('num', text = 'Numbers')
tree.heading('let', text = 'Letters')
tree.bind('<Button-1>', item_clicked)
tree.pack()

## populate the treeview with psuedo-random data
parents = ['']
for i in range(100):
    id = str(i)
    ## higher probability that this won't be under anything
    if random.randrange(50) > 40:
        parent = random.choice(parents)
    else:
        parent = ''
        
    ## make up some data
    tree.insert(parent, 'end', iid = id, text = 'Item ' + id)
    tree.set(id, 'num', ''.join(random.choices(string.digits, k=10)))
    tree.set(id, 'let', ''.join(random.choices(string.ascii_uppercase, k=10)))
    
    parents.append(id)

## main event loop (blocking)    
root.mainloop()

This code will generate a treeview with 100 items in a random heirachy. Clicking on any item will print a list of items that have children and are currently expanded (open). The issue is, as with my main code, not reporting the latest item opened (i.e. it is one step behind).

Adding update() to the treeview before querying its state (at the start of the item_clicked function) does not resolve the issue.

  • How can I accurately get the open state of the items in the event handler of the code above?
  • Is it an issue that the event is being fired before the item actually expands (opens), and if so, why is an update() not fixing this?
  • Does an alternative function exist for treeview widgets to return a list of currently visible items?

Running on Windows 10 with Python 3.7.3 (Tk version 8.6.9).


Solution

  • Changing the event binding from <Button-1> to <ButtonRelease-1> in the code above resolves the issue.

    It should be noted in the above code that the open/close state will only be printed for items with children that have no parent (i.e. it won't descend in to the tree). To resolve this, you simply need to call tree.get_children(item_id) on each item with children recursively.