Search code examples
pythontkintertkinter-canvascustomtkintertkinter-layout

Adding scroll bar destroyed the layout structure. How to fix it?


I am writing a small example with tkinter. Here what I got (things are layouted nicely) enter image description here

The codes for it are given below

import tkinter
from tkinter import ttk
from tkinter import messagebox



window = tkinter.Tk()
window.title("Example")

frame = tkinter.Frame(window)
frame.pack()

# Saving User Info
user_info_frame = tkinter.LabelFrame(frame, text="User Information")
user_info_frame.grid(row=0, column=0, padx=20, pady=10)

first_name_label = tkinter.Label(user_info_frame, text="First Name")
first_name_label.grid(row=0, column=0)
last_name_label = tkinter.Label(user_info_frame, text="Last Name")
last_name_label.grid(row=0, column=1)

first_name_entry = tkinter.Entry(user_info_frame)
last_name_entry = tkinter.Entry(user_info_frame)
first_name_entry.grid(row=1, column=0)
last_name_entry.grid(row=1, column=1)

title_label = tkinter.Label(user_info_frame, text="Title")
title_combobox = ttk.Combobox(user_info_frame, values=["", "Mr.", "Ms.", "Dr."])
title_label.grid(row=0, column=2)
title_combobox.grid(row=1, column=2)

age_label = tkinter.Label(user_info_frame, text="Age")
age_spinbox = tkinter.Spinbox(user_info_frame, from_=18, to=110)
age_label.grid(row=2, column=0)
age_spinbox.grid(row=3, column=0)

nationality_label = tkinter.Label(user_info_frame, text="Nationality")
nationality_combobox = ttk.Combobox(user_info_frame,
                                    values=["Africa", "Antarctica", "Asia", "Europe", "North America", "Oceania",
                                            "South America"])
nationality_label.grid(row=2, column=1)
nationality_combobox.grid(row=3, column=1)

for widget in user_info_frame.winfo_children():
    widget.grid_configure(padx=10, pady=5)

# Saving Course Info
courses_frame = tkinter.LabelFrame(frame)
courses_frame.grid(row=1, column=0, sticky="news", padx=20, pady=10)

registered_label = tkinter.Label(courses_frame, text="Registration Status")

reg_status_var = tkinter.StringVar(value="Not Registered")
registered_check = tkinter.Checkbutton(courses_frame, text="Currently Registered",
                                       variable=reg_status_var, onvalue="Registered", offvalue="Not registered")

registered_label.grid(row=0, column=0)
registered_check.grid(row=1, column=0)

numcourses_label = tkinter.Label(courses_frame, text="# Completed Courses")
numcourses_spinbox = tkinter.Spinbox(courses_frame, from_=0, to='infinity')
numcourses_label.grid(row=0, column=1)
numcourses_spinbox.grid(row=1, column=1)

numsemesters_label = tkinter.Label(courses_frame, text="# Semesters")
numsemesters_spinbox = tkinter.Spinbox(courses_frame, from_=0, to="infinity")
numsemesters_label.grid(row=0, column=2)
numsemesters_spinbox.grid(row=1, column=2)

for widget in courses_frame.winfo_children():
    widget.grid_configure(padx=10, pady=5)




window.mainloop()

What I wanted to do next is : 1) add a scroll bar to it; 2) make it sizable with max/min window. However, it destroyed the (evenly)layout of the app. Here what I got enter image description here

it is not sizable with max/min window. Please see the picture below: enter image description here

The codes are below:

import tkinter
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox

window = tkinter.Tk()
window.title("Example")

frame = tkinter.Frame(window)
frame.pack()

# root = tk.Tk()
# essential to enable full window resizing
window.rowconfigure(0, weight=1)
window.columnconfigure(0, weight=1)

# scrollregion is also essential when using scrollbars
canvas = tk.Canvas(
    frame, scrollregion="0 0 10000 10000", width=600, height=400)
canvas.grid(row=0, column=0, sticky=tk.NSEW)

scroll = tk.Scrollbar(frame, orient=tk.VERTICAL, command=canvas.yview, cursor="arrow")
scroll.grid(row=0, column=1, sticky=tk.NS)
canvas.config(yscrollcommand=scroll.set)

###############################

# Saving User Info
user_info_frame = tkinter.LabelFrame(frame, text="User Information")
user_info_frame.grid(row=0, column=0, padx=20, pady=10)

first_name_label = tkinter.Label(user_info_frame, text="First Name")
first_name_label.grid(row=0, column=0)
last_name_label = tkinter.Label(user_info_frame, text="Last Name")
last_name_label.grid(row=0, column=1)

