self.text.mark_set(INSERT, ‘1.0’) # установить курсор вставки в начало

self.text.mark_set(‘linetwo’, ‘2.0’) # пометить текущую строку 2

Имя INSERT является специальной предопределенной меткой, идентифицирующей позицию текстового курсора вставки, - изменение ее влечет изменение позиции курсора вставки. Чтобы создать собственную метку, достаточно просто передать уникальное имя, как показано во втором вызове выше, и использовать его везде, где требуется указать позицию в тексте. Вызов метода mark_unset удаляет метку по имени.

Теги в Text. Кроме абсолютных индексов и символических имен меток виджет Text поддерживает понятие тегов - символических имен, ассоциируемых с одной или несколькими подстроками в текстовом содержимом виджета Text. Теги можно применять с разными целями, в том числе для представления позиции там, где это требуется: помеченные тегами элементы определяются индексами начала и конца, которые можно впоследствии передавать методам, требующим указания позиции.

Например, библиотека tkinter предоставляет встроенный тег с именем SEL - переменную с предопределенным строковым значением ‘sel', -которое автоматически ссылается на текст, выделенный в данный момент. Чтобы получить текст, выделенный (подсвеченный) с помощью мыши, вызовите любой из следующих методов:

text = self.text.get(SEL_FIRST, SEL_LAST) # теги для индексов от/до

text = self.text.get(‘sel.first’, ‘sel.last’) # или строки и константы

Имена SEL_FIRST и SEL_LAST являются обычными переменными в модуле tkinter с предопределенными значениями, используемыми во втором вызове. Метод get ожидает получить два индекса. Чтобы получить текст по тегу, добавьте к его имени расширения .first и .last, которые дают индексы начала и конца.

Чтобы пометить тегом подстроку, можно вызвать метод tag_add виджета Text, передав ему строку с именем тега и позиции начала и конца (тегами можно помечать текст, добавляемый методом insert). Чтобы снять тег со всех символов в некоторой области текста, можно вызвать метод

tag_remove:

self.text.tag_add(‘alltext’, ‘1.0’, END) # пометить тегом весь текст self.text.tag_add(SEL, index1, index2) # выделить от index1 до index2 self.text.tag_remove(SEL, ‘1.0’, END) # снять выделение со всего текста

Здесь в первой строке создается новый тег для всего текста в виджете -от начальной до конечной позиции. Во второй строке во встроенный тег выделения SEL добавляется диапазон символов - эти символы автоматически подсвечиваются, поскольку для этого тега предопределена такая настройка его элементов. В третьей строке все символы текста исключаются из тега SEL (снимаются все выделения). Обратите внимание, что метод tag_remove просто снимает тег с текста в указанном диапазоне - чтобы полностью удалить тег, нужно вызвать метод tag_delete. Кроме того, имейте в виду, что эти методы применяются к самим тегам - чтобы удалить фактический текст, следует использовать метод delete, представленный выше.

Можно также динамически отображать индексы в теги. Например, метод search возвращает индекс row.column первого вхождения строки между начальной и конечной позициями. Чтобы автоматически выделить найденный текст, его индекс следует добавить во встроенный тег SEL:

where = self.text.search(target, INSERT, END) # поиск от курсора вставки pastit = where + (‘+%dc’ % len(target)) # индекс за найденной строкой

self.text.tag_add(SEL, where, pastit) # пометить и выделить найденную строку self.text.focus() # выбрать сам виджет Text

Если требуется выделить только одну строку, нужно сначала вызвать метод tag_remove, о котором говорилось выше, - этот фрагмент добавляет новое выделение к тем, которые уже существуют (таким способом на экране можно создать нескольких выделений). Вообще в тег можно добавить любое количество подстрок и обрабатывать их группой.

Подведем итоги: индексы, метки и позиции тегов можно использовать везде, где требуется указать позицию в тексте. Например, метод see прокручивает содержимое, пока требуемая позиция не окажется в области видимости, - он позволяет указывать позиции всеми тремя способами:

self.text.see(‘1.0’) # прокрутить вверх

self.text.see(INSERT) # прокрутить до метки курсора вставки

self.text.see(SEL_FIRST) # прокрутить до тега выделения

Теги также могут применяться для форматирования и привязки событий, но эти подробности будут обсуждаться в конце раздела.


Операции редактирования текста

В примере 9.11 используются некоторые из этих понятий. Он расширяет пример 9.10, добавляя поддержку четырех наиболее часто используемых операций редактирования - сохранение в файл, удаление и вставка текста и поиск строки - за счет создания подкласса, наследующего класс ScrolledText, с дополнительными кнопками и методами. В виджете Text есть набор готовых привязок клавиш, выполняющих некоторые часто используемые операции редактирования, но они могут оказаться совсем не теми, которые можно было бы ожидать на конкретных платформах. Чаще в текстовом редакторе с графическим интерфейсом предоставляются специальные элементы управления, выполняющие операции редактирования, что более дружественно по отношению к пользователю.

Пример 9.11. PP4E\Gui\Tour\simpleedit.py

за счет наследования добавляет в ScrolledText типичные инструменты редактирования; аналогичного результата можно было бы добиться, применив прием композиции (встраивания); ненадежно! -- надмножество функций имеется в PyEdit;

from tkinter import *

from tkinter.simpledialog import askstring from tkinter.filedialog import asksaveasfilename from quitter import Quitter

from scrolledtext import ScrolledText # наш, не из библиотеки Python

class SimpleEditor(ScrolledText): # доп. ф-ции смотрите в PyEdit

def __init__(self, parent=None, file=None):

frm = Frame(parent) frm.pack(fill=X)

Button(frm, text=’Save’, command=self.onSave).pack(side=LEFT)

Button(frm, text=’Cut’, command=self.onCut).pack(side=LEFT)

Button(frm, text=’Paste’, command=self.onPaste).pack(side=LEFT) Button(frm, text=’Find’, command=self.onFind).pack(side=LEFT) Quitter(frm).pack(side=LEFT)

ScrolledText.__init__(self, parent, file=file)

self.text.config(font=(‘courier’, 9, ‘normal’))

def onSave(self):

filename = asksaveasfilename() if filename:

alltext = self.gettext() # от начала до конца

open(filename, ‘w’).write(alltext) # сохранить текст в файл

def onCut(self):

text = self.text.get(SEL_FIRST, SEL_LAST) # ошибка, если нет выделения self.text.delete(SEL_FIRST, SEL_LAST) # следует обернуть в try self.clipboard_clear() self.clipboard_append(text)

def onPaste(self): # добавляет текст из буфера

try:

text = self.selection_get(selection=’CLIPBOARD’)

self.text.insert(INSERT, text) except TclError:

pass # не вставлять

def onFind(self):

target = askstring(‘SimpleEditor’, ‘Search String?’) if target:

where = self.text.search(target, INSERT, END) # от позиции курсора if where: # вернуть индекс

print(where)

pastit = where + (‘+%dc’ % len(target)) # индекс за целью #self.text.tag_remove(SEL, ‘1.0’, END) # снять выделение self.text.tag_add(SEL, where, pastit) # выделить найденное self.text.mark_set(INSERT, pastit) # установить метку вставки

self.text.see(INSERT) # прокрутить текст

self.text.focus() # выбрать виджет Text

if __name__ == ‘__main__’: if len(sys.argv) > 1:

SimpleEditor(file=sys.argv[1]).mainloop() # имя файла в ком. строке else:

SimpleEditor().mainloop() # или нет: пустой виджет

Этот сценарий также был написан с оглядкой на многократное использование - определяемый в нем класс SimpleEditor можно прикрепить или унаследовать в другой реализации графического интерфейса. Однако, как будет разъяснено в конце раздела, данный пример не настолько надежен, как требуется от библиотечного инструмента общего назначения. Тем не менее в нем реализован действующий текстовый редактор, программный код которого переносим и имеет небольшой объем. Если запустить пример как самостоятельный сценарий, он выведет окно, изображенное на рис. 9.19 (здесь он был запущен в Windows). После каждой успешной операции поиска позиции индексов выводятся в stdout - в этом примере логика снятия предыдущего выделения закомментирована, поэтому вторая операция поиска, как видно на рисунке, выделила вторую строку «def», не сняв предыдущее выделение (раскомментируйте эту строку в сценарии, чтобы операция поиска снимала предыдущее выделение):

C:\...\PP4E\Gui\Tour> python simpleedit.py simpleedit.py

PP4E scrolledtext

14.4

25.4

Операция сохранения выводит имеющийся в библиотеке tkinter стандартный диалог сохранения, который выглядит естественным в каждой из платформ. На рис. 9.20 изображен этот диалог в Windows 7. Операция поиска тоже выводит стандартный диалог для ввода строки поиска (рис. 9.21); в полноценном редакторе можно было бы сохранить эту строку для повторного поиска (что мы и сделаем в главе 11, в реа-

Рис. 9.19. Сценарий simpleedit в действии


Рис. 9.20. Диалог сохранения файла в Windows


Рис. 9.21. Диалог поиска

лизации редактора PyEdit). Для реализации операции завершения повторно был использован компонент кнопки Quit, реализованный нами в главе 8, - этот код гарантирует, что приложение не может быть завершено без подтверждения.

Использование буфера обмена

Помимо операций с виджетом Text в примере 9.11 применяются функции из библиотеки tkinter доступа к буферу обмена для реализации операций удаления и вставки текста. В совокупности эти операции позволяют перемещать текст в файле (вырезать в одном месте и вставлять в другом). Используемый ими буфер обмена служит местом для временного хранения данных - удаленный текст помещается в буфер при вырезании и извлекается оттуда при вставке. Если все происходит в одной программе, нет смысла вырезать строку текста вместо того чтобы просто сохранить ее в обычной переменной Python. Но буфер обмена является значительно более широким понятием.

Буфер обмена, используемый в этом сценарии, является общесистемным хранилищем, совместно используемым всеми программами, выполняющимися в компьютере. Поэтому его можно использовать для передачи данных между приложениями, в числе которых могут быть такие, которые понятия не имеют о библиотеке tkinter. Например, текст, вырезанный или скопированный в Microsoft Word, можно вставлять в окно SimpleEditor, а текст, вырезанный в SimpleEditor, можно вставить в Блокнот (можете попробовать). Используя буфер обмена для реализации операций вырезания и вставки, SimpleEditor автоматически интегрируется с оконной системой в целом. Кроме того, буфер обмена используется не одним только виджетом Text - его можно применять для вырезания и вставки графических объектов в виджете Canvas (обсуждается далее).

Базовый интерфейс к буферу обмена в библиотеке tkinter, использованный в сценарии из примера 9.11, выглядит так:

self.clipboard_clear() # очистить буфер

self.clipboard_append(text) # сохранить строку текста

text = self. selection_get(selection=’ CLIPBOARD’ ) # получить содержимое, если есть

Все эти вызовы доступны в виде методов, наследуемых всеми объектами виджетов tkinter, потому что они разрабатывались как глобальные. Использованное в этом сценарии выделение CLIPBOARD может применяться на всех платформах (существует еще выделение PRIMARY, но обычно им можно пользоваться только в X Window, поэтому мы здесь его не рассматриваем). Обратите внимание, что в случае неудачи метод selection_get возбуждает исключение TclError - данный сценарий ее просто игнорирует и прерывает операцию вставки, но в дальнейшем мы реализуем более удачное решение.

Композиция и наследование

В данном примере класс SimpleEditor использует наследование для расширения ScrolledText дополнительными кнопками и методами обработчиков. Как мы уже видели, допускается также прикреплять (встраивать) объекты графического интерфейса, реализованные как компоненты, подобно ScrolledText. Модель, когда компоненты прикрепляются, обычно называется композицией; существует мнение, что она проще для понимания и реже приводит к конфликту имен, чем расширение наследованием.

Чтобы дать представление о различиях между этими двумя подходами, ниже приводится набросок программного кода, в котором объект ScrolledText прикрепляется к объекту SimpleEditor. Измененные строки выделены в нем полужирным шрифтом (полную реализацию приема композиции можно найти в файле simpleedit2.py, в пакете с примерами). В основном задача состоит в передаче правильных родительских элементов и добавлении атрибута st каждый раз, когда требуется получить доступ к методам виджета Text:

class SimpleEditor(Frame):

def __init__(self, parent=None, file=None):

Frame.__init__(self, parent)

self.pack() frm = Frame(self)

frm.pack(fill=X)

Button(frm, text=’Save’, command=self.onSave).pack(side=LEFT)

...часть программного кода опущена...

Quitter(frm).pack(side=LEFT)

self.st = ScrolledText(self, file=file) # прикрепить, не подкласс self.st.text.config(font=('courier', 9, 'normal'))

def onSave(self):

filename = asksaveasfilename() if filename:

alltext = self.st.gettext() # доступ через атрибут

