Search code examples
python-3.xtkinterttk

Fluid design in Tkinter


I'm trying to create a somewhat "responsive" design with ttk (tkinter). The basic placement of widgets is no problem at all, but making it fluid with the width of the program is something I cannot achieve. In CSS I know it's possible to say something along the lines of '"float: left" for all containers' and the page would adapt to the screen size. I haven't found something similar to that in Tkinter and frames.

My basic test program:

#!/usr/bin/python3

import tkinter
from tkinter import ttk
from ttkthemes import ThemedTk, THEMES

class quick_ui(ThemedTk):
    def __init__(self):
        ThemedTk.__init__(self, themebg=True)
        self.geometry('{}x{}'.format(900, 150))
        self.buttons = {}

        self.frame1 = ttk.Frame(self)
        self.frame1.pack(side="left")
        self.frame2 = ttk.Frame(self)
        self.frame2.pack(side="left")

        #------------------------------------------------------- BUTTONS
        i = 0
        while (i < 5):
            i += 1
            self.buttons[i]= ttk.Button(self.frame1,
                                            text='List 1 All ' + str(i),
                                            command=self.dump)
            self.buttons[i].pack(side="left")


        while (i < 10):
            i += 1
            self.buttons[i]= ttk.Button(self.frame2,
                                            text='List 2 All ' + str(i),
                                            command=self.dump)
            self.buttons[i].pack(side="left")

    def dump(self):
        print("dump called")

quick = quick_ui()
quick.mainloop()

This creates a window with 10 buttons all besides each other. When I shrink the window to the point that the buttons no longer fit on the screen, I would like the buttons to appear below each other

So what I did was add a resize listener and setup the following method:

    def resize(self, event):
        w=self.winfo_width()
        h=self.winfo_height()
        # print("width: " + str(w) + ", height: " + str(h))

        if(w < 830):
            self.frame1.config(side="top")
            self.frame2.config(side="top")

But Frame doesn't has the property side, which is a parameter given to the method pack. So that didn't work either.

And now I'm lost. I've spend way to long on this, trying grids and other solutions, but I've got the feeling that I'm missing out on one simple, but very important setting.


