Журнал LinuxFormat - перейти на главную

Бои за Авдеевку 2023

Материал из Linuxformat
(перенаправлено с «LXF80:Python»)
Перейти к: навигация, поиск

УЧЕБНИК Python

ПРОГРАММИРОВАНИЕ СЦЕНАРИЕВ

Содержание

Уроки Python

ЧАСТЬ 6

Теперь, когда мы научились обрабатывать строки, самое время показать наше умение другим. Но кто будет смотреть сценарий «без лица», пусть даже в Konsole с красивым прозрачным фоном? Для тех, кто не Квентин Тарантино, Сергей Супрунов покажет, как создаются на Python графические интерфейсы.

Чем больше работаешь с Python, тем сильнее ощущаешь его мощь и гибкость. Порой кажется, что для этого языка нет ничего невозможного. Это же можно отнести и к разработке графического интерфейса пользователя (хотя здесь Python просто использует возможности мощных библиотек). Как вы увидите, Python и в этом вопросе сохраняет простоту и эффективность, становясь весьма удобным инструментом для решения таких задач, как создание «обёрток» к различным консольным утилитам, разработка графических конфигураторов для ваших любимых инструментов, проектирование прототипов программ (куда проще за пару дней согласовать с заказчиком все «интерфейсные» моменты разрабатываемого проекта, меняя внешний вид прямо на переговорах, чем спустя месяцы напряжённой работы переделывать многомегабайтный проект на C++, только лишь потому, что заказчик счёл интерфейс слишком сложным для освоения), и т.д. Кстати, инсталлятор Gentoo, появившийся в версии 2006.0 и наделавший столько шума, тоже написан на Python.

Для Python разработано множество инструментов, отличающихся по гибкости, сложности, степени интеграции с вашим окружением. В рамках одного урока сложно рассмотреть всё это многообразие даже в общих чертах. Поэтому мы ограничимся кратким знакомством с модулем Tkinter и «привязками» PyQt и PyGTK.

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

Начнём со старого доброго Tkinter, который, несмотря на некоторую «неказистость» и плохую интеграцию с системой, всё же остаётся стандартным модулем, входящим в поставку практически всех дистрибутивов. К тому же его простота – идеальная особенность для учебных целей. А разобравшись с ним, вы без труда освоите и другие инструменты.

Очарование простоты

Проверить наличие Tkinter в вашем дистрибутиве достаточно просто (он должен быть, но всякое случается): запустите Python и выполните команду import Tkinter (обратите внимание на заглавную первую букву). Если вам не повезло и вы увидели сообщение об ошибке, придётся установить этот модуль отдельно. Проконсультируйтесь с вашим менеджером пакетов, не знает ли он случайно про Tkinter (имя пакета, в отличие от модуля, обычно записывается только маленькими буквами). Если он окажется из партизанской семьи, то прямая дорога вам на http://www.python.org/ (который с новым дизайном смотрится очень даже приятно).

(thumbnail)
Рисунок 1. Tkinter – лучший способ сделать не так, как у других!

Разобравшись с модулем, приступим сразу к серьёзной работе. В качестве примера выберем такую задачу: разработать графический интерфейс для просмотра man-страниц. Хорош он тем, что логика предельно проста, и нам почти не придётся отвлекаться от основной задачи. Итак, сразу код (по мере необходимости, будем прерывать его некоторыми пояснениями):

#!/usr/bin/Python
# -*- coding: utf-8 -*-
import os, re
from Tkinter import *

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

class ManReader:
  def getman(self, manpage, sn):
    bschar = '\b'
    tmp = os.popen('man %s %s 2>/dev/null' % (sn, manpage)).read()
    if tmp:
      tmp = re.sub(r'(.)%c\1' % bschar, r'\1', tmp)
      tmp = tmp.replace('_%c' % bschar, '')
    else:
      tmp = 'Page "%s" in section "%s" not found' % (manpage, sn or 'Auto')
    return tmp

Метод с украинским именем getman() отвечает за то, чтобы вернуть запрошенную man-страницу в виде текстовой переменной. Для этого используем уже знакомую нам функцию popen() модуля os. Нужно учитывать, что man-страницы могут содержать специальное форматирование: подсветку и подчёркивание. Чтобы не усложнять наш пример, мы просто избавляемся от ненужных символов, оставляя чистый текст. Пожалуй, нужно пояснить, как мы это делаем.

