How to resize Canvas scrollable widget? - python-3.x

The idea is that the scrollable canvas and its text widgets grow or fill the entire root/toplevel when I resize it.
I can do this if I work on Frames but for a scrollable frame you need to create a canvas widget and make it scrollable. Now I don't know if the problem is the canvas or the inserted widgets on the canvas?
import tkinter as tk
from tkinter import ttk
class ScrollableFrame():
def __init__(self, container, *args, **kwargs):
self.container = container
self.canvas = tk.Canvas(self.container, bg="green")
self.scrollbar = ttk.Scrollbar(self.container, orient="horizontal", command=self.canvas.xview)
self.scrollable_frame = tk.Frame(self.canvas)
self.scrollable_frame.grid(sticky="wesn")
self.scrollable_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
self.canvas.configure(xscrollcommand=self.scrollbar.set)
self.canvas.grid(row=0, column=0, sticky="wesn")
self.scrollbar.grid(row=1, column=0, sticky="wesn")
if __name__ == "__main__":
root = tk.Tk()
root.configure(bg="grey20")
s = ScrollableFrame(root)
t = tk.Text(s.scrollable_frame)
t.grid(row=0, column=0, sticky="wesn")
t2 = tk.Text(s.scrollable_frame)
t2.grid(row=0, column=1, sticky="wesn")
root.mainloop()
I'm glad for help

Base on the requirement in the comment, you want to grow/shrink the two text boxes when the root window is resized. Below is the modified code with the necessary changes to achieve it:
import tkinter as tk
from tkinter import ttk
class ScrollableFrame():
def __init__(self, container, *args, **kwargs):
self.container = container
self.canvas = tk.Canvas(self.container, bg="green")
self.scrollbar = ttk.Scrollbar(self.container, orient="horizontal", command=self.canvas.xview)
self.scrollable_frame = tk.Frame(self.canvas)
self.scrollable_frame.grid(sticky="wesn")
self.scrollable_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw", tags="scrollable")
self.canvas.configure(xscrollcommand=self.scrollbar.set)
self.canvas.grid(row=0, column=0, sticky="wesn")
self.scrollbar.grid(row=1, column=0, sticky="we") # fill horizontally only
# make self.canvas to fill all the available space of container
container.rowconfigure(0, weight=1)
container.columnconfigure(0, weight=1)
# resize self.scrollable_frame when self.canvas is resized
self.canvas.bind("<Configure>", lambda e: self.canvas.itemconfig("scrollable", width=e.width, height=e.height))
if __name__ == "__main__":
root = tk.Tk()
root.configure(bg="grey20")
s = ScrollableFrame(root)
# make the two text boxes to fill all the space of s.scrollable_frame
s.scrollable_frame.rowconfigure(0, weight=1)
s.scrollable_frame.columnconfigure((0,1), weight=1)
t = tk.Text(s.scrollable_frame)
t.grid(row=0, column=0, sticky="wesn")
t2 = tk.Text(s.scrollable_frame)
t2.grid(row=0, column=1, sticky="wesn")
root.mainloop()
So basically the logic is when the root window is resized, the canvas is resized to fill the root window. Then the frame (scrollable_frame) inside the canvas is resized to fill the canvas and finally the two text widgets are resized to fill the scrollable_frame.

Related

tkinter: how do i make the Scrollbar react to the new size of the frame

