Ошибка атрибута Kivy - объект не имеет атрибута - Попытка подключения виджетов на языке kv - PullRequest
1 голос
/ 28 октября 2019

Кажется, у меня возникают проблемы при попытке подключить виджеты в Kivy. Я прочитал это полезное руководство , но моя ситуация непосредственно не освещена.

У меня есть 2 разных "селектора" рядом, как это: KeySigChooser test

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

ChooserButton:
    ...
    width: root.parent.width * (3/32)

, но я не люблю использовать ссылку parent;Я бы предпочел использовать прямую ссылку на гибкость, поскольку приложение становится все сложнее. Но когда я пытаюсь сделать это с

<RootNoteChooser>:
    ...
    BoxLayout:
        ...
        ChooserButton:
            ...
            width: root.box.width * (3/32)

<ModeChooser>:
    ...
    BoxLayout:
        ...
        ChooserButton:
            ...
            width: root.box.width * (3/32)

<KeySigChooserContainer>:
    BoxLayout:
        id: box
        RootNoteChooser:
            box: box
        ModeChooser:
            box: box

, я получаю ошибку атрибута: AttributeError: 'RootNoteChooser' object has no attribute 'box'

Я использовал подобную технику в другом месте в моем проекте, поэтому я понятия не имею, почему это не так. не работаетЯ также пытался сделать box ObjectProperty в классах RootNoteChooser и ModeChooser, но это не работает.

# keysigchooser.py
from kivy.app import App
from kivy.properties import NumericProperty, ObjectProperty
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.relativelayout import RelativeLayout

chrom_scale = ['C', 'C#/Db', 'D', 'D#/Eb', 'E', 'F', 'F#/Gb', 'G', 'G#/Ab', 'A', 'A#/Bb', 'B']
chrom_scale2 = ['C', 'C/D', 'D', 'D/E', 'E', 'F', 'F/G', 'G', 'G/A', 'A', 'A/B', 'B']


class ModeChooser(FloatLayout):
    pass


class RootNoteChooser(FloatLayout):
    note_idx = NumericProperty(0)

    def increment_note_idx(self):
        self.note_idx = (self.note_idx + 1) % 12

    def decrement_note_idx(self):
        self.note_idx = (self.note_idx - 1) % 12

    def on_note_idx(self, instance, value):
        self.note_text.text = chrom_scale[self.note_idx]


class KeySigChooserContainer(FloatLayout):
    def on_size(self, instance, value):
        target_ratio = 60/20
        width, height = self.size
        # check which size is the limiting factor
        if width / height > target_ratio:
            # window is "wider" than targeted, so the limitation is the height.
            self.ids.box.height = height
            self.ids.box.width = height * target_ratio
        else:
            self.ids.box.width = width
            self.ids.box.height = width / target_ratio


class KeySigChooserApp(App):
    def build(self):
        return KeySigChooserContainer()


if __name__ == "__main__":
    KeySigChooserApp().run()
# keysigchooser.kv
<ChooserButton@Button>:
    font_name: "Arial"
    font_size: self.width
    border: [2, 2, 2, 2]


<RootNoteChooser>:
    note_text: note_text
    BoxLayout:
        pos_hint: {"center": [0.5, 0.5]}
        orientation: "horizontal"
        ChooserButton:
            text: u'\u25C4'
            size_hint: [None, 1]
            width: root.box.width * (3/32)
            on_press: root.increment_note_idx()
        Label:
            id: note_text
            text: "C"
        ChooserButton:
            text: u'\u25BA'
            size_hint: [None, 1]
            width: root.box.width * (3/32)
            on_press: root.decrement_note_idx()


<ModeChooser>:
    BoxLayout:
        pos_hint: {"center": [0.5, 0.5]}
        orientation: "horizontal"
        ChooserButton:
            text: u'\u25C4'
            size_hint: [None, 1]
            width: root.box.width * (3/32)
        Label:
            text: "Major"
        ChooserButton:
            text: u'\u25BA'
            size_hint: [None, 1]
            width: root.box.width * (3/32)


