I have a problem in Python.
I'm using Tkinter and have four bind events, that listen to key presses on my form.
My problem is, that these don't run asynchronously. So, for example I can press one button, and the events are recognized. But when I press and hold two keys at the same time, just one event gets fired.
Is there an alternative way to do this?
self.f.bind("w", self.player1Up)
self.f.bind("s", self.player1Down)
self.f.bind("o", self.player2Up)
self.f.bind("l", self.player2Down)
Unfortunately, you are somewhat at the mercy of the underlying auto-repeat mechanism of your system. For example, on the mac I'm using at the moment if I press and hold "w" I'll get a stream of press and release events. While pressed, if I press "o" I get a stream of presses and releases for "o" but no more events for "w".
You will need to set up a mini state machine, and bind to both key press and key release events. This will let you track which keys are pressed and which are not. Then, each time you draw a frame you can query the machine to see which keys are pressed and act accordingly.
Here's a quick hack I threw together. I've only tested it on my mac, and only with python 2.5. I've made no real attempt at being "pythonic" or efficient. The code merely serves to illustrate the technique. With this code you can simultaneously press either "w" or "s" and "o" or "l" to move two paddles up and down.
'''Example that demonstrates keeping track of multiple key events'''
from Tkinter import *
class Playfield:
def __init__(self):
# this dict keeps track of keys that have been pressed but not
# released
self.pressed = {}
self._create_ui()
def start(self):
self._animate()
self.root.mainloop()
def _create_ui(self):
self.root = Tk()
self.p1label = Label(text="press w, s to move player 1 up, down",
anchor="w")
self.p2label = Label(text="press o, l to move player 2 up, down",
anchor="w")
self.canvas = Canvas(width=440, height=440)
self.canvas.config(scrollregion=(-20, -20, 420, 420))
self.p1label.pack(side="top", fill="x")
self.p2label.pack(side="top", fill="x")
self.canvas.pack(side="top", fill="both", expand="true")
self.p1 = Paddle(self.canvas, tag="p1", color="red", x=0, y=0)
self.p2 = Paddle(self.canvas, tag="p2", color="blue", x=400, y=0)
self._set_bindings()
def _animate(self):
if self.pressed["w"]: self.p1.move_up()
if self.pressed["s"]: self.p1.move_down()
if self.pressed["o"]: self.p2.move_up()
if self.pressed["l"]: self.p2.move_down()
self.p1.redraw()
self.p2.redraw()
self.root.after(10, self._animate)
def _set_bindings(self):
for char in ["w","s","o", "l"]:
self.root.bind("<KeyPress-%s>" % char, self._pressed)
self.root.bind("<KeyRelease-%s>" % char, self._released)
self.pressed[char] = False
def _pressed(self, event):
self.pressed[event.char] = True
def _released(self, event):
self.pressed[event.char] = False
class Paddle():
def __init__(self, canvas, tag, color="red", x=0, y=0):
self.canvas = canvas
self.tag = tag
self.x = x
self.y = y
self.color = color
self.redraw()
def move_up(self):
self.y = max(self.y -2, 0)
def move_down(self):
self.y = min(self.y + 2, 400)
def redraw(self):
x0 = self.x - 10
x1 = self.x + 10
y0 = self.y - 20
y1 = self.y + 20
self.canvas.delete(self.tag)
self.canvas.create_rectangle(x0,y0,x1,y1,tags=self.tag, fill=self.color)
if __name__ == "__main__":
p = Playfield()
p.start()
You could bind to "<Key>" and then check event.char and perform the action you want based on the value of that? Granted, I have no idea if this works when multiple keys are pressed at the same time, it may still run in to the exact same problem. I haven't used Tk in ages.
"<Key> The user pressed any key. The key is provided in the char member of the event object passed to the callback (this is an empty string for special keys)."
Related
I'm trying to create a TKinter based Space Invaders but I ahve trouble with the keyboard inputs.
Indeed, I've 3 "bind" :
If the right arrow is pressed, the "character" moves to the right of 10px
Same with left
Finally, if the space bar is pressed a shoot is created (right now it's only a white rectangle above the "character"
But there comes the problem, if I press the spacebar while holding an arrow the ship stops, I can't make it shoot while moving.
I've found some posts here already (including one saying it may be due to how the system handles keypress, and he gave a solution but I wasn't able to use it) about this problem but I couldn't apply their solutions as my english and Python knowledge are short (first time I use OOP and classes).
So I've tried creating a new type of bind : beginning a non-stop move to the right on press of the arrow and stop of it on release, same for left and a condition on double hold but I wasn't able to code it. Would it even handle the problem or I need to find something else ?
Here's my code (french notes so ask if needed) :
from tkinter import Tk, Canvas, Button, Label
class SpaceInvaders():
def __init__(self):
self.window = Tk()
self.window.geometry("1200x900")
self.label_score = Label(self.window, text="Score : ")
self.label_score.place(x=10, y=10)
self.label_vies = Label(self.window, text="Vies :")
self.label_vies.place(x=1130, y=10)
self.button_new = Button(self.window, text="New Game", width=15)
self.button_new.place(x=400, y=860)
self.button_quit = Button(self.window, text="Quit", width=15, command = lambda:self.window.destroy())
self.button_quit.place(x=680, y=860)
self.canvas = Canvas(self.window, height = "800", width = "1200", bg='black')
self.canvas.pack(expand=True)
#Vaisseau et 1er alien
self.vaisseau = Vaisseau(self.canvas)
self.alien = Alien(self.canvas,self.window)
class Alien():
def __init__(self,canvas,window):
self.canvas = canvas
self.window = window
self.alien_x = 0
self.alien_y = 0
self.direction = 1
self.alien = self.canvas.create_rectangle(self.alien_x,self.alien_y,self.alien_x+100,self.alien_y+20, fill='white')
def run(self):
direction = self.direction
if direction==1:
if self.alien_x<1100:
self.alien_x += 5
else:
self.direction = -1
elif direction == -1:
if self.alien_x>0:
self.alien_x -= 5
else:
self.direction = 1
self.canvas.coords(self.alien,self.alien_x,self.alien_y,self.alien_x+100,self.alien_y+20)
self.window.after(20,self.run) #La méthode after semble retourner une erreur à 13 chiffres
class Vaisseau():
def __init__(self,canvas):
self.canvas = canvas
self.player_x = 0
self.player = self.canvas.create_rectangle(self.player_x,780,self.player_x+60,802,fill='white')
def event_handler(self,event):
if event.keysym == 'Right':
self.move(True)
elif event.keysym == 'Left':
self.move(False)
elif event.keysym == "space":
self.tir()
def move(self,right):
if right and self.player_x<1140:
self.player_x += 10
self.canvas.coords(self.player,self.player_x,780,self.player_x+60,802)
elif not right and self.player_x>0:
self.player_x -= 10
self.canvas.coords(self.player,self.player_x,780,self.player_x+60,802)
self.right_hold = False
def tir(self):
self.canvas.create_rectangle(self.player_x+27,760,self.player_x+33,775,fill="white")
game = SpaceInvaders()
game.alien.run()
game.window.bind("<KeyPress>",game.vaisseau.event_handler)
game.window.mainloop()
If you want actions to be performed simultaneously you need to make them happen in the backround. Your code, as sits right now, is handling every action in serial fashion. For example if you hit the "left" button, your code will be moving your character left before anything else can happen. To deal with this you can use something like the threaded or multiprocess module. Both of them can help you thread your actions so they can be happening in the same time. Their docs are pretty specific on how you can use them.
threaded module : https://pypi.org/project/threaded/
multiprocess module : https://pypi.org/project/multiprocess/
I coded a game using pylet. It uses a static window with width 1600 and height 900 assuming users have a fullHD display so everything will be visible. However on some devices (with small displays) the window is way bigger as expected. I figured out that the pixel_ratio is set up (for example to 2.0) making each virtual pixel to be displayed double size (2x2) in physical pixel.
I want to prevent this behavior but can't figure out how, I know I can get the pixel ratio easily by get_pixel_ratio() but I actually don't know how to set them or prevent pyglet from automatically setting them.
I also tried to use glViewport which seemed to have an effect but it didn't worked the way I wanted.
So how can I change the pixel_ratio or prevent changing it automatically.
Asked around in the official discord server for information, as I tried to reproduce the issue myself with some code, and this is what I used to test it:
import math
from pyglet import *
from pyglet.gl import *
key = pyglet.window.key
class main(pyglet.window.Window):
def __init__ (self, width=800, height=600, fps=False, *args, **kwargs):
super(main, self).__init__(width, height, *args, **kwargs)
self.x, self.y = 0, 0
self.keys = {}
verts = []
for i in range(30):
angle = math.radians(float(i)/30 * 360.0)
x = 100*math.cos(angle) + 300
y = 100*math.sin(angle) + 200
verts += [x,y]
self.pixel_ratio = 100
self.circle = pyglet.graphics.vertex_list(30, ('v2f', verts))
self.alive = 1
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_key_release(self, symbol, modifiers):
try:
del self.keys[symbol]
except:
pass
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
self.keys[symbol] = True
def render(self):
self.clear()
glClear(pyglet.gl.GL_COLOR_BUFFER_BIT)
glColor3f(1,1,0)
self.circle.draw(GL_LINE_LOOP)
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
if __name__ == '__main__':
x = main()
x.run()
Not that the code mattered heh, since the variable pixel_ratio is just, and I quote: "indicating that the actual number of pixels in the window is larger than the size of the Window created."
This is something OSX does to cope with the high DPI, using this information you should be able to scale your graphics accordingly. Window.get_framebuffer_size() will show you the difference in requested Window size and Framebuffer size, if any.
So your only way to actually scale up, would be to use glScale or if you're using sprites you can use Sprite.scale to scale the image-data. If you're using 2D graphics I'd go with the sprite option as it's pretty easy to work with.
i try to many times to use event.x & event.y out side the function but nothing works
from tkinter import *
root = Tk()
def start_mouse_press(event):
print(f"starting mouse at {event.x, event.y}")
def stop_mouse_press(event):
print(f"stopping mouse at {event.x, event.y}")
root.bind('<ButtonPress-1>', start_mouse_press)
root.bind('<ButtonRelease-1>', stop_mouse_press)
root.mainloop()
Whilst not specifically neat, you can use global variables to store the start position for usage later.
Consider the below example
from tkinter import *
root = Tk()
start_pos = None
def start_mouse_press(event):
global start_pos
start_pos = (event.x, event.y)
def stop_mouse_press(event):
print(f"Start Pos {start_pos}\nEnd Pos {event.x, event.y}")
root.bind("<ButtonPress-1>", start_mouse_press)
root.bind("<ButtonRelease-1>", stop_mouse_press)
The start position is recorded so that when the mouse button is released, both the start and end positions can be printed.
You will never be able to access event outside of the function of which event is an argument.
This is a matter of scope, if you wish to use event elsewhere, pass event as an argument to the next function you wish to use it in.
I made this program, I recommend run it first. The main idea is popping window, it should pop after clicking on text info. This works correctly, than, an user needs to close this window, so I made red closing button (rectangle with lines) in top right corner. After clicking on this button, new window should disappear, but first window with ovals should stay.
I need function for closing this window, I was trying to find solution a long time, but I am not able to. Any idea please?
import tkinter
class Desktop:
def __init__(self):
self.canvas = tkinter.Canvas()
self.canvas.pack()
x, y = 25,25
for i in range(1,61):
self.canvas.create_oval(x-10, y-10, x+10, y+10, fill='blue')
self.canvas.create_text(x, y, text=i)
x += 30
if i%10==0:
x = 25
y += 40
self.canvas.create_text(340, 150, text='info', font='arial 17', tags='info')
self.canvas.tag_bind('info', '<Button-1>', self.window)
def window(self, event):
self.canvas.create_rectangle(40,40,310,220,fill='white')
x, y = 75,100
for i in range(1,9):
self.canvas.create_rectangle(x-30,y-30,x+30,y+30)
self.canvas.create_text(x,y,text='number ' + str(i), font='arial 9')
x += 65
if i == 4:
x = 75
y += 70
self.rec = self.canvas.create_rectangle(310,40,290,60, fill="red", tags="cancel")
self.line = self.canvas.create_line(310,40,290,60,310,60,290,40, tags="cancel")
self.canvas.tag_bind('cancel', '<Button-1>', self.close)
def close(self,event):
#?
print('should close this part of screen')
d = Desktop()
What I use in my code is:
def quit(self):
root.destroy()
self.quit()
This might be a really long winded way to do it but it works for me every time.
EDIT: Just thinking about it now, depending on the way you open a new frame,root.destroy would be the way I would do it.
It is possible to create events for when the mouse pointer enters/leaves the entire Listbox using <Enter>/<Leave>. How can I track when the mouse enters or leaves a specific entry (row) in the Listbox?
I want to color in different color the background of the entry over which the mouse pointer is currently located.
Here's an (half) attempt to do what you want by binding to the <Motion> event instead of the pair <Enter> and <Leave>. This because <Enter> is raised only when we enter the Listbox from outside it, but once we are inside a Listbox with the mouse, no other <Enter> event will be raised, and we cannot keep track of which item the mouse is above.
Calling a function every time the mouse moves might result in an overload of work, so I don't think this feature is worthing doing it (in this way).
The program does not still work perfectly, and I still have to understand why: basically, sometimes the item's background and font color are not changed properly, there's some kind of delay or something.
from tkinter import *
class CustomListBox(Listbox):
def __init__(self, master=None, *args, **kwargs):
Listbox.__init__(self, master, *args, **kwargs)
self.bg = "white"
self.fg = "black"
self.h_bg = "#eee8aa"
self.h_fg = "blue"
self.current = -1 # current highlighted item
self.fill()
self.bind("<Motion>", self.on_motion)
self.bind("<Leave>", self.on_leave)
def fill(self, number=15):
"""Fills the listbox with some numbers"""
for i in range(number):
self.insert(END, i)
self.itemconfig(i, {"bg": self.bg})
self.itemconfig(i, {"fg": self.fg})
def reset_colors(self):
"""Resets the colors of the items"""
for item in self.get(0, END):
self.itemconfig(item, {"bg": self.bg})
self.itemconfig(item, {"fg": self.fg})
def set_highlighted_item(self, index):
"""Set the item at index with the highlighted colors"""
self.itemconfig(index, {"bg": self.h_bg})
self.itemconfig(index, {"fg": self.h_fg})
def on_motion(self, event):
"""Calls everytime there's a motion of the mouse"""
print(self.current)
index = self.index("#%s,%s" % (event.x, event.y))
if self.current != -1 and self.current != index:
self.reset_colors()
self.set_highlighted_item(index)
elif self.current == -1:
self.set_highlighted_item(index)
self.current = index
def on_leave(self, event):
self.reset_colors()
self.current = -1
if __name__ == "__main__":
root = Tk()
CustomListBox(root).pack()
root.mainloop()
Note that I have used from tkinter import * for typing faster, but I recommend you to use import tkinter as tk.
No, you cannot track when it enters/leaves a specific row. However, you can track when it enters/leaves the widget, and you can compute which item the mouse is over by using the index method of the listbox. If you give an index of the form "#x,y", it will return the numerical index.
For example:
self.listbox.bind("<Enter>", self.on_enter)
...
def on_enter(self, event):
index = self.listbox.index("#%s,%s" % (event.x, event.y))
...