tkinter <KeyRelease> different behavior on Windows and Linux - python

In my CS class, my students just finished their first "clone your classic" contest. The PONG team went rapidly through the "Hey my paddle is frozen" issue with their two players on one keyboard version. I came across this problem 5 years ago and found Python bind - allow multiple keys to be pressed simultaniously that enlightened me (watch out ! The article uses python2.7). But I didn't realize then that the script only worked on windows machines.
On a linux system, the <KeyRelease-a> event triggers the callback, but the event.char then points to ' ' and not 'a' as one could expect. I tried googling the issue, but even on stackoverflow I couldn't find anything of interest.
Any hints? Next find the reproducible code sample:
import os
from tkinter import *
os.system("xset r off")
def keyup(e):
#print(f"up {e.char}")
print(f"up {e.keysym}")
def keydown(e):
#print(f"down {e.char}")
print(f"down {e.keysym}")
root = Tk()
frame = Frame(root, width=100, height=100)
frame.bind("<KeyPress>", keydown)
frame.bind("<KeyRelease>", keyup)
frame.pack()
frame.focus_set()
root.mainloop()
os.system("xset r on")
for reproducibility as asked by Bryan, which I thank for his concern about my question.

Just to close the subject, all the job has been done by Atlas435 : if you want to code a Pong with Tkinter, with two paddles listening independently to the keystrokes, follow this post Python bind - allow multiple keys to be pressed simultaniously but change e.char into e.keysym in the callbacks to get which key triggered the event Pressed or Released.

Related

Tkinter - CTRL KeyPress event is fired trough scrolling with TouchPad

The bounty expires in 6 days. Answers to this question are eligible for a +50 reputation bounty.
Thingamabobs wants to draw more attention to this question:
It seems like the issue is related to the manufacture hp. I would like to have an answer that explains the issue a little bit more, even though I think it is not really solvable.
I tried to track the control-key when pressed and released.
While everything seemed fine in the beginning a curious thing happened.
When scrolling with my touch-pad on my HP laptop on Windows 11 the KeyPress event is fired automatically.
While I used to search the C-code I didn't found a hint, but I might not fell into the right file. Is this a Windows thing and is normal behavior or is it a bug in tkinter?
My test-code:
import tkinter as tk
ctrl_pressed = None
def check_ctrl(event):
print(ctrl_pressed, 'checked')
def track_ctrl(event):
global ctrl_pressed
if (et:=event.type.name) == 'KeyPress':
ctrl_pressed = True
elif et == 'KeyRelease':
ctrl_pressed = False
print(ctrl_pressed, 'tracked')
root = tk.Tk()
root.bind('<MouseWheel>', check_ctrl)
root.bind('<KeyPress-Control_L>', track_ctrl)
root.bind('<KeyRelease-Control_L>', track_ctrl)
root.mainloop()
Using the MouseWheel first will output None - as expected
Using the Touchpad first will output True - unexpected
Pressing the Key will output first True then False - as expected
Seems to be an generated event:
def track_ctrl(event):
print(event.send_event)
produces True with touchpad.
patchlevel of tkinter 8.6.12
python version 3.11.0

Updating a TKinter Label during other loops