from tkinter import *
from tkinter import ttk
class Scrollbar:
def __init__(self, master):
frame = ttk.Frame(master)
frame.pack()
self.canvas = Canvas(frame, width=700, height=930)
self.scrollbar = ttk.Scrollbar(frame, orient=VERTICAL, command=self.canvas.yview)
self.main_frame = ttk.Frame(self.canvas)
self.placing_widgets()
def placing_widgets(self):
self.canvas.pack(side=LEFT, fill=Y)
self.scrollbar.pack(side=RIGHT, fill=Y)
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.canvas.bind('<Configure>', lambda e: self.canvas.configure(scrollregion=self.canvas.bbox('all')))
self.canvas.create_window((0,0), window=self.main_frame, anchor='n')
Frame1(self.main_frame)
class Frame1:
def __init__(self, main_frame):
self.main_frame = main_frame
for i in range(100):
self.button = ttk.Button(self.main_frame, text=f"Button {i}", command=lambda i=i: self.destroying(i))
self.button.grid(row=i, column=0, pady=5)
def destroying(self, i):
for frame in self.main_frame.winfo_children():
frame.destroy()
Frame2(self.main_frame)
class Frame2:
def __init__(self, main_frame):
self.main_frame = main_frame
self.button = ttk.Button(self.main_frame, text='back', command=self.destroying)
self.button.pack()
def destroying(self):
for frame in self.main_frame.winfo_children():
frame.destroy()
Frame1(self.main_frame)
def main():
root = Tk()
root.config(background='black')
Scrollbar(root)
root.mainloop()
if __name__ == '__main__':
main()
I couldn't find a solution to this problem anywhere on the internet.
I have given a scrollbar to a lengthy frame but when I make the frame shorter, I don't know why the scrollbar doesn't resize to the frame's length.
Above is a short program that represents the problem I am talking about. The program has 2 different pages, one with 100 buttons and the other with one button. You can switch between pages with any buttons and see that the page with one button is scrollable which shouldn't be.
Could you please provide a solution to how I could make the scrollbar resize to the frame's length? Thank you for your time.

Adding a Scrollbar to a Tkinter Graph of data which goes off the bottom for the screen [duplicate]

