2025, Nov 24 03:02

Корректные размеры виджетов Tkinter после раскладки: winfo, update и Windows

Почему winfo_geometry и winfo_width в Tkinter возвращают 1x1 на Windows после pack; когда нужен update вместо update_idletasks, и как получить точные размеры.

Получить достоверные размеры виджетов в Tkinter удивительно непросто, если они нужны сразу после раскладки интерфейса. В Windows с Python 3.11 вызовы winfo_geometry(), winfo_width(), winfo_height(), winfo_x() и winfo_y() непосредственно после упаковки виджетов могут вернуть 1x1+0+0 или нули для дочерних виджетов, хотя корневое окно выглядит корректно. Это расхождение исчезает после действия пользователя или полной обработки цикла событий GUI.

Минимальный пример, воспроизводящий проблему

Ниже приведён код, который строит простой интерфейс и выводит геометрию корневого окна и нескольких фреймов. Логика стандартная: создаём стили, размещаем виджеты, затем после вызова update_idletasks() запрашиваем их геометрию. Кроме того, при нажатии кнопок метки показывают текущие размеры и позицию.

import tkinter as tk
from tkinter import ttk
class DemoApp:
    def __init__(self, master):
        self.master = master
        self.master.title("Window title")
        self.master.geometry("800x600")
        self.init_styles()
        self.layout_widgets()
        self.dump_metrics()
    def init_styles(self):
        self.sty_dark = ttk.Style()
        self.sty_dark.configure('ThemeDark.TFrame', background='#343434')
        self.sty_lg = ttk.Style()
        self.sty_lg.configure('AccentGreen.TFrame', background='#a4edde')
        self.sty_lb = ttk.Style()
        self.sty_lb.configure('AccentBlue.TFrame', background='#73d9eb')
    def layout_widgets(self):
        self.pane_canvas = ttk.Frame(self.master, style='ThemeDark.TFrame', width=500)
        self.side_panel = ttk.Frame(self.master, style='AccentGreen.TFrame', width=200)
        self.btn_one = ttk.Button(self.pane_canvas, text='btn_1', command=self.on_btn_one)
        self.lab_one = ttk.Label(self.pane_canvas, text="lbl_1", width=40)
        self.btn_one.pack()
        self.lab_one.pack()
        self.pane_canvas.pack_propagate(0)
        self.pane_canvas.pack(side='left', fill='both', expand=True)
        self.side_panel.pack_propagate(0)
        self.side_panel.pack(side='left', fill='both', expand=False)
        self.hdr_one = ttk.Frame(self.side_panel, style='AccentBlue.TFrame')
        self.btn_two = ttk.Button(self.hdr_one, text='btn_2', command=self.on_btn_two)
        self.lab_two = ttk.Label(self.hdr_one, text="lbl_2", width=40)
        self.btn_two.pack()
        self.lab_two.pack()
        self.hdr_one.pack(side='top', fill='x', expand=False)
        self.hdr_two = ttk.Frame(self.side_panel, style='ThemeDark.TFrame')
        self.btn_three = ttk.Button(self.hdr_two, text='btn_3', command=self.on_btn_three)
        self.lab_three = ttk.Label(self.hdr_two, text="lbl_3", width=40)
        self.btn_three.pack()
        self.lab_three.pack()
        self.hdr_two.pack(side='top', fill='x', expand=False)
    def on_btn_one(self):
        self.lab_one.config(text=self.pane_canvas.winfo_geometry())
        self.dump_metrics()
    def on_btn_two(self):
        self.lab_two.config(text=self.hdr_one.winfo_geometry())
        self.dump_metrics()
    def on_btn_three(self):
        self.lab_three.config(text=self.hdr_two.winfo_geometry())
        self.dump_metrics()
    def dump_metrics(self):
        self.master.update_idletasks()
        self.pane_canvas.update_idletasks()
        self.side_panel.update_idletasks()
        self.hdr_one.update_idletasks()
        self.hdr_two.update_idletasks()
        print('self.master.winfo_geometry()', self.master.winfo_geometry())
        print('self.master.winfo_height()', self.master.winfo_height())
        print('self.master.winfo_width()', self.master.winfo_width())
        print('self.master.winfo_x()', self.master.winfo_x())
        print('self.master.winfo_y()', self.master.winfo_y())
        print('self.pane_canvas.winfo_geometry()', self.pane_canvas.winfo_geometry())
        print('self.pane_canvas.winfo_height()', self.pane_canvas.winfo_height())
        print('self.pane_canvas.winfo_width()', self.pane_canvas.winfo_width())
        print('self.pane_canvas.winfo_x()', self.pane_canvas.winfo_x())
        print('self.pane_canvas.winfo_y()', self.pane_canvas.winfo_y())
        print('self.side_panel.winfo_geometry()', self.side_panel.winfo_geometry())
        print('self.side_panel.winfo_height()', self.side_panel.winfo_height())
        print('self.side_panel.winfo_width()', self.side_panel.winfo_width())
        print('self.side_panel.winfo_x()', self.side_panel.winfo_x())
        print('self.side_panel.winfo_y()', self.side_panel.winfo_y())
        print('self.hdr_one.winfo_geometry()', self.hdr_one.winfo_geometry())
        print('self.hdr_one.winfo_height()', self.hdr_one.winfo_height())
        print('self.hdr_one.winfo_width()', self.hdr_one.winfo_width())
        print('self.hdr_one.winfo_x()', self.hdr_one.winfo_x())
        print('self.hdr_one.winfo_y()', self.hdr_one.winfo_y())
        print('self.hdr_two.winfo_geometry()', self.hdr_two.winfo_geometry())
        print('self.hdr_two.winfo_height()', self.hdr_two.winfo_height())
        print('self.hdr_two.winfo_width()', self.hdr_two.winfo_width())
        print('self.hdr_two.winfo_x()', self.hdr_two.winfo_x())
        print('self.hdr_two.winfo_y()', self.hdr_two.winfo_y())
