How to fix aspect ratio of a kivy game? - python

I've started playing with Kivy, but I'm rather new to all this and am already struggling.
I'm trying to make a board game - I want the window to be resizeable, but I don't want resizing the window to mess up the aspect ratio of the game (so in other words, I want the window to have black bars above or to the sides of the content if the window is resized to something other than the intended aspect ratio of the content)
The easiest way I could see to ensure this is to either:
a) Lock the aspect ratio of the Window itself so it's always 10:9 (the aspect ratio of the board and all on-screen elements) - even when fullscreen.
or
b) Use some sort of widget/surface/layout that is centered on the window and has a locked 10:9 aspect ratio. I then subsequently use this as the base onto which all my other images, widgets, grids etc are placed.
However, I really don't know how to do either of these. I'm not sure if I can lock the Window aspect ratio, and the only kivy-specific object I've found that lets me lock the aspect ratio is from kivy.graphics.image... which I don't seem to be able to use as a 'base' for my other stuff.
EDIT: So far I've written the code below: it creates a layout (and colours it slightly red) that 'fixes' its own aspect ratio whenever it is resized. However, it still isn't centered in the window, and more problematic, it results in an endless loop (probably because the aspect fix code corrects the size, but then kivy 'corrects' its size back to being the size of the window, triggering the aspect fix again, thought I could be wrong).
EDIT: Modified the code again, but it's still an endless loop. I though that referring only to the parent for size info would fix that, but apparently not.
I'd appreciate anyone helping me fix my code.
Code below:
test.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.image import Image
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.properties import (ObjectProperty,
NumericProperty,
ReferenceListProperty)
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Rectangle
class Board(AnchorLayout):
pass
class BackgroundLayout(RelativeLayout):
def FixAspectRatio(self, *args):
correctedsize = self.parent.size
if correctedsize[0] > correctedsize[1]*(10/9):
correctedsize[0] = correctedsize[1]*(10/9)
elif correctedsize[0] < correctedsize[1]*(10/9):
correctedsize[1] = correctedsize[0]/(10/9)
return correctedsize
class test(App):
game = ObjectProperty(None)
def build(self):
self.game = Board()
return self.game
if __name__ == '__main__':
test().run()
test.kv
<Board>
BackgroundLayout:
canvas.before:
Color:
rgba: 1, 0, 0, 0.5
Rectangle:
size: self.size
pos: self.pos
size: self.FixAspectRatio(self.parent.size)
pos: self.parent.pos

One approach would be to create a Layout that always maximize the children given an aspect ratio. Here is an example::
from kivy.lang import Builder
from kivy.app import App
from kivy.uix.relativelayout import RelativeLayout
from kivy.properties import NumericProperty
kv = """
ARLayout:
Widget:
canvas:
Color:
rgb: 1, 0, 0
Rectangle:
size: self.size
pos: self.pos
"""
class ARLayout(RelativeLayout):
# maximize the children given the ratio
ratio = NumericProperty(10 / 9.)
def do_layout(self, *args):
for child in self.children:
self.apply_ratio(child)
super(ARLayout, self).do_layout()
def apply_ratio(self, child):
# ensure the child don't have specification we don't want
child.size_hint = None, None
child.pos_hint = {"center_x": .5, "center_y": .5}
# calculate the new size, ensure one axis doesn't go out of the bounds
w, h = self.size
h2 = w * self.ratio
if h2 > self.height:
w = h / self.ratio
else:
h = h2
child.size = w, h
class TestApp(App):
def build(self):
return Builder.load_string(kv)
TestApp().run()

