Correctly use Kivy Canvas in Python - python-3.x

Assuming I define a BoxLayout but want to add, say a blue background to it, in Kivy it would look something like this:
BoxLayout:
canvas.before:
Color:
rgb: 0, 0, 1
Rectangle:
size: self.size
pos: self.pos
I tried to do this in Python like this:
box = BoxLayout()
with box.canvas.before:
Color(rgb=(0, 0, 1))
Rectangle(size=box.size, pos=box.pos)
This does draw a rectangle, but not in the correct size or position. My question is: is there a way to create a Rectangle (or another way to add a background to a BoxLayout) the same size and position of the defined BoxLayout? The closest I have ever gotten is physically setting the size and shape, but I would like this Rectangle to be dynamically resizable so that I don't have too many values hard-coded. Thanks in advance!

Typically, that code would appear in some method of a class, so a reference to the Rectangle could be saved as, for example, self.bg. So it would like something like this:
class MyBackground(FloatLayout):
def __init__(self, **kwargs):
super(MyBackground, self).__init__(**kwargs)
self.box = BoxLayout(size_hint=(0.5, 0.5))
with self.box.canvas.before:
Color(rgba=(1, 0, 0, 1))
self.bg = Rectangle(pos=self.pos, size=self.size)
# bindings to keep size and position of the Rectangle up to date
self.box.bind(pos=self.update_bg)
self.box.bind(size=self.update_bg)
# add box to this layout
self.add_widget(self.box)
def update_bg(self, *args):
self.bg.pos = self.box.pos
self.bg.size = self.box.size

Related

How to get the image coordinates with tkinter and not the canvas coordinates

