How to automatically scroll down in a Kivy ScrollView? - python

I add widgets to a GridLayout in a ScrollView, so its content expands dynamically.
By default, without user scrolling, the view stays at the top, no matter how many more widgets do you add. If the user scrolls, the view attaches to this point, but it seems a little annoying for me to have to scroll down (even a little bit) for the view to always show the latest content. How can I make it show the downmost part by default?
Here is the sample code just in case:
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
Builder.load_string('''
<MessageView>:
canvas:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
<Message>:
canvas:
Color:
rgba: 0, 1, 0, 0.3
Rectangle:
pos: self.pos
size: self.size
''')
class Message(Widget):
pass
class MessageView(ScrollView):
pass
class TestApp(App):
def msg_in(self, btn):
msg = Message()
msg.size_hint = [None, None]
self.msg_layout.add_widget(msg)
def build(self):
self.scr = Screen()
self.sv1_main = MessageView(pos_hint={"top": 0.87, "center_x": 0.5},
size_hint=(0.97, 0.65))
self.msg_layout = GridLayout(cols=1,
size_hint_y=None)
self.msg_layout.bind(minimum_height=self.msg_layout.setter('height'))
self.bt1_main = Button(size_hint=(0.1, 0.078),
pos_hint={"top": 0.097, "center_x": 0.927},
on_press=self.msg_in)
self.scr.add_widget(self.sv1_main)
self.sv1_main.add_widget(self.msg_layout)
self.scr.add_widget(self.bt1_main)
return self.scr
TestApp().run()

You can use scroll_to method after adding a new content.
class TestApp(App):
def msg_in(self, btn):
msg = Message()
msg.size_hint = [None, None]
self.msg_layout.add_widget(msg)
self.sv1_main.scroll_to(msg)
# ...

Related

KIVY: How to change label colour?

My for loop creates a simple list of labels within a scrollview, instead of changing the font size and colour in the python file, I would rather customise the labels within my KV file. Is this possible?
I know I can use ids to reference a label in the KV file, but I cant wrap my head around how to do it here.
If I create a label in my python file, is it good practice to customise in my kv file or continue to customise it in the python file. What is the best way to go about this?
*.py
import kivy
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.properties import StringProperty
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scrollview import ScrollView
from kivy.lang import Builder
from kivymd.app import MDApp
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.clock import Clock
from kivy.uix.button import Button
from kivy.core.window import Window
class FirstWindow(Screen):
def __init__(self, **kwargs):
super(FirstWindow, self).__init__(**kwargs)
Clock.schedule_once(self.create_scrollview)
def create_scrollview(self, dt):
list1 = ['1','2','3','4','5','6','7','8','9','10','11','12']
layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
layout.bind(minimum_height=layout.setter("height"))
for x in list1:
l = Label(text=x, size=(10, 50), size_hint=(1, None)) <----- Change the font colour in my kv file
layout.add_widget(l)
scrollview = ScrollView(size_hint=(1, None), size=(Window.width, Window.height))
self.view.add_widget(scrollview)
scrollview.add_widget(layout)
class WindowManager(ScreenManager):
pass
kv = Builder.load_file('NearMe.kv')
class NearMeApp(App):
def build(self):
return kv
*.kv
WindowManager:
FirstWindow:
<FirstWindow>:
view: view
BoxLayout:
orientation: 'vertical'
BoxLayout:
size: (64, 64)
size_hint: (1, None)
Label:
text: "NearMeApplications"
canvas.before:
Color:
rgba: .5, .5, .5, 1
Line:
width: 2
rectangle: self.x, self.y, self.width, self.height
ScrollView:
id: view
canvas.before:
Color:
rgba: .8, .8, .8, 1
Line:
width: 2
rectangle: self.x, self.y, self.width, self.height
You can define your own customized Label like this:
def MyLabel(Label):
pass
Then in your kv make a rule for MyLabel:
<MyLabel>:
color: 1,0,0,1
size: 10, 50
size_hint: 1, None
And in your loop:
for x in list1:
l = MyLabel(text=x) <----- Change the font colour in my kv file
layout.add_widget(l)
Correct me if I'm wrong, but it is not possible to set the value of any widget that is initiallized in the python file from the .kv file, as it is only added AFTER the entire .kv file is loaded, which would most likely result in an error. Though you can still edit the color in the python file, which would require ids to be added into individual label, something like this:
for x in list1:
l = Label(id=x, text=x, size=(10, 50), size_hint=(1, None)) #ids from the list1 list
layout.add_widget(l)
You just need to add color: r,g,b,a.
Label:
text: "NearMeApplications"
color: 1,0,1,1
class MyTestApp(App):
def build(self):
return Button(text="HELLO", color=(0, 0, 5, 1), background_color=(7, 4, 5, 1), font_size=150)
if __name__ == "__main__":
MyTestApp().run()

