Kivy widget out of expected boundaries - python

I'm making custom touch handling for my widget. The way I have it set up is that the initial size of the widget is set and fixed, however, later, I change it dynamically depending on the contents. I turned that feature down for now to shorten the code, it doesn't affect the problem. I've noticed something strange when I clicked at a blank spot to the right of the widget and the touch was registered, so I added a canvas background to inspect. Apparently, the actual widget boundaries are set much bigger than what the actual widget instance is, and I don't really know what's causing this. Those widgets are positioned in a FloatLayout, but I never set size_hints, only sizes. Could that be the case? How do I shorten the widget boundaries to what they're supposed to be?
Here is the entire source code, the problem is most likely in the kv definition:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen, ScreenManager, NoTransition
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.scrollview import ScrollView
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.textinput import TextInput
global pr_msg_y
pr_msg_y = 5
Builder.load_string('''
<ScrollView>:
canvas:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
<Message>:
width: 500
canvas:
Color:
rgba: 1, 0, 0, 0.3
Rectangle:
pos: self.pos
size: self.size
BoxLayout:
pos: root.pos
height: self.height
canvas:
Color:
rgba: 0.6, 0.6, 0.6, 1
Rectangle:
pos: root.pos
size: self.size
TextInput:
pos: root.pos
size: root.size
id: msg
background_color: 0, 0, 0, 0
text: str(msg)
''')
class Message(Widget):
def __init__(self, **kwargs):
super(Message, self).__init__(**kwargs)
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
print("Touch within {}".format(self))
class Chat(Screen):
pass
class MessageInput(TextInput):
def __init__(self, **kwargs):
super(MessageInput, self).__init__(**kwargs)
class ChatApp(App):
def build(self):
def msg_in(btn):
global pr_msg_y
inst = Message()
inst.ids['msg'].text = "test" * 15
inst.width = 456 # something random, the sizing should be dynamic
inst.height = 40
for i in inst.walk():
i.height = inst.height
i.width = inst.width
inst.y = sv1_main.height - 5 - pr_msg_y - inst.height
msg_float.add_widget(inst)
pr_msg_y += inst.height + 5
Screens = ScreenManager(transition = NoTransition())
chat = Chat(name = "main")
sv1_main = ScrollView(pos_hint = {"top":0.87, "center_x":0.5},
size_hint = (0.97, 0.65))
msg_float = FloatLayout()
bt1_main = Button(pos_hint = {"top":0.097, "center_x":0.951},
on_press = msg_in)
Screens.add_widget(chat)
chat.add_widget(sv1_main)
sv1_main.add_widget(msg_float)
chat.add_widget(bt1_main)
return Screens
ChatApp().run()

size_hint does "overwrite" size. By default size_hint is set to 1,1, so if you only set size and you put the widget into a layout that recognizes size_hints, the widget will get the maximal available size.
Set size_hint = None, None to fix.

Related

cannot put the Button in center position by widget class initialziation

I am trying to initialize the widget class by placing a button at the root.center. Strangely, root.size during initialization is only (100,100). When it's rendered, the actual size is (600,800). So my button is placed in the left bottom instead of in the center.
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import NumericProperty, ReferenceListProperty
from kivy.uix.widget import Widget
w = Builder.load_string('''
<MyWidget>:
canvas.before:
Color:
rgba: 1, 0.3, 0.3, 1
Rectangle:
size: self.size
pos: self.pos
Button:
id: score_button
center: root.score_button_center
text:'Best Scores'
''')
class MyWidget(Widget):
score_center_y = NumericProperty(-100)
score_center_x = NumericProperty(-100)
score_button_center = ReferenceListProperty(score_center_x, score_center_y)
hide_center = (-100, -100)
def __init__(self, **kwargs):
super(MyWidget, self).__init__(**kwargs)
self.score_button_center = self.width, self.height
class MyApp(App):
def build(self):
g = MyWidget()
return g
if __name__ == '__main__':
MyApp().run()
Just change:
center: root.score_button_center
to:
center: root.center
That will take advantage of the kivy feature that sets up bindings to adjust the Button position whenever the root widget changes size or position.

How to create a custom widget in kivy-python

