Tkinter drag and drop - python

I'm working on a drag and drop function that will allow me to move items around on a canvas.I have it working (Kind of) but I move only slightly but the line shoots across the screen (And eventually off the visible part of the canvas so I cannot get to it. I'm not sure where to go from here. Below is the drag and drop code I've created so far:
def onPressToMove(self, event): #get initial location of object to be moved
winX = event.x - self.workspace.canvasx(0)
winY = event.y - self.workspace.canvasy(0)
self.dragInfo["Widget"] = self.workspace.find_closest(event.x, event.y, halo = 5)[0]
self.dragInfo["xCoord"] = winX
self.dragInfo["yCoord"] = winY
def onReleaseToMove(self, event): #reset data on release
self.dragInfo["Widget"] = None
self.dragInfo["xCoord"] = 0
self.dragInfo["yCoord"] = 0
def onMovement(self, event):
winX = event.x - self.workspace.canvasx(0)
winY = event.y - self.workspace.canvasy(0)
newX = winX - self.dragInfo["xCoord"]
newY = winY - self.dragInfo["yCoord"]
self.workspace.move(self.dragInfo["Widget"], newX, newY)
dragInfo is a dictionary I'm using to store the data. Originally I thought that translating the canvas coordinates to window coordinates would help, but it acts the same as without that stuff.

This answer to the question "board drawing code to move an oval" shows how to drag an object on a canvas.
In your case, you're not resetting the base of the delta as you move the object. If the mouse moves one pixel to the right, you use move to move the mouse one pixel to the right.
Now, let's say you move it one more pixel to the right. This time, your calculation says the delta is 2 from the starting point even though you only actually moved the mouse one more pixel). Next time you move one pixel, you're calculating a delta of 3, and so on.
The solution is simple: reset dragInfo["xCoord"] and dragInfo["yCoord"] while it is moving, since you only want to compute the delta to its previous position, not the original starting position.
def onPressToMove(self, event): #get initial location of object to be moved
winX = event.x - self.canvas.canvasx(0)
winY = event.y - self.canvas.canvasy(0)
self.dragInfo["Widget"] = self.canvas.find_closest(event.x, event.y, halo = 5)[0]
# reset the starting point for the next move
self.dragInfo["xCoord"] = winX
self.dragInfo["yCoord"] = winY

Related

Why all balloons are hidden and not only the one I click?