Making part of the screen a scrollable image with kivy

I'm trying to get a part of the screen a scrollable image, so it will fit in the screen without ruining the image ratio (referring to notebook.jpg in the code). I saw some comments that suggested using ScrollView, but I couldn't really figure out how to add it to the existing class I already have (I mean as a second class in addition to NotebookScreen, so NotebookScreen will be able to use it).
Would really appreciate some help :)
Python code:
import kivy
from kivy.lang import Builder
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
GUI = Builder.load_file('style.kv')
# Window.size = (2224, 1668)
class NotebookScreen(GridLayout):
def __init__(self, **kwargs):
self.rows = 1
super(NotebookScreen, self).__init__(**kwargs)
class MainApp(App):
def build(self):
return NotebookScreen()
if __name__ == "__main__":
MainApp().run()
kivy code:
<NotebookScreen>
FloatLayout:
rows: 2
GridLayout:
size_hint: 1, .05
pos_hint: {"top": 1, "left": 1}
id: tool_bar
cols: 1
canvas:
Color:
rgba: 0, 0, 1, 1
Rectangle:
pos: self.pos
size: self.size
BoxLayout:
id: notebook_grid
size_hint: 1, .95
pos_hint: {"top": .95, "left": 0}
cols: 1
Image:
id: notebook_image
source: 'images/notebook.jpg'
allow_stretch: True
keep_ratio: True
pos: self.pos
size_hint: 1, 1
Here is a quick and dirty example of how you could do that. I simply used a Label's canvas to draw the image. I added the label to the scrollview and added scrollview along with another label to show you that you don't need to have the entire screen for your scrolling part. I only used PIL to get the size of the image, because I wanted to make sure that your window is smaller than the image you want to scroll. I hope it helps you with your approach.
from kivy.app import App
from kivy.uix.scrollview import ScrollView
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.core.window import Window
from kivy.graphics import Rectangle
from PIL import Image
class MyApp(App):
def build(self):
img = Image.open("path/to/your/image/dummy.jpg")
Window.size = (img.size[0]*0.8, img.size[1]*1.2)
layout = BoxLayout(orientation="vertical", size_hint=(None, None), size=Window.size)
lbl = Label(text="This is your picture!", size_hint=(None, None), size=(Window.width, Window.height*0.5))
layout.add_widget(lbl)
sv = ScrollView(size_hint=(None, None), size=(Window.width, Window.height*0.5))
img_box = Label(size_hint=(None, None), size=img.size)
with img_box.canvas:
Rectangle(source="path/to/your/image/dummy.jpg", size=img.size)
sv.add_widget(img_box)
layout.add_widget(sv)
return layout
MyApp().run()
[EDIT]
Here is basically the same thing I made at first, but with some outsourcing to a kv string. I hope this is now, what you are looking for.
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.core.window import Window
from kivy.lang.builder import Builder
from PIL import Image
kv = """
#:import Window kivy.core.window.Window
<MyImageView>:
orientation: "vertical"
size_hint: None, None
size: Window.size
Label:
text: "This is your picture!"
size_hint: None, None
size: Window.width, Window.height * .5
ScrollView:
size_hint: None, None
size: Window.width, Window.height * .5
Label:
id: img_box
size_hint: None, None
size: root.img.size
canvas:
Rectangle:
source: root.img_path
size: root.img.size
"""
class MyImageView(BoxLayout):
def __init__(self, img_path, **kwargs):
self.img_path = img_path
self.img = Image.open(self.img_path)
Window.size = (self.img.size[0] * 0.8, self.img.size[1] * 1.2)
super(MyImageView, self).__init__(**kwargs)
class MyApp(App):
def build(self):
Builder.load_string(kv)
layout = MyImageView("Path/to/your/image.png")
return layout
MyApp().run()

