How to correctly use kivy graphics context instruction - python

Hi I recently tried my hands on kivy graphics, context instructions(rotate, ranslate etc). So i tried to implement an animated spinning wheel(used for example to show loading screen). I used garden.iconfonts package for this purpose following closely the example implemented in the package. Heres my code
.kv
<Convert>:
pos_hint: {'center_x':.5, 'center_y':.5}
size_hint: .8,.4
auto_dismiss: False
on_open:
self.load()
loading:loading
BoxLayout:
pos: root.pos
canvas.before:
Color:
rgba: 1,1,1,1
Rectangle:
pos: self.pos
size: self.size
source: 'icons/olivine.png'
orientation: 'vertical'
Label: #STATUS 20 PERCENT
text: 'Converting file...'
id: status
size_hint: 1,.2
markup: True
RelativeLayout: #picture or rolling 60 %
size_hint: 1,.6
Label: #SPINNER
text: '{}'.format(icon('icon-spin6', 32))
size_hint: 1,1
markup: True
p: 0
id: loading
canvas:
PushMatrix
Rotate:
angle: -self.p
origin: self.center
axis: 0,0,1
PopMatrix
.py
from kivy.uix.modalview import ModalView
from kivy.properties import ObjectProperty
from kivy.animation import Animation
class Convert(ModalView):
loading= ObjectProperty()
def load(self):
anim = Animation(p = 360, duration= 1) + Animation(p=0 , duration=0)
anim.repeat = True
anim.start(self.loading)
From my code Convert is a popup that shows up when a button is clicked, then as it opens, shows the spinning wheel.
But when i run the code it just shows the wheel(i.e the iconfont), but does not spin.
The code only works when i change the canvas class under the Label, to canvas.before. I assume that my understanding of how to use these tools is still poor. So im hoping someone can help clearify what im doing wrong, and how to make this work using canvas

canvas:
PushMatrix
Rotate:
angle: -self.p
origin: self.center
axis: 0,0,1
PopMatrix
Everything between Rotate and PopMatrix will be rotated - that's the point of PushMatrix and PopMatrix, they bound the region where any matrix transformations are applied.
In this case, you didn't put anything in beween them, so you don't see anything rotated.
You probably want to put PushMatrix and Rotate in the canvas.before, and PopMatrix in the canvas.after. Since the Label drawing occurs in canvas, this will then be in the rotated state.

Related

Is it possible to set the radius value in a ScrollView in Python/Kivy?

I need a rounded scrollview that rounds along with the children, I did a lot of research on this but failed, I didn't find anything about it.
Something like:
ScrollView:
do_scroll: [False, True]
radius: [dp(30),]
MDList:
id: list
This does not round the scrollview. Any idea how I can do thiswithout adding the RoundedRectangle chart?
Edit:
Adding the RoundedRectangle graph looks good, but it doesn't solve my problem. It is as if the children of the ScrollView exceeds the radius, the children do not fit in the radius.
ScrollView Code:
<RoundedScrollView#ScrollView>:
canvas.before:
Color:
rgba: 1, 0, 0, 1
RoundedRectangle:
pos: self.pos
size: self.size
radius: [0, 0, dp(80), 0]
Where I used ScrollView:
Screen:
RoundedSrollView:
MDList:
id: list
radius: [0, 0, dp(80), 0] #does not work
Result:
You can do something like this in your kv:
<RoundedScrollView#ScrollView>:
radius: 0
canvas.before:
Color:
rgba: 1, 0, 0, 1
RoundedRectangle:
pos: self.pos
size: self.size
radius: [self.radius]
This draws a red rounded rectangle as the background of a RoundedScrollView. You can add more Properties to the RoundedScrollView to control, for example, the color.
Well... After a long time (literally) I found a solution to define a radius to a ScrollView that automatically cuts the child. The solution is to redraw the ScrollView clipping using the canvas instructions StencilPush , StencilUse , StencilUnUse and StencilPop.
main.py
from kivymd.app import MDApp
from kivy.lang import Builder
KV = """
MDScreen:
MDScrollView:
do_scroll: [False, True]
size_hint: .5, .5
pos_hint: {"center":[.5, .5]}
canvas.before:
StencilPush
RoundedRectangle:
pos: self.pos
size: self.size
radius: [dp(50),]
StencilUse
canvas.after:
StencilUnUse
StencilPop
MDBoxLayout:
adaptive_height: True
size_hint_y: None
MDLabel:
text:
'The ScrollView accepts only one child and applies a viewport' \
'/window to it according to the scroll_x and scroll_y properties.' \
' Touches are analyzed to determine if the user wants to scroll or' \
' control the child in some other manner: you cannot do both at the' \
' same time. To determine if interaction is a scrolling gesture,' \
' these properties are used:'
adaptive_height: True
"""
class MyApp(MDApp):
def build(self):
return Builder.load_string(KV)
MyApp().run()
StencilPush is being used to save the current state of the Stencil and below it a rounded rectangle with the same dimensions and size as the ScrollView is being drawn.
StencilUse defines that this rounded rectangle will be the current Stencil.
After all the modifications have been made it is necessary to clean the "old" Stencil and set the modified Stencil as the default that was made under StencilPush. This is done using the StencilUnUse instruction and then the finalization, StencilPop, it is not mandatory but this ensures that all states are cleared and that performance is not affected.

