До сих пор мы строили наборы меток и полей ввода из двух колонок. Это типичный вид форм ввода, но менеджер grid в библиотеке tkinter способен организовывать значительно более крупные матрицы. Так, в примере 9.23 создается массив меток, состоящий из пяти строк и четырех колонок, в котором каждая метка просто выводит номер своей строки и колонки (row.col). Если запустить этот сценарий, он создаст окно, изображенное на рис. 9.34.
Пример 9.23. PP4E\Gui\Tour\Grid\grid4.py
# простая двухмерная таблица, в корневом окне Tk по умолчанию
from tkinter import *
for i in range(5):
for j in range(4):
lab = Label(text=’%d.%d’ % (i, j), relief=RIDGE) lab.grid(row=i, column=j, sticky=NSEW)
mainloop()
Если вы предположили, что это выглядит как способ программирования электронных таблиц, то вы, вероятно, на правильном пути. Пример 9.24 дает дальнейшее развитие этой мысли и добавляет кнопку, которая выводит в поток stdout текущие значения полей ввода в таблице (обычно в окно консоли).

Рис. 9.34. Массив 5х4 меток с координатами
Пример 9.24. PP4E\Gui\Tour\Grid\grid5.py
# двухмерная таблица полей ввода, корневое окно Tk по умолчанию
from tkinter import *
rows = []
for i in range(5): cols = [] for j in range(4): ent = Entry(relief=RIDGE) ent.grid(row=i, column=j, sticky=NSEW) ent.insert(END, ‘%d.%d’ % (i, j)) cols.append(ent) rows.append(cols)
def onPress(): for row in rows: for col in row: print(col.get(), end=’ ‘) print()
Button(text=’Fetch’, command=onPress).grid() mainloop()
Если запустить этот сценарий, он создаст окно, изображенное на рис. 9.35, и сохранит все виджеты полей ввода в сетке в двухмерном списке списков. При нажатии кнопки Fetch сценарий выполнит обход списка списков полей ввода, чтобы получить и отобразить все текущие значения в сетке. Ниже приводится вывод после двух нажатий кнопки Fetch — одного перед изменениями в полях ввода и другого после:
# :\…\PP4E\Gui\Tour\Grid> python grid5.py
0.0 0.1 0.2 0.3
1.0 1.1 1.2 1.3
2.0 2.1 2.2 2.3
3.0 3.1 3.2 3.3
Теперь, когда мы знаем, как создавать и выполнять обход массивов полей ввода, добавим еще несколько полезных кнопок. В примере 9.25 добавлен еще один ряд, в котором отображаются суммы по столбцам, и кнопки, которые обнуляют все поля и вычисляют суммы по столбцам.
Пример 9.25. PP4E\Gui\Tour\Grid\grid5b.py
# добавляет суммирование по столбцам и очистку полей ввода
from tkinter import * numrow, numcol = 5, 4
rows = []
for i in range(numrow):
cols = []
for j in range(numcol):
ent = Entry(relief=RIDGE)
ent.grid(row=i, column=j, sticky=NSEW)
ent.insert(END, ‘%d.%d’ % (i, j)) cols.append(ent) rows.append(cols)
sums = []
for i in range(numcol):
lab = Label(text=’?’, relief=SUNKEN) lab.grid(row=numrow, column=i, sticky=NSEW) sums.append(lab)
def onPrint():
for row in rows:
for col in row: print(col.get(), end=’ ‘) print() print()
def onSum():
tots = [0] * numcol
for i in range(numcol):
for j in range(numrow):
tots[i] += eval(rows[j][i].get()) # вычислить сумму
for i in range(numcol):
sums[i].config(text=str(tots[i])) # отобразить в интерфейсе
def onClear():
for row in rows:
for col in row:
col.delete(‘0’, END)
col.insert(END, ‘0.0’)
for sum in sums:
sum.config(text=’?’)
import sys
Button(text=’Sum’, command=onSum).grid(row=numrow+1, column=0)
Button(text=’Print’, command=onPrint).grid(row=numrow+1, column=1)
Button(text=’Clear’, command=onClear).grid(row=numrow+1, column=2) Button(text=’Quit’, command=sys.exit).grid(row=numrow+1, column=3) mainloop()
На рис. 9.36 изображено окно этого сценария после вычисления сумм по четырем столбцам чисел. Чтобы получить таблицу другого размера, измените переменные numrow и numcol в начале сценария.