Currently I'm working on a project of mine involving sensors, and showing that sensory data on a display via TKinter. Everythings written in Python 3.7.3.
The issue im currently handling, is to update the label in the window, while the mainloop is running.
What i mean by this, is that if i execute the script, first the window options get defined, then the update function gets defined with a while true loop. Then its supposed to start the window. Now because of the while true loop it does not reach the window.mainloop() point (obviously, the while loop doesn't break...). My interest was peaked and i tried to put the window.mainloop() function inside the while loop of the update (please don't blame me, i know my script is a spaghetti mess.) I figured out that i could run the whole thing in threads, and so i decided to thread the whole window process, and add queues for the sensor data. Now the while loop was still in the way and didnt work properly, and after a bit of googling i found a code snippet that might help me. After trying to implement it in my script, i got an exception "function init expects 3 arguments, but 4 were given.." (code below) and I'm kinda running out of ideas on this.
Bear in mind that im not raelly a developer, i just need a script that can handle sensor data, dispaly it in a window, and export the current data to a database. So go easy on the blame please.
Current Script:
import time
import board
import adafruit_dht
import threading
import queue
from tkinter import *
dhtDevice = adafruit_dht.DHT22(board.D4, use_pulseio=False)
tempQ = queue.Queue(maxsize=0)
humQ = queue.Queue(maxsize=0)
class windowMain:
def __init__(self):
self.tempC_label = Label(fenster, text="Placeholder TempC")
self.humidity_label = Label(fenster, text="Placeholder Humidity")
self.tempC_label.pack()
self.humidity_label.pack()
self.tempC_label.after(2000, self.labelUpdate)
self.humidity_label.after(2000, self.labelUpdate)
def labelUpdate(self, tempQ, humQ):
self.tempC_label.configure(text= tempQ.get() + "°C")
#this is just to confirm if the function called or not, to see if the label updated or not.
#if the label didnt update, and the function called, there is something wrong with the function
#if the label didnt update, and the function didnt call, there is a problem somwhere else
print("Current Temp: " +tempQ.get() + "°C")
self.label.after(2000, self.labelUpdate)
if __name__ == "__main__":
windowName = Tk()
windowName.title = ("Climatemonitor")
windowMain(windowName)
windowName.mainloop()
try:
windowThread = threading.Thread(target=windowMain, args=(tempQ, humQ, ))
windowThread.start()
except:
print("Unable to start thread")
while True:
try:
temperature_c= dhtDevice.temperature
tempText= temperature_c
tempText= str(tempText)
tempQ.put(tempText)
humidity = dhtDevice.humidity
humidityP = str(humidity)
#this one is just to check if the sensor reads data
print(
"Temp: {:.1f} C Humidity: {}% ".format(
temperature_c, humidity
)
)
time.sleep(2.0)
except RuntimeError as error:
print(error.args[0])
time.sleep(2.0)
continue
except Exception as error:
dhtDevice.exit()
raise error
time.sleep(2.0)
The ultimate goal is to display my sensor data, with a 2 second refresh (the HZ rate of the Sensor), while the sensor continues to read every 2 seconds.
I'd also like to add that this is my first time using Python, since im, again, not really a developer yet.
Thanks a bunch in advance for every critique and help
most simple way of doing this would be using a button to execute a function and then including your while loop in that function,
Using an button gives you an point where you can start running while instead of directly starting it as soon as you run your program
Sample code should be something like this,
import tkinter as t
def execute():
print('hello')
window = t.Tk()
window.title("system")
window.geometry("550x250")
b1 = t.Button(window, text="Start", width=15, command=execute)
b1.grid(row=1, sticky="W", padx=4)
window.mainloop()
As there will be no user interaction, a button can invoked using button.invoke method such as following,
import tkinter as t
def execute():
print('hello')
window = t.Tk()
window.title("system")
window.geometry("550x250")
b1 = t.Button(window, text="Start", width=0, command=execute)
#b1.grid(row=1, sticky="W", padx=4)
b1.invoke()
window.mainloop()
here removing .grid() will cause the button to disapper but can affect your GUI while updating the label value later , also have a look at this ->
Is there a way to press a button without touching it on tkinter / python?
Python tkinter button.invoke method trouble

Why does my progress bar work with "print" command and not with tkinter?

I would like to understand why this code:
import time
for i in range(1,11):
print(i)
time.sleep(1)
shows (as it should!) numbers from 1 to 10, each every 1 second, while this code:
from tkinter import *
import time
root = Tk()
for i in range(1,11):
Label(root, text = i).grid(row=0, column=i-1, padx=5, pady =5)
time.sleep(1)
root.mainloop()
waits for 10 seconds, and then displays a window with the 10 numbers (instead of adding them one by one).
I am aware this is a silly question, but I really can't understand! Many Thanks! Alessandro
Most GUI's work differently to what you expect.
They work in an asynchronous way, which means, that you setup your windows and start an event loop.
This event loop will display all widgets, labels, etc, that you set up before calling the event loop and wait for any events (GUI events like mouse or keyboard events, timer events and perhaps network events).
When any event is encountered code associated to that event will be called and this code can request to change the GUI (show or hide elements, change labels or attributes of graphical widgets) However the change to the GUI will only be performed when you give control back to the event loop (when the code handling an event finished)
In your given code you change a label in a for loop with sleep statements, but only after the for loop is finished your main loop is being called and this is the moment, where the final state of your GUI will be displayed.
So what you encounter is a know issue for almost all GUI / asynhronous kind of applications.
You have to rewrite your code such, that you start a timer event, and when the timer event fires a function will set a label and increase the counter by 1. And if the counter is not 11 it will restart another timer
This is because the time.sleep function is before the root.mainloop function.
root.mainloop is what causes the window to appear on-screen and start doing things. Instead, I'd recommend using window.after, as that tells the window to run a function after some time when it's on-screen.
Here's an example of a modification you could make (it's not that good but it works):
from tkinter import *
import time
root = Tk()
progress = 0
end = 10
def update_progress():
global progress
progress += 1
Label(root, text = progress).grid(row=0, column=progress-1, padx=5, pady =5)
if progress < end: root.after(1000,update_progress) # Tell the window to call this function in 1000ms (1 second)
root.after(0,update_progress) # Tell the window to run the update_progress function 0ms after now.
root.mainloop()
I'd recommend looking at gelonida's answer for an explanation of why your original code didn't work, and what you need to keep in mind when programming with GUIs in the future.

