Tkinter: remove keyboard double-click prevention - python

I'm programming a python version of Asteroids (https://en.wikipedia.org/wiki/Asteroids_(video_game)) using Tkinter.
This is the piece of code that let the ship to move ahead:
def move(self, sx=0, sy=0, ms=2):
try:
self.root.after_cancel(self.m)
except AttributeError:
pass
ms += 1
if ms > 30:
return
self.parent.move(self.ship, sx, sy)
self.m = self.root.after(ms, lambda sx1=sx, sy1=sy, millisec=ms: self.move(sx1, sy1, ms))
And here there's the actual fuction that runs when Up Arrow is pressed:
def avanti(self, event):
self.s = -2.5
x = self.s * math.sin(math.radians(self.angle)) * -1
y = self.s * math.cos(math.radians(self.angle))
self.move(x, y)
When you press UpArrow multiple times, the code works pretty well, the only problem is that when you hold it down, the ship moves once, then there's a small break like 0.2s long, and then it starts going ahead regularly, until you release the key. I think that the computer prevents you to double-click worngly and then, when it sees that you actually want to, it removes this prevenction.
Is there a way to remove this block since the first press?
EDIT: You can actually change the typing delay on the entire computed by going on Keyboard -> typing delay but what I want to do is to remove the delay only on the python program.

You can use <Up> to set move_up = True and <KeyRelease-Up> to set move_up = False and then you can use after to run function which will check move_up and move object.
Working example - code from other question (about moving platform)
import tkinter as tk
# --- constants ---
DISPLAY_WIDTH = 800
DISPLAY_HEIGHT = 600
CENTER_X = DISPLAY_WIDTH//2
CENTER_Y = DISPLAY_HEIGHT//2
# --- functions ---
# for smooth move of platform
def up_press(event):
global platform_up
platform_up = True
def up_release(event):
global platform_up
platform_up = False
def down_press(event):
global platform_down
platform_down = True
def down_release(event):
global platform_down
platform_down = False
def eventloop():
# move platform
if platform_up:
# move
canvas.move(platform, 0, -20)
# check if not leave canvas
x1, y1, x2, y2 = canvas.coords(platform)
if y1 < 0:
# move back
canvas.move(platform, 0, 0-y1)
if platform_down:
# move
canvas.move(platform, 0, 20)
# check if not leave canvas
x1, y1, x2, y2 = canvas.coords(platform)
if y2 > DISPLAY_HEIGHT:
# move back
canvas.move(platform, 0, -(y2-DISPLAY_HEIGHT))
root.after(25, eventloop)
# --- main ---
# - init -
root = tk.Tk()
canvas = tk.Canvas(root, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT)
canvas.pack()
# - objects -
platform = canvas.create_rectangle(CENTER_X-15, CENTER_Y-15, CENTER_X+15, CENTER_Y+15, fill="green")
platform_up = False
platform_down = False
root.bind('<Up>', up_press)
root.bind('<KeyRelease-Up>', up_release)
root.bind('<Down>', down_press)
root.bind('<KeyRelease-Down>', down_release)
# - mainloop -
root.after(25, eventloop)
root.mainloop()

Related

How can I add collisions to a list of objects (circles) in tkinter?