Рис. 9.36. Добавление суммирования по столбцам
И наконец, пример 9.26, реализующий еще одно, последнее расширение, которое оформлено как класс, что обеспечивает возможность повторного его использования. Кроме того, в нем добавлена кнопка загрузки таблицы из файла с данными. Предполагается, что файл содержит строку для каждого ряда данных, а внутри строки колонки разделяются пробельными символами (пробелами или символами табуляции). При загрузке данных из файла автоматически изменяется размер таблицы, чтобы уместить все колонки.
Пример 9.26. PP4E\Gui\Tour\Grid\grid5c.py
#реализация в виде встраиваемого класса
from tkinter import *
from tkinter.filedialog import askopenfilename
from PP4E.Gui.Tour.quitter import Quitter # повт. использование, pack и grid
class SumGrid(Frame):
def __init__(self, parent=None, numrow=5, numcol=5): Frame.__init__(self, parent) self.numrow = numrow # я — контейнерный фрейм
self.numcol = numcol # компоновку выполняет вызвавшая пр.,
self.makeWidgets(numrow, numcol) # иначе можно было бы использовать
# единственным способом
def makeWidgets(self, numrow, numcol): self.rows = [] for i in range(numrow): cols = [] for j in range(numcol):
ent = Entry(self, relief=RIDGE) ent.grid(row=i+1, column=j, sticky=NSEW) ent.insert(END, ‘%d.%d’ % (i, j)) cols.append(ent)
self.rows.append(cols) self.sums = []
for i in range(numcol):
lab = Label(self, text=’?’, relief=SUNKEN) lab.grid(row=numrow+1, column=i, sticky=NSEW) self.sums.append(lab)
Button(self, text=’Sum’, command=self.onSum).grid(row=0, column=0) Button(self, text=’Print’, command=self.onPrint).grid(row=0, column=1) Button(self, text=’Clear’, command=self.onClear).grid(row=0, column=2) Button(self, text=’Load’, command=self.onLoad).grid(row=0, column=3) Quitter(self).grid(row=0, column=4) # fails: Quitter(self).pack()
def onPrint(self): for row in self.rows: for col in row: print(col.get(), end=’ ‘) print() print()
def onSum(self):
tots = [0] * self.numcol for i in range(self.numcol):
for j in range(self.numrow)
|
tots[i] += eval(self.rows[j][i].get()) for i in range(self.numcol):
self.sums[i].config(text=str(tots[i]))
|
# суммировать данные
|
def
|
onClear(self):
for row in self.rows:
for col in row:
col.delete(‘0’, END)
|
# удалить содержимое
|
|
col.insert(END, ‘0.0’)
|
# зарезерв. значение
|
def
|
for sum in self.sums: sum.config(text=’?’)
onLoad(self):
file = askopenfilename() if file:
for row in self.rows:
for col in row: col.grid_forget()
|
# очистить интерфейс
|
|
for sum in self.sums:
sum.grid_forget()
filelines = open(file, ‘r’).readlines()
|
# загрузить данные
|
|
self.numrow = len(filelines)
|
# изменить размер табл.
|
|
self.numcol = len(filelines[0].split()) self.makeWidgets(self.numrow, self.numcol)
for (row, line) in enumerate(filelines):
|
# загрузить в интерфейс
|
fields = line.split()
for col in range(self.numcol):
self.rows[row][col].delete(‘0’, END) self.rows[row][col].insert(END, fields[col])
if __name__ == ‘__main__’: import sys root = Tk() root.title(‘Summer Grid’) if len(sys.argv) != 3:
SumGrid(root).pack() # .grid() здесь тоже работает
else:
rows, cols = eval(sys.argv[1]), eval(sys.argv[2]) SumGrid(root, rows, cols).pack() mainloop()
Обратите внимание, что класс SumGrid из этого модуля не применяет к себе самому ни метод grid, ни метод pack. Чтобы дать возможность прикрепления к контейнерам, где есть другие графические элементы, скомпонованные тем или иным способом, он оставляет управление собственной компоновкой неопределенным и требует, чтобы вызывающая программа сама применяла метод pack или grid к его экземплярам. Контейнеры могут выбрать любую схему компоновки для своих дочерних элементов, потому что они независимы в своем выборе, но прикрепляемым классам компонентов, предназначенным для использования с любыми менеджерами компоновки, нельзя поручить управлять собой, так как они не могут заранее знать политику своего родителя.
Это довольно длинный пример, в котором нет почти ничего нового в отношении компоновки по сетке или виджетов в целом, поэтому я оставлю его для самостоятельного изучения и просто покажу, что он делает. На рис. 9.37 изображено начальное окно, созданное этим сценарием, после того как была изменена последняя колонка и произведено суммирование — не забудьте включить корневой каталог PP4E дерева с примерами в путь поиска модулей (например, в переменную окружения PYTHONPATH), чтобы сценарий смог импортировать пакет.

