So after hours or reading post and looking at the documentation for tkinter I have found that on windows machines the color options for tkinter scrollbar will not work due to the scrollbar getting its theme from windows directly. My problem is the color of the default theme really clashes with my program and I am trying to find a solution that does not involve importing a different GUI package such as PyQt (I don't have access to pip at work so this is a problem to get new packages)
Aside from using a separate package can anyone point me towards some documentation on how to write my own sidebar for scrolling through the text widget. All I have found so far that is even close to what I want to be able to do is an answer on this question. (Changing the apperance of a scrollbar in tkinter using ttk styles)
From what I can see the example is only changing the background of the scrollbar and with that I was still unable to use the example. I got an error on one of the lines used to configure 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 am not sure as to why it worked for them but not for me.
What I have tried so far is:
How I create my text box and the 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 does not appear to do anything on windows machine. As I have read on other post this has to do with the scrollbar getting its theme natively from windows.
vScrollBar.config(bg = mainBGcolor)
vScrollBar['activebackground'] = mainBGcolor
hScrollBar.config(bg = mainBGcolor)
hScrollBar['activebackground'] = mainBGcolor
I guess it all boils down to:
Is it possible to create my own sidebar (with colors I can change per theme) without the need to import other python packages? If so, where should I start or can someone please link me to the documentation as my searches always seam to lead me back to Tkinter scrollbar Information. As these config() options do work for linux they do not work for windows.
not a complete answer, but have you considered creating your own scrollbar lookalike:
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've left my comments in, and for the life of me i can't seem to get click and dragging the thumb to work correctly, but its a simple scrollbar with the following features:
I've revised the thumb code to fix the 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've fixed the calculation to work out where the new scroll to position will be, and changed from binding on the thumb tag for the track and release events to binding on the whole canvas, so if the user scrolls quickly the binding will still release when the mouse is let go.
I've commented out the binding for when the cursor leaves the canvas so the behavior more closely mimics the existing scroll bar, but can be re enabled if you want it to stop scrolling if the mouse leaves the widget.
As for making two classes, the amended code above lets you use the orient
keyword so you can just drop this class (with styling changes) in place of the default scrollbar, as shown in the example at the bottom.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With