Приветствую, земляне, сегодня я спешу к вам с очередным подарком. Несколько месяцев я разрабатывал программный комплекс, который позволяет в некоторой степени упростить разработку узкоспециализированных программ и использовать их в одном приложении. Ранее проект был анонсирован в моем телеграм канале, подписывайтесь, чтобы быть в курсе и иметь возможность принять участие в разработке. Эта статья дает старт рубрике ШБР (школа борцов с рутиной), в которой будут обсуждаться вопросы автоматизации различных задач.
Перед тем как начать, скажу пару слов о себе. Я являюсь разработчиком-энтузиастом, все мои продукты — это плод самообучения и нескольких лет практики. Честно сказать, я не слишком люблю корпеть над книгами и мануалами, а предпочитаю учиться в процессе решения реальных задач. Я признаю, что могу использовать в своей работе не самые лучшие практики, но это дело вкуса, главное, чтобы машина работала. Профи мой стиль скорее всего окажется не по душе, поэтому если вы испытываете лютую боль ниже спина при виде кода, написанного не по «канонам», то в таком случае лучше на этом слове и закончить, всех остальных приглашаю к прочтению.
Итак, что же я в очередной раз сотворил и зачем? Дело в том, что меня в одно время вдохновила книга «Автоматизация рутинных задач с помощью Python. Практическое руководство для начинающих», и я начал писать разные скрипты для решения всяких специфических задач. Скрипты копились, я забывал о них и их предназначении, также у них не было графического интерфейса, что я считаю, не очень круто. Поэтому я принял решение написать программу, которая стала бы хабом для всех скриптов,и фреймворк, который бы в некоторых аспектах позволял ускорить разработку, к тому же мне давно хотелось сделать программу, которая бы состояла из подключаемых модулей(плагинов). И да, я не соврал, когда говорил, что это швейцарский нож, но с одним нюансом – пока есть только рукоятка и напильник.
Так на свет появился программный комплекс PUSSY (Python Utilitarian Script System for You) – набор средств для создания утилит с графическим интерфейсом на базе Python и PySide6 и программы управления ими. Он предоставляет простые инструменты создания структур для хранения пользовательских настроек, достаточно определить один класс с перечнем свойств, для определения одного параметра достаточно одной строчки кода, а программа сама организует средства для ввода/вывода и хранения этих данных.
Далее я буду называть программу управления как Менеджер, а утилиту плагином. По сути, каждый плагин — это независимое Qt-приложение, Менеджер никак не вмешивается в их работу, только оповещает их о некоторых пользовательских действиях. Разработчику только нужно определить класс интерфейса, который Менеджер встроит в свой интерфейс. В данном случае интерфейс здесь является отправной точкой, а за ним может скрываться совершенно любая реализация. Чтобы писать плагины необходимо обладать знаниями и навыками разработки приложений на базе Qt!
Менеджер является своеобразным хабом для плагинов и никак не вмешивается в их работу, только уведомляет их о некоторых пользовательских действиях. Основой плагина является пользовательский интерфейс, основанный на модифицированном классе QWidget, который затем встраивается в интерфейс Менеджера в качестве вкладки через который пользователь взаимодействует с плагином.
Обзор Менеджера
Перед тем как приступить к разработке плагина, проведу обзор возможностей Менеджера, именно через него пользователь будет взаимодействовать с ним. Качаем код проекта и запускаем скрипт PUSSY.pyw, для запуска потребуется Python 3 и PySide6.
При запуске программы нас встречает страница со всеми активными плагинами, как можно догадаться каждому из них отведена отдельная вкладка, как в вашем любимом браузере, с помощью которого вы читаете данную статью. Плагины инициализируются не сразу, а при переходе во вкладку, или если активирована соответствующая опция, о ней расскажу далее. У неинициализированных плагинов перед именем стоит звездочка.
Быстренько пробежимся по настройкам Менеджера, чтобы перейти на страницу с ними нажмите Ctrl+Alt+S или выберите соответствующий пункт разделе "App". Тут все очень минималистично, в первом разделе первые 3 опции нужны, чтобы немного настроить внешний вид интерфейса, только акцентирую внимание на «Display error info in Logs», если отмечена, то на странице логов в случае ошибки в коде плагина будет выводиться дополнительная информация о перехваченном исключении; «External plugin directories»- здесь перечислены дополнительные директории, в которых будет производится поиск плагинов. Чтобы сохранить настройки нужно прожать «Apply Settings», a кнопка «Delete Invalid Data» нужна чтобы удалить данные конфигурации для плагинов из базы данных, которые были удалены или перемещены.
Нажатие комбинации Ctrl+Alt+P приведет нас на страницу со списком плагинов. Что мы тут видим (слева-направо): имя плагина, его активность, кнопка для перехода к настройкам, дополнительные опции (Plugin info – вывод информации о плагине; Reset settings – сброс до базовых настроек; Initialize on startup – отметка о том, что нужно инициализировать плагин при запуске Менеджера.
При нажатии на кнопку (3) покажется окно редактирования настроек, если они определены, если нет, то кнопка будет неактивной. Меню настроек по умолчанию выглядит как на нижнем рисунке, если форма вывода не определена разработчиком иным образом (данный вопрос будет рассмотрен в следующих статьях). По умолчанию они выводятся в два столбца: слева - имена свойств; справа – виджеты для ввода значений.
И в завершении прожмите Ctrl+Alt+L чтобы перейти на страницу логов, тут ничего мудреного нет, некоторые события будут отмечаться здесь, зеленым будут выделены те, с которыми все окей, в противном случае – красным. Если «Display error info in Logs» в настройках программы установлена, то в случае ошибки будет выведена информация трассировки. В некоторых ситуациях это поможет выявить ошибки в коде. Если ошибка возникнет в процессе инициализации плагина или интерфейса редактирования настроек, то вы увидите примерно вот это:
Для возврата на главную страницу нажмите Esc или кнопку с изображением домика.
Думаю, всем понятно как устроен Менеджер, тут все предельно просто и интуитивно, разработка плагина тоже предельно проста, будет достаточно минут 30 максимум чтобы во все разобраться.
Как написать свой плагин? Набираем обороты.
Для работы нам потребуется:
Инструмент для написания кода (я предпочитаю IDE PyCharm);
Знания и навыки создания Qt приложений (для знакомства с основами рекомендую данную книгу).
Если PySide6 у вас по какому-то недоразумению не установлен, то выполняем команду в терминале:
pip install PySide6
Далее качаем исходный код проекта, если ранее не сделали. Можете первым делом запустить файл PUSSY.pyw, чтобы все пощупать непосредственно. Кстати, на данный момент эта статья единственное существующее руководство, читайте внимательнее.
Комплекс состоит из двух частей: 1-Менеджер (папка manager), 2-Фреймворк (папка PyUB). Если желаете знать почему основной пакет носит имя PyUB, то все просто, проект имеет рабочее название Utilities Box. Итак, что есть главном пакете:
Пакет App – его трогать не нужно, он обеспечивает работу Менеджера;
Пакет Types – содержит все необходимые компоненты для разработки:
o Пакет InputWidgets – здесь классы виджетов для ввода данных;
o Пакет Properties – здесь классы свойств, которые служат для ввода и хранения пользовательских настроек;
o UBWidget – класс для
мордыинтерфейса, он и будет основой плагина, является модифицированным потомком QWidget (PySide6.QtWidgets);o PropertyContainer – класс-контейнер для свойств из пакета Properties;
o UBHelper – класс со вспомогательными функциями;
utils – модуль с необходимым функциями.
Как видите, арсенал вполне минималистичный, все только самое необходимое.
Пишем код плагина
Сделаем пробный плагин как на первой картинке, кодовый замок, конечно, это просто игрушка для демонстрации. Код от «замка» устанавливается через настройки, если код введен верно, то поле загорается зелеными, иначе – красным.
Посмотрим на плагин в общих чертах, по сути это пакет Python - папка, содержащая файл __init__.py, размещается она в папке manager/Plugins, либо во внешних директориях, указанных в настройках. В папке Plugins есть шаблон (папка Template) скопируйте его, когда будете писать свой плагин.
Внимание! Пакеты с именем Template игнорируются при загрузке.
В нашем случае плагин будет разделен на 4 файла (исходные файлы находятся в Plugins/Code lock):
__init__.py – здесь будет производится регистрация класса с интерфейсом и размещается дополнительная информация, ничего более тут выполнять не нужно;
View.py – здесь определен основной класс плагина, который представляет из себя пользовательский интерфейс;
Settings.py – модуль с классом описывающим пользовательские настройки;
ui_form.py – код виджета, сгенерированный в Qt Designer.
Код модуля View.py:
from PyUB.Types import UBWidget
from PySide6.QtWidgets import QPushButton
from .ui_form import Ui_Form
from .Settings import Settings
class CodeLock(UBWidget):
ub_settings = Settings #ассоциируем класс с настройками
def __init__(self):
super().__init__()
self.ui = Ui_Form() # встраивание кода, сгенерированного в Qt Designer
self.ui.setupUi(self)
self._init_gui()
self.code = self.ub_settings.get_property_value("code")
def _init_gui(self):
for name in dir(self.ui):
attr = getattr(self.ui, name)
if type(attr) is QPushButton:
attr.clicked.connect(self.on_btn)
self.ui.lineEdit.setStyleSheet("")
self.ui.lineEdit.textChanged.connect(self.text_changed)
self.ui.lineEdit.setInputMask(self.ub_settings.get_property("code").p_input_mask)
def on_btn(self):
if not self.ui.lineEdit.isEnabled(): return
s = self.sender()
self.ui.lineEdit.setText(self.ui.lineEdit.text() + s.text())
def text_changed(self, text):
if len(text) == len(self.code):
if text == self.code:
self.ui.lineEdit.setStyleSheet(u"background-color: rgb(85, 255, 127);")
else:
self.ui.lineEdit.setStyleSheet(u"background-color: rgb(240, 54, 8);")
self.ui.lineEdit.setEnabled(False)
self.id = self.startTimer(5000)
def timerEvent(self, event) -> None:
self.ui.lineEdit.setStyleSheet("")
self.ui.lineEdit.clear()
self.ui.lineEdit.setEnabled(1)
self.killTimer(self.id)
def settings_edit_finished(self, changed:bool) -> None: # менеджер вызывает этот метода, когда пользователь закончил редактировать пользовательские настройки
if not changed: return
self.code = self.ub_settings.get_property_value("code")
self.ui.lineEdit.clear()
В данном файле определен класс-потомок UBWidget, он и будет отвечать за вывод интерфейса. В данном случае в него включена вся логика программы, в более сложных случаях так делать не рекомендуется. Этот класс и будет встраиваться в окно Менеджера в качестве вкладки.
Код в методе __init__() выполняется, когда пользователь перешел во вкладку, в которой размещается экземпляр данного класса, или при запуске Менеджера, если пользователь ранее активировал параметр «Init on startup» (Инициализировать при запуске) на странице с плагинами в Менеджере, после выполнения данного метода плагин считается инициализированным.
Класс UBWidget дополняет QWidget следующими атрибутами:
Поля класса:
ub_settings - (опционально) ссылка на класс с пользовательскими настройками (потомок PropertyContainer);
ub_name:str– (опционально) имя плагина, которое будет отображаться в менеджере, если не определено, то будет использовано имя пакета (папки).
Методы:
__init__(self) - инициализация виджета;
retranslate(self) –перевод интерфейса виджета на другой язык (на момент написания статьи функция не реализована);
app_closing(self) – выполняется Менеджером перед его закрытием, когда пользователь запустил процедуру закрытия главного окна, нужно чтобы выполнить требуемые действия перед закрытием, например, остановить исполняемые процессы, сохранить данные;
settings_edit_started(self) – выполняется Менеджером перед началом редактирования настроек;
settings_edit_finished (self, changed:bool) – выполняется после завершения редактирования настроек, changed принимает значения: True – если хотя бы одно из свойств было изменено, иначе – False;
deactivated(self) – выполняется, если плагин был деактивирован пользователем, перед тем как экземпляр данного класса будет удален из окна просмотра.
Все перечисленные методы выполняются Менеджером при наступлении упомянутых событий, это нужно, например, для согласования работы потоков во избежание нежелательных коллизий. Учтите, что эти методы могут быть вызваны только у инициализированных экземпляров классов, то есть тех, которые встроены в главном окне в качестве вкладки и их имя не помечено звездочкой.
Перейдем к файлу с настройками (Settings.py);
from PyUB.Types import PropertyContainer
from PyUB.Types.Properties import StringProperty
class Settings(PropertyContainer):
code: StringProperty(default_value="12345", input_mask="00000000", name="Код") # определяем свойство для ввода строки
Здесь мы определяем класс-контейнер со свойствами, который затем ассоциируется с классом UBWidget через атрибут ub_settings. Атрибут является опциональным и нужен для хранения PropertyContainer, в котором будут находиться пользовательские настройки, пользователь может их изменить через Менеджер. Все классы свойств находятся в пакете Properties. Свойства записываются в теле класса как аннотации. Формат объявления свойств следующий:
<имя свойства>: <Свойство>(<значения параметров>)
В качестве примера я взял свойство StringProperty, которое хранит в себе значение типа str; параметрами default_value - мы задаем дефолтное значение, input_mask -маску ввода, name - имя, которое будет отображено рядом с полем ввода строки в диалоговом окне редактирования настроек. Значения свойств из данного класса редактируются пользователем через Менеджер, согласно тому как было рассказано в предыдущем разделе. Красота: одна строчка кода = одному параметру пользовательских настроек. В следующих сериях мы поговорим о механизме функционирования свойств и их взаимодействии с контейнером и Менеджером, следите за анонсами, пишите свои предложения и идеи, я планирую и дальше развивать проект по мере возможностей.
На данный момент выбор следующий:
1) BoolProperty – свойство для ввода булевых значений;
2) ComboBoxProperty – выпадающий список;
3) FilePathListProperty-список путей к файлам или папкам;
4) FilePathProperty – путь к файлу или папке;
5) FloatProperty – ввод вещественных чисел;
6) FontSelectProperty- выпадающий список для выбора шрифта;
7) IntProperty– ввод целых чисел;
8) NamedFilePathListProperty - именованный список путей к файлам или папкам;
9) PasswordStringProperty – однострочное поле ввода паролей;
10) StringListProperty– список строк;
11) StringProperty– однострочное поле ввода строк.
Не буду сейчас подробно расписывать каждый из них, можно догадаться по сигнатуре вызова конструктора и именам аргументов. Есть три параметра, которые присутствуют у всех свойств: name – имя, оно выводится в окне редактирования настроек в виде подписи, default_value – значение по умолчанию, tool_tip – всплывающая подсказка.
Чтобы контейнер был полезен, нам надо получать данные, которые он хранит. Для этого есть два способа:
1. Использовать метод get_property_value()
<контейнер>. get_property_value("<имя свойства>")
2. Создать экземпляр класса контейнера и обратиться по имени свойства:
<экземпляр контейнера>.<имя свойства>
При необходимости можно получить доступ к самому свойству:
<контейнер>. get_property ("<имя свойства>")
Не исключено, что возникнет потребность получить доступ к параметрам, чтобы прочитать или изменить их. Параметры, которые передаются конструктору можно извлечь, только имейте в виду, что согласно соглашению имен внутри экземпляра класса их имена начинаются с префикса «p_», таким образом, поле name получит имя p_name, но для извлечения имени есть метод get_name(). Данному соглашению нужно следовать, когда будете писать собственные классы свойств.
Для справки: параметры свойств и их значения сохраняются на жестком диске и загружаются при запуске Менеджера. Параметры и значения загружаются непосредственно в PropertyContainer, который присвоен атрибуту ub_settings в UBWidget; значения свойств сохраняются автоматически после того, как пользователь их изменил через соответствующее диалоговое окно; если параметры изменяются в процессе выполнения, то необходимо их сохранить на диске вызовом метода save_settings_parameters() класса UBHelper.
Итак, все важные классы мы уже определили, теперь нужно сообщить Менеджеру какой класс нужно использовать в качестве основного. Для этого в модуле utils есть функция register_ubwidget(), в нее надо передать ссылку на класс-потомок UBWidget, в плагине можно зарегистрировать только один класс, это действие нужно выполнить в файле __init__.py. Также в этом файле по желанию можно определить дополнительную информацию о плагине, создав переменную ub_info ={…}- это словарь; допускаются ключи и значения только строкового типа, все они описаны в таблице:
| Ключ | Пример | Описание |
1 | description | “Этот плагин выводит спутник на орбиту” | Описание плагина |
2 | author | “Иван Иванов” | Имя автора |
3 | author_webpage | “ivan_ivanov.com” | веб-страница автора |
4 | author_email | “ivan_ivanov@mail.com” | электронная почта автора |
5 | version | “1.3.4” | версия плагина |
6 | wiki_url | “ivan_ivanov.wiki.com” | ссылка на страницу вики |
Пользователь может просмотреть эту информацию в специальном диалоговом окне.
Гиперссылки активные и при нажатии будут открыты в браузере или почтовом клиенте. Если какое-то из ключей не определено, то будет выведена советующая информация.
В комплекте еще один класс, который остался без внимания. Чтобы его использовать надо создать экземпляр, передав в конструктор ссылку на класс-потомок UBWidget, это нужно для того, чтобы система могла определить контекст вызова. Вот перечень методов:
__init__(self, <класс-потомок UBWidget>) - принимает ссылку на класс UBWidget, которая в дальнейшем используется как ключ для определения контекста вызова других методов;
save_settings_parameters(self)- сохраняет параметры свойств на жестком диске, которые находятся в классе PropertyContainer, присвоенный атрибуту ub_settings (метод является потокобезопасным), это нужно в том случае, если плагин изменяет параметры свойств в процессе работы;
get_plugin_dir(self)->str-выводит абсолютный путь к папке, в которой находится плагин;
open_localstorage(self, flag='c', protocol=None, writeback=False)-возвращает объект базы данных, является оберткой для функции open() из модуля shelve, узнать подробнее можно по ссылке. Файлы базы данных создаются в папке localdata в корневой папке плагина.
Беглый взгляд на работу Менеджера
Когда вы уже знаете как строятся плагины, то опишу как менеджер с ними работает. При запуске Менеджер сканирует папку Plugin и те, что указаны в качестве внешних директорий на наличие в них пакетов Python; затем он по очереди их загружает(выполняется код в файле __init__.py), после этого проверяется реестр на наличие в нем зарегистрированного класса UBWidget; после проверяется база данных на наличие сохраненной конфигурации для текущего плагина (конфигурация хранит в себе параметры и значения свойств пользовательских настроек, активирован ли плагин и производить ли его инициализацию при запуске), если она присутствует, то загружается, в противном случае создается новая запись; если плагин установлен как активный, то UBWidget встраивается как вкладка в объект QTabWidget главного окна, но выполнение конструктора происходит после того как пользователь перейдет в данную вкладку или если установлена инициализация при запуске.
Вместо заключения
Вот и подошла к концу вводная часть, в следующих сериях мы поговорим о Свойствах и как они работают в составе контейнера и Менеджера, разработаем какой-нибудь класс свойства для расширения коллекции. Свои плагины я публиковать не стал по понятным причинам, поскольку они скорее всего имеют не презентабельный вид или имеют слишком специфичное назначение.
Теперь поговорим о слабых сторонах продукта. Он подойдет далеко не каждому, поскольку это только основа и чтобы сделать что-то под свои задачи нужно обладать соответствующими знаниями и навыками или иметь хорошие отношения с таким человеком. Также некоторые функции из Менеджера были удалены, например, изначально была функция перезагрузки плагинов по желанию пользователя, как оказалось, перезагружать ранее загруженные модули со всеми зависимостями оказалось нетривиальной задачей. Все плагины работают в основном потоке, поэтому при случайном или умышленном зависании одного из них произойдет зависание всего приложения. Также в планах был функционал интернационализации, но он пока не был реализован, возможно текущие наработки будут пересмотрены и переработаны. Вполне возможно, я переработаю архитектуру Менеджера, слишком уж она монолитной получилась.
Если есть какие-то вопросы и идеи, то добро пожаловать на мой Discord сервер. Также же я хочу сделать сайт вики, но не могу определиться с движком, хочется, чтобы интернационализация работала из коробки или было для этого соответствующее расширение, чтобы все работало на PHP, MySQL, кто имеет опыт напишите мне.
Обновление от 07.01.2024
Теперь у меня появился сайт-вики, где полностью изложена документация по API фреймворка, чтобы разработчики могли без проблем вести разработку, также там есть исчерпывающие уроки по разработке плагинов.
Выражаю огромную признательность Andreas Gohr, разработку замечательного и простого вики-движка dokuwiki.