Another option for a colored scrollbar in a tkinter based program?

So after hours or reading the post and looking at the documentation for tkinter, I found that on windows machines the color options for the tkinter scrollbar would not work due to the scrollbar getting its theme from windows directly. My problem is that the default theme color does clash with my program and I am trying to find a solution that is not related to importing another GUI package like PyQt (I have no pip access at work so this is a problem for new packages)

Apart from using a separate package, can anyone point me to some documentation on how to write my own scrolling bar for a text widget. Everything I have found so far that is even close to what I want to do is the answer to this question. ( Changing scrollbar attribute in tkinter using ttk styles )

From what I can see, this is only changing the background of the scrollbar and with that I still haven't been able to use this example. I got an error on one of the lines used to set the style.

    style.configure("My.Horizontal.TScrollbar", *style.configure("Horizontal.TScrollbar"))
TypeError: configure() argument after * must be an iterable, not NoneType

      

Not sure what to do with this error because I was just following the users' example and I'm not sure why it worked for them but not for me.

I've tried so far:

How do I create my textbox and scrollbars to go with it.

root.text = Text(root, undo = True)
root.text.grid(row = 0, column = 1, columnspan = 1, rowspan = 1, padx =(5,5), pady =(5,5), sticky = W+E+N+S)
root.text.config(bg = pyFrameColor, fg = "white", font=('times', 16))
root.text.config(wrap=NONE)
vScrollBar = tkinter.Scrollbar(root, command=root.text.yview)
hScrollBar = tkinter.Scrollbar(root, orient = HORIZONTAL, command=root.text.xview)
vScrollBar.grid(row = 0, column = 2, columnspan = 1, rowspan = 1, padx =1, pady =1, sticky = E+N+S)
hScrollBar.grid(row = 1 , column = 1, columnspan = 1, rowspan = 1, padx =1, pady =1, sticky = S+W+E)
root.text['yscrollcommand'] = vScrollBar.set
root.text['xscrollcommand'] = hScrollBar.set

      

Following the documentation here My attempt below doesn't seem to do anything on a windows machine. As I read in another post, this is because the scrollbar is bringing its theme out of windows.

vScrollBar.config(bg = mainBGcolor)
vScrollBar['activebackground'] = mainBGcolor
hScrollBar.config(bg = mainBGcolor)
hScrollBar['activebackground'] = mainBGcolor

      

I think it all boils down to:

Is it possible to create my own sidebar (with colors that I can change for each theme) without having to import other python packages? If so, where should I start, or someone can link me to the documentation, as my searches will always stitches to bring me back to scrolling Tkinterbar Information. Since these config () options work on Linux, they don't work for windows.

+3


source to share


1 answer


not a complete answer, but have you thought about creating your own scroll view:

import tkinter as tk

class MyScrollbar(tk.Canvas):
    def __init__(self, master, *args, **kwargs):
        if 'width' not in kwargs:
            kwargs['width'] = 10
        if 'bd' not in kwargs:
            kwargs['bd'] = 0
        if 'highlightthickness' not in kwargs:
            kwargs['highlightthickness'] = 0
        self.command = kwargs.pop('command')

        tk.Canvas.__init__(self, master, *args, **kwargs)

        self.elements = {   'button-1':None,
                            'button-2':None,
                            'trough':None,
                            'thumb':None}

        self._oldwidth = 0
        self._oldheight = 0

        self._sb_start = 0
        self._sb_end = 1

        self.bind('<Configure>', self._resize)
        self.tag_bind('button-1', '<Button-1>', self._button_1)
        self.tag_bind('button-2', '<Button-1>', self._button_2)
        self.tag_bind('trough', '<Button-1>', self._trough)

        self._track = False
        self.tag_bind('thumb', '<ButtonPress-1>', self._thumb_press)
        self.tag_bind('thumb', '<ButtonRelease-1>', self._thumb_release)
        self.tag_bind('thumb', '<Leave>', self._thumb_release)

        self.tag_bind('thumb', '<Motion>', self._thumb_track)

    def _sort_kwargs(self, kwargs):
        for key in kwargs:
            if key in ['buttontype', 'buttoncolor', 'troughcolor', 'thumbcolor', 'thumbtype']:
                self._scroll_kwargs[key] = kwargs.pop(key) # add to custom dict and remove from canvas dict
        return kwargs

    def _resize(self, event):
        width = self.winfo_width()
        height = self.winfo_height()
