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()
Related
I have a Kivy app that has a scrollview within it. In this scrollview there's a boxlayout that holds a pretty big amount of images and it changes throughout runtime (it can go from 1 to 300 at any time). When a touchdown event happens, I need to know on which image the user has pressed (meaning which on they were "on" at the moment, as they can scroll up and down), and maybe even get the coordinates of the press relative to the image and not the whole screen (I need to draw on the place they pressed and I cannot do it without knowing which image they pressed on and where). How can I do that?
That's how it's defined in the kv file:
MyScrollView:
bar_color: [1, 0, 0, 1]
id: notebook_scroll
padding: 0
spacing: 0
do_scroll: (False, True) # up and down
BoxLayout:
padding: 0
spacing: 0
orientation: 'vertical'
id: notebook_image
size_hint: 1, None
height: self.minimum_height
MyImage:
<MyImage>:
source: 'images/notebook1.png'
allow_stretch: True
keep_ratio: False
size: root.get_size_for_notebook()
size_hint: None, None
It's basically an infinite notebook, and during runtime the python code adds more "MyImage" objects to the boxlayout (which is a photo of a notebook page).
Try adding this method to your MyImage:
def to_image(self, x, y):
''''
Convert touch coordinates to pixels
:Parameters:
`x,y`: touch coordinates in parent coordinate system - as provided by on_touch_down()
:Returns: `x, y`
A value of None is returned for coordinates that are outside the Image source
'''
# get coordinates of texture in the Canvas
pos_in_canvas = self.center_x - self.norm_image_size[0] / 2., self.center_y - self.norm_image_size[1] / 2.
# calculate coordinates of the touch in relation to the texture
x1 = x - pos_in_canvas[0]
y1 = y - pos_in_canvas[1]
# convert to pixels by scaling texture_size/source_image_size
if x1 < 0 or x1 > self.norm_image_size[0]:
x2 = None
else:
x2 = self.texture_size[0] * x1/self.norm_image_size[0]
if y1 < 0 or y1 > self.norm_image_size[1]:
y2 = None
else:
y2 = self.texture_size[1] * y1/self.norm_image_size[1]
return x2, y2
Then you can add an on_touch_down() to your MyImage class:
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
print('got a touch on', self, 'at', touch.pos, ', at image pixels:', self.to_image(*touch.pos))
return True
else:
return super(MyImage, self).on_touch_down(touch)
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.
This question already has an answer here:
Kivy ids in python code
(1 answer)
Closed 4 years ago.
I tried to make a function in the kivy 1.10.1 framework that would in theory make a group of a number of circles. It calculates the position of the center of the sphere using the parametic form with a given radius. In this case I want a circle for each letter of the alphabet. I make a new Letter widget for each letter but whenever it tries to add the position properties it returns a KeyError: 'A' with whichever key (which I thought I assigned in self.add_widget(Letter(id=letter))) I try to fetch.
Main.py:
from kivy.app import App
from kivy.uix.widget import Widget
import math
import string
class Letter(Widget):
pass
class MainWidget(Widget):
def __init__(self, **kwargs):
super(MainWidget, self).__init__(**kwargs)
theta = 0
for letter in string.uppercase:
coord_x = 100 * math.cos(theta)
coord_y = 100 * math.sin(theta)
self.add_widget(Letter(id=letter))
self.ids[letter].center_x = coord_x
self.ids[letter].center_y = coord_y
theta += 360./float(len(string.uppercase))
class MainApp(App):
def build(self):
return MainWidget()
if __name__ == '__main__':
MainApp().run()
Main.kv:
#:kivy 1.10.1
<Letter>:
size: 50,50
canvas:
Color:
rgb: 0,0,0
Ellipse:
pos: self.pos
size: self.size
<MyWidget>:
canvas:
Rectangle:
pos: self.pos
size: self.size
self.ids only holds ids for widgets created in kv. You don't need it in Python because you already have a convenient reference to the widget:
l = Letter()
self.add_widget(l)
l.center_x = coord_x
l.center_y = coord_y
I am trying to create some sort of interactive "wall" with raspberry pi and the kivy library.
I would like to display images (maybe also text later) vertically one below the other and move them up/down on a keypress. Kind of scroll behavior but I would like to display many images so I do not want to load all of them at one go and use a scroll view.
So I use a FloatLayout and I change the position of the images after a keypress. My first issue is, that I have to operate with Window.width / Window.height instead of size_hint / pos_hint. I tried to use the examples from the kivy FloatLayout documentation but it did not work.
My problem is, that after hitting the keys the images are moving, but they also change the size. Here is the code I am using.
EDIT:
I realized that adding size_hint=(None, None) will keep the image size. This only shows that it would be probably better to use the relative positioning and sizing of the images in the layout. As mentioned earlier the example from the FloatLayout documentation did not work for me.
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import Image
from kivy.core.window import Window
class Wall(FloatLayout):
def __init__(self, **kwargs):
super(FloatLayout, self).__init__(**kwargs)
self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
if not self._keyboard:
return
self._keyboard.bind(on_key_down=self.on_keyboard_down)
self.posts = list()
self.orientation = 'vertical'
spacing = 25
post = Image(source='test.png',
allow_stretch=True,
keep_ratio=True,
size=(Window.width * 0.2z, Window.height * 0.2),
center=(Window.width * 0.6, Window.height * 0.5))
print '#####\nOriginal size and position: width/height', post.width, post.height, ' position x/y', post.x, post.y
self.posts.append(post)
self.add_widget(post)
while self.posts[-1].y + self.posts[-1].height <= Window.height:
post = Image(source='test.jpg',
allow_stretch=True,
keep_ratio=True,
size=(Window.width * 0.2, Window.height * 0.2),
pos=(self.posts[-1].x, self.posts[-1].y + self.posts[-1].height + spacing))
print '#####\nOriginal size and position: width/height', post.width, post.height, ' position x/y', post.x, post.y
self.posts.append(post)
self.add_widget(post)
def _keyboard_closed(self):
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
self._keyboard = None
def on_keyboard_down(self, keyboard, keycode, text, modifiers):
key = keycode[1]
for post in self.children:
if key == 'up':
post.y -= 50
print '#####\nNew size and position: width/height', post.width, post.height, ' position x/y', post.x, post.y
elif keycode[1] == 'down':
post.y += 50
elif key == 'q':
App.get_running_app().stop()
#else:
# return False
#return True
return True
class MessageBoard(App):
def build(self):
return Wall()
if __name__ == '__main__':
MessageBoard().run()
I found a solution using RelativeLayout and working with the pos_hint / size_hint properties. The positive side efect is, that the elements keep their size/position even when the window resizes.
I still need to compute absolute coordinates to determine the position of the elements. The drawback is, that in the init() method, the size of the parrent element is not knownw, so I have to work with Window size and coorinates. This will fail if my wall would not be the root element.
The moving of the elements need to be done by setting of the pos_hint property, changing the absolute coordinates would not take any effects.
class ImageAndText(RelativeLayout):
pass
class Wall(FloatLayout):
def __init__(self, **kwargs):
super(Wall, self).__init__(**kwargs)
self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
if not self._keyboard:
return
self._keyboard.bind(on_key_down=self.on_keyboard_down)
self.posts = list()
post = ImageAndText()
post.size_hint = 1, 0.3
post.size = (Window.width, Window.height * 0.3) # needs to be set in order to determine the absolute size
post.center_y = Window.height * 0.5 # needs to be set in order to determine the absolute coordinates
post.pos_hint = {'center_y': 0.5}
self.add_widget(post)
self.posts.append(post)
while self.posts[-1].top <= Window.height:
#print 'Old post top:', self.posts[-1].top
post = ImageAndText()
post.size_hint = 1, 0.25
post.size = (Window.width, Window.height * 0.25)
post.y = self.posts[-1].top # just above the first one
post.pos_hint = {'center_y': post.center_y/Window.height}
self.add_widget(post)
self.posts.append(post)
def _keyboard_closed(self):
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
self._keyboard = None
def on_keyboard_down(self, keyboard, keycode, text, modifiers):
key = keycode[1]
for post in self.posts:
if key == 'up':
post.pos_hint = {'center_y': post.pos_hint['center_y']+0.1}
elif keycode[1] == 'down':
post.pos_hint = {'center_y': post.pos_hint['center_y']-0.1}
elif key == 'q':
App.get_running_app().stop()
return True
class MyTestApp(App):
def build(self):
return Wall()
if __name__ == "__main__":
MyTestApp().run()
Here is the .kv file describing the ImageAndText element.
<ImageAndText>:
Label:
font_size: '14sp'
text: 'Lorem ipsum dolor sit'
color: (1, 1, 1, 1)
text_size: (self.parent.width*0.25, None)
pos_hint: {'center_x': 1.0/6} # set the center to 1/6 of parents width
Image:
source: 'test.png'
pos_hint: {'center_x': 2.0/3, 'center_y': .5}
size_hint: 0.5, 0.95 # the height will be the same as the parent, the width 0.5 because of the label
allow_stretch: True
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.