I am using Kivy to design an application to draw an n-sided polygon over a live video stream to demarcate regions of interest. The problem I have is that Kivy provides coordinates w.r.t to the whole window and not just the image. What I would like is to have the location of the pixel (in x and y cords) clicked. I have looked at to_local() method but it didn't make much sense, neither did it produce desired results. Any help would be appreciated, below is the MRE.
from kivy.app import App
from kivy.uix.image import Image
from kivy.uix.boxlayout import BoxLayout
from kivy.graphics import Color, Ellipse, Line
from random import random
class ImageView(Image):
def on_touch_down(self, touch):
##
# This provides touch cords for the entire app and not just the location of the pixel clicked#
print("Touch Cords", touch.x, touch.y)
##
color = (random(), 1, 1)
with self.canvas:
Color(*color, mode='hsv')
d = 30.
Ellipse(pos=(touch.x - d / 2, touch.y - d / 2), size=(d, d))
touch.ud['line'] = Line(points=(touch.x, touch.y))
def on_touch_move(self, touch):
touch.ud['line'].points += [touch.x, touch.y]
class DMSApp(App):
def build(self):
imagewidget = ImageView(source="/home/red/Downloads/600.png")
imagewidget.size_hint = (1, .5)
imagewidget.pos_hint = {"top": 1}
layout = BoxLayout(size_hint=(1, 1))
layout.add_widget(imagewidget)
return layout
if __name__ == '__main__':
DMSApp().run()
You can calculate the coordinates of the touch relative to the lower left corner of the actual image that appears in the GUI. Those coordinates can then be scaled to the actual size of the source image to get a reasonable estimate of the actual pixel coordinates within the source. Here is a modified version of your in_touch_down() method that does that (only minimal testing performed):
def on_touch_down(self, touch):
if not self.collide_point(*touch.pos):
return super(ImageView, self).on_touch_down(touch)
lr_space = (self.width - self.norm_image_size[0]) / 2 # empty space in Image widget left and right of actual image
tb_space = (self.height - self.norm_image_size[1]) / 2 # empty space in Image widget above and below actual image
print('lr_space =', lr_space, ', tb_space =', tb_space)
print("Touch Cords", touch.x, touch.y)
print('Size of image within ImageView widget:', self.norm_image_size)
print('ImageView widget:, pos:', self.pos, ', size:', self.size)
print('image extents in x:', self.x + lr_space, self.right - lr_space)
print('image extents in y:', self.y + tb_space, self.top - tb_space)
pixel_x = touch.x - lr_space - self.x # x coordinate of touch measured from lower left of actual image
pixel_y = touch.y - tb_space - self.y # y coordinate of touch measured from lower left of actual image
if pixel_x < 0 or pixel_y < 0:
print('clicked outside of image\n')
return True
elif pixel_x > self.norm_image_size[0] or \
pixel_y > self.norm_image_size[1]:
print('clicked outside of image\n')
return True
else:
print('clicked inside image, coords:', pixel_x, pixel_y)
# scale coordinates to actual pixels of the Image source
print('actual pixel coords:',
pixel_x * self.texture_size[0] / self.norm_image_size[0],
pixel_y * self.texture_size[1] / self.norm_image_size[1], '\n')
color = (random(), 1, 1)
with self.canvas:
Color(*color, mode='hsv')
d = 30.
Ellipse(pos=(touch.x - d / 2, touch.y - d / 2), size=(d, d))
touch.ud['line'] = Line(points=(touch.x, touch.y))
return True
Related
Goals of the code below are defining 3 rings of textinputs that can rotate around their center with the mouse separately and the number of each ring’s textinputs changes with a slider.
there are some problems for achieving these goals:
1.after rotating textinputs with the mouse, select each textinput by clicking on them in their apparent new position not working and should click in the starting position of them that is not desirable.
2.need to rotate each ring separately, but the program rotates all rings with each other(regions defined collide_points of each ring but that is not disjoined them).
3.define the center of the rings in the center of the window(currently by scaling the window, the center of the rings will change).
4.bind a slider to each ring to determine the number of textinputs in each ring.
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.lang import Builder
from kivy.uix.textinput import TextInput
from kivy.properties import NumericProperty
from kivy.core.window import Window
import math
from kivy.uix.slider import Slider
kv = '''
<MyLayout>:
Slider:
min : 8
max : 40
on_value : root.slide_it(*args)
<Scat>:
#pos_hint:{"x":0,"y":0}
canvas.before:
PushMatrix
Rotate:
angle: self.angle
origin: self.center
canvas.after:
PopMatrix
<-RotatableTI>:
size_hint: None, None
canvas.before:
PushMatrix
Rotate:
angle: root.angle
axis: 0,0,1
origin: self.center
Color:
rgba: self.background_color
BorderImage:
border: self.border
pos: self.pos
size: self.size
source: self.background_active if self.focus else (self.background_disabled_normal if self.disabled else self.background_normal)
Color:
rgba:
(self.cursor_color
if self.focus and not self._cursor_blink
else (0, 0, 0, 0))
Rectangle:
pos: self._cursor_visual_pos
size: root.cursor_width, -self._cursor_visual_height
Color:
rgba: self.disabled_foreground_color if self.disabled else (self.hint_text_color if not self.text else self.foreground_color)
canvas.after:
PopMatrix
'''
Builder.load_string(kv)
class RotatableTI(TextInput):
angle = NumericProperty(0)
class Scat(FloatLayout):
Window.size = (650, 650)
def __init__(self, **kwargs):
super(Scat, self).__init__(**kwargs)
pi = math.pi
txsize_x=50
txsize_y=30
r = 150
#txin=[]
for j in range(2,5):
#how to change variable div for each ring with a slider(3 slider for 3rings division)
div = 20 #tried self.div for next 5lines for matching slider code that commented below but not working
for i in range(div):
angle = 360.0 / (div - 1) * i
p_x = (Window.size[0] / 2 + math.cos(2 * pi / (div - 1) * i) *j*r/2)-txsize_x/2
p_y = (Window.size[1] / 2 + math.sin(2 * pi / (div - 1) * i) *j*r/2)-txsize_y/2
self.add_widget(RotatableTI(text="hi" + str(i), size=(txsize_x, txsize_y), pos=(p_x, p_y), angle=angle))
#txin.append(self)
# use next five lines of comment for bind slider to number of textinputs in codes above (div), but now works now well
# self.brightnessControl = Slider(min=0, max=100)
# self.add_widget(self.brightnessControl)
# self.brightnessControl.bind(value=self.on_value)
# def on_value(self, instance, division):
# self.div = "% d" % division
angle = NumericProperty(0)
def ring1(self):
#shal we use vector to make a new ring with bigger radius??
pass
def on_touch_down(self, touch):
y = (touch.y - self.center[1])
x = (touch.x - self.center[0])
calc = math.degrees(math.atan2(y, x))
self.prev_angle = calc if calc > 0 else 360+calc
self.tmp = self.angle
if self.collide_point(*touch.pos):
return super(Scat, self).on_touch_down(touch)
def on_touch_move(self, touch):
y = (touch.y - self.center[1])
x = (touch.x - self.center[0])
calc = math.degrees(math.atan2(y, x))
new_angle = calc if calc > 0 else 360+calc
if self.collide_point(*touch.pos) and 125**2<(touch.x - self.center[0]) ** 2 + (touch.y - self.center[1]) ** 2 < 175**2:
self.angle = self.tmp + (new_angle-self.prev_angle)%360
return super(Scat, self).on_touch_move(touch)
elif self.collide_point(*touch.pos) and 200 ** 2 < (touch.x - self.center[0]) ** 2 + (
touch.y - self.center[1]) ** 2 < 250 ** 2:
self.angle = self.tmp + (new_angle - self.prev_angle) % 360
return super(Scat, self).on_touch_move(touch)
elif self.collide_point(*touch.pos) and 275 ** 2 < (touch.x - self.center[0]) ** 2 + (
touch.y - self.center[1]) ** 2 < 325 ** 2:
self.angle = self.tmp + (new_angle - self.prev_angle) % 360
return super(Scat, self).on_touch_move(touch)
class AslApp(App):
def build(self):
return Scat()
if __name__ == "__main__":
AslApp().run()
I want to put a particle animation in the background screen of my software, something like the link below, but for Python and kivymd
codepen.io/JulianLaval/pen/KpLXOO
I know this may be difficult or impossible for kivymd right now but if anyone has an idea please let me know
Yes! This is absolutely possible (everything is possible in Kivy). Check out the code below:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Line, Color
from random import randint
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import ListProperty
from math import sin, cos
class ParticleMesh(Widget):
points = ListProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.direction = []
self.point_number = 50
Clock.schedule_once(lambda dt: self.plot_points(), 2)
def plot_points(self):
for _ in range(self.point_number):
x = randint(0, self.width)
y = randint(0, self.height)
self.points.extend([x, y])
self.direction.append(randint(0, 359))
Clock.schedule_interval(self.update_positions, 0)
def draw_lines(self):
self.canvas.after.clear()
with self.canvas.after:
for i in range(0, len(self.points), 2):
for j in range(i + 2, len(self.points), 2):
d = self.distance_between_points(self.points[i], self.points[i + 1], self.points[j],
self.points[j + 1])
if d > 120:
continue
color = d / 120
Color(rgba=[color, color, color, 1])
Line(points=[self.points[i], self.points[i + 1], self.points[j], self.points[j + 1]])
def update_positions(self, *args):
step = 1
for i, j in zip(range(0, len(self.points), 2), range(len(self.direction))):
theta = self.direction[j]
self.points[i] += step * cos(theta)
self.points[i + 1] += step * sin(theta)
if self.off_screen(self.points[i], self.points[i + 1]):
self.direction[j] = 90 + self.direction[j]
self.draw_lines()
#staticmethod
def distance_between_points(x1, y1, x2, y2):
return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5
def off_screen(self, x, y):
return x < -5 or x > self.width + 5 or y < -5 or y > self.height + 5
kv = """
FloatLayout:
canvas.before:
Color:
rgba: 1, 1, 1, 1
Rectangle:
size: self.size
pos: self.pos
ParticleMesh:
canvas:
Color:
rgba: 0, 0, 0, 1
Point:
points: self.points
pointsize: 2
"""
class MeshApp(App):
def build(self):
return Builder.load_string(kv)
if __name__ == '__main__':
MeshApp().run()
This code will create the following (this is just a screenshot - if you run the app, the points move about):
First plot_points is called, which creates an array of points randomly placed on the screen. A random direction for each point is also created. This direction is represented by an angle between 0 and 359. On the completion of this function, a Clock object is instantiated and calls update_positions every frame.
The update_positions moves the particles by one pixel in the angle specified by directions. If the position of the particle is greater than the screen the direction is reversed.
Finally draw_lines is called. This function first clears all existing lines then draws new ones. If the points are at a distance greater than 120 pixels no line is created. However, if they are closer than 120 pixels a line is drawn such that the closer the two points are, the darker the line will be.
You can always increase or decrease the number of points on the screen by changing the self.point_number property.
I will leave it up to you to change the colour of the points and the background - I don't think that should be too hard.
I need to create a widget that is used to pick a time. QTimeEdit widget doesn't seem intuitive or a good design. So I decided to create a time picker similar to the time picker in smartphones.
I managed to create the clock and click that makes the pointer (something similar to the pointer in the image) move to the currently clicked position (note: it's not perfect, it still looks bad). I would like to have help with making the inner clock
Here is my code:
from PyQt5 import QtWidgets, QtGui, QtCore
import math, sys
class ClockWidget(QtWidgets.QWidget): # I want to be able to reuse this class for other programs also, so please don't hard code values of the list, start and end
def __init__(self, start, end, lst=[], *args, **kwargs):
super(ClockWidget, self).__init__(*args, **kwargs)
self.lst = lst
if not self.lst:
self.lst = [*range(start, end)]
self.index_start = 0 # tune this to move the letters in the circle
self.pointer_angles_multiplier = 9 # just setting the default values
self.current = None
self.rects = []
#property
def index_start(self):
return self._index_start
#index_start.setter
def index_start(self, index):
self._index_start = index
def paintEvent(self, event):
self.rects = []
painter = QtGui.QPainter(self)
pen = QtGui.QPen()
pen.setColor(QtCore.Qt.red)
pen.setWidth(2)
painter.setPen(pen)
x, y = self.rect().x(), self.rect().y()
width, height = self.rect().width(), self.rect().height()
painter.drawEllipse(x, y, x + width, x + height)
s, t, equal_angles, radius = self.angle_calc()
radius -= 30
pen.setColor(QtCore.Qt.green)
pen.setWidth(2)
painter.setPen(pen)
""" pointer angle helps in determining to which position the pointer should be drawn"""
self.pointer_x, self.pointer_y = s + ((radius-30) * math.cos(self.pointer_angles_multiplier * equal_angles)), t \
+ ((radius-30) * math.sin(self.pointer_angles_multiplier * equal_angles))
""" The pendulum like pointer """
painter.drawLine(QtCore.QPointF(s, t), QtCore.QPointF(self.pointer_x, self.pointer_y))
painter.drawEllipse(QtCore.QRectF(QtCore.QPointF(self.pointer_x - 20, self.pointer_y - 40),
QtCore.QPointF(self.pointer_x + 30, self.pointer_y + 10)))
pen.setColor(QtCore.Qt.blue)
pen.setWidth(3)
font = self.font()
font.setPointSize(14)
painter.setFont(font)
painter.setPen(pen)
""" Drawing the number around the circle formula y = t + radius * cos(a)
y = s + radius * sin(a) where angle is in radians (s, t) are the mid point of the circle """
for index, char in enumerate(self.lst, start=self.index_start):
angle = equal_angles * index
y = t + radius * math.sin(angle)
x = s + radius * math.cos(angle)
# print(f"Add: {add_x}, index: {index}; char: {char}")
rect = QtCore.QRectF(x - 30, y - 40, x + 60, y) # clickable point
self.rects.append([index, char, rect]) # appends index, letter, rect
painter.setPen(QtCore.Qt.blue)
painter.drawRect(rect) # helps in visualizing the points where the click can received
print(f"Rect: {rect}; char: {char}")
painter.setPen(QtCore.Qt.red)
points = QtCore.QPointF(x, y)
painter.drawText(points, str(char))
def mousePressEvent(self, event):
for x in self.rects:
index, char, rect = x
if event.button() & QtCore.Qt.LeftButton and rect.contains(event.pos()):
self.pointer_angles_multiplier = index
self.current = char
self.update()
break
def angle_calc(self):
"""
This will simply return (midpoints of circle, divides a circle into the len(list) and return the
angle in radians, radius)
"""
return ((self.rect().width() - self.rect().x()) / 2, (self.rect().height() - self.rect().y()) / 2,
(360 / len(self.lst)) * (math.pi / 180), (self.rect().width() / 2))
def resizeEvent(self, event: QtGui.QResizeEvent):
"""This is supposed to maintain a Square aspect ratio on widget resizing but doesn't work
correctly as you will see when executing"""
if event.size().width() > event.size().height():
self.resize(event.size().height(), event.size().width())
else:
self.resize(event.size().width(), event.size().width())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
message = ClockWidget(1, 13)
message.index_start = 10
message.show()
sys.exit(app.exec())
The Output:
The blue rectangles represent the clickable region. I would be glad if you could also, make the pointer move to the closest number when clicked inside the clock (Not just move the pointer when the clicked inside the blue region)
There is one more problem in my code, that is the numbers are not evenly spaced from the outer circle. (like the number 12 is closer to the outer circle than the number 6)
Disclaimer: I will not explain the cause of the error but the code I provide I think should give a clear explanation of the errors.
The logic is to calculate the position of the centers of each small circle, and use the exinscribed rectangle to take it as a base to draw the text and check if the point where you click is close to the texts.
from functools import cached_property
import math
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ClockWidget(QtWidgets.QWidget):
L = 12
r = 40.0
DELTA_ANGLE = 2 * math.pi / L
current_index = 9
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
R = min(self.rect().width(), self.rect().height()) / 2
margin = 4
Rect = QtCore.QRectF(0, 0, 2 * R - margin, 2 * R - margin)
Rect.moveCenter(self.rect().center())
painter.setBrush(QtGui.QColor("gray"))
painter.drawEllipse(Rect)
rect = QtCore.QRectF(0, 0, self.r, self.r)
if 0 <= self.current_index < 12:
c = self.center_by_index(self.current_index)
rect.moveCenter(c)
pen = QtGui.QPen(QtGui.QColor("red"))
pen.setWidth(5)
painter.setPen(pen)
painter.drawLine(c, self.rect().center())
painter.setBrush(QtGui.QColor("red"))
painter.drawEllipse(rect)
for i in range(self.L):
j = (i + 2) % self.L + 1
c = self.center_by_index(i)
rect.moveCenter(c)
painter.setPen(QtGui.QColor("white"))
painter.drawText(rect, QtCore.Qt.AlignCenter, str(j))
def center_by_index(self, index):
R = min(self.rect().width(), self.rect().height()) / 2
angle = self.DELTA_ANGLE * index
center = self.rect().center()
return center + (R - self.r) * QtCore.QPointF(math.cos(angle), math.sin(angle))
def index_by_click(self, pos):
for i in range(self.L):
c = self.center_by_index(i)
delta = QtGui.QVector2D(pos).distanceToPoint(QtGui.QVector2D(c))
if delta < self.r:
return i
return -1
def mousePressEvent(self, event):
i = self.index_by_click(event.pos())
if i >= 0:
self.current_index = i
self.update()
#property
def hour(self):
return (self.current_index + 2) % self.L + 1
def minumumSizeHint(self):
return QtCore.QSize(100, 100)
def main():
app = QtWidgets.QApplication(sys.argv)
view = ClockWidget()
view.resize(400, 400)
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I have an image widget in my project and I keep on adding Line objects to its canvas according to touch input (it's a simple drawing app only with an image background). However, at some point, I change something in the screen (long story short it's actually a scrollview containing a boxlayout which contains the image, and I add more images to it during run time to make an infinite image), and the lines that were on the screen disappear. I checked and noticed that they are still inside the canvas's children list, but just not being displayed on the screen. I am however still able to draw more lines. What can cause such behavior? I even tried redrawing the old Line() objects from when they were still displayed on the screen and still nothing happens...
Here's the relevant code:
python
class NotebookScreen(GridLayout):
def __init__(self, **kwargs):
global main_screen
self.rows = 1
super(NotebookScreen, self).__init__(**kwargs)
self.bind(pos=self.update_notebook, size=self.update_notebook, on_touch_up=self.release_touch_func)
def arrow_up_on_press(self):
global scroll_up_event
if scroll_up_event is not None:
scroll_up_event.cancel()
scroll_up_event = None
scroll_up_event = Clock.schedule_interval(self.scroll_up, 0.1)
def arrow_down_on_press(self):
global scroll_down_event
if scroll_down_event is not None:
scroll_down_event.cancel()
scroll_down_event = None
scroll_down_event = Clock.schedule_interval(self.scroll_down, 0.1)
def arrow_down_on_release(self):
global scroll_down_event
if scroll_down_event is not None:
scroll_down_event.cancel()
scroll_down_event = None
def arrow_up_on_release(self):
global scroll_up_event
if scroll_down_event is not None:
scroll_up_event.cancel()
scroll_up_event = None
def scroll_down(self, arg):
global scrolls
scrl = main_screen.ids.notebook_scroll
if scrl.scroll_y - get_scroll_distance()[0] > 0:
scrl.scroll_y -= get_scroll_distance()[0]
scrolls += get_scroll_distance()[1]
else:
offset = get_scroll_distance()[0] - scrl.scroll_y
scrl.scroll_y = 0
main_screen.ids.notebook_scroll.on_scroll_y(0, 0, offset=offset)
def scroll_up(self, arg):
global scrolls
scrl = main_screen.ids.notebook_scroll
if scrl.scroll_y + get_scroll_distance()[0] < 1.:
scrl.scroll_y += get_scroll_distance()[0]
scrolls -= get_scroll_distance()[1]
else:
scrl.scroll_y = 1
def update_notebook(self, a, b, **kwargs):
for child in self.ids.notebook_image.children:
child.size = MyImage.get_size_for_notebook(child)
def release_touch_func(self, a1, a2, **kwargs):
global scroll_up_event, scroll_down_event
if scroll_up_event is not None:
scroll_up_event.cancel()
scroll_up_event = None
if scroll_down_event is not None:
scroll_down_event.cancel()
scroll_down_event = None
class MyScrollView(ScrollView):
def __init__(self, **kwargs):
super(MyScrollView, self).__init__(**kwargs)
def on_scroll_y(self, instance, scroll_val, offset=0):
global main_screen, gen_id, scrolls
if self.scroll_y == 0.: # < get_scroll_distance()[0]:
box = main_screen.ids.notebook_image
old_height = box.height
old_pos_y = self.scroll_y
new_image = MyImage()
new_image.id = next(gen_id)
box.add_widget(new_image)
old_height = (len(main_screen.ids.notebook_image.children) - 1) * main_screen.ids.notebook_image.children[
0].height
self.scroll_y = new_image.height / (old_height + new_image.height) - offset * box.height / old_height
print([child.id for child in list(main_screen.ids.notebook_image.children)])
# redraw all text from earlier
for image in main_screen.ids.notebook_image.children:
image.draw_all_lines()
def slider_change(self, s, instance, value):
if value >= 0:
# this to avoid 'maximum recursion depth exceeded' error
s.value = value
def scroll_change(self, scrlv, instance, value):
scrlv.scroll_y = value
class MyImage(Image):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.lines = []
self.line_coords = []
self.line_objects = []
def get_size_for_notebook(self, **kwargs):
global img_size
width, height = Window.size
return width, (max(img_size[0] * height / width, height))
def to_image(self, x, y):
''''
Convert touch coordinates to pixels
:Parameters:
`x,y`: touch coordinates in parent coordinate system - as provided by on_touch_down()
:Returns: `x, y`
A value of None is returned for coordinates that are outside the Image source
'''
# get coordinates of texture in the Canvas
pos_in_canvas = self.center_x - self.norm_image_size[0] / 2., self.center_y - self.norm_image_size[1] / 2.
# calculate coordinates of the touch in relation to the texture
x1 = x - pos_in_canvas[0]
y1 = y - pos_in_canvas[1]
# convert to pixels by scaling texture_size/source_image_size
if x1 < 0 or x1 > self.norm_image_size[0]:
x2 = None
else:
x2 = self.texture_size[0] * x1 / self.norm_image_size[0]
if y1 < 0 or y1 > self.norm_image_size[1]:
y2 = None
else:
y2 = self.texture_size[1] * y1 / self.norm_image_size[1]
return x2, y2
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
current_touch = self.to_image(*touch.pos)
self.add_to_canvas_on_touch_down((touch.x, touch.y))
touch.ud['line'] = Line(points=[touch.x, touch.y])
with self.canvas:
Color(0, 0, 1, 1)
l = Line(points=touch.ud['line'].points)
self.line_objects.append(l)
return True
else:
return super(MyImage, self).on_touch_down(touch)
def on_touch_move(self, touch):
if self.collide_point(*touch.pos):
current_touch = self.to_image(*touch.pos)
self.add_to_canvas_on_touch_move((touch.x, touch.y))
touch.ud['line'].points += (touch.x, touch.y)
with self.canvas:
Color(0, 0, 1, 1)
l = Line(points=touch.ud['line'].points)
self.line_objects.append(l)
return True
else:
return super(MyImage, self).on_touch_move(touch)
def add_to_canvas_on_touch_down(self, point):
with self.canvas:
self.line_coords.append([point])
self.lines.append([point[0], point[1]])
def add_to_canvas_on_touch_move(self, point):
with self.canvas:
self.lines[-1].append(point[0])
self.lines[-1].append(point[1])
self.line_coords[-1].append(point)
def draw_all_lines(self):
with self.canvas.after:
Color(0, 0, 1, 1)
Line(points=line)
kv:
MyScrollView:
bar_color: [1, 0, 0, 1]
id: notebook_scroll
padding: 0
spacing: 0
do_scroll: (False, False) # up and down
BoxLayout:
padding: 0
spacing: 0
orientation: 'vertical'
id: notebook_image
size_hint: 1, None
height: self.minimum_height
MyImage:
MyImage:
<MyImage>:
source: 'images/pic.png'
allow_stretch: True
keep_ratio: False
size: root.get_size_for_notebook()
size_hint: None, None
One solution is to draw the lines on the parent of the Images (the BoxLayout). And since the Images are added to the bottom of the BoxLayout, the y coordinates of the lines must be adjusted as new Images are added below them.
Here is a modified version of your MyImage class that does this:
class MyImage(Image):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.line_coords = {} # a dictionary with line object as key and coords as value
def get_size_for_notebook(self, **kwargs):
global img_size
width, height = Window.size
return width, (max(img_size[0] * height / width, height))
def to_image(self, x, y):
''''
Convert touch coordinates to pixels
:Parameters:
`x,y`: touch coordinates in parent coordinate system - as provided by on_touch_down()
:Returns: `x, y`
A value of None is returned for coordinates that are outside the Image source
'''
# get coordinates of texture in the Canvas
pos_in_canvas = self.center_x - self.norm_image_size[0] / 2., self.center_y - self.norm_image_size[1] / 2.
# calculate coordinates of the touch in relation to the texture
x1 = x - pos_in_canvas[0]
y1 = y - pos_in_canvas[1]
# convert to pixels by scaling texture_size/source_image_size
if x1 < 0 or x1 > self.norm_image_size[0]:
x2 = None
else:
x2 = self.texture_size[0] * x1 / self.norm_image_size[0]
if y1 < 0 or y1 > self.norm_image_size[1]:
y2 = None
else:
y2 = self.texture_size[1] * y1 / self.norm_image_size[1]
return x2, y2
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
with self.parent.canvas.after:
Color(0, 0, 1, 1)
touch.ud['line'] = Line(points=[touch.x, touch.y])
# add dictionary entry for this line
# save y coord as distance from top of BoxLayout
self.line_coords[touch.ud['line']] = [touch.x, self.parent.height - touch.y]
return True
else:
return super(MyImage, self).on_touch_down(touch)
def on_touch_move(self, touch):
if self.collide_point(*touch.pos):
touch.ud['line'].points += (touch.x, touch.y)
# save touch point with y coordinate as the distance from the top of the BoxLayout
self.line_coords[touch.ud['line']].extend([touch.x, self.parent.height - touch.y])
return True
else:
return super(MyImage, self).on_touch_move(touch)
def draw_all_lines(self):
with self.parent.canvas.after:
Color(0, 0, 1, 1)
for line, pts in self.line_coords.items():
# create new list of points
new_pts = []
for i in range(0, len(pts), 2):
new_pts.append(pts[i])
# calculate correct y coord (height of BoxLayout has changed)
new_pts.append(self.parent.height - pts[i+1])
# redraw this line using new_pts
Line(points=new_pts)
To use this capability, modify part of your MyScrollView:
def on_scroll_y(self, instance, scroll_val, offset=0):
global main_screen, gen_id, scrolls
if self.scroll_y == 0.: # < get_scroll_distance()[0]:
box = main_screen.ids.notebook_image
old_height = box.height
old_pos_y = self.scroll_y
new_image = MyImage()
new_image.id = next(gen_id)
box.add_widget(new_image)
old_height = (len(main_screen.ids.notebook_image.children) - 1) * main_screen.ids.notebook_image.children[
0].height
self.scroll_y = new_image.height / (old_height + new_image.height) - offset * box.height / old_height
print([child.id for child in list(main_screen.ids.notebook_image.children)])
# use Clock.schedule_once to do the drawing after heights are recalculated
Clock.schedule_once(self.redraw_lines)
def redraw_lines(self, dt):
# redraw all text from earlier
self.ids.notebook_image.canvas.after.clear()
for image in self.ids.notebook_image.children:
image.draw_all_lines()
I am in the process of learning tkinter on Python 3.X. I am writing a simple program which will get one or more balls (tkinter ovals) bouncing round a rectangular court (tkinter root window with a canvas and rectangle drawn on it).
I want to be able to terminate the program cleanly by pressing the q key, and have managed to bind the key to the root and fire the callback function when a key is pressed, which then calls root.destroy().
However, I'm still getting errors of the form _tkinter.TclError: invalid command name ".140625086752360" when I do so. This is driving me crazy. What am I doing wrong?
from tkinter import *
import time
import numpy
class Ball:
def bates():
"""
Generator for the sequential index number used in order to
identify the various balls.
"""
k = 0
while True:
yield k
k += 1
index = bates()
def __init__(self, parent, x, y, v=0.0, angle=0.0, accel=0.0, radius=10, border=2):
self.parent = parent # The parent Canvas widget
self.index = next(Ball.index) # Fortunately, I have all my feathers individually numbered, for just such an eventuality
self.x = x # X-coordinate (-1.0 .. 1.0)
self.y = y # Y-coordinate (-1.0 .. 1.0)
self.radius = radius # Radius (0.0 .. 1.0)
self.v = v # Velocity
self.theta = angle # Angle
self.accel = accel # Acceleration per tick
self.border = border # Border thickness (integer)
self.widget = self.parent.canvas.create_oval(
self.px() - self.pr(), self.py() - self.pr(),
self.px() + self.pr(), self.py() + self.pr(),
fill = "red", width=self.border, outline="black")
def __repr__(self):
return "[{}] x={:.4f} y={:.4f} v={:.4f} a={:.4f} r={:.4f} t={}, px={} py={} pr={}".format(
self.index, self.x, self.y, self.v, self.theta,
self.radius, self.border, self.px(), self.py(), self.pr())
def pr(self):
"""
Converts a radius from the range 0.0 .. 1.0 to window coordinates
based on the width and height of the window
"""
assert self.radius > 0.0 and self.radius <= 1.0
return int(min(self.parent.height, self.parent.width)*self.radius/2.0)
def px(self):
"""
Converts an X-coordinate in the range -1.0 .. +1.0 to a position
within the window based on its width
"""
assert self.x >= -1.0 and self.x <= 1.0
return int((1.0 + self.x) * self.parent.width / 2.0 + self.parent.border)
def py(self):
"""
Converts a Y-coordinate in the range -1.0 .. +1.0 to a position
within the window based on its height
"""
assert self.y >= -1.0 and self.y <= 1.0
return int((1.0 - self.y) * self.parent.height / 2.0 + self.parent.border)
def Move(self, x, y):
"""
Moves ball to absolute position (x, y) where x and y are both -1.0 .. 1.0
"""
oldx = self.px()
oldy = self.py()
self.x = x
self.y = y
deltax = self.px() - oldx
deltay = self.py() - oldy
if oldx != 0 or oldy != 0:
self.parent.canvas.move(self.widget, deltax, deltay)
def HandleWallCollision(self):
"""
Detects if a ball collides with the wall of the rectangular
Court.
"""
pass
class Court:
"""
A 2D rectangular enclosure containing a centred, rectagular
grid of balls (instances of the Ball class).
"""
def __init__(self,
width=1000, # Width of the canvas in pixels
height=750, # Height of the canvas in pixels
border=5, # Width of the border around the canvas in pixels
rows=1, # Number of rows of balls
cols=1, # Number of columns of balls
radius=0.05, # Ball radius
ballborder=1, # Width of the border around the balls in pixels
cycles=1000, # Number of animation cycles
tick=0.01): # Animation tick length (sec)
self.root = Tk()
self.height = height
self.width = width
self.border = border
self.cycles = cycles
self.tick = tick
self.canvas = Canvas(self.root, width=width+2*border, height=height+2*border)
self.rectangle = self.canvas.create_rectangle(border, border, width+border, height+border, outline="black", fill="white", width=border)
self.root.bind('<Key>', self.key)
self.CreateGrid(rows, cols, radius, ballborder)
self.canvas.pack()
self.afterid = self.root.after(0, self.Animate)
self.root.mainloop()
def __repr__(self):
s = "width={} height={} border={} balls={}\n".format(self.width,
self.height,
self.border,
len(self.balls))
for b in self.balls:
s += "> {}\n".format(b)
return s
def key(self, event):
print("Got key '{}'".format(event.char))
if event.char == 'q':
print("Bye!")
self.root.after_cancel(self.afterid)
self.root.destroy()
def CreateGrid(self, rows, cols, radius, border):
"""
Creates a rectangular rows x cols grid of balls of
the specified radius and border thickness
"""
self.balls = []
for r in range(1, rows+1):
y = 1.0-2.0*r/(rows+1)
for c in range(1, cols+1):
x = 2.0*c/(cols+1) - 1.0
self.balls.append(Ball(self, x, y, 0.001,
numpy.pi/6.0, 0.0, radius, border))
def Animate(self):
"""
Animates the movement of the various balls
"""
for c in range(self.cycles):
for b in self.balls:
b.v += b.accel
b.Move(b.x + b.v * numpy.cos(b.theta),
b.y + b.v * numpy.sin(b.theta))
self.canvas.update()
time.sleep(self.tick)
self.root.destroy()
I've included the full listing for completeness, but I'm fairly sure that the problem lies in the Court class. I presume it's some sort of callback or similar firing but I seem to be beating my head against a wall trying to fix it.
You have effectively got two mainloops. In your Court.__init__ method you use after to start the Animate method and then start the Tk mainloop which will process events until you destroy the main Tk window.
However the Animate method basically replicates this mainloop by calling update to process events then time.sleep to waste some time and repeating this. When you handle the keypress and terminate your window, the Animate method is still running and attempts to update the canvas which no longer exists.
The correct way to handle this is to rewrite the Animate method to perform a single round of moving the balls and then schedule another call of Animate using after and provide the necessary delay as the after parameter. This way the event system will call your animation function at the correct intervals while still processing all other window system events promptly.