Привет, земляне! Спешу к вам с радостной новостью, наконец то я готов представить вашему вниманию обновленную версию программы с открытым исходным кодом для разработки и управления мини-приложениями на Python и PySide6. Данный проект являет улучшенной версией программы PUSSY, которую я выпустил в свет около трех лет тому назад. За время её эксплуатации я выявил множество недостатков, который пришлось переосмыслить и исправить, а так же внедрить ту функциональность, которая задумывалась изначально, так что, к сожалению, обратной совместимости не будет. Но из-за своей простоты, адаптация под новый API будет несложной задачей.

Думаю следует ввести в курс дела тех, кто не знает или уже забыл о чем идет речь, так как ранее на эту тему я уже выпускал несколько статей: статья 1, статья 2. Pyrog (да, именно такое новое название получила обновленная версия программы) - это программный комплекс, который состоит из фреймворка для упрощенной разработки мини-приложений на Python и PySide6 и Менеджера, программы для эксплуатации этих самых мини-приложений.

1. Что такое Pyrog

Как было ранее сказано, Pyrog это программный комплекс для создания и управления мини-приложениями. Поскольку являюсь cg художником и на его основе создаю для себя утилиты, с помощью которых я автоматизирую некоторые рутинные задачи на подобии:

  • удалить ненужные файлы рендеров и кэшей;

  • конвертация изображений и видео;

  • выполнить другие задачи. И это только малая часть возможных применений, благодаря богатому выбору готовых библиотек для Python разработка утилит становится плевым делом, а также большому разнообразию нейросетевых чат-ботов готовый результат можно получить значительно быстрее.

Может мне кто-то возразит: "зачем в 2026 году писать скрипты на Python для автоматизации, если можно использовать ИИ-агентов, которые все сделают, только дай команду?". Ну во-первых, лучше иметь надежный и предсказуемый инструмент, чем тот, который галлюцинирует по определению, требует много вычислительных ресурсов, а иногда и значительных денежных трат. К сожалению данная программа сама не решает пользовательские задачи, для этого нужно разрабатывать свои инструменты, но Pyrog может эту задачу в некоторой степени упростить.

Что на данный момент Pyrog предлагает из коробки:

  • инструменты для быстрой разработки приложений на Python и PySide6;

  • управлять мини-приложениями (плагинами) из единой программы;

  • обеспечивает автоматическую загрузку и обновление сторонних зависимостей из хранилища pyPI;

  • механизм интернационализации приложений, система автоматически загружает и выгружает словари переводов; механизм языковых констант;

  • интерфейс управляющей программы переведен на русский и английский языки, в будущем их перечень увеличится;

  • быстрое объявление пользовательских настроек, достаточно объявить специальный класс со специализированными свойствами и система сама позаботится об их хранении в постоянной памяти и графическом интерфейсе для их ввода-вывода;

  • бесплатное использование, открытый исходный код.

1.1 Сравнение с предыдущей версией

Pyrog по сравнению со своим предшественником получил ряд новых функций и других улучшений

Нововведение

Pyrog

PUSSY

Автоматическая установка PySide6

Автоматическая установка/обновление зависимостей

Интернационализация плагинов и Менеджера

Оптимизация загрузки плагинов

Информация о событиях передается через сигналы

Улучшенный графический интерфейс

Плагины после деактивации удаляются из памяти

Значение Свойства синхронизировано с виджетом, событие изменения значения можно перехватить

Контейнер свойств может передавать сигнал об изменении значений Свойств

2. Разработка собственного мини-приложения

Для разработки собственного приложения нужно иметь минимум базовые навыки разработки на Python и PySide6.

2.1 Скачивание исходников и подготовка IDE к работе

Скачайте исходные файлы программы из этого репозитория. Установите Python, если по какой-то причине он еще не установлен, или обновите, нам нужна версия 3.13 или новее. Распакуйте архив и запустите файл ...Pyrog/manager/Pyrog.pyw, если PySide6 не установлен запустится утилита для автоматической установки, просто дайте согласие на установку и он скачается установится в систему. Если все прошло успешно, то на экране появится окно Менеджера, так далее по тексту будет называться программа для управления плагинами, теми самыми мини-приложениями, о которых ранее шла речь. Не буду подробно расписывать как его использовать, так как с этим легко разобраться самостоятельно, в крайнем случае можете обратиться к данному руководству.

Теперь, нам нужно подготовить среду разработки для работы, я опишу процесс в контексте IDE PyCharm, но вы же можете использовать другой редактор. В папке ...Pyrog/manager/plugins есть папка с именем Template, скопируйте ее и переименуйте, так как данное имя в любом регистре зарезервировано и будет проигнорировано. Затем создайте откройте папку как проект, и зайдите в настройки Settings -> Project Structure нажмите на Add content Root и выберите папку Pyrog и установите ее в качестве Source. Теперь, запустите Pyrog.pyw и увидите интерфейс шаблона.

Pyrog. Плагины
Pyrog. Плагины

Можете выключить ненужные плагины, чтобы не мешали.

2.2 Анатомия плагина

В Python пакетом является папка, которая содержит в себе файл с именем init.py, плагин тот же самый пакет, который Менеджер импортирует, когда пользователь к нему обращается. Плагин загружается в случаях, когда нужно отобразить интерфейс или пользователь обратился к настройкам плагина, это сделано для оптимизации, "зачем загружать то, что возможно не будет использовано". Ниже приводится структура папки плагина "в максимальной комплектации".

📁 <Имя папки плагина>
├── 📁translations
│   ├── 📁 ru_RU
│   │   ├── 📄 dictionary1.qm
│   │   └── 📄 dictionary2.qm
│   ├── 📁 en_US/
│   │   ├── 📄 dictionary1.qm
│   │   └── 📄 dictionary2.qm
├──📄 __init__.py
└──📄 manifest.json

В файле __init__.py должен находится атрибут с именем plugin, который является ссылкой на класс плагина, данный класс может быть объявлен в самом файле или в другом, который затем будет импортирован. В принципе, можно все приложение написать в файле __init__.py, но лично я бы так делать не стал, а вот сам класс - вполне. В шаблоне только одна строчка кода, где из модуля plugin импортируется класс MyPlugin под псевдонимом plugin.

from .plugin import MyPlugin as plugin 

Можно то же самое записать так

from .plugin import MyPlugin
plugin = MyPlugin

В файле manifest.json хранится информация о плагине, наличие этого файла желательно, но не является обязательным, просто некоторая функциональность будет ограничена. Пример содержания файла

