PyQt4-плагины для Qt Designer ∞
В прошлом посте про PyQt я немного рассказал о возможных способах создания GUI и говорил о том, что использование QtDesginer - неплохой вариант. Также я упоминал, что такой подход таит в себе как преимущества, так и недостатки. Преимущества, которые я ощутил на себе: разделение кода представления и кода логики; возможность быстро набросать прототип интерфейса. Недостатки, как это обычно бывает - продолжения достоинств: переработка интерфейса в QtDesigner часто требует больше кропотливого труда, чем в случае правильно организованного ручного кода; существует небольшой диссонанс "я знаю, как это сделать в коде, как же это делается в QtDesigner?".
Сегодня я расскажу об одной приятной возможности QtDesigner - работе с кастомными виджетами.
Шаг 1: простой виджет
В качестве рабочего примера будет QEditBox (обычное поле ввода), дополненный возможностью мигать фоном, сигнализируя о чём-нибудь (у меня была ошибка ввода).
Если бы мы делали без оглядки на QtDesigner, то вышло бы что-то похожее на это:
class QLineEditWErrState(QtGui.QLineEdit):
def __init__(self, parent=None):
QtGui.QLineEdit.__init__(self, parent)
self.timeout = 800
self.errorCss = 'background-color: antiquewhite'
self._orig_css = self.styleSheet()
def setErrorState(self):
self.emit(QtCore.SIGNAL("errorStateSet()"))
self.setStyleSheet(self.error_css)
QtCore.QTimer.singleShot(self.timeout, self.resetErrorState)
def resetErrorState(self):
self.setStyleSheet(self._orig_css)
self.emit(QtCore.SIGNAL("errorStateReseted()"))
В традициях Python, вероятно, стоило описать конструктор как
def __init__(self, timeout=800, errorCss='background-color: antiquewhite'):
QtGui.QLineEdit.__init__(self, parent)
self.timeout = timeout
self.errorCss = errorCss
self._orig_css = self.styleSheet()
Но в Qt4 принят такой API, что конструктор принимает лишь один необязательный параметр - родительский объект/виджет, а все дополнительные параметры выставляются вызовом методов/изменением атрибутов. И стоит придерживаться именно такого API, иначе ваш виджет может оказаться неинтероперабельным с нативными Qt-виджетами и/или объектами.
Описание виджета в плагине для QtDesigner
Пусть наш кастомный виджет будет в модуле widget.py. Сделаем так,
чтобы QLineEditWErrState можно было добавлять через QtDesigner.
Для этого нужно сделать описание виджета, например в модуле widget_plugin.py:
class QLineEditWErrStatePlugin(QtDesigner.QPyDesignerCustomWidgetPlugin):
"""
QLineEditWErrStatePlugin(QtDesigner.QPyDesignerCustomWidgetPlugin)
Provides a Python custom plugin for Qt Designer by implementing the
QDesignerCustomWidgetPlugin via a PyQt-specific custom plugin class.
"""
def __init__(self, parent=None):
QtDesigner.QPyDesignerCustomWidgetPlugin.__init__(self, parent)
self.initialized = True
def createWidget(self, parent):
# метод должен вернуть экземпляр класса нашего виджета
# вот тут и пригодилось согласование с принятым в Qt4 API
return QLineEditWErrState(parent)
def name(self):
# этод метод должен вернуть имя класса виджета
return "QLineEditWErrState"
def group(self):
# имя группы виджета
return "PyQt custom widgets"
def icon(self):
# иконка виджета
return QtGui.QIcon()
def toolTip(self):
# всплывающая подсказка
return "QLineEdit with error state"
def whatsThis(self):
# краткое описание
return "Custom widget QLineEditWErrState - QLineEdit with error state"
def isContainer(self):
# True, если виджет может служить контейнером других виджетов,
# при этом требуется реализация QDesignerContainerExtension
# False в противном случае
return False
def domXml(self):
# должен вернуть XML-описание виджета и параметры его свойств.
# минимально -- класс и имя экземпляра класса
# вставляется в .ui
return '<widget class="QLineEditWErrState" name=\"errStateLineEdit\" />\n'
def includeFile(self):
# возвращает имя модуля, в котором хранится наш виджет
# вставляется как `import <includeFile>` в генеренном из .ui Python-коде
return "widget"
После этого нужно сообщить QtDesigner, чтобы он "подхватил" это описание. Можно положить в условное место, но если стоит системный Qt4, то это не удобно. Я использую переменную PYQTDESIGNERPATH, в которой указываю путь, где искать плагины. Точнее я просто пишу небольшой shell-скрипт, в котором задаю необходимые переменные. Видимо, желающим попробовать это под Windows, придется написать .bat по аналогии ;-)
Результат выглядит примерно так:
Теперь мы можем в какую-нибудь форму добавить наш измененный виджет.
Пример приложения
Для экспериментов мы будем использовать небольшое бесполезное приложение.
Есть два поля ввода - обычное QLineEdit, доступное для редактирования,
и наше кастомное QLineEditWErrState, не доступное для редактирования. При изменении текста в QLineEdit,
такой же текст должен появляться в QLineEditWErrState и при вводе каждого символа должен моргать фон нашего
кастомного виджета. Если перефразировать это в терминах сигналов и слотов, то мы соединим
стандартный сигнал QLineEdit - textEdited(QString) с двумя слотами: один стандартный для QLineEdit - setText(QString) (появление текста во втором поле ввода) и второй - определенный только для QLineEditWErrState - setErrorState() (моргание фоном).
Создадим .ui и посмотрим: в него добавилась секция описания нашего виджета:
<customwidgets>
<customwidget>
<class>QLineEditWErrState</class>
<extends>QLineEdit</extends>
<header>widget</header>
</customwidget>
</customwidgets>
после генерации из .ui py-файла, получаем дополнительный импорт
from widget import QLineEditWErrState
Так что никакой магии - всё указывается явно:
-
модуль, откуда импортировать (он же тег
headerв .ui) берется изplugin.includeFile() -
имя класса, который импортировать (он же тег
classв .ui) берется изplugin.name()иplugin.domXml() -
имя переменной, которой присваивать экземпляр класса - из
plugin.domXml()
Теперь если мы желаем достичь описанного поведения, нам нужно написать приложение, которое бы до запуска соединяло нужные сигналы со слотами. Код не очень интересный, я не буду приводить его, а желающие могут посмотреть рабочий пример в hg-репо
Сразу можно отметить небольшое неудобство: для стандартных виджетов мы можем определить связи "сигнал-слот" в дизайнере. С нашим кастомным виджетом такое не получается - дизайнер не знает о том, какие слоты и сигналы доступны. Получается, либо мы стандартные связи описываем в дизайнере, а кастомные - в коде, либо все сигналы описываем в коде. И то и другое не очень здорово (особенно первое).
И второй момент, который можно отметить: управление поведением таким образом реализованного виджета возможно только из кода. Т.е. если в приложении нужно будет в одном виджете нужно моргать зеленым цветом, а в другом - красным, то такое поведение можно задать только в коде. И дальше мы посмотрим, как виджет можно улучшить.
Шаг 2: улучшенный виджет
PyQt4 даёт способ разрешить обе недоработки: при помощи нехитрых конструкций можно сообщить дизайнеру о доступных свойствах, а также описать слоты и сигналы.
Слоты - по факту просто методы, дизайнеру нужно лишь сообщить сигнатуру. Делается это при помощи декоратора pyqtSignature:
@QtCore.pyqtSignature("setErrorState()")
def setErrorState(self):
...
Доступные сигналы указываются при помощи атрибута класса __pyqtSignals__:
class QLineEditWErrState(QtGui.QLineEdit):
__pyqtSignals__ = ("errorStateSet()", "errorStateReseted()")
А свойства задаются функцией pyqtProperty(<тип>, <геттер>, [сеттер], [сброс]):
errorCss = QtCore.pyqtProperty("QString", getErrorCss, setErrorCss, resetErrorCss)
Помимо того что errorCss становится Qt-свойством, оно также ведет себя как и Python-свойство.
По-видимому, этого достаточно, чтобы исправить недостатки "первого шага". Наш виджет после улучшений принимает примерно такой вид:
class QLineEditWErrState(QtGui.QLineEdit):
__pyqtSignals__ = ("errorStateSet()", "errorStateReseted()")
def __init__(self, *args):
QtGui.QLineEdit.__init__(self, *args)
self.resetTimeout()
self.resetErrorCss()
self._orig_css = self.styleSheet()
@QtCore.pyqtSignature("setErrorState()")
def setErrorState(self):
self.emit(QtCore.SIGNAL("errorStateSet()"))
self.setStyleSheet(self.error_css)
QtCore.QTimer.singleShot(self.timeout, self.resetErrorState)
@QtCore.pyqtSignature("resetErrorState()")
def resetErrorState(self):
self.setStyleSheet(self._orig_css)
self.emit(QtCore.SIGNAL("errorStateReseted()"))
def getErrorCss(self):
return self.error_css
def setErrorCss(self, value):
self.error_css = value
def resetErrorCss(self):
self.error_css = 'background-color: antiquewhite'
errorCss = QtCore.pyqtProperty("QString", getErrorCss, setErrorCss, resetErrorCss)
def getTimeout(self):
return self.timeout
def setTimeout(self, value):
self.timeout = value
def resetTimeout(self):
self.timeout = 800
stateTimeout = QtCore.pyqtProperty("int", getTimeout, setTimeout, resetTimeout)
Если теперь запустим дизайнер, не изменяя описание виджета, то увидим, что достигли желаемого.
Теперь мы можем соединить все нужные слоты и сигналы в дизайнере.
Код приложения становится тривиальным. Более того, теперь даже генеренный из .ui py-модуль обладает нужным функционалом.
Это, кстати, достаточно интересно, поскольку позволяет задать уже в дизайнере некое поведение формы и посмотреть/протестировать его отдельно. Как мне кажется, это может быть весьма полезным при прототипировании интерфейса и его тестировании.
Ссылки по теме:
