Как стать автором
Обновить

Разговариваем про PyQt4 — Посиделка первая

Python *
image

Небольшое вступление


    Собственно, тогда, давно, я решил попробовать Qt, потому что часто слышал об удобстве разработки под него и своими глазами видел, какая шикарная документация представлена на сайте производителя. Не могу сказать, что это далось легко (я раньше немного писал на GTK), особенно путался в этих бесконечных классах на "Q", но постепенно начало нравиться все больше и больше. В частности потому, что есть отличная привязка к нему для языка Python, на котором я, собственно, в основном и пишу.
    Еще почему? Ну, я мог бы рассказать и о том, что он работает как на почти всех настольных системах, так и на многих мобильных, рассказать про совершенно гениальную объектную систему виджетов и т. п. Но — зачем? Не люблю холивары с приверженцами других визуальных библиотек :) Поэтому давайте считать этот топик чем-то вроде дележки опытом и рассуждений на тему.

Ну, поехали...


    В силу особенностей современного образования, многие программисты молодого поколения (к коим себя причисляет и ваш покорный слуга) живут с вколоченными в голову Pascal'ем и Delphi. Ну а что, ведь удобно — рисуешь мышкой окошки, связываешь компоненты, прописываешь им методы — и в минимальные сроки получаешь красивое оконное приложение. Я сам довольно долго сидел на них, даже делал пару фриланс-проектов. Но в один прекрасный день что-то щелкнуло в голове — и на ноутбуке вместо сверкающей Vista поселилась коричневая Ubuntu. Не буду рассказывать, почему и как я выбрал Python, но однажды возникла потребность вылезти из черных недр консоли и написать кое-что оконное.
    Как для Qt, так и для GTK существуют свои визуальные редакторы, интерфейс которых буквально интуитивно понятен не только бывшему делфятнику, но и разработчику, решившему сунуться в десктопную разработку первый раз. Для GTK это Glade, для Qt — идущий в комплекте с SDK инструмент Qt Designer. Причем оба они существуют как в виде самостоятельных приложений, так и как плагины к небезызвестной и нежно мной любимой среде Eclipse, а еще для Qt недавно появилась очень и очень неплохая нативная IDE — Qt Creator, которую тоже включили в SDK. Единственный минус — пока что не существует вменяемого плагина, позволяющего использовать ее для разработки на Python. На выходе у обоих — файл с xml-структурой, чем-то напоминающий по своему назначению dfm-файлы Delphi. То есть их можно положить в папку с проектом и подключить несколькими строками кода — и все практически готово.
    Для пущего удобства существует пакет-посредник между Qt Designer и Python и носит он имя pyqt4-dev-tools, а внутри него лежит программка pyuic4, служащая для удобной трансляции ui-файлов «дизайнера» в чистенький и опрятненький Python-код. НО! Как обычно в этой жизни, здесь не работает старый добрый принцип «нажмешь кнопку и красиво». Более того, я очень не советую вам употреблять pyuic4 для серьезных проектов. Почему? Сейчас расскажу.
    Pyuic4 — совершенно незаменимая вещь при освоении PyQt4. Что может быть удобнее — бросил пару виджетов на форму, транслировал получившийся файл одной командой в Python-скрипт — и уже ковыряешь код, смотря, какие методы вызываются при создании виджетов, обращении к ним, создании надписей и т. д. Но pyuic4 также генерирует кучу ненужного, на мой взгляд, кода, без которого можно обойтись и сделать все удобнее и компактнее без потери читабельности и удобства. Вот пример кода, генерируемого pyuic4 для простейшей формы с двумя кнопками и полем ввода (и да, не ругайте меня за стандартные имена для виджетов, это всего лишь пример :) ):