I am using Python to parse entries from a log file, and display the entry contents using Tkinter and so far it's been excellent. The output is a grid of label widgets, but sometimes there are more rows than can be displayed on the screen. I'd like to add a scrollbar, which looks like it should be very easy, but I can't figure it out.
The documentation implies that only the List, Textbox, Canvas and Entry widgets support the scrollbar interface. None of these appear to be suitable for displaying a grid of widgets. It's possible to put arbitrary widgets in a Canvas widget, but you appear to have to use absolute co-ordinates, so I wouldn't be able to use the grid layout manager?
I've tried putting the widget grid into a Frame, but that doesn't seem to support the scrollbar interface, so this doesn't work:
mainframe = Frame(root, yscrollcommand=scrollbar.set)
Can anyone suggest a way round this limitation? I'd hate to have to rewrite in PyQt and increase my executable image size by so much, just to add a scrollbar!
Overview
You can only associate scrollbars with a few widgets, and the root widget and Frame aren't part of that group of widgets.
There are at least a couple of ways to do this. If you need a simple vertical or horizontal group of widgets, you can use a text widget and the window_create method to add widgets. This method is simple, but doesn't allow for a complex layout of the widgets.
A more common general-purpose solution is to create a canvas widget and associate the scrollbars with that widget. Then, into that canvas embed the frame that contains your label widgets. Determine the width/height of the frame and feed that into the canvas scrollregion option so that the scrollregion exactly matches the size of the frame.
Why put the widgets in a frame rather than directly in the canvas? A scrollbar attached to a canvas can only scroll items created with one of the create_ methods. You cannot scroll items added to a canvas with pack, place, or grid. By using a frame, you can use those methods inside the frame, and then call create_window once for the frame.
Drawing the text items directly on the canvas isn't very hard, so you might want to reconsider that approach if the frame-embedded-in-a-canvas solution seems too complex. Since you're creating a grid, the coordinates of each text item is going to be very easy to compute, especially if each row is the same height (which it probably is if you're using a single font).
For drawing directly on the canvas, just figure out the line height of the font you're using (and there are commands for that). Then, each y coordinate is row*(lineheight+spacing). The x coordinate will be a fixed number based on the widest item in each column. If you give everything a tag for the column it is in, you can adjust the x coordinate and width of all items in a column with a single command.
Object-oriented solution
Here's an example of the frame-embedded-in-canvas solution, using an object-oriented approach:
import tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.frame = tk.Frame(self.canvas, background="#ffffff")
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.vsb.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True)
self.canvas.create_window((4,4), window=self.frame, anchor="nw",
tags="self.frame")
self.frame.bind("<Configure>", self.onFrameConfigure)
self.populate()
def populate(self):
'''Put in some fake data'''
for row in range(100):
tk.Label(self.frame, text="%s" % row, width=3, borderwidth="1",
relief="solid").grid(row=row, column=0)
t="this is the second column for row %s" %row
tk.Label(self.frame, text=t).grid(row=row, column=1)
def onFrameConfigure(self, event):
'''Reset the scroll region to encompass the inner frame'''
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == "__main__":
root=tk.Tk()
example = Example(root)
example.pack(side="top", fill="both", expand=True)
root.mainloop()
Procedural solution
Here is a solution that doesn't use a class:
import tkinter as tk
def populate(frame):
'''Put in some fake data'''
for row in range(100):
tk.Label(frame, text="%s" % row, width=3, borderwidth="1",
relief="solid").grid(row=row, column=0)
t="this is the second column for row %s" %row
tk.Label(frame, text=t).grid(row=row, column=1)
def onFrameConfigure(canvas):
'''Reset the scroll region to encompass the inner frame'''
canvas.configure(scrollregion=canvas.bbox("all"))
root = tk.Tk()
canvas = tk.Canvas(root, borderwidth=0, background="#ffffff")
frame = tk.Frame(canvas, background="#ffffff")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
canvas.create_window((4,4), window=frame, anchor="nw")
frame.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
populate(frame)
root.mainloop()
Make it scrollable
Use this handy class to make the frame containing your widgets scrollable. Follow these steps:
create the frame
display it (pack, grid, etc)
make it scrollable
add widgets inside it
call the update() method
import tkinter as tk
from tkinter import ttk
class Scrollable(tk.Frame):
"""
Make a frame scrollable with scrollbar on the right.
After adding or removing widgets to the scrollable frame,
call the update() method to refresh the scrollable area.
"""
def __init__(self, frame, width=16):
scrollbar = tk.Scrollbar(frame, width=width)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=False)
self.canvas = tk.Canvas(frame, yscrollcommand=scrollbar.set)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.canvas.yview)
self.canvas.bind('<Configure>', self.__fill_canvas)
# base class initialization
tk.Frame.__init__(self, frame)
# assign this obj (the inner frame) to the windows item of the canvas
self.windows_item = self.canvas.create_window(0,0, window=self, anchor=tk.NW)
def __fill_canvas(self, event):
"Enlarge the windows item to the canvas width"
canvas_width = event.width
self.canvas.itemconfig(self.windows_item, width = canvas_width)
def update(self):
"Update the canvas and the scrollregion"
self.update_idletasks()
self.canvas.config(scrollregion=self.canvas.bbox(self.windows_item))
Usage example
root = tk.Tk()
header = ttk.Frame(root)
body = ttk.Frame(root)
footer = ttk.Frame(root)
header.pack()
body.pack()
footer.pack()
ttk.Label(header, text="The header").pack()
ttk.Label(footer, text="The Footer").pack()
scrollable_body = Scrollable(body, width=32)
for i in range(30):
ttk.Button(scrollable_body, text="I'm a button in the scrollable frame").grid()
scrollable_body.update()
root.mainloop()
Extends class tk.Frame to support a scrollable Frame
This class is independent from the widgets to be scrolled and can be used to replace a standard tk.Frame.
import tkinter as tk
class ScrollbarFrame(tk.Frame):
"""
Extends class tk.Frame to support a scrollable Frame
This class is independent from the widgets to be scrolled and
can be used to replace a standard tk.Frame
"""
def __init__(self, parent, **kwargs):
tk.Frame.__init__(self, parent, **kwargs)
# The Scrollbar, layout to the right
vsb = tk.Scrollbar(self, orient="vertical")
vsb.pack(side="right", fill="y")
# The Canvas which supports the Scrollbar Interface, layout to the left
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.canvas.pack(side="left", fill="both", expand=True)
# Bind the Scrollbar to the self.canvas Scrollbar Interface
self.canvas.configure(yscrollcommand=vsb.set)
vsb.configure(command=self.canvas.yview)
# The Frame to be scrolled, layout into the canvas
# All widgets to be scrolled have to use this Frame as parent
self.scrolled_frame = tk.Frame(self.canvas, background=self.canvas.cget('bg'))
self.canvas.create_window((4, 4), window=self.scrolled_frame, anchor="nw")
# Configures the scrollregion of the Canvas dynamically
self.scrolled_frame.bind("<Configure>", self.on_configure)
def on_configure(self, event):
"""Set the scroll region to encompass the scrolled frame"""
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
Usage:
class App(tk.Tk):
def __init__(self):
super().__init__()
sbf = ScrollbarFrame(self)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
sbf.grid(row=0, column=0, sticky='nsew')
# sbf.pack(side="top", fill="both", expand=True)
# Some data, layout into the sbf.scrolled_frame
frame = sbf.scrolled_frame
for row in range(50):
text = "%s" % row
tk.Label(frame, text=text,
width=3, borderwidth="1", relief="solid") \
.grid(row=row, column=0)
text = "this is the second column for row %s" % row
tk.Label(frame, text=text,
background=sbf.scrolled_frame.cget('bg')) \
.grid(row=row, column=1)
if __name__ == "__main__":
App().mainloop()

