Source code for ProportionalContainer

import tkinter as tk


[docs] class ProportionalContainer(object): """ A container that always spans the full available width of its parent, and a height controlled entirely by a grid row weight - so it keeps the right proportions automatically whenever the window is resized, with no manual recalculating needed. This is the building block behind "every tab has three containers": an instructions container, a content container, and (shared across every tab, built separately - see ConsoleRegion) the console container. Each tab wraps its Information widget and its main region widget in one of these. Usage: container = ProportionalContainer( parent=self.unit_cell_tab, row=1, weight=ScreenSizeManager.CONTENT_HEIGHT_WEIGHT, enable_scrolling=True ) self.unit_cell_region = UnitCellRegion(container.content_frame) The weight isn't a percentage by itself - it's relative to the weights given to whatever else shares the same parent's rows. See ScreenSizeManager for the actual weight constants used across the app (INFO_HEIGHT_WEIGHT, CONTENT_HEIGHT_WEIGHT, CONSOLE_HEIGHT_WEIGHT), chosen so they add up to 100 and read directly as percentages. """ def __init__(self, parent, row, weight, enable_scrolling=False, bg="#e0e0e0"): """ :param parent: The parent widget this container will be placed in. Must have grid_columnconfigure(0, weight=1) set on it (once, by whoever owns that parent) so this container can actually stretch to full width. :param row: Which row of the parent's grid this container occupies. :param weight: This row's grid weight, relative to its sibling rows. :param enable_scrolling: If True, wraps the content area in a canvas + scrollbar, so content taller than the space available scrolls instead of getting clipped or forcing the window to grow. Leave off for small, fixed-size content (like the instructions container's buttons). :param bg: Background color. """ parent.grid_rowconfigure(row, weight=weight) self.frame = tk.Frame(master=parent, bg=bg) self.frame.grid(row=row, column=0, sticky="nsew") self.frame.grid_rowconfigure(0, weight=1) self.frame.grid_columnconfigure(0, weight=1) if enable_scrolling: self._setup_scrollable_content(bg) else: # content_frame is where the real widget (Information, a region, etc.) # gets built. Without scrolling, it's just a plain frame that fills # this container directly. self.content_frame = tk.Frame(master=self.frame, bg=bg) self.content_frame.grid(row=0, column=0, sticky="nsew") # Default to a single stretchy column, matching the scrollable branch, # so content spans full width without every caller having to set this # up themselves. A tab that needs a different column layout (e.g. the # Actions tab's bottom-right-anchored button area) can still add more # column/row configuration on top of this afterward. self.content_frame.grid_columnconfigure(0, weight=1) def _setup_scrollable_content(self, bg): """ Wraps content_frame in a canvas + scrollbar, so whatever gets built inside it can grow taller than the container without being cut off - a scrollbar appears instead. The same pattern already used in InputRegion.py, IonRegion.py, and RepeatableRowGroup.py. """ self.canvas = tk.Canvas(master=self.frame, bg=bg, highlightthickness=0) self.scrollbar = tk.Scrollbar(master=self.frame, orient="vertical", command=self.canvas.yview) self.canvas.configure(yscrollcommand=self.scrollbar.set) self.canvas.grid(row=0, column=0, sticky="nsew") self.scrollbar.grid(row=0, column=1, sticky="ns") self.content_frame = tk.Frame(master=self.canvas, bg=bg) self.content_frame.grid_columnconfigure(0, weight=1) self.canvas_window = self.canvas.create_window((0, 0), window=self.content_frame, anchor="nw") # Whenever the content grows or shrinks, tell the canvas how much # there now is to scroll through. self.content_frame.bind("<Configure>", self._update_scroll_region) # Whenever the visible canvas area is resized (e.g. the window grows), # keep the content exactly as wide as that visible area. self.canvas.bind("<Configure>", self._sync_content_width) def _update_scroll_region(self, event=None): """Keeps the scrollable area matched to the content's actual size.""" self.canvas.configure(scrollregion=self.canvas.bbox("all")) def _sync_content_width(self, event): """Keeps the inner content frame exactly as wide as the visible canvas.""" self.canvas.itemconfig(self.canvas_window, width=event.width)