open(filename, ‘w’).write(alltext)

def onCut(self):

text = self.st.text.get(SEL_FIRST, SEL_LAST)

self.st.text.delete(SEL_FIRST, SEL_LAST)

...часть программного кода опущена...

При таком подходе нет необходимости наследовать класс Frame (виджеты можно было прикреплять непосредственно к родителю), а принадлежность к фреймам позволяет весь этот пакет встраивать и настраивать целиком. Если запустить этот сценарий, он создаст схожее окно. Что лучше - композиция или наследование - судить вам. При правильном использовании классов графического интерфейса они будут работать в любом режиме.

«Простым» он назван обоснованно: PyEdit (забегая вперед)

Наконец, прежде чем вы зарегистрируете в системном реестре редактор SimpleEditor как средство по умолчанию для просмотра текстовых файлов, я должен заметить, что хотя он и обладает всеми основными функциями, но является в некотором роде урезанной версией (в действительности - прототипом) редактора PyEdit, с которым мы познакомимся в главе 11. Вы можете захотеть уже сейчас посмотреть пример с PyEdit, если вы ищете более полную реализацию обработки текста на основе библиотеки tkinter. В нем мы будем также использовать более сложные операции с текстом, такие как откат (undo) и возврат (redo) ввода, поиск с учетом регистра символов, поиск во внешних файлах и многие другие. Виджет Text обладает такой мощью, что трудно продемонстрировать диапазон его возможностей в программном коде меньшего объема, чем тот, который приведен в программе PyEdit.

Я должен также отметить, что реализация SimpleEditor не только ограничена в своих возможностях, но и весьма небрежна - многие граничные случаи проходят без проверки и вызывают неперехватываемые исключения, которые не приводят к завершению графического интерфейса, но и не обрабатываются и не выводят сообщений. Даже о перехваченных ошибках пользователю не сообщается (например, при попытке вставки, когда буфер обмена пуст). Обязательно посмотрите PyEdit, как пример более устойчивой и полной реализации операций, введенных в SimpleEditor.


Юникод и виджет Text

Я уже говорил выше, что текстовое содержимое в виджете Text всегда представлено в виде строки. Однако в Python 3.X существует два типа строк: str - для представления текста Юникода, и bytes - для представления строк байтов. Кроме того, текст Юникода может сохраняться в файлы в различных кодировках. Оказывается, что оба эти фактора могут оказывать влияние на порядок использования виджета Text в Python 3.X.

В двух словах: виджет Text и другие виджеты, так или иначе связанные с текстом, такие как Entry, поддерживают возможность отображения национальных наборов символов для обоих типов строк, str и bytes, но, чтобы обеспечить поддержку самого широкого диапазона символов, при работе с виджетом необходимо использовать декодированный текст Юникода типа str. В этом разделе мы по полочкам разберем механизмы работы с текстом в библиотеке tkinter, чтобы объяснить причину.

Типы строк в виджете Text

Вы могли обратить внимание на это или нет, но во всех предыдущих примерах текстовое содержимое было представлено в виде строк str -либо жестко определенных в тексте сценариев, либо извлекаемых из простых текстовых файлов и сохраняемых в них, что предполагает использование кодировки по умолчанию для текущей платформы. Однако технически виджет Text позволяет вставлять строки обоих типов, и str и bytes:

>>> from tkinter import Text >>> T = Text()

>>> T.insert('1.0', 'spam') # вставить строку типа str

>>> T.insert('end', b'eggs') # вставить строку типа bytes

>>> T.pack() # теперь виджет отображает строку “spameggs”

>>> T.get('1.0', 'end') # извлечь содержимое

‘spameggs\n’

Возможность вставки текста в виде строки типа bytes может пригодиться при просмотре различных видов текста Юникода, особенно когда имя кодировки не известно. Например, текст, полученный из Интернета (скажем, во вложении к электронному письму или полученный по FTP), может быть представлен в любой кодировке - сохранение его в файлах в двоичном режиме и отображение, как строк типа bytes в виджете Text, может показаться обходным решением проблемы использования кодировок в наших сценариях.

Однако, к большому сожалению, виджет Text возвращает свое содержимое только в виде строки str независимо от того, строки какого типа в него вставлялись, str или bytes, - в любом случае мы получаем обратно уже декодированный текст Юникода:

>>> T = Text()

>>> T.insert('1.0', 'Textfileline1\n')

>>> T.insert('end', 'Textfileline2\n') # при вставке str, содержимое - str >>> T.get('1.0', 'end') # вызывать pack() не обязательно,

‘Textfileline1\nTextfileline2\n\n’ # чтобы обратиться к get()

>>> T = Text()

>>> T.insert('1.0', b'Bytesfileline1\r\n') # для bytes содержимое тоже str!

>>> T.insert('end', b'Bytesfileline2\r\n') # а \r отображается, как пробел >>> T.get('1.0', 'end')

‘Bytesfileline1\r\nBytesfileline2\r\n\n’

Фактически мы получаем содержимое виджета в виде строки типа str, даже если мы вставляли строки обоих типов, str и bytes, с единственным символом \n, добавленным в конец, как показано в первом примере в этом разделе. Ниже приводится более полная иллюстрация:

>>> T = Text()

>>> T.insert('1.0', 'Textfileline1\n')

>>> T.insert('end', 'Textfileline2\n') # добавлены две строки str

>>> T.insert('1.0', b'Bytesfileline1\r\n') # \n добавляется для любого типа

>>> T.insert('end', b'Bytesfileline2\r\n') # pack() отображает 4 строки текста

>>> T.get('1.0', 'end')

‘Bytesfileline1\r\nTextfileline1\nTextfileline2\nBytesfileline2\r\n\n’

>>>

>>> print(T.get('1.0', 'end'))

Bytesfileline1

Textfileline1

Textfileline2

Bytesfileline2

Это упрощает обработку текста содержимого после его извлечения: мы можем интерпретировать его, как строку типа str независимо от того, строки каких типов вставлялись. Однако эта же особенность усложняет обработку текстовых данных с точки зрения Юникода: мы не можем сохранить возвращаемую строку str в двоичный файл, потому что операция записи в двоичном режиме ожидает получить строку типа bytes. Нам необходимо будет либо закодировать строку в тип bytes вручную, либо открыть файл в текстовом режиме и уповать на то, что строка str сможет быть закодирована. В любом случае нам необходимо будет знать имя применяемой кодировки; положиться на кодировку по умолчанию для данной платформы; в крайнем случае, сделать какие-либо предположения и надеяться, что они оправдаются; или запросить кодировку у пользователя.

Иными словами, даже при том, что библиотека tkinter позволяет вставлять текст в неизвестной кодировке, как строку типа bytes, и просматривать его, тот факт, что содержимое возвращается в виде строки str, в общем случае означает необходимость знать, как кодировать текст при сохранении, чтобы удовлетворить интерфейс файлов в Python 3.X. Кроме того, так как строки bytes, вставляемые в виджеты Text, также должны быть декодируемыми согласно ограничениям поддержки Юникода в библиотеке Tk, будет лучше, если мы будем декодировать текст в строки str самостоятельно, чтобы обеспечить более широкую поддержку Юникода. Чтобы убедиться в правоте этих слов, нам необходимо совершить короткий экскурс в страну Юникода.

Текст Юникода в строках

Причина всех этих сложностей заключается, конечно же, в том, что в мире Юникода мы не можем больше думать о «тексте», не задавая вопрос «какого он вида». Вообще текст может быть закодирован с использованием самых разных схем кодирования. В языке Python это обстоятельство неразрывно связано со строками str и может иметь отношение к строкам bytes, если они содержат кодированный текст. Строки str Юникода в языке Python - это просто строки, но вам придется принимать во внимание кодировки при записи строк в файлы и чтении их из файлов, а также при передаче их в библиотеки, накладывающие ограничения на кодирование текста.

Мы не будем рассматривать здесь проблемы кодирования Юникода во всех подробностях (подробное описание вы найдете в книге «Изучаем Python», а краткое упоминание о том, какое значение это имеет для файлов, - в главе 4), но коротко рассмотрим некоторые положения, чтобы увидеть - какое отношение они имеют к виджетам Text. Для начала вам следует запомнить, что проблемы с текстом ASCII не возникают лишь потому, что ASCII является подмножеством большинства схем кодирования Юникода. Однако данные, выходящие за диапазон представления 7-битовых символов ASCII, в разных схемах кодирования могут быть представлены разными байтами.

Например, следующие строки байтов должны декодироваться с использованием кодировки Latin-1 - попытка использовать кодировку по умолчанию или явно указанную кодировку, не соответствующую строке байтов, будет терпеть неудачу:

>>> b = b'A\xc4B\xe4C' # эти байты - текст в кодировке latin-1

>>> b

b’A\xc4B\xe4C’

>>> s = b.decode('utf8')

UnicodeDecodeError: ‘utf8’ codec can’t decode bytes in position 1-2: invalid dat...

>>> s = b.decode()

UnicodeDecodeError: ‘utf8’ codec can’t decode bytes in position 1-2: invalid dat...

>>> s = b.decode('latin1')

>>> s

‘ AABaC’

Декодировав байты в строку Юникода, вы сможете «преобразовать» ее обратно в строку байтов с применением различных кодировок. В действительности при этом будет произведено преобразование в альтернативное двоичное представление, которое позднее мы опять сможем декодировать в строку - по существу строка Юникода не относится к «типу Юникод», к нему могут относиться только двоичные данные:

>>> s.encode('latin-1')

b’A\xc4B\xe4C’

>>> s.encode('utf-8')

b’A\xc3\x84B\xc3\xa4C’

>>> s.encode('utf-16')

b’\xff\xfeA\x00\xc4\x00B\x00\xe4\x00C\x00’

>>> s.encode('ascii')

UnicodeEncodeError: ‘ascii’ codec can’t encode character ‘\xc4’ in position 1: o...

Обратите внимание на последнюю операцию в этом примере: кодируемая строка должна быть совместима с используемой схемой кодирования, в противном случае будет возбуждено исключение. В данном случае диапазон ASCII оказался слишком узким для представления символов, полученных в результате декодирования из байтов в кодировке Latin-1. То есть вы можете преобразовать строку в различные (совместимые) двоичные представления, но тем не менее для декодирования этих данных обратно в строку в общем случае вам необходимо знать кодировку, использовавшуюся при кодировании:

>>> s.encode('utf-16').decode('utf-16')

‘ AABaC’

>>> s.encode('latin-1').decode('latin-1')

‘AABaC’

>>> s.encode('latin-1').decode('utf-8')

UnicodeDecodeError: ‘utf8’ codec can’t decode bytes in position 1-2: invalid dat... >>> s.encode('utf-8').decode('latin-1')

UnicodeEncodeError: ‘charmap’ codec can’t encode character ‘\xc3’ in position 2:...

Снова обратите внимание на последнюю операцию. Технически кодирование кодовых пунктов (символов) Юникода в байты UTF-8 и обратное их декодирование с применением кодировки Latin-1 не возбуждает ошибку, но попытка вывести результат приводит к исключению: с точки зрения функции вывода он является зашифрованным мусором. Чтобы обеспечить необходимую точность, необходимо знать, какая кодировка применялась при создании двоичного представления:

>>> s ‘ AABaC’

>>> x = s.encode('utf-8').decode('utf-8') # OK: кодировки совпадают

>>> x ‘ AABaC’

>>> x = s.encode('latin-1').decode('latin-1') # можно использовать любые >>> x # совместимые кодировки

‘ AABaC’

>>> x = s.encode('utf-8').decode('latin-1') # декодирование выполняется,

>>> x # но получается мусор

UnicodeEncodeError: ‘charmap’ codec can’t encode character ‘\xc3’ in position 2:...

>>> len(s), len(x) # уже не та же самая строка

(5, 7)

>>> s.encode('utf-8') # не те же самые кодовые пункты

b’A\xc3\x84B\xc3\xa4C’

>>> x.encode('utf-8')

b’A\xc3\x83\xc2\x84B\xc3\x83\xc2\xa4C’

>>> s.encode('latin-1')

b’A\xc4B\xe4C’

>>> x.encode('latin-1')

b’A\xc3\x84B\xc3\xa4C’

Самое интересное, что после применения несовпадающих кодировок иногда оригинальную строку все еще можно восстановить. Если снова закодировать полученный мусор в кодировку Latin-1 (8-битовые символы) и затем декодировать с применением корректной кодировки, оригинальная строка будет восстановлена (благодаря этому обстоятельству в некоторых ситуациях вы можете все поправить, если при первой попытке данные были декодированы неправильно):

>>> s ‘ AABaC’

>>> s.encode('utf-8').decode('latin-1')

UnicodeEncodeError: ‘charmap’ codec can’t encode character ‘\xc3’ in position 2:... >>> s.encode('utf-8').decode('latin-1').encode('latin-1')