Copy Source | Copy HTML
  1. # -*- coding: utf-8 -*-
  2.  
  3. # Form implementation generated from reading ui file '/home/username/Рабочий стол/habr.ui'
  4. #
  5. # Created: Fri Nov 13 23:52:05 2009
  6. #      by: PyQt4 UI code generator 4.6
  7. #
  8. # WARNING! All changes made in this file will be lost!
  9.  
  10. from PyQt4 import QtCore, QtGui
  11.  
  12. class Ui_MainWindow(object):
  13.     def setupUi(self, MainWindow):
  14.         MainWindow.setObjectName("MainWindow")
  15.         MainWindow.resize(226, 146)
  16.         self.centralwidget = QtGui.QWidget(MainWindow)
  17.         self.centralwidget.setObjectName("centralwidget")
  18.         self.lineEdit = QtGui.QLineEdit(self.centralwidget)
  19.         self.lineEdit.setGeometry(QtCore.QRect(10, 10, 201, 26))
  20.         self.lineEdit.setObjectName("lineEdit")
  21.         self.pushButton = QtGui.QPushButton(self.centralwidget)
  22.         self.pushButton.setGeometry(QtCore.QRect(10, 50, 92, 28))
  23.         self.pushButton.setObjectName("pushButton")
  24.         self.pushButton_2 = QtGui.QPushButton(self.centralwidget)
  25.         self.pushButton_2.setGeometry(QtCore.QRect(120, 50, 92, 28))
  26.         self.pushButton_2.setObjectName("pushButton_2")
  27.         MainWindow.setCentralWidget(self.centralwidget)
  28.         self.menubar = QtGui.QMenuBar(MainWindow)
  29.         self.menubar.setGeometry(QtCore.QRect( 0,  0, 226, 25))
  30.         self.menubar.setObjectName("menubar")
  31.         MainWindow.setMenuBar(self.menubar)
  32.         self.statusbar = QtGui.QStatusBar(MainWindow)
  33.         self.statusbar.setObjectName("statusbar")
  34.         MainWindow.setStatusBar(self.statusbar)
  35.  
  36.         self.retranslateUi(MainWindow)
  37.         QtCore.QMetaObject.connectSlotsByName(MainWindow)
  38.  
  39.     def retranslateUi(self, MainWindow):
  40.         MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "ХабраОкно", None, QtGui.QApplication.UnicodeUTF8))
  41.         self.pushButton.setText(QtGui.QApplication.translate("MainWindow", "PushButton", None, QtGui.QApplication.UnicodeUTF8))
  42.         self.pushButton_2.setText(QtGui.QApplication.translate("MainWindow", "PushButton", None, QtGui.QApplication.UnicodeUTF8))
  43.  
  44.  
  45. if __name__ == "__main__":
  46.     import sys
  47.     app = QtGui.QApplication(sys.argv)
  48.     MainWindow = QtGui.QMainWindow()
  49.     ui = Ui_MainWindow()
  50.     ui.setupUi(MainWindow)
  51.     MainWindow.show()
  52.     sys.exit(app.exec_())
  53.  

    Во-первых, как мы видим, класс наследуется не от QMainWindow, а от object — это так называемая «новая» версия классов Python, при этом объект типа QMainWindow передается в метод класса как параметр. Это создает некоторую путаницу при работе с виджетами — объекты виджетов являются полями не класса окна, а родительского класса Ui_MainWindow. Но как с удобством, так и с неудобством этого, естественно, можно и поспорить. Тут уж каждому свое.
    Каждому объекту присваивается внутреннее имя, и это довольно любопытная вещь — нечто среднее между делфовскими свойствами Caption и Name. По сути, это имя требуется в том случае, когда к виджету не очень удобно обращаться как к полю класса. Если же у вас нет сложных генерируемых на лету форм и больших цепочек наследования — без этих имен можно спокойно обойтись, что я обычно и делаю. Кроме того, для создания интерфейса с поддержкой перевода на разные языки все текстовые данные прогоняются через транслятор и приводятся к кодировке UTF-8, причем это вынесено в отдельный метод класса. И все бы хорошо, только для каждого объекта копируется вот эта здоровенная строчка:
QtGui.QApplication.translate(«MainWindow», «ХабраОкно», None, QtGui.QApplication.UnicodeUTF8)