Custom button doesn't anymore work after adding it to a screen

My goal is to have multiple screens while one of my screens has a custom button. The button works fine before i tried working with multiple screens. I tried adding the widget of the custom button to the screen but now it doesn't respond. I've also tried adding screen as a super to my custom button, but apparently it doesn't work that way.
It doesn't give an error, my custom button just doesn't do anything.
It seems like a simple problem, but i can't find any comparable examples. I'm still pretty new to this so i would love to learn about any silly mistakes i made.
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.gridlayout import GridLayout
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.button import Button
from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior
from kivy.properties import NumericProperty
from kivy.lang import Builder
from kivy.vector import Vector
from kivy.base import runTouchApp
from kivy.core.window import Window
from kivy.uix.floatlayout import FloatLayout
from kivy.graphics import Line
from kivy.uix.dropdown import DropDown
from kivy.clock import Clock
from kivy.uix.screenmanager import ScreenManager, Screen
Builder.load_string("""
<HomeScreenLogic>:
canvas.before:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
canvas:
Color:
rgba: ((25/255, 181/255, 254/255, 1) if self.state == "normal" else (137/255, 196/255, 244/255, 1))
Ellipse:
size: root.width/4, root.width/4
pos: 0.5*root.width - root.width/8, root.height / 8
Color:
rgba: 1, 1, 1, 1
Line:
width: 2
points: [root.width/2-root.width/12, root.height/8+root.width/8,root.width/2+root.width/12, root.height/8+root.width/8]
Color:
rgba: 1, 1, 1, 1
Line:
width: 2
points: [root.width/2, root.height/8+root.width/8+root.width/12,root.width/2, root.height/8+root.width/8-root.width/12]
<AddScreen>:
canvas.before:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
""")
class HomeScreenLogic(Widget, ButtonBehavior):
def collide_point(self, x, y):
if Vector(x, y).distance((Window.size[0]/2, 7*Window.size[1]/24)) <= Window.size[0]/8:
print('True')
return True
return False
class HomeScreen(Screen):
def __init__(self, **kwargs):
super(HomeScreen, self).__init__(**kwargs)
self.Logic = HomeScreenLogic()
self.add_widget(self.Logic)
class AddScreen(Screen):
pass
SM = ScreenManager()
SM.add_widget(HomeScreen(name='Home'))
SM.add_widget(AddScreen(name='Add'))
class HelloWorldApp(App):
def build(self):
return SM
if __name__ == "__main__":
HelloWorldApp().run()
I would like to have multiple screens while having a functioning custom button in one of them.
I swapped the supers ButtonBehavior and Widget, it seems that one was over-riding the other and made sure the collide_point function didn't work.
class HomeScreenLogic(ButtonBehavior, Widget):
def collide_point(self, x, y):
if Vector(x, y).distance((Window.size[0]/2, 7*Window.size[1]/24)) <= Window.size[0]/8:
print('True')
return True
return False

My Kivy program has a random white square showing up in the bottom left corner

