How to create a tree view with checkboxes in Python How to create a tree view with checkboxes in Python tkinter tkinter

How to create a tree view with checkboxes in Python


enter image description here

import Tixclass View(object):    def __init__(self, root):        self.root = root        self.makeCheckList()    def makeCheckList(self):        self.cl = Tix.CheckList(self.root, browsecmd=self.selectItem)        self.cl.pack()        self.cl.hlist.add("CL1", text="checklist1")        self.cl.hlist.add("CL1.Item1", text="subitem1")        self.cl.hlist.add("CL2", text="checklist2")        self.cl.hlist.add("CL2.Item1", text="subitem1")        self.cl.setstatus("CL2", "on")        self.cl.setstatus("CL2.Item1", "on")        self.cl.setstatus("CL1", "off")        self.cl.setstatus("CL1.Item1", "off")        self.cl.autosetmode()    def selectItem(self, item):        print item, self.cl.getstatus(item)def main():    root = Tix.Tk()    view = View(root)    root.update()    root.mainloop()if __name__ == '__main__':    main()


I made a treeview class with checkboxes inheriting ttk.Treeview, but the checkboxes are not ttk.Checkbutton but images of checked, unchecked and tristate checkboxes.

import tkinter as tkimport tkinter.ttk as ttkclass CheckboxTreeview(ttk.Treeview):    """        Treeview widget with checkboxes left of each item.        The checkboxes are done via the image attribute of the item, so to keep        the checkbox, you cannot add an image to the item.    """    def __init__(self, master=None, **kw):        ttk.Treeview.__init__(self, master, **kw)        # checkboxes are implemented with pictures        self.im_checked = tk.PhotoImage(file='checked.png')        self.im_unchecked = tk.PhotoImage(file='unchecked.png')        self.im_tristate = tk.PhotoImage(file='tristate.png')        self.tag_configure("unchecked", image=self.im_unchecked)        self.tag_configure("tristate", image=self.im_tristate)        self.tag_configure("checked", image=self.im_checked)        # check / uncheck boxes on click        self.bind("<Button-1>", self.box_click, True)    def insert(self, parent, index, iid=None, **kw):        """ same method as for standard treeview but add the tag 'unchecked'            automatically if no tag among ('checked', 'unchecked', 'tristate')            is given """        if not "tags" in kw:            kw["tags"] = ("unchecked",)        elif not ("unchecked" in kw["tags"] or "checked" in kw["tags"]                  or "tristate" in kw["tags"]):            kw["tags"] = ("unchecked",)        ttk.Treeview.insert(self, parent, index, iid, **kw)    def check_descendant(self, item):        """ check the boxes of item's descendants """        children = self.get_children(item)        for iid in children:            self.item(iid, tags=("checked",))            self.check_descendant(iid)    def check_ancestor(self, item):        """ check the box of item and change the state of the boxes of item's            ancestors accordingly """        self.item(item, tags=("checked",))        parent = self.parent(item)        if parent:            children = self.get_children(parent)            b = ["checked" in self.item(c, "tags") for c in children]            if False in b:                # at least one box is not checked and item's box is checked                self.tristate_parent(parent)            else:                # all boxes of the children are checked                self.check_ancestor(parent)    def tristate_parent(self, item):        """ put the box of item in tristate and change the state of the boxes of            item's ancestors accordingly """        self.item(item, tags=("tristate",))        parent = self.parent(item)        if parent:            self.tristate_parent(parent)    def uncheck_descendant(self, item):        """ uncheck the boxes of item's descendant """        children = self.get_children(item)        for iid in children:            self.item(iid, tags=("unchecked",))            self.uncheck_descendant(iid)    def uncheck_ancestor(self, item):        """ uncheck the box of item and change the state of the boxes of item's            ancestors accordingly """        self.item(item, tags=("unchecked",))        parent = self.parent(item)        if parent:            children = self.get_children(parent)            b = ["unchecked" in self.item(c, "tags") for c in children]            if False in b:                # at least one box is checked and item's box is unchecked                self.tristate_parent(parent)            else:                # no box is checked                self.uncheck_ancestor(parent)    def box_click(self, event):        """ check or uncheck box when clicked """        x, y, widget = event.x, event.y, event.widget        elem = widget.identify("element", x, y)        if "image" in elem:            # a box was clicked            item = self.identify_row(y)            tags = self.item(item, "tags")            if ("unchecked" in tags) or ("tristate" in tags):                self.check_ancestor(item)                self.check_descendant(item)            else:                self.uncheck_descendant(item)                self.uncheck_ancestor(item)if __name__ == '__main__':    root = tk.Tk()    t = CheckboxTreeview(root, show="tree")    t.pack()    t.insert("", 0, "1", text="1")    t.insert("1", "end", "11", text="1")    t.insert("1", "end", "12", text="2")    t.insert("12", "end", "121", text="1")    t.insert("12", "end", "122", text="2")    t.insert("122", "end", "1221", text="1")    t.insert("1", "end", "13", text="3")    t.insert("13", "end", "131", text="1")    root.mainloop()

