Python detect two keys at once in tkinter - python

Finally figured out how to scroll the screen using only the keyboard in tkinter, took getting on the right website to show me the answer. Now I have one other small, but rather important problem that I'm experiencing.
The program is setup to scroll the underlying image using the cursor keys. If I push two keys(up/left) at the same time it will scroll either up one and left forever or up one and left forever instead of constantly switching back and forth.
How do get it to recognize that I'm pushing both and holding them down? It only recognizes one of the two, no matter which two keys I'm holding down.
import tkinter as tk
import random
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, background="bisque", width=400, height=400)
self.canvas.pack(fill="both", expand=True)
self.canvas.configure(scrollregion=(-1000, -1000, 1000, 1000))
self.canvas.bind("<Left>", self.keyleft)
self.canvas.bind("<Right>", self.keyright)
self.canvas.bind("<Up>", self.keyup)
self.canvas.bind("<Down>", self.keydown)
self.canvas.focus_set()
# the following two values cause the canvas to scroll
# one pixel at a time
self.canvas.configure(xscrollincrement=1, yscrollincrement=1)
# finally, draw something on the canvas so we can watch it move
for i in range(1000):
x = random.randint(-1000, 1000)
y = random.randint(-1000, 1000)
color = random.choice(("red", "orange", "green", "blue", "violet"))
self.canvas.create_oval(x, y, x+20, y+20, fill=color)
def keyup(self,event):
self.canvas.yview_scroll(-1,'units')
def keydown(self,event):
self.canvas.yview_scroll(1,'units')
def keyleft(self,event):
self.canvas.xview_scroll(-1,'units')
def keyright(self,event):
self.canvas.xview_scroll(1,'units')
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()

This isn't a tkinter problem; it's the way your OS handles the long pressing a key. Go to a text editor and press some keys (text keys, not arrows) and you'll see the same behavior. You OS probably has some settings to modify that behavior.
You could take over the press and hold behavior in tkinter and handle multiple keys that way, but that would require disabling this feature in your OS first. How you do that is OS-specific, and I doubt it's possible to disable it for your application only.
Edit: if you are ok with shutting off the OS key repeat feature either manually or programatically, you could use this code to have tkinter take over the key repeat:
import tkinter as tk
import random
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.keys = dict.fromkeys(('Left', 'Right', 'Up', 'Down'))
self.canvas = tk.Canvas(self, background="bisque", width=400, height=400)
self.canvas.pack(fill="both", expand=True)
self.canvas.configure(scrollregion=(-1000, -1000, 1000, 1000))
parent.bind("<KeyPress>", self.keypress)
parent.bind("<KeyRelease>", self.keypress)
self.canvas.focus_set()
# the following two values cause the canvas to scroll
# one pixel at a time
self.canvas.configure(xscrollincrement=1, yscrollincrement=1)
# finally, draw something on the canvas so we can watch it move
for i in range(1000):
x = random.randint(-1000, 1000)
y = random.randint(-1000, 1000)
color = random.choice(("red", "orange", "green", "blue", "violet"))
self.canvas.create_oval(x, y, x+20, y+20, fill=color)
self.looper() # start the looping
def keypress(self,event):
if event.keysym in self.keys:
# event type 2 is key down, type 3 is key up
self.keys[event.keysym] = event.type == '2'
def looper(self):
if self.keys['Up']:
self.canvas.yview_scroll(-1,'units')
if self.keys['Down']:
self.canvas.yview_scroll(1,'units')
if self.keys['Left']:
self.canvas.xview_scroll(-1,'units')
if self.keys['Right']:
self.canvas.xview_scroll(1,'units')
self.after(20, self.looper) # set the refresh rate here ... ie 20 milliseconds. Smaller number means faster scrolling
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Edit edit: Some googling suggests that some OS's send repeated 'press' signals rather than the press - release - press - release cycle that I see in Linux Mint. If your OS does then you may be able to use this code without disabling the autorepeat.

Related

Create functions to pack one widget and remove all others when different keys are pressed

I am making a controller for a car with Python and I was going to have 3 separate images to represent whether the wheels are turning left, right, or neutral. I need only one of these images to be shown at a time.
So far I have used bind to trigger functions because I haven't seen any other way to do so. I have looked into pack and pack_forget but I don't know how I could trigger them to be activated by other widgets (since I am using bind).
import tkinter as tk
win = tk.Tk()
def forwards(event):
print("going forwards...")
def left(event):
print("turning left...")
def right(event):
print("turning right...")
def backwards(event):
print("going backwards...")
neutralImage = tk.PhotoImage(file="neutral.gif")
leftImage = tk.PhotoImage(file="turnedLeft.gif")
rightImage = tk.PhotoImage(file="turnedRight.gif")
neutralPosition = tk.Label(win, image=neutralImage)
leftPosition = tk.Label(win, image=leftImage)
rightPosition = tk.Label(win, image=rightImage)
win.bind("w", forwards)
win.bind("a", left)
win.bind("d", right)
win.bind("s", backwards)
I have identified the problem as the following: I can't hide or show the widgets unless it is them that I press the button over.
Instead of having three widgets what you can do is replace the image of the same widget when you need it.
import tkinter as tk
def changeImage(imageLabelWidget, newImage):
imageLabelWidget.configure(image=newImage)
imageLabelWidget.image = newImage
win = tk.Tk()
neutralImage = tk.PhotoImage(file="neutral.gif")
leftImage = tk.PhotoImage(file="turnedLeft.gif")
rightImage = tk.PhotoImage(file="turnedRight.gif")
neutralPosition = tk.Label(win, image=neutralImage)
neutralPosition.image = neutralImage
neutralPosition.pack()
win.bind("w", lambda event, imageLabelWidget=neutralPosition, newImage=neutralImage:
changeImage(imageLabelWidget, newImage))
win.bind("a", lambda event, imageLabelWidget=neutralPosition, newImage=leftImage:
changeImage(imageLabelWidget, newImage))
win.bind("d", lambda event, imageLabelWidget=neutralPosition, newImage=rightImage:
changeImage(imageLabelWidget, newImage))
win.mainloop()

tkinter: Returning the (x,y) results of a mouse button click event to be used as a variable

I am eventually trying to run a code that's a bit more sophisticated than this, but this is the hump I'm trying to get over. I just need to be able to run a code that takes the x,y coordinates of the pixel clicked and records them as either a variable or appends them to a list. For right now I have them set as L_Click_x and L_Click_y, I would eventually like to be able to add these coordinates to a list or be able to call upon them in the function. Nothing I have tried seems to work, I'm pretty new to this so there's probably something fundamental that I'm just not getting. (Also the red lines that appear are just for visual confirmation of the click)
from tkinter import *
def motion(event):
x, y = event.x, event.y
print("Current Position = ",(x,y))
def LeftClick(event):
L_Click_x, L_Click_y = event.x, event.y
redline = canvas.create_line(0,L_Click_y,400,L_Click_y,fill = "red")
redline = canvas.create_line(L_Click_x,0,L_Click_x,300,fill = "red")
print("Left Click = ",(L_Click_x,L_Click_y))
root = Tk()
root.bind('<Motion>',motion)
root.bind('<Button-1>',LeftClick)
canvas = Canvas(root, width = "400",height = "300")
canvas.pack()
root.mainloop()
You will need to keep track of the first clicked point, until the second is known, and then draw a line segment between the two.
This simple example uses a global variable, and the points are discarded when drawn; you will probably want to use a container to keep track of the line segments, and a program construction that allows you to access it without making it global.
Please note that I bound the events to the canvas instead of root; I changed the imports to avoid cluttering the namespace (as your app grows, you'll be glad you did), and changed the camelcase names to a more pythonic convention.
import tkinter as tk
def motion(event):
x, y = event.x, event.y
print("Current Position = ", (x, y))
def left_click(event):
global line_segment
line_segment.append(event.x)
line_segment.append(event.y)
canvas.create_oval(event.x+1, event.y+1, event.x-1, event.y-1)
if len(line_segment) == 4:
canvas.create_line(*line_segment, fill="red")
line_segment = []
if __name__ == '__main__':
line_segment = []
root = tk.Tk()
canvas = tk.Canvas(root, width="400", height="300")
canvas.pack()
canvas.bind('<Motion>', motion)
canvas.bind('<Button-1>', left_click)
root.mainloop()

using python drag & drop selected image segment

My requirement is i need to drag an image to desired location.
Based on the link board-drawing code to move an oval
the following is the code snapshot i tried. I am not getting any errors its blank. Please let me know the way to take it forward.
Sample image segment attached
import Tkinter as tk
from Tkinter import *
from PIL import ImageTk, Image
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
# create a canvas
self.canvas = tk.Canvas(width=400, height=400)
self.canvas.pack(fill="both", expand=True)
# this data is used to keep track of an
# item being dragged
self._drag_data1 = {"x": 0, "y": 0, "item1": None}
startframe = tk.Frame(root)
canvas = tk.Canvas(startframe,width=1280,height=720)
startframe.pack()
canvas.pack()
one = tk.PhotoImage(file=r'images/test1.gif')
root.one = one # to prevent the image garbage collected.
canvas.create_image((0,0), image=one, anchor='nw',tags="img1")
self.canvas.tag_bind("img1", "<1>", self.on_token_press1)
self.canvas.tag_bind("img1", "<1>", self.on_token_release1)
self.canvas.tag_bind("token", "<B1-Motion>", self.on_token_motion1)
def on_token_press1(self, event):
print("sss")
# record the item and its location
self._drag_data1["item1"] = self.canvas.find_closest(event.x, event.y)[0]
self._drag_data1["x"] = event.x
self._drag_data1["y"] = event.y
def on_token_release1(self, event):
# reset the drag information
self._drag_data1["item1"] = None
self._drag_data1["x"] = 0
self._drag_data1["y"] = 0
def on_token_motion1(self, event):
'''Handle dragging of an object'''
# compute how much the mouse has moved
delta_x = event.x - self._drag_data1["x"]
delta_y = event.y - self._drag_data1["y"]
# move the object the appropriate amount
self.canvas.move(self._drag_data1["item1"], delta_x, delta_y)
# record the new position
self._drag_data1["x"] = event.x
self._drag_data1["y"] = event.y
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
As said in the comments:
Indentation is important!
Within the Example-class its initiation method this entire code-block is misaligned:
startframe = tk.Frame(root)
canvas = tk.Canvas(startframe,width=1280,height=720)
startframe.pack()
canvas.pack()
one = tk.PhotoImage(file=r'images/test1.gif')
root.one = one # to prevent the image garbage collected.
canvas.create_image((0,0), image=one, anchor='nw',tags="img1")
self.canvas.tag_bind("img1", "<1>", self.on_token_press1)
self.canvas.tag_bind("img1", "<1>", self.on_token_release1)
self.canvas.tag_bind("token", "<B1-Motion>", self.on_token_motion1)
If I simply add a Tab to the indentation of the entire block I was able to run the OP's code without any problem.
Since the script requires a GIF file to be placed in images/test1.gif I downloaded and used this GIF:
tkinter doesn't seem to actually play the gif (which isn't asked by the OP), but it does indeed show it.

