Kivy get the object that was pressed on - python

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)

Related

Almost working way to grab a borderless window in Kivy

I am looking to replace the windows title bar for a borderless app, I found some solutions on the internet that didn't quite work for me so I tried to do it myself.
Although the grabbing the screen and moving part works, once you release the click, the window continues to follow the cursor until eventually the program stops responding and the task is terminated.
This is an example of code that I prepared with some indications on how it works:
from kivy.app import App
from win32api import GetSystemMetrics
from kivy.lang.builder import Builder
from kivy.core.window import Window
from kivy.uix.widget import Widget
import pyautogui
import win32api
import re
Window.size=(600,300)
Window.borderless=True
#The following causes the window to open in the middle of the screen:
Window.top=((GetSystemMetrics(1)/2)-150)
Window.left=((GetSystemMetrics(0)/2)-300)
#####################################################################
Builder.load_string("""
<Grab>
GridLayout:
size:root.width,root.height
cols:2
Label:
id:label
text:'A label'
Button:
id:button
text:'The button that changes the window position'
on_press: root.grab_window()
""")
class Grab(Widget):
def grab_window(self):
#The following saves the mouse position relative to the window:
Static_Mouse_pos=re.findall('\d+',str(pyautogui.position()))
Mouse_y=int(Static_Mouse_pos[1])-Window.top
Mouse_x=int(Static_Mouse_pos[0])-Window.left
###############################################################
#The following is what causes the window to follow the mouse position:
while win32api.GetKeyState(0x01)<0: #In theory this should cause the loop to start as soon as it is clicked, I ruled out that it would start and end when the button was pressed and stopped being pressed because as soon as the screen starts to move, it stops being pressed.
Relative_Mouse_pos=re.findall('\d+',str(pyautogui.position()))
Window.left=(int(Relative_Mouse_pos[0])-Mouse_x)
Window.top=(int(Relative_Mouse_pos[1])-Mouse_y)
print(f'Mouse position: ({Mouse_x},{Mouse_y})') #To let you know the mouse position (Not necessary)
print(f'Window position: ({Window.top},{Window.left})') #To let you know the position of the window (Not necessary)
if win32api.GetKeyState(0x01)==0: #This is supposed to stop everything (Detects when you stop holding the click)
break
######################################################################
class app(App):
def build(self):
return Grab()
if __name__=='__main__':
app().run()
Is there a way to make it work fine? Or another way to grab a borderless window that might be effective?
I'm new to programming, so I apologize in advance for any nonsense you may read in my code.
EDIT: For some reason win32api.GetKeyState(0x01) is not updated once the click is done and the loop is started, nor does it help to make a variable take its value.
I've finally come up with a solution but this may not be the best one.
( Some places where I made changes are marked with comment [Modified] )
from kivy.app import App
from win32api import GetSystemMetrics # for getting screen size
from kivy.lang.builder import Builder
from kivy.core.window import Window
from kivy.uix.widget import Widget
import pyautogui
# import win32api
# import re
# set window size
# Window.size=(600,300)
# make the window borderless
Window.borderless = True
# The following causes the window to open in the middle of the screen :
Window.left = ((GetSystemMetrics(0) / 2) - Window.size[0] / 2) # [Modified] for better flexibility
Window.top = ((GetSystemMetrics(1) / 2) - Window.size[1] / 2) # [Modified] for better flexibility
#####################################################################
Builder.load_string("""
<Grab>
GridLayout:
size: root.width, root.height
rows: 2 # [modified]
Button:
id: button
text: "The button that changes the window position"
size_hint_y: 0.2
Label:
id: label
text: "A label"
""")
class Grab(Widget):
# I'm sorry I just abandoned this lol
"""
def grab_window(self):
#The following saves the mouse position relative to the window:
Static_Mouse_pos=re.findall('\d+',str(pyautogui.position()))
Mouse_y=int(Static_Mouse_pos[1])-Window.top
Mouse_x=int(Static_Mouse_pos[0])-Window.left
###############################################################
#The following is what causes the window to follow the mouse position:
while win32api.GetKeyState(0x01)<0: #In theory this should cause the loop to start as soon as it is clicked, I ruled out that it would start and end when the button was pressed and stopped being pressed because as soon as the screen starts to move, it stops being pressed.
Relative_Mouse_pos=re.findall('\d+',str(pyautogui.position()))
Window.left=(int(Relative_Mouse_pos[0])-Mouse_x)
Window.top=(int(Relative_Mouse_pos[1])-Mouse_y)
print(f'Mouse position: ({Mouse_x},{Mouse_y})') #To let you know the mouse position (Not necessary)
print(f'Window position: ({Window.top},{Window.left})') #To let you know the position of the window (Not necessary)
if win32api.GetKeyState(0x01)==0: #This is supposed to stop everything (Detects when you stop holding the click)
break
######################################################################
"""
def on_touch_move(self, touch):
if self.ids.button.state == "down": # down | normal
# button is pressed
# mouse pos relative to screen , list of int
# top left (0, 0) ; bottom right (max,X, maxY)
mouse_pos = [pyautogui.position()[0], pyautogui.position()[1]]
# mouse pos relative to the window
# ( normal rectangular coordinate sys. )
mouse_x = touch.pos[0]
mouse_y = Window.size[1] - touch.pos[1] # since the coordinate sys. are different , just to converse it into the same
# give up using touch.dx and touch.dy , too lag lol
Window.left = mouse_pos[0] - mouse_x
Window.top = mouse_pos[1] - mouse_y
class MyApp(App): # [Modified] good practice using capital letter for class name
def build(self):
return Grab()
if __name__ == "__main__":
MyApp().run()
I just gave up using button on_press or on_touch_down as suggested in the comment since it requires manual update for the mouse position.
Instead , I try using the Kivy built-in function ( ? ) on_touch_move.
It is fired when a mouse motion is detected inside the windows by the application itself. ( much more convenient compared with manual checking lol )
The concepts of window positioning are similar to yours , which is mouse pos relative to screen - mouse pos relative to app window. But the coordinate system used by Pyautogui and Kivy 's window are different , therefore I did some conversion for this as seen in the code above.
But I'm not sure whether the unit used by Pyautogui and Kivy for mouse positioning is the same or not ( ? ), so it would not be as smooth as expected / ideal case when drag-and-dropping the window via the button. Also the time delay for updating when on_touch_move of the kivy app. That's the reason why I think it may be no the best answer for your question.
Any other solutions / suggestions / improvements etc. are welcome : )
Simplified Code For Copy-Paste :
Edit : Added close / minimize window button ( at top-left corner )
#
# Windows Application
# Borderless window with button press to move window
#
import kivy
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.clock import Clock
from win32api import GetSystemMetrics # for getting screen size
import pyautogui # for getting mouse pos
# set window size
# Window.size = (600,300)
# make the window borderless
Window.borderless = True
# set init window pos : center
# screen size / 2 - app window size / 2
Window.left = (GetSystemMetrics(0) / 2) - (Window.size[0] / 2)
Window.top = (GetSystemMetrics(1) / 2) - (Window.size[1] / 2)
kivy.lang.builder.Builder.load_string("""
<GrabScreen>
Button:
id: close_window_button
text: "[b] X [b]"
font_size: 25
markup: True
background_color: 1, 1, 1, 0
size: self.texture_size[0] + 10, self.texture_size[1] + 10
border: 25, 25, 25, 25
on_release:
root.close_window()
Button:
id: minimize_window_button
text: "[b] - [b]"
font_size: 25
markup: True
background_color: 1, 1, 1, 0
size: self.texture_size[0] + 10, self.texture_size[1] + 10
border: 25, 25, 25, 25
on_release:
root.minimize_window()
Button:
id: move_window_button
text: "[b]. . .[/b]"
font_size: 25
markup: True
background_color: 1, 1, 1, 0
width: root.width / 3
height: self.texture_size[1] * 1.5
border: 25, 25, 25, 25
Label:
id: this_label
text: "Hello World !"
font_size: 25
size: self.texture_size
""")
class GrabScreen(Widget):
def close_window(self):
App.get_running_app().stop()
# def on_window_minimize(self, *args, **kwargs):
# print(args)
def minimize_window(self):
Window.minimize()
# Window.bind(on_minimize = self.on_window_minimize)
def maximize_window(self):
Window.size = [GetSystemMetrics(0), GetSystemMetrics(1)]
Window.left = 0
Window.top = 0
def update(self, dt):
# button for closing window
self.ids.close_window_button.top = self.top
# button for minimizing window
self.ids.minimize_window_button.x = self.ids.close_window_button.right
self.ids.minimize_window_button.top = self.top
# button for moving window
self.ids.move_window_button.center_x = self.center_x
self.ids.move_window_button.top = self.top
# label
self.ids.this_label.center = self.center
def on_touch_move(self, touch):
# when touching app screen and moving
if self.ids.move_window_button.state == "down": # down | normal
# (button move_window_button is pressed) and (mouse is moving)
# mouse pos relative to screen , list of int
# top left (0, 0) ; bottom right (maxX, maxY)
mouse_pos = [pyautogui.position()[0], pyautogui.position()[1]] # pixel / inch
# mouse pos relative to the window
# ( normal rectangular coordinate sys. )
# since the coordinate sys. are different , just to converse it to fit that of pyautogui
# Note :
# 1 dpi = 0.393701 pixel/cm
# 1 inch = 2.54 cm
"""
n dot/inch = n * 0.393701 pixel/cm
1 pixel/cm = 2.54 pixel/inch
n dot/inch = n * 0.393701 * 2.54 pixel/inch
"""
mouse_x = touch.x # dpi
mouse_y = self.height - touch.y # dpi
# update app window pos
Window.left = mouse_pos[0] - mouse_x
Window.top = mouse_pos[1] - mouse_y
# max / min window
if mouse_pos[1] <= 1:
self.maximize_window()
elif Window.size[0] >= GetSystemMetrics(0) and Window.size[1] >= GetSystemMetrics(1):
Window.size = [Window.size[0] / 2, Window.size[1] * 0.7]
class MyApp(App):
grabScreen = GrabScreen()
def build(self):
# schedule update
Clock.schedule_interval(self.grabScreen.update, 0.1)
return self.grabScreen
if __name__ == "__main__":
MyApp().run()
Reference
Kivy Motion Event