{
	    "name": "My cool Plugin",
	    "description": "The plugin does...",
	    "project_page_url": "https://mysite.org/my-cool-plugin",
	    "manual_url": "https://docs.mysite.org/my-cool-plugin",
	    "version": "1.0.0",
	    "version_status": "beta",
	    "init_release_date": "2025-01-01",
	    "update_date": "2025-01-01",
	    "changelog_url": "https://docs.mysite.org/my-cool-plugin/changelog",
	    "developer": "IronMesh",
	    "developer_email": "mail@mail.com",
	    "developer_webpage": "https://mysite.org",  
	    "repository_url": "https://github.com/iron-mesh/utilities-box",
	    "forum_url": "https://forum.mysite.org/my-cool-plugin",
	    "source_language": "en_GB",
	    "dependencies": [
	        "bs4,beautifulsoup4",
	        "PIL,Pillow",
	        "cv2,opencv-python",
	        "PySide6"
	    ]
}

Пояснение к атрибутам:

  • name - имя плагина, старайтесь делать его простым и лаконичным, если отсутствует или не валидно, то будет использовано имя пакета;

  • description - короткое описание плагина;

  • project_page_url - ссылка на страницу проекта в интернете;

  • manual_url - ссылка на страницу с документацией в интернете;

  • version - текущая версия плагина, обозначение может иметь только числовые обозначения, разделенные точкой, от 1 до 3 цифр ("1", "1.0", 2.34.25");

  • version_status - статус версии, любое строковое значение, но обычно это обозначение готовности релиза (alpha, beta, pre-release);

  • init_release_date - первый релиз плагина, дата в формате ISO8601 YYYY-MM-DD;

  • update_date - дата релиза текущей версии, формат как у init_release_date

  • developer - имя разработчика ;

  • developer_email - электронная почта разработчика;

  • developer_webpage - веб-страница разработчика;

  • repository_url - ссылка на git репозиторий плагина;

  • forum_url - ссылка на форум;

  • dependencies - набор пакетов, которые нужны для запуска плагина, и которые могут быть загружены из хранилища PyPI; представляет из себя список строк формата: <имя пакета для импорта>,<имя пакета для загрузки утилитой pip>, если имя пакета для импорта совпадает с именем пакета для загрузки, то вторую часть можно упустить, в таком случае запятую ставить не нужно;

  • source_language - локаль оригинального языка, код языка в формате ISO639-1, и код страны по стандарту ISO 3166-2, например, en_US, если данное поле отсутствует или некорректно, то механизм интернационализации задействован не будет.

Папка translations содержит в себе словари переводов для локализации интерфейса, если такая опция не требуется, то ее добавлять не нужно. Внутри данной папки содержатся папки со словарями в формате .QM, имена папок выбираются в соответствии с кодом языка и территории, аналогично тому как выбирается значения для поля source_language в файле манифеста.

Двигаемся дальше. Заглянем в файл plugin.py

from typing import Optional  
  
from PySide6.QtWidgets import QWidget  
  
from PyUB.Types import Plugin  
from PyUB.Types.Properties import PropertyContainer  
from .model.settings import Settings  
from .view.main_widget import MainWidget  
  
  
class MyPlugin(Plugin):  
  
    @classmethod  
    def gui(cls) -> QWidget:
        return MainWidget()  
  
    @classmethod  
    def settings(cls) -> Optional[PropertyContainer]:  
       return Settings

Там имеется класс MyPlugin, который унаследован от Plugin (PyUB.Types). У него есть два метода:
1. gui() - возвращает экземпляр класса QWidget, который собственно из себя представляет интерфейс вашей программы, который Менеджер встроит в свой интерфейс и тот будет предоставлен пользователю;
2. settings() - возвращает класс с пользовательскими настройками, если настройки не требуются, то метод должен возвращать None.
Обратите внимание, все описанные методы являются методами класса (перед объявлением указан декоратор @classmethod).

Следующая остановка ...\view\main_widget.py.

from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout  
  
class MainWidget(QWidget):  
  
    def __init__(self):  
        super().__init__()  
        layout = QVBoxLayout(self)  
        layout.addWidget(QLabel("It's template of the plugin.\nStart coding your cool application!"))

Здесь объявляется класс пользовательского интерфейса плагина, экземпляр которого возвращает метод MyPlugin.gui(). Тем кто работает с PySide6/PyQt разработка виджетов должна быть знакомой процедурой, для этого можно описывать его с помощью кода, или импортировать макет, сделанный в QtDesigner, все на усмотрение создателя.

Заглянем в файл с пользовательскими настройками ...\model\settings.py

from PyUB.Types.Properties import *  
  
class Settings(PropertyContainer):  
    int_property = IntProperty(name="Int property", default_value=5, single_step=1, minimum=0, maximum=10,  
                               tooltip="Input a number", show_reset_btn=True)  
    float_prop = FloatProperty(name="Float property", tooltip="Input a number")

Все настройки помещаются в классе Settings, который должен быть унаследован от PropertyContainer. Далее добавляются атрибуты, которые являются экземплярами Свойств. Свойства - это специальные классы, которые созданы для ввода и хранения определенного типа данных. Смотрите описание для конкретного свойства в документации API. В конструкторе задаются параметры, определяющее поведение свойства, их можно изменять в процессе работы программы. Каждое свойство имеет свой индивидуальный набор параметров, но могу выделить общие для всех:

  • name - имя свойства, нужно для подписи виджета в графическом интерфейса;

  • default_value - значение по умолчанию;

  • tooltip - всплывающая подсказка с описанием, тоже отображается в интерфейсе;

  • show_reset_btn - (может быть не у всех Свойств) задает видимость кнопки сброса значения Свойства, True - кнопка отображается, False - не отображается.

  • widget_enabled - задает активность виджета свойства, True - виджет активен, False - нет. Добавьте в тело класса атрибуты с нужными Cвойствами. Атрибуты других типов тоже допустимы. Служебные атрибуты имеют префикс pc_, имейте в виду, что служебные атрибуты не защищены от переопределения и без знания дела их изменять не следует, так как это может привести к непредсказуемым последствиям. Ниже я привел таблицу, где описаны все доступные свойства.

Имя класса свойства

Тип данных

Пояснение

IntProperty

int

Целое число

FloatProperty

float

Вещественное число

BoolListProperty

tuple[bool, ...]

Кортеж булевых значений

BoolProperty

bool

Булевое значение

ColorProperty

str

Строка с кодом цвета в формате HEX, например, #ffbbcc

FontProperty

Кортеж с данными о шрифте (имя шрифта, стиль, размер)

ComboBoxProperty

int

Индекс выбранного элемента списка

StringProperty

str

Строка

PasswordStringProperty

str

Строка

StringListProperty

tuple[str, ...]

Кортеж строк

FilePathProperty

str

Строка, в которой содержится абсолютный путь к файлу или папке

FilePathListProperty

tuple[tuple[str, str], ...]

Кортеж с абсолютными путями к файлам или папкам, плюс их псевдонимы

Чтобы использовать свойства в коде, просто импортируем класс Settings, и вызываем свойство value для нужного Свойства, например, Settings.int_property.value

Итак, вы познакомились с устройством плагина и этих минимальных сведений достаточно для разработки своих программ, вы можете построить приложение любой сложности, Pyrog не накладывает никаких ограничений.

Как вы видите для того чтобы работать с Pyrog нужно объявить специальный класс плагина, который возвращает ссылку на интерфейс плагина и опционально может предоставлять ссылку на контейнер свойств (PropertyContainer) с пользовательскими настройками, и при этом не нужно заботиться о сохранении данных, система все сделает сама. Но, не бросайте чтение, далее я раскрою многие механики и нюансы системы.

3. Расширяем познания

В предыдущем разделе мы познакомились с основами разработки плагинов, далее я расскажу о:

  • жизненном цикле плагина;

  • как получать сигналы о действиях пользователя;

  • какие есть утилитарные функции;

  • как сделать мультиязычный интерфейс;

  • как устроены Свойства и контейнер свойств;

  • как сделать собственную реализацию Свойства;

  • как сделать собственный вариант интерфейса пользовательских настроек;

  • других нюансах и особенностях.

3.1 Жизненный цикл плагина

Когда пользователь запускает Менеджер, то пакет Плагина импортируется не сразу, а при определенных условиях. Когда пользователь переходит во вкладку плагина, или ранее была установлена опция инициализации на старте Менеджера, или он запрашивает пользовательские настройки, то в данных случаях запускается процедура импорта. Менеджер импортирует пакет и ищет класс Плагина, в зависимости от требований запускается определенный метод:

  • gui() - когда был запрошен интерфейс Плагина (если плагин выключен, то это не произойдет);

  • settings() - запускается в любом случае, чтобы проверить наличие настроек. Основные рабочие модули рекомендую импортировать в методе gui(), а не в теле модуля, ради оптимизации.

Когда загружается контейнер свойств, Менеджер загружает из базы данных записи о значениях параметров и значений Свойств и применяет их, если те были обнаружены, иначе параметры и значения останутся дефолтными. У Свойства параметры определяют его поведение, а значение - это данные, которые он в себе хранит.

Если пользователь деактивирует плагин, то все ранее загруженные модули пакета будут удалены из памяти.

3.2 Ваш универсальный помощник

Для общения с Менеджером используйте класс Helper из PyUB.Types. Вызывайте его конструктор в любом месте вашего кода, в любом случае каждый Плагин может иметь только один экземпляр "Помощника".

Данный класс предоставляет следующие методы:

Метод

Выполняемые действия

save_settings_parameters

Сохраняет параметры Свойств, находящиеся в контейнере, который получен через метод Plugin.settings()

save_settings_values

Сохраняет значения Свойств, находящиеся в контейнере, который получен через метод Plugin.settings()

plugin_dir_abspath

Возвращает строку с абсолютным путем к папке плагина

plugin_localstorage_dir_abspath

Возвращает строку с абсолютным путем к индивидуальной папке в локальном хранилище Менеджера (...\Pyrog\manager\data\plugins_ls<имя индивидуальной папки>)

Helper также имеет ряд сигналов, сообщающих о действиях пользователя

Сигнал

О чем сообщает

plugin_language_changing

Пользователь изменил язык плагина. Нужно для того, чтобы произвести процедуру перевода интерфейса.

Возвращает строку с именем локали языка, например, en_US

plugin_deactivating

Пользователь деактивировал плагин. Отправляется до того, как код плагина будет удален. Полезен для безопасного завершения сессии.

app_closing

Пользователь закрыл Менеджер. Отправляется до того, как программа завершит работу. Полезен для безопасного завершения сессии.

settings_editing_starting

Пользователь открыл пользовательские настройки. Отправляется перед тем как окно редактирования будет выведено на экран. Полезно когда нужно подготовить программу к изменению настроек.

settings_editing_finished

Пользователь завершил редактирование настроек.

Возвращает булевое значение, True - пользователь выбрал сохранить настройки, False - отказался сохранять настройки, если значения Свойств были изменены, то произойдет их откат до состояния перед началом редактирования.

В любом случае вы можете отслеживать изменения значений Свойств на лету.

Весь код в Менеджере выполняется синхронно, это значит исполнение не продолжится пока не выполнится код реакции на сигналы. Напомню, что обработчики сигналам подключаются так:
helper.<сигнал>.connect(<обработчик>).

3.3 Делаем локализацию интерфейса

Итак, для разблокировки механизма интернационализации нужно в файле манифеста указать исходный язык, например, "source_language": "en_GB", таким образом даем системе понять какой язык является оригинальным. Далее в корневой папке плагина создаем директорию translations, а внутри папки для словарей, которые должны иметь имена, состоящее из кода языка и территории. Обратитесь к предыдущему разделу, где мы заполняли файл манифеста. Таким образом папки могут иметь следующие имена: en_US, es_ES, ru_RU, de_DE. Из этих папок будут загружаться словари переводов в формате .QM, вложенные папки будут игнорироваться.

3.3.1 Языковые константы

Языковые константы (ЯК) - это объекты, которые хранят в себе оригинальный текст и текст перевода, добавлены для того чтобы избежать конфликтов при использовании стандартного механизма локализации, когда перевод извлекается из загруженных словарей, например, в случае когда разные плагины используют разные языки и словари для них загружены одновременно, то если оригинальный текст перевода и контекст будут совпадать, то будет получен перевод из словаря, который был загружен последним.
Итак, разберемся как с ними работать.

# файл tranlslations.py
from PyUB.Types import LangConstant

TRANSLATION = LangConstant("Common", "Hello")
APPLE = LangConstant("Common", "apple")

class Errors:	
  Warning = LangConstant("Errors", "Warning")

В коде выше мы импортируем класс LangConstant и объявляем три ЯК в теле модуля и класса, далее я расскажу почему я так сделал. В конструкторе мы задаем следующие аргументы:

  1. контекст

  2. текст константы

  3. (опционально) строка идентификатор, когда один и тот же исходный текст используется в одном контексте, но в разных ролях. Параметры аналогичны тем, что передаются функции PySide6.QtCore.QCoreApplication.translate(), кроме аргумента n.

Извлечь перевод (при его наличии, если он отсутствует, то используется оригинальный текст) можно несколькими способами. Импортируем ранее созданный модуль tranlslations.py

from . import tranlslations as tr

У ЯК переопределен метод str , который конвертирует объект в строку автоматически

>>> print(tr.TRANSLATION)
Привет

либо можно преобразовать объект в строку явно

>>> print(str(tr.TRANSLATION))
Привет

преобразование производится при вызове экземпляра как функции, то есть с добавлением (), реализовано через call

>>> print(tr.TRANSLATION())
Привет

использовать метод get_translation()

>>> print(tr.TRANSLATION.get_translation())
Привет

либо с помощью функции get_lang_const_translation() из модуля PyUB.utils

from PyUB.utils import get_lang_const_translation
>>> print(get_lang_const_translation(tr.TRANSLATION))
Привет

это функции можно передать строку, тогда она просто вернет ее без изменений, это полезной когда на нужно обработать простую строку или ЯК.

Языковые константы поддерживают перевод для множественных форм числительных, на данные момент реализовано только для русского и английского языков. Это работает следующим образом, в модуле tranlslations.py есть переменная APPLE если мы подготовили для нее перевод с учетом множественных форм, то нам нужно передать числитель типа int или float, чтобы извлечь нужную форму множественного числа

>>> print(f"5 {tr.APPLE(5)}")
5 яблоков
>>> print(f"2 {tr.APPLE.get_translation(2)}")
2 яблока
>>> print(f"1 {get_lang_const_translation(tr.APPLE, 1)}")
1 яблоко

Обращаю ваше внимание, что ЯК не производят форматирование строки, как это сделано в стандартной системе Qt, где число подставляется на место метки %n, форматирование нужно будет реализовать самостоятельно. Языковые константы полезны там, где интерфейс создается динамически, их можно не использовать в главном виджете плагина, так как он живет в течение всей сессии, там вы можете использовать стандартную функцию PySide6.QtCore.QCoreApplication.translate()

3.3.2 Процедура перевода

Теперь, пришло время поговорить о том, как обновить переводы в константах и там где он был размечен стандартными средствами Qt. Когда пользователь меняет язык, то класс Helper отправляет сигнал plugin_language_changing с кодом языка, задача разработчика реализовать процедуру перевода.

Рассмотрим подробнее, что происходит в системе по шагам:

  1. Пользователь изменил язык

  2. Менеджер загружает доступные словари для выбранного языка

  3. Менеджер через Helper отправляет сигнал plugin_language_changing плагину о том, что язык изменился и ему нужно провести необходимые процедуры

  4. Если плагин привязал обработчики к сигналу, то производится их выполнение

  5. Менеджер выгружает ранее установленные словари

Итак, нам как разработчикам плагинов нужно сосредоточиться на шаге 4, когда у нас есть окно возможностей между загрузкой и выгрузкой словарей, для этого посмотрим на то, как это реализовано во встроенном плагине TS generator, который можно найти в папке ...Pyrog\manager\plugins\translator , в пакете view в модуле main_widget.py определен метод, который отрабатывает данную процедуру (MainWidget._on_retranslate()), посмотрим на него поближе

from PyUB.Types import  Helper, LangConstant
from PySide6.QtWidgets import QWidget
from . import lang_consts
from PyUB.utils import retranslate_nested_langconstants

class MainWidget(QWidget):  
  
    def __init__(self):  
        super().__init__()  
        self._helper = Helper()        
        self._helper.plugin_language_changing.connect(self._on_retranslate) # привязка обработчика
        ...
        
    def _on_retranslate(self, code):
	    retranslate_nested_langconstants(lang_consts) 
	    self._file_list_input.set_file_filter(f"{lang_consts.SOURCE_FILE()} (*.py *.pyw *.ui *.json)")  
	    self._toolBox.setItemText(0, lang_consts.SOURCE_FILES())  
	    self._toolBox.setItemText(1, lang_consts.SAVE_IN())  
	    self._output_paths.set_file_filter(f"TS-{lang_consts.FILE()} (*.ts)")  
	    self._save_btn.setText(lang_consts.SAVE())  
	    self._load_btn.setText(lang_consts.LOAD())  
	    self._clear_btn.setText(lang_consts.CLEAR())  
	    self._generate_btn.setText(lang_consts.GENERATE())    
        self._add_plural_forms_checkbox.setText(lang_consts.SAVE_PLURAL_FORMS())

В нем с помощью функции retranslate_nested_langconstants() выполняем перевод языковых констант, она сканирует передаваемый модуль или класс на наличие констант и выполняет их перевод, работает даже со вложенными классами, вторым аргументом можно передать код языка, который передает сигнал plugin_language_changing, но это нужно только когда нам нужно получить формы множественного числа, что не нужно в данном случае, также обратите внимание, что вызов этой функции нужно выполнить перед тем как будут использованы языковые константы. После того как константы переведены, производится их применение для виджетов интерфейса.

Когда мы используем QtDesigner для создания форм интерфейса, то в коде формы, которую он генерирует есть метод retranslateUi(), можно подключить его к сигналу plugin_language_changing или вызвать его в другом обработчике данного сигнала.

3.3.3 Подготовка словарей

После того как работа над кодом плагина закончена, можно приступить к локализации. Исходники нужно преобразовать в TS файлы, а затем перевести исходные тексты на нужный язык и скомпилировать их в QM словари. Несколько лет назад я уже писал статью на эту тему, хотя она актуальна для PySide2, но в целом принцип не изменился.

Для создания TS файлов из исходников есть встроенная утилита TS generator. Перейдите в её настройки и установите адрес папки, в которой будут сохраняться временные файлы, и путь к файлу lupdate, он находится в папке, где устанавливаются пакеты Python, для Windows это C:\Users<user name>\AppData\Roaming\Python\Python313\site-packages\PySide6. После перейдите на вкладку плагина, в разделе Исходные файлы выберите те файлы, которые желаете преобразовать, принимаются следующие типы:

  • Python файлы (.py и .pyw )

  • Файл манифеста (manifest.json)

  • Файлы форм, созданных в QtDesigner (.ui) (код созданный с его помощью не обрабатывается!) В разделе Сохранить в укажите путь к TS файлам, если файлы не существуют, то они будут созданы, а существующие будут обновлены, поэтому когда вы обновляет код, то можете записывать изменения поверх, все ранее добавленные переводы будут сохранены. Отметьте флаг Добавить формы числительным если нужно, чтобы языковые константы поддерживали формы множественного числа.

утилита импортирует исходные файлы Python и выполняет их код для поиска в них языковых констант, поэтому в коде не должно быть ошибок, приводящих к исключению!

Итак, мы получили файлы с исходными текстами, теперь нужно их перевести на целевой язык. Запускаем утилиту Qt Linguist  и открываем наши TS-файлы. При первой загрузке нужно выбрать язык оригинала и язык перевода. У утилиты есть одна фишка - можно открыть сразу несколько TS файлов, чтобы сразу выполнить перевод на несколько языков, такое поведение может быть несколько непривычным.

В разделе Контекст мы видим, что все текстовые данные сгруппированы в соответствии с тем текстом, который был перед методу translate() или языковой константе через аргумент context. В разделе Строки выделяем строку и в поле перевод на целевой язык вводим соответственно перевод исходного текста и жмем Alt+Enter, тогда строка будет помечена зеленой галочкой. Проводим эту операцию над всем строками, если некоторые из них не будут иметь перевода, то текст останется оригинальным – все просто.

После завершения перевода нужно скомпилировать файлы словарей, для этого нужно выбрать одну из команд Скомпилировать в разделе Файл главного верхнего меню. Файлы сохраняем в папке <наш плагин>/translations/<язык локализации>/<имя словаря>.qm.

Также есть возможность скомпилировать файлы переводов утилитой lrelease, можно прочитать информацию по ней здесь.

Сейчас можно не делать перевод полностью руками, а отдать TS-файл нейросети и дать ей команду сделать переводы на нужные языки, они сами "знают" разметку файлов и никак ее не ломают. Я пробовал сделать это с помощью Claude и Deepseek. Просто передал им файл и задал команду: Это ts файл pyside6 исходный язык английский, там уже есть перевод на русский. Замени русский перевод на французский. После останется только проверить перевод и скомпилировать словарь.

3.4 Система свойств

3.4.1 Как работать со Свойствами

Свойство в контексте Pyrog это программная единица, которая хранит в себе данные определенного типа; имеет набор параметров, которые определяют его поведение; и графический интерфейс для того чтобы пользователь мог манипулировать хранящимся значением.

Рассмотрим строение Свойства на конкретном примере PyUB.Types.Properties.FloatProperty, это Свойство для ввода вещественного числа, он имеет следующий конструктор:

def __init__(self, default_value: float = .0, name=tr.UNNAMED, minimum=.0, maximum=10.0,single_step=1.0, decimals=2, tooltip="", show_reset_btn=True):

Список аргументов-параметров следующий

Параметр

Тип

Описание

default_value

float

значение по умолчанию

name

str \| LangConstant

имя Свойства, используется для подписи в интерфейсе

minimum

float

минимальное значение

maximum

float

максимальное значение

single_step

float

единичный шаг, шаг изменения значения при нажатии на кнопки виджета QDoubleSpinBox

decimals

int

количество знаков после запятой

tooltip

str \| LangConstant

всплывающая подсказка с описание свойства

show_reset_btn

bool

флаг того, будет ли показана кнопка сброса значения до дефолтного рядом с виджетом, если значение Свойства не будет равно дефолтному

Как ранее было сказано, каждое свойство имеет свой набор параметров, каждый параметр является свойством Python, которое можно изменять непосредственно из программы. При изменении параметров действуют строгие правила проверки передаваемых значений, нужно передавать значение только установленного типа, также, например, в случае с FloatProperty и IntProperty параметр minimum должен быть строго меньше maximum и наоборот, в противном случае будет возбуждено исключение, поэтому при изменении диапазона нужно следить за порядком изменения параметров.

Реакция значения Свойства на изменения параметров
Значение Свойства должно находиться в диапазоне, установленном параметрами, таким образом если minimum = -10.0 и maximum=10.0, то value должно находиться строго в данном диапазоне. Если мы изменяем maximum и сделаем равным 5.0, в то время как value будет равно 7.1, то значение "прилипнет" к ближайшей границе диапазона и будет равно 5.0. Таким образом в отношении значений Свойств действует "мягкая валидация", ему можно передавать совершенно любые данные, и механизм валидации будет стараться их привести к нужному типу и диапазону значений.

Как работает мягкая валидация
Вы уже видели, что значение изменяется в соответствии с установленным диапазоном значений, заданным параметрами Свойства, также, например следующие литералы будут успешно преобразованы в тип float: "5.2", "5", 4, False, True, то есть целые числа и числа в виде строк, булевые значения преобразуются в тип float и приводятся в случае необходимости к нужному диапазону.

Сохранение параметров Свойств в базе данных Менеджера
Если по каким-то причинам вам нужно изменять значения параметров и при следующем запуске плагина их нужно восстановить, то у Helper используйте метод save_settings_parameters(). Внимание, это работает только для контейнера свойств, который используется для пользовательских настроек, но ничто не мешает извлечь и сохранить эти данные самостоятельно, о том как их получить будет рассказано при разборе контейнера свойств.

Как получить уведомление об изменении значения
Свойства поддерживают сигналы, у каждого из них есть сигнал value_changed, вы может подключить обработчик или несколько, который будет срабатывать при каждом изменении значения Свойства. Когда Свойства используются в составе контейнера, можно получать сигнал от него, о чем будет рассказано далее.

Как получить доступ к графическому интерфейсу
Каждое Свойство имеет метод get_input_widget(), который возвращает виджет для редактирования значения, его вы может использовать для встраивания в свой интерфейс.

3.4.2 Как работать с контейнером свойств

По своей сути Контейнер свойств это обычный класс, унаследованный от PropertyContainer, внутри которого объявляются атрибуты, которым присваиваются экземпляры Свойств, только не изменяйте встроенные атрибуты, чтобы не нарушить работу программы. Контейнер свойств нужен для удобной манипуляции Cвойствами и выполняет следующую работу:

  • пакетно извлекает/устанавливает параметры и значения Свойств;

  • рендерит пользовательский интерфейс для всех Свойств;

  • имеет единый сигнал, уведомляющий об изменении значения любого из Свойств.

Заполняйте ваш контейнер нужными Свойствами

from PyUB.Types.Properties import *  
  
class Settings(PropertyContainer):  
    int_property = IntProperty(name="Int property", default_value=5, single_step=1, minimum=0, maximum=10,  
    tooltip="Input a number", show_reset_btn=True) 
    
    float_property = FloatProperty(name="Float property", tooltip="Input a number")

Затем в любом месте кода импортируйте контейнер и можете обращаться к вложенным Свойствам

from .settings import Settings  
  
print(Settings.int_property.value) # вывести значение свойства
Settings.int_property.value = 7 # изменить значение свойства

print(Settings.int_property.default_value) # вывести дефольное значение
Settings.int_property.default_value = 9 # измениим дефолтное значение

Чтобы получить уведомление об изменение значений используйте нотификатор

from .settings import Settings  
  
notificator = Settings.pc_notifier() # получить нотификатор
notificator.property_value_changed(<обработчик сигнала>)

Сигнал property_value_changed передает ссылку на Свойство, в котором изменилось значение, поэтому обработчик можно реализовать подобным образом

def handler(prop: Property):
	match prop:
		case Settings.int_property:
			print("Изменено значение свойства IntProperty")
		case Settings.float_property:
			print("Изменено значение свойства FloatProperty")

Если нотификатор стал не нужен, то удалите его методом pc_delete_notifier().

Как получить доступ к общему графическому интерфейсу Свойств
Контейнер свойств может отрендерить графический интерфейс, через который пользователь имеет возможность манипулировать значениями Свойств, по умолчанию все виджеты выводятся единым списком (в левом столбце - имена Свойств; в правом - виджеты Свойств)

Диалоговое окно редактирования настроек плагина
Диалоговое окно редактирования настроек плагина

Дефолтный интерфейс дает возможность фильтровать Свойства по имени, а также сбросить значения до дефолтных для всех Свойств. Данный интерфейс можно переопределить, чтобы тот соответствовал вашим требованиям, об этом будет рассказано далее.

Как пакетно извлечь/ установить все значения и параметры Свойств
Например, вы хотите получить весь набор параметров и значений для всех Свойств в контейнере, для этого есть ряд методов:

  • pc_prop_values_as_dict() - возвращает значения Свойств в виде словаря, где ключ - имя атрибута в контейнере свойств, значение - кортеж, где указан тип Свойства и его значение.

  • pc_prop_params_as_dict() - возвращает параметры Свойств в виде словаря, где ключ - имя атрибута в контейнере свойств, значение - кортеж, где указан тип Свойства и словарь со значениями его параметров.

Чтобы установить значения и параметры используются методы:

  • pc_set_prop_values_from_dict() - принимает словарь в том же формате, что возвращает метод pc_prop_values_as_dict() и обновляет значения Свойств, система производит проверку на соответствие типу существующего Свойства и типу его значения, если проверка будет провалена значение не будет обновлено.

  • pc_set_prop_params_from_dict() - принимает словарь в том же формате, что возвращает метод pc_prop_params_as_dict() и производит обновление параметров Свойств, производится проверка на соответствие типу Свойства, если он не идентичен, то обновления не происходит.

3.4.3 Как разработать собственное Свойство

Может так случиться, что встроенных Свойств вам будет недостаточно, для таких случае я составил пошаговый гайд по разработке собственного Cвойства, и разбирать мы его будем на примере BoolListProperty, данное свойство задает кортеж значений типа bool. Итак, приступим.

Шаг 1
Объявляем класс и создаем сигнал. Имя сигнала не изменять!

class BoolListProperty(Property): # создаем новый класс
value_changed = Signal(tuple)  # создаем сигнал, посылаемый при изменении значения. Имя не изменять! В качестве аргумента указать тип значения свойства

Шаг 2
Задание схемы параметров Свойства, сохраняется в атрибуте paramschema

_param_schema = {
        '_items': {'type': tuple,
                   'comparison': {
                       'keys': [],
                       'validator': lambda self, args: all(isinstance(item, str | LangConstant) for item in self),
                       'error_msg': "all elements of [items] must be of str or LangConstant type",
                   }
                   },
        '_name': {'type': str | LangConstant},
        '_tooltip': {'type': str | LangConstant},
        '_show_reset_btn': {'type': bool},
        '_default_value': {
            'type': tuple,
            'comparison': {
                'keys': ["_items"],
                'validator': lambda self, args: len(self) == len(args[0]) and all(isinstance(item, bool) for item in self),
                'error_msg': "[default_value] and [items] must be the same length; all elements must be of bool type"
            }
        }
    }

Данный словарь представляет из себя схему валидации параметров по типу и значению. Как говорилось ранее, в отношении параметров работают строгие правила валидации. Рассмотрим одну запись

'_default_value': {
            'type': tuple,
            'comparison': {
                'keys': ["_items"],
                'validator': lambda self, args: len(self) == len(args[0]) and all(isinstance(item, bool) for item in self),
                'error_msg': "[default_value] and [items] must be the same length; all elements must be of bool type"
            }
        }

Данный параметр определяет дефолтное значение Свойства, ключ 'type' принимает тип параметра, в данном случае - кортеж; ключ 'comparison' является опциональным и нужен тогда, когда нужна сложная проверка значения, значение представляет из себя другой словарь, где:

  • 'keys' - список ключей параметров, которые участвуют в проверке, в данном случае нужен ключ _items;

  • 'validator' - функция валидатора, где 2 аргумента: 1-проверяемое значение, 2 - список значений параметров, которые были перечислены в 'keys', в случае удачной проверки должен вернуть True;

  • 'error_msg' - сообщении ошибки, если валидатор вернул False

Рассмотрим функцию-валидатор

lambda self, args: len(self) == len(args[0]) and all(isinstance(item, bool) for item in self)

параметр default_value должен принимать кортеж со значениями типа bool, аргумент self принимает его значение, args - в данному случае список с одним элементом - значением параметра _items; далее производится проверка, чтобы длины кортежей были равными и чтобы все значения кортежа были типа bool.

Шаг 3
Создаем свойства для всех параметров, используя функцию create_param_property() (PyUB.Type.Properties.utils). В качестве аргумента указываем строку с именем параметра, определенного в paramschema, функция создает приватный атрибут в классе, идентичный имени ключа.

items = create_param_property("_items")  
  
tooltip = create_param_property("_tooltip", validate_value=False)  
  
name = create_param_property("_name", validate_value=False, update_widget=False)  
  
show_reset_btn = create_param_property("_show_reset_btn", validate_value=False, update_widget=False") 
  
default_value = create_param_property("_default_value", validate_value=False, update_widget=False)

Функция create_param_property() имеет параметры:

  • attr_name - имя атрибута которое будет создано в экземпляре класса, задает алгоритм валидации на основе одноименного параметра в paramschema

  • validate_value - флаг валидации значения, если True (установлено по умолчанию) - выполняет валидацию значения после изменения параметра, False - не производит

  • update_widget - флаг обновления виджета, если True (установлено по умолчанию) - выполняет обновление виджета при изменении параметра, False - не выполняет

  • doc - строка документации

Шаг 4
Пишем конструктор. В модуле tr находятся служебные языковые константы, перед использованием надо импортировать

from . import language_constants as tr
def __init__(self, items: tuple[str | LangConstant, ...] = (), default_value: tuple[bool, ...] = (), name=tr.UNNAMED,  
             tooltip="", show_reset_btn=True):  
    super().__init__()  
  
    self.set_parameters_from_dict(
    {'_name': name,  
    '_tooltip': tooltip,  
    "_items": items,  
    "_show_reset_btn": show_reset_btn,  
    "_default_value": default_value}) 
     
    self.value = default_value    

Набор аргументов должен совпадать с набором параметров.

Шаг 5
Пишем метод создания виджета ввода

  def get_input_widget(self) -> QListWidget:  
    if hasattr(self, "_widget"):  
        return self._widget  
  
    self._widget = Resetter(QListWidget())  
    self._widget.child_widget.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum))  
    self._widget.reset_requested.connect(self.reset_value)
    self._widget.child_widget.itemChanged.connect(self._on_widget_value_changed)  
    self._update_widget_params()  
    self._update_widget_value()  
  
    return self._widget

