#!/usr/bin/env python
"""
Widget functions.
Convenience functions for adding Tkinter widgets.
This module was written by Matthias Cuntz while at Institut National de
Recherche pour l'Agriculture, l'Alimentation et l'Environnement (INRAE), Nancy,
France.
:copyright: Copyright 2020- Matthias Cuntz - mc (at) macu (dot) de
:license: MIT License, see LICENSE for details.
.. moduleauthor:: Matthias Cuntz
The following functions are provided:
.. autosummary::
callurl
Tooltip
add_button
add_checkbutton
add_combobox
add_entry
add_imagemenu
add_label
add_menu
add_scale
add_spinbox
add_tooltip
Treeview
History
* Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de)
* Added tooltips to all widgets with class Tooltip,
Jan 2021, Matthias Cuntz
* Added add_tooltip widget, Jan 2021, Matthias Cuntz
* add_spinbox returns also label widget, Jan 2021, Matthias Cuntz
* padlabel for add_entry to add space to previous widget,
Jul 2023, Matthias Cuntz
* labelwidth for add_entry to align columns with pack,
Jul 2023, Matthias Cuntz
* Replace tk constants with strings such as tk.LEFT with 'left',
Jul 2023, Matthias Cuntz
* Use Hovertip from local copy of tooltip.py, Jul 2023, Matthias Cuntz
* Added Treeview class with optional horizontal and vertical scroolbars,
Jul 2023, Matthias Cuntz
* Added callurl function, Dec 2023, Matthias Cuntz
* Use CustomTkinter, Jun 2024, Matthias Cuntz
* Use CustomTkinter only if installed, Jun 2024, Matthias Cuntz
* Small bugfix in Combobox if no CustomTkinter, Nov 2024, Matthias Cuntz
* Pass width to Checkbutton if CustomTkinter, Dec 2024, Matthias Cuntz
* Pass padx for space between label and combobox, Dec 2024, Matthias Cuntz
* Use CustomTkinter also in add_menu and add_scale,
Dec 2024, Matthias Cuntz
* Bugfix: did not make new frame in add_spinbox, Dec 2024, Matthias Cuntz
* add_button, add_label, Feb 2025, Matthias Cuntz
* Default delay for tooltips from 1 s to 0.5 s, Mar 2025, Matthias Cuntz
"""
import tkinter as tk
import tkinter.ttk as ttk
try:
from customtkinter import CTkButton as Button
from customtkinter import CTkCheckBox as Checkbutton
from customtkinter import CTkComboBox as Combobox
from customtkinter import CTkEntry as Entry
from customtkinter import CTkFrame as Frame
from customtkinter import CTkLabel as Label
from customtkinter import CTkOptionMenu as Menubutton
from customtkinter import CTkSlider as Scale
from customtkinter import CTkScrollbar as Scrollbar
ihavectk = True
except ModuleNotFoundError:
from tkinter.ttk import Button
from tkinter.ttk import Checkbutton
from tkinter.ttk import Combobox
from tkinter.ttk import Entry
from tkinter.ttk import Frame
from tkinter.ttk import Label
from tkinter.ttk import Menubutton
from tkinter.ttk import Scale
from tkinter.ttk import Scrollbar
ihavectk = False
import webbrowser
from .tooltip import Hovertip
__all__ = ['callurl', 'Tooltip',
'add_button', 'add_checkbutton', 'add_combobox', 'add_entry',
'add_imagemenu', 'add_label', 'add_menu', 'add_scale',
'add_spinbox', 'add_tooltip',
'Treeview']
# https://stackoverflow.com/questions/23482748/how-to-create-a-hyperlink-with-a-label-in-tkinter
[docs]
def callurl(url):
"""
Open url in external web browser
Parameters
----------
url : str
html url
Returns
-------
Opens *url* in external web browser
Examples
--------
>>> opthead = Frame(self)
>>> opthead.pack(side='top', fill='x')
>>> optheadlabel1 = Label(opthead, text='Options for')
>>> optheadlabel1.pack(side='left')
>>> ttk.Style().configure('blue.TLabel', foreground='blue')
>>> optheadlabel2 = Label(opthead, text='pandas.read_csv',
... style='blue.TLabel')
>>> optheadlabel2.pack(side='left')
>>> font = tkfont.Font(optheadlabel2, optheadlabel2.cget("font"))
>>> font.configure(underline=True)
>>> optheadlabel2.configure(font=font)
>>> optheadlabel2.bind("<Button-1>",
... lambda e: callurl("https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html"))
"""
webbrowser.open_new(url)
# label.grid()
[docs]
def add_combobox(frame, label="", values=[], command=None, tooltip="",
**kwargs):
"""
Add a left-aligned Combobox with a Label before.
Parameters
----------
frame : CustomTkinter widget
Parent widget
label : str, optional
Text that appears in front of the combobox (default: "")
values : list of str, optional
The choices that will appear in the drop-down menu (default: [])
command : function, optional
Handler function to be bound to the combobox for the event
<<ComboboxSelected>> (default: None).
tooltip : str, optional
Tooltip appearing after one second when hovering over
the combobox (default: "" = no tooltip)
padx : int, optional
Extra space in px added left and right of the label text (default: 1)
**kwargs : option=value pairs, optional
All other options will be passed to Combobox
Returns
-------
tk.StringVar, Combobox
variable for the text before the combobox, combobox widget
tk.StringVar
variable for the text of the tooltip, if given.
Examples
--------
>>> self.rowzxy = Frame(self)
>>> self.rowzxy.pack(side='top', fill='x')
>>> self.xlbl, self.x = add_combobox(
... self.rowzxy, label="x", values=columns, command=self.selected)
"""
iframe = Frame(frame)
width = kwargs.pop('width', 25)
cb_label = tk.StringVar()
cb_label.set(label)
lkwargs = {'textvariable': cb_label}
if ihavectk:
lkwargs.update({'padx': kwargs.pop('padx', 1)})
else:
_ = kwargs.pop('padx', 1)
label = Label(iframe, **lkwargs)
label.pack(side='left')
if ihavectk:
cb = Combobox(iframe, values=values, width=width, command=command,
**kwargs)
else:
cb = ttk.Combobox(iframe, values=values, width=width, **kwargs)
# long = len(max(values, key=len))
# cb.configure(width=(max(20, long//2)))
if command is not None:
cb.bind("<<ComboboxSelected>>", command)
cb.pack(side='left')
if tooltip:
ttip = tk.StringVar()
ttip.set(tooltip)
cbtip = Tooltip(cb, ttip)
return iframe, cb_label, cb, ttip
else:
return iframe, cb_label, cb
[docs]
def add_entry(frame, label="", text="", command=None, tooltip="",
padlabel=0, labelwidth=None, **kwargs):
"""
Add a left-aligned Entry with a Label before.
Parameters
----------
frame : CustomTkinter widget
Parent widget
label : str, optional
Text that appears in front of the entry (default: "")
text : str, optional
Initial text in the entry area (default: "")
command : function or list of functions, optional
Handler function to be bound to the entry for the events
'<FocusOut>', <Return>, '<Key-Return>', and <KP_Enter> (default: None).
If list is given than command[0] is bound to '<FocusOut>' and
command[1] is bound to the 3 events
tooltip : str, optional
Tooltip appearing after one second when hovering over
the entry (default: "" = no tooltip)
padlabel : int, optional
Prepend number of spaces to create distance to other widgets
(default: 0)
padx : int, optional
Extra space in px added left and right of the label text (default: 1)
labelwidth : int, optional
If given, set width of Label
**kwargs : option=value pairs, optional
All other options will be passed to Entry
Returns
-------
tk.StringVar, tk.StringVar
variable for the text before the entry,
variable for the text in the entry area
tk.StringVar
variable for the text of the tooltip, if given.
Examples
--------
>>> self.rowxyopt = Frame(self)
>>> self.rowxyopt.pack(side='top', fill='x')
>>> self.lslbl, self.ls = add_entry(
... self.rowxyopt, label="ls", text='-',
... width=4, command=self.selected_y)
"""
# label
iframe = Frame(frame)
entry_label = tk.StringVar()
nlab = len(label) + padlabel
lab = f'{label:>{nlab}s}'
entry_label.set(lab)
lkwargs = {'textvariable': entry_label}
if labelwidth is not None:
lkwargs.update({'width': labelwidth})
# if labelwidth is None:
# labelwidth = len(lab)
# if ihavectk:
# labelwidth *= 9
# lkwargs.update({'width': labelwidth})
if ihavectk:
lkwargs.update({'padx': kwargs.pop('padx', 1)})
else:
_ = kwargs.pop('padx', 1)
label = Label(iframe, **lkwargs)
# print(label.configure())
label.pack(side='left')
# entry
entry_text = tk.StringVar()
if text is None:
tt = 'None'
elif isinstance(text, bool):
if text:
tt = 'True'
else:
tt = 'False'
else:
tt = str(text)
entry_text.set(tt)
entry = Entry(iframe, textvariable=entry_text, **kwargs)
if command is not None:
if isinstance(command, (list, tuple)):
com0 = command[0]
if len(command) > 1:
com1 = command[1]
else:
com1 = command[0]
else:
com0 = command
com1 = command
entry.bind('<FocusOut>', com0) # tab or click
entry.bind('<Return>', com1) # return
entry.bind('<Key-Return>', com1) # return
entry.bind('<KP_Enter>', com1) # return of numeric keypad
entry.pack(side='left')
# tooltip
if tooltip:
ttip = tk.StringVar()
ttip.set(tooltip)
etip = Tooltip(entry, ttip)
return iframe, entry_label, entry_text, ttip
else:
return iframe, entry_label, entry_text
[docs]
def add_label(frame, itext="", **kwargs):
"""
Add a label with a textvariable
Parameters
----------
frame : CustomTkinter widget
Parent widget
itext : str, optional
Text that appears as textvariable in the label (default: "")
If not textvariable needed, use text='something' instead.
**kwargs : option=value pairs, optional
All other options will be passed to the Label function
Returns
-------
tk.StringVar, Label
variable for the text of the label, Label widget
or
Label
Label widget, if text=text given as keyword argument
Examples
--------
>>> self.rowzxy = Frame(self)
>>> self.rowzxy.pack(side='top', fill='x')
>>> self.xlbltext, self.xlbl = add_label(self.rowzxy, "x")
"""
if 'text' not in kwargs:
ilbl_text = tk.StringVar()
ilbl_text.set(itext)
kwargs.update({'textvariable': ilbl_text})
ilbl = Label(frame, **kwargs)
ilbl.pack(side='left')
if 'text' in kwargs:
return ilbl
else:
return ilbl_text, ilbl
[docs]
def add_scale(frame, label="", ini=0, tooltip="", **kwargs):
"""
Add a left-aligned Scale with a Label before.
Parameters
----------
frame : CustomTkinter widget
Parent widget
label : str, optional
Text that appears in front of the scale (default: "")
ini : float, optional
Initial value of scale (default: 0)
tooltip : str, optional
Tooltip appearing after one second when hovering over
the scale (default: "" = no tooltip)
**kwargs : option=value pairs, optional
All other options will be passed to Scale
Returns
-------
tk.StringVar, tk.DoubleVar, Scale
variable for the text before the scale,
value of scale,
scale widget
tk.StringVar
variable for the text of the tooltip, if given.
Examples
--------
>>> self.rowzxy = Frame(self)
>>> self.rowzxy.pack(side='top', fill='x')
>>> self.xlbl, self.x = add_scale(
... self.rowzxy, label="x", values=columns, command=self.selected)
"""
iframe = Frame(frame)
s_label = tk.StringVar()
s_label.set(label)
label = Label(iframe, textvariable=s_label)
label.pack(side='left')
s_val = tk.DoubleVar()
s_val.set(ini)
if 'from_' not in kwargs:
kwargs.update({'from_': 0})
if 'to' not in kwargs:
kwargs.update({'to': 100})
if ihavectk:
kwargs.update({'number_of_steps': kwargs['to'] - kwargs['from_'] + 1})
length = kwargs.pop('length', -1)
orient = kwargs.pop('orient', tk.HORIZONTAL)
if length < 0:
length = 100
if orient == tk.HORIZONTAL:
kwargs.update({'width': length})
if orient == tk.VERTICAL:
kwargs.update({'height': length})
s = Scale(iframe, variable=s_val, **kwargs)
s.pack(side='left')
if tooltip:
ttip = tk.StringVar()
ttip.set(tooltip)
stip = Tooltip(s, ttip)
return iframe, s_label, s_val, s, ttip
else:
return iframe, s_label, s_val, s
[docs]
def add_spinbox(frame, label="", values=[], command=None, tooltip="",
**kwargs):
"""
Add a left-aligned tk.Spinbox with a ttk.Label before.
Parameters
----------
frame : tk widget
Parent widget
label : str, optional
Text that appears in front of the spinbox (default: "")
values : list of str, optional
The list of choices on the spinbox (default: [])
command : function, optional
Handler function bound to
<Return>, '<Key-Return>', <KP_Enter>, and '<FocusOut>' (default: None).
tooltip : str, optional
Tooltip appearing after one second when hovering over
the spinbox (default: "" = no tooltip)
**kwargs : option=value pairs, optional
All other options will be passed to tk.Spinbox
Returns
-------
tk.StringVar, ttk.Label, tk.StringVar, tk.Spinbox
variable for the text before the spinbox,
label widget,
variable for the text in the spinbox area,
spinbox widget
tk.StringVar
variable for the text of the tooltip, if given.
Examples
--------
>>> self.rowlev = ttk.Frame(self)
>>> self.rowlev.pack(side='top', fill='x')
>>> self.dlval, self.dl, self.dval, self.d = add_spinbox(
... self.rowlev, label="dim", values=range(0,10),
... command=self.spinned)
"""
iframe = Frame(frame)
width = kwargs.pop('width', 1)
sbl_val = tk.StringVar()
sbl_val.set(label)
sbl = ttk.Label(iframe, textvariable=sbl_val)
sbl.pack(side='left')
sb_val = tk.StringVar()
if len(values) > 0:
sb_val.set(str(values[0]))
sb = tk.Spinbox(iframe, values=values, command=command, width=width,
textvariable=sb_val, **kwargs)
if command is not None:
sb.bind('<Return>', command) # return
sb.bind('<Key-Return>', command) # return
sb.bind('<KP_Enter>', command) # return of numeric keypad
sb.bind('<FocusOut>', command) # tab or click
sb.pack(side='left')
if tooltip:
ttip = tk.StringVar()
ttip.set(tooltip)
sbtip = Tooltip(sb, ttip)
return iframe, sbl_val, sbl, sb_val, sb, ttip
else:
return iframe, sbl_val, sbl, sb_val, sb
# https://pythonassets.com/posts/scrollbar-in-tk-tkinter/
[docs]
class Treeview(Frame):
"""
Treeview class with optional horizontal and vertical scrollbars
Examples
--------
Simple ttk.Treeview widget
.. code-block:: python
tree = Treeview(frame_widget)
ttk.Treeview widget with vertical scrollbar
.. code-block:: python
tree = Treeview(frame_widget, yscroll=True)
ttk.Treeview widget with vertical and horizontal scrollbars
.. code-block:: python
tree = Treeview(frame_widget, xscroll=True, yscroll=True)
"""
def __init__(self, *args, xscroll=False, yscroll=False, **kwargs):
super().__init__(*args, **kwargs)
# scrollbars
if xscroll:
if ihavectk:
self.hscrollbar = Scrollbar(self, orientation='horizontal')
else:
self.hscrollbar = Scrollbar(self, orient='horizontal')
if yscroll:
if ihavectk:
self.vscrollbar = Scrollbar(self, orientation='vertical')
else:
self.vscrollbar = Scrollbar(self, orient='vertical')
# treeview
self.tv = ttk.Treeview(self)
# pack scrollbars and treeview together
if xscroll:
self.tv.config(xscrollcommand=self.hscrollbar.set)
self.hscrollbar.configure(command=self.tv.xview)
self.hscrollbar.pack(side='bottom', fill='x')
if yscroll:
self.tv.config(yscrollcommand=self.vscrollbar.set)
self.vscrollbar.configure(command=self.tv.yview)
self.vscrollbar.pack(side='right', fill='y')
self.tv.pack()
# convenience functions
self.tag_configure = self.tv.tag_configure
self.config = self.tv.config
self.column = self.tv.column
self.heading = self.tv.heading
self.insert = self.tv.insert