import tkinter as tk
from typing import Any, Optional
[docs]
class FixedThreePaneLayout(tk.Frame):
"""
A fixed three-pane window layout with left, center, and right panes.
This class provides a simple three-pane layout where the side panes can have
fixed or resizable widths, and the center pane fills the remaining space.
It supports menu bars and customizable pane sizing.
Args:
master (tk.Widget): The parent widget.
side_width (int): Default width for side panes (default: 150).
sash_width (int): Width of the sash/separator between panes (default: 2).
left_width (Optional[int]): Specific width for left pane (overrides side_width).
right_width (Optional[int]): Specific width for right pane (overrides side_width).
left_fixed_width (Optional[int]): Fixed width for left pane (non-resizable).
right_fixed_width (Optional[int]): Fixed width for right pane (non-resizable).
min_pane_size (int): Minimum size for resizable panes (default: 50).
menu_bar (Optional[tk.Menu]): Menu bar to attach to the parent window.
**kwargs: Additional keyword arguments passed to tk.Frame.
"""
def __init__(
self,
master: tk.Widget,
side_width: int = 150,
sash_width: int = 2,
left_width: Optional[int] = None,
right_width: Optional[int] = None,
left_fixed_width: Optional[int] = None,
right_fixed_width: Optional[int] = None,
min_pane_size: int = 50,
menu_bar: Optional[tk.Menu] = None,
**kwargs: Any,
) -> None:
super().__init__(master, **kwargs)
self._validate_parameters(left_width, right_width, min_pane_size)
self._initialize_attributes(
side_width,
sash_width,
min_pane_size,
left_width,
right_width,
left_fixed_width,
right_fixed_width,
menu_bar,
)
self._setup_menu_bar(master)
self._create_panels()
self._setup_layout()
def _validate_parameters(
self, left_width: Optional[int], right_width: Optional[int], min_pane_size: int
) -> None:
"""Validate initialization parameters."""
if left_width is not None and left_width < 0:
raise ValueError("left_width must be non-negative")
if right_width is not None and right_width < 0:
raise ValueError("right_width must be non-negative")
if min_pane_size < 0:
raise ValueError("min_pane_size must be non-negative")
def _initialize_attributes(
self,
side_width: int,
sash_width: int,
min_pane_size: int,
left_width: Optional[int],
right_width: Optional[int],
left_fixed_width: Optional[int],
right_fixed_width: Optional[int],
menu_bar: Optional[tk.Menu],
) -> None:
"""Initialize instance attributes."""
self.side_width = side_width
self.sash_width = sash_width
self.min_pane_size = min_pane_size
# Support both parameter names for compatibility
self.left_width = left_width or left_fixed_width or side_width
self.right_width = right_width or right_fixed_width or side_width
self.left_fixed_width = left_width or left_fixed_width
self.right_fixed_width = right_width or right_fixed_width
self.menu_bar = menu_bar
def _setup_menu_bar(self, master: tk.Widget) -> None:
"""Setup menu bar if provided."""
if self.menu_bar and hasattr(master, "config"):
master.config(menu=self.menu_bar)
def _create_panels(self) -> None:
"""Create all panel frames and labels."""
self._create_left_panel()
self._create_center_panel()
self._create_right_panel()
self._create_sashes()
def _create_left_panel(self) -> None:
"""Create left panel frame and label."""
self._frame_left = tk.Frame(self, bg="#3A7CA5")
self.label_left = tk.Label(
self._frame_left,
text="Left Panel",
bg="#3A7CA5",
fg="white",
font=("Segoe UI", 12, "bold"),
)
self.label_left.pack(pady=10)
def _create_center_panel(self) -> None:
"""Create center panel frame and label."""
self._frame_center = tk.Frame(self, bg="#FFFFFF")
self.label_center = tk.Label(
self._frame_center,
text="Center Panel",
bg="#FFFFFF",
font=("Segoe UI", 12, "bold"),
)
self.label_center.pack(pady=10)
def _create_right_panel(self) -> None:
"""Create right panel frame and label."""
self._frame_right = tk.Frame(self, bg="#F4A261")
self.label_right = tk.Label(
self._frame_right,
text="Right Panel",
bg="#F4A261",
fg="black",
font=("Segoe UI", 12, "bold"),
)
self.label_right.pack(pady=10)
def _create_sashes(self) -> None:
"""Create sash frames for visual separation."""
self._sash_left = tk.Frame(self, bg="#888888")
self._sash_right = tk.Frame(self, bg="#888888")
def _setup_layout(self) -> None:
"""Setup initial layout and event bindings."""
self.place(relwidth=1, relheight=1)
self.bind("<Configure>", self._resize)
def _resize(self, event: Optional[tk.Event] = None) -> None:
w = self.winfo_width()
h = self.winfo_height()
# Use fixed widths if specified, otherwise use default side_width
left_width = self.left_fixed_width or self.side_width
right_width = self.right_fixed_width or self.side_width
center_width = w - (left_width + right_width + 2 * self.sash_width)
self._frame_left.place(x=0, y=0, width=left_width, height=h)
self._sash_left.place(x=left_width, y=0, width=self.sash_width, height=h)
self._frame_center.place(
x=left_width + self.sash_width, y=0, width=center_width, height=h
)
self._sash_right.place(
x=left_width + self.sash_width + center_width,
y=0,
width=self.sash_width,
height=h,
)
self._frame_right.place(x=w - right_width, y=0, width=right_width, height=h)
[docs]
def set_label_texts(
self,
left: Optional[str] = None,
center: Optional[str] = None,
right: Optional[str] = None,
) -> None:
if left is not None:
self.label_left.config(text=left)
if center is not None:
self.label_center.config(text=center)
if right is not None:
self.label_right.config(text=right)
[docs]
def add_to_left(self, widget: tk.Widget) -> None:
# Reparent the widget to the left frame
widget.pack_forget()
widget.master = self._frame_left
widget.pack(in_=self._frame_left, pady=5)
[docs]
def add_to_center(self, widget: tk.Widget) -> None:
# Reparent the widget to the center frame
widget.pack_forget()
widget.master = self._frame_center
widget.pack(in_=self._frame_center, fill=tk.BOTH, expand=True, pady=5)
[docs]
def add_to_right(self, widget: tk.Widget) -> None:
# Reparent the widget to the right frame
widget.pack_forget()
widget.master = self._frame_right
widget.pack(in_=self._frame_right, pady=5)
@property
def frame_left(self) -> tk.Frame:
"""Get the left frame container (legacy property name)."""
return self._frame_left
@property
def frame_center(self) -> tk.Frame:
"""Get the center frame container (legacy property name)."""
return self._frame_center
@property
def frame_right(self) -> tk.Frame:
"""Get the right frame container (legacy property name)."""
return self._frame_right
[docs]
def clear_left(self) -> None:
"""Clear all widgets from the left pane except the default label."""
for widget in self._frame_left.winfo_children():
if widget != self.label_left:
widget.destroy()
[docs]
def clear_center(self) -> None:
"""Clear all widgets from the center pane except the default label."""
for widget in self._frame_center.winfo_children():
if widget != self.label_center:
widget.destroy()
[docs]
def clear_right(self) -> None:
"""Clear all widgets from the right pane except the default label."""
for widget in self._frame_right.winfo_children():
if widget != self.label_right:
widget.destroy()
[docs]
def set_left_width(self, width: int) -> None:
"""Set the left pane width."""
if width < 0:
raise ValueError("Width must be non-negative")
width = max(width, self.min_pane_size)
self.left_width = width
self.left_fixed_width = width
self._resize()
[docs]
def set_right_width(self, width: int) -> None:
"""Set the right pane width."""
if width < 0:
raise ValueError("Width must be non-negative")
width = max(width, self.min_pane_size)
self.right_width = width
self.right_fixed_width = width
self._resize()
[docs]
def get_left_width(self) -> int:
"""Get the current left pane width."""
return self.left_fixed_width or self.side_width
[docs]
def get_right_width(self) -> int:
"""Get the current right pane width."""
return self.right_fixed_width or self.side_width
[docs]
def is_left_fixed(self) -> bool:
"""Check if left pane has fixed width."""
return self.left_fixed_width is not None
[docs]
def is_right_fixed(self) -> bool:
"""Check if right pane has fixed width."""
return self.right_fixed_width is not None
@property
def left_pane(self) -> tk.Frame:
"""Access to left pane for adding widgets."""
return self._frame_left
@property
def center_pane(self) -> tk.Frame:
"""Access to center pane for adding widgets."""
return self._frame_center
@property
def right_pane(self) -> tk.Frame:
"""Access to right pane for adding widgets."""
return self._frame_right
# Legacy property names for backward compatibility are defined above
# Modern alias - this is the preferred class name
FixedThreePaneWindow = FixedThreePaneLayout