b’A\xc3\x84B\xc3\xa4C’

>>> s.encode('utf-8').decode('latin-1').encode('latin-1').decode('utf-8')

‘AABaC’

>>> s.encode('utf-8').decode('latin-1').encode('latin-1').decode('utf-8') == s

True

С другой стороны, для декодирования данных можно использовать различные кодировки, при условии, что они совместимы с кодировкой данных - при использовании кодировок ASCII, UTF-8 и Latin-1, например, операция декодирования текста ASCII возвращает один и тот же результат:

>>> 'spam'.encode('utf8').decode('latin1')

‘spam’

>>> 'spam'.encode('latin1').decode('ascii')

‘spam’

Важно помнить, что декодированная строка никак не зависит от кодировки, с применением которой она была получена. После декодирования понятие кодировки не может применяться к строке - она является обычной последовательностью символов Юникода («кодовых пунктов»). Таким образом, заботиться о кодировках необходимо только в точках передачи данных между программой и файлами:

>>> s ‘AABaC’

>>> s.encode('utf-16').decode('utf-16') == s.encode('latin-1').decode('latin-1')

True

Текст Юникода в файлах

Те же правила действуют и для текстовых файлов, потому что строки Юникода сохраняются в файлах в виде декодированных байтов. При записи мы можем закодировать строку с применением любой кодировки, совместимой с символами, имеющимися в строке. Однако при чтении необходимо заранее знать кодировку или использовать ту, которая декодирует байты в те же самые символы:

>>> open('ldata', 'w', encoding='latin-1').write(s) # сохранить в latin-1 5

>>> open('udata', 'w', encoding='utf-8').write(s) # сохранить в utf-8 5

>>> open('ldata', 'r', encoding='latin-1').read() # OK: корректное имя ‘ AABaC’

>>> open('udata', 'r', encoding='utf-8').read()

‘ AABaC’

>>> open('ldata', 'r').read() # иначе может вернуть ошибку

‘ AABaC’

>>> open('udata', 'r').read()

UnicodeEncodeError: ‘charmap’ codec can’t encode characters in position 2-3: cha... >>> open('ldata', 'r', encoding='utf-8').read()

UnicodeDecodeError: ‘utf8’ codec can’t decode bytes in position 1-2: invalid dat... >>> open('udata', 'r', encoding='latin-1').read()

UnicodeEncodeError: ‘charmap’ codec can’t encode character ‘\xc3’ in position 2:...

Напротив, при чтении из файлов в двоичном режиме попытка декодировать данные в строку Юникода не производится. Они благополучно будут прочитаны независимо от того, в каком режиме были записаны эти данные - в текстовом, с автоматическим кодированием строк str (как в предыдущем интерактивном сеансе), или в двоичном, в виде строк bytes, закодированных вручную:

>>> open('ldata', 'rb').read()

b’A\xc4B\xe4C’

>>> open('udata', 'rb').read()

b’A\xc3\x84B\xc3\xa4C’

>>> open('sdata', 'wb').write( s.encode('utf-16') ) # вернет число: 12 >>> open('sdata', 'rb').read()

b’\xff\xfeA\x00\xc4\x00B\x00\xe4\x00C\x00’

Юникод и виджет Text

Все вышеизложенное имеет прямое отношение к виджету Text: если файл открывается в двоичном режиме, отпадает необходимость беспокоиться о кодировках - библиотека tkinter будет интерпретировать данные в соответствии с нашими ожиданиями, по крайней мере, для этих двух кодировок:

>>> from tkinter import Text >>> t = Text()

>>> t.insert('1.0', open('ldata', 'rb').read())

>>> t.pack() # строка появится в графическом интерфейсе >>> t.get('1.0', 'end')

‘ AABaC\n’

>>>

>>> t = Text()

>>> t.insert('1.0', open('udata', 'rb').read())

>>> t.pack() # строка появится в графическом интерфейсе

>>> t.get('1.0', 'end')

‘ AABaC\n’

Виджет действует, как если бы мы передали ему строку str, извлеченную в текстовом режиме, но при использовании текстового режима нам необходимо передать интерпретатору Python имя кодировки - операции чтения будут терпеть неудачу при использовании кодировки, несовместимой с данными, хранящимися в файле:

>>> t = Text()

>>> t.insert('1.0', open('ldata', 'r', encoding='latin-1').read())

>>> t.pack()

>>> t.get('1.0', 'end')

‘ AABaC\n’

>>>

>>> t = Text()

>>> t.insert('1.0', open('udata', 'r', encoding='utf-8').read())

>>> t.pack()

>>> t.get('1.0', 'end')

‘ AABaC\n’

В любом случае содержимое, извлеченное из файла, всегда будет представлено строкой Юникода str, поэтому двоичный режим оказывает влияние только на операцию чтения. Но нам тем не менее необходимо знать кодировку, сохраняем мы данные непосредственно в текстовом режиме или выполняем запись в двоичном режиме после кодирования вручную:

>>> c = t.get('1.0', 'end')

>>> c # содержимое - строка str

‘ AABaC\n’

>>> open('cdata', 'wb').write(c) # binary mode needs bytes TypeError: must be bytes or buffer, not str

>>> open('cdata', 'w', encoding='latin-1').write(c) # каждая операция записи >>> open('cdata', 'rb').read() # возвращает число 6

b’A\xc4B\xe4C\r\n’

>>> open('cdata', 'w', encoding='utf-8').write(c) # другие байты в файле >>> open('cdata', 'rb').read()

b’A\xc3\x84B\xc3\xa4C\r\n’

>>> open('cdata', 'w', encoding='utf-16').write(c)

>>> open('cdata', 'rb').read() b’\xff\xfeA\x00\xc4\x00B\x00\xe4\x00C\x00\r\x00\n\x00’

>>> open('cdata', 'wb').write( c.encode('latin-1') ) # закодировать вручную >>> open('cdata', 'rb').read() # то же, но с \r в Win

b’A\xc4B\xe4C\n’

>>> open('cdata', 'w', encoding='ascii').write(c) # должна быть совместимой UnicodeEncodeError: ‘ascii’ codec can’t encode character ‘\xc4’ in position 1: o

Обратите внимание на последнюю операцию в этом примере: операция кодирования вручную и записи в файл может терпеть неудачу, если данные не смогут быть закодированы с применением указанной кодировки. Когда такое случается, программа, вероятно, должна восстановиться после исключения и попробовать альтернативную кодировку -это особенно справедливо для платформ, где в качестве кодировки по умолчанию используется ASCII.

Проблемы обработки текста, представленного байтами

Правила, описанные в предыдущем разделе, могут показаться слишком сложными, но в действительности все они сводятся к следующему:

• Кодировку, используемую в операциях чтения/записи в текстовом режиме и при кодировании/декодировании вручную, необходимо знать только при работе со строками, кодировка символов в которых не совпадает с кодировкой, используемой на текущей платформе по умолчанию.

• Для записи в новые файлы можно использовать практически любую кодировку, при условии, что она может применяться к символам в строках, но при чтении необходимо указывать кодировку, совместимую с имеющимися двоичными данными.

• При чтении текста в двоичном режиме для отображения в виджете знать кодировку необязательно, но виджет Text всегда возвращает содержимое в виде строки str, поэтому, чтобы закодировать его перед сохранением в файл, необходимо знать кодировку.

Так почему бы всегда не загружать текст для отображения в виджете Text в двоичном режиме? Хотя чтение из файлов в двоичном режиме и кажется решением проблем кодирования, тем не менее передача текста библиотеке tkinter в виде строк bytes вместо str в действительности просто перекладывает проблему кодирования на библиотеку Tk, которая налагает собственные ограничения.

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

• Оно снимает бремя выбора кодировки с нашего сценария и перекладывает его на библиотеку Tk. Библиотеке все равно придется решать, как отображать полученные байты, и может так случиться, что она не будет поддерживать какие-то необходимые кодировки.

• Оно позволяет открывать и просматривать файлы, которые по своей природе не являются текстовыми, сводя к нулю некоторые из преимуществ, предлагаемых проверками, выполняемыми при декодировании текста.

Первый пункт является, пожалуй, наиболее важным. Поэкспериментировав в Windows, я выяснил, что библиотека Tk корректно обрабатывает строки bytes в кодировках ASCII, UTF-8 и Latin-1, но не справилась с кодировкой UTF-16 и другими, такими как CP500. Однако все эти строки отображаются корректно, когда перед передачей библиотеке Tk двоичные данные декодируются в строку str. В программах, предназначенных для использования по всему миру, такая расширенная поддержка становится жизненно значимой. Если у вас есть возможность определить кодировку или запросить ее у пользователя, то для отображения и сохранения текста в файлы лучше использовать строки str.

Независимо от того, передаете вы текстовые данные в виде строк типа str или bytes, графические интерфейсы на основе библиотеки tkinter подчиняются ограничениям, накладываемым библиотекой Tk и языком программирования Tcl, а также всеми приемами использования библиотеки tkinter в языке Python, которая служит интерфейсом к библиотеке Tk. Например:

• Tcl, внутренний язык реализации библиотеки Tk, хранит строки в кодировке UTF-8 и требует, чтобы строки передавались через его прикладной интерфейс на языке C именно в этом формате.

• Tcl пытается преобразовать строки байтов, используя кодировку UTF-8, и в целом поддерживает преобразования с использованием кодировок, определяемых региональными настройками системы и кодировки Latin-1, как последнего средства.

• Реализация библиотеки tkinter на языке Python передает строки bytes языку Tcl без промежуточных преобразований, но при использовании строк Юникода типа str копируются объекты Юникода языка Tcl.

• Библиотека Tk унаследовала все ограничения языка Tcl, связанные с Юникодом, и добавляет свои ограничения, связанные с выбором шрифта для отображения.

Иными словами, графические интерфейсы, отображающие текст с использованием средств библиотеки tkinter, находятся во власти нескольких слоев программного обеспечения, расположенных выше и ниже программного кода на языке Python. Но, как бы то ни было, Юникод достаточно полно поддерживается виджетом Text из библиотеки Tk при использовании строк типа str, хотя это не относится к строкам bytes. Как вы уже наверняка заметили, обсуждение этой проблемы быстро начинает обрастать техническими деталями, поэтому мы не будем исследовать ее дальше в этой книге - дополнительные сведения о tkinter, Tk и Tcl и интерфейсах между ними ищите в Сети или в других источниках информации.

Другие проблемы двоичного режима

Даже в ситуациях, когда достаточно использовать файлы, открытые в двоичном режиме, обойти проблемы с кодировками оказывается сложнее, чем можно было бы подумать. При записи в двоичном режиме нам всегда придется проявлять осторожность, чтобы прочитанные данные позднее были корректно записаны в файл, - при чтении в двоичном режиме строки в Windows будут завершаться последовательностью символов \r\n и было бы нежелательно, чтобы при записи в текстовом режиме они превращались в последовательности \r\r\n. Кроме того, между строками типа str и bytes в tkinter существует еще одно отличие. Строки str, прочитанные из файла в текстовом режиме, выводятся в графическом интерфейсе в ожидаемом виде, и в Windows символы конца строки отображаются должным образом:

C:\...\PP4E\Gui\Tour> python >>> from tkinter import *

>>> T = Text() # str в текстовом режиме

>>> T.insert('1.0', open('jack.txt').read()) # кодировка по умолчанию >>> T.pack() # нормально отображается в GUI

>>> T.get('1.0', 'end')[:75]

‘000) All work and no play makes Jack a dull boy.\n001) All work and no pla’

Однако если передать в графический интерфейс строку bytes, прочитанную из файла в двоичном режиме, в Windows она будет выглядеть на экране довольно странно - в конце каждой строки текста появится лишний пробел, соответствующий символу \r, который не отсекается при чтении из файлов в двоичном режиме:

C:\...\PP4E\Gui\Tour> python >>> from tkinter import *

>>> T = Text() # bytes в двоичном режиме

>>> T.insert('1.0', open('jack.txt', 'rb').read()) # без декодирования >>> T.pack() # появились пробелы в конце

>>> T.get('1.0', 'end')[:75] # строк!

‘000) All work and no play makes Jack a dull boy.\r\n001) All work and no pl’

При использовании строк bytes, чтобы обеспечить отображение произвольного текста в ожидаемом виде, нам дополнительно придется вручную удалять символы \r в концах строк. Вследствие этого предполагается, что комбинация \r\n не имеет какого-то специального значения в схеме кодирования текста, хотя, если эта последовательность в данных не будет означать конец строки, такие данные с большой долей вероятности будут вызывать другие проблемы при отображении. В следующем фрагменте реализовано удаление лишних пробелов в концах строк - входной файл открывается в двоичном режиме, и при чтении недекодированных байтов вручную удаляются символы \r:

C:\...\PP4E\Gui\Tour> python

>>> from tkinter import * # используется тип bytes, удаляются символы \r

>>> T = Text()

>>> data = open('jack.txt', 'rb').read()

>>> data = data.replace(b'\r\n', b'\n')

>>> T.insert('1.0', data)

>>> T.pack()

>>> T.get('1.0', 'end')[:75]

‘000) All work and no play makes Jack a dull boy.\n001) All work and no pla’

Чтобы позднее сохранить это содержимое, можно либо добавить символы \r, при выполнении в Windows, вручную выполнить кодирование в тип bytes и сохранить данные в двоичном режиме; либо открыть файл в текстовом режиме, чтобы объект файла сам добавил символы \r, если это необходимо, выполнил кодирование и записал содержимое строки str. Второй путь, вероятно, более простой, так как он не требует беспокоиться о различиях между платформами.

Однако в любом случае мы вновь оказываемся лицом к лицу с проблемой кодирования - мы можем либо положиться на кодировку по умолчанию для текущей платформы, либо получить имя кодировки из пользовательского интерфейса. В следующем фрагменте, например, объект текстового файла сам преобразует символы конца строки и применяет кодировку по умолчанию для текущей платформы. Если бы было необходимо обеспечить поддержку произвольного текста Юникода или работоспособность сценария на платформах, где кодировка по умолчанию не соответствует отображаемым символам, мы могли бы передавать имя кодировки явно (операция извлечения среза, используемая здесь, имеет тот же эффект, что и применение спецификатора позиции «end-1c» в библиотеке Tk):

...продолжение предыдущего сеанса...

>>> content = T.get('1.0', 'end')[:-1] # отбросит \n в конце

>>> open('copyjack.txt', 'w').write(content) # кодировка по умолчанию 12500 # текстовый режи* в*Win добавит \n

>>> ^Z

C:\...\PP4E\Gui\Tour> fc jack.txt copyjack.txt

Comparing files jack.txt and COPYJACK.TXT FC: no differences encountered

Поддержка Юникода в PyEdit (забегая вперед)

Пример использования поддержки Юникода в виджете Text мы увидим в главе 11, когда будем разбирать реализацию приложения PyEdit. В действительности под поддержкой Юникода подразумевается лишь поддержка различных кодировок при работе с файлами, открытыми в текстовом режиме, - как только текст окажется в памяти, его обработка всегда выполняется в терминах типа str, потому что библиотека tkinter возвращает содержимое именно в таком виде. Чтобы обеспечить поддержку Юникода, редактор PyEdit открывает файлы для чтения и записи в текстовом режиме, явно указывая кодировку, если это возможно, а двоичный режим использует только как последнее средство. Благодаря этому отпадает необходимость полагаться на ограниченную поддержку Юникода в библиотеке Tk, предусмотренную для отображения строк байтов.

Для этого редактор PyEdit реализует возможность получения имен кодировок из самых разных источников и позволяет пользователям указывать, какие из них желательно использовать. Кодировка может быть получена в результате диалога с пользователем, из параметров настройки в конфигурационных файлах, из настроек системы по умолчанию, из значения, сохраненного ранее в файле, и даже из внутренних значений в программе (полученных в результате анализа заголовков сообщений электронной почты, например). Все эти источники опробуются друг за другом, пока не встретится первая подходящая кодировка, при этом в некоторых ситуациях может потребоваться ограничиться единственным источником.

Эту реализацию вы увидите а главе 14. Честно признаться, версия редактора PyEdit в этом издании изначально предусматривала чтение и запись в файлы в текстовом режиме с использованием кодировки по умолчанию. Я не предполагал заострять внимание на поддержке Юникода в PyEdit, пока не столкнулся с необходимостью поддержки самых разнообразных кодировок, существующих в Интернете, при подготовке примера PyMailGUI. Если вы считаете, что строки стали намного сложнее, чем могли бы быть, то это скорее всего, потому, что спектр ваших представлений остается слишком узким.


Более сложные операции с текстом и тегами

Но достаточно разговоров о сложностях Юникода - вернемся к программированию графических интерфейсов. Помимо указания позиции, теги виджета Text могут также использоваться для форматирования и выполнения операций как над всеми символами подстроки, так и над всеми подстроками, помещенными в тег. Эта особенность составляет значительную часть мощи, предоставляемой виджетом Text:

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

• Теги позволяют выполнять привязку событий, что дает возможность реализовать, например, гиперссылки в виджете Text: щелчок на тексте вызывает обработчик события его тега. Привязка событий к тегам осуществляется с помощью метода tag_bind, во многом подобного уже знакомому общему методу bind виджетов.

С помощью тегов можно отображать текст с различными настройками внутри одного и того же виджета Text; например, ко всему виджету Text можно применить один шрифт, а к тексту в тегах - другие шрифты. Кроме того, виджет Text позволяет встраивать другие виджеты в заданное индексом место (они интерпретируются, как отдельный символ), а также графические изображения.

Пример 9.12 иллюстрирует основы применения сразу всех этих дополнительных возможностей и воспроизводит интерфейс, изображенный на рис. 9.22. Этот сценарий применяет форматирование и выполняет привязку событий к трем подстрокам, помеченным тегами, выводит текст с помощью двух разных комбинаций шрифтов и цветов, а также встраивает графическое изображение и кнопку. Двойной щелчок мышью на любой из подстрок, заключенных в теги (или на встроенной кнопке), генерирует событие, которое выводит в поток stdout сообщение «Got tag event».

Пример 9.12. PP4E\Gui\Towr\texttags.py

"демонстрация дополнительных возможностей тегов и виджета Text”

from tkinter import * root = Tk()

def hello(event): print(‘Got tag event’)

# создать и настроить виджет Text text = Text()

text.config(font=(‘courier’, 15, ‘normal’)) # общий шрифт

text.config(width=20, height=12) text.pack(expand=YES, fill=BOTH)

text.insert(END, ‘This is\n\nthe meaning\n\nof life.\n\n’) # вставить 6 строк

# встроить окна и изображения

btn = Button(text, text=’Spam’, command=lambda: hello(0)) # встроить кнопку btn.pack()

text.window_create(END, window=btn) # встроить изображение

text.insert(END, ‘\n\n’)

img = PhotoImage(file=’../gifs/PythonPowered.gif’) text.image_create(END, image=img)

# применить теги к подстрокам

text.tag_add(‘demo’, ‘1.5’, ‘1.7’) # добавить ‘is’ в тег

text.tag_add(‘demo’, ‘3.0’, ‘3.3’) # добавить ‘the’ в тег

text.tag_add(‘demo’, ‘5.3’, ‘5.7’) # добавить ‘life’ в тег

text.tag_config(‘demo’, background=’purple’) # изменить цвета тега

text.tag_config(‘demo’, foreground=’white’) # называются не bg/fg

text.tag_config(‘demo’, font=(‘times’, 16, ‘underline’)) # изменить шрифт тега text.tag_bind(‘demo’, ‘’, hello) # привязать события

root.mainloop()

Рис. 9.22. Теги виджета Text в действии

Такие средства встраивания и работы с тегами тегов можно в конечном итоге использовать для отображения веб-страницы. А стандартный модуль html.parser анализа разметки HTML может помочь с автоматизацией построения графического интерфейса веб-страницы. Как можно догадаться, виджет Text предоставляет больше возможностей программирования графических интерфейсов, чем позволяет описать объем книги. За подробностями о возможностях, предоставляемых тегами и виджетом Text, обращайтесь к другим справочникам по библиотекам Tk и tkinter. А сейчас начнутся занятия в художественной школе.


Виджет Canvas

Что касается графики, то виджет Canvas (холст) из tkinter является самым свободным по форме инструментом в этой библиотеке. Он позволяет рисовать фигуры, динамически перемещать объекты и располагать другие виджеты. Холст основан на структурированной модели графического объекта: все, что изображается на холсте, может обрабатываться как объект. Можно опуститься до уровня обработки пикселей, а можно оперировать более крупными объектами, такими как фигуры, графические изображения и встроенные виджеты. Все это делает холст достаточно мощным инструментом, как для использования в простых графических редакторах, так и в полноценных программах визуализации и воспроизведения анимации.


Базовые операции с виджетом Canvas

Холсты повсеместно используются во многих нетривиальных графических интерфейсах, и далее в этой книге можно будет увидеть более крупные примеры использования холстов, в программах PyDraw, PyPhoto, PyView, PyClock и PyTree. А сейчас сразу займемся примером, в котором демонстрируются основы его применения. В примере 9.13 используется большинство основных методов создания изображений на холсте.

Пример 9.13. PP4E\Gui\Tour\canvas1.py

"демонстрация основных возможностей холста”

from tkinter import *

canvas = Canvas(width=525, height=300, bg=’white’) # 0,0 - верхний левый угол

canvas.pack(expand=YES, fill=BOTH) # рост вниз и вправо

canvas.create_line(100, 100, 200, 200) # fromX, fromY, toX, toY

canvas.create_line(100, 200, 200, 300) # рисование фигур

for i in range(1, 20, 2):

canvas.create_line(0, i, 50, i)

canvas.create_oval(10, 10, 200, 200, width=2, fill=’blue’) canvas.create_arc(200, 200, 300, 100)

canvas.create_rectangle(200, 200, 300, 300, width=5, fill=’red’) canvas.create_line(0, 300, 150, 150, width=10, fill=’green’)

photo=PhotoImage(file=’../gifs/ora-lp4e.gif’)

canvas.create_image(325, 25, image=photo, anchor=NW) # встроить изображение

widget = Label(canvas, text=’Spam’, fg=’white’, bg=’black’) widget.pack()

canvas.create_window(100, 100, window=widget) # встроить виджет

canvas.create_text(100, 280, text=’Ham’) # нарисовать текст

mainloop()

Запущенный сценарий создаст окно, изображенное на рис. 9.23. Ранее уже было показано, как поместить на холст графическое изображение и установить соответствующие ему размеры холста (раздел «Изображения» в конце главы 8). Этот сценарий изображает также фигуры, текст и даже встроенный виджет Label. Его окно пока ограничивается только отображением - чуть ниже будет показано, как добавить обработчики событий, дающие пользователю возможность взаимодействовать с отображаемыми элементами.

Рис. 9.23. Окно сценария canvas1 с изображениями объектов


Программирование виджета Canvas

Холсты просты в использовании, при этом они имеют свою систему координат, определяют уникальные методы рисования и именуют объекты с помощью идентификаторов или тегов. Данный раздел знакомит с этими базовыми понятиями холста.

Координаты

Все элементы, отображаемые на холсте, представляют собой отдельные объекты, которые в действительности не являются виджетами. Если внимательно изучить сценарий canvas1, можно заметить, что холсты создаются и прикрепляются к родительскому контейнеру, как и любые другие виджеты tkinter. Однако к предметам, изображаемым на холсте, это не относится: фигуры, графические изображения и другие элементы располагаются и перемещаются по холсту с помощью координат, идентификаторов и тегов. При этом координаты являются наиболее фундаментальной составляющей модели холста.

Холст определяет собственную систему координат (X,Y) для своей области отображения; X обозначает горизонтальную ось, Y - вертикальную. По умолчанию координаты измеряются в пикселях (точках); левый верхний угол холста имеет координаты (0,0), а координаты X и Y возрастают вправо и вниз соответственно. Чтобы нарисовать или разместить объект на холсте, необходимо указать одну или более пар координат (X,Y), определяющих абсолютные местоположения на холсте. Такой способ отличается от ограничений, использовавшихся до сих пор для прикрепления виджетов, но он позволяет управлять графической структурой с очень большой точностью и поддерживает более свободные по форме технологии, например, анимацию.44

Создание объектов

Холст позволяет рисовать и отображать простые фигуры, такие как линии, овалы, прямоугольники, дуги и многоугольники. Кроме того, имеется возможность встраивать текст, графические изображения и другие виджеты tkinter, такие как метки и кнопки. В сценарии canvas1 продемонстрированы все основные методы конструирования графических объектов - каждому из них передается один или более наборов координат (X,Y), определяющих координаты нового объекта, начальные и конечные точки или противоположные углы рамки, содержащей фигуру:

id = canvas.create_line(fromX, fromY, toX, toY) # начало, конец отрезка прямой

id = canvas.create_oval(fromX, fromY, toX, toY) # противоположные углы овала

id = canvas.create_arc( fromX, fromY, toX, toY) # противоположные углы дуги

id = canvas.create_rectangle(fromX, fromY, toX, toY) # противоположные углы

# прямоугольника

В других методах рисования указывается только одна пара координат (X,Y), определяющая координаты левого верхнего угла объекта:

id = canvas.create_image(250, 0, image=photo, anchor=NW) # встроить изображ. id = canvas.create_window(100, 100, window=widget) # встроить виджет

