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 :'(
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 querying Mongolab for the number of documents in a collection, then I want to add one button per document found. I put this code in the on_enter function of a screen:
def on_enter(self):
topicconcepts = db.topicconcepts
topics = topicconcepts.distinct("topic")
for idx, topic in enumerate(topics):
button = Button(text=topic, id=str(idx), buttonPos = idx, size=(self.width,20), pos_hint = {'top': 1}, color= (1,1,1,1), halign='center', seq = 'pre')
# topicBox points to a BoxLayout
self.topicBox.add_widget(button)
However, the layout turns out to be this:
Basically I want the buttons to be more like this:
Any suggestions will be much appreciated :)
If you want to have fixed size buttons in a GridLayout (or a BoxLayout), you have to unset their size_hints. For example, for fixed height of 50dp, without changing width, it would be:
Button:
size_hint_y: None
height: '50dp'
on in py:
from kivy.metrics import dp
Button(size_hint_y=None, height=dp(50))
PS: remember, the buttons might overflow the screen, if too many topics are given. To correct this, use ScrollView widget to scroll them up and down.
im new to kivy , i'm trying to make an 8puzzle game, my problem is , after moving the numbers(using Buttons or labels) in a gridlayout (using animation calss to move the buttons) there is no problem till resizing the window ! after resizing window every button or label will be in it's initial position :-/
so,why gridlayout children come back to the initial position after ressizing the window ?
how can i fix this ?
python code :
class Cubes(Button):
#value=StringProperty('0')
pos_i=NumericProperty(0)
pos_j=NumericProperty(0)
class Puzzle(GridLayout):
def __init__(self,**kwargs):
super(Puzzle,self).__init__()
self._keyboard = Window.request_keyboard(None,self)
print("Tsfsdfsdfdsfsdfdsf")
if not self._keyboard:
return
game =[
[1,2,3],
[4,5,6],
[7,8,"-"]
]
for i in range (3):
for j in range (3):
self.add_widget(Cubes(text=str(game[i][j]),pos_i=i,pos_j=j))
self._keyboard.bind(on_key_down=self.move)
self.wait=False
self._index=-1
self._solution=[]
def move(self, keyboard, keycode, text,modifires):
pos=-1
for i in range(9):
if self.children[i].text=="-":
pos=self.children[i].pos
self._index=i
#print (self.children[self._index].pos)
if keycode[1]=="up":
if Expand(Node(self.generatePuzzleTable())).CanGoUp():
for m in range(9):
if self.children[m].pos_i==self.children[self._index].pos_i-1:
if self.children[m].pos_j==self.children[self._index].pos_j:
self.replace(self.children[m].pos,self.children[self._index].pos,m)
break
if keycode[1]=="right":
if Expand(Node(self.generatePuzzleTable())).CanGoRight:
for m in range(9):
if self.children[m].pos_i==self.children[self._index].pos_i:
if self.children[m].pos_j==self.children[self._index].pos_j+1:
self.replace(self.children[m].pos,self.children[self._index].pos,m)
break
if keycode[1]=="down":
if Expand(Node(self.generatePuzzleTable())).CanGoDown():
for m in range(9):
if self.children[m].pos_i==self.children[self._index].pos_i+1:
if self.children[m].pos_j==self.children[self._index].pos_j:
self.replace(self.children[m].pos,self.children[self._index].pos,m)
break
print (keycode[1])
if keycode[1]=="enter":
self.ans()
if keycode[1]=="left":
if Expand(Node(self.generatePuzzleTable())).CanGoLeft():
for m in range(9):
if self.children[m].pos_i==self.children[self._index].pos_i:
if self.children[m].pos_j==self.children[self._index].pos_j-1:
self.replace(self.children[m].pos,self.children[self._index].pos,m)
break
if keycode[1]=="spacebar":
print ("what the fuck ?!? ",keycode[1])
self.solve()
def replace(self,pos,pos1,NodeIndex):
anime=Animation(pos=(float(pos[0]),float(pos[1])),d=0.2,t="out_cubic")#.start(self.children[0])
anime.start(self.children[self._index])
anime2=Animation(pos=(float(pos1[0]),float(pos1[1])), d=0.2,t="out_cubic")#.start(self.children[1])
anime2.start(self.children[NodeIndex])
ti=self.children[self._index].pos_i
tj=self.children[self._index].pos_j
self.children[self._index].pos_i,self.children[self._index].pos_j=self.children[NodeIndex].pos_i,self.children[NodeIndex].pos_j
self.children[NodeIndex].pos_i,self.children[NodeIndex].pos_j=ti,tj
kivy code :
<Cubes>:
background_color: 1,1,1,1
<Puzzle>:
cols: 3
spacing: 2
size: (300, 300)
BoxLayout:
canvas.before:
Color:
rgb: (0.7, 0.3, .4)
Rectangle:
pos: (self.x, self.y)
size: (self.width, self.height)
size: (600, 600)
AnchorLayout:
Puzzle:
center_x: root.center_x
center_y: root.center_y*2/3
in this image i moved the dash button to top left , but i resize the window
it will be in it's initial position (bottom right)
GridLayout (or any Layout for that matter) is responsible for positioning its children widget so on each re-size (and other meaningful events) the layout will position the widgets from scratch (this is the behavior that annoys you).
In the case of GridLayout, what matters is the order of the children of the grid...
To fix your issue you have two two options:
1) after your animation is complete swap the "-" widget with other one on the grid children list.
2) use something like https://github.com/inclement/sparsegridlayout that lets you specify what is the (i,j) for each widget(grid entry)
I hope this makes things a bit more clear
I went to the MapView-Doncumentation and also to the Source code but this doesn't seems to help much.
I created this templete in kv file so that I could dynamically create a mapmarkerpopup in the Map, but when I try this it creates another widget (which is obvious as I did add_widget in the load_content method because I couldn't find any other way)
This is the map_data.kv file
#:import MapSource kivy.garden.mapview.MapSource
#:import MapMarkerPopup kivy.garden.mapview.MapMarkerPopup
[MakePopup#BoxLayout]:
MapMarkerPopup:
lat: ctx.lat
lon: ctx.lon
popup_size: 400,400
Bubble:
Image:
source: ctx.image
mipmap: True
Label:
text: ctx.label
markup: True
halign: "center"
<Toolbar#BoxLayout>:
size_hint_y: None
height: '48dp'
padding: '4dp'
spacing: '4dp'
canvas:
Color:
rgba: .2, .2, .2, .6
Rectangle:
pos: self.pos
size: self.size
<Map_Data>:
Toolbar:
top: root.top
#Spinner created to select places.
Spinner:
text: "Sydney"
values: root.map_values.keys()
on_text:
if (self.text == 'France'): root.load_content()
else: pass
MapView:
id: mapview
lat: 28.89335172
lon: 76.59449171
zoom: 24
This is the main.py file
class Map_Data(BoxLayout):
....
def load_content(self):
self.add_widget(Builder.template('MakePopup', lat ='28.89335152',
lon='76.59449153', image="goku.jpg",label='label'))
This is the output that I get from the above code. I want that marker on the map.
Now we see that mapview has a function "add_marker" but via this method I cannot add the image and label.
if (self.text == 'Sydney'):
mapview.add_marker(MapMarkerPopup(lat=-33.8670512,lon=151.206))
else: pass
It works totally fine and adds the marker on the map.
But how to add Image and label ie. content???
mapview.add_marker(MapMarkerPopup(lat=-33.8670512,lon=151.206, content=???))
Now that expected result could be generated by manually creating, as in https://github.com/kivy-garden/garden.mapview/blob/master/examples/map_with_marker_popup.py
But what about creating it dynamically???
Any help is appreciated.
EDIT 1:
I also tried to do this.
if (self.text == 'Sydney'): mapview.add_marker(MapMarkerPopup(lat=-33.8670512,
lon=151.206,popup_size=(400,400)).add_widget(Button(text = "stackoverflow")))
else: pass
but it shows this error:
marker._layer = self
AttributeError: 'NoneType' object has no attribute '_layer'
It's been a while since you asked this question but I faced the same problem recently and maybe there is some interest in an answer. You pointed out elsewhere how to add content dynamically (https://github.com/kivy-garden/garden.mapview/issues/5) but the problem that the popup showed up in the wrong place remained and you suggested that the set_marker_position method needs to be changed. Changing it to
def set_marker_position(self, mapview, marker):
x, y = mapview.get_window_xy_from(marker.lat, marker.lon, mapview.zoom)
marker.x = int(x - marker.width * marker.anchor_x)
marker.y = int(y - marker.height * marker.anchor_y)
if isinstance(marker, MapMarkerPopup):
marker.placeholder.x = marker.x - marker.width / 2
marker.placeholder.y = marker.y + marker.height
i.e. adding the last three lines did the trick for me.