Related
I am new to Tkinter, I am trying to create a full-screen scrollable frame using Tkinter and canvas. here is my code:
from tkinter import *
from tkinter import ttk
root = Tk()
root.title('Learn To Code at Codemy.com')
root.geometry("500x400")
def FrameWidth(event):
canvas_width = event.width
canvas_height = event.height
my_canvas.itemconfig(canvas_frame, width = canvas_width)
# my_canvas.itemconfig(canvas_frame, height = canvas_height)
# my_canvas.itemconfig(canvas_frame, width = canvas_width, height = canvas_height)
def OnFrameConfigure(event):
my_canvas.configure(scrollregion=my_canvas.bbox("all"))
# Create A Main Frame
main_frame = Frame(root)
main_frame.grid(row=0, column=0, sticky='news')
root.grid_columnconfigure(0, weight=1)
root.grid_rowconfigure(0, weight=1)
main_frame.grid_columnconfigure(0, weight=1)
main_frame.grid_rowconfigure(0, weight=1)
# Create A Canvas
my_canvas = Canvas(main_frame, bg='red')
my_canvas.grid(row=0, column=0, sticky='news')
my_canvas.grid_columnconfigure(0, weight=1)
my_canvas.grid_rowconfigure(0, weight=1)
# Create ANOTHER Frame INSIDE the Canvas
second_frame = Frame(my_canvas, bg='blue')
# Add that New frame To a Window In The Canvas
canvas_frame = my_canvas.create_window((0,0), window=second_frame, anchor="nw")
# Add A Scrollbar To The Canvas
my_scrollbar = ttk.Scrollbar(main_frame, orient=VERTICAL, command=my_canvas.yview)
my_scrollbar.grid(row=0, column=1, sticky='ns')
my_canvas.configure(yscrollcommand=my_scrollbar.set)
main_frame.grid_rowconfigure(0, weight=1)
main_frame.grid_columnconfigure(0, weight=5)
main_frame.grid_columnconfigure(1, weight=0)
# Configure The Canvas
my_canvas.bind('<Configure>', FrameWidth)
second_frame.bind('<Configure>', OnFrameConfigure)
for thing in range(5):
Button(second_frame, text=f'Button {thing} Yo!').grid(row=thing, column=0, pady=10, padx=10, sticky = 'news')
my_label = Label(second_frame, text="It's Friday Yo!").grid(row=3, column=1, sticky='news')
second_frame.grid_columnconfigure(0,weight=4)
second_frame.grid_columnconfigure(1,weight=1)
root.mainloop()
my problem is that the frame with a blue background does not expand to full size and fill the canvas window, here is a screenshot of my app, my question is how to expand the second frame to fill entire window:
enter image description here
I have been looking at various sources but to no avail.
In my tkinter window, I have running Labels which start with "start" to 1 and all the way to 100. I have two problems:
My labels are not stretching out in the Y direction even though fill=tk.Y is called (for self.firstlabel.pack and x.pack), as evidenced by the green background. Why is this happening?
I've also tried to get the canvas height by first calling self.update() and then printing out the canvas height using print(self.canvas.winfo_height()). However the canvas height is still 1. Why is this the case?
Thank you all in advance for your answers!
import tkinter as tk
class Test(tk.Tk):
def __init__(self, tasks=None):
super().__init__()
self.title("Test")
# setting up container
container = tk.Frame(self, background="bisque")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
container.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))
container.grid_columnconfigure(0, weight=1)
container.grid_rowconfigure(0, weight=3)
container.grid_rowconfigure(1, weight=1)
# two frames in container, container1 for labels, container2 for textbox
self.container1 = tk.Frame(container, background="yellow")
self.container2 = tk.Frame(container, background="blue")
self.container1.grid(row=0, sticky=(tk.N, tk.S, tk.E, tk.W))
self.container2.grid(row=1, sticky=(tk.N, tk.S, tk.E, tk.W))
self.canvas = tk.Canvas(self.container1, borderwidth=0, background="green")
self.frameinsidecanvas = tk.Frame(self.canvas, background="pink")
self.canvas.pack(fill="both", expand=True)
self.frameinsidecanvas.pack(fill="both", expand=True)
self.update()
print(self.canvas.winfo_height())
# setup scrollbar
self.horizontalscrollbar = tk.Scrollbar(self.container1, orient="horizontal", command=self.canvas.xview)
self.canvas.configure(xscrollcommand=self.horizontalscrollbar.set)
self.horizontalscrollbar.pack(side="bottom", fill="x")
self.canvas.create_window((0, 100), window=self.frameinsidecanvas, anchor="w")
self.textbox = tk.Text(self.container2, height=1)
self.textbox.pack(fill="both", expand=True)
# creating the instructional label
self.firstlabel = tk.Label(self.frameinsidecanvas, text="Start")
# self.tasks.append(self.firstlabel)
self.firstlabel.pack(side="left", fill=tk.Y, expand=True)
# showing the labels
for labels in range(0,100):
x = tk.Label(self.frameinsidecanvas, text=str(labels))
x.pack(side=tk.LEFT, fill=tk.Y, expand=True)
self.bind("<Configure>", self.on_frame_configure)
def on_frame_configure(self, event=None):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == "__main__":
test = Test()
test.wm_geometry("1100x500")
test.mainloop()
The problem is that at the start of the program the canvas height is equal to 1. After that the tkinter window is rendered it changes the value.
You can simply bind a canvas listner to <Configure> for get when the height changes and after modify the inner fram of consequence.
import tkinter as tk
# You want to display the labels in column or that each label had the height of the canvas?
class Test(tk.Tk):
def __init__(self, tasks=None):
super().__init__()
self.canvas_width = None
self.title("Test")
# setting up container
container = tk.Frame(self, background="bisque")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
container.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))
container.grid_columnconfigure(0, weight=1)
container.grid_rowconfigure(0, weight=3)
container.grid_rowconfigure(1, weight=1)
# two frames in container, container1 for labels, container2 for textbox
self.container1 = tk.Frame(container, background="yellow")
self.container2 = tk.Frame(container, background="blue")
self.container1.grid(row=0, sticky=(tk.N, tk.S, tk.E, tk.W))
self.container2.grid(row=1, sticky=(tk.N, tk.S, tk.E, tk.W))
self.canvas = tk.Canvas(self.container1, borderwidth=0, background="black")
self.frameinsidecanvas = tk.Frame(self.canvas, background="pink")
self.canvas.pack(fill="both", expand=True)
self.frameinsidecanvas.pack(fill="both", expand=True)
# setup scrollbar
self.horizontalscrollbar = tk.Scrollbar(self.container1, orient="horizontal", command=self.canvas.xview)
self.canvas.configure(xscrollcommand=self.horizontalscrollbar.set)
self.horizontalscrollbar.pack(side="bottom", fill="x")
self.canvas.create_window((0, 100), window=self.frameinsidecanvas, anchor="w")
self.textbox = tk.Text(self.container2, height=1)
self.textbox.pack(fill="both", expand=True)
# creating the instructional label
self.firstlabel = tk.Label(self.frameinsidecanvas, text="start")
# self.tasks.append(self.firstlabel)
self.firstlabel.pack(side="left", fill=tk.Y)
# showing the labels
for label_index in range(0, 10):
x = tk.Label(self.frameinsidecanvas, text=label_index)
x.pack(side=tk.LEFT, fill=tk.Y, expand=True)
self.canvas.bind("<Configure>", self.update_width)
def update_width(self, e):
self.canvas_width = self.canvas.winfo_width()
self.canvas_height = self.canvas.winfo_height()
self.firstlabel.configure(height=self.canvas_height)
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == "__main__":
test = Test()
test.wm_geometry("1100x500")
test.mainloop()
For the first question,
If you want to fill the label widget, use firstlabel.pack(fill = 'x')
if you want to align the text on the left side, use firstlabel = tk.Label(frameinsidecanvas, text="Start", anchor = 'w') I hope you got the idea.
The below code will align the text on the left side and fill the label widget.
self.firstlabel = tk.Label(self.frameinsidecanvas, text="Start", anchor = 'w')
self.firstlabel.pack(fill=tk.Y, expand=True)
I have a canvas which is opening in the main window, but I want it to open in the canvas inside the Frame. I tried to add some crazy (as they're in my hands) attributes, but they didn't solve my problem, so I deleted them and have no idea what to do. Could you help me - how could I realize it?
code:
from tkinter import *
from tkinter import ttk
class Example(Frame): # Frame
def __init__(self, root):
Frame.__init__(self, root)
# ---- for grid ----
canvas_width = 1000 # MUTABLE!!!
canvas_height = 1000 # MUTABLE!!!
self.canvas_width = canvas_width
self.canvas_height = canvas_height
self.warehouse_canvas = Canvas(self, width=canvas_width, height=canvas_height, background="bisque")
self.x_scrollbar = Scrollbar(self, orient="horizontal", command=self.warehouse_canvas.xview)
self.y_scrollbar = Scrollbar(self, orient="vertical", command=self.warehouse_canvas.yview)
self.warehouse_canvas.configure(yscrollcommand=self.y_scrollbar.set, xscrollcommand=self.x_scrollbar.set)
self.warehouse_canvas.configure(scrollregion=(0, 0, canvas_width, canvas_height)) # c_w c_h изменены
self.x_scrollbar.grid(row=1, column=0, sticky="ew")
self.y_scrollbar.grid(row=0, column=1, sticky="ns")
self.warehouse_canvas.grid(row=0, column=0, sticky="nsew")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
root.columnconfigure(1, weight=1)
root.rowconfigure(1, weight=1)
self.warehouse_canvas.bind("<ButtonPress-1>", self.scroll_start)
self.warehouse_canvas.bind("<B1-Motion>", self.scroll_move)
self.draw_grid(self.warehouse_canvas, canvas_width, canvas_height) # !!!
def draw_grid(root, warehouse_canvas, canvas_width, canvas_height):
for line in range(0, canvas_width, 10):
warehouse_canvas.create_line([(line, 0), (line, canvas_height)], fill='#d9d9d9', tags='grid_line_width')
for line in range(0, canvas_width, 10):
warehouse_canvas.create_line([(0, line), (canvas_width, line)], fill='#d9d9d9', tags='grid_line_height')
# warehouse_canvas.grid(row=0, column=0) color : #d9d9d9
def scroll_start(self, event):
self.warehouse_canvas.scan_mark(event.x, event.y)
def scroll_move(self, event):
self.warehouse_canvas.scan_dragto(event.x, event.y, gain=1)
if __name__ == "__main__":
root = Tk()
Example(root).grid(row=0, column=0, # положение
columnspan=2, rowspan=2, sticky="nsew")
root.mainloop()
What I have
Where I want it to be
Is there any way to do it without rewriting the whole code?
I'm trying to create a canvas widget with a number of widgets embedded within it. Since there will frequently be too many widgets to fit in the vertical space I have for the canvas, it'll need to be scrollable.
import tkinter as tk # for general gui
import tkinter.ttk as ttk # for notebook (tabs)
class instructionGeneratorApp():
def __init__(self, master):
# create a frame for the canvas and scrollbar
domainFrame = tk.LabelFrame(master)
domainFrame.pack(fill=tk.BOTH, expand=1)
# make the canvas expand before the scrollbar
domainFrame.rowconfigure(0,weight=1)
domainFrame.columnconfigure(0,weight=1)
vertBar = ttk.Scrollbar(domainFrame)
vertBar.grid(row=0, column=1, sticky=tk.N + tk.S)
configGridCanvas = tk.Canvas(domainFrame,
bd=0,
yscrollcommand=vertBar.set)
configGridCanvas.grid(row=0, column=0, sticky=tk.N + tk.S + tk.E + tk.W)
vertBar.config(command=configGridCanvas.yview)
# add widgets to canvas
l = tk.Label(configGridCanvas, text='Products')
l.grid(row=1, column=0)
r = 2
for product in ['Product1','Product2','Product3','Product4','Product5','Product6','Product7','Product8','Product9','Product10','Product11','Product12','Product13','Product14','Product15','Product16','Product17','Product18','Product19','Product20']:
l = tk.Label(configGridCanvas, text=product)
l.grid(row=r, column=0)
c = tk.Checkbutton(configGridCanvas)
c.grid(row=r, column=1)
r += 1
ButtonFrame = tk.Frame(domainFrame)
ButtonFrame.grid(row=r, column=0)
removeServerButton = tk.Button(ButtonFrame, text='Remove server')
removeServerButton.grid(row=0, column=0)
# set scroll region to bounding box?
configGridCanvas.config(scrollregion=configGridCanvas.bbox(tk.ALL))
root = tk.Tk()
mainApp = instructionGeneratorApp(root)
root.mainloop()
As best as I can tell, I'm following the effbot pattern for canvas scrollbars, but I end up with either a scrollbar that isn't bound to the canvas, or a canvas that is extending beyond the edges of its master frame:
I've attempted the solutions on these questions, but there's still something I'm missing:
resizeable scrollable canvas with tkinter
Tkinter, canvas unable to scroll
Any idea what I'm doing wrong?
I have added some comments to #The Pinapple 's solution for future reference.
from tkinter import *
class ProductItem(Frame):
def __init__(self, master, message, **kwds):
Frame.__init__(self, master, **kwds)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
self.text = Label(self, text=message, anchor='w')
self.text.grid(row=0, column=0, sticky='nsew')
self.check = Checkbutton(self, anchor='w')
self.check.grid(row=0, column=1)
class ScrollableContainer(Frame):
def __init__(self, master, **kwargs):
#our scrollable container is a frame, this frame holds the canvas we draw our widgets on
Frame.__init__(self, master, **kwargs)
#grid and rowconfigure with weight 1 are used for the scrollablecontainer to utilize the full size it can get from its parent
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
#canvas and scrollbars are positioned inside the scrollablecontainer frame
#the scrollbars take a command parameter which is used to position our view on the canvas
self.canvas = Canvas(self, bd=0, highlightthickness=0)
self.hScroll = Scrollbar(self, orient='horizontal',
command=self.canvas.xview)
self.hScroll.grid(row=1, column=0, sticky='we')
self.vScroll = Scrollbar(self, orient='vertical',
command=self.canvas.yview)
self.vScroll.grid(row=0, column=1, sticky='ns')
self.canvas.grid(row=0, column=0, sticky='nsew')
#We do not only need a command to position but also one to scroll
self.canvas.configure(xscrollcommand=self.hScroll.set,
yscrollcommand=self.vScroll.set)
#This is the frame where the magic happens, all of our widgets that are needed to be scrollable will be positioned here
self.frame = Frame(self.canvas, bd=2)
self.frame.grid_columnconfigure(0, weight=1)
#A canvas itself is blank, we must tell the canvas to create a window with self.frame as content, anchor=nw means it will be positioned on the upper left corner
self.canvas.create_window(0, 0, window=self.frame, anchor='nw', tags='inner')
self.product_label = Label(self.frame, text='Products')
self.product_label.grid(row=0, column=0, sticky='nsew', padx=2, pady=2)
self.products = []
for i in range(1, 21):
item = ProductItem(self.frame, ('Product' + str(i)), bd=2)
item.grid(row=i, column=0, sticky='nsew', padx=2, pady=2)
self.products.append(item)
self.button_frame = Frame(self.frame)
self.button_frame.grid(row=21, column=0)
self.remove_server_button = Button(self.button_frame, text='Remove server')
self.remove_server_button.grid(row=0, column=0)
self.update_layout()
#If the widgets inside the canvas / the canvas itself change size,
#the <Configure> event is fired which passes its new width and height to the corresponding callback
self.canvas.bind('<Configure>', self.on_configure)
def update_layout(self):
#All pending events, callbacks, etc. are processed in a non-blocking manner
self.frame.update_idletasks()
#We reconfigure the canvas' scrollregion to fit all of its widgets
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
#reset the scroll
self.canvas.yview('moveto', '1.0')
#fit the frame to the size of its inner widgets (grid_size)
self.size = self.frame.grid_size()
def on_configure(self, event):
w, h = event.width, event.height
natural = self.frame.winfo_reqwidth() #natural width of the inner frame
#If the canvas changes size, we fit the inner frame to its size
self.canvas.itemconfigure('inner', width=w if w > natural else natural)
#dont forget to fit the scrollregion, otherwise the scrollbar might behave strange
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
if __name__ == "__main__":
root = Tk()
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
sc = ScrollableContainer(root, bd=2)
sc.grid(row=0, column=0, sticky='nsew')
root.mainloop()
From what I can tell you are looking for something like this..
from tkinter import *
class ProductItem(Frame):
def __init__(self, master, message, **kwds):
Frame.__init__(self, master, **kwds)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
self.text = Label(self, text=message, anchor='w')
self.text.grid(row=0, column=0, sticky='nsew')
self.check = Checkbutton(self, anchor='w')
self.check.grid(row=0, column=1)
class ScrollableContainer(Frame):
def __init__(self, master, **kwargs):
Frame.__init__(self, master, **kwargs) # holds canvas & scrollbars
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.canvas = Canvas(self, bd=0, highlightthickness=0)
self.hScroll = Scrollbar(self, orient='horizontal',
command=self.canvas.xview)
self.hScroll.grid(row=1, column=0, sticky='we')
self.vScroll = Scrollbar(self, orient='vertical',
command=self.canvas.yview)
self.vScroll.grid(row=0, column=1, sticky='ns')
self.canvas.grid(row=0, column=0, sticky='nsew')
self.canvas.configure(xscrollcommand=self.hScroll.set,
yscrollcommand=self.vScroll.set)
self.frame = Frame(self.canvas, bd=2)
self.frame.grid_columnconfigure(0, weight=1)
self.canvas.create_window(0, 0, window=self.frame, anchor='nw', tags='inner')
self.product_label = Label(self.frame, text='Products')
self.product_label.grid(row=0, column=0, sticky='nsew', padx=2, pady=2)
self.products = []
for i in range(1, 21):
item = ProductItem(self.frame, ('Product' + str(i)), bd=2)
item.grid(row=i, column=0, sticky='nsew', padx=2, pady=2)
self.products.append(item)
self.button_frame = Frame(self.frame)
self.button_frame.grid(row=21, column=0)
self.remove_server_button = Button(self.button_frame, text='Remove server')
self.remove_server_button.grid(row=0, column=0)
self.update_layout()
self.canvas.bind('<Configure>', self.on_configure)
def update_layout(self):
self.frame.update_idletasks()
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
self.canvas.yview('moveto', '1.0')
self.size = self.frame.grid_size()
def on_configure(self, event):
w, h = event.width, event.height
natural = self.frame.winfo_reqwidth()
self.canvas.itemconfigure('inner', width=w if w > natural else natural)
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
if __name__ == "__main__":
root = Tk()
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
sc = ScrollableContainer(root, bd=2)
sc.grid(row=0, column=0, sticky='nsew')
root.mainloop()
I've searched a lot but found no way to properly fill a scrollable canvas with this content frame. There is always extra space.
In the code below found on SO and adapted to a simple example, I want my DummyFrame components to take all the extra space left of the canvas.
import Tkinter as tk
class DummyFrame(tk.Frame):
def __init__(self, root):
tk.Frame.__init__(self, root, borderwidth=10, background="#ff0000")
tk.Label(self, text="DummyLabel").grid(row=0, column=0, sticky="wnes")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
class Example(tk.Frame):
def __init__(self, root):
tk.Frame.__init__(self, root)
self.canvas = tk.Canvas(root, borderwidth=0, background="#ffffff")
self.frame = tk.Frame(self.canvas, borderwidth=10, background="#000000")
self.vsb = tk.Scrollbar(root, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.vsb.grid(row=0, column=1, sticky="wnes")
self.canvas.grid(row=0, column=0, sticky="wnes")
self.canvas.create_window((0,0), window=self.frame, anchor="nw",
tags="self.frame")
self.frame.columnconfigure(0, weight=1)
self.frame.rowconfigure(0, weight=1)
self.frame.bind("<Configure>", self.OnFrameConfigure)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.populate()
def populate(self):
for i_row in range(50):
#Component I want to take extra space when resizing window
DummyFrame(self.frame).grid(row = i_row+1, column = 0, sticky="we")
def OnFrameConfigure(self, event):
'''Reset the scroll region to encompass the inner frame'''
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == '__main__':
fenetre = tk.Tk()
fenetre.geometry('{}x{}'.format(800, 600))
Example(fenetre)
fenetre.columnconfigure(0, weight=1)
fenetre.rowconfigure(0, weight=1)
fenetre.mainloop()
This example is very strangely destined. So you have DummyFrames in a Frame in a canvas window which is in a canvas in a Frame. Its difficult to see what is happening here, and which widget dimension control most the resulting dimension the user sees. Nevertheless, if you want to make the DummyFrame as width as your root window (i.e. 800 px) just change the canvas window width:
self.canvas.create_window((0,0), window=self.frame, anchor="nw",
width=800,
tags="self.frame")
Finaly I found something fitted my needs using itemconfigure of the canvas:
import Tkinter as tk
class DummyFrame(tk.Frame):
def __init__(self, root):
tk.Frame.__init__(self, root, borderwidth=10, background="#ff0000")
tk.Label(self, text="DummyLabel").grid(row=0, column=0, sticky="wnes")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
pass
class Example(tk.Frame):
def __init__(self, root):
tk.Frame.__init__(self, root)
self.canvas = tk.Canvas(root, borderwidth=0, background="#0000ff")
self.vsb = tk.Scrollbar(root, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.canvas.bind("<Configure>", self.OnCanvasConfigure)
self.vsb.grid(row=0, column=1, sticky="wnes")
self.canvas.grid(row=0, column=0, sticky="wnes")
self.frame = tk.Frame(self.canvas, borderwidth=10, background="#000000")
self.frame.columnconfigure(0, weight=1)
self.frame.rowconfigure(0, weight=1)
self.window = self.canvas.create_window((0,0), window=self.frame, anchor="nw",
tags="self.frame")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.populate()
def populate(self):
for i_row in range(50):
#Component I want to take extra space when resizing window
DummyFrame(self.frame).grid(row = i_row+1, column = 0, sticky="we")
def OnCanvasConfigure(self, event):
self.canvas.itemconfigure(self.window, width=event.width)
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == '__main__':
fenetre = tk.Tk()
fenetre.geometry('{}x{}'.format(800, 600))
Example(fenetre)
fenetre.columnconfigure(0, weight=1)
fenetre.rowconfigure(0, weight=1)
fenetre.mainloop()