How to make ttk.Scale behave more like tk.Scale?
Question:
Several Tk widgets also exist in Ttk versions. Usually they have the same general behaviour, but use “styles” and “themes” rather than per-instance appearance attributes (such as bg
, etc…). This is good, as the Ttk widgets take the “standard appearance” of the OS’s window manager by default, without needing to configure anything about appearance.
However, for some reason the ttk.Scale
widget does not have two very useful options of the tk.Scale
widget: showvalue
and tickinterval
(see reference). This is strange as those are more about behaviour than about look.
It would be great to “immitate” these two options while keeping a ttk
look.
The following code is my clumsy attempt at this. The question is: is there a better way? (besides encapsulating the whole thing in a class, obviously) and how would one reasonably get a semi-automated tickinterval
substitute (rather than doing it “by hand” as in the code below).
import tkinter as tk
import tkinter.ttk as ttk
# initial setup
root = tk.Tk()
frame = tk.Frame(root)
#################################################################
# create a tk slider showing current value and ticks
# (showvalue=True is the default)
tkslider = tk.Scale(frame, from_=-4, to=4,
orient=tk.HORIZONTAL, tickinterval=2)
#################################################################
#################################################################
# create a ttk slider showing current value and ticks
# use a ttk frame to get ttk style background
ttkslider = ttk.Frame(frame)
# define a callback function to update the value label
def ttk_slider_callback(value):
# 'value' seems to be a string - bug or feature?
value_label.config(text=round(float(value)))
# 'text' can apparently be an int and gets converted into str
# (...) possibly do other stuff
# decompose frame into two ttk labels and a ttk scale
value_label = ttk.Label(ttkslider, text=0)
actual_slider = ttk.Scale(ttkslider, from_=-4, to=4,
command=ttk_slider_callback)
# (orient=tk.HORIZONTAL is the default)
ticks_label = ttk.Label(ttkslider, text=' -4 -2 0 2 4 ')
# put it all together
value_label.grid()
actual_slider.grid()
ticks_label.grid()
#################################################################
# final setup
tkslider.grid(row=0, column=0)
ttkslider.grid(row=0, column=1)
frame.grid()
root.mainloop()
The result of the previous code, before actualy “sliding” the Scales, may look like this, with the Tk Scale on the left and the Ttk Scale on the right (will vary obviously per OS / window manager):
Answers:
You can place in an automated way both the ticks and the label showing the value using place
and their position x (in pixels) given by the formula:
x = ((value - start) / extent) * (width - sliderlength) + sliderlength / 2
with:
value
: the value of the tick
start
: the starting point of the scale (i.e. the from
option)
extent
: end – start
width
: the width of the scale
((value - start) / extent)
gives the position in percent and then, I just have to multiply it by the length of the scale, but taking into account the length of the slider.
Then place the tick with:
place(in_=self.scale, bordermode='outside', x=x, rely=1, anchor='n')
(use rely=0, anchor='s'
for the label showing the value)
And below is the full code. I have also added support for the digits
option.
import tkinter as tk
import tkinter.ttk as ttk
class TtkScale(ttk.Frame):
def __init__(self, master=None, **kwargs):
ttk.Frame.__init__(self, master)
self.columnconfigure(0, weight=1)
self.showvalue = kwargs.pop('showvalue', True)
self.tickinterval = kwargs.pop('tickinterval', 0)
self.digits = kwargs.pop('digits', '0')
if 'command' in kwargs:
# add self.display_value to the command
fct = kwargs['command']
def cmd(value):
fct(value)
self.display_value(value)
kwargs['command'] = cmd
else:
kwargs['command'] = self.display_value
self.scale = ttk.Scale(self, **kwargs)
# get slider length
style = ttk.Style(self)
style_name = kwargs.get('style', '%s.TScale' % (str(self.scale.cget('orient')).capitalize()))
self.sliderlength = style.lookup(style_name, 'sliderlength', default=30)
self.extent = kwargs['to'] - kwargs['from_']
self.start = kwargs['from_']
# showvalue
if self.showvalue:
ttk.Label(self, text=' ').grid(row=0)
self.label = ttk.Label(self, text='0')
self.label.place(in_=self.scale, bordermode='outside', x=0, y=0, anchor='s')
self.display_value(self.scale.get())
self.scale.grid(row=1, sticky='ew')
# ticks
if self.tickinterval:
ttk.Label(self, text=' ').grid(row=2)
self.ticks = []
self.ticklabels = []
nb_interv = round(self.extent/self.tickinterval)
formatter = '{:.' + str(self.digits) + 'f}'
for i in range(nb_interv + 1):
tick = kwargs['from_'] + i * self.tickinterval
self.ticks.append(tick)
self.ticklabels.append(ttk.Label(self, text=formatter.format(tick)))
self.ticklabels[i].place(in_=self.scale, bordermode='outside', x=0, rely=1, anchor='n')
self.place_ticks()
self.scale.bind('<Configure>', self.on_configure)
def convert_to_pixels(self, value):
return ((value - self.start)/ self.extent) * (self.scale.winfo_width()- self.sliderlength) + self.sliderlength / 2
def display_value(self, value):
# position (in pixel) of the center of the slider
x = self.convert_to_pixels(float(value))
# pay attention to the borders
half_width = self.label.winfo_width() / 2
if x + half_width > self.scale.winfo_width():
x = self.scale.winfo_width() - half_width
elif x - half_width < 0:
x = half_width
self.label.place_configure(x=x)
formatter = '{:.' + str(self.digits) + 'f}'
self.label.configure(text=formatter.format(float(value)))
def place_ticks(self):
# first tick
tick = self.ticks[0]
label = self.ticklabels[0]
x = self.convert_to_pixels(tick)
half_width = label.winfo_width() / 2
if x - half_width < 0:
x = half_width
label.place_configure(x=x)
# ticks in the middle
for tick, label in zip(self.ticks[1:-1], self.ticklabels[1:-1]):
x = self.convert_to_pixels(tick)
label.place_configure(x=x)
# last tick
tick = self.ticks[-1]
label = self.ticklabels[-1]
x = self.convert_to_pixels(tick)
half_width = label.winfo_width() / 2
if x + half_width > self.scale.winfo_width():
x = self.scale.winfo_width() - half_width
label.place_configure(x=x)
def on_configure(self, event):
"""Redisplay the ticks and the label so that they adapt to the new size of the scale."""
self.display_value(self.scale.get())
self.place_ticks()
if __name__ == '__main__':
root = tk.Tk()
root.geometry('400x300')
style = ttk.Style(root)
style.configure('my.Horizontal.TScale', sliderlength=10)
s1 = tk.Scale(root, orient='horizontal', tickinterval=0.2, from_=-1,
to=1, showvalue=True, resolution=0.1, sliderlength=10)
s2 = TtkScale(root, style='my.Horizontal.TScale', orient='horizontal',
tickinterval=0.2, from_=-1, to=1, showvalue=True,
digits=1)
ttk.Label(root, text='tk.Scale').pack()
s1.pack(fill='x')
ttk.Label(root, text='ttk.Scale').pack()
s2.pack(fill='x')
root.mainloop()
A more complete version of this widget is available in the ttkwidgets module under the name TickScale.
Several Tk widgets also exist in Ttk versions. Usually they have the same general behaviour, but use “styles” and “themes” rather than per-instance appearance attributes (such as bg
, etc…). This is good, as the Ttk widgets take the “standard appearance” of the OS’s window manager by default, without needing to configure anything about appearance.
However, for some reason the ttk.Scale
widget does not have two very useful options of the tk.Scale
widget: showvalue
and tickinterval
(see reference). This is strange as those are more about behaviour than about look.
It would be great to “immitate” these two options while keeping a ttk
look.
The following code is my clumsy attempt at this. The question is: is there a better way? (besides encapsulating the whole thing in a class, obviously) and how would one reasonably get a semi-automated tickinterval
substitute (rather than doing it “by hand” as in the code below).
import tkinter as tk
import tkinter.ttk as ttk
# initial setup
root = tk.Tk()
frame = tk.Frame(root)
#################################################################
# create a tk slider showing current value and ticks
# (showvalue=True is the default)
tkslider = tk.Scale(frame, from_=-4, to=4,
orient=tk.HORIZONTAL, tickinterval=2)
#################################################################
#################################################################
# create a ttk slider showing current value and ticks
# use a ttk frame to get ttk style background
ttkslider = ttk.Frame(frame)
# define a callback function to update the value label
def ttk_slider_callback(value):
# 'value' seems to be a string - bug or feature?
value_label.config(text=round(float(value)))
# 'text' can apparently be an int and gets converted into str
# (...) possibly do other stuff
# decompose frame into two ttk labels and a ttk scale
value_label = ttk.Label(ttkslider, text=0)
actual_slider = ttk.Scale(ttkslider, from_=-4, to=4,
command=ttk_slider_callback)
# (orient=tk.HORIZONTAL is the default)
ticks_label = ttk.Label(ttkslider, text=' -4 -2 0 2 4 ')
# put it all together
value_label.grid()
actual_slider.grid()
ticks_label.grid()
#################################################################
# final setup
tkslider.grid(row=0, column=0)
ttkslider.grid(row=0, column=1)
frame.grid()
root.mainloop()
The result of the previous code, before actualy “sliding” the Scales, may look like this, with the Tk Scale on the left and the Ttk Scale on the right (will vary obviously per OS / window manager):
You can place in an automated way both the ticks and the label showing the value using place
and their position x (in pixels) given by the formula:
x = ((value - start) / extent) * (width - sliderlength) + sliderlength / 2
with:
value
: the value of the tickstart
: the starting point of the scale (i.e. thefrom
option)extent
: end – startwidth
: the width of the scale
((value - start) / extent)
gives the position in percent and then, I just have to multiply it by the length of the scale, but taking into account the length of the slider.
Then place the tick with:
place(in_=self.scale, bordermode='outside', x=x, rely=1, anchor='n')
(use rely=0, anchor='s'
for the label showing the value)
And below is the full code. I have also added support for the digits
option.
import tkinter as tk
import tkinter.ttk as ttk
class TtkScale(ttk.Frame):
def __init__(self, master=None, **kwargs):
ttk.Frame.__init__(self, master)
self.columnconfigure(0, weight=1)
self.showvalue = kwargs.pop('showvalue', True)
self.tickinterval = kwargs.pop('tickinterval', 0)
self.digits = kwargs.pop('digits', '0')
if 'command' in kwargs:
# add self.display_value to the command
fct = kwargs['command']
def cmd(value):
fct(value)
self.display_value(value)
kwargs['command'] = cmd
else:
kwargs['command'] = self.display_value
self.scale = ttk.Scale(self, **kwargs)
# get slider length
style = ttk.Style(self)
style_name = kwargs.get('style', '%s.TScale' % (str(self.scale.cget('orient')).capitalize()))
self.sliderlength = style.lookup(style_name, 'sliderlength', default=30)
self.extent = kwargs['to'] - kwargs['from_']
self.start = kwargs['from_']
# showvalue
if self.showvalue:
ttk.Label(self, text=' ').grid(row=0)
self.label = ttk.Label(self, text='0')
self.label.place(in_=self.scale, bordermode='outside', x=0, y=0, anchor='s')
self.display_value(self.scale.get())
self.scale.grid(row=1, sticky='ew')
# ticks
if self.tickinterval:
ttk.Label(self, text=' ').grid(row=2)
self.ticks = []
self.ticklabels = []
nb_interv = round(self.extent/self.tickinterval)
formatter = '{:.' + str(self.digits) + 'f}'
for i in range(nb_interv + 1):
tick = kwargs['from_'] + i * self.tickinterval
self.ticks.append(tick)
self.ticklabels.append(ttk.Label(self, text=formatter.format(tick)))
self.ticklabels[i].place(in_=self.scale, bordermode='outside', x=0, rely=1, anchor='n')
self.place_ticks()
self.scale.bind('<Configure>', self.on_configure)
def convert_to_pixels(self, value):
return ((value - self.start)/ self.extent) * (self.scale.winfo_width()- self.sliderlength) + self.sliderlength / 2
def display_value(self, value):
# position (in pixel) of the center of the slider
x = self.convert_to_pixels(float(value))
# pay attention to the borders
half_width = self.label.winfo_width() / 2
if x + half_width > self.scale.winfo_width():
x = self.scale.winfo_width() - half_width
elif x - half_width < 0:
x = half_width
self.label.place_configure(x=x)
formatter = '{:.' + str(self.digits) + 'f}'
self.label.configure(text=formatter.format(float(value)))
def place_ticks(self):
# first tick
tick = self.ticks[0]
label = self.ticklabels[0]
x = self.convert_to_pixels(tick)
half_width = label.winfo_width() / 2
if x - half_width < 0:
x = half_width
label.place_configure(x=x)
# ticks in the middle
for tick, label in zip(self.ticks[1:-1], self.ticklabels[1:-1]):
x = self.convert_to_pixels(tick)
label.place_configure(x=x)
# last tick
tick = self.ticks[-1]
label = self.ticklabels[-1]
x = self.convert_to_pixels(tick)
half_width = label.winfo_width() / 2
if x + half_width > self.scale.winfo_width():
x = self.scale.winfo_width() - half_width
label.place_configure(x=x)
def on_configure(self, event):
"""Redisplay the ticks and the label so that they adapt to the new size of the scale."""
self.display_value(self.scale.get())
self.place_ticks()
if __name__ == '__main__':
root = tk.Tk()
root.geometry('400x300')
style = ttk.Style(root)
style.configure('my.Horizontal.TScale', sliderlength=10)
s1 = tk.Scale(root, orient='horizontal', tickinterval=0.2, from_=-1,
to=1, showvalue=True, resolution=0.1, sliderlength=10)
s2 = TtkScale(root, style='my.Horizontal.TScale', orient='horizontal',
tickinterval=0.2, from_=-1, to=1, showvalue=True,
digits=1)
ttk.Label(root, text='tk.Scale').pack()
s1.pack(fill='x')
ttk.Label(root, text='ttk.Scale').pack()
s2.pack(fill='x')
root.mainloop()
A more complete version of this widget is available in the ttkwidgets module under the name TickScale.