Tkinter, часть 2
Содержание
Глобальные переменные
Определение: глобальная переменная – это переменная, определённая вне любой функции.
Мы уже много раз ими пользовались. Например, константы, которые мы используем внутри функций, являются для этих функций глобальными переменными
Предположим, мы хотим написать программу, которая бы рисовала кружок, медленно ползущий направо1:
1 import Tkinter as tk
2
3 W = H = 500
4 R = 10
5
6 def o_tawa():
7 t = 0
8 t = t + 1
9 x = t % W
10 y = H / 2
11 sitelen.delete("all")
12 sitelen.create_oval(x - R, y - R, x + R, y + R)
13 ijo_lawa.after(100, o_tawa)
14
15 ijo_lawa = tk.Tk()
16 sitelen = tk.Canvas(ijo_lawa, width=W, height=H)
17 sitelen.pack()
18 o_tawa()
19 ijo_lawa.mainloop()
Одна беда: круг при этом никуда не едет. Действительно, если мы присмотримся, обнаружим, что у нас почти прямым текстом сказано, что t = 1 всегда, при каждом вызове o_tawa. Беда в том, что строка t = 0 оказалась внутри функции.
Если мы вынесем t = 0 из функции:
То питону это не понравится:
... UnboundLocalError: local variable 't' referenced before assignment
Дело вот в чём: в питоне есть железное правило: глобальные переменные можно только читать. Поэтому когда мы пишем t = t + 1, питон считает, что нам и в голову не могло прийти поменять глобальную переменную t, а значит, все упоминания переменной t в нашей функции – это локальная переменная. Но тогда получается, что когда мы пишем t + 1 – это первое упоминание этой переменной, а она ещё до этого нигде не определена!
В математике есть такое правило: если нельзя, но очень хочется, то можно. Иногда оно применимо и в программировании.
Мы можем слёзно попросить питон только в этой функции разрешить нам писать только одну эту глобальную переменную – и он нам разрешит. Делает это команда global:
Не случайно то, что в питоне запрещено в глобальные переменные писать, и нужно отдельно просить, если мы хотим это делать. Это правило – следствие большого количества шишек, набитых многими граблями на лбах великих программистов. Без него мы должны были бы, сколь велика не была бы программа, следить за тем, чтобы никакие две функции не использовали одно и то же имя переменной для разных целей. За поведением изменяемых глобальных переменных всегда сложнее следить: глядя на две соседние строчки, вы не можете быть уверены, что переменная между ними не изменилась (вы могли из одной из них вызвать функцию, которая меняет глобальную переменную). Хуже того, почти всегда правда, что переменная не изменится, и поэтому в том редком случае, когда это будет не так, вы будете мучительно ломать голову, почему же программа ведёт себя не так, как хочется.
Отсюда правило: менять глобальные переменные не стоит. Но если уж этого не избежать, то используйте команду global2
Замыкания
Если задуматься, мы могли бы и обойтись без глобальных переменных. Достаточно, чтобы функция o_tawa принимала как аргумент значение переменной t, и для следующего шага просила вызвать себя с значением t + 1.
1 import Tkinter as tk
2
3 W = H = 500
4 R = 10
5
6 def o_tawa(t):
7 t = t + 1
8 x = t % W
9 y = H / 2
10 sitelen.delete("all")
11 sitelen.create_oval(x - R, y - R, x + R, y + R)
12 ijo_lawa.after(100, o_tawa(t))
13
14 ijo_lawa = tk.Tk()
15 sitelen = tk.Canvas(ijo_lawa, width=W, height=H)
16 sitelen.pack()
17 o_tawa(0)
18 ijo_lawa.mainloop()
Это не сработает. Когда мы говорим ...after(..., o_tawa(t)), мы сразу в этот же момент вызываем функцию o_tawa снова (а она в свою очередь сразу дойдёт до этой же строки и вызовет себя снова, и снова, и снова – и так до бесконечности: RuntimeError: maximum recursion depth exceeded
Беда в том, что after всегда вызывает функцию без аргументов. Так сделано умышленно3 потому, что в питоне это ограничение очень легко обойти.
Нам ведь всего-то нужно написать функцию, которая сможет прочитать значение переменной t, и попросить after исполнить её. Так как переменная t описана внутри функции o_tawa, то и нашу функцию нужно будет описать внутри o_tawa, чтобы она видела t:
Это единственный вид ситуаций, для которых допустимо писать определение одной функции внутри другой функции.
Здесь всё просто, но всё же, если нагородить таких вложений несколько, запутать себя становится очень легко, а запутать постороннего читателя проще пареной репы.
lambda
Однако вот такие простые ситуации, когда мы определяем одну функцию внутри другой функции только ради того, чтобы передать её кому-то, настолько типичны, что для этого есть упрощённый синтаксис:
абсолютно эквивалентно такому:
Конструкция lambda4 определяет новую безымянную функцию и возвращает нам саму функцию.
Общее определение: f = lambda a, b, c: E эквивалентно:
Experts only: предупреждение о замыканиях и пространствах имён
Посмотрим на такой пример:
Казалось бы, мы хотим увидеть 1 и 2.
Дело вот в чём: когда мы определяем функцию (буде то с помощью lambda или с помощью def), питон запоминает только одно: где брать значения для переменных, которые не являются локальными.
А как это "где"? Где – это в каком пространстве имён.
Пространство имён – это таблица, в которой написано, у какой (локальной) переменной какое значение.5 Такая таблица создаётся каждый раз, когда мы вызываем функцию. Обычно она сразу после выхода из функции удаляется, но если мы внутри функции определили другую функцию, то есть если пространство имён кому-то нужно, то оно сохраняется с последними значениями всех переменных.
В нашем случае мы вызываем все функции, определённые через lambda, после того, как завершилась функция f, поэтому они видят последнее значение переменной x.
Если понятие "пространство имён" вас пугает, то простое объяснение такое: функция запоминает, о какой переменной идёт речь, но смотрит, какое у неё значение, в тот момент, когда функцию вызвали, а не в тот момент, когда мы функцию определили.
Поэтому же мы можем писать так:
Питон пошёл выяснять, существует ли переменная x только в тот момент, когда мы функцию f вызвали. И только в этот момент узнал значение этой переменной, которое мы будем использовать.
Краткий обзор остальных возможностей Tkinter
Так как у нас есть прекрасный учебник Фредрика Лундха, то вместо конспектов я приведу несколько примеров со ссылками его введение. Одно маленькое напоминание: мы пока что не изучали классы в питоне (в них нет ничего сложного, но всё же это новый способ смотреть на вещи), поэтому если в примере упоминается слово class или self или __init__, то вы можете смело этот пример пропустить.
Мы смотрели на виджеты Entry, Text, http://effbot.org/tkinterbook/listbox.htm и с ними у нас был такой бессмысленный пример: окошко, в котором можно добавлять варианты в Listbox и смотреть, какой выбран:
1 import Tkinter as tk
2
3 def add():
4 text_lines = text.get("1.0", "end").split("\n")
5 entry_lines = [entry.get()]
6 for line in text_lines + entry_lines:
7 line = line.strip()
8 if line:
9 listbox.insert("end", line)
10
11 def status():
12 print "Entry:", entry.get()
13 print "Text:", text.get("1.0", "end")
14 for line_number in listbox.curselection():
15 print "Listbox [", line_number, "]:", listbox.get(line_number)
16
17 root = tk.Tk()
18 entry = tk.Entry(root)
19 entry.pack()
20 text = tk.Text(root)
21 text.pack()
22 listbox = tk.Listbox(root) # попробуйте с selectmode="multiple"
23 listbox.pack()
24 add_button = tk.Button(text="Add", command=add)
25 add_button.pack()
26 status_button = tk.Button(text="Status", command=status)
27 status_button.pack()
28 root.mainloop()
Ещё мы обнаружили склады стандартных диалогов: модуль tkMessageBox и модуль tkFileDialog (про который Фредрик Лундх почему-то не написал).
С этими диалогами мы сочинили такой половинчатый текстовый микроредактор:
1 import codecs
2 import Tkinter as tk
3 from tkMessageBox import askyesno
4 from tkFileDialog import askopenfilename
5
6 def load():
7 if askyesno("Open file?", "Are you sure? If you open file, all your previous editing will be lost!"):
8 filename = askopenfilename()
9 with codecs.open(filename, 'r', 'utf-8') as file:
10 text.delete("1.0", "end")
11 text.insert("end", file.read())
12
13 root = tk.Tk()
14 text = tk.Text()
15 text.pack(fill="both", expand="yes")
16 load_button = tk.Button(text="Open file", command=load)
17 load_button.pack()
18 root.mainloop()
Я надеюсь, что я вам дал представление об основных понятиях, на которых строится подавляющее большинство графических интерфейсов, и показал, что делать интерактивные графические программы на самом деле легко!
Но, конечно, я рассказал вам далеко не всё, что есть в Tkinter. А в нём есть очень много всего полезного, вкусного и интересного. Вам придётся это осваивать самостоятельно – если того потребует от вас судьба, шило, или научное руководство. Надеюсь, теперь это вам не покажется таким уж сложным.
Вот самое полезное:
как делать меню Menu. Здесь всё тривиально. Есть ещё два смешных виджета OptionMenu и MenuButton – это две разновидности кнопки, при нажатии на которую выпадает меню. Рекомендую пока что на них не обращать внимания. (Пример OptionMenu – это выбор размера шрифта в настройках IDLE)
полосы прокрутки Scrollbar. Теоретически с ними можно намутить кучу страшного и нерабочего, если вы вдруг попытаетесь привязывать к нему свои функции в качестве параметра command или сами будете вызвать set, но если вы делаете всё так, как показано в примере, у вас не будет никаких проблем.
правила укладки виджетов под названием grid – эти негодяи не приводят главного примера, для чего оно нужно: чтобы прикрутить к виджету горизонтальный и вертикальный скроллбар одновременно, и всё при этом выглядело прилично. Идея метода укладки такая: вы рисуете решётку, в каждую ячейку решётки кладёте виджет, говорите, к каким краям своей ячейки он должен прилипать и ещё говорите, с какими весами распределяются изменения размеров самой решётки между ячейками решётки. Заодно сразу предупреждаю о важной опасности: в Tkinter нельзя в пределах одного родителя укладывать детей разными правилами укладки. Так что если у вас везде pack, то для того места, где вам нужен grid, нужно создавать отдельный Frame. Но это и логично, иначе было бы совсем непонятно, что вы имели в виду.
- что в правилах укладки можно виджеты прятать, или что сами по себе виджеты можно удалять
кроме Frame, который обычно невидим, и нужен для логической группировки и управления укладкой виджетов ещё бывает LabelFrame – это панель, в которую можно складывать другие виджеты, у которой нарисована рамка и в левом верхнем углу на рамке симпатичненько написано название. Если вы откроете в IDLE настройки, то вы увидите сразу много таких рамок. Ещё одна разновидность – PanedWindow – это когда между двумя Frame'ами есть перегородочка, которую пользователь может двигать мышкой, чтобы менять, сколько какого виджета ему хочется видеть. Типичный пример: в эксплорере (которым вы открываете файлы) слева есть список директорий и мест в компьютере, а справа содержимое выбранной директории – и перегородочку между ними вы можете двигать мышкой.
ещё дополнительные способы получения данных от пользователя: Checkbutton (квадратик для галочки), Radiobutton (кружочек "выбери ровно один из многих"), Scale (полоска прокрутки для выбора числа), Spinbox (это как Entry для ввода чисел, у которого рядом стрелочки вверх-вниз для того, чтобы число увеличивать-уменьшать).
кроме Label бывает ещё и более удобная для многострочного текста штука Message
а если вам придётся что-то делать с рисованием, придётся заново поизучать Canvas, и обнаружить, что в нём есть микроязычок для опознавания нарисованных элементов либо по номеру (это то, что возвращают методы create_чтонибудь), либо по тэгу (тэги мы должны сами на элементы повесить при создании), либо по положению на экране. Пользуясь им вы можете не только стирать всю картинку и рисовать заново, но и двигать отдельные части нарисованного, стирать только ненужное, перекрашивать, менять настройки отдельных частей и т.п.
возможность привязывать свою функцию к любому событию любого виджета посредством bind. С его помощью вы можете реагировать на движения мыши, клики, дабл-клики и "потянуть", на нажатия кнопок на клавиатуре; многие виджеты определяют свои типы событий, например <<ListboxSelect>> – когда изменилось выделение в Listbox; более того, вы можете сами определять свои события, например, <<IFeelBad>>, и решать, в какой момент это событие произойдёт – а tkinter будет в эти моменты вызывать функции-обработчики этих событий. Это очень могучее средство (а значит, и способное заставить программу вести себя не так, как хочется), поэтому лучше им не злоупотреблять, если есть способ сделать то же самое проще.
как делать свои Диалоговые окна или программы из нескольких окон
Ещё я рекомендую игнорировать главу про Variable Wrappers (BooleanVar, DoubleVar, IntVar, StringVar), и все их упоминания в примерах. Это крайне неудачная попытка перенести в питон из tcl конструкцию, которая была мощной в tcl и позволяла сэкономить 5-10 строк в небольшой программе, а в питоне наоборот ухудшает читаемость и захламляет программу лишними 10-15 строками кода. (Разве что, если вам досталась чужая программа, которая их использует, тогда придётся разобраться, что это, и как оно работает – но здесь ничего сложного нет). В питоне вместо них всегда проще вызывать метод get у соответствующего виджета.
Вредный совет: если преподаватель требует, чтобы переменные назывались по-английски, пусть это будет латиница непонятного языка! It was hard to write, it must be hard to understand! (1)
Вторая часть правила появилась не на пустом месте. В питоне есть множество изменяемых объектов, например, списки и словари, и пользуясь ими, можно скрытно обходить запрет на запись в глобальные переменные. Не делайте этого скрытно! Даже если технически питон разрешает вам менять глобальную переменную без дополнительных усилий, всё равно объявите её global, чтобы при беглом чтении это было заметно. (2)
В родном языке tcl, из которого пришла в питон библиотека Tkinter, after умеет запоминать, с какими аргументами вызывать функцию (3)
Название пришло из функционального программирования, а туда из отдела матлогики под названием лямбда-исчисление Чёрча. ФП -- это отдельная, совершенно иная (местами более красивая и выразительная) точка зрения на то, как можно писать программы (4)
Если вам это напоминает словари, то это не случайно. В питоне это и есть словари. Функция locals() возвращает словарь локальных переменных текущей функции, а функция globals() -- словарь глобальных переменных модуля -- и вы даже можете эти словари менять, и при этом будут меняться значения соответствующих переменных! (5)