How to make ttk.Scale behave more like tk.Scale?

2024/10/13 14:23:31

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):

enter image description here

Answer

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 ttkclass 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 commandfct = kwargs['command']def cmd(value):fct(value)self.display_value(value)kwargs['command'] = cmdelse:kwargs['command'] = self.display_valueself.scale = ttk.Scale(self, **kwargs)# get slider lengthstyle = 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_']# showvalueif 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')# ticksif 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.tickintervalself.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 / 2def display_value(self, value):# position (in pixel) of the center of the sliderx = self.convert_to_pixels(float(value))# pay attention to the bordershalf_width = self.label.winfo_width() / 2if x + half_width > self.scale.winfo_width():x = self.scale.winfo_width() - half_widthelif x - half_width < 0:x = half_widthself.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() / 2if x - half_width < 0:x = half_widthlabel.place_configure(x=x)# ticks in the middlefor tick, label in zip(self.ticks[1:-1], self.ticklabels[1:-1]):x = self.convert_to_pixels(tick)label.place_configure(x=x)# last ticktick = self.ticks[-1]label = self.ticklabels[-1]x = self.convert_to_pixels(tick)half_width = label.winfo_width() / 2if x + half_width > self.scale.winfo_width():x = self.scale.winfo_width() - half_widthlabel.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()

screenshot

A more complete version of this widget is available in the ttkwidgets module under the name TickScale.

https://en.xdnf.cn/q/69523.html

Related Q&A

pandas cut multiple columns

I am looking to apply a bin across a number of columns.a = [1, 2, 9, 1, 5, 3] b = [9, 8, 7, 8, 9, 1]c = [a, b]print(pd.cut(c, 3, labels=False))which works great and creates:[[0 0 2 0 1 0] [2 2 2 2 2 0]…

Tracking the number of recursive calls without using global variables in Python

How to track the number of recursive calls without using global variables in Python. For example, how to modify the following function to keep track the number of calls?def f(n):if n == 1:return 1else…

Match string in python regardless of upper and lower case differences [duplicate]

This question already has answers here:Case insensitive in(12 answers)Closed 9 years ago.Im trying to find a match value from a keyword using python. My values are stored in a list (my_list) and in the…

Can celery celerybeat use a Database Scheduler without Django?

I have a small infrastructure plan that does not include Django. But, because of my experience with Django, I really like Celery. All I really need is Redis + Celery to make my project. Instead of usin…

Django UserCreationForm custom fields

I am trying to create form for user registration and add some custom fields. For doing that, Ive subclassed UserCretionForm and added fields as shown in django documentation. Then Ive created function-…

Why val_loss and val_acc are not displaying?

When the training starts, in the run window only loss and acc are displayed, the val_loss and val_acc are missing. Only at the end, these values are showed. model.add(Flatten()) model.add(Dense(512, ac…

Is there a python module to solve/integrate a system of stochastic differential equations?

I have a system of stochastic differential equations that I would like to solve. I was hoping that this issue was already address. I am a bit concerned about constructing my own solver because I fear m…

How does thread pooling works, and how to implement it in an async/await env like NodeJS?

I need to run a function int f(int i) with 10_000 parameters and it takes around 1sec to execute due to I/O time. In a language like Python, I can use threads (or async/await, I know, but Ill talk abou…

Calculate centroid of entire GeoDataFrame of points

I would like to import some waypoints/markers from a geojson file. Then determine the centroid of all of the points. My code calculates the centroid of each point not the centroid of all points in the …

Flask-Babel localized strings within js

Im pretty new to both Python and Flask (with Jinja2 as template engine) and I am not sure I am doing it the right way. I am using Flask-Babel extension to add i18n support to my web application. I want…