Home > Software design >  Store and call event handler in Tkinter (for treeview's selection handler)
Store and call event handler in Tkinter (for treeview's selection handler)

Time:10-04

In a Tkinter's treeview, when nodes are expanded and collapsed, the node get selected/deselected just like any child entry. I can't have that for my application, so I made a selection handler and bound it to the ButtonRelease event. The problem is that now, the shift-click bulk selection does not work - simply because I haven't written it in my handler. But it's a pain to write with treeviews, my IIDs are not easily iterable in my implementation.

I would like to intercept the message if the node is selected (which I'm already doing), and call the default handler for any other selection such that I get the shift-click bulk selection feature back without reinventing the wheel. How can I do that?

I've tried looking at the result of bind() with and without the new callback parameter in the hope of storing the default callback/handler, but it did not work so this is a dead-end as far as I know.

import tkinter as tk      
from tkinter.ttk import *

class TV: #Find a better name
    def __init__(self, window):        
        self.tree = Treeview(window, selectmode="none", show="tree")
        self.tree.pack(expan=1,fill='both')
        self.tree.bind("<ButtonRelease-1>", self.treeSelect) #HERE: how to get the default handler?
        
        #Generate dummy data
        parents = ['1', '2', '3']
        children = ['a', 'b', 'c']
        for parent in parents:
            self.tree.insert('', tk.END, iid=parent, text=parent)
            for child in children:
                #Insert the item
                self.tree.insert(parent, tk.END, iid=parent ':' child, text=child)
        
    #Private/protected methods
    
    def treeSelect(self, event):
        curItem = self.tree.focus() #Get selected item by getting the item in focus
        #If the item has no children (i.e. it's not a node)
        if(not len(self.tree.get_children(curItem))):
            #Execute the selection
            self.tree.selection_toggle(curItem) #HERE: how to call the default handler?
            

#Create the window
window = tk.Tk()
window.title("test")

tv = TV(window)
  
window.mainloop()  

I've tried this as well but for some reason Tkinter thinks the selection needs to be cleared when the selected items collapse in their node...

import tkinter as tk      
from tkinter.ttk import *

class TV: #Find a better name
    def __init__(self, window):        
        self.tree = Treeview(window, show="tree")
        self.tree.pack(expan=1,fill='both')
        self.tree.bind("<<TreeviewOpen>>", self.treeOpenCloseUnselect, ' ')
        self.tree.bind("<<TreeviewClose>>", self.treeOpenCloseUnselect, ' ') 
        
        #Generate dummy data
        parents = ['1', '2', '3']
        children = ['a', 'b', 'c']
        for parent in parents:
            self.tree.insert('', tk.END, iid=parent, text=parent)
            for child in children:
                #Insert the item
                self.tree.insert(parent, tk.END, iid=parent ':' child, text=child)
        
    #Private/protected methods
    
    def treeOpenCloseUnselect(self, event=None):
        curItem = self.tree.focus() #Get selected item by getting the item in focus
        #If the item has no children (i.e. it's not a node)
        self.tree.selection_remove(curItem)
            

#Create the window
window = tk.Tk()
window.title("test")

tv = TV(window)
  
window.mainloop()  

CodePudding user response:

The shortest way to achieve what you want seems to me, to use the selection self.tree = Treeview(window) of the treeview itself and then check for valid items. An example:

def treeSelect(self, event):
    selection = self.tree.selection() #Get selected item by getting the item in focus
    valid = []
    for item in selection:
        if len(item) != 1:
            valid.append(item)
    self.tree.selection_set(valid)

But be aware this is more an xyproblem and there are different approaches available. I would go with tags and have a custom select color that I would add to. But you maybe want to see Treeview with Checkboxes as an option.

CodePudding user response:

The only workaround I've found was to memorise the selection on the Button-1 (clicked) event, and in the release button event, if a node was last clicked, remove its selected state and restore the previous selection:

import tkinter as tk      
from tkinter.ttk import *
from tkinter import messagebox

class TV: #Find a better name
    def __init__(self, window):        
        self.tree = Treeview(window, show="tree")
        self.tree.pack(expan=1,fill='both')
        self.tree.bind("<Button-1>", self.treeSelMem, ' ')
        self.tree.bind("<ButtonRelease-1>", self.treeSelRestore, ' ') 
        
        #Generate dummy data
        parents = ['1', '2', '3']
        children = ['a', 'b', 'c']
        for parent in parents:
            self.tree.insert('', tk.END, iid=parent, text=parent)
            for child in children:
                #Insert the item
                self.tree.insert(parent, tk.END, iid=parent ':' child, text=child)
        
    #Private/protected methods
    
    def treeSelMem(self, event):
        #Memorise the selection before it changes
        self.memSel = self.tree.selection()
            
    def treeSelRestore(self, event):
        #The selection has been made. Remove nodes from the selection
        for item in self.tree.selection():
            if len(item) == 1: #If the item is a parent/node
                self.tree.selection_remove(item)       
        
        #If a parent has been clicked
        curItem = self.tree.focus()
        if(len(self.tree.get_children(curItem))):
            #Move focus to the first child to avoid not being able to select anything else
            self.tree.focus(self.tree.get_children(curItem)[0])
            #Clear its selected state
            self.tree.selection_remove(curItem)
            #And restore from memory the previously selected items
            self.tree.selection_set(self.memSel)

#Create the window
window = tk.Tk()
window.title("test")

tv = TV(window)
  
window.mainloop() 

However this only solves this particular problem, so if anyone knows how to answer this question which is more general than that please go ahead and I'll very happily accept it.

CodePudding user response:

I'm not sure if I understand what you're trying to do, but it seems like you're just trying to prevent nodes with children from being selected. If that's the case, I think you're going about the solution backwards.

I'll present two solutions. The first solution prevents the clicked-on item from being selected while allowing all other interactions to work as normal. The second will detect whenever the selection changes in any matter, and de-selects all parent nodes.

Preventing default behavior

In the following example, clicking on a parent will toggle it open or closed but won't allow it to be selected. The key is to return the string "break", which will prevent any additional processing by the default bindings.

class TV: #Find a better name
    def __init__(self, window):
        ...
        self.tree = Treeview(window, selectmode="extended", show="tree")
        self.tree.pack(expan=1,fill='both')
        self.tree.bind("<ButtonPress-1>", self.treeSelect)
        ...
    def treeSelect(self, event):
        """Disallow parent items from being selected when clicked"""
        curItem = self.tree.identify('item', event.x, event.y)
        if len(self.tree.get_children(curItem)):
            opened = self.tree.item(curItem)["open"]
            self.tree.item(curItem, open=not opened)
            return "break"

Notice that selectmode has been set to "extended", so the default bindings will be enabled. Also, the binding has been set to the button press rather than release.

This allows the user to click on a parent to toggle the visibility of the child, but won't select the parent. It allows all other bindings to work as they should.

However, this also allows the user to click on one child and then shift-click on a child under a different parent, and any parent nodes between will be selected. The user could also select a parent node with the keyboard.

Adjusting the selection programatically

If the above solution isn't quite what you want, then the solution I would suggest is to bind to <<TreeviewSelect>> and de-select items you don't want to be selected by iterating over the selection. This event will fire whenever the selection changes, either by your code or a mouse click or via the keyboard.

That solution would look something like this:

...
        self.tree.bind("<<TreeviewSelect>>", self.treeSelect)
...

    def treeSelect(self, event):
        """Deselect all parent nodes"""
        items = self.tree.selection()
        for item in items:
            if len(self.tree.get_children(item)):
                self.tree.selection_remove(item)

This is probably the better solution since it lets the user to use the keyboard in addition to the mouse, and will always prevent all parents from being selected.

  • Related