Рис. 9.37. Добавлена загрузка данных из файла
По умолчанию класс создает сетку размером 5 на 5, но существует возможность определять другие размерности, как в конструкторе класса, так и в командной строке сценария. При нажатии кнопки Load выводится стандартный диалог выбора файла, с которым мы встречались ранее (рис. 9.38).
Файл данных grid5-data1.txt содержит семь строк и шесть колонок данных:
C:\…\PP4E\Gui\Tour\Grid>type grid5-data1.txt
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
При загрузке его в наш графический интерфейс соответствующим образом изменяются размеры сетки — класс просто заново выполняет логику создания виджетов после удаления прежних элементов ввода с помощью метода grid_forget. Метод grid_forget отвязывает виджеты в сетке и в результате удаляет их с экрана. Другие способы удаления и перерисовки компонентов графического интерфейса предоставляют методы

Рис. 9.38. Диалог открытия файла в сценарии SumGrid

Рис. 9.39. Файл с данными загружен, отображен и просуммирован
pack_forget виджетов и withdraw окна, которые используются в обработчике события after примеров «будильников» в следующем разделе.
На рис. 9.39 показано, как выглядит окно после операций удаления и перерисовки виджетов, выполненных в результате щелчков на кнопках Load и Sum.
Файл с данными grid5-data2.txt имеет те же размерности, но в двух колонках он содержит не просто числа, а выражения. Так как этот сценарий преобразует значения полей ввода с помощью встроенной функции eval, в полях этой таблицы допускается использовать любые выражения Python, если они могут быть вычислены в области видимости метода onSum:
C:\…\PP4E\Gui\Tour\Grid> type grid5-data2.txt
1 2 3 2*2 5 6
1 3-1 3 2<<1 5 6
1 5%3 3 pow(2,2) 5 6
1 2 3 2**2 5 6
1 2 3 [4,3][0] 5 6
1 {‘a’:2}[‘a’] 3 len(‘abcd’) 5 6
1 abs(-2) 3 eval(‘2+2’) 5 6
При суммировании этих полей выполняется содержащийся в них программный код на языке Python, что иллюстрирует рис. 9.40. Эта особенность может оказаться достаточно мощной. Представьте себе, например, полноценную сетку электронной таблицы — значения полей могут быть «фрагментами» программного кода на языке Python, которые динамически вычисляют значения, вызывают функции из модулей и даже загружают текущие котировки акций из Интернета с помощью инструментов, с которыми мы познакомимся в следующей части книги.
Однако эта особенность может представлять опасность — в поле может содержаться выражение, удаляющее содержимое вашего жесткого диска![XL] Если вы не до конца уверены в том, какими могут быть полученные выражения, не используйте функцию eval (осуществляйте преобразование, применяя более ограниченные функции, такие как int и float) или обеспечьте выполнение процесса Python с ограниченными правами доступа к системным компонентам, которые было бы нежелательно подвергать опасности.

Рис. 9.40. Выражения на языке Python в данных и таблице
Конечно, этому сценарию еще очень далеко до настоящей электронной таблицы. Сценарий подсчитывает суммы по колонкам и способен загружать данные из файлов, но ячейки не могут содержать формулы, ссылающиеся на другие ячейки. Однако из-за недостатка места в книге дальнейшие улучшения для достижения этой цели я оставляю читателям в качестве упражнения.
Я должен также отметить, что о размещении по сетке можно сказать больше, чем позволяет объем книги. Например, путем создания вложенных фреймов с собственными сетками можно строить более сложные структуры в виде иерархий компонентов, во многом подобно тому, как размещает вложенные фреймы менеджер компоновки pack. А теперь перейдем к последней теме обзора виджетов.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011