A tkinter notebook with scrollbars if the contents are too large - python

I am trying to create an interface with tkinter. It should have a control panel on the left, and a set of 3 tabs on the right.
The tabs will show images, which may be too large for the screen, however the exact size will only be known after they have been dynamically created by the program. So I want to have a Notebook that expands to fill the top window, and if the image is too large for the notebook frame, scrollbars should appear to allow for all the image to be seen. I can't find a way to attach scrollbars to the notebook, so I have attached them to the canvases inside the notebook.
My code below expands the canvas to fit the image, but the scrollbar is useless as it stretches to fit the canvas, which is larger than the frame, and so can end up outside the window.
I think I want to set the scrollregion to the size of the containing frame. Dynamically changing scrollregion of a canvas in Tkinter seems to be related, but I can't work out how to apply that to my code:
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
class App(tk.Frame):
def __init__(self, parent, *args, **kwargs):
self.parent = parent
super().__init__(parent, *args, **kwargs)
self.pack(expand=True, fill=tk.BOTH)
self.inputframe = ttk.Frame(self)
self.inputframe.pack(side=tk.LEFT, expand=False, fill=tk.Y)
self.outputnotebook = ttk.Notebook(self)
self.outputnotebook.pack(side=tk.RIGHT, expand=True, fill=tk.BOTH)
self.build_inputframe()
self.build_outputnotebook()
def build_inputframe(self):
run_button = ttk.Button(self.inputframe, text="Run", command=self.draw)
run_button.grid(column=2, row=0, sticky=(tk.W, tk.E))
# rest of app...
def build_outputnotebook(self):
actnet_frame = ttk.Frame(self.outputnotebook)
critnet_frame = ttk.Frame(self.outputnotebook)
xscrollbar = ttk.Scrollbar(actnet_frame, orient=tk.HORIZONTAL)
xscrollbar.grid(row=1, column=0, sticky=tk.E + tk.W)
self.outputnotebook.add(
actnet_frame,
text="Tab 1",
sticky=tk.N + tk.S + tk.E + tk.W)
self.outputnotebook.add(critnet_frame, text="Tab 2")
self.actnet_canvas = tk.Canvas(actnet_frame, width=400, height=400,
xscrollcommand=xscrollbar.set)
self.actnet_canvas.grid(row=0, sticky=tk.N + tk.S + tk.E + tk.W)
xscrollbar.config(command=self.actnet_canvas.xview)
def draw(self):
act_image = Image.open('critnet.png') # image is 875 x 175
width, height = act_image.size
actphoto = ImageTk.PhotoImage(act_image)
self.actnet_canvas.delete("all")
self.actnet_canvas.create_image(0, 0, anchor=tk.NW, image=actphoto)
self.actnet_canvas.image = actphoto
self.actnet_canvas['scrollregion'] = (0, 0, width, height)
self.actnet_canvas['width'] = width
self.actnet_canvas['height'] = height
#do similar for other tabs.
root = tk.Tk()
root.geometry("800x600")
app = App(root)
app.mainloop()

The problem in your code is that you resize the canvas at the size of the picture, so the size of the canvas and of the scrollregion are the same. And it makes your canvas to big for your tab and your scrollbar useless.
I suppressed the lines
self.actnet_canvas['width'] = width
self.actnet_canvas['height'] = height
from the draw function and the scrollbar behaved as expected.
Edit: I had missed the other part of the question. To resize the canvas and scrollbar with the window, I added the following lines in build_outputnotebook:
actnet_frame.rowconfigure(0,weight=1)
actnet_frame.columnconfigure(0,weight=1)

Related

Python tkinter window resizes, but widgets never change or move

