Python Forum

Full Version: Scrollbar problem & general organization
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Pages: 1 2
Below is the relevant portion of code that I would like some assistance with. Eventually I throw a ton of scale widgets on the Frame, but I am unable to scroll with the mousewheel. Also, prior to writing this code I never used the Canvas, just frames, but I needed a scroll bar and hijacked code I found online to get the scrollbar working. Now I want to scroll with the mouse and I tried to put more hijacked code in. 

(1) Are a Canvas and a Frame both necessary to get a scrollbar added to a pile of widgets?
(2) If so, the GUI works, but is my code "pythonic"
(3) Why can't I scroll with the mouse

Thanks for any help!

### Initialize the Tkinter GUI for user interaction ###
self._customGUI = Tk(baseName="userGUIs")
arrowX = self._customGUI.winfo_pointerx()
self._customGUI.protocol('WM_DELETE_WINDOW', self._abnormAbort)
self._customGUI.title("Blend Models")

### Center the user GUI on the screen ###
totWidth = self._customGUI.winfo_screenwidth()
middlePoint = totWidth/2
if arrowX >= middlePoint:
        useWidth = middlePoint
else:
        useWidth = 0
self._guiXPos = useWidth

### Size of initial user GUI
self._setupGUIheight=425
self._setupGUIwidth=325
self._customGUI.geometry("%dx%d+%d+%d" % (self._setupGUIwidth, self._setupGUIheight, self._guiXPos, 0))

### Create a canvas within the main GUI defined early for widget organization ###
self._buttonCanvas = Canvas(self._customGUI, borderwidth=0)
### Create a frame to put in the Canvas
self._buttonFrame = Frame(self._buttonCanvas)
### Create a vertical scrollbar
self._vertScroll = Scrollbar(self._customGUI, orient="vertical",width=18,command=self._buttonCanvas.yview)
self._buttonCanvas.configure(yscrollcommand=self._vertScroll.set)
self._vertScroll.pack(side="right",fill="y")
self._buttonCanvas.pack(side="left",fill="both",expand=True)
self._buttonCanvas.create_window((0,0), window=self._buttonFrame, anchor="nw")
self._buttonFrame.bind("<Configure>", lambda event, canvas=self._buttonCanvas: self._buttonCanvas.configure(scrollregion=self._buttonCanvas.bbox("all")))
self._buttonFrame.bind("<Button-4>", lambda event, canvas=self._buttonCanvas: self._buttonCanvas.yview('scroll', -1, 'units'))
self._buttonFrame.bind("<Button-5>", lambda event, canvas=self._buttonCanvas: self._buttonCanvas.yview('scroll', 1, 'units'))
Quote:pile of widgets
This is your qualifier. For a single widget the answer would be no.
For a bunch of widgets, you wouldn't be able to control scrolling of individual widgets contents,
only all widgets as a whole.
The widgets would all have to be contained by the frame.

I am not sure if this would work for many widgets as I have not tried, but logically it seems that
it would, with the above restrictions.
I should have been more clear. I am not interested in scrolling the scale widgets. I'm interested in scrolling the Frame(?) or the Canvas(?) in order to scroll through all the widgets.
Hi weatherman

Maybe this could be a solution for you:
#!/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, master):
        self.master = master
        self.master.protocol("WM_DELETE_WINDOW", self.close)
        tk.Frame.__init__(self, master)
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)
              
        self.canvas = tk.Canvas(self, bg='steelblue', highlightthickness=0)
        self.canvas.grid(row=0, column=0, sticky='wesn')
        
        self.yscrollbar = tk.Scrollbar(self, orient="vertical",
            width=14, command=self.canvas.yview)
        self.yscrollbar.grid(row=0, column=1, sticky='ns')

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

        self.button_frame = tk.Frame(self.canvas, bg=self.canvas['bg'])
        self.button_frame.pack()
        self.canvas.create_window((0,0), window=self.button_frame, anchor="nw")
        
        for button in BUTTONS:
            tk.Button(self.button_frame, text=button, highlightthickness=0,
            command=partial(self.button_callback, button)).pack(padx=4, pady=2)
        
        self.canvas.bind('<Configure>', self.update)
        self.bind_mouse_scroll(self.canvas, self.yscroll)
        self.bind_mouse_scroll(self.xscrollbar, self.xscroll)
        self.bind_mouse_scroll(self.yscrollbar, self.yscroll)
        
        self.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.canvas.yview_scroll(1, "unit")
        elif event.num == 4 or event.delta > 0:
            self.canvas.yview_scroll(-1, "unit")

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

    def update(self, event):
        if self.canvas.bbox('all') != None:
            region = self.canvas.bbox('all')
            self.canvas.config(scrollregion=region)
            
    def button_callback(self, button):
        print(button)
                      
    def close(self):
        print("Application-Shutdown")
        self.master.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).pack(fill='both', expand=True)
    
    app_win.mainloop()
 
 
