Scale with Kivy ScatterLayout Doesn't Behave as Expected - python

I would like to zoom in and zoom out of a collection of widgets. I would also like to scroll. I tried to accomplish this with a ScrollView as the root widget and a ScatterLayout as the child.The widgets I'd like to zoom in and out of are children to the ScatterLayout. This isn't behaving as expected. Here's a minimal version.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.scatter import Scatter
from kivy.uix.scatterlayout import ScatterLayout
from kivy.uix.scrollview import ScrollView
kv = '''
#:kivy 1.11.1
<MyScatter>:
do_translation_y: False
do_rotation: False
do_scale: False
canvas:
Color:
hsv: .1, 1, .5
Rectangle:
size: 100, 100
<ScrollView>:
size_hint: None, None
size: 640, 480
pos_hint: {'center_x': .5, 'center_y': .5}
scroll_type: ['bars']
bar_width: 10
bar_inactive_color: self.bar_color
canvas:
Color:
rgb: 1, 0, 0
Rectangle:
pos: self.pos
size: self.size
<MyScatterLayout>:
size_hint: None, None
size: 1280, 720
do_translation: False
do_rotation: False
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
canvas:
Color:
rgb: 0, 0, 1
Rectangle:
pos: self.pos
size: self.size
'''
Builder.load_string(kv)
class MyScatter(Scatter):
pass
class MyScatterLayout(ScatterLayout):
pass
class MyApp(App):
def build(self):
layout = MyScatterLayout()
layout.add_widget(MyScatter())
root = ScrollView()
root.add_widget(layout)
return root
if __name__ == '__main__':
MyApp().run()
When zooming out, the child of ScatterLayout ends up outside of the layout's bounds. I would expect all the children to stay within the layout's bounds no matter what the transformation is. What am I doing wrong?
Tangential to the question: when I zoom out enough that the ScatterLayout is smaller than the ScrollView, scrolling snaps the ScatterLayout to the origin (bottom left corner). Per the ScrollView docs it looks like a ScrollView's child is expected to be larger than the ScrollView itself. I assume to prevent this from happening I need to increase the size of the ScatterLayout as I scroll.

Not sure this is related to the issue, but ScatterLayout is a subclass of Scatter so its canvas instructions must be in local coordinates.
<MyScatterLayout>:
...
canvas:
Color:
rgb: 0, 0, 1
Rectangle:
pos: 0, 0 # <- shold be this
size: self.size
And you shouldn't write any rule for ScrollView because it affects all instances that might exist outside of your code. So instead, define a subclass and write a rule for it.
class MyScrollView(ScrollView):
pass
<MyScrollView>:
...

You have to scale the size of the ScatterLayout for the children's position to be consistent. I've also incorporated Nattosai Mito's answer.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.scatter import Scatter
from kivy.uix.scatterlayout import ScatterLayout
from kivy.uix.scrollview import ScrollView
kv = '''
#:kivy 1.11.1
<MyScatter>:
do_translation_y: False
do_rotation: False
do_scale: False
canvas:
Color:
hsv: .1, 1, .5
Rectangle:
size: 100, 100
<MyScrollView>:
size_hint: None, None
size: 640, 480
pos_hint: {'center_x': .5, 'center_y': .5}
scroll_type: ['bars']
bar_width: 10
bar_inactive_color: self.bar_color
canvas:
Color:
rgb: 1, 0, 0
Rectangle:
pos: self.pos
size: self.size
<MyScatterLayout>:
size_hint: None, None
size: 1280 * self.scale, 720 * self.scale # this is the correction
do_translation: False
do_rotation: False
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
canvas:
Color:
rgb: 0, 0, 1
Rectangle:
pos: 0, 0
size: self.size
'''
Builder.load_string(kv)
class MyScatter(Scatter):
pass
class MyScatterLayout(ScatterLayout):
pass
class MyScrollView(ScrollView):
pass
class MyApp(App):
def build(self):
layout = MyScatterLayout()
layout.add_widget(MyScatter())
root = MyScrollView()
root.add_widget(layout)
return root
if __name__ == '__main__':
MyApp().run()

Related

Proper way of adding a canvas to a label