Reliable button presses captured in polling program

I'm Newbie (sorry... but thanks in advance)
I've spent the weekend reading questions and looking at examples in so many places that I've lost my mind.
I'm building a polling program that reads I/O from various sources.
While I'm polling, I need the user to be able to click on part of the screen to cause an action or go to another GUI page for a while before returning here. If he leaves this page, I can suspend the polling required by this root page.
My issue is that I cannot get reliable mouse clicks or mouse position from the bind. I've depopulated most of the code to get to basics shown below. I'm totally missing how to accomplish this, which should be a typical use case?
from tkinter import *
## Set the key variables
myTanks = [20, 30, 50, 80]
activeTanks = len(myTanks)
## Functions Defined Below
def motion(event):
print("(%s %s)" % (event.x, event.y))
def click(event):
print("clicked")
def readTankLevels(activeTanks):
global myTanks
print (myTanks)
for i in range(0,activeTanks):
print("Reading Tank ", str(i))
## I inserted the following line to emulate the network activity and processing in the real program
root.after(500, print("Waiting "+str(i)))
def drawTank():
print("Drawing the Tanks in GUI")
def updateLabels():
print("Updating the tank labels")
t1Holder=Canvas(root, width=(100), height=50, bg="black", highlightbackground="black")
t1Holder.bind('<Button-1>',motion)
t1Holder.grid(row=0, column=0, padx=5, pady=0)
t2Holder=Canvas(root, width=(50), height=75, bg="blue", highlightbackground="black")
t2Holder.bind('<Motion>',motion)
t2Holder.grid(row=0, column=1, padx=5, pady=0)
## Open GUI
root = Tk()
root.wm_title("Tank Monitor")
root.config(width=300, height=200, padx=0, pady=0, background = "black")
## Body of program
while True:
## Continuous poll of the tanks being monitored
readTankLevels(activeTanks)
drawTank
updateLabels()
root.update()
print ("You should never get here!")
mainloop()
I ended up learning about how 'after' really works via experimentation and wanted to post the answer for others that must have similar issues.
The 'After' command places the function call on the queue of things to do, with a timestamp on when to do it.
While that interim period ensues, the interpreter is basically sitting at the mainloop() command (in my case i do this by design vs having other code running during this period).
In doing it this way, the interpreter can service all mouse movements and button presses very quickly.
I'm now able to have a truly effective GUI. I was not able to find this specific answer anywhere I looked, as most posts just say to use this instead of sleep under Tkinter, but don't provide the details of what it actually does.
Note that it is important to re-establish the 'after' command within the loop that is called to function. In my case I wanted to read sensors every 10 seconds, so the line before mainloop() is the after command, and in the def, i copy the same line of code to recall the 'after' every 10 seconds. (showing as 2 seconds below to make demo quicker). This should help anyone in the boat I was in...
from tkinter import *
root = Tk() #Makes the window
root.wm_title("Remote Sensor Dashboard")
## initialize variables
## def's piled in form many functions below
def readSensors(): ## Get sensor data from web connected devices
print("sensors have been read")
updateGUI()
root.after(2000, readSensors)
def updateGUI(): ## redraw each affected frame or canvase
print("GUI has been updated")
def updateMousePosition(event): ## Print the coordinates of the mouse (can also bind to clicks)
print("(%s %s)" % (event.x, event.y))
#Create window that has your mouse targets (use buttons, color wheels etc)
mouseHouse = Frame(root, width=200, height = 200, bg="black")
mouseHouse.bind('<Motion>',updateMousePosition)
mouseHouse.grid()
## Initial Read of Sensor Network
root.after(2000, readSensors)
## Peform Initial GUI draw with the Sensor Data
print ("GUI has been drawn")
mainloop()

Tkinter's overrideredirect prevents certain events in Mac and Linux

