I'm still warming up to this Kivy stuff, hope you don't mind another question from me!
I'm trying to understand scheduled events for some of my widgets. For the sake of context, I want to be able to move nodes onto edges, and have the edges 'snap to' the node.
Here is my .kv file:
<GraphInterface>:
node: graph_node
edge: graph_edge
GraphNode:
id: graph_node
center: self.parent.center
GraphEdge:
id: graph_edge
center: 200,200
<GraphNode>:
size: 50, 50
canvas:
Color:
rgba: (root.r,1,1,1)
Ellipse:
pos: self.pos
size: self.size
<GraphEdge>:
size: self.size
canvas:
Color:
rgba: (root.r,1,1,1)
Line:
width: 2.0
close: True
I have a collision detection event between nodes and edges, and I am dynamically creating nodes and edges within my program. Here is the relevant python code:
class GraphInterface(Widget):
node = ObjectProperty(None)
edge = ObjectProperty(None)
def update(self, dt):
# detect node collision
self.edge.snap_to_node(self.node)
return True
class GraphEdge(Widget):
r = NumericProperty(1.0)
def __init__(self, **kwargs):
super(GraphEdge, self).__init__(**kwargs)
with self.canvas:
self.line = Line(points=[100, 200, 200, 200], width = 2.0, close = True)
def snap_to_node(self, node):
if self.collide_widget(node):
print "collision detected"
del self.line.points[-2:]
self.line.points+=node.center
self.size = [math.sqrt(((self.line.points[0]-self.line.points[2])**2 + (self.line.points[1]-self.line.points[3])**2))]*2
self.center = ((self.line.points[0]+self.line.points[2])/2,(self.line.points[1]+self.line.points[3])/2)
pass
class GraphApp(App):
def build(self):
node = GraphNode()
game = GraphInterface()
createNodeButton = Button(text = 'CreateNode', pos=(100,0))
createEdgeButton = Button(text = 'CreateEdge')
game.add_widget(createNodeButton)
game.add_widget(createEdgeButton)
#scatter = Scatter(do_rotation=False, do_scale=False)
#scatter.add_widget(node)
def createNode(instance):
newNode = GraphNode()
game.add_widget(newNode)
#scatter.add_widget(newNode)
print "Node Created"
def createEdge(instance):
newEdge = GraphEdge()
game.add_widget(newEdge)
#scatter.add_widget(newEdge)
print "Edge Created"
createNodeButton.bind(on_press=createNode)
createEdgeButton.bind(on_press=createEdge)
Clock.schedule_interval(game.update, 1.0/60.0)
return game
Ok, so that's a lot to read (sorry)!
Basically my collision is only detected for the initial edges and nodes I create in the .kv file. The newly created edges and nodes cannot interact via the collision mechanism.
I can put up more code if people want but I think that should be all the relevant parts. Thanks!
EDIT:
New method:
def on_touch_move(self, touch):
if touch.grab_current is self:
self.pos=[touch.x-25,touch.y-25]
for widget in self.parent.children:
if isinstance(widget, GraphEdge) and widget.collide_widget(self):
print "collision detected"
widget.snap_to_node(self)
return True
return super(GraphNode, self).on_touch_move(touch)
With this code I can still break the connection by moving the nodes quickly.
EDIT2:
I perhaps gave the answer a little prematurely. I can only interact with the first edge I spawned. And I also have a weird bug where the first edge and the first node seem to have the same color properties. Though perhaps this should be posed in it's own question.
The problem is that you don't react to new GraphEdges or GraphNodes being added to your GraphInterface. In update:
self.edge.snap_to_node(self.node)
self.edge is always the kv-created GraphEdge. Same with self.node.
You should try adding this interaction to the GraphNode class itself, in your touch interactions. Try something like this in GraphNode.on_touch_move():
for widget in self.parent.children:
if isinstance(widget, GraphEdge) and widget.collide_widget(self):
print "collision detected"
...
break
This way, each GraphNode searches through its siblings for any GraphEdge with which it may collide.
Related
I am having a custom widget with a rectangle and horizontal line both created using Vertex Instruction. I want to check whether users is touching within rectangle or horizontal line in my widget. Tried using Group but unable to find whether user touched rectangle or Line. Can you please provide me with clue.
Find below sample code.
from kivy.app import App
from kivy.graphics import Line
from kivy.uix.scatter import Scatter
from kivy.uix.relativelayout import RelativeLayout
from kivy.lang import Builder
KV = '''
<Actor>:
id: Actor
canvas:
Color:
rgba: 0,1,0,1
Rectangle:
group: 'rect'
size: 100, 30
pos: 0, root.height - 30
Line:
group: 'line'
points: 50, root.height - 30, 50, 0
width:2
Label:
id: _actr_lbl
text: 'Hello World'
markup: True
color: 0,0,0,1
size_hint: None, None
size: 100, 30
pos: 0, root.height - 30
'''
class Actor(Scatter):
def __init__(self, **kwargs):
super(Actor, self).__init__(**kwargs)
def on_touch_down(self, touch):
print('Touch location {} Actor location {} Actor Size {}'.format(touch, self.pos, self.size))
if self.collide_point(*touch.pos) :
for aVertex in self.canvas.get_group('rect') :
try:
print ('Vertex size {} and pos'.format(aVertex.size, aVertex.pos))
except:
pass
return True
return super(Actor, self).on_touch_down(touch)
class MyPaintApp(App):
def build(self):
Builder.load_string(KV)
root = RelativeLayout()
root.add_widget(Actor(pos_hint={'center_x':0.5, 'center_y':0.5}, size_hint=(.2, 1.)))
return root
if __name__ == '__main__':
MyPaintApp().run()
Thanks in advance
You can do a simple bounding box check, but you must take into account the fact that the touch is in the parent coordinate system. So you can convert the touch position to local coordinates, then do the test. Here is an example for the Rectangle:
def on_touch_down(self, touch):
print('Touch location {} Actor location {} Actor Size {}'.format(touch, self.pos, self.size))
if self.collide_point(*touch.pos) :
localTouchPos = self.to_local(*touch.pos)
for aVertex in self.canvas.get_group('rect') :
print('\tVertex size {} and pos {}'.format(aVertex.size, aVertex.pos))
intersection = True
if localTouchPos[0] < aVertex.pos[0]:
intersection = False
elif localTouchPos[0] > aVertex.pos[0] + aVertex.size[0]:
intersection = False
if localTouchPos[1] < aVertex.pos[1]:
intersection = False
elif localTouchPos[1] > aVertex.pos[1] + aVertex.size[1]:
intersection = False
print('intersection =', intersection)
return True
return super(Actor, self).on_touch_down(touch)
You can do something similar for the Line, but it may be a bit more complicated if you want to do a general Line. If your Line is always vertical, it hould be very similar.
Desired result:
Currently my code is as follows:
class KeypadButton(Factory.Button):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
FocusBehavior.ignored_touch.append(touch)
return super(KeypadButton, self).on_touch_down(touch)
class Keypad(Factory.GridLayout):
target = Factory.ObjectProperty(None, allownone=True)
def __init__(self, **kwargs):
super(Keypad, self).__init__(**kwargs)
self.cols = 3
for x in list(range(1, 10)) + ['<-', 0, 'Enter']:
btn = KeypadButton(text=str(x), on_release=self._on_release)
self.add_widget(btn)
def _on_focus(self, ti, value):
self.target = value and ti
def _on_release(self, instance, *largs):
if self.target:
if instance.text == 'Enter':
print("Enter: {}".format(self.target.text))
self.target.text = ''
elif instance.text == '<-':
if self.target.text:
self.target.text = self.target.text[:-1]
else:
self.target.text += str(instance.text)
runTouchApp(Builder.load_string('''
<KeypadTextInput#TextInput>:
keypad: None
on_focus: root.keypad._on_focus(*args)
BoxLayout:
orientation: 'vertical'
KeypadTextInput:
keypad: keypad
Keypad:
id: keypad
size_hint_x: 0.5
pos_hint: {'center_x': 0.5}
'''))
What I want to achieve is that when I press 12345 the login screen disappears and a new screen pop ups. I have an image of the format that is given by the following what I want to achieve.
Not sure how exactly how close you want to get to your desired result, but you can get much closer by just changing your 'kv' to:
<KeypadTextInput#TextInput>:
keypad: None
on_focus: root.keypad._on_focus(*args)
BoxLayout:
orientation: 'vertical'
KeypadTextInput:
keypad: keypad
Keypad:
id: keypad
size_hint_x: 0.5
pos_hint: {'center_x': 0.5}
First when it comes to the issue of focus, I think it depends on how you load up the screen. Is it your root widget or a screen you load up?
As yours looks to be a root widget of sorts you'd probably want to do this with the start up of your app. You can use the 'on_start' event for this
class MyApp(App):
def on_start(self,*args):
self.ids.mytextinput.focus = True #replace mytextinput with whatever id name you give to your text input in the kv string
For the text input firing off events when you type a certain number of digits you could use on_text. For this I think it's best to instantiate your own class if you're starting out.
class KeyPadTextInput(TextInput):
def on_text(self,*args):
if len(self.text)==3:
#put your python code here
#you can launch MyApp functions by using app.function_name()
Another thing I've noticed is that you use on_focus to trigger your own '_on_focus' event with the same *args. You could achieve the same thing by removing the on_focus from your kv string and adjusting the class on_focus event, calling super().on_focus(*args) so the inherited function also fires as such:
class KeyPadTextInput(TextInput):
def on_focus(self,*args):
#your code either before the super call
super().on_focus(*args)
#or your code after the super call
Hope that helps point you in the right direction.
PS. TextInputs have a few prebuilt input filters such as a filter so you can only input numbers! This is handy if the users keyboard comes up or they have access to one too.
in the kv string simply add
input_filter: 'int'
I'm completely new to Kivy, and I'm trying to figure out the best way to even structure what I mean to make. I'd like to be able have some drawn figure on a screen (a triangle made out of 3 circles and a line drawn to connect all of them, for example) wherein I can click on a circle and drag it to another position, redrawing the line with the new position of the circle. Preferably, when dragging, I'd like the circle to "stick" with the cursor/touch input.
Would I be making each of these points Widgets? I know the graphics tools Kivy provides can draw the shapes I need, but I'm not quite sure how I'd be interacting with them again after they're drawn. Additionally, I'm not sure how I'd do the "stick" part of the drag with cursor, since it seems like Kivy would just be drawing the circle repeatedly alongside the cursor, which would result in it overlaying the figure repeatedly.
You might be able to take inspiration from the Bezier example for this, as it does such manipulation for points that guide the line, and allows dragging them, much like you line would.
https://github.com/kivy/kivy/blob/master/examples/canvas/bezier.py
i also did a more complex example here
https://gist.github.com/tshirtman/78669a514f390bf246627b190e2eba1a
which allows creation of multiple lines.
Basically, the idea, if you have multiple interaction points in a widget, is to keep track of the position of these points, in a property, and to use this property to draw the canvas, so the instructions are automatically update when the property change, and to also use the property in the on_touch_down method to check for distance of the touch to them, to decide which (if any) point to interact with, once this is decided, you just need to somehow link that touch to that point, so further interactions (on_touch_move and on_touch_up) with it are consistent (touch.ud is good for that), and to grab the touch so you don't miss any update (a parent widget can always decide this touch is actually not to propagate anymore).
code from the gist for reference (and because SO doesn't like much answers that point to external resources).
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.widget import Widget
from kivy.properties import ListProperty, NumericProperty
from kivy.metrics import dp
KV = '''
#:import chain itertools.chain
FloatLayout:
Label:
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
pos_hint: {'top': 1}
color: 0, 0, 0, 1
padding: 10, 10
text:
'\\n'.join((
'click to create line',
'click near a point to drag it',
'click near a line to create a new point in it',
'double click a point to delete it'
))
canvas.before:
Color:
rgba: 1, 1, 1, .8
Rectangle:
pos: self.pos
size: self.size
BezierCanvas:
<BezierLine>:
_points: list(chain(*self.points))
canvas:
Color:
rgba: 1, 1, 1, .2
SmoothLine:
points: self._points or []
Color:
rgba: 1, 1, 1, 1
Line:
bezier: self._points or []
width: 2
Color:
rgba: 1, 1, 1, .5
Point:
points: self._points or []
pointsize: 5
'''
def dist(a, b):
return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** .5
class BezierLine(Widget):
points = ListProperty()
select_dist = NumericProperty(10)
delete_dist = NumericProperty(5)
def on_touch_down(self, touch):
if super(BezierLine, self).on_touch_down(touch):
return True
max_dist = dp(self.select_dist)
l = len(self.points)
for i, p in enumerate(self.points):
if dist(touch.pos, p) < max_dist:
touch.ud['selected'] = i
touch.grab(self)
return True
for i, p in enumerate(self.points[:-1]):
if (
dist(touch.pos, p)
+ dist(touch.pos, self.points[i + 1])
- dist(p, self.points[i + 1])
< max_dist
):
self.points = (
self.points[:i + 1]
+ [list(touch.pos)]
+ self.points[i + 1:]
)
touch.ud['selected'] = i + 1
touch.grab(self)
return True
def on_touch_move(self, touch):
if touch.grab_current is not self:
return super(BezierLine, self).on_touch_move(touch)
point = touch.ud['selected']
self.points[point] = touch.pos
def on_touch_up(self, touch):
if touch.grab_current is not self:
return super(BezierLine, self).on_touch_up(touch)
touch.ungrab(self)
i = touch.ud['selected']
if touch.is_double_tap:
if len(self.points) < 3:
self.parent.remove_widget(self)
else:
self.points = (
self.points[:i] + self.points[i + 1:]
)
class BezierCanvas(Widget):
def on_touch_down(self, touch):
if super(BezierCanvas, self).on_touch_down(touch):
return True
bezierline = BezierLine()
bezierline.points = [(touch.pos), (touch.pos)]
touch.ud['selected'] = 1
touch.grab(bezierline)
self.add_widget(bezierline)
return True
class BezierApp(App):
def build(self):
return Builder.load_string(KV)
if __name__ == '__main__':
try:
BezierApp().run()
except:
import pudb; pudb.post_mortem()
I just picked up Kivy and encountered this problem. If there is a better way to achieve what I'm trying to in general I'd love to hear about it, though.
What I've noticed is that, when I add a widget to another widget, if I do so through Python code it will be slightly at a different position than had I done so through Kivy. I'll paste my code below (it's pretty short right now) and you can just try it yourself and you'll see what I mean.
client.py:
import kivy
kivy.require('1.9.1') # current kivy version
from kivy.config import Config
Config.set('graphics', 'width', '360')
Config.set('graphics', 'height', '640')
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty, NumericProperty, ReferenceListProperty
from kivy.graphics import Color, Rectangle
from kivy.clock import Clock
from kivy.vector import Vector
from random import randint
class Bird(Widget):
'''
Bird Widget defines each sprite. / Test version: blue and red cards
Defined attributes:
SIZE
POSITION
(COLOR)
Upade the position of this widget individually every 0.7 seconds with 60 fps
'''
# set attributes
border_color = (1,1,1)
r = NumericProperty(0)
g = NumericProperty(0)
b = NumericProperty(0)
color = ReferenceListProperty(r, g, b) # initial color = red // maybe make it random
velocity_x = NumericProperty(0)
velocity_y = NumericProperty(-3)
velocity = ReferenceListProperty(velocity_x, velocity_y)
def __init__(self, **kwargs):
super(Bird, self).__init__(**kwargs)
self.pick_color()
# Randomly generate 0 or 1, and pick a color based on that
def pick_color(self):
color_num = randint(0,1)
if color_num == 0: # blue
self.color = (0,0,1)
elif color_num == 1: # red
self.color = (1,0,0)
# Move the widget by -3y increment at 60 fps
def increment(self, dt):
self.pos = Vector(*self.velocity) + self.pos
def move(self):
# While the sprite moves at 60 fps, the movement is "cancelled" after 0.3 seconds
# This event sequence is refreshed every 0.7 seoncds in MainApp class
move = Clock.schedule_interval(self.increment, 1.0/60.0)
stop = Clock.schedule_once(lambda dt: move.cancel(), 0.3)
class GameMain(Widget):
'''
Contains two functions: ADD_NEW_BIRD() and UPDATE().
All controls happen in this widget
Not using kivy.screen because there is only one screen used
ADD_NEW_BIRD() adds a new bird to list_of_birds AND add it as a child widget to the GameMain Widget. UPDATE() calls MOVE() (Bird Widget) and receives events
Create global variable limit = 0; if limit == 4, game over; variable accessed in update function, which checks whether the limit has been reached. If the player makes the right decision, then limit -= 1
'''
limit = 0
def add_new_bird(self):
self.new_bird = Bird(center_x=self.center_x, center_y=self.height/1.5)
print (self.center_x, self.height)
self.new_bird.pick_color()
self.add_widget(self.new_bird)
def update(self, dt):
for bird in self.children:
bird.move()
self.add_new_bird()
class MainApp(App):
def build(self):
game = GameMain()
Clock.schedule_interval(game.update, 0.7)
return game
if __name__ == '__main__':
MainApp().run()
main.kv:
#:kivy 1.9
<Bird>:
size: 70, 80
canvas:
Color:
rgb: self.border_color
Rectangle:
size: self.size
pos: self.pos
Color:
rgb: self.color
Rectangle:
size: root.width - 10, root.height - 10
pos: root.x + 5, root.y + 5
<GameMain>
Bird:
center_x: root.center_x
center_y: root.height / 1.5
The code does exactly what I want it to do (I'm going to touch on the z-values later) except that the very first card is slightly off to the left. I'm just really confused because center_x: root.center_x in main.kv should not be any different from Bird(center_x=self.center_x in client.py as far as I understand. I've tried initializing the first instance of Bird() inside of an init function like so:
def __init__(self, **kwargs):
super(GameMain, self).__init__(**kwargs)
self.bird = Bird(center_x=self.center_x, center_y=self.height/1.5)
self.bird.pick_color()
self.add_widget(self.bird)
And the problem was still there! If anyone could explain what's going on/what I'm doing wrong and maybe even suggest a better way to approach this, I'd appreciate it.
Just in case you're curious, I need to add widgets directly from Python code because I need the app to constantly produce a new card at a constant time interval. The first card, however, is initialized in the Kivy file for the sake of simplicity. To be fair it works pretty well except for the offset. And lastly I'm not using a Layout because I wasn't sure which one to use... I did lay my hands on FloatLayout for a bit but it didn't seem like it was going to fix my problem anyway.
When constructed, Widget has an initial size of (100, 100). If you change size from this:
<Bird>:
size: 70, 80
to this:
<Bird>:
size: 100, 80
rectangles will align correctly. Initial rectangle, created in kv file, is centered at the parent window, other ones that are created in Python code are offset to the left.
If you change Bird constructor in Python code from this:
def __init__(self, **kwargs):
super(Bird, self).__init__(**kwargs)
self.pick_color()
to this (effectively overriding the default widget size from (100, 100) to be (50,50)):
def __init__(self, **kwargs):
self.size = (50, 50)
super(Bird, self).__init__(**kwargs)
self.pick_color()
you'll notice that rectangles created in Python code will shift to the right. Change kv file from:
<Bird>:
size: 70, 80
to:
<Bird>:
size: 50, 80
which matches (new) initial widget size of (50,50) in width, all rectangles will be aligned again.
Solution to your problem would be to leave all as is, except to set size for new birds in Python to be equal to that in kv file:
def __init__(self, **kwargs):
self.size = (70, 80)
super(Bird, self).__init__(**kwargs)
self.pick_color()
and all will work as intended.
This all means that size property from kv file is not applied to your Python-side created Birds, only to the one created by kv declaration. Is this Kivy bug or maybe you are missing one more step in the Python code to make Builder apply size from kv file to Python-created Birds, I have no idea right now.
In my experience, at this point of Kivy development, mixing too much kv and Python code will result in these kind of weird issues you have here. It is best to either handle all view related stuff in kv or to ditch kv completely and build everything in Python.
Some things don't work at all in kv, i.e. setting cols property of GridLayout (v1.9.1).
Personally, for now, I stick to well organized Python code to build UI and don't use kv files almost at all.
Hope this helps a bit...
I am a new programmer (and first time stackoverflow poster) so please correct me if I use terminology incorrectly or make any other missteps in etiquette or proper coding style.
I am trying to write a game where you draw tiles to your tile rack and then play them on a board. I have already written a game that works without graphics via text input. Now I would like to use Kivy to make a graphical interface.
One of my problems involves the positioning of widgets. I would like to center my rack widget at the center of the screen x-axis. I can have it draw a rectangle there and have it appear to be positioned where I want it, but its x position is (as you might guess) 0. I think part of my problem is that I have passed a Game object to my widget and using a list of symbols (game.symbols) and an init method, I tried to load create tile widgets with a label(text=symbol) and then load them on the rack. As you probably have guessed, my tiles also are not positioned correctly.
How can I center my tile rack and load my tiles correctly so they have the proper position (which I think is necessary for my collision detection).
Please explain the way init method and the KV file are executed when both are used.
What is the proper way to pass objects and attributes to widgets in regards to my issues here. Should I have created an ObjectProperty?
I also may just have a fundamental misunderstanding of positioning and layouts in Kivy and if so, please educate me.
Thank you,
Cliff
import kivy
kivy.require('1.7.0')
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.scatter import Scatter
from kivy.uix.label import Label
kv_Game= '''
<TileWidget>:
size_hint: None, None
size: 50,50
canvas.before:
Color:
rgba: 0.5,0.5,1,0.3
Rectangle:
size: self.width, self.height
canvas.after:
Line:
rectangle: self.x, self.y, self.width, self.height
dash_offset: 5
dash_length: 3
<RackWidget>:
size_hint: None, None
size: 350, 50
pos_hint: {'center_x': 0.5}
y: 75
canvas.after:
Color:
rgba: 1,0,0,0.5
Line:
rectangle: self.x, self.y, self.width, self.height
'''
Builder.load_string(kv_Game)
class Game(FloatLayout):
def __init__(self, **kwargs):
super(Game, self).__init__(**kwargs)
self.symbols = ['!','#','#','$','%','^','&']
self.rackWidget = RackWidget(self)
self.add_widget(self.rackWidget)
class TileWidget(Scatter):
def __init__(self, symbol="?", **kwargs):
super(TileWidget, self).__init__(**kwargs)
tileLabel = Label(text=symbol, size_hint=(None,None), size=(50,50))
self.add_widget(tileLabel)
class RackWidget(FloatLayout):
def __init__(self, game, **kwargs):
super(RackWidget, self).__init__(**kwargs)
print("TileRackWidget pos:", self.pos)
x, y = self.pos
for symbol in game.symbols:
tileWidget = TileWidget(symbol=symbol, pos= (x,y))
self.add_widget(tileWidget)
print("tileWidget pos:", tileWidget.pos)
x+=50
class GameTest1App(App):
def build(self):
game = Game()
return game
if __name__=="__main__":
GameTest1App().run()
pos is not set to a usable value yet when you create your RackWidget instance. When __init__ is running, the widget has not yet been added to the Game widget, so it has no parent and no positioning information. You could solve this by binding to the changes in RackWidget.pos, but there's an easier way to do this: RelativeLayout. The position of each child of a RelativeLayout will be based on the position of the layout itself.
Here's a modified version of RackWidget using RelativeLayout:
class RackWidget(RelativeLayout):
def __init__(self, game, **kwargs):
super(RackWidget, self).__init__(**kwargs)
print("TileRackWidget pos:", self.pos)
x, y = 0, 0
for symbol in game.symbols:
tileWidget = TileWidget(symbol=symbol, pos= (x,y))
self.add_widget(tileWidget)
print("tileWidget pos:", tileWidget.pos)
x+=50