How to target canvas.before in kv

In a button i have made a rounded button with canvas.before, and it changes colors as it should. The line is:
canvas.before:
Color:
rgba: btn_color_not_pressed if self.state=='normal' else btn_color_pressed
RoundedRectangle:
size: self.size
pos: self.pos
radius: [40]
The variables btn_color_not_pressed and btn_color_not_pressed are made with #:set in the start of the kv-file
I have tried to target the line with self.canvas.before.Color.rgba, as i am used to normally, but i get following error:
AttributeError: 'kivy.graphics.instructions.CanvasBase' object has no attribute 'Color'
How do i target that line from within kv and replace the variables ... or if necessary from the python file.?
How do i target the source: "some_file.jpg under Rectangle?
My goal is that when a user has clicked an option all the button colors (and maybe the background) in the app must change.
You cannot change the variables created in kv. Once your app is running, those variables no longer exist. You can, however, use a Property that is created in kv (or python) as an attribute of a class (or the App itself). Such Properties continue to exist while the App is running, and kivy recognizes such Properties and will automatically handle changes to those Properties. An example is to create a new class that extends Button and has Properties like you want:
<-MyButton#Button>:
# create the desired properties
btn_color_not_pressed: [.5, .5, .5,1]
btn_color_pressed: [.25, .25, .25, 1]
canvas:
Color:
# reference the above properties
rgba: self.btn_color_not_pressed if self.state=='normal' else self.btn_color_pressed
RoundedRectangle:
size: self.size
pos: self.pos
radius: [40]
# this is copied from style.kv to show the Button text
Color:
rgba: 1, 1, 1, 1
Rectangle:
texture: self.texture
size: self.texture_size
pos: int(self.center_x - self.texture_size[0] / 2.), int(self.center_y - self.texture_size[1] / 2.)
# actually make an instance of the new MyButton class
MyButton:
btn_color_not_pressed: [1,0,0,1]
btn_color_pressed: [0,1,0,1]
text: 'Button Test'
The <-MyButton#Button> creates a new class (MyButton) that extends Button. The prepended - indicates that the default canvas instructions for Button are not to be used and the provided instructions are used instead. Those new Properties can be changed in python code as usual. You can use a similar approach for the source property.
That brought me a step closer.
My problem now is that the colors only change the button itself or togglebutton-group, but only when you click on them. It only reacts to the new colors when activated (button og group).
The design is not updated
I tried the solution with the - but it made no difference
main.py
import kivy
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.behaviors import ToggleButtonBehavior
from kivy.factory import Factory
kivy.require("1.11.1")
class Controller(BoxLayout):
def __init__(self):
super(Controller, self).__init__()
ToggleButtonBehavior.allow_no_selection = False
def change_button_color(self,theme):
if theme == "red":
Factory.My_tgl_btn.btn_color_pressed = (1,0,0,.7)
Factory.My_tgl_btn.btn_color_not_pressed = (1,0,0,.5)
else: # theme == "blue":
Factory.My_tgl_btn.btn_color_pressed = (0,0,1,.7)
Factory.My_tgl_btn.btn_color_not_pressed = (0,0,1,.5)
class mainApp(App):
def build(self):
return Controller()
if __name__ == "__main__":
mainApp().run()
main.kv
#:set bg_color 1,1,1,.7
#:set txt_color 0,0,0,1
#:import Factory kivy.factory.Factory
<Controller>
BoxLayout:
orientation: "vertical"
background_color: 1,1,1,.5
background_normal: ""
Label:
text: "THIS IS THE MAIN TEKST"
color: txt_color
size_hint_y:.7
BoxLayout:
size_hint_y: .15
My_tgl_btn:
text: "RED theme"
group: 1
state: "down"
on_press: root.change_button_color("red")
on_release: root.change_button_color("red")
My_tgl_btn:
text: "Blue theme"
group: 1
on_press: root.change_button_color("blue")
on_release: root.change_button_color("blue")
BoxLayout:
size_hint_y: .15
My_tgl_btn:
text: "Option1"
group: 2
state: "down"
My_tgl_btn:
text: "Option2"
group: 2
state: "normal"
<My_tgl_btn#ToggleButton>
btn_color_pressed: 1,0,0,.7
btn_color_not_pressed: 1,0,0,.5
color: txt_color
background_color: 0,0,0,0
background_normal: ""
canvas.before:
Color:
rgba:self.btn_color_not_pressed if self.state=='normal' else self.btn_color_pressed
RoundedRectangle:
size: self.size
pos: self.pos
radius: [40]
Found a solution (here: Kivy: resetting toggle buttons to "normal" on re-entering screen)
It's kind'a ugly, but it works..
Give every button an id ... and then use on_enter for each button and set and change the state.
In the code above it would mean:
on_enter:
button1.state = "down"
button1.state = "normal"
button2.state = "down"
button2.state = "normal"
button3.state = "down"
button3.state = "normal"
button4.state = "down"
button4.state = "normal"
It works ... but it is not pretty :|

