Fork me on GitHub
7/5/2008

PyQt: unpythonic GUI

Если посмотреть на моих знакомых питонистов, то большинство из них занимаются web-разработкой. Это вполне себе показатель - Python заслуженно пользуется популярностью у web-разработчиков. Хочу немного разбавить восторги от Django и немногочисленные голоса о web.py или иных web-инструмента рассказом о том, как на Python создавать GUI-приложения.

Для Python есть биндинги к трем основным кроссплатформенным UI-тулкитам: PyGTK (GTK+), wxPython (wxWidgets) и PyQt (Qt). Я имел некий опыт с wxPython, пробовал PyGTK, для меня темной лошадкой оставался PyQt. Пока актуальной была версия 3, стоп-фактором были нелюбимые мной from pyqt import * и неопределенный статус Qt3 для Windows (несмотря на то, что я использую Linux, большинство наших клиентов работают под Windows). PyQt4 в этом плане стал интересной альтернативой. Я вспомнил про него, когда возникла задачка мелкого интерфейса для поиска по БД.

PyQt весьма неплох. Весьма. У него самая большая проблема - он абсолютно не вписывается в питон-стиль. Но это общая проблема: из майнстримовых UI-тулкитов ни один не обладает тем, что называется pythonic style.

Python или Qt?

Первая проблема, с которой сталкиваются разработчики, начинающие писать на PyQt: какие инструменты использовать, стандартные для Python, или стандартные для Qt? Я придерживаюсь Python-идиом, т.е. использую Qt исключительно как GUI.

Для того, чтобы вы могли прикинуть по своему вкусу, пара примеров.

Первый пример - со строками. В Qt используется QString, в Python - unicode-строки. PyQt сделан так, что в Qt-функции, ожидающие QString, можно передавать unicode-строки, но возвращаются всегда QString.

>>> from PyQt4 import QtCore
>>> unicode_string = u'some text, немного на русском'
>>> q_string = QtCore.QString(unicode_string)

# размер строки
>>> len(unicode_string)
<<< 29
>>> q_string.length()
<<< 29

# выравнивание по правому краю
>>> unicode_string.rjust(50)
<<< u'                     some text, \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u043d\u0430 \u0440\u0443\u0441\u0441\u043a\u043e\u043c'
>>> print _
                     some text, немного на русском
>>> q_string.rightJustified(50)
<<< <PyQt4.QtCore.QString object at 0xb7832bac>
>>> print unicode(_)
                     some text, немного на русском

Еще один пример - доступ к директориям/файлам:

>>> import os
>>> from PyQt4 import QtCore

## абсолютный путь к текущей директории
# pythonic
>>> os.path.abspath(os.path.curdir)
<<< '/home/j2a'
# Qt-ish
>>> QtCore.QDir().absolutePath()
<<< <PyQt4.QtCore.QString object at 0xb71d2d2c>
>>> print unicode(_)
/home/j2a

## запись текстовых данных в файл
# pythonic
>>> py_file = file('/tmp/flpy.txt', 'w')
>>> py_file.write('test')
>>> py_file.close()
# Qt-ish
>>> q_file = QtCore.QFile('/tmp/flq.txt')
>>> q_file.open(QtCore.QIODevice.WriteOnly | QtCore.QIODevice.Text)
<<< True
>>> q_file.write('test')
<<< 4L
>>> q_file.close()

## чтение из файла
# pythonic
>>> py_file = file('/tmp/flpy.txt')
>>> py_file.readlines()
['test']
>>> py_file.close()
# Qt-ish
>>> q_file = QtCore.QFile('/tmp/flq.txt')
>>> q_file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text)
<<< True
>>> q_file.readAll('test')
<<< <PyQt4.QtCore.QByteArray object at 0xb77f2a6c>
>>> unicode(QtCore.QString(_))
<<< u'test'
>>> q_file.close()

Единственно, где я предпочитаю Qt-way, это потоки (threads). Причина проста: чаще всего в GUI потоки нужны, чтобы при выполнении каких-то "долгоиграющих" операций интерфейс не "замораживался". И в этом плане, работа QtGui на пару с QtCore.QThread приносит меньше неожиданностей, чем QtGui + threading.Thread. Хотя всякое бывает ;)

Как делать GUI?

В PyQt есть два подхода создания GUI: руками и дизайнером. Тут сложно сказать, какой подход работает лучше всего. Если делать руками, то высока вероятность получить "спагетти", где код логики не отделен от кода представления. Однако для того чтобы эффективно использовать дизайнер, нужно не только изучить концепции Qt, но и переложить эти концепции на QtDesigner. Без этого получится интерфейс в лучших традициях "программистов на Delphi".

В общем, имея негативный опыт с разбором чужой "лапши", я решил попробовать в деле QtDesigner. Однако и здесь нет единственного решения:

  • .ui-файл можно либо подгружать динамически, либо "компилировать" в Python-код
  • от UI-класса можно наследоваться, либо иметь экземпляр UI-класса как атрибут

Поясню чуть подробнее.

QtDesigner на выходе дает описание формы в XML -- .ui файл. При помощи команды pyuic4 можно на основе .ui сгенерировать Python-код. Править его крайне не рекомендуется. Рекомендуется либо наследоваться от генеренного класса:

from PyQt4 import QtCore, QtGui
from designed_ui import Ui_DesignedWidget

class InheritedWidget(QtGui.QWidget, Ui_DesignedWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.setupUi(self)

либо представлять экземпляр этого класса как атрибут:

from PyQt4 import QtCore, QtGui
from designed_ui import Ui_DesignedWidget

class WidgetWithUiAsAttr(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.ui = Ui_DesignedWidget()
        self.ui.setupUi(self)

Мне более симпатичен последний вариант (например, тем, что все ui-атрибуты вынесены в отдельное пространство имен). Собственно такой подход я и использовал в своем приложеньице.

Однако "компилирование" .ui в .py - не обязательный шаг: есть возможность подгружать .ui на лету:

from PyQt4 import QtCore, QtGui, uic

class WidgetWithDynLoadedUiAsAttr(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        uiClass, qtBaseClass = uic.loadUiType('designed_ui.ui')
        self.ui = uiClass()
        self.ui.setupUi(self)

Это красивей, чем генерировать Py-код. Хотя бы потому, что не надо писать Make-файл ;) Но меня беспокоит вот что: на компьютере пользователя окружение наверняка не совпадает с таковым у разработчика, и какое-нибудь хитрое сочетание кастомного виджета, навороченного ui и версии PyQt даст гремучую смесь. А с генеренным Py-кодом мне спокойнее, его глазами можно посмотреть.

Итог

Если вышесказанное обобщить до одной фразы, то мой совет таков: если PyQt подходит по лицензионным соображениям - смело используйте; где в приложении можно использовать Python - используйте Python; а GUI создавайте так как вам удобней, лишь следите за качеством кода.

P.S. Код к статьям теперь в Mercurial-репозитории sandbox.

Комментарии

Все статьи