if __name__ == '__main__':
    main()      
Please also have a look at this page:
PEP 8 -- Style Guide for Python Code
https://www.python.org/dev/peps/pep-0008/

wuf Smile
So as an update, it appears as though my initial snippet "works", but I have to find an empty space on the GUI for it to work. For example if my mouse is hovered over one of the scales, the canvas wont scroll up and down, but if I move to the edge, the top, or the bottom, it will scroll. How do I get it to scroll the whole canvas while hovered over the widgets too?
Hi weatherman

Here my modified script:
#!/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, master, **options):
        self.master = master
        self.master.protocol("WM_DELETE_WINDOW", self.close)
        
        tk.Frame.__init__(self, master, **options)
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)     
        self.canvas = tk.Canvas(self, bg='steelblue', highlightthickness=0)
            
        self.canvas.grid(row=0, column=0, sticky='wesn')
        
        self.yscrollbar = tk.Scrollbar(self, orient="vertical",
            width=14, command=self.canvas.yview)
        self.yscrollbar.grid(row=0, column=1, sticky='ns')

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

        self.button_frame = tk.Frame(self.canvas, bg=self.canvas['bg'])
        self.button_frame.pack()
        self.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.canvas.bind('<Configure>', self.update)
        self.bind_mouse_scroll(self.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.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.canvas.yview_scroll(1, "unit")
        elif event.num == 4 or event.delta > 0:
            self.canvas.yview_scroll(-1, "unit")

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

    def update(self, event):
        if self.canvas.bbox('all') != None:
            region = self.canvas.bbox('all')
            self.canvas.config(scrollregion=region)
            
    def button_callback(self, button):
        print(button)
                      
    def close(self):
        print("Application-Shutdown")
        self.master.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()      
It is necessary to bind each widget which is place in the self.button frame with the method:
self.bind_mouse_scroll(widget, self.yscroll)

In the above script are these:
All 'buttons' with:
self.bind_mouse_scroll(button, self.yscroll)
and also the container frame for the buttons with:
self.bind_mouse_scroll(self.button_frame, self.yscroll)

Hope this shall help you.

wuf Smile
Getting closer! It now scrolls when over the widgets, but only when the mouse is in ~the nw 1/8th of the canvas. So nw corner it will scroll, anywhere else (besides scrollbar) it does not. Really appreciate the help thus far wuf!
Hi weatherman

Here the scrolling works without problems, if the mouse is somewhere on the canvas surface, including the vertical scroll bar. The scrolling works for this case, of course, only in the vertical direction. For the horizontal direction, the mouse must be positioned on the horizontal scroll bar. Unfortunately i can not determine the behavior described by you when running the script here.

Which OS do you use? I use Ubuntu 16.04. The script works for both Python 2.7 and Python 3.5

wuf Smile
My mistake, I neglected to place the bind statement in the second of two loops that grids the scale widgets so only the first column of widgets would scroll under the mouse. All fixed now.

Thank you so much, your functions really cleaned up my code and make it more understandable. Take care.
Ok, I have another related question that cropped up. I added the following line in the update function to resize the canvas if the window is resized. Note that the variables are named to match my program's syntax and self._customGUI is the root window.

self._buttonCanvas.itemconfig(self._buttonCanvas, width=self._customGUI.winfo_width(), height=self._customGUI.winfo_height())
That worked fine but the frame needed to be resized too. So I tried the following:

self._buttonFrame.pack_forget()
self._buttonFrame.pack()
This successfully resized the frame to the new canvas/root window. However, the problem is that it doesn't scroll anymore. My thought is that  when I did pack_forget() it forgot all the widgets that I binded to the button frame? Do these all need to be binded again even though they remain packed like before? Is there an easy way to do this without starting from scratch if this is indeed the problem? To be clear, all of the code above is placed in the update function, outside of the if statement.

Thanks!
Pages: 1 2