#       print("canvas: (%s, %s)" % (width, height))
        if self.elements['button-1']: # exists
            if self._oldwidth != width:
                self.delete(self.elements['button-1'])
                self.elements['button-1'] = None
            else:
                pass
        if not self.elements['button-1']: # create
            self.elements['button-1'] = self.create_oval((0,0,width, width), fill='#006cd9', outline='#006cd9', tag='button-1')


        if self.elements['button-2']: # exists
            coords = self.coords(self.elements['button-2'])
            if self._oldwidth != width:
                self.delete(self.elements['button-2'])
                self.elements['button-2'] = None
            elif self._oldheight != height:
                self.move(self.elements['button-2'], 0, height-coords[3])
            else:
                pass
        if not self.elements['button-2']: # create
            self.elements['button-2'] = self.create_oval((0,height-width,width, height), fill='#006cd9', outline='#006cd9', tag='button-2')

        if self.elements['trough']: # exists
            coords = self.coords(self.elements['trough'])
            if (self._oldwidth != width) or (self._oldheight != height):
                self.delete(self.elements['trough'])
                self.elements['trough'] = None
            else:
                pass
        if not self.elements['trough']: # create
            self.elements['trough'] = self.create_rectangle((0,int(width/2),width, height-int(width/2)), fill='#00468c', outline='#00468c', tag='trough')

        self.set(self._sb_start, self._sb_end) # hacky way to redraw thumb
        self.tag_raise('thumb') # ensure thumb always on top of trough

        self._oldwidth = width
        self._oldheight = height

    def _button_1(self, event):
        self.command('scroll', -1, 'pages')
        return 'break'

    def _button_2(self, event):
        self.command('scroll', 1, 'pages')
        return 'break'

    def _trough(self, event):
        width = self.winfo_width()
        height = self.winfo_height()

        size = (self._sb_end - self._sb_start) / 1

        thumbrange = height - width
        thumbsize = int(thumbrange * size)
        thumboffset = int(thumbrange * self._sb_start) + int(width/2)

        thumbpos = int(thumbrange * size / 2) + thumboffset
        if event.y < thumbpos:
            self.command('scroll', -1, 'pages')
        elif event.y > thumbpos:
            self.command('scroll', 1, 'pages')
        return 'break'

    def _thumb_press(self, event):
        print("thumb press: (%s, %s)" % (event.x, event.y))
        self._track = True

    def _thumb_release(self, event):
        print("thumb release: (%s, %s)" % (event.x, event.y))
        self._track = False

    def _thumb_track(self, event):
        if self._track:
#           print("*"*30)
            print("thumb: (%s, %s)" % (event.x, event.y))
            width = self.winfo_width()
            height = self.winfo_height()

#           print("window size: (%s, %s)" % (width, height))

            size = (self._sb_end - self._sb_start) / 1
#           print('size: %s' % size)
            thumbrange = height - width
#           print('thumbrange: %s' % thumbrange)
            thumbsize = int(thumbrange * size)
#           print('thumbsize: %s' % thumbsize)
            clickrange = thumbrange - thumbsize
#           print('clickrange: %s' % clickrange)
            thumboffset = int(thumbrange * self._sb_start) + int(width/2)
#           print('thumboffset: %s' % thumboffset)

            thumbpos = int(thumbrange * size / 2) + thumboffset

#           print("mouse point: %s" % event.y)
#           print("thumbpos: %s" % thumbpos)

            point = (event.y - (width/2) - (thumbsize/2)) / clickrange