first_name_entry = tkinter.Entry(user_info_frame)
last_name_entry = tkinter.Entry(user_info_frame)
first_name_entry.grid(row=1, column=0)
last_name_entry.grid(row=1, column=1)

title_label = tkinter.Label(user_info_frame, text="Title")
title_combobox = ttk.Combobox(user_info_frame, values=["", "Mr.", "Ms.", "Dr."])
title_label.grid(row=0, column=2)
title_combobox.grid(row=1, column=2)

age_label = tkinter.Label(user_info_frame, text="Age")
age_spinbox = tkinter.Spinbox(user_info_frame, from_=18, to=110)
age_label.grid(row=2, column=0)
age_spinbox.grid(row=3, column=0)

nationality_label = tkinter.Label(user_info_frame, text="Nationality")
nationality_combobox = ttk.Combobox(user_info_frame,
                                    values=["Africa", "Antarctica", "Asia", "Europe", "North America", "Oceania",
                                            "South America"])
nationality_label.grid(row=2, column=1)
nationality_combobox.grid(row=3, column=1)

for widget in user_info_frame.winfo_children():
    widget.grid_configure(padx=10, pady=5)

# Saving Course Info
courses_frame = tkinter.LabelFrame(frame)
courses_frame.grid(row=1, column=0, sticky="news", padx=20, pady=10)

registered_label = tkinter.Label(courses_frame, text="Registration Status")

reg_status_var = tkinter.StringVar(value="Not Registered")
registered_check = tkinter.Checkbutton(courses_frame, text="Currently Registered",
                                       variable=reg_status_var, onvalue="Registered", offvalue="Not registered")

registered_label.grid(row=0, column=0)
registered_check.grid(row=1, column=0)

numcourses_label = tkinter.Label(courses_frame, text="# Completed Courses")
numcourses_spinbox = tkinter.Spinbox(courses_frame, from_=0, to='infinity')
numcourses_label.grid(row=0, column=1)
numcourses_spinbox.grid(row=1, column=1)

numsemesters_label = tkinter.Label(courses_frame, text="# Semesters")
numsemesters_spinbox = tkinter.Spinbox(courses_frame, from_=0, to="infinity")
numsemesters_label.grid(row=0, column=2)
numsemesters_spinbox.grid(row=1, column=2)

for widget in courses_frame.winfo_children():
    widget.grid_configure(padx=10, pady=5)

window.columnconfigure(0, weight=1)
window.rowconfigure(1, weight=1)

frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)

user_info_frame.rowconfigure(0, weight=1)
user_info_frame.columnconfigure(0, weight=1)

courses_frame.rowconfigure(0, weight=1)
courses_frame.columnconfigure(0, weight=1)

item = canvas.create_window((0, 0), anchor=tk.NW, window=user_info_frame)
item1 = canvas.create_window((0, 150), anchor=tk.NW, window=courses_frame)

window.mainloop()

I would truly appreciate if someone could help me to fix this or giving me a pointer on how to fix this. THANK YOU SO MUCH!!