На первом шаге проверяем был ли ранее создан виджет, если был, то возвращаем его. Обратите внимание, сам экземпляр виджета должен сохранятся в приватном атрибуте widget. Затем оборачиваем его в Resetter, этот класс-обертка нужен для создания рядом с виджетом кнопки для сброса значения. Затем устанавливаем политику размера виджета, вложенный виджет доступен через свойство childwidgets у Resetter. Когда пользователь вызывает сброс значения, то Resetter отправляет сигнал reset_requested, и мы привязываем метод reset_value() самого свойства в качестве обработчика. Затем сигналу itemChanged виджета подключаем обработчик onwidget_value_changed и в конце вызываем методы для обновления параметров и значения виджета.

Шаг 6
Пишем методы обновления параметров и значения виджета

 def _update_widget_value(self) -> None:  
    self._widget.child_widget.blockSignals(True)  
    for index, value in enumerate(self.items):  
        item = self._widget.child_widget.item(index)  
        if item:  
            if item.text() != utils.get_lang_const_translation(value):  
                item.setText(utils.get_lang_const_translation(value))  
            item_check_state = True if item.checkState() == Qt.CheckState.Checked else False  
            if self.value[index] != item_check_state:  
                item.setCheckState(Qt.CheckState.Checked if self.value[index] else Qt.CheckState.Unchecked)  
        else:  
            item = QListWidgetItem(utils.get_lang_const_translation(value))  
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)  
            item_state = Qt.CheckState.Checked if self.value[index] else Qt.CheckState.Unchecked  
            item.setCheckState(item_state)  
            self._widget.child_widget.addItem(item)  
  
    if self._widget.child_widget.count() > len(self.items):  
        for i in range(len(self.items), self._widget.child_widget.count()):  
            self._widget.child_widget.takeItem(i)  
    self._adjust_widget_height()  
    self._widget.set_reset_btn_visibility(self._value != self._default_value and self.show_reset_btn)  
    self._widget.child_widget.blockSignals(False)  
  