<KeySigChooserContainer>:
    BoxLayout:
        id: box
        pos_hint: {"center": [0.5, 0.5]}
        size_hint: [None, None]
        orientation: "horizontal"
        RootNoteChooser:
            id: rootnotechooser
            box: box
            size_hint: [0.4, 1]
            canvas:
                Color:
                    rgba: [1, 0, 0, 0.5]
                Rectangle:
                    pos: self.pos
                    size: self.size
        ModeChooser:
            id: modechooser
            box: box
            size_hint: [0.6, 1]
            canvas:
                Color:
                    rgba: [0, 1, 0, 0.5]
                Rectangle:
                    pos: self.pos
                    size: self.size

Очевидно, что здесь что-то не хватает ... любая помощь приветствуется.

ОБНОВЛЕНИЕ Кажется, это одна из тех ситуаций, когда нет замечательного способа решения проблемы. Благодаря @JohnAnderson вот что я узнал:

  1. Идентификатор ограничен в области действия правилом, в котором он объявлен.
  2. самое внешнееВиджет применяет правила kv ко всем своим внутренним виджетам до применения любых других правил
  3. Правила всегда применяются до экземпляров.

Проблема здесья использую атрибут (box) в <RootNoteChooser> и <ModeChooser> правилах , но этот атрибут создается в экземпляре из RootNoteChooser и ModeChooser,Поскольку правила применяются первыми, box еще не существует.

Обходное решение, которое я использую для этого, заключается в том, чтобы также создать атрибут box в обоих правилах и установить для него что-то, что делаетсмысл (и не вызовет ошибку). Затем в экземплярах RootNoteChooser и ModeChooser (в правиле <KeySigChooser>) box будет сброшен на нужный объект. Вот суть этого:

<RootNoteChooser>:
    box: self.parent  # Initially we'll set it to something reasonable.
    BoxLayout:
        ...
        ChooserButton:
            ...
            width: root.box.width * (3/32)

<ModeChooser>:
    box: self.parent  # Initially we'll set it to something reasonable.
    BoxLayout:
        ...
        ChooserButton:
            ...
            width: root.box.width * (3/32)

<KeySigChooserContainer>:
    BoxLayout:
        id: box
        RootNoteChooser:
            box: box  # Now box attribute is correct, and can be pointed at any
        ModeChooser:
            box: box  # id that is within this rule.

Ответы [ 2 ]

1 голос
/ 28 октября 2019

При настройке ссылок на свойства в kivy вы должны следить за тем, когда эти свойства будут доступны. Ваш исходный код кажется разумным, но проблема в том, что свойство box для RootNoteChooser и ModeChooser доступно до его настройки. Вы можете обойти это, определив свойство, которое можно использовать до того, как его значение будет фактически установлено. В этом случае использование NumericProperty(0) позволит вашему коду использовать начальное значение ноль, даже если это неправильное значение. Затем, когда будет назначено правильное значение (от Kivy), оно будет работать так, как вы ожидаете. Вот модифицированная версия вашего кода с использованием этого подхода:

# keysigchooser.py
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import NumericProperty
from kivy.uix.floatlayout import FloatLayout

chrom_scale = ['C', 'C#/Db', 'D', 'D#/Eb', 'E', 'F', 'F#/Gb', 'G', 'G#/Ab', 'A', 'A#/Bb', 'B']
chrom_scale2 = ['C', 'C/D', 'D', 'D/E', 'E', 'F', 'F/G', 'G', 'G/A', 'A', 'A/B', 'B']


class ModeChooser(FloatLayout):
    box_width = NumericProperty(0)   # starts off as zero, just so there is  number available


class RootNoteChooser(FloatLayout):
    box_width = NumericProperty(0)   # starts off as zero, just so there is  number available
    note_idx = NumericProperty(0)

    def increment_note_idx(self):
        self.note_idx = (self.note_idx + 1) % 12

    def decrement_note_idx(self):
        self.note_idx = (self.note_idx - 1) % 12

    def on_note_idx(self, instance, value):
        self.note_text.text = chrom_scale[self.note_idx]


class KeySigChooserContainer(FloatLayout):
    def on_size(self, instance, value):
        target_ratio = 60/20
        width, height = self.size
        # check which size is the limiting factor
        if width / height > target_ratio:
            # window is "wider" than targeted, so the limitation is the height.
            self.ids.box.height = height
            self.ids.box.width = height * target_ratio
        else:
            self.ids.box.width = width
            self.ids.box.height = width / target_ratio

