Creating a responsive layout, avoid infinite Configure loop

Question:

I am trying to create a responsive layout where the content of the app changes depending on the width of the window (basically like any website works). The code I have so far is this:

import tkinter as tk
from tkinter import ttk

class App(tk.Tk):
    def __init__(self, start_size, min_size):
        super().__init__()
        self.title('Responsive layout')
        self.geometry(f'{start_size[0]}x{start_size[1]}')
        self.minsize(min_size[0],min_size[1])
        self.frame = ttk.Frame(self)
        self.frame.pack(expand = True, fill = 'both')
        size_notifier = SizeNotifier(self, {300: self.create_small_layout, 600: self.create_medium_layout}, min_size[0])
        self.mainloop()

    def create_small_layout(self):
        self.frame.pack_forget()
        self.frame = ttk.Frame(self)
        ttk.Label(self.frame, text = 'Label 1', background = 'red').pack(expand = True, fill = 'both')
        ttk.Label(self.frame, text = 'Label 2', background = 'green').pack(expand = True, fill = 'both')
        ttk.Label(self.frame, text = 'Label 3', background = 'blue').pack(expand = True, fill = 'both')
        ttk.Label(self.frame, text = 'Label 4', background = 'yellow').pack(expand = True, fill = 'both')
        self.frame.pack(expand = True, fill = 'both')

    def create_medium_layout(self):
        self.frame.pack_forget()
        
        self.frame = ttk.Frame(self)
        self.frame.columnconfigure((0,1), weight = 1, uniform = 'a')
        self.frame.rowconfigure((0,1), weight = 1, uniform = 'a')
        self.frame.pack(expand = True, fill = 'both')

        ttk.Label(self.frame, text = 'Label 1', background = 'red').grid(column = 0, row = 0, sticky = 'nsew') # sticky = 'nsew' causes configure loop
        ttk.Label(self.frame, text = 'Label 2', background = 'green').grid(column = 1, row = 0)
        ttk.Label(self.frame, text = 'Label 3', background = 'blue').grid(column = 0, row = 1)
        ttk.Label(self.frame, text = 'Label 4', background = 'yellow').grid(column = 1, row = 1)

class SizeNotifier:
    def __init__(self, window, size_dict, min_width):
        self.window = window
        self.min_width = min_width
        self.size_dict = {key: value for key, value in sorted(size_dict.items())}
        self.current_min_size = None
        self.window.bind('<Configure>', self.check)


    def check(self, event):
        checked_size = None
        window_width = event.width
        if window_width >= self.min_width:
            for min_size in self.size_dict:
                delta = window_width - min_size
                if delta >= 0:
                    checked_size = min_size

            if checked_size != self.current_min_size:
                self.current_min_size = checked_size
                print(self.current_min_size) # infinite loop visible here -> print never stops 
                self.size_dict[self.current_min_size]()

app = App((400,300), (300,300))

The basic idea is this: There are 2 functions that create different layouts (create_small_layout and create_medium_layout) and the app hides/reveals one of them depending on the app width. Getting the width of the application is handled by the SizeNotifier class, the logic in there was added to only trigger a layout build function when a new minimum size was reached.

This entire thing works to a degree: In the create_medium_layout function, where I use the grid layout methods, things do work without the sticky argument. However, once I stick a widget to all sides of a cell (so ‘nsew’) configure is thrown into an infinite loop and the app stops displaying anything. using sticky with just 3 sides or less is fine though.

Is there a way around this?

Asked By: Another_coder

||

Answers:

When you bind to a root window, the binding applies to all widgets. This is because bindings don’t actually get added to a widget, but rather to a binding tag. Every widget has a set of binding tags, and every widget has the root window as one of its binding tags.

For more information about bind tags see this answer to the question Basic query regarding bindtags in tkinter. Also, the canonical description can be found in the tcl/tk manual page on the bind command.

That means that self.check will be called when the root window resizes, and it will also be called once when every frame and label resizes. Since you’re using event.width, sometimes that value will be the width of the window and sometimes it will be the width of a frame or label.

You should modify your check feature to only run when it is called due to the window itself changing size. In your case you can add a check like the following:

def check(self, event):
    if event.widget == self.window:
        ...
Answered By: Bryan Oakley
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.