Tkhtml with python 3.5 - python

I was searching for Tkinter widget for displaying webpage/Url/ads in my Tkinter window widget.and after some google/stack search, I found a widget called (tk_html_widget) and this page where I found Tkhtml...but I don't know how to use both(tk_html_widgets and Tkhtml). Please refer me something for study these(tk_html_widgets and Tkhtml)
Thank you

for an example of hyperlink type behaviour:
__all__ = ["LinkScrolledText"]
try:
from tkinter import scrolledtext
except ImportError:
import ScrolledText as scrolledtext
class HyperlinkManager(object):
"""A class to easily add clickable hyperlinks to Text areas.
Usage:
callback = lambda : webbrowser.open("http://www.google.com/")
text = tk.Text(...)
hyperman = tkHyperlinkManager.HyperlinkManager(text)
text.insert(tk.INSERT, "click me", hyperman.add(callback))
From http://effbot.org/zone/tkinter-text-hyperlink.htm
"""
def __init__(self, text, statusfunc=None):
self.text = text
self.statusfunc = statusfunc
self.text.tag_config("hyper", foreground="blue", underline=1)
self.text.tag_bind("hyper", "<Enter>", self._enter)
self.text.tag_bind("hyper", "<Leave>", self._leave)
self.text.tag_bind("hyper", "<Button-1>", self._click)
self.reset()
def reset(self):
self.links = {}
def add(self, action, tooltip=None):
"""Adds an action to the manager.
:param action: A func to call.
:return: A clickable tag to use in the text widget.
"""
tag = "hyper-%d" % len(self.links)
self.links[tag] = [action, tooltip]
return ("hyper", tag)
def _enter(self, event):
self.text.config(cursor="hand2")
for tag in self.text.tag_names(tk.CURRENT):
if (tag[:6] == "hyper-"):
tooltip = self.links[tag][1]
if self.statusfunc:
self.statusfunc(tooltip) # don't care if no tooltip as function clears if it doesn't
return
def _leave(self, event):
self.text.config(cursor="")
if self.statusfunc:
self.statusfunc()
def _click(self, event):
for tag in self.text.tag_names(tk.CURRENT):
if (tag[:6] == "hyper-"):
func = self.links[tag][0]
if func:
func()
return
class LinkScrolledText(scrolledtext.ScrolledText):
"""A class to add hyperlink functionality to a scrolledtext widget
the link does not actually have to be an actual hyperlink,
just a callable action.
an optional tooltip can be provided that will be displayed in the bottom left
just like a url would be in a browser when hovering over a link.
"""
def __init__(self, master=None, *args, **kwargs):
scrolledtext.ScrolledText.__init__(self, master, *args, **kwargs)
self.status = tk.Label(self)
self._hyper = HyperlinkManager(self, self._showstatus)
self.reset_links()
def _showstatus(self, status=None):
if status:
self.status.configure(text=status)
self.status.place(relx=0, rely=1, anchor='sw')
else:
self.status.place_forget()
def reset_links(self):
self._hyper.reset()
def insert_hyperlink(self, position, text, action, tag=None, tooltip=None):
tags = self._hyper.add(action, tooltip)
if type(tag) == list:
tags = tags + tag
elif tag != None:
tags.append(tag)
self.insert(position, text, tags)
if __name__ == "__main__":
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
root = tk.Tk()
tb = LinkScrolledText(root)
tb.pack(fill="both", expand=True)
tb.insert_hyperlink("end", "Test", action=None, tooltip="This is a test")
root.mainloop()

Related

How does one get tooltips working for notebook tabs in python tkinter?

