Widget size set unexectedly late

32 views
Skip to first unread message

Simon J

unread,
Jan 13, 2018, 8:07:06 AM1/13/18
to Kivy users support
Hi everybody - My first post, so firstly many thanks to the Kivy group for creating Kivy, I've really enjoyed playing with the framework these last few months.

I am finding that widgets are having their size and position set later than expected in the following example. This is a cut down version of a 'Break Out' clone where the Tile widgets are added to a TileArea FloatLayout which inherits its size and position from parent FloatLayouts using size_hints and pos_hints. I have deliberately left the setup_tiles call after the parent objects are created (by creating the Game widget), expecting that by this stage the TileArea layout would have had its size and position defined. However as you can see from the output the TileArea size is still (1, 1) at this point, but if you run the code you'll see that the TileArea and Tiles do go on to get their sizes set to their expected values on screen.

To monitor what's going on I added a scheduled call to display the TileArea and a Tile's size every 0.1 seconds. You can see that the size changes from (1, 1) to the expected values after about 0.1 secs into the loop. This call to schedule display the size is the only scheduled task in the code. The reason this is important to me is so that the set_corners function is called after the size and position are set.

I also tried to bind a call to display the TileArea size when the size is set, expecting to see output when the size changes from (1, 1) but this is not displayed.

Is this a;ll expected behaviour, and if so what have I missed?

Although I can work around this behaviour by scheduling the call to set_corners after 1-2 secs my main concern is understanding why this happens so I don't fall foul of this again. This is a learning exercise for me.

I am running Kivy 1.7.2 with python 2.7 on a 12-year old Linux Mint 17 laptop. Please let me know if I've missed any useful info.

Thanks in advance - Simon

KV file:

#:kivy 1.7.2

<Game>:
    orientation
: 'vertical'
   
ScoreExit:
        id
: sce
        size
: root.width, 30
        pos
: 0, root.height - 30

   
GameSpace:
        id
: gamespace
        size
: root.width, root.height - 30
        pos
: 0, 0
       
# Note - in order to have access to the Paddle id mypad, need to define
       
# GameSpace separately below and not nest the ref to Paddle here

<ScoreExit>:
    orientation
: 'horizontal'
   
Label:
        id
: lab
        size_hint_x
: 0.45
        font_size
: 30
        center_x
: root.width / 3
        text
: "Score: " + str(root.score)

   
Label:
        id
: liv
        size_hint_x
: 0.45
        font_size
: 30
        center_x
: root.width * 2/3
        text
: "Lives: " + str(root.lives)

   
Button:
        id
: scebut
        size_hint_x
: 0.1
        text
: 'Exit'
        font_size
: 30
        on_press
: root.sce_exit

<GameOver>:
    pos_hint
: {'center_x': 0.5, 'center_y': 0.5}
   
Button:
        text
: 'No more lives!\nPress to play again'

<GameSpace>:
   
Paddle:
        id
: mypad
        size_hint
: 0.1, None # Note the None is needed to use height below
       
# Don't set pos_hint here as it will stop us moving the paddle
   
Ball:
        id
: ball
        size_hint
: 0.02, None
   
TileArea:
        id
: tilearea
        size_hint
: 1, 0.3
        pos_hint
: {'top': 1}

<Paddle>:
    height
: self.width/3
    canvas
:
       
Color:
            rgba
: 1, 1, 0.2, 0.8
       
Rectangle:
            size
: self.size
            pos
: self.pos

<Ball>:
    height
: self.width
    canvas
:
       
Color:
            rgba
: 0, 1, 0.7, 0.8
       
Ellipse:
            pos
: self.pos
            size
: self.size

<Tile>:
    canvas
:
       
Rectangle:
            id
: rect
            size
: self.size
            pos
: self.pos


Main file:

#!/usr/bin/python

__version__
= "0.1"

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.properties import NumericProperty, ReferenceListProperty, StringProperty, ObjectProperty, ListProperty
from kivy.core.window import Window
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.gridlayout import GridLayout
from kivy.graphics import Line, Rectangle, Color, Mesh
from kivy.clock import Clock
from kivy.vector import Vector
from kivy.config import Config
from random import random, choice, uniform, randint

Config.set('graphics', 'multisamples', '0')

class CollidableObject(Widget):
   