I am trying to create a program that outputs random 10x10 grids of black and white squares. It mostly works except that the bottom left corner has an unwanted white square covering up part of the grid.
I can't even figure out what widget would be causing this. I've tried printing all of the children starting at root to no avail.
import random
import kivy
kivy.require("1.10.1")
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.config import Config
from kivy.graphics import Color
from kivy.graphics import Rectangle
Config.set('graphics', 'width', '400')
Config.set('graphics', 'height', '400')
class Container(FloatLayout):
pass
class ColorLabel(Label):
def __init__(self, **kwargs):
super(ColorLabel, self).__init__(**kwargs)
with self.canvas:
Color(1, 1, 1, 1)
self.rect = Rectangle(size=self.size, pos=self.pos)
self.bind(size=self._update_rect, pos=self._update_rect)
def _update_rect(self, instance, value):
self.rect.pos = instance.pos
self.rect.size = instance.size
def changeBG(self):
with self.canvas.after:
Color(0,0,0,1)
self.rect = Rectangle(size=self.size, pos=self.pos)
class Main(App):
def build(self):
Builder.load_file("EveryImage.kv")
the_grid = GridLayout(cols=10, spacing=1)
i = 100
while i > 0:
i -= 1
newLabel = ColorLabel()
the_grid.add_widget(newLabel)
x = random.randint(0,1)
if x == 0:
newLabel.changeBG()
root = Container()
root.add_widget(the_grid)
return root
# Keep everything below this last!
if __name__ == '__main__':
Main().run()
And here is the .kv file:
#EveryImage.kv
Container:
#Container holds all the other layouts
<Container>:
id: contain
canvas.before:
Color:
rgba: 0,0,0.5,1 #blue, just for the grid
Rectangle:
pos: self.pos
size: self.size
<ColorLabel>:
canvas.before:
Color:
rgba: 1,1,1,1 #white
Rectangle:
pos: self.pos
size: self.size
The problem is that you are painting several times in different places, precisely in the function changeBG, instead you just have to paint in one place and set the background color as property so when this value is changed the Label will be repainted.
Another error is that you are creating a Container that you do not use in the .kv.
In the case of the while loop this can be simplified using a for loop.
*.py
import random
import kivy
kivy.require("1.10.1")
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.config import Config
Config.set('graphics', 'width', '400')
Config.set('graphics', 'height', '400')
class Container(FloatLayout):
pass
class ColorLabel(Label):
pass
class Main(App):
def build(self):
Builder.load_file("EveryImage.kv")
the_grid = GridLayout(cols=10, spacing=1)
for _ in range(100):
newLabel = ColorLabel()
the_grid.add_widget(newLabel)
if random.choice([True, False]):
newLabel.bg_color = [0,0,0,1]
root = Container()
root.add_widget(the_grid)
return root
# Keep everything below this last!
if __name__ == '__main__':
Main().run()
*.kv
#Container holds all the other layouts
<Container>:
id: contain
canvas.before:
Color:
rgba: 0,0,0.5,1 #blue, just for the grid
Rectangle:
pos: self.pos
size: self.size
<ColorLabel>:
bg_color: 1, 1, 1, 1
canvas.before:
Color:
rgba: self.bg_color # white
Rectangle:
pos: self.pos
size: self.size

Kivy: undesirable behavior of custom mouse cursor when crossing left or top edge of app window

