Reputation: 123
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:
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
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
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