Python beginner. I placed a scrollbar widget in window and that works, but no matter what I do I can't get the scrollbox widget to change size. Could go with a larger scrollbox or for it to resize when the window resizes, but can't figure out how to force either to happen. Tried lots of different solutions, but feels like the grid and canvas are defaulting to a size and can't figure out how to change that. Help would be appreciated. Code is below:
import tkinter as tk
from tkinter import ttk
import os
import subprocess
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=True)
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))
root = tk.Tk()
root.title("application")
root.geometry('750x800')
dbEnvs = ['a','b']
x = 1
header = ttk.Frame(root)
body = ttk.Frame(root)
footer = ttk.Frame(root)
header.pack(side = "top")
body.pack()
footer.pack(side = "top")
#setup Environment selection
envLabel = tk.Label(header, text="Environment:")
envLabel.grid(row=0,column=0,sticky='nw')
dbselection = tk.StringVar()
scrollable_body = Scrollable(body, width=20)
x = 1
for row in range(50):
checkboxVar = tk.IntVar()
checkbox = ttk.Checkbutton(scrollable_body, text=row, variable=checkboxVar)
checkbox.var = checkboxVar # SAVE VARIABLE
checkbox.grid(row=x, column=1, sticky='w')
x += 1
scrollable_body.update()
#setup buttons on the bottom
pullBtn = tk.Button(footer, text='Pull')
pullBtn.grid(row=x, column=2, sticky='ew')
buildBtn = tk.Button(footer, text='Build')
buildBtn.grid(row=x, column=3, sticky='ew')
compBtn = tk.Button(footer, text='Compare')
compBtn.grid(row=x, column=4, sticky='ew')
root.mainloop()
have tried anchoring, changing the window base size and multiple other things (8 or 19 different items, plus reading lots of posts), but they normally involve packing and since I used grids that normally and ends with more frustration and nothing changed.
If you want the whole scrollbox to expand to fill the body frame, you must instruct pack to do that using the expand and fill options:
body.pack(side="top", fill="both", expand=True)
Another problem is that you're setting expand to True for the scrollbar. That's probably not something you want to do since it means the skinny scrollbar will be allocated more space than is needed. So, remove that attribute or set it to False.
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=False)
tip: when debugging layout problems, the problems are easier to visualize when you temporarily give each widget a unique color. For example, set the canvas to one color, body to another, the instance of Scrollable to another, etc. This will let you see which parts are visible, which are growing or shrinking, which are inside the borders of others, etc.

Python - Passing a frame containing tkinter widgets into a class

