Наши партнеры

UnixForum






Книги по Linux (с отзывами читателей)

Библиотека сайта rus-linux.net

На главную -> MyLDP -> Тематический каталог -> Программирование и алгоритмические языки в Linux

Применение управляющих размещениями Tkinter

Шапошникова С.В., 19.08.2007 г.



Что это?
Grid – плетём сети для создания ячеек
Pack – пакуем вещи: просто и ... в чемоданы
Place – ищем удобное местечко, возможны относительные варианты
Выводы

Что это?

В Tkinter (библиотеке компонентов графического интерфейса, входящей в Python по "умолчанию") есть три стандартных «менеджера геометрии» (англ. «Geometry Manager») - управляющих размещениями: Grid, Pack и Place. Занимаются они тем, что располагают на главном окне остальные виджеты, причем каждый из трех делает это по-своему. Подобным вопросам при невизуальном программировании приходится уделять немалое внимание в связи с отсутствием возможности весело тыкать мышкой по виджетам на панели, а затем сосредоточенно развозить их по окну, как, например, в VisualBasic.

Возможно, наиболее полная информация по управляющим размещениями представлена в «Введение в Tkinter» (автор Fredrik Lundh), главы 28, 35 и 37. Однако, отсутствие примеров в описании менеджера Pack и, так и оставшийся за гранью моего понимания, смысл некоторых англоязычных фраз в описании Place, привели к мысли "поставить несколько опытов" и увидеть на наглядных примерах принципы работы, преимущества и недостатки каждого из трех методов.

Попробуем организовать некий прототип диалогового окна (создание настоящего диалогового окна – это отдельная история) тремя разными способами. За примером далеко ходить не будем ... Таблица -> Вставить -> Строки ...



Образец диалогового окна


Вполне подойдет. Напишем часть кода, которая останется почти неизменной в будущих опытах (шкала рядом с текстовым полем опущена):


# -*- coding: UTF-8 -*-
from Tkinter import *

# Виджеты
root = Tk() # окно
root.title(
"Вставка строки")

l_in=Label(root,text="Вставить") # метки
l_num=Label(root,text="Кол-во")
l_pos=Label(root,text=
"Положение")

e_num=Entry(root, width=5, bg="White") # текстовое поле

pos = IntVar() # радиокнопки
pos.set(0)
r_pos1=Radiobutton(root,text=
"До", variable=pos, value=0)
r_pos2=Radiobutton(root,text=
"После", variable=pos, value=1)

b_ok=Button(root,text=
"OK", width=10) # Кнопки
b_canc=Button(root,text="Отмена", width=10)
b_help=Button(root,text=
"Справка", width=10)

# Размещение виджетов
#...

root.mainloop()

Теперь приступим к делу. Не нарушая логики "An Introduction to Tkinter", начнем с Grid...

Grid – плетём сети для создания ячеек

Grid делит пространство родительского виджета на ячейки, количество которых определяется дочерними виджетами. Ячейка идентифицируется номером строки (row) и номером столбца (column). Ячейки можно объединять как по горизонтали (columnspan), так и по вертикали (rowspan).

В соответствие с вышеприведенным примером разработаем схему, которая поможет определить количество необходимых ячеек, их адреса и объединения:


Схема Grid


«Жилые» ячейки пронумерованы. Первое число – номер строки, второе – столбца (нумерация начинается с нуля).

Теперь очень легко написать вторую часть кода:

...
# Размещение виджетов
l_in.grid(row=0,column=0,columnspan=2)
l_num.grid(row=1,column=0)
e_num.grid(row=1,column=1)
l_pos.grid(row=2,column=0,columnspan=2)
r_pos1.grid(row=3,column=0)
r_pos2.grid(row=4,column=0)
b_ok.grid(row=1,column=2)
b_canc.grid(row=2,column=2)
b_help.grid(row=3,column=2)
...

При выполнении получаем следующее:


Окно, полученное с помощью Grid


