# -*- coding: utf-8 -*-
"""
Author: Juliette Monsel
License: GNU GPLv3
Source: This repository
Treeview with checkboxes at each item and a noticeable disabled style
"""
try:
import ttk
except ImportError:
from tkinter import ttk
import os
from PIL import Image, ImageTk
from ttkwidgets.utilities import get_assets_directory
IM_CHECKED = os.path.join(get_assets_directory(), "checked.png") # These three checkbox icons were isolated from
IM_UNCHECKED = os.path.join(get_assets_directory(), "unchecked.png") # Checkbox States.svg (https://commons.wikimedia.org/wiki/File:Checkbox_States.svg?uselang=en)
IM_TRISTATE = os.path.join(get_assets_directory(), "tristate.png") # by Marekich [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)]
[docs]class CheckboxTreeview(ttk.Treeview):
"""
:class:`ttk.Treeview` widget with checkboxes left of each item.
.. note::
The checkboxes are done via the image attribute of the item,
so to keep the checkbox, you cannot add an image to the item.
"""
[docs] def __init__(self, master=None, **kw):
"""
Create a CheckboxTreeview.
:param master: master widget
:type master: widget
:param kw: options to be passed on to the :class:`ttk.Treeview` initializer
"""
ttk.Treeview.__init__(self, master, style='Checkbox.Treeview', **kw)
# style (make a noticeable disabled style)
style = ttk.Style(self)
style.map("Checkbox.Treeview",
fieldbackground=[("disabled", '#E6E6E6')],
foreground=[("disabled", 'gray40')],
background=[("disabled", '#E6E6E6')])
# checkboxes are implemented with pictures
self.im_checked = ImageTk.PhotoImage(Image.open(IM_CHECKED), master=self)
self.im_unchecked = ImageTk.PhotoImage(Image.open(IM_UNCHECKED), master=self)
self.im_tristate = ImageTk.PhotoImage(Image.open(IM_TRISTATE), master=self)
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)
[docs] def expand_all(self):
"""Expand all items."""
def aux(item):
self.item(item, open=True)
children = self.get_children(item)
for c in children:
aux(c)
children = self.get_children("")
for c in children:
aux(c)
[docs] def collapse_all(self):
"""Collapse all items."""
def aux(item):
self.item(item, open=False)
children = self.get_children(item)
for c in children:
aux(c)
children = self.get_children("")
for c in children:
aux(c)
[docs] def state(self, statespec=None):
"""
Modify or inquire widget state.
:param statespec: Widget state is returned if `statespec` is None,
otherwise it is set according to the statespec
flags and then a new state spec is returned
indicating which flags were changed.
:type statespec: None or sequence[str]
"""
if statespec:
if "disabled" in statespec:
self.bind('<Button-1>', lambda e: 'break')
elif "!disabled" in statespec:
self.unbind("<Button-1>")
self.bind("<Button-1>", self._box_click, True)
return ttk.Treeview.state(self, statespec)
else:
return ttk.Treeview.state(self)
[docs] def change_state(self, item, state):
"""
Replace the current state of the item.
i.e. replace the current state tag but keeps the other tags.
:param item: item id
:type item: str
:param state: "checked", "unchecked" or "tristate": new state of the item
:type state: str
"""
tags = self.item(item, "tags")
states = ("checked", "unchecked", "tristate")
new_tags = [t for t in tags if t not in states]
new_tags.append(state)
self.item(item, tags=tuple(new_tags))
[docs] def tag_add(self, item, tag):
"""
Add tag to the tags of item.
:param item: item identifier
:type item: str
:param tag: tag name
:type tag: str
"""
tags = self.item(item, "tags")
self.item(item, tags=tags + (tag,))
[docs] def tag_del(self, item, tag):
"""
Remove tag from the tags of item.
:param item: item identifier
:type item: str
:param tag: tag name
:type tag: str
"""
tags = list(self.item(item, "tags"))
if tag in tags:
tags.remove(tag)
self.item(item, tags=tuple(tags))
[docs] def insert(self, parent, index, iid=None, **kw):
"""
Creates a new item and return the item identifier of the newly created item.
:param parent: identifier of the parent item
:type parent: str
:param index: where in the list of parent's children to insert the new item
:type index: int or "end"
:param iid: item identifier, iid must not already exist in the tree. If iid is None a new unique identifier is generated.
:type iid: None or str
:param kw: other options to be passed on to the :meth:`ttk.Treeview.insert` method
:return: the item identifier of the newly created item
:rtype: str
.. note:: Same method as for the standard :class:`ttk.Treeview` but
add the tag for the box state accordingly to the parent
state if no tag among
('checked', 'unchecked', 'tristate') is given.
"""
if self.tag_has("checked", parent):
tag = "checked"
else:
tag = 'unchecked'
if "tags" not in kw:
kw["tags"] = (tag,)
elif not ("unchecked" in kw["tags"] or "checked" in kw["tags"] or
"tristate" in kw["tags"]):
kw["tags"] += (tag,)
return ttk.Treeview.insert(self, parent, index, iid, **kw)
[docs] def get_checked(self):
"""Return the list of checked items that do not have any child."""
checked = []
def get_checked_children(item):
if not self.tag_has("unchecked", item):
ch = self.get_children(item)
if not ch and self.tag_has("checked", item):
checked.append(item)
else:
for c in ch:
get_checked_children(c)
ch = self.get_children("")
for c in ch:
get_checked_children(c)
return checked
def _check_descendant(self, item):
"""Check the boxes of item's descendants."""
children = self.get_children(item)
for iid in children:
self.change_state(iid, "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.change_state(item, "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.change_state(item, "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.change_state(iid, "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.change_state(item, "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)
if self.tag_has("unchecked", item) or self.tag_has("tristate", item):
self._check_ancestor(item)
self._check_descendant(item)
else:
self._uncheck_descendant(item)
self._uncheck_ancestor(item)