如何让 ttk.Scale 表现得更像 tk.Scale?

Dal*_*ker 6 python tkinter ttk python-3.x

Ttk 版本中也存在几个 Tk 小部件。通常它们具有相同的一般行为,但使用“样式”和“主题”而不是每个实例的外观属性(例如bg,等...)。这很好,因为 Ttk 小部件默认采用操作系统窗口管理器的“标准外观”,无需配置任何有关外观的内容。

但是,由于某种原因,ttk.Scale小部件没有两个非常有用的tk.Scale小部件选项:showvaluetickinterval(请参阅参考资料)。这很奇怪,因为这些更多是关于行为而不是外观。

在保持ttk外观的同时“模仿”这两个选项会很棒。以下代码是我对此的笨拙尝试。问题是:有没有更好的方法?(显然,除了将整个内容封装在一个类中)以及如何合理地获得半自动tickinterval替代品(而不是像下面的代码那样“手动”完成)。

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()
Run Code Online (Sandbox Code Playgroud)

之前代码的结果,在实际“滑动”比例之前,可能如下所示,左侧是 Tk 比例,右侧是 Ttk 比例(每个操作系统/窗口管理器都会明显不同):

在此处输入图片说明

j_4*_*321 6

place您可以使用以下公式给出的位置 x(以像素为单位)以自动方式放置显示值的刻度线和标签:

x = ((value - start) / extent) * (width - sliderlength) + sliderlength / 2
Run Code Online (Sandbox Code Playgroud)

和:

  • value: 刻度值
  • start:刻度的起点(即选项from
  • extent: 结束 - 开始
  • width:刻度的宽度

((value - start) / extent)给出以百分比表示的位置,然后,我只需将其乘以刻度的长度,但要考虑滑块的长度。

然后勾选:(
place(in_=self.scale, bordermode='outside', x=x, rely=1, anchor='n')
用于rely=0, anchor='s'显示值的标签)

下面是完整的代码。我还添加了对该digits选项的支持。

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()
Run Code Online (Sandbox Code Playgroud)

截屏

该小部件的更完整版本可在ttkwidgets模块中使用,名称为 TickScale。