Очень даже ничего, но вот положение радио-кнопок относительно друг друга явно не в порядке. Данный казус результат того, что размер ячейки не всегда совпадает с размером виджета; а по-умолчанию положение последнего определяется по центру. Для изменения положения виджета в ячейке используется опция sticky ("липучка"), значение которой может быть любой комбинацией констант S, N, E и W, или NW, NE, SW и SE. Например, W (запад) означает, что виджет должен быть выравнен по левой границе ячейки. W+E – виджет будет вытянут горизонтально и заполнит целую ячейку по данной оси. W+E+N+S означает, что виджет будет расширен в обоих направлениях.

Еще один «недочет» - кнопки расположены слишком близко друг к другу. Исправляется с помощью опций padx и pady, которые позволяют заполнить пустотой место вокруг виджета.

В конечном итоге, я остановилась на таком варианте:

...
# Размещение виджетов
l_in.grid(row=0,column=0,columnspan=2,sticky=W,pady=5)
l_num.grid(row=1,column=0)
e_num.grid(row=1,column=1,sticky=W)
l_pos.grid(row=2,column=0,columnspan=2,sticky=W)
r_pos1.grid(row=3,column=0,sticky=W,padx=5)
r_pos2.grid(row=4,column=0,sticky=W,padx=5)
b_ok.grid(row=1,column=2,padx=10,pady=5)
b_canc.grid(row=2,column=2,padx=10,pady=5)
b_help.grid(row=3,column=2,padx=10,pady=5)
...

Pack – пакуем вещи: просто и ... в чемоданы

Менеджер Pack только и делает, что располагает виджеты друг за другом по той или иной стороне. Опция side может принимать следующие значения: TOP (значение по-умолчанию, а значит можно не указывать), LEFT, BOTTOM и RIGHT. Для дочерних виджетов, располагающихся на одном родительском окне, можно указывать разные стороны, однако при этом можно и не добиться желаемого результата.

Очевидно, что pack () подойдет лишь для более простых схем расположения, а не таких как пример выше. Для того, чтобы все-таки выяснить как работает pack (), разберем более простой пример: расположим четыре рамки сначала горизонтально, затем вертикально и, наконец, попробуем создать расположение «2х2».

Общая часть кода:

from Tkinter import *

root = Tk()

f_1=Frame(root,width=50,height=50,bg="Yellow")
f_2=Frame(root,width=50,height=50,bg="Blue")
f_3=Frame(root,width=50,height=50,bg="Green")
f_4=Frame(root,width=50,height=50,bg="Red")

# Размещение рамок
# ...

root.mainloop()

Размещение рамок:

горизонтально

f_1.pack(side=LEFT)
f_2.pack(side=LEFT)
f_3.pack(side=LEFT)
f_4.pack(side=LEFT)

Окно с горизонтальным располож. рамок

вертикально

f_1.pack()
f_2.pack()
f_3.pack()
f_4.pack()

Окно с вертикальным располож. рамок

более сложное расположение

f_1.pack(side=LEFT)
f_2.pack(side=RIGHT)
f_3.pack(side=TOP)
f_4.pack(side=BOTTOM)

Окно со сложным расположением

Как я ни пыталась проявить чудеса логического мышления, «2х2» так и не вышло. Но, на самом деле, существует известная хитрость, позволяющая использовать Pack где угодно и как угодно. Заключается она в том, что вводятся дополнительные рамки (фреймы). Поэтому для варианта «2х2» код может быть таким:

Размещение «2х2»

from Tkinter import *

root = Tk()

f_01=Frame(root)
f_02=Frame(root)
f_1=Frame(f_01,width=50,height=50,bg="Yellow")
f_2=Frame(f_01,width=50,height=50,bg="Blue")
f_3=Frame(f_02,width=50,height=50,bg="Green")
f_4=Frame(f_02,width=50,height=50,bg="Red")

f_01.pack(side=LEFT)
f_02.pack(side=LEFT)
f_1.pack()
f_2.pack()
f_3.pack()
f_4.pack()

root.mainloop()

Окно с рамками 2х2

Теперь можно реализовать и более сложный пример. Сначала создадим схему:


Схема Pack