How can I update a Tkinter canvas in the middle of a function call?

I'm writing Conway's game of life using Tkinter, and I want to have a "Go" button which allows the animation to begin, and continue stepping automatically until terminated. I'm using a Canvas to draw the environment, but since the "Go" button needs to complete the function call before it updates the canvas, the window just hangs until I kill the process. I've attempted to use canvas.update_idletasks() and canvas.update() (followed by a few seconds of sleeping) at points where I want to updated the canvas but that doesn't seem to do the trick. Any Ideas? Below is my GameOfLife class, the Environment class just manages the "board" of cells.
from Tkinter import *
from random import *
from time import time
from Environment import *
class GameOfLife(object):
def __init__(self, master, envDim):
self.unitSize = 10
self.dimension = envDim * self.unitSize
self.environment = Environment(envDim)
self.environment.seedBoard()
self.started = False
frame = Frame(master)
frame.pack()
Button(frame, text = "Go", command = self.go_call).pack(side = LEFT)
Button(frame, text = "Clear", command = self.reset_call).pack(side = LEFT)
Button(frame, text = "Close", command = frame.quit).pack(side = RIGHT)
canvas = self.drawCanvas(master, self.dimension)
def drawCanvas(self, master, dimension):
self.canvas = Canvas(master, width = self.dimension, height = self.dimension)
self.canvas.pack()
return self.canvas
def go_call(self):
print "<< Go Call >>"
if self.environment.started == False:
self.environment.seedBoard()
self.drawState(self.environment)
self.environment.nextBoard()
self.started = True
while True:
self.environment.nextBoard()
self.canvas.delete(ALL)
self.drawState(self.environment)
self.canvas.update_idletasks()
sleep(4)
def reset_call(self):
print "<< Reset Call >>"
self.canvas.delete(ALL)
self.environment = Environment(self.environment.dim)
def drawState(self, environment):
size = self.unitSize
for x in range(environment.dim):
for y in range(environment.dim):
if environment.matrix[x][y].alive == True:
xs = x * size
ys = y * size
self.canvas.create_rectangle(xs, ys, xs+size, ys+size, fill = 'black')
envDim = 70
root = Tk()
gol = GameOfLife(root, envDim)
root.mainloop()
You should never put an infinite loop inside your GUI program, and you definitely should never, ever call sleep. You already have an infinite loop running -- the event loop -- so take advantage of it.
The way to do animations like this is to write a function that draws one frame of the animation (or turn of the game, pick your metaphor). Then, have that function call itself via after.
Roughly speaking your code should look like this:
def draw(self):
# draw the board according to the current state
...
# arrange for the next frame to draw in 4 seconds
self.after(4000, self.draw)
def __init__(self, ...):
...
self.go = tk.Button(self, text="Go", command=self.draw)
...
If you want to add a stop button, all it needs to do is set a flag. Then, in draw, simply check for the flag before calling self.after

