Разработка интерфейса для Контейнера Свойств
В данном уроке я расскажу как разработать альтернативный интерфейс для Контейнера свойств, по умолчанию он генерирует интерфейс единым списком без группировки по разделам и прочего, если свойств немного, то этого будет вполне достаточно, но если их десятки или даже сотни, то таким интерфейсом будет пользоваться не слишком удобно, также может возникнуть желание добавить дополнительные функции, например, сортировка, фильтр и т.п. Ниже показана предопределенная форма вывода свойств
Подробнее о концепции свойств читайте здесь.
Ставим задачу
Для начала разберемся с тем, что мы хотим получить. Допустим у нас есть перечень свойств, которые имеют имена с суфиксами _s1,_s2…_s5, зная этот факт я хочу сгруппировать их все в пяти секциях, для этого я выберу виджет, который позволяет разделять макет на страницы, к примеру, возьмем QToolBox, далее на каждой странице будут собраны свойства, имена которых будут иметь суффикс «_sX», форма страниц будет аналогичная той, которую имеет дефолтный интерфейс: в поле с полосой прокрутки (QScrollArea) будут собраны свойства в макете-форме (QFormLayout), где в левом столбце будут виджеты QLable с именами свойств, а в правом виджеты самих свойств.
Решаем задачу
Приступим к разработке, для теста я создам новый Плагин (о разработке плагинов читайте в материале Быстрый старт), чтобы можно было легко наблюдать результат проделанной работы. В файле MySettings.py есть класс с тем же названием, удалим определение одного свойства и вместо ручного ввода, пожалуй, я сделаю метод, который заполнит класс свойствами автоматически, поскольку, все нам известно, что они должны храниться в поле _ _annotations_ _. Вот код файла, который у меня получился
from PySide6.QtWidgets import QWidget,QFormLayout, QLabel, QScrollArea, QToolBox from PySide6.QtCore import Qt, QCoreApplication from PyUB.Types import PropertyContainer from PyUB.Types.Properties import * class MySettings(PropertyContainer): @classmethod def generate_properties(cls) -> None: for i in range(1, 101): cls.__annotations__[f"int_property{i}_s1"] = IntProperty(default_value=i, name=f"Int Property {i}", maximum=1000) cls.__annotations__[f"float_property{i}_s2"] = FloatProperty(default_value=float(i), name=f"Float Property {i}",maximum=1000) cls.__annotations__[f"string_property{i}_s3"] = StringProperty(default_value=f"Just String {i}", name=f"String Property {i}") cls.__annotations__[f"bool_property{i}_s4"] = BoolProperty(default_value=True, name=f"Bool Property {i}") cls.__annotations__[f"combobox_property{i}_s5"] = ComboBoxProperty(default_value=0, name=f"Combobox Property {i}", items=["item1", "item2"]) MySettings.generate_properties()
заметьте, что после определения класса метод generate_properties() нужно вызвать, чтобы свойства были созданы. Теперь, можно посмотреть как будет выглядеть дефолтная форма с сгенерированными свойствами
выглядит она не очень презентабельно, тут собрано порядка 500 свойств, на практике с этим было бы трудно работать.
Напомню, что в новом интерфейсе имя свойства определяет, на какую страницу оно попадет, то есть свойства с префиксом «s1» попадают на первую страницу, с «s2» на вторую и так далее. У PropertyContainer за вывод интерфейса отвечает метод render_layout() , а метод retranslate() отвечает за перевод отображаемой информации, Менеджер автоматически вызывает данные методы, поэтому retranslate() не нужно вызывать в коде, как это делается в Свойствах. Если перевод не нужен, то заполнение интерефейса можно произвести в методе render_layout(), а в методе retranslate() вписать ключевое слово pass.
Итак, в итоге у меня получился следующий код для методов render_layout() и retranslate()
@classmethod def render_layout(cls) -> QToolBox: cls._lable_list = {} cls._tool_box = QToolBox() def render_page(suffix:str) ->QScrollArea: scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) widget_content = QWidget() layout = QFormLayout(widget_content) layout.setVerticalSpacing(15) layout.setLabelAlignment(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter) row: int = 0 for key, prop in cls.__annotations__.items(): if not key.endswith(suffix): continue cls._lable_list[key] = QLabel() cls._lable_list[key].setWordWrap(True) layout.setWidget(row, QFormLayout.LabelRole, cls._lable_list[key]) layout.setWidget(row, QFormLayout.FieldRole, prop.get_input_widget()) row += 1 scroll_area.setWidget(widget_content) return scroll_area for i in range(1, 6): cls._tool_box.addItem(render_page(f"s{i}"), "") return cls._tool_box @classmethod def retranslate(cls) -> None: for key, label in cls._lable_list.items(): label.setText(cls.get_property(key).get_name()) cls.__annotations__[key].retranslate() for i in range(cls._tool_box.count()): cls._tool_box.setItemText(i, QCoreApplication.translate("properties", f"Section {i+1}"))
А вот так выглядит новый интерфейс, уже намного лучше. Это только один из вариант группировки свойств по разделам, также можно для таких целей использовать виджеты: QTabWidget, QStackedWidget совместно с виджетом для выбора элементов, например, QListWidget, QComboBox и другие.Теперь, попробую объяснить код, который я написал, начну с метода render_layout(), в самом начале определяем словарь _lable_list, в котором будут содержаться все подписи для свойств, которые представляют из себя экземпляры виджета QLable, а в переменной _tool_box будет находиться экземпляр QToolBox, обе эти переменные сохраняются в классе, чтобы был к ним доступ из других методов. Затем я ввел вспомогательную вложенную функцию render_page(), которая отбирает свойства, имена которых имеют указанный суффикс и упаковывает их в форму (QFormLayout) внутри виджета, который в свою очередь вложен в область (QScrollArea), затем функция возвращает сформированный интерфейс; в цикле for производится обход всех Свойств, которые хранятся в словаре _ _annotations_ _, имена свойств, которые не имеют нужный суффикс игнорируются, а те что соответствуют, у них берется виджет для ввода методом get_input_widget(), затем он добавляется в форму layout(QFormLayout) в правый столбец (роль QFormLayout.FieldRole), а подписи для свойств (экземпляры QLable) помещаются в левый столбец (роль QFormLayout.LabelRole). Заметьте, что для подписей Свойств текст не устанавливается, это будет сделано позднее. В конце производится добавление страниц в _tool_box, для этого вызыватся метод addItem(), которому передается результат функции render_page(), которой передаются строки с суффиксами, определенные нами ранее.
В методе retranslate() выполняется перевод отображаемой информации, а именно: подписи разделов для _tool_box, подписей Свойств и самих Свойств. Для перевода подписей свойств мы обходим все элементы в _lable_list и устанавливаем текст, которые вернет метод свойства get_name(), заметьте, что в качестве ключей использованы имена свойств в контейнере; также у каждого свойства вызывается метод retranslate(), чтобы перевести отображаемую информацию; в конце производится обход всех элементов в _tool_box и устанавливает их текст на предопределенный, для поиска перевода используется команда QCoreApplication.translate(<контекст>, <текст>).
Подытожу все вышесказанное. Для того чтобы определить свой вариант интерефейса нужно переопределить всего 2 метода: render_layout() и retranslate(), первый метод должен вернуть виджет с сгенерированным интерфесом, а второй выполнить перевод отображаемой информации. Элементы для интерефейса мы извлекаем из самих свойст, вызовом метода get_input_widget(), а текст с именем свойства нам вернет метод get_name(); сами свойства хранятся в классе, в поле _ _annotations_ _, которое представляет из себя обычный словар, где ключ - идентификатор свойства, а значение - ссылка на само свойство.
Ну, вот и все, урок по разработке альтернативного интерфейса для Контейнера свойств подошел к концу, как вы могли убедиться это очень простой процесс, который не займет много времени и сил, при наличии опыта разработки интерфейсов, если опыта мало, то можно сверстать его в QtDesigner, а затем посмотреть как устроен код и перенести его в свой проект с некоторыми изменениями или импортировать его целиков. Кстати, перед использованием не забывайте тестировать свой код. Если что-то не понятно, то можно задать вопрос в Discord.