Key Error when binding minimum_height to boxlayout in scrollview - python

How do I bind minimum_height to a BoxLayout in a ScrollView on the python side?
This is where I am at:
class BrokenScreen(screen):
def __init__(self, **kwargs):
super(BrokenScreen,self).__init__(**kwargs)
# build widgets
self.pop = Popup(auto_dismiss=False,size=(.8,.8))
self.box = BoxLayout(orientation='vertical', size_hint_y=None)
self.scroll = ScrollView()
# bind height, this is the line bringing about the error
self.box.bind(minimum_height=self.box.setter('height'))
# integrate
self.scroll.add_widget(self.box)
self.pop.add_widget(self.scroll)
When I attempt to compile, I receive the following error:
KeyError: 'minimum_height'
What do?

BoxLayout does have an attribute named minimum_height.
Which is automatically computed minimum height needed to contain all children.
Therefore, you are not suppose to modify minimum_height in BoxLayout
To prevent such modification, Kivy has made it as read only.
That is the reason why you have no access to modify it.
Best way to prove it is to open up boxlayout.py inside of your Kivy folder.
minimum_height = NumericProperty(0)
'''Automatically computed minimum height needed to contain all children.
.. versionadded:: 1.10.0
:attr:`minimum_height` is a :class:`~kivy.properties.NumericProperty` and
defaults to 0. It is read only.
'''

Miket25 asked if box has the minimum_height argument.
I was able to resolve my problem by using a GridLayout instead. So, probably not (EDIT: I was wrong about whether or not box has minimum_height, see the accepted response).
# build widgets
self.pop = Popup(auto_dismiss=False,size=(.8,.8))
self.grid = GridLayout(cols=1, size_hint_y=None) #<- change this
self.scroll = ScrollView(size=self.size)
# bind height, this is the line bringing about the error
self.grid.bind(minimum_height=self.grid.setter('height')) # only changed var name
# integrate
self.scroll.add_widget(self.grid) # changed var name
self.pop.add_widget(self.scroll)

Related

Kivy ScrollView code error with scrollbars (or bug?)

I'm playing around with a Kivy Scrollview, adding scrollbars, etc, and getting a strange crash. I don't specifically think it's a bug, it's probably some configuration element on Scrollviews that I'm missing, but who knows?
Given this code:
"""
Source: https://stackoverflow.com/questions/35626320/kivy-image-scrolling
"""
from kivy.app import App
from kivy.uix.image import Image
from kivy.uix.scrollview import ScrollView
from kivy.core.window import Window
class TutorialApp(App):
def build(self):
some_img = Image(source='/home/data/map/Map_07C.jpg', size_hint=(None, None),
keep_ratio=True, size=(Window.width * 2, Window.height * 2))
sv = ScrollView(size=Window.size, bar_width=50,
scroll_type=['bars', 'content'], effect_cls='ScrollEffect')
sv.add_widget(some_img)
return sv
if __name__ == "__main__":
TutorialApp().run()
if I click or touch the Scrollbars in any way, I get this error:
File "kivy_env/lib/python3.8/site-packages/kivy/uix/scrollview.py", line 908, in on_scroll_move
self.effect_x.update(touch.x)
File "kivy_env/lib/python3.8/site-packages/kivy/effects/scroll.py", line 116, in update
self.displacement += abs(val - self.history[-1][1])
IndexError: list index out of range
However - if I first click the bitmap being scrolled, I can use the scrollbars with no problem.
So what's up? Is there some Scrollview configuration I'm missing? (It took me a while to even find the scroll_type option to enable the bars, at first I could only mouse-drag the bitmap). Or is it a bug - given that it's referencing history[-1], maybe that doesn't exist yet?
Yep, it's a bug. Just searched the Kivy repo on Github, found:
Effects Scroll Exception
The link does have a "workaround" patch, that you can manually apply to the installed library. Just tested the patch, it fixes my problem. Basically puts a try/except/return block around the line with history[-1]
technically is a bug but we can note from the desktop and mobile apps that they use different scroll_type=['bars', 'content'] in the desktop app we use bars
and in the mobile app we use content so the bug only occurs when we use two types of scroll_type so we can say that the scrollview does not design to use two type of scroll_type in the same time
Another workaround is calling a function on_touch_down that checks the mouse's x-position and changes the scroll type to either only ['Bars'] or only ['Content'] accordingly. Note, I set mine to check Window.width - 12 as that is the width of scroll bar that I use. Default is 2.
# tutorial.kv
<SV>:
on_touch_down: self.check_pos()
bar_width: 12
# main.py
from kivy.uix.scrollview import ScrollView
class SV(ScrollView):
def check_pos(self):
if Window.mouse_pos[0] <= (Window.width - 12):
self.scroll_type = ['content']
else:
self.scroll_type = ['bars']