So, I am using this code: Tkinter: How to scroll an entire canvas using arrow keys?. And this guy's code: Selecting an area of an image with a mouse and recording the dimensions of the selection and more specifically this:
def get_mouse_posn(event):
global topy, topx
topx, topy = event.x, event.y
def update_sel_rect(event):
global rect_id
global topy, topx, botx, boty
botx, boty = event.x, event.y
canvas.coords(rect_id, topx, topy, botx, boty) # Update selection rect.
The idea is that I depict a big image, that does not fit in my laptop's screen. It is 10.000 * 9.000 pixels. So by using the arrows: Up, Down, Right, Left from my keyboard I can navigate throughout the image. Everything works fine up to here. But, when I use mouse click I use the guy's code in order to get the pixel coordinates (x,y). The problem is that I get the canvas' coordinates, so even I navigate to the top down of the image, when I place the mouse at the upper left corner it will give me 0,0. The latter are the canvas' coordinates and not the image's coordinates. I searched on google, and I found this suggestion: Coordinates of image pixel on tkinter canvas in OOP which does not bring something new. The rationale is more or less implemented in the first code (link that I posted). So, how can I get the image's coordinates and not the canvas' coordinates? I cannot post minimum runnable code, things are too complex and the whole code contains many lines....
You can actually create a function that will check the anchor position of the image, based on the tag you give it(or the Id) and then give the image coordinate based on the clicked canvas coordinate.
def canvas_to_image_cords(canvas: Canvas, x: int, y: int, image: PhotoImage, tagOrId=''):
anchor = 'center'
if tagOrId:
anchor = canvas.itemcget(tagOrId, 'anchor')
w, h = canvas.winfo_reqwidth(), canvas.winfo_reqheight()
if anchor == 'center':
img_xpos, img_ypos = image.width()/2, image.height()/2
start_x, start_y = img_xpos-w/2, img_ypos-h/2
elif anchor == 'nw':
start_x, start_y = 0, 0
# And so on for different anchor positions if you want to make this more modular
req_x, req_y = start_x+x, start_y+y
return req_x, req_y
The way this would work in 'center' anchor is because image is centered and kept in the canvas, so now you can find out where the image starts from the width/height of the canvas. And then you will have to subtract it from the anchor point. By default, the anchor of a canvas image is center, so you may not have to create it for all the other anchor positions if you leave the anchor option empty.
Usage:
from tkinter import * # Avoid this and use import tkinter as tk
from PIL import Image, ImageTk
root = Tk()
root['bg'] = 'black'
def callback(e):
print()
x,y = canvas_to_image_cords(canvas=canvas, x=e.x, y=e.y, image=img, tagOrId='img') # Can also pass img_tag as tagOrId
print(x,y)
def canvas_to_image_cords(canvas: Canvas, x: int, y: int, image: PhotoImage, tagOrId=''):
anchor = 'center'
if tagOrId:
anchor = canvas.itemcget(tagOrId, 'anchor')
w, h = canvas.winfo_reqwidth(), canvas.winfo_reqheight()
if anchor == 'center':
img_xpos, img_ypos = image.width()/2, image.height()/2
start_x, start_y = img_xpos-w/2, img_ypos-h/2
elif anchor == 'nw':
start_x, start_y = 0, 0
req_x, req_y = start_x+x, start_y+y
return req_x, req_y
img = ImageTk.PhotoImage(Image.open('hero-big.png')) # 10000x9000 pixel image
canvas = Canvas(root, highlightthickness=0)
canvas.pack(padx=20, pady=20)
img_tag = canvas.create_image(0, 0, image=img, tag='img') # By default anchor is center
canvas.bind('<1>', callback)
root.mainloop()
Hopefully the color coded image will explain this better.
Our current image position is at the green circle at the center and we need it to be the top left corner of the canvas(the black & white circle). So we need to push back(subtract) half of canvas's width/height inorder to reach the black & white circle. And then you can add the x,y coordinate you need and continue to get the image position you need.
And why do we need to push back to the top left corner? Because e.x and e.y starts from top left corner. It is rather easy(IMO) to find the image coordinate at the top left corner of the canvas, than to make e.x and e.y work with the image coordinate at the center.
Note that (event.x, event.y) is the screen coordinates relative to the top-left corner of the canvas. To get the real canvas coordinates, you can use canvas.canvasx() and canvas.canvasy():
# get the real coordinates in the canvas
# note that canvasx() and canvasy() return float number
x, y = int(canvas.canvasx(event.x)), int(canvas.canvasy(event.y))
So if the top-left corner of the image is at (img_x, img_y) inside the canvas, then the coordinates in the image will be (x-img_x, y-img_y).
To apply on your posted code:
def get_mouse_posn(event):
global topx, topy
topx, topy = int(canvas.canvasx(event.x)), int(canvas.canvasy(event.y))
def update_sel_rect(event):
global botx, boty
botx, boty = int(canvas.canvasx(event.x)), int(canvas.canvasy(event.y))
canvas.coords(rect_id, topx, topy, botx, boty)
To get the selected region in the image:
def get_selected_region():
global topx, topy, botx, boty
# make sure topx < botx and topy < boty
topx, botx = min(topx, botx), max(topx, botx)
topy, boty = min(topy, boty), max(topy, boty)
# top-left corner of image is at (img_x, img_y) inside canvas
region = (topx-img_x, topy-img_y, botx-img_x, boty-img_y)
return region

How to put a Lable on a Rectangle in Kivy

I am quite a novice programming with kivy, and I just wanted to put a green rectangle under a label to make it look a little bit like a display. I've tried making a label and then, inside of it, a canvas with the rectangle, but when it's time to put them together, the self.pos doesn't put them on the same place... Here's part of the code:
'''.kv file'''
Label:
id: s_label
size:50,50
text:'[font=Digital-7][color=D20000][size=24] S= [/color][/font][/size]'
markup:True
pos_hint:{'x':0.3,'y':-0.2}
canvas:
canvas.before:
Color:
rgba: 0, 153, 0, 0.5
RoundedRectangle:
pos: self.pos[0] , self.pos[1]
size: (self.width/6, self.width/16)
id: S_rect
The Label is inside a FloatLayout, but I don't know if that is relevant.
this is what I get. See, the green rectangle is far away from the red S label
Two problems with your code:
You must include size_hint: None, None in your Label in the kv. Without that, the size has no effect, and the Label fills its parent.
The pos_hint:{'x':0.3,'y':-0.2} positions the Label below the bottom of the parent. Try something like pos_hint:{'x':0.3,'y':0.2}.

how can I modify my parameter without writing whole line again?

Hi I'm trying some method overwriting but I dont know how to do it. I have this class called RectCreator
class RectCreator:
def __init__(self, location_x, location_y, width, height, ):
self.location_x = location_x
self.location_y = location_y
self.width = width
self.height = height
def creating_rect(self, display, color):
creating_rect = pygame.Rect(self.location_x, self.location_y, self.width, self.height)
drawing_rect = pygame.draw.rect(display, color, creating_rect, border_radius=20)
return creating_rect, drawing_rect
this class is in a file. I imported the file in my main.py and I'm using the class like this:
button_1 = RectCreator(350, 350, 100, 100)
btn_1 = button_1.creating_rect(display_surface, blue)
Now here is what I want to do. I dont know how to change the color of the btn_1 without writing all the line again like this:
btn_1 = button_1.creating_rect(display_surface, green) ---------------> I DONT WANT TO WRITE THAT
I tried to add a color method to the class and put that method in the method that uses color.
def color(self, color):
return color
def creating_rect(self, display):
creating_rect = pygame.Rect(self.location_x, self.location_y, self.width, self.height)
drawing_rect = pygame.draw.rect(display, self.color(), creating_rect, border_radius=20)
return creating_rect, drawing_rect
That was my solution but self.color() asks me for a parameter. All I want to do is:
btn_1 = button_1.creating_rect(display_surface, blue)
**output : a blue box that display on my app**
btn_1.change_color(green)
**output : now that blue box turned to green box**
Once you draw something on the screen (like a rect), it's there to stay and won't go away until you draw something new at the same position. E.g. if you set the pixel of the screen at position 5, 12 to the color green, you can't magically change the color at that position without interacting with the screen surface again.
So if you draw a blue rect and now want a green rect, you have to call pygame.draw.rect again. Changing a random variable is not enough.
What you could do is add a color attribute to your class and use that to change the color of the rect that you have to draw every frame anyway (exceptions may exist):
class RectCreator:
def __init__(self, location_x, location_y, width, height, color='blue'):
self.rect = pygame.Rect((location_x, location_y, width, height))
self.color = color
def draw(self, display):
pygame.draw.rect(display, self.color, self.rect, border_radius=20)
Then, create an instance and in your main loop, keep calling draw. You can then change the color simply by setting the color attribute
import random
...
btn_1 = RectCreator(350, 350, 100, 100)
...
while True:
for e in pygame.event.get():
if e.type == pygame.KEYDOWN:
btn_1.color = random.choice(('blue', 'green', 'red', 'yellow'))
btn_1.draw(display_surface)
...
pygame.display.flip()

Updating image in tkinter window with newly made images