I'm working with python 3.8 and tkinter to create a GUI that uses a ttk notebook (tabbed window). I want to have tooltips displayed for the individual tabs but I can't see how to add the tooltip for the tab. I have a class defined that takes an object and text and adds the tooltip and it works for buttons and other objects.
I have example code here:
'''
At no time were Katz, Dawgs, Mice or Squirrels harmed in any way during the development of this application. But...
Well just lets say that skunk next door had it coming!
'''
import tkinter as tk
from tkinter import ttk
class CreateToolTip(object):
"""
create a tooltip for a given widget
"""
def __init__(self, widget, text='widget info'):
self.waittime = 250 #miliseconds
self.wraplength = 180 #pixels
self.widget = widget
self.text = text
self.widget.bind("<Enter>", self.enter)
self.widget.bind("<Leave>", self.leave)
self.widget.bind("<ButtonPress>", self.leave)
self.id = None
self.tw = None
def enter(self, event=None):
self.schedule()
def leave(self, event=None):
self.unschedule()
self.hidetip()
def schedule(self):
self.unschedule()
self.id = self.widget.after(self.waittime, self.showtip)
def unschedule(self):
id = self.id
self.id = None
if id:
self.widget.after_cancel(id)
def showtip(self, event=None):
x = y = 0
x, y, cx, cy = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 25
y += self.widget.winfo_rooty() + 20
# creates a toplevel window
self.tw = tk.Toplevel(self.widget)
# Leaves only the label and removes the app window
self.tw.wm_overrideredirect(True)
self.tw.wm_geometry("+%d+%d" % (x, y))
label = tk.Label(self.tw, text=self.text, justify='left',
background="cornsilk", relief='solid', borderwidth=1,
wraplength = self.wraplength)
label.pack(ipadx=1)
def hidetip(self):
tw = self.tw
self.tw= None
if tw:
tw.destroy()
class MyCanvas(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tabControl = ttk.Notebook(self)
self.geometry('600x300')
self.title('An Indepth study of the Differences Between Katz and Dawgs!')
self.tab = {}
self.tab['dawg tab'] = ttk.Frame(self.tabControl)
self.tabControl.add(self.tab['dawg tab'], text='dawgs', state='normal')
self.tab['katz tab'] = ttk.Frame(self.tabControl)
self.tabControl.add(self.tab['katz tab'], text='katz', state='normal')
self.tabControl.enable_traversal()
btn1 = tk.Button(self.tab['dawg tab'], text='Squirrel')
btn1.grid(row=1, column=0)
CreateToolTip(btn1, 'Despised by dawgs!!')
btn2 = tk.Button(self.tab['katz tab'], text='Mice')
btn2.grid(row=1, sticky='EW')
CreateToolTip(btn2, 'Katz find them a tasty treat.')
self.tabControl.grid(row=0, column=0, sticky="W")
# testing ...
if __name__ == '__main__':
root = MyCanvas()
root.mainloop()
Any advice or constructive comments would be greatly appreciated. And yes, the tooltip class was totally plagiarized from someone else here on stack overflow.
Cheers!!
So after a few days and lots of help from a friend here is what I have that actually works:
'''
At no time were Katz, Dawgs, Mice or Squirrels harmed in any way during the development of this application. But...
Well lets just say that skunk next door had it coming!
'''
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.scrolledtext as tkst
class MyCanvas(ttk.Notebook):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tooltips = dict()
self.tab_list = dict()
self.current_tab = None
self.waittime = 250 #miliseconds
self.wraplength = 180 #pixels
self.id = None
self.tw = None
self.bind('<Motion>', self.motion)
self.bind('<ButtonPress>', self.leave)
self.line_number = 0
def leave(self, event=None):
self.unschedule()
self.hide_tip()
def schedule(self):
self.unschedule()
self.id = self.after(self.waittime, self.show_tip)
def unschedule(self):
id = self.id
self.id = None
if id:
self.after_cancel(id)
def show_tip(self):
if self.tw:
return
if self.current_tab in self.tooltips.keys():
x = y = 0
x, y, cx, cy = self.bbox('insert')
x += self.winfo_rootx() + 25
y += self.winfo_rooty() - 20
# creates a toplevel window
self.tw = tk.Toplevel(self)
# Leaves only the label and removes the app window
self.tw.wm_overrideredirect(True)
self.tw.wm_geometry(f'+{x}+{y}')
self.tw.wm_attributes('-topmost', True)
label = ttk.Label(self.tw, text=self.tooltips[self.current_tab], background='cornsilk')
label.pack()
def add(self, *args, **kwargs):
if 'tooltip' in kwargs.keys():
tooltip = kwargs['tooltip']
del kwargs['tooltip']
else:
tooltip = None
self.tab_list[kwargs.get('text')] = args[0]
super().add(*args, **kwargs)
if tooltip and ('text' in kwargs.keys()):
self.set_tooltip(kwargs['text'], tooltip)
def set_tooltip(self, tab_text, tooltip):
for idx in range(self.index('end')):
if self.tab(idx, 'text') == tab_text:
self.tooltips[tab_text] = tooltip
return True
return False
def motion(self, event):
if self.identify(event.x, event.y) == 'label':
index = self.index(f'#{event.x},{event.y}')
if self.current_tab != self.tab(index, 'text'):
self.current_tab = self.tab(index, 'text')
self.show_tip()
else:
self.hide_tip()
self.current_tab = None
def hide_tip(self):
tw = self.tw
self.tw = None
if tw:
tw.destroy()
def insert_text_to_tab(self, **kwargs):
string_var = tk.StringVar()
if 'tab' in kwargs.keys():
the_tab = kwargs.get('tab')
string_var.set(kwargs.get('data'))
the_label = tk.Label(self.tab_list[the_tab], textvariable=string_var, background='lightblue')
the_label.pack()
def get_scrolled_text_widget(parent):
st_widget = tkst.ScrolledText(parent)
return st_widget
if __name__ == '__main__':
root = tk.Tk()
root.title('An Indepth study of the Differences Between Katz and Dawgs!')
canvas = MyCanvas(root)
canvas.pack(fill='both', expand=1)
canvas.add(get_scrolled_text_widget(canvas), text='Dawgz', tooltip='A quadraped that doesn\'t look like a kat')
canvas.add(get_scrolled_text_widget(canvas), text='Katz', tooltip='A quadraped that doesn\'t look like a dawg')
canvas.insert_text_to_tab(data='Dawgz hate squirrels!!', tab='Dawgz')
canvas.insert_text_to_tab(data='Katz find mice a tasty treat.', tab='Katz')
root.mainloop()
So it would appear that I was thinking about it a bit wrong and I had only to think of each tabbed entry as an object instead of an attribute (I'm sure the python peanut gallery are going to be all over me for saying that) the solution is quit straight forward.
Cheers!!

python tkinter tooltip on modal window

I searched for ways of implementing tooltips for an application and I found in some comment or answer in this site some while ago a link to this page.
I've been using this class since then and I've been happy with the result.
But recently I noticed that the tooltips came up behind modal windows, when they refer to widgets on that modal window.
Below in the code downloaded from that GitHub link, where I just made the changes of replacing from tkinter import * with import tkinter as tk, and using the prefix tk throughout the code accordingly.
"""Tools for displaying tool-tips.
This includes:
* an abstract base-class for different kinds of tooltips
* a simple text-only Tooltip class
"""
import tkinter as tk
class TooltipBase:
"""abstract base class for tooltips"""
def __init__(self, anchor_widget):
"""Create a tooltip.
anchor_widget: the widget next to which the tooltip will be shown
Note that a widget will only be shown when showtip() is called.
"""
self.anchor_widget = anchor_widget
self.tipwindow = None
def __del__(self):
self.hidetip()
def showtip(self):
"""display the tooltip"""
if self.tipwindow:
return
self.tipwindow = tw = tk.Toplevel(self.anchor_widget)
# show no border on the top level window
tw.wm_overrideredirect(1)
try:
# This command is only needed and available on Tk >= 8.4.0 for OSX.
# Without it, call tips intrude on the typing process by grabbing
# the focus.
tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
"help", "noActivates")
except tk.TclError:
pass
self.position_window()
self.showcontents()
self.tipwindow.update_idletasks() # Needed on MacOS -- see #34275.
self.tipwindow.lift() # work around bug in Tk 8.5.18+ (issue #24570)
def position_window(self):
"""(re)-set the tooltip's screen position"""
x, y = self.get_position()
root_x = self.anchor_widget.winfo_rootx() + x
root_y = self.anchor_widget.winfo_rooty() + y
self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y))
def get_position(self):
"""choose a screen position for the tooltip"""
# The tip window must be completely outside the anchor widget;
# otherwise when the mouse enters the tip window we get
# a leave event and it disappears, and then we get an enter
# event and it reappears, and so on forever :-(
#
# Note: This is a simplistic implementation; sub-classes will likely
# want to override this.
return 20, self.anchor_widget.winfo_height() + 1
def showcontents(self):
"""content display hook for sub-classes"""
# See ToolTip for an example
raise NotImplementedError
def hidetip(self):
"""hide the tooltip"""
# Note: This is called by __del__, so careful when overriding/extending
tw = self.tipwindow
self.tipwindow = None
if tw:
try:
tw.destroy()
except tk.TclError: # pragma: no cover
pass
class OnHoverTooltipBase(TooltipBase):
"""abstract base class for tooltips, with delayed on-hover display"""
def __init__(self, anchor_widget, hover_delay=1000):
"""Create a tooltip with a mouse hover delay.
anchor_widget: the widget next to which the tooltip will be shown
hover_delay: time to delay before showing the tooltip, in milliseconds
Note that a widget will only be shown when showtip() is called,
e.g. after hovering over the anchor widget with the mouse for enough
time.
"""
super(OnHoverTooltipBase, self).__init__(anchor_widget)
self.hover_delay = hover_delay
self._after_id = None
self._id1 = self.anchor_widget.bind("<Enter>", self._show_event)
self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event)
self._id3 = self.anchor_widget.bind("<Button>", self._hide_event)
def __del__(self):
try:
self.anchor_widget.unbind("<Enter>", self._id1)
self.anchor_widget.unbind("<Leave>", self._id2) # pragma: no cover
self.anchor_widget.unbind("<Button>", self._id3) # pragma: no cover
except tk.TclError: # pragma: no cover
pass
super(OnHoverTooltipBase, self).__del__()
def _show_event(self, event=None):
"""event handler to display the tooltip"""
if self.hover_delay:
self.schedule()
else:
self.showtip()
def _hide_event(self, event=None):
"""event handler to hide the tooltip"""
self.hidetip()
def schedule(self):
"""schedule the future display of the tooltip"""
self.unschedule()
self._after_id = self.anchor_widget.after(self.hover_delay,
self.showtip)
def unschedule(self):
"""cancel the future display of the tooltip"""
after_id = self._after_id
self._after_id = None
if after_id:
self.anchor_widget.after_cancel(after_id)
def hidetip(self):
"""hide the tooltip"""
try:
self.unschedule()
except tk.TclError: # pragma: no cover
pass
super(OnHoverTooltipBase, self).hidetip()
def showcontents(self):
"""content display hook for sub-classes"""
# See ToolTip for an example
raise NotImplementedError
class Hovertip(OnHoverTooltipBase):
"""A tooltip that pops up when a mouse hovers over an anchor widget."""
def __init__(self, anchor_widget, text, hover_delay=1000):
"""Create a text tooltip with a mouse hover delay.
anchor_widget: the widget next to which the tooltip will be shown
hover_delay: time to delay before showing the tooltip, in milliseconds
Note that a widget will only be shown when showtip() is called,
e.g. after hovering over the anchor widget with the mouse for enough
time.
"""
super(Hovertip, self).__init__(anchor_widget, hover_delay=hover_delay)
self.text = text
def showcontents(self):
label = tk.Label(self.tipwindow, text=self.text, justify=tk.LEFT,
background="#ffffe0", relief=tk.SOLID, borderwidth=1)
label.pack()
Now some code illustrating the problem I'm having:
class PopupWindow:
def __init__(self, parent):
self.parent = parent
self.gui = tk.Toplevel(self.parent)
self.gui.geometry("100x30")
self.gui.wait_visibility()
self.ok_button = tk.Button(self.gui, text="OK", command=self.on_ok_button)
self.ok_button.pack()
Hovertip(self.ok_button, text="OK button", hover_delay=500)
def on_ok_button(self):
self.gui.destroy()
def show(self):
self.gui.grab_set()
# Hovertip(self.ok_button, text="OK button", hover_delay=500)
self.gui.wait_window()
return 0
class App(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
button = tk.Button(parent, text="Button -- no hover delay", command=self.button)
button.pack()
Hovertip(button, "This is tooltip\ntext for button.", hover_delay=0)
def button(self):
window = PopupWindow(self.parent)
window.show()
if __name__ == '__main__':
root = tk.Tk()
App(root)
root.mainloop()
You'll notice that the tooltip for the OK button in the modal window appears behind the window (I'm changing the geometry of the window for otherwise it would be so small that we wouldn't actually notice this).
Of course this becomes a problem in a real window with several widgets the tips for some of them will not be seen at all.
Apparently there are two ways around the problem: one is to delete the line self.gui.wait_visibility() in the __init__ of the PopupWindow class;
the other is to delete the self.gui.grab_set() in the show() method.
With any of these the window is no longer modal (if I get the meaning right: I mean I want the window to stay on top and prevent changes in the parent window while it exists).
The commented line in the show method was my tentative of working around it by defining the tooltip after the grab_set so that it might come on top, but it doesn't work either.
I suppose there must be an way of doing this properly using this class for tooltips.
How can I do it?
Thanks.
I found a solution.
Apparently, someone had the same problem, although with a different class for tooltips.
I refer to this question and answer.
The solution is to add the line tw.wm_attributes("-topmost", 1) somewhere in the showtip method of the TooltipBase class.
I did it as a last line, but I'm not sure it doesn't work some other place; I know it doesn't if immediately after tw.wm_overrideredirect(1).

CheckableComboBox in PyQGIS (waiting for choice)

I am using this code in QGIS's built-in python-editor.
How can I make the script wait while I mark the necessary lines, and only then continue execution? So far, I cannot do this: the window starts, but the script continues to execute and therefore there is no way to use the select list. For example, can I somehow add the "Ok" button there (as in a standard dialog box)?
from qgis.PyQt import *
from qgis.core import *
class CheckableComboBox(QComboBox):
# Subclass Delegate to increase item height
class Delegate(QStyledItemDelegate):
def sizeHint(self, option, index):
size = super().sizeHint(option, index)
size.setHeight(20)
return size
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make the combo editable to set a custom text, but readonly
self.setEditable(True)
self.lineEdit().setReadOnly(True)
# Make the lineedit the same color as QPushButton
palette = qApp.palette()
palette.setBrush(QPalette.Base, palette.button())
self.lineEdit().setPalette(palette)
# Use custom delegate
self.setItemDelegate(CheckableComboBox.Delegate())
# Update the text when an item is toggled
self.model().dataChanged.connect(self.updateText)
# Hide and show popup when clicking the line edit
self.lineEdit().installEventFilter(self)
self.closeOnLineEditClick = False
# Prevent popup from closing when clicking on an item
self.view().viewport().installEventFilter(self)
def resizeEvent(self, event):
# Recompute text to elide as needed
self.updateText()
super().resizeEvent(event)
def eventFilter(self, object, event):
if object == self.lineEdit():
if event.type() == QEvent.MouseButtonRelease:
if self.closeOnLineEditClick:
self.hidePopup()
else:
self.showPopup()
return True
return False
if object == self.view().viewport():
if event.type() == QEvent.MouseButtonRelease:
index = self.view().indexAt(event.pos())
item = self.model().item(index.row())
if item.checkState() == Qt.Checked:
item.setCheckState(Qt.Unchecked)
else:
item.setCheckState(Qt.Checked)
return True
return False
def showPopup(self):
super().showPopup()
# When the popup is displayed, a click on the lineedit should close it
self.closeOnLineEditClick = True
def hidePopup(self):
super().hidePopup()
# Used to prevent immediate reopening when clicking on the lineEdit
self.startTimer(100)
# Refresh the display text when closing
self.updateText()
def timerEvent(self, event):
# After timeout, kill timer, and reenable click on line edit
self.killTimer(event.timerId())
self.closeOnLineEditClick = False
def updateText(self):
texts = []
for i in range(self.model().rowCount()):
if self.model().item(i).checkState() == Qt.Checked:
texts.append(self.model().item(i).text())
text = ", ".join(texts)
# Compute elided text (with "...")
metrics = QFontMetrics(self.lineEdit().font())
elidedText = metrics.elidedText(text, Qt.ElideRight, self.lineEdit().width())
self.lineEdit().setText(elidedText)
def addItem(self, text, data=None):
item = QStandardItem()
item.setText(text)
if data is None:
item.setData(text)
else:
item.setData(data)
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
item.setData(Qt.Unchecked, Qt.CheckStateRole)
self.model().appendRow(item)
def addItems(self, texts, datalist=None):
for i, text in enumerate(texts):
try:
data = datalist[i]
except (TypeError, IndexError):
data = None
self.addItem(text, data)
def currentData(self):
# Return the list of selected items data
res = []
for i in range(self.model().rowCount()):
if self.model().item(i).checkState() == Qt.Checked:
res.append(self.model().item(i).data())
return res
print("Starting...")
comunes = ['Ameglia', 'Arcola', 'Bagnone', 'Bolano', 'Carrara', 'Casola', 'Barnaul', 'London']
combo = CheckableComboBox()
combo.resize(350,300)
combo.setWindowTitle('CheckableComboBox')
combo.setWindowFlags(Qt.WindowStaysOnTopHint)
combo.show()
combo.addItems(comunes)
print(combo.currentData())
The problem solved by adding checkable_combobox to the QDialog. Here is the code using QgsCheckableComboBox():
https://gis.stackexchange.com/questions/376901/how-to-put-qgscheckablecombobox-into-standby
from qgis.PyQt import QtGui
from qgis.core import *
planet_list = ["Venus", "Earth", "Mars", "Jupiter", "Pluto"]
items = QgsCheckableComboBox()
items.addItems(planet_list)
dlg = QDialog()
layout = QVBoxLayout()
layout.addWidget(items)
dlg.setLayout(layout)
dlg.exec_()
print('\n\n-----------CheckedItems: ', items.checkedItems())

Activate other widgets while mouse dragging

I have multiple Tkinter.labels in a row and i would like the user to be able to click and drag their mouse over each one, activating them in the process.
I know about bindings, but i need multiple events in a single binding. Ive been messing around with <Button-1> and <Enter>, however i need a callback to be called only when both are true.
I know l.bind('<Button-1>,<Enter>', ...) is not valid.
Anyone with more Tkinter experience know of a way to chain binding, or make a multi-bind??
The way you solve this particular problem is to have a binding on ButtonPress and ButtonRelease that sets a flag. Then, in your binding for <Enter> (or any other event) you check for that flag.
However, while the button is pressed you won't get any <Enter> events. This is because the widget you clicked over grabs the pointer and owns it until you release the button. The only <Enter> events you'll get while the button is pressed are when you enter the widget you originally clicked on.
What you want to do instead is bind to <B1-Motion>. You can then use the x/y coordinates of the event and winfo_containing to determine what widget you are over.
That being said, trying to simulate selection over a row of labels is a lot of work for very little benefit. Why not just use a text widget that already has selection built in? You can tweak it so that it looks like a bunch of labels (ie: make the background the same color as a frame) and you can turn editing off. That might be an easier way to go.
I encountered this same problem today and thanks to #Bryan Oakley's answer I was able to code a working solution. I will share my code in the hope that it will help someone someday.
This example builds 2 tkinter TreeViews, and enables dragging-and-dropping treeItems between the 2 trees. The key point is that by binding both trees to the B1-motion event, both trees are able to respond to the events.
import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showinfo
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class TreeItem:
"""
Keeps a reference to a treeItem together with its parent tree.
"""
def __init__(self, tree, item):
self.tree = tree
self.item = item
self.itemTxt = tree.item(item,"text")
def __str__(self):
"""
Prints 'treename, itemname' upon calling str(TreeItem)
"""
return f'{self.tree}, {self.itemTxt}'
class Mouse(metaclass=Singleton):
"""
Handles treeitem clicking, dragging, dropping and shows feedback messages about them.
"""
def __init__(self, root):
self.root = root
self.clicked_item = None
self.is_dragging = False
self.drag_time = 0
self.current_hovering_widget = None
def OnMouseDown(self, event):
clicked_item = self.get_item_under_mouse(event)
print("You clicked on", str(clicked_item))
self.clicked_item = clicked_item
def OnDrag(self, event):
self.is_dragging = True
self.show_drag_init_msg()
self.show_hovering_item_change_msg(event)
self.drag_time += 1
def OnMouseUp(self, event):
if self.is_dragging:
self.finish_drag()
self.show_drop_msg()
self.clicked_item = None
def finish_drag(self):
self.is_dragging = False
self.drag_time = 0
def show_drag_init_msg(self):
if self.drag_time == 0:
print("You are now dragging item", self.clicked_item.tree, self.clicked_item.itemTxt)
def show_hovering_item_change_msg(self, event):
currently_hovering = self.get_item_under_mouse(event)
if str(self.current_hovering_widget) != str(currently_hovering):
print("Mouse is above", str(currently_hovering))
self.current_hovering_widget = currently_hovering
def show_drop_msg(self):
dragged_item:TreeItem = self.clicked_item
dragged_onto:TreeItem = self.current_hovering_widget
print(f'You dropped {str(dragged_item)} onto {str(dragged_onto)}')
def get_item_under_mouse(self, event):
current_tree = self.root.winfo_containing(event.x_root, event.y_root)
current_tree_item = current_tree.identify("item", event.x, event.y)
return TreeItem(tree=current_tree, item=current_tree_item)
class Tree:
def __init__(self, root, row, col):
self.root: tk.Tk = root
self.create_tree(root, row, col)
def OnDrag(self,event):
Mouse(self.root).OnDrag(event)
def OnMouseDown(self, event):
Mouse(self.root).OnMouseDown(event)
def OnMouseUp(self, event):
Mouse(self.root).OnMouseUp(event)
def create_tree(self, root, row, col):
self.tree = ttk.Treeview(root)
self.tree.heading('#0', text='Departments', anchor='w')
self.tree.grid(row=row, column=col, sticky='nsew')
self.add_dummy_data()
# add bindings
self.tree.bind("<ButtonPress-1>", self.OnMouseDown)
self.tree.bind("<ButtonRelease-1>", self.OnMouseUp)
self.tree.bind("<B1-Motion>", self.OnDrag)
def add_dummy_data(self):
# adding data
self.tree.insert('', tk.END, text='Administration', iid=0, open=False)
self.tree.insert('', tk.END, text='Logistics', iid=1, open=False)
self.tree.insert('', tk.END, text='Sales', iid=2, open=False)
self.tree.insert('', tk.END, text='Finance', iid=3, open=False)
self.tree.insert('', tk.END, text='IT', iid=4, open=False)
# adding children of first node
self.tree.insert('', tk.END, text='John Doe', iid=5, open=False)
self.tree.insert('', tk.END, text='Jane Doe', iid=6, open=False)
self.tree.move(5, 0, 0)
self.tree.move(6, 0, 1)
root = tk.Tk()
root.geometry('620x200')
# make two trees
tree1 = Tree(root,0,0)
tree2 = Tree(root,0,1)
# run the app
root.mainloop()

How to create a tree view with checkboxes in Python

I've been using Tkinter and Tix to write a small program.
I'm at a point where I need a tree view with checkboxes (checkbuttons) so I can select items from the tree view.
Is there an easy way to do this?
I've been looking at ttk.Treeview () and it looks easy to get the tree view but is there a way to insert a checkbutton to the view?
A simple code snippet would be really appreciated.
I'm not limited to ttk. Anything will do; as long as I have an example or good docs I can make it work
import Tix
class 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 tk
import tkinter.ttk as ttk
class 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 tk
from tkinter import ttk
BALLOT_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 is
clicked. 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)
I would add to jferard's great answer that if you want to have a table of values rather than a tree structure change the following:
In the init add:
self.column('#1', width=width, stretch=tk.YES)
for each column you want.
add_item should be:
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)
self.insert('', index='end', iid=item, values = item, text=self._unchecked, open=True)
Change the example as such:
cols = ['One', 'Two']
items = [('A', '1',),('B','2')]
check_list = TtkCheckList(root, columns = cols, height=len(items))

Categories