import time
from tkinter import *
import random
class SpaceField:
def __init__(self):
self.window = Tk()
self.window.title("Asteriods")
self.canvas = self.canvas_display() #creates canvas
self.asteriods = self.asteriod_creation_seperation() #creates asteroids
self.active = True
self.move_active() #Moves asteroids
self.canvas.update()
def asteriod_creation_seperation(self): #creation of multple asteriods
asteriod_spacingx = random.randint(1,800)
asteriod_spacingy = random.randint(1,800)
asteriod_list = list() # could list([])
for i in range(15):
asteriod = self.canvas.create_oval( 30, 50 , 80 , 100 , tags="asteriod", width=2, outline="white")
asteriod_list.append("asteriod")
self.canvas.move(asteriod, asteriod_spacingx, asteriod_spacingy)
asteriod_spacingx = random.randint(1,500)
asteriod_spacingy = random.randint(1,500)
print(asteriod_spacingy)
return asteriod_list
Asteroid Creation. Creates asteroids and gives them random positions.
def asteriod_update(self): #asteriods movement method #MAin problem
x12 = 1
self.canvas.move("asteriod", 3, x12)
pos = self.canvas.coords("asteriod")
print(pos)
if (pos)[2] > 500:
x12 *= 5
I think this is where I need to add the collision detection. I just have no idea how to combine the lists of the circles and the collisions.
def move_active(self): #Basically a true loop
if self.active:
self.asteriod_update()
self.window.after(40, self.move_active)
def canvas_display(self): #canvas
canvas = Canvas(self.window, width=500, height=400, background='black')
canvas.pack(expand=True, fill="both")
canvas.update()
return canvas
Canvas display nothing special
def run(self):
self.window.mainloop()
if __name__ == '__main__':
SpaceF = SpaceField()
SpaceF.run()
Asteroids is a classic game but there were a number of problems in your code. The main one was calling move_active during initialization. This prevented the code from completing its mainloop initialization.
The other problem was the asteroid_update method that basically didn't do anything, also using tags to control all asteroids didn't work either.
Everything else was OK, although you might consider using polygons.
Here is one way to produce a bouncing objects program. I've inserted remarks that describe the methods used.
Objects change the speed and direction when they hit the boundary so their trajectories are randomized.
from tkinter import *
from random import randint as rnd
class SpaceField:
def __init__(self):
self.window = Tk()
self.window.title("Asteriods")
# Define canvas size and active flag
self.wide, self.high, self.active = 500, 400, True
self.canvas_display()
self.asteriod_creation_seperation()
def asteriod_creation_seperation(self):
self.asteroids, self.speed = [], []
size, radius = 50, 25
for i in range(15):
spacex = rnd(size, self.wide - size)
spacey = rnd(size, self.high - size)
self.asteroids.append( # Store oval item id
self.canvas.create_oval(
spacex, spacey, spacex+size, spacey+size,
width=2, tags = "asteriod", outline = "white"))
self.speed.append((rnd(1,4),rnd(1,4))) # Store random speed x, y
def asteriod_update(self): # MAIN DRIVER: Work on ALL asteroids
for i, a in enumerate(self.asteroids):
xx, yy = self.speed[i] # get speed data
x, y, w, h = self.canvas.coords(a)
# check for boundary hit then change direction and speed
if x < 0 or w > self.wide:
xx = -xx * rnd(1, 4)
if y < 0 or h > self.high:
yy = -yy * rnd(1, 4)
# Limit max and min speed then store it
self.speed[i] = (max( -4, min( xx, 4)), max( -4, min( yy, 4 )))
self.canvas.move(a, xx, yy) # update asteroid position
def move_active(self):
if self.active:
self.asteriod_update()
self.window.after(40, self.move_active)
def canvas_display(self):
self.canvas = Canvas(
self.window, width = self.wide,
height = self.high, background = "black")
self.canvas.pack(expand = True, fill = "both")
def run(self): # Begin asteroids here so that mainloop is executed
self.window.after(200, self.move_active)
self.window.mainloop()
if __name__ == "__main__":
SpaceF = SpaceField()
SpaceF.run()

Turtle mouse event only responds after timeout, not as you click

