Инструменты пользователя

Инструменты сайта


products:pyrog:tutorials:dev:main

Различия

Показаны различия между двумя версиями страницы.

Ссылка на это сравнение

Предыдущая версия справа и слеваПредыдущая версия
products:pyrog:tutorials:dev:main [2026/07/25 18:47] – [2.3.2 Процедура перевода] ironmeshproducts:pyrog:tutorials:dev:main [2026/08/01 23:09] (текущий) ironmesh
Строка 2: Строка 2:
  
 Для разработки собственного приложения нужно иметь минимум базовые навыки разработки на Python и PySide6. Если возникли вопросы, присоединяйтесь к [[https://discord.gg/A7AfhnSacA|💬Форуму]] Для разработки собственного приложения нужно иметь минимум базовые навыки разработки на Python и PySide6. Если возникли вопросы, присоединяйтесь к [[https://discord.gg/A7AfhnSacA|💬Форуму]]
 +
  
 ===== 1. Быстрый старт ===== ===== 1. Быстрый старт =====
Строка 30: Строка 31:
 </code> </code>
  
-В файле ''__init__.py'' должен находится атрибут с именем ''plugin'', который является ссылкой на класс плагина, данный класс может быть объявлен в самом файле или в любом другом, который затем будет импортирован. В принципе, можно все приложение написать в файле ''____init____.py'', но лично я бы так делать не стал, а вот сам класс - вполне. В шаблоне только одна строчка кода, где из модуля plugin импортируется класс **MyPlugin **под псевдонимом ''plugin''.+В файле ''____init____.py'' должен находится атрибут с именем ''plugin'', который является ссылкой на класс плагина (о том, как создать такой класс будет сказано позднее), данный класс может быть объявлен в самом файле или в любом другом, который затем будет импортирован. В принципе, можно все приложение написать в файле ''____init____.py'', но лично я бы так делать не стал, а вот сам класс - вполне. В шаблоне только одна строчка кода, где из модуля //plugin// импортируется класс **MyPlugin** под псевдонимом ''plugin''.
  
 <code python> <code python>
Строка 88: Строка 89:
 | **forum_url** | ссылка на форум | "[[https://myforum.org]]" | | **forum_url** | ссылка на форум | "[[https://myforum.org]]" |
 | **dependencies** | набор пакетов, которые нужны для запуска плагина, и которые могут быть загружены из хранилища PyPI; представляет из себя список строк формата: ''<имя пакета для импорта>,<имя пакета для загрузки утилитой pip>'', если имя пакета для импорта совпадает с именем пакета для загрузки, то вторую часть можно упустить, в таком случае запятую ставить не нужно | ["bs4,beautifulsoup4", "PIL,Pillow",         "cv2,opencv-python",         "PySide6"] | | **dependencies** | набор пакетов, которые нужны для запуска плагина, и которые могут быть загружены из хранилища PyPI; представляет из себя список строк формата: ''<имя пакета для импорта>,<имя пакета для загрузки утилитой pip>'', если имя пакета для импорта совпадает с именем пакета для загрузки, то вторую часть можно упустить, в таком случае запятую ставить не нужно | ["bs4,beautifulsoup4", "PIL,Pillow",         "cv2,opencv-python",         "PySide6"] |
-| **source_language** | локаль оригинального языка, код языка в формате ISO639-1 и код страны согласно стандарту ISO 3166-2,  если данное поле отсутствует или некорректно, то механизм интернационализации задействован не будет | "en_US" |+| **source_language** | локаль оригинального языка, код языка в формате [[https://ru.wikipedia.org/wiki/ISO_639-1|ISO639-1]] и код страны согласно стандарту [[https://en.wikipedia.org/wiki/ISO_3166-2|ISO 3166-2]],  если данное поле отсутствует или некорректно, то механизм интернационализации задействован не будет | "en_US" |
  
-Папка ''translations'' содержит в себе словари переводов для локализации интерфейса, если такая опция не требуется, то ее добавлять не нужно. Внутри данной папки содержатся папки со словарями в формате ''.QM'', имена папок выбираются в соответствии с кодом языка и территории, аналогично тому как выбирается значения для поля ''source_language'' в файле манифеста.+Папка ''translations'' содержит в себе словари переводов для локализации интерфейса, если такая опция не требуется, то ее добавлять не нужно. Внутри данной папки содержатся папки со словарями в формате ''.QM'', имена папок выбираются в соответствии с кодом языка (согласно стандарту [[https://ru.wikipedia.org/wiki/ISO_639-1|ISO639-1]]) и территории (согласно стандарту [[https://en.wikipedia.org/wiki/ISO_3166-2|ISO 3166-2]]), аналогично тому как выбирается значения для поля ''source_language'' в файле манифеста.
  
 Двигаемся дальше. Заглянем в файл ''plugin.py'' Двигаемся дальше. Заглянем в файл ''plugin.py''
Строка 148: Строка 149:
 </code> </code>
  
-Все настройки помещаются в классе ''Settings'', который должен быть унаследован от ''PropertyContainer''. Далее добавляются атрибуты, которые являются экземплярами Свойств. Свойства - это специальные классы, которые созданы для ввода и хранения определенного типа данных. Смотрите описание для конкретного свойства в документации API. В конструкторе задаются параметры, определяющее поведение свойства, их можно изменять в процессе работы программы. Каждое свойство имеет свой индивидуальный набор параметров, но могу выделить общие для всех:+Все настройки помещаются в классе ''Settings'', который должен быть унаследован от ''PropertyContainer''. Далее добавляются атрибуты, которые являются экземплярами Свойств. Свойства - это специальные классы, которые созданы для ввода и хранения данных определенного типа. Смотрите описание для конкретного свойства в документации API. В конструкторе задаются параметры, определяющее поведение свойства, их можно изменять в процессе работы программы. Каждое свойство имеет свой индивидуальный набор параметров, но могу выделить общие для всех:
  
   * **name** - имя свойства, нужно для подписи виджета в графическом интерфейса;   * **name** - имя свойства, нужно для подписи виджета в графическом интерфейса;
Строка 154: Строка 155:
   * **tooltip** - всплывающая подсказка с описанием, тоже отображается в интерфейсе;   * **tooltip** - всплывающая подсказка с описанием, тоже отображается в интерфейсе;
   * **show_reset_btn** - (может быть не у всех Свойств) задает видимость кнопки сброса значения Свойства, ''True'' - кнопка отображается, ''False'' - не отображается.   * **show_reset_btn** - (может быть не у всех Свойств) задает видимость кнопки сброса значения Свойства, ''True'' - кнопка отображается, ''False'' - не отображается.
-  * **widget_enabled** - задает активность виджета свойства, ''True'' - виджет активен, ''False'' - нет. Добавьте в тело класса атрибуты с нужными Cвойствами. Атрибуты других типов тоже допустимы. Служебные атрибуты имеют префикс ''pc_'', имейте в виду, что служебные атрибуты не защищены от переопределения и без знания дела их изменять не следует, так как это может привести к непредсказуемым последствиям. Ниже я привел таблицу, где описаны все доступные свойства.+  * **widget_enabled** - задает активность виджета свойства, ''True'' - виджет активен, ''False'' - нет.  
 +  
 +Добавьте в тело класса атрибуты с нужными Cвойствами. Атрибуты других типов тоже допустимы. Служебные атрибуты имеют префикс ''pc_'', имейте в виду, что служебные атрибуты не защищены от переопределения и без знания дела их изменять не следует, так как это может привести к непредсказуемым последствиям. Ниже я привел таблицу, где описаны все доступные свойства.
  
 ^ Имя класса свойства ^ Тип данных ^ Пояснение ^ ^ Имя класса свойства ^ Тип данных ^ Пояснение ^
Строка 169: Строка 172:
 | FilePathProperty | str | Строка, в которой содержится абсолютный путь к файлу или папке | | FilePathProperty | str | Строка, в которой содержится абсолютный путь к файлу или папке |
 | FilePathListProperty | tuple[tuple[str, str], ...] | Кортеж с абсолютными путями к файлам или папкам, плюс их псевдонимы | | FilePathListProperty | tuple[tuple[str, str], ...] | Кортеж с абсолютными путями к файлам или папкам, плюс их псевдонимы |
-| PlainTextProperty | str | Строка, отличается от StringListProperty тем, что предоставляет многострочное поле ввода |+| PlainTextProperty | str | Представляет строковое значение. Отличается от **StringListProperty** тем, что предоставляет многострочное поле ввода |
 | DateTimeProperty | str | Дата и время в виде строки, в формате: ''2026-12-25_01:25:16 (<год>-<месяц>-<числов>_<час>:<минута>:<секунда>)'' | | DateTimeProperty | str | Дата и время в виде строки, в формате: ''2026-12-25_01:25:16 (<год>-<месяц>-<числов>_<час>:<минута>:<секунда>)'' |
  
Строка 193: Строка 196:
 ==== 2.1 Жизненный цикл плагина ==== ==== 2.1 Жизненный цикл плагина ====
  
-Когда пользователь запускает Менеджер, то пакет Плагина импортируется не сразу, а при определенных условиях. Когда пользователь переходит во вкладку плагина, или ранее была установлена опция инициализации на старте Менеджера, или он запрашивает пользовательские настройки, то в данных случаях запускается процедура импорта. Менеджер импортирует пакет и ищет класс Плагина, в зависимости от требований запускается определенный метод:+Когда пользователь запускает Менеджер, то пакет Плагина импортируется не сразу, а при определенных условиях: когда пользователь переходит во вкладку плагина, или если ранее была установлена опция инициализации на старте Менеджера, или он запрашивает пользовательские настройки, то в данных случаях запускается процедура импорта. Менеджер импортирует пакет и ищет класс Плагина, в зависимости от требований запускается определенный метод:
  
   * ''gui()'' - когда был запрошен интерфейс Плагина (если плагин выключен, то это не произойдет);   * ''gui()'' - когда был запрошен интерфейс Плагина (если плагин выключен, то это не произойдет);
   * ''settings()'' - запускается в любом случае, чтобы проверить наличие настроек. Основные рабочие модули рекомендую импортировать в методе ''gui()'', а не в теле модуля, ради оптимизации.   * ''settings()'' - запускается в любом случае, чтобы проверить наличие настроек. Основные рабочие модули рекомендую импортировать в методе ''gui()'', а не в теле модуля, ради оптимизации.
  
-Когда загружается контейнер свойств, Менеджер загружает из базы данных записи о значениях параметров и значений Свойств и применяет их, если те были обнаружены, иначе параметры и значения останутся дефолтными. У Свойства параметры определяют его поведение, а значение - это данные, которые он в себе хранит.+Когда загружается контейнер свойств, Менеджер загружает из базы данных записи о значениях параметров и значений Свойств и применяет их, если записи о них имеются в базе, иначе параметры и значения останутся дефолтными. У Свойства параметры определяют его поведение, а значение - это данные, которые он в себе хранит.
  
 Если пользователь деактивирует плагин, то все ранее загруженные модули пакета будут удалены из памяти. Если пользователь деактивирует плагин, то все ранее загруженные модули пакета будут удалены из памяти.
Строка 204: Строка 207:
 ==== 2.2 Ваш универсальный помощник ==== ==== 2.2 Ваш универсальный помощник ====
  
-Для общения с Менеджером используйте класс ''Helper'' из ''PyUB.Types''. Вызывайте его конструктор в любом месте вашего кода, в любом случае каждый Плагин может иметь только один экземпляр "Помощника". +Для общения с Менеджером используйте класс **Helper** из ''PyUB.Types''. Вызывайте его конструктор в любом месте вашего кода, в любом случае каждый Плагин может иметь только один экземпляр "Помощника".
- +
-Данный класс предоставляет следующие методы:+
  
 === 2.2.1 Методы === === 2.2.1 Методы ===
Строка 219: Строка 220:
  
 **plugin_dir_abspath()** **plugin_dir_abspath()**
- +
 Возвращает строку с абсолютным путем к папке плагина Возвращает строку с абсолютным путем к папке плагина
  
 **plugin_localstorage_dir_abspath()** **plugin_localstorage_dir_abspath()**
  
-Возвращает строку с абсолютным путем к индивидуальной папке в локальном хранилище Менеджера (''...\Pyrog\manager\data\plugins_ls\<имя индивидуальной папки>'')+Возвращает строку с абсолютным путем к индивидуальной папке в локальном хранилище Менеджера (''...\Pyrog\manager\data\plugins_ls\<имя индивидуальной папки, присвоенное автоматически>'')
  
 +**set_menu()**
 +
 +Устанавливает меню (объект класса [[https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QMenu.html|QMenu]]) в указанную позицию. Полезен для расширения возможностей интерфейса плагина.
 +
 +Для того чтобы установить собственное меню, передайте аргументу //menu// объект класса **QMenu**, а аргументу //position// передайте строку с указанием позиции установки, доступны следующие варианты:
 +
 +  * ''"top_bar"'' - устанавливает меню на верхней панели в главном окне;
 +  * ''"tab_whole"'' - переопределяет контекстное меню, выводимое при нажатии ПКМ по вкладке плагина;
 +  * ''"tab_before"'' - добавляет меню **ПЕРЕД** стандартным контекстным меню, выводимым при нажатии ПКМ по вкладке плагина;
 +  * ''"tab_after"'' - добавляет меню **ПОСЛЕ** стандартного контекстного меню, выводимого при нажатии ПКМ по вкладке плагина;
 +
 +В качестве демонстрации посмотрите на изображение ниже, здесь демонстрируются варианты использовать контекстных меню вкладок\\ {{ https://wiki.ironmesh.ru/_media/products:pyrog:changelogs:ver1-1x:all_context_menus.jpg?700&direct |Варианты меню}}
 +
 +Описания обозначений:
 +
 +  - Встроенное меню
 +  - Переопределенное встроенное меню
 +  - Встроенное меню + Пользовательское меню в начале
 +  - Встроенное меню + Пользовательское меню в конце
 +  - Встроенное меню + Пользовательское меню в начале и в конце
 +
 +Чтобы удалить ранее установленное меню - передайте аргументу //menu// объект **None**.
 +
 +**get_menu()**
 +
 +Возвращает ранее установленное меню в указанное позиции, литералы для конкретной позиции приведены в описании метода //set_menu()//.
  
 === 2.2.2 Сигналы === === 2.2.2 Сигналы ===
 +
 Helper также имеет ряд сигналов, сообщающих о действиях пользователя Helper также имеет ряд сигналов, сообщающих о действиях пользователя
  
Строка 238: Строка 266:
  
 Весь код в Менеджере выполняется синхронно, это значит исполнение не продолжится пока не выполнится код реакции на сигналы. Напомню, что обработчики сигналам подключаются так: ''helper.<сигнал>.connect(<обработчик>)''. Весь код в Менеджере выполняется синхронно, это значит исполнение не продолжится пока не выполнится код реакции на сигналы. Напомню, что обработчики сигналам подключаются так: ''helper.<сигнал>.connect(<обработчик>)''.
 +
  
 ==== 2.3 Делаем локализацию интерфейса ==== ==== 2.3 Делаем локализацию интерфейса ====
Строка 245: Строка 274:
 ==== 2.3.1 Языковые константы ==== ==== 2.3.1 Языковые константы ====
  
-Языковые константы (ЯК) - это объекты, которые хранят в себе оригинальный текст и текст перевода, добавлены для того чтобы избежать конфликтов при использовании стандартного механизма локализации, когда перевод извлекается из загруженных словарей, например, в случае когда разные плагины используют разные языки и словари для них загружены одновременно, то если оригинальный текст перевода и контекст будут совпадать, то будет получен перевод из словаря, который был загружен последним. Итак, разберемся как с ними работать.+Языковые константы (ЯК) - это объекты, которые хранят в себе оригинальный текст и текст перевода. Добавлены для того чтобы избежать конфликтов при использовании стандартного механизма локализации, когда перевод извлекается из загруженных словарей, например, в случае когда разные плагины используют разные языки и словари для них загружены одновременно, то если оригинальный текст перевода и контекст будут совпадать, то будет получен перевод из словаря, который был загружен последним. Итак, разберемся как с ними работать.
  
 <code python> <code python>
Строка 262: Строка 291:
   * контекст   * контекст
   * текст константы   * текст константы
-  * (опционально) строка идентификатор, когда один и тот же исходный текст используется в одном контексте, но в разных ролях. Параметры аналогичны тем, что передаются функции ''PySide6.QtCore.QCoreApplication.translate()'', кроме аргумента n.+  * (опционально) строка идентификатор, когда один и тот же исходный текст используется в одном контексте, но в разных ролях. Параметры аналогичны тем, что передаются функции ''PySide6.QtCore.QCoreApplication.translate()'', кроме аргумента ''n''.
  
 Извлечь перевод (при его наличии, если он отсутствует, то используется оригинальный текст) можно несколькими способами. Импортируем ранее созданный модуль ''tranlslations.py'' Извлечь перевод (при его наличии, если он отсутствует, то используется оригинальный текст) можно несколькими способами. Импортируем ранее созданный модуль ''tranlslations.py''
Строка 270: Строка 299:
 </code> </code>
  
-У ЯК переопределен метод ''__str__'', который конвертирует объект в строку автоматически+У ЯК переопределен метод ''_ _str_ _'', который конвертирует объект в строку автоматически
  
 <code python> <code python>
Строка 284: Строка 313:
 </code> </code>
  
-преобразование производится при вызове экземпляра как функции, то есть с добавлением ''()'', реализовано через ''__call__''+преобразование производится при вызове экземпляра как функции, то есть с добавлением ''()'', реализовано через ''_ _call_ _''
  
 <code python> <code python>
Строка 306: Строка 335:
 </code> </code>
  
-этой функции можно передать строку, тогда она просто вернет ее без изменений, это полезной когда на нужно обработать простую строку или ЯК.+функции  //get_lang_const_translation()// можно передать строку, тогда она просто вернет ее без изменений, это полезнокогда заранее не известно что нужно обработатьпростую строку или языковую константу.
  
-Языковые константы поддерживают перевод для множественных форм числительных, на данные момент реализовано только для русского и английского языков. Это работает следующим образом, в модуле ''tranlslations.py'' есть переменная ''APPLE'' если мы подготовили для нее перевод с учетом множественных форм, то нам нужно передать числитель типа ''int'' или ''float'', чтобы извлечь нужную форму множественного числа+Языковые константы поддерживают перевод для множественных форм числительных, на данные момент реализовано только для русского и английского языков. Это работает следующим образом, в модуле ''tranlslations.py'' есть переменная ''APPLE'' если мы подготовили для нее перевод с учетом множественных форм, то нам нужно передать числитель типа ''int'' или ''float'', чтобы извлечь нужную форму множественного числа:
  
 <code python> <code python>
Строка 325: Строка 354:
 </code> </code>
  
-Обращаю ваше внимание, что ЯК не производят форматирование строки, как это сделано в стандартной системе Qt, где число подставляется на место метки ''%n'', форматирование нужно будет реализовать самостоятельно. Языковые константы полезны там, где интерфейс создается динамически, их можно не использовать в главном виджете плагина, так как он живет в течение всей сессии, там вы можете использовать стандартную функцию ''PySide6.QtCore.QCoreApplication.translate()''+Обращаю ваше внимание, что ЯК не производит форматирование строки, как это сделано в стандартной системе Qt, где число подставляется на место метки ''%n'', форматирование нужно будет реализовать самостоятельно. Языковые константы полезны там, где интерфейс создается динамически, их можно не использовать в главном виджете плагина, так как он "живетв течение всей сессии, там вы можете использовать стандартную функцию ''PySide6.QtCore.QCoreApplication.translate()''
  
 ==== 2.3.2 Процедура перевода ==== ==== 2.3.2 Процедура перевода ====
  
-Теперь, пришло время поговорить о том, как обновить переводы в константах и там где он был размечен стандартными средствами Qt. Когда пользователь меняет язык, то класс Helper отправляет сигнал ''plugin_language_changing'' с кодом языка, задача разработчика реализовать процедуру перевода.+Теперь, пришло время поговорить о том, как обновить переводы в константах и тамгде он был размечен стандартными средствами Qt. Когда пользователь меняет язык, то класс Helper отправляет сигнал ''plugin_language_changing'' с кодом языка, задача разработчика реализовать процедуру перевода.
  
 Рассмотрим подробнее, что происходит в системе по шагам: Рассмотрим подробнее, что происходит в системе по шагам:
 +
   - Пользователь изменил язык   - Пользователь изменил язык
   - Менеджер загружает доступные словари для выбранного языка   - Менеджер загружает доступные словари для выбранного языка
-  - Менеджер через Helper отправляет сигнал plugin_language_changing плагину о том, что язык изменился и ему нужно провести необходимые процедуры+  - Менеджер через **Helper** отправляет сигнал ''plugin_language_changing'' плагину о том, что язык изменился и ему нужно провести соответствующие действия
   - Если плагин привязал обработчики к сигналу, то производится их выполнение   - Если плагин привязал обработчики к сигналу, то производится их выполнение
   - Менеджер выгружает ранее установленные словари   - Менеджер выгружает ранее установленные словари
  
-Итак, нам как разработчикам плагинов нужно сосредоточиться на шаге 4, когда у нас есть окно возможностей между загрузкой и выгрузкой словарей, для этого посмотрим на то, как это реализовано во встроенном плагине TS generator, который можно найти в папке ''...Pyrog\manager\plugins\translator'', в пакете ''view'' в модуле ''main_widget.py'' определен метод, который отрабатывает данную процедуру (''MainWidget._on_retranslate()''), посмотрим на него поближе+Итак, нам как разработчикам плагинов нужно сосредоточиться на шаге 4, когда на нужно произвести требуемые действия между загрузкой и выгрузкой словарей, для этого посмотрим на то, как это реализовано во встроенном плагине TS generator, который можно найти в папке ''...Pyrog\manager\plugins\translator'', в пакете ''view'' в модуле ''main_widget.py'' определен метод, который выполняет данную процедуру (''MainWidget._on_retranslate()''), посмотрим на него поближе
  
 <code python> <code python>
Строка 356: Строка 386:
     def _on_retranslate(self, code):     def _on_retranslate(self, code):
      retranslate_nested_langconstants(lang_consts)       retranslate_nested_langconstants(lang_consts) 
 +
      self._file_list_input.set_file_filter(f"{lang_consts.SOURCE_FILE()} (*.py *.pyw *.ui *.json)")        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(0, lang_consts.SOURCE_FILES())  
Строка 364: Строка 395:
      self._clear_btn.setText(lang_consts.CLEAR())        self._clear_btn.setText(lang_consts.CLEAR())  
      self._generate_btn.setText(lang_consts.GENERATE())          self._generate_btn.setText(lang_consts.GENERATE())    
-        self._add_plural_forms_checkbox.setText(lang_consts.SAVE_PLURAL_FORMS())+            self._add_plural_forms_checkbox.setText(lang_consts.SAVE_PLURAL_FORMS())
 </code> </code>
  
-В нем с помощью функции ''retranslate_nested_langconstants()'' выполняем перевод языковых констант, она сканирует передаваемый модуль или класс на наличие констант и выполняет их перевод, работает даже со вложенными классами, вторым аргументом можно передать код языка, который передает сигнал ''plugin_language_changing'', но это нужно только когда нам нужно получить формы множественного числа, что не нужно в данном случае, также обратите внимание, что вызов этой функции нужно выполнить перед тем как будут использованы языковые константы. После того как константы переведены, производится их применение для виджетов интерфейса.+В нем с помощью функции //retranslate_nested_langconstants()// выполняем перевод языковых констант, она сканирует передаваемый модуль или класс на наличие констант и выполняет их перевод, вложенные классы также подвергаются обработке; вторым аргументом можно передать код языка, который передает сигнал ''plugin_language_changing'', но это нужно только когда нам нужно получить формы множественного числа, что не нужно в данном случае, также обратите внимание, что вызов этой функции нужно выполнить перед тем как будут использованы языковые константы. После того как константы переведены, производится их применение для виджетов интерфейса.
  
-Когда мы используем QtDesigner для создания форм интерфейса, то в коде формы, которую он генерирует есть метод ''retranslateUi()'', можно подключить его к сигналу ''plugin_language_changing'' или вызвать его в другом обработчике данного сигнала.+Когда мы используем QtDesigner для создания форм, то в коде формы, которую он генерирует есть метод ''retranslateUi()'', можно подключить его к сигналу ''plugin_language_changing'' или вызвать его в другом обработчике данного сигнала.
  
 ==== 2.3.3 Подготовка словарей ==== ==== 2.3.3 Подготовка словарей ====
Строка 375: Строка 406:
 После того как работа над кодом плагина закончена, можно приступить к локализации. Исходники нужно преобразовать в TS файлы, а затем перевести исходные тексты на нужный язык и скомпилировать их в QM словари. Несколько лет назад я уже писал статью на эту тему, хотя она актуальна для PySide2, но в целом принцип не изменился. После того как работа над кодом плагина закончена, можно приступить к локализации. Исходники нужно преобразовать в TS файлы, а затем перевести исходные тексты на нужный язык и скомпилировать их в QM словари. Несколько лет назад я уже писал статью на эту тему, хотя она актуальна для PySide2, но в целом принцип не изменился.
  
-Для создания TS файлов из исходников есть встроенная утилита TS generator. Перейдите в её настройки и установите адрес папки, в которой будут сохраняться временные файлы, и путь к файлу lupdate, он находится в папке, где устанавливаются пакеты Python, для Windows это ''C:\Users\<user name>\AppData\Roaming\Python\Python313\site-packages\PySide6''. После перейдите на вкладку плагина, в разделе **Исходные файлы** выберите те файлы, которые желаете преобразовать, принимаются следующие типы:+Для создания TS файлов из исходников есть встроенная утилита TS generator. При первом применении перейдите в её настройки и установите адрес папки, в которой будут сохраняться временные файлы, и путь к файлу //lupdate//, он находится в папке, где устанавливаются пакеты Python, для Windows это ''C:\Users\<user name>\AppData\Roaming\Python\Python313\site-packages\PySide6''. После перейдите на вкладку плагина, в разделе **Исходные файлы** выберите те файлы, которые желаете преобразовать, принимаются следующие типы:
  
   * Python файлы (''.py'' и ''.pyw'' )   * Python файлы (''.py'' и ''.pyw'' )
   * Файл манифеста (''manifest.json'')   * Файл манифеста (''manifest.json'')
-  * Файлы форм, созданных в QtDesigner (''.ui'') (код созданный с его помощью не обрабатывается!) В разделе **Сохранить в** укажите путь к TS файлам, если файлы не существуют, то они будут созданы, а существующие будут обновлены, поэтому когда вы обновляет код, то можете записывать изменения поверх, все ранее добавленные переводы будут сохранены. Отметьте флаг **Добавить формы числительным** если нужно, чтобы языковые константы поддерживали формы множественного числа.+  * Файлы форм, созданных в QtDesigner (''.ui'') (код созданный с его помощью не обрабатывается!) 
  
-  утилита импортирует исходные файлы Python и выполняет их код для поиска в них языковых констант, поэтому в коде не должно быть ошибокприводящих к исключению!+В разделе **Сохранить в** укажите путь к TS файлам, если файлы не существуют, то они будут созданыа существующие будут обновленыпоэтому когда вы обновляет код, то можете записывать изменения поверх, все ранее добавленные переводы будут сохранены. Отметьте флаг **Добавить формы числительным** если нужно, чтобы языковые константы поддерживали формы множественного числа.
  
-Итак, мы получили файлы с исходными текстами, теперь нужно их перевести на целевой язык. Запускаем утилиту Qt Linguist и открываем наши TS-файлы. При первой загрузке нужно выбрать язык оригинала и язык перевода. У утилиты есть одна фишка - можно открыть сразу несколько TS файлов, чтобы сразу выполнить перевод на несколько языков, такое поведение может быть несколько непривычным.+<WRAP center round important 80%> 
 +Утилита импортирует исходные файлы Python и выполняет их код для поиска в них языковых констант, поэтому в коде не должно быть ошибок, приводящих к исключению! 
 +</WRAP> 
 + 
 +Итак, мы получили файлы с исходными текстовыми данными, теперь нужно их перевести на целевой язык. Запускаем утилиту Qt Linguist и открываем наши TS-файлы. При первой загрузке нужно выбрать язык оригинала и язык перевода. У утилиты есть одна фишка - можно открыть сразу несколько TS файлов, чтобы сразу выполнить перевод на несколько языков, такое поведение может быть несколько непривычным.
  
 В разделе **Контекст** мы видим, что все текстовые данные сгруппированы в соответствии с тем текстом, который был перед методу ''translate()'' или языковой константе через аргумент ''context''. В разделе **Строки** выделяем строку и в поле перевод на целевой язык вводим соответственно перевод исходного текста и жмем ''Alt+Enter'', тогда строка будет помечена зеленой галочкой. Проводим эту операцию над всем строками, если некоторые из них не будут иметь перевода, то текст останется оригинальным – все просто. В разделе **Контекст** мы видим, что все текстовые данные сгруппированы в соответствии с тем текстом, который был перед методу ''translate()'' или языковой константе через аргумент ''context''. В разделе **Строки** выделяем строку и в поле перевод на целевой язык вводим соответственно перевод исходного текста и жмем ''Alt+Enter'', тогда строка будет помечена зеленой галочкой. Проводим эту операцию над всем строками, если некоторые из них не будут иметь перевода, то текст останется оригинальным – все просто.
Строка 391: Строка 426:
 Также есть возможность скомпилировать файлы переводов утилитой lrelease, можно прочитать информацию по ней [[https://doc.qt.io/qt-6/linguist-manager.html#using-lrelease|здесь]]. Также есть возможность скомпилировать файлы переводов утилитой lrelease, можно прочитать информацию по ней [[https://doc.qt.io/qt-6/linguist-manager.html#using-lrelease|здесь]].
  
-Сейчас можно не делать перевод полностью руками, а отдать TS-файл нейросети и дать ей команду сделать переводы на нужные языки, они сами "знают" разметку файлов и никак ее не ломают. Я пробовал сделать это с помощью Claude и Deepseek. Просто передал им файл и задал команду: "Это ts файл pyside6 исходный язык английский, там уже есть перевод на русский. Замени русский перевод на французский". После останется только проверить перевод и скомпилировать словарь.+<WRAP center round tip 80%> 
 +Можно автоматизировать процесс перевода, отправив TS-файлы нейросети и дать ей команду сделать переводы на нужные языки, они сами "знают" разметку файлов и проблем с нарушением разметки не наблюдалось. Я пробовал сделать это с помощью Claude и Deepseek. Просто передал им файл и задал команду: //"Это ts файл pyside6исходный язык английский, там уже есть перевод на русский. Замени русский перевод на французский"//. После останется только проверить перевод и скомпилировать словарь. 
 +</WRAP>
  
 ==== 2.4 Система свойств ==== ==== 2.4 Система свойств ====
Строка 397: Строка 434:
 ==== 2.4.1 Как работать со Свойствами ==== ==== 2.4.1 Как работать со Свойствами ====
  
-Свойство в контексте Pyrog это программная единица, которая хранит в себе данные определенного типа; имеет набор параметров, которые определяют его поведение; и графический интерфейс для того чтобы пользователь мог манипулировать хранящимся значением.+Свойство в контексте Pyrog это программная единица, которая хранит в себе данные определенного типа; имеет набор параметров, которые определяют ее поведение; графический интерфейс для того чтобы пользователь мог манипулировать хранящимся значением.
  
 Рассмотрим строение Свойства на конкретном примере ''PyUB.Types.Properties.FloatProperty'', это Свойство для ввода вещественного числа, он имеет следующий конструктор: Рассмотрим строение Свойства на конкретном примере ''PyUB.Types.Properties.FloatProperty'', это Свойство для ввода вещественного числа, он имеет следующий конструктор:
Строка 407: Строка 444:
 Список аргументов-параметров следующий Список аргументов-параметров следующий
  
-^ Параметр ^ Тип ^ Описание |   +^ Параметр ^ Тип ^ Описание | 
-| default_value | float | значение по умолчанию |   | +| default_value | ''float'' | значение по умолчанию |   
-| name | str | LangConstant | имя Свойства, используется для подписи в интерфейсе | +| name | ''str | LangConstant'' | имя Свойства, используется для подписи в интерфейсе | 
-| minimum | float | минимальное значение |   | +| minimum | ''float'' | минимальное значение |   
-| maximum | float | максимальное значение |   | +| maximum | ''float'' | максимальное значение |   
-| single_step | float | единичный шаг, шаг изменения значения при нажатии на кнопки виджета QDoubleSpinBox |   +| single_step | ''float'' | единичный шаг, шаг изменения значения при нажатии на кнопки виджета QDoubleSpinBox | 
-| decimals | int | количество знаков после запятой |   +| decimals | ''int'' | количество знаков после запятой | 
-| tooltip | str | LangConstant | всплывающая подсказка с описание свойства | +| tooltip | ''str | LangConstant'' | всплывающая подсказка с описание свойства | 
-| show_reset_btn | bool | флаг того, будет ли показана кнопка сброса значения до дефолтного рядом с виджетом, если значение Свойства не будет равно дефолтному |   |+| 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'' и приводятся в случае необходимости к нужному диапазону.
  
-Как ранее было сказано, каждое свойство имеет свой набор параметров, каждый параметр является свойством Python, которое можно изменять непосредственно из программы. При изменении параметров действуют строгие правила проверки передаваемых значений, нужно передавать значение только установленного типа, также, например, в случае с FloatProperty и IntProperty параметр minimum должен быть строго меньше maximum и наоборот, в противном случае будет возбуждено исключение, поэтому при изменении диапазона нужно следить за порядком изменения параметров.+=== Сохранение параметров Свойств в базе данных Менеджера ===
  
-**Реакция значения Свойства на изменения параметров** Значение Свойства должно находиться в диапазоне, установленном параметрами, таким образом если ''minimum = -10.0'' и ''maximum=10.0'', то ''value'' должно находиться строго в данном диапазоне. Если мы изменяем maximum и сделаем равным 5.0, в то время как ''value'' будет равно 7.1, то значение "прилипнет" к ближайшей границе диапазона и будет равно 5.0. Таким образом в отношении значений Свойств действует ягкая валидация", ему можно передавать совершенно любые данные, и механизм валидации будет стараться их привести к нужному типу и диапазону значений.+Если по каким-то причинам вам нужно изменять значения параметров и при следующем запуске плагина их нужно восстановитьто у **Helper** используйте метод ''save_settings_parameters()''Внимание, это работает только для контейнера свойств, который используется для пользовательских настроек, но ничто не мешает извлечь и сохранить эти данные самостоятельно, о том как их получить будет рассказано при разборе контейнера свойств.
  
-**Как работает мягкая валидация** Вы уже видели, что значение изменяется в соответствии с установленным диапазоном значений, заданным параметрами Свойства, также, например следующие литералы будут успешно преобразованы в тип ''float'': "5.2", "5", 4, False, True, то есть целые числа и числа в виде строк, булевые значения преобразуются в тип ''float'' и приводятся в случае необходимости к нужному диапазону.+=== Как получить уведомление об изменении значения ===
  
-**Сохранение параметров Свойств в базе данных Менеджера** Если по каким-то причинам вам нужно изменять значения параметров и при следующем запуске плагина их нужно восстановить, то у Helper используйте метод ''save_settings_parameters()''. Внимание, это работает только для контейнера свойств, который используется для пользовательских настроек, но ничто не мешает извлечь и сохранить эти данные самостоятельно, о том как их получить будет рассказано при разборе контейнера свойств.+Свойства поддерживают сигналы, у каждого из них есть сигнал ''value_changed'', вы можете подключить обработчик или несколько, который будет срабатывать при каждом изменении значения Свойства. Когда Свойства используются в составе контейнера, можно получать сигнал от него, о чем будет рассказано далее.
  
-**Как получить уведомление об изменении значения** Свойства поддерживают сигналы, у каждого из них есть сигнал ''value_changed'', вы может подключить обработчик или несколько, который будет срабатывать при каждом изменении значения Свойства. Когда Свойства используются в составе контейнера, можно получать сигнал от него, о чем будет рассказано далее.+=== Как получить доступ к графическому интерфейс === 
  
-**Как получить доступ к графическому интерфейсу** Каждое Свойство имеет метод ''get_input_widget()'', который возвращает виджет для редактирования значения, его вы может использовать для встраивания в свой интерфейс.+Каждое Свойство имеет метод ''get_input_widget()'', который возвращает виджет для редактирования значения, его вы можете использовать для встраивания в свой интерфейс.
  
 ==== 2.4.2 Как работать с контейнером свойств ==== ==== 2.4.2 Как работать с контейнером свойств ====
Строка 454: Строка 501:
 from .settings import Settings   from .settings import Settings  
  
-print(Settings.int_property.value) # вывести значение свойства +print(Settings.int_property.value) # вывести значение Свойства 
-Settings.int_property.value = 7 # изменить значение свойства+Settings.int_property.value = 7 # изменить значение Свойства
  
-print(Settings.int_property.default_value) # вывести дефольное значение +print(Settings.int_property.default_value) # вывести дефолтное значение Свойства 
-Settings.int_property.default_value = 9 # измениим дефолтное значение+Settings.int_property.default_value = 9 # изменить дефолтное значение Свойства
 </code> </code>
  
-Чтобы получить уведомление об изменение значений используйте нотификатор+Чтобы получить уведомление об изменении значений используйте нотификатор
  
 <code python> <code python>
Строка 483: Строка 530:
 Если нотификатор стал не нужен, то удалите его методом ''pc_delete_notifier()''. Если нотификатор стал не нужен, то удалите его методом ''pc_delete_notifier()''.
  
-**Как получить доступ к общему графическому интерфейсу Свойств** Контейнер свойств может отрендерить графический интерфейс, через который пользователь имеет возможность манипулировать значениями Свойств, по умолчанию все виджеты выводятся единым списком (в левом столбце - имена Свойств; в правом - виджеты Свойств)+=== Как получить доступ к общему графическому интерфейсу Свойств === 
 + 
 +Контейнер свойств может отрендерить графический интерфейс, через который пользователь имеет возможность манипулировать значениями Свойств, по умолчанию все виджеты выводятся единым списком (в левом столбце - имена Свойств; в правом - виджеты Свойств)
  
 Дефолтный интерфейс дает возможность фильтровать Свойства по имени, а также сбросить значения до дефолтных для всех Свойств. Данный интерфейс можно переопределить, чтобы тот соответствовал вашим требованиям, об этом будет рассказано далее. Дефолтный интерфейс дает возможность фильтровать Свойства по имени, а также сбросить значения до дефолтных для всех Свойств. Данный интерфейс можно переопределить, чтобы тот соответствовал вашим требованиям, об этом будет рассказано далее.
  
-**Как пакетно извлечь/ установить все значения и параметры Свойств** Например, вы хотите получить весь набор параметров и значений для всех Свойств в контейнере, для этого есть ряд методов:+=== Как пакетно извлечь/ установить все значения и параметры Свойств === 
 + 
 +Например, вы хотите получить весь набор параметров и значений для всех Свойств в контейнере, для этого есть ряд методов:
  
   * ''pc_prop_values_as_dict()'' - возвращает значения Свойств в виде словаря, где ключ - имя атрибута в контейнере свойств, значение - кортеж, где указан тип Свойства и его значение.   * ''pc_prop_values_as_dict()'' - возвращает значения Свойств в виде словаря, где ключ - имя атрибута в контейнере свойств, значение - кортеж, где указан тип Свойства и его значение.
Строка 499: Строка 550:
 ==== 2.4.3 Как разработать собственное Свойство ==== ==== 2.4.3 Как разработать собственное Свойство ====
  
-Может так случиться, что встроенных Свойств вам будет недостаточно, для таких случае я составил пошаговый гайд по разработке собственного Cвойства, и разбирать мы его будем на примере BoolListProperty, данное свойство задает кортеж значений типа ''bool''. Итак, приступим.+Может так случиться, что встроенных Свойств вам будет недостаточно, для таких случаев я составил пошаговый гайд по разработке собственного Cвойства, и разбирать мы его будем на примере **BoolListProperty**, данное Свойство хранит кортеж значений типа ''bool''. Итак, приступим.
  
 **Шаг 1.** Объявляем класс и создаем сигнал. Имя сигнала не изменять! **Шаг 1.** Объявляем класс и создаем сигнал. Имя сигнала не изменять!
Строка 505: Строка 556:
 <code python> <code python>
 class BoolListProperty(Property): # создаем новый класс class BoolListProperty(Property): # создаем новый класс
-    value_changed = Signal(tuple)  # создаем сигнал, посылаемый при изменении значения. Имя не изменять! В качестве аргумента указать тип значения свойства+    value_changed = Signal(tuple)  # создаем сигнал, посылаемый при изменении значения. Имя не изменять! В качестве аргумента указать тип значения Свойства
 </code> </code>
  
Строка 550: Строка 601:
   * ''keys'' - список ключей параметров, которые участвуют в проверке, в данном случае нужен ключ ''_items'';   * ''keys'' - список ключей параметров, которые участвуют в проверке, в данном случае нужен ключ ''_items'';
   * ''validator'' - функция валидатора, где 2 аргумента: 1-проверяемое значение, 2 - список значений параметров, которые были перечислены в ''keys'', в случае удачной проверки должен вернуть ''True'';   * ''validator'' - функция валидатора, где 2 аргумента: 1-проверяемое значение, 2 - список значений параметров, которые были перечислены в ''keys'', в случае удачной проверки должен вернуть ''True'';
-  * ''error_msg'' - сообщении ошибки, если валидатор вернул ''False''+  * ''error_msg'' - сообщение об ошибке, если валидатор вернул ''False''
  
 Рассмотрим функцию-валидатор Рассмотрим функцию-валидатор
Строка 558: Строка 609:
 </code> </code>
  
-параметр ''default_value'' должен принимать кортеж со значениями типа ''bool'', аргумент self принимает его значение, ''args'' - в данному случае список с одним элементом - значением параметра ''_items''; далее производится проверка, чтобы длины кортежей были равными и чтобы все значения кортежа были типа ''bool''.+параметр ''default_value'' должен принимать кортеж со значениями типа ''bool'', аргумент //self// принимает его значение, ''args'' - в данному случае список с одним элементом - значением параметра ''_items''; далее производится проверка, чтобы длины кортежей были равными и чтобы все значения кортежа были типа ''bool''.
  
-**Шаг 3.** Создаем свойства для всех параметров, используя функцию ''create_param_property()'' (PyUB.Type.Properties.utils). В качестве аргумента указываем строку с именем параметра, определенного в ''_param_schema'', функция создает приватный атрибут в классе, идентичный имени ключа.+**Шаг 3.** Создаем свойства для всех параметров, используя функцию ''create_param_property()'' (PyUB.Type.Properties.utils). В качестве аргумента указываем строку с именем параметра, определенного в ''_param_schema'', функция создает приватный атрибут в классе, идентичный имени ключа, в котором будет храниться значение параметра.
  
 <code python> <code python>
 items = create_param_property("_items")   items = create_param_property("_items")  
- 
 tooltip = create_param_property("_tooltip", validate_value=False)   tooltip = create_param_property("_tooltip", validate_value=False)  
- 
 name = create_param_property("_name", validate_value=False, update_widget=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)
-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) default_value = create_param_property("_default_value", validate_value=False, update_widget=False)
 </code> </code>
Строка 581: Строка 628:
   * ''doc'' - строка документации   * ''doc'' - строка документации
  
-**Шаг 4.** Пишем конструктор. В модуле ''tr'' находятся служебные языковые константы, перед использованием надо импортировать+**Шаг 4.** Пишем конструктор. В модуле ''language_constants'' находятся служебные языковые константы, перед использованием его нужно импортировать
  
 <code python> <code python>
Строка 602: Строка 649:
 </code> </code>
  
-Набор аргументов должен совпадать с набором параметров.+Набор аргументов должен совпадать с набором параметров Свойства.
  
 **Шаг 5.** Пишем метод создания виджета ввода **Шаг 5.** Пишем метод создания виджета ввода
  
 <code python> <code python>
-  def get_input_widget(self) -> QListWidget:   +     def get_input_widget(self) -> QListWidget: 
-    if hasattr(self, "_widget"):   +        if hasattr(self, "_widget"): 
-        return self._widget  +            return self._widget
  
-    self._widget = Resetter(QListWidget())   +        self._widget = Resetter(QListWidget()) 
-    self._widget.child_widget.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum))   +        self._widget.child_widget.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum)) 
-    self._widget.reset_requested.connect(self.reset_value) +        self._widget.reset_requested.connect(self.reset_value) 
-    self._widget.child_widget.itemChanged.connect(self._on_widget_value_changed)   +        self._widget.setEnabled(self.widget_enabled) 
-    self._update_widget_params()   +        self._widget.child_widget.itemChanged.connect(self._on_widget_value_changed) 
-    self._update_widget_value()  +        self._update_widget_params() 
 +        self._update_widget_value()
  
-    return self._widget+        return self._widget
 </code> </code>
  
-На первом шаге проверяем был ли ранее создан виджет, если был, то возвращаем его. Обратите внимание, сам экземпляр виджета должен сохранятся в приватном атрибуте ''_widget''. Затем оборачиваем его в Resetter, этот класс-обертка нужен для создания рядом с виджетом кнопки для сброса значения. Затем устанавливаем политику размера виджета, вложенный виджет доступен через свойство ''child_widget'' у Resetter. Когда пользователь вызывает сброс значения, то Resetter отправляет сигнал ''reset_requested'', и мы привязываем метод ''reset_value()'' самого свойства в качестве обработчика. Затем сигналу ''itemChanged'' виджета подключаем обработчик ''_on_widget_value_changed'' и в конце вызываем методы для обновления параметров и значения виджета.+На первом шаге проверяем был ли ранее создан виджет, если был, то возвращаем его. Обратите внимание, сам экземпляр виджета должен сохранятся в приватном атрибуте ''_widget''. Затем оборачиваем его в **Resetter**, этот класс-обертка нужен для создания рядом с виджетом кнопки для сброса значения. Затем устанавливаем политику размера виджета, вложенный виджет доступен через свойство ''child_widget'' у **Resetter**. Когда пользователь вызывает сброс значения, то **Resetter** отправляет сигнал ''reset_requested'', и мы привязываем метод ''reset_value()'' самого свойства в качестве обработчика. Затем сигналу ''itemChanged'' виджета подключаем обработчик ''_on_widget_value_changed'' и в конце вызываем методы для обновления параметров и значения виджета.
  
 **Шаг 6.** Пишем методы обновления параметров и значения виджета **Шаг 6.** Пишем методы обновления параметров и значения виджета
  
 <code python> <code python>
- def _update_widget_value(self) -> None:   +   def _update_widget_value(self) -> None: 
-    self._widget.child_widget.blockSignals(True)   +        self._widget.child_widget.blockSignals(True) 
-    for index, value in enumerate(self.items):   +        for index, value in enumerate(self.items): 
-        item = self._widget.child_widget.item(index)   +            item = self._widget.child_widget.item(index) 
-        if item:   +            if item: 
-            if item.text() != utils.get_lang_const_translation(value):   +                if item.text() != utils.get_lang_const_translation(value): 
-                item.setText(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   +                item_check_state = True if item.checkState() == Qt.CheckState.Checked else False 
-            if self.value[index] != item_check_state:   +                if self.value[index] != item_check_state: 
-                item.setCheckState(Qt.CheckState.Checked if self.value[index] else Qt.CheckState.Unchecked)   +                    item.setCheckState(Qt.CheckState.Checked if self.value[index] else Qt.CheckState.Unchecked) 
-        else:   +            else: 
-            item = QListWidgetItem(utils.get_lang_const_translation(value))   +                item = QListWidgetItem(utils.get_lang_const_translation(value)) 
-            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)   +                item.setFlags(item.flags() | Qt.ItemIsUserCheckable) 
-            item_state = Qt.CheckState.Checked if self.value[index] else Qt.CheckState.Unchecked   +                item_state = Qt.CheckState.Checked if self.value[index] else Qt.CheckState.Unchecked 
-            item.setCheckState(item_state)   +                item.setCheckState(item_state) 
-            self._widget.child_widget.addItem(item)  +                self._widget.child_widget.addItem(item)
  
-    if self._widget.child_widget.count() > len(self.items):   +        if self._widget.child_widget.count() > len(self.items): 
-        for i in range(len(self.items), self._widget.child_widget.count()):   +            for i in range(len(self.items), self._widget.child_widget.count()): 
-            self._widget.child_widget.takeItem(i)   +                self._widget.child_widget.takeItem(i) 
-    self._adjust_widget_height()   +        self._adjust_widget_height() 
-    self._widget.set_reset_btn_visibility(self._value != self._default_value and self.show_reset_btn)   +        self._widget.set_reset_btn_visibility(self._value != self._default_value and self.show_reset_btn) 
-    self._widget.child_widget.blockSignals(False)  +        self._widget.child_widget.blockSignals(False)
  
-def _update_widget_params(self) -> None:   +    def _update_widget_params(self) -> None: 
-    if hasattr(self, '_widget'):   +        if hasattr(self, '_widget'): 
-        self._widget.child_widget.setToolTip(utils.get_lang_const_translation(self.tooltip))   +            self._widget.child_widget.setToolTip(utils.get_lang_const_translation(self.tooltip)) 
-        self._update_widget_value()+            self._update_widget_value()
 </code> </code>
  
-В методе ''_update_widget_value()'' производится обновление виджета с учетом обновленного значения Свойства, также процедура срабатывает при изменении параметра ''items''. Этот метод всегда вызывается в случаях, когда значение Свойства изменилось. На первом этапе проверяются существующие элементы в списке, текст элемента сравнивается с тем, что есть в ''items'' на данной позиции и если нужно - обновляет его; также сравнивается состояние флага со значением в кортеже ''value'' для текущей позиции и также в случае необходимости его обновляет; если в списке не хватает элементов, то добавляет их. На втором этапе производится проверка на то, нет ли в списке лишних элементов, если в списке виджета больше элементов, чем есть в ''items'', то лишние из них удаляются. В конце производится обновление состояния видимости кнопки сброса Resetter.+В методе ''_update_widget_value()'' производится обновление виджета с учетом обновленного значения Свойства, также процедура срабатывает при изменении параметра ''items''. Этот метод всегда вызывается в случаях, когда значение Свойства изменилось. На первом этапе проверяются существующие элементы в списке, текст элемента сравнивается с тем, что есть в ''items'' на данной позиции и если нужно - обновляет его; также сравнивается состояние флага со значением в кортеже ''value'' для текущей позиции и также в случае необходимости его обновляет; если в списке не хватает элементов, то добавляет их. На втором этапе производится проверка на то, нет ли в списке лишних элементов, если в списке виджета больше элементов, чем есть в ''items'', то лишние из них удаляются. В конце производится обновление состояния видимости кнопки сброса **Resetter**.
  
 В методе ''_update_widget_params()'' выполняется код только при условии, что объект имеет атрибут ''_widget''. В данном случае для виджета устанавливается текст всплывающей подсказки. И вызывается метод ''_update_widget_value()'', это нужно в данном примере, так как он связан с параметром ''items''. В методе ''_update_widget_params()'' выполняется код только при условии, что объект имеет атрибут ''_widget''. В данном случае для виджета устанавливается текст всплывающей подсказки. И вызывается метод ''_update_widget_value()'', это нужно в данном примере, так как он связан с параметром ''items''.
Строка 665: Строка 713:
  
 <code python> <code python>
- def _on_widget_value_changed(self) -> None:   +    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._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)+        self._widget.set_reset_btn_visibility(self._value != self._default_value and self.show_reset_btn)
 </code> </code>
  
-Данный метод вызывается, когда в виджете изменилось значение. Здесь производится обновление значения Свойства в соответствии с данными виджета и обновление видимости кнопки сброса у Resetter.+Данный метод вызывается, когда в виджете изменилось значение. Здесь производится обновление значения Свойства в соответствии с данными виджета и обновление видимости кнопки сброса у **Resetter**.
  
 Альтернативный способ обновления видимости кнопки сброса; для его использования нужно в конструкторе передать ссылку на Свойство. Альтернативный способ обновления видимости кнопки сброса; для его использования нужно в конструкторе передать ссылку на Свойство.
Строка 682: Строка 730:
 </code> </code>
  
-Метод ''update_reset_btn_visibility()'' сам сравнит значение Свойства с дефолтным и учтет значение параметра ''show_reset_btn''.+Метод ''update_reset_btn_visibility()'' сам сравнит значение Свойства с дефолтным и учтет значение параметра ''show_reset_btn'', поэтому метода //_on_widget_value_changed()// можно записать так: 
 + 
 +<code python> 
 +    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.update_reset_btn_visibility() 
 +</code>
  
 **Шаг 8.** Пишем метод валидации значения **Шаг 8.** Пишем метод валидации значения
Строка 707: Строка 761:
 </code> </code>
  
-Примите во внимание, что валидация производится уже после обновления значения. В случае данного Свойства валидация производится в несколько этапов. Сначала проверяем является ли значение последовательностью, если нет, то сбрасываем его до дефолтного и завершаем процедуру. На следующем этапе узнаем является ли значение кортежем, если нет, то преобразуем последовательность в него. Потом длину кортежа сравниваем с длиной ''items'', если значение длиннее, то лишние элементы удаляются, если короче, то добавляются недостающие элементы, имеющие значение ''False''. На завершающем этапе проверяется: все ли элементы имеют тип ''bool'', если это не так, то все они конвертируются в данный тип, по правилам преобразования.+Примите во внимание, что валидация производится уже после обновления значения, валидация осуществляется, если значение Свойства было изменено. В контексте описываемого Свойства валидация производится в несколько этапов: сначала проверяем является ли значение последовательностью, если нет, то сбрасываем его до дефолтного и завершаем процедуру; на следующем этапе узнаем является ли значение кортежем, если нет, то преобразуем последовательность в кортеж. Потом длину кортежа значения Свойства сравниваем с длиной кортежа параметра ''items'', если кортеж значения длиннее, то лишние элементы удаляются, если короче, то добавляются недостающие элементы, имеющие значение ''False''; на завершающем этапе проверяется: все ли элементы имеют тип ''bool'', если это не так, то все они конвертируются в данный тип, по правилам преобразования.
  
-**Важно!** в процедуре валидации присваивайте значения атрибуту ''_value'', а не свойству ''value'', иначе программа войдет в бесконечный цикл.+<WRAP center round important 60%> 
 +В методе //_validate_value()// присваивайте значения атрибуту ''_value'', а не свойству ''value'', иначе программа войдет в бесконечный цикл
 +</WRAP> 
 + 
 +**Шаг 9 (ситуативный)** Иногда нужно переопределить код свойства ''value'', например, как это было сделано для Свойства **FloatProperty**, так как код класса **Property** использует для сравнения значений операцию ''!='', что не корректно использовать с объектами типа ''float''. Таким образом, если тип данных Свойства не поддерживает сравнение на равенство, то код свойства ''value'' придется переопределить.
  
-**Шаг 9 (ситуативный)** Иногда нужно переопределить код свойства value, например, как это было нужно для Свойства FloatProperty, так как код класса Property использует для сравнения значений операцию ''!='', что не корректно использовать с объектами типа ''float''. Таким образом, если тип данных Свойства не поддерживает сравнение на равенство, то код свойства ''value'' придется переопределить. 
  
 ==== 2.4.4 Разработка кастомного интерфейса для контейнера свойств ==== ==== 2.4.4 Разработка кастомного интерфейса для контейнера свойств ====
Строка 783: Строка 840:
 Проект будет развиваться, будут добавляться новые функции и возможности, а существующие дорабатываться. Совместимость API будет сохраняться в любом случае. Проект будет развиваться, будут добавляться новые функции и возможности, а существующие дорабатываться. Совместимость API будет сохраняться в любом случае.
  
 +
 +{{section>products:pyrog:includes#footer&noheader}}
  
products/pyrog/tutorials/dev/main.1784994471.txt.gz · Последнее изменение: ironmesh