Почему бы не вынести ее в отдельный метод класса? :) Давайте в своем коде (который будем писать на основе сгенеренного файла) сделаем, например, так:
Copy Source | Copy HTML
  1. def toUtf(self, text):
  2.     return QtGui.QApplication.translate("MainWindow", text, None, QtGui.QApplication.UnicodeUTF8)

    Код не ухудшится с точки зрения читабельности, зато возвращаемые из сторонних функций строки будет легко преобразовывать к нужному типу.
    Кстати, то, что в генерируемом pyuic4 коде указывается полный адрес каждого модуля первого уровня — это в данном случае плюс, так как для разработки действительно полезно знать родителя для отдельного метода. Хотя баталии между любителями полных адресов и приверженцами строчек типа "from module import *" не утихают никогда.

Даешь прааактику!


    Чувствую, я уже надоел вам своей болтовней на отвлеченные темы, поэтому давайте рванем с места в карьер и разберем несколько типовых примеров, с которыми сталкиваются люди, изучающие PyQt4. Нет, я не буду здесь писать руководство для новичков, а просто опишу свой собственный опыт и подводные камни, с которыми пришлось столкнуться.
    Для начала давайте сделаем такую вещь — вынесем форму в отдельный модуль, который подключается к проекту. Из-за двойной классовой структуры, описанной выше, из функции создания окна придется возвращать два аргумента, а при создании главного окна — целых три. Итак, на повестке дня два файла — запускающий:
Copy Source | Copy HTML
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. import sys
  4. from forms import MainForm
  5.  
  6. def main():
  7.     app, mainForm, window = MainForm.init()
  8.     window.show()
  9.     sys.exit(app.exec_())
  10.  
  11. if __name__ == "__main__":
  12.     main()

и файл, собственно, формы:
Copy Source | Copy HTML
  1. # -*- coding: utf-8 -*-
  2. import sys
  3. from PyQt4 import QtCore, QtGui, Qt
  4.  
  5. class mainWindow(object):
  6.     def setupUi(self, MainWindow):
  7.         # установим для окна фиксированный размер (знание метода лишним не будет)
  8.         MainWindow.setFixedSize(950, 550)
  9.         # я главный, а все виджеты от меня наследуются
  10.         self.main = QtGui.QWidget(MainWindow)
  11.         MainWindow.setCentralWidget(self.main)
  12.         # для полноты ощущений создадим меню "Файл"
  13.         self.menubar = QtGui.QMenuBar(MainWindow)
  14.         self.menubar.setGeometry(QtCore.QRect( 0,  0, 559, 25))
  15.         # создаем триггер-действие QAction, чтобы привязать его к пункту меню
  16.         self.menu_file_exit = QtGui.QAction(self.main)
  17.         self.menu_file_exit.setText(self.toUtf("&Выход"))
  18.         MainWindow.connect(self.menu_file_exit, QtCore.SIGNAL('triggered()'), sys.exit)
  19.         # создаем пункт меню и добавляем в него наш QAction
  20.         self.menu_file = self.menubar.addMenu(self.toUtf('&Файл'))
  21.         self.menu_file.addAction(self.menu_file_exit)
  22.         MainWindow.setMenuBar(self.menubar)
  23.         # для солидности приделываем статусбар
  24.         self.statusbar = QtGui.QStatusBar(MainWindow)
  25.         MainWindow.setStatusBar(self.statusbar)
  26.         # двигаем окно куда нам хочется
  27.         MainWindow.move(140, 80)
  28.         self.retranslateUi(MainWindow)
  29.  
  30.     def retranslateUi(self, MainWindow):
  31.         # даем окну название
  32.         MainWindow.setWindowTitle(self.toUtf("ХабраОкно 2.0"))
  33.  
  34.     def toUtf(self, text):
  35.         # та самая функция перевода
  36.         return QtGui.QApplication.translate("MainWindow", text, None, QtGui.QApplication.UnicodeUTF8)
  37.  
  38. def init():
  39.     # инициализируем Qt
  40.     app = QtGui.QApplication(sys.argv)
  41.     # создаем отдельный, независимый объект окна...
  42.     MainWindow = QtGui.QMainWindow()
  43.     # ...и прогоняем его через наш класс
  44.     form = mainWindow()
  45.     form.setupUi(MainWindow)
  46.     return app, form, MainWindow

    В данном случае проект имеет такую структуру:
