Python Forum
[Tkinter] Scrollbar problem & general organization
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] Scrollbar problem & general organization
#11
Hi weatherman

Python programmers try to follow a certain styleguide by writing a script. If you have time, I would take a look at it. Contact the following link:
PEP 8 -- Style Guide for Python Code
https://www.python.org/dev/peps/pep-0008/

So my style differs from yours
weatherman              wuf
--------------------------------------------
self._customGUI          self.app_win
self._buttonFrame       self.button_frame
self._buttonCanvas      self.button_canvas
--------------------------------------------
The following script changes the size of the 'button_frame' depending on the change of the main window 'app_win'. The width and height of the button_canvas must be used as the reference for width and height for 'button_frame', not the width & height of the main window 'app_win'. Der Grund hierfür ist weil das 'button_frame' befindet sich auf der Fläche des 'button_canvas'. The reason for this is because the 'button_frame' is located within the 'button_canvas'. That means the 'buttonframe' is not directly on the 'button_canvas' but in a canvas object 'window'.

Following modifications are necessary:
1)
self.button_frame = tk.Frame(self.button_canvas, bg='yellow') #self.button_canvas['bg'])
self.button_frame.pack()
self.button_frame.propagate(False)
self.button_canvas.create_window((0,0), window=self.button_frame, anchor="nw")
2)
def update(self, event):
    if self.button_canvas.bbox('all') != None:
        region = self.button_canvas.bbox('all')
            self.button_canvas.config(scrollregion=region)
    self.button_frame.config(width=self.button_canvas.winfo_width(),
        height=self.button_canvas.winfo_height())
To highlight the 'button_frame' I changed its background to 'bg = yellow'.
Here is the modified script with sizing of 'button_frame' (scrolling does not work!):
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from functools import partial

try:
    # Tkinter for Python 2.xx
    import Tkinter as tk
except ImportError:
    # Tkinter for Python 3.xx
    import tkinter as tk

APP_TITLE = "Srollable Canvas"
APP_XPOS = 100
APP_YPOS = 100
APP_WIDTH = 325
APP_HEIGHT = 425

NUM_OF_BUTTONS = 20
BUTTONS = ['Button-{0:02d}'.format(nr) for nr in range(1, NUM_OF_BUTTONS+1)]


class Application(tk.Frame):

    def __init__(self, app_win, **options):
        self.app_win = app_win
        self.app_win.protocol("WM_DELETE_WINDOW", self.close)
        
        tk.Frame.__init__(self, app_win, **options)
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)
            
        self.button_canvas = tk.Canvas(self, bg='steelblue', highlightthickness=0)
        self.button_canvas.grid(row=0, column=0, sticky='wesn')
        
        self.yscrollbar = tk.Scrollbar(self, orient="vertical",
            width=14, command=self.button_canvas.yview)
        self.yscrollbar.grid(row=0, column=1, sticky='ns')

        self.xscrollbar = tk.Scrollbar(self, orient="horizontal",
            width=14, command=self.button_canvas.xview)
        self.xscrollbar.grid(row=1, column=0, sticky='we')
        
        self.button_canvas.configure(
            xscrollcommand=self.xscrollbar.set,
            yscrollcommand=self.yscrollbar.set)

        self.button_frame = tk.Frame(self.button_canvas, bg='yellow') #self.button_canvas['bg'])
        self.button_frame.pack()
        self.button_frame.propagate(False)
        self.button_canvas.create_window((0,0), window=self.button_frame, anchor="nw")
        
        for button in BUTTONS:
            button = tk.Button(self.button_frame, text=button, 
            highlightthickness=0, command=partial(self.button_callback, button))
            button.pack(padx=4, pady=2)
            self.bind_mouse_scroll(button, self.yscroll)
        
        self.button_canvas.bind('<Configure>', self.update)
        self.bind_mouse_scroll(self.button_canvas, self.yscroll)
        self.bind_mouse_scroll(self.xscrollbar, self.xscroll)
        self.bind_mouse_scroll(self.yscrollbar, self.yscroll)
        self.bind_mouse_scroll(self.button_frame, self.yscroll)
        
        self.button_canvas.focus_set()
        
    def bind_mouse_scroll(self, parent, mode):
        #~~ Windows only
        parent.bind("<MouseWheel>", mode)
        #~~ Unix only        
        parent.bind("<Button-4>", mode)
        parent.bind("<Button-5>", mode)

    def yscroll(self, event):
        if event.num == 5 or event.delta < 0:
            self.button_canvas.yview_scroll(1, "unit")
        elif event.num == 4 or event.delta > 0:
            self.button_canvas.yview_scroll(-1, "unit")

    def xscroll(self, event):
        if event.num == 5 or event.delta < 0:
            self.button_canvas.xview_scroll(1, "unit")
        elif event.num == 4 or event.delta > 0:
            self.button_canvas.xview_scroll(-1, "unit")

    def update(self, event):
        if self.button_canvas.bbox('all') != None:
            region = self.button_canvas.bbox('all')
            self.button_canvas.config(scrollregion=region)
        self.button_frame.config(width=self.button_canvas.winfo_width(),
            height=self.button_canvas.winfo_height())
        
    def button_callback(self, button):
        print(button)
                      
    def close(self):
        print("Application-Shutdown")
        self.app_win.destroy()

    