if __name__ == '__main__':
    win = tk.Tk()
    app = DemoApp(win)
    win.mainloop()

Что на самом деле происходит

Tk определяет размеры и позиции виджетов лениво. Сразу после создания и упаковки виджетов согласование геометрии может ещё не завершиться, и ранние вызовы winfo_… способны вернуть заглушки вроде 1x1+0+0. В документации это поведение описано прямо.

w.winfo_geometry() Возвращает строку геометрии, описывающую размер и расположение w на экране.

Предупреждение Геометрия может быть неточной, пока приложение не обновит фоновые задачи. В частности, поначалу все геометрии равны '1x1+0+0', пока виджеты и менеджер геометрии не согласуют свои размеры и позиции. См. метод .update_idletasks() выше в этом разделе, чтобы убедиться, что геометрия виджета актуальна.

Прежде чем использовать методы семейства winfo_, дайте Tk завершить создание и раскладку виджетов на внутреннем уровне GUI. Это можно сделать через update_idletasks() или через update(). Либо используйте winfo_reqwidth() и winfo_reqheight(), которые возвращают желаемый размер виджета, а не его итоговую геометрию.

В Ubuntu на X11 пример выше выдаёт корректные значения после update_idletasks(). В Windows 10 и Windows 11 тот же код всё ещё может печатать 1x1+0+0 для дочерних виджетов, пока не будет использован update(). Переход на update() решает проблему в Windows.

Исправление: принудительно выполнить полное обновление GUI перед чтением геометрии

Замена update_idletasks() на update() гарантирует, что Tk обработает всё необходимое, и геометрия станет точной в описанных окружениях.

