Well I am using an 'experimental' feature in kivy called Tesselator (https://kivy.org/doc/stable/api-kivy.graphics.tesselator.html). Currently my app draws a wee shape in the bottom left. It doesn't use a .kv file.
I would like to know the best way to make the shape remain centred on the screen as you change the window's size? I have seen how to do this with the basic shapes, but I can't figure out how to do this with a tessellated shape.
Any suggestions and advice is much appreciated! The code is below.
import kivy
from kivy.app import App
from kivy.app import App
from kivy.graphics import Mesh
from kivy.graphics.tesselator import Tesselator
from kivy.uix.floatlayout import FloatLayout
class MapBackground(FloatLayout):
def __init__(self, **kwargs):
super(MapBackground, self).__init__(**kwargs)
self.shapes = [
[100, 100, 300, 100, 300, 300, 100, 300],
[150, 150, 250, 150, 250, 250, 150, 250]
]
self.draw()
# draws the map shape
def draw(self):
# make the tesselator object
tess = Tesselator()
# add all the shapes
for shape in self.shapes:
if len(shape) >= 3:
tess.add_contour(shape)
# call the tesselate method to compute the points
if not tess.tesselate(): # returns false if doesn't work
print('tesselator did\'t work:(')
return
# clear canvas
self.canvas.clear()
# draw shapes
for vertices, indices in tess.meshes:
self.canvas.add(Mesh(
vertices=vertices,
indices=indices,
mode='triangle_fan'
))
class TimmApp(App):
def build(self):
self.title = 'The Interactive Museum Map'
return MapBackground()
if __name__ == '__main__':
TimmApp().run()
I did what I wanted by making the widget the shape was drawn in to be inside a float layout. Then (using the kv file seen below) I binded a method called draw() to the event on_size. This meant that the shape was redrawn whenever the window changed shape. I won't show the code for draw() because it's a lot.
#:kivy 1.11.1
FloatLayout:
MapBackground:
on_size: self.draw()
Related
I am trying to implement real time plots for measurements to a Kivy application, but I do not understand the inner workings of the kivy garden library.
My goal that I want to achieve: I want to have multiple plots inside a ScrollView such that I can add multiple real time plots programmatically from a dictionary and scroll through them if they occupy more space than one screen height. The main problem I have is that the Graph does not behave like an ordinary Widget but rather behaves like a canvas. I already tried to implement this with matplotlib as backend_kivyagg, but I failed to have fixed size for every subplot that I created.
There are multiple things that I do not understand why they happen like they happen.
from math import sin, cos
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.scrollview import ScrollView
from kivy.uix.widget import Widget
from kivy.garden.graph import Graph, MeshLinePlot
class Plot(Widget):
def __init__(self):
super(Plot, self).__init__()
self.graph = Graph(xlabel="x", ylabel="y", x_ticks_minor=5, x_ticks_major=25, y_ticks_major=1,
y_grid_label=True, x_grid_label=True, x_grid=True, y_grid=True,
xmin=-0, xmax=100, ymin=-1, ymax=1, draw_border=False)
# graph.size = (1200, 400)
# self.graph.pos = self.center
self.plot = MeshLinePlot(color=[1, 1, 1, 1])
self.plot.points = [(x, sin(x / 10.)) for x in range(0, 101)]
self.plot2 = MeshLinePlot(color=[1, 0, 0, 1])
self.plot2.points = [(x, cos(x / 10.)) for x in range(0, 101)]
self.add_widget(self.graph)
self.graph.add_plot(self.plot)
self.graph.add_plot(self.plot2)
class GraphLayoutApp(App):
def build(self):
scroll_view = ScrollView()
grid_layout = GridLayout(cols=1, row_force_default=True, padding=20, spacing=20)
graph = Plot()
graph2 = Plot()
label = Label(text="Hello World!")
label2 = Label(text="Hello World!")
grid_layout.add_widget(label)
grid_layout.add_widget(graph)
grid_layout.add_widget(label2)
grid_layout.add_widget(graph2)
scroll_view.add_widget(grid_layout)
return scroll_view
if __name__ == '__main__':
GraphLayoutApp().run()
Questions:
Inside the class GraphLayoutApp I create two objects graph and graph2 but if I add them to the GridLayout() only one does appear? How
can I add multiple graphs?
Also Inside the build method of the class GraphLayoutApp I create two lables. I wanted to first display the first label then the graph
and then the second label label2. But it seems to me that the graph
is always displayed in the lower left corner. I guess this has to do
with the canvas on which it is drawn, but I cannot solve it.
Here is a modified version with some things fixed:
The issue here is that you made your Plot inherit from Widget, which really the most basic widget in the framework, and as it's not a layout, doesn't manage anything in the children you add to it, so the Graph widget you added to it was left at the default size/position (respectively [100, 100] and [0, 0]), and so they stacked on each others, i used RelativeLayout, which sets children by default to its own size and position, so the Graph follows the widget. Another solution is to simply make Plot inherit from Graph, since it's the only child, and to use "self.add_plot" instead of "self.graph.add_plot", and to override the Graph parameters, the best solution is probably to create a KV rule for the Plot class. But the first solution was the minimal change from your code.
The general principle you missed is that in kivy, widgets are by default not constrained to any position/size, unless their parents are layouts that specifically manage that (like GridLayout did for your labels).
I also made the GridLayout automatically size itself to the minimum_size (determined by the hardcoded size i put in all widgets), so you actually have something to scroll on.
This code is also very python-driven, in kivy, you usually want to do more things using KV, rather than using add_widget for static things.
from math import sin, cos
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.scrollview import ScrollView
from kivy.uix.widget import Widget
from kivy.uix.relativelayout import RelativeLayout
from kivy_garden.graph import Graph, MeshLinePlot
class Plot(RelativeLayout):
def __init__(self, **kwargs):
super(Plot, self).__init__(**kwargs)
self.graph = Graph(xlabel="x", ylabel="y", x_ticks_minor=5, x_ticks_major=25, y_ticks_major=1,
y_grid_label=True, x_grid_label=True, x_grid=True, y_grid=True,
xmin=-0, xmax=100, ymin=-1, ymax=1, draw_border=False)
# graph.size = (1200, 400)
# self.graph.pos = self.center
self.plot = MeshLinePlot(color=[1, 1, 1, 1])
self.plot.points = [(x, sin(x / 10.)) for x in range(0, 101)]
self.plot2 = MeshLinePlot(color=[1, 0, 0, 1])
self.plot2.points = [(x, cos(x / 10.)) for x in range(0, 101)]
self.add_widget(self.graph)
self.graph.add_plot(self.plot)
self.graph.add_plot(self.plot2)
class GraphLayoutApp(App):
def build(self):
scroll_view = ScrollView()
grid_layout = GridLayout(cols=1, padding=20, spacing=20, size_hint_y=None)
grid_layout.bind(minimum_size=grid_layout.setter('size'))
graph = Plot(size_hint_y=None, height=500)
graph2 = Plot(size_hint_y=None, height=500)
label = Label(text="Hello World!", size_hint_y=None)
label2 = Label(text="Hello World!", size_hint_y=None)
grid_layout.add_widget(label)
grid_layout.add_widget(graph)
grid_layout.add_widget(label2)
grid_layout.add_widget(graph2)
scroll_view.add_widget(grid_layout)
# return grid_layout
return scroll_view
if __name__ == '__main__':
GraphLayoutApp().run()
I don't understand why the following code will not construct a graphics window with the circle. It does construct an object, but not graphically, when I run SomeObject = Tracker()
Why is that? This is a simple snippet of code, just for me to understand why I'm not getting a graphics window.
# tracker.py
from graphics import *
class Tracker:
def __inti__(self):
self.win = GraphWin('tracker', 500, 500)
self.circle = Circle(Point(0, 0), 0.5)
self.circle.draw(self.win)
You cannot see your circle because:
The radius of your circle (0.5) is too small
You placed the center of this small circle at (0, 0), which is at the top left corner
Your class's initializer was mispelled inti, hence the code was not called when you created your object.
Here is code that works:
from graphics import *
class Tracker:
def __init__(self):
# Window of size 500 x 500
self.win = GraphWin('tracker', 500, 500)
# Circle of radius 10 centered at (250, 250)
self.circle = Circle(Point(250, 250), 10)
self.circle.draw(self.win)
self.win.getMouse() # Pause to view result
self.win.close()
def main():
tracker = Tracker()
if __name__ == "__main__":
main()
Where graphics.py is presumably taken from this link.
I modified the sample code from the above link (which is probably also what you did) to call getMouse() to keep the window around.
The result is
I am trying to create a line and update is after a fixed interval of time (say 5 seconds). I wrote the code below but it does not update the line. Can anyone help me figure out who to do it?
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.widget import Widget
from kivy.lang import Builder
from kivy.graphics import Color, Ellipse, Line
import time
from kivy.clock import Clock
class MyWidget(Widget):
def my_callback(dt,ds):
Line(points=[100, 100, 200, 100, 100, 200], width=10)
pass
def __init__(self, **kwargs):
super(MyWidget, self).__init__(**kwargs)
with self.canvas:
self.line = Line(points=[100, 100, 200, 100, 100, 200], width=1)
self.line.width = 2
Clock.schedule_once(self.my_callback, 5)
pass
# add your instruction for main canvas here
with self.canvas.before:
pass
# you can use this to add instructions rendered before
with self.canvas.after:
pass
# you can use this to add instructions rendered after
class LineExtendedApp(App):
def build(self):
root = GridLayout(cols=2, padding=50, spacing=50)
root.add_widget(MyWidget())
return root
if __name__ == '__main__':
LineExtendedApp().run()
my_callback is not actually called within the with statement but rather 5s later when that is long gone, and even it if was it wouldn't do what you want - it would draw a new Line, not modify the existing one.
Instead you can change my_callback to do:
self.line.points = [100, 100, 200, 100, 100, 200]
self.line.width = 10
This will modify the existing line as you want. You can also take the clock scheduling out of the with statement but its position doesn't actually matter as long as it stays in the __init__
I have an array with x and y positions. I want to show these points linking a line successively for each point, then, creating an animation. It is just like a path tracking whose trail is the line. I'm using python-kivy to try to display it.
I couldn't find any help on google.
There's a button that triggers this animation.
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics.vertex_instructions import Line
from kivy.graphics.context_instructions import Color
from time import sleep
x_moves = [250, 250.4305, 249.8804, 246.0923, 239.7496, 233.8188, 225.7797, 215.8385, 205.8413, 196.6497, 189.7026, 181.2445, 174.9816, 171.9882, 166.1171, 161.6505, 159.9929, 161.1338, 164.853, 168.2874, 170.768, 178.6918, 184.5233, 190.0262, 195.607, 202.0255, 210.5954, 216.1031, 219.6285, 224.9134, 230.2314, 237.7017, 243.7408, 250.5839, 256.2949]
y_moves = [250, 240.0093, 230.0244, 220.7697, 213.0386, 204.9872, 199.0396, 197.9567, 197.7209, 201.6598, 208.8527, 214.1875, 221.9834, 231.5249, 239.62, 248.567, 258.4287, 268.3634, 277.6461, 287.0379, 296.7253, 302.8256, 310.9492, 319.299, 327.5969, 335.2652, 340.4185, 348.7651, 358.1231, 366.6125, 375.0812, 381.7291, 389.6996, 396.9915, 405.2003]
class my_App(App):
def build(self):
self.widget = Widget()
self.widget.on_touch_down = self.touch
with self.widget.canvas:
Color(0, 1, 0, 1) #just initial config
Line(points = [0,0,500,0,500,500,0,500], close = True) #just initial config
return self.widget
def touch(self, touch): #executes the animation
pts = []
for i in range(len(x_moves)):
pts.append(x_moves[i])
pts.append(y_moves[i])
self.widget.canvas.add(Line(points = pts))
sleep(0.1)
if __name__ == '__main__':
obj = my_App()
obj.run()
This is my code that doesn't work. But that's the idea.
You're blocking the UI from updating because your function doesn't return and you use sleep(). For obvious reasons, Kivy can't do anything while your code is running. If you want to wait before running some more code, you can use Kivy's Clock for this. Then Kivy will be able to update the screen.
But you should probably just look at Kivy's Animation, which is built to do this much better.
I need to draw a line inside a Widget. But I've seen that the relative position and size of the widget aren't setted until the init method finish.
How can I draw a graphic element inside a widget with his relative position and dimension while I instantiate the class? (I would like to avoid Kv lang)
class Track(Widget):
def __init__(self,**kw):
super(Track, self).__init__(**kw)
with self.canvas:
Color(1,0,0)
Line(points = (self.x, (self.y + self.height) / 2, self.x + self.width, (self.y + self.height) / 2))
In this way the line it's drawn using the initial size and position which are 100,100 and 0,0 but the Widget it's inside a Layout so I'd like to use the relative position and size and I'd like to drawn it in the init
I would like to avoid Kv lang
I recommend discarding this restriction.
In this way the line it's drawn using the initial size and position which are 100,100 and 0,0 but the Widget it's inside a Layout so I'd like to use the relative position and size and I'd like to drawn it in the init
You have three options. The first is to draw it in a clock (kivy.clock.Clock) scheduled function that runs after the widget has been positioned - it should be sufficient to do Clock.schedule_once(the_func, 0), with the 0 deferring the calculation to after the widget is positioned (assuming a normal layout) but before the next frame. The downside is that the line will then be fixed, and won't match the widget if if ever moves, e.g. potentially during window resize.
The second (and better) option is to draw the line as you are now, but bind to the widget pos and size a function that repositions it appropriately. e.g. self.bind(pos=self.line_setter, size=self.line_setter) and have self.line_setter be a method with self.line.points = [...] as appropriate. You would also need to save a reference to the line with self.line = Line(...).
The third (and normally best) option is to use kv language, which automatically creates the bindings for you with no additional syntax.
Demo of Inclement's Brillant answer!
The goal is to reposition a figure to the center of the widget
from kivy.app import App
from kivy.graphics.vertex_instructions import Line
class MyWidget(Widget):
def __init__(self):
super().__init__()
print(f"on init: {self.width}, {self.height}")
with self.canvas:
self.coordinate = Translate(0, 0)
Line(points=[0,0,100,100], width=20)
#Comment the following line
self.bind(pos=self.reposition, size=self.reposition)
def reposition(self, *args):
print(f"on reposition: {self.width}, {self.height}")
self.coordinate.xy = self.width/2, self.height/2
class myApp(App):
def build(self):
return MyWidget()
myApp().run()
Results:
on init: 100, 100
on reposition: 1000, 500
Before|After binding: