I encountered some issue related to scatter object.
From my codes below. After I resize a Scatter (self.size_hint_x, self.size_hint_y = 0.3, 0.3), the objects (canvas, label) inside the Scatter is not resized as well. I did tried to apply size_hint=1 to the Canvas and Label inside the Scatter, however the result still the same.
Another issue I encountered is the retrieving of the X, Y position (relative to the parent) for the Canvas/Label in a Scatter. It always give me (0,0).
My Code
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.scatter import Scatter
from kivy.graphics import Color, Rectangle, Canvas
class Avatar(Scatter):
def __init__(self, **kwargs):
super(Avatar, self).__init__(size_hint=(None,None), **kwargs)
with self.canvas:
Color(0, 0, 0)
Rectangle(pos=(self.x, self.y), size=(self.width, self.height))
self.lbl = Label(text='Test', size_hint_x=1, size_hint_y=1)
self.add_widget(self.lbl)
# Scatter size is 30% of the GameBackground
# ISSUE: After resize my Scatter, the objects inside is not resized as well.
self.size_hint_x, self.size_hint_y = 0.3, 0.3
class GameBackground(FloatLayout):
def __init__(self, **kwargs):
super(GameBackground, self).__init__(**kwargs)
with self.canvas:
Color(1, 0, 1)
Rectangle(pos = (0, 0), size = (Window.width,Window.height))
self.elf = Avatar()
self.add_widget(self.elf)
self.elf.x = 200
self.elf.y = 300
# Get the X, Y position of the Scatter and the label inside the Scatter relative to the parent.
print self.elf.pos #<-- This works.
print self.elf.lbl.pos #<-- ISSUE: This not working.
class GameApp(App):
def build(self):
return GameBackground()
if __name__ == '__main__':
GameApp().run()
Did I miss something? Thanks for any advise.
I'm new to Kivy. So pardon me if my qns is dumb. :P
Did you read the documentation for Scatter at all. It says that
...specific behavior makes the scatter unique, and there are some advantages / constraints that you should consider:
The children are positioned relative to 0, 0. The scatter position has no impact of the position of children.
This applies to the size too. If you want to resize the scatter, use scale, not size. (read #1.)
That answer your first question. It says use scale, not size. There is also the the method apply_transform that you may find useful for other transformations. I have never tried this method though but I cannot see the translate value (I can see Rotate and Scale)
Regarding to your second question. You are adding a Rectangle in the self.x and self.y position that is (0,0). So, your Rectangle is in that position. If you drag (using your fingers or mouse) your widget. The position of the Rectangle remains relative to the Scatter. So, unless you change the position of the Rectangle (with code) it will be always be in the (0,0). The transformations are always relative to the Scatter.
This question might be related and explains a few issues with not using the Kivy Language to add Vertex Instructions (i.e. Rectangles). You should consider it because what you are doing seems to be related.
* EDIT - just the necessarily corrections according to my understanding of what you are trying to achieve *
1) Don't use size hints like the ones you are using.
1.1) Instead of:
self.lbl = Label(text='Test', size_hint_x=1, size_hint_y=1)
use:
self.lbl = Label(text='Test', width=self.width, height=self.height)
1.2) And, instead of:
self.size_hint_x, self.size_hint_y = 0.3, 0.3
use:
self.scale = 0.3
2) The position is relative to the scatter. Therefore you need to transale the coordinates to the parent.
2.1) Instead of:
print self.elf.lbl.pos #<-- ISSUE: This not working.
use:
print self.elf.to_parent(*self.elf.lbl.pos)
Here is the complete code:
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.scatter import Scatter
from kivy.graphics import Color, Rectangle, Canvas
class Avatar(Scatter):
def __init__(self, **kwargs):
super(Avatar, self).__init__(size_hint=(None,None), **kwargs)
with self.canvas:
Color(0, 0, 0)
Rectangle(pos=(self.x, self.y), size=(self.width, self.height))
#self.lbl = Label(text='Test', size_hint_x=1, size_hint_y=1)
self.lbl = Label(text='Test', width=self.width, height=self.height)
self.add_widget(self.lbl)
# Scatter size is 30% of the GameBackground
# ISSUE: After resize my Scatter, the objects inside is not resized as well.
# self.size_hint_x, self.size_hint_y = 0.3, 0.3
self.scale = 0.3
class GameBackground(FloatLayout):
def __init__(self, **kwargs):
super(GameBackground, self).__init__(**kwargs)
with self.canvas:
Color(0, 0, 1)
Rectangle(pos = (0, 0), size = (Window.width,Window.height))
self.elf = Avatar()
self.add_widget(self.elf)
self.elf.x = 200
self.elf.y = 300
# Get the X, Y position of the Scatter and the label inside the Scatter relative to the parent.
print self.elf.pos #<-- This works.
print self.elf.lbl.pos #<-- ISSUE: This not working.
print self.elf.to_parent(*self.elf.lbl.pos)
class GameApp(App):
def build(self):
return GameBackground()
if __name__ == '__main__':
GameApp().run()
Related
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))
I've got the following code snippet that will draw a board. I've read about RelativeLayout and seen tutorials for manipulating the .kv file, but no explanation on how to relativize the coordinates to the widget with just Python code.
from kivy.uix.widget import Widget
from kivy import graphics
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.app import App
class Chessboard(Widget):
def __init__(self, **kwargs):
super(Chessboard, self).__init__(**kwargs)
self.bind(pos=self.drawBoardandPieces)
self.bind(size=self.drawBoardandPieces)
self.drawBoardandPieces()
def drawBoardandPieces(self, *args):
with self.canvas:
# Reset the canvas in case of redraws.
self.canvas.clear()
# Define the lengths of the edges of the squares.
edge_len = min(self.height, self.width) // 8
for column in range(0, 8):
for row in range(0, 8):
if ((row + column) % 2) == 0:
graphics.Color(0, 0, 1)
self.dark_rect = graphics.Rectangle(pos=(column*edge_len, row*edge_len), size=(edge_len, edge_len))
else:
graphics.Color(1, 1, 1)
self.light_rect = graphics.Rectangle(pos=(column*edge_len, row*edge_len), size=(edge_len, edge_len))
def on_touch_down(self, touch):
print(touch.pos)
class SCApp(App):
def build(self):
app_layout = BoxLayout(orientation="vertical")
app_layout.add_widget(widget=Chessboard())
play_again_btn = Button(text="Play Again", size_hint=(1, 1))
app_layout.add_widget(widget=play_again_btn)
return app_layout
SCApp().run()
Instead of what this code renders, I need the board drawn on the canvas to appear above the button in its own BoxLayout container.
Also, will click event need to be contained in the RelativeLayout container, as well, to work only in the context of the board?
Note that the documentation specifies:
Kivy drawing instructions are not automatically relative to the
widgets position or size.
So you must also handle the position of your chessboard. You can handle the position using something like:
def drawBoardandPieces(self, *args):
with self.canvas:
# Reset the canvas in case of redraws.
self.canvas.clear()
# Define the lengths of the edges of the squares.
edge_len = min(self.height, self.width) // 8
for column in range(0, 8):
for row in range(0, 8):
if ((row + column) % 2) == 0:
graphics.Color(0, 0, 1)
self.dark_rect = graphics.Rectangle(pos=(self.width/2 - 4*edge_len + column*edge_len, self.y + row*edge_len), size=(edge_len, edge_len))
else:
graphics.Color(1, 1, 1)
self.light_rect = graphics.Rectangle(pos=(self.width/2 - 4*edge_len + column*edge_len, self.y + row*edge_len), size=(edge_len, edge_len))
This uses self.y as the bottom of the chess board and calculates the x based on the width of the Widget and the edge_len. Probably should also add self.x to the x position, but that is zero in your case.
In general, your on_touch_down() method should check if the touch is on your Widget as stated in the documentation. If you determine that no other Widgets need to know about that touch, your on_touch_down() method can return True, otherwise it should return False.
I solved it. Here's the corrected code in the app. All you need to do is to place the widget into its own RelativeLayout. Then, the RelativeLayout widget can, itself, be placed inside of a box in BoxLayout, a grid position in GridLayout, etc.
from kivy.uix.relativelayout import RelativeLayout
# All of that other code.
class SCApp(App):
def build(self):
app_layout = BoxLayout(orientation="vertical")
chessboard_layout = RelativeLayout()
chessboard_layout.add_widget(widget=Chessboard())
app_layout.add_widget(widget=chessboard_layout))
play_again_btn = Button(text="Play Again", size_hint=(1, 1))
app_layout.add_widget(widget=play_again_btn)
return app_layout
I am trying update Label on canvas , but it changed only if I call update() method from MyApp(APP).
And what is interesting that method obj Slider (on touch) works perfectly
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.uix.label import Label
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.graphics import (Color, Ellipse)
from kivy.uix.slider import Slider
import random
from functools import partial
class Canvas_w(Widget):
def __init__(self, **kwargs):
self.i = 0
super(Canvas_w, self).__init__(**kwargs)
self.label_text =Label(text= "0",color = (255,0,0))
def update_text(self, text):
self.label_text.text = text
def on_touch_down(self, touch):
with self.canvas:
d=30
self.i+=1
Color(1., 0, 0)
Ellipse(pos=(touch.x - d/2, touch.y - d/2), size=(20,20))
self.label_text=str(self.i)
class Label_l(GridLayout):
bones = 0
def __init__(self, **kwargs):
super(Label_l, self).__init__(**kwargs)
self.label_text =Label(text= "0")
self.add_widget(self.label_text)
#self.label_text.text=self.C.i
def update_text(self, text):
Label_l.bones=text
self.label_text.text = str(Label_l().bones)#
Label_l.text=str(Label_l().bones)
class WidgetContainer(GridLayout):
sld_value = 0.00001
def __init__(self, **kwargs):
super(WidgetContainer, self).__init__(**kwargs)
self.cols = 1
self.speedControl = Slider(min=0.00001, max=3, step=0.0001)
self.add_widget(Label(text="Speed Iteration"))
self.add_widget(self.speedControl)
self.add_widget(Label(text=''))
self.speedValue = Label(text="0.00001")
self.add_widget(self.speedValue)
self.speedControl.bind(value=self.on_value)
def on_value(self, instance, speed):
self.sld_value = speed
WidgetContainer.sld_value = self.sld_value
self.speedValue.text = str(round(speed,5))
class MyApp(App):
def build(self):
b3 = BoxLayout(orientation="horizontal")
b1 = FloatLayout()
self.painter = Canvas_w()
self.label_l= Label_l()
b1.add_widget(self.label_l)
b1.add_widget(self.painter)
b1.add_widget(WidgetContainer())
b3.add_widget(b1)
return(b3)
if __name__=="__main__":
MyApp().run()
I am trying update Label on canvas , but it changed only if I call update() method from MyApp(APP).
And what is interesting that method obj Slider (on touch) works perfectly
A few problems with your code:
The color attribute of a Label requires a list of four values with each value in the range 0 to 1. Change self.label_text =Label(text= "0",color = (255,0,0)) to self.label_text =Label(text= "0",color = (1,0,0,1)) in the Canvas_w class.
The above Label is never getting drawn. You need to add self.add_widget(self.label_text) to the __init__() method of Canvas_w.
You have another Label in the Label_l class that is being drawn on top of the Label in the Canvas_w.
After fixing the above problems, I believe your code will work as you expect.
If you call self.canvas.clear(), it appears as though the label_text child of Canvas_w is removed. Actually it is not removed, but is just not visible (perhaps overdrawn). One way to make it visible is to just make sure it is drawn last by removing it and then re-adding it (last added child is drawn last), like this:
self.canvas.clear()
self.remove_widget(self.label_text)
self.add_widget(self.label_text)
After a bit more research, I believe that the canvas.clear() is removing the drawing of the Label from the Canvas. You can, however, do your drawing on canvas.before by using:
with self.canvas.before:
in the on_touch_down() method. And rather than using:
self.canvas.clear()
use:
self.canvas.before.clear()
I believe that your Label will be displayed correctly after doing the above.
I have some simple code below that is just creating some rectangles with a color assigned to them, and then storing them into a FloatLayout. For some reason, the very first rectangle 'Brick' that I create, doesnt get a color, but all subsequent ones do. I have issues with my game also where when another widget collides with a brick, it updates the attributes of the brick to its left, and not itself. I think the two issues are related.
What is going on with the first instance of Brick (brick1) that is being added to the FloatLayout that it doesnt get the color created?
import kivy
kivy.require('1.10.0')
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.core.window import Window
from kivy.properties import NumericProperty, ReferenceListProperty
from kivy.clock import Clock
from kivy.graphics import Rectangle, Ellipse, Color
from kivy.uix.floatlayout import FloatLayout
from kivy.vector import Vector
from kivy.utils import get_color_from_hex
import random
from kivy.config import Config
Window.size = (300,600)
class Brick(Widget):
def __init__(self, xloc, yloc, **kwargs):
super().__init__(**kwargs)
with self.canvas:
self.size = (25,25)
self.x = xloc
self.y = yloc
self.pos = (self.x,self.y)
self.body = Rectangle(pos=self.pos,size = self.size)
self.c = Color(1,0,1)
class Game(Widget):
def __init__(self,**kwargs):
super().__init__(**kwargs)
self.brick_container = FloatLayout(size = (25,25))
brick1 = Brick(50,100)
brick2 = Brick(100,100)
self.brick_container.add_widget(brick1)
self.brick_container.add_widget(brick2)
self.add_widget(self.brick_container)
def update(self,dt):
self.name = 'nothing'
class MyApp(App):
def build(self):
game = Game()
Clock.schedule_interval(game.update, 1.0/60.0)
return game
if __name__ == '__main__':
MyApp().run()
You have to set the color first and then the rectangle
class Brick(Widget):
def __init__(self, xloc, yloc, **kwargs):
super().__init__(**kwargs)
self.size = (25,25)
self.pos = (xloc , yloc)
with self.canvas:
self.c = Color(1,0,1)
self.body = Rectangle(pos=self.pos,size = self.size)
According to the docs when using a Drawing Instruction, use the color set above.
Drawing instructions
Drawing instructions range from very simple
ones, like drawing a line or a polygon, to more complex ones, like
meshes or bezier curves:
with self.canvas:
# draw a line using the default color
Line(points=(x1, y1, x2, y2, x3, y3))
# lets draw a semi-transparent red square
Color(1, 0, 0, .5, mode='rgba')
Rectangle(pos=self.pos, size=self.size)
For that reason in your original code the first Brick used the color by default(white), and the others if they had the correct color.
I am trying to construct a widget in the middle of the screen. I am not using pos_hint or size_hint because I will be altering the widget's position later but when I construct the widget, its size and position is not correct. Here is my code:
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.widget import Widget
from kivy.graphics import *
from kivy.clock import Clock
class Sprite(Widget):
def __init__(self, **kwargs):
super(Sprite, self).__init__(**kwargs)
Clock.schedule_interval(self.update_canvas, 1.0/60)
def update_canvas(self, dt):
with self.canvas:
Rectangle(size=self.size, pos=self.pos)
class RootWidget(FloatLayout):
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
self.add_widget(Sprite(
size=(Window.width*0.1, Window.height*0.1),
center=(Window.width*0.5, Window.height*0.5)
))
class MyApp(App):
def build(self):
app = RootWidget()
return app
if __name__=="__main__":
MyApp().run()
Why is the widget's size not equal to 1/10th of the window size and why is it's center at the top right corner of the window?
The size hint defaults to a value unless you define it explicitly and it takes priority over the size property. You may not want to want to use the size hint functionality but unless you disable it, the size that you set manually will be overridden by default. Try adding the following modification and compare the result with and without the size_hint line:
self.add_widget(Sprite(
size_hint=(None,None),
size=(Window.width*0.1, Window.height*0.1),
center=(Window.width*0.3, Window.height*0.5)
))
You can see the actual dimensions set by using the inspector: python myscript.py -m inspector and hitting control+E in the kivy window
reference: http://kivy.org/docs/api-kivy.modules.inspector.html
center is an alias for (x + width/.2, y + height/.2) but the width and height default to 100. At the time of creation these default measurements are what are used for the reverse calculation to set x and y when you make your call so if your height and width are different from 100 then you'll be offset by the difference. Use x and y directly (i.e. pos=(..)) when instantiating, not center.