import tkinter as tk
from tkinter import ttk
class DemoApp:
    def __init__(self, master):
        self.master = master
        self.master.title("Window title")
        self.master.geometry("800x600")
        self.init_styles()
        self.layout_widgets()
        self.dump_metrics()
    def init_styles(self):
        self.sty_dark = ttk.Style()
        self.sty_dark.configure('ThemeDark.TFrame', background='#343434')
        self.sty_lg = ttk.Style()
        self.sty_lg.configure('AccentGreen.TFrame', background='#a4edde')
        self.sty_lb = ttk.Style()
        self.sty_lb.configure('AccentBlue.TFrame', background='#73d9eb')
    def layout_widgets(self):
        self.pane_canvas = ttk.Frame(self.master, style='ThemeDark.TFrame', width=500)
        self.side_panel = ttk.Frame(self.master, style='AccentGreen.TFrame', width=200)
        self.btn_one = ttk.Button(self.pane_canvas, text='btn_1', command=self.on_btn_one)
        self.lab_one = ttk.Label(self.pane_canvas, text="lbl_1", width=40)
        self.btn_one.pack()
        self.lab_one.pack()
        self.pane_canvas.pack_propagate(0)
        self.pane_canvas.pack(side='left', fill='both', expand=True)
        self.side_panel.pack_propagate(0)
        self.side_panel.pack(side='left', fill='both', expand=False)
        self.hdr_one = ttk.Frame(self.side_panel, style='AccentBlue.TFrame')
        self.btn_two = ttk.Button(self.hdr_one, text='btn_2', command=self.on_btn_two)
        self.lab_two = ttk.Label(self.hdr_one, text="lbl_2", width=40)
        self.btn_two.pack()
        self.lab_two.pack()
        self.hdr_one.pack(side='top', fill='x', expand=False)
        self.hdr_two = ttk.Frame(self.side_panel, style='ThemeDark.TFrame')
        self.btn_three = ttk.Button(self.hdr_two, text='btn_3', command=self.on_btn_three)
        self.lab_three = ttk.Label(self.hdr_two, text="lbl_3", width=40)
        self.btn_three.pack()
        self.lab_three.pack()
        self.hdr_two.pack(side='top', fill='x', expand=False)
    def on_btn_one(self):
        self.lab_one.config(text=self.pane_canvas.winfo_geometry())
        self.dump_metrics()
    def on_btn_two(self):
        self.lab_two.config(text=self.hdr_one.winfo_geometry())
        self.dump_metrics()
    def on_btn_three(self):
        self.lab_three.config(text=self.hdr_two.winfo_geometry())
        self.dump_metrics()
    def dump_metrics(self):
        self.master.update()
        self.pane_canvas.update()
        self.side_panel.update()
        self.hdr_one.update()
        self.hdr_two.update()
        print('self.master.winfo_geometry()', self.master.winfo_geometry())
        print('self.master.winfo_height()', self.master.winfo_height())
        print('self.master.winfo_width()', self.master.winfo_width())
        print('self.master.winfo_x()', self.master.winfo_x())
        print('self.master.winfo_y()', self.master.winfo_y())
        print('self.pane_canvas.winfo_geometry()', self.pane_canvas.winfo_geometry())
        print('self.pane_canvas.winfo_height()', self.pane_canvas.winfo_height())
        print('self.pane_canvas.winfo_width()', self.pane_canvas.winfo_width())
        print('self.pane_canvas.winfo_x()', self.pane_canvas.winfo_x())
        print('self.pane_canvas.winfo_y()', self.pane_canvas.winfo_y())
        print('self.side_panel.winfo_geometry()', self.side_panel.winfo_geometry())
        print('self.side_panel.winfo_height()', self.side_panel.winfo_height())
        print('self.side_panel.winfo_width()', self.side_panel.winfo_width())
        print('self.side_panel.winfo_x()', self.side_panel.winfo_x())
        print('self.side_panel.winfo_y()', self.side_panel.winfo_y())
        print('self.hdr_one.winfo_geometry()', self.hdr_one.winfo_geometry())
        print('self.hdr_one.winfo_height()', self.hdr_one.winfo_height())
        print('self.hdr_one.winfo_width()', self.hdr_one.winfo_width())
        print('self.hdr_one.winfo_x()', self.hdr_one.winfo_x())
        print('self.hdr_one.winfo_y()', self.hdr_one.winfo_y())
        print('self.hdr_two.winfo_geometry()', self.hdr_two.winfo_geometry())
        print('self.hdr_two.winfo_height()', self.hdr_two.winfo_height())
        print('self.hdr_two.winfo_width()', self.hdr_two.winfo_width())
        print('self.hdr_two.winfo_x()', self.hdr_two.winfo_x())
        print('self.hdr_two.winfo_y()', self.hdr_two.winfo_y())
if __name__ == '__main__':
    win = tk.Tk()
    app = DemoApp(win)
    win.mainloop()

Почему это важно

Точные данные о геометрии — основа для динамических раскладок, пользовательского рисования и размещения оверлеев. Если ваш код принимает решения по компоновке или пишет метрики сразу после упаковки, различия между платформами могут приводить к запутанным результатам. Дождавшись, пока GUI продвинется достаточно далеко перед запросом, вы избегаете неконсистентного состояния и получаете предсказуемое поведение в разных окружениях.

Выводы

Запрашивайте геометрию только после того, как Tk завершил раскладку. Перед вызовами методов winfo_… используйте update_idletasks() или update(); если нужен желаемый (целевой) размер, а не окончательный, полагайтесь на winfo_reqwidth() и winfo_reqheight(). В Windows замена update_idletasks() на update() устраняет случай, когда виджеты после раскладки всё ещё сообщают 1x1+0+0. Помня об этом, вы сможете создавать кроссплатформенные интерфейсы Tkinter, которые с момента появления сообщают согласованные размеры.