def set_corners(self):
#        print "Set cors: ", self.size
       
self.corners = []
       
self.corners.append(Vector(self.x, self.y))
       
self.corners.append(Vector(self.right, self.y))
       
self.corners.append(Vector(self.right, self.top))
       
self.corners.append(Vector(self.x, self.top))
       
self.corners.append(self.corners[0])
#        print "Corners: ", self.corners

class Paddle(CollidableObject):
   
pass

class Ball(Widget):
   
pass

class TileArea(FloatLayout):
   
def __init__(self, **kwargs):
       
super(TileArea, self).__init__(**kwargs)
       
self.bind(on_size=self.show_tilearea(dt = 'Bind'))

   
def setup_tiles(self):
       
print "Setup tiles"
       
print "Self ", self.size
        numrows
= 5
        numcols
= 10
       
for m in range(numrows):
           
for n in range(numcols):
                newt
= (Tile(size_hint = (float(1)/numcols, float(1)/numrows),
                             pos_hint
={'x': float(n)/numcols, 'y': float(m)/numrows}))
               
self.add_widget(newt)
#                print "newt size: ", newt.size
#                print "newt pos: ", newt.pos
#                newt.set_corners()

   
def show_tilearea(self, dt):
       
print "%s TA size: %s"% (dt, self.size)
       
for child in self.children:
           
print "Tile size: ", child.size
#            print "Tile corners: ", child.corners
           
break

class Tile(CollidableObject):
    col_list
= ListProperty([])

   
def __init__(self, **kwargs):
       
super(Tile, self).__init__(**kwargs)
       
self.name = "Tile"
       
self.col = choice(Tile.col_list)
       
self.canvas.add(Color(self.col[0], self.col[1], self.col[2], self.col[3], mode = 'rgba'))
       
self.hits = 0

   
def initialise_colours(self):
        col_list
= []
        trans
= 0.8
        col_list
.append([1, 0.2, 0, trans]) # red
        col_list
.append([1, 1, 0.2, trans]) # yel
        col_list
.append([0.4, 1, 0.4, trans]) # green
        col_list
.append([0, 1, 0.8, trans]) # blue
       
Tile.col_list = col_list

class ScoreExit(BoxLayout):
   
def __init__(self, **kwargs):
       
super(ScoreExit, self).__init__(**kwargs)
       
self.score = 0
       
self.lives = 3

class GameOver(AnchorLayout):
   
pass

class Game(Widget):
   
def start(self):
       
self.ids.gamespace.reset_game()

class GameSpace(FloatLayout):
   
def __init__(self, **kwargs):
       
super(GameSpace, self).__init__(**kwargs)
       
self.initialise_colours()
       
# Absence of the time parameter means it processes at once
       
# The lambda dt allows us to call a function that doesn't take dt
#        Clock.schedule_once(lambda dt: self.reset_game())
       
self.bind(on_size=self.show_gamespace(dt = 'Bind'))

   
def show_gamespace(self, dt):
       
print "%s GS size: %s" % (dt, self.size)

   
def initialise_colours(self):
        col_list
= []
        trans
= 0.8
        col_list
.append([1, 0.2, 0, trans]) # red
        col_list
.append([1, 1, 0.2, trans]) # yel
        col_list
.append([0.4, 1, 0.4, trans]) # green
        col_list
.append([0, 1, 0.8, trans]) # blue
       
Tile.col_list = col_list

class BreakOutApp(App):
   
def build(self):
       
Config.set('graphics', 'fullscreen', 1)
#        Window.size = (400, 700) # Roughly proportion of phone
#        Window.size = (800, 500) # Roughly proportion of hudl
        wid
= Game(size=Window.size)
       
print "Game size: ", wid.size
       
print "Gamespace size: ", wid.ids.gamespace.size
       
print "Tilearea size: ", wid.ids.gamespace.ids.tilearea.size
       
print "SCE size: ", wid.ids.sce.size

        wid
.ids.gamespace.ids.tilearea.setup_tiles()
#        for child in wid.ids.gamespace.ids.tilearea.children:
#            print "Child x: ", child.pos
#            child.set_corners()

       
Clock.schedule_interval(wid.ids.gamespace.ids.tilearea.show_tilearea, 0.1)

#        wid.start()

       
return wid

if __name__ == '__main__':
   
BreakOutApp().run()


Output:

Bind GS size: [1, 1]
Bind TA size: [1, 1]
Game size:  [800, 600]
Gamespace size:  [800, 570]
Tilearea size:  [1, 1]
SCE size:  [800, 30]
Setup tiles
Self  [1, 1]
[INFO   ] [OSC         ] using <multiprocessing> for socket
[DEBUG  ] [Base        ] Create provider from mouse
[DEBUG  ] [Base        ] Create provider from probesysfs
[DEBUG  ] [ProbeSysfs  ] using probsysfs!
[INFO   ] [Base        ] Start application main loop
6.41130590439 TA size: [1, 1]
Tile size:  [100, 100]
[INFO   ] [GL          ] NPOT texture support is available
0.157588005066 TA size: [800, 171.0]
Tile size:  [80.0, 34.2]
0.102021932602 TA size: [800, 171.0]
Tile size:  [80.0, 34.2]




breakout.kv
test.py

ZenCODE

unread,
Jan 13, 2018, 5:11:44 PM1/13/18
to Kivy users support

Hi


Okay, line 105 needs to be:

    self.bind(on_size=lambda dt: self.show_gamespace(dt='Bind'))



Similarly for line 43:


    self.bind(on_size=lambda dt: self.show_tilearea(dt = 'Bind'))


Aside from that, the behavior is expected. You need to bind to size and pos changes to accurately determine size and positions when using hinting. You can do this here by adding this to the CollidableObject class


class CollidableObject(Widget):
     
def set_corners(self):
 
#        print "Set cors: ", self.size
         
self.corners = []
         
self.corners.append(Vector(self.x, self.y))
         
self.corners.append(Vector(self.right, self.y))
         
self.corners.append(Vector(self.right, self.top))
         
self.corners.append(Vector(self.x, self.top))
         
self.corners.append(self.corners[0])

         
print "Corners: ", self.corners
 
 
     
def on_size(self, widget, value):
         
self.set_corners()
 
 
     
def on_pos(self, widget, value):
         
self.set_corners()

Simon J

unread,
Jan 14, 2018, 7:26:34 AM1/14/18
to Kivy users support
Thanks for your reply, I hadn't been able to figure out how to get the syntax of the on_size and on_pos functions right before. I can see that these are generally useful good practice, e.g. if the screen is resized.

I'm still curious as to why the setting of size and pos is an asynchronous process that continues to play out after control has passed back to BreakOutApp.build from creating the Game object (i.e. build is allowed to continue on with the print statements and setup_tiles before Game has finished building). Is this something that only happens for size and pos, or is this more widespread?

To investigate I added a simple loop in TileArea.init and monitored the cou parameter it adds to the TileArea object in my show_tilearea function, this loop always seems to complete before control moves on to setup_tiles.

class TileArea(FloatLayout):
   
def __init__(self, **kwargs):
       
super(TileArea, self).__init__(**kwargs)

       
self.cou = 0
       
for x in range(10000000):
           
self.cou = x



My main interest is knowing when I should allow for processes to be asynchronous: is this limited to size and pos, is this a general Kivy thing, or indeed a general python thing? I'm coming back to programming after 20 years so I'm still not entirely up to speed with object-oriented languages.

Thanks once again for your help - Simon

ZenCODE

unread,
Jan 14, 2018, 1:14:43 PM1/14/18
to Kivy users support
It's quite a uniquely Kivy thing, so it's understandable that it's not immediately obvious. Working through this may help: https://kivy.org/docs/guide/graphics.html

Essentially, it because Kivy is event based.  Technically, it's not asynchronous as code does run sequentially, but layout is done after your code runs.

Kicv uses an event loop which has to render X frames per second (usually 60). So you don't want to do to much in between frames. Eg. your loop above would totally block anything from happening until it's finished. In addition, a lot of startup code is runs before the layout is done: your code needs to exit and free up the GPU to calculate the widget size and positions from the size/pos_hints. It works really well, but it does take some getting used to.

It one of the reasons the kv lang is really powerful, and it really eases to job of binding to all those events. Rather that calculates everything by looping, you react to size and pos changes. 

Simon J

unread,
Jan 21, 2018, 2:25:42 PM1/21/18
to Kivy users support
Thanks, that's useful to know. I'll keep plugging away...
Reply all
Reply to author
Forward
0 new messages