I'm posting this because I want to add a canvas behind, inside, on top, I don't know, I want a background for the labels, and I have found out that I need a canvas to do that, but I don't know how to do it. I have been able to add a canvas as a background for a label, but since my app needs to be refreshed and resized I bump into problems that I am not able to fix.
I want to make the canvas so that it has the same size as the labels beside eachother, and I want to be able to remove it like when I remove the labels inside "theLayout".
I believe the problem is in the method "refresh" inside the class "HovedVindu".
If it is possible to not change the whole code and fix it so that a beginner like me is able to understand and modify it, I would appreciate it very much, if it is not possible I will still appreciate any help :)
I used paint 3D to visualize what I want to achieve:
main.py:
import kivy
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
import kivy.uix.boxlayout
from kivy.uix.popup import Popup
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.graphics import Color, Rectangle, Canvas
Builder.load_file("struktur.kv")
sm = ScreenManager()
produkter = [["Melk", "04.06.2022"], ["Ost", "28.07.2022"], ["Tomat", "27.05.2022"], ["Banan", "02.06.2022"]]
class produktManager(object):
def lagreProdukt(self, navn, dato):
self.navn = navn
self.dato = dato
nyProdukt = [self.navn.text, self.dato.text]
if nyProdukt != ["", ""]:
produkter.insert(0, nyProdukt)
class HovedVindu(Screen):
def nyttProdukt(self):
show_popup()
def Refresh(self):
theLayout = self.ids.theLayout
for i in range(10):
for v in theLayout.children:
theLayout.remove_widget(v)
for i in produkter:
newLabel_navn = theLayout.add_widget(Label(text = i[0]))
newLabel_dato = theLayout.add_widget(Label(text = i[1]))
#with theLayout.canvas:
#(Color(0, 1, 1, .1))
#(Rectangle(size = (self.size), pos_hint = (self.pos)))
class P(FloatLayout):
def leggTilNyttProdukt(self, Navn, Dato):
produktManager().lagreProdukt(Navn, Dato)
Navn.text = ""
Dato.text = ""
class MainApp(App):
def build(self):
sm.add_widget(HovedVindu(name="hoved"))
return sm
def show_popup():
show = P()
popupWindow = Popup(title="Legg til ett nytt produkt", content=show, size_hint=(None,None),size=(400,400), title_align="center")
popupWindow.open()
if __name__ == "__main__":
MainApp().run()
struktur.kv:
#:kivy 2.1.0
#:import RiseInTransition kivy.uix.screenmanager.RiseInTransition
<HovedVindu>:
FloatLayout:
size_hint: 1, .1
pos_hint: {"x": 0, "top": 1}
canvas.before:
Color:
rgba: 1, 1, 1, .8
Rectangle:
pos: self.pos
size: self.size
Button:
text: "+"
size_hint: .15, .8
pos_hint: {"x": .8, "top": .9}
on_release: root.nyttProdukt()
Button:
text: "Refresh"
size_hint: .2, .8
pos_hint: {"x": .2, "top": .9}
on_release:
root.Refresh()
FloatLayout:
size_hint: 1, .9
canvas.before:
Color:
rgba: 0, 0, 0, 0
Rectangle:
pos: self.pos
size: self.size
GridLayout:
id: theLayout
cols: 2
rows: 10
Label:
text: "NAVN"
canvas.before:
Color:
rgba: 1, 0, 0, .2
Rectangle:
pos: self.pos
size: self.size
Label:
text: "DATO"
canvas.before:
Color:
rgba: 1, 0, 0, .2
Rectangle:
pos: self.pos
size: self.size
<P>:
TextInput:
id: navn
hint_text: "Navnet på produktet"
size_hint: .6, .15
pos_hint: {"x": .2, "top": .9}
TextInput:
id: dato
hint_text: "Siste forbruksdag"
size_hint: .6, .15
pos_hint: {"x": .2, "top": .7}
Button:
text: "Legg til produkt"
size_hint: 0.8, 0.2
pos_hint: {"x":0.1, "y":0.1}
on_release: root.leggTilNyttProdukt(navn, dato)
In order to have a label with some background color and use it dynamically you can create a dynamic class inherited from Label. You can also create any desired prop. for advanced usage.
First define a class in .py inherited from Label,
class CustomLabel(Label):
background_color = ListProperty([1, 1, 1, 1])
Now design it using kvlang,
<CustomLabel>:
canvas.before:
Color:
rgba: self.background_color
Rectangle:
pos: self.pos
size: self.size
Now your label is ready to use.
def Refresh(self):
...
for n, i in enumerate(produkter, start = 1):
newLabel_navn = theLayout.add_widget(CustomLabel(text = i[0], background_color = [1, 1/n, 1, 0.5]))
newLabel_dato = theLayout.add_widget(CustomLabel(text = i[1], background_color = [1/n, 1, 1, 0.8]))
...

