marcus_afailius
marcus_afailius

Reputation: 123

Kivy Attribute Error - object has no attribute - Trying to connect widgets in kv language

I seem to be having nonstop problems with trying to connect widgets in Kivy. I've read this useful guide but my situation isn't directly covered.

I have 2 different "choosers" side by side like this: KeySigChooser test

Each chooser will be its own class, held by the KeySigChooserContainer. I want to size the buttons based on the size of the KeySigChooserContainer, so that the buttons will have consistent sizes. This is accomplished with

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

but I don't like using the parent reference; I'd much rather use a direct reference for flexibility as the app grows in complexity. But when I try doing that with

<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

I get an attribute error: AttributeError: 'RootNoteChooser' object has no attribute 'box'

I've used a similar technique elsewhere in my project so I have no idea why this isn't working. I have also tried making box an ObjectProperty within the RootNoteChooser and ModeChooser classes but that doesn't work.

# 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

Clearly I'm missing something here... any help is appreciated.

UPDATE This seems to be one of those situations where there's not a great way to solve the problem. Thanks to @JohnAnderson, here's what I learned:

  1. An id is limited in scope to the rule it is declared in.
  2. the outermost widget applies the kv rules to all its inner widgets before any other rules are applied
  3. Rules are always applied before instances.

The problem here is I am using an attribute (box) in the <RootNoteChooser> and <ModeChooser> rules, but that attribute gets created in the instance of RootNoteChooser and ModeChooser. Since rules are applied first, box does not yet exist.

The work-around I'm using for this is to also create the box attribute in both rules, and set it to something that makes sense (and won't cause an error). Then, in the RootNoteChooser and ModeChooser instances (in the <KeySigChooser> rule), box will get reset to the proper object. Here's the gist of it:

<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.

Upvotes: 2

Views: 985

Answers (2)

John Anderson
John Anderson

Reputation: 38822

One thing you must watch when setting up references to properties in kivy is when those properties will be available. Your original code seems reasonable, but the problem is that the box property of RootNoteChooser and ModeChooser is accessed before it is set-up. You can get around that by defining a property that can be used before its value is actually set. In this case, using a NumericProperty(0) will allow your code to use the initial value of zero, even though that is not the correct value. Then when the correct value is assigned (by Kivy), it will work as you expect. Here is a modified version of your code using that approach:

# 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()

I put your keysigchooser.kv into a Builder.load_string() call just for my own convenience.

There are numerous ways to accomplish what you want. Another way is to set the ChooserButton sizes using your on_size() method of KeySigChooserContainer. To do this, add ids to the ChooserButtons and add the following code to the end of that method:

    # 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

And yet another method is to remove the <RootNoteChooser> and <ModeChooser> rules from your kv file and place the contents of those rules directly under the ModeChooser and RootNoteChooser sections of the <KeySigChooserContainer> rule. This would allow you to set the ChooserButton widths using:

width: box.width * (3/32)

similar to your original code.

Upvotes: 2

John Anderson
John Anderson

Reputation: 38822

If you want the children of the BoxLayout to be a certain fraction of the BoxLayout width, you can just use size_hint (that is what its intended use is). For example, in your 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()

In a BoxLayout, those children with size_hint assigned will take up that fraction of the remaining space after the other children's size is subtracted. So, in the above example, the Label space is subtracted from the parent BoxLayout and the remaining space is divided between the ChooserButtons. Adding a similar size_hint for the Label makes it clearer.

Upvotes: 1

Related Questions