When clicking within the random squares, it says "You clicked!" only when it draws the next square, not when you initially click.
import turtle
import random
import time
t1 = turtle.Turtle()
t2 = turtle.Turtle()
s = turtle.Screen()
def turtle_set_up():
t1.hideturtle()
t2.hideturtle()
t2.penup()
s.tracer(1, 0)
def draw():
for i in range(2):
t1.fd(400)
t1.lt(90)
t1.fd(400)
t1.lt(90)
def drawbox():
t2.pendown()
for i in range(2):
t2.fd(50)
t2.lt(90)
t2.fd(50)
t2.lt(90)
t2.penup()
def box1():
X = random.randint(0, 350)
Y = random.randint(0, 350)
Xleft = X
Xright = X + 50
Ytop = Y + 50
Ybottom = Y
t2.goto(X, Y)
drawbox()
Here is where it checks if your mouse click is within the box drawn. If it is, it erases the old box for a new one to be drawn every 3 seconds.
I don't understand why it waits to say "You clicked!" until the 3 seconds are up for the loop instead of saying it when you initially click.
I cant place a break command after the print statement that prints "You clicked!" because it's outside the loop.
between here:
t_end = time.time() + 60 * 0.05
while time.time() < t_end:
def get_mouse_click_coor(x, y):
X_click = x
Y_click = y
if Xright > X_click > Xleft and Ytop > Y_click > Ybottom:
print('You clicked!')
s.onclick(get_mouse_click_coor)
t2.clear()
and here:
def starter():
turtle_set_up()
draw()
while 1:
box1()
starter()
Applications with interactive GUI (Graphical User Interface) are event-based, which means that they perform their actions when some events happen. For such applications, if you create a waiting loop for a given amount of time (as does your code) the whole application will be blocked for this amount of time. That's why the print is only executed after the 3s delay.
All GUI libraries include a scheme to activate some timer events. For the turtle API, there is a on_timer(func, delay) method that calls a function func after some delay (expressed in milliseconds). The idea is to repeatedly call your drawbox function every 3000ms. So, your code will be based on two main callback functions: get_mouse_click called on click events, and drawbox called on timer events. Here is the modified code, I propose:
import turtle
import random
t1 = turtle.Turtle()
t2 = turtle.Turtle()
s = turtle.Screen()
def turtle_set_up():
t1.hideturtle()
t2.hideturtle()
s.tracer(1, 0)
s.onclick(get_mouse_click) # define the 'click' event callback
def draw():
for i in range(2):
t1.fd(400)
t1.lt(90)
t1.fd(400)
t1.lt(90)
def drawbox():
global box # store box coordinates as global variable
X = random.randint(0, 350) # so that they are available for testing
Y = random.randint(0, 350) # in the mouse click callback
box = (X, Y, X+50, Y+50)
t2.clear() # clear previous box before drawing new one
t2.penup()
t2.goto(X, Y)
t2.pendown()
for i in range(2):
t2.fd(50)
t2.lt(90)
t2.fd(50)
t2.lt(90)
s.ontimer(drawbox, 3000) # define timer event callback
def get_mouse_click(x, y):
if box[0] <= x <= box[2] and box[1] <= y <= box[3]:
print('You clicked!')
def starter():
turtle_set_up()
draw()
drawbox()
starter()
Computers have to run in sequence so it can only process one line at a time, so, unless i'm mistaken, your program gets 'caught' on the timer then runs through the program and back to the start.
you could develop a while loop with a nested if statement that gets datetime from the device, if turtles have datetime
I believe you can simplify this problem. Primarily by making a turtle be the inner box rather than drawing the inner box. This simplifies drawing, erasing and event handling. Avoid invoking the tracer() method until you have a working program as it only complicates debugging. We can also stamp rather than draw the border.
If we simply want to be able to click on the inner box and have it randomly move to a new location:
from turtle import Turtle, Screen
from random import randint
BORDER_SIZE = 400
BOX_SIZE = 50
CURSOR_SIZE = 20
def move_box():
x = randint(BOX_SIZE/2 - BORDER_SIZE/2, BORDER_SIZE/2 - BOX_SIZE/2)
y = randint(BOX_SIZE/2 - BORDER_SIZE/2, BORDER_SIZE/2 - BOX_SIZE/2)
turtle.goto(x, y)
def on_mouse_click(x, y):
print("You clicked!")
move_box()
screen = Screen()
turtle = Turtle('square', visible=False)
turtle.shapesize(BORDER_SIZE / CURSOR_SIZE)
turtle.color('black', 'white')
turtle.stamp()
turtle.shapesize(BOX_SIZE / CURSOR_SIZE)
turtle.onclick(on_mouse_click)
turtle.penup()
turtle.showturtle()
move_box()
screen.mainloop()
If we want to make the program more game-like and require the user to click on the inner box within 3 seconds of each move, or lose the game, then we can introduce the ontimer() event as #sciroccorics suggests:
from turtle import Turtle, Screen
from random import randint
BORDER_SIZE = 400
BOX_SIZE = 50
CURSOR_SIZE = 20
CLICK_TIMEOUT = 3000 # milliseconds
def move_box():
x = randint(BOX_SIZE/2 - BORDER_SIZE/2, BORDER_SIZE/2 - BOX_SIZE/2)
y = randint(BOX_SIZE/2 - BORDER_SIZE/2, BORDER_SIZE/2 - BOX_SIZE/2)
turtle.goto(x, y)
screen.ontimer(non_mouse_click, CLICK_TIMEOUT)
def on_mouse_click(x, y):
global semaphore
print("You clicked!")
semaphore += 1
move_box()
def non_mouse_click():
global semaphore
semaphore -= 1
if semaphore < 1:
turtle.onclick(None)
turtle.color('black')
print("Game Over!")
screen = Screen()
semaphore = 1
turtle = Turtle('square', visible=False)
turtle.shapesize(BORDER_SIZE / CURSOR_SIZE)
turtle.color('black', 'white')
turtle.stamp()
turtle.shapesize(BOX_SIZE / CURSOR_SIZE)
turtle.onclick(on_mouse_click)
turtle.penup()
turtle.showturtle()
move_box()
screen.mainloop()