Getting Tkinter pack_forget to work only on unique objects

Ubuntu Linux 11.04 / Python 2.7 / Tkinter
I'm a novice at Python GUIs and I'm having trouble making single objects disappear and reappear. pack_forget kind of works for me, but the way I'm doing it makes all the objects blink in and out, when I only want one at a time. In the boiled-down example below, I'm trying to only make object "a" appear and then disappear and then do the same for object "b".
I would be very grateful for any help, plus some better docs on this would be cool too. The stuff I've been able to find has been light on examples.
from Tkinter import *
import time
class Box:
def __init__(self, canvas):
self.canvas = canvas
def type(self, type):
if type == "1":
self.if = self.canvas.create_rectangle(0, 0, 50, 50, fill="red")
elif type == "2":
self.if = self.canvas.create_rectangle(50, 50, 100, 100, fill="blue")
def hide(self):
self.canvas.pack_forget()
self.canvas.update_idletasks()
root.update()
def unhide(self):
self.canvas.pack()
self.canvas.update_idletasks()
root.update()
root = Tk()
frame = Frame(root)
frame.pack()
canvas = Canvas(frame, width=500, height=200)
canvas.pack()
root.update()
a = Box(canvas)
a.type("1")
b = Box(canvas)
b.type("2")
a.unhide()
time.sleep(.5)
a.hide()
time.sleep(1)
b.unhide()
time.sleep(.5)
b.hide()
time.sleep(1)
There are many things wrong or curious about your code. For one, you should never call sleep in a program that has an event loop. When you call that, your whole program will freeze. If you want something to happen after a fixed period of time, use the after method to schedule a function to run in the future.
Second, there is no point in calling root.update after calling self.canvas.update_idletasks. For one, assume it is a rule written in stone that you should never call update. It's OK to call update if you know the ramifications of that, but since you are just learning and don't know, assume it's unsafe to call it. Plus, update does the same work as update_idletasks and more, so if you do choose to call update, calling update_idletasks is unnecessary.
As to your real problem. You seem to be wanting to create two distinct "Box" objects, and want to hide and show them independently (?). However, both boxes are rectangles that are drawn on the same canvas. When you call pack_forget, that affects the whole canvas so both of these objects will disappear and then reappear.
It isn't clear what your intention is. If each instance of "Box" is just a rectangle on the canvas, you don't want to use pack_forget because that works on widgets, not canvas objects.
If you just want the rectangles to appear and disappear you have several choices. You can destroy and recreate them each time, or you can use the canvas move or coords method to move the item to a non-visible portion of the canvas. You could also manipulate the stacking order to raise or lower an object above or below any or all other objects.
Here's a quick example that uses the trick of moving the items outside the visible area of the canvas.
from Tkinter import *
import time
class Box:
def __init__(self, canvas, type):
self.canvas = canvas
if type == "1":
self.rect = canvas.create_rectangle(0, 0, 50, 50, fill="red")
elif type == "2":
self.rect = canvas.create_rectangle(50, 50, 100, 100, fill="blue")
self.coords = canvas.coords(canvas, self.rect)
def hide(self):
# remember where this object was
self.coords = canvas.coords(self.rect)
# move it to an invisible part of the canvas
self.canvas.move(self.rect, -1000, -1000)
def unhide(self):
# restore it to where it was
self.canvas.coords(self.rect, *self.coords)
root = Tk()
frame = Frame(root)
frame.pack()
canvas = Canvas(frame, width=500, height=200)
canvas.pack()
a = Box(canvas, type="1")
b = Box(canvas, type="2")
root.after(1000, a.hide)
root.after(2000, a.unhide)
root.after(3000, b.hide)
root.after(4000, b.unhide)
root.mainloop()
Finally, if what you really want is for an object to blink continuously, you can have the hide method automatically call the unhide method and visa versa:
def hide(self):
# remember where this object was
self.coords = canvas.coords(self.rect)
# move it to an invisible part of the canvas
self.canvas.move(self.rect, -1000, -1000)
# unhide after a second
self.canvas.after(1000, self.unhide)
def unhide(self):
# restore it to where it was
self.canvas.coords(self.rect, *self.coords)
# re-hide after a second
self.canvas.after(1000, self.hide)

Categories