I managed to find a solution I was happy with with a lot of help from kived from the kivy chat.
The below bases the game bounds on a 'background' image, then places a Relative Layout over this background image. For all subsequent children, the coordinate (0, 0) will refer to the bottom left of the (aspect locked) background image, and they can use the 'magnification' property of the Background to adjust their size/position according to the screen size.
Provided any gamebounds.png image, the below code will implement this. Note that the RelativeLayout is shaded red (with 50% transparency) just to show its position (and show that it matches the gamebounds.png image size and position at all times):
test.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.image import Image
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.properties import (ObjectProperty,
NumericProperty,
ReferenceListProperty,
ListProperty)
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Rectangle
class Game(FloatLayout):
pass
class Background(Image):
offset = ListProperty()
magnification = NumericProperty(0)
class Bounds(RelativeLayout):
pass
class test(App):
game = ObjectProperty(None)
def build(self):
self.game = Game()
return self.game
if __name__ == '__main__':
test().run()
test.kv
<Game>
Background:
id: BackgroundId
#Provide the image that will form the aspect-locked bounds of the
#game. Image widgets have aspect_lock set to True by default.
source: 'gamebounds.png'
#All the image to stretch to fill the available window space.
allow_stretch: True
size: self.parent.size
#Find the coordinates of the bottom-left corner of the image from
#the bottom-left corner of the window. Call this the 'offset'.
offset: [self.center_x - (self.norm_image_size[0]/2),
self.center_y - (self.norm_image_size[1]/2)]
#Find out the factor by which the image is magnified/shrunk from
#its 'default' value:
magnification: self.norm_image_size[0] / self.texture_size[0]
Bounds:
#The canvas below isn't needed, it's just used to show the
#position of the RelativeLayout
canvas:
Color:
rgba: 1, 0, 0, 0.5
Rectangle:
size: self.size
pos: (0, 0)
#Set the position of the RelativeLayout so it starts at the
#bottom left of the image
pos: self.parent.offset
#Set the size of the RelativeLayout to be equal to the size of
#the image in Background
size: self.parent.norm_image_size

Related

How to set text color to gradient texture in kivy?

I have used to create a Texture with gradient color and set to the background of Label, Button and etc. But I am wondering how to set this to color of Label?
Sounded like a fun puzzle. So here is what I came up with:
from kivy.lang import Builder
from kivy.graphics.texture import Texture
from kivy.properties import ObjectProperty, ListProperty
from kivy.uix.label import Label
class LabelGradient(Label):
gradient = ObjectProperty(None)
bg_color = ListProperty([0, 0, 0, 255])
def __init__(self, **kwargs):
super(LabelGradient, self).__init__(**kwargs)
# bind to texture to trigger use of gradient
self.bind(texture=self.fix_texture)
def fix_texture(self, instance, texture):
if self.gradient is None:
return
# unbind, so we don't loop
self.unbind(texture=self.fix_texture)
# The normal Label texture is transparent except for the text itself
# This code changes the texture to make the text transparent, and everything else
# gets set to self.bg_color (a property of LabelGradient)
pixels = list(self.texture.pixels)
for index in range(3, len(pixels)-4, 4):
if pixels[index] == 0:
# change transparent pixels to the bg_color
pixels[index-3:index+1] = self.bg_color
else:
# make the text itself transparent
pixels[index] = 0
# create a new texture, blit the new pixels, and apply the new texture
new_texture = Texture.create(size=self.texture.size, colorfmt='rgba')
new_texture.blit_buffer(bytes(pixels), colorfmt='rgba', bufferfmt='ubyte')
new_texture.flip_vertical()
self.texture = new_texture
Builder.load_string('''
<LabelGradient>:
canvas.before:
# draw the gradient below the normal Label Texture
Color:
rgba: 1,1,1,1
Rectangle:
texture: self.gradient
size: self.texture_size
pos: int(self.center_x - self.texture_size[0] / 2.), int(self.center_y - self.texture_size[1] / 2.)
''')
if __name__ == '__main__':
from kivy.app import App
from kivy.lang import Builder
class LabelGradientApp(App):
grad = ObjectProperty(None)
def build(self):
# create a 64x64 texture, defaults to rgba / ubyte
self.grad = Texture.create(size=(64, 64))
# create 64x64 rgb tab, and fill with values from 0 to 255
# we'll have a gradient from black to white
size = 64 * 64 * 3
buf = bytes([int(x * 255 / size) for x in range(size)])
# then blit the buffer
self.grad.blit_buffer(buf, colorfmt='rgb', bufferfmt='ubyte')
return theRoot
app = LabelGradientApp()
theRoot = Builder.load_string('''
FloatLayout:
LabelGradient:
text: 'Abba Dabba Doo'
font_size: 30
gradient: app.grad
''')
app.run()
My approach is to make the text transparent, and to draw a gradient behind the text.
The normal Texture created by the Label is transparent except for the actual text.
The LabelGradient above extends Label and draws the gradient using the <LabelGradient> rule in the kv. The fix_texture() method is bound to the texture property of the Label, so it is called as soon as the Texture is created. The fix_texture() method makes the actual text transparent and sets the rest of the Texture to the bg_color.
You can't set the color property to a gradient, that just isn't what it does. Gradients should be achieved using images or textures directly applied to canvas vertex instructions.