def _update_widget_params(self) -> None:  
    if hasattr(self, '_widget'):  
        self._widget.child_widget.setToolTip(utils.get_lang_const_translation(self.tooltip))  
        self._update_widget_value()

В методе updatewidget_value() производится обновление виджета с учетом обновленного значения Свойства, также процедура срабатывает при изменении параметра items. Этот метод всегда вызывается в случаях, когда значение Свойства изменилось. На первом этапе проверяются существующие элементы в списке, текст элемента сравнивается с тем, что есть в items на данной позиции и если нужно - обновляет его; также сравнивается состояние флага со значением в кортеже value для текущей позиции и также в случае необходимости его обновляет; если в списке не хватает элементов, то добавляет их. На втором этапе производится проверка на то, нет ли в списке лишних элементов, если в списке виджета больше элементов, чем есть в items, то лишние из них удаляются. В конце производится обновление состояния видимости кнопки сброса Resetter.

В методе updatewidget_params() выполняется код только при условии, что объект имеет атрибут widget. В данном случае для виджета устанавливается текст всплывающей подсказки. И вызывается метод update_widget_value(), это нужно в данном примере, так как он связан с параметром items.

Используем функцию get_lang_const_translation() для извлечения перевода из языковой константы или выводит исходный текст.

Шаг 7
Пишем обработчик события: изменение значения в виджете

 def _on_widget_value_changed(self) -> None:  
    self._set_value(tuple(True if (self._widget.child_widget.item(i).checkState() == Qt.CheckState.Checked) else False for i in range(self._widget.child_widget.count())))  
    self._widget.set_reset_btn_visibility(self._value != self._default_value and self.show_reset_btn)