#           point = (event.y - (width / 2)) / (thumbrange - thumbsize)
#           print(event.y - (width/2))
#           print(point)
            if point < 0:
                point = 0
            elif point > 1:
                point = 1
#           print(point)
            self.command('moveto', point)
            return 'break'

    def set(self, *args):
        oldsize = (self._sb_end - self._sb_start) / 1

        self._sb_start = float(args[0])
        self._sb_end = float(args[1])

        size = (self._sb_end - self._sb_start) / 1

        width = self.winfo_width()
        height = self.winfo_height()

        if oldsize != size:
            self.delete(self.elements['thumb'])
            self.elements['thumb'] = None

        thumbrange = height - width
        thumbsize = int(thumbrange * size)
        thumboffset = int(thumbrange * self._sb_start) + int(width/2)

        if not self.elements['thumb']: # create
            self.elements['thumb'] = self.create_rectangle((0, thumboffset,width, thumbsize+thumboffset), fill='#4ca6ff', outline='#4ca6ff', tag='thumb')
        else: # move
            coords = self.coords(self.elements['thumb'])
            if (thumboffset != coords[1]):
                self.move(self.elements['thumb'], 0, thumboffset-coords[1])
        return 'break'

if __name__ == '__main__':
    root = tk.Tk()
    lb = tk.Listbox(root)
    lb.pack(side='left', fill='both', expand=True)
    for num in range(0,100):
        lb.insert('end', str(num))

    sb = MyScrollbar(root, width=50, command=lb.yview)
    sb.pack(side='right', fill='both', expand=True)

    lb.configure(yscrollcommand=sb.set)
    root.mainloop()

      

I left my comments and for the life of me I can't get the thumb to click and drag to work properly, but its a simple scrollbar with the following features:

  • up and down buttons that can be colored
  • thumb and trough, which can be individually colored
  • tracks movement in a scrollable widget
  • thumb resizes with the size of the scroll area.


Edit

I revised my thumb code to fix click and drag scrolling:

import tkinter as tk

class MyScrollbar(tk.Canvas):
    def __init__(self, master, *args, **kwargs):
        self._scroll_kwargs = { 'command':None,
                                'orient':'vertical',
                                'buttontype':'round',
                                'buttoncolor':'#006cd9',
                                'troughcolor':'#00468c',
                                'thumbtype':'rectangle',
                                'thumbcolor':'#4ca6ff',
                                }

        kwargs = self._sort_kwargs(kwargs)
        if self._scroll_kwargs['orient'] == 'vertical':
            if 'width' not in kwargs:
                kwargs['width'] = 10
        elif self._scroll_kwargs['orient'] == 'horizontal':
            if 'height' not in kwargs:
                kwargs['height'] = 10
        else:
            raise ValueError
        if 'bd' not in kwargs:
            kwargs['bd'] = 0
        if 'highlightthickness' not in kwargs:
            kwargs['highlightthickness'] = 0

        tk.Canvas.__init__(self, master, *args, **kwargs)

        self.elements = {   'button-1':None,
                            'button-2':None,
                            'trough':None,
                            'thumb':None}

        self._oldwidth = 0
        self._oldheight = 0

        self._sb_start = 0
        self._sb_end = 1

        self.bind('<Configure>', self._resize)
        self.tag_bind('button-1', '<Button-1>', self._button_1)
        self.tag_bind('button-2', '<Button-1>', self._button_2)
        self.tag_bind('trough', '<Button-1>', self._trough)

        self._track = False
        self.tag_bind('thumb', '<ButtonPress-1>', self._thumb_press)
        self.bind('<ButtonRelease-1>', self._thumb_release)
