Я использую Python 3.14 в Windows 11.
У меня есть 2 фрейма (левый и правый) внутри верхнего фрейма, а также есть нижний фрейм, который будет действовать как строка состояния.
Я хочу, чтобы левый фрейм был квадратом, отдавая остальную часть горизонтального пространства правому фрейму, если окно увеличивается.
Поскольку я не мог заставить его работать должным образом, я прочитал несколько сообщений, особенно это один:
Изменение размера фреймов Tkinter с фиксированным соотношением сторон.
Решение Брайана Окли не подходит для моих нужд, поскольку я хочу упаковать или разместить больше виджетов в правом кадре.
Однако решение Гэри Керра работает хорошо в теории, но в моем примере я не могу заставить его работать должным образом. На следующем рисунке общая ширина окна составляет 700 пикселей, а высота — 200 пикселей (181 пиксель верхнего контейнера и 19 пикселей нижнего контейнера). Я хочу, чтобы левый (темно-серый) контейнер имел размер 181x181, а правый (зеленый) контейнер — 700–181 = 519 пикселей в ширину и 181 пиксель в высоту. Эти размеры являются лишь примером после изменения размера главного окна.
После принудительного задания относительных весов с помощью columnsconfigure и расширения окна вправо левый контейнер продолжает расти и не является квадратом.
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
# Calculate the desired aspect ratio of the window
# To do that, maximize the window, calculate its dimensions
# and restore it to its initial state
self.root.state('zoomed') # self.root.attributes("-zoomed", True) for Linux?
self.root.update_idletasks()
self.rW = self.root.winfo_width()
self.rH = self.root.winfo_height()
self.root.state('normal')
# self.rW / self.rH is the desired aspect ratio of the window
# When the window first appears onscreen, set initial dimensions
# according to that aspect ratio
initial_width = 800
initial_height = int(initial_width / self.rW * self.rH)
self.root.geometry(f'{initial_width}x{initial_height}+50+50')
self.root.minsize(width=400, height=int(400 / self.rW * self.rH))
# Styles
self.style = ttk.Style(self.root)
self.style.configure('Dark.TFrame', background='#343434')
self.style.configure('LightGreen.TFrame', background='#a4edde')
# Main frames
self.top_container = ttk.Frame(self.root)
self.top_container.pack(fill='both', expand=True)
self.status_bar = ttk.Frame(self.root)
self.status_bar.config(height=19)
self.status_bar.pack_propagate(False)
self.status_bar.pack(fill='x', expand=False)
# Calculate height of status bar. It should be 19, as set earlier
self.root.update_idletasks()
self.status_bar_h = self.status_bar.winfo_height()
# Frames inside top_container
w = initial_height - self.status_bar_h
h = initial_height - self.status_bar_h
self.frm_canvas = ttk.Frame(self.top_container, style='Dark.TFrame')
self.frm_data_display = ttk.Frame(self.top_container, style='LightGreen.TFrame')
self.frm_canvas.pack(side='left', fill='both', expand=True)
self.frm_canvas.config(height=h, width=w) # Force square
self.frm_canvas.pack_propagate(False)
self.frm_data_display.pack(side='left', fill='both', expand=True)
self.frm_data_display.config(height=h, width=initial_width - h)
self.frm_data_display.pack_propagate(False)
# Labels inside the frames
# 1: in the status bar
self.lbl_left = ttk.Label(self.status_bar, relief=tk.GROOVE, text="Status bar size: ")
self.lbl_right = ttk.Label(self.status_bar, relief=tk.GROOVE, text="Root window size: ")
self.lbl_left.pack(side='left', fill='x', expand=True)
self.lbl_right.pack(side='left', fill='x', expand=True)
# 2: in the canvas frame
self.lbl_canvas = ttk.Label(self.frm_canvas, text="Canvas size: ")
self.lbl_canvas.pack(side='top', expand=True)
# 3: in the data display frame
self.lbl_data = ttk.Label(self.frm_data_display, text="Data frame size: ")
self.lbl_data.pack(side='left', expand=True)
#self.root.update_idletasks()
# I need to update() each widget individually because
# it doesn't work with self.root.update_idletasks()
# TO DO: investigate
self.top_container.update()
self.status_bar.update()
self.frm_canvas.update()
self.frm_data_display.update()
# Update the labels with the initial dimensions of the frames and the window
self.lbl_left.config(text=f"Status bar size: {self.status_bar.winfo_width()}x{self.status_bar.winfo_height()}")
self.lbl_right.config(text=f"Window size: {self.root.winfo_width()}x{self.root.winfo_height()}")
self.lbl_canvas.config(text=f"Canvas size: {self.frm_canvas.winfo_width()}x{self.frm_canvas.winfo_height()}")
self.lbl_data.config(text=f"Data frame size: {self.frm_data_display.winfo_width()}x{self.frm_data_display.winfo_height()}")
self.resizing = False
# Enforce certain dimension constraints:
# 1. The canvas frame should be a square (aspect ratio of 1:1)
# 2. The canvas should not be smaller than 300x300
# 3. The data frame should always be 200 pixels wide
# 4. The window should not be smaller than the sum of the canvas size and the status bar height.
self.enforce_fixed_aspect_ratio()
def enforce_fixed_aspect_ratio(self):
self.root.bind("", self.adjust_frame_sizes)
def adjust_frame_sizes(self, event):
if not self.resizing:
self.resizing = True
# Unbind the event to prevent recursive calls. Is this a good practice?
self.root.unbind("")
w = self.root.winfo_height() - self.status_bar_h
h = self.root.winfo_height() - self.status_bar_h
# Fixed (square) dimensions for frm_canvas Frame
self.frm_canvas.config(height=h, width=w)
self.frm_canvas.pack_propagate(False)
self.frm_data_display.pack(side='left', fill='both', expand=True)
# For frm_data_display Frame, same height but calculate width to ensure the window aspect ratio
self.frm_data_display.config(height=h, width=int(h / self.rH * self.rW))
self.frm_data_display.pack_propagate(False)
# I need to use these 4 lines of code because, otherwise, the resizing doesn't work as desired
# TO DO: why is it necessary to set self.root.geometry() after setting each widget individually?
window_width = self.root.winfo_width()
window_height = self.root.winfo_height()
desired_window_width = int(window_height / self.rH * self.rW)
self.root.geometry(f'{desired_window_width}x{window_height}')
#self.root.update_idletasks()
# I need to update() each widget individually because
# it doesn't work with self.root.update_idletasks()
# TO DO: investigate
self.top_container.update()
self.status_bar.update()
self.frm_canvas.update()
self.frm_data_display.update()
self.lbl_left.config(text=f"Canvas size: {self.status_bar.winfo_width()}x{self.status_bar.winfo_height()}")
self.lbl_right.config(text=f"Root window size: {self.root.winfo_width()}x{self.root.winfo_height()}")
self.lbl_canvas.config(text=f"Canvas size: {self.frm_canvas.winfo_width()}x{self.frm_canvas.winfo_height()}")
self.lbl_data.config(text=f"Data frame size: {self.frm_data_display.winfo_width()}x{self.frm_data_display.winfo_height()}")
if self.resizing:
self.resizing = False
# Re-bind the event to allow further resizing
self.root.bind("", self.adjust_frame_sizes)
if __name__ == '__main__':
root = tk.Tk()
app = App(root)
root.mainloop()
Предостережения этого решения:
Поскольку я использую .state('zoomed') в начале и .state('normal') после этого, можно увидеть некоторое быстрое мигание.
Просто горизонтальное изменение размера невозможно. Вы можете перетаскивать границы снизу окна или из углов.
При изменении размера можно увидеть больше мерцаний, и это более очевидно, чем начальное мерцание, для расчета соотношения сторон.
Плюсы:
В отличие от трех (на данный момент) предложенных решений, невозможно заставить окно иметь высоту > ширину. Это интересно, потому что квадрат всегда можно рассматривать как квадрат.
Я использую Python 3.14 в Windows 11. У меня есть 2 фрейма (левый и правый) внутри верхнего фрейма, а также есть нижний фрейм, который будет действовать как строка состояния. Я хочу, чтобы левый фрейм был квадратом, отдавая остальную часть горизонтального пространства правому фрейму, если окно увеличивается. Поскольку я не мог заставить его работать должным образом, я прочитал несколько сообщений, особенно это один: Изменение размера фреймов Tkinter с фиксированным соотношением сторон. Решение Брайана Окли не подходит для моих нужд, поскольку я хочу упаковать или разместить больше виджетов в правом кадре. Однако решение Гэри Керра работает хорошо в теории, но в моем примере я не могу заставить его работать должным образом. На следующем рисунке общая ширина окна составляет 700 пикселей, а высота — 200 пикселей (181 пиксель верхнего контейнера и 19 пикселей нижнего контейнера). Я хочу, чтобы левый (темно-серый) контейнер имел размер 181x181, а правый (зеленый) контейнер — 700–181 = 519 пикселей в ширину и 181 пиксель в высоту. Эти размеры являются лишь примером после изменения размера главного окна. После принудительного задания относительных весов с помощью columnsconfigure и расширения окна вправо левый контейнер продолжает расти и не является квадратом. [img]https://i.sstatic.net/CUvKLlfr.png[/img]
Это минимальный рабочий пример: [code]import tkinter as tk from tkinter import ttk from tkinter import Label
class App: def __init__(self, root): self.root = root self.root.geometry("400x200+900+400")
# Main frames: top (for content) and bottom (status bar) self.top_container = ttk.Frame(self.root) self.bottom_container = ttk.Frame(self.root) self.top_container.pack(fill='both', expand=True) self.bottom_container.pack(fill='x', expand=False)
# Left container for future Canvas map self.left_container = ttk.Frame(self.top_container, style='Dark.TFrame') # Right container for data display self.right_container = ttk.Frame(self.top_container, style='LightGreen.TFrame')
if W > H: if lw != H: # self.left_container.grid_forget() # self.right_container.grid_forget() self.top_container.columnconfigure(0, weight=H) self.top_container.columnconfigure(1, weight=W-H) # self.left_container.grid(row=0, column=0, sticky='NSWE') # self.right_container.grid(row=0, column=1, sticky='NSWE') else: self.top_container.columnconfigure(0, weight=1) self.top_container.columnconfigure(1, weight=1) elif H > W: # TO DO pass
# Enable binding again self.root.bind('', self.resize)
if __name__ == '__main__': root = tk.Tk() app = App(root) root.mainloop() [/code] [EDIT] Я придумал другое возможное решение: [code]import tkinter as tk from tkinter import ttk
class App: def __init__(self, root): self.root = root
# Calculate the desired aspect ratio of the window # To do that, maximize the window, calculate its dimensions # and restore it to its initial state self.root.state('zoomed') # self.root.attributes("-zoomed", True) for Linux? self.root.update_idletasks() self.rW = self.root.winfo_width() self.rH = self.root.winfo_height() self.root.state('normal') # self.rW / self.rH is the desired aspect ratio of the window
# When the window first appears onscreen, set initial dimensions # according to that aspect ratio initial_width = 800 initial_height = int(initial_width / self.rW * self.rH) self.root.geometry(f'{initial_width}x{initial_height}+50+50')
# Labels inside the frames # 1: in the status bar self.lbl_left = ttk.Label(self.status_bar, relief=tk.GROOVE, text="Status bar size: ") self.lbl_right = ttk.Label(self.status_bar, relief=tk.GROOVE, text="Root window size: ") self.lbl_left.pack(side='left', fill='x', expand=True) self.lbl_right.pack(side='left', fill='x', expand=True) # 2: in the canvas frame self.lbl_canvas = ttk.Label(self.frm_canvas, text="Canvas size: ") self.lbl_canvas.pack(side='top', expand=True) # 3: in the data display frame self.lbl_data = ttk.Label(self.frm_data_display, text="Data frame size: ") self.lbl_data.pack(side='left', expand=True)
#self.root.update_idletasks() # I need to update() each widget individually because # it doesn't work with self.root.update_idletasks() # TO DO: investigate self.top_container.update() self.status_bar.update() self.frm_canvas.update() self.frm_data_display.update()
# Update the labels with the initial dimensions of the frames and the window self.lbl_left.config(text=f"Status bar size: {self.status_bar.winfo_width()}x{self.status_bar.winfo_height()}") self.lbl_right.config(text=f"Window size: {self.root.winfo_width()}x{self.root.winfo_height()}") self.lbl_canvas.config(text=f"Canvas size: {self.frm_canvas.winfo_width()}x{self.frm_canvas.winfo_height()}") self.lbl_data.config(text=f"Data frame size: {self.frm_data_display.winfo_width()}x{self.frm_data_display.winfo_height()}")
self.resizing = False
# Enforce certain dimension constraints: # 1. The canvas frame should be a square (aspect ratio of 1:1) # 2. The canvas should not be smaller than 300x300 # 3. The data frame should always be 200 pixels wide # 4. The window should not be smaller than the sum of the canvas size and the status bar height.
def adjust_frame_sizes(self, event): if not self.resizing: self.resizing = True # Unbind the event to prevent recursive calls. Is this a good practice? self.root.unbind("")
w = self.root.winfo_height() - self.status_bar_h h = self.root.winfo_height() - self.status_bar_h # Fixed (square) dimensions for frm_canvas Frame self.frm_canvas.config(height=h, width=w) self.frm_canvas.pack_propagate(False) self.frm_data_display.pack(side='left', fill='both', expand=True) # For frm_data_display Frame, same height but calculate width to ensure the window aspect ratio self.frm_data_display.config(height=h, width=int(h / self.rH * self.rW)) self.frm_data_display.pack_propagate(False)
# I need to use these 4 lines of code because, otherwise, the resizing doesn't work as desired # TO DO: why is it necessary to set self.root.geometry() after setting each widget individually? window_width = self.root.winfo_width() window_height = self.root.winfo_height() desired_window_width = int(window_height / self.rH * self.rW) self.root.geometry(f'{desired_window_width}x{window_height}')
#self.root.update_idletasks() # I need to update() each widget individually because # it doesn't work with self.root.update_idletasks() # TO DO: investigate self.top_container.update() self.status_bar.update() self.frm_canvas.update() self.frm_data_display.update()
if self.resizing: self.resizing = False # Re-bind the event to allow further resizing self.root.bind("", self.adjust_frame_sizes)
if __name__ == '__main__': root = tk.Tk() app = App(root) root.mainloop() [/code] Предостережения этого решения: [list] [*]Поскольку я использую .state('zoomed') в начале и .state('normal') после этого, можно увидеть некоторое быстрое мигание. [*]Просто горизонтальное изменение размера невозможно. Вы можете перетаскивать границы снизу окна или из углов. [*]При изменении размера можно увидеть больше мерцаний, и это более очевидно, чем начальное мерцание, для расчета соотношения сторон. [/list] Плюсы: [list] [*]В отличие от трех (на данный момент) предложенных решений, невозможно заставить окно иметь высоту > ширину. Это интересно, потому что квадрат всегда можно рассматривать как квадрат. [/list]