Данный метод вызывается, когда в виджете изменилось значение. Здесь производится обновление значения Свойства в соответствии с данными виджета и обновление видимости кнопки сброса у Resetter.

Альтернативный способ обновления видимости кнопки сброса; для его использования нужно в конструкторе передать ссылку на Свойство.

def get_input_widget(self) -> QListWidget:
	...
	self._widget = Resetter(QListWidget(), self)
...
self._widget.update_reset_btn_visibility()

Метод update_reset_btn_visibility() сам сравнит значение Свойства с дефолтным и учтет значение параметра show_reset_btn.

Шаг 8
Пишем метод валидации значения

def _validate_value(self):  
    value = self._value  
  
    if not isinstance(value, Sequence):  
        self._value = self.default_value  
        return  
  
    if not isinstance(value, tuple):  
        self._value = tuple(value)  
  
    if len(value) > len(self.items):  
        self._value = self._value[:len(self.items)]  
    elif len(value) < len(self.items):  
        add_items = len(self.items) - len(value)  
        self._value = self._value + tuple(False for i in range(add_items))  
  
    if not all(isinstance(item, bool) for item in self._value):  
        self._value = tuple(bool(item) for item in self._value)

Примите во внимание, что валидация производится уже после обновления значения. В случае данного Свойства валидация производится в несколько этапов. Сначала проверяем является ли значение последовательностью, если нет, то сбрасываем его до дефолтного и завершаем процедуру. На следующем этапе узнаем является ли значение кортежем, если нет, то преобразуем последовательность в него. Потом длину кортежа сравниваем с длиной items, если значение длиннее, то лишние элементы удаляются, если короче, то добавляются недостающие элементы, имеющие значение False. На завершающем этапе проверяется: все ли элементы имеют тип bool, если это не так, то все они конвертируются в данный тип, по правилам преобразования.