Здесь, прямоугольники – это рамки; внешние рамки являются родительскими по отношению к внутренним.

Конечный код может быть таким:

# -*- coding: UTF-8 -*-
from Tkinter import *

# Виджеты
root = Tk() # окно
root.title("Вставка строки")

f_1=Frame(root)
# рамки
f_2=Frame(root)
f_11=Frame(f_1)
f_12=Frame(f_1)
f_13=Frame(f_1)

l_in=Label(f_11,text=
"Вставить --------------------") # метки
l_num=Label(f_12,text="Кол-во")
l_pos=Label(f_13,text=
"Положение -----------------")

e_num=Entry(f_12, width=5, bg=
"White") # текстовое поле

pos = IntVar()
# радиокнопки
pos.set(0)
r_pos1=Radiobutton(f_13,text=
"До ", variable=pos, value=0)
r_pos2=Radiobutton(f_13,text=
"После ", variable=pos, value=1)

b_ok=Button(f_2,text=
"OK", width=10) # Кнопки
b_canc=Button(f_2,text="Отмена", width=10)
b_help=Button(f_2,text=
"Справка", width=10)

# Размещение виджетов
f_1.pack(side=LEFT)
f_2.pack(side=LEFT,fill=Y)
f_11.pack()
f_12.pack()
f_13.pack()

l_in.pack()
l_num.pack(side=LEFT)
e_num.pack(side=LEFT)
l_pos.pack()
r_pos1.pack()
r_pos2.pack()

b_ok.pack()
b_canc.pack()
b_help.pack(side=BOTTOM)

root.mainloop()

Обратите внимание на параметр fill в методе pack (), примененном к рамке f_2. Он позволяет заполнять виджету свободное пространство по оси X, Y или обоим направлениям (BOTH). В данном случае, это позволило f_2 стать по высоте равной f_1; иначе ее высота равнялась бы суммарной высоте дочерних виджетов (трех кнопок), что не позволило бы развезти их по разным сторонам.

Place – ищем удобное местечко, возможны относительные варианты

С помощью Place можно явно указывать виджету на его место с помощью координат, как в абсолютных значениях (в пикселях), так и относительно родительского окна (в долях). Также относительно и абсолютно можно задавать размер самого виджета.

Перечислю наиболее важные, на мой взгляд, параметры данного метода:

  • anchor (якорь) – определяет часть виджета, для которой задаются координаты. Принимает значения - N, NE, E, SE, SW, W, NW, или CENTER. По умолчанию NW (верхний левый угол).
  • relwidth, relheight (относительные ширина и высота) – определяют размер виджета по отношению к его родителю.
  • relx, rely – определяют относительную позицию обычно в родительском виджете. Координата (0;0) – у левого верхнего угла, (1;1) - у правого нижнего.
  • width, height – абсолютный размер в пикселах. Значения по умолчанию (когда данные опции опущены) приравниваются к "естественному" размеру виджета.
  • x, y - абсолютная позиция в пикселах. Значения по умолчанию приравниваются к 0.

Создадим для наглядности общую схему, с указанием ключевых относительных координат...


Схема Place


, а затем напишем код:

# -*- coding: UTF-8 -*-
from Tkinter import *

# Виджеты
root = Tk() # окно
root.title("Вставка строки")

f_1=Frame(root,width=300,height=150) # рамка

l_in=Label(f_1,text="Вставить --------------------") # метки
l_num=Label(f_1,text="Кол-во")
l_pos=Label(f_1,text="Положение -----------------")

e_num=Entry(f_1, width=5, bg="White") # текстовое поле

pos = IntVar() # радиокнопки
pos.set(0)
r_pos1=Radiobutton(f_1,text="До", variable=pos, value=0)
r_pos2=Radiobutton(f_1,text="После", variable=pos, value=1)

b_ok=Button(f_1,text="OK", width=8) # Кнопки
b_canc=Button(f_1,text="Отмена", width=8)
b_help=Button(f_1,text="Справка", width=8)

