Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 128, АВГУСТ 2009 г.

GUI Python’у! Вкуриваем в кодинг графических интерфейсов на питоне

Вадим Шпаковский (shpak.vadim@gmail.com)

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

GUI в Python'e

Практически любой серьезный язык программирования имеет средства для написания GUI. Python – не исключение. Но разработка GUI на нем имеет два преимущества. Во-первых, это высокая скорость разработки, что и следовало ожидать от такого языка как Python. Во-вторых, – огромное количество GUI фреймворков. Существуют расширения для почти всех основных GUI-библиотек: Tkinter на основе Tcl/Tk, wxPython для wxWidgets, PyQt для Qt и многое другое (с полным списком можно познакомиться, пройдя по ссылке на боковом выносе). Tkinter поставляется вместе с Python, и работу с GUI можно начинать сразу после его установки.

В этой статье будет рассматриваться библиотека wxPython. Она является оберткой над популярной кроссплатформенной GUI-библиотекой wxWindows.

Выбираем IDE

По ссылке на боковом выносе можно выбрать подходящий IDE, заточенный для работы с GUI. Выбор весьма богат, так что, скорее всего, ты не разочаруешься. К примеру, для работы именно с wxPython мне приглянулся BoaConstructor. Он удобен для визуального проектирования GUI: избавляет от рутины и, в целом, имеет интуитивно понятный интерфейс (что не отменяет чтения туториала). Какой IDE выбрать – это дело вкуса, поэтому я больше не буду акцентировать на нем внимание. Дальнейшее чтение статьи должно быть понятным независимо от сделанного выбора. Так что устанавливай wxPython и приступим непосредственно к кодингу.

Hello, world!

Немного перефразирую известную фразу: лучший способ изучить работу с GUI - это сразу начать писать GUI! Не откладывая дело в долгий ящик, сразу продемонстрирую программу «Hello, world!» на wxPython'e.

import wx

class HelloFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, id=-1, parent=None,
pos=wx.Point(422, 270), size=
wx.Size(300, 200), title='Hello Frame')
self.panel = wx.Panel(self)
self.helloButton = wx.Button(id=-1, label=
'Push me.',parent=self.panel,
pos=wx.Point(110, 75), size=wx.Size(80, 30))
self.panel.Bind(wx.EVT_BUTTON,
self.OnButtonClick, self.helloButton)

def OnButtonClick(self, event):
print 'Hello, world!'

class HelloApp(wx.App):
def OnInit(self):
frame = HelloFrame()
frame.Show(True)
return True

if __name__ == '__main__':
app = HelloApp()
app.MainLoop()

При запуске программы создается окно с кнопкой, при нажатии на которую выводится строка «Hello, world!».

Что происходит «за кулисами»?

Теперь немного теории. Любая wxPython программа должна содержать два объекта: прикладной объект и главное окно. Прикладной объект управляет главным циклом обработки событий, а окно содержит элементы интерфейса, посредством которых пользователь может управлять данными. Разберем жизненный цикл нашей программы:

1) app = wx.PySimpleApp() – создается прикладной объект, который должен быть объектом класса wx.App или производного от него. Пока он не создан, невозможно создать никакие другие графические объекты wxPython.

2) Вызов метода OnInit() для созданного прикладного объекта. Обычно здесь выполняется начальная инициализация виджетов. Если он возвратит False, то приложение тут же завершится.

3) frame = HelloFrame() - создание объекта главного окна (должен быть объектом класса wx.Frame или производного от него). Причем, необязательно, чтобы создание этого объекта происходило в методе OnInit() прикладного объекта – лишь бы главное окно создалось не раньше, чем прикладной объект! Приложение может содержать несколько окон верхнего уровня (не имеющих родителя) и только одно из них является главным. Главное окно можно объявить явно (вызвав метод SetTopWindow()) либо неявно (главным считается фрейм верхнего уровня, который создан первым).

4) app.MainLoop() - вызов главного цикла обработки событий. Этот цикл отвечает на события, посылая их соответствующим обработчикам (к событиям я подробнее вернусь чуть позже). Когда все окна верхнего уровня закрываются, то происходит возврат из метода MainLoop() и приложение завершает работу. Так же как метод OnInit() вызывается сразу после создания прикладного объекта, метод OnExit() у этого объекта вызывается после закрытия последнего окна, но перед внутренней очисткой wxPython. Его можно использовать для очистки не-wxPython ресурсов. Если по каким-то причинам приложение должно жить даже после закрытия всех окон, можно изменить поведение прикладного объекта, вызвав у него метод SetExitOnFrameDelete(False). После этого приложение будет жить до тех пор, пока явно не вызовет глобальную функцию wx.Exit().
Надеюсь, ты получил общее представление о том, что происходит «за кулисами». Можно перейти к созданию виджетов, которым будет посвящена оставшаяся часть статьи.