How to use pos_hint with FloatLayout in kivy?

I am trying to align labels and button in my test UI this is my kv file
<test>:
Label:
text: "foo"
color: 0,1,0,1
#pos:120,20
pos_hint:{"right":0.1,"top":1}
Label:
text:"boo"
color: 0,0,1,1
#pos:80,20
pos_hint:{"right":0.1,"top":0.5}
Label:
text:"bar"
color: 1,0,0,1
#pos:20,120
pos_hint:{"right":0.1,"top":0.1}
Button:
text:"goo"
size_hint:0.1,0.1
I am able to succesfully create labels foo,boo and bar using pos but when I used pos_hint it returns blank output?
You are getting "blank" output because the text of the labels is off screen (and the labels themselves are transparent).
Since your layout <test> has no size_hint so it takes on the
default of (1,1) which makes it the size of the Window (which is
800 x 600).
Your labels also don't have a size_hint so they default to the size of their parent - the layout, so they end up having size [800, 600]. The text in the labels is centered by default, and their background is transparent. (maybe you should try this with buttons first so you have a visual representation of the sizes)
Thus, the text a label with pos = (0,0) will appear in the center of the screen
Then we have the pos_hint taking different arguments (the below description might not be accurate for things outside of a FloatLayout):
pos_hint:{"right":v1,"top":v2} sets the pos to (self.parent.right*v1 - self.width, self.parent.top*v2 - self.height) - you are setting the top and right of the widget you are placing. Thus your labels get such negative coordinates that their texts never appear on screen (because bottom left is 0,0)
then we have pos_hint:{"x":v1,"y":v2} (which you may find more useful for your case), and pos_hint:{"center_x":v1,"center_y":v2}. You should be able to figure out how they work bearing in mind that the size affects how things looks, since pos only sets the bottom left coordinate.. you can play around with this .kv file:
#:kivy 1.0.9
<test>:
#size: (500, 500)
#size_hint:(None, None)
canvas:
Color:
rgb: 1,0,0
Rectangle:
size: (5,5)
pos: (0,0)
Widget:
id:wig
pos: (250,250)
canvas:
Color:
rgb: 1,1,1
Rectangle:
size: (5,5)
pos: self.pos
Label:
id: boo
text:"boo"
color: 0,0,1,1
#size_hint:(1,1)
pos_hint:{"center_x":1,"center_y":1}
Label:
id: foo
text: "foo"
color: 0,1,0,1
#size_hint: (.6,.6)
pos_hint:{"x":1,"y":1}
Label:
id: bar
text:"bar"
color: 1,0,0,1
#size:(500,500)
#size_hint:(None, None)
pos_hint:{"right":1,"top":1}
#pos:100, 10
Button:
text:"goo"
size_hint:0.1,0.1
pos:(1,1)
#some debug info, i know the code is ugly
on_press: print self.parent.size,'\n', self.parent.right, self.parent.top, self.parent.x, self.parent.y, self.parent.center_x, self.parent.center_y, "\n","bar_right_top:", bar.pos,"foo_x_y:", foo.pos,"boo_center:", boo.pos, "\nwhite square:", wig.pos, "\n", bar.size, foo.size, boo.size

What is a good way to handle widget placement to account for screen rotation using python / kivy?