How to remove a kivy child that are made dynamically?

I create a sample kivy script that dynamically adding button to the widget then remove it automatically using clock.schedule_once function.
I was able to referred to the widget children to change the button text. However when i try to remove the widget itself, it did not work. Instead it gave me RecursionError. Please help me out on how to remove the widget. I added some comment that causes the error in the script :
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.properties import ObjectProperty
class nbutton(Button):
buttonid=ObjectProperty(None)
class MyWidget(BoxLayout):
childs = []
def __init__(self,*args):
Clock.schedule_once(self.add_widgets,1)
Clock.schedule_once(self.remove_widget,2)
return super().__init__(*args)
def add_widgets(self,*args):
#self.r = BoxLayout(size_hint=(1,1),id="nice")
#self.add_widget(self.r)
self.add_widget(Button(text="Hello",id='dasd'))
self.add_widget(Button(text="World",id='q'))
self.add_widget(Button(text="nice",id='dz'))
self.add_widget(Button(text="good",id='m'))
def remove_widget(self,*args):
#m = App.get_running_app().root.ids.nice
#self.remove_widget(self.children.ids.dasd)
#self.clear_widgets()
#for child in [child for child in self.children if child.text != "Hello"]:
#self.remove_widget(child)
#print(child)
#self.remove_widget(self.child.m)
for chi in self.children:
self.childs.append(chi)
print(chi)
#print(self.childs.text)
#try:
self.childs[2].text = 'hihih'
#------------------------ this part is broken ------------------------------
self.remove_widget(self.childs[2])
#------------------------ this part is broken ------------------------------
# error message:
# RecursionError: maximum recursion depth exceeded while calling a Python object
# except:
# pass
# for c in childs:
# self.remove_widget(c)
#for child in self.children:
#self.remove(child)
#print(child.id)
# if child.text == 'Hello':
# #print(child)
# self.remove_widget(child)
#pass
class MpApp(App):
def build(self):
return MyWidget()
if __name__ == '__main__':
MpApp().run()```
Within your remove_widget() method, you are recursing into remove_widget() with the call:
self.remove_widget(self.childs[2])
If you are going to recurse like that, you need some test to end the recursion, otherwise it will recurse infinitely.
Also, you are redefining the remove_widget() method of MyWidget (which is a BoxLayout) without ever calling the actual remove_widget() method of the superclass (BoxLayout). So the child widget never actually gets removed. Perhaps naming your remove_widget() in MyWidget to something like my_remove_widget() might help.

Center of Canvas in Python vs kivy language

I have made an easy app where i try to show my issue. When using python to draw a line in kivy (using the with self.canvas method) the line gets drawn from a center_x and center_y of 50.
Using kivy Lang draws the line correctly in the center. Here is a simple code example:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Line
class TestWidget(Widget):
def draw(self):
with self.canvas:
Line(points=[self.center_x,self.center_y,self.center_x,self.center_y+100], width=2)
class TestApp(App):
def build(self):
test = TestWidget()
test.draw()
return test
if __name__ == '__main__':
TestApp().run()
and the corresponding test.kv file:
#:kivy 1.11.1
<TestWidget>:
canvas:
Line:
width: 5
points: (self.center_x,self.center_y,self.center_x,self.center_y+100)
result is like this:
Any idea why using python code is not working?
Both codes are not equivalent, in the case of python you are only establishing that the position of the line is based on the center of the widget at that time, that is, at the beginning, instead in .kv it is indicating that the line position is always it will be with respect to the center of the Widget.
TL; DR;
Explanation:
In kv the binding is a natural and implicit process that in python is not doing it, besides that its implementation is simpler, for example the following code:
Foo:
property_i: other_object.property_j
In that case its equivalent in python is:
foo = ...
other_object = ...
other_object.bind(property_j=foo.setter("property_i"))
And it is not:
foo.property_i = other_object.property_j
Since with the bind you are indicating before a change of property_j update property_i with that value, but in the last code you only indicate at this moment copy the value of property_j in property_i.
In your particular case, the center you take is before displaying the widget and kivy taking default configurations and other properties into consideration, changes the geometry of the widget after it is displayed.
Solution
Making the necessary modifications taking into account the previous explanation the following code is an equivalent of the .kv code
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Line
class TestWidget(Widget):
def __init__(self, **kwargs):
super(TestWidget, self).__init__(**kwargs)
with self.canvas:
self.line = Line(width=2)
self.bind(center=lambda *args: self.draw())
def draw(self):
self.line.points = [
self.center_x,
self.center_y,
self.center_x,
self.center_y + 100,
]
class TestApp(App):
def build(self):
test = TestWidget()
return test
if __name__ == "__main__":
TestApp().run()
With kv language your declarations automatically update when their dependencies change. That means that when the widget's centre goes from (50, 50) to something else, the line is redrawn in the new location.
Your Python code doesn't do this, it just draws the line once using whatever its centre is at the moment the code runs. That value is (50, 50), since that's the default before positioning has taken place.
The solution is to write code in Python that updates the line when the widget centre changes, something along the lines of declaring the line with self.line = Line(...) and self.bind(centre=some_function_that_updates_self_dot_line).

Extending the Kivy ScrollView contents down

I have a FloatLayout as a child of a ScrollView with size_hint_y set to None. I want to be able to extend it as I add more and more content. The problem is that since Kivy's coordinate system starts at the bottom-left, when I add to the FloatLayout height, all the content stays at the bottom. Can I somehow make it extend down? Because I don't think that moving all widgets up is efficient, especially if there's a lot of them and I need to handle the position of all children as well.
Here is a snippet that explains the problematic behaviour:
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.scrollview import ScrollView
from kivy.uix.label import Label
class TestApp(App):
def extend_h(self, *args):
global msg_float
msg_float.height += 50
def build(self):
global msg_float
msg_float = FloatLayout(size_hint_y = None)
bt1_main = Button(on_press = self.extend_h)
bl = BoxLayout()
sc = ScrollView()
sc.add_widget(msg_float)
bl.add_widget(sc)
bl.add_widget(bt1_main)
lb = Label(text = "Test",
size=(100,200),
size_hint = (None, None))
msg_float.add_widget(lb)
return bl
TestApp().run()
With a press of a button, the view extends and the "Test" label stays at the bottom, but I'd want it to stay on top.
You could use a relative layout instead of a float layout to fix the coords, but instead you should just omit using any of these, and add labels to a grid layout. Check examples at kivy repo:
https://github.com/kivy/kivy/blob/master/examples/widgets/scrollview.py
https://github.com/kivy/kivy/blob/master/examples/widgets/scrollview.kv

Using and moving Widgets/Buttons in Kivy

I'm just starting off with Kivy and it's different to what I'm used to, apologies if I'm making stupid mistakes!
Right now I'm trying to create an app that does the following:
Allows creation of Nodes (ellipses for now).
Allows the user to position the nodes by dragging.
Allows the user to connect the nodes with a line.
So far I've achieved the first, and the second somewhat.
Right now my dragging is not working too well. If I move the mouse too quickly it cancels the move method (as it is no longer in contact). Is there a better way to produce dragging or do I just increase the refresh rate (if so how?).
def on_touch_move(self, touch):
if self.collide_point(touch.x, touch.y):
self.pos=[touch.x-25, touch.y-25]
I've tried using Buttons instead, using the on_press method to track the moving better. However now I'm having difficulty updating the position of the button (mostly just syntax).
class GraphNode(Button):
background_disabled_down=1
background_disabled_normal=1
def moveNode(self):
with touch:
self.pos=[touch.x-25, touch.y-25]
I have no idea how to use the touch value, and keep getting an array of errors. (Obviously the current attempt doesn't work, I just thought it was funny).
As you could probably tell, I also don't know how to get rid of the button graphics, as I want to use the ellipse. As an added bonus if someone could show me how to change the colour of the ellipse on button press that would be cool!
The kv file:
<GraphNode>:
size: 50, 50
canvas:
Ellipse:
pos: self.pos
size: self.size
on_press:
root.moveNode()
I want to be able to use the touch information to update the position, but don't know how to implement it here.
Full core python code:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.properties import NumericProperty, ReferenceListProperty,\
ObjectProperty
from kivy.graphics import Color, Ellipse, Line
class GraphInterface(Widget):
node = ObjectProperty(None)
class GraphApp(App):
def build(self):
node = GraphNode()
game = GraphInterface()
createNodeButton = Button(text = 'CreateNode', pos=(100,0))
createEdgeButton = Button(text = 'CreateEdge')
game.add_widget(createNodeButton)
game.add_widget(createEdgeButton)
def createNode(instance):
game.add_widget(GraphNode())
print "Node Created"
def createEdge(instance):
game.add_widget(GraphEdge())
print "Edge Created"
createNodeButton.bind(on_press=createNode)
createEdgeButton.bind(on_press=createEdge)
return game
class GraphNode(Button):
def moveNode(self):
with touch:
self.pos=[touch.x-25, touch.y-25]
#def onTouchMove(self, touch):
# if self.collide_point(touch.x, touch.y):
# self.pos=[touch.x-25, touch.y-25]
pass
class GraphEdge(Widget):
def __init__(self, **kwargs):
super(GraphEdge, self).__init__(**kwargs)
with self.canvas:
Line(points=[100, 100, 200, 100, 100, 200], width=1)
pass
if __name__ == '__main__':
GraphApp().run()
If you need any other info, or anything is unclear, please let me know!
Edit: Second question moved to: Creating a dynamically drawn line in Kivy.
First, you should read up on touch events in Kivy. Of particular interest here is the section on grabbing touch events. Basically, you can "grab" a touch to ensure that the grabbing widget will always receive further events from that touch - in other words, the on_touch_move and on_touch_up following that touch event will be sent to your widget.
Basic example:
class MyWidget(Widget):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
touch.grab(self)
# do whatever else here
def on_touch_move(self, touch):
if touch.grab_current is self:
# now we only handle moves which we have grabbed
def on_touch_up(self, touch):
if touch.grab_current is self:
touch.ungrab(self)
# and finish up here
But, even better! If you want to be able to drag widgets around, Kivy already has that: Scatter.
Just wrap your widget in a Scatter and you can drag it around. You can also use multitouch to rotate and scale a Scatter, but you can easily disable that (kv):
FloatLayout:
Scatter:
do_scale: False
do_rotation: False
MyWidget:
Side note - this is incorrect:
class GraphNode(Button):
background_disabled_down=1
background_disabled_normal=1
background_disabled_down and background_disabled_normal are Kivy properties - you should set those values in __init__.
Force these values:
class GraphNode(Button):
def __init__(self, **kwargs):
super(GraphNode, self).__init__(background_disabled_down='',
background_disabled_normal='', **kwargs)
Suggest these values (better option):
class GraphNode(Button):
def __init__(self, **kwargs):
kwargs.setdefault('background_disabled_down', '')
kwargs.setdefault('background_disabled_normal', '')
super(GraphNode, self).__init__(**kwargs)
Finally, note that these properties are filenames pointing to the images used for the disabled Button. If you remove these values, and disable your button, it will draw no background whatsoever.

Categories