Небольшое вступление
Собственно, тогда, давно, я решил попробовать 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
- # -*- coding: utf-8 -*-
-
- # Form implementation generated from reading ui file '/home/username/Рабочий стол/habr.ui'
- #
- # Created: Fri Nov 13 23:52:05 2009
- # by: PyQt4 UI code generator 4.6
- #
- # WARNING! All changes made in this file will be lost!
-
- from PyQt4 import QtCore, QtGui
-
- class Ui_MainWindow(object):
- def setupUi(self, MainWindow):
- MainWindow.setObjectName("MainWindow")
- MainWindow.resize(226, 146)
- self.centralwidget = QtGui.QWidget(MainWindow)
- self.centralwidget.setObjectName("centralwidget")
- self.lineEdit = QtGui.QLineEdit(self.centralwidget)
- self.lineEdit.setGeometry(QtCore.QRect(10, 10, 201, 26))
- self.lineEdit.setObjectName("lineEdit")
- self.pushButton = QtGui.QPushButton(self.centralwidget)
- self.pushButton.setGeometry(QtCore.QRect(10, 50, 92, 28))
- self.pushButton.setObjectName("pushButton")
- self.pushButton_2 = QtGui.QPushButton(self.centralwidget)
- self.pushButton_2.setGeometry(QtCore.QRect(120, 50, 92, 28))
- self.pushButton_2.setObjectName("pushButton_2")
- MainWindow.setCentralWidget(self.centralwidget)
- self.menubar = QtGui.QMenuBar(MainWindow)
- self.menubar.setGeometry(QtCore.QRect( 0, 0, 226, 25))
- self.menubar.setObjectName("menubar")
- MainWindow.setMenuBar(self.menubar)
- self.statusbar = QtGui.QStatusBar(MainWindow)
- self.statusbar.setObjectName("statusbar")
- MainWindow.setStatusBar(self.statusbar)
-
- self.retranslateUi(MainWindow)
- QtCore.QMetaObject.connectSlotsByName(MainWindow)
-
- def retranslateUi(self, MainWindow):
- MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "ХабраОкно", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButton.setText(QtGui.QApplication.translate("MainWindow", "PushButton", None, QtGui.QApplication.UnicodeUTF8))
- self.pushButton_2.setText(QtGui.QApplication.translate("MainWindow", "PushButton", None, QtGui.QApplication.UnicodeUTF8))
-
-
- if __name__ == "__main__":
- import sys
- app = QtGui.QApplication(sys.argv)
- MainWindow = QtGui.QMainWindow()
- ui = Ui_MainWindow()
- ui.setupUi(MainWindow)
- MainWindow.show()
- sys.exit(app.exec_())
-
Во-первых, как мы видим, класс наследуется не от QMainWindow, а от object — это так называемая «новая» версия классов Python, при этом объект типа QMainWindow передается в метод класса как параметр. Это создает некоторую путаницу при работе с виджетами — объекты виджетов являются полями не класса окна, а родительского класса Ui_MainWindow. Но как с удобством, так и с неудобством этого, естественно, можно и поспорить. Тут уж каждому свое.
Каждому объекту присваивается внутреннее имя, и это довольно любопытная вещь — нечто среднее между делфовскими свойствами Caption и Name. По сути, это имя требуется в том случае, когда к виджету не очень удобно обращаться как к полю класса. Если же у вас нет сложных генерируемых на лету форм и больших цепочек наследования — без этих имен можно спокойно обойтись, что я обычно и делаю. Кроме того, для создания интерфейса с поддержкой перевода на разные языки все текстовые данные прогоняются через транслятор и приводятся к кодировке UTF-8, причем это вынесено в отдельный метод класса. И все бы хорошо, только для каждого объекта копируется вот эта здоровенная строчка:
QtGui.QApplication.translate(«MainWindow», «ХабраОкно», None, QtGui.QApplication.UnicodeUTF8)
Почему бы не вынести ее в отдельный метод класса? :) Давайте в своем коде (который будем писать на основе сгенеренного файла) сделаем, например, так:
Copy Source | Copy HTML
- def toUtf(self, text):
- return QtGui.QApplication.translate("MainWindow", text, None, QtGui.QApplication.UnicodeUTF8)
Код не ухудшится с точки зрения читабельности, зато возвращаемые из сторонних функций строки будет легко преобразовывать к нужному типу.
Кстати, то, что в генерируемом pyuic4 коде указывается полный адрес каждого модуля первого уровня — это в данном случае плюс, так как для разработки действительно полезно знать родителя для отдельного метода. Хотя баталии между любителями полных адресов и приверженцами строчек типа "from module import *" не утихают никогда.
Даешь прааактику!
Чувствую, я уже надоел вам своей болтовней на отвлеченные темы, поэтому давайте рванем с места в карьер и разберем несколько типовых примеров, с которыми сталкиваются люди, изучающие PyQt4. Нет, я не буду здесь писать руководство для новичков, а просто опишу свой собственный опыт и подводные камни, с которыми пришлось столкнуться.
Для начала давайте сделаем такую вещь — вынесем форму в отдельный модуль, который подключается к проекту. Из-за двойной классовой структуры, описанной выше, из функции создания окна придется возвращать два аргумента, а при создании главного окна — целых три. Итак, на повестке дня два файла — запускающий:
Copy Source | Copy HTML
- #!/usr/bin/python
- # -*- coding: utf-8 -*-
- import sys
- from forms import MainForm
-
- def main():
- app, mainForm, window = MainForm.init()
- window.show()
- sys.exit(app.exec_())
-
- if __name__ == "__main__":
- main()
и файл, собственно, формы:
Copy Source | Copy HTML
- # -*- coding: utf-8 -*-
- import sys
- from PyQt4 import QtCore, QtGui, Qt
-
- class mainWindow(object):
- def setupUi(self, MainWindow):
- # установим для окна фиксированный размер (знание метода лишним не будет)
- MainWindow.setFixedSize(950, 550)
- # я главный, а все виджеты от меня наследуются
- self.main = QtGui.QWidget(MainWindow)
- MainWindow.setCentralWidget(self.main)
- # для полноты ощущений создадим меню "Файл"
- self.menubar = QtGui.QMenuBar(MainWindow)
- self.menubar.setGeometry(QtCore.QRect( 0, 0, 559, 25))
- # создаем триггер-действие QAction, чтобы привязать его к пункту меню
- self.menu_file_exit = QtGui.QAction(self.main)
- self.menu_file_exit.setText(self.toUtf("&Выход"))
- MainWindow.connect(self.menu_file_exit, QtCore.SIGNAL('triggered()'), sys.exit)
- # создаем пункт меню и добавляем в него наш QAction
- self.menu_file = self.menubar.addMenu(self.toUtf('&Файл'))
- self.menu_file.addAction(self.menu_file_exit)
- MainWindow.setMenuBar(self.menubar)
- # для солидности приделываем статусбар
- self.statusbar = QtGui.QStatusBar(MainWindow)
- MainWindow.setStatusBar(self.statusbar)
- # двигаем окно куда нам хочется
- MainWindow.move(140, 80)
- self.retranslateUi(MainWindow)
-
- def retranslateUi(self, MainWindow):
- # даем окну название
- MainWindow.setWindowTitle(self.toUtf("ХабраОкно 2.0"))
-
- def toUtf(self, text):
- # та самая функция перевода
- return QtGui.QApplication.translate("MainWindow", text, None, QtGui.QApplication.UnicodeUTF8)
-
- def init():
- # инициализируем Qt
- app = QtGui.QApplication(sys.argv)
- # создаем отдельный, независимый объект окна...
- MainWindow = QtGui.QMainWindow()
- # ...и прогоняем его через наш класс
- form = mainWindow()
- form.setupUi(MainWindow)
- return app, form, MainWindow
В данном случае проект имеет такую структуру:
main.py
forms/
> __init__.py (пустой файл, нужен, чтобы папка распознавалась как Python-пакет)
> MainForm.py
Я знаю, что такой способ разделения можно ругать и ругать, однако ж сколько полезного мы узнали о создании окна! :) А теперь давайте не будем останавливаться на достигнутом и создадим дочернее окно, чтобы нашему было не так одиноко (не зря же я создавал отдельный каталог forms). Более того, сделаем его модальным (родительское окно не будет реагировать на действия пользователя, пока открыто дочернее), дабы шаловливые ручки юзеров не привели к плохим последствиям. Для этого создадим в каталоге forms файл ChildForm.py, в котором опишем дочернюю форму:
Copy Source | Copy HTML
- # -*- coding: utf-8 -*-
- from PyQt4 import QtCore, QtGui
-
- class childWindow(object):
- def setupUi(self, SmallWindow):
- SmallWindow.setFixedSize(330, 200)
- SmallWindow.setWindowFlags(QtCore.Qt.Window)
- self.retranslateUi(SmallWindow)
- SmallWindow.setWindowModality(QtCore.Qt.WindowModal)
-
- def retranslateUi(self, Form):
- Form.setWindowTitle(self.toUtf("Я - дочернее окно"))
-
- def toUtf(self, text):
- return QtGui.QApplication.translate("SmallWindow", text, None, QtGui.QApplication.UnicodeUTF8)
-
- def init(parentwindow):
- SmallWindow = QtGui.QWidget(parentwindow)
- form = childWindow()
- form.setupUi(SmallWindow)
- return form, SmallWindow
а также внесем некоторые изменения в файл основной формы:
Copy Source | Copy HTML
- ...
- from forms import ChildForm
- ...
- # создаем свой класс окна, это нужно, чтобы решить кое-какие проблемы с наследованием
- class myQMainWindow(QtGui.QMainWindow):
- def __init__(self, parent=None):
- QtGui.QMainWindow.__init__(self, parent)
- ...
- def setupUi(self, MainWindow):
- ...
- # выносим окно в поле класса для более удобного наследования
- self.mainwindow = MainWindow
- # рисуем кнопку, по которой будет отображаться дочернее окно
- self.btnHello = QtGui.QPushButton(self.main)
- self.btnHello.setGeometry(QtCore.QRect(20, 19, 92, 28))
- MainWindow.connect(self.btnHello, QtCore.SIGNAL('clicked()'), self.showChildWindow)
- ...
-
- def retranslateUi(self, MainWindow):
- ...
- self.btnHello.setText(self.toUtf("Жми!"))
- ...
-
- def showChildWindow(self):
- self.childForm, self.childWindow = ChildForm.init(self.mainwindow)
- self.childWindow.show()
-
- def init():
- ...
- #MainWindow = QtGui.QMainWindow()
- MainWindow = myQMainWindow()
- ...
Вуаля! У нас есть родительское и дочернее окна.
Думаю, на сегодня посиделку завершим, но это только начало :) Я знаю, что этот топик содержит не так много полезной информации, как хотелось бы, но обещаю исправиться — на следующей посиделке будет сплошная практика.
И да, спасибу юзернейму poltergeist с форума python.su за кучу полезных советов!