I'm creating a simple game with Python using turtle package.
My aim is to have some balloons on the screen running from right to left and then when one of them is clicked to make it disappear.
What I have going wrong is that when I click one balloon, all of them are disappeared!
Here is my code
1- Main
from turtle import Screen
from balloon import Balloon
import time
screen = Screen()
screen.title('Balloons Nightmare')
screen.setup(width=600, height=600)
screen.bgpic(picname='sky-clouds.gif')
screen.tracer(0)
balloons_manager = Balloon()
current_x = 0
current_y = 0
screen.listen()
screen.onclick(fun=balloons_manager.explode_balloon, btn=1)
game_is_on = True
while game_is_on:
time.sleep(0.1)
screen.update()
balloons_manager.create_balloon()
balloons_manager.move_balloon()
screen.exitonclick()
2- balloon module
import random
from turtle import Turtle
COLORS = ["red", "yellow", "green", "blue", "black"]
MOVEMENT_SPEED = 2
class Balloon:
def __init__(self):
self.all_balloons = []
self.balloon_speed = MOVEMENT_SPEED
self.x = 0
self.y = 0
self.hidden = None
def create_balloon(self):
random_choice = random.randint(1, 9)
if random_choice == 1:
new_balloon = Turtle("circle")
new_balloon.penup()
new_balloon.turtlesize(stretch_wid=2, stretch_len=0.75)
new_balloon.color(random.choice(COLORS))
random_y_cor = random.randint(-50, 280)
new_balloon.goto(320, random_y_cor)
self.hidden = new_balloon.isvisible()
self.all_balloons.append(new_balloon)
def move_balloon(self):
for balloon in self.all_balloons:
balloon.backward(self.balloon_speed)
def explode_balloon(self, x, y):
for balloon in range(len(self.all_balloons)):
print(self.all_balloons[balloon].position())
self.all_balloons[balloon].hideturtle()
# for balloon in self.all_balloons:
# balloon.hideturtle()
I tried so many changes but nothing helped me, example of what I tried so far
getting the current x,y coordinates of the balloon so that on click to hide only the one with these coordinates but didn't work for me or I did something wrong with it
Any hints will be appreciated.
Thank you
I fixed the issue by adding an inner function in create_balloon function
def create_balloon(self):
random_choice = random.randint(1, 9)
if random_choice == 1:
new_balloon = Turtle("circle")
new_balloon.penup()
new_balloon.turtlesize(stretch_wid=2, stretch_len=0.75)
new_balloon.color(random.choice(COLORS))
random_y_cor = random.randint(-50, 280)
new_balloon.goto(320, random_y_cor)
def hide_the_balloon(x, y):
return new_balloon.hideturtle()
new_balloon.onclick(hide_the_balloon)
self.all_balloons.append(new_balloon)
Here's your code triggered by the click handler:
def explode_balloon(self, x, y):
for balloon in range(len(self.all_balloons)):
print(self.all_balloons[balloon].position())
self.all_balloons[balloon].hideturtle()
This loops over all balloons and hides them unconditionally.
You probably want to use an if in there to only conditionally trigger the hiding behavior. Compare the x and y coordinates of the click against the current balloon in the loop. Only hide the balloon if the distance is less than a certain amount (say, the radius of the balloon).
Another approach is to use turtle.onclick to add a handler function that will be trigged when the turtle is clicked.
Related:
How to see if a mouse click is on a turtle in python
Python find closest turtle via mouse click
Why is my Python Turtle program slowing down drastically the longer it runs? (since you're never removing turtles and constantly adding new ones, this is a good thread to take a look at)

Converting scroll bar coordinates to mouse coordinates

I'm trying to make a scroll bar and at the moment, the scroll works by changing the coordinates when blitting (as opposed to changing the actual rect coordinates). This means that rect collisions for buttons do not work when they are moved. I am attempting to combat this by calculating the percentage that the scroll bar has scrolled, converting that to some multiplier or screen coordinate, and then getting the mouse position.
Some notes:
Self.bar is the actual slider handle (the small thing you use to scroll)
Self.rect is the entire slider, and its height is equal to screen height
Self.total_h is the total height that the scroll bar needs to scroll, for example if it needed to scroll to 2x the screen height then total_h would equal screen_height * 2.
Some code I have tried so far:
# Calculate the distance between the top of the handle and the top of the overall bar and divide by the handle height
# (shortened from ((self.bar.rect.top - self.rect.top) / self.rect.h) * (self.rect.h / self.bar.rect.h) which makes more intuitive sense.
self.scroll_percent = ((self.bar.rect.top - self.rect.top) / self.bar.rect.h)
# These all do not work:
# pos_y = pg.mouse.get_pos()[1] * self.scroll_percent
# pos_y = pg.mouse.get_pos()[1] * (self.total_h / self.scroll_percent)
# pos_y = (self.total_h / self.scroll_percent) * pg.mouse.get_pos()[1]
# etc
The logic just doesn't make sense to me, and I've got no idea how to do this. To clarify, my goal is to allow the user to scroll the screen using a scroll bar, and depending on the scroll bar's position, we change the mouse pos accordingly.
I don't really understand why you bother with some percentage ? If I understood correctly you are only scrolling up and down so the only thing you need to know is the y offset, which is 0 when the scroll bar is at the top and then it is just the y value at which you are blitting your surface. So simply remove the y offset to your mouse y when you check for collision.
Maybe I missed something ?
If I understood corretly, here is an simple example of what to do :
(I didn't recreate the scroll bar since you said you've got this part working. I just made the surface go up automatically. I'm sure you will figure out a way to integrate this solution to your own code)
# General import
import pygame as pg
import sys
# Init
pg.init()
# Display
screen = pg.display.set_mode((500, 500))
FPS = 30
clock = pg.time.Clock()
# Surface declaration
drawing_surface = pg.Surface((screen.get_width(), screen.get_height() * 2))
drawing_surface.fill((255,0,0))
drawing_surface_y = 0
# Button
test_btn = pg.Rect(20, 400, 100, 40)
# Main functions
def update():
global drawing_surface_y
drawing_surface_y -= 1
def draw():
# Clear the screen
screen.fill((0,0,0))
# Render the button
pg.draw.rect(drawing_surface, (0,0,255), test_btn)
# Blit the drawing surface
screen.blit(drawing_surface, (0, drawing_surface_y))
def handle_input():
for evt in pg.event.get():
if evt.type == pg.QUIT:
exit()
if evt.type == pg.MOUSEBUTTONDOWN:
if evt.button == 1:
on_click()
def on_click():
mx, my = pg.mouse.get_pos()
if test_btn.collidepoint(mx, my - drawing_surface_y):
print("Test button has been clicked")
def exit():
pg.quit()
sys.exit()
# Other functions
# Main loop
if __name__ == "__main__":
while True:
handle_input()
update()
draw()
pg.display.update()
clock.tick(FPS)
Test this code and let me know if it answers your question !

Moving surface within smaller surface doesn't show previously hidden components

I'm coding some custom GUI objects for usage in pygame menus, while coding a scrollable box I hit an error.
This box works by moving a surface (which contains the components which are moved when scrolling) within a smaller surface which acts like a window to the confined surface. The surfaces mostly display correctly: the contents of the inner surface which are visible initially (the parts which fit within the window surface) display correctly, but when the inner surface is moved to reveal previously hidden components they are not displayed, the initial visible move correctly and are displayed when they return.
I think the issue is with the outer surface's clipping area thinking that only the already revealed components should be displayed and that the others are still hidden but I don't know.
The custom GUI components always have a Rect (returns the bounding rect for that component) and Draw (blits the component to the screen) functions.
Here is the code for the scroll area (and it's parent class):
class ScrollArea(BaseComponent):
"Implements a section of screen which is operable by scroll wheel"
def __init__(self,surface,rect,colour,components):
"""surface is what this is drawn on
rect is location + size
colour is colour of screen
components is iterable of components to scroll through (they need Draw and Rect functions), this changes the objects location and surface
"""
super().__init__(surface)
self.rect = pygame.Rect(rect)
self.colour = colour
self.components = components
self.Make()
def HandleEvent(self, event):
"Pass events to this; it enables the area to react to them"
if event.type == pygame.MOUSEBUTTONDOWN and self.rect.collidepoint(event.pos) and self._scroll_rect.h > self.rect.h:
if event.button == 4: self.scroll_y = min(self.scroll_y + 15,self._scroll_y_min)
if event.button == 5: self.scroll_y = max(self.scroll_y - 15,self._scroll_y_max)
def Make(self):
"Updates the area, activates any changes made"
_pos = self.rect.topleft
self._sub_surface = pygame.Surface(self.rect.size,pygame.SRCALPHA)
self.rect = pygame.Rect(_pos,self._sub_surface.get_rect().size)
self._sub_surface.unlock()#hopefully fixes issues
self._scroll_surf = pygame.Surface(self.rect.size)
self._scroll_rect = self._scroll_surf.get_rect()
scroll_height = 5
for component in self.components:
component.surface = self._scroll_surf
component.Rect().y = scroll_height
component.Rect().x = 5
component.Draw()
scroll_height += component.Rect().h + 5
self._scroll_rect.h = max(self.rect.h,scroll_height)
self.scroll_y = 0
self._scroll_y_min = 0
self._scroll_y_max = -(self._scroll_rect.h - self.rect.h)
def Draw(self):
"Draw the area and its inner components"
self._sub_surface.fill((255, 255, 255, 0))
self._sub_surface.blit(self._scroll_surf,(0,self.scroll_y))
pygame.draw.rect(self._sub_surface,self.colour,((0,0),self.rect.size),2)
self.surface.blit(self._sub_surface,self.rect.topleft)
def Rect(self):
"Return the rect of this component"
return self.rect
class BaseComponent:
def __init__(self,surface):
"surface is what this is drawn on"
self.surface = surface
def HandleEvent(self,event):
"Pass events into this for the component to react ot them"
raise NotImplementedError()
def Make(self):
"Redo calculations on how component looks"
raise NotImplementedError()
def Draw(self):
"Draw component"
raise NotImplementedError()
def ReDraw(self):
"Call Make then draw functions of component"
self.Make()
self.Draw()
def Rect(self):
"Return the rect of this component"
raise NotImplementedError()
To test this I used this code and a label component:
screen_width = 640
screen_height = 480
font_label = pygame.font.Font("freesansbold.ttf",22)
screen = pygame.display.set_mode((screen_width,screen_height))
grey = (125,125,125)
def LoadLoop():
#objects
scroll_components = []
for i in range(20):
scroll_components.append(Components.Label(screen,(0,0),str(i),font_label,grey))
scroll_area = Components.ScrollArea(screen,Components.CenterRect(screen_width/2,3*screen_height/16 + 120,300,200),grey,scroll_components)
clock = pygame.time.Clock()
running = True
while running:
#events
for event in pygame.event.get():
scroll_area.HandleEvent(event)
if event.type == pygame.QUIT:
running = False
pygame.quit()
exit()
#graphics
screen.fill(black)
scroll_area.Draw(components)
#render
pygame.display.update()
clock.tick(60)
This is the label component's code (it basically just prints text to screen with the location given as it's center):
class Label(BaseComponent):
"Class which implements placing text on a screen"
def __init__(self,surface,center,text,font,text_colour):
"""surface is what this is drawn on
center is the coordinates of where the text is to be located
text is the text of the label
font is the font of the label
text_colour is the text's colour
"""
super().__init__(surface)
self.center = center
self.text = text
self.font = font
self.text_colour = text_colour
self.Make()
def HandleEvent(self,event):
"Labels have no events they react to,\nso this does nothing"
def Make(self):
"(Re)creates the label which is drawn,\nthis must be used if any changes to the label are to be carried out"
self._text_surf = self.font.render(self.text, True, self.text_colour)
self._text_rect = self._text_surf.get_rect()
self._text_rect.center = self.center
def Draw(self):
"Draw the label , will not react to any changes made to the label"
self.surface.blit(self._text_surf,self._text_rect)
def Rect(self):
"Return the rect of this component"
return self._text_rect
This is the window produced by this code:
Before scrolling
After scrolling
I also did it with a different size of ScrollArea, one of the Labels was positioned through the bottom and it was cut in half, when scrolled the cut remained.
Please help.
Sidenote on conventions
First, a sidenote on conventions: class names should start with an uppercase letter, function and method names should be all lowercase.
They are conventions, so you are free to not follow them, but following the conventions will make your code more readable to people used to them.
The quick fix
The error is in the ScrollArea.Make() method. Look carefully at these two lines:
self._sub_surface = pygame.Surface(self.rect.size,pygame.SRCALPHA)
self._scroll_surf = pygame.Surface(self.rect.size)
self._sub_surface is the surface of the window of the scroll area. self._scroll_surf is the scrolling surface. The latter should be higher, but you set them to the same size (same width is fine, same height not).
Obviously when you loop over your component list to blit the Label, the ones which are outside self._sub_surface are also outside self._scroll_surf and hence are not blit at all. You should make self._scroll_surf higher. Try for example:
self._scroll_surf = pygame.Surface((self.rect.width, self.rect.height*10)
Better would be to estimate the proper height to contains all your labels, which should be scroll_height, but you calculate it later in the method, so you should figure how to do properly this part.
A general advice
In general, I think you have a design problem here:
for i in range(20):
scroll_components.append(Label(screen,(0,0),str(i),font_label,grey))
scroll_area = ScrollArea(screen, pygame.Rect(screen_width/2,3*screen_height/16 + 120,300,200),grey,scroll_components)
When you create each label, you pass the screen as the reference surface where the Draw method blits.
But these labels should be blitted on the scroll_surf of your ScrollArea. But you cannot do it because you have not instantiated yet the ScrollArea, and you cannot instantiate before the scroll area because you require the Labels to be passed as an argument.
And in fact in the ScrollArea.Make() method you overwrite each label surface attribute with the _scroll_surf Surface.
I think would be better to pass to ScrollArea a list of strings, and let the ScrollArea.__init__() method to create the labels.
It will look less patched and more coherent.

tkinter - the shape isn't uniform and the animation looks bad

Started playing with python's tkinter today and ran into some problems.
I created an animation that moves a ball around the screen, with a given speed. (and when it hits the screen, it goes back)
Why does my ball look bad? it's shape is not uniform? (its like blinking a lot)
Is there a better way to do it?
the code:
from tkinter import *
import time
WIDTH = 800
HEIGHT = 500
SIZE = 100
tk = Tk()
canvas = Canvas(tk, width=WIDTH, height=HEIGHT, bg="grey")
canvas.pack()
color = 'black'
class Ball:
def __init__(self):
self.shape = canvas.create_oval(0, 0, SIZE, SIZE, fill=color)
self.speedx = 3
self.speedy = 3
def update(self):
canvas.move(self.shape, self.speedx, self.speedy)
pos = canvas.coords(self.shape)
if pos[2] >= WIDTH or pos[0] <= 0:
self.speedx *= -1
if pos[3] >= HEIGHT or pos[1] <= 0:
self.speedy *= -1
ball = Ball()
while True:
ball.update()
tk.update()
time.sleep(0.01)
errors after terminating the program:
Traceback (most recent call last):
File "C:/..py", line 29, in <module>
ball.update()
File "C:/Users/talsh/...py", line 20, in update
canvas.move(self.shape, self.speedx, self.speedy)
File "C:\Users\...\tkinter\__init__.py", line 2585, in move
self.tk.call((self._w, 'move') + args)
_tkinter.TclError: invalid command name ".!canvas"
Is it normal? Am I doing anything wrong?
I would imaging the problem is coming from sleep(). The methods sleep() and wait() should not be used in tkinter as they will pause the entire application instead of just providing a timer.
Update:
Its also not a good idea to name a method the same name as a built in method.
you have self.update() and update() is already in the name space for canvas. Change self.update() to something else like: self.ball_update()
UPDATE:
It looks like tikinter refreshes at a 15ms rate and trying to fire an even faster than that might cause issues. The closest I was able to get to stopping the circle from distorting while moving at the same rate as your original code was to change the timer to 30ms and to change your speed variables to 9 from 3.
Always make sure you have mainloop() at the end of you tkinter app. mainloop() is required to make sure tkinter runs properly and without there may be bugs caused by it missing so at the end add tk.mainloop()
You should use after() instead. This should probably be done using a function/method as your timed loop. Something like this:
def move_active(self):
if self.active == True:
self.ball_update()
tk.after(30, self.move_active)
tk.update()
Replace your while loop with the above method and add the class attribute self.active = True to your __init__ section. Let me know if this clears up your stuttering:
from tkinter import *
import time
WIDTH = 800
HEIGHT = 500
SIZE = 100
tk = Tk()
canvas = Canvas(tk, width=WIDTH, height=HEIGHT, bg="grey")
canvas.pack()
color = 'black'
class Ball:
def __init__(self):
self.shape = canvas.create_oval(0, 0, SIZE, SIZE, fill=color)
self.speedx = 9 # changed from 3 to 9
self.speedy = 9 # changed from 3 to 9
self.active = True
self.move_active()
def ball_update(self):
canvas.move(self.shape, self.speedx, self.speedy)
pos = canvas.coords(self.shape)
if pos[2] >= WIDTH or pos[0] <= 0:
self.speedx *= -1
if pos[3] >= HEIGHT or pos[1] <= 0:
self.speedy *= -1
def move_active(self):
if self.active == True:
self.ball_update()
tk.after(30, self.move_active) # changed from 10ms to 30ms
ball = Ball()
tk.mainloop() # there should always be a mainloop statement in tkinter apps.
Here are some links to Q/A's related to refresh timers.
Why are .NET timers limited to 15 ms resolution?
Why does this shape in Tkinter update slowly?
All that being said you may want to use an alternative that might be able to operate at a faster refreash rate like Pygame
UPDATE:
Here is an image of what is happening to the circle while its moving through the canvas. As you can see its getting potions of the circle visibly cut off. This appears to happen the faster the update is set. The slower the update( mostly above 15ms) seams to reduce this problem:
After suffering the same flattened leading edges of fast moving objects I am inclined to agree with #Fheuef's response, although I solved the problem in a different manner.
I was able to eliminate this effect by forcing a redraw of the entire canvas just by resetting the background on every update.
Try adding:
canvas.configure(bg="grey")
to your loop. Of course we compromise performance, but it's a simple change and it seems a reasonable trade off here.
Basically I've found that this has to do with the way Tkinter updates the canvas image : instead of redrawing the whole canvas everytime, it forms a box around things that have moved and it redraws that box. The thing is, it seems to use the ball's old position (before it moved) so if the ball moves too fast, its new position is out of the redraw box.
One simple way to solve this however is to create a larger invisible ball with outline='' around it, which will move to the ball's position on every update, so that the redraw box takes that ball into account and the smaller one stays inside of it. Hope that's clear enough...

The score in my python game doesn't add up correctly

So I've been making a little game just to test myself and I can't seem to make the score update at all, could I please have some help? EDIT: Sorry for being unspecific with the question, what the problem is when the Asteroid goes past the screen, the first one gives ten points but then after that the score doesn't go up? I think it's a problem with this segment of code, I'd want it so after the asteroid as passed below the screen, the asteroid is deleted and then it adds 10 points to the score. I'm the Livewires module as well.
class Asteroid(games.Sprite):
"""
A asteroid which falls through space.
"""
image = games.load_image("asteroid_med.bmp")
speed = 1
def __init__(self, x, y = 10):
""" Initialize a asteroid object. """
super(Asteroid, self).__init__(image = Asteroid.image,
x = x, y = y,
dy = Asteroid.speed)
self.score = games.Text(value = 0, size = 25, color = color.green,
top = 5, right = games.screen.width-10)
games.screen.add(self.score)
def update(self):
""" Check if bottom edge has reached screen bottom. """
self.add_score()
def add_score(self):
if self.bottom > games.screen.height:
self.score.value+=10
self.score.right = games.screen.width - 10
self.destroy()
you're destroying the object after incrementing the score.
It would be a better design to have the asteroid dispatch an event or something that would update the score in a global object. Currently, the asteroid itself is keeping track of the score, which doesn't work because it gets destroyed.
You destroy the object after updating its score, and it appears that the score is an instance variable of the object. You should try making the score a static member, rather than creating a global variable. This will let your score persist through every instance of the Asteroid.
http://linuxwell.com/2011/07/21/static-variables-and-methods-in-python/

Categories

Resources