Виджеты

Рассмотрим создание окна (фрейма):

wx.Frame(parent, id=-1, title="",
pos=wx.DefaultPosition, size=wx.DefaultSize,
style=wx.DEFAULT_FRAME_STYLE, name="frame")

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

  1. Самому генерировать уникальное положительное число и передавать его в конструктор.
  2. Использовать функцию wx.NewId().
  3. Передать в конструктор виджета константу wx.ID_ANY или -1 (что и сделано в примере).

Между вторым и третьим стилями нет никакого функционального различия. При создании фрейма, в том числе, происходит создание виджетов, которые он должен содержать. Хорошим ходом считается создание виджета wx.Panel такого же размера, как рабочая область окна, и который, по существу, является простым контейнером для других объектов. Это позволяет отделить содержание окна от других элементов, типа панели инструментов и строки состояния. Затем в примере создается виджет кнопки. Список параметров аналогичен таковому у конструктора фрейма. Обрати внимание, что родителем является объект панели. Здесь нужно, чтобы размеры внутреннего виджета не вылезали за рамки виджета-родителя (параметры pos и size). После создания виджета доступ ко всем его параметрам осуществляется через Get/Set методы (такой подход не свойственен Питону, но тут просматривается влияние C++, так как wxPython - это оболочка над библиотекой wxWindows, которая написана как раз на C++).

События

Как уже упоминалось, главная задача прикладного объекта – обработка событий, которые происходят во время работы приложения. События (events) в wxPython'e - это очень обширная тема, поэтому я рассмотрю только азы. После того как у прикладного объекта вызван метод MainLoop(), программа большую часть времени по сути ничего не делает. Все события, которые происходят при работе программы, помещаются в очередь. Время от времени программа проверяет эту очередь. Если в ней что-то появилось, она обрабатывает это событие, вызывая программный код, связанный с ним.

Любое событие является потомком wx.Event и имеет свой тип. Например, событие wx.MouseEvent имеет 14 типов, таких как wx.EVT_RIGHT_DOWN, wx.EVT_LEFT_UP и т.п.

В wxPython, большинство виджетов генерирует высокоуровневые события в ответ на события более низкого уровня. Например, щелчок мыши на кнопке wx.Button генерирует событие wx.CommandEvent типа EVT_BUTTON. Преимущество такого подхода – в том, что он позволяет сосредоточиться на самих событиях, вместо того, чтобы отслеживать каждый щелчок мыши.

Чтобы связать событие, виджет, который его вызвал, и обработчик события, используется так называемый биндер - объект класса wx.PyEventBinder. Для каждого типа события существует свой биндер. Объекты биндеров предопределены и глобальны, но есть возможность создать собственный биндер для собственного типа события.

Любой виджет является потомком класса wx.EvtHandler, а значит, имеет метод Bind. Он-то как раз и создает биндеры событий, про которые только что шла речь. Этот метод имеет следующую сигнатуру: Bind(event, handler, source=None, id=wx.ID_ANY, id2=wx.ID_ANY). Первые два параметра обязательны. Event - объект класса wx.PyEventBinder, и он описывает событие; handler - объект, поддерживающий вызов, обычно это метод или функция с единственным параметром – объектом событием. Параметр source задает виджет, который является источником события (его следует задавать, если этот виджет не тот, который используется как обработчик).

Рассмотрим, как это все работает в нашей программе. В ней есть такая строчка: self.panel.Bind(wx.EVT_BUTTON, self.OnButtonClick, self.helloButton).
В этой строчке объект panel создает биндер, при помощи которого, при нажатии кнопки helloButton, произойдет вызов метода OnButtonClick(self, event).

Хочу обратить внимание, что код, который вызывается в ответ на событие, обычно не определяется виджетом, вызвавшим это событие (так называемая распределенная архитектура). В нашем случае событие вызывается виджетом helloButton, но обработчик этого события является методом OnButtonClick() объекта Frame, который и содержит виджет.

Стоит упомянуть еще об одной возможности обработки событий: если в обработчике вызван метод Skip(), это позволяет продолжить поиск и выполнение других обработчиков того же самого события (иначе будет выполнен только тот обработчик, который был найден первым). Иногда такое поведение необходимо.

Пишем свой калькулятор

Возможно, тебе покажется, что для такой простой программки, как «Hello, world!», тут слишком много теории. Может быть. Но зато теперь ты готов писать GUI практически любой сложности. Основной алгоритм написания большинства GUI:

  1. Создание прикладного объекта и фрейма.
  2. Создание и размещение виджетов в фрейме.
  3. Размещение всей логики работы GUI в обработчиках событий.

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

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

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