I want to make a custom mouse cursor in kivy.
This is what I have at the moment:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.scatter import Scatter
from kivy.core.window import Window
#Window.show_cursor = False
KV = """
FloatLayout
BoxLayout
MyTextInput
MyMouse
<MyTextInput>:
font_size: 40
text: 'Some text'
<MyMouse>:
mouse_im_size: mouse_im.size
auto_bring_to_front: True
do_rotation:False
do_scale:False
do_translation_y:False
Image
id: mouse_im
size: 100, 100 / self.image_ratio
source: 'cursor-pink.png'
"""
class MyTextInput(TextInput):
pass
class MyMouse(Scatter):
def __init__(self, **kwargs):
Window.bind(mouse_pos=self.on_mouse_pos)
super(MyMouse, self).__init__(**kwargs)
def on_touch_down(self, *touch):
return
def on_mouse_pos(self, *args):
x,y = args[1]
self.pos = [x,y-self.mouse_im_size[1]]
class MyApp(App):
def build(self):
self.root = Builder.load_string(KV)
MyApp().run()
the problem is that when I move the mouse beyond the left or upper edge of application, the cursor image remains within the app, and I want the mouse image to disappear just like when I move the mouse beyond the right or lower edge.
It seems the problem is that on_mouse_pos() only works when the mouse is inside the window.
I found a way to get the position of the mouse when it is outside the window, but I do not know how this can be used in my task. And maybe there is a better way to do this.
You can accomplish this by using the Window events on_cursor_enter and on_cursor_leave and making the cursor visible/invisible by using the opacity property:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.scatter import Scatter
from kivy.core.window import Window
#Window.show_cursor = False
KV = """
FloatLayout
BoxLayout
MyTextInput
MyMouse
id: themouse
<MyTextInput>:
font_size: 40
text: 'Some text'
<MyMouse>:
mouse_im_size: mouse_im.size
auto_bring_to_front: True
do_rotation:False
do_scale:False
do_translation_y:False
Image
id: mouse_im
size: 100, 100 / self.image_ratio
source: 'cursor-pink.png'
"""
class MyTextInput(TextInput):
pass
class MyMouse(Scatter):
def __init__(self, **kwargs):
Window.bind(mouse_pos=self.on_mouse_pos)
Window.bind(on_cursor_leave=self.on_cursor_leave)
Window.bind(on_cursor_enter=self.on_cursor_enter)
super(MyMouse, self).__init__(**kwargs)
def on_touch_down(self, *touch):
return
def on_mouse_pos(self, *args):
x,y = args[1]
self.pos = [x,y-self.mouse_im_size[1]]
def on_cursor_leave(self, *args):
App.get_running_app().root.ids.themouse.opacity = 0
def on_cursor_enter(self, *args):
App.get_running_app().root.ids.themouse.opacity = 1
class MyApp(App):
def build(self):
self.root = Builder.load_string(KV)
MyApp().run()
I added the themouse id to the MyMouse widget to accomplish this.
Here is another approach, but it requires a border around your basic layout:
from kivy.app import App
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.scatter import Scatter
from kivy.core.window import Window
#Window.show_cursor = False
KV = """
FloatLayout
canvas.before:
Color:
rgba: (1, 0, 0, 1)
Rectangle:
size: self.size
pos: self.pos
FloatLayout
id: mainlayout
size_hint: (None, None)
#pos: (50, 50)
#pos_hint: {'center_x': 0.5, 'center_y': 0.5}
canvas.before:
Color:
rgba: (0, 1, 0, 1)
Rectangle:
size: self.size
pos: self.pos
BoxLayout
size_hint: (1.0, 0.2)
pos_hint: {'center_x': 0.5, 'top': 1.0}
MyTextInput
StencilView:
size_hint: (1.0, 1.0)
pos_hint: {'x': 0, 'y': 0}
MyMouse
id: themouse
<MyTextInput>:
font_size: 40
text: 'Some text'
<MyMouse>:
mouse_im_size: mouse_im.size
auto_bring_to_front: True
do_rotation:False
do_scale:False
do_translation_y:False
Image
id: mouse_im
size: 100, 100 / self.image_ratio
source: 'cursor-pink.png'
"""
class MyTextInput(TextInput):
pass
class MyMouse(Scatter):
def __init__(self, **kwargs):
Window.bind(mouse_pos=self.on_mouse_pos)
super(MyMouse, self).__init__(**kwargs)
def on_touch_down(self, *touch):
return
def on_mouse_pos(self, *args):
x,y = args[1]
self.pos = [x,y-self.mouse_im_size[1]]
class MyApp(App):
def build(self):
self.mainlayout = None
self.mymouse = None
self.root = Builder.load_string(KV)
Window.bind(size=self.do_size)
Clock.schedule_once(self.do_size)
def do_size(self, *args):
if self.mainlayout is None:
self.mainlayout = self.root.ids.mainlayout
if self.mymouse is None:
self.mymouse = self.root.ids.themouse
self.mainlayout.size = (self.root.width - 2.0 * self.mymouse.mouse_im_size[0], self.root.height - 2.0 * self.mymouse.mouse_im_size[1])
self.mainlayout.pos = self.mymouse.mouse_im_size
MyApp().run()
This uses a StencilView to clip the drawing of the cursor to the inside of the man layout.

Categories