When I build an application using kivy, I can get everything where I want it, but when I rotate the screen from portrait to landscape, my widgets start colliding with one another. What is a good way to prevent this from happening?
In the attached example, I was able to bind the placement of a settings button to my header label widget, but I was unsuccessful in getting my scrollview to bind to it so when it rotates it stays X amount of space away from the bottom of the label.
Python:
class First_Screen(Screen):
def __init__(self, **kwargs):
super(First_Screen, self).__init__(**kwargs)
list_of_buttons = [
'button1',
'button2',
'button3',
'button4',
'button5',
'button6 ',
'button7',
'button8',
]
class My_Grid(FloatLayout, ScrollView):
grid = ObjectProperty(None)
def on_grid(self, *args):
for btn in list_of_buttons:
btn = Button(
text = btn,
)
btn.bind(on_press= First_Screen.print_message)
self.grid.add_widget(btn)
def print_message(self):
print ('Congratulations! You have clicked a button!')
class ScreenManagement(ScreenManager):
pass
class Stack_Overflow(App):
def build(self):
sm = ScreenManager(transition = FadeTransition())
sc0 = First_Screen(name = 'first_screen')
sm.add_widget(sc0)
return sm
if __name__ == '__main__':
Stack_Overflow().run()
kv:
ScreenManagement:
First_Screen:
<ImageButton#ButtonBehavior+Image>:
<First_Screen>:
name: 'first_screen'
Label:
id: header
text: 'header'
pos_hint: ({'left' : 1, 'top' : 1})
size_hint: (1,None)
height: dp(50)
canvas.before:
Color:
rgba: (.6, .6, .6, 1)
Rectangle:
pos: self.pos
size: self.size
ScrollView:
size_hint: (1,.8)
pos_hint: ({'center_x' : .5, 'center_y' : .5})
My_Grid:
grid: grid
GridLayout:
id: grid
cols: 1
size_hint_y: None
row_default_height: "40dp"
height: self.minimum_height
ImageButton:
id: settings
size_hint: None, None
height: dp(30)
width: dp(30)
pos: header.x, header.y + 10
pos_hint: {'right': 1}
source: 'settings_black.png'
Images: portrait, landscape
Thanks!
Edit: I misunderstood the question, I thought you were unsatisfied with the Image. In the case of ScrollView the problem isn't the ScrollView itself, because that uses relative positions and sizes.
The problem is the Label(header), which has height set as an absolute number i.e. 50 dense pixels. It means that whatever screen you have or however you size anything else, this widget will have 50 pixels corrected with dpi of the screen.
In reality this means that if your screen reaches 400x100 resolution, your Label will take half of the screen and ScrollView will be placed on top of that (most likely you won't see the Label then).
To fix that you have two options. If you want to play with FloatLayout alone, you'll need to use correct positioning/sizing i.e. relative and make sure widgets aren't overlaping.
Otherwise just drop the widgets to BoxLayout or a similar other layout that manages such things on its own:
class First_Screen(BoxLayout, Screen):
def __init__(self, **kwargs):
super(First_Screen, self).__init__(orientation='vertical', **kwargs)
or a little bit different (and better looking):
class First_Screen(BoxLayout, Screen):
def __init__(self, **kwargs):
and in kv:
<First_Screen>:
name: 'first_screen'
orientation: 'vertical'
Also note, that there's a screen module, which makes testing such things a way more easier than dropping the code to apk and to android when it's just unnecessary. Example:
python main.py -m screen:note2,landscape,scale=.5

Kivy behavior of drawing in canvas, to change it's background

I'm trying to set the background of my layout with the following kv language:
<RootWidget>:
Menu:
orientation: 'vertical'
Button:
text: 'btn1'
on_release: print('btn1')
Button:
text: 'btn'
on_release: print('btn2')
BoxLayout:
BoxLayout:
canvas.before:
Rectangle:
pos: self.pos
size: self.size
source: 'main_background.png'
ToggleMenuButton:
on_release: root.toggle_state()
size_hint: None, None
size: 60, 60
background_color: 0, 0, 0, 0
Image:
size: self.parent.size
pos: self.parent.pos
allow_stretch: True
source: './data/images/white_menu.png'
And this is my code:
from kivy.app import App
from kivy.garden.navigationdrawer import NavigationDrawer
from layouts.menu import Menu
from layouts.menu import ToggleMenuButton
from layouts.content import Content
class RootWidget(NavigationDrawer):
pass
class MainApp(App):
def build(self):
self.root = RootWidget()
return self.root
if __name__ == '__main__':
MainApp().run()
The Menu and Content classes are Kivy BoxLayout, the ToggleMenuButton is a Kivy Button.
When I run this code I get the following screen:
The background should be my image, not black. If I but the white_menu.png as my background, I also get a black braground. But, if put the following code:
BoxLayout:
orientation: 'vertical'
BoxLayout:
orientation: 'vertical'
canvas.before:
Rectangle:
pos: self.pos
size: self.size
source: 'main_background.png'
I get the following:
Like you can notice on the bottom, the image is transparent, with 3 white rectangles, but on the main layout the image is red. I kwnow that it is red, because the rgba code, but why the image changes it's color?
If I put the background on canvas.after it works correctly, I can see my background, but I can't see the widgets.
Summing up, I want to understand what is happening? Why I cant see the changes on canvas.before? Why/How putting a color on my canvas changes my image?
If I change, the RootWidget to be a BoxLayout instead of a NavigationDrawer, it works correctaly. Why? Why changing the canvas of a child of a child of a NavigationDrawer does not work?

Categories