Assume I have this UI adapted from the ScrollView example:
from kivy.app import App
from kivy.uix.slider import Slider
from kivy.uix.label import Label
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
class ScrollViewApp(App):
def build(self):
layout = GridLayout(cols=1, padding=10, spacing=10,
size_hint=(None, None), width=500)
layout.bind(minimum_height=layout.setter('height'))
for i in range(15):
blt = BoxLayout(size_hint=(None, None), size=(480, 40))
blt.add_widget(Label(text="Slider %d" % i))
btn = Slider(value=i, min=0, max=42, size=(400, 40),
size_hint=(None, None))
blt.add_widget(btn)
layout.add_widget(blt)
scroll = ScrollView(size_hint=(None, None), size=(500, 320),
pos_hint={'center_x': .5, 'center_y': .5}, do_scroll_x=False)
scroll.add_widget(layout)
return scroll
if __name__ == '__main__':
ScrollViewApp().run()
Due to scroll_timeout, interactions with the Sliders are delayed. Is it possible to define areas in the ScrollView in which touch events are just passed through to children without the delay (and without initiating a scroll)?
Have a look at Widget touch event bubbling.
I've never had to do the same thing as you but maybe you could create a custom class that inherits ScrollView and override on_touch_down event where you could:
Disable scroll
Call super.on_touch_down
Enable scroll.
Another way might be creating a custom widget that inherits Slider class that the user clicks. Then overload it's on_touch_down method with return True. Documentation says:
In order to stop this event bubbling, one of these methods must return True
ScrollView also fires on_scroll_start event so maybe you could do something similar there.
I came up with a simple override of ScrollView's on_touch_down, which so far seems to satisfy my needs. The idea is to test whether a touch event falls into an 'exclusion zone' (in the example below only the x dimension is checked, but it'd be trivial to extend this to arbitrary rectangular areas). If it does fall into that exclusion zone, the on_touch_down event will be dispatched to the ScrollView's children. If a child captures it, the event is swallowed. In all other cases, super.on_touch_down will be called, i.e. normal scroll behaviour is initiated. That has the benefit of still being able to scroll if the touch does not land on a Slider (in the question's example).
class MSV(ScrollView):
x_exclusion_lower = NumericProperty(None, allownone=True)
x_exclusion_upper = NumericProperty(None, allownone=True)
x_exclusion = ReferenceListProperty(x_exclusion_lower, x_exclusion_upper)
def on_touch_down(self, touch):
pos_in_sv = self.to_local(*touch.pos)
if (self.x_exclusion_lower is not None or self.x_exclusion_upper is not None) and (self.x_exclusion_lower is None or self.x_exclusion_lower <= pos_in_sv[0]) and \
(self.x_exclusion_upper is None or self.x_exclusion_upper >= pos_in_sv[0]):
touch.push()
touch.apply_transform_2d(self.to_local)
if self.dispatch_children('on_touch_down', touch):
return True
touch.pop()
super(MSV, self).on_touch_down(touch)
Related
I'm stuck trying to position widgets in kivy in a FloatLayout using pos_hint.
If the label exists from the beginning, e.g. if I can define the pos_hint in the .kv file, everything works as expected. However, I'm trying to create buttons later on. Using this .kv file (named layouttest.kv):
<NewButton#Button>:
size_hint: (0.1, 0.1)
<BasicFloatLayout#FloatLayout>:
Button:
size_hint: (0.4, 0.2)
pos_hint: {'x': 0.0, 'top': 1.0}
text: 'create Button'
on_release: self.parent.create_Button()
and this python code, I am trying to position newly created blank buttons at a random y-position ranging from 0%-100% of the size of my BasicFloatLayout, and at a random x-position ranging from 0-200px.
If I press the button once, everything behaves as expected. On a second press, the first created button will change its y-position such that it is identical with the newly created button. On a third press, both old buttons will align with the newly created button and so on. The x-positioning will however remain as expected. Can someone please explain what I'm doing wrong here?
(Bonus points if you can help me moving the buttons using the update function and pos_hint)
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.properties import NumericProperty
from kivy.clock import Clock
import random
class BasicFloatLayout(FloatLayout):
timer = NumericProperty(0)
def update(self, *args):
self.timer += 1
for child in self.children:
try: child.update()
except AttributeError:
pass
def create_Button(self):
button = NewButton( (random.random(), random.random()) )
self.add_widget(button)
class NewButton(Button):
def __init__(self, pos, **kwargs):
super(NewButton, self).__init__(**kwargs)
self.pos[0] = 200*pos[0]
self.pos_hint['y'] = pos[1]
class layouttestApp(App):
def build(self):
GUI = BasicFloatLayout()
Clock.schedule_interval(GUI.update, 1/30.)
return GUI
if __name__ == "__main__":
layouttestApp().run()
First, make pos_hint: {'x': 0.0, 'y': 0.5}(because it's hard to get 2 different things work, if you are using x, use y instead of Top, if you are using Top, then use Bottom insead of x)
Second, instead of giving on_release: self.parent.create_Button() in the kv file, do this: on_release: root.create_Button()
Third, you have only assigned for the y value , you should also assign for the x value, the line is self.pos_hint['y'] = pos[1] inside the NewButton class.
But you can make it more simple by doing this:
#from your first class......
def create_Button(self):
button = NewButton(self.pos_hint{'x' : random.random(), 'y' : random.random()}
self.add_widget(button)
class NewButton(Button):
def __init__(self, *kwargs):
pass
Hope this makes some kind of sense, and you can modify it more.
(Note: I haven't wrote the begining part of your main class, I am lazy ;-p)
I'm having issues to get a kivy.DropDown widget to work with a screen manager.
I am using the dropdown code that the kivy documentation provides, and add it to a screen widget, which I then add to a screen manager to display. The following code should reproduce the problem by itself.
import kivy
kivy.require('1.10.1')
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.anchorlayout import AnchorLayout
class MyScreen(Screen):
def __init__(self, **kwargs):
super(MyScreen, self).__init__(**kwargs)
anchor = AnchorLayout()
anchor.anchor_x = "center"
anchor.anchor_y = "center"
anchor.size = self.size
anchor.pos = self.pos
dropdown = DropDown()
for index in range(10):
# When adding widgets, we need to specify the height manually
# (disabling the size_hint_y) so the dropdown can calculate
# the area it needs.
btn = Button(text='Value %d' % index, size_hint_y=None, height=44)
# for each button, attach a callback that will call the select() method
# on the dropdown. We'll pass the text of the button as the data of the
# selection.
btn.bind(on_release=lambda btn: dropdown.select(btn.text))
# then add the button inside the dropdown
dropdown.add_widget(btn)
# create a big main button
mainbutton = Button(text='Hello', size_hint=(None, None))
# show the dropdown menu when the main button is released
# note: all the bind() calls pass the instance of the caller (here, the
# mainbutton instance) as the first argument of the callback (here,
# dropdown.open.).
mainbutton.bind(on_release=dropdown.open)
# one last thing, listen for the selection in the dropdown list and
# assign the data to the button text.
dropdown.bind(on_select=lambda instance, x: setattr(mainbutton, 'text', x))
anchor.add_widget(mainbutton)
self.add_widget(anchor)
sm = ScreenManager() # transition = NoTransition())
sm.add_widget(MyScreen(name='screen'))
class MyApp(App):
def build(self):
return sm
if __name__ == '__main__':
MyApp().run()
Why is it that if put in a screen widget inside a ScreenManager, the dropdown widget does not work? Clarifications are welcome.
PS:
For anybody finding this issue, you can use the spinner widget to have the same functionality already implemented.
I believe your problem is due to garbage collection. The dropdown reference in your __init__() method is not saved (The bind uses a weakref which will not prevent garbage collection). So I think all you need to do is replace your dropdown local variable with a self.dropdown instance variable as:
import kivy
kivy.require('1.10.1')
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.anchorlayout import AnchorLayout
class MyScreen(Screen):
def __init__(self, **kwargs):
super(MyScreen, self).__init__(**kwargs)
anchor = AnchorLayout()
anchor.anchor_x = "center"
anchor.anchor_y = "center"
anchor.size = self.size
anchor.pos = self.pos
self.dropdown = DropDown()
for index in range(10):
# When adding widgets, we need to specify the height manually
# (disabling the size_hint_y) so the dropdown can calculate
# the area it needs.
btn = Button(text='Value %d' % index, size_hint_y=None, height=44)
# for each button, attach a callback that will call the select() method
# on the dropdown. We'll pass the text of the button as the data of the
# selection.
btn.bind(on_release=lambda btn: self.dropdown.select(btn.text))
# then add the button inside the dropdown
self.dropdown.add_widget(btn)
# create a big main button
mainbutton = Button(text='Hello', size_hint=(None, None))
# show the dropdown menu when the main button is released
# note: all the bind() calls pass the instance of the caller (here, the
# mainbutton instance) as the first argument of the callback (here,
# dropdown.open.).
mainbutton.bind(on_release=self.dropdown.open)
# one last thing, listen for the selection in the dropdown list and
# assign the data to the button text.
self.dropdown.bind(on_select=lambda instance, x: setattr(mainbutton, 'text', x))
anchor.add_widget(mainbutton)
self.add_widget(anchor)
sm = ScreenManager() # transition = NoTransition())
sm.add_widget(MyScreen(name='screen'))
class MyApp(App):
def build(self):
return sm
if __name__ == '__main__':
MyApp().run()
I'm looking to make a popup on the python-side that has a dynamic height.
So far, I have this within the screens __init__ class. The kv file has another widget that called the popup on_release. Anyways, I have found that this produces a popup with very wonky formatting:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.screenmanager import Screen, ScreenManager
kv = '''
ScreenManagement:
id: 'manager'
BrokenPopup:
name: 'broken'
manager: 'manager'
<BrokenPopup>:
BoxLayout:
Button:
text: 'Test'
on_release: root.p.open()
'''
class ScreenManagement(ScreenManager):
pass
class BrokenPopup(Screen):
def __init__(self, **kwargs):
super(BrokenPopup,self).__init__(**kwargs)
self.p = Popup(auto_dismiss=False, size_hint_x=.6, size_hint_y=None, title='A popup')
self.g = GridLayout(cols=1, spacing=10)
self.g.add_widget(Button(text='Test1', size_hint_y=None, height=32))
self.g.add_widget(Button(text='Test2', size_hint_y=None, height=32))
self.g.bind(minimum_height=self.g.setter('height'))
self.p.add_widget(self.g)
self.p.bind(height=self.g.setter('height')) #<- this does not work to change the popup height!
class TheApp(App):
def build(self):
return Builder.load_string(kv)
TheApp().run()
The popup size is set to fit only one widget, leaving the second button (and all others that may be included) to float beyond the confines of the popup border.
How should I change the code so that all of the widgets fit within the confines of the popup? I am trying to do that by dynamically setting the height of the popup, however that is not proving effective.
I have modified your code to do what I think you want. Basically it adds the minimum_height from the GridLayout, that is added to your Popup, to the calculated height of the title and the dividing bar. The first Button in the GridLayout now adds another Button to the GridLayout for testing.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.screenmanager import Screen, ScreenManager
kv = '''
ScreenManagement:
id: 'manager'
BrokenPopup:
name: 'broken'
manager: 'manager'
<BrokenPopup>:
BoxLayout:
Button:
text: 'Test'
on_release: root.p.open()
'''
class ScreenManagement(ScreenManager):
pass
class BrokenPopup(Screen):
def __init__(self, **kwargs):
super(BrokenPopup,self).__init__(**kwargs)
self.popup_title_height = None
self.p = Popup(auto_dismiss=False, size_hint_x=.6, size_hint_y=None, title='A popup')
self.g = GridLayout(cols=1, spacing=10)
self.g.bind(minimum_height=self.fix_size)
self.g.add_widget(Button(text='Test1', size_hint_y=None, height=32, on_release=self.add_one))
self.g.add_widget(Button(text='Test2', size_hint_y=None, height=32))
self.p.add_widget(self.g)
def add_one(self, *args):
self.g.add_widget(Button(text='Another', size_hint_y=None, height=32))
def get_popup_title_height(self):
height = 0
popupGrid = self.p.children[0]
height += popupGrid.padding[1] + popupGrid.padding[3]
for child in popupGrid.children:
if isinstance(child, BoxLayout):
continue
else:
height += child.height + popupGrid.spacing[1]
self.popup_title_height = height
def fix_size(self, *args):
if self.popup_title_height is None:
self.get_popup_title_height()
self.p.height = self.g.minimum_height + self.popup_title_height
class TheApp(App):
def build(self):
return Builder.load_string(kv)
TheApp().run()
I cheated a bit by looking at the code for Popup and the style.kv file to see how the Popup is displayed. So, if any of that is changed, this may not work.
I have found a solution for my original problem that is influenced by John Anderson's answer. I'll provide a walkthrough below for how I came to this solution.
1) Here's a photo of my original problem; I needed to dynamically set the popup height based on the widgets that are assigned to it. Before finding the below solution, my popup looked like this with the code in the OP:
As you can see, the widgets go beyond the borders of the popup.
2) I found something interesting while looking inside the popup widget with the inspector tool.
python '/path/to/your/file.py' -m inspector
Using control-e, I can click widgets and inspect their attributes. I clicked the popup button and cycled through the parent widgets until I found the popup widget.
As you can see in the photo, the popup widget has one child: a grid layout. Here are the children of that grid layout:
Interestingly, the grid layout contains:
One label, with a height of 33
One line, with a height of 4
A box layout, which contains the contents of the popup
2 units of spacing between these three widgets
12 units of padding all-around; so 24 additional units to consider for the height
3) In my solution, I have hard-written the default heights of the label, the line widget, and all default popup spacing and padding. Then, I cycle through the children inside the box layout, and add their heights. I also add 10 to those children heights, as the gridlayout that contains all of these widgets uses a spacing of 10.
Solution:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.screenmanager import Screen, ScreenManager
kv = '''
ScreenManagement:
id: 'manager'
BrokenPopup:
name: 'broken'
manager: 'manager'
<BrokenPopup>:
BoxLayout:
Button:
text: 'Test'
on_release: root.p.open()
'''
class ScreenManagement(ScreenManager):
pass
class BrokenPopup(Screen):
def __init__(self, **kwargs):
super(BrokenPopup,self).__init__(**kwargs)
self.p = Popup(auto_dismiss=False, size_hint_x=.6, size_hint_y=None, title='A popup')
self.g = GridLayout(cols=1, spacing=10, padding=[0,10,0,-5])
self.g.bind(minimum_height=self.fix_popup_height) # <- here's the magic
self.g.add_widget(Button(text='Test1', size_hint_y=None, height=32))
self.g.add_widget(Button(text='Test2', size_hint_y=None, height=32))
self.p.add_widget(self.g)
def fix_popup_height(self, grid, *args):
# a generalized function that, when bound to minimum_height for a grid with popup widgets,
# this will set the height of the popup
height = 0
height += 33 # for popup label
height += 4 # for popup line widget
height += 24 # for popup padding
height += 2 # for spacing between main popup widgets
for child in grid.children:
height += child.height + 10 # adds 10 for the spacing between each child
grid.parent.parent.parent.height = height # sets popup height
pass
class TheApp(App):
def build(self):
return Builder.load_string(kv)
TheApp().run()
Notable changes from the OP:
Bind the minimum_height of the widget container to the fix_popup_height() function; this will trigger each time a widget is added to the popup.
Declare the fix_popup_height() within the screen class.
Here's the fixed popup:
again! I'm trying to add a character counter to my TextInput widget, but I don't know what parameters to pass for the four arguments, or much on how they're supposed to function. I checked the documentation, but it was putting me further into the woods. Anyway, here are the relevant snippets.
def charsLeft(window, keycode, text, modifiers):
# Do some magic, pass some parameters, and then...
ansLen.text = str(len(hidden.text) - len(answer.text))
And here's the code for my layout:
ansLen = Label(bold=True, halign="center", size_hint=(.2, .5), text_size=self.size, valign="middle")
answer = TextInput(id="sbt", multiline=False, size_hint=(.8, .5), text="")
answer.bind(keyboard_on_key_down=charsLeft)
I figure since it's on virtually every website, it ought to be fairly straightforward. I just don't know what I don't know here.
if you want to set a text counter you do not need to use keyboard_on_key_down, you just need to catch the text change for them we use bind, then we can use a lambda function to update the values since the bind returns the instance and the property changed, to set the value we use setattr:
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.uix.boxlayout import BoxLayout
class MyApp(App):
def build(self):
layout = BoxLayout(orientation='vertical')
answer = TextInput(multiline=False, text="", size_hint=(1, 0.5))
ansLen = Label(bold=True, halign="center", text="", size_hint=(1, 0.5))
answer.bind(text=lambda instance, text: setattr(ansLen, "text", str(len(text))))
layout.add_widget(answer)
layout.add_widget(ansLen)
return layout
if __name__ == '__main__':
MyApp().run()
I am trying to construct a widget in the middle of the screen. I am not using pos_hint or size_hint because I will be altering the widget's position later but when I construct the widget, its size and position is not correct. Here is my code:
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.widget import Widget
from kivy.graphics import *
from kivy.clock import Clock
class Sprite(Widget):
def __init__(self, **kwargs):
super(Sprite, self).__init__(**kwargs)
Clock.schedule_interval(self.update_canvas, 1.0/60)
def update_canvas(self, dt):
with self.canvas:
Rectangle(size=self.size, pos=self.pos)
class RootWidget(FloatLayout):
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
self.add_widget(Sprite(
size=(Window.width*0.1, Window.height*0.1),
center=(Window.width*0.5, Window.height*0.5)
))
class MyApp(App):
def build(self):
app = RootWidget()
return app
if __name__=="__main__":
MyApp().run()
Why is the widget's size not equal to 1/10th of the window size and why is it's center at the top right corner of the window?
The size hint defaults to a value unless you define it explicitly and it takes priority over the size property. You may not want to want to use the size hint functionality but unless you disable it, the size that you set manually will be overridden by default. Try adding the following modification and compare the result with and without the size_hint line:
self.add_widget(Sprite(
size_hint=(None,None),
size=(Window.width*0.1, Window.height*0.1),
center=(Window.width*0.3, Window.height*0.5)
))
You can see the actual dimensions set by using the inspector: python myscript.py -m inspector and hitting control+E in the kivy window
reference: http://kivy.org/docs/api-kivy.modules.inspector.html
center is an alias for (x + width/.2, y + height/.2) but the width and height default to 100. At the time of creation these default measurements are what are used for the reverse calculation to set x and y when you make your call so if your height and width are different from 100 then you'll be offset by the difference. Use x and y directly (i.e. pos=(..)) when instantiating, not center.