#       self.bind('<Leave>', self._thumb_release)

        self.bind('<Motion>', self._thumb_track)

    def _sort_kwargs(self, kwargs):
        to_remove = []
        for key in kwargs:
            if key in [ 'buttontype', 'buttoncolor', 'buttonoutline',
                        'troughcolor', 'troughoutline',
                        'thumbcolor', 'thumbtype', 'thumboutline',
                        'command', 'orient']:
                self._scroll_kwargs[key] = kwargs[key] # add to custom dict
                to_remove.append(key)

        for key in to_remove:
            del kwargs[key]
        return kwargs

    def _get_colour(self, element):
        if element in self._scroll_kwargs: # if element exists in settings
            return self._scroll_kwargs[element]
        if element.endswith('outline'): # if element is outline and wasn't in settings
            return self._scroll_kwargs[element.replace('outline', 'color')] # fetch default for main element

    def _width(self):
        return self.winfo_width() - 2 # return width minus 2 pixes to ensure fit in canvas

    def _height(self):
        return self.winfo_height() - 2 # return height minus 2 pixes to ensure fit in canvas

    def _resize(self, event):
        width = self._width()
        height = self._height()
        if self.elements['button-1']: # exists
            # delete element if vertical scrollbar and width changed
            # or if horizontal and height changed, signals button needs to change
            if (((self._oldwidth != width) and (self._scroll_kwargs['orient'] == 'vertical')) or
                ((self._oldheight != height) and (self._scroll_kwargs['orient'] == 'horizontal'))):
                self.delete(self.elements['button-1'])
                self.elements['button-1'] = None
        if not self.elements['button-1']: # create
            size = width if (self._scroll_kwargs['orient'] == 'vertical') else height
            rect = (0,0,size, size)
            fill = self._get_colour('buttoncolor')
            outline = self._get_colour('buttonoutline')
            if (self._scroll_kwargs['buttontype'] == 'round'):
                self.elements['button-1'] = self.create_oval(rect, fill=fill, outline=outline, tag='button-1')
            elif (self._scroll_kwargs['buttontype'] == 'square'):
                self.elements['button-1'] = self.create_rectangle(rect, fill=fill, outline=outline, tag='button-1')

        if self.elements['button-2']: # exists
            coords = self.coords(self.elements['button-2'])
            # delete element if vertical scrollbar and width changed
            # or if horizontal and height changed, signals button needs to change
            if (((self._oldwidth != width) and (self._scroll_kwargs['orient'] == 'vertical')) or
                ((self._oldheight != height) and (self._scroll_kwargs['orient'] == 'horizontal'))):
                self.delete(self.elements['button-2'])
                self.elements['button-2'] = None
            # if vertical scrollbar and height changed button needs to move
            elif ((self._oldheight != height) and (self._scroll_kwargs['orient'] == 'vertical')):
                self.move(self.elements['button-2'], 0, height-coords[3])
            # if horizontal scrollbar and width changed button needs to move
            elif ((self._oldwidth != width) and (self._scroll_kwargs['orient'] == 'horizontal')):
                self.move(self.elements['button-2'], width-coords[2], 0)
        if not self.elements['button-2']: # create
            if (self._scroll_kwargs['orient'] == 'vertical'):
                rect = (0,height-width,width, height)
            elif (self._scroll_kwargs['orient'] == 'horizontal'):
                rect = (width-height,0,width, height)
            fill = self._get_colour('buttoncolor')
            outline = self._get_colour('buttonoutline')
            if (self._scroll_kwargs['buttontype'] == 'round'):
                self.elements['button-2'] = self.create_oval(rect, fill=fill, outline=outline, tag='button-2')
            elif (self._scroll_kwargs['buttontype'] == 'square'):
                self.elements['button-2'] = self.create_rectangle(rect, fill=fill, outline=outline, tag='button-2')

        if self.elements['trough']: # exists
            coords = self.coords(self.elements['trough'])
            # delete element whenever width or height changes
            if (self._oldwidth != width) or (self._oldheight != height):
                self.delete(self.elements['trough'])
                self.elements['trough'] = None
        if not self.elements['trough']: # create
            if (self._scroll_kwargs['orient'] == 'vertical'):
                rect = (0, int(width/2), width, height-int(width/2))
            elif (self._scroll_kwargs['orient'] == 'horizontal'):
                rect = (int(height/2), 0, width-int(height/2), height)
            fill = self._get_colour('troughcolor')
            outline = self._get_colour('troughoutline')
            self.elements['trough'] = self.create_rectangle(rect, fill=fill, outline=outline, tag='trough')

        self.set(self._sb_start, self._sb_end) # hacky way to redraw thumb without moving it
        self.tag_raise('thumb') # ensure thumb always on top of trough

        self._oldwidth = width
        self._oldheight = height

    def _button_1(self, event):
        command = self._scroll_kwargs['command']
        if command:
            command('scroll', -1, 'pages')
        return 'break'

    def _button_2(self, event):
        command = self._scroll_kwargs['command']
        if command:
            command('scroll', 1, 'pages')
        return 'break'

    def _trough(self, event):
