Kivy 属性错误 - 对象没有属性 - 尝试以 kv 语言连接小部件

2022-01-15 00:00:00 python python-3.x kivy

问题描述

我似乎在尝试连接 Kivy 中的小部件时遇到了不间断的问题.我已阅读

每个选择器都是它自己的类,由 KeySigChooserContainer 持有.我想根据 KeySigChooserContainer 的大小调整按钮的大小,以便按钮具有一致的大小.这是通过

完成的

选择按钮:...宽度:root.parent.width * (3/32)

但我不喜欢使用 parent 引用;随着应用程序复杂性的增加,我更愿意使用直接参考来获得灵活性.但是当我尝试这样做时

<RootNoteChooser>:...盒子布局:...选择器按钮:...宽度:root.box.width * (3/32)<模式选择器>:...盒子布局:...选择器按钮:...宽度:root.box.width * (3/32)<KeySigChooserContainer>:盒子布局:编号:盒子RootNoteChooser:盒子:盒子模式选择器:盒子:盒子

我得到一个属性错误:AttributeError: 'RootNoteChooser' object has no attribute 'box'

我在项目的其他地方使用了类似的技术,所以我不知道为什么这不起作用.我还尝试在 RootNoteChooser 和 ModeChooser 类中将 box 设为 ObjectProperty,但这不起作用.

#keysigchooser.py从 kivy.app 导入应用程序从 kivy.properties 导入 NumericProperty、ObjectProperty从 kivy.uix.floatlayout 导入 FloatLayout从 kivy.uix.relativelayout 导入 RelativeLayoutchrom_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']类模式选择器(浮动布局):经过类 RootNoteChooser(FloatLayout):note_idx = NumericProperty(0)def increment_note_idx(self):self.note_idx = (self.note_idx + 1) % 12def decrement_note_idx(self):self.note_idx = (self.note_idx - 1) % 12def on_note_idx(self, instance, value):self.note_text.text = chrom_scale[self.note_idx]类 KeySigChooserContainer(FloatLayout):def on_size(self, instance, value):目标比率 = 60/20宽度,高度 = self.size# 检查哪个尺寸是限制因素如果宽度/高度 >目标比率:# 窗口比目标宽",所以限制是高度.self.ids.box.height = 高度self.ids.box.width = 高度 * target_ratio别的:self.ids.box.width = 宽度self.ids.box.height = 宽度/target_ratio类 KeySigChooserApp(App):定义构建(自我):返回 KeySigChooserContainer()如果 __name__ == "__main__":KeySigChooserApp().run()

#keysigchooser.kv<ChooserButton@Button>:font_name: "宋体"字体大小:self.width边框:[2, 2, 2, 2]<RootNoteChooser>:注释文本:注释文本盒子布局:pos_hint: {"center": [0.5, 0.5]}方向:水平"选择器按钮:文本:u'u25C4'size_hint:[无,1]宽度:root.box.width * (3/32)on_press:root.increment_note_idx()标签:编号:note_text文字:C"选择器按钮:文本:u'u25BA'size_hint:[无,1]宽度:root.box.width * (3/32)on_press:root.decrement_note_idx()<模式选择器>:盒子布局:pos_hint: {"center": [0.5, 0.5]}方向:水平"选择器按钮:文本:u'u25C4'size_hint:[无,1]宽度:root.box.width * (3/32)标签:文字:主要"选择器按钮:文本:u'u25BA'size_hint:[无,1]宽度:root.box.width * (3/32)<KeySigChooserContainer>:盒子布局:编号:盒子pos_hint: {"center": [0.5, 0.5]}size_hint:[无,无]方向:水平"RootNoteChooser:id: rootnotechooser盒子:盒子size_hint: [0.4, 1]帆布:颜色:rgba: [1, 0, 0, 0.5]长方形:pos: self.pos尺寸:self.size模式选择器:id: 模式选择器盒子:盒子size_hint: [0.6, 1]帆布:颜色:RGBA:[0, 1, 0, 0.5]长方形:pos: self.pos尺寸:self.size

显然我在这里遗漏了一些东西......感谢任何帮助.

更新这似乎是没有解决问题的好方法的情况之一.感谢@JohnAnderson,这就是我学到的东西:

  1. 一个 id 的范围仅限于声明它的规则.
  2. 最外面的小部件将 kv 规则应用于其所有内部应用任何其他规则之前的小部件
  3. 规则总是在在实例之前应用.

这里的问题是我在 <RootNoteChooser><ModeChooser> 中使用了一个属性(box)规则,但该属性是在 RootNoteChooserModeChooser 的 instance 中创建的.由于首先应用规则,因此 box 尚不存在.

我为此使用的解决方法是同时在两个规则中创建 box 属性,并将其设置为有意义的内容(并且不会导致错误).然后,在 RootNoteChooserModeChooser 实例中(在 <KeySigChooser> 规则中),box 将被重置到合适的对象.这是它的要点:

<RootNoteChooser>:box: self.parent # 最初我们会将其设置为合理的值.盒子布局:...选择器按钮:...宽度:root.box.width * (3/32)<模式选择器>:box: self.parent # 最初我们会将其设置为合理的值.盒子布局:...选择器按钮:...宽度:root.box.width * (3/32)<KeySigChooserContainer>:盒子布局:编号:盒子RootNoteChooser:box: box # 现在box属性正确,可以指向任意模式选择器:box: box # id 在这个规则内.

解决方案

在 kivy 中设置对属性的引用时必须注意的一件事是这些属性何时可用.您的原始代码似乎合理,但问题是 RootNoteChooserModeChooserbox 属性在设置之前被访问.您可以通过定义一个可以在实际设置其值之前使用的属性来解决这个问题.在这种情况下,使用 NumericProperty(0) 将允许您的代码使用初始值零,即使这不是正确的值.然后,当(通过 Kivy)分配正确的值时,它将按您的预期工作.这是使用该方法的代码的修改版本:

#keysigchooser.py从 kivy.app 导入应用程序从 kivy.lang 导入生成器从 kivy.properties 导入 NumericProperty从 kivy.uix.floatlayout 导入 FloatLayoutchrom_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']类模式选择器(浮动布局):box_width = NumericProperty(0) # 从零开始,所以有可用的数字类 RootNoteChooser(FloatLayout):box_width = NumericProperty(0) # 从零开始,所以有可用的数字note_idx = NumericProperty(0)def increment_note_idx(self):self.note_idx = (self.note_idx + 1) % 12def decrement_note_idx(self):self.note_idx = (self.note_idx - 1) % 12def on_note_idx(self, instance, value):self.note_text.text = chrom_scale[self.note_idx]类 KeySigChooserContainer(FloatLayout):def on_size(self, instance, value):目标比率 = 60/20宽度,高度 = self.size# 检查哪个尺寸是限制因素如果宽度/高度 >目标比率:# 窗口比目标宽",所以限制是高度.self.ids.box.height = 高度self.ids.box.width = 高度 * target_ratio别的:self.ids.box.width = 宽度self.ids.box.height = 宽度/target_ratiobuilder.load_string('''# keyigchooser.kv<ChooserButton@Button>:font_name: "宋体"字体大小:self.width边框:[2, 2, 2, 2]<RootNoteChooser>:注释文本:注释文本盒子布局:pos_hint: {"center": [0.5, 0.5]}方向:水平"选择器按钮:文本:u'u25C4'size_hint:[无,1]宽度:root.box_width * (3/32)on_press:root.increment_note_idx()标签:编号:note_text文字:C"选择器按钮:文本:u'u25BA'size_hint:[无,1]宽度:root.box_width * (3/32)on_press:root.decrement_note_idx()<模式选择器>:盒子布局:pos_hint: {"center": [0.5, 0.5]}方向:水平"选择器按钮:文本:u'u25C4'size_hint:[无,1]宽度:root.box_width * (3/32)标签:文字:主要"选择器按钮:文本:u'u25BA'size_hint:[无,1]宽度:root.box_width * (3/32)<KeySigChooserContainer>:盒子布局:编号:盒子pos_hint: {"center": [0.5, 0.5]}size_hint:[无,无]方向:水平"RootNoteChooser:id: rootnotechooserbox_width: box.width # 设置 box_widthsize_hint: [0.4, 1]帆布:颜色:rgba: [1, 0, 0, 0.5]长方形:pos: self.pos尺寸:self.size模式选择器:id: 模式选择器box_width: box.width # 设置 box_widthsize_hint: [0.6, 1]帆布:颜色:RGBA:[0, 1, 0, 0.5]长方形:pos: self.pos尺寸:self.size''')类 KeySigChooserApp(App):定义构建(自我):返回 KeySigChooserContainer()如果 __name__ == "__main__":KeySigChooserApp().run()

我将您的 keysigchooser.kv 放入 Builder.load_string() 调用中只是为了我自己的方便.

有很多方法可以实现您想要的.另一种方法是使用 KeySigChooserContaineron_size() 方法设置 ChooserButton 大小.为此,请将 id 添加到 ChooserButtons 并将以下代码添加到该方法的末尾:

 # 设置按钮大小self.ids.rootnotechooser.ids.butt1.width = self.ids.box.width * 3/32self.ids.rootnotechooser.ids.butt2.width = self.ids.box.width * 3/32self.ids.modechooser.ids.butt1.width = self.ids.box.width * 3/32self.ids.modechooser.ids.butt2.width = self.ids.box.width * 3/32

还有一种方法是从 kv 文件中删除 <RootNoteChooser><ModeChooser> 规则并放置内容<KeySigChooserContainer> 规则的 ModeChooserRootNoteChooser 部分下的这些规则.这将允许您使用以下方法设置 ChooserButton 宽度:

宽度:box.width * (3/32)

类似于您的原始代码.

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:

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.

解决方案

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.

相关文章