2025, Nov 23 18:02

Стили из .kv не применяются к внедрённому виджету в Kivy: как исправить

Почему внедрённый виджет в Kivy остаётся без стилей из .kv? Вся суть в порядке загрузки. Даем два решения: создавать после build() или заранее загрузить kv.

Когда вы подмешиваете заранее созданный виджет в приложение Kivy, легко нарваться на тонкую проблему: внедренный экземпляр не учитывает стили из .kv и выглядит «без оформления», тогда как виджеты, созданные позже, получают нужный вид. Поведение кажется нелогичным, пока не вспомнишь, когда Kivy фактически загружает правила из .kv.

Постановка задачи

Представим, что разметка описана в .kv-файле, а пользовательский виджет создается заранее и передается в приложение как зависимость. Такой внедренный виджет отображается без стилей, заданных в .kv.

# minimal_example/sample.kv
<MainShell>
    BrandedButton:
        text: 'Custom Button'
<BrandedButton>
    text: 'This is a custom button'
    background_color: (0.5, 0.5, 0.5, 1)
    font_size: 25
# minimal_example/custom_widgets.py
from kivy.uix.button import Button
class BrandedButton(Button):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
WIDGET_SINGLETON = BrandedButton(text='Constant')
# minimal_example/app_with_kv.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from minimal_example.custom_widgets import BrandedButton, WIDGET_SINGLETON
class MainShell(BoxLayout):
    def __init__(self, injected_button: BrandedButton, **kwargs):
        super().__init__(**kwargs)
        self.add_widget(BrandedButton(text='Added dynamically'))  # Оформление применяется корректно
        self.add_widget(injected_button)  # Без стилей
class SampleApp(App):
    def __init__(self, injected_button: BrandedButton, **kwargs):
        super().__init__(**kwargs)
        self._injected_button = injected_button
    def build(self):
        return MainShell(self._injected_button)
if __name__ == '__main__':
    SampleApp(WIDGET_SINGLETON).run()

Что происходит на самом деле

Причина — в порядке выполнения. Экземпляр, сохраненный в WIDGET_SINGLETON, создается в custom_widgets.py до загрузки правил из .kv. Класс App в Kivy автоматически подхватывает .kv-файл с подходящим именем для этого класса, и делает он это внутри build(). Любой виджет, созданный до этого момента, не получит классовые правила из .kv, поэтому у внедренной кнопки нет оформления. Виджеты, созданные после build() (например, добавленные динамически), получают правила и выглядят как задумано.

Как это исправить

Есть два простых пути. Первый — не создавать виджет до загрузки .kv, а создавать его в build(), когда правила уже применены. То есть перенести место, где появляется экземпляр.

# minimal_example/custom_widgets.py
from kivy.uix.button import Button
class BrandedButton(Button):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
# Примечание: на уровне модуля экземпляр не создается
# minimal_example/app_with_kv_fixed_build.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from minimal_example.custom_widgets import BrandedButton
class MainShell(BoxLayout):
    def __init__(self, injected_button: BrandedButton, **kwargs):
        super().__init__(**kwargs)
        self.add_widget(BrandedButton(text='Added dynamically'))
        self.add_widget(injected_button)
class SampleApp(App):
    def build(self):
        late_instance = BrandedButton(text='Constant')
        return MainShell(late_instance)
if __name__ == '__main__':
    SampleApp().run()

Если важно сохранить схему с внедрением зависимости и продолжать создавать экземпляр вне приложения, второй вариант — загрузить .kv заранее, до импорта, который создает виджет. Чтобы один и тот же .kv не подхватился дважды, переименуйте файл и загрузите его явно. Например, переименуйте sample.kv в sample_manual.kv и убедитесь, что он загружается до импорта custom_widgets.py.

# minimal_example/app_with_kv_early_load.py
from kivy.lang import Builder
# Загрузите переименованный kv-файл заранее
Builder.load_file('minimal_example/sample_manual.kv')
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
# Импорт выполняется после загрузки kv-файла
from minimal_example.custom_widgets import BrandedButton, WIDGET_SINGLETON
class MainShell(BoxLayout):
    def __init__(self, injected_button: BrandedButton, **kwargs):
        super().__init__(**kwargs)
        self.add_widget(BrandedButton(text='Added dynamically'))
        self.add_widget(injected_button)
class SampleApp(App):
    def __init__(self, injected_button: BrandedButton, **kwargs):
        super().__init__(**kwargs)
        self._injected_button = injected_button
    def build(self):
        return MainShell(self._injected_button)
if __name__ == '__main__':
    SampleApp(WIDGET_SINGLETON).run()

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

Почему это важно

В Kivy порядок действий определяет, применятся ли к экземплярам виджетов классовые правила из .kv. Любой код, который создает виджеты во время импорта модуля, может незаметно обойти эти правила, если .kv еще не загружен. Это особенно актуально, когда вы используете такие подходы, как внедрение зависимостей или держите синглтоны на уровне модуля.

Выводы

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