Сделать виджет квадратнымPython

Программы на Python
Ответить
Anonymous
 Сделать виджет квадратным

Сообщение Anonymous »

Я использую 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
from tkinter import Label

class App:
def __init__(self, root):
self.root = root
self.root.geometry("400x200+900+400")

# Styles
self.style = ttk.Style(self.root)
self.style.configure('Dark.TFrame', background='#343434')
self.style.configure('LightGreen.TFrame', background='#a4edde')

# 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')

self.top_container.rowconfigure(0, weight=1)
self.top_container.columnconfigure(0, weight=1)
self.top_container.columnconfigure(1, weight=2)

self.left_container.grid(row=0, column=0, sticky='NSWE')
self.right_container.grid(row=0, column=1, sticky='NSWE')

# Labels just for displaying sizes
self.left_label = ttk.Label(self.left_container, text="Left")
self.right_label = ttk.Label(self.right_container, text="Right")
self.bottom_label = ttk.Label(self.bottom_container, text="Bottom")
self.left_label.pack()
self.right_label.pack()
self.bottom_label.pack()

self.root.bind('', self.resize)

def resize(self, event):
# Remove the binding.  It will be bound again later.
self.root.unbind('')

w, h = event.width, event.height
w1, h1 = self.root.winfo_width(), self.root.winfo_height()

self.root.update_idletasks()
W = self.top_container.winfo_width()
H = self.top_container.winfo_height()
lw = self.left_container.winfo_width()
lh = self.left_container.winfo_height()
rw = self.right_container.winfo_width()
rh = self.right_container.winfo_height()

self.bottom_label.config(text=f'{W=} {H=}')
self.left_label.config(text=f'{lw=} {lh=}')
self.right_label.config(text=f'{rw=} {rh=}')

print(f'SIZES: {W=}  {H=}  {w=}  {h=}  {lw=}  {rw=}  {lh=}  {rh=}')
print('LEFT WEIGHT', self.top_container.columnconfigure(0)['weight'])
print('RIGHT WEIGHT', self.top_container.columnconfigure(1)['weight'])

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()
[EDIT] Я придумал другое возможное решение:

Код: Выделить всё

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') после этого, можно увидеть некоторое быстрое мигание.
  • Просто горизонтальное изменение размера невозможно. Вы можете перетаскивать границы снизу окна или из углов.
  • При изменении размера можно увидеть больше мерцаний, и это более очевидно, чем начальное мерцание, для расчета соотношения сторон.
Плюсы:
  • В отличие от трех (на данный момент) предложенных решений, невозможно заставить окно иметь высоту > ширину. Это интересно, потому что квадрат всегда можно рассматривать как квадрат.


Подробнее здесь: https://stackoverflow.com/questions/798 ... e-a-square
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Python»