Adaptive_width property of MDLabel is not working correctly

I am making an app using kivy & kivymd and in one part of it, I would like the labels to take as much space as the actual text.
This seems pretty straightforward with kivy itself but for some reason, nothing works with the MDLabel class. I tried setting the adaptive_width property to True and I also tried to directly set the width to the texture_size[0] property but none of them worked (and yes I installed kivymd directly from github).
Here is my code:
from kivy.lang import Builder
from kivymd.app import MDApp
class MainApp(MDApp):
def __init__(self, **kwargs):
super(MainApp, self).__init__(**kwargs)
self.kv = Builder.load_string('''
#:kivy 2.0.0
BoxLayout:
MDLabel:
text: "Supposedly adaptive width (KivyMD)"
font_size: "21sp"
halign: "center"
adaptive_width: True
# I also tried directly setting the width to the texture_size but the results were worse
# size_hint_x: None
# width: self.texture_size[0]
canvas.before:
Color:
rgba: .8, .1, .2, .5
Rectangle:
pos: self.pos
size: self.size
Widget:
MDSeparator:
orientation: "vertical"
Widget:
Label:
text: "Actual adaptive width (Standard Kivy)"
font_size: "21sp"
color: 0, 0, 0, 1
size_hint_x: None
width: self.texture_size[0]
canvas.before:
Color:
rgba: 0, .6, .2, .5
Rectangle:
pos: self.pos
size: self.size
''')
def build(self):
return self.kv
if __name__ == '__main__':
MainApp().run()
Here is my results:
I don't believe that MDLabel supports the adaptive_width property. In using the width: self.texture_size[0], it seems that you must also add the text_size: None, None to the MDLabel, and it seems that its location in the kv is important. Here is a version of part of your kv that seems to work:
BoxLayout:
MDLabel:
text: "Supposedly adaptive width (KivyMD)"
font_size: "21sp"
halign: "center"
# adaptive_width: True
# I also tried directly setting the width to the texture_size but the results were worse
size_hint_x: None
width: self.texture_size[0]
text_size: None, None # added, and must be in this location
canvas.before:
Color:
rgba: .8, .1, .2, .5
Rectangle:
pos: self.pos
size: self.size

how to center canvas? kivy