def main():
    app_win = tk.Tk()
    app_win.title(APP_TITLE)
    app_win.geometry("+{}+{}".format(APP_XPOS, APP_YPOS))
    app_win.geometry("{}x{}".format(APP_WIDTH, APP_HEIGHT))
    
    app = Application(app_win)
    app.pack(fill='both', expand=True)
    
    app_win.mainloop()
 
 
if __name__ == '__main__':
    main()      
If you now change the size of the main window 'app_win', the 'button_frame' always adapts the new size of the 'button_canvas'! Since the 'button_frame' is located in a canvas window object, its size automatically changes with the 'button_frame'. This means the largest object within the 'button_canvas' is the window object and not the arrangement of the buttons on the 'button_frame'! That's why the scrolling does not work anymore!. If you place each button in a separate 'canvas window object' and place it on the 'button_canvas' using the coordinates, the scrolling will work again! If you want to place additional widgets on the 'button_canvas', you have to put them in a 'canvas window object' and position them by means of coordinates.

Here the script for placing widgets in 'canvas window objects':
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from functools import partial

try:
    # Tkinter for Python 2.xx
    import Tkinter as tk
except ImportError:
    # Tkinter for Python 3.xx
    import tkinter as tk

APP_TITLE = "Srollable Canvas"
APP_XPOS = 100
APP_YPOS = 100
APP_WIDTH = 325
APP_HEIGHT = 425

NUM_OF_BUTTONS = 20
BUTTONS = ['Button-{0:02d}'.format(nr) for nr in range(1, NUM_OF_BUTTONS+1)]


class Application(tk.Frame):

    def __init__(self, app_win, **options):
        self.app_win = app_win
        self.app_win.protocol("WM_DELETE_WINDOW", self.close)
        
        tk.Frame.__init__(self, app_win, **options)
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)
            
        self.button_canvas = tk.Canvas(self, bg='steelblue', highlightthickness=0)
        self.button_canvas.grid(row=0, column=0, sticky='wesn')
        
        self.yscrollbar = tk.Scrollbar(self, orient="vertical",
            width=14, command=self.button_canvas.yview)
        self.yscrollbar.grid(row=0, column=1, sticky='ns')

        self.xscrollbar = tk.Scrollbar(self, orient="horizontal",
            width=14, command=self.button_canvas.xview)
        self.xscrollbar.grid(row=1, column=0, sticky='we')
        
        self.button_canvas.configure(
            xscrollcommand=self.xscrollbar.set,
            yscrollcommand=self.yscrollbar.set)
        
        xpos = 0
        ypos = 0
        ygap = 30
        for button in BUTTONS:
            button = tk.Button(self.button_canvas, text=button, highlightthickness=0,
                command=partial(self.button_callback, button))
            button.pack(padx=4, pady=2)
            self.button_canvas.create_window((xpos, ypos), window=button, anchor="nw")
            ypos += ygap
            
            self.bind_mouse_scroll(button, self.yscroll)

        
        self.button_canvas.bind('<Configure>', self.update)
        self.bind_mouse_scroll(self.button_canvas, self.yscroll)
        self.bind_mouse_scroll(self.xscrollbar, self.xscroll)
        self.bind_mouse_scroll(self.yscrollbar, self.yscroll)
        
        self.button_canvas.focus_set()
        
    def bind_mouse_scroll(self, parent, mode):
        #~~ Windows only
        parent.bind("<MouseWheel>", mode)
        #~~ Unix only        
        parent.bind("<Button-4>", mode)
        parent.bind("<Button-5>", mode)

    def yscroll(self, event):
        if event.num == 5 or event.delta < 0:
            self.button_canvas.yview_scroll(1, "unit")
        elif event.num == 4 or event.delta > 0:
            self.button_canvas.yview_scroll(-1, "unit")

    def xscroll(self, event):
        if event.num == 5 or event.delta < 0:
            self.button_canvas.xview_scroll(1, "unit")
        elif event.num == 4 or event.delta > 0:
            self.button_canvas.xview_scroll(-1, "unit")

    def update(self, event):
        if self.button_canvas.bbox('all') != None:
            region = self.button_canvas.bbox('all')
            self.button_canvas.config(scrollregion=region)
        
    def button_callback(self, button):
        print(button)
                      
    def close(self):
        print("Application-Shutdown")
        self.app_win.destroy()

    
