Vertical ttk Scales#

Vertical Scaling#

vertical calibration 0 - 100 range

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#

horizontal and vertical Scales as 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()