Builder.load_string('''
# keysigchooser.kv
<ChooserButton@Button>:
    font_name: "Arial"
    font_size: self.width
    border: [2, 2, 2, 2]


<RootNoteChooser>:
    note_text: note_text
    BoxLayout:
        pos_hint: {"center": [0.5, 0.5]}
        orientation: "horizontal"
        ChooserButton:
            text: u'\u25C4'
            size_hint: [None, 1]
            width: root.box_width * (3/32)
            on_press: root.increment_note_idx()
        Label:
            id: note_text
            text: "C"
        ChooserButton:
            text: u'\u25BA'
            size_hint: [None, 1]
            width: root.box_width * (3/32)
            on_press: root.decrement_note_idx()


<ModeChooser>:
    BoxLayout:
        pos_hint: {"center": [0.5, 0.5]}
        orientation: "horizontal"
        ChooserButton:
            text: u'\u25C4'
            size_hint: [None, 1]
            width: root.box_width * (3/32)
        Label:
            text: "Major"
        ChooserButton:
            text: u'\u25BA'
            size_hint: [None, 1]
            width: root.box_width * (3/32)


<KeySigChooserContainer>:
    BoxLayout:
        id: box
        pos_hint: {"center": [0.5, 0.5]}
        size_hint: [None, None]
        orientation: "horizontal"
        RootNoteChooser:
            id: rootnotechooser
            box_width: box.width   # this sets the box_width
            size_hint: [0.4, 1]
            canvas:
                Color:
                    rgba: [1, 0, 0, 0.5]
                Rectangle:
                    pos: self.pos
                    size: self.size
        ModeChooser:
            id: modechooser
            box_width: box.width   # this sets the box_width
            size_hint: [0.6, 1]
            canvas:
                Color:
                    rgba: [0, 1, 0, 0.5]
                Rectangle:
                    pos: self.pos
                    size: self.size
''')
class KeySigChooserApp(App):
    def build(self):
        return KeySigChooserContainer()


if __name__ == "__main__":
    KeySigChooserApp().run()

Я поместил ваш keysigchooser.kv в Builder.load_string() вызов только для собственного удобства.

Существует множество способоввыполнить то, что вы хотите. Другой способ - установить размеры ChooserButton, используя метод on_size(), равный KeySigChooserContainer. Для этого добавьте идентификаторы в ChooserButtons и добавьте следующий код в конец этого метода:

    # set button sizes
    self.ids.rootnotechooser.ids.butt1.width = self.ids.box.width * 3/32
    self.ids.rootnotechooser.ids.butt2.width = self.ids.box.width * 3/32
    self.ids.modechooser.ids.butt1.width = self.ids.box.width * 3/32
    self.ids.modechooser.ids.butt2.width = self.ids.box.width * 3/32

И еще один способ - удалить правила <RootNoteChooser> и <ModeChooser> из вашегоФайл kv и поместите содержимое этих правил непосредственно в разделы ModeChooser и RootNoteChooser правила <KeySigChooserContainer>. Это позволит вам установить ширину ChooserButton, используя:

width: box.width * (3/32)

аналогично исходному коду.

1 голос
/ 28 октября 2019

Если вы хотите, чтобы дочерние элементы BoxLayout составляли определенную долю ширины BoxLayout, вы можете просто использовать size_hint (это то, что предполагается по назначению). Например, в вашем RootNoteChooser:

<RootNoteChooser>:
    note_text: note_text
    BoxLayout:
        pos_hint: {"center": [0.5, 0.5]}
        orientation: "horizontal"
        ChooserButton:
            text: u'\u25C4'
            size_hint: [3/32, 1]
            #width: root.box.width * (3/32)
            on_press: root.increment_note_idx()
        Label:
            id: note_text
            text: "C"
            size_hint: [26/32, 1]
        ChooserButton:
            text: u'\u25BA'
            size_hint: [3/32, 1]
            #width: root.box.width * (3/32)
            on_press: root.decrement_note_idx()

В BoxLayout дети с назначенным size_hint будут занимать ту долю оставшегося пространства после вычитания размера других детей. Так, в приведенном выше примере пространство Label вычитается из родительского BoxLayout, а оставшееся пространство делится между ChooserButtons. Добавление аналога size_hint для Label делает это более понятным.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...