Открытое программное обеспечение и Camaro
Некоторые из программ с графическим интерфейсом, представленные в этой главе и в оставшейся части книги, являются аналогами утилит, которые можно найти в наиболее распространенных операционных системах, таких как Windows. Например, мы рассмотрим калькуляторы, текстовые редакторы, средства просмотра графических изображений, часы, клиенты электронной почты и другие.
Но, в отличие от большинства утилит, эти программы являются переносимыми - благодаря тому, что они написаны на языке Python с применением библиотеки tkinter, эти программы способны работать на всех основных платформах (Windows, Unix/Linux и Mac). Но самое важное, пожалуй, - они могут настраиваться под личные предпочтения, благодаря доступности исходных текстов, - вы можете изменять их внешний вид или функциональные возможности, просто дописав или изменив программный код на языке Python.
Приведу аналогию, чтобы подчеркнуть важность возможности что-то настраивать и переделывать под себя. Среди нас еще есть люди, которые помнят времена, когда считалось нормальным, если владелец автомобиля сам ухаживал за ним и ремонтировал его. Я с нежностью вспоминаю, как в годы моей юности мы с друзьями увлеченно копались под капотом Chevrolet Camaro 1970, ремонтируя и отлаживая его двигатель. Приложив совсем немного усилий, мы смогли увеличить его скорость, приемистость и придать его работе звучание, услаждавшее наш слух. Кроме того, поломка какого-то из наших старых автомобилей не была для нас концом света. Всегда оставался шанс самостоятельно починить его.
Сейчас все изменилось. С появлением электронных средств управления и дьявольски тесных моторных отсеков владельцы автомобилей стали предпочитать пользоваться услугами специалистов в любых, даже в самых простых случаях. В целом, автомобили перестали быть продуктом, доступным для самостоятельного ремонта. И если в моем новеньком, сверкающем экипаже случится поломка, я наверняка застряну на дороге, пока подготовленный специалист не найдет время, чтобы отбуксировать его и отремонтировать.
Я люблю сравнивать закрытую и открытую модели разработки программного обеспечения, оперируя теми же понятиями. Когда я использую программы корпорации Microsoft, такие как Notepad и Outlook, я ограничен возможностями, предусмотренными компанией-производителем, а также вынужден мириться со всеми ошибками, которые могут скрываться в этих программах. А в случае с такими программными инструментами, как PyEdit и PyMailGUI, у меня сохраняется возможность «залезть под капот». Я могу добавлять новые особенности, настраивать систему и исправлять любые ошибки, какие будут обнаружены. И могу сделать это намного быстрее, чем Microsoft выпустит очередной набор исправлений или новую версию своего продукта. Я не завишу от компании, действующей в общем-то в своих интересах, если мне требуется поддержка или даже продолжение разработки инструментов, которыми я пользуюсь.
Конечно, я по-прежнему завишу от языка Python и от тех изменений, которые могут в него вноситься с течением времени (после обновления двух книг, по тысяче страниц с лишним в каждой, под Python 3.X, я с определенной уверенностью могу сказать, что эта зависимость не всегда является тривиальной). Однако наличие исходных текстов для всех программных инструментов, на которые вы полагаетесь, все равно можно считать мощной поддержкой и крупным преимуществом. А кроме того, открытая модель способствует повышению надежности, предоставляя возможность сообществам людей тестировать и развивать систему.
В конечном счете, открытое программное обеспечение и Python чаще всего ассоциируются со свободой, тогда как закрытое - с ценой. Последнее слово здесь остается за пользователями, а не за какой-то далекой компанией. Конечно, не каждый захочет возиться со своим автомобилем. Но, с другой стороны, программное обеспечение имеет свойство терпеть неудачу намного чаще, чем автомобили - ломаться, да и программирование на языке Python является менее грязной работой, чем работа автомеханика.
PyEdit: программа/объект текстового редактора
За последние десятилетия мне пришлось набирать текст во многих программах. Большинство из них были закрытыми системами (мне приходилось довольствоваться теми решениями, которые были воплощены их разработчиками), и многие работали только на одной платформе. Представленная в этом разделе программа PyEdit более удачна в обоих отношениях: она реализует полноценный текстовый редактор с графическим интерфейсом в 1133 строках переносимого программного кода на языке Python, включая пробельные символы и комментарии, из которых 1088 строк содержатся в главном файле и 45 строк - в модуле с настройками (к моменту выхода этого издания книги; в будущем его размер может измениться). Несмотря на свой относительно скромный размер, редактор PyEdit оказался достаточно мощным и надежным, чтобы послужить основным инструментом для разработки большинства примеров, приведенных в этой книге.
PyEdit поддерживает все обычные операции редактирования текста с помощью мыши и клавиатуры: удаление и вставка, поиск и замена, открытие и сохранение, отмена и возврат ввода и так далее. Но в действительности PyEdit представляет собой нечто большее, чем просто текстовый редактор, - его можно использовать как программу и как библиотечный компонент, и он может использоваться в разных режимах:
Автономный режим
В качестве автономной программы текстового редактора, с возможностью передачи имени редактируемого файла в командной строке. В этом режиме PyEdit напоминает другие утилиты редактирования текста (например, Notepad в Windows), но, кроме того, предоставляет дополнительные возможности, такие как запуск редактируемой программы на языке Python, изменение шрифта и цвета, поиск во внешних файлах, многооконный интерфейс и так далее. Но особенно важно, что текстовый редактор PyEdit легко можно модифицировать и использовать в Windows, X Window и Macintosh, потому что он написан на языке Python.
Всплывающий режим
Внутри нового всплывающего окна, позволяя программе одновременно выводить произвольное количество экземпляров. Поскольку информация о состоянии хранится в атрибутах экземпляра класса, каждый созданный объект PyEdit действует независимо от других. В этом и в следующем режимах PyEdit служит библиотечным объектом, используемым другими сценариями, а не готовым приложением. Например, приложение PyMailGUI, представленное в главе 14, использует PyEdit во всплывающем режиме для отображения вложений в электронные письма и простого текста, и оба приложения, PyMailGUI и PyDemos из предыдущей главы, отображают исходный программный код таким способом.
Встроенный режим
В качестве прикрепляемого компонента - виджета редактирования текста для других графических интерфейсов. Будучи прикрепленным, PyEdit использует меню, основанное на фрейме, и может отключать некоторые его пункты, не имеющие смысла во встроенном режиме. Например, программа PyView (рассматривается далее в этой главе) использует PyEdit во встроенном режиме в качестве редактора подписей к фотографиям, а PyMailGUI (в главе 14) прикрепляет его и бесплатно получает редактор текста электронных писем.
Может показаться, что такое поведение с разными режимами трудно реализовать, но на самом деле режимы PyEdit по большей части являются естественным побочным продуктом разработки графического интерфейса с использованием подхода, основанного на применении классов, рассматривавшегося на протяжении последних четырех глав.
Запуск PyEdit
Редактор PyEdit обладает массой возможностей, и лучший способ понять, как он действует, - поработать с ним самостоятельно. Его можно открыть, запустив главный файл textEditor.py или файлы textEditor-
NoConsole.pyw и pyedit.pyw, если желательно подавить появление окна консоли в Windows, или воспользовавшись панелями запуска демонстрационных программ PyDemos и PyGadgets, которые были описаны в конце главы 10 (сами запускающие программы находятся на верхнем уровне дерева каталогов примеров книги). Чтобы вы могли получить представление об интерфейсах PyEdit, на рис. 11.1 изображено главное окно программы - как оно выглядит по умолчанию в Windows 7 после открытия файла с исходными текстами PyEdit.
Рис. 11.1. Главное окно редактора PyEdit с программным кодом самого редактора
Главную часть этого окна составляет виджет Text, и если вы прочли его описание в главе 9, вам должны быть знакомы операции редактирования текста, выполняемые PyEdit. В нем используются метки, теги и индексы, и реализованы операции удаления и вставки через системный буфер обмена, который позволяет вставлять скопированные данные, даже после того как приложение-источник было закрыто. Для поддержки произвольного перемещения по содержимому файлов с виджетом Text взаимно связаны вертикальная и горизонтальная полосы прокрутки.
Меню и панели инструментов
Меню и панели инструментов редактора PyEdit должны показаться вам знакомыми - он строит главное окно, используя минимальный объем программного кода, и обеспечивает действие соответствующих правил обрезания и растягивания путем внедрения класса GuiMaker, с которым мы познакомились в предыдущей главе (пример 10.3). Панель инструментов внизу окна содержит кнопки для быстрого доступа к операциям, которыми я пользуюсь чаще всего; если ваши вкусы не совпадают с моими, просто измените список кнопок панели инструментов в исходном программном коде, чтобы в нем оказались кнопки, которые вам нужны (в конце концов, это Python).
Как обычно, в меню tkinter для быстрого вызова элементов меню можно использовать горячие клавиши - следует нажать ALt и все подчеркнутые клавиши на пути к нужному действию. Меню могут также отрываться по пунктирной линии и тем самым обеспечить быстрый доступ к пунктам меню в новом окне верхнего уровня (удобно, когда отсутствует панель инструментов с кнопками).
Диалоги
PyEdit выводит различные модальные и немодальные диалоги, стандартные и собственные. На рис. 11.2 изображены нестандартные, немодальные диалоги поиска с заменой, выбора шрифта и поиска во внешних файлах, а также стандартный диалог для вывода информации о файле (окончательные значения счетчиков в последних строках могут измениться, потому что я имею обыкновение изменять программный код и добавлять комментарии вплоть до окончания проекта).
Рис. 11.2. PyEdit с измененными цветами, шрифтом и некоторыми диалогами
В главном окне на рис. 11.2 установлены новые цвета переднего плана и фона (с помощью стандартного диалога выбора цвета) и новый шрифт, который можно установить либо с помощью диалога выбора шрифта, либо из имеющегося в сценарии готового списка, который пользователи могут изменять в соответствии со своими предпочтениями (в конце концов, это Python). Другие операции, выполняемые с помощью панели инструментов и меню, обычно используют стандартные диалоги с некоторыми дополнениями. Например, при работе со стандартными диалогами открытия и сохранения файлов в PyEdit используются интерфейсы на основе объектов, которые запоминают каталог, выбиравшийся последним, и устраняют необходимость каждый раз заново переходить к нему.
Запуск программного кода
Одной из уникальных особенностей PyEdit является возможность запуска редактируемого в нем программного кода на языке Python. Это не так сложно, как может показаться. В Python имеются встроенные функции компиляции и выполнения строк программного кода, а также запуска программ, поэтому редактору PyEdit остается лишь выполнить вызовы нужных функций. В частности, на языке Python легко можно написать простенький интерпретатор Python, как показано ниже (если вы захотите поэкспериментировать с ним, найдите файл simpleShell.py в каталоге с реализацией PyEdit), хотя осуществить обработку многострочных инструкций и отображение результатов выражений несколько сложнее.
# читает и выполняет строки с инструкциями на языке Python: подобно тому,
# как действует пункт ‘Run Code’ в меню PyEdit
namespace = {}
while True: try:
line = input(‘>>> ‘) # только однострочные инструкции
except EOFError: break else:
exec(line, namespace) # или eval() и вывод результата
В зависимости от предпочтений пользователя редактор PyEdit или делает что-то подобное этому, чтобы выполнять программный код, выбираемый из текстового виджета, или использует модуль launchmodes, который мы написали в конце главы 5, чтобы запустить файл с кодом как независимую программу. В обеих схемах могут быть использованы различные варианты, которые можно настроить по своему вкусу (в конце концов, это Python). Детали реализации смотрите в методе onRunCode или просто отредактируйте и выполните свой собственный программный код на языке Python. Когда выполняется только выбранная строка программного кода, вы сможете наблюдать результаты в окне консоли редактора PyEdit. Как уже говорилось в примечании о функциях eval и exec в главе 9, этим функциям следует передавать программный код только из проверенных источников - он получает доступ ко всему, что доступно процессу интерпретатора Python.
Несколько окон
Редактор PyEdit способен выводить не только множество окон специального назначения, он также позволяет одновременно открывать несколько окон редактирования - в пределах одного процесса или за счет запуска независимых экземпляров программы. Для иллюстрации на рис. 11.3 показаны три независимо выполняющиеся экземпляра PyEdit с различными размерами, цветовыми схемами и шрифтами. Поскольку все они были запущены как независимые программы, закрытие одной из них не оказывает влияния на другие. На этом рисунке внизу видны также оторванные меню PyEdit и всплывающее окно диалога справки справа. Фоном окон редактирования служат оттенки зеленого, красного и голубого цветов - для установки желаемого цвета выберите в меню Tools элемент Pick.
Рис. 11.3. Несколько окон PyEdit, открытых одновременно
Так как во всех этих трех сеансах PyEdit редактируется программный код на языке Python, их содержимое можно выполнить, выбрав пункт Run Code раскрывающегося меню Tools. Программный код из файлов выполняется независимо - стандартные потоки ввода-вывода программного кода, выполняемого не из файла (например, полученные из самого
текстового виджета), отображаются в окно консоли сеанса PyEdit. Это никоим образом нельзя рассматривать, как IDE (интегрированную среду разработки), - я добавил эту возможность только потому, что она показалась мне полезной. Очень удобно иметь возможность запускать редактируемый программный код, не разыскивая его в дереве каталогов.
Чтобы открыть несколько окон редактирования в пределах одного процесса, используйте пункт CLone в меню TooLs, при выборе которого открывается новое пустое окно без уничтожения содержимого в другом окне. На рис. 11.4 показана ситуация, когда в одном процессе открыты два окна, наряду со всплывающими окнами, имеющими отношение к пункту Grep меню Search, о котором рассказывается в следующем разделе, -который позволяет произвести обход дерева каталогов в параллельных потоках выполнения, отобрать файлы с именами, соответствующими шаблону и содержащими искомую строку, и открыть их. На рис. 11.4 видно, что выбор пункта Grep меню выводит диалог ввода, список файлов, удовлетворяющих критериям поиска и новое окно PyEdit, открытое и позиционированное после двойного щелчка на файле в списке.
Рис. 11.4. Множество окон PyEdit в рамках единственного процесса
В процессе выполнения операций, предусмотренных пунктом Grep меню, на экране появляется еще одно окно, но при этом графический интерфейс остается полностью активным. Фактически вы можете снова выбрать пункт Grep и выполнить еще один поиск, пока другой еще не закончился. Обратите внимание, что диалог Grep также позволяет указывать кодировку символов, которая будет использоваться для декодирования содержимого всех текстовых файлов, просматриваемых в процессе поиска. Подробнее о том, как это действует, я расскажу в следующем ниже разделе с описанием изменений, однако в большинстве случаев можно просто использовать системную кодировку по умолчанию.
Ради интереса попробуйте вызвать диалог Grep и выполнить в каталоге C:\Python31 поиск всех файлов *.py, содержащих строку %, чтобы получить представление о том, как часто используется это распространенное выражение форматирования строк в стандартной библиотеке Python 3.1. Разумеется, не все вхождения % будут иметь отношение к форматированию строк, но большинство из них - точно. Согласно сообщениям, выводимым в стандартный поток вывода перед завершением поиска, строка %’ (которая также соответствует целям для подстановки) встречается 6050 раз, а строка ‘ % ‘ (с пробелами вокруг знака процента, чтобы ограничить круг совпадений только оператором форматирования) встречается 3741 раз, включая 130 совпадений, обнаруженных в установленном расширении PIL, - не самый редко используемый инструмент языка! Ниже приводятся сообщения, которые были выведены в стандартный поток вывода в процессе поиска, - совпадения выводятся также в окно списка:
...ошибки могут отличаться от типа кодировки...
Unicode error in: C:\Python31\Lib\lib2to3\tests\data\different_encoding.py
Unicode error in: C:\Python31\Lib\test\test_doctest2.py
Unicode error in: C:\Python31\Lib\test\test_tokenize.py
Matches for % : 3741
Редактор PyEdit выводит дополнительные всплывающие окна - включая кратковременные диалоги Goto и Find, диалоги выбора цвета, диалоги ввода аргументов и выбора режимов для пункта Run Code меню, и диалоги, запрашивающие имена кодировок символов в файлах при выборе в меню пунктов Open и Save, если редактор настроен так, что он должен запрашивать эти данные (подробнее об этом рассказывается ниже). В интересах экономии места я оставляю исследование большинства других подобных черт поведения за вами.
Существенно обновленный в этом издании и поддерживающий настройки в соответствии с пользовательскими предпочтениями, редактор PyEdit может запрашивать имя кодировки символов при открытии файла, сохранять совершенно новый файл или выполнять операцию Save As. Например, на рис. 11.5 изображен момент, когда я открыл файл, содержащий символы китайского алфавита, и затем снова выбрал в меню пункт Open, чтобы открыть новый файл с текстом на русском языке. Диалог выбора имени кодировки, изображенный на рисунке, появляется сразу же после закрытия стандартного диалога выбора файла, а поле ввода в нем предварительно заполнено именем кодировки по умолчанию (которое определяется явно или берется из настроек платформы). В большинстве случаев можно использовать имя, предлагаемое по умолчанию, если только заранее не известно точно, что символы в файле представлены в другой кодировке.
Вообще говоря, редактор PyEdit поддерживает любые кодировки, которые поддерживаются языком Python и библиотекой tkinter. Текст, который можно видеть на рис. 11.5, например, содержал символы китайского алфавита в специфической кодировке (в кодировке «gb2321», в файле email-part--gb2312). В том же каталоге присутствует тот же текст в альтернативной кодировке UTF-8 (файл email-part--gb2312--utf8), который можно открывать в PyEdit и Notepad, используя кодировку по умолчанию, используемую в Windows. Но, чтобы открыть файл в специфической китайской кодировке и получить корректное отображение символов в PyEdit, требуется явно указать имя кодировки (содержимое этого файла абсолютно неправильно отображается в Notepad).
Рис. 11.5. PyEdit отображает китайский текст и запрашивает имя кодировки при открытии файла
После того как я ввел имя кодировки для выбранного файла в диалоге на рис. 11.5 («koi8-r» - для файла, выбранного в диалоге открытия), редактор PyEdit декодировал и отобразил его содержимое. На рис. 11.6 изображен момент, после того как файл был открыт и я выбрал в меню пункт Save As, - сразу после того как был закрыт диалог выбора файла, редактор вывел новый диалог ввода имени кодировки для нового файла, поле ввода в котором было предварительно заполнено именем кодировки, известным по последним операциям Open или Save. В соответствии с настройками операция Save повторно использует известную кодировку, но операция Save As всегда запрашивает кодировку, чтобы дать возможность указать ее явно для нового файла, прежде чем пытаться использовать умолчания. Подробнее об алгоритмах применения кодировок и интернационализации в PyEdit я буду рассказывать в следующем разделе, при обсуждении изменений в версии 2.1, а пока отмечу, что из-за того, что предпочтения пользователя не могут быть предугаданы, выбор среди алгоритмов поддерживается настройками.
Наконец, когда приходит время завершать работу, редактор PyEdit делает все возможное, чтобы не потерять несохраненные изменения. Когда в любом окне редактирования запрашивается выполнение операции завершения, PyEdit проверяет наличие несохраненных изменений и запрашивает подтверждение. Поскольку в одном и том же процессе может быть открыто несколько окон редактирования, когда операция завершения запрашивается в главном окне, PyEdit проверяет наличие изменений во всех остальных открытых окнах и запрашивает подтверждение, если хотя бы в одном из них будут обнаружены несохраненные изменения. В противном случае операция завершения будет выполнена без дополнительных вопросов. Попытка выполнить операцию завершения во всплывающем окне редактирования закроет только это окно, то есть никаких проверок между процессами выполняться не будет. При отсутствии изменений операция завершения просто закроет окна графического интерфейса и завершит программу. Другие операции проверяют наличие изменений похожим способом.
Рис. 11.6. PyEdit отображает текст на русском языке и запрашивает кодировку для операции Save As
Другие примеры и рисунки с изображением PyEdit в этой книге
Дополнительные рисунки с изображением PyEdit можно найти в описаниях следующих программ-клиентов:
• PyDemos в главе 10 использует PyEdit во всплывающем режиме для отображения файлов с исходными текстами.
• PyView далее в этой главе использует PyEdit во встроенном режиме для отображения примечаний к файлам изображений.
• PyMailGUI в главе 14 использует PyEdit для отображения сообщений электронной почты, текстовых вложений и исходных текстов.
Последнее приложение особенно интенсивно использует функциональные возможности PyEdit, а в главе с его описанием имеются рисунки, демонстрирующие возможности PyEdit по отображению текста Юникода с национальными наборами символов. При таком использовании текст либо извлекается из сообщений, либо загружается из временных файлов, кодировка которых определяется из заголовков сообщений электронной почты.
Изменения в версии PyEdit 2.0 (третье издание)
Я изменял этот пример в обоих, в третьем и четвертом, изданиях этой книги. Поскольку эта глава призвана отражать практические приемы программирования, а также потому, что этот пример демонстрирует процесс развития программного обеспечения с течением времени, этот и следующий разделы дают краткое описание основных изменений, выполненных за это время, чтобы помочь вам в изучении программного кода.
Поскольку текущая версия наследует все улучшения от предшествующих ей, начнем с дополнений, появившихся в предыдущей версии. В третьем издании редактор PyEdit был дополнен следующими возможностями:
• Простой диалог выбора шрифта
• Поддержка неограниченного количества отмен и возвратов операций редактирования
• Проверка наличия изменений в файле, когда его содержимое могло быть удалено или изменено
• Модуль для хранения настроек пользователя
Далее приводятся некоторые краткие примечания, касающиеся этих дополнений.
Диалог выбора шрифта
В третьем издании в редактор PyEdit был добавлен диалог выбора шрифта - простой немодальный диалог с тремя полями, куда можно ввести название семейства шрифта, размер и стиль, вместо того чтобы выбирать из предопределенного списка возможных вариантов. Хотя вы можете найти более сложные диалоги выбора шрифта на основе библиотеки tkinter, используемые в общедоступных приложениях и в реализации стандартной среды разработки IDLE на языке Python (как уже упоминалось ранее, среда IDLE сама по себе является программой, написанной на языке Python и использующей библиотеку tkinter).
Отмена, возврат и проверка наличия изменений
Еще одной новинкой в версии PyEdit для третьего издания стала поддержка неограниченного количества отмен и возвратов (undo/redo) операций редактирования, проверка наличия изменений перед завершением редактора, а также перед выполнением операций открытия, запуска и создания нового файла, чтобы при необходимости запросить сохранение этих изменений. Теперь запрос подтверждения на выход или перезапись файла выводится уже не каждый раз, а только если текст в окне редактора действительно изменился. Библиотека Tk версии 8.4 (или выше) предоставляет прикладной интерфейс, который упрощает реализацию обеих этих возможностей, - Tk сохраняет стеки отмены и возврата операций редактирования автоматически. Они включаются с помощью параметра undo настройки виджета Text и доступны с помощью методов edit_undo и edit_redo. Аналогично метод edit_reset очищает стеки (например, после открытия нового файла), а метод edit_modified проверяет или устанавливает признак наличия изменений в тексте.
Отмену вырезания и вставки текста из буфера обмена сразу после их выполнения реализовать совсем несложно (простой вставкой текста из буфера обмена или вырезанием вставленного и выделенного текста), но усовершенствованная поддержка операций отмены/возврата более полная и проще в использовании. Во втором издании книги реализация отмены была предложена в качестве самостоятельного упражнения, но она стала практически тривиальной благодаря новому прикладному интерфейсу библиотеки Tk.
Модуль с настройками
Для большего удобства версия PyEdit в третьем издании была дополнена возможностью определять начальные параметры настройки за счет присваивания значений переменным в модуле textConfig.py. Если поместить этот файл в путь поиска модулей, при импортировании или запуске редактор PyEdit будет импортировать начальные значения, определяющие шрифт, цвета, размеры текстового окна и необходимость учета регистра символов в операциях поиска. Настройки шрифта и цветов могут изменяться интерактивно с помощью меню, а окна позволяют свободно изменять их размер, поэтому данные настройки предусмотрены в значительной степени для удобства. Обратите также внимание, что этот модуль с настройками будет подключаться всеми экземплярами PyEdit, если он окажется доступным для импортирования клиентским программам, - даже при использовании редактора во всплывающих окнах или при встраивании в другие графические интерфейсы. Однако при необходимости клиентские приложения могут определять собственные версии этого файла с настройками или изменять существующий в пути поиска.
Изменения в версии PyEdit 2.1 (четвертое издание)
Помимо изменений, описанных в предыдущем разделе, при подготовке текущего четвертого издания в PyEdit были внесены следующие дополнительные улучшения:
• Редактор PyEdit был перенесен на новую версию Python 3.1 и его библиотеку tkinter.
• Были исправлены немодальные диалоги поиска с заменой и выбора шрифта - была обеспечена корректная их работа при наличии нескольких окон редактирования, за счет сохранения информации о каждом диалоге отдельно.
• При выполнении операции завершения, вызванной из главного окна, теперь проверяется наличие изменений в других окнах редактирования, открытых в пределах этого же процесса.
• Появился новый пункт Grep меню и диалог поиска во внешних файлах - поиск поддерживает текст Юникода и производится в отдельном потоке выполнения, чтобы избежать блокирования графического интерфейса и позволить одновременно выполнять несколько операций поиска.
• Было внесено небольшое исправление в начальное позиционирование, когда текст изначально вставляется во вновь созданное окно редактора, вызванное изменением в базовых библиотеках.
• Пункт Run Code меню для запуска файлов теперь использует не полное имя файла, содержащее полный путь к нему, а только базовую его часть, чтобы обеспечить поддержку относительных путей; позволяет определять аргументы командной строки для запускаемых файлов; и наследует исправление, выполненное в главе 5 в модуле launchmodes, которое преобразует / в \ в строках путей в файловой системе. Кроме того, этот пункт теперь всегда вызывает метод update между диалогами, чтобы гарантировать корректное отображение.
• Но самое заметное, пожалуй, изменение заключается в том, что теперь PyEdit позволяет отображать и редактировать содержимое файлов в любых кодировках, до той степени, до которой это позволяет библиотека Tk. В частности, имена кодировок учитываются при открытии и сохранении файлов, при отображении текста в графическом интерфейсе и когда выполняется поиск по файлам в каталогах.
В следующих разделах приводятся дополнительные примечания к перечисленным изменениям.
Исправление проблемы состояния модальных диалогов
Диалог поиска с заменой в предыдущей версии сохранял свои поля ввода в объекте текстового редактора, а это означает, что для всех открытых диалогов поиска с заменой использовались самые последние созданные экземпляры полей ввода. Это могло приводить к аварийному завершению программы при попытке выполнить поиск с помощью диалога, открытого ранее, если к этому моменту был закрыт диалог, открытый позднее, так как при закрытии виджеты уничтожаются, - неожиданное поведение диалога, которое существовало, по крайней мере, со второго издания и которое я склонен был отнести к категории ошибок в операторах, но оказалось, что все дело в сохранении состояния! Тот же эффект наблюдался в диалоге выбора шрифта - самый последний экземпляр диалога затирал данные экземпляров, открытых перед ним, однако его обработчик исключений предотвращал аварийное завершение программы (он выводил окно с сообщением об ошибке). Чтобы исправить ошибки в диалогах поиска с заменой и выбора шрифта, теперь поля ввода в каждом диалоге передаются их обработчикам в виде аргументов. Вместо этого можно было бы разрешить создавать только по одному экземпляру этих диалогов, но это решение менее функционально.
Проверка наличия изменений в других окнах того же процесса при завершении
Кроме того, редактор PyEdit имел обыкновение игнорировать наличие изменений в других окнах редактирования при закрытии главного окна. В соответствии с реализацией, щелчок на кнопке Quit во всплывающем окне приводит к закрытию только этого окна, но операция закрытия главного окна вызывает метод quit из библиотеки tkinter, который завершает всю программу. В предыдущей версии при закрытии любого окна выполнялась проверка наличия изменений только в этом окне, а остальные окна игнорировались - закрытие главного окна могло привести к потере изменений в других окнах, закрываемых автоматически при завершении программы.
Чтобы исправить этот недостаток, текущая версия сохраняет список всех открытых в процессе окон редактирования - при закрытии главного окна теперь выполняется проверка наличия изменений во всех окнах и при необходимости предлагается подтвердить завершение программы. Это решение не устраняет все возможные проблемы (оно не устраняет проблему потери изменений, когда завершение приложения производится с помощью виджетов, находящихся за пределами PyEdit), но это существенное улучшение. Более полное решение может заключаться в переопределении или перехвате вызова метода quit библиотеки tkinter. Однако, чтобы не углубляться в детали, я отложу эту тему до более позднего обсуждения в этом же разделе (смотрите обсуждение реализации обработки события
Новый диалог Grep: поиск в дереве файлов с поддержкой Юникода и многопоточной модели выполнения
Кроме всего прочего, в меню Search появился новый пункт Grep, реализующий поиск по внешним файлам. Этот инструмент выполняет сканирование целого дерева каталогов в поисках файлов с именами, соответствующими шаблону и содержащими указанную строку. Результаты поиска отображаются в новом немодальном окне с прокручиваемым списком, где выводятся имена файлов, номера и содержимое строк с найденными совпадениями. Щелчок на элементе списка открывает соответствующий файл в новом немодальном окне редактирования PyEdit, при этом автоматически выполняется переход к строке с совпадением и ее выделение. В этой реализации повторно используется программный код, написанный нами ранее:
• Утилита find, написанная нами в главе 6, выполняющая обход дерева каталогов
• Реализация списка с прокруткой из главы 9 для отображения результатов поиска
• Конструктор рядов с полями для форм ввода, созданный в главе 10, для создания немодального диалога ввода
• Существующая реализация всплывающего режима использования PyEdit для отображения содержимого файлов по запросу
• Существующий обработчик события перехода в PyEdit для перемещения к строке с найденным совпадением
Поддержка многопоточной модели выполнения в операции поиска.
Чтобы избежать блокирования графического интерфейса в процессе поиска, поиск производится в параллельных потоках выполнения. Это позволяет запускать сразу несколько операций поиска и выполнять их параллельно (что особенно полезно, когда поиск выполняется в больших деревьях каталогов, таких как стандартная библиотека Python или полное дерево его исходных текстов). При этом применяются такие приемы и механизмы, как стандартные потоки выполнения, очереди и цикл обработки событий от таймера after, с которыми мы познакомились в главе 10, - потоки-производители, не имеющие отношения к графическому интерфейсу, отыскивают совпадения и помещают их в очередь, которая проверяется главным потоком, управляющим графическим интерфейсом, в цикле обработки событий от таймера.
В данной реализации цикл обработки событий от таймера запускается только при выполнении операции поиска, и для каждого поиска используются отдельный поток, отдельный цикл обработки событий от таймера и отдельная очередь. В одном процессе одновременно может выполняться множество потоков и циклов обработки событий от таймера, связанных с поиском, а также могут существовать другие независимые потоки, очереди и циклы обработки событий от таймера. Например, прикрепляемый компонент PyEdit в программе PyMailGUI, представленной в главе 14, может выполнять собственные операции поиска, в то время как программа PyMailGUI выполняет собственные потоки и очереди, используемые для отправки или приема электронной почты. Каждый цикл обработки событий от таймера управляется независимо от процессора событий tkinter. Из-за упрощенной архитектуры в этом примере не используется универсальная реализация threadtools очереди обработчиков из главы 10. Дополнительные примечания о реализации потоков выполнения в операции поиска ищите в исходных текстах, что приводятся далее, и сравните их с файлом _unthreadedtextEditor.py в дереве примеров, где содержится версия PyEdit без поддержки многопоточной модели выполнения.
Поддержка Юникода. Если внимательно изучить реализацию поиска по внешним файлам, можно заметить, что она позволяет определять кодировку для всего дерева и обрабатывает любые исключения, связанные с ошибками декодирования символов, возникающими как при обработке содержимого файлов, так и при обработке имен файлов во время обхода дерева. Как мы узнали в главах 4 и 6, при открытии текстовых файлов в Python 3.X должны декодироваться с применением кодировки, указанной явно или используемой на текущей платформе по умолчанию. Это представляет определенную проблему для операции поиска по файлам, так как деревья каталогов могут содержать файлы с символами в различных кодировках.
Фактически в Windows в одном дереве каталогов часто можно встретить файлы с содержимым в кодировках ASCII, UTF-8 и UTF-16 (варианты ANSI, Utf-8 и Unicode выбора кодировки в Notepad) и даже в других кодировках, особенно в каталогах, где сохраняются файлы, загруженные из Интернета или полученные по электронной почте. Операция открытия таких файлов с применением кодировки UTF-8 в Python 3.X будет приводить к исключениям, а при открытии их в двоичном режиме программа будет получать закодированный текст, в котором едва ли возможно будет отыскать совпадение с искомой строкой. Технически, чтобы выполнить сравнение, необходимо выполнить декодирование байтов, прочитанных из файла, или закодировать искомую строку в байты. При этом совпадение может быть обнаружено только при использовании согласованных кодировок.
Чтобы обеспечить возможность поиска в деревьях каталогов со смешанными кодировками, диалог Grep открывает файлы в текстовом режиме и позволяет вводить имя кодировки, которая будет использоваться для декодирования содержимого всех файлов в просматриваемом дереве каталогов. Для удобства поле ввода с именем кодировки предварительно заполняется значением по умолчанию для текущей платформы, так как этого часто бывает вполне достаточно. Чтобы выполнить поиск в дереве каталогов с файлами разных типов, пользователи могут выполнить несколько операций поиска, указывая различные имена кодировок. При поиске могут также возникать ошибки декодирования имен файлов, но они практически никак не обрабатываются в текущей версии: предполагается, что имена файлов удовлетворяют соглашениям, принятым в файловой системе на данной платформе, в противном случае это приводит к завершению поиска (дополнительные сведения об утилите find обхода дерева каталогов, повторно используемой здесь, а также о проблемах, связанных с кодированием имен файлов в Python, вы найдете в главах 4 и 6).
Кроме того, реализация операции Grep должна предусматривать обработку исключений, связанных с ошибками декодирования файлов, имена которых соответствуют шаблону, но содержимое не может быть декодировано с применением указанной кодировки, и фактически вообще может не быть текстом. Например, операция поиска в стандартной библиотеке Python 3.1 (как в примере поиска строки %, описанном выше) сталкивается с несколькими файлами, которые не смогли быть корректно декодированы в Windows на моем компьютере и могли бы вызвать крах PyEdit. Двоичные файлы, имена которых по случайности соответствуют шаблону, являются еще более худшим вариантом.
В целом программы могут избежать ошибок, вызванных применением неправильных кодировок, либо обрабатывая исключения, либо открывая файлы в двоичном режиме. Так как операция поиска может оказаться не в состоянии интерпретировать содержимое некоторых файлов как текст вообще, при ее реализации был выбран первый подход. В действительности, открытие даже текстовых файлов в двоичном режиме и чтение из них строк двоичных байтов в версии 3.X имитирует поведение текстовых файлов в версии 2.X и позволяет понять, почему принудительный переход на использование Юникода иногда является благом, - двоичный режим позволяет избежать появления исключений, связанных с декодированием, но сам текст по-прежнему остается закодированным и не может использоваться в привычных операциях. В этом случае операция сравнения может давать неверные результаты.
Дополнительные детали, касающиеся поддержки Юникода в реализации диалога Grep, а также описание проблем, связанных с этой поддержкой, и способов их решения, вы найдете в исходном программном коде, который приводится ниже. Дополнительные предложения по улучшению можно найти в главе 19, в описании модуля re - инструмента, который можно использовать для организации поиска по шаблону, а не только по определенной строке.
Исправление проблемы начального позиционирования
В этой версии текстовый редактор также обновляет свой графический интерфейс перед вставкой текста в текстовый виджет на этапе конструирования, когда ему передается имя файла в аргументе loadFirst. Спустя некоторое время, после выхода третьего издания и версии Python 2.5, в Tk или tkinter были внесены какие-то изменения, в результате такая операция вставки текста перед вызовом метода update стала приводить к прокручиванию виджета на одну строку - текст вставлялся, начиная со второй строки, а не с первой. Эта же проблема наблюдалась в версии для третьего издания, при использовании Python 2.6, но не 2.5. Добавление вызова метода update обеспечило корректное позиционирование текстового виджета. Это неприятно, но такое вполне может происходить в мире, зависящем от внешних библиотек!50
Клиенты, использующие классы редактора, также должны вызывать метод update перед вставкой текста вручную во вновь созданный (или скомпонованный) объект текстового редактора, чтобы обеспечить более точное позиционирование, - программа PyView, рассматриваемая далее в этой главе, и PyMailGUI в главе 14 учитывают эту особенность. Редактор PyEdit не может обновлять себя при каждом создании, потому что он может создаваться или даже скрываться вмещающими его графическими интерфейсами (например, это могло бы привести к отображению неполного окна в PyView). Кроме того, PyEdit мог бы автоматически обновлять себя в начале метода setAllText, чтобы исключить необходимость выполнять этот шаг клиентами, но принудительный вызов update требуется выполнить только один раз после компоновки (а не перед каждой вставкой текста), а кроме того, в некоторых случаях это могло бы приводить к нежелательным эффектам. Как правило, добавление лишних операций в методы, как в данном случае, обычно ограничивает область применения компонентов.
Улучшения в операции запуска программного кода
В реализацию операции Run Code из меню TooLs было внесено три исправления, которые сделали еще более удобным запуск редактируемого программного кода из внешнего файла:
1. После перехода в каталог, где хранится файл, для обеспечения правильности всех относительных путей к файлам в его программном коде редактор PyEdit теперь отбрасывает путь из имени файла, прежде чем запустить его, потому что оригинальный путь к файлу может оказаться ошибочным, если он был относительным, а не абсолютным. Например, пути к файлам, открываемым вручную, являются абсолютными, но пути к файлам в программном коде PyDemos, вызывающем редактор, являются относительными - они откладываются относительно корневого каталога с примерами и после выполнения команды chdir могут оказаться недействительными.
2. Теперь в режиме запуска файла с программным кодом PyEdit использует инструменты запуска, поддерживающие возможность передачи аргументов командной строки в Windows.
3. Редактор PyEdit унаследовал исправление, выполненное в модуле launchmodes, которое преобразует символы прямого слеша в обратные в строках путей в файловой системе (хотя позднее, из-за удаления префиксов относительных путей, полезность этого исправления стала вызывать сомнения). Необходимость преобразования прямых символов слеша в PyEdit обусловлена тем, что несмотря на допустимость их использования при вызове функции open в Windows, они не могут использоваться с некоторыми инструментами запуска в этой операционной системе.
Кроме того, в реализацию запуска программного кода как из внешних файлов, так и из строк в памяти, в этой версии был добавлен вызов метода update между вызовами диалогов, чтобы гарантировать, что последний диалог будет появляться на экране в любом случае (ранее в некоторых редких случаях второй диалог не отображался на экране). Даже с этими исправлениями операция Run Code по-прежнему не отличается надежностью. Например, при запуске программного кода из строки, а не из внешнего файла он выполняется внутри процесса, а не в отдельном потоке выполнения, и поэтому может заблокировать графический интерфейс. Кроме того, не совсем понятно, как лучше всего обрабатывать пути импортирования и каталоги для файлов при выполнении программного кода в виде строк, и стоит ли вообще сохранять этот режим. Измените эту особенность в соответствии со своими пожеланиями.
Поддержка текста Юникода (интернационализированного)
Наконец, из-за того, что теперь Python 3.X полностью поддерживает текст Юникода, редактор PyEdit также обеспечивает эту поддержку -он позволяет открывать, сохранять, просматривать, редактировать и отыскивать в дереве каталогов любой текст, в любой кодировке и с любыми наборами символов. Эта поддержка находит множество отражений в пользовательском интерфейсе PyMailGUI:
• При открытии файла у пользователя запрашивается имя кодировки (при этом предлагается системная кодировка по умолчанию), если она не была указана в настройках или при вызове редактора клиентским приложением*
• При сохранении нового файла запрашивается имя кодировки, если она не указана в настройках*
• При отображении и редактировании используется поддержка Юникода, реализованная в инструментах создания графических интерфейсов*
• Операция поиска в дереве каталогов позволяет явно указывать кодировку, которая будет применяться ко всем файлам в дереве, и пропускает файлы, декодировать которые не удалось, как было описано выше*
Благодаря этому обеспечивается поддержка интернационализированного текста, кодировка которого может отличаться от кодировки по умолчанию, используемой на текущей платформе. Это, в частности, удобно для просмотра текстовых файлов, полученных из Интернета, по электронной почте или через FTP. Приложение PyMailGUI из главы 14, например, использует встроенный объект PyEdit для отображения текста вложений различного происхождения и в различных кодировках. Поддержка Юникода в операции поиска по файлам была описана выше - остальные аспекты этой модели, по сути, сводятся к операциям открытия и сохранения файлов, как описывается в следующем разделе.
Файлы с текстом Юникода и модель отображения их содержимого.
Поскольку строки в Python 3.X всегда интерпретируются как последовательности кодовых пунктов Юникода, поддержка Юникода в действительности означает поддержку различных кодировок при чтении и записи в текстовые файлы. Напомню, что текст может сохраняться в файлах в различных кодировках, - данные декодируются при чтении и кодируются при записи с применением этих кодировок. Если текст не всегда сохраняется в файлах с использованием кодировки по умолчанию для данной платформы, то для таких случаев нам необходимо знать, какую кодировку использовать при чтении и записи.
Чтобы обеспечить такую поддержку, редактор PyEdit использует подходы, подробно описанные в главе 9. Мы не будем повторно обсуждать их здесь, тем не менее в двух словах отмечу, что виджет Text принимает содержимое в виде строки типа str или bytes и всегда возвращает его как строку str. Редактор PyEdit отображает этот интерфейс на интерфейс объектов файлов языка Python следующим образом:
Входные файлы (открытие)
Чтобы декодировать байты из файла в строки, в общем случае требуется знать название кодировки, совместимой с данными в файле. Если кодировка окажется несовместимой, операция декодирования потерпит неудачу (например, при попытке декодировать 8-битовые данные с использованием кодировки ASCII). В некоторых случаях кодировка открываемого текстового файла может оказаться неизвестной.
Чтобы при загрузке содержимого входных файлов прочитать данные в виде строк str, редактор PyEdit сначала пытается открывать их в текстовом режиме, применяя кодировки, полученные из разных источников: из аргумента метода, когда кодировка известна заранее (например, из заголовков вложений в сообщениях электронной почты или из исходных файлов, открываемых демонстрационными примерами), из диалога, запрашивающего кодировку у пользователя, из модуля с настройками и из параметров по умолчанию текущей платформы. При выводе диалога, запрашивающего кодировку при открытии файла, поле ввода предварительно заполняется вариантом из файла с настройками, который считается значением по умолчанию.
Если с помощью всех этих кодировок не удается декодировать файл, он открывается в двоичном режиме и текст из него читается как строка bytes, без декодирования, что фактически перемещает задачу декодирования в библиотеку Tk. В этом случае в Windows все последовательности \r\n вручную преобразуются в символы \n, чтобы обеспечить корректное отображение текста и последующее сохранение его в файл. Двоичный режим используется только в самом крайнем случае, чтобы лишний раз не полагаться на логику декодирования и ограниченную поддержку кодировок в библиотеке Tk.
Обработка текста
При обращении к виджету Text он возвращает свое содержимое в виде строки str, независимо от того, в каком виде, str или bytes, был вставлен текст. Вследствие этого вся обработка текстового содержимого производится с применением методов строк str Юникода.
Выходные файлы (сохранение)
Операция кодирования строк в байты при записи в файлы обычно отличается большей гибкостью, чем операция декодирования. При этом не требуется использовать ту же самую кодировку, которая применялась для декодирования данных в строку, но и эта операция может потерпеть неудачу, если выбранная схема кодирования окажется слишком узкой для содержимого строки (например, попытка кодировать 8-битовый текст с применением кодировки ASCII).
Для сохранения текста в файл редактор PyEdit открывает выходной файл в текстовом режиме, чтобы обеспечить отображение символов конца строки и кодирование содержимого строки str Юникода. Имя кодировки извлекается из одного из источников - это может быть кодировка, использовавшаяся при открытии или первоначальном сохранении файла (если была указана), или кодировка, полученная из диалога с пользователем, из модуля с настройками или из параметров по умолчанию текущей платформы. В отличие от операции открытия, когда операция сохранения выводит диалог запроса имени кодировки, поле ввода заполняется именем известной кодировки, если она была определена прежде, в противном случае берется вариант из файла с настройками, как и в случае с операцией открытия.
Диалоги ввода кодировки, вызываемые операциями открытия и сохранения файлов, - это лишь одно из воплощений описанных правил в графическом интерфейсе; другие варианты определяются в модуле с настройками. Поскольку заранее невозможно предугадать все возможные случаи использования, в редакторе PyEdit применяется либеральный подход: он поддерживает все мыслимые режимы и обеспечивает пользователям возможность влиять на определение кодировок с помощью определения настроек в их собственном модуле textConfig. Он пытается применить одну кодировку за другой из разных источников, если это разрешено в модуле textConfig, пока не будет найдена кодировка, дающая положительные результаты. Тем самым достигается максимальная маневренность перед лицом переменчивого мира Юникода.
Например, согласно параметрам в файле с настройками операция сохранения повторно использует кодировку, которая применялась при открытии файла или при первой операции сохранения, если она известна. Для новых файлов (созданных выбором пункта New в меню или вставкой текста вручную) и для файлов, открытых в двоичном режиме, кодировка остается неизвестной до момента сохранения, но для файлов, которые удалось открыть в текстовом режиме, она известна. Кроме того, с помощью параметров в файле с настройками мы можем определить необходимость запрашивать кодировку у пользователя при выполнении операции Save As (и, возможно, Save), потому что у него могут быть свои предпочтения при создании новых файлов. Мы также можем запрашивать кодировку при открытии существующих файлов, потому что для этого необходимо знать его текущую кодировку. В некоторых случаях (например, для файлов, полученных из Интернета) пользователь может не знать ее, но в других случаях он может предпочесть указать кодировку явно. Вместо того чтобы выбирать тот или иной порядок действий в таких ситуациях, мы просто опираемся на пользовательские настройки.
На практике все это относится только к клиентам PyEdit, которые запрашивают начальную загрузку файлов или позволяют открывать и сохранять файлы с помощью графического интерфейса. Поскольку содержимое может вставляться как строка типа str или bytes, клиенты всегда имеют возможность читать входные файлы самостоятельно, до создания объекта текстового редактора, и вставлять в него текст вручную. Кроме того, клиенты могут получать содержимое вручную и сохранять его любым предпочитаемым способом. Такое выполнение операций вручную может оказаться полезным, если в каком-то контексте методика, реализованная в редакторе PyEdit, окажется нежелательной. Поскольку виджет Text всегда возвращает содержимое в виде строки str, остальной части этой программы безразлично, строка какого типа была в него вставлена.
Имейте в виду, что описанная методика по-прежнему зависит от поддержки Юникода и от ограничений, заложенных в библиотеке Tk, а также от интерфейса tkinter к ней. Редактор PyEdit позволяет загружать и сохранять текст в любой кодировке, но он не может гарантировать, что библиотека графического интерфейса сможет отобразить такой текст. То есть даже если мы совершенно корректно обрабатываем текст Юникода на стороне Python, мы все равно остаемся во власти других слоев программного обеспечения, обсуждение которых выходит далеко за рамки этой книги. Библиотека Tk достаточно надежно работает с самыми разными наборами символов, если ей передавать уже декодированные строки str Юникода (смотрите, например, описание поддержки интернационализации в PyMailGUI в главе 14), но ситуации в конкретных случаях могут сильно различаться.
Порядок выбора кодировки и возможные варианты. Имейте также в виду, что политика редактора PyEdit в отношении Юникода отражает предпочтения единственного текущего пользователя и не проверялась на универсальность и эргономику - будучи книжным примером, редактор не использует встроенную среду тестирования, как это свойственно проектам с открытыми исходными текстами. Неплохие результаты можно было бы получать, используя другие схемы и порядки следования источников, и совершенно невозможно предугадать предпочтения каждого пользователя в каждом конкретном случае. Например:
• Непонятно, следует ли сначала запрашивать кодировку у пользователя, а потом пытаться использовать кодировку, указанную в файле с настройками, или наоборот.
• Возможно также, что мы всегда должны спрашивать кодировку у пользователя, полагаясь в основном на это, независимо от параметров настройки.
• При сохранении мы могли бы также попробовать самостоятельно определить кодировку для применения к строке str (например, попробовать применить UTF-8, Latin-1 или другую распространенную кодировку), но наши предположения могут не совпадать с тем, что имел в виду пользователь.
• Весьма вероятно, что пользователь пожелает сохранить файл в той же кодировке, которая применялась при открытии файла или при сохранении в первый раз. Редактор PyEdit предоставляет поддержку этого варианта, в противном случае графический интерфейс запрашивал бы кодировку для данного файла более чем один раз. Однако, поскольку некоторым пользователям может потребоваться повторно использовать операцию Save, чтобы сохранить тот же файл в другой кодировке, то предусмотрена возможность отключения этого варианта в модуле с настройками. На первый взгляд, для этой цели было бы лучше использовать операцию Save As, однако следующий пункт объясняет, почему это не всегда так.
• Точно так же неочевидно, должна ли операция Save As повторно использовать кодировку, которая применялась при открытии или при сохранении файла в первый раз, или она должна запрашивать новую кодировку - действительно ли при этом сохраняется совершенно новый файл или только копия предыдущего содержимого с установленной кодировкой, но под новым именем? Из-за такой неоднозначности мы даем возможность запретить использование установленной кодировки в операции Save As или в обеих операциях, Save и Save As, в модуле с настройками. По умолчанию использование известной кодировки разрешено только для операции Save и запрещено для Save As. В любом случае, операции сохранения выводят диалоги, запрашивающие имя кодировки, в которых поле ввода заполнено известной кодировкой.
• Порядок выбора вариантов вообще выглядит весьма спорным. Например, возможно, операция Save As должна использовать известную кодировку, если настройки запрещают запрашивать ее у пользователя, - в данной реализации, если настройки запрещают использовать известную кодировку и запрашивать ее у пользователя, эта операция будет определять кодировку из файла с настройками или использовать системную кодировку по умолчанию (например, UTF-8), что может оказаться не самым лучшим решением при сохранении составных частей сообщений электронной почты, кодировка которых уже известна.
И так далее. Поскольку такой пользовательский интерфейс обеспечивает широчайший выбор вариантов, в целях иллюстрации в этой книге реализованы общий и частично эвристический алгоритмы поддержки каждого из возможных вариантов, и в качестве опоры при выборе используются настройки пользователя. Однако на практике такая гибкость может оказаться совершенно излишней - большинству пользователей наверняка будет достаточно поддержки какого-то одного алгоритма из числа поддерживаемых здесь.
Кроме того, вероятно, было бы удобнее, если бы алгоритмом выбора кодировки можно было управлять непосредственно в графическом интерфейсе, вместо того чтобы вручную определять его в модуле с настройками. Например, возможно, каждая операция - Open, Save и Save As - должна позволять выбирать кодировку и по умолчанию использовать последнюю известную кодировку, если таковая определена. Реализация этой возможности в виде раскрывающихся списков с именами кодировок или полей ввода в диалогах Save и Open позволила бы отказаться от лишних диалогов и достичь практически той же гибкости.
В текущей реализации редактора PyEdit имеется возможность определить в файле с настройками необходимость запрашивать кодировку у пользователя для обеих операций, открытия и сохранения, что дает практически тот же эффект, по крайней мере в тех ситуациях, с которыми я сталкивался до настоящего момента, и, возможно, является лучшим решением для большинства контекстов.
Таким образом, по умолчанию:
• Операция Open использует переданную ей кодировку, если она была указана, или запрашивает имя кодировки у пользователя
• Операция Save повторно использует известную кодировку, если она была определена ранее, а при сохранении новых файлов запрашивает ее у пользователя
• Операция Save As всегда запрашивает имя кодировки у пользователя, как при сохранении нового файла
• Операция Grep позволяет вводить кодировку в диалоге определения параметров поиска и применяет ее при поиске во всех файлах, имеющихся в дереве каталогов
С другой стороны, поскольку умолчаний, определяемых платформой, будет вполне достаточно для работы без лишних сложностей взаимодействия с графическим интерфейсом, по крайней мере, для подавляющего числа пользователей, с помощью параметров в модуле textConfig можно предотвратить вывод диалога запроса кодировки и вернуться к использованию кодировки, указанной явно или определяемой платформой по умолчанию. В конечном счете, определение наиболее удачного алгоритма выбора кодировки требует анализа предпочтений широкого круга пользователей, а не предположений единственного разработчика. Как всегда, вы свободно можете адаптировать этот алгоритм под свои потребности.
В подкаталоге test в дереве примеров вы найдете несколько текстовых файлов в различных кодировках, с которыми вы можете экспериментировать при изменении алгоритмов выбора кодировки в модуле textConfig для операций открытия и сохранения файлов. Этот каталог содержит файлы, изображенные на рис. 11.5 и 11.6, в которых используются национальные наборы символов и сохраненные в различных кодировках. Например, файл email-part--koi8-r содержит текст на русском языке, сохраненный в кодировке koi8-r, а файл email-part--koi8-r--utf8 содержит тот же текст, сохраненный в кодировке UTF-8, - последний можно открыть в программе Блокнот (Notepad) в Windows, но первый будет корректно отображен только при передаче PyEdit явно указанной кодировки.
Еще лучше, сохраните сами один и тот же файл в нескольких кодировках, жестко определяя кодировку в модуле textConfig или указывая разные кодировки при сохранении, - благодаря широкомасштабной поддержке Юникода в Python 3.X, редактор PyEdit позволяет сохранять и загружать файлы практически в любой кодировке.
Еще о проверке наличия изменений при завершении: событие
Необходимо сказать несколько слов о еще одном изменении в версии 2.1, прежде чем перейти к программному коду, поскольку он иллюстрирует основы закрытия окон tkinter в действующей программе. В главе 8 мы узнали, что библиотека tkinter позволяет выполнять с помощью метода bind привязку обработчика к событию
Кроме того, как уже упоминалось в главе 8, вызов метода quit не возбуждает события
Иными словами, событие
Чтобы поэкспериментировать с событием
def onDeleteRequest():
print(‘Got wm delete’) # щелчок на кнопке X в окне: можно отменить
root.destroy() # возбудит событие
def doRootDestroy(event):
print(‘Got event
print(text.edit_modified()) # <= ошибка Tcl: неверный виджет
ans = askyesno(‘Save stuff?’, ‘Save?’) # <= некорректное поведение
if ans: print(text.get(‘1.0’, END+’-1c’)) # <= ошибка Tcl: неверный
# виджет
root = Tk()
text = Text(root, undo=1, autoseparators=1) text.pack()
root.bind(‘
root.protocol(‘WM_DELETE_WINDOW’, onDeleteRequest) # на кнопке X окна
Button(root, text=’Destroy’, command=root.destroy).pack() # возбудит
# возбуждает
Дополнительные подробности, касающиеся всего, о чем говорилось выше, ищите в листингах, которые приводятся в следующем разделе. Кроме того, обязательно прочитайте строку документирования в главном файле, где приводится список предлагаемых расширений и решений проблемы открытия файлов (под заголовком «TBD»). На реализацию редактора PyEdit значительное влияние оказали влияние мои личные предпочтения, но вы можете настроить его под себя.
Исходный программный код PyEdit
Программа PyEdit состоит из одного маленького модуля с настройками и одного главного файла с реализацией, содержащего чуть больше 1000 строк программного кода, с расширением .py, который можно запускать или импортировать. Для использования в Windows предоставляется еще один однострочный файл с расширением .pyw, который просто запускает файл .py вызовом exec(open(‘textEditor.py’).read()). Расширение .pyw предотвращает появление консоли DOS на экране при запуске в Windows.
В настоящее время файлы с расширением .pyw могут импортироваться и выполняться как обычные файлы с расширением .py (их можно запускать двойным щелчком мыши или с помощью таких инструментов языка Python, как os.system и os.startfile), поэтому в действительности нет необходимости создавать отдельный файл, чтобы обеспечить возможность импортирования и запуска без вывода окна консоли. Однако я оставил расширение .py, чтобы в процессе разработки видеть сообщения, которые выводятся в окно консоли, и использовать PyEdit, как простую интегрированную среду разработки, - когда операция запуска программного кода настроена на выполнение отдельных инструкций (а не файлов), вывод, который производится программным кодом, отображается в окне консоли DOS редактора PyEdit. Предполагается, что клиенты будут в обычном случае импортировать файл .py.
Файл с настройками пользователя
Итак, перейдем к программному коду. В первую очередь рассмотрим модуль с настройками пользователя, который приводится в примере 11.1. Он предназначен главным образом для того, чтобы было удобнее определять параметры внешнего вида, отличные от значений по умолчанию. Редактор PyEdit реализован так, что может работать и без этого модуля, и если он содержит синтаксические ошибки. Этот файл предназначен, прежде всего, для использования редактором PyEdit, когда он запускается как самостоятельный сценарий (в этом случае файл с настройками импортируется из текущего каталога), но вы также можете определить собственную версию файла с настройками PyEdit в любом другом каталоге, включенном в путь поиска модулей.
Дополнительно о том, какие настройки загружаются редактором, смотрите исходный программный код textEditor.py далее. Содержимое этого файла импортируется двумя различными способами - одна инструкция импортирования, которая загружает настройки внешнего вида, предполагает, что этот модуль (а не содержащий его пакет) находится в пути поиска модулей, и пропускает его, если его не находит, а другая, загружающая настройки порядка выбора кодировок, всегда отыскивает этот файл, независимо от способа запуска. Ниже описывается, что означает такое деление настроек для клиентов:
• Поскольку первая операция импортирования, загружающая настройки внешнего вида, ищет файл в пути поиска модулей, а не в каталоге основного пакета, то для каждого клиентского приложения, в его домашнем каталоге, можно определить собственный файл textConfig.py и тем самым обеспечить индивидуальные настройки PyEdit для каждого клиента.
• Настройки порядка выбора кодировки, напротив, всегда загружаются из файла, находящегося в каталоге пакета, с использованием операции импортирования по относительному пути, потому что они имеют более важное значение и маловероятно, что они будут отличаться от одного приложения к другому. Используемая здесь операция импортирования по относительному пути в пакете является эквивалентом импортирования всего пакета от корня PP4E, но не зависит от структуры каталогов.
Подобно эвристическим алгоритмам выбора кодировки символов, описанным выше, эта модель импортирования может считаться ориентировочной и может быть пересмотрена в соответствии с требованиями практической реализации.
Пример 11.1. PP4E\Gui\TextEditor\textConfig.py
модуль с начальными настройками PyEdit (textEditor.py);
#-----------------------------------------------------------------------------
# Общие настройки
# закомментируйте любые настройки в этом разделе, чтобы принять настройки по
# умолчанию библиотеки Tk или программы; шрифт/цвет можно также менять из меню
# в графическом интерфейсе, а также менять размеры окон после их открытия;
# импортируются из пути поиска модулей: могут определять отдельные настройки
# для каждого клиентского приложения, игнорируется, если находится не в пути
# поиска модулей;
# -----------------------------------------------------------------------------
# начальные настройки шрифта # семейство, размер, стиль
font = (‘courier’, 9, ‘normal’) # например, стиль: ‘bold italic’
# начальные настройки цвета # по умолчанию = white, black
bg = ‘lightcyan’ # название цвета или шестнадцатеричный код RGB
fg = ‘black’ # например, ‘powder blue’, ‘#690f96’
# начальные настройки размеров
height = 20 # умолчания Tk: 24 строки
width = 80 # умолчания Tk: 80 символов
# нечувствительность к регистру при поиске
caseinsens = True # по умолчанию = 1/True (включена)
# -----------------------------------------------------------------------------
# 2.1: Порядок выбора кодировки для содержимого и имен файлов в операциях
# открытия и сохранения;
# опробует каждый случай из перечисленных ниже в указанном порядке, пока не
# будет обнаружен первый, дающий положительный результат; запишите во все
# переменные false/пустое значение/0, чтобы перейти к использованию умолчаний
# для вашей платформы (то есть ‘utf-8’ - в Windows, или ‘ascii’, ‘latin-1’
# или другая кодировка в иных системах, таких как Unix);
# savesUseKnownEncoding: 0=Нет, 1=Да, только для операции Save, 2=Да для
# операций Save и SaveAs;
# всегда импортируются из этого файла: sys.path - если главный модуль, иначе -
# относительно пакета;
# -----------------------------------------------------------------------------
# 1) Сначала выполняется попытка применить известную
# кодировку (например, из заголовка сообщения
# электронной почты)
opensAskUser = True # 2) Если True, далее выполняется запрос у пользователя
# (предварительно заполняется значением по умолчанию)
opensEncoding = '' # 3) Если непустое значение, далее будет выполнена попытка
# применить эту кодировку: ‘latin-1’, ‘cp500’
# 4) Далее выполняется попытка применить
# sys.getdefaultencoding() - системное значение
# по умолчанию
# 5) В крайнем случае текст передается в двоичном виде и
# используются алгоритмы Tk
savesUseKnownEncoding = 1 # 1) Если > 0, выполняется попытка применить
# кодировку, известную по последней операции Open
# или Save
savesAskUser = True # 2) Если True, далее выполняется запрос у
# пользователя (предварительно заполняется
# известным значением?)
savesEncoding = ‘’ # 3) Если непустое значение, далее будет выполнена
# попытка применить эту кодировку: ‘utf-8’ и так
# далее
# 4) В крайнем случае выполняется попытка применить
# sys.getdefaultencoding()
Файлы запуска для Windows (и других систем)
Далее, в примере 11.2 приводится файл запуска с расширением .pyw, используемый для подавления окна консоли DOS в Windows, которое выводится при запуске в некоторых режимах (например, двойным щелчком). Окно консоли по-прежнему можно получить при запуске файла с расширением .py (например, чтобы увидеть вывод, генерируемый программным кодом, запускаемым редактором в режиме выполнения единственной инструкции). Двойной щелчок на этом файле дает тот же эффект, что и запуск PyEdit с помощью панели запуска PyDemos или PyGadgets.
Пример 11.2. PP4E\Gui\TextEditor\textEditorNoConsole.pyw
запускает редактор и подавляет вывод окна консоли DOS в Windows; с тем же успехом можно было бы просто присвоить расширение .pyw основному файлу, что не помешало бы возможности импортировать его, но файл .py был оставлен, чтобы иметь возможность наблюдать вывод в консоли
exec(open(‘textEditor.py’).read()) # как будто содержимое файла вставляется
# сюда (или textEditor.main())
Пример 11.2 прекрасно справляется с возложенной на него задачей, но когда я обновлял эту книгу, мне надоело использовать каждый раз Notepad для просмотра текстовых файлов из командных строк, запускаемых из произвольных мест, поэтому я написал сценарий, представленный в примере 11.3, который запускает PyEdit более универсальным и автоматизированным способом. Этот сценарий подавляет вывод консоли DOS, подобно примеру 11.2, когда запускается щелчком мыши на ярлыке в Windows, но дополнительно выполняет настройку пути поиска модулей на компьютерах, где я не использовал Панель Управления (Control PaneL) для этого, и позволяет запускать редактор, даже когда он находится за пределами текущего рабочего каталога.
Пример 11.3. PP4E\Gui\TextEditor\pyedit.pyw
#!/usr/bin/python
удобный сценарий для запуска pyedit из произвольного каталога, выполняет необходимую корректировку пути поиска модулей; sys.path при импортировании и функции open() требуется передавать путь относительно известного пути к каталогу со сценарием, а не относительно текущего рабочего каталога, потому что текущим является каталог сценария, только если сценарий запускается щелчком на ярлыке, а при вводе команды в командной строке он может находиться в любом другом каталоге: использует путь из argv; этому файлу дано расширение .pyw, чтобы подавить вывод окна консоли в Windows; добавьте каталог с этим сценарием в системную переменную PATH, чтобы иметь возможность запускать его из командной строки; также может использоваться в Unix: символы / и \ обрабатываются переносимым образом;
import sys, os
mydir = os.path.dirname(sys.argv[0]) # использовать каталог сценария для
# open, sys.path
sys.path.insert(1, os.sep.join([mydir] + [‘..’]*3)) # импорт: PP4E - корень,
# 3 уровнями выше
exec(open(os.path.join(mydir, ‘ textEditor.py')).read())
Чтобы запустить его из командной строки в окне консоли, достаточно, чтобы путь к каталогу со сценарием находился в системной переменной окружения PATH, - действие, выполняемое в первой строке в следующем фрагменте, достаточно было бы выполнить один раз в Панели Управления (Control Panel) Windows:
C:\..d\PP4E\Internet\Web....... set PATH=XPATHX;C:\...\PP4E\Gui\TextEditor
C:\...\PP4E\Internet\Web> pyedit.pyw test-cookies.py
Этот сценарий также работает и в Unix, хотя в нем нет необходимости, если правильно установить переменные окружения PYTHONPATH и PATH (после этого можно запускать textEditor.py непосредственно), - а я не стал выполнять эти настройки на всех моих компьютерах. Ради интереса можно попробовать зарегистрировать этот сценарий как средство автоматического открытия файлов «.txt» при щелчке на них или при вводе их имен в командной строке (если, конечно, вы спокойно перенесете расставание с Notepad).
Реализация главного файла
Наконец, модуль в примере 11.4 представляет собой реализацию PyEdit. Этот файл может запускаться как самостоятельный сценарий или импортироваться другими приложениями. Его программный код организован по пунктам главного меню. Главные классы, используемые для запуска и встраивания объекта PyEdit, находятся в конце файла. Во время экспериментов с PyEdit изучайте этот листинг, чтобы разобраться в его возможностях и используемых приемах.
Пример 11.4. PP4E\Gui\TextEditor\textEditor.py
##############################################################################
PyEdit 2.1: Текстовый редактор и компонент на Python/tkinter.
Использует текстовый виджет из библиотеки Tk, меню и панель инструментов GuiMaker для реализации полнофункционального текстового редактора, который может выполняться, как самостоятельная программа, или прикрепляться к другим графическим интерфейсам, как компонент. Используется также в PyMailGUI и PyView для редактирования сообщений электронной почты и примечаний к файлам изображений. Кроме того, используется в PyMailGUI и PyDemos во всплывающем режиме для отображения текстовых файлов и файлов с исходными текстами.
Новое в версии 2.1 (4 издание)
- работает под управлением Python 3.X (3.1)
- добавлен пункт "grep” меню и диалог: многопоточный поиск в файлах
- проверяет все окна на наличие несохраненных изменений при завершении
- поддерживает произвольные кодировки для файлов: в соответствии с настройками в файле textConfig.py
- переработаны диалоги поиска с заменой и выбора шрифта, чтобы обеспечить возможность одновременного вывода нескольких диалогов
- вызывает self.update() перед вставкой текста в новое окно
- различные улучшения в реализации операции Run Code, как описывается в следующем разделе
2.1 улучшения в реализации операции Run Code:
- после команды chdir использует базовое имя запускаемого файла, а не относительные пути
- в Windows использует инструмент запуска, поддерживающий передачу аргументов командной строки
- операция Run Code наследует преобразование символов обратного слеша от модуля launchmodes (необходимость в этом уже отпала)
Новое в версии 2.0 (3 издание)
- добавлен простой диалог выбора шрифта
- использует прикладной интерфейс Tk 8.4 к стеку отмен, чтобы добавить поддержку отмены/возврата (undo/redo) операций редактирования
- запрос подтверждения при выполнении операций Quit, Open, New, Run выполняется, только если имеются несохраненные изменения
- поиск теперь по умолчанию выполняется без учета регистра символов
- создан модуль с настройками для начальных значений шрифта/цвета/размера/чувствительности к регистру при поиске
TBD1 (и предложения для самостоятельной реализации):
- необходимость учета регистра символов при поиске можно было бы задавать в графическом интерфейсе (а не только в файле с настройками)
- при поиске по файлу или в операции Grep можно было бы использовать поддержку регулярных выражений, реализованную в модуле re (см. следующую главу)
- можно было бы попробовать реализовать подсветку синтаксиса (как в IDLE или в других редакторах)
- можно было бы попробовать проверить завершение работы программы методом quit() в неподконтрольных окнах
- можно было бы помещать в очередь каждый результат, найденный в диалоге Grep, чтобы избежать задержек
- можно было бы использовать изображения на кнопках в панели инструментов (как в примерах из главы 9)
- можно было бы просматривать строки, чтобы определить позицию вставки Tk для оформления отступов в окне Info
- можно было бы поэкспериментировать с проблемой кодировок в диалоге "grep” (смотрите примечания в программном коде);
##############################################################################
Version = ‘2.1’
import sys, os # платформа, аргументы,
# инструменты запуска
from tkinter import * # базовые виджеты, константы
from tkinter.filedialog import Open, SaveAs # стандартные диалоги from tkinter.messagebox import showinfo, showerror, askyesno from tkinter.simpledialog import askstring, askinteger from tkinter.colorchooser import askcolor
from PP4E.Gui.Tools.guimaker import * # Frame + построители
# меню/панелей инструментов
# общие настройки try:
import textConfig # начальный шрифт и цвета
configs = textConfig.__dict__ # сработает, даже если модуль отсутствует в
except: # пути поиска или содержит ошибки
configs = {}
helptext = """PyEdit, версия %s
апрель, 2010 (2.0: январь, 2006)
(1.0: октябрь, 2000)
Программирование на Python, 4 издание
Марк Лутц (Mark Lutz), для издательства O’Reilly Media, Inc.
Программа и встраиваемый компонент текстового редактора, написанный на Python/tkinter. Для быстрого доступа к операциям использует отрывные меню, панели инструментов и горячие клавиши в меню.
To Be Done - что еще можно сделать. - Прим. ред.
Дополнения в версии %s:
- поддержка 3.X
- новый диалог "grep” поиска во внешних файлах
- проверка несохраненных изменений при завершении
- поддержка произвольных кодировок для файлов
- допускает одновременный вывод нескольких диалогов поиска с заменой и выбора шрифта
- различные улучшения в операции Run Code
Дополнения в предыдущей версии:
- диалог выбора шрифта
- неограниченное количество отмен/возвратов
- quit/open/new/run предлагают сохранить, только если есть несохраненные изменения
- поиск выполняется без учета регистра символов
- модуль с начальными настройками textConfig.py
START = ‘1.0’ # индекс первого символа: строка=1,столбец=0
SEL_FIRST = SEL + ‘.first’ # отобразить тег sel в индекс
SEL_LAST = SEL + ‘.last’ # то же, что ‘sel.last’
FontScale = 0 # использовать увеличенный шрифт в Linux
if sys.platform[:3] != ‘win’: # и в других не-Windows системах FontScale = 3
##############################################################################
# Главные классы: реализуют графический интерфейс редактора, операции
# разновидности GuiMaker должны подмешиваться в более специализированные
# подклассы, а не наследоваться непосредственно, потому что этот класс
# принимает множество форм.
##############################################################################
class TextEditor: # смешать с классом Frame, имеющим меню/панель инструментов startfiledir = ‘.’ # для диалогов editwindows = [] # для проверки при завершении
# Настройки порядка выбора кодировки
# импортируется в класс, чтобы обеспечить возможность переопределения в
# подклассе
if__name__== ‘__main__’:
from textConfig import ( # мой каталог в пути поиска
opensAskUser, opensEncoding,
savesUseKnownEncoding, savesAskUser, savesEncoding)
else:
from .textConfig import ( # 2.1: всегда из этого пакета
opensAskUser, opensEncoding,
savesUseKnownEncoding, savesAskUser, savesEncoding)
ftypes = [(‘All files’, ‘*’), # для диалога открытия файла
(‘Text files’, ‘.txt’), # настроить в подклассе или
(‘Python files’, ‘.py’)] # устанавливать в каждом экземпляре
colors = [{‘fg’:’black’, ‘bg’:’white’}, # список цветов для выбора
{‘fg’:’yellow’, ‘bg’:’black’}, # первый элемент по умолчанию
{‘fg’:’white’, ‘bg’:’blue’}, # переделать по-своему или
{‘fg’:’black’, ‘bg’:’beige’}, # использовать элемент выбора
{‘fg’:’yellow’, ‘bg’:’purple’},# PickBg/Fg
{‘fg’:’black’, ‘bg’:’brown’},
{‘fg’:’lightgreen’, ‘bg’:’darkgreen’},
{‘fg’:’darkblue’, ‘bg’:’orange’},
{‘fg’:’orange’, ‘bg’:’darkblue’}]
fonts = [(‘courier’, 9+FontScale, ‘normal’), # шрифты, нейтральные
(‘courier’, 12+FontScale, ‘normal’), # в отношении платформы
(‘courier’, 10+FontScale, ‘bold’), # (семейство, размер, стиль)
(‘courier’, 10+FontScale, ‘italic’), # или вывести в списке
(‘times’, 10+FontScale, ‘normal’), # увеличить в Linux
(‘helvetica’, 10+FontScale, ‘normal’), # использовать
(‘ariel’, 10+FontScale, ‘normal’), # ‘bold italic’ для 2
(‘system’, 10+FontScale, ‘normal’), # а также ‘underline’
(‘courier’, 20+FontScale, ‘normal’)]
def __init__(self, loadFirst=’’, loadEncode=’’):
if not isinstance(self, GuiMaker):
raise TypeError(‘TextEditor needs a GuiMaker mixin’) self.setFileName(None) self.lastfind = None self.openDialog = None self.saveDialog = None
self.knownEncoding = None # 2.1 кодировки: заполняется Open или Save self.text.focus() # иначе придется щелкнуть лишний раз
if loadFirst:
self.update() # 2.1: иначе строка 2;
self.onOpen(loadFirst, loadEncode) # см. описание в книге
def start(self): # вызывается из GuiMaker.__init__
self.menuBar = [ # настройка меню/панелей
(‘File’, 0, # определение дерева меню GuiMaker
[(‘Open...’, 0, self.onOpen), # встроен. метод для self
(‘Save’, 0, self.onSave), # метка, клавиша, обработчик
(‘Save As...’, 5, self.onSaveAs),
(‘New’, 0, self.onNew),
‘separator’,
(‘Quit...’, 0, self.onQuit)]
),
(‘Edit’, 0,
[(‘Undo’, 0, self.onUndo),
(‘Redo’, 0, self.onRedo),
‘separator’,
(‘Cut’, 0, self.onCut),
(‘Copy’, 1, self.onCopy), (‘Paste’, 0, self.onPaste),
‘separator’,
(‘Delete’, 0, self.onDelete),
(‘Select All’, 0, self.onSelectAll)]
),
(‘Search’, 0,
[(‘Goto...’, 0, self.onGoto),
(‘Find...’, 0, self.onFind),
(‘Refind’, 0, self.onRefind),
(‘Change...’, 0, self.onChange),
(‘Grep...’, 3, self.onGrep)]
),
(‘Tools’, 0,
[(‘Pick Font...’, 6, self.onPickFont),
(‘Font List’, 0, self.onFontList),
‘separator’,
(‘Pick Bg...’, 3, self.onPickBg),
(‘Pick Fg...’, 0, self.onPickFg),
(‘Color List’, 0, self.onColorList),
‘separator’,
(‘Info...’, 0, self.onInfo),
(‘Clone’, 1, self.onClone),
(‘Run Code’, 0, self.onRunCode)]
)]
self.toolBar = [
(‘Save’, self.onSave, {‘side’: LEFT}),
(‘Cut’, self.onCut, {‘side’: LEFT}),
(‘Copy’, self.onCopy, {‘side’: LEFT}),
(‘Paste’, self.onPaste, {‘side’: LEFT}),
(‘Find’, self.onRefind, {‘side’: LEFT}),
(‘Help’, self.help, {‘side’: RIGHT}),
(‘Quit’, self.onQuit, {‘side’: RIGHT})]
def makeWidgets(self): # вызывается из GuiMaker.__init__
name = Label(self, bg=’black’, fg=’white’) # ниже меню, выше панели name.pack(side=TOP, fill=X) # компоновка меню/панелей
# фрейм GuiMaker
# компонуется сам
vbar = Scrollbar(self)
hbar = Scrollbar(self, orient=’horizontal’)
text = Text(self, padx=5, wrap=’none’) # запретить перенос строк
text.config(undo=1, autoseparators=1) # 2.0, по умолчанию 0, 1
vbar.pack(side=RIGHT, fill=Y)
hbar.pack(side=BOTTOM, fill=X) # скомпоновать Text последним
text.pack(side=TOP, fill=BOTH, expand=YES) # иначе обрежутся полосы
# прокрутки
text.config(yscrollcommand=vbar.set) # вызывать vbar.set при
text.config(xscrollcommand=hbar.set) # перемещении по тексту
vbar.config(command=text.yview) # вызывать text.yview при прокрутке hbar.config(command=text.xview) # или hbar[‘command’]=text.xview
# 2.0: применить пользовательские настройки или умолчания startfont = configs.get(‘font’, self.fonts[0]) startbg = configs.get(‘bg’, self.colors[0][‘bg’]) startfg = configs.get(‘fg’, self.colors[0][‘fg’]) text.config(font=startfont, bg=startbg, fg=startfg) if ‘height’ in configs: text.config(height=configs[‘height’]) if ‘width’ in configs: text.config(width =configs[‘width’]) self.text = text self.filelabel = name
##########################################################################
# Операции меню File
##########################################################################
def my_askopenfilename(self): # объекты запоминают каталог/файл if not self.openDialog: # последней операции
self.openDialog = Open(initialdir=self.startfiledir, filetypes=self.ftypes) return self.openDialog.show()
def my_asksaveasfilename(self): # объекты запоминают каталог/файл if not self.saveDialog: # последней операции
self.saveDialog = SaveAs(initialdir=self.startfiledir, filetypes=self.ftypes)
return self.saveDialog.show()
def onOpen(self, loadFirst=’’, loadEncode=’’):
2.1: полностью переписан для поддержки Юникода; открывает в текстовом режиме с кодировкой, переданной в аргументе, введенной пользователем, заданной в модуле textconfig или с кодировкой по умолчанию; в крайнем случае открывает файл в двоичном режиме и отбрасывает символы \r в Windows, если они присутствуют, чтобы обеспечить нормальное отображение текста; содержимое извлекается и возвращается в виде строки str, поэтому при сохранении его требуется кодировать: сохраняет кодировку, используемую здесь;
предварительно проверяет возможность открытия файла; мы могли бы также вручную загружать и декодировать bytes в str, чтобы избежать необходимости выполнять несколько попыток открытия, но этот прием подходит не для всех случаев;
порядок выбора кодировки настраивается в локальном textConfig.py:
1) сначала применяется кодировка, переданная клиентом (например, кодировка из заголовка сообщения электронной почты)
2) затем, если opensAskUser возвращает True, применяется кодировка, введенная пользователем (предварительно в диалог записывается кодировка по умолчанию)
3) затем, если opensEncoding содержит непустую строку, применяется эта кодировка: ‘latin-1’ и так далее.
4) затем выполняется попытка применить кодировку sys.getdefaultencoding()
5) в крайнем случае выполняется чтение в двоичном режиме и используется алгоритм, заложенный в библиотеку Tk
if self.text_edit_modified(): # 2.0
if not askyesno(‘PyEdit’, ‘Text has changed: discard changes?’): return
file = loadFirst or self.my_askopenfilename() if not file: return
if not os.path.isfile(file):
showerror(‘PyEdit’, ‘Could not open file ‘ + file) return
# применить известную кодировку, если указана
# (например, из заголовка сообщения электронной почты)
text = None # пустой файл = ‘’ = False: проверка на None! if loadEncode: try:
text = open(file, ‘r’, encoding=loadEncode).read() self.knownEncoding = loadEncode except (UnicodeError, LookupError, IOError): # Lookup: ошибка pass # в имени
# применить кодировку, введенную пользователем,
# предварительно записать в диалог следующий вариант, как значение
# по умолчанию
if text == None and self.opensAskUser:
self.update() # иначе в некоторых случаях диалог не появится askuser = askstring(‘PyEdit’, ‘Enter Unicode encoding for open’, initialvalue=(self.opensEncoding or
sys.getdefaultencoding() or ''))
if askuser: try:
text = open(file, ‘r’, encoding=askuser).read() self.knownEncoding = askuser except (UnicodeError, LookupError, IOError): pass
# применить кодировку из файла с настройками (может быть, выполнять
# эту попытку до того, как запрашивать кодировку у пользователя?) if text == None and self.opensEncoding:
try:
text = open(file, ‘r’, encoding=self.opensEncoding).read() self.knownEncoding = self.opensEncoding except (UnicodeError, LookupError, IOError): pass
# применить системную кодировку по умолчанию (utf-8 в windows;
# всегда пытаться использовать utf8?) if text == None:
try:
text = open(file, ‘r’,
encoding=sys.getdefaultencoding()).read() self.knownEncoding = sys.getdefaultencoding() except (UnicodeError, LookupError, IOError): pass
# крайний случай: использовать двоичный режим и положиться на
# возможности Tk if text == None:
try:
text = open(file, ‘rb’).read() # строка bytes text = text.replace(b’\r\n’, b’\n’) # для отображения self.knownEncoding = None # и последующего сохранения
except IOError: pass
if text == None:
showerror(‘PyEdit’, ‘Could not decode and open file ‘ + file) else:
self.setAllText(text)
self.setFileName(file)
self.text.edit_reset() # 2.0: очистка стеков undo/redo self.text.edit_modified(0) # 2.0: сбросить флаг наличия изменений
def onSave(self):
self.onSaveAs(self.currfile) # may be None
def onSaveAs(self, forcefile=None):
2.1: полностью переписан для поддержки Юникода: виджет Text всегда возвращает содержимое в виде строки str, поэтому нам необходимо побеспокоиться о кодировке, чтобы сохранить файл, независимо от режима, в котором открывается выходной файл (для двоичного режима необходимо будет получить bytes, а для текстового необходимо указать кодировку); пытается применить кодировку, использовавшуюся при открытии или сохранении (если известна), предлагаемую пользователем, указанную в файле с настройками, и системную кодировку по умолчанию; в большинстве случаев можно использовать системную кодировку по умолчанию;
в случае успешного выполнения операции сохраняет кодировку для использования в дальнейшем, потому что это может быть первая операция Save после операции New или вставки текста вручную; в файле с настройками можно определить, чтобы обе операции, Save и Save As, использовали последнюю известную кодировку (однако если для операции Save это оправданно, то в случае с операцией Save As это не так очевидно); графический интерфейс предварительно записывает эту кодировку в диалог, если она известна;
выполняет text.encode() вручную, чтобы избежать создания файла; для текстовых файлов автоматически выполняется преобразование символов конца строки: в Windows добавляются символы \r, отброшенные при открытии файла в текстовом (автоматически) или в двоичном (вручную) режиме; Если содержимое вставлялось вручную, здесь необходимо предварительно удалить символы \r, иначе они будут продублированы; knownEncoding=None перед первой операцией Open или Save, после New и если операция Open открыла файл в двоичном режиме;
порядок выбора кодировки настраивается в локальном textConfig.py:
1) если savesUseKnownEncoding > 0, применить кодировку, использованную в последней операции Open или Save
2) если savesAskUser = True, применить кодировку, указанную пользователем (предлагать известную в качестве значения по умолчанию?)
3) если savesEncoding - непустая строка, применить эту кодировку: ‘utf-8’ и так далее
4) в крайнем случае применить sys.getdefaultencoding()
filename = forcefile or self.my_asksaveasfilename() if not filename: return
text = self.getAllText() # 2.1: строка str, без символов \r, encpick = None # даже если текст читался/вставлялся
# в двоичном виде
# применить известную кодировку, использовавшуюся в последней операции
# Open или Save, если известна
if self.knownEncoding and ( # известна?
(forcefile and self.savesUseKnownEncoding >= 1) or # для Save? (not forcefile and self.savesUseKnownEncoding >= 2)):# для SaveAs? try:
text.encode(self.knownEncoding) encpick = self.knownEncoding except UnicodeError: pass
# применить кодировку, введенную пользователем,
# предварительно записать в диалог следующий вариант, как значение
# по умолчанию
if not encpick and self.savesAskUser:
self.update()# иначе в некоторых случаях диалог не появится askuser = askstring(‘PyEdit’, ‘Enter Unicode encoding for save’, initialvalue=(self.knownEncoding or self.savesEncoding or sys.getdefaultencoding() or ''))
if askuser: try:
text.encode(askuser) encpick = askuser
except (UnicodeError, LookupError): # LookupError: ошибка в имени pass # UnicodeError: ошибка
# кодирования
# применить кодировку из файла с настройками if not encpick and self.savesEncoding:
try:
text.encode(self.savesEncoding) encpick = self.savesEncoding except (UnicodeError, LookupError): pass
# применить системную кодировку по умолчанию (utf8 в windows) if not encpick:
try:
text.encode(sys.getdefaultencoding()) encpick = sys.getdefaultencoding() except (UnicodeError, LookupError): pass
# открыть в текстовом режиме, чтобы автоматически выполнить
# преобразование символов конца строки и применить кодировку if not encpick:
showerror(‘PyEdit’, ‘Could not encode for file ‘ + filename) else: try:
file = open(filename, ‘w’, encoding=encpick) file.write(text) file.close() except:
showerror(‘PyEdit’, ‘Could not write file ‘ + filename) else:
self.setFileName(filename) # может быть вновь созданным self.text.edit_modified(0) # 2.0: сбросить флаг изменений self.knownEncoding = encpick # 2.1: запомнить кодировку
# не сбрасывать стеки undo/redo!
def onNew(self):
запускает редактирование совершенно нового файла в текущем окне; смотрите метод onClone, который вместо этого создает независимое окно редактирования;
if self.text_edit_modified(): # 2.0
if not askyesno(‘PyEdit’, ‘Text has changed: discard changes?’): return
self.setFileName(None)
self.clearAllText()
self.text.edit_reset() # 2.0: очистить стеки undo/redo self.text.edit_modified(0) # 2.0: сбросить флаг наличия изменений self.knownEncoding = None # 2.1: кодировка неизвестна
def onQuit(self):
вызывается выбором операции Quit в меню/панели инструментов и щелчком на кнопке X в заголовке окна;
2.1: не завершать приложение при наличии несохраненных изменений;
2.0: не выводить запрос на подтверждение, если нет изменений в self; перемещен в классы окон верхнего уровня ниже, так как его реализация может зависеть от особенностей использования: операция Quit в графическом интерфейсе может вызывать метод quit() для завершения, destroy() - чтобы просто закрыть окно Toplevel, Tk или фрейм с редактором, эта операция может даже вообще не предоставляться, если редактор присоединяется, как компонент; проверяет self на наличие несохраненных изменений, а если предполагается вызов метода quit(), главные окна должны также проверить наличие несохраненных изменений в других окнах, присутствующих в глобальном списке процесса;
assert False, ‘onQuit must be defined in window-specific sublass’
def text_edit_modified(self):
2.1: теперь действует! кажется, проблема заключалась в типе bool результата в tkinter;
2.0: self.text.edit_modified() не работает в Python 2.4: выполнить проверку вручную;
return self.text.edit_modified()
#return self.tk.call((self.text._w, ‘edit’) + (‘modified’, None))
##########################################################################
# Операции меню Edit
##########################################################################
def onUndo(self): # 2.0
try: # tk8.4 поддерживает стеки undo/redo
self.text.edit_undo() # возбуждает исключение, если стеки пустые except TclError: # меню открывается для быстрого доступа
showinfo(‘PyEdit’, ‘Nothing to undo’) # к операциям
def onRedo(self): # 2.0: возврат отмененной операции
try: # редактирования
self.text.edit_redo() except TclError:
showinfo(‘PyEdit’, ‘Nothing to redo’)
def onCopy(self): # получить текст, выделенный мышью
if not self.text.tag_ranges(SEL): # сохранить в системном буфере showerror(‘PyEdit’, ‘No text selected’) else:
text = self.text.get(SEL_FIRST, SEL_LAST)
self.clipboard_clear()
self.clipboard_append(text)
def onDelete(self): # удалить выделенный текст без сохранения
if not self.text.tag_ranges(SEL):
showerror(‘PyEdit’, ‘No text selected’) else:
self.text.delete(SEL_FIRST, SEL_LAST)
def onCut(self):
if not self.text.tag_ranges(SEL):
showerror(‘PyEdit’, ‘No text selected’) else:
self.onCopy() # сохранить и удалить выделенный текст
self.onDelete()
def onPaste(self): try:
text = self.selection_get(selection=’CLIPBOARD’) except TclError:
showerror(‘PyEdit’, ‘Nothing to paste’) return
self.text.insert(INSERT, text) # вставить в текущую позицию курсора self.text.tag_remove(SEL, ‘1.0’, END) self.text.tag_add(SEL, INSERT+’-%dc’ % len(text), INSERT) self.text.see(INSERT) # выделить, чтобы можно было вырезать
def onSelectAll(self):
self.text.tag_add(SEL, ‘1.0’, END+’-1c’)# выделить весь текст self.text.mark_set(INSERT, ‘1.0’) # переместить позицию в начало
self.text.see(INSERT) # прокрутить в начало
##########################################################################
# Операции меню Search
##########################################################################
def onGoto(self, forceline=None):
line = forceline or askinteger(‘PyEdit’, ‘Enter line number’)
self.text.update()
self.text.focus()
if line is not None:
maxindex = self.text.index(END+’-1c’) maxline = int(maxindex.split(‘.’)[0]) if line > 0 and line <= maxline:
self.text.mark_set(INSERT, ‘%d.0’ % line) # перейти к стр.
self.text.tag_remove(SEL, ‘1.0’, END) # снять выделен.
self.text.tag_add(SEL, INSERT, ‘insert + 1l’) # выделить стр.
self.text.see(INSERT) # прокрутить
else: # до строки
showerror(‘PyEdit’, ‘Bad line number’)
def onFind(self, lastkey=None):
key = lastkey or askstring(‘PyEdit’, ‘Enter search string’) self.text.update()
self.text.focus() self.lastfind = key
if key: # 2.0: без учета регистра символов
nocase = configs.get(‘caseinsens’, True) # 2.0: настройки where = self.text.search(key, INSERT, END, nocase=nocase) if not where: # не переходить
showerror(‘PyEdit’, ‘String not found’)# в начало else:
pastkey = where + ‘+%dc’ % len(key) # позиция после ключа self.text.tag_remove(SEL, ‘1.0’, END) # снять выделение self.text.tag_add(SEL, where, pastkey) # выделить ключ self.text.mark_set(INSERT, pastkey) # для след. поиска self.text.see(where) # прокрутить экран
def onRefind(self):
self.onFind(self.lastfind)
def onChange(self):
немодальный диалог поиска с заменой
2.1: поля ввода диалога передаются обработчику, допускается открывать одновременно несколько диалогов поиска с заменой
new = Toplevel(self) new.title(‘PyEdit - change’)
Label(new, text=’Find text?’, relief=RIDGE, width=15).grid(row=0,
column=0)
Label(new, text=’Change to?’, relief=RIDGE, width=15).grid(row=1,
column=0)
entry1 = Entry(new) entry2 = Entry(new)
entry1.grid(row=0, column=1, sticky=EW) entry2.grid(row=1, column=1, sticky=EW)
def onFind(): # использует поле ввода из внешней обл. видимости
self.onFind(entry1.get()) # вызов обработчика диалога поиска
def onApply():
self.onDoChange(entry1.get(), entry2.get())
Button(new, text=’Find’, command=onFind ).grid(row=0,
column=2, sticky=EW)
Button(new, text=’Apply’, command=onApply).grid(row=1,
column=2, sticky=EW)
new.columnconfigure(1, weight=1) # растягиваемые поля ввода
def onDoChange(self, findtext, changeto):
# реализует замену для диалога поиска с заменой:
# заменяет и повторяет поиск
if self.text.tag_ranges(SEL): # сначала найти
self.text.delete(SEL_FIRST, SEL_LAST) self.text.insert(INSERT, changeto) # удалит, если пусто self.text.see(INSERT)
self.onFind(findtext) # переход к следующему
self.text.update() # принудительное обновление
def onGrep(self):
новое в версии 2.1: многопоточная реализация поиска во внешних файлах; выполняет поиск указанной строки в файлах, имена которых соответствуют заданному шаблону; щелчок на элементе в списке открывает соответствующий файл, при этом выполняется переход к строке с найденным вхождением;
поиск выполняется в отдельном потоке, чтобы графический интерфейс не блокировался и оставался активным и чтобы позволить одновременно выполнять несколько операций поиска; можно было бы использовать модуль, если прекращать цикл проверки при отсутствии активных операций поиска;
алгоритм выбора кодировки при выполнении поиска: содержимое текстовых файлов в дереве, где выполняется поиск, может храниться в любых кодировках: мы не предлагаем вводить имя кодировки для каждого файла (как при открытии), однако позволяем указать кодировку для всего дерева, предварительно устанавливая общесистемную кодировку по умолчанию, используемую файловой системой или для представления текста, и пропускаем файлы, декодирование которых терпит неудачу; в самом тяжелом случае пользователю может потребоваться выполнить поиск N раз, если в дереве могут присутствовать файлы с текстом в N различных кодировках; иначе операция открытия будет возбуждать исключение, а открытие в двоичном режиме может не дать совпадения кодированного текста с испытуемой строкой;
TBD: может, лучше было бы выводить сообщение об ошибке при встрече с файлом, который не удалось декодировать?
но файлы с кодировкой utf-16 (2 байта на символ), созданные в Notepad, благополучно могут декодироваться с применением кодировки utf-8, однако строка при этом не будет найдена;
TBD: можно было бы позволить вводить несколько имен кодировок, отделяя их друг от друга запятыми, и пробовать применять их поочередно к каждому файлу, помимо loadEncode
from PP4E.Gui.ShellGui.formrows import makeFormRow
# немодальный диалог: ввод имени каталога, шаблон имени файла,
# искомая строка popup = Toplevel() popup.title(‘PyEdit - grep’)
var1 = makeFormRow(popup, label=’Directory root’, width=18, browse=False)
var2 = makeFormRow(popup, label=’Filename pattern’, width=18, browse=False)
var3 = makeFormRow(popup, label=’Search string’, width=18, browse=False)
var4 = makeFormRow(popup, label=’Content encoding’, width=18, browse=False)
var1.set(‘.’) # текущий каталог
var2.set(‘*.py’) # начальные значения
var4.set(sys.getdefaultencoding()) # для содержимого файлов, а не имен cb = lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(),
var4.get())
Button(popup, text=’Go’,command=cb).pack()
def onDoGrep(self, dirname, filenamepatt, grepkey, encoding):
вызывается щелчком на кнопке Go в диалоге Grep: заполняет список найденными совпадениями
tbd: возможно, следует запускать поток-производитель как демон, чтобы он автоматически завершался вместе с приложением?
import threading, queue
# создать немодальный и незакрываемый диалог mypopup = Tk()
mypopup.title(‘PyEdit - grepping’) status = Label(mypopup,
text=’Grep thread searching for: %r...’ % grepkey) status.pack(padx=20, pady=20)
mypopup.protocol(‘WM_DELETE_WINDOW’, lambda: None) # игнорировать
# кнопку X
# запустить поток-производитель, цикл проверки результатов myqueue = queue.Queue()
threadargs = (filenamepatt, dirname, grepkey, encoding, myqueue) threading.Thread(target=self.grepThreadProducer, args=threadargs).start()
self.grepThreadConsumer(grepkey, encoding, myqueue, mypopup)
def grepThreadProducer(self, filenamepatt, dirname, grepkey, encoding, myqueue):
выполняется в параллельном потоке, не имеющем отношения к графическому интерфейсу: помещает в очередь список с результатами find.find; найденные совпадения можно было бы помещать в очередь по мере их обнаружения, но для этого необходимо обеспечить сохранение окна на экране; здесь могут возникать ошибки декодирования не только содержимого, но и имен файлов;
TBD: чтобы избежать ошибок декодирования имен файлов в os.walk/listdir, можно было бы передавать методу find() строку bytes, но какую кодировку использовать: sys.getfilesystemencoding(), если она не равна None? Смотрите также примечание в разделе "Модуль fnmatch” в главе 6: в версии 3.1 модуль fnmatch всегда преобразует текст в двоичное представление, используя кодировку Latin-1;
from PP4E.Tools.find import find
matches = [] try:
for filepath in find(pattern=filenamepatt, startdir=dirname): try:
textfile = open(filepath, encoding=encoding) for (linenum, linestr) in enumerate(textfile): if grepkey in linestr:
msg = ‘%s@%d [%s]’ % (filepath,
linenum + 1, linestr)
matches.append(msg)
except UnicodeError as X: # напр.: декодир.
print(‘Unicode error in:’, filepath, X) # двоичный режим except IOError as X:
print(‘IO error in:’, filepath, X) # напр.: права доступа
finally:
myqueue.put(matches) # остановить цикл потребителя при исключении # имена файлов?
def grepThreadConsumer(self, grepkey, encoding, myqueue, mypopup):
выполняется в главном потоке графического интерфейса: просматривает очередь в ожидании результатов или []; может иметься несколько активных потоков/циклов/очередей, связанных с поиском; в процессе могут присутствовать другие типы потоков/циклов проверки, особенно если PyEdit прикрепляется как компонент (PyMailGUI);
import queue try:
matches = myqueue.get(block=False) except queue.Empty:
myargs = (grepkey, encoding, myqueue, mypopup) self.after(250, self.grepThreadConsumer, *myargs) else:
mypopup.destroy() # закрыть информационный диалог self.update() # и стереть его с экрана if not matches:
showinfo(‘PyEdit’, ‘Grep found no matches for: %r’ % grepkey) else:
self.grepMatchesList(matches, grepkey, encoding)
def grepMatchesList(self, matches, grepkey, encoding):
заполняет список найденными совпадениями в случае успеха; так как поиск увенчался успехом, кодировка уже известна: использовать ее в обработчике щелчка на файле в списке, чтобы обеспечить его открытие без обращения к пользователю;
from PP4E.Gui.Tour.scrolledlist import ScrolledList print(‘Matches for %s: %s’ % (grepkey, len(matches)))
# перехватывает двойной щелчок на списке class ScrolledFilenames(ScrolledList):
def runCommand(self, selection):
file, line = selection.split(‘ [‘, 1)[0].split(‘@’) editor = TextEditorMainPopup(
loadFirst=file, winTitle=’ grep match’, loadEncode=encoding) editor.onGoto(int(line))
editor.text.focus_force() # на самом деле не требуется
# новое модальное окно popup = Tk()
popup.title(‘PyEdit - grep matches: %r (%s)’ % (grepkey, encoding)) ScrolledFilenames(parent=popup, options=matches)
##########################################################################
# Операции меню Tools
##########################################################################
def onFontList(self):
self.fonts.append(self.fonts[0]) # выбрать следующий шрифт в списке del self.fonts[0] # изменит размер текстовой области
self.text.config(font=self.fonts[0])
def onColorList(self):
self.colors.append(self.colors[0]) # выбрать следующий цвет в списке del self.colors[0] # текущий сместить в конец
self.text.config(fg=self.colors[0][‘fg’], bg=self.colors[0][‘bg’])
def onPickFg(self):
self.pickColor(‘fg’) # добавлено 10/02/00
def onPickBg(self): # выбрать произвольный цвет
self.pickColor(‘bg’) # в стандартном диалоге выбора цвета
def pickColor(self, part): # это очень просто (triple, hexstr) = askcolor() if hexstr:
self.text.config(**{part: hexstr}) def onInfo(self):
диалог с информацией о тексте и о местоположении курсора;
ВНИМАНИЕ (2.1): при вычислении позиции курсора библиотека Tk считает символ табуляции, как один символ: следует умножать их на 8, чтобы обеспечить соответствие с визуальным положением?
text = self.getAllText() # добавлено 5/3/00 за 15 мин.
bytes = len(text) # словами считается все, что
lines = len(text.split(‘\n’)) # отделяется пробелами
words = len(text.split()) # 3.x: в bytes - символы
index = self.text.index(INSERT) # в str - кодовые пункты Юникода
where = tuple(index.split(‘.’)) showinfo(‘PyEdit Information’,
‘Current location:\n\n’ +
‘line:\t%s\ncolumn:\t%s\n\n' % where +
‘File text statistics:\n\n’ +
‘chars:\t%d\nlines:\t%d\nwords:\t%d\n’ % (bytes, lines,
words))
def onClone(self, makewindow=True):
открывает новое окно редактора, не изменяя уже открытое (onNew); наследует поведение операции Quit и других от окна, копия которого создается;
2.1: подклассы должны переопределять/замещать этот метод, если будут создавать собственные окна,
иначе этот метод создаст дополнительное поддельное пустое окно; if not makewindow:
new = None # предполагается, что класс создает
else: # собственное окно
new = Toplevel() # новое окно редактора в том же процессе
myclass = self.__class__ # объект класса экземпляра (самый нижний)
myclass(new) # прикрепить/запустить экземпляр моего класса
def onRunCode(self, parallelmode=True):
выполнение редактируемого программного кода Python -- это не IDE, но удобно; пытается выполнить в каталоге файла, не в cwd (может быть корнем PP4E); вводит и добавляет аргументы командной строки для файлов сценариев;
stdin/out/err для программного кода = стартовое окно редактора, если оно есть: запускайте редактор в окне консоли, чтобы увидеть вывод, производимый программным кодом; если parallelmode=True, открывает окно DOS для операций ввода-вывода; путь поиска модулей будет включать ‘.’ при запуске; при выполнении программного кода как отдельной строки корневым окном может быть окно PyEdit; здесь также можно использовать модули subprocess и multiprocessing;
2.1: исправлено на использование базового имени файла после chdir, без пути;
2.1: использует StartArgs для передачи аргументов в режиме запуска файлов в Windows;
2.1: вызывает update() после первого диалога, в противном случае второй диалог иногда не появляется на экране;
def askcmdargs():
return askstring(‘PyEdit’, ‘Commandline arguments?’) or ''
from PP4E.launchmodes import System, Start, StartArgs, Fork filemode = False
thefile = str(self.getFileName()) if os.path.exists(thefile):
filemode = askyesno(‘PyEdit’, ‘Run from file?’) self.update() # 2.1: вызывает update()
if not filemode: # выполнить как строку
cmdargs = askcmdargs()
namespace = {‘__name__’: ‘__main__’} # выполнить как сценарий
sys.argv = [thefile] + cmdargs.split() # можно использов. потоки
exec(self.getAllText() + ‘\n’, namespace)# игнорировать исключения
elif self.text_edit_modified(): # 2.0: проверка изменений
showerror(‘PyEdit’, ‘Text changed: you must save before run’) else:
cmdargs = askcmdargs()
mycwd = os.getcwd() # cwd может быть корнем
dirname, filename = os.path.split(thefile) # каталог, базовое имя
os.chdir(dirname or mycwd) # cd для файлов
thecmd = filename + ‘ ‘ + cmdargs # 2.1: не theFile
if not parallelmode: # выполнить как файл
System(thecmd, thecmd)() # блокировать редактор
else:
if sys.platform[:3] == ‘win’: # породить параллельно
run = StartArgs if cmdargs else Start # 2.1: аргументы run(thecmd, thecmd)() # или всегда Spawn
else:
Fork(thecmd, thecmd)() # породить параллельно
os.chdir(mycwd) # вернуться в каталог
def onPickFont(self):
2.0 немодальный диалог выбора шрифта
2.1: поля ввода диалога передаются обработчику, допускается открывать одновременно несколько диалогов поиска выбора шрифта
from PP4E.Gui.ShellGui.formrows import makeFormRow popup = Toplevel(self) popup.title(‘PyEdit - font’)
var1 = makeFormRow(popup, label=’Family’, browse=False) var2 = makeFormRow(popup, label=’Size’, browse=False) var3 = makeFormRow(popup, label=’Style’, browse=False) var1.set(‘courier’)
var2.set(‘12’) # предлагаемые значения
var3.set(‘bold italic’) # смотрите допустимые значения в списке выбора Button(popup, text=’Apply’, command=
lambda: self.onDoFont(var1.get(), var2.get(), var3.get())).pack()
def onDoFont(self, family, size, style): try:
self.text.config(font=(family, int(size), style)) except:
showerror(‘PyEdit’, ‘Bad font specification’)
##############################################################################
# Готовые к употреблению классы редактора, подмешиваемые в подкласс
# фрейма GuiMaker, создающий меню и панели инструментов.
#
# Эти классы реализуют типичные случаи использования, однако возможны и другие
# реализации; для запуска PyEdit, как самостоятельной программы, следует
# вызвать метод TextEditorMain().mainloop(); переопределяйте/расширяйте
# в подклассах метод onQuit, чтобы обеспечить перехват события завершения
# приложения или уничтожения окна (смотрите пример PyView);
# ВНИМАНИЕ: можно было бы использовать windows.py для создания ярлыков,
# но здесь используется собственный протокол завершения. ##############################################################################
# -----------------------------------------------------------------------------
# 2.1: в quit(), не завершать без предупреждения, если в процессе открыты
# другие окна редактора и в них имеются несохраненные изменения - изменения
# будут потеряны, потому что все остальные окна тоже закрываются, включая
# множественные родительские окна Tk, включающие редактор; для слежения за
# всеми окнами PyEdit используется список экземпляров, созданных в процессе;
# это может оказаться чрезмерной мерой (если вместо quit() вызывается
# destroy(), когда достаточно проверить только дочернее окно редактирования
# уничтожаемого родителя), но лучше перестраховаться; метод onQuit перемещен
# сюда, потому что его реализация отличается для окон разных типов и может
# присутствовать не во всех окнах;
#
# предполагается, что TextEditorMainPopup никогда не будет играть роль
# родителя для других окон редактирования - дочерние виджеты Toplevel
# уничтожаются вместе со своими родителями; это не позволяет предотвратить
# закрытие из-за пределов классов PyEdit (метод quit в tkinter доступен
# во всех виджетах, и любой виджет может быть родителем для Toplevel!);
# ответственность за проверку наличия изменений в содержимом редактора
# полностью возлагается на клиента; обратите внимание, что в данной ситуации
# привязка события
# обработчик не может выполнять операции с графическим интерфейсом, такие как
# проверка наличия изменений и извлечение текста, - дополнительную информацию