def main():
    app_win = tk.Tk()
    app_win.title(APP_TITLE)
    app_win.geometry("+{}+{}".format(APP_XPOS, APP_YPOS))
    app_win.geometry("{}x{}".format(APP_WIDTH, APP_HEIGHT))
    
    app = Application(app_win)
    app.pack(fill='both', expand=True)
    
    app_win.mainloop()
 
 
if __name__ == '__main__':
    main()      
Have fun studying the whole.

wuf Smile
Reply
#12
UPDATE:
I was able to mimic the same structure you outlined for me, but was still unable to get the buttons to fill the entire window. An alternative would be to always center the buttons on the resized window. Either way is fine. I just don't want a lot of empty space in the window if someone stretches it out. And I have the scales in a 5x5 grid, so that entire 5x5 grid could be centered or expanded.

Thank you for all the information, wuf!

I'm having a slight issue completely integrating your suggestions into my code. This script is not stand-alone, and is part of a software system powered by Java. There are a lot of under-the-hood processes going on that call I can't control and there are some built in GUI functions, so the nomenclature I'm using is mainly to avoid stepping on things that are going on out of my control. Also because of this, I haven't ever been able to crack how to make separate classes, passing GUI stuff around without crashing things (this part is probably just due to my lack of knowledge, I imagine it's possible). Anyway, the problem I'm having is that the buttons don't fill the window when it's resized. My guess is that it is because I do not have the app.pack(fill=BOTH, expand=True) command as you do because I am already in a class and don't call a separate one, I do everything in one. So assuming all of the functions are in one class, how would I integrate that pack command to fill the window?

Thanks!
Reply
#13
Hi weatherman

With this small modification the buttons size should adapt themselves by changing the size of the main window.
for button in BUTTONS:
    button = tk.Button(self.button_frame, text=button, 
        highlightthickness=0, command=partial(self.button_callback, button))
    button.pack(fill='both', expand=True) #padx=4, pady=2)
    self.bind_mouse_scroll(button, self.yscroll)
wuf Smile
Reply
#14
Update: If I add the following to the update method, I get my desired behavior. But is this really the best way to do this? What if I have multiple columns of data, it's not as simple as just making the button the width of the window, must then divide by number of columns. Hopefully there is a way to do this without having to know the geometry of the widgets on the canvas.

for id in self.buttonCanvas.find_all():
    self.buttonCanvas.itemconfigure(id, width=self.winfo_width())

So what appears to be happening is that, yes, the button is filling the widget window created on the canvas, but the window is not filling the canvas. So what I'd like to see is behavior as if you are feeding a new value to width in the create_window command as the screen is resized. I want the 'steelblue' to always be covered in other words.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [Tkinter] Help create scrollbar in chatbot with tkinter on python fenec10 4 1,509 Aug-07-2023, 02:59 PM
Last Post: deanhystad
  [Tkinter] Scrollbar apffal 7 3,118 Oct-11-2021, 08:26 PM
Last Post: deanhystad
Question [Tkinter] How to configure scrollbar dimension? water 6 3,418 Jan-03-2021, 06:16 PM
Last Post: deanhystad
  [PyQt] scrollbar in tab issac_n 1 3,593 Aug-04-2020, 01:33 PM
Last Post: deanhystad
  [Tkinter] Scrollbar in tkinter PatrickNoir 2 3,299 Jul-26-2020, 06:02 PM
Last Post: deanhystad
  [Tkinter] Help with Scrollbar JJota 6 3,647 Mar-10-2020, 05:25 AM
Last Post: Larz60+
  [Tkinter] Scrollbar doesn't work on Canvas in Tkinter DeanAseraf1 3 9,351 Sep-19-2019, 03:26 PM
Last Post: joe_momma
  [Tkinter] Same Scrollbar for two text area smabubakkar 3 2,852 Jun-19-2019, 05:26 PM
Last Post: Denni
  Scrollbar rturus 5 16,826 Jun-06-2019, 01:04 PM
Last Post: heiner55
  [PyGUI] Create a scrollbar in GUI to add test cases mamta_parida 1 3,615 Sep-27-2018, 11:57 AM
Last Post: Larz60+

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020