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

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()

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.

A tkinter notebook with scrollbars if the contents are too large

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)

Python & tkinter: canvas.lift and canvas.lower on overlapping buttons does not work

I created two overlapping buttons on a canvas, using tkinter and python 3.4:
Now I would like to bring button1 to the front (the button you cannot see right now, because it is under button2)
self.canvas.lift(self.button1)
But for some reason this does not work. Just nothing happens. Also lowering button2 has no effect. Can you tell me why?
import tkinter as tk
class Example(tk.Frame):
def __init__(self, root):
tk.Frame.__init__(self, root)
self.canvas = tk.Canvas(self, width=400, height=400, background="bisque")
self.canvas.create_text(50,10, anchor="nw", text="Click to lift button1")
self.canvas.grid(row=0, column=0, sticky="nsew")
self.canvas.bind("<ButtonPress-1>", self.click_on_canvas)
self.button1 = tk.Button(self.canvas, text="button1")
self.button2 = tk.Button(self.canvas, text="button2")
x = 40
self.canvas.create_window(x, x, window=self.button1)
self.canvas.create_window(x+5, x+5, window=self.button2)
def click_on_canvas(self, event):
print("lifting", self.button1)
self.canvas.lift(self.button1)
self.canvas.lower(self.button2)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Instead of calling lift() on the canvas, you need to call it on the widget instance directly:
def click_on_canvas(self, event):
print("lifting", self.button1)
self.button1.lift()
self.button2.lower() # Not necessary to both lift and lower
This is only true for widgets displayed via a window on your canvas.
If you were to draw objects such as lines or rectangles, you would use lift() or tag_raise() on the canvas instance as you were doing before.

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.

How to make reusable scrollbars in Tkinter?

I'd like to create an easy way to add scrollbars to any frame I like. So far, only one works. What's wrong with this script? What's a proper way to do this? I still only have a faint grasp of all the concepts hidden in this, sorry.
import Tkinter as tk
def data(parent):
for i in range(50):
tk.Label(parent,text=i).grid(row=i,column=0)
tk.Label(parent,text="my text"+str(i)).grid(row=i,column=1)
tk.Label(parent,text="..........").grid(row=i,column=2)
class ScrollBar():
#def __init__(self, tk):
# self.canvas = tk.Canvas()
def myfunction(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"), width=200, height=200)
print("orig: ",self.canvas)
def makeScrollBar(self, tk, parent):
self.outerframe = tk.Frame(parent, relief="groove", width=50, height=100, bd=1)
self.outerframe.pack()
self.canvas = tk.Canvas(self.outerframe)
self.innerframe = tk.Frame(self.canvas)
self.myscrollbar = tk.Scrollbar(self.outerframe, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.myscrollbar.set)
self.myscrollbar.pack(side="right",fill="y")
self.canvas.pack(side="left")
self.canvas.create_window((0,0), window=self.innerframe, anchor="nw")
self.innerframe.bind("<Configure>", self.myfunction)
print("orig: ",self.canvas)
return self.innerframe
root = tk.Tk()
root.wm_geometry("800x600+100+100")
scrollbargenerator = ScrollBar()
b = scrollbargenerator.makeScrollBar(tk, root)
c = scrollbargenerator.makeScrollBar(tk, root)
data(b)
data(c)
root.mainloop()
First, you manage to create your scrollbars. If you scroll your mouse over the first scrollbar it will actually scroll the canvas. The thumb does not appear (or do not move) because the scrollregion is never set for this canvas.
In fact, your use of classes and objects is broken. Your Scrollbar class is instantiated once and this only instance update its fields each time makeScrollBar is called. Thus in myfunction callback, self.canvas always refer to the lastly created canvas.
You can easily fix your code my using different scrollbar generators
scrollbargenerator = ScrollBar()
b = scrollbargenerator.makeScrollBar(tk, root)
scrollbargenerator = ScrollBar()
c = scrollbargenerator.makeScrollBar(tk, root)
or capturing the canvas in a closure
def myfunction(self, canvas):
canvas.configure(scrollregion=canvas.bbox("all"), width=200, height=200)
def makeScrollBar(self, tk, parent):
#(...)
self.innerframe.bind("<Configure>", (lambda canvas: (lambda event: self.myfunction(canvas)))(self.canvas))
or relying on the information already present in event
#staticmethod
def myfunction(event):
event.widget.master.configure(scrollregion=event.widget.master.bbox("all"), width=200, height=200)

Categories

Resources