#       print('trough: (%s, %s)' % (event.x, event.y))
        width = self._width()
        height = self._height()

        coords = self.coords(self.elements['trough'])

        if (self._scroll_kwargs['orient'] == 'vertical'):
            trough_size = coords[3] - coords[1]
        elif (self._scroll_kwargs['orient'] == 'horizontal'):
            trough_size = coords[2] - coords[0]
#       print('trough size: %s' % trough_size)

        size = (self._sb_end - self._sb_start) / 1
        if (self._scroll_kwargs['orient'] == 'vertical'):
            thumbrange = height - width
        elif (self._scroll_kwargs['orient'] == 'horizontal'):
            thumbrange = width - height
        thumbsize = int(thumbrange * size)

        if (self._scroll_kwargs['orient'] == 'vertical'):
            thumboffset = int(thumbrange * self._sb_start) + int(width/2)
        elif (self._scroll_kwargs['orient'] == 'horizontal'):
            thumboffset = int(thumbrange * self._sb_start) + int(height/2)
        thumbpos = int(thumbrange * size / 2) + thumboffset

        command = self._scroll_kwargs['command']
        if command:
            if (((self._scroll_kwargs['orient'] == 'vertical') and (event.y < thumbpos)) or
                ((self._scroll_kwargs['orient'] == 'horizontal') and (event.x < thumbpos))):
                command('scroll', -1, 'pages')
            elif (((self._scroll_kwargs['orient'] == 'vertical') and (event.y > thumbpos)) or
                ((self._scroll_kwargs['orient'] == 'horizontal') and (event.x > thumbpos))):
                command('scroll', 1, 'pages')
        return 'break'

    def _thumb_press(self, event):
        self._track = True

    def _thumb_release(self, event):
        self._track = False

    def _thumb_track(self, event):
#       print('track')
        if self._track:
            width = self._width()
            height = self._height()
#           print("window size: (%s, %s)" % (width, height))

            size = (self._sb_end - self._sb_start) / 1

            coords = self.coords(self.elements['trough'])
#           print('trough coords: %s' % coords)

            if (self._scroll_kwargs['orient'] == 'vertical'):
                trough_size = coords[3] - coords[1]
                thumbrange = height - width
            elif (self._scroll_kwargs['orient'] == 'horizontal'):
                trough_size = coords[2] - coords[0]
                thumbrange = width - height
#           print('trough size: %s' % trough_size)

            thumbsize = int(thumbrange * size)

            if (self._scroll_kwargs['orient'] == 'vertical'):
                pos = max(min(trough_size, event.y - coords[1] - (thumbsize/2)), 0)
            elif (self._scroll_kwargs['orient'] == 'horizontal'):
                pos = max(min(trough_size, event.x - coords[0] - (thumbsize/2)), 0)

#           print('pos: %s' % pos)

            point = pos / trough_size
#           print('point: %s' % point)

            command = self._scroll_kwargs['command']
            if command:
                command('moveto', point)
            return 'break'

    def set(self, *args):
