Открыв для себя Linux несколько лет назад, я пережил такое чувство, как будто заново обрел свой компьютер. Каким наслаждением было открывать окно терминала и запускать основную машину (basic machine) без всех этих мастеров и сложностей окружения Windows. Я начал изучать Python. На то, чтобы разобраться с графическим интерфейсом Tk, у меня ушло довольно много времени. Окончательно освоившись, я никак не мог понять, почему столь простые вещи казались такими сложными. По-видимому, книги, по крайней мере те, с которыми мне приходилось иметь дело, были либо слишком простыми, либо слишком сложными. Они не были предназначены для таких непрофессиональных ремесленников как я. Короче говоря, я начал писать небольшое учебное руководство по Tkinter, но вмешалась судьба. В дом, где я работал с Linux, ударила молния, и мой компьютер за секунду превратился в хорошо поджаренный тост. Итак, мой первый период увлечения Linux закончился трагически. В то время мне все время приходилось работать, так что ни времени, ни энергии, на то, чтобы собрать еще одну Linux-машину, попросту не было.
Но сейчас я не работаю, и время у меня есть. И когда моему ноутбуку потребовалось заменить жесткий диск, я подумал: "Почему бы не дать Linux еще один шанс?" Linux сильно вырос за эти годы, но сохранил то базовое управление, которое я ценил больше всего. Затем нашлась дискета с моим старым незаконченным руководством по Tkinter, которое меня удивило. Мое собственное учебное пособие серьезно ускорило обучение. Итак, я довел его до конца и предлагаю таким же как я новичкам, которых не устраивают существующие книги. Конечно, создать графический интерфейс для Python можно не только с помощью Tkinter, но, полагаю, есть люди, которые, так же как и я, любят изучать все основательно, вручную составляя код своих приложений.
Настоящее руководство предназначено для людей, начинающих программировать на Python/Tkinter. Простейший способ узнать, подходит ли оно вам,- это просмотреть пять файлов, составляющих итоговую программу [см. ниже]. Если вы в состоянии читать и понимать код в этих файлах, пособие вам не нужно. Если возникают затруднения, вы в нужном месте...
Фактически, это четыре кратких учебных руководства, объединенные в одно.
Дойдя до конца этой части пособия, вы узнаете, как упаковывать сложные окна, подключать меню, а также открывать и сохранять файлы. В оставшихся двух частях мы займемся "начинкой окон".
Именно тут Tkinter перестанет казаться неуклюжим и станет более похожим на те оконные приложения, к которым мы привыкли.
В первом [code.tgz (22K)] содержится весь код python и графические файлы для всех четырех руководств. Перед распаковкой проверьте, что выбранный для этого каталог прописан в переменной окружения Python Path. В другом файле [tutorial.tgz (465K)] - полный текст руководства с рисунками, на случай если вы захотите читать его оффлайн с собственного компьютера. [В русском варианте оформление документа изменено, и файл tutorial.tgz не нужен. Вместо него можно сохранить на свой компьютер HTML-файл, который вы сейчас читаете. Он содержит полный текст руководства с рисунками.- Прим. пер.]
Скопируйте загруженный файл в нужную директорию. Находясь в оболочке bash, перейдите в этот каталог и наберите команду tar xzf code.tgz
[или tar xzf tutorial.tgz
], чтобы файлы распаковались в эту папку.
Чтобы проверить, "видит" ли Python каталог с кодом, запустите editor.py
. Должен загрузиться текстовый редактор!
Python распространяется вместе с графической библиотекой языка Tk/Tcl, содержащейся в модуле Tkinter. Хотя для создания графических интерфейсов пользователя с помощью языка Python существует много других инструментов, полезно, начиная изучать этот язык, уделить время стандартному пакету, прежде чем браться за более современные. Настоящее краткое руководство поможет быстро разобраться с Tkinter. Дойдя до конца, вы овладеете всеми принципами, необходимыми для построения более сложных графических интерфейсов, и будете представлять общую объектно-ориентированную модель языка Python. Лично для меня вполне достаточно возможностей Tkinter. Для других - это трамплин к более сложным графическим решениям.
Это учебное пособие также "похоже на веб-страницу 90-х". Каждая из последующих страниц следует определенному шаблону. Окно с кодом сопровождается подробными комментариями. Перемещаться по ним можно посредством обычных кнопок [prev][home][next] или с помощью оглавления, приводимого ниже:
Наверное самый лучший способ работы с предлагаемым материалом - изучать код, запускать его на компьютере и затем снова анализировать код. В комментариях разъясняется каждый новый фрагмент кода по мере его расширения.
Данный модуль Python
Экземпляры классов - это объекты, обладающие свойствами и методами. Объектно-ориентированные читатели уже знают об этом.
Приведенный здесь модуль Python -
Мы движемся быстро. Мы умеем создавать родительское окно и любое число дочерних окон. Закрытие родительского окна вызывает закрытие и окна дочернего, но не наоборот.
Уже похоже на интерфейс...
Python - это объектно-ориентированный язык программирования.
Следующей задачей данного руководства будет воспроизведение визуального эффекта, достигнутого в последнем примере [window_02.py], объектно-ориентированными средствами, заложенными в Python, т.е. путем создания классов. Приведенный выше код объявляет и определяет класс. Объект - это экземпляр класса. В данном случае мы собираемся создать простые классы родительских и дочерних окон. Позже мы будем формировать классы с их собственными (добавленными) свойствами и методами.
Оставим на время разработку дочернего окна, чтобы сосредоточить все внимание на классах. Рассмотрим, как передавать классу информацию, и добавим к классу элемент управления [в данном случае кнопку (Button)] [и объект и окно].
Строка
Любая кнопка должна что-нибудь делать. В данном случае мы свяжем событие "нажатие кнопки" с открытием дочернего окна из одного из прошлых примеров. Для этого в класс
В некоторых программах требуется для вывода информации создать дочернее окно, продолжая в тоже время использовать главное. При этом часто бывает нужно, чтобы все процессы, происходящие в дочернем окне, завершились до того, как вы продолжите работу. Такое окно называют модальным. Это значит, что оно будет удерживать фокус пока не будет закрыто. В Python дочернее окно можно превратить в модальное с помощью трех методов
Мы хотим, чтобы информация шла от родительского окна к дочернему И от дочернего к родительскому. Последнего можно добиться, добавив к дочернему окну кнопку
При вызове дочернего метода
При возврате метод
Запускаем
Пока все просто. Размещение в дочерних диалоговых окнах
"Перехват" закрытия пользователем главного окна немного сложнее. Для чего это может потребоваться? Допустим, в случае текстового редактора нам бы хотелось фиксировать ситуации, когда не был сохранен измененный файл, чтобы предохранить пользователя от нечаянного уничтожения плодов всех его усилий из-за преждевременного закрытия окна.
Секрет спрятался в строке
Запустите
Основной файл
Просмотрите завершающие части файлов
Итак, запустим все три файла -
Это учебное руководство было написано пять или шесть лет тому назад. Тогда оно не было опубликовано, поскольку я посчитал его слишком примитивным. Позднее это руководство выручило меня самого, облегчив вспоминание Tkinter. Сейчас я сознаю, что оно уже содержало все необходимые идеи, чтобы начать использовать Tkinter для интересующих меня задач. Если вы похожи на меня, то будете применять Python для написания различных утилит, которые часто бывают полезны в повседневной жизни. Это превосходный язык для таких целей [а Tkinter - почти совершенный GUI]. Но для создания коммерческих мегапрограмм, чтобы заработать много денег, ни Python, ни Tkinter не годятся. [Как, впрочем, и само программирование в целом. Лучше идите учиться на менеджера!].
Перечислим элементы управления, с которыми мы уже успели познакомиться, вместе с их свойствами и методами, использованными в примерах. Общий стиль Tkinter - это размещение свойств внутри круглых скобок:
Мы уже извлекли из этой демонстрационной программы всю пользу, какую только можно было. Настало время заняться созданием "настоящей программы" [более крупного "Hello World!] - на этот раз - текстового редактора. В ходе работы мы повстречаемся с новыми элементами управления: линейками прокрутки, линейками меню и кнопками меню, выпадающими списками, кнопками-флажками и т.п. Приятный сюрприз: за исключением пары ухищрений вы уже умеете пользоваться ими. Цель нового проекта - разработка работающего текстового редактора [если хотите его испытать, запустите
Итак, пришло время переходить к следующей части руководства.Создание окна
window_01.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
# импортирование модулей python
from Tkinter import *
#создание окна
root = Tk()
root.title('myWindow')
root.geometry('200x150+300+225')
# вывод окна на экран
root.mainloop()
window_01.py
создает пустое окно 200x150 с названием "myWindow", расположенное в центре экрана.
#!/usr/bin/python
# импортирование модулей python
from Tkinter import *# создание окна
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
# -*- coding: utf-8 -*-
# импортирование модулей 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()
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
# -*- coding: utf-8 -*-
# импортирование модулей 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()
Tk()
и Toplevel()
являются классами Tkinter, принимающими форму объектов для создания на экране графических окон. Программирование на Tkinter подразумевает комбинирование и преобразование встроенных классов Tkinter в новые классы с индивидуальными свойствами и методами.
# класс родительских окон
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()
main
и child
. Команды, приведенные выше, сначала задают значение переменной root
, чтобы создать экземпляр Tk()
, затем открывают окно, активируя main
[который, в свою очередь, активирует child
]. Обратите внимание, что mainloop()
расположен "внутри" класса main
. Запустите window_03.py
. Он должен сделать то же самое, что и предыдущий пример.
Если этот пример - ваша первая встреча с классами, объектами и т.п., в голову вполне может прийти мысль: "К чему такие сложности?" Но стоит нам перейти к более сложным примерам, как тотчас станет очевидно, что инкапсуляция кода внутрь классов - это отличный способ писать лаконичные, пригодные для многократного использования программы на Python. Потерпите немного. Некоторые из нас помнят, как учились составлять "макаронные" программы ['spaghetti' coding - слабо структурированные программы с большим размером процедур и интенсивным использованием оператора goto; трудны для изучения и модификаций (Электронный словарь ABBYY Lingvo 10).- Прим. пер.], а потом изучали "структурное" программирование. Так что "объектно-ориентированное" программирование - это еще одна новинка...Классы и элементы управления
window_04.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
# импортирование модулей 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)
Между прочим, эта кнопка пока ничего не делает. Итак, запускаем
# класс главного окна
class main:
def __init__(self, master):
self.master = master
...
# создание окна
root = Tk()
# запуск окна
main(root)
main
содержит ссылку на переменную root
, которая в программе еще не создана. Я сделал это специально, чтобы продемонстрировать, что классы ничего не делают, пока на их базе не будут созданы экземпляры объектов. Но есть лучший путь достичь того же самого. Объекту можно передать глобальную переменную root
, и именно так сделано в нашем коде. Благодаря вызову main(root)
параметр root
передается переменной master
класса main
. В последующих примерах подобным же образом мы будем передавать классам множество различных параметров.
self.button = Button(self.master, text = 'myButton')
self.button.pack(side = BOTTOM)
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
# -*- coding: utf-8 -*-
# импортирование модулей 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
через параметр master
[в main
], а затем пересылается другому параметру master
[в child
]. Всякий класс имеет свою переменную master
, локальную по отношению к данному классу. Их имена могут быть различными.window_05.py
и испытываем работающую кнопку...Модальное дочернее окно
window_06.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
# импортирование модулей 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)
Toplevel()
:
self.slave.grab_set()
child
перехватывает все события, происходящие в приложении.
self.slave.focus_set()
child
захватывает фокус.
self.slave.wait_window()
child
ждет, когда будет уничтожен текущий объект, не возобновляя работы [но и не оказывая влияния на основной цикл].
Итак, рецепт. Если нужно создать модальное окно, воспользуйтесь этими тремя методами. Как говорится, "Просто сделай это!"Отсылка сообщения от родительского элемента к дочернему
Модальные дочерние окна используются, главным образом, в роли диалоговых окон. Нам нужен способ передачи информации от родительского окна к дочернему и наоборот. Сперва пойдем от
window_07.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
# импортирование модулей 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
. В данный пример внесено три добавления по сравнению с предыдущими:
main
.child
для отображения введенной информации.main
текстовому окну в child
.
self.text = Text(self.master, background = 'white')
self.text.pack(side = TOP, fill = BOTH, expand = YES)
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)
def __init__(self, master, myText = '')
. Информация из myText
вставляется в элемент управления text с помощью метода self.text.insert('0.0', myText)
, который помещает ее, начиная со строки 0 символа 0. Испытайте window_07.py
, напечатав в нем какой-нибудь текст и отослав его...
Общение - хорошая штука, но оно должно быть улицей с двухсторонним движением...
Отсылка сообщений в обоих направлениях
window_08.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
# импортирование модулей 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
. Нам придется еще довольно много иметь дело с этим методом...Усложнение диалогов
Мы приближаемся к концу начала. Рассмотрим еще несколько приемов, которые будут использоваться позднее в "реальном" приложении: кнопки принятия (accept) и отмены (cancel), а также "перехват" закрытия основного окна с соответствующим диалогом.
window_09.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
# импортирование модулей 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)
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
# -*- coding: utf-8 -*-
# импортирование модулей 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
# -*- coding: utf-8 -*-
# импортирование модулей 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
# -*- coding: utf-8 -*-
# импортирование модулей 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. Кроме того, в легенде о Граале рассказывается о том, как нечто искали повсюду, и, когда, наконец, оно было найдено, оказалось, что ищущий обладал им с самого начала.
Имя_класса(свойство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)
editor.py
]. Это учебное руководство посвящено только Tkinter, а не всему Python. Таким образом, хотя в предлагаемом вашему вниманию коде будут программные функции, реализованные на Python, обсуждение будет сфокусировано преимущественно на интерфейсе Tkinter. И снова вы будете удивлены. Даже если вы никогда раньше не работали с Python, подавляющая часть кода будет понятной.
Tk(), Toplevel(), Button(), Text(), Label()
и Frame()
. В этом руководстве нам предстоит сконструировать простой, работающий текстовый редактор. В процессе придется поработать с некоторыми другими элементами управления Tkinter и научиться как связывать разные части интерфейса воедино.Первый шаг в разработке графического интерфейса - получение нужных окон:
Итак, на следующих нескольких страницах мы создадим классы для этих окон [диалог подтверждения уже есть, например yesno], а в завершение свяжем эти диалоги друг с другом.
Замечание: Код для этой версии главного окна редактора довольно длинный. Возможно на следующих страницах вам потребуется к нему обращаться. Здесь он не приводится, но его можно вызвать в выпадающем окне, если щелкнуть мышкой по текстовой иконке, расположенной выше [ниже]. И далее в тексте с помощью этой текстовой иконки также можно будет вызывать код, обсуждаемый в том или ином разделе. [В русском переводе оформление документа изменено. Текст программы вывести в выпадающее окно нельзя. Пожалуйста, для просмотра открывайте нужный код в любом текстовом редакторе. Прим. пер.]
Запустите editor_01.py.
Все модули спроектированы так, что их можно запускать как отдельные программы. В конце мы соберем их всех вместе. Фрагмент кода, связанный с текущим обсуждением, будет приводиться прямо здесь.
Главному окну нашего текстового редактора нужны панель меню с двумя заголовками главного меню [
file
(файл) и edit
(правка)], элемент управления text и линейка прокрутки. Использование метода pack
для создания интерфейса Tkinter подразумевает, что элементы управления будут размещаться в определенной последовательности. Посмотрите анимацию, потом взгляните на разъяснение и код:
Tk()
].
def __init__(self, master): self.master = master self.master.title('editor') self.master.iconname('editor') self.master.geometry('600x400+100+100') |
Frame( )
].
self.myBar = Frame(self.master, relief = RAISED, bd=2) |
Menubutton()
], дочерний по отношению к фрейму панели меню, и упаковываем [pack] его к левой стороне [LEFT].
self.fileMenu() self.editMenu() |
self.myBar = Frame(self.master, relief = RAISED, bd=2) |
Scrollbar()
]. Упакуем ее [pack] к правой [RIGHT] стороне. Пусть она заполнит все доступное пространство по оси X. И сделаем ее расширяемой при изменении размеров окна.
self.myScroll = Scrollbar(self.master) self.myScroll.pack(side=RIGHT, fill=Y) |
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()
, отображающее команды подменю для каждого пункта главного меню. В коде, приведенном ниже:
Menubutton()
как дочерний по отношению к фрейму меню [myBar
].
Menu()
, дочернее по отношению к кнопке меню Menubutton [mbutton
].
add_command()
и add_separator()
класса Menu()
, чтобы добавить в меню новые пункты.
mButton.configure(menu = menu)
. После того как мы заполнили меню [Menu()
] командами, его следует связать с Menubutton()
. Метод configure()
- это способ, каким можно добавлять параметры потом. Параметр menu = 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] окно сообщений. Позднее мы вернемся назад и добавим настоящие команды, сделав это приложение функциональным.
класс 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')
self.myScroll.configure(command = self.myText.yview)
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()
, чтобы скомпоновать окно. Иллюстрации могут подсказать все, что нам нужно, тем не менее, ниже приводятся подробные комментарии. Они довольно многословны [иными словами, скучны]. Если суть схвачена, дальше достаточно бегло просмотреть список и просто читать сам код:
Toplevel()
как дочернее по отношению к главному окну.
self.toplevel = Toplevel(master) self.toplevel.geometry('400x300+200+150') |
bottomFrame = Frame()
] для командных кнопок. Упаковываем [pack] его к нижней [BOTTOM] стороне окна.
self.bottomFrame = Frame(self.toplevel) self.bottomFrame.pack(side=BOTTOM, pady=5) |
padx
и pady
в методе pack()
добавляют пространства вокруг элементов управления по осям X и Y, украшая их таким образом.
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()
фиксирует ширину кнопки. Это тоже для красоты.
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) |
Toplevel()
того же самого цвета и его границы невидимы. Незаполнение сохраняет неизменным расстояние между кнопками [еще одно "украшательство"]. Один фрейм готов - четыре на подходе.
[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
[фрейм_следующий_вверх_от_нижнего], но это было бы уже слишком.
[selection = Entry()]
для выбора файла, дочерний по отношению к фрейму selectionFrame, и упакуем его к левой [LEFT] стороне этого фрейма. Пусть он заполнит доступное пространство по оси X. Сделаем этот элемент управления расширяемым при изменении размеров окна.
self.selection = Entry(self.selectionFrame, background = 'white') self.selection.pack(side=LEFT, expand=YES, fill=X) |
[directoryFrame = Frame()]
для меню каталогов как дочерний по отношению к окну верхнего уровня Toplevel. Упаковываем [pack] его к верхней [TOP] стороне этого окна.
self.directoryFrame.pack(side=TOP, pady=8) self.directory = self.createMenu(self.directoryFrame) |
directoryFrame
и упаковываем [pack] его к левой [LEFT] стороне этого фрейма. Пусть этот элемент управления заполняет свободное место по оси X. Сделаем его расширяемым при изменении размеров окна.
self.directory = self.createMenu(self.directoryFrame) self.directory.pack(side=LEFT, expand=YES, fill=X) |
[labelFrame = Frame()]
для текстовых надписей 'folders:' и ' files: ' как дочерний по отношению к окну верхнего уровня Toplevel. Упаковываем [pack] его к верхней [TOP] стороне этого окна.
self.labelFrame = Frame(self.toplevel) self.labelFrame.pack(side=TOP, padx=5, fill=X) |
[folderLabel = Label()]
для надписи 'folders:' как дочерний по отношению к labelFrame и упаковываем [pack] его к левой [LEFT] стороне этого фрейма. Разрешаем этому элементу заполнить все свободное место в направлении оси X. Делаем его расширяемым при изменении размеров окна.
self.folderLabel = Label(self.labelFrame, text = 'folders:') self.folderLabel.pack(side=LEFT, expand=YES, fill=X) |
[fileLabel = Label()]
для надписи ' files: ' как дочерний по отношению к labelFrame и упаковываем [pack] его к правой [RIGHT] стороне этого фрейма. Разрешаем этому элементу заполнить все свободное место в направлении оси X. Делаем его расширяемым при изменении размеров окна. Остался только один фрейм!
self.fileLabel = Label(self.labelFrame, text = ' files: ') self.fileLabel.pack(side=RIGHT, expand=YES, fill=X) |
[middleFrame = Frame()]
для списков папок и файлов, дочерний по отношению к окну верхнего уровня Toplevel. Разрешаем фрейму заполнять доступное пространство в направлении обоих [BOTH] осей. Делаем фрейм расширяемым при изменении размеров окна.
self.middleFrame = Frame(self.toplevel) self.middleFrame.pack(expand=YES, padx=5, fill=BOTH) |
[filesBar = Scrollbar()]
. Упаковываем [pack] ее к правой [RIGHT] стороне. Разрешим ей заполнять доступное пространство по оси Y.
self.filesBar = Scrollbar(self.middleFrame) self.filesBar.pack(side=RIGHT, fill=Y) |
[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()
тоже новый для нас элемент управления. Он содержит списки [да ну!]. Мы вскоре увидим, как он работает.
[subBar = Scrollbar()]
. Упаковываем [pack] ее к левой [LEFT] стороне. Пусть она заполняет все доступное пространство в направлении оси Y.
self.subBar = Scrollbar(self.middleFrame) self.subBar.pack(side=LEFT, fill=Y) |
[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(): процесс привязки линейки прокрутки 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]) |
('.-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 |
Если вы - опытный программист или уже знаете Python и просто ищете здесь чего-нибудь новенького, эта последняя часть вряд ли затруднила вас и вы готовы читать дальше. Но если все сказанное было новым и трудным для понимания, сделайте сейчас перерыв. Сходите, посмотрите Comedy Central [Американский кабельный юмористический телеканал. На нем, в частности, был создан известный анимационный сериал "Южный парк".- Прим. пер.], вернемся к этому позже.
В последних шести разделах мы построили существенную часть графического интерфейса текстового редактора. Следующие пять страниц будут посвящены кое-чему другому, а именно - как заставить его работать. Думаю, что мы создаем объекты Tkinter наилучшим способом. Сначала компонуем графический интерфейс и добиваемся, чтобы он выглядел так как надо. Затем так же поступаем с кодом, приводящим все это в действие,- проектируем методы почти тем же способом, что применялся при создании графической части.
refreshDisplay
и parseDirectory
. При смене каталога [либо через верхнее меню папок, либо через находящийся слева список подкаталогов] вызывается метод refreshDisplay
. В свою очередь refreshDisplay
использует метод parseDirectory
для построения списка для верхнего меню, обновляет его, затем корректирует все остальные элементы управления. Именно refreshDisplay
приводит в действие диалог для работы с файлами. Итак, первоочередная задача: придать диалогу для работы с файлами форму автономного объекта [объекта на экране и компьютерного класса/объекта].openDialog
(диалог для открытия файлов) и saveDialog
(диалог для сохранения файлов), выполняющих соответствующие функции.
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 |
set_selection(file)
- это функция, получающая в качестве аргумента имя файла и выводящая его на экран через переменную selection
элемента управления Entry().# переменной selection присваивается значение, равное имени файла def set_selection(self, file): self.selection.delete('0', END) self.selection.insert('0', file) |
get_selection
осуществляет обратный процесс - возвращает имя файла из переменной selection
элемента управления Entry().# возвращает имя выделенного файла из переменной selection def get_selection(self): return self.selection.get() |
accept
реагирует на нажатие кнопки accept_button
таким образом, что записывает имя файла из переменной selection
элемента управления Entry() в новую переменную newValue
, затем закрывает окно.# при нажатии мышкой на кнопку accept def accept(self): self.newValue = self.get_selection() self.toplevel.destroy() |
cancel
реагирует на нажатие кнопки cancel_button, закрывая окно и не присваивая переменной newValue
нового значения.
# при нажатии мышкой на кнопку cancel def cancel(self): self.toplevel.destroy() |
evaluateDirectory
реагирует на выбор директории в меню directory
и передает ее имя refreshDisplay()
.# обработка строки с именем текущего каталога def evaluateDirectory(self): self.refreshDisplay(self.menu.entrycget(ACTIVE, 'label')) |
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))) |
files_select_event
реагирует на выбор файла в списке files
и выводит его имя в переменную selection
элемента управления Entry().# при щелчке мышкой по списку файлов def files_select_event(self, event): self.set_selection(self.files.get('active')) |
files_double_event
реагирует на двойной щелчок мышью по файлу в списке files
и выводит его имя на экран через переменную selection
элемента управления Entry(); затем вызывается accept
.# при двойном щелчке мышкой по списку файлов def files_double_event(self, event): self.accept() |
Сперва взглянем на метод go
. Он инициализирует окно/объект. Обратите внимание, что он по умолчанию начинает свою работу с текущего рабочего каталога. Если передать ему startPath
, в элементе управления отобразится заданный файл или подкаталог. Переменная newValue
вначале остается пустой. В ней будет храниться имя файла, если его запишет в нее класс files.
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
, обновляющие визуальные элементы управления, когда ничего не изменяется.
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('') |
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 |
os
и string
. Это хороший пример программирования автономного модуля Python в том смысле, что при этом создается класс, который можно использовать во многих приложениях.
Базовая идея ООП - писать код в виде объектов, выполняющих какие-то действия. Получить доступ к объектам можно через их свойства и методы. Объекты могут быть многократного использования, изменяемыми или расширяемыми, но их внутренняя работа должна оставаться невидимой, почти неосязаемой. Характеристики объекта:
files
, который представляет собой родовой (generic) диалог для работы с файлами. Мы хотим применить его в частности для двух разных окон - Open File Dialog (диалог открытия файла) и Save File Dialog (диалог сохранения файла). Прежде всего воспользуемся полиморфизмом объекта, взаимодействуя только с его свойствами и методами. У нашего объекта есть свойство title
, присвоим ему значение Open File Dialog с помощью команды:files.go(title = "Open File Dialog")
files.go(title = "Save File Dialog")
Начало положено, но нам нужно, чтобы они и вели себя по-разному. Как и для заголовка, для входной информации есть свои переменные.
thisDirectory
], и мы хотим, чтобы диалог открылся именно в ней, можно написать:files.go(title = "Open File Dialog", startPath = thisDirectory)
startPath
и диалог откроется в каталоге по умолчанию:
files.go(title = "Open 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
:
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
, путь к текущему рабочему каталогу; fileName
, полный путь к открытому в настоящий момент файлу; и fileDirty
, принимающая значение TRUE, после того как файл подвергся редакции.
<KeyRelease>
[любая клавиша отпущена] к методу update_dirty
. Это значит, что любое нажатие клавиши пометит файл как отредактированный [bind_class означает любое нажатие клавиши в объекте, полностью порожденном классом main]. [Замечание: Это слишком чувствительное условие. Мы еще поговорим о нем позже.]
isdirty()
[описанный ниже] в методы exitMethod
и openFile
, так чтобы пользователь смог сохранить текущий файл, если тот был отредактирован.
изменения! |
---|
# глобальные переменные 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 |
Есть три метода для открытия файлов:
new
: new
служит для открытия новых файлов. Заметьте, что глобальные переменные, чтобы стать "видимыми", должны быть объявлены "внутри" метода. Итак, сначала метод проверяет, не нужно ли сохранить уже существующий файл. Затем он очищает элемент управления text, называет его "text editor" ["текстовый редактор"], очищает переменную fileName
и присваивает параметру fileDirty
значение "чистый" [fileDirty=FALSE
.- Прим. пер.].open
: open
содержит глобальные переменные и проверяет, является ли текущий файл "чистым". Следующий фрагмент кода просматривает текущий файл, и если его путь указывает на существующую папку, то диалог открытия файлов откроется именно в ней. В противном случае, по умолчанию он начнет работу в каталоге, где находится приложение. Наконец, путь выбранной папки пересылается диалогу открытия файлов как описано ранее. Если этот диалог возвращает файл, вызывается fileOpen
.fileOpen
: fileOpen
открывает данный файл и помещает его в очищенное текстовое окно, меняя значение переменной fileDirty
, а также присваивая значение переменной fileName
[имя файла] и заголовку окна title
.
классы для открытия файлов |
---|
# открытие нового файла 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) |
isDirty
: isDirty
- это не о грязных пятнах, а о том, что файл был изменен. [Dirty - по-английски означает "грязный".- Прим. пер.] Он вызывается, когда вы открываете файл или выходите из редактора. Если текущий файл был изменен, используется диалог question
, задающий вопрос, хотите ли вы сохранить этот файл. Если ответ - "да", вызывается метод save
.save
: save
проверяет, существует ли текущий файл. Если существует, то сохранение происходит без вызова диалога сохранения файлов и данный файл помечается как "чистый". Если нет [скажем, файл новый], он передается методу saveAs
.
saveAs
: saveAs
сначала делает проверку. Если текущий файл существует, он пересылается диалогу. Если нет, но существует путь к подкаталогу, то последний передается диалогу. В противном случае передается appPath
. Открытие диалога сохранения файлов происходит так, как описано выше. Если возвращается имя файла, оно пересылается методу fileSave
.
fileSave
: fileSave
фактически записывает файл на диск по указанию остальных трех методов, затем определяет значения заголовка [title
] редактора, переменной filename
[имя файла] и помечает этот файл как "чистый" [fileDirty=FALSE
.- Прим. пер.].
классы для сохранения файлов |
---|
# сохранение отредактированного файла 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) |
<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
'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
Эта часть пособия не просто о внешнем виде программы, она сконцентрирована на том, как интерфейс выглядит на экране, даже если мы говорим о функциональности. Мы рассмотрим "горячие" сочетания клавиш, создадим панель инструментов и покажем, как управлять внешним видом элементов управления 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 |
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 останется пустой. Очевидно, что в такой ситуации опция сохранения в меню не играет никакой роли. Раз так, пусть она будет отключена [рисунок сверху справа]. Хотя сейчас этот момент не так уж и важен, мы используем его, чтобы научиться динамически изменять команды интерфейса. Позднее этот прием приобретет большее значение, когда мы будем добавлять программе функциональности.
save
была неактивной.
# добавление меню 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', state = DISABLED, 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 |
state
значение DISABLED
- вот и весь фокус, если дело касается меню.update_menu()
и перемещаем в него команду привязки.
# модификация меню 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) else: self.master.unbind_class(self.master, '<Control-Key-s>') self.fileMenu.entryconfigure(3, state = DISABLED) |
fileName
. По тому же принципу другая команда [self.fileMenu.entryconfigure(3, state = ___)
] задает значение переменной state
этого пункта меню. В данном случае речь идет о пункте меню номер 3 [s
ave
].Text()
.
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.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() |
Запустите editor_04.py
и посмотрите, как все работает...
Эта страница проста - нет никаких новых крупных концепций, а все результаты сразу видны на экране. Мы хотим добавить к нашему интерфейсу панель инструментов. Вот несколько иконок [все они в формате .gif по определенной причине]. В их число я включил и те, что мы добавим позднее. Конечно, вы можете сделать свои собственные:
Единственная новая вещь в этом разделе - это то, каким образом Tkinter имеет дело с графическими изображениями. Имеются две встроенные функции PhotoImage()
и BitmapImage()
для работы с файлами в формате .gif и .bmp, соответственно. Для изображений других типов есть свободная библиотека PIL Library. Мы воспользуемся PhotoImage()
. Два важных момента:
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
, чтобы испытать полностью завершенные "горячие" клавиши. Я рассмотрел эту тему, пока программа была еще сравнительно простой. При добавлении оставшихся пунктов меню код ее будем модифицировать автоматически. Учиться тут будет нечему, знай повторяй те же самые процедуры...
Запустите прямо сейчас 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. Мне лично он не нравится [не достаточно контроля]. Информация об этом есть в книжках.
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.
Ну, как вам? Вот как это делается:
configure_01.py
как configure.bak
[так чтобы потом можно было к нему вернуться].configure.py
[ниже] как configure_01.py
.editor_06.py
В последнем примере мы изменили только определенные свойства элементов управления, в остальных случаях довольствуясь значениями, заданными по умолчанию. В версии, приведенной ниже, 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'] |
Теперь перейдем к обработке текста...
Содержание |
Вырезать, копировать и вставить |
Окно диалога поиска |
Код диалога поиска 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. Последние три уже готовы! |
<<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() |
Find
и Find Next
работают в точности так, как это и можно было ожидать. Если открыть Replace
и нажать Find Next
, программа будет искать заданную строку. Когда она обнаружится, в диалоговом окне будет предложено заменить ее [или удалить, если поле replace with
оставить пустым]. Replace All [заменить все] делает именно то, что означают эти слова.
Сначала обратимся к коду класса editor. Мы уже перешли к файлу editor_09.py
. Меню, горячие клавиши и иконки панели инструментов пояснений не требуют. Появилась новая глобальная переменная, содержащая в себе текущую строку поиска, currentFind
. В зависимости от состояния этой переменной или наличия выделенного текста меняется меню. В одном случае команда find next доступна. Если же переменная currentFind
пустая или никакого текста не выделено, эта команда не доступна.
Три метода, которые представляют интерес:
find
: Открывает окно find dialog. Он проверяет, выделен ли какой-нибудь текстовый фрагмент. Если да, то изменяется значение переменной currentFind
. Затем этот метод ссылается на содержимое текстового элемента управления и пересылает currentFind
в класс search [задавая replace
равным FALSE]. Возвращается новое значение currentFind
, и по закрытии диалога модифицируется меню.
again
: Никаких диалоговых окон с этим методом не связано. Он делает ту же проверку, есть ли выделенный текст, а потом осуществляет поиск этого текста, начиная со следущего за курсором символа ['insert + 1 chars'
]. Если искомый текст обнаружен, курсор перемещается на него.
replace
: Тот же самый процесс, что и в случае с find, с двумя изменениями. Переменная replace
равна TRUE
, а класс search возвращает replaced
как TRUE
, если в тексте были сделаны изменения. В этом случае переменной fileDirty
присваивается значение TRUE
.
# ЗАДАНИЕ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ ... 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
...
search
имеет четыре метода:
go
: Вездесущий метод go
вставляет полученную строку currentFind
в текстовое поле ввода find
и передает фокус ему. findString
содержит текстовую строку поиска, а myObject
указывает на текст в текстовом элементе управления редактора. При выходе возвращаются новые значения findString
и replaced
.
findText
: Это метод двойного назначения. Если есть строка поиска, он ищет ее в тексте. Когда обнаруживается фрагмент, совпадающий со строкой поиска, метод использует ограничители myIndex
и myEndex
, чтобы задать две вещи: метку [mark
] под названием insert
и теги [tags
] myString
. Метка mark
- это куда перемещается курсор. Теги tags
идентифицируют строку и подсвечивают ее. Поэтому в этой точке результатом будет подсветка следующего фрагмента текста, совпадающего со строкой поиска. Если речь идет о диалоге поиска, то на этом все и кончается.
Если, однако, вы находитесь в диалоге замены, если имеется строка replace with
, вас спросят, не хотите ли вы заменить найденный текст. Если строки replace with
нет, вам предложат удалить найденный текст. При положительном ответе запрашиваемая операция будет проведена, при этом вставка [удаление] будут определены при помощи myIndex
и myEndex
. replaced
присваивается значение TRUE
.
replaceAll
: replaceAll
действует в точности как findText
, за исключением того, что не задает никаких вопросов и совершает полный проход по всему тексту, пока не будут заменены все найденные совпадения со строкой поиска. По завершении он возвращает вас туда, откуда вы начали.
exitFind
: закрывает диалоговое окно и удаляет тег.
# запуск диалога 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() |
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()
Python для инженеров и исследователей
Автор: Микки Нардо
Перевод на русский язык: Филипп Занько
Лицензия перевода: Документ переведен на русский язык с любезного разрешения автора. Разрешается свободное распространение и использование настоящего перевода для любых целей при условии сохранения текста перевода в неизменном виде.
О замеченных ошибках, неточностях, опечатках просьба сообщать по электронному адресу:
russianlutheran@gmail.com