# Расположение виджетов
f_1.pack()
l_in.place(relx=0.03,rely=0.05)
l_num.place(relx=0.07,rely=0.25)
e_num.place(relx=0.3,rely=0.25)
l_pos.place(relx=0.03,rely=0.5)
r_pos1.place(relx=0.07,rely=0.65)
r_pos2.place(relx=0.07,rely=0.8)
b_ok.place(relx=0.7,rely=0.2)
b_canc.place(relx=0.7,rely=0.4)
b_help.place(relx=0.7,rely=0.7)

root.mainloop()

Обратите внимание, что виджеты размещаются не на главном окне, а рамке f_1. Рамка введена для того, чтобы диалоговое окно приняло нужную форму. К ней применен метод pack(). Хотя нельзя использовать два менеджера внутри одного родительского окна, к данному случаю это правило не относится, т.к. у рамки родителем является главное окно, а у остальных виджетов – рамка.

Размер виджета не указан, а значит равняется их «естественному» размеру. Указание относительных размеров чревато тем, что при изменении размера родительского окна будет изменяться и сам виджет. Указание же абсолютных координат при изменяемом родительском окне также может приводить к нежелательным результатам. Чтобы убедиться воочию в «непредсказуемости» менеджера place, достаточно немного подправить вышеприведенный код. Позволим рамке расширяться и занимать все свободное пространство:

f_1.pack(fill=BOTH,expand=1)

Для кнопок укажем абсолютные координаты и относительные размеры:

b_ok.place(x=200,y=25, relwidth=0.2, relheight=0.2)
b_canc.place(x=200,y=60, relwidth=0.2, relheight=0.2)
b_help.place(x=200,y=95, relwidth=0.2, relheight=0.2)

Запускаем, разворачиваем окно на полный экран и удивляемся.

Выводы

Теперь сравним результаты «опыта» с характеристиками менеджеров, данными в «Введении в Tkinter».

Fredrik Lundh характеризует менеджер Grid как самый гибкий и универсальный. Последнее понятно: его можно применять в любой ситуации и не бояться неожиданных спецэффектов. А вот что понимать под гибкостью? В примере, для того, чтобы увеличить расстояние между кнопками были использованы опции padx и pady. Это привело к увеличению самих ячеек, и, как следствие, увеличению самого окна. Конечно, при использовании большего числа параметров и вычислении соотношений размеров виджета, можно добиться желаемого результата. Но реализовать схему, где необходима тонкая настойка положений виджета, может оказаться слишком сложно. Следует также помнить, что пустые строки и столбцы игнорируются. Например, если в примере вместо столбца 2 указать 3, ничего не изменится.

В «Введении» менеджер Pack никак не характеризуется, но автор часто рекомендует попробовать Grid вместо него. Ясно, что Pack – это идеальный вариант для простых схем. Разработка более сложных требуется ввода дополнительных рамок, что приводит к увеличению кода; хотя понять работу этого метода легче всего. Также, исключается тонкая подстройка.

Place характеризуется как самый простой из трех управляющих. Действительно: указываешь координаты и получаешь готовый результат. В примере, Place позволил быстро и просто получить самое изящное и близкое к оригиналу окно. Сложности начинаются, когда требуется создать обычное окно, изменяемое в размерах. Lundh рекомендует использовать Place лишь в специфических случаях. Например, размещение виджета строго по центру (w.place(relx=0.5, rely=0.5, anchor=CENTER)) или когда требуется наложить один виджет на другой. Мне же метод place() нравится куда больше grid(). Часто оказывается очень удобным, что текстовое поле или картинка увеличиваются при увеличении окна. Конфуз с кнопками легко избежать или поместив их в отдельную рамку, или умело комбинируя опции. Конечно, простота метода при этом исчезает, но зато появляется гибкость.

Понятно, что для каждого идеален тот управляющий размещениями, пользоваться которым можешь в совершенстве. Однако, если целью ставить создание наиболее адекватного кода, необходимо уметь пользоваться тремя методами и грамотно их комбинировать; при этом никогда не забывая о том, что нельзя использовать два менеджера внутри одного родительского окна.