Problem that figures were not drawn in expected cell location on grid layout using canvas

In the following code, there are problems (1) and (2) according to the title.
If kvLang is used as described in this code, the figure (blue ellipse) will be drawn to the expected Cell position (#(1, 1) = upper left).
(However, the character string specified by text: is not displayed at this time. Please tell me how to display text characters .... Problem (1))
I have intended to have coded a Python script to draw with .add_widget method followed to the kvLang.
In this script, the yellow ellipse appears in the lower right instead of the expected Cell position ((2, 2) => lower left) ... problem (2)
For the purpose, it is necessary to add a widget to Grid Laytout using the .add_widget method so that the shape drawn in canvas can be displayed in the cell.
Please tell us how to solve it.
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Ellipse
from kivy.lang import Builder
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.app import App
Builder.load_string('''
<MyGridLayout#GridLayout>:
cols: 2
Label:
text:"From Kv_1" #(1)Not show. What's problem?
canvas:
Color:
rgb:0,0,1
Ellipse:
pos:self.pos
size:self.size
''')
class MyGridLayout(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add_widget(Button(text="From_Py_1"))
self.add_widget(Button(text="From_Py_2"))
labl = Button(text="From_Py_canvas_1")
self.add_widget(labl)
with labl.canvas:
# (2) Expected to draw at cell(2,2) which is same locaton as the lable of "From_Py_canvas_1" but not.
Color(rgb=(1, 1, 0))
Ellipse(pos=labl.pos, size_hint=labl.size_hint)
class MyApp(App):
def build(self):
return MyGridLayout()
if __name__ == '__main__':
MyApp().run()
Problem 1: You just need to use canvas.before: instead of canvas: in your kv. This draws the ellipse before the text, instead of after (obscuring the text).
Problem 2: Your Ellipse is the using the current properties of the labl, which are default values until the app is displayed. To fix that, create the Ellipse after by using Clock.schedule_once(). In the modified code below, I saved a reference to labl in order to access it in the draw_yellow_ellipse() method:
from kivy.clock import Clock
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Ellipse
from kivy.lang import Builder
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.app import App
Builder.load_string('''
<MyGridLayout#GridLayout>:
cols: 2
Label:
text:"From Kv_1" #(1)Not show. What's problem?
canvas.before:
Color:
rgb:0,0,1
Ellipse:
pos:self.pos
size:self.size
''')
class MyGridLayout(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add_widget(Button(text="From_Py_1"))
self.add_widget(Button(text="From_Py_2"))
self.labl = Button(text="From_Py_canvas_1")
self.add_widget(self.labl)
Clock.schedule_once(self.draw_yellow_ellipse)
def draw_yellow_ellipse(self, dt):
with self.labl.canvas:
# (2) Expected to draw at cell(2,2) which is same locaton as the lable of "From_Py_canvas_1" but not.
Color(rgb=(1, 1, 0))
Ellipse(pos=self.labl.pos, size_hint=self.labl.size_hint)
class MyApp(App):
def build(self):
return MyGridLayout()
if __name__ == '__main__':
MyApp().run()
Also, I don't think the Ellipse honors size_hint. Perhaps you meant to use size=self.labl.size. And if you do that, you will again have an Ellipse obscuring, in this case, your Button.

Width of root - kivy

I've got a problem with my kivy program... Especially with the width of my root, it's more less than the width of the window...
Like this :
here
I don't understand...
Here my code:
First python file :
from kivy.app import App
from kivy.config import Config
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.uix.button import Button
Config.set('graphics','width','450')
Config.set('graphics','height','800')
class Saisi(Widget):
pass
class Jeu(Widget):
pass
class WorDown(App):
def build(self):
return Jeu()
if __name__ == '__main__':
WorDown().run()
And my kivy file:
<Saisi>:
canvas:
Rectangle:
pos: self.pos
size: root.width , 50 ← I think, this is it...
<Jeu>:
Saisi:
y: root.height / 2
Someone can help me ? I just want to "resize" the "root width", because all my elements have a max width like this...
Thanks for reading.
<Jeu>:
Saisi:
y: root.height / 2
Jeu is a widget and not a special layout type, so it doesn't impose any position or size on its children, therefore the Saisi instance has the default position of (0, 0) and size of (100, 100).
Make Jeu inherit from e.g. BoxLayout (recommended), or alternatively manually set the Saisi pos/size to match that of the Jeu in the above rule.

Kivy - Adding elements in a Screen

import os,sys,random
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.graphics import Color, Rectangle
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import Image
from kivy.uix.screenmanager import ScreenManager, Screen , SlideTransition
from kivy.animation import Animation
from kivy.properties import StringProperty
class Page(Screen):
source = StringProperty()
class Imglayout(FloatLayout):
def __init__(self,**args):
super(Imglayout,self).__init__(**args)
with self.canvas.before:
Color(0,0,0,0)
self.rect=Rectangle(size=self.size,pos=self.pos)
self.rootpath = os.path.dirname(os.path.realpath(sys.argv[0]))
self.images=[]
for img in os.listdir(self.rootpath+'/images'):
self.images.append(self.rootpath+'/images/'+img)
self.index=random.randint(0,len(self.images)-1)
self.im=Image(source=self.images[self.index])
self.im.keep_ratio= False
self.im.allow_stretch = True
#self.add_widget(self.im)
self.sm = ScreenManager(transition=SlideTransition())
self.page1=Page(name='page1', source = self.images[self.index])
self.page2=Page(name='page2', source = self.images[self.index+1])
self.sm.add_widget(self.page1)
self.sm.add_widget(self.page2)
self.bind(size=self.updates,pos=self.updates)
def updates(self,instance,value):
self.rect.size=instance.size
self.rect.pos=instance.pos
def on_touch_down(self,touch):
if self.collide_point(*touch.pos):
if(self.sm.current == 'page1'):
next='page2'
page=self.page2
else:
next='page1'
page=self.page1
self.index=(self.index+1)%len(self.images)
page.source = self.images[self.index]
page.background.scale = 1.0
self.sm.transition=SlideTransition()
self.sm.current = next
anim = Animation(
scale=page.background.scale*1.3,
duration=15.0
)
anim.start(page.background)
return True
return False
class MainTApp(App):
def build(self):
root = BoxLayout(orientation='vertical',spacing=10)
c = Imglayout()
root.add_widget(c)
cat=Button(text="Categories",size_hint=(1,.05))
cat.bind(on_press=self.callback)
root.add_widget(cat);
return root
def callback(self,value):
print "CALLBACK CAT"
if __name__ == '__main__':
MainTApp().run()
Taking some hints from here i made the above program. It says that Page does not have a background attribute in both my and the referenced code. Its kind of obvious since there is no background attribute. I thought it inherited that from Screen. I am trying to make a slideshow kind of thing. But i cant find any information on how to set the background of a screen.
And if i comment out everything with .background and run the app. click on the black space, then i start getting this error continuously on the terminal
[ERROR ] [OSC ] Address 127.0.0.1:3333 already in use, retry
in 2 second
And i still dont get any background on the app.(its all black.)
and if i print self.sm.current on the touich function. Then i find that its always page1, it never changes.
The Kivy Guide explains how to add a background to a Widget. Briefly, you can do it in Python using the following code which binds to the position and size of the widget to make sure the background moves with the widget.
with widget_instance.canvas.before:
Color(0, 1, 0, 1) # green; colors range from 0-1 instead of 0-255
self.rect = Rectangle(size=layout_instance.size,
pos=layout_instance.pos)
widget_instance.bind(size=self._update_rect, pos=self._update_rect)
...
def _update_rect(self, instance, value):
self.rect.pos = instance.pos
self.rect.size = instance.size
Or you can do it more naturally with the kv language e.g.
MyWidget:
canvas.before:
Color:
rgba: 0, 1, 0, 1
Rectangle:
# self here refers to the widget i.e screen
pos: self.pos
size: self.size

Kivy background in scroll view

Summary of test application: I am writing a Kivy app with a scrollable view (named Scroller) with many fields (named Field) to look at. These separate fields are really difficult to distinguish on occasion, so I decided to use alternating background colors for each field to help distinguish each other. My testing application uses 20 individual fields each of which alternates between dark grey and darker grey.
Testing trials:
Starting the application, the program looks great. The alternating background appear just fine. Even when I scroll down the application looks fine. However, the application seems to get bizarre when I scroll up on the application. The text scrolls with the application, but the background does not. Even better (sarcastically), the text starts to fade away into their neighbors background. The problem just seems to vanish when I scroll down again (passed the point of the furthest scroll up point).
Brief problem description: The Field's "background color" messes up the application during scrolling up events.
Side note: I have also noticed that the application got a little sluggish after scrolling too much. I am not that familiar with the drawing cycle of Kivy, but blitting backgrounds should not yield an excessive slowdown.
Testing application:
import kivy
kivy.require('1.0.7')
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.graphics import Color, Rectangle
class Main(App):
def build(self):
self.root = GridLayout(rows = 1)
self.root.add_widget(Scroller())
return self.root
class Scroller(ScrollView):
def __init__(self):
ScrollView.__init__(self)
self.view = GridLayout(cols = 1, size_hint = (1, None))
self.add_widget(self.view)
self.view.bind(minimum_height = self.view.setter('height'))
for i in range(20):
self.view.add_widget(Field('Test field {}'.format(i),i%2 is 0))
class Field(GridLayout):
def __init__(self, name, bg):
assert isinstance(name, str)
assert isinstance(bg, bool)
self.bg = bg
GridLayout.__init__(self,
rows = 1,
padding = 10,
size = (0, 60),
size_hint = (1, None))
self.add_widget(Label(text = name))
self.add_widget(Button(text = 'Test button',
size = (200, 0),
size_hint = (None, 1)))
self.bind(pos = self.change_background)
self.bind(size = self.change_background)
def change_background(self, *args):
with self.canvas.before:
if self.bg:
Color(0.2, 0.2, 0.2, mode = 'rgb')
else:
Color(0.1, 0.1, 0.1, mode = 'rgb')
Rectangle(pos = self.pos, size = self.size)
if __name__ in ('__main__', '__android__'):
app = Main()
app.run()
def change_background(self, *args):
self.canvas.before.clear()#<- clear previous instructions
with self.canvas.before:
if self.bg:
Color(0.2, 0.2, 0.2, mode = 'rgb')
else:
Color(0.1, 0.1, 0.1, mode = 'rgb')
Rectangle(pos = self.pos, size = self.size)
You are adding/piling instructions to the canvas every time the Field's position/size changes, without clearing the previous instructions.
You should also look into using kv as for anything more than a small snippet it ends up saving you a lot of time. You can convert you code using kv like so ::
import kivy
kivy.require('1.0.7')
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scrollview import ScrollView
from kivy.properties import ObjectProperty, BooleanProperty
from kivy.lang import Builder
Builder.load_string('''
<Scroller>
# root is Scroller here
# create a new ObjectProperty in kv that holds the ref to Gridlayout
# so you can access the instance in python code
view: glayout
GridLayout:
id: glayout
cols: 1
size_hint: (1, None)
height: self.minimum_height
<Field>
canvas.before:
Color:
rgba: (0.2, 0.2, 0.2, 1) if self.bg else (0.1, 0.1, 0.1, 1)
Rectangle:
# binding properties is done implicitly and instructions aren't
# piled up while doing that.
pos: self.pos
# self here refers to Field as `self` is supposed to refer to the
# Widget not the drawing instruction
size: self.size
rows: 1
padding: 10
size: (0, 60)
size_hint: (1, None)
Label:
text: root.name
Button:
text: 'test button'
size: (200, 0)
size_hint: (None, 1)
''')
class Main(App):
def build(self):
self.root = GridLayout(rows = 1)
self.root.add_widget(Scroller())
return self.root
class Scroller(ScrollView):
def __init__(self, **kwargs):
super(Scroller, self).__init__(**kwargs)
for i in range(20):
# access self.view that was set in kv
self.view.add_widget(
Field(
name = 'Test field {}'.format(i),
bg = i%2 is 0))
class Field(GridLayout):
# use kivy's Properties so it becomes easier to observe and apply changes
# as a plus these can also be directly used in kv. As a advantage of using this now
# you can change name and bg dynamically and the changes should be reflected on
# screen
name = ObjectProperty('Test field uninitialized')
bg = BooleanProperty(False)
if __name__ in ('__main__', '__android__'):
app = Main()
app.run()

Categories