CustomTkinter
CustomTkinter copied to clipboard
Dynamically resize window to its content
I have made collapsable frames in like this:
class CollapsFrame(customtkinter.CTkFrame):
collapsed = False
def __init__(self, *args,
width: int = 100,
height: int = 32,
frameLable: any = "",
initalCollapsed: bool = False,
**kwargs):
super().__init__(*args, width=width, height=height, **kwargs)
self.columnconfigure((0), weight=0)
self.buttonText = tk.StringVar()
if initalCollapsed:
self.buttonText.set("˃")
else:
self.buttonText.set("˅")
self.collapsButton = customtkinter.CTkButton(self, textvariable=self.buttonText, width=20, height=20, command=self.collapsHide)
self.collapsButton.grid(row=0, column=0, sticky="w", padx=5, pady=5)
self.frameLabl = customtkinter.CTkLabel(self, text=frameLable, anchor='w')
self.frameLabl.grid(row=0, column=1, sticky="w", padx=5, pady=5)
def collapsHide(self):
self.collapsed = not self.collapsed
if self.collapsed:
for child in self.winfo_children():
if child != self.collapsButton and child != self.frameLabl:
child.grid_remove()
self.buttonText.set("˃")
else:
for child in self.winfo_children():
if child != self.collapsButton and child != self.collapsButton:
child.grid()
self.buttonText.set("˅")
and I want the window to resize itself when I uncollaps a frame and it would not completly fit the current window size.
Is there any way I can get this to work. I tried calling geometry() on the topleven but it does not seam to care if the children widgets get cliped.
Actually I would like a scrollabe frame, this would render the need for autorezising redundant but still i think it would be a usefull feature.
I would make click event access a tkinter global variable containing the screen size and I would use geometry to fit the desired size for the collapsible frame. Check : https://felipetesc.github.io/CtkDocs/#/resize_window
You mean fist full screen it and than use geometry()?
Since some parts of my ui will always expand to the size of the window this would result in the window staying the size of the screen. I would want it to shrink to the smalest possible size with no widget cliped
No. The sample is merely an example
I believe the key element here is to bind the window to some event. If I click somewhere, it triggers a window event to expand, or to shrink the top level size.
I know this and I already have bound the attemt to resize to the button button that changes the layout. From the begining on the problem was never to trigger the event but rather that the event is not doing anything to the window size. The fist picture showes the collapsed frame with everything else in bounds:
The second picture showes the expanded frame. the lowes frame gets cliped at the botom and geometry() has been called:
Since geometry() seems to checks wether everything is in bounds and the bottomn frame gets clipes it is not out of bounds and geometry() thinks: no need to change anything. At least this is my understanding of the documentation of what happens when geometry() is called without arguments.
I am looking for a way to resize without the need to specify dimensions. I basically want the window as small as possilbe with everything thats on the grid to be in frame.
First of all .geometry() called without arguments does nothing but returns the current geometry string of the window.
I came up with a possible solution, but this works currently only with a scaling of 1, because the methods winfo_width() and winfo_reqheight are not implemented with scaling at the moment for all CTk widgets. But I will do this in the next time.
First of all I added a command attribute to the CollapsFrame class, that gets called when collapsHide gets called. Also note that I moved the collapsed attribute into the init method of the class, so that its an instance attribute instead of a class attribute. It needs to be an instance attribute because its different for every instance. A class attribute is shared between all instances, which is not intended here.
import customtkinter
import tkinter as tk
from typing import Callable
class CollapsFrame(customtkinter.CTkFrame):
def __init__(self, *args,
width: int = 100,
height: int = 30,
frameLable: any = "",
initalCollapsed: bool = False,
command: Callable = None,
**kwargs):
super().__init__(*args, width=width, height=height, **kwargs)
self.columnconfigure((0, ), weight=0)
self.collapsed = False
self.command = command
self.buttonText = tk.StringVar()
if initalCollapsed:
self.buttonText.set("˃")
else:
self.buttonText.set("˅")
self.collapsButton = customtkinter.CTkButton(self, textvariable=self.buttonText, width=20, height=20, command=self.collapsHide)
self.collapsButton.grid(row=0, column=0, sticky="w", padx=5, pady=5)
self.frameLabl = customtkinter.CTkLabel(self, text=frameLable, anchor='w')
self.frameLabl.grid(row=0, column=1, sticky="w", padx=5, pady=5)
def collapsHide(self):
self.collapsed = not self.collapsed
if self.collapsed:
for child in self.winfo_children():
if child != self.collapsButton and child != self.frameLabl:
child.grid_remove()
self.buttonText.set("˃")
else:
for child in self.winfo_children():
if child != self.collapsButton and child != self.collapsButton:
child.grid()
self.buttonText.set("˅")
if self.command is not None:
self.command()
To update the window height to the height that's needed, I created a function called manage_window_height. Its passed to the frames with the command attribute. It reads the current width of the window and calculates the required height of the window, so that all frames fit perfectly in. Then it updates the window geometry.
def manage_window_height():
app.update_idletasks() # update idletasks to update window width
current_width = app.winfo_width() # get current window width
c_frame_1.update_idletasks() # update idletasks to update frame height
c_frame_2.update_idletasks()
height_needed = 10 # y-padding
height_needed += c_frame_1.winfo_reqheight() # get requested height of frame
height_needed += 10 + 10 # y-padding
height_needed += c_frame_2.winfo_reqheight() # get requested height of frame
height_needed += 10 # y-padding
app.geometry(f"{current_width}x{height_needed}") # update geometry
app = customtkinter.CTk()
c_frame_1 = CollapsFrame(app, frameLable="c_frame_1", command=manage_window_height)
c_frame_1.pack(padx=10, pady=10)
button_1 = customtkinter.CTkButton(c_frame_1)
button_1.grid(row=1, column=0, columnspan=2, padx=20, pady=100)
c_frame_2 = CollapsFrame(app, frameLable="c_frame_2", command=manage_window_height)
c_frame_2.pack(padx=10, pady=10)
button_2 = customtkinter.CTkButton(c_frame_2)
button_2.grid(row=1, column=0, columnspan=2, padx=20, pady=100)
manage_window_height() # initial call to set window geometry
app.mainloop()
I tested the function with two frames and it works for me like I think you want it. The only downside is that you have to modify this function every time you change the layout of the window. So when you change a padding value, you also have to change it in the function.
Thank you very mutch for taking the time to look into it.
I think at some point there won't be mutch change in my layout anymore and I'll adjust the hight neede paddings once and than it should be fine, so not mutch of a downside in my mind.
Isn't there a way to dynamically get the padding values. Because then, one could iterate through the list of chidren of the collapsed frames parent and add the values up.
Anyway feel free to add the CollapsFrame to the wiki if you want. Maby as section of derived widgets together with the spinbox.
It is possible to do it dynamicly:
def get_pady(self, widget):
try:
padding = sum(widget.grid_info()['pady'])
except:
padding = widget.grid_info()['pady'] * 2
return padding
def manage_window_height(self, parentFrame):
self.update_idletasks() # update idletasks to update window width
current_width = self.winfo_width() # get current window width
height_needed = self.get_pady(parentFrame)
for child in parentFrame.winfo_children():
child.update_idletasks()
height_needed += (self.get_pady(child) + child.winfo_reqheight())
self.geometry(f"{current_width}x{height_needed}") # update geometry
I removed the default geometry() calls from the CTk and CTkToplevel constructors, so a dynamic window size is now possible. But it will not work in after a change of the scaling happened, because then I need to call geometry() and the window size will be fixed from there on.
could you make an example on how this is supposed to work
You just make no geometry call and then the window will be as big as it needs to be to fit all widgets you created.
Ah I see thanks.