Важно! в процедуре валидации присваивайте значения атрибуту _value, а не свойству value, иначе программа войдет в бесконечный цикл.

Шаг 9 (ситуативный)
Иногда нужно переопределить код свойства value, например, как это было нужно для Свойства FloatProperty, так как код класса Property использует для сравнения значений операцию !=, что не корректно использовать с объектами типа float. Таким образом, если тип данных Свойства не поддерживает сравнение на равенство, то код свойства value придется переопределить.

3.4.4 Разработка кастомного интерфейса для контейнера свойств

Если вас не удовлетворяет стандартный интерфейс вывода виджетов Свойств, то ничего не мешает сделать свой вариант. Давайте сделаем интерфейс, где Свойства разбиты на категории, и каждая из них занимает отдельную вкладку. Ниже показана моя реализация.

Кастомный интерфейс редактирования значений Свойств
Кастомный интерфейс редактирования значений Свойств


Для начала создам типовой плагин, скопировав шаблон и в файле Settings.py заполню контейнер Свойствами.

class Settings(PropertyContainer):  
    daughter_name_t1 = StringProperty(name="Daughter's name")  
    son_name_t1 = StringProperty(name="Son's name")  
    mother_name_t1 = StringProperty(name="Mother's name")  
    father_name_t1 = StringProperty(name="Father's name")  
  
    daughter_birthdate_t2 = StringProperty(name="Daughter's birthdate")  
    son_birthdate_t2 = StringProperty(name="Son's birthdate")  
    mother_birthdate_t2 = StringProperty(name="Mother's birthdate") 
    father_birthdate_t2 = StringProperty(name="Father's birthdate")  
  
    num1_t3 = IntProperty(name="Num 1's name")  
    num2_t3 = FloatProperty(name="Num 2's name")  
    num3_t3 = IntProperty(name="Num 3's name")  
  
    action1_t4 = StringProperty(name="Action 1")  
    action2_t4 = StringProperty(name="Action 2")  
    action3_t4 = StringProperty(name="Action 3")  
    action4_t4 = StringProperty(name="Action 4")  
    action5_t4 = StringProperty(name="Action 5")