Python pygame function not defined

I've been asked to find a problem with my cousin's Pygame code. I'm not big on Python, using other languages more and I haven't been able to find the issue by googling or debugging. Basically he's getting a "playGame is not defined" error, playGame being a function. Other questions about this are usually because:
The function is called before it is declared
The function is declared inside a different scope from which it is called
Neither of these seems to be the issue so I'm hoping someone more versed in Python can spot it. I've copied his code below with a lot of what (I hope) is irrelevant to the question removed to simplify.
The function playGame is not working and is called by a button click under
def button(msg, x, y, action = None):
. Interestingly the exit function is working fine which is called and declared exactly the same as playGame as far as I can tell.
# --------------- SETUP ---------------
# Importing necessary modules
import pygame
import math
import random
# --------------- DISPLAY ---------------
# Setting up the display
pygame.init()
screen = pygame.display.set_mode((width, height))
pygame.display.set_caption(title)
# --------------- GLOBALS ---------------
#removed globals from stackoverflow version
# --------------- FUNCTIONS ---------------
# Blitting the text
def blitText(angle, power):
#code
# Drawing the tank model
def drawTank():
#code
# Creating the buttons
def button(msg, x, y, action = None):
mousePos = pygame.mouse.get_pos() # Gets the mouse position
mouseClick = pygame.mouse.get_pressed()
(buttonWidth, buttonHeight) = (175, 45) # Sets the button width and height
if x + (buttonWidth / 2) > mousePos[0] > x - (buttonWidth / 2) and y + buttonHeight > mousePos[1] > y: # Checks if the mouse is over the button
pygame.draw.rect(screen, darkGrey, [x - (buttonWidth / 2), y, buttonWidth, buttonHeight]) # Draws a dark grey button
if mouseClick[0] == 1 and action != None: # Checks if the button is clicked
if action == "play":
playGame()
elif action == "exit":
exit()
else:
pygame.draw.rect(screen, grey, [x - (buttonWidth / 2), y, buttonWidth, buttonHeight]) # Draws a light grey button if not
screen.blit(msg, [x - (buttonWidth / 2), y]) # Writes the text over the button
# Defining the shell
class shell(pygame.sprite.Sprite): # Creates the shell() class
def __init__(self): # Defines an initiation fuction for this class
super().__init__() # Call the parent class constructor
self.image = pygame.Surface([2, 2]) # Defines the bullet as a 2x4 surface
self.image.fill(black) # Paints the bullet black
self.rect = self.image.get_rect() # Gets the area size of the bullet
def update(self): # Defines a function as update for this class
(bulletChangeX, bulletChangeY) = (((maxAngle - angle) / maxAngle) * (bulletSpeed * power), (angle / maxAngle) * (bulletSpeed * power)) # Ccalculates the changes in x and y
bulletChangeY -= vert # Changes the trajectory of the bullet
self.rect.y -= bulletChangeY # Moves the bullet in the y axis
self.rect.x += bulletChangeX # Moves the bullet in the x axis
# --------------- TITLE SCREEN ---------------
# Creating the main menu
menu = True
while menu: # Starts the loop
for event in pygame.event.get():
if event.type == pygame.QUIT: # Checks if pygame has been closed
exit() # Exits python
screen.fill(white) # Fills the screen white
screen.blit(titleText, [0, 0]) # Writes the title text
button(startButton, width / 2, (height / 3) * 2, "play") # Calls the button function for the start button
button(exitButton, width / 2, ((height / 3) * 2) + 70, "exit") # Calls the button function for the exit button
# Updating the display
pygame.display.update() # Updates the display
clock.tick(fps)
# --------------- MAIN LOOP ---------------
# Running the program
def playGame():
#code. This function has no return value.
# --------------- EXIT ---------------
# Exits PyGame and Python
def exit():
pygame.quit()
quit()
Hopefully the mistake is obvious here to someone and I haven't removed any key code that is causing the problems (I removed start variable declarations and the contents of function code) I can provide the full code if people need it.
Yes, themistake is obvious - as you put it:
The cod e is trying to call the function before it is defined -
the while menu code which draws the menu screen and draws the button is placed before the playGame function - which name is undeclared at that point.
While Python does run code on the module top-level, the best pratice is to leave only some constant and variable declarations on the toplevel, and putting code like the block while menu: ... inside a function. (Which may be called main - but there is no top language requirement on its name)
Then, at the very bottom of the file, make a call to that function, with a call placed - this time correctly, at the module body -
So - something along:
def main():
# Creating the main menu
menu = True
while menu: # Starts the loop
for event in pygame.event.get():
if event.type == pygame.QUIT: # Checks if pygame has been closed
exit() # Exits python
screen.fill(white) # Fills the screen white
screen.blit(titleText, [0, 0]) # Writes the title text
button(startButton, width / 2, (height / 3) * 2, "play") # Calls the button function for the start button
button(exitButton, width / 2, ((height / 3) * 2) + 70, "exit") # Calls the button function for the exit button
# Updating the display
pygame.display.update() # Updates the display
clock.tick(fps)
And at the very bottom, place a single main() call would make that particular error go away.