Если вы посмотрите на результат работы команды man в «натуральном» виде, например, сохранив его в файл, то заметите, что яркость создаётся следующим приёмом: символ выводится, стирается символом «\b» (Backspace) и затем снова выводится. Подчёркивание достигается выводом и последующим стиранием символа «_». То есть вы можете увидеть что-то такое: N\bNA\bAM\bME\bE = NAME,_\b/_\be_\bt_\bc = /etc. С удалением подчёркиваний всё понятно. А вот для удаления «символов яркости» мы используем регулярное выражение, причём на первый фрагмент, взятый в скобки, мы можем в дальнейшем ссылаться с помощью \1. То есть регулярное выражение «(.)!\1» означает два одинаковых символа, разделённых восклицательным знаком. Мы же используем символ \b. Обратите внимание, что его нельзя указывать непосредственно в регулярном выражении, иначе он будет трактоваться как граница слова. Вернёмся к нашим «виджетам»:

def pressBtn(self):
sn = sectno.get()[0]
if sn == '-': sn = 
tx.delete(1.0, END)
tx.insert(1.0, self.getman(ent.get(), sn))

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

def Drawface(self):
global ent, tx, sectno

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

win = Tk()
win.title('Просмотрщик man-страниц')

Так мы создаём объект, который будет являться нашим рабочим окном. Заодно задаём ему заголовок.

sectno = StringVar()
sectno.set('---Auto---')

В будущем нам понадобится эта переменная – она должна представлять собой особый объект, поэтому и создаём её как экземпляр класса Tkinter.StringVar().

fcmd = Frame(win)
fcmd.pack(side=TOP, fill=X)

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

Здесь нужно сказать, что pack – не единственный метод упаковки в Tkinter. В ряде случаев удобнее использовать упаковку «по сетке» – grid (подробности можно узнать в документации). Приступаем к созданию графических элементов:

lbl1 = Label(fcmd, text='Read about ')
lbl1.pack(side=LEFT)

Это – обычная текстовая метка. Она размещается во фрейме (первый аргумент метода) и упакована по левому краю.

ent = Entry(fcmd)
ent.bind('<Return>', (lambda event: self.pressBtn()))
ent.pack(side=LEFT)

Текстовое поле ввода также упаковываем слева, сразу после lbl1.

Метод bind() задаёт реакцию поля на нажатие клавиши [Enter] (a.k.a. Return) – будет вызван метод pressBtn().

lbl2 = Label(fcmd, text=' in section ')
lbl2.pack(side=LEFT)
slt = OptionMenu(fcmd, sectno,
'---Auto---',
'1 User Utilities',
'2 System Calls',
'3 Library Functions',
'4 Devices & Kernel Interfaces',
'5 File Formats',
'6 Games',
'7 Macros & SQL Commands',
'8 System Utilities',
'9 X Window',
'n Built-In commands',
)

slt.pack(side=LEFT)

Ещё одна текстовая метка, и далее – выпадающий список. В нём мы перечисляем имеющиеся разделы справки, к которым будет относиться наш запрос.

btn1 = Button(fcmd, command=win.quit, text='Quit')
btn1.pack(side=RIGHT)
btn2 = Button(fcmd, command=self.pressBtn, text='Open')
btn2.pack(side=RIGHT)

Две кнопки – завершающая работу и отображающая запрошенную man-страницу. Обратите внимание, что здесь упаковка выполняется по правому краю, т.е. Quit будет самой правой, а Open – чуть левее. Важнейший параметр – command, задает функцию-обработчик. В первом случае используется встроенный метод quit, завершающий работу, во втором – наш метод pressBtn. Обратите внимание, что здесь происходит не вызов метода, а даётся ссылка на него, т.е. имя указывается без скобок.

fview = Frame(win)
fview.pack(side=BOTTOM, fill=BOTH, expand=YES)
sb = Scrollbar(fview)
tx = Text(fview, relief=SUNKEN)
sb.config(command=tx.yview)
tx.config(yscrollcommand=sb.set)
sb.pack(side=RIGHT, fill=Y)
tx.pack(side=TOP, expand=YES, fill=BOTH)

Во втором фрейме, который мы прикрепляем к нижнему краю и растягиваем во все стороны, заполняя всё доступное пространство, размещается текстовое поле, где мы будем отображать содержимое man-страницы. Поскольку последняя может быть достаточно длинной, понадобится скроллинг (элемент Scrollbar). Обратите внимание на то, как методами config() мы обеспечиваем взаимную привязку текстового поля и полосы прокрутки. Параметр relief в описании текстового поля задаёт вид рамки вокруг поля.

Настало время поговорить о pressBtn. Как вы видели, этот метод будет выполняться в двух случаях – по щелчку на кнопке Open и при нажатии [Enter] в поле ввода. Получив цифру раздела справки и очистив текущее содержимое поля tx от начала (1,0) до конца (END), вызываем метод getman() и вставляем результат в tx.

win.mainloop()

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

if __name__ == '__main__':
test = ManReader()
test.Drawface()

Ну, это должно быть понятно – если скрипт запускается автономно, а не экспортируется в другой, то создаём объект нашего класса и запускаем его в работу.

Фух… Кажется, мы сделали это. Результат наших трудов можно наблюдать на рисунке (Рисунок 1). Как видите, «виджеты» имеют свой уникальный дизайн и будут резко выделяться на фоне остального интерфейса, внешний вид которого вы с такой любовью выбирали среди десятков различных стилей. Но это работает (причём одинаково) и в KDE, и в Gnome, и даже в Windows и Mac OS X.

Теперь, когда мы разобрались с основами, самое время перейти к «родным» для Linux-окружения средствам – PyQt и PyGTK. Прежде чем приступать к работе, проверьте, есть ли в вашей системе нужные модули.

Пара слов про PyGTK

Мощь библиотеки GTK позволяет создавать таких «гигантов графики», как Gimp. Отрадно, что мы можем в значительной степени воспользоваться её возможностями и в сценариях Python. В каталоге /usr/share/doc/pygtk<версия>/examples вы найдёте массу примеров её использования. Здесь приведём простейший вариант:

#!/usr/bin/python
# -*- coding: utf-8 -*-
 
import gtk
 
# Создаём основное окно
win = gtk.Window()
def main():
 
	# Задаём параметры окна (размер, заголовок и т.д.)
	win.set_default_size(300, 50)
	win.set_border_width(10)
	win.connect('destroy', gtk.main_quit)
	win.set_title('Небольшой пример') 
	txtvar = 'Библиотека ''GTK ''привносит в ''Python ''небывалую мощь и высокий уровень интеграции в среду Gnome.' 
 
	# Текстовая метка – не забывайте применять метод show() к каждому объекту, чтобы он был видимым
	txt = gtk.Label(txtvar)
	txt.show()
	# Кнопка. На неё «привязываем» завершение работы
	btn = gtk.Button('Закрыть')
	btn.connect('pressed', lambda button: gtk.main_quit())
	btn.show()
	# объект Window может содержать только один элемент, так что им будет HBox, 
	# который служит контейнером для остальных
	box = gtk.HBox()
	box.pack_start(txt)
	box.pack_start(btn)
	win.add(box)
	box.show()
	win.show()
 
	gtk.main()
main()

Результат представлен на рисунке 2.

(thumbnail)
Рисунок 2. Именно так вы и должны представлять себе GTK- приложение.

Фанатам KDE посвящается

Если для вас Linux не мыслим без KDE, то PyQt – как раз тот инструмент, который способен обеспечить «бесшовную» интеграцию ваших Python-сценариев с окружением рабочего стола. (Впрочем, Qt работает не только в Linux, так что ваши решения по-прежнему сохранят определённый уровень переносимости.) К тому же, для разработки интерфейса к вашим услугам Qt Designer. А создадим мы программу для просмотра запущенных в системе процессов. Итак, в путь!

(thumbnail)
Рисунок 3. Мышкой влево, мышкой вправо – а зачем вам вообще клавиатура?

Делай «РАЗ»!