id = canvas.create_text(100, 280, text=’Ham’) # нарисовать текст

Холст также предоставляет метод create_polygon, принимающий произвольное множество аргументов координат, определяющих точки, соединенные линиями. Его удобно использовать для рисования произвольных фигур, образованных отрезками прямых линий.

Помимо координат большинство методов рисования позволяет определять обычные параметры настройки, такие как ширина границы (width), цвет заливки (fill), цвет границы (outline) и так далее. У некоторых типов объектов есть собственные уникальные параметры настройки; например, для линий можно указать форму необязательной стрелки, а текст, виджеты и изображения можно привязывать по направлениям сторон света (что похоже на параметр anchor менеджера компоновки, но в действительности определяет точку объекта, помещаемую в координаты (X,Y), указанные в вызове метода create; NW, например, помещает в координаты (X,Y) левый верхний угол объекта).

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

Идентификаторы объектов и операции

Сценарий canvas1 не использует тот факт, что у каждого помещаемого на холст объекта есть идентификатор. Его возвращает метод create_, который рисует или встраивает объект (в примерах предыдущего раздела он был представлен переменной id). Этот идентификатор можно впоследствии передавать другим методам, чтобы переместить объект в новые координаты, установить параметры его настройки, удалить с холста, поднять или опустить относительно других перекрывающихся объектов и так далее

Например, метод move холста может принимать идентификатор объекта и смещения (не координаты) X и Y, и перемещать объект согласно заданному смещению:

canvas.move(objectIdOrTag, offsetX, offsetY) # переместить объект(ы)

Если при этом объект смещается за пределы холста, он просто обрезается (не показывается). К объектам можно также применять другие часто используемые операции:

canvas.delete(objectIdOrTag) # удалить объект(ы) с холста

canvas.tkraise(objectIdOrTag) # поднять объект(ы) вверх

canvas.lower(objectIdOrTag) # опустить объект(ы) вниз

canvas.itemconfig(objectIdOrTag, fill=’red’) # залить объект(ы) красным цветом

Обратите внимание на имя tkraise - слово raise является в языке Python зарезервированным. Заметьте также, что для настройки объектов, изображенных на холсте, после их создания используется метод itemconfig; метод config применяется для изменения параметров самого холста. Однако главное, что нужно отметить, - это возможность обработать сразу весь графический объект, поскольку библиотека tkinter оперирует структурированными объектами - не нужно поднимать и перерисовывать каждый пиксель вручную, чтобы осуществить перемещение или подъем объекта.

Теги объектов в виджете Canvas

Однако холсты способны предложить еще более мощные возможности, чем мы видели до сих пор. Помимо идентификаторов объектов существуют теги. Тег - создаваемое программистом имя, с которым можно связать ряд отображаемых объектов и применить ту или иную операцию сразу ко всей группе. Пометка объектов тегами в виджете Canvas по крайней мере по духу сходна с пометкой тегами подстрок в виджете Text, рассматривавшемся в предшествующем разделе. В общих чертах методы холста принимают идентификатор отдельного объекта или имя тега.

Например, можно переместить целую группу отображаемых объектов, привязав их к одному и тому же тегу и передав его методу move холста. Именно по этой причине метод move принимает смещения, а не координаты - получая тег, он перемещает каждый ассоциированный с этим тегом объект на указанные смещения (X,Y); если бы метод принимал абсолютные координаты, все связанные с тегом объекты могли бы оказаться в одном и том же месте друг над другом.

Чтобы связать объект с тегом, нужно указать имя тега в параметре tag метода, отображающего объект, или вызвать метод холста addtag_ withtag(tag, objectIdOrTag) (или родственный ему). Например:

canvas.create_oval(x1, y1, x2, y2, fill=’red’, tag=’bubbles’) canvas.create_oval(x3, y3, x4, y4, fill=’red’, tag=’bubbles’) objectId = canvas.create_oval(x5, y5, x6, y6, fill=’red’) canvas.addtag_withtag(‘bubbles’, objectId) canvas.move(‘bubbles’, diffx, diffy)

Эти инструкции создают три овала и перемещают их одновременно, благодаря связыванию с одним и тем же именем тега. У многих объектов может быть один и тот же тег, многие теги могут ссылаться на один и тот же объект, и каждый тег можно настраивать и обрабатывать независимо.

Как и виджет Text, виджет Canvas имеет теги с предопределенными именами: тег all ссылается на все объекты, имеющиеся на холсте, а current ссылается на тот объект, который находится под указателем мыши. Можно не только запрашивать идентификатор объекта под указателем мыши, но и осуществлять поиск объектов с помощью методов find_ холста: например, метод canvas.find_closest(X,Y) возвращает кортеж, первый элемент которого содержит идентификатор объекта, находящегося ближе всего к точке с указанными координатами, - это удобно, когда уже есть координаты, полученные в обработчике события, сгенерированного щелчком мыши.

К представлению о тегах холста мы вернемся снова в более позднем примере из этой главы (если вам нужны подробности прямо сейчас, можете посмотреть сценарии воспроизведения анимации ближе к концу). Холсты поддерживают другие операции и параметры, для рассказа о которых здесь недостаточно места (например, метод холста postscript позволяет сохранить холст в файле в формате PostScript). Дополнительные сведения можно найти в примерах, имеющихся далее в книге, таких как PyDraw, а полный список параметров объекта холста можно найти в справочниках по библиотеке Tk или tkinter.


Прокрутка холстов

Однако одна из операций над холстами настолько часто используется в практике, что действительно заслуживает внимания. Как демонстрирует пример 9.14, полосы прокрутки можно перекрестно связывать с холстами, используя те же протоколы, которые ранее использовались для добавления их к виджетам Listbox и Text, но с некоторыми особыми требованиями.

Пример 9.14. PP4E\Gui\Tour\scrolledcanvas.py

"простой компонент холста с вертикальной прокруткой”

from tkinter import *

class ScrolledCanvas(Frame):

def __init__(self, parent=None, color=’brown’):

Frame.__init__(self, parent)

self.pack(expand=YES, fill=BOTH) # сделать растягиваемым

canv = Canvas(self, bg=color, relief=SUNKEN)

canv.config(width=300, height=200) # размер видимой области

canv.config(scrollregion=(0, 0, 300, 1000)) # углы холста

canv.config(highlightthickness=0) # без рамки

sbar = Scrollbar(self)

sbar.config(command=canv.yview) # связать sbar и canv canv.config(yscrollcommand=sbar.set) # сдвиг одного = сдвиг другого sbar.pack(side=RIGHT, fill=Y) # первым добавлен - посл. обрезан

canv.pack(side=LEFT, expand=YES, fill=BOTH) # canv обрезается первым

self.fillContent(canv)

canv.bind(‘’, self.onDoubleClick) # установить обр. события self.canvas = canv

def fillContent(self, canv): # переопределить при

for i in range(10): # наследовании

canv.create_text(150, 50+(i*100), text=’spam’+str(i),fill=’beige’)

def onDoubleClick(self, event): # переопределить при

print(event.x, event.y) # наследовании

print(self.canvas.canvasx(event.x), self.canvas.canvasy(event.y))

if __name__ == ‘__main__’: ScrolledCanvas().mainloop()

Этот сценарий создает окно, изображенное на рис. 9.24. Оно аналогично предыдущим примерам реализации прокрутки, но в модели прокрутки холстов есть две особенности:

Размеры области прокрутки и видимой области

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

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

Кроме того, нужно устанавливать соответствие между координатами видимой области и координатами всего холста, если холст больше, чем видимая его область. Если холст поддерживает возможность прокрутки, он почти всегда больше видимой его области, и в этих случаях часто требуется вычислять это соответствие. В некоторых случаях необходимость в таком вычислении отсутствует, потому что виджеты, встраиваемые в холст, самостоятельно откликаются на действия пользователей (например, кнопки в примере PyPhoto, в главе 11). Однако если пользователь взаимодействует непосредственно с холстом (например, в графическом редакторе), преобразование из системы координат видимой области в систему координат всего холста становится насущной необходимостью.

Рис. 9.24. Сценарий scrolledcanvas в действии

Размеры определяются как параметры настройки. Размер видимой области определяется с помощью параметров холста width и height. Чтобы определить общий размер холста, в параметре scrollregion следует передать кортеж с четырьмя координатами верхнего левого и нижнего правого углов холста. Если размер видимой области не указан, используется размер по умолчанию. Если не задан параметр scrollregion, он принимается по умолчанию равным размеру видимой области. При этом полоса прокрутки становится бесполезной, так как видимая область в этом случае вмещает весь холст целиком.

Пересчет координат осуществляется несколько более сложным образом. Если прокручиваемая видимая область холста оказывается меньше, чем холст в целом, то координаты (X,Y), возвращаемые в объектах событий, представляют собой координаты в видимой области, а не в холсте в целом. Обычно требуется перевести координаты события в координаты холста, для чего они передаются методам canvasx и can-vasy прежде чем их можно будет использовать для обработки объектов.

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

C:\...\PP4E\Gui\Tour> python scrolledcanvas.py

2 0 координаты x,y события, если холст прокручен доверху

2.0 0.0 координаты х,у холста - те же, если нет пикселей границы

150 106

150.0 106.0

299 197

299.0 197.0

3 2 координаты x,y события, если холст прокручен донизу

3.0 802.0 координаты x,y холста - координата y отличается

296 192

296.0 992.0

152 97 при прокрутке в среднюю часть холста

152.0 599.0

16 187

16.0 689.0

Здесь отображаемая координата X видимой области холста всегда совпадает с координатой X всего холста, поскольку видимая область и холст имеют одинаковую ширину 300 пикселей (из-за автоматической установки границ могло бы появиться расхождение в два пикселя, если бы не значение highlightthickness, установленное в сценарии). Обратите внимание, что после щелчка на вертикальной полосе прокрутки отображаемая координата Y видимой области становится отличной от координаты Y холста. Без преобразования координат значение координаты Y события неверно указывало бы на место, находящееся на холсте значительно выше.

В большинстве примеров с холстами в этой книге выполнять такое преобразование не требуется - координаты (0,0) всегда соответствует левому верхнему углу холста, в котором происходит щелчок мышью, но лишь потому, что эти холсты не поддерживают прокрутку. Пример холста с двумя полосами прокрутки, горизонтальной и вертикальной, вы найдете в следующем разделе. Программа PyTree, что приводится далее в этой книге, также демонстрирует похожий холст, но при этом она динамически изменяет размеры области прокрутки при отображении новых деревьев.

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


Холсты с поддержкой прокрутки и миниатюр изображений

В конце главы 8 мы рассматривали коллекцию сценариев, отображающих миниатюры всех изображений, хранящихся в каталоге. Там отмечалось, что поддержка прокрутки является важным требованием для больших коллекций изображений. Теперь, когда мы знаем, как объединять холсты и полосы прокрутки, можно, наконец, задействовать их для реализации этого крайне необходимого расширения и закончить реализацию инструмента просмотра изображений, начатую в главе 8 (ну, или почти закончить).

Пример 9.15 представляет собой измененную версию последнего примера из предыдущей главы, которая отображает миниатюры в холсте с прокруткой. Описание особенностей функционирования сценария, а также модуля ImageTk (необходим для создания миниатюр и отображения изображений в формате JPEG), импортируемого из сторонней библиотеки Python Imaging Library (PIL), смотрите в предыдущей главе.

Фактически, чтобы полностью понять реализацию примера 9.15, необходимо также вспомнить пример 8.45, поскольку здесь мы повторно используем инструменты создания миниатюр и просмотра изображений из этого модуля. Здесь мы просто добавляем холст, позиционируем кнопки фиксированного размера по абсолютным координатам в холсте и вычисляем размеры прокручиваемой области, используя понятия, обозначенные в предыдущем разделе. Наличие горизонтальной и вертикальной полос прокрутки обеспечивает возможность свободного перемещения по всему холсту с кнопками независимо от их количества.

Пример 9.15. PP4E\Gui\PIL\viewer_thumbs_scrolled.py

расширенная версия сценария просмотра изображений: отображает миниатюры на кнопках фиксированного размера, чтобы обеспечить равномерное их размещение, и добавляет возможность прокрутки при просмотре больших коллекций изображений, отображая миниатюры в виджете Canvas с полосами прокрутки; требует наличия библиотеки PIL для отображения изображений в таких форматах, как JPEG, и повторно использует инструменты создания миниатюр и просмотра единственного изображения из сценария viewer_thumbs.py; предостережение/что сделать: можно также реализовать возможность прокрутки при отображении единственного изображения, если его размеры оказываются больше размеров экрана, которое сейчас обрезается в Windows; более полная версия представлена в виде приложения PyPhoto в главе 11;

import sys, math

from tkinter import *

from PIL.ImageTk import PhotoImage

from viewer_thumbs import makeThumbs, ViewOne

def viewer(imgdir, kind=Toplevel, numcols=None, height=300, width=300):

использует кнопки фиксированного размера и холст с возможностью прокрутки; определяет размер области прокрутки (всего холста) и располагает миниатюры по абсолютным координатам x,y холста; предупреждение: предполагается, что все миниатюры имеют одинаковые размеры

win = kind()

win.title(‘Simple viewer: ‘ + imgdir)

quit = Button(win, text=’Quit’, command=win.quit, bg=’beige’) quit.pack(side=BOTTOM, fill=X)

canvas = Canvas(win, borderwidth=0) vbar = Scrollbar(win)

hbar = Scrollbar(win, orient=’horizontal’)

vbar.pack(side=RIGHT, fill=Y) # прикрепить холст после полос прокрутки hbar.pack(side=BOTTOM, fill=X) # чтобы он обрезался первым canvas.pack(side=TOP, fill=BOTH, expand=YES)

vbar.config(command=canvas.yview) # обработчики событий

hbar.config(command=canvas.xview) # перемещения полос прокрутки

canvas.config(yscrollcommand=vbar.set) # обработчики событий

canvas.config(xscrollcommand=hbar.set) # прокрутки холста

canvas.config(height=height, width=width) # начальные размеры видимой

# области, изменяемой при

# изменении размеров окна

thumbs = makeThumbs(imgdir) # [(imgfile, imgobj)]

numthumbs = len(thumbs) if not numcols:

numcols = int(math.ceil(math.sqrt(numthumbs))) # фиксиров. или N x N numrows = int(math.ceil(numthumbs / numcols)) # истинное деление в 3.x

linksize = max(thumbs[0][1].size) # (ширина, высота)

fullsize = (0, 0, # верхний левый угол X,Y

(linksize * numcols), (linksize * numrows) ) # нижний правый угол X,Y canvas.config(scrollregion=fullsize) # размер области

# прокрутки

rowpos = 0 savephotos = [] while thumbs:

thumbsrow, thumbs = thumbs[:numcols], thumbs[numcols:] colpos = 0

for (imgfile, imgobj) in thumbsrow: photo = PhotoImage(imgobj) link = Button(canvas, image=photo)

handler = lambda savefile=imgfile: ViewOne(imgdir, savefile) link.config(command=handler, width=linksize, height=linksize) link.pack(side=LEFT, expand=YES) canvas.create_window(colpos, rowpos, anchor=NW,

window=link, width=linksize, height=linksize) colpos += linksize savephotos.append(photo) rowpos += linksize return win, savephotos

if __name__ == ‘__main__’:

imgdir = ‘images’ if len(sys.argv) < 2 else sys.argv[1]

main, save = viewer(imgdir, kind=Tk)

main.mainloop()

Чтобы увидеть эту программу в действии, убедитесь, что у вас установлено расширение PIL, как описывается в главе 8, и запустите сценарий из командной строки, передав ему имя каталога с изображениями в виде аргумента:

...\PP4E\Gui\PIL> viewer_thumbs_scrolled.py C:\Users\mark\temp\101MSDCF

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

Можно также просто запустить сценарий без аргументов из командной строки щелчком на ярлыке файла или из среды IDLE. В этом случае он отобразит содержимое подкаталога с примерами изображений в дереве примеров для книги, как показано на рис. 9.26.

Еще о прокрутке изображений: PyPhoto (забегая вперед)

Несмотря на все эволюционные изменения, сценарий отображения миниатюр с прокруткой из примера 9.15 все еще имеет одно ограничение: изображения, размеры которых превышают размеры физического экрана, просто обрезаются при отображении в Windows. Этот недостаток наиболее очевидно проявляется при открытии больших цифровых фотографий, таких как на рис. 9.25. Кроме того, сценарий не позволяет изменять размеры уже открытых изображений, открывать другие ка-

Рис. 9.25. Сценарий отображения коллекции миниатюр с прокруткой


Рис. 9.26. Отображение каталога с изображениями по умолчанию

талоги и так далее. Это довольно упрощенная демонстрация приемов программирования холста.

В главе 11 мы узнаем, как решить все эти проблемы, когда встретимся с программой PyPhoto. Эта программа добавляет возможность прокрутки и в окно просмотра полноразмерного изображения. Кроме того, она позволяет изменять размеры изображения, поддерживает возможность сохранения изображений в файлы и открытия других каталогов в процессе выполнения. При этом в основе PyPhoto лежат те же приемы, которые были продемонстрированы в простом сценарии, представленном здесь, а также реализация генератора миниатюр, написанная в предыдущей главе. Как и простой текстовый редактор, демонстрировавшийся выше в этой главе, данная реализация является, по сути, прототипом более полнофункциональной программы PyPhoto, которую мы рассмотрим в главе 11. Оставайтесь с нами до волнующего финала истории развития PyPhoto (или, если ожидание для вас слишком томительно, шагайте вперед прямо сейчас).

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


События холстов

Подобно виджетам Text и Listbox виджет Canvas не имеет параметра настройки command для назначения обработчика событий. Вместо этого программы, содержащие холсты, обычно используют другие виджеты (такие как кнопки с миниатюрами в примере 9.15) или низкоуровневый метод bind, чтобы установить обработчики щелчков мышью, нажатий клавиш и других событий (как в примере 9.14 реализации холста с прокруткой). Пример 9.16 берет за основу последний подход и демонстрирует, как привязать события самого холста, чтобы реализовать некоторые наиболее типичные операции рисования.

Пример 9.16. PP4E\Gui\Tour\canvasDraw.py

реализует возможность рисования эластичных фигур на холсте при перемещении указателя мыши с нажатой правой кнопкой; версии этого сценария, дополненные тегами и анимацией, вы найдете в файлах canvasDraw_tags*.py

from tkinter import * trace = False

class CanvasEventsDemo:

def __init__(self, parent=None):

canvas = Canvas(width=300, height=300, bg=’beige’) canvas.pack()

canvas.bind(‘’, self.onStart) # щелчок canvas.bind(‘’, self.onGrow) # и вытягивание canvas.bind(‘’, self.onClear) # удалить все canvas.bind(‘’, self.onMove) # перемещать последнюю self.canvas = canvas

self.drawn = None

self.kinds = [canvas.create_oval, canvas.create_rectangle]

def onStart(self, event):

self.shape = self.kinds[0]

self.kinds = self.kinds[1:] + self.kinds[:1] # начало вытягивания self.start = event self.drawn = None

def onGrow(self, event): # удалить и перерисовать

canvas = event.widget if self.drawn: canvas.delete(self.drawn)

objectId = self.shape(self.start.x, self.start.y, event.x, event.y) if trace: print(objectId) self.drawn = objectId

def onClear(self, event):

event.widget.delete(‘all’) # использовать тег all

def onMove(self, event):

if self.drawn: # передвинуть в позицию

if trace: print(self.drawn) # щелчка

canvas = event.widget

diffX, diffY = (event.x - self.start.x), (event.y - self.start.y) canvas.move(self.drawn, diffX, diffY) self.start = event

if __name__ == ‘__main__’:

CanvasEventsDemo()

mainloop()

Этот сценарий перехватывает и обрабатывает три действия, выполняемые мышью:

Очистка холста

Чтобы удалить все имеющееся на холсте, сценарий привязывает событие двойного щелчка левой кнопкой к методу delete холста с тегом all - встроенным тегом, который автоматически ассоциируется с каждым объектом на экране. Обратите внимание, что доступ к виджету холста, на котором выполнен щелчок, осуществляется через объект события, передаваемый обработчику (он также доступен через self.canvas).

Вытягивание фигур

При нажатии левой кнопки мыши и перетаскивании (перемещении при нажатой кнопке) создается прямоугольник или овал. Этот прием часто называется вытягиванием фигуры - фигура увеличивается и сжимается как резиновая, и окончательный ее размер и положение определяются позицией указателя мыши, где будет отпущена кнопка мыши.

Чтобы выполнить такую работу с помощью tkinter, нужно всякий раз, когда возникает событие перетаскивания, стирать старую фигуру и рисовать новую. Обе операции, удаления и рисования, выполняются настолько быстро, что возникает эффект эластичного вытягивания. Конечно, чтобы начертить фигуру в соответствии с текущим положением указателя мыши, нужна начальная точка, а чтобы удалить фигуру перед тем, как вычерчивать новую, нужно запоминать идентификатор объекта, нарисованного последним. Здесь участвуют два события: исходное событие нажатия кнопки сохраняет начальные координаты (точнее, объект события нажатия кнопки, который содержит начальные координаты), а события перемещения указателя мыши стирают прежнюю фигуру и рисуют новую от начальных координат до новых координат мыши, сохраняя идентификатор нового объекта для операции стирания в следующем событии.

Перемещение объектов

При щелчке правой кнопкой мыши (кнопкой 3) сценарий сразу перемещает объект, нарисованный последним, в то место, где произведен щелчок. Аргумент события event дает координаты (X,Y) щелчка, из которых вычитаются начальные координаты последнего нарисованного объекта, чтобы получить смещения (X,Y), передаваемые методу move холста (напомню, что метод move ожидает получить смещения, а не координаты). Не следует забывать о необходимости сначала пересчитать координаты события, если холст прокручен.

В итоге после взаимодействия с пользователем получается окно, подобное изображенному на рис. 9.27. При вытягивании объектов сценарий поочередно вычерчивает овалы и прямоугольники. Установите в сценарии глобальную переменную trace и вы увидите в stdout идентификаторы новых объектов, вычерчиваемых при вытягивании и перемещении. Этот снимок экрана получен после вытягивания и перемещения нескольких объектов, что невозможно понять, только глядя на снимок -запустите пример на своем компьютере, чтобы лучше ощутить выполняемые им операции.

Привязка событий к конкретным элементам

Подобно тому как мы делали это для виджета Text, мы можем привязать события к одному или нескольким конкретным объектам, нарисованным в виджете Canvas, с помощью его метода tag_bind. Этот метод принимает в качестве первого аргумента строку с именем тега или идентификатор объекта. Например, можно зарегистрировать отдельные обработчики событий щелчков мыши для каждого нарисованного элемента или для группы нарисованных и помеченных тегом элементов вместо обработчика для холста в целом. В примере 9.17 для иллюстрации выполняется привязка обработчика двойного щелчка как к самому холсту, так и к двум конкретным текстовым элементам на нем. Он создает окно, изображенное на рис. 9.28.

Рис. 9.27. Окно сценария canvasDraw после нескольких вытягиваний и перемещений


Рис. 9.28. Окно сценария canvas-bind

Пример 9.17. PP4E\Gui\Tour\canvas-bind.py

# привязка обработчиков событий к холсту и к элементам на нем from tkinter import *

def onCanvasClick(event):

print(‘Got canvas click’, event.x, event.y, event.widget) def onObjectClick(event):

print(‘Got object click’, event.x, event.y, event.widget, end=’ ‘) print(event.widget.find_closest(event.x, event.y)) # найти ID текстового

# объекта

root = Tk()

canv = Canvas(root, width=100, height=100)

obj1 = canv.create_text(50, 30, text=’Click me one’)

obj2 = canv.create_text(50, 70, text=’Click me two’)

canv.bind(‘’, onCanvasClick) # привязать к самому холсту

canv.tag_bind(obj1, ‘’, onObjectClick) # привязать к элементу canv.tag_bind(obj2, ‘’, onObjectClick) # теги тоже можно canv.pack() # использовать

root.mainloop()

Здесь методу tag_bind передаются идентификаторы объектов, но ему можно также передавать строку с именем тега, что позволяет привязывать обработчики событий к группам элементов. Когда щелчок выполняется в окне сценария за границами текстовых элементов, вызывается обработчик события холста. Когда щелчок выполняется на любом текстовом элементе, вызываются оба обработчика событий - холста и элемента. Ниже показан вывод в stdout после двух щелчков на холсте и одного щелчка на каждом из текстовых элементов. Чтобы получить идентификатор объекта для конкретного текстового элемента, ближайшего к точке щелчка, вызывается метод find_closest холста:

C:\...\PP4E\Gui\Tour> python canvas-bind.py

Got canvas click 3 6 .8217952 щелчки на холсте

Got canvas click 46 52 .8217952

Got object click 51 33 .8217952 (1,) щелчок на первом текстовом элементе

Got canvas click 51 33 .8217952

Got object click 55 69 .8217952 (2,) щелчок на втором текстовом элементе

Got canvas click 55 69 .8217952

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


Сетки

До сих пор мы размещали виджеты на экране, вызывая их метод pack -интерфейс к менеджеру компоновки в библиотеке tkinter. Мы также использовали абсолютные координаты в холстах, которые тоже можно считать своеобразным механизмом компоновки, хотя и не таким высокоуровневым, как методы менеджера компоновки. В данном разделе мы познакомимся с методом grid, наиболее часто используемой альтернативой методу pack. Мы предварительно уже рассматривали эту альтернативу в главе 8, когда обсуждали формы ввода и упорядочивали миниатюры изображений. А сейчас познакомимся с механизмом компоновки по сетке во всей полноте.

Как уже говорилось, действие менеджеров компоновки в библиотеке tkinter заключается в размещении дочерних виджетов внутри контейнера - родительского виджета (обычно родительскими являются элементы Frame или окна верхнего уровня). Когда виджету предлагается присоединить себя вызовом метода pack или grid, в действительности мы предлагаем его родительскому элементу расположить данный виджет среди собратьев. При использовании метода pack определяются некоторые ограничения, и менеджеру компоновки предлагается расположить виджеты соответствующим образом. При использовании метода grid виджеты располагаются в родительском элементе-контейнере по рядам и колонкам, как если бы он был таблицей.

Расположение по сетке является в библиотеке tkinter совершенно отдельной системой управления компоновкой. На момент написания этой книги методы pack и grid взаимно исключают друг друга; при расположении виджетов в одном и том же родительском элементе - в одном контейнере можно использовать либо метод pack, либо метод grid, но не тот и другой одновременно. Это разумно, если усвоить, что менеджеры компоновки выполняют свою работу в родительских элементах, и виджет может размещаться только одним менеджером компоновки.


В чем преимущества размещения по сетке?

Однако это означает, что, по крайней мере, внутри одного контейнера придется выбирать между методами grid и pack и придерживаться этого метода. Зачем тогда нужна сетка? В целом метод grid удобно использовать, когда необходимо расположить несвязанные между собой виджеты по рядам. Сюда относятся табличные интерфейсы и формы - расположение полей ввода по рядам и колонкам ничуть не сложнее, чем организация структуры интерфейса с помощью вложенных фреймов.

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

Однако, как мы увидим, на практике метод grid не позволяет заметно уменьшить объем программного кода или его сложность, в сравнении с эквивалентными решениями на основе метода pack, особенно если в графическом интерфейсе должны решаться задачи изменения размеров. Иными словами, выбор между двумя схемами компоновки является в значительной мере вопросом стиля, а не техники.


Основы работы с сеткой: еще раз о формах ввода

Начнем с базовых понятий. В примере 9.18 создается таблица из меток и полей ввода - уже знакомых нам виджетов Label и Entry. Однако в данном случае они располагаются по сетке.

Пример 9.18. PP4E\Gui\Tour\Grid\grid1.py

from tkinter import *

colors = [‘red’, ‘green’, ‘orange’, ‘white’, ‘yellow’, ‘blue’] r = 0

for c in colors:

Label(text=c, relief=RIDGE, width=25).grid(row=r, column=0)

Entry(bg=c, relief=SUNKEN, width=50).grid(row=r, column=1) r += 1

mainloop()

Расположение по сетке заключается в назначении виджетам номеров рядов и колонок, отсчет которых начинается с 0, - библиотека tkinter использует эти координаты, а также размеры виджетов, чтобы расположить виджеты внутри контейнера. Это напоминает действие метода pack, только в данном случае понятия сторон и порядка прикрепления заменяются рядами и колонками.

Если запустить этот сценарий, то он создаст окно, изображенное на рис. 9.29 с данными, введенными в некоторые поля. И снова эта книга не позволяет увидеть цвета, отображаемые в правой части, поэтому вам придется включить свое воображение (или запустить сценарий на своем компьютере).

Рис. 9.29. Менеджер компоновки grid в псевдоживых цветах

В действительности это та же самая классическая структура формы ввода, которую мы видели в предыдущей главе. Метки в левой части описывают данные, которые должны вводиться в поля справа.

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

Label(...).grid(row=r, column=0)

Entry(...).grid(row=r, column=1)

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

То есть благодаря размещению виджетов по рядам и колонкам они должным образом выравниваются по обоим направлениям. Использование метода pack с фреймами-рядами позволяет добиться того же эффекта, если метки будут иметь фиксированную длину (как было показано в главе 8), однако сетки точнее отражают табличную структуру интерфейсов, включая формы ввода, а также большие таблицы в целом. Следующий раздел иллюстрирует эти различия в программном коде.


Сравнение методов grid и pack

Настало время сделать некоторые сравнения и противопоставления: пример 9.19 реализует одинаковые расцвеченные формы ввода с помощью методов g rid и pack, чтобы легче было обнаружить различия между двумя подходами.

Пример 9.19. PP4E\Gui\Tour\Grid\grid2.py

добавляет эквивалентное окно, используя фреймы-ряды и метки фиксированной длины; использование фреймов-колонок не обеспечивает точного взаимного расположения виджетов Label и Entry по горизонтали; программный код в обоих случаях имеет одинаковую длину, хотя применение встроенной функции enumerate позволило бы сэкономить 2 строки в реализации компоновки по сетке;

from tkinter import *

colors = [‘red’, ‘green’, ‘orange’, ‘white’, ‘yellow’, ‘blue’] def gridbox(parent):

"компоновка по номерам рядов/колонок в сетке” row = 0

for color in colors:

lab = Label(parent, text=color, relief=RIDGE, width=25)

ent = Entry(parent, bg=color, relief=SUNKEN, width=50)

lab.grid(row=row, column=0)

ent.grid(row=row, column=1)

ent.insert(0, ‘grid’)

row += 1

def packbox(parent):

"фреймы-ряды и метки фиксированной длины” for color in colors:

row = Frame(parent)

lab = Label(row, text=color, relief=RIDGE, width=25)

ent = Entry(row, bg=color, relief=SUNKEN, width=50)

row.pack(side=TOP)

lab.pack(side=LEFT)

ent.pack(side=RIGHT)

ent.insert(0, ‘pack’)

if __name__ == ‘__main__’: root = Tk() gridbox(Toplevel()) packbox(Toplevel())

Button(root, text=’Quit’, command=root.quit).pack() mainloop()

Версия на основе метода pack в этом примере использует фреймы-ряды и метки фиксированной длины (напомню еще раз, что схема с фреймами-колонками не позволяет добиться требуемой точности). Эти две функции создают виджеты меток и полей ввода одинаковым способом, но размещаются они совершенно разными путями:

• При использовании метода pack метки и поля ввода прикрепляются к левому и правому краям с помощью параметров side, и для каждого ряда создается виджет Frame (который прикрепляется к верхнему краю родителя).

• При использовании метода grid каждому виджету назначается положение с помощью параметров row (ряд) и column (колонка) в предполагаемой табличной сетке родителя.

Как мы уже знаем, при использовании метода pack порядок добавления виджетов имеет большое значение: виджет получает остаток стороны родительского контейнера, к которой он прикрепляется (что в данном случае неактуально), и элементы, прикрепляемые первыми, обрезаются последними (метки и самые верхние строки здесь обрезаются последними). При компоновке с помощью метода g rid тот же эффект обрезания достигается благодаря особенностям поведения сетки. Если запустить этот сценарий, он создаст окна, изображенные на рис. 9.30, - по одному окну для каждой схемы размещения.

При внимательном рассмотрении можно заметить, что разница в объеме программного кода, необходимого для каждой схемы, ничтожна, по крайней мере для случая простой формы. Схема на основе метода pack должна создать отдельный виджет Frame для каждого ряда, а схема на основе метода grid должна отслеживать номер текущего ряда.

Фактически обе схемы требуют для реализации одинаковое количество строк программного кода. Однако справедливости ради следует заметить, что реализации обеих схем можно было бы сократить на одну строку, если добавлять метку немедленно, а реализацию на основе метода grid можно было бы сократить еще на две строки, применив встроенную функцию enumerate, чтобы избавиться от необходимости вести счет рядов вручную. Ниже приводится уменьшенная версия функции

gridbox:

def gridbox(parent):

for (row, color) in enumerate(colors):

Label(parent,text=color,relief=RIDGE,width=25).grid(row=row,column=0) ent = Entry(parent, bg=color, relief=SUNKEN, width=50) ent.grid(row=row, column=1) ent.insert(0, ‘grid’)

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

Рис. 9.30. Эквивалентные окна для схем размещения на основе grid и pack


Сочетание grid и pack

Обратите внимание, что в примере 9.19 каждой функции-конструктору формы передается совершенно новый виджет Toplevel, благодаря чему версии на основе методов grid и pack создают различные окна верхнего уровня. Так как два менеджера компоновки не могут одновременно использоваться в одном родительском окне, необходимо следить за тем, чтобы по недосмотру не смешать их. Пример 9.20 демонстрирует возможность компоновки виджетов с помощью методов pack и grid в одном и том же окне, но только после заключения их в отдельные контейнерные виджеты Frame.

Пример 9.20. PP4E\Gui\Tour\Grid\grid2-same.py

создает формы с применением методов pack и grid в отдельных фреймах в одном и том же окне; методы grid и pack не могут одновременно использоваться в одном родительском контейнере (например, в корневом окне), но могут использоваться в разных фреймах в одном и том же окне;

from tkinter import *

from grid2 import gridbox, packbox

root = Tk()

Label(root, text=’Grid:’).pack() frm = Frame(root, bd=5, relief=RAISED) frm.pack(padx=5, pady=5)

gridbox(frm)

Label(root, text=’Pack:’).pack() frm = Frame(root, bd=5, relief=RAISED) frm.pack(padx=5, pady=5) packbox(frm)

Button(root, text=’Quit’, command=root.quit).pack() mainloop()

Если запустить этот сценарий, получится составное окно с двумя формами идентичного вида (рис. 9.31), но эти два вложенные фрейма в действительности управляются совершенно разными менеджерами компоновки.

C другой стороны, такой программный код, как приводится в примере 9.21, вызывает грубую ошибку, поскольку пытается вызывать методы pack и grid в одном и том же родителе - только один менеджер компоновки может использоваться в каждом отдельном родительском окне.

Пример 9.21. PP4E\Gui\Tour\Grid\grid2-fails.py

ОШИБКА -- методы pack и grid не могут одновременно использоваться в одном и том же родительском контейнере (здесь, корневое окно)

from tkinter import *

from grid2 import gridbox, packbox

root = Tk()

gridbox(root)

packbox(root)

Рис. 9.31. grid и pack в одном окне

Button(root, text=’Quit’, command=root.quit).pack() mainloop()

Этот сценарий передает каждой из функций одного и того же родителя (окно верхнего уровня), пытаясь вывести обе формы в одном окне. На моей машине он полностью подвешивает процесс Python, не выводя вообще никаких окон (в некоторых версиях Windows мне пришлось прибегнуть к CtrL+ALt+DeLete, чтобы уничтожить процесс, в других версиях достаточно было перезапустить программу Командная строка (Command Prompt)).

Комбинирование менеджеров компоновки может представлять сложность, пока с этим не освоишься. Чтобы сделать этот сценарий работоспособным, например, требуется просто изолировать форму с сеткой в собственном родительском контейнере, чтобы оградить его от влияния метода pack, используемого в корневом окне, - как демонстрирует следующая альтернативная реализация, где изменения выделены полужирным шрифтом:

root = Tk() frm = Frame(root)

frm.pack() # это работает

gridbox(frm) # у gridbox должен быть собственный родитель

packbox(root)

Button(root, text=’Quit’, command=root.quit).pack() mainloop()

Еще раз напомню, что в настоящее время внутри одного родителя допускается использовать либо метод pack, либо метод grid, но не тот и другой одновременно. Возможно, в будущем это давнее ограничение будет снято, что, впрочем, маловероятно с учетом различий в схемах двух менеджеров компоновки, но на всякий случай проверьте свою версию Python.


Реализация возможности растягивания виджетов, размещаемых по сетке

А теперь некоторые практические замечания: сетки, которые мы видели до сих пор, имеют фиксированный размер - они не увеличиваются в размере при увеличении размеров содержащего их окна. Пример 9.22 реализует чрезвычайно патриотическую форму ввода с применением обоих методов, grid и pack, но в нем выполняются дополнительные настройки, необходимые, чтобы обеспечить растягивание всех виджетов в обоих окнах вместе со своими окнами.

Пример 9.22. PP4E\Gui\Tour\Grid\grid3.py

"добавляет метку в верхней части окна и возможность растягивания форм”

from tkinter import *

colors = [‘red’, ‘white’, ‘blue’]

def gridbox(root):

Label(root, text=’Grid’).grid(columnspan=2) row = 1

for color in colors:

lab = Label(root, text=color, relief=RIDGE, width=25) ent = Entry(root, bg=color, relief=SUNKEN, width=50) lab.grid(row=row, column=0, sticky=NSEW) ent.grid(row=row, column=1, sticky=NSEW) root.rowconfigure(row, weight=1) row += 1