I am trying to make a custom button with border and rounded corners using kivy and I'm new to kivy and python (and its object oriented concepts).
I have partially implemented what I want but whenever I try to increase/decrease the border size or corner radius using an event like on_press it happens but as soon as I resize the window, the border_size/corner_radius becomes to what it was initialized.
Here is my code:
from kivy.uix.button import Button
from kivy.properties import ListProperty
from kivy.properties import NumericProperty
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.app import App
class CustomButton(Button):
border_color = ListProperty([1, 1, 1, 1])
corner_radius = NumericProperty(0)
bg_color = ListProperty([0, 0, 0, 1])
border_width = NumericProperty(0)
Builder.load_string("""
<CustomButton#Button>:
# should I define border_color, corner_radius, bg_color and border_width
# here or within my python script like I did here?
background_color: 0, 0, 0, 0
background_normal: ''
background_down: ''
canvas.before:
Color:
rgba: self.border_color
RoundedRectangle:
size: self.size
pos: self.pos
radius: [self.corner_radius]
Color:
rgba: self.bg_color
RoundedRectangle:
size: [i - 2 * self.border_width for i in self.size]
pos: [i + self.border_width for i in self.pos]
radius: [self.corner_radius - self.border_width if self.corner_radius != 0 else 0]
""")
class RootWidget(FloatLayout):
pass
Builder.load_string("""
<RootWidget>:
CustomButton:
size_hint: 0.2, 0.1
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
text: 'HELLO'
border_width: 0.1 * self.height
on_press:
self.border_width = 0.2 * self.height
""")
class TestApp(App):
def build(self):
return RootWidget()
TestApp().run()
Result:-
After running the script
After pressing the button
After resizing the window
I have tried this code snippet in a separate .kv file as well but I'm getting the same result.
Also I have tried to implement border using Line rounded_rectangle but its kinda bleeding.
Thank you for your time and efforts.
The line in your kv:
border_width: 0.1 * self.height
will reset the border_width whenever the height of the CustomButton changes. So when you resize the window, the above code is activated.
You can avoid this problem by making the 0.1 or 0.2 values into Properties like this:
bw_ratio: 0.1
border_width: self.bw_ratio * self.height
on_press:
self.bw_ratio = 0.2
Another option is to define an on_size() method for CustomButton:
def on_size(self, instance, new_size):
self.border_width = 0.2 * new_size[1]
And remove the border_width stuff from the 'kv'.
Then you can do anything that you want for setting border_width in the new method. Just don't do anything in that method that changes the size of the CustomButton or you will cause an infinite loop.

How to Periodically Update Parent (Screen) Class UI from Child (BoxLayout) Class (Python with Kivy)

