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()
Related
BTW, thanks to everyone at stackoverflow for being so supportive.
Thanks to crystal clear help from 'John Anderson' I've learned a megaton about using splitters, layouts & widgets in Kivy.
I've achieved the desired 'look' of my GUI thus far, but ran into an odd quirk that eludes me. My buttons stopped accepting clicks.
If one looks closely at the depiction of my GUI below, the buttons named 'White' & 'Black' have a line inside them through the text. When I pull the horizontal splitter bar down enough, the lines in the button texts move as well until they disappear causing my buttons to accept clicks again. I know it must have something to do with the layouts, but don't see how to fix it. Can someone explain what's going on?
Side note:--The positioning of widgets inside layouts within splitters is more convoluted than expected since any adjustments of size_hints & pos_hints, spacing and padding affect each other.
Here's my GUI:
Here's the code:
import kivy
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.splitter import Splitter
from kivy.uix.image import Image
kivy.require('2.0.0')
class ChessBoardWidget(RelativeLayout):
def __init__(self, **kwargs):
super(ChessBoardWidget, self).__init__(**kwargs)
# adjust where the left side of vertical layout starts by changing 'center_x'
repertoire_boxlayout_vert = BoxLayout(orientation='vertical', size_hint_y=.05,
pos_hint={'center_x': .774}) # >center_x moves right
# Padding between layout box and children: [padding_left, padding_top, padding_right, padding_bottom]
# Padding puts space between widgets and the edge of layout holding the widgets
# Spacing puts space between the widgets inside a layout
repertoire_boxlayout_horz = BoxLayout(orientation='horizontal', size_hint=(.45, .05),
spacing=10, padding=[0, 0, 0, 30])
repertoire_boxlayout_horz.add_widget(Label(text='Repertoire for:', size_hint=(.08, 1)))
repertoire_boxlayout_horz.add_widget(Button(text='White', size_hint=(.04, 1)))
repertoire_boxlayout_horz.add_widget(Button(text='Black', size_hint=(.04, 1)))
repertoire_boxlayout_vert.add_widget(repertoire_boxlayout_horz)
chessboard_gui_boxlayout = BoxLayout(orientation='vertical', spacing=40)
chessboard_gui_boxlayout.add_widget(
Image(source="./data/images/chess-pieces/DarkerGreenGreyChessBoard.png", pos=self.pos, keep_ratio=True,
allow_stretch=True)) # default size_hint of (1,1) claims all of remaining height
chessboard_gui_boxlayout.add_widget(repertoire_boxlayout_vert)
self.add_widget(chessboard_gui_boxlayout)
class SplitterGui(BoxLayout):
def __init__(self, **kwargs):
super(SplitterGui, self).__init__(**kwargs)
self.orientation = 'horizontal'
# Splitter 1
split1_boxlayout = BoxLayout(orientation='vertical')
split1 = Splitter(sizable_from='bottom', min_size=74,
max_size=1100, size_hint=(1, .8)) # size_hint=(..,y is smaller, bar moves up
chessboard_widget = ChessBoardWidget()
split1.add_widget(chessboard_widget)
split1_boxlayout.add_widget(split1)
s3_button = Button(text='s3', size_hint=(1, 1))
split1_boxlayout.add_widget(s3_button)
self.add_widget(split1_boxlayout)
# Splitter 2
split2 = Splitter(sizable_from='left', min_size=74,
max_size=1800, size_hint=(3.33, 1)) # size_hint=(x is larger, bar moves left
s2_button = Button(text='s2', size_hint=(.1, 1))
split2.add_widget(s2_button)
self.add_widget(split2)
class ChessBoxApp(App):
def build(self):
return SplitterGui() # root
if __name__ == '__main__':
ChessBoxApp().run()
The problem is that your repertoire_boxlayout_horz with its size_hint, spacing and padding settings leaves no space for the Buttons and Label. So those three widgets end up with a height of 0 and that results in mouse click calculations saying that the clicks do not occur on those widgets. Even though those three widgets have a height of 0, The text and background is still drawn for each. Possible fixes are to eliminate the spacing and padding settings, or set size_hint_y to None for the repertoire_boxlayout_horz with a specified height that allows for those settings, or switch to kv to define your GUI where you can use the minimum_height property of BoxLayout.
all right? i hope so, I will be very grateful if you try to solve this problem that I'm facing about programming.
I'm trying to make a quadratic function plotter, however I can only display one side of the function.
If you uncomment that part of the code you will see that kivy only displays the other side, but the first side of the graphic is still hidden.
What I want to do and if you can help me here is to display both sides of the quadratic function in the same graph and code.
My code is bellow:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.lang import Builder
from kivy.properties import ListProperty
from kivy.graphics import Rectangle, Color
Builder.load_string(
'''
<QuadraticApp>:
my_size: [root.size[0]*4, root.size[1]*0.5]
canvas:
Color:
rgba: 1, 1, 1, 0
Rectangle:
pos: (root.pos[0], root.pos[1])
size:(500, 500)
'''
)
class QuadraticApp(Widget):
my_size = ListProperty([0, 0])
def __init__(self):
super(QuadraticApp, self).__init__()
new_y = self.my_size
new_x = self.my_size
#'''
for x in range(100):
with self.canvas:
Color(0.1, 0.75, 0.1, 1, mode='rgba') # GREEN
rectx = Rectangle(pos=(new_x), size=(10, 10))
new_x[0] += (x*0.25-50)/2
new_x[1] += eval('(x)**2')
#'''
# Comment the for loop above to hide the another showing graphic
# And Uncoment this code bellow to show the another part of graphic
'''
for y in range(100):
with self.canvas:
Color(0.75, 0.1, 0.1, 1, mode='rgba') # RED
recty = Rectangle(pos=(new_y), size=(10, 10))
new_y[0] += -(y*0.25-50)/2
new_y[1] += eval('(y)**2')
'''
class MathApp(App):
def build(self):
return QuadraticApp()
if __name__ == '__main__':
MathApp().run()
This image bellow show the graphic at the left simetric side
https://i.stack.imgur.com/i1WTy.jpg
This image bellow show the graphic at the right simetric side
https://i.stack.imgur.com/HVhAz.jpg
Now i can say thank you for u support.
The problem is that the code:
new_y = self.my_size
new_x = self.my_size
is just creating references to self.my_size, so when you change new_x, or new_y, or self.my_size, you are changing all three. At the start of your second loop, new_y starts out with the values from new_x from the end of the first loop. Try changing those two lines to:
new_y = self.my_size.copy()
new_x = self.my_size.copy()
I have made a square shaped grid of labels using Gridlayout. Now i want to add a background color the labels(each having different rectangles). I tried to do this by the following code.
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.gridlayout import GridLayout
from kivy.graphics import Rectangle, Color
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
class MyGrid(FloatLayout):
def __init__(self,**kwargs):
super(MyGrid,self).__init__(**kwargs)
self.grid=GridLayout()
self.grid_size=4
self.grid.cols=self.grid_size
for k in range(self.grid_size):
for i in range(self.grid_size):
with self.grid.canvas:
Rectangle(size=(100,100),pos=(k*160+100,i*160+100),source="52852.JPG")
for h in range(self.grid_size):
for j in range(self.grid_size):
self.grid.add_widget(Label(text="labwl"+str(h)+str(j),size=(100,100),pos=(h*160+100,j*160+100)))
self.add_widget(self.grid)
class GameApp(App):
def build(self):
return MyGrid()
if __name__ == '__main__':
GameApp().run()
In this code if I do not specify "self.grid.cols" it generates a warning in the python console and also when the window is turned to full screen mode the rectangles from canvas retain there original size and position but the labels do not. I want to get the labels in front of the rectangles of canvas and they should also retain the size of the screen as specified. Moreover if I change the "self.grid.size" to any other number it should make the grid of labels of that length and corresponding number of canvas too. I tried float layout for this purpose also but it was of no help. The canvas rectangles and labels should fit in the window whatever the size of window has. It would be better if I can get the solution to above problem written in python file(not in .kv file). If you know any other solution to this problem or any other widget please let me know. Like for button widget we can specify the background color and text also your can add any of that widget which will do above task. You should replace the "source" in the rectangle canvas to any known image file. I hope you understand. If you do not please do let me know. :)
Setting the Widgets to not change size or pos is the easiest solution. Basically just use size_hint=(None, None) and don't use the GridLayout:
from kivy.app import App
from kivy.graphics import Rectangle
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
class MyGrid(FloatLayout):
def __init__(self,**kwargs):
super(MyGrid,self).__init__(**kwargs)
self.grid_size=4
for k in range(self.grid_size):
for i in range(self.grid_size):
with self.canvas:
Rectangle(size=(100,100),pos=(k*160+100,i*160+100),source="52852.JPG")
for h in range(self.grid_size):
for j in range(self.grid_size):
self.add_widget(Label(text="labwl"+str(h)+str(j),size=(100,100),pos=(h*160+100,j*160+100), size_hint=(None, None)))
class GameApp(App):
def build(self):
return MyGrid()
if __name__ == '__main__':
GameApp().run()
To make the Rectangles and Labels change pos and size is a bit more complex. In the modified version of your code below, I keep lists of the Labels and the Rectangles, and bind the adjust_common_size() method to run whenever the size of MyGrid changes. That method then adjusts the size and pos of the Labels and Rectangles to match:
from kivy.app import App
from kivy.properties import ListProperty
from kivy.graphics import Rectangle
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
class MyGrid(FloatLayout):
common_size = ListProperty((100, 100))
def __init__(self,**kwargs):
super(MyGrid,self).__init__(**kwargs)
self.grid_size=4
self.rects = []
self.labels = []
for k in range(self.grid_size):
one_row = []
self.rects.append(one_row)
for i in range(self.grid_size):
with self.canvas:
one_row.append(Rectangle(size=self.common_size,pos=(k*160+100,i*160+100),source="52852.JPG"))
for h in range(self.grid_size):
one_row = []
self.labels.append(one_row)
for j in range(self.grid_size):
label = Label(text="labwl"+str(h)+str(j),size=self.common_size,pos=(h*160+100,j*160+100), size_hint=(None, None))
one_row.append(label)
self.add_widget(label)
self.bind(size=self.adjust_common_size)
def adjust_common_size(self, instance, new_size):
self.common_size = (new_size[0] * 0.9 / self.grid_size, new_size[1] * 0.9 / self.grid_size)
for k in range(self.grid_size):
for i in range(self.grid_size):
adjusted_pos = (k * new_size[0] / self.grid_size, i * new_size[1] / self.grid_size)
rect = self.rects[k][i]
label = self.labels[k][i]
label.size = self.common_size
label.pos = adjusted_pos
rect.size = self.common_size
rect.pos = adjusted_pos
class GameApp(App):
def build(self):
return MyGrid()
if __name__ == '__main__':
GameApp().run()
Using a ListProperty for the common_size is not necessary, but would be handy if you decide to use kv.
This is an interesting question. Here is a better way to make the Rectangle and the Label match. The code below uses the GridLayout, but defines MyLabel to include its own Rectangle and to keep its Rectangle matched in pos and size:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.graphics import Rectangle
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
class MyLabel(Label):
def __init__(self, **kwargs):
super(MyLabel, self).__init__(**kwargs)
with self.canvas.before:
self.rect = Rectangle(size=self.size,pos=self.pos,source="52852.JPG")
self.bind(size=self.adjust_size)
self.bind(pos=self.adjust_pos)
def adjust_pos(self, instance, new_pos):
self.rect.pos = new_pos
def adjust_size(self, instance, new_size):
self.rect.size = new_size
class MyGrid(FloatLayout):
def __init__(self,**kwargs):
super(MyGrid,self).__init__(**kwargs)
self.grid=GridLayout()
self.grid_size=4
self.grid.cols=self.grid_size
for h in range(self.grid_size):
for j in range(self.grid_size):
self.grid.add_widget(MyLabel(text="labwl"+str(h)+str(j),size=(100,100),pos=(h*160+100,j*160+100)))
self.add_widget(self.grid)
class GameApp(App):
def build(self):
return MyGrid()
if __name__ == '__main__':
GameApp().run()
With this approach, you don't have to create the Rectangles in the MyGrid at all, since each Label creates its own.
I am learning to use Kivy. My objective is to create an app to display a sound wave chart of a running sound.
Unfortunately, I cannot get the chart to update in real-time. I get "NameError: name 'graph' is not defined", and I don't really know how to fix it.
Code below:
from math import sin
from kivy.garden.graph import Graph, MeshLinePlot
from kivy.garden.matplotlib.backend_kivyagg import FigureCanvasKivyAgg
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
class MyApp(App):
plot = MeshLinePlot(color=[1, 0, 0, 1])
graph = Graph(xlabel='X', ylabel='Y', x_ticks_minor=5,
x_ticks_major=25, y_ticks_major=1,
y_grid_label=False, x_grid_label=False, padding=5,
x_grid=False, y_grid=False, xmin=-0, xmax=100, ymin=-1, ymax=1,)
def build(self):
box = BoxLayout()
Clock.schedule_interval(self.update_points, 1/60.)
Clock.schedule_interval(self.update_xaxis, 1/60.)
box.add_widget(graph)
return box
def update_xaxis(self,*args):
global graph
graph.xmin = 0
graph.xmax = 100
def update_points(self, *args):
#self.plot.points = [(i,i)]
self.plot.points = [(x, sin(x / 10.)) for x in range(0, 101)]
MyApp().run()
Your references to plot and graph (except where they are created) should all be self.plot and self.graph. Also, you don't want the global graph line in your update_xaxis() method. After you make that correction, you next question should be a separate post.
I am trying to dynamically change the background color of a Label in Kivy with this simple program. it is intended to produce a grid of red and black cells. However, all I get is a red cell in position (7,0) (I am printing the positions).
import kivy
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.graphics import Color, Rectangle
class LabelX(Label):
def set_bgcolor(self,r,b,g,o,*args):
self.canvas.after.clear()
with self.canvas.after:
Color(r,g,b,o)
Rectangle(pos=self.pos,size=self.size)
class MyGrid(GridLayout):
def __init__(self,cols,rows,**kwargs):
super(MyGrid,self).__init__(**kwargs)
self.cols = cols
for i in range(rows):
for j in range(cols):
l = LabelX(text=str(i)+","+str(j))
if (i*rows+j)%2:
l.set_bgcolor(1,0,0,1)
else:
l.set_bgcolor(0,0,0,1)
self.add_widget(l)
class GridApp(App):
def build(self):
g = MyGrid(8,8)
return g
if __name__=="__main__":
GridApp().run()
Any ideas on how to get a red/black checker board?
If you print self.pos using the following:
with self.canvas.after:
[...]
print(self.pos)
[...]
only the values [0, 0] are obtained, and that leads to the conclusion that all the rectangles are drawn in that position, so they are superimposed, and that is what you observe.
For example, to verify this we pass a parameter more to the function set_bgcolor () that will be the index of the loop and we will use it as a parameter to locate the position:
def set_bgcolor(self, r, b, g, o, i):
[..]
Rectangle(pos=[100*i, 100*i],size=self.size)
class MyGrid(GridLayout):
def __init__(self,cols,rows,**kwargs):
[...]
if (i*rows+j)%2:
l.set_bgcolor(1,0,0,1, i)
else:
l.set_bgcolor(0,0,0,1, i)
self.add_widget(l)
We get the following:
also if we change the size of the window the rectangle does not change either:
So if you want the Rectangle to be the bottom of the Label, the position and size of the Rectangle should be the same as the Label, so you have to make a binding with both properties between the Label and Rectangle. You must also use canvas.before instead of canvas.after otherwise the Rectangle will be drawn on top of the Label.
class LabelX(Label):
def set_bgcolor(self,r,b,g,o):
self.canvas.before.clear()
with self.canvas.before:
Color(r,g,b,o)
self.rect = Rectangle(pos=self.pos,size=self.size)
self.bind(pos=self.update_rect,
size=self.update_rect)
def update_rect(self, *args):
self.rect.pos = self.pos
self.rect.size = self.size
The advantage of my solution is that it is independent of the external widget or layout since the position and size of the Rectangle only depends on the Label.
The problem is that each labels position is (0,0) and size is (100,100) until the app is actually built. so all your canvas drawing is done at those positions and sizes. You can get the checkerboard effect by waiting until after the positions and sizes are assigned, then doing the checkerboard effect. I do this with a Clock.schedule_once:
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.graphics import Color, Rectangle
from kivy.clock import Clock
from kivy.core.window import Window
class LabelX(Label):
def set_bgcolor(self,r,b,g,o,*args):
self.canvas.after.clear()
with self.canvas.after:
Color(r,g,b,o)
Rectangle(pos=self.pos,size=self.size)
class MyGrid(GridLayout):
def __init__(self,cols,rows,**kwargs):
super(MyGrid,self).__init__(**kwargs)
self.cols = cols
for i in range(rows):
for j in range(cols):
l = LabelX(text=str(i)+","+str(j))
l.rowcol = (i,j)
self.add_widget(l)
class GridApp(App):
def build(self):
self.g = MyGrid(8,8)
Window.bind(size=self.checkerboard)
return self.g
def checkerboard(self, *args):
for l in self.g.children:
count = l.rowcol[0] + l.rowcol[1]
if count % 2:
l.set_bgcolor(1, 0, 0, 1)
else:
l.set_bgcolor(0, 0, 0, 1 )
if __name__=="__main__":
app = GridApp()
Clock.schedule_once(app.checkerboard, 1)
app.run()