root.columnconfigure(0, weight=1) root.columnconfigure(1, weight=1)

def packbox(root):

Label(root, text=’Pack’).pack() for color in colors: row = Frame(root)

lab = Label(row, text=color, relief=RIDGE, width=25) ent = Entry(row, bg=color, relief=SUNKEN, width=50) row.pack(side=TOP, expand=YES, fill=BOTH) lab.pack(side=LEFT, expand=YES, fill=BOTH) ent.pack(side=RIGHT, expand=YES, fill=BOTH)

root = Tk()

gridbox(Toplevel(root))

packbox(Toplevel(root))

Button(root, text=’Quit’, command=root.quit).pack() mainloop()

Если запустить этот сценарий, он создаст картину, изображенную на рис. 9.32. Снова создаются отдельные окна для методов pack и grid с полями ввода в правой части, окрашенными в красный, белый и голубой цвета (или для читателей, которые не работают параллельно на компьютере: серый, белый и несколько более темно-серый).

Рис. 9.32. Окна для схем размещения на основе grid и pack до изменения размеров

Однако на этот раз изменение размеров обоих окон с помощью мыши заставляет все встроенные в них метки и поля ввода растягиваться вместе с окнами, как показано на рис. 9.33 (где в поля ввода был введен текст).

Рис. 9.33. Окна для схем размещения на основе grid и pack после изменения размера

В данной реализации при уменьшении размеров окна со схемой размещения на основе метода pack первыми обрезаются виджеты, которые были добавлены последними. В окне со схемой размещения на основе метода grid обрезаются все метки и поля ввода сразу, в отличие от сценария grid2, где демонстрируется поведение по умолчанию (попробуйте у себя на компьютере).

Изменение размеров в сетках

Теперь, когда я показал, что делают эти окна, нужно объяснить, как они это делают. В главе 7 мы узнали, как заставить графические элементы растягиваться при использовании метода pack: мы использовали параметры expand и fill, чтобы увеличить отводимое им пространство и заставить их растягиваться в пределах этого пространства соответственно. Чтобы обеспечить растягивание виджетов, размещаемых с помощью метода grid, требуется использовать другие протоколы. Ряды и колонки становятся растягиваемыми, когда они помечены с помощью параметра weight (вес), а виджеты растягиваются в отведенных им ячейках сетки, когда помечены с помощью параметра sticky (липкий):

Тяжелые ряды и колонки

При использовании метода pack ряды становятся растягиваемыми, если способность к растягиванию придается соответствующему виджету Frame, в результате задания значений параметров expand=YES и fill=BOTH. Для сетки нужно быть несколько конкретнее: чтобы обеспечить полную способность к растягиванию, требуется вызвать метод rowconfigure контейнера сетки для каждого ряда и метод column-configure для каждой колонки. Обоим методам нужно передать параметр weight веса со значением больше нуля, чтобы ряды и колонки стали растягиваемыми. По умолчанию вес принимается равным нулю (что означает отсутствие поддержки растягивания), а контейнером сетки в данном сценарии служит просто окно верхнего уровня. Использование разных весов для разных рядов и колонок заставляет их растягиваться в различных пропорциях.

Липкие виджеты.

При использовании метода pack виджеты растягиваются по горизонтали или вертикали, заполняя отведенное им пространство, если передать этому методу параметр fill, а для позиционирования виджетов в отведенном им пространстве используется параметр anchor. Параметр sticky метода grid играет роли обоих параметров, fill и anchor, метода pack. Чтобы заставить растягиваться виджеты, размещаемые по сетке, можно прилепить их к одному краю отведенной им ячейки (как с помощью параметра anchor) или более чем к одному краю (как с помощью параметра fill). Приклеивать виджеты можно в четырех направлениях - N (север), S (юг), E (восток) и W (запад), а комбинируя эти четыре буквы, можно обеспечить приклеивание сразу к нескольким сторонам. Например, значение W в параметре sticky обеспечит выравнивание виджета по левому краю отведенного ему пространства (подобно anchor=W в методе pack), а значение NS заставит виджет растягиваться по вертикали в выделенном пространстве (подобно fill=Y в методе pack).

Приклеивание виджетов не использовалось в предыдущих примерах, потому что структуру интерфейса образовывали виджеты с постоянными размерами (виджеты были не меньше пространства, отведенного им в ячейке сетки), а изменение размеров вообще не поддерживалось. В данном случае для параметра sticky определено в значение NSEW, чтобы графические элементы растягивались во всех направлениях вместе с отведенными им ячейками.

Различные сочетания весов рядов и колонок, а также значений параметра sticky создают различные эффекты при изменении размеров. Например, если удалить вызов метода columnconfigure из сценария grid3, это приведет к тому, что интерфейс будет растягиваться только в вертикальном направлении. Попробуйте сами поэкспериментировать с этими настройками и посмотреть, к каким эффектам это приведет.

Объединение колонок или рядов

Есть еще одно важное отличие в том, как сценарий grid3 настраивает свои окна. Оба окна - с методами grid и pack - выводят вверху метку, которая размещается по ширине всего окна. В схеме размещения на основе метода pack метка просто прикрепляется к верхнему краю окна в целом (напомню, что параметр side по умолчанию имеет значение TOP):

Label(root, text=’Pack’).pack()

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

Label(root, text=’Grid’).grid(columnspan=2)

Чтобы виджет охватывал сразу несколько колонок, методу grid передается параметр columnspan с указанием количества охватываемых колонок. В данном случае он указывает, что метка в верхней части окна должна простираться на все окно, охватывая и колонку с метками, и колонку с полями ввода. Если нужно, чтобы графический элемент охватывал несколько рядов, следует передать параметр rowspan. Правильная структура сеток может быть и преимуществом, и недостатком - в зависимости от того, насколько равномерно должны располагаться виджеты; эти два параметра установки диапазонов позволяют при необходимости организовать исключения из правила.

Так какой же менеджер компоновки оказывается здесь победителем? Если имеет значение изменение размеров, как в этом сценарии, то подход на основе сетки оказывается несколько более сложным (в данном примере для реализации размещения по сетке потребовалось написать три дополнительных строки программного кода). С другой стороны, использование функции enumerate снова может изменить общий счет, метод grid остается удобным для создания простых форм, да и ваши схемы компоновки на основе методов grid и pack могут быть другими.

Дополнительная информация о способах компоновки элементов форм ввода приводится в разделе, где обсуждаются утилиты мастеров форм, которые мы реализуем ближе к концу главы 12 и будем использовать в главе 13, при разработке пользовательского интерфейса программы передачи файлов и клиента FTP. Как будет показано далее, автоматизировав процедуру создания привлекательных форм, мы сможем избавить себя от необходимости вникать в детали позднее. Кроме того, в главе 11 мы реализуем менее обычную компоновку формы в диалоге замены программы PyEdit и при размещении полей заголовков электронного письма в примере PyMailGUI, в главе 14.


Создание крупных таблиц с помощью grid

До сих пор мы строили наборы меток и полей ввода из двух колонок. Это типичный вид форм ввода, но менеджер 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. Массив 5x4 меток с координатами

Пример 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 - одного перед изменениями в полях ввода и другого после:

C:\...\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.35. Более крупная сетка полей ввода

Теперь, когда мы знаем, как создавать и выполнять обход массивов полей ввода, добавим еще несколько полезных кнопок. В примере 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’) # зарезерв. значение

for sum in self.sums: sum.config(text=’?’)

def 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. Чтобы дать возможность прикрепления к контейнерам, где есть другие графические элементы, скомпонованные тем или иным способом, он оставляет управление собственной компоновкой неопределенным и требует, чтобы вызывающая программа сама применяла метод pac k или g rid к его экземплярам. Контейнеры могут выбрать любую схему компоновки для своих дочерних элементов, потому что они независимы в своем выборе, но прикрепляемым классам компонентов, предназначенным для использования с любыми менеджерами компоновки, нельзя поручить управлять собой, так как они не могут заранее знать политику своего родителя.

Это довольно длинный пример, в котором нет почти ничего нового в отношении компоновки по сетке или виджетов в целом, поэтому я оставлю его для самостоятельного изучения и просто покажу, что он делает. На рис. 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, которые динамически вычисляют значения, вызывают функции из модулей и даже загружают текущие котировки акций из Интернета с помощью инструментов, с которыми мы познакомимся в следующей части книги.

Однако эта особенность может представлять опасность - в поле может содержаться выражение, удаляющее содержимое вашего жесткого диска!45 Если вы не до конца уверены в том, какими могут быть полученные выражения, не используйте функцию eval (осуществляйте преобразование, применяя более ограниченные функции, такие как int и float) или обеспечьте выполнение процесса Python с ограниченными правами доступа к системным компонентам, которые было бы нежелательно подвергать опасности.

Рис. 9.40. Выражения на языке Python в данных и таблице

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

Я должен также отметить, что о размещении по сетке можно сказать больше, чем позволяет объем книги. Например, путем создания вложенных фреймов с собственными сетками можно строить более сложные структуры в виде иерархий компонентов, во многом подобно тому, как размещает вложенные фреймы менеджер компоновки pack. А теперь перейдем к последней теме обзора виджетов.


Инструменты синхронизации, потоки выполнения и анимация

Последняя остановка в нашей экскурсии по виджетам, вероятно, самая необычная. Библиотека tkinter предоставляет ряд инструментов, которые связаны с моделью программирования, управляемого событиями, а не с отображением графики на экране.

Некоторым приложениям с графическим интерфейсом требуется периодически выполнять действия в фоновом режиме. Например, чтобы придать виджету «мерцающий» вид, можно зарегистрировать обработчик, который будет вызываться через равные промежутки времени. Аналогично при выполнении длительной операции с файлом неверно было бы заблокировать прочие действия в графическом интерфейсе -если бы удалось заставить цикл событий периодически выполнять обновления, графический интерфейс мог бы оставаться активным. В библиотеке tkinter есть средства для планирования таких отложенных действий и принудительного обновления интерфейса:

widget.after(milliseconds, function, *args)

Этот инструмент планирует вызов указанной функции по истечении заданного числа миллисекунд. Данная форма вызова не останавливает программу - функция обратного вызова будет запущена позднее из обычного цикла событий tkinter, а вызывающая программа продолжит свою работу как обычно и графический интерфейс останется активным, пока функция ожидает вызова. Как уже говорилось в главе 5, в отличие от объекта Timer из модуля threading, события widget.after распространяются в главном потоке выполнения графического интерфейса и потому могут выполнять в нем любые изменения.

Аргумент function может быть любым вызываемым объектом Python: функцией, связанным методом, lambda-выражением и так далее. Аргумент milliseconds определяет интервал времени в миллисекундах и является целым числом - если разделить значение этого аргумента на 1000, получится эквивалентное число секунд. Любые значения в кортеже args будут переданы функции function в виде позиционных аргументов.

На практике вместо отдельных аргументов можно использовать lambda-выражения, чтобы сделать связь аргументов с функцией более явной, но это не является обязательным. Когда в качестве функции передается связанный метод, он может получать дополнительную информацию из атрибутов объекта, а не из аргументов. Метод after возвращает идентификатор, который можно передать методу after_cancel, чтобы отменить вызов обработчика. Метод after используется очень часто, поэтому несколько ниже о нем будет рассказываться более подробно и с примерами.

widget.after(milliseconds)

Этот инструмент останавливает выполнение программы на заданное количество миллисекунд. Например, если передать в аргументе число 5000, программа будет приостановлена на 5 секунд. В сущности, это то же самое, что библиотечная функция Python time. sleep(seconds), и обе функции могут применяться для создания задержки при отображении (например, в анимационных программах, таких как PyDraw и более простых примерах ниже).

widget.after_idle(function, *args)

Этот инструмент планирует вызов указанной функции при отсутствии других событий, которые должны обрабатываться. То есть функция function становится обработчиком холостого времени, который вызывается, когда графический интерфейс не занят ничем другим.

widget.after_cancel(id)

Этот инструмент отменяет вызов обработчика, запланированный методом after до того, как он произойдет. Аргумент id - значение, возвращаемое методом after.

widget.update()

Этот инструмент вынуждает библиотеку tkinter обработать все ожидающие события, имеющиеся в очереди событий, в том числе изменение геометрии окна, а также обновление и перерисовку виджетов. Его можно периодически вызывать из долго выполняющегося обработчика, чтобы обновить экран и произвести те изменения, которые уже запросил ваш обработчик. Если этого не делать, произведенные обработчиком изменения появятся на экране только после выхода из него. На время работы обработчика, выполняющегося продолжительное время, интерфейс может вообще зависнуть, если не обновлять его вручную (обработчики не выполняются в отдельных потоках, о чем говорится в следующем разделе); окно даже не будет перерисовывать себя при перекрытии или открытии другими окнами, пока не произойдет возврат из обработчика.

Загрузка...