Tkinter Python Frame with Scroll bar

I have below code which will create Label and Entry widgets. Also I created Scroll bar for this window but both scroll bars are not working.
Please let me know why the scroll bar is disabled. Did the code is doing wrong ?
Below is my code which will create 50 labels and entry widgets but still scroll bars are not enabled.
import tkinter as tk
from tkinter import ttk
class DoubleScrollbarFrame(ttk.Frame):
def __init__(self, master, **kwargs):
'''
Initialisation. The DoubleScrollbarFrame consist of :
- an horizontal scrollbar
- a vertical scrollbar
- a canvas in which the user can place sub-elements
'''
ttk.Frame.__init__(self, master, **kwargs)
# Canvas creation with double scrollbar
self.hscrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
self.vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
self.sizegrip = ttk.Sizegrip(self)
self.canvas = tk.Canvas(self, bd=0, highlightthickness=0,
yscrollcommand=self.vscrollbar.set,
xscrollcommand=self.hscrollbar.set)
self.vscrollbar.config(command=self.canvas.yview)
self.hscrollbar.config(command=self.canvas.xview)
def pack(self, **kwargs):
'''
Pack the scrollbar and canvas correctly in order to recreate the
same look as MFC's windows.
'''
self.hscrollbar.pack(side=tk.BOTTOM, fill=tk.X, expand=tk.FALSE)
self.vscrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=tk.FALSE)
self.sizegrip.pack(in_=self.hscrollbar, side=tk.BOTTOM, anchor="se")
self.canvas.pack(side=tk.LEFT, padx=5, pady=5,
fill=tk.BOTH, expand=tk.TRUE)
ttk.Frame.pack(self, **kwargs)
def get_frame(self):
'''
Return the "frame" useful to place inner controls.
'''
return self.canvas
if __name__ == '__main__':
# Top-level frame
root = tk.Tk()
root.title("Double scrollbar with tkinter")
root.minsize(width=600, height=600)
frame = DoubleScrollbarFrame(root, relief="sunken")
# Add controls here
subframe = ttk.Frame(frame.get_frame())
txt = ttk.Label(subframe, text="Add things here !")
for i in range(50):
ttk.Label(subframe, text="Field %d: " % i).grid(row=i, column=0,
sticky="w")
ttk.Entry(subframe, width=25).grid(row=i, column=1, sticky="ew")
subframe.pack(padx=15, pady=15, fill=tk.BOTH, expand=tk.TRUE)
frame.pack(padx=5, pady=5, expand=True, fill=tk.BOTH)
# launch the GUI
root.mainloop()

Arranging size of a frame in tkinter