I'm tring to build a scrollable GUI window containing ttk.Entry and ttk.Label.
The only way doing so (as noted in many questions here )- is by creating a Canvas, with a frame that contains all that widgets.
So- my goal is to make such class- that gets as a parameter a frame containing all needed widgets, and display it in a window with horizontal and vertical scroll bars ( since I need it in many displays inside my code ).
After coding successfully - I tried to make a class, but it shows only empty green canvas.
Any ideas what am I doing wrong?
import tkinter as tk
from tkinter import ttk
class CanvasWidgets(ttk.Frame):
def __init__(self, master, frame_in, width=100, height=100):
ttk.Frame.__init__(self, master)
self.master = master
self.frame = frame_in
self.width, self.height = width, height
self.build_gui()
def build_gui(self):
self.canvas = tk.Canvas(self.master, self.width, self.height, bg='light green')
# self.frame = ttk.Frame(self.frame_in)
self.frame.bind("<Configure>", self.onFrameConfigure)
self.vsb = tk.Scrollbar(self.frame, orient="vertical", command=self.canvas.yview)
self.hsb = tk.Scrollbar(self.frame, orient="horizontal", command=self.canvas.xview)
self.canvas.configure(yscrollcommand=self.vsb.set, xscrollcommand=self.hsb.set)
self.vsb.grid(row=0, column=1, sticky=tk.N + tk.S + tk.W)
self.hsb.grid(row=1, column=0, sticky=tk.W + tk.N + tk.E)
self.canvas.create_window((4, 4), window=self.frame, anchor="nw")
self.canvas.grid(row=0, column=0)
def onFrameConfigure(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
root = tk.Tk()
frame = ttk.Frame(root)
rows, cols = 5, 5
for row in range(rows):
for col in range(cols):
ttk.Label(frame, text=[row, col], relief=tk.SUNKEN, width=5).grid(row=row, column=col, sticky=tk.E)
a = CanvasWidgets(root, frame)
a.grid()
root.mainloop()
The first problem is that you are placing the canvas in master when it needs to be in self. Think of the instance of CanvasWindow as a box in which you are going to put everything else.
The second problem is that, because the frame was created before the canvas, the frame has a lower stacking order than the canvas. You need to call lift on the frame to get it to be above the canvas.
The third problem is that you're putting the scrollbars in frame. You can't put them in the inner frame because they control the inner frame. Instead, they also need to be in self. Both the scrollbar and the canvas need to share a common parent.
The fourth problem is that the frame isn't a child of the canvas, so it won't be clipped by the borders of the canvas. It would be better if the CanvasWidgets created the frame, and then the caller can get the frame and add widgets to it.
For example:
a = CanvasWidgets(root)
rows, cols = 5, 5
for row in range(rows):
for col in range(cols):
label = ttk.Label(a.frame, text=[row, col], relief=tk.SUNKEN, width=5)
label.grid(row=row, column=col, sticky=tk.E)

Scrollbar disappears from Tkinter canvas, and refuses to adhere to side-packing

I am trying to attach a scrollbar to a Tkinter canvas.
To test the scrollbar, I dynamically generated 100 "Hello World" labels, and packed these inside a content-holding frame.
My code fails. Issues:
The scrollbar does not even appear.
The content frame instead expands to the full height of the 100-packed labels.
Interestingly, the scrollbar magically reappears if I shift self.frame_for_content.pack() under self.scrollbar.pack(). (Why this is so is beyond me, but the scrollbar still does not work or adhere to the side packing behaviour either.)
I have attempted to incorporate some of Brian Oakley's suggestions on scroll region and bbox, to no avail.
I have reduced the code to the bare minimum, but am unable to push through. Would appreciate the help.
import tkinter as tk
import tkinter.ttk as ttk
class TestGUI(tk.Tk):
def __init__(self):
super().__init__()
self.canvas = tk.Canvas(self)
self.frame_for_content = tk.Frame(self.canvas)
self.canvas_frame = self.canvas.create_window((0,0), window=self.frame_for_content, anchor=tk.NW)
self.scrollbar = tk.Scrollbar(self.canvas, orient=tk.VERTICAL, command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.frame_for_content.pack()
self.canvas.pack(side=tk.LEFT)
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
for i in range(100):
tk.Label(self.frame_for_content, text="Hello World - " + str(i)).pack()
self.update()
self.canvas.config(scrollregion=self.canvas.bbox(tk.ALL))
TestGUI().mainloop()
You don't want self.frame_for_content.pack() because you're using the canvas like a geometry manager for that widget, so self.canvas.create_window takes the place of .pack or .grid.
To get the sizes right, you can get the width & height from the canvas bounding box.
I think this does what you want:
import tkinter as tk
class TestGUI(tk.Tk):
def __init__(self):
super().__init__()
self.canvas = tk.Canvas(self)
self.frame_for_content = tk.Frame(self.canvas)
self.canvas_frame = self.canvas.create_window((0,0), window=self.frame_for_content, anchor=tk.NW)
self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL, command=self.canvas.yview)
self.canvas.pack(side=tk.LEFT)
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
for i in range(100):
tk.Label(self.frame_for_content, text="Hello World - " + str(i)).pack()
self.update()
bbox = self.canvas.bbox(tk.ALL)
self.canvas.config(yscrollcommand=self.scrollbar.set,
width=bbox[2], height=bbox[3], scrollregion=bbox)
TestGUI().mainloop()

Scrollable Frame Python Tkinter

I am working n a project that has a scroll able frame. It lets me add widgets to the frame but I can not get the frame to scroll and show the rest of the widgets. I have compared my code to other scroll able frames online and I could not notice the difference. Any one see the solution.
Code:
from Tkinter import *
import ttk
import os
class GUI(Frame):
def __init__(self, parent):
Frame.__init__(self,parent)
self.pack(fill=BOTH, expand=YES)
def gameView(self):
self.mainFrame = Frame(self)
self.mainFrame.pack(side=TOP)
self.scroller = ttk.Scrollbar(self.mainFrame, orient=VERTICAL)
self.scroller.pack(side=RIGHT, fill=Y)
self.canvas = Canvas(self.mainFrame, bd=0)
self.canvas.pack(fill=BOTH, side=LEFT)
self.viewArea = Frame(self.canvas, bg="Pink")
self.viewArea.pack(side=TOP, fill=BOTH)
self.canvas.config(yscrollcommand=self.scroller.set)
self.scroller.config(command=self.canvas.yview)
self.canvas.create_window((0,0), window=self.viewArea, anchor=NW, width=783, height=650)
self.viewArea.bind("<Configure>", self.scrollCom)
self.itemHolder = Frame(self.viewArea, bg="Pink")
self.itemHolder.pack(side=TOP)
self.gameGather()
def scrollCom(self, event):
self.canvas.config(scrollregion=self.canvas.bbox("all"), width=783, height=650)
def gameGather(self):
for i in range(0, 50):
label = Label(self.viewArea, text="Pie")
label.pack(side=TOP)
root = Tk()
root.title("School Vortex 2.0")
root.geometry("800x650")
root.resizable(0,0)
gui = GUI(root)
gui.gameView()
root.mainloop()
When you put the window on the canvas you are explicitly giving it a height and a width. Because of that, the actual width and height of the frame is completely ignored. Because the frame is almost exactly the height of the canvas, there's nothing to scroll.
If you remove the width and height options from the call to create_window your frame will be scrollable.

Python Tkinter Scrollbar for entire window

Is there a way to add a scrollbar to my entire window without putting everything into a frame? I've set everything up with .grid, and I don't like the idea of wrapping a frame around everything.
root = Tk()
root.maxsize(900,600)
circus()#calls the function to set up everything
root.mainloop()
How to add scrollbars to full window in tkinter ?
here is the answer for python 3...
from tkinter import *
from tkinter import ttk
root = Tk()
root.title('Full Window Scrolling X Y Scrollbar Example')
root.geometry("1350x400")
# Create A Main frame
main_frame = Frame(root)
main_frame.pack(fill=BOTH,expand=1)
# Create Frame for X Scrollbar
sec = Frame(main_frame)
sec.pack(fill=X,side=BOTTOM)
# Create A Canvas
my_canvas = Canvas(main_frame)
my_canvas.pack(side=LEFT,fill=BOTH,expand=1)
# Add A Scrollbars to Canvas
x_scrollbar = ttk.Scrollbar(sec,orient=HORIZONTAL,command=my_canvas.xview)
x_scrollbar.pack(side=BOTTOM,fill=X)
y_scrollbar = ttk.Scrollbar(main_frame,orient=VERTICAL,command=my_canvas.yview)
y_scrollbar.pack(side=RIGHT,fill=Y)
# Configure the canvas
my_canvas.configure(xscrollcommand=x_scrollbar.set)
my_canvas.configure(yscrollcommand=y_scrollbar.set)
my_canvas.bind("<Configure>",lambda e: my_canvas.config(scrollregion= my_canvas.bbox(ALL)))
# Create Another Frame INSIDE the Canvas
second_frame = Frame(my_canvas)
# Add that New Frame a Window In The Canvas
my_canvas.create_window((0,0),window= second_frame, anchor="nw")
for thing in range(100):
Button(second_frame ,text=f"Button {thing}").grid(row=5,column=thing,pady=10,padx=10)
for thing in range(100):
Button(second_frame ,text=f"Button {thing}").grid(row=thing,column=5,pady=10,padx=10)
root.mainloop()
you might be able to set a scrollbarr to root.
scrollderoot = tkinter.Scrollbar(orient="vertical", command=root.yview)
scrollderoot.grid(column=5, row=0, sticky='ns', in_=root) #instead of number 5, set the column as the expected one for the scrollbar. Sticky ns will might be neccesary.
root.configure(yscrollcommand=scrollderoot.set)
Honestly i didn't tried this but "should" work. Good luck.
This approach uses no Frame objects and is different in that it creates a very large Canvas with Scrollbars and asks you for an image to display on it.
The screen is then set with self.root.wm_attributes("-fullscreen", 1)
and self.root.wm_attributes("-top", 1)
Press the Escape key or Alt-F4 to close.
import tkinter as tk
from tkinter import filedialog as fido
class BigScreen:
def __init__( self ):
self.root = tk.Tk()
self.root.rowconfigure(0, weight = 1)
self.root.columnconfigure(0, weight = 1)
w, h = self.root.winfo_screenwidth(), self.root.winfo_screenheight()
self.canvas = tk.Canvas(self.root, scrollregion = f"0 0 {w*2} {h*2}")
self.canvas.grid(row = 0, column = 0, sticky = tk.NSEW)
self.makescroll(self.root, self.canvas )
self.imagename = fido.askopenfilename( title = "Pick Image to View" )
if self.imagename:
self.photo = tk.PhotoImage(file = self.imagename).zoom(2, 2)
self.window = self.canvas.create_image(
( 0, 0 ), anchor = tk.NW, image = self.photo)
self.root.bind("<Escape>", self.closer)
self.root.wm_attributes("-fullscreen", 1)
self.root.wm_attributes("-top", 1)
def makescroll(self, parent, thing):
v = tk.Scrollbar(parent, orient = tk.VERTICAL, command = thing.yview)
v.grid(row = 0, column = 1, sticky = tk.NS)
thing.config(yscrollcommand = v.set)
h = tk.Scrollbar(parent, orient = tk.HORIZONTAL, command = thing.xview)
h.grid(row = 1, column = 0, sticky = tk.EW)
thing.config(xscrollcommand = h.set)
def closer(self, ev):
self.root.destroy()
if __name__ == "__main__":
Big = BigScreen()
Big.root.mainloop()
My previous answer went well beyond the question asked so this is a cut-down version more accurately answers the question.
I did try the answer of Akash Shendage which didn't work for me out of the box. But with a few adjustments go it working.
#!/bin/env python3
from tkinter import ttk
import tkinter as tk
root = tk.Tk()
root.title('Full Window Scrolling X Y Scrollbar Example')
root.geometry("1350x400")
# Create A Main frame
main_frame = tk.Frame(root)
main_frame.pack(fill=tk.BOTH,expand=1)
# Create Frame for X Scrollbar
sec = tk.Frame(main_frame)
sec.pack(fill=tk.X,side=tk.BOTTOM)
# Create A Canvas
my_canvas = tk.Canvas(main_frame)
my_canvas.pack(side=tk.LEFT,fill=tk.BOTH,expand=1)
# Add A Scrollbars to Canvas
x_scrollbar = ttk.Scrollbar(sec,orient=tk.HORIZONTAL,command=my_canvas.xview)
x_scrollbar.pack(side=tk.BOTTOM,fill=tk.X)
y_scrollbar = ttk.Scrollbar(main_frame,orient=tk.VERTICAL,command=my_canvas.yview)
y_scrollbar.pack(side=tk.RIGHT,fill=tk.Y)
# Configure the canvas
my_canvas.configure(xscrollcommand=x_scrollbar.set)
my_canvas.configure(yscrollcommand=y_scrollbar.set)
my_canvas.bind("<Configure>",lambda e: my_canvas.config(scrollregion= my_canvas.bbox(tk.ALL)))
# Create Another Frame INSIDE the Canvas
second_frame = tk.Frame(my_canvas)
# Add that New Frame a Window In The Canvas
my_canvas.create_window((0,0),window= second_frame, anchor="nw")
for thing in range(100):
tk.Button(second_frame ,text=f"Button {thing}").grid(row=5,column=thing,pady=10,padx=10)
for thing in range(100):
tk.Button(second_frame ,text=f"Button {thing}").grid(row=thing,column=5,pady=10,padx=10)
root.mainloop()
From the great effbot docs:
In Tkinter, the scrollbar is a separate widget that can be attached to
any widget that support the standard scrollbar interface. Such widgets
include:
the Listbox widget.
the Text widget.
the Canvas widget
the Entry widget
So, you cannot directly use a scrollbar in a Frame. It may be possible to create your own Frame sub-class that supports the scrollbar interface.
Out of the 4 widgets listed above, only one allows other widgets within it: the Canvas. You can use a Canvas to have scrollable content, but placing widgets within a Canvas does not use pack or grid, but uses explicit pixel locations (i.e. painting on the Canvas).
Here's a class, and some example usage, that uses the .place method to add a scrollbar for the whole window. You can create a Frame object, and place it at your desired (x, y) coordinates. Then, simply pass your Frame object in place of root in main.frame to create a scrollable window at your desired coordinates.
from tkinter import *
class ScrollableFrame:
"""A scrollable tkinter frame that will fill the whole window"""
def __init__ (self, master, width, height, mousescroll=0):
self.mousescroll = mousescroll
self.master = master
self.height = height
self.width = width
self.main_frame = Frame(self.master)
self.main_frame.pack(fill=BOTH, expand=1)
self.scrollbar = Scrollbar(self.main_frame, orient=VERTICAL)
self.scrollbar.pack(side=RIGHT, fill=Y)
self.canvas = Canvas(self.main_frame, yscrollcommand=self.scrollbar.set)
self.canvas.pack(expand=True, fill=BOTH)
self.scrollbar.config(command=self.canvas.yview)
self.canvas.bind(
'<Configure>',
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
)
self.frame = Frame(self.canvas, width=self.width, height=self.height)
self.frame.pack(expand=True, fill=BOTH)
self.canvas.create_window((0,0), window=self.frame, anchor="nw")
self.frame.bind("<Enter>", self.entered)
self.frame.bind("<Leave>", self.left)
def _on_mouse_wheel(self,event):
self.canvas.yview_scroll(-1 * int((event.delta / 120)), "units")
def entered(self,event):
if self.mousescroll:
self.canvas.bind_all("<MouseWheel>", self._on_mouse_wheel)
def left(self,event):
if self.mousescroll:
self.canvas.unbind_all("<MouseWheel>")
# Example usage
obj = ScrollableFrame(
master,
height=300, # Total required height of canvas
width=400 # Total width of master
)
objframe = obj.frame
# use objframe as the main window to make widget

Categories

Resources