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.
source to share
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.
source to share