Autocheck children items in Tix with Python using CheckList and Hlist

1.7k views Asked by At

I am writing a small program in Python using Tix that builds and displays a tree view of certain folders (only the ones having a certain xml file in them) with checkbox capabilities. Every Item is a folder or subfolder that can be selected (checked). I used CheckList and Hlist for this

Based on this answer: I managed to display the folder structure that I want with checkboxes.

The problem is that I need the nested Items to be autochecked whenever a parent is checked so that I do not need to go through every Item under the same parent Item. I am having a lot of trouble with the documentation with Tix.

There seems to be contradictory information on the internet about the objects and methods, and they are often different depending on the source of information. I am almost certain that there is not a built-in feature in Hlist that enables this "autocheck" function so correct me if I am wrong I would have to develop it myself.

Any hints or ideas on this? I will post the pieces of code involved and the folder treeview

First I create the Checklist and find the directories I am interested in:

def startCheckList(self):

    self.cl = Tix.CheckList(self.testsFrame, browsecmd=self.selectItem, width=600, height=600)

    self.cl.hlist.configure(indent=20, pady=2, padx=2, bg='#eef4fa', relief=GROOVE, font=self.customFont)
    self.cl.pack()

    for root, dirs, files in os.walk(EA.TESTSFOLDER):
        for aFile in files:
            (fileName, extension) = os.path.splitext(aFile)
            if (fileName == EA.TESTNAME):
                self.testPaths.append(root)

Once I have the folder list I add the associated elements to the Hlist to be displayed

def display_paths(self):

    for path in self.testPaths:
        L = []
        path2list(L, path)

        self.create_recursive(L)

    self.cl.autosetmode()

If an element exists I do not create a new entry

def create_recursive(self, list):
    path = '.'.join(list)

    if path:

        if self.cl.hlist.info_exists(path) == '1':
            pass

        elif self.cl.hlist.info_exists(path) == '0':

            if list:

                self.create_recursive(list[:-1])
                self.cl.hlist.add(path, text=list[-1])
                self.cl.setstatus(path, "off")

How would you proceed?

View of the program so far

3

There are 3 answers

0
Son of a Beach On BEST ANSWER

I know this is an old post, but after struggling with this for a while, I've just come up with a solution.

According to the documentation, the reason for multiple events is that the browsecmd is triggered for mouse button down, mouse moved and mouse button up. So it will be fired at least twice - for mouse down and mouse up.

I've solved this problem by binding the checklist to the mouse up event, with a callback that sets a variable to indicate that a mouse up even has been received. My browsecmd function can then check the status of this variable, and if it is True, then it proceeds (AND THEN SETS THE VARIABLE TO False AGAIN). If it is False it ignores it because it is not a mouse up event.

Below is the relevant part of my code.

def __init__(self):
    # Insert your own init code here
    self.clMouseUpEvent = False
def checkListClicked(self, event):
    self.clMmouseUpEvent = True
def browseEvent(self, itemID):
    if self.clMmouseUpEvent:
        if self.cl.getstatus(itemID) == 'off':
            status = 'on'
        else:
            status = 'off'
        self.setChildrenStatus(itemID, status)
        self.clMmouseUpEvent = False
def setChildrenStatus(self, itemID, status):
    self.cl.setstatus(itemID, status)
    for childID in self.cl.hlist.info_children(itemID):
        self.setChildrenStatus(childID, status)
def someFunction(self):
    # Insert your own Tix check list set up code here
    self.cl.hlist.config(bg='white', selectbackground='white', selectforeground='black', header=True, browsecmd=self.browseEvent)
    self.cl.hlist.bind("<ButtonRelease-1>", self.checkListClicked)

UPDATE:

I subsequently found that clicking on the disclosure indicators also fired the checkListclicked function setting clMouseUpEvent to True which then resulted in the next click to an actual checkbox to process both the mouse down and mouse up events.

I have now modified my code to set the clMouseUpEvent variable to 0 instead of False and time.time() instead of True. Then in my browseEvent instead of checking for clMouseUpEvent == True it checks for time.time() - self.clMouseUpEvent < 0.01. This seems to be working OK now.

0
Jorge On

OK, as I couldnt find any built-in method for this I wrote the next code:

def selectItem(self, item):

    if self.cl.getstatus(item) == 'on':
        self.autoCheckChildren(item, True)
    if self.cl.getstatus(item) == 'off':
        self.autoCheckChildren(item, False)


def autoCheckChildren(self, i_item, stat):
    item = i_item
    if stat:
        if self.cl.hlist.info_children(item):
            for child in self.cl.hlist.info_children(item):
                self.cl.setstatus(child, "on")
                self.autoCheckChildren(child, True)
    elif not stat:
        if self.cl.hlist.info_children(item):
            for child in self.cl.hlist.info_children(item):
                self.cl.setstatus(child, "off")
                self.autoCheckChildren(child, False)

Where selectItem is the function called when a checkbutton is selected. I used recursion again but this might not be the best solution.

Another thing that I found is that when selecting a checkbox (any one from the checklist) the associated function specified by browsecmd=function is always called twice. I read somewhere that this is due to the fact that the mouse pressed and released are considered as two events.

This is different from the behaviour of the single checkbutton from Tkinter, where the associated function is only called once. I do not know why there is such difference. Would it be possible to call this function only once?

0
Dennis On

I used a similar approach as @Son of a Beach did, but I also changed the indicatorcmd in the hlist of the CheckList. This command gets ran every time the indicator (collapse button) is pressed it seems, and overwriting it made it so it no longer triggers the browsecmd.

This is the code:

        def toggle_path(tree: tix.CheckList, path: str):
            # delay clicks to not trigger it twice
            if time.time() - self._event_toggle > App.TOGGLE_EVENT_DELAY:
                getattr(tree, tree.getmode(path))(path)
            self._event_toggle = time.time()

        self.checklist = tix.CheckList(self.master,
                                      browsecmd=lambda path: self.select(self.checklist, path),
                                      command=lambda path: self.select(self.checklist, path))
        self.checklist.hlist.configure(indicatorcmd=lambda path: toggle_path(self.to_tree, path))

I hope this could be of any help to people using this in the future!