【问题标题】:Kivy Attribute Error - object has no attribute - Trying to connect widgets in kv languageKivy 属性错误 - 对象没有属性 - 尝试以 kv 语言连接小部件
【发布时间】:2020-02-23 17:40:35
【问题描述】:

我似乎在尝试连接 Kivy 中的小部件时遇到了不间断的问题。我已经阅读了this useful guide,但我的情况并未直接涵盖。

我有 2 个不同的“选择器”并排像这样:

每个选择器都是它自己的类,由 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'

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

# 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. 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. 规则总是在实例之前应用。

这里的问题是我在 &lt;RootNoteChooser&gt;&lt;ModeChooser&gt; rules 中使用了一个属性 (box),但该属性是在 instance 中创建的RootNoteChooserModeChooser。由于首先应用规则,box 尚不存在。

我为此使用的解决方法是同时在两个规则中创建box 属性,并将其设置为有意义的内容(并且不会导致错误)。然后,在RootNoteChooserModeChooser 实例中(在&lt;KeySigChooser&gt; 规则中),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.

【问题讨论】:

    标签: python python-3.x kivy


    【解决方案1】:

    在 kivy 中设置对属性的引用时必须注意的一件事是这些属性何时可用。您的原始代码似乎是合理的,但问题是RootNoteChooserModeChooserbox 属性在设置之前已被访问。您可以通过定义一个可以在实际设置其值之前使用的属性来解决这个问题。在这种情况下,使用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() 电话只是为了我自己的方便。

    有很多方法可以实现您想要的。另一种方法是使用KeySigChooserContaineron_size() 方法设置ChooserButton 大小。为此,请将 id 添加到 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
    

    还有一种方法是从kv 文件中删除&lt;RootNoteChooser&gt;&lt;ModeChooser&gt; 规则,并将这些规则的内容直接放在ModeChooser 规则的ModeChooserRootNoteChooser 部分下。这将允许您使用以下方法设置 ChooserButton 宽度:

    width: box.width * (3/32)
    

    类似于您的原始代码。

    【讨论】:

    • 我看到这行得通,但恕我直言,感觉更像是一种解决方法而不是答案。这是这样做的正确方式吗?创建一个已经存在 (box.width) 的属性 (box_width) 似乎是一种不好的做法。使用root.parent.width 有效,因此很明显存在这些“选择器”的 BoxLayout。但似乎 rules 在它们各自的创建属性的 instance 之前执行 (box: box)。有没有更好的方法来构造 kv 文件以避免该问题? (感谢您花时间写这些答案!)
    • 在我上面的回答中,我添加了两种替代方法来完成你想要的。
    • 您如何看待第 4 个选项:在 规则中,设置 box: self.parent 以使 box 存在。然后稍后在实例化时,box 将被您附加到它的任何id 覆盖(我认为)。这让我们仍然可以使用box.width(而不是box_width),并且仍然可以将kv文件分成每个类的规则。
    • @marcus_afailius,我认为这可行,但我认为您需要使用root.box.width 而不是box.width。许多不同的方式来实现相同的结果!
    • 谢谢!使用root.box.width 是我目前正在使用的。在我的主应用程序中,box 在小部件树中实际上比我在此处解释的要高,因此这似乎是最干净的选项。不过,我并不特别喜欢这些解决方案中的任何一个,因为它们似乎都有点 hacky.. 但也许它就是这样。我会让这篇文章逗留一会儿,看看是否有其他人有意见,否则我会更新我的 OP 并将其标记为答案。再次感谢您的洞察!
    【解决方案2】:

    如果您希望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 之间分配。为Label 添加一个类似的size_hint 会更清楚。

    【讨论】:

    • 对 - 但我想要 RootNoteChooser 和 ModeChooser 的 不同 宽度和 ChooserButtons 的 相同 宽度(抱歉,如果不清楚) .无论我直接使用size_hint 还是width,按钮宽度都需要基于KeySigChooser 的宽度,这是所有ChooserButtons 的共同父级。
    猜你喜欢
    • 1970-01-01
    • 2018-09-01
    • 1970-01-01
    • 2016-11-12
    • 2021-05-03
    • 1970-01-01
    • 1970-01-01
    • 2015-01-29
    • 2020-10-04
    相关资源
    最近更新 更多