Tkinter canvas animation flicker

I wrote a Python program with Tkinter that makes a ball bounce around the screen. It works great, except for one problem: the outermost edges of the ball flicker as the ball moves.
I understand Tkinter automatically does double buffering, so I thought I shouldn't be having problems with tearing. I'm not sure where the error is coming from. Any ideas on how I can get rid of it?
Code is below. Here's the gist of it: class Ball extends Tk and gets created when the program is run. It sets up the game, encapsulating it in a GModel. GModel then runs the game with run() and just loops over and over, calling update().
import threading
from time import sleep, clock
from tkinter import *
from tkinter import ttk
SPEED = 150 # Pixels per second
WIDTH = 400
HEIGHT = 500
RAD = 25
PAUSE = 0 # Time added between frames. Slows things down if necessary
class GModel:
# Contains the game data.
def __init__(self, can):
# can is Canvas to draw on
self.can = can
self.circ = can.create_oval(0,0, 2*RAD, 2*RAD)
# Position and velocity of the ball
self.x, self.y = RAD, RAD
self.dx, self.dy = SPEED, SPEED
# Ball is moving?
self.is_running = True
def update(self, elapsed_time):
# Updates the dynamic game variables. elapsed_time is in seconds.
change_x = self.dx * elapsed_time
change_y = self.dy * elapsed_time
self.can.move(self.circ, change_x, change_y)
self.x += change_x
self.y += change_y
self.resolve_collisions()
def resolve_collisions(self):
# If the ball goes off the edge, put it back and reverse velocity
if self.x - RAD < 0:
self.x = RAD
self.dx = SPEED
elif self.x + RAD > WIDTH:
self.x = WIDTH - RAD
self.dx = -SPEED
if self.y - RAD < 0:
self.y = RAD
self.dy = SPEED
elif self.y + RAD > HEIGHT:
self.y = HEIGHT - RAD
self.dy = -SPEED
def end_game(self):
self.is_running = False
def run(self):
last_time = 0.0
while self.is_running:
# Get number of seconds since last iteration of this loop
this_time = clock()
elapsed_time = this_time - last_time
last_time = this_time
try: # Use this here in case the window gets X'd while t still runs
self.update(elapsed_time)
except:
self.is_running = False
if (PAUSE > 0):
sleep(PAUSE) # Slow things down if necessary
class Ball(Tk):
def __init__(self):
Tk.__init__(self)
self.can = Canvas(self, width=WIDTH, height=HEIGHT)
self.can.grid(row=0, column=0, sticky=(N, W, E, S))
self.gm = GModel(self.can)
def mainloop(self):
t = threading.Thread(target=self.gm.run, daemon=True)
t.start()
Tk.mainloop(self)
def main():
Ball().mainloop()
if __name__ == "__main__":
main()