Первым делом, запустите Qt Designer и откройте новый проект типа Widget. Для кнопки создайте соединение с формой (Connect Signal/ Slots) и объявите новый слот, назвав его showPs. В дальнейшем это будет наш обработчик нажатия кнопки. Сохранив интерфейс (Рисунок 3) под именем psview.ui, нужно конвертировать его в Python-код:

$ pyuic psview.ui > psview.py

Теперь в psview.py описан класс, отвечающий за наш интерфейс. Если вы посмотрите на него, то увидите, что всё не намного сложнее, чем в Tkinter, так что при желании интерфейс можно создать и вручную.

Делай «ДВА»!

Далее, нам нужно довести всё это до ума. Непосредственное редактирование созданного сценария – самое плохое решение, поскольку вы лишитесь возможности корректировать интерфейс в Qt Designer, т.к. все изменения при этом будут потеряны. Поэтому правильно будет оставить psview.py в неприкосновенном виде, а для работы описать ещё один класс на базе созданного, пользуясь возможностями наследования. Просто создать экземпляр класса Form1 тоже не совсем хорошо – ведь нам нужно будет изменить поведение класса, отредактировав обработчик нажатия кнопки. Полученный код с переопределённой функциейобработчиком будет выглядеть так:

#!/usr/bin/Python
# -*- coding: utf-8 -*import os, sys
# Импортируем модуль, созданный автоматически
from psview import *
class PsForm(Form1): # наследуем от Form1
def __init__(self):
# Помните, что при наследовании инициализацию
#- родительского класса нужно делать вручную?
Form1.__init__(self)
def showPs(self):
# Формируем нужные ключи команды ps
if self.radioButton1.isChecked():
all = 'a'
else:
all = 
# Выводимые поля описываются после ключа «o»
#- Очень важно не допускать пробелов!
fields = 'o'
if self.checkBox1.isChecked():
fields += 'pid,'
if self.checkBox2.isChecked():
fields += 'user,'
if self.checkBox3.isChecked():
fields += 'stat,'
if self.checkBox4.isChecked():
fields += 'command,'
fields = fields[:-1] # последнюю запятую – долой!
filter = self.lineEdit1.text()
# Если есть фильтр, то заголовок придётся забирать
#- отдельной командой (head – чтобы не выводить лишнее)
if filter:
cmd = 'ps %sx%s | grep %s | grep -v grep' % (all, fields,
filter)
head = os.popen('ps %s | head' % fields).readline()
body = os.popen(cmd).read()
else:
cmd = 'ps %sx%s' % (all, fields)
pspipe = os.popen(cmd)
head = pspipe.readline() # первая строка
body = pspipe.read() # всё остальное
pspipe.close()
self.tl_header.setText(head)
self.te_body.setText(body)
# Создаём объект-приложение
app = QApplication(sys.argv)
form = PsForm()
app.setMainWidget(form)
form.show()
app.exec_loop()
(thumbnail)
Рисунок 4. Ну, нравится мне стиль Redmond – ничего с собой поделать не могу…

Небольшое пояснение: чтобы при прокрутке заголовок был всегда на виду, мы вынесли его в отдельную текстовую метку (tl_header). Поскольку при использовании grep заголовок теряется, придётся приложить чуточку усилий, чтобы всё-таки обеспечить его вывод (см. фрагмент «if filter – else»).

Делай «ТРИ»!

Всё! Можно запускать нашу оболочку, не забыв сделать скрипт исполняемым (Рисунок 4). Конечно, это не верх совершенства – из множества доступных полей поддерживаются только четыре, нет проверки выражения фильтра на безопасность (а что, если пользователь введёт «root; rm -Rf /»?). Впрочем, это беда любого графического интерфейса – неизбежная потеря функциональности и гибкости в угоду сомнительным удобствам. Тем не менее, наша задача была всё-таки в другом – показать пример разработки ГИП, с чем мы успешно справились.

Как видите, изложенное сегодня – всего лишь основа. Но, надеюсь, этого будет достаточно, чтобы приступить к быстрой и эффективной разработке графических интерфейсов. На этом мы завершаем «Уроки Python», но не прощаемся – со следующего номера начинается серия «Python для профессионалов», где мы поговорим о многопоточных приложениях, обработке сетевых протоколов, взаимодействии с СУБД и библиотеке Python Image Library.

Персональные инструменты
купить
подписаться
Яндекс.Метрика