1163 lines
40 KiB
Python
1163 lines
40 KiB
Python
|
import tkinter as tk
|
||
|
from tkinter import ttk
|
||
|
from tkinter import font
|
||
|
from tkinter.ttk import Button, Checkbutton, Combobox
|
||
|
from tkinter.ttk import Entry, Frame, Label
|
||
|
from tkinter.ttk import Labelframe, LabelFrame, Menubutton
|
||
|
from tkinter.ttk import Notebook, OptionMenu, PanedWindow
|
||
|
from tkinter.ttk import Panedwindow, Progressbar, Radiobutton
|
||
|
from tkinter.ttk import Scale, Scrollbar, Separator
|
||
|
from tkinter.ttk import Sizegrip, Spinbox, Treeview
|
||
|
from ttkbootstrap.constants import *
|
||
|
|
||
|
# date entry imports
|
||
|
from ttkbootstrap.dialogs import Querybox
|
||
|
from datetime import datetime
|
||
|
|
||
|
# floodgauge imports
|
||
|
import math
|
||
|
|
||
|
# meter imports
|
||
|
from PIL import Image, ImageTk, ImageDraw
|
||
|
from ttkbootstrap.style import Colors
|
||
|
from ttkbootstrap import utility
|
||
|
from ttkbootstrap.style import Bootstyle
|
||
|
|
||
|
M = 3 # meter image scale, higher number increases resolution
|
||
|
|
||
|
TTK_WIDGETS = (
|
||
|
ttk.Button,
|
||
|
ttk.Checkbutton,
|
||
|
ttk.Combobox,
|
||
|
ttk.Entry,
|
||
|
ttk.Frame,
|
||
|
ttk.Labelframe,
|
||
|
ttk.Label,
|
||
|
ttk.Menubutton,
|
||
|
ttk.Notebook,
|
||
|
ttk.Panedwindow,
|
||
|
ttk.Progressbar,
|
||
|
ttk.Radiobutton,
|
||
|
ttk.Scale,
|
||
|
ttk.Scrollbar,
|
||
|
ttk.Separator,
|
||
|
ttk.Sizegrip,
|
||
|
ttk.Spinbox,
|
||
|
ttk.Treeview,
|
||
|
ttk.OptionMenu,
|
||
|
)
|
||
|
|
||
|
TK_WIDGETS = (
|
||
|
tk.Tk,
|
||
|
tk.Toplevel,
|
||
|
tk.Button,
|
||
|
tk.Label,
|
||
|
tk.Text,
|
||
|
tk.Frame,
|
||
|
tk.Checkbutton,
|
||
|
tk.Radiobutton,
|
||
|
tk.Entry,
|
||
|
tk.Scale,
|
||
|
tk.Listbox,
|
||
|
tk.Menu,
|
||
|
tk.Menubutton,
|
||
|
tk.LabelFrame,
|
||
|
tk.Canvas,
|
||
|
tk.OptionMenu,
|
||
|
tk.Spinbox,
|
||
|
)
|
||
|
|
||
|
|
||
|
class DateEntry(ttk.Frame):
|
||
|
"""A date entry widget combines the `Combobox` and a `Button`
|
||
|
with a callback attached to the `get_date` function.
|
||
|
|
||
|
When pressed, a date chooser popup is displayed. The returned
|
||
|
value is inserted into the combobox.
|
||
|
|
||
|
The date chooser popup will use the date in the combobox as the
|
||
|
date of focus if it is in the format specified by the
|
||
|
`dateformat` parameter. By default, this format is "%Y-%m-%d".
|
||
|
|
||
|
The bootstyle api may be used to change the style of the widget.
|
||
|
The available colors include -> primary, secondary, success,
|
||
|
info, warning, danger, light, dark.
|
||
|
|
||
|
The starting weekday on the date chooser popup can be changed
|
||
|
with the `firstweekday` parameter. By default this value is
|
||
|
`6`, which represents "Sunday".
|
||
|
|
||
|
The `Entry` and `Button` widgets are accessible from the
|
||
|
`DateEntry.Entry` and `DateEntry.Button` properties.
|
||
|
|
||
|
![](../../assets/widgets/date-entry.png)
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
master=None,
|
||
|
dateformat=r"%x",
|
||
|
firstweekday=6,
|
||
|
startdate=None,
|
||
|
bootstyle="",
|
||
|
**kwargs,
|
||
|
):
|
||
|
"""
|
||
|
Parameters:
|
||
|
|
||
|
master (Widget, optional):
|
||
|
The parent widget.
|
||
|
|
||
|
dateformat (str, optional):
|
||
|
The format string used to render the text in the entry
|
||
|
widget. For more information on acceptable formats, see https://strftime.org/
|
||
|
|
||
|
firstweekday (int, optional):
|
||
|
Specifies the first day of the week. 0=Monday, 1=Tuesday,
|
||
|
etc...
|
||
|
|
||
|
startdate (datetime, optional):
|
||
|
The date that is in focus when the widget is displayed. Default is
|
||
|
current date.
|
||
|
|
||
|
bootstyle (str, optional):
|
||
|
A style keyword used to set the focus color of the entry
|
||
|
and the background color of the date button. Available
|
||
|
options include -> primary, secondary, success, info,
|
||
|
warning, danger, dark, light.
|
||
|
|
||
|
**kwargs (Dict[str, Any], optional):
|
||
|
Other keyword arguments passed to the frame containing the
|
||
|
entry and date button.
|
||
|
"""
|
||
|
self._dateformat = dateformat
|
||
|
self._firstweekday = firstweekday
|
||
|
|
||
|
self._startdate = startdate or datetime.today()
|
||
|
self._bootstyle = bootstyle
|
||
|
super().__init__(master, **kwargs)
|
||
|
|
||
|
# add visual components
|
||
|
entry_kwargs = {"bootstyle": self._bootstyle}
|
||
|
if "width" in kwargs:
|
||
|
entry_kwargs["width"] = kwargs.pop("width")
|
||
|
|
||
|
self.entry = ttk.Entry(self, **entry_kwargs)
|
||
|
self.entry.pack(side=tk.LEFT, fill=tk.X, expand=tk.YES)
|
||
|
|
||
|
self.button = ttk.Button(
|
||
|
master=self,
|
||
|
command=self._on_date_ask,
|
||
|
bootstyle=f"{self._bootstyle}-date",
|
||
|
)
|
||
|
self.button.pack(side=tk.LEFT)
|
||
|
|
||
|
# starting value
|
||
|
self.entry.insert(tk.END, self._startdate.strftime(self._dateformat))
|
||
|
|
||
|
def __getitem__(self, key: str):
|
||
|
return self.configure(cnf=key)
|
||
|
|
||
|
def __setitem__(self, key: str, value):
|
||
|
self.configure(cnf=None, **{key: value})
|
||
|
|
||
|
def _configure_set(self, **kwargs):
|
||
|
"""Override configure method to allow for setting custom
|
||
|
DateEntry parameters"""
|
||
|
|
||
|
if "state" in kwargs:
|
||
|
state = kwargs.pop("state")
|
||
|
if state in ["readonly", "invalid"]:
|
||
|
self.entry.configure(state=state)
|
||
|
elif state in ("disabled", "normal"):
|
||
|
self.entry.configure(state=state)
|
||
|
self.button.configure(state=state)
|
||
|
else:
|
||
|
kwargs[state] = state
|
||
|
if "dateformat" in kwargs:
|
||
|
self._dateformat = kwargs.pop("dateformat")
|
||
|
if "firstweekday" in kwargs:
|
||
|
self._firstweekday = kwargs.pop("firstweekday")
|
||
|
if "startdate" in kwargs:
|
||
|
self._startdate = kwargs.pop("startdate")
|
||
|
if "bootstyle" in kwargs:
|
||
|
self._bootstyle = kwargs.pop("bootstyle")
|
||
|
self.entry.configure(bootstyle=self._bootstyle)
|
||
|
self.button.configure(bootstyle=[self._bootstyle, "date"])
|
||
|
if "width" in kwargs:
|
||
|
width = kwargs.pop("width")
|
||
|
self.entry.configure(width=width)
|
||
|
|
||
|
super(ttk.Frame, self).configure(**kwargs)
|
||
|
|
||
|
def _configure_get(self, cnf):
|
||
|
"""Override the configure get method"""
|
||
|
if cnf == "state":
|
||
|
entrystate = self.entry.cget("state")
|
||
|
buttonstate = self.button.cget("state")
|
||
|
return {"Entry": entrystate, "Button": buttonstate}
|
||
|
if cnf == "dateformat":
|
||
|
return self._dateformat
|
||
|
if cnf == "firstweekday":
|
||
|
return self._firstweekday
|
||
|
if cnf == "startdate":
|
||
|
return self._startdate
|
||
|
if cnf == "bootstyle":
|
||
|
return self._bootstyle
|
||
|
else:
|
||
|
return super(ttk.Frame, self).configure(cnf=cnf)
|
||
|
|
||
|
def configure(self, cnf=None, **kwargs):
|
||
|
"""Configure the options for this widget.
|
||
|
|
||
|
Parameters:
|
||
|
|
||
|
cnf (Dict[str, Any], optional):
|
||
|
A dictionary of configuration options.
|
||
|
|
||
|
**kwargs:
|
||
|
Optional keyword arguments.
|
||
|
"""
|
||
|
if cnf is not None:
|
||
|
return self._configure_get(cnf)
|
||
|
else:
|
||
|
return self._configure_set(**kwargs)
|
||
|
|
||
|
def _on_date_ask(self):
|
||
|
"""Callback for pushing the date button"""
|
||
|
_val = self.entry.get() or datetime.today().strftime(self._dateformat)
|
||
|
try:
|
||
|
self._startdate = datetime.strptime(_val, self._dateformat)
|
||
|
except Exception as e:
|
||
|
print("Date entry text does not match", self._dateformat)
|
||
|
self._startdate = datetime.today()
|
||
|
self.entry.delete(first=0, last=tk.END)
|
||
|
self.entry.insert(
|
||
|
tk.END, self._startdate.strftime(self._dateformat)
|
||
|
)
|
||
|
|
||
|
old_date = datetime.strptime(_val, self._dateformat)
|
||
|
|
||
|
# get the new date and insert into the entry
|
||
|
new_date = Querybox.get_date(
|
||
|
parent=self.entry,
|
||
|
startdate=old_date,
|
||
|
firstweekday=self._firstweekday,
|
||
|
bootstyle=self._bootstyle,
|
||
|
)
|
||
|
self.entry.delete(first=0, last=tk.END)
|
||
|
self.entry.insert(tk.END, new_date.strftime(self._dateformat))
|
||
|
self.entry.focus_force()
|
||
|
|
||
|
|
||
|
class Floodgauge(Progressbar):
|
||
|
"""A widget that shows the status of a long-running operation
|
||
|
with an optional text indicator.
|
||
|
|
||
|
Similar to the `ttk.Progressbar`, this widget can operate in
|
||
|
two modes. *determinate* mode shows the amount completed
|
||
|
relative to the total amount of work to be done, and
|
||
|
*indeterminate* mode provides an animated display to let the
|
||
|
user know that something is happening.
|
||
|
|
||
|
Variable are generated automatically for this widget and can be
|
||
|
linked to other widgets by referencing them via the
|
||
|
`textvariable` and `variable` attributes.
|
||
|
|
||
|
![](../../assets/widgets/floodgauge.gif)
|
||
|
|
||
|
Examples:
|
||
|
|
||
|
```python
|
||
|
import ttkbootstrap as ttk
|
||
|
from ttkbootstrap.constants import *
|
||
|
|
||
|
app = ttk.Window(size=(500, 500))
|
||
|
|
||
|
gauge = ttk.Floodgauge(
|
||
|
bootstyle=INFO,
|
||
|
font=(None, 24, 'bold'),
|
||
|
mask='Memory Used {}%',
|
||
|
)
|
||
|
gauge.pack(fill=BOTH, expand=YES, padx=10, pady=10)
|
||
|
|
||
|
# autoincrement the gauge
|
||
|
gauge.start()
|
||
|
|
||
|
# stop the autoincrement
|
||
|
gauge.stop()
|
||
|
|
||
|
# manually update the gauge value
|
||
|
gauge.configure(value=25)
|
||
|
|
||
|
# increment the value by 10 steps
|
||
|
gauge.step(10)
|
||
|
|
||
|
app.mainloop()
|
||
|
```
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
master=None,
|
||
|
cursor=None,
|
||
|
font=None,
|
||
|
length=None,
|
||
|
maximum=100,
|
||
|
mode=DETERMINATE,
|
||
|
orient=HORIZONTAL,
|
||
|
bootstyle=PRIMARY,
|
||
|
takefocus=False,
|
||
|
text=None,
|
||
|
value=0,
|
||
|
mask=None,
|
||
|
**kwargs,
|
||
|
):
|
||
|
"""
|
||
|
Parameters:
|
||
|
|
||
|
master (Widget, optional):
|
||
|
Parent widget. Defaults to None.
|
||
|
|
||
|
cursor (str, optional):
|
||
|
The cursor that will appear when the mouse is over the
|
||
|
progress bar. Defaults to None.
|
||
|
|
||
|
font (Union[Font, str], optional):
|
||
|
The font to use for the progress bar label.
|
||
|
|
||
|
length (int, optional):
|
||
|
Specifies the length of the long axis of the progress bar
|
||
|
(width if orient = horizontal, height if if vertical);
|
||
|
|
||
|
maximum (float, optional):
|
||
|
A floating point number specifying the maximum `value`.
|
||
|
Defaults to 100.
|
||
|
|
||
|
mode ('determinate', 'indeterminate'):
|
||
|
Use `indeterminate` if you cannot accurately measure the
|
||
|
relative progress of the underlying process. In this mode,
|
||
|
a rectangle bounces back and forth between the ends of the
|
||
|
widget once you use the `Floodgauge.start()` method.
|
||
|
Otherwise, use `determinate` if the relative progress can be
|
||
|
calculated in advance.
|
||
|
|
||
|
orient ('horizontal', 'vertical'):
|
||
|
Specifies the orientation of the widget.
|
||
|
|
||
|
bootstyle (str, optional):
|
||
|
The style used to render the widget. Options include
|
||
|
primary, secondary, success, info, warning, danger, light,
|
||
|
dark.
|
||
|
|
||
|
takefocus (bool, optional):
|
||
|
This widget is not included in focus traversal by default.
|
||
|
To add the widget to focus traversal, use
|
||
|
`takefocus=True`.
|
||
|
|
||
|
text (str, optional):
|
||
|
A string of text to be displayed in the Floodgauge label.
|
||
|
This is assigned to the attribute `Floodgauge.textvariable`
|
||
|
|
||
|
value (float, optional):
|
||
|
The current value of the progressbar. In `determinate`
|
||
|
mode, this represents the amount of work completed. In
|
||
|
`indeterminate` mode, it is interpreted modulo `maximum`;
|
||
|
that is, the progress bar completes one "cycle" when the
|
||
|
`value` increases by `maximum`.
|
||
|
|
||
|
mask (str, optional):
|
||
|
A string format that can be used to update the Floodgauge
|
||
|
label every time the value is updated. For example, the
|
||
|
string "{}% Storage Used" with a widget value of 45 would
|
||
|
show "45% Storage Used" on the Floodgauge label. If a
|
||
|
mask is set, then the `text` option is ignored.
|
||
|
|
||
|
**kwargs:
|
||
|
Other configuration options from the option database.
|
||
|
"""
|
||
|
# progress bar value variables
|
||
|
if 'variable' in kwargs:
|
||
|
self._variable = kwargs.pop('variable')
|
||
|
else:
|
||
|
self._variable = tk.IntVar(value=value)
|
||
|
if 'textvariable' in kwargs:
|
||
|
self._textvariable = kwargs.pop('textvariable')
|
||
|
else:
|
||
|
self._textvariable = tk.StringVar(value=text)
|
||
|
self._bootstyle = bootstyle
|
||
|
self._font = font or "helvetica 10"
|
||
|
self._mask = mask
|
||
|
self._traceid = None
|
||
|
|
||
|
super().__init__(
|
||
|
master=master,
|
||
|
class_="Floodgauge",
|
||
|
cursor=cursor,
|
||
|
length=length,
|
||
|
maximum=maximum,
|
||
|
mode=mode,
|
||
|
orient=orient,
|
||
|
bootstyle=bootstyle,
|
||
|
takefocus=takefocus,
|
||
|
variable=self._variable,
|
||
|
**kwargs,
|
||
|
)
|
||
|
self._set_widget_text(self._textvariable.get())
|
||
|
self.bind("<<ThemeChanged>>", self._on_theme_change)
|
||
|
self.bind("<<Configure>>", self._on_theme_change)
|
||
|
|
||
|
if self._mask is not None:
|
||
|
self._set_mask()
|
||
|
|
||
|
def _set_widget_text(self, *_):
|
||
|
ttkstyle = self.cget("style")
|
||
|
if self._mask is None:
|
||
|
text = self._textvariable.get()
|
||
|
else:
|
||
|
value = self._variable.get()
|
||
|
text = self._mask.format(value)
|
||
|
self.tk.call("ttk::style", "configure", ttkstyle, "-text", text)
|
||
|
self.tk.call("ttk::style", "configure", ttkstyle, "-font", self._font)
|
||
|
|
||
|
def _set_mask(self):
|
||
|
if self._traceid is None:
|
||
|
self._traceid = self._variable.trace_add(
|
||
|
"write", self._set_widget_text
|
||
|
)
|
||
|
|
||
|
def _unset_mask(self):
|
||
|
if self._traceid is not None:
|
||
|
self._variable.trace_remove("write", self._traceid)
|
||
|
self._traceid = None
|
||
|
|
||
|
def _on_theme_change(self, *_):
|
||
|
text = self._textvariable.get()
|
||
|
self._set_widget_text(text)
|
||
|
|
||
|
def _configure_get(self, cnf):
|
||
|
if cnf == "value":
|
||
|
return self._variable.get()
|
||
|
if cnf == "text":
|
||
|
return self._textvariable.get()
|
||
|
if cnf == "bootstyle":
|
||
|
return self._bootstyle
|
||
|
if cnf == "mask":
|
||
|
return self._mask
|
||
|
if cnf == "font":
|
||
|
return self._font
|
||
|
else:
|
||
|
return super(Progressbar, self).configure(cnf=cnf)
|
||
|
|
||
|
def _configure_set(self, **kwargs):
|
||
|
if "value" in kwargs:
|
||
|
self._variable.set(kwargs.pop("value"))
|
||
|
if "text" in kwargs:
|
||
|
self._textvariable.set(kwargs.pop("text"))
|
||
|
if "bootstyle" in kwargs:
|
||
|
self._bootstyle = kwargs.get("bootstyle")
|
||
|
if "mask" in kwargs:
|
||
|
self._mask = kwargs.pop("mask")
|
||
|
if "font" in kwargs:
|
||
|
self._font = kwargs.pop("font")
|
||
|
if "variable" in kwargs:
|
||
|
self._variable = kwargs.get("variable")
|
||
|
Progressbar.configure(self, cnf=None, **kwargs)
|
||
|
if "textvariable" in kwargs:
|
||
|
self.textvariable = kwargs.pop("textvariable")
|
||
|
else:
|
||
|
Progressbar.configure(self, cnf=None, **kwargs)
|
||
|
|
||
|
def __getitem__(self, key: str):
|
||
|
return self._configure_get(cnf=key)
|
||
|
|
||
|
def __setitem__(self, key: str, value):
|
||
|
self._configure_set(**{key: value})
|
||
|
|
||
|
def configure(self, cnf=None, **kwargs):
|
||
|
"""Configure the options for this widget.
|
||
|
|
||
|
Parameters:
|
||
|
|
||
|
cnf (Dict[str, Any], optional):
|
||
|
A dictionary of configuration options.
|
||
|
|
||
|
**kwargs:
|
||
|
Optional keyword arguments.
|
||
|
"""
|
||
|
if cnf is not None:
|
||
|
return self._configure_get(cnf)
|
||
|
else:
|
||
|
self._configure_set(**kwargs)
|
||
|
|
||
|
@property
|
||
|
def textvariable(self):
|
||
|
"""Returns the textvariable object"""
|
||
|
return self._textvariable
|
||
|
|
||
|
@textvariable.setter
|
||
|
def textvariable(self, value):
|
||
|
"""Set the new textvariable property"""
|
||
|
self._textvariable = value
|
||
|
self._set_widget_text(self._textvariable.get())
|
||
|
|
||
|
@property
|
||
|
def variable(self):
|
||
|
"""Returns the variable object"""
|
||
|
return self._variable
|
||
|
|
||
|
@variable.setter
|
||
|
def variable(self, value):
|
||
|
"""Set the new variable object"""
|
||
|
self._variable = value
|
||
|
if self.cget('variable') != value:
|
||
|
self.configure(variable=self._variable)
|
||
|
|
||
|
|
||
|
class Meter(ttk.Frame):
|
||
|
"""A radial meter that can be used to show progress of long
|
||
|
running operations or the amount of work completed; can also be
|
||
|
used as a dial when set to `interactive=True`.
|
||
|
|
||
|
This widget is very flexible. There are two primary meter types
|
||
|
which can be set with the `metertype` parameter: 'full' and
|
||
|
'semi', which shows the arc of the meter in a full or
|
||
|
semi-circle. You can also customize the arc of the circle with
|
||
|
the `arcrange` and `arcoffset` parameters.
|
||
|
|
||
|
The meter indicator can be displayed as a solid color or with
|
||
|
stripes using the `stripethickness` parameter. By default, the
|
||
|
`stripethickness` is 0, which results in a solid meter
|
||
|
indicator. A higher `stripethickness` results in larger wedges
|
||
|
around the arc of the meter.
|
||
|
|
||
|
Various text and label options exist. The center text and
|
||
|
meter indicator is formatted with the `meterstyle` parameter.
|
||
|
You can set text on the left and right of this center label
|
||
|
using the `textleft` and `textright` parameters. This is most
|
||
|
commonly used for '$', '%', or other such symbols.
|
||
|
|
||
|
If you need access to the variables that update the meter, you
|
||
|
you can access these via the `amountusedvar`, `amounttotalvar`,
|
||
|
and the `labelvar`. The value of these properties can also be
|
||
|
retrieved via the `configure` method.
|
||
|
|
||
|
![](../../assets/widgets/meter.gif)
|
||
|
|
||
|
Examples:
|
||
|
|
||
|
```python
|
||
|
import ttkbootstrap as ttk
|
||
|
from ttkbootstrap.constants import *
|
||
|
|
||
|
app = ttk.Window()
|
||
|
|
||
|
meter = ttk.Meter(
|
||
|
metersize=180,
|
||
|
padding=5,
|
||
|
amountused=25,
|
||
|
metertype="semi",
|
||
|
subtext="miles per hour",
|
||
|
interactive=True,
|
||
|
)
|
||
|
meter.pack()
|
||
|
|
||
|
# update the amount used directly
|
||
|
meter.configure(amountused = 50)
|
||
|
|
||
|
# update the amount used with another widget
|
||
|
entry = ttk.Entry(textvariable=meter.amountusedvar)
|
||
|
entry.pack(fill=X)
|
||
|
|
||
|
# increment the amount by 10 steps
|
||
|
meter.step(10)
|
||
|
|
||
|
# decrement the amount by 15 steps
|
||
|
meter.step(-15)
|
||
|
|
||
|
# update the subtext
|
||
|
meter.configure(subtext="loading...")
|
||
|
|
||
|
app.mainloop()
|
||
|
```
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
master=None,
|
||
|
bootstyle=DEFAULT,
|
||
|
arcrange=None,
|
||
|
arcoffset=None,
|
||
|
amounttotal=100,
|
||
|
amountused=0,
|
||
|
wedgesize=0,
|
||
|
metersize=200,
|
||
|
metertype=FULL,
|
||
|
meterthickness=10,
|
||
|
showtext=True,
|
||
|
interactive=False,
|
||
|
stripethickness=0,
|
||
|
textleft=None,
|
||
|
textright=None,
|
||
|
textfont="-size 20 -weight bold",
|
||
|
subtext=None,
|
||
|
subtextstyle=DEFAULT,
|
||
|
subtextfont="-size 10",
|
||
|
stepsize=1,
|
||
|
**kwargs,
|
||
|
):
|
||
|
"""
|
||
|
Parameters:
|
||
|
|
||
|
master (Widget):
|
||
|
The parent widget.
|
||
|
|
||
|
arcrange (int):
|
||
|
The range of the arc if degrees from start to end.
|
||
|
|
||
|
arcoffset (int):
|
||
|
The amount to offset the arc's starting position in degrees.
|
||
|
0 is at 3 o'clock.
|
||
|
|
||
|
amounttotal (int):
|
||
|
The maximum value of the meter.
|
||
|
|
||
|
amountused (int):
|
||
|
The current value of the meter; displayed in a center label
|
||
|
if the `showtext` property is set to True.
|
||
|
|
||
|
wedgesize (int):
|
||
|
Sets the length of the indicator wedge around the arc. If
|
||
|
greater than 0, this wedge is set as an indicator centered
|
||
|
on the current meter value.
|
||
|
|
||
|
metersize (int):
|
||
|
The meter is square. This represents the size of one side
|
||
|
if the square as measured in screen units.
|
||
|
|
||
|
bootstyle (str):
|
||
|
Sets the indicator and center text color. One of primary,
|
||
|
secondary, success, info, warning, danger, light, dark.
|
||
|
|
||
|
metertype ('full', 'semi'):
|
||
|
Displays the meter as a full circle or semi-circle.
|
||
|
|
||
|
meterthickness (int):
|
||
|
The thickness of the indicator.
|
||
|
|
||
|
showtext (bool):
|
||
|
Indicates whether to show the left, center, and right text
|
||
|
labels on the meter.
|
||
|
|
||
|
interactive (bool):
|
||
|
Indicates that the user may adjust the meter value with
|
||
|
mouse interaction.
|
||
|
|
||
|
stripethickness (int):
|
||
|
The indicator can be displayed as a solid band or as
|
||
|
striped wedges around the arc. If the value is greater than
|
||
|
0, the indicator changes from a solid to striped, where the
|
||
|
value is the thickness of the stripes (or wedges).
|
||
|
|
||
|
textleft (str):
|
||
|
A short string inserted to the left of the center text.
|
||
|
|
||
|
textright (str):
|
||
|
A short string inserted to the right of the center text.
|
||
|
|
||
|
textfont (Union[str, Font]):
|
||
|
The font used to render the center text.
|
||
|
|
||
|
subtext (str):
|
||
|
Supplemental text that appears below the center text.
|
||
|
|
||
|
subtextstyle (str):
|
||
|
The bootstyle color of the subtext. One of primary,
|
||
|
secondary, success, info, warning, danger, light, dark.
|
||
|
The default color is Theme specific and is a lighter
|
||
|
shade based on whether it is a 'light' or 'dark' theme.
|
||
|
|
||
|
subtextfont (Union[str, Font]):
|
||
|
The font used to render the subtext.
|
||
|
|
||
|
stepsize (int):
|
||
|
Sets the amount by which to change the meter indicator
|
||
|
when incremented by mouse interaction.
|
||
|
|
||
|
**kwargs:
|
||
|
Other keyword arguments that are passed directly to the
|
||
|
`Frame` widget that contains the meter components.
|
||
|
"""
|
||
|
super().__init__(master=master, **kwargs)
|
||
|
|
||
|
# widget variables
|
||
|
self.amountusedvar = tk.IntVar(value=amountused)
|
||
|
self.amountusedvar.trace_add("write", self._draw_meter)
|
||
|
self.amounttotalvar = tk.IntVar(value=amounttotal)
|
||
|
self.labelvar = tk.StringVar(value=subtext)
|
||
|
|
||
|
# misc settings
|
||
|
self._set_arc_offset_range(metertype, arcoffset, arcrange)
|
||
|
self._towardsmaximum = True
|
||
|
self._metersize = utility.scale_size(self, metersize)
|
||
|
self._meterthickness = utility.scale_size(self, meterthickness)
|
||
|
self._stripethickness = stripethickness
|
||
|
self._showtext = showtext
|
||
|
self._wedgesize = wedgesize
|
||
|
self._stepsize = stepsize
|
||
|
self._textleft = textleft
|
||
|
self._textright = textright
|
||
|
self._textfont = textfont
|
||
|
self._subtext = subtext
|
||
|
self._subtextfont = subtextfont
|
||
|
self._subtextstyle = subtextstyle
|
||
|
self._bootstyle = bootstyle
|
||
|
self._interactive = interactive
|
||
|
self._bindids = {}
|
||
|
|
||
|
self._setup_widget()
|
||
|
|
||
|
def _setup_widget(self):
|
||
|
self.meterframe = ttk.Frame(
|
||
|
master=self, width=self._metersize, height=self._metersize
|
||
|
)
|
||
|
self.indicator = ttk.Label(self.meterframe)
|
||
|
self.textframe = ttk.Frame(self.meterframe)
|
||
|
self.textleft = ttk.Label(
|
||
|
master=self.textframe,
|
||
|
text=self._textleft,
|
||
|
font=self._subtextfont,
|
||
|
bootstyle=(self._subtextstyle, "metersubtxt"),
|
||
|
anchor=tk.S,
|
||
|
padding=(0, 5),
|
||
|
)
|
||
|
self.textcenter = ttk.Label(
|
||
|
master=self.textframe,
|
||
|
textvariable=self.amountusedvar,
|
||
|
bootstyle=(self._bootstyle, "meter"),
|
||
|
font=self._textfont,
|
||
|
)
|
||
|
self.textright = ttk.Label(
|
||
|
master=self.textframe,
|
||
|
text=self._textright,
|
||
|
font=self._subtextfont,
|
||
|
bootstyle=(self._subtextstyle, "metersubtxt"),
|
||
|
anchor=tk.S,
|
||
|
padding=(0, 5),
|
||
|
)
|
||
|
self.subtext = ttk.Label(
|
||
|
master=self.meterframe,
|
||
|
text=self._subtext,
|
||
|
bootstyle=(self._subtextstyle, "metersubtxt"),
|
||
|
font=self._subtextfont,
|
||
|
)
|
||
|
|
||
|
self.bind("<<ThemeChanged>>", self._on_theme_change)
|
||
|
self.bind("<<Configure>>", self._on_theme_change)
|
||
|
self._set_interactive_bind()
|
||
|
self._draw_base_image()
|
||
|
self._draw_meter()
|
||
|
|
||
|
# set widget geometery
|
||
|
self.indicator.place(x=0, y=0)
|
||
|
self.meterframe.pack()
|
||
|
self._set_show_text()
|
||
|
|
||
|
def _set_widget_colors(self):
|
||
|
bootstyle = (self._bootstyle, "meter", "label")
|
||
|
ttkstyle = Bootstyle.ttkstyle_name(string="-".join(bootstyle))
|
||
|
textcolor = self._lookup_style_option(ttkstyle, "foreground")
|
||
|
background = self._lookup_style_option(ttkstyle, "background")
|
||
|
troughcolor = self._lookup_style_option(ttkstyle, "space")
|
||
|
self._meterforeground = textcolor
|
||
|
self._meterbackground = Colors.update_hsv(background, vd=-0.1)
|
||
|
self._metertrough = troughcolor
|
||
|
|
||
|
def _set_meter_text(self):
|
||
|
"""Setup and pack the widget labels in the appropriate order"""
|
||
|
self._set_show_text()
|
||
|
self._set_subtext()
|
||
|
|
||
|
def _set_subtext(self):
|
||
|
if self._subtextfont:
|
||
|
if self._showtext:
|
||
|
self.subtext.place(relx=0.5, rely=0.6, anchor=tk.CENTER)
|
||
|
else:
|
||
|
self.subtext.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
|
||
|
|
||
|
def _set_show_text(self):
|
||
|
self.textframe.pack_forget()
|
||
|
self.textcenter.pack_forget()
|
||
|
self.textleft.pack_forget()
|
||
|
self.textright.pack_forget()
|
||
|
self.subtext.pack_forget()
|
||
|
|
||
|
if self._showtext:
|
||
|
if self._subtext:
|
||
|
self.textframe.place(relx=0.5, rely=0.45, anchor=tk.CENTER)
|
||
|
else:
|
||
|
self.textframe.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
|
||
|
|
||
|
self._set_text_left()
|
||
|
self._set_text_center()
|
||
|
self._set_text_right()
|
||
|
self._set_subtext()
|
||
|
|
||
|
def _set_text_left(self):
|
||
|
if self._showtext and self._textleft:
|
||
|
self.textleft.pack(side=tk.LEFT, fill=tk.Y)
|
||
|
|
||
|
def _set_text_center(self):
|
||
|
if self._showtext:
|
||
|
self.textcenter.pack(side=tk.LEFT, fill=tk.Y)
|
||
|
|
||
|
def _set_text_right(self):
|
||
|
self.textright.configure(text=self._textright)
|
||
|
if self._showtext and self._textright:
|
||
|
self.textright.pack(side=tk.RIGHT, fill=tk.Y)
|
||
|
|
||
|
def _set_interactive_bind(self):
|
||
|
seq1 = "<B1-Motion>"
|
||
|
seq2 = "<Button-1>"
|
||
|
|
||
|
if self._interactive:
|
||
|
self._bindids[seq1] = self.indicator.bind(
|
||
|
seq1, self._on_dial_interact
|
||
|
)
|
||
|
self._bindids[seq2] = self.indicator.bind(
|
||
|
seq2, self._on_dial_interact
|
||
|
)
|
||
|
return
|
||
|
|
||
|
if seq1 in self._bindids:
|
||
|
self.indicator.unbind(seq1, self._bindids.get(seq1))
|
||
|
self.indicator.unbind(seq2, self._bindids.get(seq2))
|
||
|
self._bindids.clear()
|
||
|
|
||
|
def _set_arc_offset_range(self, metertype, arcoffset, arcrange):
|
||
|
if metertype == SEMI:
|
||
|
self._arcoffset = 135 if arcoffset is None else arcoffset
|
||
|
self._arcrange = 270 if arcrange is None else arcrange
|
||
|
else:
|
||
|
self._arcoffset = -90 if arcoffset is None else arcoffset
|
||
|
self._arcrange = 360 if arcrange is None else arcrange
|
||
|
self._metertype = metertype
|
||
|
|
||
|
def _draw_meter(self, *_):
|
||
|
"""Draw a meter"""
|
||
|
img = self._base_image.copy()
|
||
|
draw = ImageDraw.Draw(img)
|
||
|
if self._stripethickness > 0:
|
||
|
self._draw_striped_meter(draw)
|
||
|
else:
|
||
|
self._draw_solid_meter(draw)
|
||
|
|
||
|
self._meterimage = ImageTk.PhotoImage(
|
||
|
img.resize((self._metersize, self._metersize), Image.CUBIC)
|
||
|
)
|
||
|
self.indicator.configure(image=self._meterimage)
|
||
|
|
||
|
def _draw_base_image(self):
|
||
|
"""Draw base image to be used for subsequent updates"""
|
||
|
self._set_widget_colors()
|
||
|
self._base_image = Image.new(
|
||
|
mode="RGBA", size=(self._metersize * M, self._metersize * M)
|
||
|
)
|
||
|
draw = ImageDraw.Draw(self._base_image)
|
||
|
|
||
|
x1 = y1 = self._metersize * M - 20
|
||
|
width = self._meterthickness * M
|
||
|
# striped meter
|
||
|
if self._stripethickness > 0:
|
||
|
_from = self._arcoffset
|
||
|
_to = self._arcrange + self._arcoffset
|
||
|
_step = 2 if self._stripethickness == 1 else self._stripethickness
|
||
|
for x in range(_from, _to, _step):
|
||
|
draw.arc(
|
||
|
xy=(0, 0, x1, y1),
|
||
|
start=x,
|
||
|
end=x + self._stripethickness - 1,
|
||
|
fill=self._metertrough,
|
||
|
width=width,
|
||
|
)
|
||
|
# solid meter
|
||
|
else:
|
||
|
draw.arc(
|
||
|
xy=(0, 0, x1, y1),
|
||
|
start=self._arcoffset,
|
||
|
end=self._arcrange + self._arcoffset,
|
||
|
fill=self._metertrough,
|
||
|
width=width,
|
||
|
)
|
||
|
|
||
|
def _draw_solid_meter(self, draw: ImageDraw.Draw):
|
||
|
"""Draw a solid meter"""
|
||
|
x1 = y1 = self._metersize * M - 20
|
||
|
width = self._meterthickness * M
|
||
|
|
||
|
if self._wedgesize > 0:
|
||
|
meter_value = self._meter_value()
|
||
|
draw.arc(
|
||
|
xy=(0, 0, x1, y1),
|
||
|
start=meter_value - self._wedgesize,
|
||
|
end=meter_value + self._wedgesize,
|
||
|
fill=self._meterforeground,
|
||
|
width=width,
|
||
|
)
|
||
|
else:
|
||
|
draw.arc(
|
||
|
xy=(0, 0, x1, y1),
|
||
|
start=self._arcoffset,
|
||
|
end=self._meter_value(),
|
||
|
fill=self._meterforeground,
|
||
|
width=width,
|
||
|
)
|
||
|
|
||
|
def _draw_striped_meter(self, draw: ImageDraw.Draw):
|
||
|
"""Draw a striped meter"""
|
||
|
meter_value = self._meter_value()
|
||
|
x1 = y1 = self._metersize * M - 20
|
||
|
width = self._meterthickness * M
|
||
|
|
||
|
if self._wedgesize > 0:
|
||
|
draw.arc(
|
||
|
xy=(0, 0, x1, y1),
|
||
|
start=meter_value - self._wedgesize,
|
||
|
end=meter_value + self._wedgesize,
|
||
|
fill=self._meterforeground,
|
||
|
width=width,
|
||
|
)
|
||
|
else:
|
||
|
_from = self._arcoffset
|
||
|
_to = meter_value - 1
|
||
|
_step = self._stripethickness
|
||
|
for x in range(_from, _to, _step):
|
||
|
draw.arc(
|
||
|
xy=(0, 0, x1, y1),
|
||
|
start=x,
|
||
|
end=x + self._stripethickness - 1,
|
||
|
fill=self._meterforeground,
|
||
|
width=width,
|
||
|
)
|
||
|
|
||
|
def _meter_value(self) -> int:
|
||
|
"""Calculate the value to be used to draw the arc length of the
|
||
|
progress meter."""
|
||
|
value = int(
|
||
|
(self["amountused"] / self["amounttotal"]) * self._arcrange
|
||
|
+ self._arcoffset
|
||
|
)
|
||
|
return value
|
||
|
|
||
|
def _on_theme_change(self, *_):
|
||
|
self._draw_base_image()
|
||
|
self._draw_meter()
|
||
|
|
||
|
def _on_dial_interact(self, e: tk.Event):
|
||
|
"""Callback for mouse drag motion on meter indicator"""
|
||
|
dx = e.x - self._metersize // 2
|
||
|
dy = e.y - self._metersize // 2
|
||
|
rads = math.atan2(dy, dx)
|
||
|
degs = math.degrees(rads)
|
||
|
|
||
|
if degs > self._arcoffset:
|
||
|
factor = degs - self._arcoffset
|
||
|
else:
|
||
|
factor = 360 + degs - self._arcoffset
|
||
|
|
||
|
# clamp the value between 0 and `amounttotal`
|
||
|
amounttotal = self.amounttotalvar.get()
|
||
|
lastused = self.amountusedvar.get()
|
||
|
amountused = (amounttotal / self._arcrange * factor)
|
||
|
|
||
|
# calculate amount used given stepsize
|
||
|
if amountused > self._stepsize//2:
|
||
|
amountused = amountused // self._stepsize * self._stepsize + self._stepsize
|
||
|
else:
|
||
|
amountused = 0
|
||
|
# if the number is the name, then do not redraw
|
||
|
if lastused == amountused:
|
||
|
return
|
||
|
# set the amount used variable
|
||
|
if amountused < 0:
|
||
|
self.amountusedvar.set(0)
|
||
|
elif amountused > amounttotal:
|
||
|
self.amountusedvar.set(amounttotal)
|
||
|
else:
|
||
|
self.amountusedvar.set(amountused)
|
||
|
|
||
|
def _lookup_style_option(self, style: str, option: str):
|
||
|
"""Wrapper around the tcl style lookup command"""
|
||
|
value = self.tk.call(
|
||
|
"ttk::style", "lookup", style, "-%s" % option, None, None
|
||
|
)
|
||
|
return value
|
||
|
|
||
|
def _configure_get(self, cnf):
|
||
|
"""Override the configuration get method"""
|
||
|
if cnf == "arcrange":
|
||
|
return self._arcrange
|
||
|
elif cnf == "arcoffset":
|
||
|
return self._arcoffset
|
||
|
elif cnf == "amounttotal":
|
||
|
return self.amounttotalvar.get()
|
||
|
elif cnf == "amountused":
|
||
|
return self.amountusedvar.get()
|
||
|
elif cnf == "interactive":
|
||
|
return self._interactive
|
||
|
elif cnf == "subtextfont":
|
||
|
return self._subtextfont
|
||
|
elif cnf == "subtextstyle":
|
||
|
return self._subtextstyle
|
||
|
elif cnf == "subtext":
|
||
|
return self._subtext
|
||
|
elif cnf == "metersize":
|
||
|
return self._metersize
|
||
|
elif cnf == "bootstyle":
|
||
|
return self._bootstyle
|
||
|
elif cnf == "metertype":
|
||
|
return self._metertype
|
||
|
elif cnf == "meterthickness":
|
||
|
return self._meterthickness
|
||
|
elif cnf == "showtext":
|
||
|
return self._showtext
|
||
|
elif cnf == "stripethickness":
|
||
|
return self._stripethickness
|
||
|
elif cnf == "textleft":
|
||
|
return self._textleft
|
||
|
elif cnf == "textright":
|
||
|
return self._textright
|
||
|
elif cnf == "textfont":
|
||
|
return self._textfont
|
||
|
elif cnf == "wedgesize":
|
||
|
return self._wedgesize
|
||
|
elif cnf == "stepsize":
|
||
|
return self._stepsize
|
||
|
else:
|
||
|
return super(ttk.Frame, self).configure(cnf)
|
||
|
|
||
|
def _configure_set(self, **kwargs):
|
||
|
"""Override the configuration set method"""
|
||
|
meter_text_changed = False
|
||
|
|
||
|
if "arcrange" in kwargs:
|
||
|
self._arcrange = kwargs.pop("arcrange")
|
||
|
if "arcoffset" in kwargs:
|
||
|
self._arcoffset = kwargs.pop("arcoffset")
|
||
|
if "amounttotal" in kwargs:
|
||
|
amounttotal = kwargs.pop("amounttotal")
|
||
|
self.amounttotalvar.set(amounttotal)
|
||
|
if "amountused" in kwargs:
|
||
|
amountused = kwargs.pop("amountused")
|
||
|
self.amountusedvar.set(amountused)
|
||
|
if "interactive" in kwargs:
|
||
|
self._interactive = kwargs.pop("interactive")
|
||
|
self._set_interactive_bind()
|
||
|
if "subtextfont" in kwargs:
|
||
|
self._subtextfont = kwargs.pop("subtextfont")
|
||
|
self.subtext.configure(font=self._subtextfont)
|
||
|
self.textleft.configure(font=self._subtextfont)
|
||
|
self.textright.configure(font=self._subtextfont)
|
||
|
if "subtextstyle" in kwargs:
|
||
|
self._subtextstyle = kwargs.pop("subtextstyle")
|
||
|
self.subtext.configure(bootstyle=[self._subtextstyle, "meter"])
|
||
|
if "metersize" in kwargs:
|
||
|
self._metersize = utility.scale_size(kwargs.pop("metersize"))
|
||
|
self.meterframe.configure(
|
||
|
height=self._metersize, width=self._metersize
|
||
|
)
|
||
|
if "bootstyle" in kwargs:
|
||
|
self._bootstyle = kwargs.pop("bootstyle")
|
||
|
self.textcenter.configure(bootstyle=[self._bootstyle, "meter"])
|
||
|
if "metertype" in kwargs:
|
||
|
self._metertype = kwargs.pop("metertype")
|
||
|
if "meterthickness" in kwargs:
|
||
|
self._meterthickness = self.scale_size(
|
||
|
kwargs.pop("meterthickness")
|
||
|
)
|
||
|
if "stripethickness" in kwargs:
|
||
|
self._stripethickness = kwargs.pop("stripethickness")
|
||
|
if "subtext" in kwargs:
|
||
|
self._subtext = kwargs.pop("subtext")
|
||
|
self.subtext.configure(text=self._subtext)
|
||
|
meter_text_changed = True
|
||
|
if "textleft" in kwargs:
|
||
|
self._textleft = kwargs.pop("textleft")
|
||
|
self.textleft.configure(text=self._textleft)
|
||
|
meter_text_changed = True
|
||
|
if "textright" in kwargs:
|
||
|
self._textright = kwargs.pop("textright")
|
||
|
meter_text_changed = True
|
||
|
if "showtext" in kwargs:
|
||
|
self._showtext = kwargs.pop("showtext")
|
||
|
meter_text_changed = True
|
||
|
if "textfont" in kwargs:
|
||
|
self._textfont = kwargs.pop("textfont")
|
||
|
self.textcenter.configure(font=self._textfont)
|
||
|
if "wedgesize" in kwargs:
|
||
|
self._wedgesize = kwargs.pop("wedgesize")
|
||
|
if "stepsize" in kwargs:
|
||
|
self._stepsize = kwargs.pop("stepsize")
|
||
|
if meter_text_changed:
|
||
|
self._set_meter_text()
|
||
|
|
||
|
try:
|
||
|
if self._metertype:
|
||
|
self._set_arc_offset_range(
|
||
|
metertype=self._metertype,
|
||
|
arcoffset=self._arcoffset,
|
||
|
arcrange=self._arcrange,
|
||
|
)
|
||
|
except AttributeError:
|
||
|
return
|
||
|
|
||
|
self._draw_base_image()
|
||
|
self._draw_meter()
|
||
|
|
||
|
# pass remaining configurations to `ttk.Frame.configure`
|
||
|
super(ttk.Frame, self).configure(**kwargs)
|
||
|
|
||
|
def __getitem__(self, key: str):
|
||
|
return self._configure_get(key)
|
||
|
|
||
|
def __setitem__(self, key: str, value) -> None:
|
||
|
self._configure_set(**{key: value})
|
||
|
|
||
|
def configure(self, cnf=None, **kwargs):
|
||
|
"""Configure the options for this widget.
|
||
|
|
||
|
Parameters:
|
||
|
cnf (Dict[str, Any], optional):
|
||
|
A dictionary of configuration options.
|
||
|
|
||
|
**kwargs: Optional keyword arguments.
|
||
|
"""
|
||
|
if cnf is not None:
|
||
|
return self._configure_get(cnf)
|
||
|
else:
|
||
|
self._configure_set(**kwargs)
|
||
|
|
||
|
def step(self, delta=1):
|
||
|
"""Increase the indicator value by `delta`
|
||
|
|
||
|
The indicator will reverse direction and count down once it
|
||
|
reaches the maximum value.
|
||
|
|
||
|
Parameters:
|
||
|
|
||
|
delta (int):
|
||
|
The amount to change the indicator.
|
||
|
"""
|
||
|
amountused = self.amountusedvar.get()
|
||
|
amounttotal = self.amounttotalvar.get()
|
||
|
if amountused >= amounttotal:
|
||
|
self._towardsmaximum = True
|
||
|
self.amountusedvar.set(amountused - delta)
|
||
|
elif amountused <= 0:
|
||
|
self._towardsmaximum = False
|
||
|
self.amountusedvar.set(amountused + delta)
|
||
|
elif self._towardsmaximum:
|
||
|
self.amountusedvar.set(amountused - delta)
|
||
|
else:
|
||
|
self.amountusedvar.set(amountused + delta)
|