CustomTkinter icon indicating copy to clipboard operation
CustomTkinter copied to clipboard

Dynamically resize window to its content

Open EN20M opened this issue 3 years ago • 9 comments

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.

EN20M avatar Oct 11 '22 14:10 EN20M

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

felipetesc avatar Oct 11 '22 15:10 felipetesc

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

EN20M avatar Oct 12 '22 05:10 EN20M

No. The sample is merely an example

felipetesc avatar Oct 12 '22 22:10 felipetesc

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.

felipetesc avatar Oct 12 '22 22:10 felipetesc

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:

collapsed

The second picture showes the expanded frame. the lowes frame gets cliped at the botom and geometry() has been called:

expanded

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.

EN20M avatar Oct 13 '22 05:10 EN20M

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.

EN20M avatar Oct 13 '22 06:10 EN20M

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.

TomSchimansky avatar Oct 13 '22 11:10 TomSchimansky

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.

EN20M avatar Oct 14 '22 12:10 EN20M

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

EN20M avatar Oct 19 '22 08:10 EN20M

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.

TomSchimansky avatar Dec 02 '22 12:12 TomSchimansky

could you make an example on how this is supposed to work

EN20M avatar Dec 05 '22 10:12 EN20M

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.

TomSchimansky avatar Dec 05 '22 13:12 TomSchimansky

Ah I see thanks.

EN20M avatar Dec 06 '22 09:12 EN20M