I need to change orientation to vertical, but it does not work the same way as BoxLayout. There is also no information about this in the Kivy official documentation. In addition, is there any way to change the page by swiping from any place on the screen and not only the border?
Python:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
class MainScreen(Screen):
pass
class ScreenManagement(ScreenManager):
pass
presentation = Builder.load_file("final.kv")
class MainApp(App):
def build(self):
return presentation
if __name__ == "__main__":
MainApp().run()
Kivy:
ScreenManagement:
MainScreen:
<MainScreen>:
canvas:
Rectangle:
source: "nakedman.jpg"
pos: self.pos
size: self.size
name: "main"
PageLayout:
orientation: "vertical"
BoxLayout:
Button:
text: "Button1"
Button:
text: "Button2"
BoxLayout:
Button:
text: "Button3"
Button:
text: "Button4"
PageLayout does not have the orientation attribute, so you have to build everything, in this case use the source code of PageLayout to obtain this new code:
__all__ = ('PageLayoutVertical', )
from kivy.uix.layout import Layout
from kivy.properties import NumericProperty, DictProperty
from kivy.animation import Animation
class PageLayoutVertical(Layout):
'''PageLayout class. See module documentation for more information.
'''
page = NumericProperty(0)
'''The currently displayed page.
:data:`page` is a :class:`~kivy.properties.NumericProperty` and defaults
to 0.
'''
border = NumericProperty('50dp')
'''The width of the border around the current page used to display
the previous/next page swipe areas when needed.
:data:`border` is a :class:`~kivy.properties.NumericProperty` and
defaults to 50dp.
'''
swipe_threshold = NumericProperty(.5)
'''The thresold used to trigger swipes as percentage of the widget
size.
:data:`swipe_threshold` is a :class:`~kivy.properties.NumericProperty`
and defaults to .5.
'''
anim_kwargs = DictProperty({'d': .5, 't': 'in_quad'})
'''The animation kwargs used to construct the animation
:data:`anim_kwargs` is a :class:`~kivy.properties.DictProperty`
and defaults to {'d': .5, 't': 'in_quad'}.
.. versionadded:: 1.11.0
'''
def __init__(self, **kwargs):
super(PageLayoutVertical, self).__init__(**kwargs)
trigger = self._trigger_layout
fbind = self.fbind
fbind('border', trigger)
fbind('page', trigger)
fbind('parent', trigger)
fbind('children', trigger)
fbind('size', trigger)
fbind('pos', trigger)
def do_layout(self, *largs):
l_children = len(self.children) - 1
w = self.width
x_parent, y_parent = self.pos
p = self.page
border = self.border
half_border = border / 2.
top = self.top
height = self.height - border
for i, c in enumerate(reversed(self.children)):
if i < p:
y = y_parent
elif i == p:
if not p: # it's first page
y = y_parent
elif p != l_children: # not first, but there are post pages
y = y_parent + half_border
else: # not first and there are no post pages
y = y_parent + border
elif i == p + 1:
if not p: # second page - no left margin
y = top - border
else: # there's already a left margin
y = top - half_border
else:
y = top
c.width = w
c.height = height
Animation(
x=x_parent,
y=y,
**self.anim_kwargs).start(c)
def on_touch_down(self, touch):
if (
self.disabled or
not self.collide_point(*touch.pos) or
not self.children
):
return
page = self.children[-self.page - 1]
if self.y <= touch.y < page.y:
touch.ud['page'] = 'previous'
touch.grab(self)
return True
elif page.top <= touch.y < self.top:
touch.ud['page'] = 'next'
touch.grab(self)
return True
return page.on_touch_down(touch)
def on_touch_move(self, touch):
if touch.grab_current != self:
return
p = self.page
border = self.border
half_border = border / 2.
page = self.children[-p - 1]
if touch.ud['page'] == 'previous':
# move next page upto right edge
if p < len(self.children) - 1:
self.children[-p - 2].y = min(
self.top - self.border * (1 - (touch.sy - touch.osy)),
self.top)
# move current page until edge hits the right border
if p >= 1:
b_right = half_border if p > 1 else border
b_left = half_border if p < len(self.children) - 1 else border
self.children[-p - 1].y = max(min(
self.y + b_left + (touch.y - touch.oy),
self.top - b_right),
self.y + b_left)
# move previous page left edge upto left border
if p > 1:
self.children[-p].y = min(
self.y + half_border * (touch.sy - touch.osy),
self.y + half_border)
elif touch.ud['page'] == 'next':
# move current page upto left edge
if p >= 1:
self.children[-p - 1].y = max(
self.y + half_border * (1 - (touch.osy - touch.sy)),
self.y)
# move next page until its edge hit the left border
if p < len(self.children) - 1:
b_right = half_border if p >= 1 else border
b_left = half_border if p < len(self.children) - 2 else border
self.children[-p - 2].y = min(max(
self.top - b_right + (touch.y - touch.oy),
self.y + b_left),
self.top - b_right)
# move second next page upto right border
if p < len(self.children) - 2:
self.children[-p - 3].y = max(
self.top + half_border * (touch.sy - touch.osy),
self.top - half_border)
return page.on_touch_move(touch)
def on_touch_up(self, touch):
if touch.grab_current == self:
if (
touch.ud['page'] == 'previous' and
abs(touch.y - touch.oy) / self.height > self.swipe_threshold
):
self.page -= 1
elif (
touch.ud['page'] == 'next' and
abs(touch.y - touch.oy) / self.height > self.swipe_threshold
):
self.page += 1
else:
self._trigger_layout()
touch.ungrab(self)
if len(self.children) > 1:
return self.children[-self.page + 1].on_touch_up(touch)
if __name__ == '__main__':
from kivy.base import runTouchApp
from kivy.uix.button import Button
pl = PageLayoutVertical()
for i in range(1, 10):
b = Button(text='page%s' % i)
pl.add_widget(b)
runTouchApp(pl)
Related
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
need to rotate each ring of the textinputs separately, but the program rotates all the canvas at the same time. To solve this problem, I defined regions for the collide_point of each ring and also append each ring separately in a list to call them by index in collide_point but they still stick together in rotating!
What am I missing?
from kivy.app import App
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
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)
_registry = []
def __init__(self, **kwargs):
super(Scat, self).__init__(**kwargs)
pi = math.pi
txsize_x=50
txsize_y=30
r = 150
for j in range(2,5):
div = 20
for i in range(1,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))
self._registry.append(self)
angle = NumericProperty(0)
def ring1(self):
#shal we use vector to make a new ring with bigger radius??
center_x=self.center[0]
center_y = self.center[1]
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._registry[0]).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._registry[1]).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._registry[2]).on_touch_move(touch)
class AslApp(App):
def build(self):
return Scat()
if __name__ == "__main__":
AslApp().run()
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 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 having problems with the below code. Can someone please help and explain why the game over/reset and score functions are not working?
Below is the code im working from. My problem is that i can not see why the game over/reset function does not work. When the ship crashes into an astroid the game abruptly ends, but when it crashes into the barrier it does not indicate game over. Also, it's not reflecting the score.
Sorry if i wasn't clear before.
Here starts the code:
import kivy
kivy.require('1.8.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.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.image import Image
from kivy.properties import NumericProperty, ObjectProperty
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.graphics import Rectangle, Color, Canvas
from kivy.config import Config
from functools import partial
from random import *
Config.set('graphics', 'resizable', 0)
Window.clearcolor = (0, 0, 0, 1.)
class MyButton(Button):
#class used to get uniform button styles
def __init__(self, **kwargs):
super(MyButton, self).__init__(**kwargs)
self.font_size = Window.width*0.018
class SmartMenu(Widget):
#the instance created by this class will appear
#when the game is started for the first time
buttonList = []
def __init__(self, **kwargs):
#create custom events first
self.register_event_type('on_button_release')
super(SmartMenu, self).__init__(**kwargs)
self.layout = BoxLayout(orientation = 'vertical')
self.layout.width = Window.width/2
self.layout.height = Window.height/2
self.layout.x = Window.width/2 - self.layout.width/2
self.layout.y = Window.height/2 - self.layout.height/2
self.add_widget(self.layout)
def on_button_release(self, *args):
#print 'The on_button_release event was just dispatched', args
#don't need to do anything here. needed for dispatch
pass
def callback(self,instance):
#print('The button %s is being pressed' % instance.text)
self.buttonText = instance.text
self.dispatch('on_button_release') #dispatching the callback event 'on_button_release' to tell teh parent instance to read the button text
def addButtons(self):
for k in self.buttonList:
tmpBtn = MyButton(text = k)
tmpBtn.background_color = [.4, .4, .4, .4]
tmpBtn.bind(on_release = self.callback) #when the button is released the callback function is called
self.layout.add_widget(tmpBtn)
def buildUp(self):
#self.colorWindow()
self.addButtons()
class SmartStartMenu(SmartMenu):
#setup the menu button names
buttonList = ['start', 'about']
def __init__(self, **kwargs):
super(SmartStartMenu, self).__init__(**kwargs)
self.layout = BoxLayout(orientation = 'vertical')
self.layout.width = Window.width/2
self.layout.height = Window.height/2
self.layout.x = Window.width/2 - self.layout.width/2
self.layout.y = Window.height/2 - self.layout.height/2
self.add_widget(self.layout)
self.msg = Label(text = 'Flappy Ship')
self.msg.font_size = Window.width*0.07
self.msg.pos = (Window.width*0.45,Window.height*0.75)
self.add_widget(self.msg)
self.img = Image(source = 'lens2.png')
self.img.size = (Window.width*1.5,Window.height*1.5)
self.img.pos = (-Window.width*0.2,-Window.height*0.2)
self.img.opacity = 0.35
self.add_widget(self.img)
class WidgetDrawer(Widget):
#This widget is used to draw all of the objects on the screen
#it handles the following:
# widget movement, size, positioning
def __init__(self, imageStr, **kwargs):
super(WidgetDrawer, self).__init__(**kwargs)
with self.canvas:
self.size = (Window.width*.002*25,Window.width*.002*25)
self.rect_bg=Rectangle(source=imageStr, pos=self.pos, size=self.size)
self.bind(pos=self.update_graphics_pos)
self.x = self.center_x
self.y = self.center_y
self.pos = (self.x, self.y)
self.rect_bg.pos = self.pos
def update_graphics_pos(self, instance, value):
self.rect_bg.pos = value
def setSize(self, width, height):
self.size = (width, height)
def setPos(self, xpos, ypos):
self.x = xpos
self.y = ypos
class ScoreWidget(Widget):
def __init__(self, **kwargs):
super(ScoreWidget, self).__init__(**kwargs)
self.asteroidScore = 0
self.currentScore = 0
with self.canvas:
tmpPos = (Window.width*0.25, Window.height*0.25)
tmpSize = (Window.width*0.5, Window.height*0.5)
Color(0.1, .1, .1)
self.scoreRect = Rectangle(pos=tmpPos, size=tmpSize)
def prepare(self):
#calculate the score
try:
self.finalScore = self.asteroidScore*100
except:
print 'problems getting score'
self.animateScore()
def animateScore(self):
#display score at 0 and every time interval add 100 until
#we reach the final score
#draw a score widget and schedule updates
scoreText = 'Score: 0'# + str(self.finalScore)
self.scoreLabel = Label(text=scoreText,font_size = '20sp')
self.scoreLabel.x = Window.width*0.3
self.scoreLabel.y = Window.height*0.3
self.add_widget(self.scoreLabel)
Clock.schedule_once(self.updateScore, .1)
self.drawStars()
def updateScore(self,dt):
self.currentScore = self.currentScore +100
self.scoreLabel.text = 'Score: ' + str(self.currentScore)
if self.currentScore < self.finalScore:
Clock.schedule_once(self.updateScore, 0.1)
def drawStars(self):
#0-10 asteroids 0 stars
#11-50 asteroids 1 star
#51-200 asteroids 2 stars
#201-500 asteroids 3 stars
#501-1000 asteroids 4 stars
#1001+ asteroids 5 stars
starNumber = 0
if self.asteroidScore > 10:
starNumber = 1
if self.asteroidScore > 50:
starNumber = 2
if self.asteroidScore > 200:
starNumber = 3
if self.asteroidScore > 500:
starNumber = 4
if self.asteroidScore > 1000:
starNumber = 5
with self.canvas:
#draw stars
#rect one
starPos = Window.width*0.27, Window.height*0.42
starSize = Window.width*0.06,Window.width*0.06
starString = 'gold_star.png'
if starNumber < 1:
starString = 'gray_star.png'
starRectOne = Rectangle(source=starString,pos=starPos, size = starSize)
#rect two
starPos = Window.width*0.37, Window.height*0.42
if starNumber < 2:
starString = 'gray_star.png'
starRectTwo = Rectangle(source=starString,pos=starPos, size = starSize)
#rect three
starPos = Window.width*0.47, Window.height*0.42
if starNumber < 3:
starString = 'gray_star.png'
starRectThree = Rectangle(source=starString,pos=starPos, size = starSize)
#rect four
starPos = Window.width*0.57, Window.height*0.42
if starNumber < 4:
starString = 'gray_star.png'
starRectFour = Rectangle(source=starString,pos=starPos, size = starSize)
#rect five
starPos = Window.width*0.67, Window.height*0.42
if starNumber < 5:
starString = 'gray_star.png'
starRectFive = Rectangle(source=starString,pos=starPos, size = starSize)
class Asteroid(WidgetDrawer):
#Asteroid class. The flappy ship will dodge these
imageStr = './sandstone_1.png'
rect_bg = Rectangle(source=imageStr)
velocity_x = NumericProperty(0)
velocity_y = NumericProperty(0)
def move(self):
self.x = self.x + self.velocity_x*5
self.y = self.y + self.velocity_y
def update(self):
self.move()
class Ship(WidgetDrawer):
#Ship class. This is for the main ship object.
#velocity of ship on x/y axis
#setup constants, health, etc
#choose default image:
impulse = 3
grav = -0.1
velocity_x = NumericProperty(0)
velocity_y = NumericProperty(0)
flameSize = (Window.width*.03, Window.width*.03)
def move(self):
self.x += self.velocity_x
self.y += self.velocity_y
#don't let the ship go too far
if self.y < Window.height*0.05:
#give upwards impulse
Clock.unschedule(self.update)
self.explode()
if self.y > Window.height*0.95:
Clock.unschedule(self.update)
self.explode()
def checkBulletNPCCollision(self, j):
if self.k.collide_widget(j):
j.health = j.health - self.k.bulletDamage
j.attackFlag = 'True'
#age the bullet
self.k.age = self.k.lifespan+10
def checkBulletStageCollision(self,q):
if self.k.collide_widget(q):
#if object type is asteorid
try:
if q.type == 'asteroid':
q.health = q.health - self.k.bulletDamage
self.k.age = self.k.lifespan+10
except:
print 'couldnt hit asteroid'
def determineVelocity(self):
#move the ship up and down
#we need to take into account our acceleration
#also want to look at gravity
self.grav = self.grav*1.05 #increase gravity
#set a grav limit
if self.grav < -4:
self.grav = -4
self.velocity_y = self.impulse + self.grav
self.impulse = 0.95*self.impulse
def drawArrow(self, *largs):
#draw the arrows directly onto the canvas
with self.canvas:
flamePos = (self.pos[0]-Window.width*.02, self.pos[1]+Window.width*.01)
flameRect = Rectangle(source='./flame.png', pos=flamePos, size=self.flameSize)
#schedule removal
def removeArrows(arrow, *largs):
self.canvas.remove(arrow)
Clock.schedule_once(partial(removeArrows, flameRect), .5)
Clock.schedule_once(partial(self.updateArrows, flameRect), 0.1)
def updateArrows(self, arrow, dt):
with self.canvas:
arrow.pos = (arrow.pos[0]-10, arrow.pos[1])
Clock.schedule_once(partial(self.updateArrows, arrow), 0.1)
return
def explode(self):
tmpSize = Window.width*0.25,Window.width*0.2
tmpPos = (self.x-Window.width*0.095, self.y-Window.width*0.08)
with self.canvas:
self.explosionRect = Rectangle(source ='./explosion1.png',pos=tmpPos,size=tmpSize)
def changeExplosion(rect, newSource, *largs):
rect.source = newSource
Clock.schedule_once(partial(changeExplosion, self.explosionRect, './explosion2.png'), 0.2)
Clock.schedule_once(partial(changeExplosion, self.explosionRect, './explosion3.png'), 0.4)
Clock.schedule_once(partial(changeExplosion, self.explosionRect, './explosion4.png'), 0.6)
Clock.schedule_once(partial(changeExplosion, self.explosionRect, './explosion5.png'), 0.8)
def removeExplosion(rect, *largs):
self.canvas.remove(rect)
Clock.schedule_once(partial(removeExplosion, self.explosionRect), 1)
def update(self):
self.determineVelocity()
self.move()
class GUI(Widget):
#this is the main widget that contains the game. This is the primary object
#that runs
asteroidList =[]
#important to use numericproperty here so we can bind a callback
#to use every time the number changes
asteroidScore = NumericProperty(0)
minProb = 1780
def __init__(self, **kwargs):
super(GUI, self).__init__(**kwargs)
#setup label for the score
self.score = Label(text = '0')
self.score.y = Window.height*0.8
self.score.x = Window.width*0.2
def check_score(self, obj):
#update credits
self.score.text = str(self.asteroidScore)
self.bind(asteroidScore = check_score)
self.add_widget(self.score)
#now we create a ship object
self.ship = Ship(imageStr = './ship.png')
self.ship.x = Window.width/4
self.ship.y = Window.height/2
self.add_widget(self.ship)
#self.ship.drawArrow()#start the flames
Clock.schedule_interval((self.ship.drawArrow), 0.1)
def addAsteroid(self):
#add an asteroid to the screen
#self.asteroid
imageNumber = randint(1, 4)
imageStr = './sandstone_'+str(imageNumber)+'.png'
tmpAsteroid = Asteroid(imageStr)
tmpAsteroid.x = Window.width*0.99
#randomize y position
ypos = randint(1, 16)
ypos = ypos*Window.height*.0625
tmpAsteroid.y = ypos
tmpAsteroid.velocity_y = 0
vel = 550#randint(10,25)
tmpAsteroid.velocity_x = -0.1*vel
self.asteroidList.append(tmpAsteroid)
self.add_widget(tmpAsteroid)
def drawTouchResponse(self,x,y):
#draw the arrows directly onto the canvas
with self.canvas:
tmpSize = Window.width*0.07, Window.width*0.07
tmpPos = (x-self.width/4,y-self.height/4)
self.arrowRect = Rectangle(source='./flame1.png',pos=tmpPos, size = tmpSize)
#schedule removal
def removeArrows(arrow, *largs):
self.canvas.remove(arrow)
def changeExplosion(rect, newSource, *largs):
rect.source = newSource
#schedule explosion two
Clock.schedule_once(partial(changeExplosion, self.arrowRect, './flame2.png'), 0.15)
#schedule explosion three
Clock.schedule_once(partial(changeExplosion, self.arrowRect, './flame3.png'), 0.3)
#schedule explosoin four
Clock.schedule_once(partial(changeExplosion, self.arrowRect, './flame4.png'), 0.45)
Clock.schedule_once(partial(removeArrows, self.arrowRect), 0.6)
#handle input events
def on_touch_down(self, touch):
self.ship.impulse = 3
self.ship.grav = -0.1
self.drawTouchResponse(touch.x, touch.y)
def showScore(self):
#this function will draw the score keeping widget, tabulate the score
#and rank with stars
self.scoreWidget = ScoreWidget()
self.scoreWidget.asteroidScore = self.asteroidScore #pass on score
self.scoreWidget.prepare()
self.add_widget(self.scoreWidget)
def removeScore(self):
self.remove_widget(self.scoreWidget)
def gameOver(self):
#add a restart button
restartButton = MyButton(text='Try Again')
#restartButton.background_color = (.5,.5,1,.2)
def restart_button(obj):
#reset game
self.removeScore()
for k in self.asteroidList:
self.remove_widget(k)
self.ship.xpos = Window.width*0.25
self.ship.ypos = Window.height*0.5
self.minProb = 1780
self.asteroidScore = 0
self.asteroidList = []
self.parent.remove_widget(restartButton)
Clock.unschedule(self.update)
Clock.schedule_interval(self.update, 1.0/60.0)
restartButton.size = (Window.width*.3,Window.width*.1)
restartButton.pos = Window.width*0.5-restartButton.width/2, Window.height*0.53
restartButton.bind(on_release=restart_button)
#we will want to bind the parent to listen for things from certain bubbles
#*** It's important that the parent get the button so you can click on it
#otherwise you can't click through the main game's canvas
self.parent.add_widget(restartButton)
#now draw the score widget
self.showScore()
def update(self, dt):
#This update function is the main update function for the game
#All of the game logic has its origin here
#events are setup here as well
#update game objects
#update ship
self.ship.update()
#update asteroids
#randomly add an asteroid
tmpCount = randint(1, 1800)
if tmpCount > self.minProb:
self.addAsteroid()
if self.minProb < 1300:
self.minProb = 1300
self.minProb -= 1
for k in self.asteroidList:
#check for collision with ship
if k.collide_widget(self.ship):
#game over routine
self.gameOver()
Clock.unschedule(self.update)
#add reset button
self.ship.explode()
k.update()
#check to see if asteroid is off of screen
if k.x < -100:
#since it's off the screen, remove the asteroid
self.remove_widget(k)
self.asteroidScore += 1
#remove asteroids off screen
tmpAsteroidList = self.asteroidList
tmpAsteroidList[:] = [x for x in tmpAsteroidList if (x.x > - 100)]
self.asteroidList = tmpAsteroidList
class ClientApp(App):
def build(self):
#this is where the root widget goes
#should be a canvas
self.parent = Widget() #
self.app = GUI()
#Start the game clock (runs update function once every (1/60) seconds
#Clock.schedule_interval(app.update, 1.0/60.0)
#add the start menu
self.sm = SmartStartMenu()
self.sm.buildUp()
def check_button(obj):
#check to see which button was pressed
if self.sm.buttonText == 'start':
#remove menu
self.parent.remove_widget(self.sm)
#start the game
print ' we should start the game now'
Clock.unschedule(self.app.update)
Clock.schedule_interval(self.app.update, 1.0/60.0)
try:
self.parent.remove_widget(self.aboutText)
except:
pass
if self.sm.buttonText == 'about':
self.aboutText = Label(text = 'Flappy Ship is made by Molecular Flow Games \n Check out: https://kivyspacegame.wordpress.com')
self.aboutText.pos = (Window.width*0.45,Window.height*0.35)
self.parent.add_widget(self.aboutText)
#bind a callback function that repsonds to event 'on_button_release' by calling function check_button
self.sm.bind(on_button_release = check_button)
#setup listeners for smartstartmenu
self.parent.add_widget(self.sm)
self.parent.add_widget(self.app) #use this hierarchy to make it easy to deal w/buttons
return self.parent
if __name__ == '__main__':
ClientApp().run()