Search code examples
pythontkinterttkttkwidgets

Graphic glitch ttk widgets


It all started from the fact that a normal tk.Scale was not feared correctly (I'm using a custom theme).
I then switched to ttk.Scale but it was not showing the value above the slider when I changed it.
I then discovered ttkwidget, which seems to work but with graphical glitches. Anyone have any ideas to fix?
Code: https://pastebin.com/bxgvsjSF
Screenshot: https://ibb.co/tLdjLrx
P.S. Furthermore, the the widgets that have problems are very slow to load widgets

EDIT: solution is in the comment of the best answer


Solution

  • I have found a way to get rid of the glitches in ttk.LabeledScale. I think the issue is the redrawing of the whole Label widget so I used a Canvas instead. With a Canvas, when the text is moved, there is no need to redraw the background so the animation is smoother.

    The code below is based on the source code of the ttk.LabeledScale (from Python 3.9.1, you can find it in tkinter/ttk.py). But the widget is now based on a Canvas in which the scale and text are added with create_window() and ćreate_text(). I modified the __init__() and _adjust() methods and I added a _on_theme_change() method which is called when the theme changes to update the styling of the canvas and adjust the positions of the elements.

    import tkinter as tk
    from tkinter import ttk
    
    
    class LabeledScale(tk.Canvas):
    
        def __init__(self, master=None, variable=None, from_=0, to=10, **kw):
            self._label_top = kw.pop('compound', 'top') == 'top'
    
            tk.Canvas.__init__(self, master, **kw)
            self._variable = variable or tk.IntVar(master)
            self._variable.set(from_)
            self._last_valid = from_
    
            # use style to set the Canvas background color
            self._style = ttk.Style(self)
            self.configure(bg=self._style.lookup('Horizontal.TScale', 'background'))
            # create the scale
            self.scale = ttk.Scale(self, variable=self._variable, from_=from_, to=to)
            self.scale.bind('<<RangeChanged>>', self._adjust)
    
            # put scale in canvas
            self._scale = self.create_window(0, 0, window=self.scale, anchor='nw')
            # put label in canvas (the position will be updated later)
            self._label = self.create_text(0, 0, text=self._variable.get(),
                                           fill=self._style.lookup('TLabel', 'foreground'),
                                           anchor='s' if self._label_top else 'n')
            # adjust canvas height to fit the whole content
            bbox = self.bbox(self._label)
            self.configure(width=self.scale.winfo_reqwidth(),
                           height=self.scale.winfo_reqheight() + bbox[3] - bbox[1])
            # bindings and trace to update the label
            self.__tracecb = self._variable.trace_variable('w', self._adjust)
            self.bind('<Configure>', self._adjust)
            self.bind('<Map>', self._adjust)
            # update sizes, positions and appearances on theme change
            self.bind('<<ThemeChanged>>', self._on_theme_change)
    
        def destroy(self):
            """Destroy this widget and possibly its associated variable."""
            try:
                self._variable.trace_vdelete('w', self.__tracecb)
            except AttributeError:
                pass
            else:
                del self._variable
            super().destroy()
            self.label = None
            self.scale = None
    
        def _on_theme_change(self, *args):
            """Update position and appearance on theme change."""
            def adjust_height():
                bbox = self.bbox(self._label)
                self.configure(height=self.scale.winfo_reqheight() + bbox[3] - bbox[1])
    
            self.configure(bg=self._style.lookup('Horizontal.TScale', 'background'))
            self.itemconfigure(self._label, fill=self._style.lookup('TLabel', 'foreground'))
            self._adjust()
            self.after_idle(adjust_height)
    
        def _adjust(self, *args):
            """Adjust the label position according to the scale."""
            def adjust_label():
                self.update_idletasks() # "force" scale redraw
                x, y = self.scale.coords()
                if self._label_top:
                    y = 0
                else:
                    y = self.scale.winfo_reqheight()
                # avoid that the label goes off the canvas
                bbox = self.bbox(self._label)
                x = min(max(x, 0), self.winfo_width() - (bbox[2] - bbox[0])/2)
                self.coords(self._label, x, y)  # move label
                self.configure(scrollregion=self.bbox('all'))
                self.yview_moveto(0)  # make sure everything is visible
    
            self.itemconfigure(self._scale, width=self.winfo_width())
            from_ = ttk._to_number(self.scale['from'])
            to = ttk._to_number(self.scale['to'])
            if to < from_:
                from_, to = to, from_
            newval = self._variable.get()
            if not from_ <= newval <= to:
                # value outside range, set value back to the last valid one
                self.value = self._last_valid
                return
    
            self._last_valid = newval
            self.itemconfigure(self._label, text=newval)
            self.after_idle(adjust_label)
    
        @property
        def value(self):
            """Return current scale value."""
            return self._variable.get()
    
        @value.setter
        def value(self, val):
            """Set new scale value."""
            self._variable.set(val)
    
    root = tk.Tk()
    style = ttk.Style(root)
    style.theme_use('alt')
    
    scale = LabeledScale(root, from_=0, to=100, compound='bottom')
    scale.pack(expand=True, fill='x')
    
    root.mainloop()