Dealing with ScrollViews in Kivy - python

I want to draw a border around a ScrollView in my Kivy app. The problem is that the content of the ScrollView overlap that border since I'm drawing it inside the widget.
So I'm wondering if one of those is a possible solution:
How can I draw outside the widget's boundaries?
When I tried to move a part of a canvas element outside the widget, it simply cut off that part, which is no surprise. Perhaps I could make another widget outside this one and draw on it?
How can I limit the content of the ScrollView? So, can I change the scroll boundaries? What I mean is that I don't want the children to go beyond a certain point in the widget to make them not touch the border
Here is some test code to demonstrate the issue. It's a slightly modified official example. The buttons overlap the green border when scrolling, which is what I don't want:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
Builder.load_string('''
<ScrollView>:
canvas:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
Color:
rgba: 0, 1, 0, 1
Line:
points: self.x, self.y + self.height,\
self.x + self.width, self.y + self.height,\
self.x + self.width, self.y, self.x, self.y,\
self.x, self.y + self.height
width: 1.2
''')
class TestApp(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(30):
btn = Button(text=str(i), size=(480, 40),
size_hint=(None, None))
layout.add_widget(btn)
root = ScrollView(size_hint=(None, None), size=(500, 320),
pos_hint={'center_x': .5, 'center_y': .5}, do_scroll_x=False)
root.add_widget(layout)
return root
TestApp().run()

How can I draw outside the widget's boundaries?
When I tried to move a part of a canvas element outside the widget, it simply cut off that part, which is no surprise. Perhaps I could make another widget outside this one and draw on it?
Don't really have a clue what you mean by it. canvas has no boundaries, except if you use Stencil. When you try to draw outside of ScrollView, content out of the widget's space will be hidden, because originally it uses Stencil so that the unnecessary content is hidden and you could scroll through it as you can see here.
Now about that overlaping. canvas.after before the second Color makes it draw after the widgets are drawn. Similar to canvas.before, which is used for backgrounds.
canvas:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
canvas.after:
...
Basic canvas is drawn in the "same" time as the widgets are, therefore the item that is drawn later will be on the top no matter what you do. after will draw after all is drawn again with that order and before does the same thing before the widgets are even drawn at all.
How can I limit the content of the ScrollView? So, can I change the scroll boundaries? What I mean is that I don't want the children to go beyond a certain point in the widget to make them not touch the border
Use layout with fixed size that handles it's children's pos e.g. FloatLayout with pos_hint or BoxLayout depends on your need, therefore it will push the widgets inside to imaginary rectangle, which is basically [0, 0] to [width, height] i.e. size of your layout(if you use it right).

Related

How do I make my custom widget appear in my splitter using Kivy?

I'm still learning Kivy nuances, & I just can't seem to make my Chessboard widget appear in my splitter. I substituted a working button widget in the splitter for my custom widget, but it's not acting the same. What am I doing wrong?
import kivy
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.splitter import Splitter
from kivy.graphics import Color, Rectangle
from kivy.uix.image import Image
from kivy.uix.widget import Widget
kivy.require('2.0.0')
class ChessBoardWidget(Widget): # FloatLayout
def __init__(self, **kwargs):
super(ChessBoardWidget, self).__init__(**kwargs)
with self.canvas.before:
Color(0, 1, 0, 1)
self.rect = Rectangle(size=(self.width, self.height), pos=self.pos)
self.add_widget(
Image(source="./data/images/chess-pieces/DarkerGreenGreyChessBoard.png", pos=self.pos,
size_hint=(1, 1), keep_ratio=True, allow_stretch=True))
class SplitterGui(BoxLayout):
def __init__(self, **kwargs):
super(SplitterGui, self).__init__(**kwargs)
self.orientation = 'horizontal'
split1_boxlayout = BoxLayout(orientation='vertical')
split1 = Splitter(sizable_from='bottom', min_size=100, max_size=1000)
chessboard_widget = ChessBoardWidget() # was s1_button = Button(text='s1', size_hint=(1, 1)) WORKED!
s3_button = Button(text='s3', size_hint=(1, 1))
split1.add_widget(chessboard_widget) # was split1.add_widget(s1_button) WORKED!
split1_boxlayout.add_widget(split1)
split1_boxlayout.add_widget(s3_button)
self.add_widget(split1_boxlayout)
split2 = Splitter(sizable_from='left', min_size=100, max_size=1000)
s2_button = Button(text='s2', size_hint=(.1, 1))
split2.add_widget(s2_button)
self.add_widget(split2)
class SplitterTestApp(App):
def build(self):
return SplitterGui() # root
if __name__ == '__main__':
SplitterTestApp().run()
Your ChessBoardWidget is being drawn, it's just not where you expect it. Three things to keep in mind:
Using the Widget class as a container for other widgets eliminates the capabilities of widgets that are intended for use as containers such as size_hint and pos_hint. From the documentation:
A Widget is not a Layout: it will not change the position or the size
of its children. If you want control over positioning or sizing, use a
Layout.
When you reference pos or size of a widget in its __init__() method, you will always get the default values of (0,0) for pos and (100,100) for size.
References to properties (such as pos and size) in a widgets __init__() method use the current values of those properties (see #2 above), and there is no binding to handle changes in those values. So, for example, creating a Rectangle in a widgets __init__() using the widgets pos and size will create an unchanging Rectangle at pos of (0,0) and size of (100,100).
So, a fix for getting the ChessBoardWidget drawn where you expect is to just change the base class of ChessBoardWidget to RelativeLayout. The nice thing about RelativeLayout is that adding a child with the default pos and size_hint will result in the child (in your case, the Image) being drawn with the same position and size as its parent (the ChessBoardWidget). Something like this:
class ChessBoardWidget(RelativeLayout):
def __init__(self, **kwargs):
super(ChessBoardWidget, self).__init__(**kwargs)
with self.canvas.before:
Color(0, 1, 0, 1)
self.rect = Rectangle(size=(self.width, self.height), pos=self.pos)
self.add_widget(
Image(source="./data/images/chess-pieces/DarkerGreenGreyChessBoard.png", keep_ratio=True, allow_stretch=True))
Note that the green Rectangle will still be drawn at the lower left corner of the ChessBoardWidget with size of (100,100). To fix that, you either need to define the Rectangle in a kivy language file or string that gets loaded before the ChessBoardWidget gets created. Or you need to set up bindings that will redraw the Rectangle whenever the ChessBoardWidget gets re-sized.
I believe this is the easiest way to draw the green background, using Builder.load_string():
Builder.load_string('''
<ChessBoardWidget>:
canvas.before:
Color:
rgba: 0,1,0,1
Rectangle:
pos: 0,0
size: self.size
''')
class ChessBoardWidget(RelativeLayout):
def __init__(self, **kwargs):
super(ChessBoardWidget, self).__init__(**kwargs)
# with self.canvas.before:
# Color(0, 1, 0, 1)
# self.rect = Rectangle(size=(self.width, self.height), pos=self.pos)
self.add_widget(
Image(source="./data/images/chess-pieces/DarkerGreenGreyChessBoard.png", keep_ratio=True, allow_stretch=True))

Why Kivy ScrollView children of child are not scrollable?

This is a part of a python-script running kivy:
class someclass(Widget):
# code
# code
Clock.schedule_interval(self.timeandlog, 0.1)
self.x = 20
def timeandlog(self,dt):
if condition == True: #
self.ids.pofchild.add_widget(Label(text=logmsg, pos = (10, self.x)))
self.x = self.x + 10 ### just playing with position
condition = False
kv file:
<someclass>
#somelabels and buttons:
ScrollView:
do_scroll_x: False
do_scroll_y: True
pos: root.width*0.3, root.height*0.7
size: root.width*0.8, root.height*0.7
Widget:
cols: 1
spacing: 10
id: pofchild
Now I know the ScrollView accepts one Widget so I added just one with an id: pofchild then I added labels inside it with self.ids.pofchild.add_widget(Label() and changing every new label's pos with pos=(20, self.x) but the labels are not scrollable and only fill the widget height then stop appearing. What are right attributions so they will be scrollable?
In general, when you want a Widget to contain other Widgets, you should use a Layout Widget. A simple Widget does not honor size_hint or pos_hint, so the children of a simple Widget often end up with the default size of (100,100) and the default position of (0,0).
So, a good start is to change:
class someclass(Widget):
to something like:
class Someclass(FloatLayout):
Note that the class name starts with a capital letter. Although it does not cause any difficulties in your example, it can produce errors when you use kv and your classname starts with lower case.
Similarly, the child of the ScrollView is also normally a Layout. A possibility is GridLayout, like this:
GridLayout:
size_hint_y: None
height: self.minimum_height
cols: 1
spacing: 10
id: pofchild
Keys attributes here are the size_hint_y: None and height: self.minimum_height. They allow the GridLayout to grow as more children are added, and its height will be calculated as the minimum height needed to contain the children.
Then, you can add children like this:
self.ids.pofchild.add_widget(Label(text=logmsg, pos=(10, self.x), size_hint_y=None, height=50))
Since we are expecting the GridLayout to calculate its minimum height, we must provide an explicit height for its children, thus the size_hint_y=None, height=50.

How to change the background color of tree-view in kivy python?

I need help in changing the background color of the treeview on kivy.
I am working on the kivy framework in python that will list some labels.
But what happens while executing the application is, my apps background color is white and the tree-view gets the background-color from the application background.
Below is the sample-screenshot
Sample Code: To create tree view.
list_label=TreeView(root_options=dict(text='My root label'),hide_root=False)
list_label.add_node(TreeViewLabel(text='My first item'))
Add the following to your .py:
Builder.load_string('''
<TreeView>:
canvas.before:
Color:
rgba: 1, 0, 0, 1
Rectangle:
pos: self.pos
size: self.size
''')
This changes the background to red. You can replace 1, 0, 0, 1 with any color you prefer.
You can do this entirely in Python, but you will need to manually create bindings that kv creates for you automatically:
list_label=TreeView(root_options=dict(text='My root label'),hide_root=False)
with list_label.canvas.before:
Color(1, 0, 0, 1)
self.background_rect = Rectangle()
list_label.bind(pos=self.adjust_rect_pos)
list_label.bind(size=self.adjust_rect_size)
def adjust_rect_size(self, treeview, new_size):
self.background_rect.size = new_size
def adjust_rect_pos(self, treeview, new_pos):
self.background_rect.pos = new_pos
EDIT: I kind of answered the wrong question, my solution below changes the color of the even/odd nodes which overlay the background. I'll leave it here in case it's helpful.
Original Answer:
There are a couple ways to skin this cat. The easiest is to use the even_color and odd_color properties of the TreeViewNode widget. Here's how you would use that in your case:
list_label=TreeView(root_options=dict(text='My root label'),hide_root=False)
list_label.add_node(TreeViewLabel(text='My first item', \
even_color=[0.5,0.1,0.1,1],odd_color=[0.1,0.1,0.5,1]))
It would definitely be more DRY to create your own custom widget which is equally easy:
from kivy.uix.treeview import TreeViewLabel
from kivy.uix.button import Button
class MyTreeViewLabel(Button, TreeViewLabel):
def __init__(self, **kwargs):
super(MyTreeViewLabel, self).__init__(**kwargs)
self.even_color = [0.5,0.1,0.1,1]
self.odd_color = [0.1,0.1,0.5,1]
Then, your code would just be:
list_label=TreeView(root_options=dict(text='My root label'),hide_root=False)
list_label.add_node(MyTreeViewLabel(text='My first item'))

Change Kivy Slider Color

Is it possible to fully customize the Kivy slider? Specifically, how do you change the slider background color, add a slider "fill" color, and customize the knob image/color?
I've been struggling with the all week since there is not much on Kivy style customization online, so I decided to post this question/answer here for others. It turns out you need to essentially rewrite the Kivy style class for Sliders.
Using the default style sheet from Kivy's GitHub repo, I found the default style Slider class. I then created Color and BorderImage children of the canvas childer under the Slider class, copying the existing childen to start with. Remember that Color applies to the next children, so you would then need a total of 5 children under canvas if you are completely modifying the Slider. Also, I had to modify the size attribute of the BorderImage so that it dynamically changes as you move the knob.
If you are just interested in adding a "fill" color, here is my code:
from kivy.lang import Builder
Builder.load_string('''
<Slider>:
canvas:
Color:
rgb: 0, 0, 0
BorderImage:
border: (0, 18, 0, 18) if self.orientation == 'horizontal' else (18,
0, 18, 0)
pos: (self.x + self.padding, self.center_y - sp(18)) if self.orienta
tion == 'horizontal' else (self.center_x - 18, self.y + self.padding)
size: (max(self.value_pos[0] - self.padding * 2 - sp(16), 0), sp(36)
) if self.orientation == 'horizontal' else (sp(36), max(self.value_pos[1] - self
.padding * 2 - sp(16), 0))
source: 'atlas://data/images/defaulttheme/slider{}_background{}'.format(self.orientation[0], '_disabled' if self.disabled else '')
''')
Of course, you could just as easily move the string to a .kv file. Let me know if you need samples for the other two customizations, but they can be done similarly.

How to fix aspect ratio of a kivy game?

I've started playing with Kivy, but I'm rather new to all this and am already struggling.
I'm trying to make a board game - I want the window to be resizeable, but I don't want resizing the window to mess up the aspect ratio of the game (so in other words, I want the window to have black bars above or to the sides of the content if the window is resized to something other than the intended aspect ratio of the content)
The easiest way I could see to ensure this is to either:
a) Lock the aspect ratio of the Window itself so it's always 10:9 (the aspect ratio of the board and all on-screen elements) - even when fullscreen.
or
b) Use some sort of widget/surface/layout that is centered on the window and has a locked 10:9 aspect ratio. I then subsequently use this as the base onto which all my other images, widgets, grids etc are placed.
However, I really don't know how to do either of these. I'm not sure if I can lock the Window aspect ratio, and the only kivy-specific object I've found that lets me lock the aspect ratio is from kivy.graphics.image... which I don't seem to be able to use as a 'base' for my other stuff.
EDIT: So far I've written the code below: it creates a layout (and colours it slightly red) that 'fixes' its own aspect ratio whenever it is resized. However, it still isn't centered in the window, and more problematic, it results in an endless loop (probably because the aspect fix code corrects the size, but then kivy 'corrects' its size back to being the size of the window, triggering the aspect fix again, thought I could be wrong).
EDIT: Modified the code again, but it's still an endless loop. I though that referring only to the parent for size info would fix that, but apparently not.
I'd appreciate anyone helping me fix my code.
Code below:
test.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.image import Image
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.properties import (ObjectProperty,
NumericProperty,
ReferenceListProperty)
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Rectangle
class Board(AnchorLayout):
pass
class BackgroundLayout(RelativeLayout):
def FixAspectRatio(self, *args):
correctedsize = self.parent.size
if correctedsize[0] > correctedsize[1]*(10/9):
correctedsize[0] = correctedsize[1]*(10/9)
elif correctedsize[0] < correctedsize[1]*(10/9):
correctedsize[1] = correctedsize[0]/(10/9)
return correctedsize
class test(App):
game = ObjectProperty(None)
def build(self):
self.game = Board()
return self.game
if __name__ == '__main__':
test().run()
test.kv
<Board>
BackgroundLayout:
canvas.before:
Color:
rgba: 1, 0, 0, 0.5
Rectangle:
size: self.size
pos: self.pos
size: self.FixAspectRatio(self.parent.size)
pos: self.parent.pos
One approach would be to create a Layout that always maximize the children given an aspect ratio. Here is an example::
from kivy.lang import Builder
from kivy.app import App
from kivy.uix.relativelayout import RelativeLayout
from kivy.properties import NumericProperty
kv = """
ARLayout:
Widget:
canvas:
Color:
rgb: 1, 0, 0
Rectangle:
size: self.size
pos: self.pos
"""
class ARLayout(RelativeLayout):
# maximize the children given the ratio
ratio = NumericProperty(10 / 9.)
def do_layout(self, *args):
for child in self.children:
self.apply_ratio(child)
super(ARLayout, self).do_layout()
def apply_ratio(self, child):
# ensure the child don't have specification we don't want
child.size_hint = None, None
child.pos_hint = {"center_x": .5, "center_y": .5}
# calculate the new size, ensure one axis doesn't go out of the bounds
w, h = self.size
h2 = w * self.ratio
if h2 > self.height:
w = h / self.ratio
else:
h = h2
child.size = w, h
class TestApp(App):
def build(self):
return Builder.load_string(kv)
TestApp().run()
I managed to find a solution I was happy with with a lot of help from kived from the kivy chat.
The below bases the game bounds on a 'background' image, then places a Relative Layout over this background image. For all subsequent children, the coordinate (0, 0) will refer to the bottom left of the (aspect locked) background image, and they can use the 'magnification' property of the Background to adjust their size/position according to the screen size.
Provided any gamebounds.png image, the below code will implement this. Note that the RelativeLayout is shaded red (with 50% transparency) just to show its position (and show that it matches the gamebounds.png image size and position at all times):
test.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.image import Image
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.properties import (ObjectProperty,
NumericProperty,
ReferenceListProperty,
ListProperty)
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Rectangle
class Game(FloatLayout):
pass
class Background(Image):
offset = ListProperty()
magnification = NumericProperty(0)
class Bounds(RelativeLayout):
pass
class test(App):
game = ObjectProperty(None)
def build(self):
self.game = Game()
return self.game
if __name__ == '__main__':
test().run()
test.kv
<Game>
Background:
id: BackgroundId
#Provide the image that will form the aspect-locked bounds of the
#game. Image widgets have aspect_lock set to True by default.
source: 'gamebounds.png'
#All the image to stretch to fill the available window space.
allow_stretch: True
size: self.parent.size
#Find the coordinates of the bottom-left corner of the image from
#the bottom-left corner of the window. Call this the 'offset'.
offset: [self.center_x - (self.norm_image_size[0]/2),
self.center_y - (self.norm_image_size[1]/2)]
#Find out the factor by which the image is magnified/shrunk from
#its 'default' value:
magnification: self.norm_image_size[0] / self.texture_size[0]
Bounds:
#The canvas below isn't needed, it's just used to show the
#position of the RelativeLayout
canvas:
Color:
rgba: 1, 0, 0, 0.5
Rectangle:
size: self.size
pos: (0, 0)
#Set the position of the RelativeLayout so it starts at the
#bottom left of the image
pos: self.parent.offset
#Set the size of the RelativeLayout to be equal to the size of
#the image in Background
size: self.parent.norm_image_size

Categories