I have written a program that simulate orbits of planets around a star, I would like a visualiser to display current planet positions around the star. Since all the planets have their x and y co-ords stored, plotting them on an image is straightforward, but I don't know a good way to update the image and re-display after each simulation step.
Currently I draw the image like so (use the planet position to turn a pixel red)
def createwindow():
img = Image.new( 'RGB', (750,730), "black")
pixels = img.load()
for thing in celestials:
pixels[thing.xpos+375+sun.xpos,thing.ypos+375+sun.xpos]=250,20,20
So I have an image which displays the planets quite well, which can be remade after each time the planets move. But how do I display this in a tkinter window?
Is there a way of doing this without using a Photoimage which must be saved and loaded? Or a better way of doing this all together? Maybe assigning each planet a label and drawing them directly on a tkinter black window, updating the position of the label with each simulation step?
All help appreciated
You should probably draw the animation on a canvas, instead of on a static image; You can then move the elements of the animation that are modified at each step of the simulation.
Something along those lines: (press start at the bottom to start)
import tkinter as tk
def animate():
canvas.move(1, 5, 0) # 1 refers to the object to be moved ID, dx=5, dy=0
root.update()
root.after(50, animate)
if __name__ == '__main__':
root = tk.Tk()
frame = tk.Frame(root)
canvas = tk.Canvas(root, width=800, height=400)
canvas.pack()
canvas.create_polygon(10, 10, 50, 10, 50, 50, 10, 50) # canvas object ID created here by tkinter
btn = tk.Button(root, text='start', command=animate)
btn.pack()
root.mainloop()
There're many ways to display dynamic content, like moving planets. It depends on whether you prefer working with widgets, pixels, images or graphical primitives.
Widgets
If you prefer to move widgets, as you said, you can assign each planet to a Label. Then you can place it on a window using Place Geometry Manager:
yourLabel.place(anchor=CENTER, relx=0.5, rely=0.5, x=-100, y=50)
That'd put a center of the label to coords (-100,50) relative to its parent center (relx=0.5, rely=0.5). To change the position call .place() again with different (x,y) coords.
Pixels
To change image pixels at runtime you can use .blank(), .get() and .put() methods of PhotoImage. They're not in TkInter book, but you can see them in help(PhotoImage) in python console. To set a single pixel (x,y) to color #fa1414 use:
yourPhotoImage.put("{#fa1414}", to=(x,y))
The "{#fa1414}" argument here is actually a pixels list. Tk can put a rectangular block of pixels in one call. Colors are specified row-by-row, separated by spaces, each row enclosed in curly braces. Block of NxM colors would look like "{C0,0 C1,0 ... CN-1,0} {C0,1 C1,1 ... CN-1,1} ... {C0,M-1 C1,M-1 ... CN-1,M-1}". For example, to put 2x2 pixels to coords (4,6) use:
yourPhotoImage.put("{red green} {#7f007f blue}", to=(4,6))
Tk recognizes many symbolic color names in addition to #rrggbb syntax.
Note that all the changes are displayed right away. You don't need to manually "re-display" images. E.g. if you have a Label(image=yourPhotoImage) defined somewhere, it'd display the changes after you modify yourPhotoImage.
Images
Also PhotoImage can load images from base64-encoded strings. So, technically, to skip saving/loading a file, you can load your image from a string, containing base64-encoded .gif or .png (formats supported natively in Tk):
newimg = PhotoImage(data="R0lGODlhHgAUAPAAAP//AAAAACH5BAAAAAAALAAAAAAeABQAAAIXhI+py+0Po5y02ouz3rz7D4biSJZmUgAAOw==")
.put() also supports base64-strings. For example, if you have planets as images, loaded into base64-strings, you can put them into your image with:
yourPhotoImage.put(base64_mars_image_string, to=(100,50))
Finally, if you're using PIL anyway, you can create PIL.ImageTk.PhotoImage from your PIL.Image img and use it everywhere Tkinter accepts an image object[ref]
from PIL import ImageTk
photoimg = ImageTk.PhotoImage(img)
Graphical primitives
If you want lines or arcs you can also use Canvas. It provides a way to put together different widgets, images, texts, graphical primitives, and move them around, or configure interaction with them. A canvas-based planetary system could look like this (python2 syntax):
from Tkinter import *
import math, time
color = ["#fffc00", "#a09991", "#b6b697", "#2c81ca", "#a36447", "#b16811", "#e5ca9d", "#bcdaf2", "#7e96bc", "#d3ac8b"]
size = [20, 4, 7, 7, 5, 15, 13, 9, 9, 4]
start_time = time.time()
def update_positions():
dt = (time.time() - start_time)*5
for i in range(len(planets)):
px, py = 350+35*i*math.cos(dt/(i+1)/size[i]+4*i), 350+35*i*math.sin(dt/(i+1)/size[i]+4*i)
canvas.coords(planets[i], (px-size[i],py-size[i],px+size[i],py+size[i]))
root.after(100, update_positions)
root = Tk()
canvas = Canvas(root, width=700, height=700, bg="black")
arcs = [canvas.create_oval(350-35*i, 350-35*i, 350+35*i, 350+35*i, outline=color[i]) for i in range(len(color))]
planets = [canvas.create_oval(350, 350, 350, 350, fill=color[i]) for i in range(len(color))]
update_positions()
canvas.pack()
root.mainloop()
Note that items created on the canvas are kept until you remove them. If you want to change the drawing, you can either use methods like .coords(), .itemconfig(), and .move() to modify the items, or use .delete() to remove them.

