TKinter: Scrollable Canvas, Expandable Widgets, and Pack - python

I am creating a table using Labels laid out as a grid within a Frame. I'd like to embed this Frame in a scrollable canvas, and I'd like the Labels to expand to fill the horizontal space while maintaining vertical scrolling capability. To see what I am trying to do, here is my code:
class ScrollableTable(Frame):
def __init__(self, parent, rows=4, columns=5):
self.rows = rows
self.columns = columns
Frame.__init__(self, parent, borderwidth=0, background='green')
self.canvas = Canvas(self, borderwidth=0)
self.frame = Frame(self.canvas, borderwidth=0, background='black')
self.vsb = Scrollbar(parent, orient=VERTICAL, command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.canvas.pack(side=LEFT, fill=X, expand=TRUE)
# This is where I fix the height I want this window to take up
self.canvas.configure(yscrollcommand=self.vsb.set, width=720, height=80)
self.height = self.canvas.winfo_reqheight()
self.width = self.canvas.winfo_reqwidth()
self._widgets = []
for row in range(rows):
current_row = []
for column in range(columns):
label = Label(self.frame, text="N/A",
borderwidth=0, width=5, anchor=W, justify=LEFT)
label.grid(row=row, column=column, sticky='NESW', padx=0.5, pady=0.5)
current_row.append(label)
self._widgets.append(current_row)
self.canvas.create_window((0,0), window=self.frame, anchor='w', tags=self.frame)
self.frame.bind("<Configure>", self.OnFrameConfigure)
self.canvas.bind("<Configure>", self.onResize)
#self.frame.pack(side=LEFT, fill=X, expand=True)
self.vsb.pack(side=RIGHT, fill=Y, expand=True)
In the above code, the 2nd to last line is commented. When it is commented out, I get a table that doesn't expand to fill the space, but the scrollbar works. However, if I uncomment that lines and use the frame.pack approach, the table fills the horizontal space, but I lose scrolling capability. I'm having trouble wrapping my head around why I'm losing the ability to scroll - any pointers?

You need to not call pack on the frame, and because of that you'll need to manage the width of the frame manually. Typically you do this by binding to the <Configure> event on the canvas. In the bound function, get the width of the canvas, and then use the itemconfigure method to set the width of the window object.

Related

scrollable canvas working with .pack but not .place

I am trying to get my scrollable canvas to work. It works when I pack the elements using .pack, however when I insert the elements via .place, the scrollbar stops working. Here is a minimal reproducable example of my code.
startup.py file:
import frame as f
import placeWidgetsOnFrame as p
p.populate3()
f.window.mainloop()
frame.py file:
#Creates widnow
window = customtkinter.CTk()
window.geometry("1900x980")
customtkinter.set_appearance_mode("dark")
window.resizable(False, False)
#Creates Frame for GUI
mainFrame = customtkinter.CTkFrame(window, width=1900, height=980, corner_radius=0)
mainFrame.pack(expand=True, fill=tk.BOTH)
mainFrame.pack_propagate(False)
topFrame = customtkinter.CTkFrame(master=mainFrame, width=1865, height=140, corner_radius=10)
topFrame.grid(columnspan=2, padx=15, pady=15)
topFrame.pack_propagate(0)
leftFrame = customtkinter.CTkFrame(master=mainFrame, width=380, height=530, corner_radius=10)
leftFrame.grid(row=1, column=0, padx=15, pady=10)
leftFrame.pack_propagate(False)
rightFrame = customtkinter.CTkFrame(master=mainFrame, width=1450, height=775, corner_radius=10)
rightFrame.grid(row=1, column=1, padx=15, pady=10, rowspan=2)
rightFrame.pack_propagate(False)
bottomLeftFrame = customtkinter.CTkFrame(mainFrame, width=380, height=220, corner_radius=10)
bottomLeftFrame.grid(row=2, column=0, padx=15, pady=10)
bottomLeftFrame.pack_propagate(False)
#Creates Scrollbar for right Frame
#Creates a canvas for the right Frame
canvas2=tk.Canvas(rightFrame, bg="#000000", highlightthickness=0, relief="flat")
canvas2.pack(side="left", fill="both", expand=True)
#Creates a scroll bar for the right Frame
scrollbar = customtkinter.CTkScrollbar(master=rightFrame, orientation="vertical", command=canvas2.yview, corner_radius=10)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
#Configures scrollbar to canvas
canvas2.configure(yscrollcommand=scrollbar.set)
canvas2.bind("<Configure>", lambda *args, **kwargs: canvas2.configure(scrollregion=canvas2.bbox("all")))
#Creates a scrollable frame to place widgets on
scrollableFrame = customtkinter.CTkFrame(canvas2, fg_color=("#C0C2C5", "#343638"), corner_radius=10)
canvasFrame = canvas2.create_window((0,0), window=scrollableFrame, anchor="nw", tags=("cf"))
#TO DO - resize canvas and to fit all widgets
def handleResize(event):
c = event.widget
cFrame = c.nametowidget(c.itemcget("cf", "window"))
minWidth = cFrame.winfo_reqwidth()
minHeight = cFrame.winfo_reqheight()
print (event.width)
print (event.height)
if minWidth < event.width:
c.itemconfigure("cf", width=event.width)
if minHeight < event.height:
c.itemconfigure("cf", height=event.height)
print (event.width)
print (event.height)
c.configure(scrollregion=c.bbox("all"))
canvas2.bind('<Configure>', handleResize)
def onMousewheel(event):
canvas2.yview_scroll(-1 * round(event.delta / 120), "units")
canvas2.bind_all("<MouseWheel>", onMousewheel)
canvas2.bind("<Destroy>", lambda *args, **kwargs: canvas2.unbind_all("<MouseWheel>"))
placeWidgetsOnFrame.py file:
import tkinter
import customtkinter
import frame as f
rightFrame = f.scrollableFrame
def populate2():
for i in range(30):
emailLabel = customtkinter.CTkLabel(master=rightFrame, text="Please enter your email:")
emailLabel.pack(padx=10, pady=10)
def populate3():
x=50
for i in range(30):
emailLabel = customtkinter.CTkLabel(master=rightFrame, text="Please enter your email:")
emailLabel.place(x=40, y=x)
x=x+50
Here is the output when populate3() is run:
Here
Here is the output when populate2() is run
Here
Does anyone know why this is? I can always go back and change the way I insert widgets to .pack rather than .place, however I would rather use .place as I find it easier to place widgets where I want to.
The reason is because pack by default will cause the containing frame to grow or shrink to fit all of the child widgets, but place does not. If your frame starts out as 1x1 and you use place to add widgets to it, the size will remain 1x1. When you use place, it is your responsibility to make the containing widget large enough to contain its children.
This single feature is one of the most compelling reasons to choose grid or pack over place - these other geometry managers do a lot of work for you so that you can think about the layout logically without getting bogged down in the details of the layout.

How to expand a canvas frame to the root mainwindow using grid tkinter?

I just created a canvas and a frame with vertical & horizontal scrollbars where I placed some objects. Now what I want to do is to let the size of the frame containing the scrollbars to be the same as my mainwindow, in other words, when I make the root bigger, then the frame should get bigger and have exactly the size of the root. In my code, I have to set the width and height of my canvas otherwise I get a window with size "0x0".
This is my code:
self.root = Tk()
self.root.geometry("400x400")
self.canvas = Frame(self.root)
#self.canvas.config(width=700, height=900)
self.canvas.grid(row=0,column=0,sticky="nsew")
self.canvas.grid_rowconfigure(0,weight=1)
self.canvas.grid_columnconfigure(0, weight=1)
self.canvas.grid_propagate(True)
self.new_canvas = Canvas(self.canvas)
self.new_canvas.grid(row=0,column=0,sticky="news")
self.vsb = Scrollbar(self.canvas, orient="vertical", command=self.new_canvas.yview)
self.vsb.grid(row=0, column=1, sticky='ns')
self.vsbb = Scrollbar(self.canvas, orient="horizontal", command=self.new_canvas.xview)
self.vsbb.grid(row=1, column=0, sticky='news')
self.new_canvas.configure(yscrollcommand=self.vsb.set)
self.new_canvas.configure(xscrollcommand=self.vsbb.set)
self.out_frame = Frame(self.new_canvas)
self.new_canvas.create_window((0, 0), window=self.out_frame, anchor="nw")
self.root.title("TIxS3 Master Control v3.1")
self.root.protocol("WM_DELETE_WINDOW", self.callback)
# Here come my Objects to insert in the frame "out_frame"
self.out_frame.update_idletasks()
self.new_canvas.config(scrollregion=self.new_canvas.bbox("all"))
How can I solve that using grid? I am not sure about the solution in pack but I think I should do the same thing as pack(fill= BOTH) does right?

Python - Widget alignment when using multiple frames

I'm facing some trouble aligning tkinter widgets when located in different frames, as shown
I have 3 frames: main_frame - colored in blue, containing 3 subframes: buttons_frame,timers_frame,switches_frame, which all centered to main_frame, one on top of other.
Design requires: all widgets in all subframes will be centered inside its frame. As shown in attached pic, and code - middle sub-frame, timers_frame is streched OK to max size using tk.E+tk.w, BUT widgets inside positioned at row=0, column=0 and not aligning to center of streched frame. Using tk.E+tk.W didn't solve it.
Solving it without using sub-frames, works OK.
Relevant portion of code:
class CoreButton(ttk.Frame):
def __init__(self, master, nickname='CoreBut.inc', hw_in=[], hw_out=[], ip_in='', \
ip_out='', sched_vector=[], num_buts=1):
ttk.Frame.__init__(self, master)
# Styles
self.style = ttk.Style()
self.style.configure("Azure.TFrame", background='azure4')
self.style.configure("Blue.TFrame", background='blue')
self.style.configure("Blue2.TFrame", background='light steel blue')
self.style.configure("Red.TButton", foreground='red')
# Frames
# Buttons&Indicators
py, px = 4, 4
self.main_frame = ttk.Frame(self, style="Blue2.TFrame", relief=tk.RIDGE)
self.main_frame.grid()
self.buttons_frame = ttk.Frame(self.main_frame, relief=tk.RIDGE, style="Azure.TFrame")
self.buttons_frame.grid(row=0, column=0, pady=py, padx=px)
# Counters
self.timers_frame = ttk.Frame(self.main_frame, relief=tk.RIDGE, border=5, style="Azure.TFrame")
self.timers_frame.grid(row=1, column=0, pady=py, padx=px, sticky=tk.E+tk.W)
# Extra GUI
self.switches_frame = ttk.Frame(self.main_frame, relief=tk.RIDGE, border=5, style="Azure.TFrame")
self.switches_frame.grid(row=2, column=0, pady=py)
# Run Gui
# self.build_gui()
self.extras_gui()
def extras_gui(self):
ck1 = tk.Checkbutton(self.switches_frame, text='On/Off', variable=self.on_off_var, \
indicatoron=1, command=self.disable_but)
ck1.grid(row=0, column=0)
ck2 = tk.Checkbutton(self.switches_frame, text='Schedule', variable=self.enable_disable_sched_var, \
indicatoron=1,
command=lambda: self.disable_sched_task(s=self.enable_disable_sched_var.get()))
ck2.grid(row=1, column=0)
You have to give the column your timers are in weight!
self.timers_frame.columnconfigure(0, weight=1)
Otherwise the column has exactly the width of your timers and not the width of your timers_frame.

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)