How to place the white square to the center? I tried a lot of variants, but nothing worked. It work for Label and Buttons, but not for canvas. Or maybe I'm doing everything in wrong way. maybe you suggest the best solution for this task. I need window with background, label in left corner, label in right corner and a square in center
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.config import Config
from kivy.animation import Animation
from kivy.vector import Vector
from kivy.clock import Clock
from kivy.properties import NumericProperty, ReferenceListProperty,\
ObjectProperty
Config.set('graphics', 'resizable', 'true')
Config.set('graphics', 'width', '900')
Config.set('graphics', 'height', '450')
Config.write()
class Helicopter(Widget):
pass
class Background(Widget):
pass
class Root(FloatLayout):
#def on_touch_down(self, touch):
# Animation(center=touch.pos).start(self)
pass
class FriendsApp(App):
def build(self):
return Root()
if __name__ == '__main__':
FriendsApp().run()
.kv file
#: kivy 1.10.0
<Root>
AnchorLayout:
canvas.before:
Color:
rgba: 1, 1, 1, 1 # white
Rectangle:
source: 'background.jpg'
pos: self.pos
size: self.size
size: self.parent.size
anchor_x: 'center'
anchor_y: 'center'
AnchorLayout:
anchor_y:'top'
anchor_x:'left'
padding: 20
Label:
text: 'Lives: x2'
size: self.texture_size
size_hint: None, None
AnchorLayout:
anchor_x: 'right'
anchor_y: 'top'
padding: 20
Label:
text: 'Score: 0000000'
size: self.texture_size
size_hint: None,None
AnchorLayout:
anchor_x: 'center'
anchor_y: 'center'
canvas:
Rectangle:
size: 100, 100
pos: self.pos
The AnchorLayout has its own canvas and you cannot aligns itself. There are two solutions to this problem. In the example, colors were added for visualization.
The AnchorLayout aligns its children to a border (top, bottom,
left, right) or center.
Solution 1
Add a Widget as a children.
AnchorLayout:
anchor_x: 'center'
anchor_y: 'center'
Widget:
canvas.before:
Color:
rgba: 1, 1, 1, 1 # white
Rectangle:
size: 100, 100
pos: self.pos
size_hint: None,None
Solution 2
Replace the last AnchorLaoyout with Widget.
Widget:
canvas:
Rectangle:
pos: self.center_x - 50, self.center_y - 50
size: 100, 100
Example - Solution 1
kv file
#: kivy 1.10.0
<Root>
AnchorLayout:
anchor_y:'top'
anchor_x:'left'
padding: 20
Label:
canvas.before:
Color:
rgba: 1, 0, 0, 1 # red
Rectangle:
pos: self.pos
size: self.size
text: 'Lives: x2'
size: self.texture_size
size_hint: None, None
AnchorLayout:
anchor_x: 'right'
anchor_y: 'top'
padding: 20
Label:
canvas.before:
Color:
rgba: 0, 0, 1, 1 # blue
Rectangle:
pos: self.pos
size: self.size
text: 'Score: 0000000'
size: self.texture_size
size_hint: None,None
AnchorLayout:
anchor_x: 'center'
anchor_y: 'center'
Widget:
canvas.before:
Color:
rgba: 1, 1, 1, 1 # white
Rectangle:
size: 100, 100
pos: self.pos
size_hint: None,None
Output

kivy scrollview inside gridlayout on label