1. Рекомендуется давать объектам виджетов «говорящие» имена, чтобы по названию сразу можно было понять, для чего они предназначены (например, кнопку умножения есть смысл назвать buttonMul вместо button_12).

2. То же самое касается именования обработчиков событий. Их название принято начинать с префикса «On». За ним идет название виджета, с которым этот обработчик связан, а за тем – название события, которое обрабатывает обработчик (например, из названия обработчика OnButtonEraseClick ясно, что он обрабатывает событие, возникающее при «клике» на кнопку buttonErase). Возможно, это провоцирует появление длинных имен, но я рекомендую делать выбор в пользу понятности, пусть и в ущерб краткости.

3. Часто есть возможность уменьшить количество обработчиков путем назначения одного и того же обработчика на разные события. В нашем случае вместо того, чтобы на каждый «клик» на кнопке с цифрой назначать отдельный обработчик (который всего лишь должен эту цифру дописать в окно вывода), можно обойтись одним единственным обработчиком. Правда, перед тем как дописать цифру, этому обработчику вначале придется найти ту кнопку, которая вызвала это событие, так как эта цифра расположена на label'e этой кнопки. Что делается следующим образом:

# Получаем список всех виджетов, которые содержит
# panel.
children = self.panel.GetChildren()
# Находим виджет, который вызвал событие.
for child in children:
if child.GetId() == event.GetId():
# Знак, который нужно вывести, содержится
# в label'e виждета.
self.textCtrlInfo.AppendText(child.GetLabel())

Тут быстродействие приносится в жертву ради большей «прозрачности» кода. Но именно такой подход и соответствует идеологии Python'a, поэтому настоятельно рекомендую придерживаться его и впредь. Кроме того, на нажатие кнопок арифметических операций и кнопки вывода результата («=») так же можно назначить один и тот же обработчик (в примере это метод OnOperationClick). Правда, при разборе этого метода могут возникнуть некоторые трудности для тех, кто слабо разбирается в Python'e. Но это лишь из-за не совсем тривиальной логики, по которой должен работать калькулятор (например, если было введено 2+3, то ввод следующей операции подразумевает вычисление этого выражения 2+3=5 и применения новой операции к результату). Даже если ты не до конца разберешься с логикой, – ничего страшного, ведь наша главная задача это GUI.
Есть несколько нюансов, о которых начинающие разработчики часто забывают.

1. Важно помнить, что пользователю может прийти в голову мысль изменить размер твоего приложения (например, развернуть его на весь экран). Обычно после этого приложение приобретает неприглядный вид - все виджеты будут расположены в рамках старого размера, а оставшаяся часть останется пустой. Самый простой вариант избежать этой неприятности - запретить изменения размера окна и его максимизацию. Это можно сделать при инициализации фрейма, инициализировав параметр style значением wx.DEFAULT_FRAME_STYLE & (~(wx.MAXIMIZE_BOX | wx.RESIZE_BORDER).

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

try:
number = float(self.textCtrlInfo.GetValue())
except (TypeError, ValueError):
self.errorStatusBar.SetStatusText('ОШИБКА! '
'Введите число правильно.')
return

Этот код пытается привести строку к типу float; если не удается, то бросается исключение, которое ловится и в виджет errorStatusBar выводится сообщение об ошибке. Обрати внимание, что ловятся ошибки определенного типа (TypeError, ValueError). Все остальные ошибки ввода уже не будут выводиться в виджет errorStatusBar. Это считается хорошим стилем - ловить только те ошибки, которые ты готов обработать.

3. По умолчанию, длина входной строки не ограничена, и при вводе большого числа, оно не будет влезать в поле ввода. Чтобы этого избежать, можно указать максимальную длину вводимой строки при создании виджета: textCtrlInfo.SetMaxLength(30).

Заключение

Надеюсь, теперь тебе не составит труда продолжить изучение wxPython самостоятельно. На примере двух простых программок был рассмотрен фундамент любого wxPython-приложения. А если есть фундамент, – возвести фасад труда не составит. В процессе дальнейшего изучения wxPython я настоятельно рекомендую обратить внимание на wxPython Demo, который поставляется вместе с самой библиотекой. Это большая сборка различных примеров с исходным кодом. Если захочется изучить новый виджет - в первую очередь ищи там. Также есть замечательная книга «WxPython in action» авторов Noel Rappin и Robin Dunn. Ссылку для скачивания можно найти на боковом выносе. Написана она на английском, но если погуглить, то можно найти русский перевод нескольких глав (я нашел главы 1,2,3,11,14). Для начального знакомства с wxPython вполне хватит первых трех. Я вскрыл только верхушку айсберга программирования GUI на Python'e, все остальное остается за тобой. Удачи!

CD

На диске лежат полные исходные коды калькулятора. Для его запуска надо установить wxPython.

WWW

Содержание
ttfb: 7.3490142822266 ms