2025, Oct 31 18:16
Стабильная компоновка Tkinter без конфликтов pack и grid
Почему окно Tkinter плывет до загрузки изображения и как это исправить: не смешивайте pack и grid, используйте вложенные фреймы и pack_propagate. С примером.
Если интерфейс Tkinter при запуске выглядит хаотично, а выравнивается только после действия вроде загрузки изображения, почти всегда виновата неверная организация компоновки. В этой ситуации UI растягивается почти на весь экран до загрузки картинки и лишь затем принимает корректные габариты. Причина — способ комбинирования менеджеров размещения и то, как разрешено распространяться геометрии виджетов.
Пример проблемы
Ниже фрагмент, который создает окно с кнопкой, набором элементов управления и фреймом для показа загруженного изображения. До загрузки картинка интерфейс выглядит перекошенным, а после загрузки размер окна меняется.
from tkinter import filedialog, ttk
from tkinter import *
from PIL import Image, ImageTk
import numpy as np
# кэш для ссылки на изображение, чтобы его не собрал сборщик мусора
thumb_ref = None
# открыть изображение и показать его в метке
def handle_open():
global thumb_ref
path = filedialog.askopenfilename()
if path:
pic = Image.open(path)
thumb_ref = ImageTk.PhotoImage(pic)
if thumb_ref.width() < 1200 or thumb_ref.height() < 600:
lbl_preview.image = thumb_ref
lbl_preview.config(image=thumb_ref)
# построение интерфейса
root = Tk()
pane_img = Frame(root)
lbl_preview = Label(pane_img, width=1200, height=600)
btn_upload = Button(root, text='Upload Image', command=handle_open)
lbl_size = Label(root, text="Text size:", font=('calibre',12,'bold'))
cmb_size = ttk.Combobox(root, font=('calibre',12,'normal'), values=list(np.arange(8, 51, step=7)))
lbl_rot = Label(root, text="Rotation:", font=('calibre',12,'bold'))
ent_rot = Entry(root, font=('calibre',12,'normal'))
lbl_op = Label(root, text="Opacity:", font=('calibre',12,'bold'))
sld_op = Scale(root, from_=0, to=100, orient='horizontal')
# размещение элементов
pane_img.grid(column=3, row=2, columnspan=5, rowspan=10, padx=50)
lbl_preview.pack()
btn_upload.grid(column=1, row=1, padx=60, pady=60)
lbl_size.grid(column=1, row=2)
cmb_size.grid(row=2, column=2)
lbl_rot.grid(row=3, column=1)
ent_rot.grid(row=3, column=2)
lbl_op.grid(row=4, column=1)
sld_op.grid(row=4, column=2)
root.mainloop()
Что происходит на самом деле
Здесь сходятся два фактора. Во‑первых, смешивать менеджеры компоновки внутри одного и того же контейнера нельзя — это порождает конфликты. Надежный подход: один менеджер на контейнер и вложенные контейнеры там, где требуется другое поведение.
В одном виджете одновременно используются несколько схем компоновки, которые несовместимы.
Во‑вторых, даже с правильным менеджером виджеты могут расширяться или сжиматься под содержимое, если их об этом не попросить иначе. Корректное применение sticky в grid, а также fill и expand в pack и управление распространением размеров там, где это нужно, обеспечивает предсказуемую геометрию. Отключение распространения геометрии для области изображения не даст ей сжиматься или раздуваться в зависимости от того, загружена картинка или нет.
Решение и исправленный пример
В доработанной структуре используются два фрейма, размещенные бок о бок с помощью pack. Левый фрейм применяет grid для элементов управления. Правый содержит метку с изображением и отключает распространение геометрии, чтобы компоновка не зависела от факта загрузки изображения.
from tkinter import filedialog, ttk
from tkinter import *
from PIL import Image, ImageTk
import numpy as np
class UiShell(Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title('Example')
w = self.winfo_screenwidth()
h = self.winfo_screenheight()
self.geometry(f"{w}x{h}")
self.minsize(1200, 600)
self._build_ui()
def _build_ui(self):
self.panel_controls = Frame(self)
self.panel_preview = Frame(self)
# запретить панели предпросмотра изменять размер под содержимое
self.panel_preview.pack_propagate(False)
self.panel_controls.pack(side='left', fill='y', padx=8, pady=8)
self.panel_preview.pack(side='left', fill='both', expand=True)
# правая сторона: область изображения
self.img_holder = Label(self.panel_preview)
self.img_holder.pack(fill='both', expand=True)
# левая сторона: элементы управления
self.btn_open = Button(self.panel_controls, text='Upload Image', command=self.on_pick_image)
self.lbl_size = Label(self.panel_controls, text="Text size:", font=('calibre',12, 'bold'))
self.cmb_size = ttk.Combobox(self.panel_controls, font=('calibre',12, 'normal'), values=list(np.arange(8, 51, step=7)))
self.lbl_angle = Label(self.panel_controls, text="Rotation:", font=('calibre',12, 'bold'))
self.ent_angle = Entry(self.panel_controls, font=('calibre',12, 'normal'))
self.lbl_opacity = Label(self.panel_controls, text="Opacity:", font=('calibre',12, 'bold'))
self.sld_opacity = Scale(self.panel_controls, from_=0, to=100, orient='horizontal')
self.btn_open.grid(column=0, row=0, sticky='nsew', columnspan=2, pady=8)
self.lbl_size.grid(column=0, row=1, sticky='nsew', pady=4)
self.cmb_size.grid(column=1, row=1, sticky='nsew', pady=4)
self.lbl_angle.grid(column=0, row=2, sticky='nsew', pady=4)
self.ent_angle.grid(column=1, row=2, sticky='nsew', pady=4)
self.lbl_opacity.grid(column=0, row=3, sticky='nsew', pady=4)
self.sld_opacity.grid(column=1, row=3, sticky='nsew', pady=4)
def on_pick_image(self):
fp = filedialog.askopenfilename()
if fp:
im = Image.open(fp)
photo = ImageTk.PhotoImage(im)
if photo.width() < 1200 or photo.height() < 600:
self.img_holder.image = photo
self.img_holder.config(image=photo)
if __name__ == '__main__':
app = UiShell()
app.mainloop()
Почему это важно
Поведение компоновки определяет, будет ли интерфейс Tkinter стабильным или начнет «перестраиваться» по мере появления данных. Понимание того, что pack и grid нельзя смешивать у одного родителя, а также когда применять sticky, fill, expand и pack_propagate, помогает избежать тонких ошибок, которые всплывают только при определенных условиях выполнения, например при загрузке первого изображения.
Итоги
Используйте один менеджер компоновки на контейнер, делите интерфейс на вложенные фреймы, если нужны разные менеджеры, и контролируйте распространение геометрии там, где размер содержимого меняется. Тогда окно будет предсказуемо выглядеть и до, и после загрузки изображения — без внезапных растяжений или схлопываний.