How to make Canvas vertex instructions relative to a widget in Kivy

I just started learning Kivy and I was trying to understand how the Canvas instructions are affected on resizing of the window of the kivyApp.
In the Kivy documentation it is mentioned that -
Note
Kivy drawing instructions are not automatically relative to the position or size of the widget. You, therefore, need to consider these factors when drawing. In order to make your drawing instructions relative to the widget, the instructions need either to be declared in the KvLang or bound to pos and size changes.
The example which follows, shows how to bind the size and position of a Rectangle instruction to the size and position of the widget in which it is being drawn. Therefore the size and the position changes proportionally when the window is resized.
But, how can I do the same for somthing like a Bezier instruction, which uses points.
I have a custom widget HangManFig1 which extend the Widget class, which is defined in KVlang like this:
<HangManFig1>:
canvas:
Line:
points: (150, 100, 150, 700)
width: 10
Line:
points: (150, 700, 600, 700)
width: 10
Line:
points: (150, 600, 250, 700)
width: 10
Line:
points: (600, 700, 600, 600)
width: 3
Ellipse:
pos: (550, 500)
Line:
bezier: (610, 510, 630, 400, 570, 350)
width: 10
Line:
bezier: (570, 350, 510, 370, 450, 270)
width: 10
Line:
bezier: (570, 350, 600, 300, 550, 200)
width: 10
Line:
bezier: (610, 480, 530, 430, 500, 430)
width: 10
Line:
bezier: (610, 480, 630, 500, 680, 390)
width: 10
I use this widget in a Screen, in the following manner:
#:import HangManFig1 figures.hangmanfig1
<MainScreen>:
name: '_main_screen_'
BoxLayout:
RelativeLayout:
size_hint_x: 0.7
HangManFig1:
RelativeLayout:
size_hint_x: 0.3
Button:
text: 'Play'
pos_hint: {'x': 0.1, 'y': 0.80}
size_hint: (0.8, 0.1)
on_release:
root.manager.transition.direction = 'left'
root.manager.current = '_game_screen_'
Button:
text: 'Practice'
pos_hint: {'x': 0.1, 'y': 0.60}
size_hint: (0.8, 0.1)
on_release:
root.manager.transition.direction = 'left'
root.manager.current = '_game_screen_'
Button:
text: 'Share'
pos_hint: {'x': 0.1, 'y': 0.40}
size_hint: (0.8, 0.1)
on_release:
root.manager.transition.direction = 'left'
root.manager.current = '_share_screen_'
Button:
text: 'Credits'
pos_hint: {'x': 0.1, 'y': 0.20}
size_hint: (0.8, 0.1)
on_release:
root.manager.transition.direction = 'left'
root.manager.current = '_credits_screen_'
When I am resizing the window, I see that although the Buttons are being positioned correctly, but not HangManFig1.
Is there a way, in which I can bind the size of this widget to that of the Parent Widget so that it is positioned correctly even when the Window size changes?
While you used RelativeLayout to make the coordinates of your instructions relative to the position of your widget, it doesn't do anything regarding its size.
As you hardcoded all the positions by numeric values, you'll need a way to scale these values relative to the size of your widget, and you have to consider what you want to happen regarding the width of your lines in this situation, should it grow relative to the size of the widget as well? linearly? Something else? Depending on what you want various possibility exist.
The easiest starting point, IMHO, would be to use a Scale instruction, to set all the canvas instructions relative to the size of your widget, over the size you used to design your current hangman.
<HangManFig1>:
h: 600
w: 800 # just guessing the size you used to design it, adjust as needed
canvas:
PushMatrix:
Scale:
xy: self.width / self.w, self.height / self.h
Line:
points: (150, 100, 150, 700)
width: 10
Ellipse:
pos: (550, 500)
... # etc, all the other instructions remain unchanged
PopMatrix: # restore the initial canvas so it doesn't affect other instructions after your drawing
If that's not enough for you, because you want to keep the width of the line constant for example, you could either not do it with a Scale instruction, but instead have a function that takes the size of your widget and a set of coordinates as input, and returns the value relative to that size:
def rscale(size, *args):
w, h = size
ws = w / 800 # adjust accordingly, as in the first example
hs = h / 600
return (x / (ws if i % 2 else hs) for i, x in enumerate(args))
This function could be used like this.
Line:
points: rscale(self.size, 150, 100, 150, 700)
And if you want something more sophisticated, like preserving the aspect ratio of your hangman, while staying in the boundaries of your size, you could adjust accordingly to something like:
def rscale(size, *args):
w, h = size
scale = min(w / 800, h / 600) # pick the smallest scale of the two
return (x / s for x in args)
Yes, you can. It takes a bit of work, but instead of using explicit coordinates for the Canvas Lines, use values that are based on the size of the HangManFig1 object. For example, the first Line of your drawing could be something like:
<HangManFig1>:
canvas:
Line:
points: (root.width*0.1, root.height * 0.1, root.width * 0.1, root.height * 0.8)
width: 10
I've got:
class ourLine(Line):
widget=None
vLines=[]
firstime=True
def __init__(self,relto=Window,**kwargs):
self.kwargs=kwargs
if not self.widget:
print('you should inherit a new class with \'widget\' attr')
super(ourLine,self).__init__(**kwargs)
W, H = Window.width, Window.height
self.rBezier=kwargs['bezier']
self.vBezier=[]
c=0
for p in self.rBezier:
c+=1
if not c%2:
self.vBezier.append(p/H)
else:
self.vBezier.append(p/W)
self.__class__.vLines.append(self)
del self.kwargs['bezier']
def deAbstractor(self):
W, H = self.__class__.widget.width, self.__class__.widget.height
_vBezier=[]
c=0
with self.__class__.widget.canvas:
for p in self.vBezier:
c+=1
if not c%2:
_vBezier.append(p*H)
else:
_vBezier.append(p*W)
Line(bezier=_vBezier, **self.kwargs)
def dyna(c,w,s):
print(w)
for l in c.vLines:
l.__class__.widget.canvas.clear()
for l in c.vLines:
l.deAbstractor()
def activate(c):
c.widget.bind(size=lambda w,s: myLine.dyna(myLine,w,s))
This may be a little messy and can contain something unnecessary. That's because, first I wanted to make this more advanced. But then it went a little crazy. But still it is a good way to do vector lines. It supports width and other properties you give in Line (I hope). color isn't changing, but it can be implemented. Let’s see it in action:
import kivy
from kivy.app import App
from kivy.graphics import *
from kivy.core.window import Window
from kivy.uix.floatlayout import FloatLayout
from guy.who.helped import ourLine
root=FloatLayout()
#tell us your widget so we can refer
class myLine(ourLine):
widget=root
#you may want to start listening later, so it is manual.
ourLine.activate(myLine)
#you can start using like a normal Line
with root.canvas:
Color(1,1,0,1)
myLine(bezier=[2,70,90,80,Window.width,Window.height])
myLine(bezier=[200,170,309,80,Window.width/2,Window.height*0.8], width=12)
class MyApp(App):
def build(self):
return root
if __name__ == '__main__':
MyApp().run()
With what, is this post better than others? Well, this can use Bézier curves, not points and is responsive.
And weirdly, it is not so bad with performance.
It fires when you resize the window. So, the rest of the time, as good as basic Line + a few bytes of RAM to hold relative values and extras.
Well, I have bad news. Once I wanted to animate Bézier curves, I could animate the Bezier class beziers, but not Line(bezier=(...)). and width of Bezier is can't be changed, because there is no such property. And I ended up with animating only 1px width Bézier curves. So, there is not much with dynamic vector lines in Kivy... yet (I hope).
I love the simplicity of that library, but it is not mature yet I guess. And I decided to move on.
There are a lot of things that doesn't have Kivy's problems, such as web and webframeworks (which I chose over Kivy). I love Python, and Kivy is so capable. but when it comes a little bit down to specific things, Kivy really lacks :'(

KIVY collide_point vertex Rectangle

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.

Kivy: Click-drag Screen Visuals

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()

Placing and moving a group of images with kivy relative to the layout

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

Categories

Resources