table display in tkinter with scroll bars

I'm writing a small GUI in python3/tkinter. What I want to do is generate a window with a table of data (like a spreadsheet) and have the table be scrollable both horizontally and vertically. Right now I'm just trying to display data, so I'm using a grid of Labels. The data display works fine but I can't get the scroll bars to act correctly. Here is the relevant part of my code; the class that this is in inherits from tk.Toplevel
frame = self.frame = tk.Frame(self)
self.frame.grid(row=1, columnspan=2, padx=2, pady=2, sticky=tk.N+tk.E+tk.S+tk.W)
self.text_area = tk.Canvas(self.frame, background="black", width=400, height=500, scrollregion=(0,0,1200,800))
self.hscroll = tk.Scrollbar(self.frame, orient=tk.HORIZONTAL, command=self.text_area.xview)
self.vscroll = tk.Scrollbar(self.frame, orient=tk.VERTICAL, command=self.text_area.yview)
self.text_area['xscrollcommand'] = self.hscroll.set
self.text_area['yscrollcommand'] = self.vscroll.set
self.text_area.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
self.hscroll.grid(row=1, column=0, sticky=tk.E+tk.W)
self.vscroll.grid(row=0, column=1, sticky=tk.N+tk.S)
self._widgets = []
for row in range(rows):
current_row = []
for column in range(columns):
label = tk.Label(self.text_area, text="",
borderwidth=0, width=width)
label.grid(row=row, column=column, sticky="nsew", padx=1, pady=1)
current_row.append(label)
self._widgets.append(current_row)
The table displays OK and the scrollbars appear but are not functional:
Any ideas?
You have a couple of problems. First, you can't use grid to place labels in the canvas and expect them to scroll. When you scroll a canvas, only the widgets added with create_window will scroll. However, you can use grid to put the labels in a frame, and then use create_window to add the frame to the canvas. There are several examples of that technique on this site.
Second, you need to tell the canvas how much of the data in the canvas should be scrollable. You use this by setting the scrollregion attribute of the canvas. There is a method, bbox which can give you a bounding box of all of the data in the canvas. Usually it's used like this:
canvas.configure(scrollregion=canvas.bbox("all"))
An option I have used is the leave the labels static and changed the data when the scrollbar is used. This is easy for vertical scrolling but if the columns are of different widths does not work for horizontal scrolling - it would require resizing the labels.

Categories

Resources