Открыв для себя Linux несколько лет назад, я пережил такое чувство, как будто заново обрел свой компьютер. Каким наслаждением было открывать окно терминала и запускать основную машину (basic machine) без всех этих мастеров и сложностей окружения Windows. Я начал изучать Python. На то, чтобы разобраться с графическим интерфейсом Tk, у меня ушло довольно много времени. Окончательно освоившись, я никак не мог понять, почему столь простые вещи казались такими сложными. По-видимому, книги, по крайней мере те, с которыми мне приходилось иметь дело, были либо слишком простыми, либо слишком сложными. Они не были предназначены для таких непрофессиональных ремесленников как я. Короче говоря, я начал писать небольшое учебное руководство по Tkinter, но вмешалась судьба. В дом, где я работал с Linux, ударила молния, и мой компьютер за секунду превратился в хорошо поджаренный тост. Итак, мой первый период увлечения Linux закончился трагически. В то время мне все время приходилось работать, так что ни времени, ни энергии, на то, чтобы собрать еще одну Linux-машину, попросту не было.

Но сейчас я не работаю, и время у меня есть. И когда моему ноутбуку потребовалось заменить жесткий диск, я подумал: "Почему бы не дать Linux еще один шанс?" Linux сильно вырос за эти годы, но сохранил то базовое управление, которое я ценил больше всего. Затем нашлась дискета с моим старым незаконченным руководством по Tkinter, которое меня удивило. Мое собственное учебное пособие серьезно ускорило обучение. Итак, я довел его до конца и предлагаю таким же как я новичкам, которых не устраивают существующие книги. Конечно, создать графический интерфейс для Python можно не только с помощью Tkinter, но, полагаю, есть люди, которые, так же как и я, любят изучать все основательно, вручную составляя код своих приложений.

Настоящее руководство предназначено для людей, начинающих программировать на Python/Tkinter. Простейший способ узнать, подходит ли оно вам,- это просмотреть пять файлов, составляющих итоговую программу [см. ниже]. Если вы в состоянии читать и понимать код в этих файлах, пособие вам не нужно. Если возникают затруднения, вы в нужном месте...

Фактически, это четыре кратких учебных руководства, объединенные в одно.

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

II. Управление файлами:
В этом руководстве мы разработаем основной интерфейс редактора с файловым меню, с помощью которого возможно открывать и сохранять файлы, делая резервную копию, если текст был изменен. Мы создадим файловый браузер и несколько родовых (generic) диалоговых окон.

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

III. "Горячие" клавиши и внешний вид:
Это руководство о том, как все выглядит и работает на экране компьютера. Мы построим интерфейс с "горячими" клавишами [ALT]-Клавиша и [CTRL]-Клавиша для команд в меню и панелью инструментов. Затем добавим модуль настройки, который позволит выбирать, как будет выглядеть на экране это [или какое-нибудь другое будущее] приложение.

Именно тут Tkinter перестанет казаться неуклюжим и станет более похожим на те оконные приложения, к которым мы привыкли.

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

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

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

Загрузочные файлы:
Вам предлагается загрузить два архива: code.tgz и tutorial.tgz.

В первом [code.tgz (22K)] содержится весь код python и графические файлы для всех четырех руководств. Перед распаковкой проверьте, что выбранный для этого каталог прописан в переменной окружения Python Path. В другом файле [tutorial.tgz (465K)] - полный текст руководства с рисунками, на случай если вы захотите читать его оффлайн с собственного компьютера. [В русском варианте оформление документа изменено, и файл tutorial.tgz не нужен. Вместо него можно сохранить на свой компьютер HTML-файл, который вы сейчас читаете. Он содержит полный текст руководства с рисунками.- Прим. пер.]

Скопируйте загруженный файл в нужную директорию. Находясь в оболочке bash, перейдите в этот каталог и наберите команду tar xzf code.tgz [или tar xzf tutorial.tgz], чтобы файлы распаковались в эту папку.

Чтобы проверить, "видит" ли Python каталог с кодом, запустите editor.py. Должен загрузиться текстовый редактор!


I. Основы Tkinter

Моя дочь, рассматривая экран моего Linux-компьютера, сказала: "Похоже на компьютер 80-х". Думаю, что это на самом деле так. Linux можно разукрасить, чтобы он был похож на машины с Windows XP или Маки, заполняющие мир сегодня, но многие пользователи Linux, подобно мне, так не делают. Нам нравятся команды, подаваемые с терминала, и угловатый рабочий стол Gnome. Это похоже на старые добрые времена, когда большая часть программного обеспечения писалась вручную. И до сих пор в очень многих случаях самостоятельно написанное приложение является наилучшим решением, и язык Python - прекрасный выбор для задач такого рода.

Python распространяется вместе с графической библиотекой языка Tk/Tcl, содержащейся в модуле Tkinter. Хотя для создания графических интерфейсов пользователя с помощью языка Python существует много других инструментов, полезно, начиная изучать этот язык, уделить время стандартному пакету, прежде чем браться за более современные. Настоящее краткое руководство поможет быстро разобраться с Tkinter. Дойдя до конца, вы овладеете всеми принципами, необходимыми для построения более сложных графических интерфейсов, и будете представлять общую объектно-ориентированную модель языка Python. Лично для меня вполне достаточно возможностей Tkinter. Для других - это трамплин к более сложным графическим решениям.

Это учебное пособие также "похоже на веб-страницу 90-х". Каждая из последующих страниц следует определенному шаблону. Окно с кодом сопровождается подробными комментариями. Перемещаться по ним можно посредством обычных кнопок [prev][home][next] или с помощью оглавления, приводимого ниже:

Содержание
Создание окна
Создание "дочернего" окна
Работа с классами
Классы и элементы управления
Методы
Модальное дочернее окно
Отсылка сообщения от родительского элемента к дочернему
Отсылка сообщений в обоих направлениях
Усложнение диалогов
Святой Грааль [модули]
Мини-руководство по Tkinter

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

Создание окна

window_01.py

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *

#создание окна
root = Tk()
root.title('myWindow')
root.geometry('200x150+300+225')

# вывод окна на экран
root.mainloop()
          

Данный модуль Python window_01.py создает пустое окно 200x150 с названием "myWindow", расположенное в центре экрана.

#!/usr/bin/python
С этой строки начинается всякий исполняемый модуль Python [в Linux]. Символ # используется в Python для комментариев, так что интерпретатор строчку проигнорирует. Но в bash, базовом языке сценариев Linux, это не комментарий, и данная строка будет прочитана. В ней содержатся путь к интерпретатору python и указание исполнить код, записанный на Python. Такие инструкции называют "the pound bang" ["Pound" - "фунт" - так часто называют знак "#". "Bang" - сленговое название восклицательного знака "!".- Прим. пер.], и их ставят в каждой программе на первой строке. Можно забыть, для чего они нужны, но нельзя забывать вставлять их в код.

# импортирование модулей python
from Tkinter import *
Данная строка загружает в Python весь модуль Tkinter целиком. В результате у Python появляется библиотека для построения оконного интерфейса.

# создание окна
root = Tk()
root.title('myWindow')
root.geometry('200x150+300+225')
Tk() - это функция Tkinter, открывающая главное окно любого приложения. Здесь мы создаем экземпляр с именем root. Это общая черта всех классов Tkinter - они должны быть присвоены какой-либо переменной. В следующих двух командах задаются некоторые свойства root, определяющие заголовок и размеры окна. Это другой общий факт - взаимодействие с объектами Tkinter происходит через задание свойств [и вызов методов]. '200x150+300+225' означает [ширина x высота + координата_x_верхнего_левого_угла + координата_y_верхнего_левого_угла].

# запуск окна
root.mainloop()
Наконец, в этой строке происходит вызов метода Tk() под названием mainloop(), который держит окно раскрытым, пока оно не будет закрыто нажатием кнопки [x] на окне или вызовом метода Tk() destroy().

Экземпляры классов - это объекты, обладающие свойствами и методами. Объектно-ориентированные читатели уже знают об этом.



Довольно просто, не так ли? Понадобилось всего шесть строчек кода. Запустите Python; загрузите модуль Tkinter; присвойте какой-либо переменной значение Tk(); затем настройте три метода Tk() для этой переменной. Итак, запускаем window_01.py. Voila! Вы создали окно - фундаментальный элемент любого графического интерфейса пользователя Tkinter...

Создание "дочернего" окна

window_02.py

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *

#создание окна
root = Tk()
root.title('parent')
root.geometry('200x150+200+150')

#создание дочернего окна
child = Toplevel(root)
child.title('child')
child.geometry('200x150+400+300')

# запуск окна
root.mainloop()
          

Приведенный здесь модуль Python - window_02.py помещает на экран еще одно пустое "дочернее" окно 200x150. В большинстве графических интерфейсов для организации диалога используют всплывающие окна. "Дочернее" окно исчезает при закрытии основного окна.

# создание дочернего окна
child = Toplevel(root)
child.title('child')
child.geometry('200x150+400+300')
Toplevel() - это класс Tkinter, с помощью которого можно создавать любое окно кроме главного. Он также присваивается переменной child. Toplevel(root) означает дочернее окно child, относящееся и зависящее от родительского окна root. Следующие две команды формируют заголовок и размеры дочернего окна аналогично случаю родительского окна. Коду дочернего окна не нужен свой метод mainloop(). Он запускается из родительского окна.



Снова довольно просто. Потребовалось лишь три дополнительные строчки кода. Присвойте Toplevel() новой переменной; свяжите его с корневым окном root с помощью Toplevel(root); затем задайте значения пары свойств Toplevel(root). Наконец, запустите window_02.py.

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

Уже похоже на интерфейс...

Работа с классами

window_03.py

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *

# класс родительских окон
class main:
  def __init__(self):
    self.master = root
    self.master.title('parent')
    self.master.geometry('200x150+200+150')
    child()
    self.master.mainloop()

# класс дочерних окон
class child:
  def __init__(self):
    self.slave = Toplevel(root)
    self.slave.title('child')
    self.slave.geometry('200x150+400+300')

# создание окна 
root = Tk() 

# запуск окна 
main()
          

Python - это объектно-ориентированный язык программирования. Tk() и Toplevel() являются классами Tkinter, принимающими форму объектов для создания на экране графических окон. Программирование на Tkinter подразумевает комбинирование и преобразование встроенных классов Tkinter в новые классы с индивидуальными свойствами и методами.

Следующей задачей данного руководства будет воспроизведение визуального эффекта, достигнутого в последнем примере [window_02.py], объектно-ориентированными средствами, заложенными в Python, т.е. путем создания классов. Приведенный выше код объявляет и определяет класс. Объект - это экземпляр класса. В данном случае мы собираемся создать простые классы родительских и дочерних окон. Позже мы будем формировать классы с их собственными (добавленными) свойствами и методами.

# класс родительских окон
class main:
  def __init__(self):
    self.master = root
    self.master.title('parent')
    self.master.geometry('200x150+200+150')
    child()
    self.master.mainloop()
Вот обобщенная форма для создания классов. Замечание: отступы обязательны!
  class {имя-класса}:
    def __init__(self):
      self.{переменная-класса} = ...
Здесь указывается, что будет создан класс с именем имя-класса со следующими определениями [def]. Команду __init__(self) проще показать в действии, чем объяснить. __init__() - это конструктор объектов, позволяющий создавать экземпляр объекта во время исполнения программы. self - это метка экземпляра, необходимая для привязки переменных класса к данному объекту. Таким образом инструкция self.master = root создает переменную master и присваивает ей глобальное значение root [пока еще не определенное]. В оставшейся части кода вы увидите, как теперь определяется то же самое окно внутри класса main. Итак, что же такое child()?

# класс дочерних окон
class child:
  def __init__(self):
    self.slave = Toplevel(root)
    self.slave.title('child')
    self.slave.geometry('200x150+400+300')
child() - это вызов другого класса, определенного в модуле. Так класс main генерирует экземпляр дочернего окна.

# создание окна
root = Tk()

# запуск окна
main()
При запуске модуля на выполнение происходит вызов Python, загрузка Tkinter и сохранение определений классов main и child. Команды, приведенные выше, сначала задают значение переменной root, чтобы создать экземпляр Tk(), затем открывают окно, активируя main [который, в свою очередь, активирует child]. Обратите внимание, что mainloop() расположен "внутри" класса main. Запустите window_03.py. Он должен сделать то же самое, что и предыдущий пример.



Если этот пример - ваша первая встреча с классами, объектами и т.п., в голову вполне может прийти мысль: "К чему такие сложности?" Но стоит нам перейти к более сложным примерам, как тотчас станет очевидно, что инкапсуляция кода внутрь классов - это отличный способ писать лаконичные, пригодные для многократного использования программы на Python. Потерпите немного. Некоторые из нас помнят, как учились составлять "макаронные" программы ['spaghetti' coding - слабо структурированные программы с большим размером процедур и интенсивным использованием оператора goto; трудны для изучения и модификаций (Электронный словарь ABBYY Lingvo 10).- Прим. пер.], а потом изучали "структурное" программирование. Так что "объектно-ориентированное" программирование - это еще одна новинка...

Классы и элементы управления

window_04.py

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *

# класс главного окна
class main:
  def __init__(self, master):
    self.master = master
    self.master.title('myWindow')
    self.master.geometry('200x150+300+225')
    self.button = Button(self.master,
                         text = 'myButton')
    self.button.pack(side = BOTTOM)
    self.master.mainloop()

# создание окна
root = Tk()

# запуск окна
main(root)

Оставим на время разработку дочернего окна, чтобы сосредоточить все внимание на классах. Рассмотрим, как передавать классу информацию, и добавим к классу элемент управления [в данном случае кнопку (Button)] [и объект и окно].

# класс главного окна
class main:
  def __init__(self, master):
    self.master = master
...
# создание окна
root = Tk()

# запуск окна
main(root)
В последнем примере [window_3.py] вас, возможно, удивило, что класс main содержит ссылку на переменную root, которая в программе еще не создана. Я сделал это специально, чтобы продемонстрировать, что классы ничего не делают, пока на их базе не будут созданы экземпляры объектов. Но есть лучший путь достичь того же самого. Объекту можно передать глобальную переменную root, и именно так сделано в нашем коде. Благодаря вызову main(root) параметр root передается переменной master класса main . В последующих примерах подобным же образом мы будем передавать классам множество различных параметров.

self.button = Button(self.master, text = 'myButton')
self.button.pack(side = BOTTOM)
Ну, наконец и элемент управления. В Tkinter имеется определенное число графических элементов управления, которые можно размещать в нашем окне. В данном случае это будет кнопка (Button). Мы уже пользовались двумя элементами управления - Tk() и Toplevel(). Код self.button = Button(self.master, text = 'myButton') связывает элемент управления Button() с классом main [все элементы управления кроме Tk() кому-нибудь "принадлежат"], а text = 'myButton' задает значение свойства text [текст, который отобразится на кнопке во время исполнения программы].

Строка self.button.pack(side = BOTTOM) определяет, в какой части окна появится наша кнопка. Позже мы рассмотрим pack подробнее.

Между прочим, эта кнопка пока ничего не делает. Итак, запускаем window_04.py и нажимаем бесполезную кнопку...

Методы

window_05.py

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *

# класс главного окна
class main:
  def __init__(self, master):
    self.master = master
    self.master.title('parent')
    self.master.geometry('200x150+300+225')
    self.button = Button(self.master,
                         text = 'myButton',
                         command = self.openDialog)
    self.button.pack(side = BOTTOM)
    self.master.mainloop()

  def openDialog(self):
    child(self.master)

# класс дочерних окон
class child:
  def __init__(self, master):
    self.slave = Toplevel(master)
    self.slave.title('child')
    self.slave.geometry('200x150+500+375')

# создание окна
root = Tk()

# запуск окна
main(root)

Любая кнопка должна что-нибудь делать. В данном случае мы свяжем событие "нажатие кнопки" с открытием дочернего окна из одного из прошлых примеров. Для этого в класс main вводится метод openDialog, который создает экземпляр объекта child.

 def openDialog(self):
   child(self.master)
Для определения метода openDialog не нужна функция __init__(). Не создается экземпляра метода, - но метод создает экземпляр объекта child. Метод - это то, что класс main делает, а не то, чем класс main является...
 
command = self.openDialog
Обращение к openDialog содержит вездесущее self. Это означает, что метод openDialog является внутренним по отношению к main.

 
# класс дочерних окон
class child:
  def __init__(self, master):
    self.slave = Toplevel(master)
Toplevel() из класса child узнает о том, что относится к дочернему классу класса main, довольно мучительным путем. Tk(), связанный с root, передается классу main через параметр mastermain], а затем пересылается другому параметру masterchild]. Всякий класс имеет свою переменную master, локальную по отношению к данному классу. Их имена могут быть различными.

Ага! Метод. Запускаем window_05.py и испытываем работающую кнопку...

Модальное дочернее окно

window_06.py

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *

# класс главного окна
class main:
  def __init__(self, master):
    self.master = master
    self.master.title('parent')
    self.master.geometry('200x150+300+225')
    self.button = Button(self.master,
                         text = 'myButton',
                         command = self.openDialog)
    self.button.pack(side = BOTTOM)
    self.master.mainloop()

  def openDialog(self):
    child(self.master)

# класс дочерних окон
class child:
  def __init__(self, master):
    self.slave = Toplevel(master)
    self.slave.title('child')
    self.slave.geometry('200x150+500+375')
    self.slave.grab_set()
    self.slave.focus_set()
    self.slave.wait_window()

# создание окна
root = Tk()

# запуск окна
main(root)

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

 self.slave.grab_set()
child перехватывает все события, происходящие в приложении.

 self.slave.focus_set()
child захватывает фокус.

 self.slave.wait_window()