I am making a simple toolbox for my app. I have used the class method, which inherits Frame as its super class. In my main file I import this class.
It will be a main window which all widgets will be in it. But there is a problem, here is the source code:
from tkinter import *
class ToolBox(Frame):
def __init__(self, master=None,
width=100, height=300):
Frame.__init__(self, master,
width=100, height=300)
self.pack()
Button(self, text="B").grid(row=0, sticky=(N,E,W,S))
Button(self, text="B").grid(row=0, column=1, sticky=(N,E,W,S))
Button(self, text="B").grid(row=1, column=0,sticky=(N,E,W,S))
Button(self, text="B").grid(row=1, column=1, sticky=(N,E,W,S))
Button(self, text="B").grid(row=2, column=0, sticky=(N,E,W,S))
Button(self, text="B").grid(row=2, column=1, sticky=(N,E,W,S))
I import this in here:
from tkinter import *
import toolbox as tl
root = Tk()
frame = Frame(root, width=400, height=400)
frame.pack()
tl.ToolBox(frame).pack()
root.mainloop()
Main window, which is the root who has the frame, must be 400 in widht and height. But it appears in dimensions of my toolbox. I want the toolbox to be in the main window. How can I solve this?
You can force the root window to have specific dimensions using the geometry method.
root = Tk()
root.geometry("400x400")
If you would also like the buttons to stretch evenly to fill the whole root window, you need to do two things:
Call rowconfigure and columnconfigure to set the weight of the root and each frame that is a parent of your buttons.
specify the sticky parameter for every button and frame that is a child of your root.
Here's an example. I removed your frame Frame, since it didn't seem to be doing anything. Toolbox is already a frame, after all, and there's not much point putting a frame inside a frame.
from tkinter import *
class ToolBox(Frame):
def __init__(self, master=None,
width=100, height=300):
Frame.__init__(self, master,
width=width, height=height)
for i in range(2):
self.grid_columnconfigure(i, weight=1)
for j in range(3):
self.grid_rowconfigure(j, weight=1)
Button(self, text="B").grid(row=0, sticky=(N,E,W,S))
Button(self, text="B").grid(row=0, column=1, sticky=(N,E,W,S))
Button(self, text="B").grid(row=1, column=0,sticky=(N,E,W,S))
Button(self, text="B").grid(row=1, column=1, sticky=(N,E,W,S))
Button(self, text="B").grid(row=2, column=0, sticky=(N,E,W,S))
Button(self, text="B").grid(row=2, column=1, sticky=(N,E,W,S))
root = Tk()
root.geometry("400x400")
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
ToolBox(root).grid(sticky="news")
root.mainloop()
Now your root is properly sized, and your buttons stretch to fill it.

Resize background image to window size with Tkinter grid manager

I don't know how to resize the background image to the window size with the tkinter grid manager. My image is resizing alone, without resizing the window. It functions with the pack manager but I want to use it with the grid manager.
from tkinter import *
from PIL import Image, ImageTk
root = Tk()
root.title("Title")
root.geometry("800x600")
class Example(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.grid(sticky=N+S+E+W)
self.image = Image.open("courbe.gif")
self.img_copy= self.image.copy()
self.background_image = ImageTk.PhotoImage(self.image)
self.background = Label(self, image=self.background_image)
self.background.grid(row =0, column =0,sticky="nsew")
self.background.grid_rowconfigure(0, weight=1)
self.background.grid_columnconfigure(0, weight=1)
self.background.bind('<Configure>', self._resize_image)
def _resize_image(self,event):
new_width = event.width
new_height = event.height
self.image = self.img_copy.resize((new_width, new_height))
self.background_image = ImageTk.PhotoImage(self.image)
self.background.configure(image = self.background_image)
e = Example(root)
e.grid(row =0, column =0,sticky="nsew")
e.grid_rowconfigure(0, weight=1)
e.grid_columnconfigure(0, weight=1)
root.mainloop()
You shouldn't bind to the background changing, but the window (master) changing. Then you can get the new height and width of the window using master.winfo_width() and master.winfo_height().
So in your __init__ use
self.master = master
self.master.bind('<Configure>', self._resize_image)
and in your self._resize_image use
new_width = self.master.winfo_width()
new_height = self.master.winfo_height()

Resources