main.py
forms/
>    __init__.py (пустой файл, нужен, чтобы папка распознавалась как Python-пакет)
>    MainForm.py

Я знаю, что такой способ разделения можно ругать и ругать, однако ж сколько полезного мы узнали о создании окна! :) А теперь давайте не будем останавливаться на достигнутом и создадим дочернее окно, чтобы нашему было не так одиноко (не зря же я создавал отдельный каталог forms). Более того, сделаем его модальным (родительское окно не будет реагировать на действия пользователя, пока открыто дочернее), дабы шаловливые ручки юзеров не привели к плохим последствиям. Для этого создадим в каталоге forms файл ChildForm.py, в котором опишем дочернюю форму:
Copy Source | Copy HTML
  1. # -*- coding: utf-8 -*-
  2. from PyQt4 import QtCore, QtGui
  3.  
  4. class childWindow(object):
  5.     def setupUi(self, SmallWindow):
  6.         SmallWindow.setFixedSize(330, 200)
  7.         SmallWindow.setWindowFlags(QtCore.Qt.Window)
  8.         self.retranslateUi(SmallWindow)
  9.         SmallWindow.setWindowModality(QtCore.Qt.WindowModal)
  10.  
  11.     def retranslateUi(self, Form):
  12.         Form.setWindowTitle(self.toUtf("Я - дочернее окно"))
  13.  
  14.     def toUtf(self, text):
  15.         return QtGui.QApplication.translate("SmallWindow", text, None, QtGui.QApplication.UnicodeUTF8)
  16.  
  17. def init(parentwindow):
  18.     SmallWindow = QtGui.QWidget(parentwindow)
  19.     form = childWindow()
  20.     form.setupUi(SmallWindow)
  21.     return form, SmallWindow

а также внесем некоторые изменения в файл основной формы:
Copy Source | Copy HTML
  1. ...
  2. from forms import ChildForm
  3. ...
  4. # создаем свой класс окна, это нужно, чтобы решить кое-какие проблемы с наследованием
  5. class myQMainWindow(QtGui.QMainWindow):
  6.     def __init__(self, parent=None):
  7.         QtGui.QMainWindow.__init__(self, parent)
  8. ...
  9. def setupUi(self, MainWindow):
  10.         ...
  11.         # выносим окно в поле класса для более удобного наследования
  12.         self.mainwindow = MainWindow
  13.         # рисуем кнопку, по которой будет отображаться дочернее окно
  14.         self.btnHello = QtGui.QPushButton(self.main)
  15.         self.btnHello.setGeometry(QtCore.QRect(20, 19, 92, 28))
  16.         MainWindow.connect(self.btnHello, QtCore.SIGNAL('clicked()'), self.showChildWindow)
  17.         ...
  18.  
  19. def retranslateUi(self, MainWindow):
  20.         ...
  21.         self.btnHello.setText(self.toUtf("Жми!"))
  22.         ...
  23.  
  24. def showChildWindow(self):
  25.         self.childForm, self.childWindow = ChildForm.init(self.mainwindow)
  26.         self.childWindow.show()
  27.  
  28. def init():
  29.     ...
  30.     #MainWindow = QtGui.QMainWindow()
  31.     MainWindow = myQMainWindow()
  32.     ...

    Вуаля! У нас есть родительское и дочернее окна.

Думаю, на сегодня посиделку завершим, но это только начало :) Я знаю, что этот топик содержит не так много полезной информации, как хотелось бы, но обещаю исправиться — на следующей посиделке будет сплошная практика.
И да, спасибу юзернейму poltergeist с форума python.su за кучу полезных советов!
Теги:
Хабы:
Всего голосов 73: ↑67 и ↓6 +61
Просмотры 27K
Комментарии Комментарии 28

Работа

Python разработчик
222 вакансии
Data Scientist
127 вакансий