#       print('set: %s' % str(args))
        oldsize = (self._sb_end - self._sb_start) / 1

        self._sb_start = float(args[0])
        self._sb_end = float(args[1])

        size = (self._sb_end - self._sb_start) / 1

        width = self._width()
        height = self._height()

        if oldsize != size:
            self.delete(self.elements['thumb'])
            self.elements['thumb'] = None

        if (self._scroll_kwargs['orient'] == 'vertical'):
            thumbrange = height - width
            thumboffset = int(thumbrange * self._sb_start) + int(width/2)
        elif (self._scroll_kwargs['orient'] == 'horizontal'):
            thumbrange = width - height
            thumboffset = int(thumbrange * self._sb_start) + int(height/2)
        thumbsize = int(thumbrange * size)

        if not self.elements['thumb']: # create
            if (self._scroll_kwargs['orient'] == 'vertical'):
                rect = (0, thumboffset,width, thumbsize+thumboffset)
            elif (self._scroll_kwargs['orient'] == 'horizontal'):
                rect = (thumboffset, 0, thumbsize+thumboffset, height)
            fill = self._get_colour('thumbcolor')
            outline = self._get_colour('thumboutline')
            if (self._scroll_kwargs['thumbtype'] == 'round'):
                self.elements['thumb'] = self.create_oval(rect, fill=fill, outline=outline, tag='thumb')
            elif (self._scroll_kwargs['thumbtype'] == 'rectangle'):
                self.elements['thumb'] = self.create_rectangle(rect, fill=fill, outline=outline, tag='thumb')
        else: # move
            coords = self.coords(self.elements['thumb'])
            if (self._scroll_kwargs['orient'] == 'vertical'):
                if (thumboffset != coords[1]):
                    self.move(self.elements['thumb'], 0, thumboffset-coords[1])
            elif (self._scroll_kwargs['orient'] == 'horizontal'):
                if (thumboffset != coords[1]):
                    self.move(self.elements['thumb'], thumboffset-coords[0], 0)
        return 'break'

if __name__ == '__main__':
    root = tk.Tk()
    root.grid_rowconfigure(1, weight=1)
    root.grid_columnconfigure(1, weight=1)

    root.grid_rowconfigure(3, weight=1)
    root.grid_columnconfigure(3, weight=1)

    lb = tk.Listbox(root)
    lb.grid(column=1, row=1, sticky="nesw")
    for num in range(0,100):
        lb.insert('end', str(num)*100)

    sby1 = MyScrollbar(root, width=50, command=lb.yview)
    sby1.grid(column=2, row=1, sticky="nesw")

    sby2 = MyScrollbar(root, width=50, command=lb.yview, buttontype='square', thumbtype='round')
    sby2.grid(column=4, row=1, sticky="nesw")

    sbx1 = MyScrollbar(root, height=50, command=lb.xview, orient='horizontal', buttoncolor='red', thumbcolor='orange', troughcolor='green')
    sbx1.grid(column=1, row=2, sticky="nesw")

    sbx2 = MyScrollbar(root, height=50, command=lb.xview, orient='horizontal', thumbtype='round')
    sbx2.grid(column=1, row=4, sticky="nesw")

    def x_set(*args):
        sbx1.set(*args)
        sbx2.set(*args)

    def y_set(*args):
        sby1.set(*args)
        sby2.set(*args)

    lb.configure(yscrollcommand=y_set, xscrollcommand=x_set)
    root.mainloop()

      

so I corrected the calculation to figure out where the new scroll would be in position and changed from an anchor to the thumb tag for the track and a release event to anchor to the whole canvas, so if the user quickly scrolls the anchor will still be released when the mouse is released ...
I commented out the snapping when the cursor leaves the canvas, so the behavior more closely mimics an existing scrollbar, but can be enabled if you want it to stop scrolling if the mouse leaves the widget.
Regarding creating the two classes, the modified code above allows the keyword to be used orient

, so you can simply drop that class (with style changes) instead of the default scrollbar as shown in the example below.

+1


source







All Articles