Для идентификации категории я добавил именам соответствующие суффиксы tX. Далее переопределяем метод pcrender_gui()

@classmethod  
def pc_render_gui(cls) -> QWidget:  
    tab_widget = QTabWidget()  
  
    tab_suffixes = [f"_t{i+1}" for i in range(4)]  
    tab_names = ["Names", "Birthdates", "Nums", "Actions"]  
  
    for suffix, tab_name in zip(tab_suffixes, tab_names):  
        widget = QWidget()  
        scroll_area = QScrollArea()  
        scroll_area.setWidgetResizable(True)  
        scroll_area.setWidget(widget)  
        layout = QFormLayout(widget)  
        layout.setVerticalSpacing(15)  
        layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | \  
                                 Qt.AlignmentFlag.AlignTrailing | \ 
                                 Qt.AlignmentFlag.AlignVCenter)  
  
        properties = [prop for name, prop in cls.pc_properties() if name.endswith(suffix)]  
        for row, prop in enumerate(properties):  
            sign_label = QLabel(prop.get_name())  
            sign_label.setWordWrap(True)  
            layout.setWidget(row, QFormLayout.ItemRole.LabelRole, sign_label)  
            layout.setWidget(row, QFormLayout.ItemRole.FieldRole, prop.get_input_widget())  
        tab_widget.addTab(scroll_area, tab_name)  
    return tab_widget