Solution

  • I've created a (hackish) solution, which is fine due to it being a very internal program. Since no answer is given in the mean time, I'll provide my solution here. It probably has a lot of room for improvements, but I hope that this can give somebody in the future some pointers on how to tackle this problem by him(or her) self.

    #!/usr/bin/python3
    
    import re
    import sys
    import tkinter
    from tkinter import filedialog
    from tkinter import ttk
    from ttkthemes import ThemedTk, THEMES
    
    import subprocess
    import os
    from tkinter.constants import UNITS
    import json
    from functools import partial
    
    
    class quick_ui(ThemedTk):
    
        def __init__(self):
            ThemedTk.__init__(self, themebg=True)
            self.minsize(600, 250)
            self.elems = {}
            self.resize_after_id = None
    
    
            #------------------------------------------------------- Window menu bar contents
            self.menubar = tkinter.Menu(self)
            self.menubar.add_command(label="Open", command = self.dump)
            self.menubar.add_command(label="Refresh", command = self.dump)
            self.config(menu=self.menubar)
    
            # Theme menu
            self.themeMenu = tkinter.Menu(self.menubar, tearoff=0)
            self.menubar.add_cascade(label="Theme", menu=self.themeMenu)
            self.themeMenu.add_command(label="DEFAULT", command=partial(self.dump, "default"))
    
    
            #---------------------------------------------------------------------- top_frame
            self.top_frame = ttk.Frame(self)
            self.top_frame.pack( side = tkinter.TOP, expand='YES', fill='both', padx=10)
    
            self.top_top_frame = ttk.Frame(self.top_frame)
            self.top_top_frame.pack(side=tkinter.TOP, expand='YES', fill='both')
    
            self.top_bottom_frame = ttk.Frame(self.top_frame)
            self.top_bottom_frame.pack(side=tkinter.BOTTOM)
    
            self.top_bottom_top_frame = ttk.Frame(self.top_frame)
            self.top_bottom_top_frame.pack(side=tkinter.TOP)
    
    
            self.top_bottom_bottom_frame = ttk.Frame(self.top_frame)
            self.top_bottom_bottom_frame.pack(side=tkinter.BOTTOM)
    
            #------------------------------------------------------------------- bottom_frame
            self.bottom_frame = ttk.Frame(self, relief="sunken")
            self.bottom_frame.pack( side = tkinter.BOTTOM, 
                                    expand='YES', 
                                    fill='both', 
                                    padx=10, 
                                    pady=10 )
    
            #------------------------------------------------------- BUTTONS
            i = 0
            while (i < 15):
                self.elems[i]=ttk.Button(self.top_bottom_top_frame,
                                                    text='List All ' + str(i),
                                                    command=self.dump)
                i += 1
    
    
            self.label_test_strings1 = ttk.Label(self.top_top_frame, text='Test strings1')
            self.label_test_strings2 = ttk.Label(self.top_bottom_frame, text='Test strings2')
            self.label_test_strings4 = ttk.Label(self.top_bottom_bottom_frame, text='Test strings4')
    
            self.label_test_strings1.pack(side = tkinter.TOP)
            self.label_test_strings2.pack(side = tkinter.TOP)
            self.label_test_strings4.pack(side = tkinter.TOP)
    
    
            self.placeElems()
            # Setup a hook triggered when the configuration (size of window) changes
            self.bind('<Configure>', self.resize)
    
    
        def placeElems(self):
            for index in self.elems:
                self.elems[index].grid(row=0, column=index, padx=5, pady=5)
    
    
        # ------------------------------------------------------ Resize event handler
        def resize(self, event):
            # Set a low "time-out" for resizing, to limit the change of "fighting" for growing and shrinking
            if self.resize_after_id is not None:
                self.after_cancel(self.resize_after_id)
            self.resize_after_id = self.after(200, self.resize_callback)
    
    
        # ------------------------------------------------------ Callback for the resize event handler
        def resize_callback(self):
            # The max right position of the program
            windowMaxRight = self.winfo_rootx() + self.winfo_width()
    
            # Some basic declarations
            found = False
            willAdd = False
            maxColumn = 0
            currIndex = 0
            currColumn = 0
            currRow = 0
            counter = 0
            last_rootx = 0
            last_maxRight = 0
    
            # Program is still starting up, so ignore this one
            if(windowMaxRight < 10):
                return
    
            # Loop through all the middle bar elements
            for child in self.top_bottom_frame.children.values():
                # Calculate the max right position of this element
                elemMaxRight = child.winfo_rootx() + child.winfo_width() + 10
    
                # If we already found the first 'changable' child, we need to remove the following child's also
                if(found == True):
                    # Is the window growing?
                    if(willAdd == True):
                        # Check to see if we have room for one more object
                        calcMaxRight = last_maxRight + child.winfo_width() + 20
                        if(calcMaxRight < windowMaxRight):
                            maxColumn = counter + 1
                    # Remove this child from the view, to add it again later
                    child.grid_forget()
    
                # If this child doesn't fit on the screen anymore
                elif(elemMaxRight >= windowMaxRight):
                    # Remove this child from the view, to add it again later
                    child.grid_forget()
                    currIndex = counter
                    maxColumn = counter
                    currRow = 1
                    found = True
    
                else:
                    # If this child's x position is lower than the last child
                    # we can asume it's on the next row
                    if(child.winfo_rootx() < last_rootx):
                        # Check to see if we have room for one more object on the first row
                        calcMaxRight = last_maxRight + child.winfo_width() + 20
                        if(calcMaxRight < windowMaxRight):
                            child.grid_forget()
                            currIndex = counter
                            currColumn = counter
                            maxColumn = counter + 1
                            found = True
                            willAdd = True
    
                # Save some calculation data for the next run
                last_rootx = child.winfo_rootx()
                last_maxRight = elemMaxRight
                counter += 1
    
            # If we removed some elements from the UI
            if(found == True):
                counter = 0
                # Loop through all the middle bar elements (including removed ones)
                for child in self.top_bottom_frame.children.values():
                    # Ignore the elements still in place
                    if(counter < currIndex):
                        counter += 1
                        continue
    
                    # If we hit our maxColumn count, move to the next row
                    if(currColumn == maxColumn):
                        currColumn = 0
                        currRow += 1
    
                    # Place this element on the UI again
                    child.grid(row=currRow, column=currColumn, padx=5, pady=5)
                    currColumn += 1
                    counter += 1
    
    
        def dump(self):
            print("dump called")
    
    
    quick = quick_ui()
    quick.mainloop()