I'm writing a data scraper for my school that displays information live in an app form. I'm trying to create buttons in a for loop, use lambda to bind each button to a function and a value to use when calling (in this case, to print their value for testing), add each button (as well as a couple other widgets) to a layout and add this layout to the screen.
For some reason, however, although I should be saving the value of "a" in the lambda function: btn.bind(on_press=lambda btn, a=a: self.getlink(a)), lambda cannot seem to "remember" more than around 3 iterations back (this loop goes for about 20 cycles). The last three buttons (most recently generated) on the screen return correct values, but the further you go up on the screen (a kivy RecycleView layout), the less accurate the value. When I press the first few buttons on the screen, literally nothing happens. As I go down the screen pressing buttons, I occasionally get a random number spit out, sometimes a significantly different number (spits out 2 when it should be 11 for example).
There are no errors being thrown back when I run it, and everything else works just fine. I just cannot seem to get these buttons to bind to the proper value. I even tried using a counter in the loop that adds one for every iteration and using that instead of a, but it did the same thing.
Any help would be much appreciated, thank you!
Here is the relevant code:
from kivy.app import App
from kivy.base import runTouchApp
from kivy.core.window import Window
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.widget import Widget
from kivy.uix.image import AsyncImage
from kivy.uix.button import Button
from kivy.lang import Builder
class ScreenThree(RecycleView, Screen):
def __init__(self, **kwargs):
super(ScreenThree, self).__init__(**kwargs)
self.data = [{'padding': '7sp',
'size_hint': (6, None),
'allow_stretch': True,
'height': ((Window.height / 4) * (22))}]
def screenlength(self):
return Window.height * (20 / 4)
def screenSize(self, modifier=1):
window_s = Window.size
if window_s[1] >= window_s[0]:
return str((window_s[0] * 0.007) * modifier) + 'sp'
else:
return str((window_s[1] * 0.007) * modifier) + 'sp'
class ScreenFour(TabbedPanel, Screen):
def __init__(self, **kwargs):
super(ScreenFour, self).__init__(**kwargs)
self.default_tab_text = 'Events'
self.tab_width = Window.width / 2.7
self.tab_height = Window.height / 25
self.default_tab_content = ScreenThree()
self.background_color = (0, 0, 0, 1)
def screenSize(self, modifier=1):
window_s = Window.size
if window_s[1] >= window_s[0]:
return str((window_s[0] * 0.007) * modifier) + 'sp'
else:
return str((window_s[1] * 0.007) * modifier) + 'sp'
class AddWidget(GridLayout, Widget):
def __init__(self, **kwargs):
super(AddWidget, self).__init__(**kwargs)
for a in range(20):
asyncimage1 = AsyncImage(source='https://images-na.ssl-images-amazon.com/images/I/51zLZbEVSTL._AC_SX679_.jpg',
allow_stretch=True,
size_hint_y=None,
size_hint_x=5,
height=Window.height / 4)
self.add_widget(asyncimage1)
label = Label(text='title goes here',
bold=True,
text_size=self.size,
size_hint=(1, 1),
valign='top',
padding=(0, '11sp'),
font_size=self.screenSize(1.2))
label.bind(size=label.setter('text_size'))
layout2 = GridLayout(rows=3)
label2 = Label(text='date goes here',
text_size=self.size,
size_hint=(1, 1),
valign='top',
padding=(0, '1sp'),
font_size=self.screenSize(0.9))
label2.bind(size=label2.setter('text_size'))
layout2.add_widget(label)
layout2.add_widget(label2)
btn = Button(text='Visit Website',
font_size=self.screenSize(1.4),
size_hint=(1, 1),
id = str(a),
padding=("10sp", "10sp"))
btn.bind(on_press=lambda btn, a=a: self.getlink(a))
layout2.add_widget(btn)
self.add_widget(layout2)
def getlink(self, position):
print(position)
def screenSize(self, modifier=1):
window_s = Window.size
if window_s[1] >= window_s[0]:
return str((window_s[0] * 0.007) * modifier) + 'sp'
else:
return str((window_s[1] * 0.007) * modifier) + 'sp'
class ScreenManagement(ScreenManager):
pass
pres1 = runTouchApp(Builder.load_file('screenskv2.kv'))
class MyApp(App):
def build(self):
return ScreenManagement
MyApp().run()
And the relevant code in the kv file:
#:import Window kivy.core.window.Window
ScreenManagement:
ScreenThree:
<AddWidget>:
orientation: 'horizontal'
cols:2
spacing: '10sp'
col_default_width: (Window.width/2)-50
<ScreenThree>:
name: 'screen_three'
rv_layout: 'layout'
viewclass: 'AddWidget'
RecycleBoxLayout:
id: 'layout'
pos_hint_x: 0
size_hint: (None, None)
height: self.minimum_height
orientation: 'vertical'
Related
I am building an app using Kivy and would like to draw a circle to the middle of a Widget as soon as the app starts up. I found how to run code on start in this question (How do I run a function once the form is loaded Kivy). However, as a comment points out, widths and heights are not initialized yet when calling on_start(). Does anyone know how I could do this?
I have the following code. Using this, the circle is drawn at position 50, 50, while I would like it in the middle of the Field widget.
main.py:
import kivy
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Rectangle, Color, Ellipse
from kivy.lang import Builder
from kivy.properties import ObjectProperty
class Field(Widget):
def __init__(self, **kwargs):
super(Field, self).__init__(**kwargs)
def init_circle(self):
with self.canvas:
Color(1,1,1,1, mode='rgba')
r = 20
self.circle = Ellipse(pos=(self.width//2 - r, self.height//2 - r), size=(2*r, 2*r))
print(self.circle)
def move_circle(self):
cx, cy = self.circle.pos
self.circle.pos = (cx + 10, cy)
class RootClass(Widget):
field = ObjectProperty(None)
def __init__(self, **kwargs):
super(RootClass, self).__init__(**kwargs)
class MyMainApp(App):
def build(self):
self.r = RootClass()
return self.r
def on_start(self, **kwargs):
self.r.field.init_circle()
if __name__ == '__main__':
Builder.load_file("my.kv")
MyMainApp().run()
my.kv:
<RootClass>
field: field_id
BoxLayout:
size: root.size
orientation: "vertical"
Field:
id: field_id
size_hint: 1, 0.9
Button:
size_hint: 1, 0.1
text: "Move"
on_press:
root.field.move_circle()
Method 1:
Using bind by binding to a callback method,
class Field(Widget):
def __init__(self, **kwargs):
super(Field, self).__init__(**kwargs)
# Bind a callback method, say here 'init_circle' to the prop. 'size' and 'pos'
# so that whenever those prop. change the method will be called.
# To prevent redrawing you may use method 'clear' or some other strategy.
self.bind(size = self.init_circle, pos = self.init_circle)
def init_circle(self, *args):
with self.canvas:
...
Method 2:
Using Clock by scheduling the process,
def on_start(self, **kwargs):
Clock.schedule_once(self.r.field.init_circle)
I want to create a list of coloured labels. The thing is that I could do it with the kv file, but I need to do it through the build() method. So I tried replicate what I have done, but it does not work. And I can't understand why.
This is what I've coded
from kivy.app import App
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.graphics import *
class RL(RelativeLayout): # Creates the background colour for each label
def __init__(self, **kwargs):
super().__init__(**kwargs)
with self.canvas:
Color(.7, 0, .5, 1)
Rectangle(size_hint=self.size)
class MainMenu(BoxLayout):
N_LBLS = 8
labels_text = []
RL_list = []
def __init__(self, **kwargs):
super().__init__(**kwargs)
button = Button(text='do something')
button.bind(on_release=self.change_text)
box = BoxLayout(orientation='vertical', padding= 10, spacing = 15)
for i in range(0, self.N_LBLS):
self.RL_list.append(RL())
self.labels_text.append(Label(text=f'{i}º label', size_hint=self.size))
self.RL_list[i].add_widget(self.labels_text[i])
box.add_widget(self.RL_list[i])
self.add_widget(button)
self.add_widget(box)
def change_text(self, instance):
for lbl in self.labels_text:
if lbl.text[0] == '5':
lbl.text = 'Text changed'
class MainApp(App):
def build(self):
return MainMenu()
if __name__ == '__main__':
MainApp().run()
It's supposed to make a button to the left, and a list of 8 coloured labels to the right.
The problem is that you are setting size_hint=self.size in each Label. The self.size is the size of the MainMenu, which is [100,100] when that code is executed. Note that size_hint is a multiplier that is applied to the parents size to calculate the widgets size. So a size_hint of [100,100] makes each Label 100 times bigger than the MainMenu. So your code is working, but the Labels are so large that the text is off the screen. Start by just removing size_hint=self.size.
And, to set a background color on a Label, you can just use the canvas of that Label, rather than some container. Here is a version of your code that does that:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
class ColorLabel(Label):
pass
Builder.load_string('''
<ColorLabel>:
bg_color: [.7, 0, .5, 1]
canvas.before:
Color:
rgba: self.bg_color
Rectangle:
pos: self.pos
size: self.size
''')
class MainMenu(BoxLayout):
N_LBLS = 8
labels_text = []
def __init__(self, **kwargs):
super().__init__(**kwargs)
button = Button(text='do something')
button.bind(on_release=self.change_text)
box = BoxLayout(orientation='vertical', padding=10, spacing=15)
for i in range(0, self.N_LBLS):
self.labels_text.append(ColorLabel(text=f'{i}º label'))
box.add_widget(self.labels_text[i])
self.add_widget(button)
self.add_widget(box)
def change_text(self, instance):
for lbl in self.labels_text:
if lbl.text[0] == '5':
lbl.text = 'Text changed'
lbl.bg_color = [0, 1, 0, 1]
class MainApp(App):
def build(self):
return MainMenu()
if __name__ == '__main__':
MainApp().run()
It seems a lot of people have trouble with the on_press argument with Kivy, but I haven't found answers to my issue...
Here is what's happening:
I'm getting started with my first app in python/kivy. I know python, but perhaps not enough concerning classes... I am able to create a button, with a on_press action that opens a popup.
Now the goal is the following: I have a function affiche_grille, which displays a grid with lines on the screen. Inside each square, I create a button, with a text (a number). This works. I would like to bind an on_press event on these buttons : but now, the syntax does not work any more... Maybe is it because the button is created in a "with self.canvas" instruction, and the self.function is not appropriate any more ?
Here is the (complete after edit) code :
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.uix.popup import Popup
from kivy.graphics import Color, Line
from kivy.core.window import Window
from kivy.core.text.text_layout import layout_text
from kivy.uix.floatlayout import FloatLayout
import numpy as np
from functools import partial # for on_press syntax
class Ecran_Principal(BoxLayout):
def build(self):
self.orientation='vertical'
self.liste_txt = np.zeros([9,9], dtype=object) # will contain Label
self.grille_affichee = np.zeros([9,9])
self.lGrille() # layout for the grid
self.lMenu() # layout for the menu
def lGrille(self):
LayGrille = GridLayout(cols=1,size_hint_y=0.8)
# window dimensions, in pixels
(L, H) = Window.size
H = int(0.8*H) # because of the 20% menu
if L > H: # landscape format - computer case
self.ymin = int(0.25*H) + int(0.05*H)
self.delta = int(0.1*H)
self.xmin = int((L-9*self.delta)/2.)
else: # portrait format - phone case
self.xmin = int(0.05*L)
self.delta = int(0.1*L)
self.ymin = int(0.25*H) + int((H-9*self.delta)/2.)
# end dimensions
self.deltaxrel = self.delta/H
self.deltayrel = self.delta/L
# grid definition (without numbers)
with self.canvas:
Color(1, 1, 1) # white
# automatic line traces
for i in range(4):
# big vertical lines
ymax = self.ymin+9*self.delta
xligne = self.xmin+i*3*self.delta
Line(points=[xligne, self.ymin, xligne, ymax], width=2)
# big horizontal lines
xmax = self.xmin+9*self.delta
yligne = self.ymin+i*3*self.delta
Line(points=[self.xmin, yligne, xmax, yligne], width=2)
# little intermediary lines
for ipetit in range(3):
if i ==3:
break
xpetit = xligne + ipetit*self.delta
Line(points=[xpetit, self.ymin, xpetit, ymax], width=1)
ypetit = yligne + ipetit*self.delta
Line(points=[self.xmin, ypetit, xmax, ypetit], width=1)
# end little lines
# end for
# grid display :
self.affiche_grille()
self.add_widget(LayGrille)
# end with
def affiche_grille(self):
# I tried to remove this 'with' instruction and does not change anything
with self.canvas:
for i in range(9): # line
for j in range(9): # colomn
valeur = self.grille_affichee[i,j]
val = "{0:.0f}".format(valeur)
layout = FloatLayout(size=(self.xmin + (j+0.5)*self.delta,
self.ymin + (8.5-i)*self.delta),
pos_hint=(None, None))
montexte = Button(text=val,
size_hint=(self.deltaxrel,
self.deltayrel),
pos=(self.xmin + (j+0.5)*self.delta,
self.ymin + (8.5-i)*self.delta),
background_color = (0,0.2,0,1),
background_normal = '',
on_press=partial(self.choisir_valeur, i, j)
)
self.liste_txt[i, j] = montexte
# THE BUTTONS AND THE TEXT ARE DISPLAYED,
# BUT NOTHING HAPPENS WHEN YOU PRESS THE BUTTONS
layout.add_widget(self.liste_txt[i, j])
# end j
# end i
# end with
def choisir_valeur(self, i, j):
print("Hello !") # NEVER DISPLAYED :(
#champ = TextInput(text=str(self.grille_affichee[i, j]),
# font_size=30,
# focus=True,
# multiline=False)
champ = Button(text=str(self.grille_affichee[i, j]))
popup = Popup(title='Value in line {} - colomn {}'.format(i, j),
content=champ,
size_hint=(0.5,0.5))
champ.bind(on_press=popup.dismiss)
popup.open()
def lMenu(self):
LayMenu = GridLayout(cols=2, rows=2, size_hint_y=0.2)
# Bouton Résoudre
self.BoutonResoudre=Button(text='Resoudre',size_hint=(0.5,0.1),pos_hint={'left': 0.},background_color=[0.9,0.9,0.9,1])
self.BoutonResoudre.bind(on_press=self.resoudre)
LayMenu.add_widget(self.BoutonResoudre)
# Bouton Remplir
self.BoutonScanner=Button(text='Scanner',size_hint=(0.5,0.1),pos_hint={'left': 0.5},background_color=[0.9,0.9,0.9,1])
self.BoutonScanner.bind(on_press=self.scanner)
LayMenu.add_widget(self.BoutonScanner)
# Bouton Indice
self.BoutonIndice=Button(text='Indice',size_hint=(0.5,0.1),pos_hint={'bottom': 0.},background_color=[0.9,0.9,0.9,1])
self.BoutonIndice.bind(on_press=self.indice)
LayMenu.add_widget(self.BoutonIndice)
# Bouton Quitter
self.BoutonQuitter=Button(text='Quitter',size_hint=(0.5,0.1),background_color=[0.9,0.9,0.9,1])
self.BoutonQuitter.bind(on_press=self.quitter)
LayMenu.add_widget(self.BoutonQuitter)
self.add_widget(LayMenu)
def resoudre(self, instance):
content = Button(text='Resolution du sudoku', font_size=20)
popup = Popup(title='RESOLUTION',content=content, size_hint=(0.5,0.5))
content.bind(on_press=popup.dismiss)
popup.open()
def scanner(self, instance):
content = Button(text='Remplissage auto par photo', font_size=20)
popup = Popup(title='SCAN PHOTO',content=content, size_hint=(0.5,0.5))
content.bind(on_press=popup.dismiss)
popup.open()
def indice(self, instance):
content = Button(text='Affichage d\'un indice', font_size=20)
popup = Popup(title='INDICE',content=content, size_hint=(0.5,0.5))
content.bind(on_press=popup.dismiss)
popup.open()
def quitter(self, instance):
content = Button(text='Au revoir !', font_size=20)
popup = Popup(title='QUITTER',content=content, size_hint=(0.5,0.5))
content.bind(on_press=exit())
popup.open()
class Sudoku(App):
def build(self):
ecran=Ecran_Principal()
ecran.build()
return ecran
if __name__ == '__main__':
Sudoku().run()
Everything is interpreted, but the buttons inside the grid are not working...
I've seen the functools.partial() tip, but it does not seem to be enough...
Does anyone have an idea of what is happening ? I am not very familiar with classes in python and I have certainly missed something.
Thank you in advance, and sorry if the question is too basic.
Well, you now know that you can't add a widget to canvas. Also, you shouldn't have a build method in your Ecran_Principal class. build() only belongs in the Sudoku() App class. Use __init__ instead.
I think if you try separating the visual stuff into kv language things will be easier. Below is an example utilizing spacing and padding with GridLayouts to 'draw' the game board. The buttons are hooked up with a simple callback.
btngrid.py
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.button import Button
class SmallGrid(GridLayout):
pass
class BigGrid(GridLayout):
pass
class GameBoard(AnchorLayout):
# A nice layout to use to keep things centered. Only one widget can be added to this.
def __init__(self, *args, **kwargs):
super(GameBoard, self).__init__(*args, **kwargs)
big = BigGrid()
for i in range(9):
small = SmallGrid()
for j in range(9):
small.add_widget(Button(text="{}, {}".format(i, j), on_release=self.callback))
big.add_widget(small)
self.add_widget(big)
def callback(self, button):
print button.text
class RootWidget(BoxLayout):
def __init__(self, *args, **kwargs):
super(RootWidget, self).__init__(*args, **kwargs)
self.orientation = 'vertical'
self.add_widget(GameBoard())
bottom_btns_container = GridLayout(cols=2, size_hint=(1, .2))
for i in range(4):
# Just for show, they don't do anything
bottom_btns_container.add_widget(Button(text="Button {}".format(i)))
self.add_widget(bottom_btns_container)
class BtnGridApp(App):
def build(self):
return RootWidget()
btngird.kv
<BigGrid>:
cols: 3
size_hint: (None, .8) # scales
width: self.height # scales
spacing: 5 # Width of lines
padding: 5 # perimeter border
# This draws a background for the whole grid.
# When used with spacing and padding, part of the background will show.
# Same with SmallGrid below
canvas.before:
Color:
rgba: [.9, .9, .9, 1]
Rectangle:
pos: self.pos
size: self.size
<SmallGrid>:
cols: 3
size_hint: (None, .8) # scales
width: self.height # scales
spacing: .25
canvas.before:
Color:
rgba: [.6, .6, .6, 1] # White lines
Rectangle:
pos: self.pos
size: self.size
<GameBoard>:
anchor_x: "center"
anchor_y: "center"
I want to center a single line of text in Kivy text input.
I'm going to use padding
widget.padding = [ (self.textinput.width - width of line) / 2, 20, 0, 0]
but i can't find the width of the line. How can I calculate or access the width of the line?
There is an internal TextInput._get_text_width method you can use to calculate proper padding:
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.lang import Builder
Builder.load_string('''
<MyWidget>:
TextInput:
multiline: False
on_text: root.update_padding(args[0])
padding_x: self.width/2 # initial padding
''')
class MyWidget(FloatLayout):
def update_padding(self, text_input, *args):
text_width = text_input._get_text_width(
text_input.text,
text_input.tab_width,
text_input._label_cached
)
text_input.padding_x = (text_input.width - text_width)/2
class MyApp(App):
def build(self):
return MyWidget()
if __name__ == '__main__':
MyApp().run()
The solution above almost worked for me. Sometimes the padding wouldn't be updated correctly. Here is my slight change, setting text_width as a NumericProperty:
In Kivy:
<CenteredTextInput#TextInput>:
multiline: False
on_text: root.update_padding()
padding_x: (self.width - self.text_width) / 2
In Python:
class CenteredTextInput(TextInput):
'''
A centered TextInput.
'''
text_width = NumericProperty()
'''The text width
'''
def update_padding(self, *args):
'''
Update the padding so the text is centered
'''
self.text_width = self._get_text_width(
self.text,
self.tab_width,
self._label_cached
)
You could make a textinput behind a button, and make the button visualize as the text input.
When pushing the button, put the focus to the textinput, and update the buttons text.
I have made an example here.
from kivy.uix.textinput import TextInput
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.clock import Clock
from kivy.app import App
from kivy import require
require('1.9.1')
class MyWidget(BoxLayout):
def __init__(self,**kwargs):
super(MyWidget,self).__init__(**kwargs)
self.orientation = "vertical"
self.cur = False
self.textinput = TextInput(text='',halign="center",multiline=False)
self.textinput.bind(text=self.on_text)
self.button = Button(background_normal="",background_color=[0,0,0.1,1],font_size="40sp")
self.button.bind(on_release=self.button_click)
self.my_float_layout = FloatLayout()
self.my_float_layout.add_widget(self.textinput)
self.my_float_layout.add_widget(self.button)
self.add_widget(Label(text="type text below",font_size="40sp"))
self.add_widget(self.my_float_layout)
Clock.schedule_interval(self.cursor, 0.5)
def cursor(self,dt): # function to visualize a cursor
if self.textinput.focus:
cur_pos = self.textinput.cursor[0]
if not self.cur:
self.button.text = self.textinput.text[:cur_pos] + "|" + self.textinput.text[cur_pos:]
self.cur = True
else:
self.button.text = self.textinput.text[:cur_pos] + " " + self.textinput.text[cur_pos:]
self.cur = False
elif self.cur:
self.button.text = self.textinput.text + " "
self.cur = False
def on_text(self, *args): # function to set the button text
self.button.text = self.textinput.text
def button_click(self,*args): # function to focus the input
self.textinput.focus = True
class MyApp(App):
def build(self):
return MyWidget()
if __name__ == "__main__":
MyApp().run()
Summary of test application: I am writing a Kivy app with a scrollable view (named Scroller) with many fields (named Field) to look at. These separate fields are really difficult to distinguish on occasion, so I decided to use alternating background colors for each field to help distinguish each other. My testing application uses 20 individual fields each of which alternates between dark grey and darker grey.
Testing trials:
Starting the application, the program looks great. The alternating background appear just fine. Even when I scroll down the application looks fine. However, the application seems to get bizarre when I scroll up on the application. The text scrolls with the application, but the background does not. Even better (sarcastically), the text starts to fade away into their neighbors background. The problem just seems to vanish when I scroll down again (passed the point of the furthest scroll up point).
Brief problem description: The Field's "background color" messes up the application during scrolling up events.
Side note: I have also noticed that the application got a little sluggish after scrolling too much. I am not that familiar with the drawing cycle of Kivy, but blitting backgrounds should not yield an excessive slowdown.
Testing application:
import kivy
kivy.require('1.0.7')
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.graphics import Color, Rectangle
class Main(App):
def build(self):
self.root = GridLayout(rows = 1)
self.root.add_widget(Scroller())
return self.root
class Scroller(ScrollView):
def __init__(self):
ScrollView.__init__(self)
self.view = GridLayout(cols = 1, size_hint = (1, None))
self.add_widget(self.view)
self.view.bind(minimum_height = self.view.setter('height'))
for i in range(20):
self.view.add_widget(Field('Test field {}'.format(i),i%2 is 0))
class Field(GridLayout):
def __init__(self, name, bg):
assert isinstance(name, str)
assert isinstance(bg, bool)
self.bg = bg
GridLayout.__init__(self,
rows = 1,
padding = 10,
size = (0, 60),
size_hint = (1, None))
self.add_widget(Label(text = name))
self.add_widget(Button(text = 'Test button',
size = (200, 0),
size_hint = (None, 1)))
self.bind(pos = self.change_background)
self.bind(size = self.change_background)
def change_background(self, *args):
with self.canvas.before:
if self.bg:
Color(0.2, 0.2, 0.2, mode = 'rgb')
else:
Color(0.1, 0.1, 0.1, mode = 'rgb')
Rectangle(pos = self.pos, size = self.size)
if __name__ in ('__main__', '__android__'):
app = Main()
app.run()
def change_background(self, *args):
self.canvas.before.clear()#<- clear previous instructions
with self.canvas.before:
if self.bg:
Color(0.2, 0.2, 0.2, mode = 'rgb')
else:
Color(0.1, 0.1, 0.1, mode = 'rgb')
Rectangle(pos = self.pos, size = self.size)
You are adding/piling instructions to the canvas every time the Field's position/size changes, without clearing the previous instructions.
You should also look into using kv as for anything more than a small snippet it ends up saving you a lot of time. You can convert you code using kv like so ::
import kivy
kivy.require('1.0.7')
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scrollview import ScrollView
from kivy.properties import ObjectProperty, BooleanProperty
from kivy.lang import Builder
Builder.load_string('''
<Scroller>
# root is Scroller here
# create a new ObjectProperty in kv that holds the ref to Gridlayout
# so you can access the instance in python code
view: glayout
GridLayout:
id: glayout
cols: 1
size_hint: (1, None)
height: self.minimum_height
<Field>
canvas.before:
Color:
rgba: (0.2, 0.2, 0.2, 1) if self.bg else (0.1, 0.1, 0.1, 1)
Rectangle:
# binding properties is done implicitly and instructions aren't
# piled up while doing that.
pos: self.pos
# self here refers to Field as `self` is supposed to refer to the
# Widget not the drawing instruction
size: self.size
rows: 1
padding: 10
size: (0, 60)
size_hint: (1, None)
Label:
text: root.name
Button:
text: 'test button'
size: (200, 0)
size_hint: (None, 1)
''')
class Main(App):
def build(self):
self.root = GridLayout(rows = 1)
self.root.add_widget(Scroller())
return self.root
class Scroller(ScrollView):
def __init__(self, **kwargs):
super(Scroller, self).__init__(**kwargs)
for i in range(20):
# access self.view that was set in kv
self.view.add_widget(
Field(
name = 'Test field {}'.format(i),
bg = i%2 is 0))
class Field(GridLayout):
# use kivy's Properties so it becomes easier to observe and apply changes
# as a plus these can also be directly used in kv. As a advantage of using this now
# you can change name and bg dynamically and the changes should be reflected on
# screen
name = ObjectProperty('Test field uninitialized')
bg = BooleanProperty(False)
if __name__ in ('__main__', '__android__'):
app = Main()
app.run()