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, QtGuifrom designed_ui import Ui_DesignedWidgetclass InheritedWidget(QtGui.QWidget, Ui_DesignedWidget):    def __init__(self, parent=None):        QtGui.QWidget.__init__(self, parent)        self.setupUi(self)

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

from PyQt4 import QtCore, QtGuifrom designed_ui import Ui_DesignedWidgetclass 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, uicclass 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.

Подписаться Комментировать

Комментарии

8.05.2008 1:05 magik
Спасибо, очень интересная статья, особенно порадовал итог )) У меня меганубский вопрос :) Если я хочу написать мультиплатформенное приложение то какой UI-тулкит лучше всего использовать. Так чтобы пользователь совершал минимум дополнительных действий (правильно я понимаю что при использовании PyQt, например, на виндоуз, надо устанавливать Qt библиотеки). Да и ссылка с RSS на статью битая.
8.05.2008 7:50 Vooon
Сейчас без дополнительных телодвижений есть только Tk, который в будущем будет заменен на wxPython (wxWidgets). Для PyQt4 есть инсталлер для виндовс, который включает в себя Qt4. Для линукса как правило достаточно попросить менеджер пакетов поставить python-qt4 и он сам позаботится о зависимостях.
8.05.2008 8:58 Юревич Юрий
@magik С wxPython и PyQt хорошо работает py2exe (c GTK похуже), так что можно сделать, чтобы пользователюне нужно было совершать лишних телодвижений на Windows.
8.05.2008 16:31 magik
2Vooon 2Юревич Юрий Спасибо за ответы, я примерно что то такое и предполагал ) Остановлюсь наверное всё таки на wxWidgets
17.05.2008 17:17 x
Tk, который в будущем будет заменен на wxPython (wxWidgets) Откуда это ?

Форма комментирования для «PyQt: unpythonic GUI»

Обязательное поле. Не больше 30 символов.

Обязательное поле

Пожалуйста, введите символы, которые вы видите на изображении

19.05.2008 11:57 koder
> Сейчас без дополнительных телодвижений есть только Tk, > который в будущем будет заменен на wxPython (wxWidgets). Это не так )), никуда tk не денут, к нему сейчас ttk прикручивают, что-бы он выглядел прилично. А уж wx’а точно не будет в stdlib — никто такого монстра не будет таскать с питоном, даже разговоров таких нет. > Спасибо за ответы, я примерно что то такое и > предполагал ) Остановлюсь наверное всё таки на > wxWidgets Я бы все-же советовал использовать Qt, если не смущает GPL лицензия (впрочем GUI можно и нужно отделять от кода, так что под GPL может идти только GUI). По моему опыту работы он 1) Заметно менее глючен и предсказуем (собсвенно с PyQt3/4 проблем у меня вообще не было/я их не помню, хоты давно использую) 2) Быстрее 3) Приятнее выглядит и более аккуратен по дизайну (css для управления внешним видом элементов, например) 4) Вышел 4.4 с интеграцией с webkit, аппартным ускорение, svg графикой и многими другими приятными вещами. 5) Отличная документация — IMHO абсолютный фаворит в этом плане перед всеми другими тулкитами. Для сравнения просто поставьте Qt4 + PyQt и посмотрите на него )).
20.05.2008 3:36 Юревич Юрий
4) Вышел 4.4 с интеграцией с webkit, аппартным ускорение, svg графикой и многими другими приятными вещами.
Опа :)
29.05.2008 12:56 Захари
I think the title of the article is a bit misleading, and someone would think that PyQt authors avoided this issue. In fact the PyQt author(s) tried to make the library pythonic as much as possible. For example, as you wrote in the article, you don’t need to pass QStrings to Qt methods as in the C++ implementation, but you may pass a python string instead. The only unpythonic thing in the library that has bothered me aesthetically is the PyQt naming convention, which follows the Qt one. That is probably unavoidable though. PyQt works really well with py2exe under Windows as stated above. It works well with it’s Mac OS X equivalent — py2app as well. At SpiderOak we are successfully using them to produce a product, which in terms of usability is no different than a C++ Qt application.
2.06.2008 17:23 Юревич Юрий
Захари, thanks for comment. Title should be ‘All GUIs are unpythonic, but PyQt less’ ;)
8.08.2008 21:25 Vadim Fint
> впрочем GUI можно и нужно отделять от кода, так что под > GPL может идти только GUI). так прокатит только если бизнес-логика либо под GPL-совместимой либо под copyleft (а-ля bsd).
9.08.2008 6:56 Юревич Юрий
так прокатит только если бизнес-логика либо под GPL-совместимой либо под copyleft (а-ля bsd).
Можно придумать техническое решение, которое бы позволило обойти этот нюанс (ну, например, отдельный слой логики между GUI и собственно бизнес-логикой, этот отдельный слой лицензируется под BSD), но по мне проще решить административно: использовать для разработки GPL-версию PyQt, а для “сборки” купить одну-две проприетарные лицензии на PyQt.
29.08.2008 11:13 qewerty
> Title should be ‘All GUIs are unpythonic, but PyQt less’ ;) Which all GUIs? PyGTK is more pythonic. It can receive Python strings as arguments and returns Python strings. New signals may be created at runtime in Python code using Python types. And gtk.TreeModel also can use Python types as types of columns, etc.
22.03.2009 18:20 enp

Подгружать .ui на лету — это действительно прикольная идея, но она странно работает. Имею такой код:

<pre><code>сlass QStorageWindow(QtGui.QWidget): def __init__(self, parent=None): QtGui.QWidget.__init__(self, parent) uic.loadUi(‘qstorage.ui’, self) def on_buttonMount_clicked(self): print “mounting storage …” </code></pre>

И при нажатии на конпку с именем buttonMount я получаю 2 срабатывания метода on_buttonMount_clicked. При явном connect() я вообще 3 срабатывания получу, поэтому, видимо, и здесь есть что-то лишнее … Что?

22.03.2009 18:47 enp

Это штатная фича, оказывается, судя по документации, а ее обход ничем не лучше явного connect() :(

2.04.2010 1:23 g_nikolai

Метод с именем on_buttonMount_clicked будет автоматически связан с сигналом clicked кнопки buttonMount, так как он соответсвует правилам именования методов для автоконнекта. А два раза он сработает потому, что иногда сигналы с одним и тем же именем бывают с разным составом и типом аргументов. Например, при выборе нового значения QComboBox будут сгенерированы 2 сигнала — один передаст индекс строки, второй — текст. В этом случае для фильтрации сигналов перед определением метода нужно поставить декоратор:@QtCore.pyqtSignature(‘int’) или (вроде бы)@QtCore.pyqtSlot(int)Это если мы хотим, чтобы принимались сигналы только с индексом

1.12.2009 9:30 Крайст

Жаль, такие замечательные статьи — лишь обрывочные данные для тех, кто не особо шарит в программировании чего-то большего, чем скрипты, но очень хочет этого большего.Отличная статья, познавательная и вполне самодостаточная.

Вот бы собрать комьюнити, попереводить все возможные маны по PyQT. Если бы я понимал — сам бы взялся. Ещё и видео бы и интерактивный курс замутил бы. Да вот, мозги не с той стороны начали Python изучать.

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

В любом случае, Автор, спасибо! Я кинул в закладки эту статьи и ещё не раз к ней вернусь. Не раз — это комплимент, а вовсе не обидное “я ничо не понял” )