Working with kivy, how can I check if a widget overlap another widget after they have been rotated. Using the collide_widget method does not take the rotation into account, so the following code snippet prints out "is colliding" even you can see that the rectangles are not overlapping. Is there something else clever I can do with kivy that would allow me to check a collision with rectangles after they have been rotated?
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics.context_instructions import PopMatrix, PushMatrix
from kivy.graphics import Rectangle, Rotate
class RotatableRect(Widget):
def __init__(self, angle=0, **kwargs):
super(RotatableRect, self).__init__(**kwargs)
with self.canvas.before:
PushMatrix()
self.rot = Rotate()
self.rot.angle = angle
self.rot.origin = self.center
self.rot.axis = (0, 0, 1)
self.rect = Rectangle(pos=self.pos, size=self.size)
with self.canvas.after:
PopMatrix()
class MainWidget(Widget):
def __init__(self, **kwargs):
super(MainWidget, self).__init__(**kwargs)
self.rect1 = RotatableRect(pos=[10, 100], size=[100, 50])
self.rect2 = RotatableRect(pos=[100, 50], size=[100, 50], angle=45)
self.add_widget(self.rect1)
self.add_widget(self.rect2)
if self.rect1.collide_widget(self.rect2):
print 'is colliding'
class TheApp(App):
def build(self):
parent = Widget()
parent.add_widget(MainWidget())
return parent
if __name__ == '__main__':
TheApp().run()
Check the Rotabox widget in the kivy-garden.
I made it originally for this exact collision+rotation purpose, although it evolved a lot since then.
Firstly, include rotabox.py in your project.
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Rectangle
from rotabox import Rotabox
class RotatableRect(Rotabox):
def __init__(self, **kwargs):
super(RotatableRect, self).__init__(**kwargs)
with self.canvas:
self.rect = Rectangle(pos=self.pos, size=self.size)
class MainWidget(Widget):
def __init__(self, **kwargs):
super(MainWidget, self).__init__(**kwargs)
self.rect1 = RotatableRect(pos=[10, 100], size=[100, 50])
self.rect2 = RotatableRect(pos=[100, 50], size=[100, 50], angle=45)
self.add_widget(self.rect1)
self.add_widget(self.rect2)
# for a stationary rotabox, it's essential to wait for it to setup,
# before any collision checks
self.rect1.bind(ready=self.check)
def check(self, *args):
if self.rect1.collide_widget(self.rect2):
print ' is colliding'
class TheApp(App):
def build(self):
return MainWidget()
if __name__ == '__main__':
TheApp().run()
The rotation itself is handled by the widget as long as an angle is given and the default origin of rotation is the widget's center.
Related
I'm trying to draw a rectangle and place it on the screen depending
on the size of the window / screen.
I've got this but it doesn't work:
with self.canvas:
Color: xyz
Rectangle(pos=(Window.size[0]-50,Window.size[1]-50),size=(50,50))
This didn't have any effect either:
with self.canvas:
Color: xyz
Rectangle(pos=(Window.height-50,Window.width-50),size=(50,50))
Runnable example:
from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.app import App
from kivy.graphics import *
Window.size = (375, 812)
class Temporary(Widget):
def __init__(self, **kwargs):
super(Temporary, self).__init__(**kwargs)
with self.canvas:
Color(255/255.0, 99/255.0, 71/255.0)
Rectangle(pos=(0,0),size=(50,50))
class MainApp(App):
def build(self):
return Temporary()
if __name__ == '__main__':
MainApp().run()
I've managed to make it work as I intended:
class Temporary(Widget):
def __init__(self, **kwargs):
super(Temporary, self).__init__(**kwargs)
with self.canvas:
Color(255/255.0, 99/255.0, 71/255.0)
Rectangle(pos=(Window.size[0] / 2,Window.size[1] / 2),size=(50,50))
Nothing else worked. The above will do the trick.
Hello I'm relatively new to kivy. So far doing basic stuff has been relatively straightforward but this has stumped me. I'm making an app that needs to dynamically add rectangular canvas items to a grid in a scrollview. Since I'm doing this I need to create the scrollview in python and not in the .kv file. How can I do this so that the size of the rectangles will be the same as the window size upon resizing the windows?
.py file:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.graphics import Line,Rectangle
from kivy.uix.carousel import Carousel
from kivy.uix.scrollview import ScrollView
from kivy.core.window import Window
class Scroll(ScrollView):
def __init__(self, **kwargs):
super(Scroll, self).__init__(**kwargs)
layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
layout.bind(minimum_height=layout.setter('height'))
# Make sure the height is such that there is something to scroll.
for i in range(100):
SkillStat = RelativeLayout(pos=(0,0), height=100, size_hint_y=None, size_hint_x=self.width)
with SkillStat.canvas:
Rectangle(pos=self.pos,size=(self.width, 90))
layout.add_widget(SkillStat)
self.add_widget(layout)
pass
pass
class Sheet(Carousel):
pass
class SheetApp(App):
def build(self):
return Sheet()
if __name__ == '__main__':
SheetApp().run()
.kv file:
# file name: Sheet.kv
<Sheet>:
RelativeLayout:
Scroll:
size_hint:(1,1)
The two main problem in your code are:
You are doing all your size and position setting of your SkillStat and its canvas in an __init__() method. In an __init__() method of a widget, the position of the widget is always (0,0), and the size is (100, 100). Those properties are not set to real values until the widget is actually drawn.
You are doing all this in python instead of in kv. In kv, bindings are created for many properties that you set, and get automatically updated. If you do your widget setup in python, you must provide those bindings yourself.
Here is a modified version of your Scroll class and a new MyRelativeLayout class that handle those bindings:
class MyRelativeLayout(RelativeLayout):
def adjust_size(self, *args):
self.rect.size = self.size # set the size of the Rectangle
class Scroll(ScrollView):
def __init__(self, **kwargs):
super(Scroll, self).__init__(**kwargs)
layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
layout.bind(minimum_height=layout.setter('height'))
# Make sure the height is such that there is something to scroll.
for i in range(100):
SkillStat = MyRelativeLayout(pos=(0,0), height=100, size_hint=(1.0, None))
with SkillStat.canvas.before:
SkillStat.rect = Rectangle()
SkillStat.bind(size=SkillStat.adjust_size)
layout.add_widget(SkillStat)
self.add_widget(layout)
Note the SkillStat.bind() call to create the needed bindings, and the Rectangle is saved as SkillStat.rect in each MyRelativeLayout instance. Those bindings will get triggered as soon as the SkillStat gets displayed, so the initial pos and size of the Rectangle are not needed.
EDIT: Setting the pos of the Rectangle in a binding was probably causing problems. The default pos of the Rectangle is (0,0), which is what it should always be. So, we only need to adjust the size of the Rectangle. I have removed the binding for pos.
Solution
Create a class with inheritance from RelativeLayout.
Update or remove the instructions you have added to a canvas by using bind function.
Snippets
class CustomLayout(RelativeLayout):
def __init__(self, **kwargs):
super(CustomLayout, self).__init__(**kwargs)
with self.canvas:
self.rect = Rectangle(pos=self.pos, size=(self.width, 90))
self.bind(pos=self.update_rect, size=self.update_rect)
def update_rect(self, *args):
self.rect.pos = self.pos
self.rect.size = self.size
Example
main.py
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.graphics import Line, Rectangle
from kivy.uix.carousel import Carousel
from kivy.uix.scrollview import ScrollView
from kivy.core.window import Window
from kivy.lang import Builder
class CustomLayout(RelativeLayout):
def __init__(self, **kwargs):
super(CustomLayout, self).__init__(**kwargs)
with self.canvas:
self.rect = Rectangle(pos=self.pos, size=(self.width, 90))
self.bind(pos=self.update_rect, size=self.update_rect)
def update_rect(self, *args):
self.rect.pos = self.pos
self.rect.size = self.size
class Scroll(ScrollView):
def __init__(self, **kwargs):
super(Scroll, self).__init__(**kwargs)
layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
layout.bind(minimum_height=layout.setter('height'))
# Make sure the height is such that there is something to scroll.
for i in range(100):
SkillStat = CustomLayout(pos=(0, 0), height=100, size_hint_y=None, size_hint_x=self.width)
layout.add_widget(SkillStat)
self.add_widget(layout)
class Sheet(Carousel):
pass
Builder.load_file('main.kv')
class SheetApp(App):
def build(self):
return Sheet()
if __name__ == '__main__':
SheetApp().run()
main.kv
#:kivy 1.11.0
<Sheet>:
RelativeLayout:
Scroll:
size_hint:(1,1)
bar_width: 10
effect_cls: "ScrollEffect"
scroll_type: ['bars']
bar_color: [1, 0, 0, 1] # red color
bar_inactive_color: [0, 0, 1, 1] # blue color
Output
I'm trying to translate the beginnings of a simple canvas app I wrote in JavaScript to the Kivy framework. I have been able to distribute vertices along the perimeter of a circle, but I have been unsuccessful in registering click events on each vertex whether attempted in Python or Kv language. A nice start might be changing the size of any vertex clicked. Any tips, feedback, solutions welcome.
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.graphics import *
from kivy.properties import NumericProperty
from random import randint
import math
class Vertex(Widget):
def __init__(self, position=(50,50), **kwargs):
super(Vertex, self).__init__(**kwargs)
self.position = position
self.size = (10,10)
def draw(self):
with self.canvas:
Color(1., 0, 0)
Ellipse(pos=self.position, size=self.size)
class ChromaticCircle(Widget):
vertices = []
def __init__(self, radius=100, **kwargs):
super(ChromaticCircle, self).__init__(**kwargs)
self.radius = radius
self.draw()
def draw(self):
interval = (math.pi * 2) / 12
with self.canvas:
Color(1., 0, 0)
for i in range(1, 13):
angle = (math.radians(360) / 12) * (i + 9)
position = ((self.center_x + 200) + (self.radius*math.cos(angle)), (self.center_y + 200)+(self.radius)*math.sin(angle))
self.vertices.append(Vertex(position))
for j in range(len(self.vertices)):
self.vertices[j].draw()
class MyApp(App):
def build(self):
return ChromaticCircle()
if __name__ == '__main__':
MyApp().run()
You can capture clicks on your Vertex widget by using the collide_point method in the on_touch_down method to check if a Touch event occurs within a Vertex:
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.graphics import *
from kivy.properties import NumericProperty
from random import randint
import math
class Vertex(Widget):
def __init__(self, position=(50,50), **kwargs):
super(Vertex, self).__init__(**kwargs)
self.position = position
self.size = (10,10)
self.pos = position # need to set the `pos` as `collide_point` uses it
def draw(self):
with self.canvas:
Color(1., 0, 0)
Ellipse(pos=self.position, size=self.size)
class ChromaticCircle(Widget):
vertices = []
def __init__(self, radius=100, **kwargs):
super(ChromaticCircle, self).__init__(**kwargs)
self.radius = radius
self.draw()
def on_touch_down(self, touch):
# check if the touch is on a Vertex
for vert in self.vertices:
if vert.collide_point(touch.x, touch.y):
print('click on vertex: ' + str(vert.pos))
return True
return super(ChromaticCircle, self).on_touch_down(touch)
def draw(self):
interval = (math.pi * 2) / 12
with self.canvas:
Color(1., 0, 0)
for i in range(1, 13):
angle = (math.radians(360) / 12) * (i + 9)
position = ((self.center_x + 200) + (self.radius*math.cos(angle)), (self.center_y + 200)+(self.radius)*math.sin(angle))
print('adding vertex at ' + str(position))
self.vertices.append(Vertex(position))
for j in range(len(self.vertices)):
self.vertices[j].draw()
class MyApp(App):
def build(self):
return ChromaticCircle()
if __name__ == '__main__':
MyApp().run()
There are some additional details that you may want to consider. The pos of the Ellipse is the lower left corner, so your vertex position is not at the center of the drawn Ellipse.
I have two button widgets. The Button1 was rotated and the Button2 wasn't. I'm trying to test if the two button widgets will collide.
Here is my code:
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.graphics import *
from kivy.logger import Logger
class RotateMe(Button):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
with self.canvas.before:
PushMatrix()
self.rot = Rotate(angle= 45, origin= self.center)
with self.canvas.after:
PopMatrix()
class Main(Widget):
def __init__(self):
super(Main, self).__init__()
self.size = Window.size
self.rotatethis = RotateMe(text= "Button1", center= self.center)
self.add_widget(self.rotatethis)
self.somebutton = Button()
self.somebutton.text = "Button2"
self.somebutton.right = self.center_x - 60
self.somebutton.y = self.center_y
self.add_widget(self.somebutton)
if self.somebutton.collide_widget(self.rotatethis):
Logger.info("The buttons collided! Much happiness")
else:
Logger.info("The buttons didn't collide. Such sadness.")
class TestApp(App):
def build(self):
return Main()
TestApp().run()
This is how it looked when I ran my code.
I can clearly see from the output (the image) that the two buttons collided but when I checked the log , it says that the Button Widgets didn't collide. It seems that the widget position of Button1 didn't change/rotate.
I tried to change the position of the Button2 to see at which point it collides with Button1. And I found out that the Widget position of Button1 didn't change.
Like this:
The white square at the back of Button1 is the actual position of the Button1 widget.
Is there a way to rotate the position of Button1 widget?
UPDATE: I tried to use the ScatterLayout as suggested in here.
Here is my updated code:
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.graphics import *
from kivy.logger import Logger
from kivy.uix.scatterlayout import ScatterLayout
from kivy.uix.floatlayout import FloatLayout
class RotateMe(ScatterLayout):
def __init__(self):
ScatterLayout.__init__(self)
self.size_hint = (None, None)
self.size = (100, 100)
self.rotation = 45
self.do_rotation = False
self.add_widget(Button(text= "Button1"))
class Main(FloatLayout):
def __init__(self):
super(Main, self).__init__()
self.size = Window.size
self.rotatethis = RotateMe()
self.rotatethis.center = self.center
self.add_widget(self.rotatethis)
self.button2 = Button(text= "Button2")
self.button2.size_hint = (None, None)
self.button2.size = (100, 100)
self.button2.right = self.center_x - 60
self.button2.y = self.center_y
self.add_widget(self.button2)
if self.button2.collide_widget(self.rotatethis):
Logger.info("The buttons collided! Much happiness")
else:
Logger.info("The buttons didn't collide. Such sadness.")
class TestApp(App):
def build(self):
return Main()
TestApp().run()
Now the buttons were collided but when I try to change the position of the Button2, like this:
self.button2.y = self.center_y + 45
The output is like this:
Looking at the output (image), the two buttons' widgets doesn't seem to collide with each other but when I look at the log, it says that they collided with each other.
It turns out that the widget size of Button1 is this (the colored teal background):
I want to draw circle in center of FloatLayout. With my knowledges I obtained only default values for this. Why circle in showed code isn't red? Can You explain me process for obtaining necessary coordinates, please?
import kivy
from kivy.config import Config
kivy.config.Config.set('graphics','resizable', False)
from kivy.app import App
from kivy.graphics import Color, Ellipse
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
class Scene(FloatLayout):
def __init__(self, **kwargs):
super(Scene, self).__init__(**kwargs)
def draw_circle(self):
with self.canvas:
Color=(1,0,0)
circ = Ellipse(pos = (self.center_x, self.center_y), size=(20,20))
def on_touch_down(self, touch):
pass
class Game(BoxLayout):
def __init__ (self,**kwargs):
super(Game, self).__init__(**kwargs)
self.orientation = 'vertical'
but1 = Button(text = 'button 1')
self.add_widget(but1)
self.scene = Scene()
self.add_widget(self.scene)
class TestApp(App):
def build(self):
game = Game()
game.scene.draw_circle()
return game
if __name__ == '__main__':
TestApp().run()
You should define the size of your float layout when you create it.
self.scene = Scene(size=(300, 300))
Then your circle should be at the center of the FloatLayout dimensions.
I also think FloatLayout is better used with size_hint and pos_hint instead of fixed coordinates.
You can call draw_circle with Clock to make sure the layout is completely initiated first.
Then make sure to create your color like this Color(1, 0, 0). Not Color = ()
from kivy.config import Config
Config.set('graphics','resizable', False)
from kivy.app import App
from kivy.graphics import Color, Ellipse
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.clock import Clock
class Scene(FloatLayout):
def draw_circle(self, dt):
with self.canvas:
Color(1,0,0)
circ = Ellipse(pos = (self.center_x, self.center_y), size=(20,20))
class Game(BoxLayout):
def __init__ (self,**kwargs):
super(Game, self).__init__(**kwargs)
self.orientation = 'vertical'
but1 = Button(text = 'button 1')
self.add_widget(but1)
self.scene = Scene()
self.add_widget(self.scene)
class TestApp(App):
def build(self):
game = Game()
Clock.schedule_once(game.scene.draw_circle) # call draw_circle on next frame
return game
if __name__ == '__main__':
TestApp().run()