Swinging pendulum does not work

I am having a little trouble with this project. I have to create a pendulum using key handles and the code I have for the key's up and down don't seem to be working. "up" is suppose to make the pendulum go faster and "down" makes it go slower. This is the code that I have so far. can somebody please help.
from tkinter import * # Import tkinter
import math
width = 200
height = 200
pendulumRadius = 150
ballRadius = 10
leftAngle = 120
rightAngle = 60
class MainGUI:
def __init__(self):
self.window = Tk() # Create a window, we may call it root, parent, etc
self.window.title("Pendulum") # Set a title
self.canvas = Canvas(self.window, bg = "white",
width = width, height = height)
self.canvas.pack()
self.angle = leftAngle # Start from leftAngle
self.angleDelta = -1 # Swing interval
self.delay = 200
self.window.bind("<Key>",self.key)
self.displayPendulum()
self.done = False
while not self.done:
self.canvas.delete("pendulum") # we used delete(ALL) in previous lab
# here we only delete pendulum object
# in displayPendulum we give the tag
# to the ovals and line (pendulum)
self.displayPendulum() # redraw
self.canvas.after(self.delay) # Sleep for 100 milliseconds
self.canvas.update() # Update canvas
self.window.mainloop() # Create an event loop
def displayPendulum(self):
x1 = width // 2;
y1 = 20;
if self.angle < rightAngle:
self.angleDelta = 1 # Swing to the left
elif self.angle > leftAngle:
self.angleDelta = -1 # Swing to the right
self.angle += self.angleDelta
x = x1 + pendulumRadius * math.cos(math.radians(self.angle))
y = y1 + pendulumRadius * math.sin(math.radians(self.angle))
self.canvas.create_line(x1, y1, x, y, fill="blue", tags = "pendulum")
self.canvas.create_oval(x1 - 2, y1 - 2, x1 + 2, y1 + 2,
fill = "red", tags = "pendulum")
self.canvas.create_oval(x - ballRadius, y - ballRadius,
x + ballRadius, y + ballRadius,
fill = "green", tags = "pendulum")
def key(self,event):
print(event.keysym)
print(self.delay)
if event.keysym == 'up':
print("up arrow key pressed, delay is",self.delay)
if self.delay >10:
self.delay -= 1
if event.keysym == 'Down':
print("Down arrow key pressed,delay is",self.delay)
if self.delay < 200:
self.delay += 1
if event.keysym=='q':
print ("press q")
self.done = True
self.window.destroy()
MainGUI()
The root of the problem is that the event keysym is "Up" but you are comparing it to the all-lowercase "up". Naturally, the comparison fails every time. Change the if statement to if event.keysym == 'Up':
Improving the animation
In case you're interested, there is a better way to do animation in tkinter than to write your own infinite loop. Tkinter already has an infinite loop running (mainloop), so you can take advantage of that.
Create a function that draws one frame, then have that function arrange for itself to be called again at some point in the future. Specifically, remove your entire "while" loop with a single call to displayFrame(), and then define displayFrame like this:
def drawFrame(self):
if not self.done:
self.canvas.delete("pendulum")
self.displayPendulum()
self.canvas.after(self.delay, self.drawFrame)
Improving the bindings
Generally speaking, your code will be marginally easier to manage and test if you have specific bindings for specific keys. So instead of a single catch-all function, you can have specific functions for each key.
Since your app supports the up key, the down key, and the "q" key, I recommend three bindings:
self.window.bind('<Up>', self.onUp)
self.window.bind('<Down>', self.onDown)
self.window.bind('<q>', self.quit)
You can then define each function to do exactly one thing:
def onUp(self, event):
if self.delay > 10:
self.delay -= 1
def onDown(self, event):
if self.delay < 200:
self.delay += 1
def onQuit(self, event):
self.done = True
self.window.destroy()

Categories

Resources