Kivy different type of coordinates

I try to under stand the different types of coordinates:
Global,
Local,
Window and
Widget
coordinates.
using the program:
class TargetUI(BoxLayout):
js_type = NumericProperty(0)
def __init__(self, **arg):
super(TargetUI, self).__init__(**arg)
btn1 = Button(text='Hello world ' + str(self.js_type))
self.add_widget(btn1)
def on_touch_up(self, touch):
# here, you don't check if the touch collides or things like that.
# you just need to check if it's a grabbed touch event
Logger.info("in touch up")
Logger.info("global coordinates: " + str(touch.pos))
if self.collide_point(*touch.pos):
touch.push()
# if the touch collides with our widget, let's grab it
touch.grab(self)
Logger.info("In widget " + str(self.js_type))
touch.apply_transform_2d(self.to_local)
Logger.info("Local coordinates " + str(touch.pos))
touch.apply_transform_2d(self.to_window)
Logger.info("Windows coordinates " + str(touch.pos))
touch.apply_transform_2d(self.to_widget)
Logger.info("Widget coordinates " + str(touch.pos))
touch.ungrab(self)
# and accept the touch.
return True
class CombWidget(Widget):
pass
class MyPaintApp(App):
def build(self):
return CombWidget()
if __name__ == '__main__':
MyPaintApp().run()
and
#:kivy 1.7.1
<CombWidget>:
tg1: tg1
tg2: tg2
tg3: tg3
BoxLayout:
size: root.size
orientation: 'vertical'
padding: 20
TargetUI:
js_type: 1
id: tg1
TargetUI:
js_type: 2
id: tg2
TargetUI:
id: tg3
js_type: 3
All the coordinates written out by on_touch_up is the same, but expected some difference. Why are are all the coordinates the same?
I also expected to see the Button text to end with 1,2 or 3 but their are all 1. How can I make the Button text be depended in self.js_type?
These are useful when there are coordinate changes. For example, with the scatter widget, here is an example where one of the widgets is put in a Scatter and you can move it (somehow it gets back in place when you click it again, but it's convenient). When you do that, you should see that the coordinates are no longer the same. understanding the difference between them is left as an exercise to the reader :)
from kivy.base import runTouchApp
from kivy.lang import Builder
kv = '''
GridLayout:
cols: 2
spacing: 10
ClickBox
Scatter:
ClickBox:
pos: 0, 0
size: self.parent.size
Widget:
ClickBox:
pos: self.parent.pos
size: self.parent.size
<ClickBox#Widget>:
canvas:
Rectangle:
pos: self.pos
size: self.size
on_touch_move:
if self.collide_point(*args[1].pos): print self.to_window(*args[1].pos)
if self.collide_point(*args[1].pos): print self.to_parent(*args[1].pos)
if self.collide_point(*args[1].pos): print self.to_widget(*args[1].pos)
if self.collide_point(*args[1].pos): print self.to_local(*args[1].pos)
'''
if __name__ == '__main__':
runTouchApp(Builder.load_string(kv))
The Documentation for RelativeLayout clarified this issue for me.
Parent coordinates
Other RelativeLayout type widgets are Scatter, ScatterLayout, and
ScrollView. If such a special widget is in the parent stack, only then
does the parent and local coordinate system diverge from the window
coordinate system. For each such widget in the stack, a coordinate
system with (0, 0) of that coordinate system being at the bottom left
corner of that widget is created. Position and touch coordinates
received and read by a widget are in the coordinate system of the most
recent special widget in its parent stack (not including itself) or in
window coordinates if there are none. We
call these coordinates parent coordinates.
So you must use one of the above special widgets for local coordinates to diverge from window coordinates. Tshirtman's answer works because he used the scatter widget which is one of the special widgets.

Resources