0

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()  
Mister Mystère
  • 952
  • 2
  • 16
  • 39
  • 1
    Please refer to this guide on how to provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example), and read about [how to ask](https://stackoverflow.com/help/how-to-ask). Remember, we can't help you if we don't know what you've already tried. – JRiggles Oct 03 '22 at 19:28
  • I don't think this question requires a code snippet because it's more of a conceptual question (any lead is welcome) rather than a debugging question, and I've already said what I tried. I'll happily write such an example if someone else comes here and shares your opinion though. – Mister Mystère Oct 03 '22 at 19:31
  • That's fair - I've updated my post with a complete minimal example. In this example, the IIDs and texts are actually very close to how I have to handle them in my application. – Mister Mystère Oct 03 '22 at 19:50
  • Have you looked into using the `'<>'` virtual event instead of a mouse button binding? – JRiggles Oct 03 '22 at 19:53
  • No, but I've looked into TreeviewOpen and TreeviewClose in the way I've now added to my post and it did not work. I don't see how TreeviewSelect would be different than the mouse button, because everywhere I look I need to call the default handler... – Mister Mystère Oct 03 '22 at 20:16
  • It's still unclear to me what you are trying to achieve with this code. You just don't want to have *parents* selected? – Thingamabobs Oct 03 '22 at 20:21
  • Yes, I need to prevent the user from selecting a node. Only children. Because only the combination of parent and child defines a unique item to be processed (you can see that like directories for the nodes, and files for the children) – Mister Mystère Oct 03 '22 at 20:26
  • Should it at all be impossible to select a *parent/node* or just if any *child* is selected? – Thingamabobs Oct 03 '22 at 20:27
  • It should be impossible to select a parent/node at all times, because the user might request processing to happen at any given time and I process all the selected items. Which I cannot do with parents, even when some of their children are selected. I could just ignore parents when processing, but it gives the false impression to the user they selected the node (implying all children) which is confusing and misleading. – Mister Mystère Oct 03 '22 at 20:29
  • I would argue that the selection allows also orientation to the user. Anyway when you press shift, the selection should just select all children of this parent or every open parent? – Thingamabobs Oct 03 '22 at 20:47
  • I don't understand your last comment. The usual way shift is handled is that all items between the last selection and the new one are selected. In my case I don't need that to be done across multiple branches, just within one branch. – Mister Mystère Oct 03 '22 at 21:08

3 Answers3

1

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.

Thingamabobs
  • 7,274
  • 5
  • 21
  • 54
  • 1
    Thanks - the problem with that code though is that I lose my selection when I open or close a branch. However, you gave me an idea and I've added it as an answer so thank you for the help (+1). – Mister Mystère Oct 03 '22 at 22:13
1

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.


For more information about why returning "break" works the way that it does, see this answer to the question Basic query regarding bindtags in tkinter. That question deals with key presses, but mouse clicks are handled in exactly the same way.

Thingamabobs
  • 7,274
  • 5
  • 21
  • 54
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
0

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.

Mister Mystère
  • 952
  • 2
  • 16
  • 39