I am writing a program in Python with a Tkinter UI. I want to have a small window with no title bar. This window must receive keyboard input. I am not picky whether this is in the form of an Entry widget or just binding to KeyPress. overrideredirect(True) is typically how the title bar is disabled. Unfortunately, (except in Windows), this seems to prevent many events from being received. I wrote this code to illustrate the problem:
#!/usr/bin/env python
from __future__ import print_function
import Tkinter
class AppWindow(Tkinter.Tk):
def __init__(self, *args, **kwargs):
Tkinter.Tk.__init__(self, *args, **kwargs)
self.overrideredirect(True)
self.geometry("400x25+100+300")
titleBar = Tkinter.Frame(self)
titleBar.pack(expand = 1, fill = Tkinter.BOTH)
closeButton = Tkinter.Label(titleBar, text = "x")
closeButton.pack(side = Tkinter.RIGHT)
closeButton.bind("<Button-1>", lambda event: self.destroy())
self.bind("<KeyPress>", lambda event: print("<KeyPress %s>" % event.char))
self.bind("<Button-1>", lambda event: print("<Button-1>"))
self.bind("<Enter>", lambda event: print("<Enter>"))
self.bind("<Leave>", lambda event: print("<Leave>"))
self.bind("<FocusIn>", lambda event: print("<FocusIn>"))
self.bind("<FocusOut>", lambda event: print("<FocusOut>"))
if __name__ == "__main__":
app = AppWindow()
app.mainloop()
This creates a little window (with no title bar) that prints the name of common events when it receives them. I have run this script on Windows 7, Mac OSX (El Capitan), and Ubuntu 14.04.1. I ran only Ubuntu in a virtual machine (VMWare).
In Windows, this seems to work as expected. All events that my code tests for can be received.
In Ubuntu, the Tkinter window receives <Enter>, <Leave>, and <Button-1> events as expected, but <KeyPress>, <FocusIn>, and <FocusOut> are never received. In fact, even after the window has been clicked on, the last window with focus continues to receive the key presses.
In OSX, the Tkinter window receives <Button-1> events as expected, but <KeyPress>, <FocusIn>, and <FocusOut> are never received. The last window with focus does not continue to receive key presses like in Ubuntu. The <Enter> and <Leave> events behave a little oddly. The <Enter> event is not received until the window is clicked. Then, once the <Leave> event occurs, the window needs to be clicked again to receive another <Enter> event.
I have also tried self.focus_force() just before the end of the __init__ function. This causes the window to receive a <FocusIn> event when the program starts, but no further <KeyPress>, <FocusIn>, or <FocusOut> events are never received.
Ultimately, my question is this: is there any way to hide the title bar but continue to receive keyboard input in OSX and Linux?
I am aware of some other questions dealing with this same problem. In these three questions:
python tkinter overrideredirect; cannot receive keystrokes (Linux)
root.overrideredirect and <Any-KeyPress> binding
How to bind Tkinter destroy() to a key in Debian?
The accepted answer is to use self.attributes('-fullscreen', True), which will not work for me as I want a tiny little window, not a fullscreen application.
There is one other question: Tkinter overrideredirect no longer receiving event bindings. This seems very close to my question, but provided less detail and has no answer.
Update: I have been attempting to investigate the underlying mechanism of my problem. I know that Tkinter is a wrapper around Tcl/Tk, so I thought I would try rewriting my code in Tcl. I don't really know Tcl, but I think I managed to (more or less) translate my Python:
#!/usr/bin/env wish
wm overrideredirect . True
wm geometry . "400x25+100+300"
bind . <KeyPress> {puts "<KeyPress %K>"}
bind . <Button-1> {puts "<Button-1>"}
bind . <Enter> {puts "<Enter>"}
bind . <Leave> {puts "<Leave>"}
bind . <FocusIn> {puts "<FocusIn>"}
bind . <FocusOut> {puts "<FocusOut>"}
I tried the resulting program in Windows and Mac OSX. In Windows I received <KeyPress> events, but in OSX I did not. Without the wm overrideredirect . True line, OSX does receive the <KeyPress> events. Therefore it looks like this problem is not with Python, but with Tcl/Tk.
I have submitted a bug report to Tk for this situation.
You can use the devilspie program to remove the decorations from your window. Use the wm title . myname command to give your window a specific name and use that name in the devilspie configuration fragment below. Remove the overrideredirect command from your program.
I have tested this (as a Tk program), and the undecorated window will still receive the keypress &etc. bindings.
Note that devilspie is written as a daemon process and stays active. The daemon can be killed after it is started and the window changes it made will still be in effect. Or it can be left running, and any time your window is activated, the devilspie configuration will be applied.
(if (is (application_name) "t.tcl")
(begin (undecorate)))

Categories