marcus_afailius
marcus_afailius

Reputation: 123

Kivy - Updating Canvas with on_size events not working correctly

All I'm trying to do is draw 12 circles, and keep them centered in the window.

I have a FloatLayout which holds a BoxLayout. The BoxLayout should "hold" the 12 circles.

Since I want the circles in a row, the BoxLayout's width should be 12 times it's height. The FloatLayout resizes the BoxLayout so this is always the case. (FloatLayout is green, BoxLayout is red).

correctly_drawn

The problem occurs when I resize the window small enough. Now the circles are not centered in the BoxLayout. The circles also "jump" left to right as I resize the window (making it look totally unsmooth).

incorrectly_drawn1

Also, if I maximize the window (instead of resizing by dragging an edge or corner of the window) then it draws the circles where ever.

enter image description here

Any clue whats happening here? I am using the BoxLayout's lower left (x,y) coordinates to start drawing circles. The leftmost circle should use the same exact coordinates, but obviously it's off.

Instead of binding the canvas instructions to the on_size event with self.bind(size=self.update_canvas, pos=self.update_canvas), I also tried calling self.redraw_note_markers at the end of self.on_size. That way, when we're drawing the circles, the inner BoxLayout should have already been resized. But that is producing the same result as shown above.

UPDATE I've incorporated this into a larger app, where it is part of a TabbedPanel, and these circles are getting drawn totally out of place.

# filename: keysigdisplay.py
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.graphics import InstructionGroup, Ellipse, Color
from kivy.clock import Clock


class KeySigDisplay(FloatLayout):

    def __init__(self, **kwargs):
        # To get note_markers drawn in center of self.ids.box, need super() at top
        # of __init__ and then Clock.schedule_once(self.redraw_note_markers)...?
        super().__init__(**kwargs)
        self.note_markers = InstructionGroup()
        self.bind(size=self.update_canvas, pos=self.update_canvas)
        Clock.schedule_once(self.update_canvas)

    def update_canvas(self, *args):
        self.redraw_note_markers()

    def redraw_note_markers(self, *args):
        self.canvas.remove(self.note_markers)
        self.note_markers.clear()
        x, y = self.ids.box.pos
        for i in range(12):
            self.redraw_note_marker(i, x, y)
        self.canvas.add(self.note_markers)

    def redraw_note_marker(self, i, x, y):
        white = Color(1, 1, 1, 1)  # white
        blue = Color(68 / 255, 93 / 255, 209 / 255, 1)  # blue
        black = Color(0, 0, 0, 1)

        # Draw 2 concentric circles, c1 and c2.
        # Circles are defined by a square's lower left corner.
        r1 = self.ids.box.height / 2
        r2 = r1 * 0.9
        rdiff = r1 - r2
        c1x, c1y = (2*r1)*i + x, y
        c2x, c2y = c1x + rdiff, c1y + rdiff

        self.note_markers.add(white)
        self.note_markers.add(Ellipse(pos=[c1x, c1y], size=[2 * r1, 2 * r1]))
        self.note_markers.add(blue)
        self.note_markers.add(Ellipse(pos=[c2x, c2y], size=[2 * r2, 2 * r2]))

    def on_size(self, instance, value):
        width, height = self.size
        target_ratio = 12
        if width / height > target_ratio:
            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 KeySigDisplayApp(App):
    def build(self):
        return KeySigDisplay()


if __name__ == "__main__":
    KeySigDisplayApp().run()
# filename: keysigdisplay.kv
<KeySigDisplay>:
    canvas:
        Color:
            rgba: [0, 1, 0, 0.25]
        Rectangle:
            size: self.size
            pos: self.pos
    BoxLayout:
        id: box
        size_hint: [None, None]
        pos_hint: {"center_x": 0.5, "center_y": 0.5}
        canvas:
            Color:
                rgba: [1, 0, 0, 0.25]
            Rectangle:
                size: self.size
                pos: self.pos

Upvotes: 0

Views: 1648

Answers (2)

marcus_afailius
marcus_afailius

Reputation: 123

Answering my own question...

I was not able to get to the root of the problem. The FloatLayout's on_size event should 1) resize the BoxLayout and 2) update the canvas based on the size/position change of the BoxLayout. It does (1) just fine, but seems to lag with (2). The changes are not in sync. The result is graphics "jumping" around as the window is resized, and being just a little off. I still am unsure why it is behaving this way.

The work-around I'm using is to bind to the BoxLayout's position, and update the canvas there.

# filename: keysigdisplay.py
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.graphics import InstructionGroup, Ellipse, Color
from kivy.properties import NumericProperty, ReferenceListProperty


class KeySigDisplay(FloatLayout):
    # Added all these properties. box_pos is bound to box.pos in kv file.
    box_x = NumericProperty(0)
    box_y = NumericProperty(0)
    box_pos = ReferenceListProperty(box_x, box_y)  

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.note_markers = InstructionGroup()

    def update_canvas(self, *args):
        self.redraw_note_markers()

    def redraw_note_markers(self, *args):
        self.canvas.remove(self.note_markers)
        self.note_markers.clear()
        x, y = self.ids.box.pos
        for i in range(12):
            self.redraw_note_marker(i, x, y)
        self.canvas.add(self.note_markers)

    def redraw_note_marker(self, i, x, y):
        white = Color(1, 1, 1, 1)  # white
        blue = Color(68 / 255, 93 / 255, 209 / 255, 1)  # blue
        black = Color(0, 0, 0, 1)

        # Draw 2 concentric circles, c1 and c2.
        # Circles are defined by a square's lower left corner.
        r1 = self.ids.box.height / 2
        r2 = r1 * 0.9
        rdiff = r1 - r2
        c1x, c1y = (2*r1)*i + x, y
        c2x, c2y = c1x + rdiff, c1y + rdiff

        self.note_markers.add(white)
        self.note_markers.add(Ellipse(pos=[c1x, c1y], size=[2 * r1, 2 * r1]))
        self.note_markers.add(blue)
        self.note_markers.add(Ellipse(pos=[c2x, c2y], size=[2 * r2, 2 * r2]))

    def on_size(self, instance, value):
        width, height = self.size
        target_ratio = 12
        if width / height > target_ratio:
            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

    # Added this method to receive the event.
    def on_box_pos(self, instance, value):
        self.update_canvas(instance, value)

class KeySigDisplayApp(App):
    def build(self):
        return KeySigDisplay()


if __name__ == "__main__":
    KeySigDisplayApp().run()
# filename: keysigdisplay.kv
<KeySigDisplay>:
    box_pos: box.pos
    canvas:
        Color:
            rgba: [0, 1, 0, 0.25]
        Rectangle:
            size: self.size
            pos: self.pos
    BoxLayout:
        id: box
        size_hint: [None, None]
        pos_hint: {"center_x": 0.5, "center_y": 0.5}
        canvas:
            Color:
                rgba: [1, 0, 0, 0.25]
            Rectangle:
                size: self.size
                pos: self.pos

Not sure if this is the right way to be doing this, but it works. Hopefully someone finds this helpful.

Upvotes: 1

John Anderson
John Anderson

Reputation: 38937

Try putting:

from kivy.config import Config
Config.set('graphics', 'maxfps', 0)

at the top of your keysigdisplay.py. This will mean that kivy will maximize its cpu usage and produce the smoothest display that it can. The default value for maxfps is 60 (fps). If this improves your app's graphics performance, you can then try different values of maxfps to balance between cpu usage and graphics smoothness.

Upvotes: 0

Related Questions