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.
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()