Solution

  • As stated in the comments you have to specify the width for the canvas object, using grid beforehand will not result in the desired outcome.

    Note that user_info_frame.grid(...) and courses_frame.grid(...) are useless because they are override by later canvas.create_window(...). You need to specify the width option in canvas.create_window(...) in order to make the two label frames having same width. – acw1668

    I adapted your code to an object oriented approach and adapted the widget sizing in such a way that both the width of the frames as well as the widgets inside adapt to window resizing:

    import tkinter as tk  # stick to one logic either using tk or tkinter!
    from tkinter import ttk
    
    
    class GUI(tk.Tk):
        def __init__(self):
            super().__init__()  # this basically replaces window as self
            self.title("Example")
            self.minsize(width=600, height=400)  # do not allow resizing smaller than your initial canvas size
            max_height = 1000  # use this to determine length of scroll heigth and if desired max window heigth
            #self.maxsize(width=650, height=max_height)  # → specify if needed
    
            main_frame = tk.Frame(self)  # name it more descriptive
            main_frame.pack(expand=True, fill="both")  # let the canvas parent frame stretch out when resizing
    
            # let the canvas expand to fill main_frame both in width and height
            main_frame.rowconfigure(0, weight=1)
            main_frame.columnconfigure(0, weight=1)
    
            # scrollregion is also essential when using scrollbars → but you only need y in your case!
            self.canvas = tk.Canvas(main_frame, scrollregion=f"0 0 0 {max_height}", width=600, height=400)
            self.canvas.grid(row=0, column=0, sticky=tk.NSEW)
    
            self.scroll = tk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.canvas.yview, cursor="arrow")
            self.scroll.grid(row=0, column=1, sticky=tk.NS)
            self.canvas.config(yscrollcommand=self.scroll.set)
    
            ###############################
    
            # Saving User Info
            user_info_frame = tk.LabelFrame(main_frame, text="User Information")  # unnecessary to manage with .grid
    
            first_name_label = tk.Label(user_info_frame, text="First Name")
            first_name_label.grid(row=0, column=0)
            last_name_label = tk.Label(user_info_frame, text="Last Name")
            last_name_label.grid(row=0, column=1)
    
            first_name_entry = tk.Entry(user_info_frame)
            last_name_entry = tk.Entry(user_info_frame)
            first_name_entry.grid(row=1, column=0)
            last_name_entry.grid(row=1, column=1)
    
            title_label = tk.Label(user_info_frame, text="Title")
            title_combobox = ttk.Combobox(user_info_frame, values=["", "Mr.", "Ms.", "Dr."])
            title_label.grid(row=0, column=2)
            title_combobox.grid(row=1, column=2)
    
            age_label = tk.Label(user_info_frame, text="Age")
            age_spinbox = tk.Spinbox(user_info_frame, from_=18, to=110)
            age_label.grid(row=2, column=0)
            age_spinbox.grid(row=3, column=0)
    
            nationality_label = tk.Label(user_info_frame, text="Nationality")
            nationality_combobox = ttk.Combobox(user_info_frame,
                                                values=["Africa", "Antarctica", "Asia", "Europe", "North America",
                                                        "Oceania",
                                                        "South America"])
            nationality_label.grid(row=2, column=1)
            nationality_combobox.grid(row=3, column=1)
    
            for widget in user_info_frame.winfo_children():
                widget.grid_configure(padx=10, pady=5, sticky="nsew")  # add sticky to stretch widget to fill grid cell
    
            # Saving Course Info
            courses_frame = tk.LabelFrame(main_frame)  # unnecessary to manage with .grid
    
            registered_label = tk.Label(courses_frame, text="Registration Status")
    
            reg_status_var = tk.StringVar(value="Not Registered")
            registered_check = tk.Checkbutton(courses_frame, text="Currently Registered",
                                              variable=reg_status_var, onvalue="Registered", offvalue="Not registered")
    
            registered_label.grid(row=0, column=0)
            registered_check.grid(row=1, column=0)
    
            numcourses_label = tk.Label(courses_frame, text="# Completed Courses")
            numcourses_spinbox = tk.Spinbox(courses_frame, from_=0, to='infinity')
            numcourses_label.grid(row=0, column=1)
            numcourses_spinbox.grid(row=1, column=1)
    
            numsemesters_label = tk.Label(courses_frame, text="# Semesters")
            numsemesters_spinbox = tk.Spinbox(courses_frame, from_=0, to="infinity")
            numsemesters_label.grid(row=0, column=2)
            numsemesters_spinbox.grid(row=1, column=2)
    
            for widget in courses_frame.winfo_children():
                widget.grid_configure(padx=10, pady=5, sticky="nsew")
    
            # rowconfigure seems to make no sense except you want to stretch in height as well (but why then a scrollbar?)
            # but giving all cols the same weight will let them stretch evenly
            user_info_frame.columnconfigure(0, weight=1)
            user_info_frame.columnconfigure(1, weight=1)
            user_info_frame.columnconfigure(2, weight=1)
    
            courses_frame.columnconfigure(0, weight=1)
            courses_frame.columnconfigure(1, weight=1)
            courses_frame.columnconfigure(2, weight=1)
    
            # add the pady and padx in the position tuple & specify width for the frames by callback
            self.padx = 20
            self.canvas.create_window((self.padx, 10), anchor=tk.NW, window=user_info_frame, tags='frame')
            self.canvas.create_window((self.padx, 160), anchor=tk.NW, window=courses_frame, tags='frame')
    
            self.canvas.bind("<Configure>", self.onCanvasConfigure)
    
        def onCanvasConfigure(self, e):
            """stretch canvas frames to match canvas width on resize (respecting the scrollbar and the padding)"""
            self.canvas.itemconfig('frame', width=self.canvas.winfo_width() - self.padx - self.scroll.winfo_width())
    
    
    if __name__ == '__main__':
        app = GUI()
        app.mainloop()