An improved version of CheckboxTreeview is available in the ttkwidgets module.


If you can use Tix, go with @Brandon's solution. If you are stuck with Ttk (as I am), here is an solution based on @j_4231's idea. Rather than using an image to represent the checkbox, we can use two characters provided by Unicode:

  • 'BALLOT BOX' (U+2610) :
  • 'BALLOT BOX WITH X (U+2612)' : .

Those character are located after the item name and are used to check the current state: treeview.item(iid, "text")[-1] is either or . We can update the item name when the text is clicked.

The class TtkCheckList inherits ttk.Treeview, hence the usual parameters/methods of Treeview can be used.

import tkinter as tkfrom tkinter import ttkBALLOT_BOX = "\u2610"BALLOT_BOX_WITH_X = "\u2612"class TtkCheckList(ttk.Treeview):    def __init__(self, master=None, width=200, clicked=None, separator='.',                 unchecked=BALLOT_BOX, checked=BALLOT_BOX_WITH_X, **kwargs):        """        :param width: the width of the check list        :param clicked: the optional function if a checkbox is clicked. Takes a                        `iid` parameter.        :param separator: the item separator (default is `'.'`)        :param unchecked: the character for an unchecked box (default is                          "\u2610")        :param unchecked: the character for a checked box (default is "\u2612")        Other parameters are passed to the `TreeView`.        """        if "selectmode" not in kwargs:            kwargs["selectmode"] = "none"        if "show" not in kwargs:            kwargs["show"] = "tree"        ttk.Treeview.__init__(self, master, **kwargs)        self._separator = separator        self._unchecked = unchecked        self._checked = checked        self._clicked = self.toggle if clicked is None else clicked        self.column('#0', width=width, stretch=tk.YES)        self.bind("<Button-1>", self._item_click, True)    def _item_click(self, event):        assert event.widget == self        x, y = event.x, event.y        element = self.identify("element", x, y)        if element == "text":            iid = self.identify_row(y)            self._clicked(iid)    def add_item(self, item):        """        Add an item to the checklist. The item is the list of nodes separated        by dots: `Item.SubItem.SubSubItem`. **This item is used as `iid`  at        the underlying `Treeview` level.**        """        try:            parent_iid, text = item.rsplit(self._separator, maxsplit=1)        except ValueError:            parent_iid, text = "", item        self.insert(parent_iid, index='end', iid=item,                    text=text+" "+self._unchecked, open=True)    def toggle(self, iid):        """        Toggle the checkbox `iid`        """        text = self.item(iid, "text")        checked = text[-1] == self._checked        status = self._unchecked if checked else self._checked        self.item(iid, text=text[:-1] + status)    def checked(self, iid):        """        Return True if checkbox `iid` is checked        """        text = self.item(iid, "text")        return text[-1] == self._checked    def check(self, iid):        """        Check the checkbox `iid`        """        text = self.item(iid, "text")        if text[-1] == self._unchecked:            self.item(iid, text=text[:-1] + self._checked)    def uncheck(self, iid):        """        Uncheck the checkbox `iid`        """        text = self.item(iid, "text")        if text[-1] == self._checked:            self.item(iid, text=text[:-1] + self._unchecked)

Here is an example:

items = [    'Item',    'Item.SubItem1',    'Item.SubItem2',    'Item.SubItem2.SubSubItem1',    'Item.SubItem2.SubSubItem2',    'Item.SubItem2.SubSubItem3',    'Item.SubItem3',    'Item.SubItem3.SubSubItem1',    'Item.SubItem4']root = tk.Tk()root.title('Test')root.geometry('400x300')check_list = TtkCheckList(root, height=len(items))for item in items:    check_list.add_item(item)check_list.pack()root.mainloop()

You can use the clicked parameter to define a new behavior when an item isclicked. For instance:

def obey_ancestor(iid):    """    If the status of an item is toggled, the status of all its descendants    is also set to the new status.    """    set_status = check_list.uncheck if check_list.checked(iid) else check_list.check    stack = [iid]    while stack:        iid = stack.pop()        set_status(iid)        stack.extend(check_list.get_children(iid))

And:

check_list = TtkCheckList(root, height=len(items),                      clicked=obey_ancestor)