Goal:
Periodic update of parent (screen) class / UI from child (boxlayout) class. Theconf2.dat is occasionally updated (from various other screens), and I want the UI to update every 5 seconds or so by re-running this class.
Latest code update:
In the __init__ function, I have Clock.schedule_interval(self.create_button, 1), which should cause the create_button function to rerun every second.
At the top of the create_button function, I have self.box_share.clear_widgets(), which should clear all the widgets so they can be repopulated (per the instructions outlined further down the create_button function).
Action:
I run the code
I navigate to NoTags screen by clicking the button with text title 'updating sequence'
I make changes to buttons that were dynamically created under scrollview by clicking on them. They successfully change color. This information is written to the conf2.dat file.
I navigate to SequenceScreen screen by first clicking 'home' button, then clicking 'sequence display' button. This SequenceScreen screen is the screen I wish to have updated to reflect the changes made to conf2.dat file.
Result:
UI associated withSequenceScreen(Screen) class still does not update per changes made from UI associated with NoTags(Screen) class.
However, when I restart the app altogether, I find the SequenceScreen UI successfully updated.
Suspicion:
I'm just one line of code away from getting this UI to update properly.
Python Code:
from kivy.app import App
# kivy.require("1.10.0")
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
from kivy.uix.textinput import TextInput
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.properties import NumericProperty
from kivy.clock import Clock
from kivy.uix.widget import Widget
from kivy.uix.scrollview import ScrollView
#from kivy.base import runTouchApp
from kivy.properties import StringProperty, ObjectProperty, NumericProperty
from kivy.storage.dictstore import DictStore
import pickle
import datetime, threading
import time
from kivy.clock import mainthread
class BackHomeWidget(Widget):
pass
class SequenceBoxLayout_NoEdits(BoxLayout):
box_share = ObjectProperty()
config_file = DictStore('conf2.dat')
def __init__(self, **kwargs):
super(SequenceBoxLayout_NoEdits, self).__init__(**kwargs)
self.orientation = "vertical"
Clock.schedule_interval(self.create_button, 1)
def create_button(self, *args):
self.box_share.clear_widgets()
top_button_share = 1.1
color = (.4, .4, .4, 1)
for i in range(25):
top_button_share -= .4
id_ = "part" + str(i + 1)
if self.config_file.exists(id_):
btn_color = self.config_file[id_]["background_color"]
else:
self.config_file.put(id_, background_color=color)
btn_color = color
button_share = Button(background_normal='',
background_color=btn_color,
id=id_,
pos_hint={"x": 0, "top": top_button_share},
size_hint_y=None,
height=60,
font_size = 30,
text= str(i+1)
self.box_share.add_widget(button_share)
#Clock.schedule_interval(self.parent.ids.updatedisplay.create_button(self, *args) , 1)
#self.parent.ids.updatedisplay.create_button(self, *args)
class SequenceBoxLayout_NoTag(BoxLayout):
box_share = ObjectProperty()
config_file = DictStore('conf2.dat')
def __init__(self, **kwargs):
super(SequenceBoxLayout_NoTag, self).__init__(**kwargs)
self.orientation = "vertical"
Clock.schedule_once(self.create_button)
def create_button(self, *args):
df = pd.read_excel("Test.xlsx","Sheet1")
parts = df['parts'].values.tolist()
top_button_share = 1.1
color = (.4, .4, .4, 1)
for i in range(len(parts)):
top_button_share -= .4
id_ = "part" + str(i + 1)
if self.config_file.exists(id_):
btn_color = self.config_file[id_]["background_color"]
else:
self.config_file.put(id_, background_color=color)
btn_color = color
button_share = Button(background_normal='',
background_color=btn_color,
id=id_,
pos_hint={"x": 0, "top": top_button_share},
size_hint_y=None,
height=60,
font_size = 30,
text= str(i+1)+ ". " + str(parts[i]))
if self.parent.name == 'notags':
button_share.bind(on_press=self.update_buttons_notag)
self.box_share.add_widget(button_share)
def update_buttons_notag(self, button):
button.background_color = 0.86,0.54,0.04,1
self.config_file.put(button.id, background_color=(0.86,0.54,0.04,1))
class MainScreen(Screen):
pass
class AnotherScreen(Screen):
pass
class SequenceScreen(Screen):
pass
class NoTags(Screen):
pass
class ScreenManagement(ScreenManager):
pass
presentation = Builder.load_file("updatelistexample.kv")
class MainApp(App):
def build(self):
return presentation
if __name__ == "__main__":
MainApp().run()
KV Code:
#: import FadeTransition kivy.uix.screenmanager.FadeTransition
ScreenManagement:
transition: FadeTransition()
MainScreen:
AnotherScreen:
NoTags:
SequenceScreen:
<SmallNavButton#Button>:
font_size: 32
size: 125, 50
color: 0,1,0,1
<BigButton#Button>:
font_size: 40
size_hint: 0.5, 0.15
color: 0,1,0,1
<BackHomeWidget>:
SmallNavButton:
on_release: app.root.current = "main"
text: "Home"
pos: root.x, root.top - self.height
<MainScreen>:
name: "main"
FloatLayout:
BigButton:
on_release: app.root.current = "notags"
text: "updating sequence"
pos_hint: {"x":0.25, "top": 0.4}
BigButton:
on_release: app.root.current = "sequence"
text: "sequence display"
pos_hint: {"x":0.25, "top": 0.7}
<AnotherScreen>:
name: "newgarage"
<NoTags>:
name: "notags"
SequenceBoxLayout_NoTag:
BackHomeWidget:
FloatLayout:
BigButton:
text: "Select Parts w/o Tags"
pos_hint: {"x":0.5, "top": 0.6}
background_normal: ''
background_color: (0.4,0.4,0.4,1)
<SequenceBoxLayout_NoEdits>:
box_share: box_share
ScrollView:
GridLayout:
id: box_share
cols: 1
size_hint_y: None
size_hint_x: 0.5
spacing: 5
padding: 130
height: self.minimum_height
canvas:
Color:
rgb: 0, 0, 0
Rectangle:
pos: self.pos
size: self.size
<SequenceBoxLayout_NoTag>:
box_share: box_share
ScrollView:
GridLayout:
id: box_share
cols: 1
size_hint_y: None
size_hint_x: 0.5
spacing: 5
padding: 130
height: self.minimum_height
canvas:
Color:
rgb: 0, 0, 0
Rectangle:
pos: self.pos
size: self.size
<SequenceScreen>:
name: "sequence"
SequenceBoxLayout_NoEdits:
id: updatedisplay
BackHomeWidget:
Credit:
Based on advice provided by #Tshirtman in the comments thread of the posted question...
Summary:
The problem with the code had to do with the fact that I had two different DictStore pointing to the same file, which was tripping up the communication between both classes.
The solution was to instead use only one DictStore and define that variable in the App class, then reference that particular variable in the child classes [using App.get_running_app()], like so:
Define config_file in App class:
class MainApp(App):
config_file = DictStore('conf2.dat')
def build(self):
return presentation
Reference App variable in child classes:
class SequenceBoxLayout_NoEdits(BoxLayout):
...
def __init__(self, **kwargs):
super(SequenceBoxLayout_NoEdits, self).__init__(**kwargs)
self.orientation = "vertical"
Clock.schedule_interval(self.create_button, 1)
def create_button(self, *args):
self.box_share.clear_widgets()
app = App.get_running_app()
...
for i in range(len(parts)):
...
if app.config_file.exists(id_):
btn_color = app.config_file[id_]["background_color"]
else:
app.config_file.put(id_, background_color=color)
...
...
class SequenceBoxLayout_NoTag(BoxLayout):
...
def __init__(self, **kwargs):
super(SequenceBoxLayout_NoTag, self).__init__(**kwargs)
self.orientation = "vertical"
Clock.schedule_once(self.create_button)
def create_button(self, *args):
...
app = App.get_running_app()
...
for i in range(len(parts)):
...
if app.config_file.exists(id_):
btn_color = app.config_file[id_]["background_color"]
else:
app.config_file.put(id_, background_color=color)
...
...
def update_buttons_notag(self, button):
app = App.get_running_app()
...
app.config_file.put(button.id, background_color=(0.86,0.54,0.04,1))
Clock has a schedule_interval method, which works much like the schedule_once one, but will be called every n seconds instead of just once after n seconds.
So you could certainly just change that call in __init__ and start the create_button by calling self.box_share.clear_widgets() to remove widgets from the previous call before re-creating them.
This might be a bit wasteful, if you find yourself recreating a lot of widgets even if nothing changed, so you could add some logic to first check if the data didn't change, or even if it did, just reuse the old buttons if possible.
box = self.box_share
old_buttons = box.children[:]
box.clear_widgets()
# [your logic about computing the list of parts]
for i, p in enumerate(parts): # this is much better than doing range(len(parts))
# [the logic to get the content of the button]
if old_buttons: # check there are still buttons in the list of old buttons
btn = old_buttons.pop()
else:
btn = Button(
background_normal='',
background_color=btn_color,
pos_hint={"x": 0, "top": top_button_share},
# etc, all the things that are common to all your buttons
# but really, hardcoding them like this is a bit painful,
# you should use a subclass of button so you can style it
# in a kv rule so it would apply to all of them directly
)
btn.id=id_
btn.text = "{}. {}".format(i+1, p)
btn.pos_hint["top"] = top_button_share
# re-apply any other property that might have changed for this part
box.add_widget(btn)
But this logic is quite a common one, actually, and there are other things you can do to improve things in even more situations, though that's quite some work.
Thankfully, you are not the first one to need such thing, and we have been blessed with the wonderful RecycleView, which automates all of this and more, you just need to feed it a data directory, and it'll create the necessary widgets to fill the visible part of the scrollview if there is enough widgets to warrant scrolling, and automatically update when data changes, and when you scroll to see different parts of the list. I encourage you to check it, yourself. but the end result would certainly be something like.
<PartButton#Button>:
id_: None
part: ''
text: '{}. {}'.format(self.id, self.part)
<SequencedBoxLayout#Recycleview>:
parts: self.get_parts()
viewclass: 'PartButton'
data:
[
{
id_: i,
part: part
} for i, p in enumerate(self.parts or [])
]
RecycleBoxLayout:

Kivy Bubble arrow blur

I created a Bubble widget around a custom widget to display some options, but the arrow of that Bubble turned out unexpectedly big and blurry. All those widgets are positioned in a GridLayout that is put inside of a ScrollView. What could cause this issue?
Here is the code:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
from kivy.uix.bubble import Bubble
from kivy.properties import ListProperty
Builder.load_string('''
<PopupBubble>:
size_hint: None, None
<ScrollView>:
canvas:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
<Message>:
width: 500
BoxLayout:
pos: root.pos
height: self.height
canvas:
Color:
rgba: 0.8, 0.8, 0.8, 1
RoundedRectangle:
pos: root.pos
size: self.size
TextInput:
pos: root.pos
size: root.size
id: msg
background_color: 0, 0, 0, 0
cursor_color: 0, 0, 0, 0
PopupBubble:
pos: root.x, self.height + root.y + root.height
size: 100, 40
''')
class Message(Widget):
bg_color = ListProperty([0.99, 0.99, 0.99, 1])
class PopupBubble(Bubble):
pass
class Chat(Screen):
pass
class TestApp(App):
def msg_in(self):
msg_stack = self.msg_stack
msg = "test"
msg_stack.append(Message())
msg_stack[-1].ids['msg'].text = msg
msg_stack[-1].width = 160
msg_stack[-1].height = (len(msg_stack[-1].ids['msg']._lines_labels) + 1) * (msg_stack[-1].ids['msg'].line_height + 5)
for i in msg_stack[-1].walk():
i.height = msg_stack[-1].height
i.width = msg_stack[-1].width
self.msg_layout.add_widget(msg_stack[-1])
def build(self):
self.msg_stack = []
self.chat = Chat()
self.sv1_main = ScrollView(pos_hint = {"top":1, "center_x":0.5},
size_hint = (0.97, 0.65))
self.msg_layout = GridLayout(height = 10,
cols = 1,
padding = 10,
spacing = 10,
size_hint_y = None)
self.chat.add_widget(self.sv1_main)
self.sv1_main.add_widget(self.msg_layout)
for i in range(15):
self.msg_layout.add_widget(Widget())
# Just to bring the next widget down
self.msg_in()
return self.chat
TestApp().run()
Your line msg_stack[-1].width = 160 cripples it. Or rather the loop after it, where you set Bubble's width from that variable.
You set Bubble's size in kv, then in python you play its width again as you add Message as a widget to GridLayout and the result is clearly visible. Set its size once, in kv or in python and/or try to set one property only. Otherwise it works as expected:
from kivy.lang import Builder
from kivy.base import runTouchApp
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.bubble import Bubble, BubbleButton
Builder.load_string('''
<Test>:
Button:
on_release: root.show_bubble()
FloatLayout:
id: box
''')
class Test(BoxLayout):
def show_bubble(self):
bubble = Bubble(size_hint=[None,None],size=[100,40],pos=[100,300])
bubble.add_widget(BubbleButton(text='Copy', size=[30,10]))
bubble.add_widget(BubbleButton(text='Paste'))
self.ids.box.add_widget(bubble)
runTouchApp(Test())

kivy - Bind button to spawn additional instance of widget?

I've created two Scatter widgets that can be resized, what I'm looking for is a way to spawn more Scatter widgets (potentially up to 20-ish) each time a button is pressed. I tried to write and bind an addfunction() to a Button, but it doesn't seem to work.
I'm looking for a way to spawn a new instance of the Scatterer widget (along with the child widget Drawer which has a canvas to show it's occupied space) when prompted.
Here's my python code:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.scatter import Scatter
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
class Scatterer(Scatter):
pass
class Drawer(FloatLayout):
pass
class MainWindowWidget(FloatLayout):
def addfunction(*args):
root = MainWindowWidget()
s = Scatterer()
d = Drawer()
root.add_widget(s)
s.add_widget(d)
print("button is pressed")
class TestApp(App):
def build(self):
return MainWindowWidget()
if __name__ == '__main__':
TestApp().run()
and my .kv file:
#:kivy 1.0.9
<MainWindowWidget>
Scatterer:
id: scatter2
size: 80, 80
Drawer:
size: scatter2.size
Scatterer:
id: scatter1
size: 80, 80
Drawer:
size: scatter1.size
Button:
text: 'press'
size_hint: None, None
size: 100, 100
pos: 100, 0
on_release: root.addfunction()
<Scatterer>:
do_rotation: False
size_hint: None, None
size: self.size
canvas.after:
Color:
rgba: 1, 1, 0, 0.5
Rectangle:
size: self.size
pos: self.pos
<Drawer>:
size: self.size
canvas:
Color:
rgba: 0, 1, 0, 0.3
Rectangle:
pos: self.pos
size: self.size
root = MainWindowWidget()
Your problem is that you write this, then add the new Scatter to root, but then never do anything with root so it never gets displayed.
You don't want to add the Scatterer to a new MainWindowWidget, but to the current one whose method is running to make the new Scatterer. So replace the declaration of your method with
def addfunction(self, *args):
s = Scatterer()
d = Drawer()
self.add_widget(s)
s.add_widget(d)
print("button is pressed")

Categories

Resources