Reputation: 151
I am trying to create a number of irregular rectangular buttons stacked on top of each other. I have achieved most of the task and the code is below, the code creates and saves .pngs for the various shapes then uses these shapes in the kivy app. I got the sizing and positioning the way I want it but the problem I am having is that the widget that is the image seems to be rectangular, so therefore the widgets overlap even though the shapes dont. The problem that this creates is the the on_press() function is not always triggering the correct button especially near the edges as can be seen by the image here. (The red box was hand drawn on the screenshot to show the shape of the widget!)
The idea I am trying to achieve is that pressing inside the shape will active the button. Any ideas how I can reduce the area to just inside the shape.
Thanks
from PIL import Image, ImageDraw
from kivy.app import App
from kivy.uix.image import Image as Img
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.floatlayout import FloatLayout
def calculate_hw(tuple_list, ht = False, wt = False):
'using the coordinates of the shapes, returns the height and/or width'
if not tuple_list:
return None
min_x = min(tuple_list, key=lambda tup: tup[0])[0]
max_x = max(tuple_list, key=lambda tup: tup[0])[0]
min_y = min(tuple_list, key=lambda tup: tup[1])[1]
max_y = max(tuple_list, key=lambda tup: tup[1])[1]
h = max_y - min_y
w = max_x - min_x
if ht and wt:
return h, w
elif ht:
return h
elif wt:
return w
def calculate_y_difference(tuple_list):
'''
returns the height of the objecy at its widest part
used to calculate the value of the pos_y of the next button
'''
sorted_by_x = sorted(tuple_list, key=lambda tup: tup[0], reverse=True)
y_diff = sorted_by_x[0][1] - sorted_by_x[1][1]
return abs(y_diff)
shapes = {
'1': [(500.0, 0.0), (500.0, 93.38888888888945), (0.0, 25.0), (0.0, 0.0), (500.0, 0.0)],
'2': [(500.0, 93.38888888888945), (500.0, 133.33333333333357), (0.0, 75.0), (0.0, 25.0), (500.0, 93.38888888888945)],
'3': [(500.0, 133.33333333333357), (500.0, 188.3333333333333), (0.0, 125.0), (0.0, 75.0), (500.0, 133.33333333333357)]}
shapes_height = {k: calculate_hw(shapes[k], ht = True) for k in shapes.keys() }
shape_pos_height = {k: calculate_y_difference(shapes[k]) for k in shapes.keys() }
shape_width = {k: calculate_hw(shapes[k], wt = True) for k in shapes.keys() }
#Loop through each shape, create it, crop it and save it as a .png
for shape_name, vertices in shapes.items():
# Create a new transparent image
width, height = 4000, 4000
background_color = (0, 0, 0, 0) # Transparent background
image_normal = Image.new("RGBA", (width, height), background_color)
image_down = Image.new("RGBA", (width, height), background_color)
# Create a drawing object
draw_normal = ImageDraw.Draw(image_normal)
draw_down = ImageDraw.Draw(image_down)
# Draw the irregular shape
draw_normal.polygon(vertices, ) #fill=(255, 0, 0, 128)) # Fill color with transparency
draw_down.polygon(vertices, fill=(0, 255, 0, 128)) # Fill color with transparency
# Find the bounding box of the polygon
min_x = min(point[0] for point in vertices)
min_y = min(point[1] for point in vertices)
max_x = max(point[0] for point in vertices)
max_y = max(point[1] for point in vertices)
#crop the image
image_normal = image_normal.crop((min_x, min_y, max_x, max_y))
image_down = image_down.crop((min_x, min_y, max_x, max_y))
# Save the image as a PNG file
image_normal.save(f"{shape_name}.png", "PNG", dpi=(2000, 2000), resolution_unit="in")
image_down.save(f"{shape_name}_down.png", "PNG", dpi=(2000, 2000), resolution_unit="in")
class MyButton(ButtonBehavior, Img):
'''
a very basic button with an image used instead of a Label
'''
def __init__(self,normal, down, **kwargs):
super(MyButton, self).__init__(**kwargs)
self.normal = normal
self.down = down
self.source = self.normal
def on_press(self):
self.source = self.down
def on_release(self):
self.source = self.normal
class Main(FloatLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
y = 300
for shape in shapes: #Loop through the shape names
y -= shape_pos_height[shape] #the y pos of the first shape, this is reduced by the width of the shape to achieve a stacking effect
#add the custom budget
self.add_widget(MyButton(
down = f'{shape}_down.png',
normal = f'{shape}.png',
size_hint = (None, None),
size = (shape_width[shape], shapes_height[shape]),
pos = (100,y) ))
class MainApp(App):
def build(self):
return Main()
MainApp().run()
Upvotes: 0
Views: 114
Reputation: 291
Rather than rely on ButtonBehavior, you need to create your own function to test for collision. Here is a complete example that uses a circle. I have also added creating and dispatching the on_release and on_press events.
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import ColorProperty, NumericProperty
from kivy.uix.label import Label
kv = """
<CircleButton>:
size_hint: None, None
size: self.radius * 2, self.radius * 2
canvas.before:
Color:
rgba: self._button_color
Ellipse:
size: self.size
pos: self.pos
AnchorLayout:
CircleButton:
text: 'Push'
radius: dp(75)
on_press: print('Button Pressed')
on_release: print('Button Released')
"""
class CircleButton(Label):
radius = NumericProperty(50)
normal_color = ColorProperty('gray')
down_color = ColorProperty('blue')
_button_color = ColorProperty('gray')
def __init__(self, **kwargs):
self.register_event_type('on_release')
self.register_event_type('on_press')
super().__init__(**kwargs)
def on_press(self):
pass
def on_release(self):
pass
def is_inside_circle(self, touch_x, touch_y):
dx = abs(touch_x - self.center_x)
dy = abs(touch_y - self.center_y)
return dx ** 2 + dy ** 2 <= self.radius ** 2
def on_touch_down(self, touch):
if self.is_inside_circle(*touch.pos):
touch.grab(self)
self._button_color = self.down_color
self.dispatch('on_press')
return True
super().on_touch_down(touch)
def on_touch_up(self, touch):
if touch.grab_current is self:
self._button_color = self.normal_color
self.dispatch('on_release')
touch.ungrab(self)
return True
return super().on_touch_up(touch)
class CircleButtonExampleApp(App):
def build(self):
return Builder.load_string(kv)
CircleButtonExampleApp().run()
Upvotes: 1
Reputation: 151
So, after some inspiration from the above answer I got what I was trying to do!!
Not sure if it was the best way to do it but I wrote a new on_touch_down and on_touch_up method to override the same in the ButtonBehaviour class. The shapley module has a method to check if a point is inside a polygon and as I knew the coordinates for my shapes so I was able to use this. In the new on_touch_down method I added a line..
touch_point = Point(touch.x, touch.y)
if shape.contains(touch_point):
(do the normal button stuff)
return True
else:
return False
The one problem I had was that the buttons were stacked so the coordinates used to draw the shapes did not line up with the coordinates for the touch, so I used an x_offset and y_offset value to position the buttons and then alter the coordinates appropriately. Might need more work when I go to use it in the project but for now this seems to work as a nice example of irregular shaped buttons!!!
Thanks for the help!
from PIL import Image, ImageDraw
from kivy.app import App
from kivy.uix.image import Image as Img
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.floatlayout import FloatLayout
from time import time
from kivy.clock import Clock
from shapely.geometry import Polygon, Point
def calculate_hw(tuple_list, ht = False, wt = False):
'using the coordinates of the shapes, returns the height and/or width'
if not tuple_list:
return None
min_x = min(tuple_list, key=lambda tup: tup[0])[0]
max_x = max(tuple_list, key=lambda tup: tup[0])[0]
min_y = min(tuple_list, key=lambda tup: tup[1])[1]
max_y = max(tuple_list, key=lambda tup: tup[1])[1]
h = max_y - min_y
w = max_x - min_x
if ht and wt:
return h, w
elif ht:
return h
elif wt:
return w
def calculate_y_difference(tuple_list):
'''
returns the height of the objecy at its widest part
used to calculate the value of the pos_y of the next button
'''
sorted_by_x = sorted(tuple_list, key=lambda tup: tup[0], reverse=True)
y_diff = sorted_by_x[0][1] - sorted_by_x[1][1]
return abs(y_diff)
shapes = {
'1': [(500.0, 0.0), (500.0, 93.38888888888945), (0.0, 25.0), (0.0, 0.0), (500.0, 0.0)],
'2': [(500.0, 93.38888888888945), (500.0, 133.33333333333357), (0.0, 75.0), (0.0, 25.0), (500.0, 93.38888888888945)],
'3': [(500.0, 133.33333333333357), (500.0, 188.3333333333333), (0.0, 125.0), (0.0, 75.0), (500.0, 133.33333333333357)]}
shapes_height = {k: calculate_hw(shapes[k], ht = True) for k in shapes.keys() }
shape_pos_height = {k: calculate_y_difference(shapes[k]) for k in shapes.keys() }
shape_width = {k: calculate_hw(shapes[k], wt = True) for k in shapes.keys() }
#Loop through each shape, create it, crop it and save it as a .png
for shape_name, vertices in shapes.items():
# Create a new transparent image
width, height = 4000, 4000
background_color = (0, 0, 0, 0) # Transparent background
image_normal = Image.new("RGBA", (width, height), background_color)
image_down = Image.new("RGBA", (width, height), background_color)
# Create a drawing object
draw_normal = ImageDraw.Draw(image_normal)
draw_down = ImageDraw.Draw(image_down)
# Draw the irregular shape
draw_normal.polygon(vertices, ) #fill=(255, 0, 0, 128)) # Fill color with transparency
draw_down.polygon(vertices, fill=(0, 255, 0, 128)) # Fill color with transparency
# Find the bounding box of the polygon
min_x = min(point[0] for point in vertices)
min_y = min(point[1] for point in vertices)
max_x = max(point[0] for point in vertices)
max_y = max(point[1] for point in vertices)
#crop the image
image_normal = image_normal.crop((min_x, min_y, max_x, max_y))
image_down = image_down.crop((min_x, min_y, max_x, max_y))
# Save the image as a PNG file
image_normal.save(f"{shape_name}.png", "PNG", dpi=(2000, 2000), resolution_unit="in")
image_down.save(f"{shape_name}_down.png", "PNG", dpi=(2000, 2000), resolution_unit="in")
class MyButton(ButtonBehavior, Img):
'''
a very basic button with an image used instead of a Label
'''
def __init__(self, vert, normal, down, **kwargs):
super(MyButton, self).__init__(**kwargs)
self.vertices = vert
self.normal = normal
self.down = down
self.source = self.normal
def on_press(self):
self.source = self.down
def on_release(self):
self.source = self.normal
def on_touch_down(self, touch):
shape = Polygon(self.vertices) # Create a Shapely Polygon object from the vertices
if super(ButtonBehavior, self).on_touch_down(touch):
return True
if touch.is_mouse_scrolling:
return False
if not self.collide_point(touch.x, touch.y):
return False
if self in touch.ud:
return False
touch_point = Point(touch.x, touch.y) #Create a Shaply Touch Point from the touch
if shape.contains(touch_point): #Take the touch if inside the shape, else return False
touch.grab(self)
touch.ud[self] = True
self.last_touch = touch
self.__touch_time = time()
self._do_press()
self.dispatch('on_press')
return True
else:
return False
def on_touch_up(self, touch):
if touch.grab_current is not self:
return super(ButtonBehavior, self).on_touch_up(touch)
assert(self in touch.ud)
touch.ungrab(self)
self.last_touch = touch
if (not self.always_release and
not self.collide_point(*touch.pos)):
self._do_release()
return
touchtime = time() - self.__touch_time
if touchtime < self.min_state_time:
self.__state_event = Clock.schedule_once(
self._do_release, self.min_state_time - touchtime)
else:
self._do_release()
self.dispatch('on_release')
return True
class Main(FloatLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
x_offset = 100
y_offset = 300
initial_y_offset = y_offset
for shape in shapes: #Loop through the shape names
y_offset -= shape_pos_height[shape] #the y pos of the first shape, this is reduced by the width of the shape to achieve a stacking effect
#add the custom budget
B = (MyButton(
vert = [(tup[0]+x_offset, initial_y_offset - tup[1]) for tup in shapes[shape]], #alter the vertices of the shape based on the x_offset, y_offset
down = f'{shape}_down.png',
normal = f'{shape}.png',
size_hint = (None, None),
size = (shape_width[shape], shapes_height[shape]),
pos = (x_offset,y_offset)))
self.add_widget(B)
class MainApp(App):
def build(self):
return Main()
MainApp().run()
Upvotes: 0