I trying to create a one screen app with one root rule set. by pressing a button i should see a scrollable text in the grid next to the button. In all the example solutions I see that scrollview is working in similar KV files that I have seen in other questions. Could someone please identify what I have missed in KV file.
my .py file:
import kivy
import string
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.properties import ObjectProperty
from kivy.properties import StringProperty
from kivy.uix.scrollview import ScrollView
from kivy.core.window import Window
class RootContainer(BoxLayout):
instance = ObjectProperty(None)
def __init__(self, **kwargs):
super(RootContainer, self).__init__(**kwargs)
def clickAction1(self, instance):
#identify the button pressed
buttonText = instance.text
self.lbl1.text = instance.text + " some text goes here ... "
myresult = " this is scrolling text.\n " * 30
self.lbl5.text = myresult
class MBApp(App):
def build(self):
return RootContainer()
if __name__ == '__main__':
MBApp().run()
my KV file:
#:kivy 1.0.9
<RootContainer>:
id: theRoot
lbl1: my_labelC
lbl5: my_labelCS
BoxLayout:
orientation: 'vertical'
spacing: 20
padding: 20
canvas:
Color:
rgb: 0, .33, 0
Rectangle:
pos: self.pos
size: self.size
Button:
text: "This is 1st button"
text_size: self.size
size_hint: (.5,1)
on_press: theRoot.clickAction1(self)
Button:
text: "This is 2nd button"
text_size: self.size
size_hint: (.5,1)
on_press: root.clickAction1(self)
GridLayout:
rows: 2
cols: 1
spacing: 10
padding: 10
canvas:
Color:
rgb: .7, .63, 0
Rectangle:
pos: self.pos
size: self.size
Label:
id: my_labelC
canvas.before:
Color:
rgb: 0,0,0
Rectangle:
pos: self.pos
size: self.size
text: "Header text for button clicked ......."
text_size: self.size
ScrollView:
GridLayout:
cols:1
rows:1
height: self.minimum_height
Label:
id: my_labelCS
text: "Scrolling text goes here ....."
I hope this is not a duplicate. Any other suggestions to code are also welcome. Thank you.
You don't set the size of your Label, so the default applies, as any widget, the defaults are
size: 100, 100
size_hint: 1, 1
since size_hintis 1, 1, and the parent is a layout, size itself is overriden by the parent's available space
since you set size: minimum_size in the parent layout, it'll give the minimum size its children require, but the Label doesn't ask for any space, it's size_hint of 1, 1 means that it's happy to take all the available space. Kivy solve this situation by not giving it any space, so the Label size ends up being 0, 0, and the GridLayout's size as well.
So you want to disable size_hint (at least for the height, of the Label, and instead set it to the texture heights
size_hint_y: None
height: self.texture_size[1]
Which is usually sided with setting the texture width to the available width, by setting text_size.
text_size: self.width, None
size_hint_x: 1 # this is the default, so this line is not required, but you want to be sure not to disable this default
Do you really need the GridLayout inside your scrollview? This works for me:
ScrollView:
Label:
id: my_labelCS
size_hint: 1, None
text_size: self.width, None
height: self.texture_size[1]
text: "Scrolling text goes here ....."

How to center buttons in Kivy?

I am new to Python UI programming, and I am trying out Kivy. I want to center some buttons on my screen, but the buttons do not move from the bottom right of the window. I was wondering if anyone could point out what I am doing wrong or am missing?
vgx.kv:
#:kivy 1.9.1
<VgxMainScreen>:
BoxLayout:
size_hint: 0.2,0.2
size: 200, 100
orientation: 'vertical'
Button:
text: 'Game Select'
Button:
text: 'Options'
Button:
text: 'Controllers'
<VgxUI>:
AnchorLayout:
anchor_x: 'center'
VgxMainScreen:
size_hint: 0.2,0.2
<VgxApp>:
FloatLayout:
VgxUI:
center: 0.5, 0.5
VgxApp.py:
import kivy
from kivy.app import App
from kivy.uix.widget import Widget
kivy.require('1.9.1')
class VgxMainScreen(Widget):
pass
class VgxUI(Widget):
pass
class VgxApp(App):
def build(self):
return VgxUI()
if __name__ == '__main__':
VgxApp().run()
Edit: Best way to debug what's going on with you widgets is to change their background colors :).
canvas.before:
Color:
rgba: X, X, X, 1
Rectangle:
pos: self.pos
size: self.size
The problem was that your BoxLayout wasn't positioned relatively to its parent even though it was inside it.
What I did here is that I positioned the BoxLayout to its parent's size and position using:
size: self.parent.size
pos: self.parent.pos
I also moved the size property from VgxMainScreen to VgxUI because I assume this is more common use-case (you can have multiple VgxMainScreens each with different size). Full code with background colors:
#:kivy 1.9.1
<VgxMainScreen>:
size_hint: None, None
canvas.before:
Color:
rgba: 1, 0, 0, 1 # red
Rectangle:
pos: self.pos
size: self.size
BoxLayout:
canvas.before:
Color:
rgba: 0, 0, 1, 1 # blue
Rectangle:
pos: self.pos
size: self.size
size: self.parent.size # important!
pos: self.parent.pos # important!
orientation: 'vertical'
Button:
text: 'Game Select'
Button:
text: 'Options'
Button:
text: 'Controllers'
<VgxUI>:
AnchorLayout:
canvas.before:
Color:
rgba: 1, 1, 1, 1 # white
Rectangle:
pos: self.pos
size: self.size
size: self.parent.size
anchor_x: 'center'
anchor_y: 'center'
VgxMainScreen:
size: 200, 100
<VgxApp>:
FloatLayout:
VgxUI:
center: 0.5, 0.5
There's yet another solution to this.
You can use RelativeLayout and all widgets inside it will position relatively to this layout.
<VgxMainScreen>:
size_hint: None, None
RelativeLayout:
pos: self.parent.pos
size: self.parent.size
BoxLayout:
orientation: 'vertical'
Button:
text: 'Game Select'
Button:
text: 'Options'
Button:
text: 'Controllers'
I've personally scratched my head many times when using Kivy and wondered why my widgets aren't positioned as I think they should :).
VgxMainScreen is a Widget, so it doesn't apply any positioning or sizing to its children (only Layouts do that), so the BoxLayout has the default position of (0, 0) and size of (100, 100).
Make VgxMainScreen inherit from something else, like a FloatLayout.

Categories

Resources