child ждет, когда будет уничтожен текущий объект, не возобновляя работы [но и не оказывая влияния на основной цикл].



Итак, рецепт. Если нужно создать модальное окно, воспользуйтесь этими тремя методами. Как говорится, "Просто сделай это!"

Отсылка сообщения от родительского элемента к дочернему

window_07.py

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *

# класс главного окна
class main:
  def __init__(self, master):
    self.master = master
    self.master.title('parent')
    self.master.geometry('200x150+300+225')
    self.button = Button(self.master,
                         text = 'myButton',
                         command = self.openDialog)
    self.button.pack(side = BOTTOM)
    self.text = Text(self.master,
                     background = 'white')
    self.text.pack(side = TOP,
                   fill = BOTH,
                   expand = YES)
    self.master.mainloop()

  def openDialog(self):
    child(self.master, self.text.get('0.0', END)

# класс дочерних окон
class child:
  def __init__(self, master, myText = ''):
    self.slave = Toplevel(master)
    self.slave.title('child')
    self.slave.geometry('200x150+500+375')
    self.text = Text(self.slave,
                     background = 'white')
    self.text.pack(side = TOP,
                   fill = BOTH,
                   expand = YES)
    self.text.insert('0.0', myText)
    self.slave.grab_set()
    self.slave.focus_set()
    self.slave.wait_window()

# создание окна
root = Tk()

# запуск окна
main(root) 
          
Модальные дочерние окна используются, главным образом, в роли диалоговых окон. Нам нужен способ передачи информации от родительского окна к дочернему и наоборот. Сперва пойдем от main к child. В данный пример внесено три добавления по сравнению с предыдущими:
 
self.text = Text(self.master, background = 'white')
self.text.pack(side = TOP, fill = BOTH, expand = YES)
Стоит создать один элемент управления, как все остальное становится ясно. Единственно стоит отметить, что элемент управления text имеет свойство background (фон) со значением 'white' (белый) и что пришло время поговорить о pack. Метод pack() размещает в окне элемент управления. Итак:
  • side (сторона) определяет, какой стороны окна будет "держаться" элемент управления
    варианты: TOP (сверху) RIGHT (справа) LEFT (слева) BOTTOM (снизу)
    по умолчанию: NONE (никак)
  • fill (заполнение) показывает, заполнит элемент доступное пространство или нет.
    варианты: X Y BOTH (X Y оба)
    по умолчанию: NONE (никак)
  • expand (растяжение) указывает, будет ли элемент управления менять свой размер при изменении размеров окна.
    варианты: YES (да)
    по умолчанию: 0
 
def openDialog(self):
  child(self.master, self.text.get('0.0', END))
В метод openDialog() мы ввели инструкцию self.text.get('0.0', END), которая является методом элемента управления Text. Она собирает все содержимое текстового окна от строки 0 символа 0 и до конца, чтобы передать его классу/окну child [как myText].
 
# класс дочерних окон
class child:
  def __init__(self, master, myText = ''):
    ...
    self.text = Text(self.slave,
                     background = 'white')
    self.text.pack(side = TOP,
                   fill = BOTH,
                   expand = YES)
    self.text.insert('0.0', myText)
Как работает текстовый элемент управления child интуитивно ясно. Он заполняется текстом из главного окна посредством def __init__(self, master, myText = ''). Информация из myText вставляется в элемент управления text с помощью метода self.text.insert('0.0', myText), который помещает ее, начиная со строки 0 символа 0. Испытайте window_07.py, напечатав в нем какой-нибудь текст и отослав его...



Общение - хорошая штука, но оно должно быть улицей с двухсторонним движением...

Отсылка сообщений в обоих направлениях

window_08.py

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *

# класс главного окна
class main:
  def __init__(self, master):
    self.master = master
    self.master.title('parent')
    self.master.geometry('200x150+300+225')
    self.button = Button(self.master,
                         text = 'dialog',
                         command = self.openDialog)
    self.button.pack(side = BOTTOM)
    self.text = Text(self.master,
                     background = 'white')
    self.text.pack(side = TOP,
                   fill = BOTH,
                   expand = YES)
    self.master.mainloop()

  def openDialog(self):
    self.dialog = child(self.master)
    self.sendValue = self.text.get('0.0', END)
    self.returnValue = self.dialog.go(self.sendValue)
    if self.returnValue:
      self.text.delete('0.0', END)
      self.text.insert('0.0', self.returnValue)

# класс дочернего окна
class child:
  def __init__(self, master):
    self.slave = Toplevel(master)
    self.slave.title('child')
    self.slave.geometry('200x150+500+375')
    self.button = Button(self.slave,
                         text = 'accept',
                         command = self.accept)
    self.button.pack(side = BOTTOM)
    self.text = Text(self.slave,
                     background = 'white')
    self.text.pack(side = TOP,
                   fill = BOTH,
                   expand = YES)

  def go(self, myText = ''):
    self.text.insert('0.0', myText)
    self.newValue = None
    self.slave.grab_set()
    self.slave.focus_set()
    self.slave.wait_window()
    return self.newValue

  def accept(self):
    self.newValue = self.text.get('0.0', END)
    self.slave.destroy()

# создание окна
root = Tk()

# запуск окна
main(root) 
          

Мы хотим, чтобы информация шла от родительского окна к дочернему И от дочернего к родительскому. Последнего можно добиться, добавив к дочернему окну кнопку accept и метод для регистрации всех изменений, произведенных в передаваемом тексте; также введем метод go как для создания экземпляра дочернего окна, так и для управления процессом обмена информацией. Такое применение методов [go] для открытия дочерних окон - полезный инструмент, который приобретет особую важность, когда мы будем иметь дело с более сложными операциями в последующих руководствах. Сейчас же он создает пустую переменную newValue, в которую будет записан измененный текст [если он был изменен].

 
# класс дочернего окна
class child:
  def __init__(self, master):
    ...
    self.button = Button(self.slave,
                         text = 'accept',
                         command = self.accept)
    self.button.pack(side = BOTTOM)
    ...

  def go(self, myText = ''):
    self.text.insert('0.0', myText)
    self.newValue = None
    self.slave.grab_set()
    self.slave.focus_set()
    self.slave.wait_window()
    return self.newValue

  def accept(self):
    self.newValue = self.text.get('0.0', END)
    self.slave.destroy()

При вызове дочернего метода go введенный текст вставляется в дочернее текстовое окно. Если текст редактировался и пользователь нажимает кнопку accept, исправленный текст возвращается как newValue. Если дочернее окно просто закрывается, newValue возвращается с пустым значением.

 
  def openDialog(self):
    self.dialog = child(self.master)
    self.sendValue = self.text.get('0.0', END)
    self.returnValue = self.dialog.go(self.sendValue)
    if self.returnValue:
      self.text.delete('0.0', END)
      self.text.insert('0.0', self.returnValue)

При возврате метод openDialog класса main осуществляет проверку. Если возвращаемая строка не пустая, возвращаемый текст будет вставлен в текстовое окно main.

Запускаем window_08.py. Убедитесь, что вы "уловили", как работает go. Нам придется еще довольно много иметь дело с этим методом...

Усложнение диалогов

window_09.py

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *

# класс главного окна
class main:
  def __init__(self, master):
    self.master = master
    self.master.title('parent')
    self.master.geometry('400x300+200+150')
    self.button = Button(self.master,
                         text = 'dialog',
                         command = self.openDialog)
    self.button.pack(side = BOTTOM)
    self.text = Text(self.master,
                     background = 'white')
    self.text.pack(side = TOP,
                   fill = BOTH,
                   expand = YES)
    self.master.protocol('WM_DELETE_WINDOW', 
                         self.exitMethod)
    self.master.mainloop()

  def openDialog(self):
    self.dialog = child(self.master)
    self.sendValue = self.text.get('0.0', END)
    self.returnValue = self.dialog.go(self.sendValue)
    if self.returnValue:
      self.text.delete('0.0', END)
      self.text.insert('0.0', self.returnValue)

  def exitMethod(self):
    self.dialog = yesno(self.master)
    self.returnValue = self.dialog.go('question',
                                      'Do you want to exit?')
    if self.returnValue:
      self.master.destroy()

# класс дочернего окна
class child:
  def __init__(self, master):
    self.slave = Toplevel(master)
    self.slave.title('child')
    self.slave.geometry('200x150+500+375')
    self.frame = Frame(self.slave)
    self.frame.pack(side = BOTTOM)
    self.accept_button = Button(self.frame,
                                text = 'accept',
                                command = self.accept)
    self.accept_button.pack(side = LEFT)
    self.cancel_button = Button(self.frame,
                                text = 'cancel',
                                command = self.cancel)
    self.cancel_button.pack(side = RIGHT)
    self.text = Text(self.slave, background = 'white')
    self.text.pack(side = TOP, fill = BOTH, expand = YES)
    self.slave.protocol('WM_DELETE_WINDOW', self.cancel)

  def go(self, myText = ''):
    self.text.insert('0.0', myText)
    self.newValue = None
    self.slave.grab_set()
    self.slave.focus_set()
    self.slave.wait_window()
    return self.newValue

  def accept(self):
    self.newValue = self.text.get('0.0', END)
    self.slave.destroy()

  def cancel(self):
    self.slave.destroy()

# класс диалогового окна выхода
class yesno:
  def __init__(self, master):
    self.slave = Toplevel(master)
    self.slave.title('exit dialog')
    self.slave.geometry('200x100+300+250')
    self.frame = Frame(self.slave)
    self.frame.pack(side = BOTTOM)
    self.yes_button = Button(self.frame,
                             text = 'yes',
                             command = self.yes)
    self.yes_button.pack(side = LEFT)
    self.no_button = Button(self.frame,
                            text = 'no',
                            command = self.no)
    self.no_button.pack(side = RIGHT)
    self.label = Label(self.slave)
    self.label.pack(side = TOP, fill = BOTH, expand = YES)
    self.slave.protocol('WM_DELETE_WINDOW', self.no)

  def go(self, title = '', message = ''):
    self.slave.title(title)
    self.label.configure(text = message)
    self.booleanValue = TRUE
    self.slave.grab_set()
    self.slave.focus_set()
    self.slave.wait_window()
    return self.booleanValue

  def yes(self):
    self.booleanValue = TRUE
    self.slave.destroy()

  def no(self):
    self.booleanValue = FALSE
    self.slave.destroy()

# создание окна
root = Tk()

# запуск окна
main(root) 
          
Мы приближаемся к концу начала. Рассмотрим еще несколько приемов, которые будут использоваться позднее в "реальном" приложении: кнопки принятия (accept) и отмены (cancel), а также "перехват" закрытия основного окна с соответствующим диалогом.

 
class child:
  def __init__(self, master):
    ...
    self.frame = Frame(self.slave)
    self.frame.pack(side = BOTTOM)
    self.accept_button = Button(self.frame,
                                text = 'accept',
                                command = self.accept)
    self.accept_button.pack(side = LEFT)
    self.cancel_button = Button(self.frame,
                                text = 'cancel',
                                command = self.cancel)
    self.cancel_button.pack(side = RIGHT)
    ...

  def accept(self):
    self.newValue = self.text.get('0.0', END)
    self.slave.destroy()

  def cancel(self):
    self.slave.destroy()

Пока все просто. Размещение в дочерних диалоговых окнах кнопки отмены (cancel) - довольно стандартный прием. При этом происходит уничтожение дочернего окна без изменения newValue.

 
# класс главного окна
class main:
  def __init__(self, master):
    ...
    self.master.protocol('WM_DELETE_WINDOW', self.exitMethod)
    ...

  def exitMethod(self):
    self.dialog = yesno(self.master)
    self.returnValue = self.dialog.go('question', 'Do you want to exit?')
    if self.returnValue:
      self.master.destroy()

    ...

# класс диалогового окна выхода
class yesno:
  def __init__(self, master):
    self.slave = Toplevel(master)
    self.slave.title('exit dialog')
    self.slave.geometry('200x100+300+250')
    self.frame = Frame(self.slave)
    self.frame.pack(side = BOTTOM)
    self.yes_button = Button(self.frame,
                             text = 'yes',
                             command = self.yes)
    self.yes_button.pack(side = LEFT)
    self.no_button = Button(self.frame,
                            text = 'no',
                            command = self.no)
    self.no_button.pack(side = RIGHT)
    self.label = Label(self.slave)
    self.label.pack(side = TOP, fill = BOTH, expand = YES)
    self.slave.protocol('WM_DELETE_WINDOW', self.no)

  def go(self, title = '', message = ''):
    self.slave.title(title)
    self.label.configure(text = message)
    self.booleanValue = TRUE
    self.slave.grab_set()
    self.slave.focus_set()
    self.slave.wait_window()
    return self.booleanValue

  def yes(self):
    self.booleanValue = TRUE
    self.slave.destroy()

  def no(self):
    self.booleanValue = FALSE
    self.slave.destroy()

"Перехват" закрытия пользователем главного окна немного сложнее. Для чего это может потребоваться? Допустим, в случае текстового редактора нам бы хотелось фиксировать ситуации, когда не был сохранен измененный файл, чтобы предохранить пользователя от нечаянного уничтожения плодов всех его усилий из-за преждевременного закрытия окна.

Секрет спрятался в строке self.master.protocol('WM_DELETE_WINDOW', self.exitMethod). WM_DELETE_WINDOW - это часть оконного протокола, которая обычно бывает связана с self.destroy(). Но в данной строке вместо этого она связывается с одним из разработанных нами методов. Итак, взгляните на def exitMethod(self): здесь вызывается message и выход происходит, только если message (сообщение) равно TRUE (ИСТИНА). message управляется целым новым классом yesno, обладающим своим собственным методом go [Я предупреждал, что go еще пригодится.].

Запустите window_09.py, чтобы увидеть нашу программу в действии.

Святой Грааль

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

Основной файл myWindow.py использует классы dialog и yesno так же, как и раньше, но, вместо того чтобы включить их в код, они импортируются из других файлов - myDialog.py и myBoolean.py.

Просмотрите завершающие части файлов myDialog.py и myBoolean.py. Там есть тестовая команда. Включать подобную команду в конец любого файла, который не выполняется напрямую, - стандартное правило. С помощью оператора if __name__ == '__main__': она проверяет, запущен файл из другой программы или сам по себе. В последнем случае открывается пустое окно, которое затем убирается с экрана командой root.withdraw(). Но именно к этому фиктивному окну привязывается рабочий код, позволяющий запускать данный файл. Это очень полезный инструмент для отладки. Использование тестов приводит к тому, что все модули становятся "исполняемыми".

Итак, запустим все три файла - myWindow.py, myDialog.py, myBoolean.py - и посмотрим, что произойдет.

myWindow.py
 

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *
from myBoolean import *
from myDialog import *

# класс главного окна
class main:
  def __init__(self, master):
    self.master = master
    self.master.title('main')
    self.master.geometry('400x300+200+150')
    self.button = Button(self.master,
                         text = 'dialog',
                         command = self.openDialog)
    self.button.pack(side = BOTTOM)
    self.text = Text(self.master,
                      background = 'white')
    self.text.pack(side = TOP,
                   fill = BOTH,
                   expand = YES)
    self.master.protocol('WM_DELETE_WINDOW',
                         self.exitMethod)
    self.master.mainloop()

  def openDialog(self):
    self.dialog = dialog(self.master)
    self.sendValue = self.text.get('0.0', END)
    self.returnValue = self.dialog.go(self.sendValue)
    if self.returnValue:
      self.text.delete('0.0', END)
      self.text.insert('0.0', self.returnValue)

  def exitMethod(self):
    self.dialog = yesno(self.master)
    self.myMssg = 'Do you want to exit?'
    self.returnValue = self.dialog.go(message = self.myMssg)
    if self.returnValue:
      self.master.destroy()

# создание окна
root = Tk()

# запуск окна
main(root)
          

myDialog.py
 

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *

# класс дочернего окна
class dialog:
  def __init__(self, master):
    self.top = Toplevel(master)
    self.top.title('dialog')
    self.top.geometry('200x100+300+250')
    self.frame = Frame(self.top)
    self.frame.pack(side = BOTTOM)
    self.accept_button = Button(self.frame,
                                text = 'accept',
                                command = self.accept)
    self.accept_button.pack(side = LEFT)
    self.cancel_button = Button(self.frame,
                                text = 'cancel',
                                command = self.cancel)
    self.cancel_button.pack(side = RIGHT)
    self.text = Text(self.top,
                     background = 'white')
    self.text.pack(side = TOP,
                   fill = BOTH,
                   expand = YES)
    self.top.protocol('WM_DELETE_WINDOW', self.cancel)

  def go(self, myText = '',):
    self.text.insert('0.0', myText)
    self.newValue = None
    self.top.grab_set()
    self.top.focus_set()
    self.top.wait_window()
    return self.newValue

  def accept(self):
    self.newValue = self.text.get('0.0', END)
    self.top.destroy()

  def cancel(self):
    self.top.destroy()

# тестовая команда
if __name__ == '__main__':
  root = Tk()
  root.withdraw()
  myTest = dialog(root)
  print myTest.go('Hello World!')
         

myBoolean.py
 

#!/usr/bin/python

# импортирование модулей python
from Tkinter import *

# класс диалогового окна выхода
class yesno:
  def __init__(self, master):
    self.slave = Toplevel(master)
    self.frame = Frame(self.slave)
    self.frame.pack(side = BOTTOM)
    self.yes_button = Button(self.frame, 
                             text = 'yes', 
                             command = self.yes)
    self.yes_button.pack(side = LEFT)
    self.no_button = Button(self.frame, 
                            text = 'no', 
                            command = self.no) 
    self.no_button.pack(side = RIGHT)   
    self.label = Label(self.slave)
    self.label.pack(side = TOP,
                    fill = BOTH,
                    expand = YES)
    self.slave.protocol('WM_DELETE_WINDOW', self.no)

  def go(self, title = 'question', 
               message = '[question goes here]', 
               geometry = '200x70+300+265'):
    self.slave.title(title)
    self.slave.geometry(geometry)
    self.label.configure(text = message)
    self.booleanValue = TRUE
    self.slave.grab_set()
    self.slave.focus_set()
    self.slave.wait_window()
    return self.booleanValue

  def yes(self):
    self.booleanValue = TRUE
    self.slave.destroy()

  def no(self):
    self.booleanValue = FALSE
    self.slave.destroy()

# тестовая команда
if __name__ == '__main__':
  root = Tk()
  root.withdraw()
  myTest = yesno(root)
  if myTest.go(message = 'Is it working?'):
    print 'Yes'
  else:
    print 'No'
          

Мини-руководство по Tkinter

Программа, которую мы составили, не сильно впечатляет - что-то вроде импровизации на тему "Hello World!". Предыдущая страница была названа "святым граалем" по двум причинам. Прежде всего, Python никак не связан со змеей. Это название взято из известного сериала Monty Python and the Quest for the Holy Grail. Кроме того, в легенде о Граале рассказывается о том, как нечто искали повсюду, и, когда, наконец, оно было найдено, оказалось, что ищущий обладал им с самого начала.

Это учебное руководство было написано пять или шесть лет тому назад. Тогда оно не было опубликовано, поскольку я посчитал его слишком примитивным. Позднее это руководство выручило меня самого, облегчив вспоминание Tkinter. Сейчас я сознаю, что оно уже содержало все необходимые идеи, чтобы начать использовать Tkinter для интересующих меня задач. Если вы похожи на меня, то будете применять Python для написания различных утилит, которые часто бывают полезны в повседневной жизни. Это превосходный язык для таких целей [а Tkinter - почти совершенный GUI]. Но для создания коммерческих мегапрограмм, чтобы заработать много денег, ни Python, ни Tkinter не годятся. [Как, впрочем, и само программирование в целом. Лучше идите учиться на менеджера!].

Перечислим элементы управления, с которыми мы уже успели познакомиться, вместе с их свойствами и методами, использованными в примерах. Общий стиль Tkinter - это размещение свойств внутри круглых скобок:
      Имя_класса(свойство1 = значение1, свойство2 = значение2)
и использование методов в роли особых "точечных" команд:
      Имя_класса.метод(параметры)

Класс Свойства Методы
Tk()    
Toplevel() parent title(string)
geometry(string)
mainloop()
destroy()
Button() parent
text = string
command = method
pack(side)
Text() parent pack(side, fill, expand)
insert(index, string)
get(index1, index2)
delete(index1, index2)
Label() parent configure(text = string
pack(side, fill, expand)
Frame() parent pack(side, fill)

Мы уже извлекли из этой демонстрационной программы всю пользу, какую только можно было. Настало время заняться созданием "настоящей программы" [более крупного "Hello World!] - на этот раз - текстового редактора. В ходе работы мы повстречаемся с новыми элементами управления: линейками прокрутки, линейками меню и кнопками меню, выпадающими списками, кнопками-флажками и т.п. Приятный сюрприз: за исключением пары ухищрений вы уже умеете пользоваться ими. Цель нового проекта - разработка работающего текстового редактора [если хотите его испытать, запустите editor.py]. Это учебное руководство посвящено только Tkinter, а не всему Python. Таким образом, хотя в предлагаемом вашему вниманию коде будут программные функции, реализованные на Python, обсуждение будет сфокусировано преимущественно на интерфейсе Tkinter. И снова вы будете удивлены. Даже если вы никогда раньше не работали с Python, подавляющая часть кода будет понятной.

Итак, пришло время переходить к следующей части руководства.

II. Управление файлами

В Основах Tkinter мы узнали, как выводить на экран окно, использовать классы Python, создавать дочерние окна и налаживать взаимодействие между родительскими и дочерними окнами. Мы познакомились со следующими элементами управления: Tk(), Toplevel(), Button(), Text(), Label() и Frame(). В этом руководстве нам предстоит сконструировать простой, работающий текстовый редактор. В процессе придется поработать с некоторыми другими элементами управления Tkinter и научиться как связывать разные части интерфейса воедино.

Первый шаг в разработке графического интерфейса - получение нужных окон:

Главное окно редактора
Меню включает в себя пункты, которые, считается, должны быть в файловом меню любого редактора: new (новый), open (открыть), save (сохранить) и save as (сохранить как). Нетрудно определить, для каких из них нужен диалог - для тех, где есть "...":

Диалоговое окно Open File (открыть файл)
Диалоговое окно Save As File (сохранить как файл)
Диалоги file.open и file.save выглядят одинаково. Оба они легко могут быть описаны с помощью одного класса, если применить небольшой фокус при инициализации диалогового объекта:

Диалог подтверждения
Он нам потребуется при выходе из программы или чтобы задать вопрос: "Этот файл был изменен. Не хотите ли сохранить изменения?":

Диалог сообщений
Через него пользователь будет предупреждаться о возможных проблемах:

Итак, на следующих нескольких страницах мы создадим классы для этих окон [диалог подтверждения уже есть, например yesno], а в завершение свяжем эти диалоги друг с другом.

Содержание
Главный текстовый элемент управления редактора
Главное меню редактора
Элемент управления Text, линейка прокрутки Scrollbar и управление закрытием окна
Диалоговое окно для работы с файлами
Элемент управления Listbox для работы со списками
'R and R'
Методы диалога для работы с файлами
Диалоговые объекты для открытия и сохранения файлов
Диалоговые модули многократного использования
Текстовый редактор: собирая все воедино
Текстовый редактор: подробнее о деталях

Главный текстовый элемент управления редактора

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

Запустите editor_01.py.

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

Главному окну нашего текстового редактора нужны панель меню с двумя заголовками главного меню [file (файл) и edit (правка)], элемент управления text и линейка прокрутки. Использование метода pack для создания интерфейса Tkinter подразумевает, что элементы управления будут размещаться в определенной последовательности. Посмотрите анимацию, потом взгляните на разъяснение и код:

  1. Инициализируем главное окно [Tk()].
      def __init__(self, master):
        self.master = master
        self.master.title('editor')
        self.master.iconname('editor')
        self.master.geometry('600x400+100+100')
  2. Добавляем фрейм для панели меню, который является дочерним по отношению к главному окну [Frame( )].
        self.myBar = Frame(self.master, relief = RAISED, bd=2)
  3. Добавляем в меню пункт file [Menubutton()], дочерний по отношению к фрейму панели меню, и упаковываем [pack] его к левой стороне [LEFT].
        self.fileMenu()
        self.editMenu()
  4. Теперь упаковываем (pack) заполненный фрейм панели меню к верхней [TOP] стороне окна. Пусть фрейм заполнит все доступное пространство по оси X. Сделаем фрейм расширяемым при изменении размеров окна.
        self.myBar = Frame(self.master, relief = RAISED, bd=2)
  5. Сейчас добавим дочернюю к главному окну линейку прокрутки [Scrollbar()]. Упакуем ее [pack] к правой [RIGHT] стороне. Пусть она заполнит все доступное пространство по оси X. И сделаем ее расширяемой при изменении размеров окна.
        self.myScroll = Scrollbar(self.master)
        self.myScroll.pack(side=RIGHT, fill=Y)
  6. Наконец, добавляем элемент управления Text как дочерний к главному окну [Text()]. Упаковываем его к левой [LEFT] стороне. Пусть он заполнит свободное место вдоль обоих [BOTH] осей. И сделаем его расширяемым при изменении размеров окна.
        self.myText = Text(self.master, 
                           background = 'white', 
                           height = 30, 
                           width = 90,
                           yscrollcommand=(self.myScroll, 'set'))
        self.myText.pack(side=LEFT, fill=BOTH, expand=YES)
Сначала это может показаться трудным для понимания, но, если читать вдумчиво, смысл уловить можно. Со временем это будет получаться автоматически. "Ну-ка, посмотрим, что у нас идет вначале?" Вот полный рабочий код. [Просто представьте, что после fileMenu() идет editMenu(). Я опустил этот кусок кода для ясности. Довольно скоро мы с ним встретимся.]:

класс main

# класс родительского окна
class main:
  def __init__(self, master):
    self.master = master
    self.master.title('editor')
    self.master.iconname('editor')
    self.master.geometry('600x400+100+100')
    self.myBar = Frame(self.master, relief = RAISED, bd=2)
    self.fileMenu()
    self.myBar.pack(side = TOP, expand = YES, fill = X)
    self.myScroll = Scrollbar(self.master)
    self.myScroll.pack(side=RIGHT, fill=Y)
    self.myText = Text(self.master, 
                       background = 'white', 
                       height = 30, 
                       width = 90,
                       yscrollcommand=(self.myScroll, 'set'))
    self.myText.pack(side=LEFT, fill=BOTH, expand=YES)
    self.myScroll.configure(command = self.myText.yview)
    self.master.protocol('WM_DELETE_WINDOW', self.exitMethod)
    self.master.mainloop()

    

Строки fileMenu() создают меню. Это метод будет описан на следующей странице. После этого мы исследуем взаимосвязь между линейкой прокрутки и элементом управления Text, а также exitMethod.

Главное меню редактора

На предыдущей странице командой fileMenu() в панели главного меню был создан пункт file. Он вызывает один из методов класса main, чтобы вставить кнопку меню. Так сделано главным образом для ясности [чтобы не было "лишнего" кода].

Чтобы создать главное меню, в Tkinter используются три элемента управления: фрейм Frame() как контейнер для кнопок главного меню, кнопка меню Menubutton() для каждого пункта главного меню и меню Menu(), отображающее команды подменю для каждого пункта главного меню. В коде, приведенном ниже:

метод fileMenu

# добавление меню file в панель меню
  def fileMenu(self):
    mButton = Menubutton(self.myBar, text = 'file  ', underline = 0)
    mButton.pack(side = LEFT)
    menu = Menu(mButton, tearoff = 0)
    menu.add_command(label = 'new', command = self.getMessage)
    menu.add_command(label = 'open...', command = self.getMessage)
    menu.add_separator({})
    menu.add_command(label = 'save', command = self.getMessage)
    menu.add_command(label = 'save as...', command = self.getMessage)
    mButton.configure(menu = menu)
    return mButton
    
Все вызовы команд command в этих примерах указывают на одну и ту же штуку [self.getMessage] - обобщенное или родовое [generic] окно сообщений. Позднее мы вернемся назад и добавим настоящие команды, сделав это приложение функциональным.

Элемент управления Text, линейка прокрутки Scrollbar и управление закрытием окна

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

класс main

# класс родительского окна
class main:
  def __init__(self, master):
    self.master = master
    self.master.title('editor')
    self.master.iconname('editor')
    self.master.geometry('600x400+100+100')
    self.myBar = Frame(self.master, relief = RAISED, bd=2)
    self.fileMenu()
    self.myBar.pack(side = TOP, expand = YES, fill = X)
    self.myScroll = Scrollbar(self.master)
    self.myScroll.pack(side=RIGHT, fill=Y)
    self.myText = Text(self.master, 
                       background = 'white', 
                       height = 30, 
                       width = 90,
                       yscrollcommand=(self.myScroll, 'set'))
    self.myText.pack(side=LEFT, fill=BOTH, expand=YES)
    self.myScroll.configure(command = self.myText.yview)
    self.master.protocol('WM_DELETE_WINDOW', self.exitMethod)
    self.master.mainloop()
    

yscrollcommand=(self.myScroll, 'set')
Эта строчка говорит, что элемент управления Text управляется линейкой прокрутки Scrollbar.
self.myScroll.configure(command = self.myText.yview)
Эта строка говорит, что линейка прокрутки Scrollbar должна отслеживать текст в текстовом элементе управления. Метод configure() дает возможность вносить изменения в установки свойств. Таким образом self.myScroll = Scrollbar(self.master) фактически становится self.myScroll = Scrollbar(self.master, command = self.myText.yview) [но эта команда не может быть определена, пока не будет объявлен элемент управления Text()].

Строка self.master.protocol('WM_DELETE_WINDOW', self.exitMethod) передает программе управление над закрытием окна. Команда WM_DELETE_WINDOW это часть протокола окна, вызываемого при нажатии кнопки закрытия. Эта строчка перехватывает WM_DELETE_WINDOW и перенаправляет его методу self.exitMethod. Это дает нам возможность спросить у пользователя, действительно ли он хочет выйти, вызвав соответстующий диалог.

метод exitMethod

# выход из редактора
  def exitMethod(self):
    self.dialog = yesno(self.master)
    self.myMssg = 'Do you want to exit?'
    self.returnValue = self.dialog.go(message = self.myMssg)
    if self.returnValue:
      self.master.destroy()
    

Итак, у нас есть главное окно текстового редактора с несколькими работающими элементами управлениями. Пришло время заняться диалоговыми окнами меню file.

Диалоговое окно для работы с файлами

Замечание: Сейчас мы создадим диалоговое окно для работы с файлами. Хотя оно и большое, но в основном это вариация на одну и ту же тему. Его код длинный [в частности из-за того, что для того чтобы модуль стал автономным, в него дополнительно включены несколько диалоговых классов]. Весь код целиком [files_01.py] можно просмотреть в выпадающем окне с помощью текстовой иконки, расположенной выше [ниже]. [В русском переводе текст программы вывести в выпадающее окно нельзя. Пожалуйста, для просмотра открывайте нужный код в любом текстовом редакторе. Прим. пер.]

Запустите файл files_01.py. Немножко с ним поиграйте [он должен быть полностью функциональным]. Особенно обратите внимание на то, как работает выпадающее меню каталогов. Измените размеры окна, посмотрите, как при этом будут вести себя элементы управления. Значок ".." в папке переместит вас на один уровень вверх. Для операций в папке и со списками файлов используйте однократные щелчки мышкой.

Здесь, как и в любом окне Tkinter, порядок имеет значение. В этом диалоге нам потребуется пять фреймов Frame(), чтобы скомпоновать окно. Иллюстрации могут подсказать все, что нам нужно, тем не менее, ниже приводятся подробные комментарии. Они довольно многословны [иными словами, скучны]. Если суть схвачена, дальше достаточно бегло просмотреть список и просто читать сам код:

  1. Инициализируем окно Toplevel() как дочернее по отношению к главному окну.
        self.toplevel = Toplevel(master)
        self.toplevel.geometry('400x300+200+150')
  2. Создаем дочерний по отношению к окну Toplevel фрейм [bottomFrame = Frame()] для командных кнопок. Упаковываем [pack] его к нижней [BOTTOM] стороне окна.
        self.bottomFrame = Frame(self.toplevel)
        self.bottomFrame.pack(side=BOTTOM, pady=5)
    Параметры padx и pady в методе pack() добавляют пространства вокруг элементов управления по осям X и Y, украшая их таким образом.

  3. Добавляем кнопку accept [Button()] как дочернюю по отношению к bottomFrame и упаковываем [pack] ее к левой [LEFT] стороне.
        self.accept_button = Button(self.bottomFrame, 
                                    width=7, 
                                    text="accept", 
                                    command = self.accept)
        self.accept_button.pack(side=LEFT, expand=YES, padx=8)
    Свойство width класса Button() фиксирует ширину кнопки. Это тоже для красоты.

  4. Создаем кнопку cancel [Button( )] как дочернюю по отношению к bottomFrame и упаковываем [pack] ее к прaвой [RIGHT] стороне.
        self.cancel_button = Button(self.bottomFrame, 
                                    width=7, 
                                    text="cancel", 
                                    command = self.cancel)
        self.cancel_button.pack(side=RIGHT, expand=YES, padx=8)
    Почему не нужно, чтобы фрейм заполнял все доступное пространство по оси X? Дело в том, что Toplevel() того же самого цвета и его границы невидимы. Незаполнение сохраняет неизменным расстояние между кнопками [еще одно "украшательство"]. Один фрейм готов - четыре на подходе.

  5. Добавляем фрейм [selectionFrame = Frame()] для поля ввода, дочернего по отношению к окну верхнего уровня Toplevel. Упаковываем [pack] его к нижней [BOTTOM] границе окна. Разрешаем фрейму заполнить все свободное место по оси X.
        self.selectionFrame = Frame(self.toplevel)
        self.selectionFrame.pack(side=BOTTOM, padx=36, pady=5, fill=X)
    Вероятно, я мог бы назвать этот фрейм next_up_from_the_bottomFrame [фрейм_следующий_вверх_от_нижнего], но это было бы уже слишком.

  6. Введем элемент управления entry [selection = Entry()] для выбора файла, дочерний по отношению к фрейму selectionFrame, и упакуем его к левой [LEFT] стороне этого фрейма. Пусть он заполнит доступное пространство по оси X. Сделаем этот элемент управления расширяемым при изменении размеров окна.
        self.selection = Entry(self.selectionFrame, background = 'white')
        self.selection.pack(side=LEFT, expand=YES, fill=X)
    Entry() - это новый элемент управления для нас. Фактически это просто поле, содержащее одну текстовую строку для ввода или для отображения на экране. Два фрейма готовы - осталось еще три.

  7. Переходим к верхней части окна. Создаем фрейм [directoryFrame = Frame()] для меню каталогов как дочерний по отношению к окну верхнего уровня Toplevel. Упаковываем [pack] его к верхней [TOP] стороне этого окна.
        self.directoryFrame.pack(side=TOP, pady=8)
        self.directory = self.createMenu(self.directoryFrame)
  8. Вводим выпадающее меню каталогов как дочернее по отношению к фрейму directoryFrame и упаковываем [pack] его к левой [LEFT] стороне этого фрейма. Пусть этот элемент управления заполняет свободное место по оси X. Сделаем его расширяемым при изменении размеров окна.
        self.directory = self.createMenu(self.directoryFrame)
        self.directory.pack(side=LEFT, expand=YES, fill=X)
    Мы создадим это выпадающее меню в другом методе этого класса. Три фрейма долой.

  9. Добавляем фрейм [labelFrame = Frame()] для текстовых надписей 'folders:' и ' files: ' как дочерний по отношению к окну верхнего уровня Toplevel. Упаковываем [pack] его к верхней [TOP] стороне этого окна.
        self.labelFrame = Frame(self.toplevel)
        self.labelFrame.pack(side=TOP, padx=5, fill=X)
  10. Введем элемент управления [folderLabel = Label()] для надписи 'folders:' как дочерний по отношению к labelFrame и упаковываем [pack] его к левой [LEFT] стороне этого фрейма. Разрешаем этому элементу заполнить все свободное место в направлении оси X. Делаем его расширяемым при изменении размеров окна.
        self.folderLabel = Label(self.labelFrame, text = 'folders:')
        self.folderLabel.pack(side=LEFT, expand=YES, fill=X)
  11. Добавляем элемент управления [fileLabel = Label()] для надписи ' files: ' как дочерний по отношению к labelFrame и упаковываем [pack] его к правой [RIGHT] стороне этого фрейма. Разрешаем этому элементу заполнить все свободное место в направлении оси X. Делаем его расширяемым при изменении размеров окна. Остался только один фрейм!
        self.fileLabel = Label(self.labelFrame, text = ' files: ')
        self.fileLabel.pack(side=RIGHT, expand=YES, fill=X)
  12. Создаем фрейм [middleFrame = Frame()] для списков папок и файлов, дочерний по отношению к окну верхнего уровня Toplevel. Разрешаем фрейму заполнять доступное пространство в направлении обоих [BOTH] осей. Делаем фрейм расширяемым при изменении размеров окна.
        self.middleFrame = Frame(self.toplevel)
        self.middleFrame.pack(expand=YES, padx=5, fill=BOTH)
  13. Введем еще линейку прокрутки, дочернюю по отношению к главному окну [filesBar = Scrollbar()]. Упаковываем [pack] ее к правой [RIGHT] стороне. Разрешим ей заполнять доступное пространство по оси Y.
        self.filesBar = Scrollbar(self.middleFrame)
        self.filesBar.pack(side=RIGHT, fill=Y)
  14. Добавим дочерний по отношению к главному окну список [files = Listbox()]. Упакуем [pack] его к правой [RIGHT] стороне. Разрешим ему заполнять свободное место в направлении обоих [BOTH] осей. Делаем список расширяемым при изменении размеров окна.
        self.files = Listbox(self.middleFrame, 
                             background = 'white',
                             exportselection=0,
                             yscrollcommand=(self.filesBar, 'set'))
        self.files.pack(side=RIGHT, expand=YES, fill=BOTH)
    Listbox() тоже новый для нас элемент управления. Он содержит списки [да ну!]. Мы вскоре увидим, как он работает.

  15. Создаем линейку прокрутки, дочернюю по отношению ко главному окну [subBar = Scrollbar()]. Упаковываем [pack] ее к левой [LEFT] стороне. Пусть она заполняет все доступное пространство в направлении оси Y.
        self.subBar = Scrollbar(self.middleFrame)
        self.subBar.pack(side=LEFT, fill=Y)
  16. Добавим окно списка как дочернее по отношению к главному окну [subDirectory = Listbox()]. Упакуем [pack] его к левой [LEFT] стороне. Пусть оно заполняет доступное пространство по обеим [BOTH] осям. Сделаем список расширяемым при изменении размеров окна.
        self.subDirectory = Listbox(self.middleFrame, 
                                    background = 'white', 
                                    exportselection=0,
                                    yscrollcommand=(self.subBar, 'set'))
        self.subDirectory.pack(side=LEFT, expand=YES, fill=BOTH)
Ну, вот и все! Это, между прочим, одно из сложных окон. Разобравшись с ним, вы разберетесь и с любым другим.

Теперь взглянем на весь код целиком:

класс files

# класс диалога для работы с файлами
class files:
  def __init__(self, master = None):
    self.toplevel = Toplevel(master)
    self.toplevel.geometry('400x300+200+150')
    self.bottomFrame = Frame(self.toplevel)
    self.bottomFrame.pack(side=BOTTOM, pady=5 )
    self.accept_button = Button(self.bottomFrame, 
                                width=7, 
                                text="accept", 
                                command = self.accept)
    self.accept_button.pack(side=LEFT, expand=YES, padx=8)
    self.cancel_button = Button(self.bottomFrame, 
                                width=7, 
                                text="cancel", 
                                command = self.cancel)
    self.cancel_button.pack(side=RIGHT, expand=YES, padx=8)
    self.selectionFrame = Frame(self.toplevel)
    self.selectionFrame.pack(side=BOTTOM, padx=36, pady=5, fill=X)
    self.selection = Entry(self.selectionFrame, background = 'white')
    self.selection.pack(side=LEFT, expand=YES, fill=X)
    self.directoryFrame = Frame(self.toplevel)
    self.directoryFrame.pack(side=TOP, pady=8)
    self.directory = self.createMenu(self.directoryFrame)
    self.directory.pack(side=LEFT, expand=YES, fill=X)
    self.labelFrame = Frame(self.toplevel)
    self.labelFrame.pack(side=TOP, padx=5, fill=X)
    self.folderLabel = Label(self.labelFrame, text = 'folders:')
    self.folderLabel.pack(side=LEFT, expand=YES, fill=X)
    self.fileLabel = Label(self.labelFrame, text = ' files: ')
    self.fileLabel.pack(side=RIGHT, expand=YES, fill=X)
    self.middleFrame = Frame(self.toplevel)
    self.middleFrame.pack(expand=YES, padx=5, fill=BOTH)
    self.filesBar = Scrollbar(self.middleFrame)
    self.filesBar.pack(side=RIGHT, fill=Y)
    self.files = Listbox(self.middleFrame, 
                         background = 'white',
                         exportselection=0,
                         yscrollcommand=(self.filesBar, 'set'))
    self.files.pack(side=RIGHT, expand=YES, fill=BOTH)
    btags = self.files.bindtags()
    self.files.bindtags(btags[1:] + btags[:1])
    self.files.bind('<ButtonRelease-1>', self.files_select_event)
    self.files.bind('<Double-ButtonRelease-1>', self.files_double_event)
    self.filesBar.configure(command = self.files.yview)
    self.subBar = Scrollbar(self.middleFrame)
    self.subBar.pack(side=LEFT, fill=Y)
    self.subDirectory = Listbox(self.middleFrame, 
                                background = 'white', 
                                exportselection=0,
                                yscrollcommand=(self.subBar, 'set'))
    self.subDirectory.pack(side=LEFT, expand=YES, fill=BOTH)
    self.subBar.configure(command = self.subDirectory.yview)
    btags = self.subDirectory.bindtags()
    self.subDirectory.bindtags(btags[1:] + btags[:1])
    self.subDirectory.bind('<ButtonRelease-1>', self.subDirectory_select_event)
    

Работа с элементами управления Listbox() потребует некоторых разъяснений [на следующей странице].

Элемент управления Listbox для работы со списками

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

Сначала, элемент управления Listbox(): процесс привязки линейки прокрутки Scrollbar() к элементу управления Listbox() тот же самый, что использовался в случае с Textbox(). В Listbox() хранятся упорядоченные списки, которые можно прокручивать и в которых можно выбирать отдельные пункты. Как вносить в списки названия файлов и папок будет рассмотрено позднее. Сейчас же мы рассмотрим процесс связывания элементов списка с событиями [такими как щелчок кнопкой мыши], вызывающими методы для обработки выделения. Говоря по-русски: "Как выбрать каталог или файл".

Снова код. Настало время взглянуть на списки [Listboxes]:

класс files

# класс диалога для работы с файлами
class files:
  def __init__(self, master = None):
    self.toplevel = Toplevel(master)
    self.toplevel.geometry('400x300+200+150')
    self.bottomFrame = Frame(self.toplevel)
    self.bottomFrame.pack(side=BOTTOM, pady=5 )
    self.accept_button = Button(self.bottomFrame, 
                                width=7, 
                                text="accept", 
                                command = self.accept)
    self.accept_button.pack(side=LEFT, expand=YES, padx=8)
    self.cancel_button = Button(self.bottomFrame, 
                                width=7, 
                                text="cancel", 
                                command = self.cancel)
    self.cancel_button.pack(side=RIGHT, expand=YES, padx=8)
    self.selectionFrame = Frame(self.toplevel)
    self.selectionFrame.pack(side=BOTTOM, padx=36, pady=5, fill=X)
    self.selection = Entry(self.selectionFrame, background = 'white')
    self.selection.pack(side=LEFT, expand=YES, fill=X)
    self.directoryFrame = Frame(self.toplevel)
    self.directoryFrame.pack(side=TOP, pady=8)
    self.directory = self.createMenu(self.directoryFrame)
    self.directory.pack(side=LEFT, expand=YES, fill=X)
    self.labelFrame = Frame(self.toplevel)
    self.labelFrame.pack(side=TOP, padx=5, fill=X)
    self.folderLabel = Label(self.labelFrame, text = 'folders:')
    self.folderLabel.pack(side=LEFT, expand=YES, fill=X)
    self.fileLabel = Label(self.labelFrame, text = ' files: ')
    self.fileLabel.pack(side=RIGHT, expand=YES, fill=X)
    self.middleFrame = Frame(self.toplevel)
    self.middleFrame.pack(expand=YES, padx=5, fill=BOTH)
    self.filesBar = Scrollbar(self.middleFrame)
    self.filesBar.pack(side=RIGHT, fill=Y)
    self.files = Listbox(self.middleFrame, 
                         background = 'white',
                         exportselection=0,
                         yscrollcommand=(self.filesBar, 'set'))
    self.files.pack(side=RIGHT, expand=YES, fill=BOTH)
    btags = self.files.bindtags()
    self.files.bindtags(btags[1:] + btags[:1])
    self.files.bind('<ButtonRelease-1>', self.files_select_event)
    self.files.bind('<Double-ButtonRelease-1>', self.files_double_event)
    self.filesBar.configure(command = self.files.yview)
    self.subBar = Scrollbar(self.middleFrame)
    self.subBar.pack(side=LEFT, fill=Y)
    self.subDirectory = Listbox(self.middleFrame, 
                                background = 'white', 
                                exportselection=0,
                                yscrollcommand=(self.subBar, 'set'))
    self.subDirectory.pack(side=LEFT, expand=YES, fill=BOTH)
    self.subBar.configure(command = self.subDirectory.yview)
    btags = self.subDirectory.bindtags()
    self.subDirectory.bindtags(btags[1:] + btags[:1])
    self.subDirectory.bind('<ButtonRelease-1>', self.subDirectory_select_event)
    

Прежде всего, отложим в сторону код, вызывающий недоумение.


    btags = self.files.bindtags()
    self.files.bindtags(btags[1:] + btags[:1])
    
Не думайте об этом! Этот код перестраивает кортеж bindtag для Listbox() из чего-то вроде ('.-1208941972', 'Listbox', '.', 'all') в ('Listbox', '.', 'all', '-1208941972'). Как бы то ни было, без этой операции привязка к Listbox() в следующей строчке корректно работать не будет. Уверен, что это взято из давно забытой книги, которую я читал в прошлой жизни. Пока что относитесь к этому как к тому, что по какой-то причине нужно для правильной работы привязки к Listbox(). Если хотите узнать саму причину, закомментируйте эти две строчки и запустите код на выполнение...


    self.files.bind('<ButtonRelease-1>', self.files_select_event)
    self.files.bind('<Double-ButtonRelease-1>', self.files_double_event)
    self.subDirectory.bind('<ButtonRelease-1>', self.subDirectory_select_event)
    
А вот в этих строках заключен большой смысл. Метод bind() привязывает события, связанные с нажатием кнопки мыши, к элементам управления. В данном случае одиночный щелчок мышкой по списку files вызывает событие self.files_select_event, двойной щелчок по списку files вызывает событие self.files_double_event и одиночный щелчок по списку subdirectory вызывает событие self.subDirectory_select_event. Все три события вызывают методы класса files, которые мы будем обсуждать на следующей странице.

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


    self.directoryFrame = Frame(self.toplevel)
    self.directoryFrame.pack(side=TOP, pady=8)
    self.directory = self.createMenu(self.directoryFrame)
    self.directory.pack(side=LEFT, expand=YES, fill=X)
    

Это вызов метода создания меню createMenu(), такой же, какой был использован в главном окне. Только меню пустое! Строк add_command() нет.

Это меню будет заполнено другим методом - refreshDisplay() [вызываемым при инициализации диалога File и каждый раз, когда что-нибудь в окне меняется во время выполнения программы]. Этот метод [приведенный ниже] создает иерархический список и добавляет список подкаталогов в меню с вызовом evaluateDirectory() для каждого из них.


# добавление элемента управления меню каталогов в диалог для работы с файлами
  def createMenu(self, master = None):
    self.button = Menubutton(master, indicatoron = 1, relief = RAISED)
    self.button.pack()
    self.menu = Menu(self.button, tearoff = 0)
    self.button.configure(menu = self.menu)
    return self.button

# обновление изображения при выделении пользователем новых элементов
  def refreshDisplay(self, pathString):
    self.myDirectory = pathString
    myList = self.parseDirectory(self.myDirectory)
    self.button.configure(text = myList[0])
    self.menu.delete(0, END)
    for i in myList[1:]:
      self.menu.add_command(label = i, command = self.evaluateDirectory)
...
    

Это не руководство по всему Python, только по программированию на Tkinter. Однако, на примере этого класса [files] мы познакомились по крайней мере с азами всех его методов, увидели, как можно сконструировать автономный графический браузер файлов с различными событиями, управляющими поведением программы, и методами, заставляющими все это работать. Кроме того, мы введем принцип объектно-ориентированного программирования под названием "наследование", когда будем обсуждать, как использовать этот класс и для открытия, и для сохранения файлов.

А сейчас, запустите на выполнение программу и ознакомьтесь со сводкой событий и вызываемых ими методов.

Элемент управления Событие Вызываемый метод
Toplevel() щелчок мышкой по закрывающей кнопке WM_DELETE_WINDOW
accept_button() щелчок мышкой по кнопке accept()
cancel_button() щелчок мышкой по кнопке cancel()
directory Menubutton() щелчок мышкой по пункту меню evaluateDirectory
subDirectory Listbox() <ButtonRelease-1> - кнопка мыши 1 отпущена subDirectory_select_event
files Listbox() <ButtonRelease-1> - кнопка мыши 1 отпущена files_select_event
files Listbox() <Double-ButtonRelease-1> - кнопка мыши 1 отпущена
после двойного щелчка
files_double_event

'R and R'

['R and R' - 'Rest and Recuperation' - "отдых и восстановление"; сокращение, используемое американскими военными.- Прим. пер.]

Если вы - опытный программист или уже знаете Python и просто ищете здесь чего-нибудь новенького, эта последняя часть вряд ли затруднила вас и вы готовы читать дальше. Но если все сказанное было новым и трудным для понимания, сделайте сейчас перерыв. Сходите, посмотрите Comedy Central [Американский кабельный юмористический телеканал. На нем, в частности, был создан известный анимационный сериал "Южный парк".- Прим. пер.], вернемся к этому позже.

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

Диалог для работы с файлами:
Сперва добавим в диалог для работы с файлами код, который соединит находящееся сверху меню каталогов со списком подкаталогов слева, со списком файлов справа и с полем ввода выбранных файлов, находящимся снизу. Хотя внутри и вне разных элементов управления задействовано множество методов, в центре всего находятся методы refreshDisplay и parseDirectory. При смене каталога [либо через верхнее меню папок, либо через находящийся слева список подкаталогов] вызывается метод refreshDisplay. В свою очередь refreshDisplay использует метод parseDirectory для построения списка для верхнего меню, обновляет его, затем корректирует все остальные элементы управления. Именно refreshDisplay приводит в действие диалог для работы с файлами. Итак, первоочередная задача: придать диалогу для работы с файлами форму автономного объекта [объекта на экране и компьютерного класса/объекта].

Диалоги для открытия и сохранения файлов:
В этой части руководства формально описывается объектно-ориентированное программирование. Мы воспользуемся полиморфизмом и наследованием для создания двух различных классов - openDialog (диалог для открытия файлов) и saveDialog (диалог для сохранения файлов), выполняющих соответствующие функции.

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

Делаем мир красивее: меню, горячие клавиши и панели инструментов
Это тема всей следующей части руководства.

Если то, что мы уже сделали, осталось неясным, вернитесь к прошлому материалу, прежде чем возобновлять чтение. Это наиболее важная часть.

Методы диалога для работы с файлами

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

# добавление меню для управления каталогами в диалог для работы с файлами
  def createMenu(self, master = None):
    self.button = Menubutton(master, indicatoron = 1, relief = RAISED)
    self.button.pack()
    self.menu = Menu(self.button, tearoff = 0)
    self.button.configure(menu = self.menu)
    return self.button

2. set_selection(file) - это функция, получающая в качестве аргумента имя файла и выводящая его на экран через переменную selection элемента управления Entry().

# переменной selection присваивается значение, равное имени файла
  def set_selection(self, file):
    self.selection.delete('0', END)
    self.selection.insert('0', file)

3. get_selection осуществляет обратный процесс - возвращает имя файла из переменной selection элемента управления Entry().

# возвращает имя выделенного файла из переменной selection
  def get_selection(self):
    return self.selection.get()

4. accept реагирует на нажатие кнопки accept_button таким образом, что записывает имя файла из переменной selection элемента управления Entry() в новую переменную newValue, затем закрывает окно.

# при нажатии мышкой на кнопку accept
  def accept(self):
    self.newValue = self.get_selection()
    self.toplevel.destroy()

5. cancel реагирует на нажатие кнопки cancel_button, закрывая окно и не присваивая переменной newValue нового значения.

# при нажатии мышкой на кнопку cancel
  def cancel(self):
    self.toplevel.destroy()

6. evaluateDirectory реагирует на выбор директории в меню directory и передает ее имя refreshDisplay().

# обработка строки с именем текущего каталога  
  def evaluateDirectory(self):
    self.refreshDisplay(self.menu.entrycget(ACTIVE, 'label'))

7. subDirectory_select_event реагирует на выбор подкаталога в списке subDirectory и передает его имя refreshDisplay().

# при щелчке мышкой по списку подкаталогов
  def subDirectory_select_event(self, event):
    subdir = self.subDirectory.get('active')
    self.refreshDisplay(os.path.normpath(os.path.join(self.myDirectory, subdir)))

8. files_select_event реагирует на выбор файла в списке files и выводит его имя в переменную selection элемента управления Entry().

# при щелчке мышкой по списку файлов
  def files_select_event(self, event):
    self.set_selection(self.files.get('active'))

9. files_double_event реагирует на двойной щелчок мышью по файлу в списке files и выводит его имя на экран через переменную selection элемента управления Entry(); затем вызывается accept.

# при двойном щелчке мышкой по списку файлов
  def files_double_event(self, event):
    self.accept()

Теперь о важных методах.

Сперва взглянем на метод go. Он инициализирует окно/объект. Обратите внимание, что он по умолчанию начинает свою работу с текущего рабочего каталога. Если передать ему startPath, в элементе управления отобразится заданный файл или подкаталог. Переменная newValue вначале остается пустой. В ней будет храниться имя файла, если его запишет в нее класс files.

10. go инициализирует окно, отображает имя файла в поле selection элемента управления Entry(), затем передает имя начальной директории refreshDisplay(). Окно остается модальным пока не будет закрыто, затем возвращается newValue.

# запуск диалога
  def go(self, title = 'file dialog', startPath = os.getcwd()):
    self.newValue = None
    self.toplevel.title(title)
    if os.path.isfile(startPath):
      startPath, myFile = os.path.split(startPath)
      self.refreshDisplay(startPath)
      self.set_selection(myFile)
    else:
      self.refreshDisplay(startPath)
    self.toplevel.grab_set()
    self.toplevel.focus_set()
    self.toplevel.wait_window()
    return self.newValue

Методы, благодаря которым работа модуля становится видимой, - это refreshDisplay и parseDirectory, обновляющие визуальные элементы управления, когда ничего не изменяется.
11. Метод refreshDisplay вызывается либо когда меняется директория в меню каталогов, либо при выборе нового подкаталога в списке subDirectory.

  • В первую очередь нужно обновить меню directory. refreshDisplay пересылает новый путь методу parseDirectory, который возвращает иерархический список элементов адреса каталога [в обратном порядке]. Идущий первым элемент списка [элементы которого составляют текущий путь] отображается на самой кнопке меню. Остальные элементы попадают в меню [напоминаю, что myList[1:] означает "весь список, начиная со второго по счету элемента и до последнего"].

    # изображение обновляется, когда пользователь выбирает новые элементы
      def refreshDisplay(self, pathString):
        self.myDirectory = pathString
        myList = self.parseDirectory(self.myDirectory)
        self.button.configure(text = myList[0])
        self.menu.delete(0, END)
        for i in myList[1:]:
          self.menu.add_command(label = i, command = self.evaluateDirectory)

  • Во вторую очередь нужно получить список всего, что содержится в текущей папке, а потом разделить его на два списка - подкаталогов [subdirs] и файлов [matchingfiles]...

        try:
          names = os.listdir(self.myDirectory)
        except os.error:
          self.toplevel.bell()
          return
        names.sort()
        if (self.myDirectory == os.sep) or (not os.sep in self.myDirectory):
          subdirs = []
        else:
          subdirs = [os.pardir]
        matchingfiles = []
        for name in names:
          fullname = os.path.join(self.myDirectory, name)
          if os.path.isdir(fullname) and name[0] != '.':
            subdirs.append(name)
          elif name[0] != '.':
            matchingfiles.append(name)

  • сохраняем список подкаталогов [subdirs] в переменной subDirectory элемента управления Listbox...

        self.subDirectory.delete(0, END)
        for name in subdirs:
          self.subDirectory.insert(END, name)

  • и сохраняем список файлов [matchingfiles] в переменной files элемента управления Listbox.

        self.files.delete(0, END)
        for name in matchingfiles:
          self.files.insert(END, name)

  • Наконец, нужно "обнулить" переменную selection элемента управления Entry.

        self.set_selection('')

12. parseDirectory подготавливает список каталогов для refreshDisplay(). Идея, стоящая за этим кодом, очевидна. Путь к каталогу разбивается на части, которые затем снова собираются вместе, чтобы воссоздать иерархию от корневой папки вниз вплоть до текущего каталога. По завершении порядок элементов полученного списка меняется на обратный, чтобы он соответствовал заданному формату.

# анализ имени директории для меню каталогов
  def parseDirectory(self, directoryString):
    if directoryString == os.sep:
      return [directoryString]
    if not os.sep in directoryString:
      return [directoryString]
    myList = []
    myString = ''
    myElements = string.split(directoryString, os.sep)
    for element in myElements:
      myString = myString + element
      if element == '':
        myString = os.path.normpath(myString + os.sep)
        myPost = ''
      else:
        myPost = os.sep
      myList.append(myString)
      myString = myString + myPost
    myList.reverse()
    return myList

Очевидно, чтобы разобраться в этом коде нужно немного понимать язык Python, а также знать команды из импортируемых модулей os и string. Это хороший пример программирования автономного модуля Python в том смысле, что при этом создается класс, который можно использовать во многих приложениях.

Диалоговые объекты для открытия и сохранения файлов

Помню те далекие дни, когда люди только начали говорить об объектно-ориентированном программировании (ООП). Звучало это потрясающе. Мы, хакеры-любители, читали про все это, но наши программы не поменялись. Структурное программирование с функциями и процедурами достаточно хорошо делало свое дело. Я, непрофессиональный, не-C++ программист, вплоть до знакомства с Python не мог осознать, что объектная ориентация - это больше, чем фантазия профессоров из колледжа. Она действительно облегчает программирование. [Программирование графических интерфейсов, а не программирование вообще. Объектно-ориентированное программирование в отличие от структурного - неуниверсальный инструмент. Оно хорошо подходит для написания компактного, многократно используемого кода в программах, управляющих сходными друг с другом объектами с повторяющимися свойствами (окна, меню, кнопки и т.п.). В других случаях применение ООП ведет к неоправданному усложнению читабельности кода. Однозначно не стоит начинать изучение программирования с ООП.- Прим. пер.]

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

  1. Инкапсуляция: Взаимодействие с самим кодом невозможно. Оно происходит через определение значений свойств и вызывание методов. Все, что "внутри" объекта - спрятано.
  2. Полиморфизм: Данный объект может принимать много форм за счет варьирования значений свойств, использования различных методов и даже изменения самого объекта.
  3. Наследование: Изменить объект можно с помощью наследования: создается новый объект, наследующий свойства старого объекта, но с добавлением новой функциональности или изменением некоторых старых функций.
У нас есть инкапсулированный объект класса files, который представляет собой родовой (generic) диалог для работы с файлами. Мы хотим применить его в частности для двух разных окон - Open File Dialog (диалог открытия файла) и Save File Dialog (диалог сохранения файла). Прежде всего воспользуемся полиморфизмом объекта, взаимодействуя только с его свойствами и методами. У нашего объекта есть свойство title , присвоим ему значение Open File Dialog с помощью команды:
files.go(title = "Open File Dialog")
и Save File Dialog с помощью инструкции:
files.go(title = "Save File Dialog")

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

Для File Open Dialog:
Если нам нужна последняя рабочая папка [например thisDirectory], и мы хотим, чтобы диалог открылся именно в ней, можно написать:
files.go(title = "Open File Dialog", startPath = thisDirectory)
В ином случае, можно опустить переменную startPath и диалог откроется в каталоге по умолчанию:
files.go(title = "Open File Dialog")
Для Save File Dialog:
Если файл новый, мы можем записать:
files.go(title = "Save File Dialog", startPath = thisDirectory)
или
files.go(title = "Save File Dialog")
Если такой файл уже существует и было нажато "Сохранить как ...", можно передать путь этого файла [скажем, thisFilesPath] таким образом:
files.go(title = "Save File Dialog", startPath = thisFilesPath)
Но еще нужно, чтобы эти два диалога вели себя по-разному при нажатии кнопки accept:
Open File Dialog:
Очевидно, стоит проверить имя, которое ввел пользователь, действительно ли такой файл существует. Если нет, скажем: "Это не файл!" и не будем закрывать диалог.
Save File Dialog:
Если пользователь уже нажал кнопку accept, должно быть он уже ввел имя файла. Если все-таки нет, мы говорим: "Пожалуйста, введите имя файла". Если файл с таким именем уже есть, стоит спросить: "Такой файл уже существует. Перезаписать его?"
Это два решительно разных поведения. Чтобы добиться такой функциональности, воспользуемся наследованием. Создадим новые классы - клоны files, но с разными методами accept [новые методы "перезапишут" старый метод accept]. Взгляните на два класса, приводимые ниже:

метод fileMenu

# диалог открытия файлов
class openDialog(files):
  def accept(self):
    self.myFile = os.path.join(self.myDirectory, self.get_selection())
    self.newValue = None
    if os.path.isfile(self.myFile):
      self.newValue = self.myFile
      self.toplevel.destroy()
    else:
      message(title = 'error',
              message =  self.myFile + ' is not a file',
              geometry = '250x70+387+349')

# диалог сохранения файлов
class saveDialog(files):
  def accept(self):
    self.myFile = os.path.join(self.myDirectory, self.get_selection())
    self.newValue = None
    if os.path.isdir(self.myFile):
      message(title = self.myFile,
              message = 'Please enter a file name.',
              geometry = '250x70+387+349')
    elif os.path.isfile(self.myFile):
        self.dialog = question()
        self.answer = self.dialog.go(
                      title = self.myFile,
                      message = 'This file exists!\nOverwrite it?')
        if self.answer:
          self.newValue = self.myFile
          self.toplevel.destroy()
    else:
        self.newValue = self.myFile
        self.toplevel.destroy()
    

В данном случае класс openDialog(files) означает, что openDialog наследует класс files. Итак, классы openDialog и saveDialog наследуют все свойства и методы класса files и затем заменяют метод accept своими собственными уникальными методами.

openDialog.go(title = "Open File Dialog", startPath = thisDirectory)
теперь открывает файл, и
saveDialog.go(title = "Save File Dialog", startPath = thisFilesPath)
сохраняет файл.

Довольно круто, по-моему...

Диалоговые модули многократного использования

Итак, у нас есть два модуля для нашего текстового редактора:
editor_01.py:
Интерфейс полностью завершен, но кнопки, увы, бездействуют.
files_01.py:
Это полностью функциональный модуль для организации файловых диалогов. С некоторыми исправлениями он готов к применению.

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

Возвращаемся туда, откуда ушли.
editor_01.py включает в себя код класса сообщений и класса вопросов [yesno] для обмена информацией.
files_01.py содержит код для класса сообщений и класса вопросов.

Явно избыточная повторяемость. Итак, первым делом создадим новый модуль dialogs_01.py, в который войдут все эти классы для обмена короткими сообщениями с пользователем. В нем у нас будут три класса: сообщений, вопросов и один новый для ввода. Нет смысла подробно комментировать этот код: если вы дошли до этого места, он должен быть полностью ясен. [Доступ к инкапсулированным объектам осуществляется через свойства и методы]. Как и последний модуль, он содержит тестовый код, так что этот файл можно запустить, и он последовательно выведет на экран все свои окна.

Теперь переделаем editor_01.py, удалив из него классы сообщений и yesno и импортировав dialogs_01.py [Замечание: мы заменяем yesno на question]. Назовем новую версию editor_02.py. Проверьте код, затем запустите его на выполнение.

Раз уж мы проводим модернизацию модулей, уберем классы сообщений и вопросов из files_01.py и импортируем dialogs_01.py. Назовем это files_02.py. Проверяем код и запускаем его.

А вот окна, создаваемые dialogs_01.py:

Текстовый редактор: собирая все воедино

А сейчас свяжем все имеющиеся части друг с другом и сделаем из них программу. Сперва просмотрим editor_03.py, чтобы объять всю картину. Далее:

изменения!

# глобальные переменные
appPath = os.getcwd()
fileName = ''
fileDirty = FALSE

# класс родительских окон
class main:
...
    self.myText.bind_class('Text', '<KeyRelease>', self.update_dirty)
...

# добавление пунктов файлового меню
  def fileMenu(self):
    mButton = Menubutton(self.myBar, 
                         text = 'file  ')
    mButton.pack(side = LEFT)
    menu = Menu(mButton, tearoff = 0)
    menu.add_command(label = 'new', command = self.new)
    menu.add_command(label = 'open...', command = self.open)
    menu.add_separator({})
    menu.add_command(label = 'save', command = self.save)
    menu.add_command(label = 'save as...', command = self.saveAs)
    mButton['menu'] = menu
    return mButton

# выход из редактора
  def exitMethod(self):
    self.isDirty()
    self.master.destroy()

# МЕТОДЫ УПРАВЛЕНИЯ ФАЙЛАМИ

# обновление isDirty при нажатии любой клавиши [KeyRelease]
  def update_dirty(self, event):
    global fileDirty
    fileDirty = TRUE
    

Есть три метода для открытия файлов:

классы для открытия файлов

# открытие нового файла
  def new(self, event = None):
    global fileName, fileDirty
    self.isDirty()
    self.myText.delete('0.0', END)
    self.master.title('text editor')
    fileName = None
    fileDirty = FALSE

# открытие файла
  def open(self, event = None):
    global fileName, fileDirty
    self.isDirty()
    if fileName:
      self.path, self.file = os.path.split(fileName)
      if os.path.isdir(self.path):
        self.myFile = self.path
      else:
        self.myFile = appPath
    else:
      self.myFile = appPath
    self.dialog = openDialog(self.master)
    self.myFile = self.dialog.go(title = 'Open File Dialog',
                                 startPath = self.myFile)
    if self.myFile:
      self.fileOpen(self.myFile)

# примитив для открытия файлов
  def fileOpen(self, myFile):
    global fileName, fileDirty
    self.myText.delete('0.0', END)
    self.fileInput = open(self.myFile, 'r')
    self.myText.insert('0.0', self.fileInput.read())
    self.fileInput.close()
    fileName = self.myFile
    fileDirty = FALSE
    self.master.title(fileName)
    
Есть четыре метода для сохранения файлов:

классы для сохранения файлов

# сохранение отредактированного файла
  def isDirty(self):
    global fileName, fileDirty
    if fileDirty:
      self.dialog = question(self.master)
      self.answer = self.dialog.go(
                    title = 'Editor Dialog',
                    message = 'This file has been edited.\nSave the changes?')
      if self.answer:
        self.save()

# сохранение файла
  def save(self, event = None):
    global fileName, fileDirty
    if os.path.isfile(fileName):
      self.fileSave(fileName)
      fileDirty = FALSE
    else:
      self.saveAs()

# сохранение файла как
  def saveAs(self, event = None):
    global fileName
    if fileName:
      if os.path.isfile(fileName):
        self.myFile = fileName
      else:
        self.path, self.file = os.path.split(fileName)
        if os.path.isdir(self.path):
          self.myFile = self.path
        else:
          self.myFile = appPath
    else:
      self.myFile = appPath
    self.dialog = saveDialog(self.master)
    self.myFile = self.dialog.go(title = 'Save File Dialog',
                                 startPath = self.myFile)
    if self.myFile:
      self.fileSave(self.myFile)

# примитив для сохранения файлов
  def fileSave(self, myFile):
    global fileName, fileDirty
    self.fileOutput = open(self.myFile, 'w')
    self.fileOutput.write(self.myText.get('0.0', END))
    self.fileOutput.close()
    fileName = self.myFile
    fileDirty = FALSE
    self.master.title(fileName)
    
Это все. Было трудно, потому что интерфейс был большим. Чтобы он заработал, потребовалось написать множество строк с кодом Python. Это особенно не просто для тех, кто не знаком с Python. Большая часть оставшегося материала намного легче - сравнительно простое программирование и несколько маленьких окон. После следующей страницы, резюмирующей сделанное, сделайте перерыв на кофе!

Текстовый редактор: подробнее о деталях

В этой части пособия было много программирования, решалась задача управления файлами. При этом мы несколько отклонились от главной цели - изучения Tkinter. Поэтому я хотел бы по ходу дела остановиться подробнее на некоторых деталях.
События:
С несколькими мы уже столкнулись:
    <ButtonRelease-1> - Кнопка 1 отпущена
    <Double-ButtonRelease-1> - Кнопка мыши 1 отпущена после двойного щелчка
    <ButtonRelease-1> - Кнопка 1 отпущена
    <KeyRelease> - Любая клавиша отпущена
Tkinter генерирует множество таких событий. Важнейшие из них генерируются кнопками мыши либо клаиватуры, как в примере, показанном выше. Это мощный инструмент построения интерфейса, дополняющий функции элементов управления command при использовании совместно с методом bind().

метод bind():
Метод bind() общий для всех элементов управления. Он связывает друг с другом элементы управления, события и вызовы методов.
self.files.bind('<ButtonRelease-1>', self.files_select_event)
self.files.bind('<Double-ButtonRelease-1>', self.files_double_event)
self.subDirectory.bind('<ButtonRelease-1>', self.subDirectory_select_event)
self.myText.bind_class('Text', '<KeyRelease>', self.update_dirty)
bind() имеет несколько версий:
  • bind(event, method) привязывает событие к определенному экземпляру элемента управления.
  • bind-class(class, event, method) привязывает событие к классу элементов управления.
  • bind-all(event, method) привязывает событие ко всему приложению [всем элементам управления].

Глобальные переменные:
Переменные с глобальной областью видимости могут быть прочитаны внутри методов классов, но не изменены. Чтобы присвоить глобальной переменной новое значение внутри метода, она должна быть объявлена внутри выражения def этого метода.
# обновление isDirty при нажатии любой клавиши [KeyRelease]
  def update_dirty(self, event):
    global fileDirty
    fileDirty = TRUE
protocol:
Протокол менеджера окон слегка отличается от привязки событий. Он больше похож на перехват событий. По умолчанию 'WM_DELETE_WINDOW' привязан к методу окна destroy(). То есть, когда вы щелкаете мышкой по кнопке закрытия окна, она вызывает 'WM_DELETE_WINDOW', и окно закрывается. Строка self.master.protocol('WM_DELETE_WINDOW', self.exitMethod) привязывает 'WM_DELETE_WINDOW' к self.exitMethod, позволяя провести дополнительные операции перед вызовом метода destroy().

метод configure():
Часто встречаются ситуации, когда вам нужно определить свойство/метод в элементе управления, но в этом определении участвует другой, пока еще не объявленный, элемент управления. С этим мы сталкивались в случаях с комбинацией Text()/Scrollbar(), комбинацией Listbox()/Scrollbar и с комбинацией Menubutton()/Menu(). Решением проблемы является метод configure(). В приведенном ниже примере свойству menu Menubutton() должно быть присвоено меню, но никакого меню пока нет. И Menu() должно быть объявлено как дочернее по отношению к Menubutton(). Итак:
  • Сперва объявляем Menubutton()
  • Объявляем Menu() как дочернее по отношению к Menubutton()
  • Создаем Menu()
  • Затем присваиваем свойству menu Menubutton() значение Menu() с помощью configure()
Замкнутый круг? Но он работает...
# добавление пунктов файлового меню
  def fileMenu(self):
    mButton = Menubutton(self.myBar, 
                         text = 'file  ')
    mButton.pack(side = LEFT)
    menu = Menu(mButton, tearoff = 0)
    menu.add_command(label = 'new', command = self.new)
    menu.add_command(label = 'open...', command = self.open)
    menu.add_separator({})
    menu.add_command(label = 'save', command = self.save)
    menu.add_command(label = 'save as...', command = self.saveAs)
    mButton.configure(menu = menu)
    return mButton

Настало время для чашечки кофе! В следующей часть пособия мы поработаем над внешним видом программы!

III. "Горячие" клавиши и внешний вид

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

Первым типом "горячих" клавиш я лично пользуюсь редко, но это стандартная опция программ с окнами и меню - сочетания с клавишей [ALT]. На экране они выделяются подчеркнутой буквой в строке меню или меню. Так, [ALT-f] открывает меню file. После того как оно открылось, [ALT-o] вызывает диалог открытия файлов. Те, кто пользуется этими клавиатурными комбинациями, нажимают [ALT-fo] автоматически, когда думают "открыть".

Чтобы использовать сочетания с клавишей [CTRL], не нужно лазить по меню, они вызывают команду сразу. Большинство обычно использует [CTRL-x], [CTRL-c] и [CTRL-v] для вырезания, копирования и вставки, соответственно. Эти сокращения принято показывать в самом меню. Те, кто пользуется этими клавиатурными комбинациями, нажимают [CTRL-o] автоматически, когда думают "открыть".

Наконец, есть люди, предпочитающие мышь. Те, кто как я печатают двумя пальцами и управляют интерфейсами в основном через иконки. Для нас есть панель инструментов. Щелкнул по иконке и готово.

Содержание
"Горячие" клавиши с [ALT]
"Горячие" клавиши с [CTRL]
Модификация меню/"горячих" клавиш
Панель инструментов
Внешний вид I
Внешний вид II
Внешний вид III

"Горячие" клавиши с [ALT]

Начнем с сочетаний с клавишей [ALT], потому что они самые простые. В Tkinter все уже встроено в элемент управления Menu(). Просто передайте меню с помощью метода add_command(), какую букву следует подчеркнуть [underline], и все не только правильно отобразится на экране, но и заработает без всякого дополнительного программирования.


# добавление меню file в панель меню
  def fileMenu(self):
    self.fileButton = Menubutton(self.myBar, 
                                 text = 'file  ', 
                                 underline = 0)
    self.fileButton.pack(side = LEFT)
    self.fileMenu = Menu(self.fileButton, tearoff = 0)
    self.fileMenu.add_command(label = 'new', 
                     underline = 0,
                     command = self.new)
    self.fileMenu.add_command(label = 'open...', 
                     underline = 0, 
                     command = self.open)
    self.fileMenu.add_separator({})
    self.fileMenu.add_command(label = 'save',
                     underline = 0, 
                     command = self.save)
    self.fileMenu.add_command(label = 'save as...', 
                     underline = 5, 
                     command = self.saveAs)
    self.fileButton.configure(menu = self.fileMenu)
    return self.fileButton
    

"Горячие" клавиши с [CTRL]

Добиться, чтобы заработали команды с клавишей Control лишь чуть-чуть сложнее. Сначала нужно вписать в параметр accelerator метода Menu() add_command() то, что вы хотите видеть отображенным в самом меню [menu.add_command(... accelerator = 'ctrl+n'...)]:


# добавление меню file в панель меню
  def fileMenu(self):
    self.fileButton = Menubutton(self.myBar, 
                                 text = 'file  ', 
                                 underline = 0)
    self.fileButton.pack(side = LEFT)
    self.fileMenu = Menu(self.fileButton, tearoff = 0)
    self.fileMenu.add_command(label = 'new', 
                     underline = 0, 
                     accelerator = 'ctrl+n', 
                     command = self.new)
    self.fileMenu.add_command(label = 'open...', 
                     underline = 0, 
                     accelerator = 'ctrl+o', 
                     command = self.open)
    self.fileMenu.add_separator({})
    self.fileMenu.add_command(label = 'save',
                     underline = 0, 
                     accelerator = 'ctrl+s',
                     command = self.save)
    self.fileMenu.add_command(label = 'save as...', 
                     underline = 5, 
                     accelerator = 'ctrl+a', 
                     command = self.saveAs)
    self.fileButton.configure(menu = self.fileMenu)
    return self.fileButton
    
Пока мы "научили" меню лишь выводить правильные надписи. Функциональность клавиш Control еще не работает. Для этого воспользуемся методом bind(). События, относящиеся к клавише Control, - это <Control-Key-x >, где x - это клавиша.

  def __init__(self, master):
    self.master = master
    self.master.title('text editor')
    self.master.geometry('600x400+100+100')
    self.master.bind_class(self.master, '<Control-Key-n>', self.new)
    self.master.bind_class(self.master, '<Control-Key-o>', self.open)
    self.master.bind_class(self.master, '<Control-Key-s>', self.save)
    self.master.bind_class(self.master, '<Control-Key-a>', self.saveAs)
    self.myBar = Frame(self.master, relief = RAISED, borderwidth=2)
    self.fileMenu()
    ...
    
Обратите внимание, что был применен метод bind_class(), а не bind() или bind_all(). Если бы мы использовали bind() [привязку к элементу управления Text()], то для активации привязки фокус должен был бы находиться в элементе управления Text() [слишком узкое условие]. Если бы мы использовали bind_all(), то эта команда была бы активной и в диалоговых окнах [слишком широкое условие]. Привязка же этих команд к классу main функционирует в точности так, как требуется.

[Этот вариант кода еще не окончательный. На следующей странице мы внесем в него небольшое улучшение.]

Модификация меню/"горячих" клавиш

  

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

Панель инструментов

Эта страница проста - нет никаких новых крупных концепций, а все результаты сразу видны на экране. Мы хотим добавить к нашему интерфейсу панель инструментов. Вот несколько иконок [все они в формате .gif по определенной причине]. В их число я включил и те, что мы добавим позднее. Конечно, вы можете сделать свои собственные:

Единственная новая вещь в этом разделе - это то, каким образом Tkinter имеет дело с графическими изображениями. Имеются две встроенные функции PhotoImage() и BitmapImage() для работы с файлами в формате .gif и .bmp, соответственно. Для изображений других типов есть свободная библиотека PIL Library. Мы воспользуемся PhotoImage(). Два важных момента:

Вот дополнения, нужные для добавления панели инструментов: еще один фрейм Frame(), упакованный под фреймом панели меню, включающий в себя кнопки, упакованные к левой [LEFT] стороне. [Заметьте, что кнопка saveButton() отключена, т.е. DISABLED]:

  def __init__(self, master):
    self.master = master
    self.master.title('text editor')
    self.master.geometry('600x400+100+100')
    self.master.bind_class(self.master, '<Control-Key-n>', self.new)
    self.master.bind_class(self.master, '<Control-Key-o>', self.open)
    self.master.bind_class(self.master, '<Control-Key-a>', self.saveAs)
    self.myBar = Frame(self.master, 
                       relief = RAISED, 
                       borderwidth=2)
    self.fileMenu()
    self.myBar.pack(side = TOP, fill = X)
    self.myTools = Frame(self.master)
    self.myTools.pack(side = TOP, fill = X)
    self.newImage = PhotoImage(file= os.path.join(appPath, 'new.gif'))
    self.newButton = Button(self.myTools, 
                            image = self.newImage , 
                            command = self.new)
    self.newButton.pack(side = LEFT)
    self.openImage = PhotoImage(file= os.path.join(appPath, 'open.gif'))
    self.openButton = Button(self.myTools, 
                             image = self.openImage, 
                             command = self.open)
    self.openButton.pack(side = LEFT)
    self.saveImage = PhotoImage(file= os.path.join(appPath, 'save.gif'))
    self.saveButton = Button(self.myTools, 
                             image = self.saveImage,
                             state = DISABLED, 
                             command = self.save)
    self.saveButton.pack(side = LEFT)
    self.saveAsImage = PhotoImage(file= os.path.join(appPath, 'saveas.gif'))
    self.saveAsButton = Button(self.myTools, 
                               image = self.saveAsImage, 
                               command = self.saveAs)
    self.saveAsButton.pack(side = LEFT)
    self.myScroll = Scrollbar(self.master)
    self.myScroll.pack(side=RIGHT, fill=Y)
    self.myText = Text(self.master,
                       background = 'white',
                       yscrollcommand=(self.myScroll, 'set'))
    self.myText.pack(side=LEFT, fill=BOTH, expand=YES)
    self.myText.bind_class('Text', '<KeyRelease>',      self.update_dirty)
    self.myText.bind_class('Text', '<ButtonRelease-1>', self.update_menu)
    self.myScroll.configure(command = self.myText.yview)
    self.master.protocol('WM_DELETE_WINDOW', self.exitMethod)
    self.master.mainloop()
    

Динамика меню

  

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


  def update_menu(self, event = None):
    if fileName:
      self.master.bind_class(self.master, 
                             '<Control-Key-s>', 
                             self.save)
      self.fileMenu.entryconfigure(3, state = NORMAL)
      self.saveButton.configure(state = NORMAL)
    else:
      self.master.unbind_class(self.master, 
                               '<Control-Key-s>')
      self.fileMenu.entryconfigure(3, state = DISABLED)
      self.saveButton.configure(state = DISABLED)
    
Запустите editor_05.py, чтобы испытать полностью завершенные "горячие" клавиши. Я рассмотрел эту тему, пока программа была еще сравнительно простой. При добавлении оставшихся пунктов меню код ее будем модифицировать автоматически. Учиться тут будет нечему, знай повторяй те же самые процедуры...

Внешний вид I

 

 

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

Запустите прямо сейчас editor_06.py [для его запуска потребуются следующие файлы: files_03.py, dialogs_02.py и configure_01.py]. Он выглядит по-другому и, надеюсь, лучше. Как это получилось?

Вот краткий ответ:


widget = {}
config = {}

# Здесь задается цвет активного пункта меню и желобка линейки прокрутки
widget['dark']   = '#c1c0b2'

# Здесь задается гарнитура и размер шрифта элементов управления
widget['workfont'] = 'helvetica 12'

# Здесь задается шрифт для текстовых полей ввода
widget['textfont'] = 'courier 12'

# Здесь задается толщина границы меню
widget['m_border']   = '0'

# Здесь задается толщина границы для остальных приподнятых/утопленных элементов управления
widget['border']   = '1'
    
Это начало файла configure_01.py. В нем задаются значения нескольких переменных, которые вы можете скорректировать, чтобы поменять созданный мной внешний вид. Я выбрал в качестве интерфейсного шрифта [workfont] шрифт переменной ширины [helvetica], а в качестве текстового шрифта моноширинный [courier], потому что текстовый редактор мне нужен в основном для программирования. Если вы текстовый редактор используете, только чтобы набирать тексты, стоит выбрать helvetica и в том и в другом случае. Так или иначе, откройте файл configure_01.py, поменяйте в нем, скажем, толщину границы и посмотрите, что изменится.

Как это работает? Нижняя часть файла configure_01.py преобразовывает эти переменные и некоторые другие в словари свойств. Эти словари импортируются в editor_06.py, files_03.py и dialogs_02.py. Все объявления элементов управления в этих программах имеют дополнительный параметр config['имя_элемента_управления'], который передает эти изменения элементам управления. Вот несколько строк из рабочего кода editor_06.py.

...
from configure_01 import *
...
def __init__(self, master):
    ...
    self.myBar = Frame(self.master, 
                       config['frame'])
    ...
    self.myTools = Frame(self.master, 
                         config['frame'])
    ...
    self.newButton = Button(self.myTools, 
                            config['button'], 
                            image = self.newImage, 
                            command = self.new)
    ...
    self.openButton = Button(self.myTools, 
                             config['button'], 
                             image = self.openImage, 
                             command = self.open)
    ...
    self.saveButton = Button(self.myTools, 
                             config['button'], 
                             image = self.saveImage,
                             state = DISABLED, 
                             command = self.save)
    ...
    self.saveAsButton = Button(self.myTools, 
                               config['button'], 
                               image = self.saveAsImage, 
                               command = self.saveAs)
    ...
    self.myText = Text(self.master, 
                       config['text'],
                       yscrollcommand=(self.myScroll, 'set'))
    ...
    
Если вас устраивают сделанные мной изменения или те поправки, которые вы сами можете внести в начальной части файла configure_01.py, можете здесь остановиться и перейти сразу к следующей части руководства, где говорится об обработке текста.

Но есть и длинный ответ на вопрос: "Как тут все поменять?" Если пробудилось любопытство, в данном руководстве дальше можно найти подробное разъяснение кода, с помощью которого можно менять внешний вид Tkinter почти как угодно. Если вы новичок в Python, оставьте пока эту тему. Прочитать о ней можно и потом. Просто будет не совсем понятно, когда речь зайдет о цветах в следующей части руководства...

Замечание: Существует еще один путь добиться сходного результата. Речь идет об использовании базы данных опций. Примерно таким же способом реализовано управление внешним видом в системе X Window. Мне лично он не нравится [не достаточно контроля]. Информация об этом есть в книжках.

Внешний вид II

Tkinter не рисует окна сам. Окна выводятся на экран библиотекой Tk, позаимствованной из более старого языка Tk/Tcl. Tkinter - это интерфейс Python к Tk, как и следует из названия: Tk interface. Он написан на Python и использует the Python dictionary object type to store the various properties for each of the widgets. Вместо простых массивов в Python есть списки [упорядоченные наборы элементов, к которым адресуются по их номеру], tuples [неизменяемые списки - их можно только читать] и словари [упорядоченные наборы элементов, к которым обращаются через их ключи]. Словари могут быть записаны несколькими способами:


myDictionary={}
myDictionary['animal'] = 'horse'
myDictionary['vegetable'] = 'radish'
myDictionary['mineral'] = 'quartz'
    
или так


myDictionary={'animal' : 'horse', 'vegetable' : 'radish', 'mineral' : 'quartz'}
    
В любом случае, print myDictionary['vegetable'] возвращает radish.

Итак, сначала создается словарь, состоящий из пар свойство/значение, который мы хотим передать элементу управления:


textWidget = {'borderwidth' : '1', 'font': 'helvetica', 'relief' : 'sunken'}
    
Затем мы составляем словарь, включающий в себя словари элементов управления [он находится в файле configure_01.py]:

widget = {}
config = {}

# Здесь задается цвет активного пункта меню и желобка линейки прокрутки
widget['dark']   = '#c1c0b2'

# Здесь задается гарнитура и размер шрифта элементов управления
widget['workfont'] = 'helvetica 12'

# Здесь задается шрифт для текстовых полей ввода
widget['textfont'] = 'courier 12'

# Здесь задается толщина границы меню
widget['m_border']   = '0'

# Здесь задается толщина границы для остальных приподнятых/утопленных элементов управления
widget['border']   = '1'

#-----------------------------------------------------------#
# Здесь определяются списки свойств элементов управления

config['frame']  = {'borderwidth' : widget['border'],
                    'relief' : 'flat'}
config['label']  = {'borderwidth' : widget['border'],
                    'font': widget['workfont'],
                    'relief' : 'flat'}
config['button'] = {'borderwidth' : widget['border'],
                    'font': widget['workfont'],
                    'relief' : 'raised'}
config['menubutton'] = {
                    'borderwidth' : widget['m_border'],
                    'font': widget['workfont'],
                    'relief' : 'flat'}
config['menu']   = {'activeborderwidth' : widget['m_border'],
                    'activebackground': widget['dark'],
                    'borderwidth' : widget['border'],
                    'font': widget['workfont'],
                    'relief' : 'raised'}
config['list']   = {'borderwidth' : widget['border'],
                    'font': widget['workfont'],
                    'relief' : 'sunken'}
config['entry']  = {'background' : 'white',
                    'borderwidth' : widget['border'],
                    'font': widget['textfont'],
                    'relief' : 'sunken',}
config['text']   = {'background' : 'white',
                    'borderwidth' : widget['border'],
                    'font': widget['textfont'],
                    'relief' : 'sunken',}
config['scroll'] = {'borderwidth' : widget['border'],
                    'elementborderwidth' : widget['border'],
                    'relief' : 'sunken',
                    'troughcolor': widget['dark'],
                    'width' : '10'}
    
Если до сих пор еще не ясно, то мы начинаем с маленького словаря значений [widget] и переходим к словарю словарей [config]. Если внимательно изучать configure_01.py, все станет до конца понятно!

На следующей странице при помощи этой концепции мы будем управлять всеми свойствами всех элементов управления Tkinter.

Внешний вид III

 

Ну, как вам? Вот как это делается:

В последнем примере мы изменили только определенные свойства элементов управления, в остальных случаях довольствуясь значениями, заданными по умолчанию. В версии, приведенной ниже, config содержит свойства, определяющие внешний вид всех элементов управления. Итак, в начале этого файла зададим общие для всего интерфейса параметры в widget, далее ниже по тексту распределим их по элементам управления с помощью метода config словаря словарей [наряду с некоторыми особыми изменениями отдельных элементов управления]. Все это приводится, только чтобы показать, как это сделано. Можно вернуться к первоначальному файлу configure_01, объединить его с этим, или произвольно его отредактировать, сделав свою собственную версию.

Есть еще пара вещей, которые стоит знать об этом файле. Имеется пять настраиваемых параметров. Если любому из них в качестве значения присвоить пустую строку:

widget['finish'] = ''

то для него будет использоваться значение по умолчанию для вашей системы. Код Python, который идет сразу за этой строкой, делает еще одно "волшебство". Он расщепляет цвет 'finish' и создает из него два цвета того же оттенка - один светлый и один темный. Светлый применяется в роли "активного" ['active'] цвета и цвета желобка линейки прокрутки, а темный - как цвет выделения ['select']. Два параметра - f1 [светлее] и f2 [темнее] - масштабируют изменения. Если полученный эффект вам не совсем нравится, их можно подрегулировать. Итак, меняйте цвета, пока вам не понравится то, что вы видите. Если не хватает "тяжеловесности", задайте widget['border'] равным '2' или '', и она вернется обратно.

configure.py

import string

widget = {}
config = {}

widget['finish']   = '#c2bfa5'
widget['canvas']   = '#f8fae6'
widget['workfont'] = 'helvetica 12'
widget['textfont'] = 'courier 12'
widget['border']   = '1'

# ---------- НЕ РЕДАКТИРОВАТЬ ТО, ЧТО НИЖЕ ЭТОЙ ЛИНИИ ---------- #
if widget['finish']:
  Color1 = widget['finish']
  Color2 = '#'
  Color3 = '#'
  f1 = 1.15
  f2 = 0.65
  fx = [string.atoi(Color1[1:3],16), 
        string.atoi(Color1[3:5],16), 
        string.atoi(Color1[5:7],16)]
  for i in fx:
    j = i*f1
    k = i*f2
    if j > 255: j = 255
    if k > 255: k = 255
    Color2 = '%s%02x' % (Color2, j)
    Color3 = '%s%02x' % (Color3, k)
  widget['active'] = Color2
  widget['select'] = Color3
else:
  widget['active'] = ''
  widget['select'] = ''

# Используйте 'text' для элементов управления Text, Listbox и Entry.
#         (их параметры идентичны)

config['frame'] = {'borderwidth' : '0', 'relief' : 'flat'}
config['label'] = {'borderwidth' : '0', 'relief' : 'flat'}
config['button'] = {'relief' : 'raised'}
config['check'] = {'relief' : 'raised'}
config['menubutton'] = {'borderwidth' : '0', 'relief' : 'flat'}
config['menu'] = {'activeborderwidth' : '0', 'relief' :'raised'}
config['text'] = { 'relief' : 'sunken'}
config['scroll'] = {'relief' : 'sunken', 'width' : '10'}

if widget['border']:
  config['button']['borderwidth'] = widget['border']
  config['check']['borderwidth'] = widget['border']
  config['menu']['borderwidth'] = widget['border']
  config['text']['borderwidth'] = widget['border']
  config['scroll']['borderwidth'] = widget['border']
  config['scroll']['elementborderwidth'] = widget['border']
  
if widget['finish']:
  config['frame']['background'] = widget['finish']
  config['frame']['highlightbackground'] = widget['finish']
  config['label']['background'] = widget['finish']
  config['label']['highlightbackground'] = widget['finish']
  config['button']['background'] = widget['finish']
  config['button']['highlightbackground'] = widget['finish']
  config['check']['background'] = widget['finish']
  config['check']['highlightbackground'] = widget['finish']
  config['menubutton']['background'] = widget['finish']
  config['menubutton']['highlightbackground'] = widget['finish']
  config['menu']['background'] = widget['finish']
  config['text']['highlightbackground'] = widget['finish']
  config['scroll'][ 'activebackground'] = widget['finish']                   
  config['scroll'][ 'background'] = widget['finish']
  config['scroll'][ 'highlightbackground'] = widget['finish']

# Если цвет 'canvas' равен пустой строке, подставляется белый.

if widget['canvas']:
  config['text']['background'] = widget['canvas']
else:
  config['text']['background'] = '#ffffff'    

if widget['active']:
  config['button']['activebackground'] = widget['active']
  config['check']['activebackground'] = widget['active']
  config['menubutton']['activebackground'] = widget['active']
  config['menu']['activebackground'] = widget['active']
  config['scroll'][ 'troughcolor'] = widget['active']

if widget['select']:
  config['menu']['selectcolor'] = widget['select']
  config['check']['selectcolor'] = widget['select']
  config['text']['selectbackground'] = widget['select']                    

if widget['workfont']:
  config['label']['font'] = widget['workfont']
  config['button']['font'] = widget['workfont']
  config['check']['font'] = widget['workfont']
  config['menubutton']['font'] = widget['workfont']
  config['menu']['font'] = widget['workfont']

if widget['textfont']:
  config['text']['font'] = widget['textfont']
    

Теперь перейдем к обработке текста...

IV. Обработка текста

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

Содержание
Вырезать, копировать и вставить
Окно диалога поиска
Код диалога поиска I
Код диалога поиска II
Строка состояния и печать

  def __init__(self, master):
    ...
    self.fileMenu()
    self.editMenu()
    ...

# добавление меню file в панель меню
  def fileMenu(self):
    ...
    self.fileMenu.add_separator({})
    self.fileMenu.add_command(label = 'print',
                     underline = 0, 
                     accelerator = 'ctrl+p',
                     command = self.message())
    ... 

# добавление меню edit в панель меню редактора
  def editMenu(self):
    self.editButton = Menubutton(self.myBar, text = 'edit  ', underline = 0)
    self.editButton.pack(side = LEFT)
    self.editMenu = Menu(self.editButton, tearoff = 0)
    self.editMenu.add_command(label = 'cut', 
                     underline = 2, 
                     accelerator = 'ctrl+x',
                     command = self.message())
    self.editMenu.add_command(label = 'copy', 
                     underline = 0, 
                     accelerator = 'ctrl+c',
                     command = self.message())
    self.editMenu.add_command(label = 'paste', 
                     underline = 0, 
                     accelerator = 'ctrl+v', 
                     command = self.message())
    self.editMenu.add_separator({})
    self.editMenu.add_command(label = 'find...', 
                     underline = 0, 
                     accelerator = 'ctrl+f',
                     command = self.message())
    self.editMenu.add_command(label = 'find next', 
                     accelerator = 'ctrl+g',
                     underline = 6, 
                     command = self.message())
    self.editMenu.add_command(label = 'replace...', 
                     accelerator = 'ctrl+r',
                     underline = 0, 
                     command = self.message())
    self.editButton.configure(menu = self.editMenu)
    return self.editButton
    
Кроме того есть иконки панели инструментов и их привязки [Обратите внимание, как пустые фреймы используются в качестве разделителей]:


  def __init__(self, master):
    ...
    self.master.bind_class(self.master, '<Control-Key-n>', self.new)
    self.master.bind_class(self.master, '<Control-Key-o>', self.open)
    self.master.bind_class(self.master, '<Control-Key-a>', self.saveAs)
    self.master.bind_class(self.master, '<Control-Key-p>', self.message())
    self.master.bind_class(self.master, '<Control-Key-x>', self.message())
    self.master.bind_class(self.master, '<Control-Key-c>', self.message())
    self.master.bind_class(self.master, '<Control-Key-v>', self.message())
    self.master.bind_class(self.master, '<Control-Key-f>', self.message())
    self.master.bind_class(self.master, '<Control-Key-g>', self.message())
    self.master.bind_class(self.master, '<Control-Key-r>', self.message())
    ...
    self.newImage = PhotoImage(file= os.path.join(appPath, 'new.gif'))
    self.newButton = Button(self.myTools, config['button'], 
                            image = self.newImage, 
                            command = self.new)
    self.newButton.pack(side = LEFT)
    self.openImage = PhotoImage(file= os.path.join(appPath, 'open.gif'))
    self.openButton = Button(self.myTools, config['button'], 
                             image = self.openImage, 
                             command = self.open)
    self.openButton.pack(side = LEFT)
    self.separator0 = Frame(self.myTools, 
                            config['frame'], 
                            width = 15, 
                            height = 30)
    self.separator0.pack(side = LEFT)
    self.saveImage = PhotoImage(file= os.path.join(appPath, 'save.gif'))
    self.saveButton = Button(self.myTools, config['button'], 
                             image = self.saveImage,
                             state = DISABLED, 
                             command = self.save)
    self.saveButton.pack(side = LEFT)
    self.saveAsImage = PhotoImage(file= os.path.join(appPath, 'saveas.gif'))
    self.saveAsButton = Button(self.myTools, config['button'], 
                               image = self.saveAsImage, 
                               command = self.saveAs)
    self.saveAsButton.pack(side = LEFT)
    self.separator1 = Frame(self.myTools, 
                            config['frame'], 
                            width = 15, 
                            height = 30)
    self.separator1.pack(side = LEFT)
    self.printImage = PhotoImage(file= os.path.join(appPath, 'print.gif'))
    self.printButton = Button(self.myTools, config['button'], 
                              image = self.printImage, 
                              command = self.getMessage)
    self.printButton.pack(side = LEFT)
    self.separator2 = Frame(self.myTools, 
                            config['frame'], 
                            width = 15, 
                            height = 30)
    self.separator2.pack(side = LEFT)
    self.cutImage = PhotoImage(file= os.path.join(appPath, 'cut.gif'))
    self.cutButton = Button(self.myTools, config['button'], 
                            image = self.cutImage, 
                            command = self.getMessage)
    self.cutButton.pack(side = LEFT)
    self.copyImage = PhotoImage(file= os.path.join(appPath, 'copy.gif'))
    self.copyButton = Button(self.myTools, config['button'], 
                             image = self.copyImage, 
                             command = self.getMessage)
    self.copyButton.pack(side = LEFT)
    self.pasteImage = PhotoImage(file= os.path.join(appPath, 'paste.gif'))
    self.pasteButton = Button(self.myTools, config['button'], 
                              image = self.pasteImage, 
                              command = self.getMessage)
    self.pasteButton.pack(side = LEFT)
    self.separator3 = Frame(self.myTools, 
                            config['frame'], 
                            width = 15, 
                            height = 30)
    self.separator3.pack(side = LEFT)
    self.findImage = PhotoImage(file= os.path.join(appPath, 'find.gif'))
    self.findButton = Button(self.myTools, config['button'], 
                             image = self.findImage, 
                             command = self.getMessage)
    self.findButton.pack(side = LEFT)
    self.nextImage = PhotoImage(file= os.path.join(appPath, 'next.gif'))
    self.nextButton = Button(self.myTools, config['button'], 
                             image = self.nextImage, 
                             command = self.getMessage)
    self.nextButton.pack(side = LEFT)
    self.replaceImage = PhotoImage(file= os.path.join(appPath, 'replace.gif'))
    self.replaceButton = Button(self.myTools, config['button'], 
                                image = self.replaceImage, 
                                command = self.getMessage)
    self.replaceButton.pack(side = LEFT)
    ...
    
Погодите минутку! Почему сразу добавляется так много кода?

Чтобы показать, как легко его читать, когда вы знаете, что он значит. Весь этот код записан в файле editor_07.py, где его можно и просмотреть, и запустить.


# ИМПОРТИРОВАНИЕ ПОЛЬЗОВАТЕЛЬСКИХ МОДУЛЕЙ

from dialogs import *
from files import *
from configure import *

Замечание: editor_07.py импортирует files.py, dialogs_py и configure.py. Последние три уже готовы!
    
Наша маленькая программа растет...

Вырезать, копировать и вставить

Запрограммировать вырезание, копирование и вставку на Tkinter нетрудно. Имеются встроенные события для всех трех действий [ <<Cut>>, <<Copy>> и <<Paste>> ]. Все, что нужно сделать,- это связать их с управляющими клавишами, нашим меню и иконками панели управления, а затем генерировать события с помощью коротких методов. В данном фрагменте кода несколько раз встречается строка state = DISABLED или несуществующие привязки [Cut и Copy]. Обратимся к ним ниже при рассмотрении изменений в update_menu():


  def __init__(self, master):
    ...
    self.cutImage = PhotoImage(file= os.path.join(appPath, 'cut.gif'))
    self.cutButton = Button(self.myTools, config['button'], 
                            image = self.cutImage, 
                            state = DISABLED, 
                            command = self.cut)
    self.cutButton.pack(side = LEFT)
    self.copyImage = PhotoImage(file= os.path.join(appPath, 'copy.gif'))
    self.copyButton = Button(self.myTools, config['button'], 
                             image = self.copyImage, 
                             state = DISABLED, 
                             command = self.copy)
    self.copyButton.pack(side = LEFT)
    self.pasteImage = PhotoImage(file= os.path.join(appPath, 'paste.gif'))
    self.pasteButton = Button(self.myTools, config['button'], 
                              image = self.pasteImage, 
                              command = self.paste)
    self.pasteButton.pack(side = LEFT)
    self.separator3 = Frame(self.myTools, 
                            config['frame'], 
                            width = 15, 
                            height = 30)
    ...
    self.master.bind_class('Text', '<Control-Key-v>', self.paste)
    ...
    self.myText.bind_class('Text', '<ButtonRelease-1>', self.update_menu)
    ...

  def editMenu(self):
    ...
    self.editMenu.add_command(label = 'cut', 
                     underline = 2, 
                     state = DISABLED, 
                     accelerator = 'ctrl+x',
                     command = self.getMessage)
    self.editMenu.add_command(label = 'copy', 
                     underline = 0, 
                     state = DISABLED, 
                     accelerator = 'ctrl+c',
                     command = self.getMessage)
    self.editMenu.add_command(label = 'paste', 
                     underline = 0, 
                     accelerator = 'ctrl+v', 
                     command = self.getMessage)
    ...
    
Методы не громоздкие. Новый метод event_generate() (генерация события) делает именно то, о чем говорится в его названии. С помощью getSelection() fileDirty фиксирует операции Cut и модифицирует меню. [Я честно не могу вспомнить, зачем я ввел метод deleteSelection(), но уверен, что по какой-то серьезной причине, так что он все еще тут.]

# МЕТОДЫ РЕДАКТИРОВАНИЯ ТЕКСТА

# модификация isDirty при нажатии любой клавиши [KeyRelease]
  def update_dirty(self, event):
    global fileDirty
    fileDirty = TRUE

# метод для вырезания
  def cut(self, event = None):
    global fileDirty
    if self.getSelection():
      fileDirty = TRUE
      self.myText.event_generate('<<Cut>>')
      self.myText.selection_clear()
      self.update_menu()

# метод для копирования
  def copy(self, event = None):
    self.myText.event_generate('<<Copy>>')
    self.myText.selection_clear()
    self.update_menu()

# метод для вставки
  def paste(self, event = None):
    global fileDirty
    fileDirty = TRUE
    self.deleteSelection()
    self.myText.event_generate('<<Paste>>')
    self.myText.selection_clear()
    self.update_menu()

# получение выделенного текста - примитив
  def getSelection(self):
    try:
      mySelection = self.myText.selection_get()
    except:
      mySelection = None
    return mySelection

# удаление текущего выделения - примитив
  def deleteSelection(self, event = None):
    mySelection = self.getSelection()
    if mySelection:
      self.myText.event_generate('<Delete>')
    
Теперь об update_menu(). Если что-то уже выделено, getSelection() возвратит TRUE, и команды Cut и Copy будут доступны через меню/иконки. Если ничего не выделено, они будут неактивны:

# модификация меню
  def update_menu(self, event = None):
    if self.getSelection():
      self.myText.bind_class('Text', '<Control-Key-x>', self.cut)
      self.editMenu.entryconfigure(0, state = NORMAL)
      self.cutButton.configure(state = NORMAL)
      self.myText.bind_class('Text', '<Control-Key-c>', self.copy)
      self.editMenu.entryconfigure(1, state = NORMAL)
      self.copyButton.configure(state = NORMAL)
    else:
      self.myText.unbind_class('Text', '<Control-Key-x>')
      self.editMenu.entryconfigure(0, state = DISABLED)
      self.cutButton.configure(state = DISABLED)
      self.myText.unbind_class('Text', '<Control-Key-c>')      
      self.editMenu.entryconfigure(1, state = DISABLED)
      self.copyButton.configure(state = DISABLED)
    if fileName:
      self.master.bind_class(self.master, '<Control-Key-s>', self.save)
      self.fileMenu.entryconfigure(3, state = NORMAL)
      self.saveButton.configure(state = NORMAL)
    else:
      self.master.unbind_class(self.master, '<Control-Key-s>')
      self.fileMenu.entryconfigure(3, state = DISABLED)
      self.saveButton.configure(state = DISABLED)
    
Все вышеизложенное находится в файле editor_08.py. Запустите его, вырезайте и вставляйте как вам заблагорассудится!

Окно диалога поиска

   

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

Просматривая код, нетрудно заметить, как мы адаптируем класс, чтобы из него получилось два весьма отличных друг от друга диалога, с помощью булевой переменной replace [снова полиморфизм!]. В данном случае заголовок окна тоже зависит от replace. Обратите внимание, что в geometry записаны только координаты угла. Пусть диалог "сам" определяет свой размер. Появился новый элемент управления - Checkbutton() [включить, выключить]. Его значение хранится в переменной caseVar. Остальное работает как обычно:

# класс окна поиска/замены
class search:
  def __init__(self, master = None, replace = TRUE):
    self.master = master
    self.toplevel = Toplevel(master, config['frame'])
    self.replace = replace
    self.replaced = FALSE
    self.caseVar = IntVar()
    self.toplevel.geometry('+265+230')
    self.main_frame = Frame(self.toplevel, 
                            config['frame'])
    self.main_frame.pack(side = TOP, 
                         fill = BOTH, 
                         expand = YES)
    self.bottom_frame = Frame(self.main_frame, 
                              config['frame'])
    self.bottom_frame.pack(side = BOTTOM, 
                           padx = 10, 
                           pady = 5)
    self.find_button = Button(self.bottom_frame,
                              config['button'], 
                              text = 'find next', 
                              width = 8,  
                              command = self.find)
    self.find_button.pack(side = LEFT)
    if self.replace:
      self.toplevel.title('replace dialog')
      self.replace_button = Button(self.bottom_frame,
                                   config['button'], 
                                   text = 'replace', 
                                   width = 8,
                                   command = self.replace)
      self.replace_button.pack(side = LEFT)
      self.replaceAll_button = Button(self.bottom_frame,
                                      config['button'], 
                                      text = 'replace all', 
                                      width = 8, 
                                      command = self.replaceAll)
      self.replaceAll_button.pack(side = RIGHT)
    else:
      self.toplevel.title('find dialog')
    self.top_frame = Frame(self.main_frame, 
                           config['frame'])
    self.top_frame.pack(side = LEFT, 
                        fill = BOTH, 
                        expand = YES, 
                        padx = 10, 
                        pady = 5)
    self.findString_frame = Frame(self.top_frame, 
                                  config['frame'])
    self.findString_frame.pack(side = TOP, fill = X)
    self.findString_label = Label(self.findString_frame,
                                  config['label'],
                                  text = '   find string: ',
                                  width = 12)
    self.findString_label.pack(side = LEFT)
    self.findString_entry = Entry(self.findString_frame,
                                  config['text'])
    self.findString_entry.pack(side = RIGHT, 
                               fill = X, 
                               expand = YES)
    if self.replace:
        self.replaceString_frame = Frame(self.top_frame, 
                                         config['frame'])
        self.replaceString_frame.pack(side = TOP, fill = X)
        self.replaceString_label = Label(self.replaceString_frame,
                                         config['label'],
                                         text = 'replace with: ',
                                         width = 12)
        self.replaceString_label.pack(side = LEFT)
        self.replaceString_entry = Entry(self.replaceString_frame,
                                         config['text'])
        self.replaceString_entry.pack(side = RIGHT, 
                                      fill = X, 
                                      expand = YES)
    self.options_frame = Frame(self.top_frame, config['frame'])
    self.options_frame.pack(side = TOP, fill = X)
    self.case_check = Checkbutton(self.options_frame, 
                                  config['check'],
                                  text = ' match case? ',
                                  onvalue = 0,
                                  offvalue = 1,
                                  variable = self.caseVar)
    self.case_check.pack(side = RIGHT)
    self.case_check.deselect()
    
Мы рассмотрим код, который выполняет всю эту работу на следующей странице, но пока заметьте, что он слегка отличается от других версий. Нет никакой кнопки replace [замена] - только find next [найти следующее], replace all [заменить все] и close [закрыть]. Почему - довольно скоро станет ясно.

Код диалога поиска I

В разных программах функции текстового поиска работают по-разному. Find и Find Next работают в точности так, как это и можно было ожидать. Если открыть Replace и нажать Find Next, программа будет искать заданную строку. Когда она обнаружится, в диалоговом окне будет предложено заменить ее [или удалить, если поле replace with оставить пустым]. Replace All [заменить все] делает именно то, что означают эти слова.

Сначала обратимся к коду класса editor. Мы уже перешли к файлу editor_09.py. Меню, горячие клавиши и иконки панели инструментов пояснений не требуют. Появилась новая глобальная переменная, содержащая в себе текущую строку поиска, currentFind. В зависимости от состояния этой переменной или наличия выделенного текста меняется меню. В одном случае команда find next доступна. Если же переменная currentFind пустая или никакого текста не выделено, эта команда не доступна.

Три метода, которые представляют интерес:


# ЗАДАНИЕ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ
    ...
currentFind = None

# КЛАСС EDITOR

class editor:
    ...
    self.findImage = PhotoImage(file= os.path.join(appPath, 'find.gif'))
    self.findButton = Button(self.myTools, config['button'], 
                             image = self.findImage, 
                             command = self.find)
    self.findButton.pack(side = LEFT)
    self.nextImage = PhotoImage(file= os.path.join(appPath, 'next.gif'))
    self.nextButton = Button(self.myTools, config['button'], 
                             image = self.nextImage, 
                             state = DISABLED, 
                             command = self.again)
    self.nextButton.pack(side = LEFT)
    self.replaceImage = PhotoImage(file= os.path.join(appPath, 'replace.gif'))
    self.replaceButton = Button(self.myTools, config['button'], 
                                image = self.replaceImage, 
                                command = self.replace)
    self.replaceButton.pack(side = LEFT)  
    ...
    self.master.bind_class('Text', '<Control-Key-f>', self.find)
    self.master.bind_class('Text', '<Control-Key-r>', self.replace)
    self.myText.bind_class('Text', '<KeyRelease>',      self.update_dirty)
    self.myText.bind_class('Text', '<ButtonRelease-1>', self.update_menu) 
    ...
# МЕТОДЫ МЕНЮ
    ...
# добавление меню edit в панель меню редактора
  def editMenu(self):
    ...
    self.editMenu.add_command(label = 'find...', 
                     underline = 0, 
                     accelerator = 'ctrl+f',
                     command = self.find)
    self.editMenu.add_command(label = 'find next', 
                     accelerator = 'ctrl+g',
                     underline = 6, 
                     command = self.again)
    self.editMenu.add_command(label = 'replace...', 
                     accelerator = 'ctrl+r',
                     underline = 0, 
                     command = self.replace
    ...

# открытие диалога поиска
  def find(self, event = None):
    global currentFind
    mySelection = self.getSelection()
    self.findDialog = search(self.master, replace = FALSE)
    if mySelection:
      self.myText.selection_clear()
      currentFind = mySelection
    currentFind, dummy = self.findDialog.go(object = self.myText, 
                                            string = currentFind)
    self.update_menu()

# найти снова
  def again(self, event = None):
    global currentFind
    mySelection = self.getSelection()
    if mySelection:
      self.myText.selection_clear()
      currentFind = mySelection
    if currentFind:
      myIndex = self.myText.search(currentFind, 'insert + 1 chars', nocase = 1)
      self.myText.mark_set('insert', myIndex)
      self.myText.see(myIndex) 

# открытие диалога замены
  def replace(self, event = None):
    global fileDirty, currentFind
    mySelection = self.getSelection()
    self.findDialog = search(self.master, replace = TRUE)
    if mySelection:
      self.myText.selection_clear()
      currentFind = mySelection
    currentFind, replaced = self.findDialog.go(object = self.myText, 
                                               string = currentFind)
    if replaced:
      fileDirty = TRUE
    self.update_menu()
    ...
# МЕТОДЫ МОДИФИКАЦИИ

# модификация меню
  def update_menu(self, event = None):
    ...
    if currentFind or self.getSelection():
      self.editMenu.entryconfigure(5, state = NORMAL)
      self.myText.bind_class('Text', '<Control-Key-g>', self.again)
      self.nextButton.configure(state = NORMAL)
    else:
      self.editMenu.entryconfigure(5, state = DISABLED)
      self.myText.unbind_class('Text', '<Control-Key-g>')
      self.nextButton.configure(state = DISABLED)
    
Теперь перейдем к коду класса search...

Код диалога поиска II

Класс search имеет четыре метода:

# запуск диалога
  def go(self, object, string = None):
    if string:
        self.findString_entry.insert('0', string)
    else:
        self.findString_entry.delete('0', END)
    self.findString = string
    self.myObject = object
    self.findString_entry.focus_set()
    self.toplevel.grab_set()
    self.toplevel.focus_set()
    self.toplevel.wait_window()
    self.myObject.tag_delete('myString')
    return self.findString, self.replaced

# найти или найти/заменить строку
  def findText(self, event = None):
    self.myObject.tag_delete('myString')
    try:
      myEntry = self.findString_entry.get()
    except:
      myEntry = None
    if myEntry:
      try:
        self.findString = myEntry
        myIndex = self.myObject.search(myEntry, 
                                      'insert + 1 chars', 
                                       nocase = self.caseVar.get())
        self.myObject.mark_set('insert', myIndex)
        myEndex = '%s + %d %s' % ('insert', len(myEntry), 'chars')
        self.myObject.tag_add('myString',myIndex, myEndex)
        self.myObject.tag_configure('myString', 
                                     background = '#800000', 
                                     foreground = '#FFFFFF')
        self.myObject.see(myIndex)
        if self.replace:
          try:
            mySubstitute = self.replaceString_entry.get()
          except:
            mySubstitute = None
          replaceDialog = question(self.toplevel)
          if mySubstitute:
            answer = replaceDialog.go(title = 'replace',
                     geometry = '269x117+352+334',
                     message = 'Replace ' + myEntry + ' with ' + mySubstitute + '?')
          else:
            answer = replaceDialog.go(title = 'replace',
                     geometry = '269x117+352+334',
                     message = 'Delete this instance of ' + myEntry + '?')
          if answer:
            if not mySubstitute:
              mySubstitute = ''
            self.myObject.tag_delete('myString')
            self.myObject.delete(myIndex, myEndex)
            self.myObject.insert(myIndex, mySubstitute)
            self.replaced = TRUE
      except:
        self.findString = None
        message(self.toplevel,
                geometry = '269x117+352+334',
                message = '"' + myEntry + '" not found')

# заменить все экземпляры строки
  def replaceAll(self):
    self.myObject.tag_delete('myString')
    myInsert = self.myObject.index(INSERT)
    try:
      myEntry = self.findString_entry.get()
    except:
      myEntry = None
    if myEntry:
      try:
        mySubstitute = self.replaceString_entry.get()
      except:
        mySubstitute = ''
      again = TRUE
      while again:
        try:
          myIndex = self.myObject.search(myEntry, 
                                        'insert + 1 chars', 
                                         nocase = self.caseVar.get())
          self.myObject.mark_set('insert', myIndex)
          myEndex = '%s + %d %s' % ('insert', len(myEntry), 'chars')
          self.myObject.delete(myIndex, myEndex)
          self.myObject.insert(myIndex, mySubstitute)
          self.replaced = TRUE
        except:
          again = FALSE
    self.findString = None
    self.myObject.mark_set('insert', myInsert)

# выход из диалога
  def exitFind(self, event = None):
    self.myObject.tag_delete('myString')
    self.toplevel.destroy()
    
Так, это было началом конца. Осталась еще одна деталь - строка состояния...

Строка состояния и печать

Именно в этом редакторе я пишу свои программы на Python, HTML и т.д. - отсюда моноширинный шрифт в текстовом окне и необычный формат Find/Replace. Другая вещь, которая мне нужна,- это номера строк [для сообщений об ошибках]. Строка состояния - простое дополнение, представляющее собой фрейм с меткой внутри, упакованный к левой стороне. Она обновляется при каждом нажатии клавиши и щелчке мыши [всякий раз, когда вызывается либо метод update_menu, либо метод update_dirty]. Все, что он [update_status] делает, - это переводит текущее положение курсора в номера строки и колонки и вставляет их в метку [Label] с помощью configure().

# СОЗДАНИЕ ОКНА РЕДАКТОРА

  def __init__(self, master):
    ...
    self.myStatus = Frame(self.master, config['frame'])
    self.myStatus.pack(side = BOTTOM, fill = X)
    self.statusLabel = Label(self.myStatus, config['label'])
    self.statusLabel.pack(side=LEFT)
    ...

# МЕТОДЫ МОДИФИКАЦИИ

# модификация меню
  def update_menu(self, event = None):
    self.update_status()
    ...
# модификация isDirty при нажатии любой клавиши [KeyRelease]
  def update_dirty(self, event):
    global fileDirty
    self.update_status()
    ...

# модификация строки состояния
  def update_status(self):
    myIndex = self.myText.index('insert')
    myLine, myColumn = string.split(myIndex, ".")
    self.statusLabel.configure(text = 'line: %s column: %s' % (myLine, myColumn))
    
Вот и все! В самом начале предлагался тест, чтобы узнать, нужно ли вам это руководство. Вот и снова он. Попробуйте. Внимательно читайте код и проверяйте, понимаете ли вы его.

Еще одна короткая страничка...

Десять лет назад, когда мир был моложе [и я тоже], я наткнулся на одну новую штуку, когда играл со своей совершенно новой игрушкой под названием Интернет. Она называлась Javascript и была именно тем, что мне было нужно в то время, - давала возможность управлять web-страницами. Я написал учебное руководство Путь Javascript - своего рода путешествие дзэн-буддиста по этому языку. Оно было одним из первых в то время, и я получил много удовольствия от общения с читателями.

Когда пришел Linux, Python стал такой же удачной находкой. Это искрометный, интерпретируемый язык, с помощью которого любители могут писать программы, которые обычно пишутся любителями,- как этот редактор. И Tkinter - это самый настоящий любительский язык [Здесь опущена непереводимая игра слов.- Прим. пер.]. Поначалу мне пришлось затратить больше усилий, чем по-хорошему нужно было бы. Как я уже говорил, думаю, это из-за того, что книги, по которым я учился, были написаны для "настоящих программистов". Итак, это мое второе учебное руководство.

У меня не было никаких проблем с самим Python. Гвидо ван Россум знал что делал на каждом этапе. Особенно мне нравится его подход к упорядоченным наборам данных. Он делает управление данными по-настоящему легким. Книга Марка Лутца и Дэвида Эшера Изучаем Python, выпущенная издательством O'Reilly,- одна из моих любимейших книг по программированию. Если у вас ее все еще нет, обязательно купите. Python and Tkinter Programming ["Python и программирование на Tkinter"] Джона Грейсона оказалась для меня более трудной. Переход к Python Megawidgets был сделан в ней слишком быстро, и я не до конца усвоил основы [вот почему я и написал это руководство]. Спешу добавить, что эта книга является моим настольным справочником по Tkinter, и я пропал бы без ее страниц с уже загнутыми углами. На мой вкус, туда просто стоит кое-что добавить. Эти две книги я считаю лучшими.

Я продвигался весьма медленно в начале этого руководства и, возможно, слишком быстро в конце. Никто не знает, как учатся другие. Надеюсь, что "прогулка" по этому приложению от начала до конца была полезной, и вы узнали кое-что о Tkinter. О своих предложениях пишите мне на e-mail.


Ах, да, печать [думали я забыл?] Существует несколько уровней, на которых можно получить доступ к принтеру. Попробуйте тот, что приведен ниже. Он наиболее "родовой" ("generic"). Если возникли проблемы, обращайтесь сюда: http://www.faqts.com/knowledge_base/view.phtml/aid/6376/fid/551 [Сама ссылка не работает. Осталась ее копия в архиве Интернета.- Прим. пер.]

    self.master.bind_class(self.master, '<Control-Key-p>', self.printor)
    ...
    self.printorImage = PhotoImage(file= os.path.join(appPath, 'print.gif'))
    self.printorButton = Button(self.myTools, config['button'], 
                              image = self.printorImage, 
                              command = self.printor)
    self.printorButton.pack(side = LEFT)
    ....
    self.fileMenu.add_command(label = 'print',
                 underline = 0, 
                 accelerator = 'ctrl+p', 
                 command = self.printor)
....
# печать файла
  def printor(self, event = None):
    openPort = os.popen('lpr', 'w')
    openPort.write(self.myText.get('0.0', END))
    openPort.close()


Автор: Микки Нардо
Web-адрес: http://doctormickey.com/python/
[Сама ссылка не работает. Осталась ее копия в архиве Интернета.- Прим. пер.]

Перевод на русский язык: Филипп Занько
Web-адрес перевода: http://www.russianlutheran.org/python/python.html
Лицензия перевода: Документ переведен на русский язык с любезного разрешения автора. Разрешается свободное распространение и использование настоящего перевода для любых целей при условии сохранения текста перевода в неизменном виде.

О замеченных ошибках, неточностях, опечатках просьба сообщать по электронному адресу:
russianlutheran@gmail.com