Тут все просто, генерируем список суффиксов для отбора Свойств; затем для каждого суффикса создаем виджет QWidget, вкладываем его в область прокрутки QScrollArea, чтобы длинные списки не вылезали за пределы экрана, и виджету задаем макет QFormLayout, который выводит виджеты в 2 столбца; далее отсеиваем свойства по имени с нужным суффиксом, для получения доступа к Свойствам используем метод pc_properties() - это функция-генератор, которая выводит все Свойства в контейнере, возвращает кортеж (<имя атрибута>, <ссылка на экземпляр Свойства>); далее располагаем виджеты в макете и готовый виджет раздела добавляет в качестве вкладки экземпляру QTabWidget, в конце возвращаем его качестве результата метода.

3.5 Несколько слов о недостатках

При импорте плагины выполняются не в изолированной среде, а фактически становятся частью всей программы и каждому из них доступен весь код в рамках сессии. Таким образом любой плагин может вмешаться в работу Менеджера и других плагинов, только такое возможно при намеренном вредительстве, это нужно учитывать при использовании сторонних плагинов. Так или иначе нарушить выполнение может и допущенная ошибка в коде, например, занять весь главный поток приложения, что повесит всю программу и потребует её перезапуска, поэтому рекомендуется выполнять ресурсоемкие операции в отдельных потоках. Решение проблемы с зависанием на данный момент рассматривается и возможно в скором времени он будет внедрено.

Программа поставляется в виде скриптов, а не в виде готовой сборки. Да, для запуска программы нужен установленный Python, для кого-то это может быть недостатком, так как для запуска нужно скачивать и устанавливать Python. Возможно в будущем варианты поставки расширятся.

Программа является портативной и не создает рабочие файлы в операционной системе, поэтому ее нельзя будет использовать в многопользовательском режиме, зато можно записать ее на съемный носитель и запускать на другой машине.

Проект будет развиваться, будут добавляться новые функции и возможности, а существующие дорабатываться. Совместимость API будет сохраняться в любом случае.

4. Вместо заключения

Спасибо всем, кто внимательно дочитал статью до конца. Сегодня вы узнали как создавать плагины для Pyrog на Python + PySide6, надеюсь вы найдете эту программу полезной и для себя. Кто знает, может мы вместе сделаем из этого нечто грандиозное, или хотя что принесет пользу другим людям. Если у вас есть предложения по улучшению программы, то не стесняйтесь высказать свое мнение в комментариях. Подписывайтесь на телеграм канал чтобы быть первым в курсе свежих обновлений. Если хотите помочь проект, то сообщите о нем в социальных сетях.

Ссылки
🏠Страничка проекта (скачать бесплатно)
🔗Git репозиторий проекта
🆘Страница вики
Больше в Telegram-канале
📧Почтовый ящик для отзывов и предложений
👑Поддержать проект