Vertical ttk Scales#
Vertical Scaling#
Calibrating ttk vertical Scale at minimum slider travel on a 0 to 100 range#
Horizontal lines have been inserted at ticks on range with tickinterval set at 10. The Scale needs to be adjusted as shown here.
For the most part these should be similar to horizontal Scales,except that
the y value is used instead of the x value. There will be a
difference when estimating the length, as the range values and displayed
values are displayed horizontally and the range values are separated by their
height rather than their width.
Base the vertical script on the calibration script 10ttk_range_calibrate.py.
The calibrating line is an Em Dash U2014.
Be careful when positioning the range and display values, also ensure that the padding added to the layout manager is wide enough for the display values. Make sure that the minimum value is at the top and increases to the maximum value at the bottom.
Show/Hide Code 12vert_range_calibrate.py
import tkinter as tk
from tkinter import font
import tkinter.ttk as ttk
import numpy as np
import ctypes
# increased geometry in x and padx for ttk scale and spinbox
ctypes.windll.shcore.SetProcessDpiAwareness(1)
###############################################
from_val = 0 # from_
to_val = 100 # to
tick_val = 10 # tickinterval
res_val = 10 # resolution
dig_val = 0 # digits
bw_val = 1 # trough border width
slider_val = 32 # sliderlength
#################################################
root = tk.Tk()
def_font = font.nametofont('TkDefaultFont')
# using numpy arange instead of range so tick intervals less than 1 can be used
data = np.arange(from_val, (to_val+1 if tick_val >=1 else to_val+tick_val), tick_val) # tick_val
data = np.round(data,1)
range_vals = tuple(data)
lspace = def_font.metrics('linespace')
len_rvs = len(range_vals)
data_size = len_rvs * lspace
space_size = len_rvs * 3
sizes = data_size + space_size
len_val = (sizes if sizes % 50 == 0 else sizes + 50 - sizes % 50)
theme_sl = {'alt': 9, 'clam': 30, 'classic': 30, 'default': 30,
'lime': 9, 'winnative': 9}
theme_bw = {'alt': 0, 'clam': 1, 'classic': 2, 'default': 1,
'lime': 6, 'winnative': 0}
root.geometry("350x"+str(len_val+200)+"+500+300")
s = ttk.Style()
############################
s.theme_use('alt') # default
###############################
theme_used = s.theme_use()
if theme_used in ('alt', 'clam', 'classic', 'default', 'lime', 'winnative'):
bw_val = theme_bw[theme_used]
slider_val = theme_sl[theme_used]
else:
bw_val = 1
fr = ttk.Frame(root)
fr.pack(fill='y', expand=1)
def show_y(val):
print('sch.get()',sch.get(),'val', val.y)
sch = tk.Scale(fr, from_=from_val, to=to_val, label='tk', orient='vertical',
resolution=res_val, showvalue=1, tickinterval=tick_val, digits=dig_val,
length=len_val)
sch.grid(sticky='ns')
sch.bind("<ButtonRelease-1>", show_y)
def resolve(evt):
if res_val < 1 or tick_val < 1:
pass
else:
value = scth.get()
curr_y = convert_to_acty(value)
if evt.y < curr_y - slider_val / 2:
scth.set(value - res_val + 1)
elif evt.y > curr_y + slider_val / 2:
scth.set(value + res_val - 1)
def convert_to_acty(curr_val):
return ((curr_val - from_val) * (y_max - y_min) / (to_val - from_val) \
+ y_min)
def display_value(value):
# position (in pixel) of the center of the slider
act_y = convert_to_acty(float(value))
disp_lab.place_configure(y=act_y)
disp_lab.configure(text=f'{float(value):.{dig_val}f}')
act_var = tk.StringVar()
act_var.set('0.00')
scth = ttk.Scale(fr, from_=from_val, to=to_val, length=len_val,
command=display_value, variable=act_var, orient='vertical')
scth.grid(row=0, column=1, sticky='ns', pady=5, padx=25)
scth.bind("<Button-1>", resolve)
y_min = slider_val // 2 + bw_val
y_max = len_val - slider_val // 2 - bw_val
if range_vals[-1] == to_val:
pass
else:
max_rv = range_vals[-1]
mult_y = ((max_rv-from_val)*y_max/(to_val-from_val))
for i, rv in enumerate(range_vals):
################################################
item = ttk.Label(fr, text=rv) # text='—'
################################################
item.place(in_=scth, bordermode='outside',
y=(y_min + i / (len_rvs - 1) *
((y_max if range_vals[-1] == to_val else mult_y) - y_min)),
relx=0.7, anchor='w') # rely=1
disp_lab = ttk.Label(fr)
act_y = convert_to_acty(float(scth.get()))
disp_lab.place(in_=scth, bordermode='outside',
y=act_y, relx=0, anchor='e')
display_value(scth.get())
sbh = ttk.Spinbox(fr, from_=from_val, to=to_val, textvariable=act_var,
width=5, increment=res_val)
sbh.grid(row=0, column=2, sticky='ew', padx=15)
root.mainloop()
Vertical Scale Class#
Once the calibration script has been run in vertical mode the conversion to a class is straightforward.
Show/Hide Code 13vert_scale_class.py
from tkinter import Tk, font
from tkinter.ttk import Style, Scale, Label, Frame
import numpy as np
class TtkScale(Scale):
def __init__(self, parent, length=0, from_=0, to=255, orient='vertical',
variable=0, digits=None, tickinterval=None,
command=None, style=None, showvalue=True, resolution=1):
self.from_ = from_
self.to = to
self.variable = variable
self.length = length
self.command = command
self.parent = parent
super().__init__(parent, length=length, from_=from_, to=to, orient=orient,
variable=variable, command=command, style=style)
self.digits = digits
self.tickinterval = tickinterval
self.showvalue = showvalue
self.resolution = resolution
# set sliderlength
st = Style(self)
self.bw_val = bw_val = st.lookup('Vertical.Scale.trough','borderwidth')
self.sliderlength = sliderlength = 32
if showvalue:
self.configure(command=self.display_value)
def_font = font.nametofont('TkDefaultFont')
# if from_ more than to swap values
if from_ < to:
pass
else:
from_, to = to, from_
data = np.arange(from_, (to+1 if tickinterval >=1 else to+tickinterval),
tickinterval)
self.data = data = np.round(data,1)
range_vals = tuple(data)
len_rvs = len(range_vals)
lspace = def_font.metrics('linespace')
len_rvs = len(range_vals)
data_size = len_rvs * lspace
space_size = len_rvs * 3
sizes = data_size + space_size
min_len = (sizes if sizes % 50 == 0 else sizes + 50 - sizes % 50)
self.len_val = len_val = min_len if length < min_len else length
self.configure(length=len_val)
if bw_val == "":
bw_val = 0
self.rel_min = rel_min = (sliderlength / 2 + bw_val) / len_val
self.rel_max = rel_max = 1 - (sliderlength /2 - bw_val) / len_val
if range_vals[-1] == to:
pass
else:
max_rv = range_vals[-1]
self.mult_y = mult_y = ((max_rv - from_)*rel_max/(to - from_))
self.bind("<Button-1>", self.resolve)
self.build(from_, to, rel_min, rel_max, range_vals, len_rvs)
def build(self, from_, to, rel_min, rel_max, range_vals, len_rvs):
for i, rv in enumerate(range_vals):
item = Label(self.parent, text=rv)
item.place(in_=self, bordermode='outside',
rely=(rel_min + i / (len_rvs - 1) *
((rel_max if range_vals[-1] == to else self.mult_y) - rel_min)) ,
relx=1, anchor='w')
if self.showvalue:
self.disp_lab = Label(self.parent, text=self.get())
rel_y = self.convert_to_rely(float(self.get())) #, textvariable = self.act_val)
self.disp_lab.place(in_=self, bordermode='outside',
rely=rel_y, relx=0, anchor='e')
def convert_to_rely(self, curr_val):
return ((curr_val - self.from_) * (self.rel_max - self.rel_min) /
(self.to - self.from_) + self.rel_min)
def convert_to_acty(self, curr_val):
y_max = self.rel_max * self.len_val
y_min = self.rel_min * self.len_val
return ((curr_val - self.from_) * (y_max - y_min) /
(self.to - self.from_) + y_min)
def display_value(self, value):
# position (in pixel) of the center of the slider
rel_y = self.convert_to_rely(float(value))
self.disp_lab.config(text=value) # text=""
self.disp_lab.place_configure(rely=rel_y)
self.disp_lab.configure(text=f'{float(value):.{dig_val}f}')
# if your python is not 3.6 or above use the following 2 lines
# instead of the line above
#my_precision = '{:.{}f}'.format
#self.disp_lab.configure(text=my_precision(float(value), digits))
def resolve(self, evt):
resolution = self.resolution
if resolution < 1 or self.tickinterval < 1:
pass
else:
value = self.get()
curr_y = self.convert_to_acty(value)
if evt.y < curr_y - self.sliderlength / 2:
self.set(value - resolution + 1)
elif evt.y > curr_y + self.sliderlength / 2:
self.set(value + resolution - 1)
if __name__ == "__main__":
root = Tk()
len_val = 400
from_val = 0
to_val = 255
tick_val = 10
dig_val = 0 # dig_val = 2
res_val = 5
style = Style()
style.theme_use('default')
style.configure('my.Vertical.TScale')
fr = Frame(root)
fr.pack(fill='y')
ttks = TtkScale(fr, from_=from_val, to=to_val, orient='vertical',
tickinterval=tick_val, digits=dig_val,
style='my.Vertical.TScale', resolution=res_val)
ttks.pack(fill='y', pady=5, padx=40)
root.mainloop()
General Scale Class#
Scale class horizontal and vertical#
Running the general Scale class
Once we have both the horizontal and vertical Scales written as a class, it is straightforward to write a general class that can work with both orientations, The name is without a preceding number so that it can be used as an external module in a script.
In Roll your own
and The Third Theme
we can replace the Scale widget with the new widget, the Labels with the
range values are also replaced. This highlights the problem with different
themes the sliderlength changes and this is critical when placing the
range.
Show/Hide Code gen_scale_class.py
from tkinter import Tk, IntVar, font
from tkinter.ttk import Style, Scale, Label, Frame
import numpy as np
class TtkScale(Scale):
def __init__(self, parent, length=0, from_=0, to=255, orient='horizontal',
variable=0, digits=0, tickinterval=None, sliderlength=32,
command=None, style=None, showvalue=True, resolution=1):
self.from_ = from_
self.to = to
self.variable = variable
self.length = length
self.command = command
self.parent = parent
self.orient = orient
super().__init__(parent, length=length, from_=from_, to=to, orient=orient,
variable=variable, command=command, style=style)
self.digits = digits
self.tickinterval = tickinterval
self.showvalue = showvalue
self.resolution = resolution
self.sliderlength = sliderlength # = 32
theme_sl = {'alt': 9, 'clam': 30, 'classic': 30, 'default': 30,
'lime': 9, 'winnative': 9}
theme_bw = {'alt': 0, 'clam': 1, 'classic': 2, 'default': 1,
'lime': 6, 'winnative': 0}
# set trough borderwidth
st = Style(self)
theme_used = st.theme_use()
if theme_used in ('alt', 'clam', 'classic', 'default','lime', 'winnative'):
self.bw_val = bw_val = theme_bw[theme_used]
self.sliderlength = sliderlength = theme_sl[theme_used]
else:
self.bw_val = bw_val = 1
if showvalue:
self.configure(command=self.display_value)
def_font = font.nametofont('TkDefaultFont')
data = np.arange(from_, (to+1 if tickinterval >=1 else to+tickinterval),
tickinterval)
self.data = data = np.round(data,1)
range_vals = tuple(data)
len_rvs = len(range_vals)
if self.orient == 'horizontal':
vals_size = [def_font.measure(str(i)) for i in range_vals]
data_size = sum(vals_size)
space_size = len_rvs * def_font.measure('0')
else:
lspace = def_font.metrics('linespace')
data_size = len_rvs * lspace
space_size = len_rvs * 3
sizes = data_size + space_size
min_len = (sizes if sizes % 50 == 0 else sizes + 50 - sizes % 50)
self.len_val = len_val = min_len if length < min_len else length
self.configure(length=len_val)
self.rel_min = rel_min = (sliderlength // 2 + bw_val) / len_val
self.rel_max = rel_max = 1 - (sliderlength // 2 - bw_val) / len_val
if range_vals[-1] == to:
pass
else:
max_rv = range_vals[-1]
self.mult_l = ((max_rv - from_)*rel_max/(to - from_))
self.bind("<Button-1>", self.resolve)
self.build(to, rel_min, rel_max, range_vals, len_rvs)
def build(self, to, rel_min, rel_max, range_vals, len_rvs):
if self.orient == 'horizontal':
for i, rv in enumerate(range_vals):
item = Label(self.parent, text=rv)
item.place(in_=self, bordermode='outside',
relx=(rel_min + i / (len_rvs - 1) *
((rel_max if range_vals[-1] == to else self.mult_l) - rel_min)) ,
rely=1, anchor='n')
else:
for i, rv in enumerate(range_vals):
item = Label(self.parent, text=rv)
item.place(in_=self, bordermode='outside',
rely=(rel_min + i / (len_rvs - 1) *
((rel_max if range_vals[-1] == to else self.mult_l) - rel_min)) ,
relx=1, anchor='w')
if self.showvalue:
self.disp_lab = Label(self.parent, text=self.get())
rel_l = self.convert_to_rel(float(self.get()))
if self.orient == 'horizontal':
self.disp_lab.place(in_=self, bordermode='outside',
relx=rel_l, rely=0, anchor='s')
else:
self.disp_lab.place(in_=self, bordermode='outside',
rely=rel_l, relx=0, anchor='e')
def convert_to_rel(self, curr_val):
return ((curr_val - self.from_) * (self.rel_max - self.rel_min) /
(self.to - self.from_) + self.rel_min)
def convert_to_act(self, curr_val):
l_max = self.rel_max * self.len_val
l_min = self.rel_min * self.len_val
return ((curr_val - self.from_) * (l_max - l_min) /
(self.to - self.from_) + l_min)
def display_value(self, value):
# position (in pixel) of the centre of the slider
rel_l = self.convert_to_rel(float(value))
self.disp_lab.config(text=value) # text=""
if self.orient == 'horizontal':
self.disp_lab.place_configure(relx=rel_l)
else:
self.disp_lab.place_configure(rely=rel_l)
digits = self.digits
self.disp_lab.configure(text=f'{float(value):.{digits}f}')
# if your python is not 3.6 or above use the following 2 lines
# instead of the line above
#my_precision = '{:.{}f}'.format
#self.disp_lab.configure(text=my_precision(float(value), digits))
def resolve(self, evt):
resolution = self.resolution
if resolution < 1 or self.tickinterval < 1:
pass
else:
value = self.get()
curr_l = self.convert_to_act(value)
if self.orient == 'horizontal':
if evt.x < curr_l - self.sliderlength / 2:
self.set(value - resolution + 1)
elif evt.x > curr_l + self.sliderlength / 2:
self.set(value + resolution - 1)
else:
if evt.y < curr_l - self.sliderlength / 2:
self.set(value - resolution + 1)
elif evt.y > curr_l + self.sliderlength / 2:
self.set(value + resolution - 1)
if __name__ == "__main__":
root = Tk()
len_val = 400
from_val = 0
to_val = 255
tick_val = 10
dig_val = 0 # dig_val = 2
res_val = 5
or_val = 'vertical'
if or_val =='horizontal':
style_val = 'my.Horizontal.TScale'
#root.geometry(str(len_val+200)+"x200+500+500")
else:
style_val = 'my.Vertical.TScale'
#root.geometry("200x"+str(len_val+200)+"+500+300")
style = Style()
style.theme_use('default')
style.configure(style_val)
fr = Frame(root)
fr.pack(fill='y')
ttks = TtkScale(fr, from_=from_val, to=to_val, orient=or_val,
tickinterval=tick_val, digits=dig_val,
style=style_val, resolution=res_val)
if or_val =='horizontal':
ttks.pack(fill='x', pady=40, padx=5)
else:
ttks.pack(fill='y', pady=5, padx=40)
root.mainloop()