Программирование на
Python с использованием PyQt4
Установка.
Так как PyQt - привязка библиотеки Qt к Python-у, то в первую очередь нам
требуется наличие самой Qt 4.x.
Под Linux большинство дистрибутивов включают Qt4 в стандартную
поставку, при отсутствии же можно поискать в соответствующих для данной
системы репозитариях. Альтернативный вариант: скачать
с Trolltech-а исходники и собрать библиотеку самому. В последнем
случае у меня проблем не возникало, стандартная последовательность действий:
./configure
make
sudo make install
ну и при желании в configure задать нужные ключи.
ОС Windows. Для сей операционной системы существуют варианты Qt4 под
разные компиляторы, но свободной лицензией (GPL) обладает только Qt для
компилятора MinGW (это реализация GNU C/C++ под Windows). Поэтому, в первую
очередь качаем
и ставим MinGW, в системной переменной PATH прописываем путь до
директории MinGW/bin. Далее можно с Trolltech-а скачать
либо исходники последней версии Qt и собрать самому (под Win я этим не
занимался, поэтому здесь не советчик), либо скачать и поставить уже
откомпилированную библиотеку.
Итак, мы являемся счастливыми обладателями Qt4 и на C++ уже можем ваять, но
мы любим Python. Сие предполагает, что в нашей системе уже установлен
интерпретатор Python-а, и нам осталось только воткнуть в него привязку PyQt4.
ОС Linux. Пока что PyQt для Qt4 предустановленной видел только в
Sabayon Linux, в других линуксах (правда я их совсем не много видел) либо
вообще не находил пакетов с PyQt, либо была привязка PyQt к старой библиотеке
Qt3 (хотя сейчас ситуация может уже улучшилась). Итак, будем собирать из
исходников (фраза не для пользователей Gentoo ).
Для этого во-первых необходимо, чтобы в нашей операционной системе кроме
собственно Python-а были еще поставлены его dev-пакеты (пакеты для
разработчика). Ну или альтернативный вариант: Python также собран из
исходников
Последовательность действий:
- качаем от разработчика исходники последней версии SIP
- распаковываем
- python ./configure.py
- make
- sudo make install
- качаем от разработчика исходники последней версии PyQt4
- распаковываем
- python ./configure.py
- make
- sudo make install
ОС Windows. На данный момент мы уже имеем MinGW, Qt4, Python. Качаем
от разработчика уже собранную под win32 последнюю версию PyQt4, ставим,
радуемся жизни.
Привет
мир.
Итак, мы уже имеем рабочий вариант PyQt4 и дальнейшие наши программы не зависят
от конкретной системы, одинаково запускаясь везде, где есть PyQt. В Qt
имеется симпатичный набор примеров на C++ по использованию библиотеки, и в
PyQt те же примеры переписаны на Python-е (еще одна демонстрация лаконичности
и красоты синтаксиса Python при использовании идентичных
классов/методов/объектов). Плюс обширная документация Qt по всем классам, и
почти зеркальное ее отражение в документации по PyQt - все это существенным
образом уменьшает ценность моих здесь стараний, но я продолжу.
Самый элементарный пример, демонстрирующий создание приложения Qt и вывод
окна на экран:
Python
|
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
|
#
импортируем необходимые модули:
import sys
from PyQt4 import QtGui
if __name__=="__main__":
# создаем объект Qt-приложения, передаем его
конструктору
#
параметры командной строки:
app = QtGui.QApplication(sys.argv)
# создаем объект класса QLabel (метка), в
конструкторе задаем подпись для метки:
label = QtGui.QLabel("Hello
World!\n\tThis is the very simple example for PyQt4.")
# показываем метку:
label.show()
# запускаем цикл обработки событий, происходящих с
элементами приложения:
sys.exit(app.exec_())
|
|
После запуска программы (в консоли линукса $ python
./helloworld01.py ,в windows можно двойным кликом мыши по *.py файлу) увидим
такое окошко:
Заметим, что здесь мы не создавали отдельно окно, как контейнер для метки.
Любой графический объект Qt - это виджет (и наследуется от QWidget), и если
он не привязан к какому-либо контейнеру, то отображается в отдельном окошке.
Если мы хотим использовать русские подписи, то простейшим вариантом будет
использование юникода (префикс u перед записью строки):
Python
|
1:
2:
3:
4:
5:
6:
7:
8:
9:
|
#
-*- coding: utf-8 -*-
import sys
from PyQt4 import QtGui
if __name__=="__main__":
app = QtGui.QApplication(sys.argv)
label = QtGui.QLabel(u"Привет
мир!\n\tЭто простейший пример для PyQt4.")
label.show()
sys.exit(app.exec_())
|
|
Итак, мы можем создавать элементарное окошко и даже задавать элементарное форматирование
для надписей через escape-символы (например \n - новая строка, \t -
табуляция). Qt позволяет на нашем примере построить и более красивое
оформление через поддержку простых элементов HTML-форматирования:
Python
|
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
|
#
-*- coding: utf-8 -*-
import sys
from PyQt4 import QtGui
if __name__=="__main__":
app = QtGui.QApplication(sys.argv)
label = QtGui.QLabel(u"""<center><h1>Привет
мир!</h1></center>
<h2>Это <i>простейший
пример</i> для <font
color=red>PyQt4</font>.</h2>""")
label.show()
sys.exit(app.exec_())
|
|
и окно той же самой программы, запущенной без каких либо изменений в ОС
Windows:
(На самом деле, Qt поддерживает даже CSS для своих виджетов, но об этом
позднее)
Сигналы/слоты
- теория.
(раздел не обязателен для прочтения, но желателен, для лучшего понимания
внутреннего механизма)
Qt реализует более правильную идеологию ООП, нежели иные виденные мной
GUI-библиотеки. Что у нас в теории ООП (и даже лучше сказать, не ОО
Программирования, а ОО Проектирования) сказано про взаимодействие объектов?
Правильно, объекты посылают сообщения (сигналы), объекты принимают сообщения,
объекты выполняют операции (методы) и меняют состояние (а также испускают
новые сигналы) в зависимости от принятых сообщений. В языке C++ подобная
идеология не была реализована в полной мере (по крайней мере до появления
Boost.Signals), потому Qt представляет надстройку над синтаксисом C++. Отсюда
следует необходимость использования специального препроцессора (moc) для
Qt-программы, дабы получить компилирующийся C++ код.
Язык же Python интерпретируемый, динамически типизируемый и легко
расширяемый, что дает нам возможность
избежать "мучений" с препроцессором. PyQt предоставляет
расширение языка, позволяющее использовать механизм сигнал/слотов Qt
как "родной" для Python-а.
Итак, объект может испускать произвольное количество сигналов.
Каждый сигнал имеет свою сигнатуру: имя сигнала и типы передаваемых этим
сообщением (сигналом) данных.
Объект может принимать произвольное количество сообщений от других объектов.
Механизм принятия сообщения реализуется через соединение (connect).
В соединении участвуют:
- объект, отсылающий сигнал;
- сигнал объекта-отправителя;
- объект, принимающий сигнал;
- метод объекта-приемника (слот), выполняемый при получении сигнала.
Сигнатура (порядок и типы) входных аргументов слота должна совпадать с
сигнатурой передаваемых сигналом данных.
- Вместо слота последним участником соединения может быть сигнал
объекта-приемника, который приемник испускает при получении сигнала от
отправителя, сигнатура параметров сигналов естественно также должна
совпадать.
С одним сигналом может быть соединено несколько слотов и/или сигналов.
Один слот может быть соединен с несколькими сигналами.
Одно и то же соединение можно создать в нескольких экземплярах, тогда один
сигнал будет инициировать соответственно многократное выполнение слота либо
многократную генерацию нового сигнала.
Все классы Qt являются наследниками класса QObject. Класс QObject в PyQt
имеет метод connect, который и осуществляет соединение сигнал-слот или
сигнал-сигнал. Также класс QObject имеет метод emit, генерирующий сигнал от
объекта данного класса.
Пример:
Python
|
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
|
#
-*- coding: utf-8 -*-
from PyQt4.QtCore import QObject, SIGNAL
class A:
# метод, который будем использовать в качестве слота:
def main_slot(self,text):
print "Объектом
класса A получено сообщение с текстом:",text
class B(QObject):
def send_signal(self,text):
print "Из
объекта класса B отправляется сообщение с текстом:",text
self.emit( SIGNAL("main_signal(PyQt_PyObject)"), text )
if __name__=="__main__":
a = A()
b = B()
# а вот, собственно, и
соединение (Источник,Сигнал,СлотПриемника):
QObject.connect( b, SIGNAL("main_signal(PyQt_PyObject)"), a.main_slot )
# и теперь пошлем сигнал:
b.send_signal("Траляля")
|
|
и выполнение:
Цитата
|
$ python ./signals01.py
Из объекта класса B отправляется сообщение с текстом: Траляля
Объектом класса A получено сообщение с текстом: Траляля
$
|
Примечание: в Python-е любые функции (а соответственно, и методы) являются
полноценными объектами 1-го класса (в Python-е вообще всё является объектом),
поэтому неимеет смысла в соединении указывать отдельно объект-приемник и
отдельно его слот (т.к. слот также является объектом и имеет ссылку на
объект, для которого он выступает методом). Отсюда отличие синтаксиса
соединения Python/PyQt от C++/Qt:
(Источник,Сигнал,СлотПриемника) для Python-а и
(Источник,Сигнал,Приемник,Слот) для C++.
С тем же свойством Python-овских функций связано еще одно отличие: в качестве
СлотаПриемника может выступать и простая функция, не являющаяся методом
какого-либо класса (объект-приемник и слот в одном лице ).
Для совместимости же с откомпилированными методами (слотами) классов
библиотеки Qt (которые написаны на C++ и соответственно не являются объектами
1-го класса) в PyQt оставлен и 4-х аргументный вариант соединения.
Варианты соединений между Qt-шными и Python-овскими сигналами и слотами для
случая сигнала без параметров:
Python
|
1:
2:
3:
4:
5:
6:
|
QtCore.QObject.connect( qtObj_src, QtCore.SIGNAL("qtSig()"), pyFunction_trg )
QtCore.QObject.connect( qtObj_src, QtCore.SIGNAL("qtSig()"), pyObj_trg.pyMethod )
QtCore.QObject.connect( qtObj_src, QtCore.SIGNAL("qtSig()"), qtObj_trg, QtCore.SLOT("qtSlot()") )
QtCore.QObject.connect( qtObj_src, QtCore.SIGNAL("pySig"), pyFunction_trg )
QtCore.QObject.connect( qtObj_src, QtCore.SIGNAL("pySig"), pyObj_trg.pyMethod )
QtCore.QObject.connect( qtObj_src, QtCore.SIGNAL("pySig()"), qtObj_trg, QtCore.SLOT("qtSlot()") )
|
|
Здесь:
qtObj_src - объект-источник сигнала, является экземпляром класса-потомка
класса QObject, наследование может быть как реализованное в Qt (C++), так и
как наследование Python-класса от Qt-класса;
qtSig() - сигнал источника, определенный в Qt;
pySig() - сигнал источника, определенный в Python-е;
pyFunction_trg - приемник и слот в одном лице - произвольная Python-функция;
pyObj_trg и pyMethod - приемник и его слот - произвольный Python-объект и его
метод
qtObj_trg - приемник, как и qtObj_src экземпляр потомка от
QObject; qtSlot() - его слот, определенный в Qt
Разрыв соединения достигается использованием метода disconnect с теми же
аргументами, что использовались для connect. Допишем в конце предыдущего
примера после
Python
|
1:
|
b.send_signal("Траляля")
|
|
следующие строки:
Python
|
1:
2:
3:
|
QObject.disconnect( b, SIGNAL("main_signal(PyQt_PyObject)"), a.main_slot )
# а теперь сигнал будет послан, но никем не принят:
b.send_signal("Ту-ту!")
|
|
и выполним программу:
Цитата
|
$ python ./signals02.py
Из объекта класса B отправляется сообщение с текстом: Траляля
Объектом класса A получено сообщение с текстом: Траляля
Из объекта класса B отправляется сообщение с текстом: Ту-ту!
$
|
Соединение вида сигнал-сигнал объявляется таким образом:
Python
|
1:
|
QtCore.QObject.connect( qtObj_src, QtCore.SIGNAL("anySig1"), qtObj_trg, QtCore.SIGNAL("anySig2") )
|
|
где и источник, и "приемо-передатчик" - оба экземпляры
C++ или Python потомков класса QObject,
anySig1 и anySig2 - произвольные сигналы (либо определенные в Qt, либо в
Python-программе по месту использования).
Для примера построим последовательную цепь соединений вида
Python
|
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
|
#
-*- coding: utf-8 -*-
from PyQt4.QtCore import QObject, SIGNAL
class A:
def main_slot(self):
print "Объектом
класса A получен сигнал"
class B(QObject):
pass
class C(QObject):
def send_signal(self):
print "Из
объекта класса C отправляется сигнал"
self.emit(SIGNAL("signal01()"))
if __name__=="__main__":
a = A()
b = B()
c = C()
QObject.connect( c, SIGNAL("signal01()"), b, SIGNAL("signal02()") )
QObject.connect( b, SIGNAL("signal02()"), a.main_slot )
c.send_signal()
|
|
Цитата
|
$ python ./sig2sig.py
Из объекта класса C отправляется сигнал
Объектом класса A получен сигнал
$
|
Знакомым с программированием на Qt/C++ после приведенных примеров очевидно
явное отличие. В Qt/C++ необходимо было методы, являющиеся слотами, описывать
в отдельных секциях, public slots например, а если программист
неудовлетворялся набором стандартных сигналов класса-предка, от которого он
наследовался, то каждый новый сигнал описывался в секции signals .
Препроцессор же потом переводил эти объявления в пригодные для компилирования
конструкции языка C++. Следствием динамической типизации Python-а является не
только то, что мы можем динамически назначить слотом произвольно выбранный
метод/функцию или любой иной объект с методом __call__(), но и то, что
динамически можем определять для объекта новые сигналы, о которых становится
известно только на этапе выполнения connect.
Сигналы/слоты
- GUI-шная практика.
На самом деле все не так страшно, как может показаться прочитавшему
предыдущий раздел, и сейчас мы займемся вещами более наглядными
Все графические объекты библиотеки Qt имеют наборы сигналов, которые они
испускают при при наступлении некоторых внешних (например клик мыши) либо
внутренних (например таймер) событий. Кроме того, сигналы объектами
испускаются при вызове методов, изменяющих состояние объектов. В данных,
передаваемых сигналами, обычно содержится информация о произошедших
изменениях в состоянии объекта. Проиллюстрируем на примерах.
Python
|
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
#
-*- coding: utf-8 -*-
import sys
from PyQt4 import QtCore, QtGui
if __name__=="__main__":
app = QtGui.QApplication(sys.argv)
button = QtGui.QPushButton(u"Выход")
button.setFont( QtGui.QFont("Arial", 16, QtGui.QFont.Bold) )
QtCore.QObject.connect( button, QtCore.SIGNAL("clicked()"), app, QtCore.SLOT("quit()") )
button.show()
sys.exit(app.exec_())
|
|
Здесь, как видно, мы связали сигнал clicked() кнопки со слотом quit() объекта
приложения, по названиям не трудно догадаться и о значении этих сигнала и
слота. Связали не-Python-овские сигнал и слот, потому использована
четырёхаргументная запись соединения.
Другой пример. Создадим в окне два расположенных горизонтально объекта:
SpinBox и Slider (ползунок), с помощью которых мы будем выбирать свой
возраст. Создадим между ними двустороннее соединение, то есть два соединения,
при которых изменение одного объекта будет сразу автоматически изменять и
другой объект. И добавим Python-овский слот для отображения изменений в
консоли.
Python
|
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
|
# -*-
coding: utf-8 -*-
import sys
from PyQt4 import QtCore, QtGui
class AgeSelector(QtGui.QWidget):
def __init__(self,*args):
QtGui.QWidget.__init__(self,*args)
self.setWindowTitle(u"Вводим
свой возраст")
# создаем объекты:
spinbox = QtGui.QSpinBox()
slider = QtGui.QSlider(QtCore.Qt.Horizontal)
# устанавливаем
границы значений:
spinbox.setRange(0, 130)
slider.setRange(0, 130)
# создаем
соединения:
self.connect(spinbox, QtCore.SIGNAL("valueChanged(int)"), \
slider, QtCore.SLOT("setValue(int)"))
self.connect(slider, QtCore.SIGNAL("valueChanged(int)"), \
spinbox, QtCore.SLOT("setValue(int)"))
self.connect(spinbox, QtCore.SIGNAL("valueChanged(int)"), self.log_to_console)
# задаем начальное
значение:
spinbox.setValue(27)
# создаем
горизонтальное размещение объектов в окне:
layout = QtGui.QHBoxLayout()
layout.addWidget(spinbox)
layout.addWidget(slider)
self.setLayout(layout)
# слот, пишущий лог изменений в консоль:
def log_to_console(self,i):
print i
if __name__=="__main__":
app = QtGui.QApplication(sys.argv)
age_sel = AgeSelector()
age_sel.show()
sys.exit(app.exec_())
|
|
Имеем окно приложения:
а при передвижении ползунка со значения 27 до значения 35 в консоли получим
ряд:
Цитата
|
27
28
29
31
32
33
34
35
|
Коль скоро мы затронули взаиморасположение объектов в окне, то следующий
раздел этому и посвятим:
Геометрия/размещения
Это сообщение отредактировал(а) Artemios - 20.6.2007,
02:56
--------------------
fib
= 1: 1: [ x+y | (x,y) <- zip fib (tail fib) ]
|