New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Entry Widget not editable on Windows 10, but is on Linux Ubuntu 16.04 #87033
Comments
I've built an application using tkinter (see below). I'm fairly new to tkinter, despite having substantial software experience (33 years), so the layout might be a bit odd/wrong, and the example obviously doesn't follow PEP-8 guidelines, however... Basically this application (which is a bit more than minimal, but should be easy to follow) is designed to manage pairs of text values as JSON, saving/loading from a file. When it starts, the user is asked if they want to read in an existing file of data; if they select NO, a fairly empty frame with 3 buttons showing "+", "Save" and "Quit" is displayed. At this point, if "+" is pressed, a new row with two labels and two Entry widgets is added to the frame. However (and this is the problem which appears to be identical to that reported in Issue bpo-9673), it is not possible to set focus into either of the Entry widgets on Windows 10; there is no problem doing this on Ubuntu 16.04 (although I've only got Python 3.5 on there) If the "Save" button is then pressed, a message box pops up telling the use that no changes have been saved. Once OK has been pressed on that, it becomes possible to set focus into the Entry widgets. One of the problems with Issue bpo-9673 was that no 'minimal' example was provided showing this behaviour, and the very minimal example given in https://bugs.python.org/issue9673#msg218765 doesn't exhibit the problem, hence the example below being a bit more than minimal, while still not being particularly complicated. ==== import os
import tkinter as tk
from tkinter import filedialog, messagebox
import json
class Application(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.master = master
self.grid()
self.originalJson = {}
self.inFileName = ""
self.leftRightEntries = []
self.fileDlgOpts = { "initialdir" : os.getcwd(),
"initialfile" : "file.json",
"filetypes" : (("JSON File", "*.json"), ("All Files","*.*")),
"defaultextension" : '.json',
"title" : "Select A File" }
self.createWidgets()
def openInFile(self):
fileName = ""
reuse = tk.messagebox.askquestion("Use An Existing File", "Do you want to load and use an existing file?")
if reuse == "yes":
fileName = tk.filedialog.askopenfilename(**self.fileDlgOpts)
if fileName is not "":
try:
with open(fileName, 'r') as json_file:
self.originalJson = json.load(json_file)
json_file.close()
except Exception:
tk.messagebox.showerror("Use An Existing File", "File could not be loaded; continuing without one.")
fileName = ""
else:
tk.messagebox.showwarning("Use An Existing File", "No existing file specified; continuing without one.")
return fileName
def createWidgets(self):
self.inFileName = self.openInFile()
# We add the buttons to some huge numbered row because we might want to insert more
# rows, and the layout manager will collapse everything in between. Also we
# add these first because of the way the tab order is set up
self.addBtn = tk.Button(self.master, text = "+", command = self.addNew)
self.addBtn.grid(row = 100, column = 0, sticky = tk.W)
# Save button; pretty self-explanatory
self.saveBtn = tk.Button(self.master, text = "Save", command = self.save)
self.saveBtn.grid(row = 100, column = 2, sticky = tk.W)
# Quit button; pretty self-explanatory
self.quitBtn = tk.Button(self.master, text = "QUIT", fg = "red", command = self.quit)
self.quitBtn.grid(row = 100, column = 3, sticky = tk.E)
# If there is original json, work through each key and put the fields on the display
rowNum = 0
for leftText in sorted(self.originalJson.keys()):
self.insertRow(rowNum, leftText);
rowNum = rowNum + 1
self.nextEmptyRow = rowNum
self.redoPadding()
def redoPadding(self):
for child in self.master.winfo_children():
child.grid_configure(padx = 5, pady = 5)
def focusNextWidget(self, event):
event.widget.tk_focusNext().focus()
return("break")
def insertRow(self, rowNum, initialLeft = None):
tk.Label(self.master, height = 1, text = "Left: ").grid(row = rowNum, column = 0, sticky = tk.W)
leftBox = tk.Entry(self.master, width = 20)
leftBox.grid(row = rowNum, column = 1, sticky = tk.W)
leftBox.bind("<Tab>", self.focusNextWidget)
if initialLeft is not None:
leftBox.insert(tk.END, initialLeft)
tk.Label(self.master, height = 1, text = "Right: ").grid(row = rowNum, column = 2, sticky = tk.W)
rightBox = tk.Entry(self.master, width = 20)
rightBox.grid(row = rowNum, column = 3, sticky = tk.W)
rightBox.bind("<Tab>", self.focusNextWidget)
if initialLeft is not None:
rightBox.insert(tk.END, initialLeft)
self.leftRightEntries.append((leftBox, rightBox))
leftBox.focus_set()
def addNew(self):
# Add a new row before the button
self.insertRow(self.nextEmptyRow)
self.nextEmptyRow = self.nextEmptyRow + 1
self.redoPadding()
def getCurrent(self):
# Work through the rows and check stuff
current = {}
for (leftEntry, rightEntry) in self.leftRightEntries:
leftText = leftEntry.get()
rightText = rightEntry.get()
if leftText == "" and rightText == "":
pass
elif leftText == "":
print("No leftText specified for rightText [{}]".format(rightText))
elif rightText == "":
print("No rightText specified for leftText [{}]".format(leftText))
else:
print("lefText: {}, rightText: {}".format(leftText, rightText))
current[leftText] = rightText
return current
def save(self):
# Get the current values, and then dump the new json to a file, if it's changed!
finalResult = self.getCurrent()
if finalResult != self.originalJson:
if self.inFileName == "":
self.inFileName = tk.filedialog.asksaveasfilename(**self.fileDlgOpts)
if self.inFileName != "":
with open(self.inFileName, 'w') as json_file:
json.dump(finalResult, json_file, indent = 4)
self.originalJson = finalResult
tk.messagebox.showinfo("Save Data", "Data saved to {}".format(self.inFileName))
else:
tk.messagebox.showwarning("Save Data", "Data has not been saved; no file name was supplied!")
else:
tk.messagebox.showwarning("Save Data", "Data has not been saved; there are no changes")
def quit(self):
# Deal with quitting when the file's been modified, check original vs current JSON
reallyQuit = True
finalResult = self.getCurrent()
if finalResult != self.originalJson:
answer = tk.messagebox.askquestion("Quit", "Data has changed; do you really want to quit?", icon = "warning")
if answer != "yes":
reallyQuit = False
if reallyQuit:
self.master.destroy()
if __name__ == "__main__":
root = tk.Tk()
root.title("Inactive Entry Example")
app = Application(root)
root.protocol("WM_DELETE_WINDOW", app.quit)
app.mainloop() |
Hi, bugs.python.org is an issue tracker for bugs and feature requests. Please use platforms like Python user mailing list, stack overflow, or reddit for general help with Python and libraries. |
Is behaviour that differs between platforms, using components that are listed in the "classification" -> "Components" section NOT a bug then? |
Can your produce the issue with Python 3.8 or newer on any platform? 3.6 and 3.7 are in security fix-only mode. If it's truly a bug in Python, then we won't fix the issue any way. |
It's reproducible in both 3.8 and 3.9 on Windows 10. |
In addition, changing "Entry" to "Text" (+ necessary associated changes) makes no difference to the outcome. Removing the call to tk.messagebox.askquestion() does. It appears that tk.messagebox.askquestion() is screwing something up on Windows; perhaps it's locking some resources, or not correctly releasing them. |
I'm involving TJ and Serhiy. They might have free resources and might be able to assist. I initially suggested to get assistance in user forums, because we have very limited resources. To give you an impression, there are more than 7,500 open bugs on BPO and more than 1,400 open PRs on Github.. |
Thank you. Wrt to your initial suggestion, I recognise Python's popularity will make things busy, and I will ask if anyone knows of a workaround in other fora, but a bug's a bug and, IMO, if something behaves differently on different platforms, using packages that are part of the Python install, for no apparent reason, then that's a bug! (Especially as the same thing was reported over 10 years ago, on what would be a very different version of Python). |
This is a Tk/Windows issue, not tkinter. I tested the following on Windows 10 using Tk 8.6.9: # Our entry # Causes the entry to fail # Does not cause the entry to fail I have not tried on a later version of Tk so it may be fixed but it also may be a fundamental Windows issue. The workaround would be to either use .after(1, ...) or .after_idle(...) |
@epaine Thank you for your comments. Although the order of events in the example you quoted isn't the same as in the application I'm using (tk.messagebox.askquestion() is called a long time before the Enter widget is created in the application, not the other way round), what you've suggested, and a bit of extra thought, has led to a solution for me. I wrapped the self.createWidgets call in self.after_idle() (so self.after_idle(self.createWidgets)) and that appears to do the job. Many thanks; it's appreciated. |
I added this note to bpo-9673 after rereading the posts and the 2010 tkinter list thread. "My testing was inadequate. From bpo-42867, it appears that 0. the bug is limited to Windows; 1. opening a canned dialog or message box is part of getting the buggy behavior; 2. timing is involved; 3. the bug can be exhibited on Windows directly with wish/tk, so that it is a 3rd party tcl/tk issue and not a tkinter issue. Hence changing the issue resolution. A workaround mentioned in both the referenced tkinter thread and bpo-42867 is to call after_idle() at some point." Paine, thank you for verifying both the bug and workaround directly with tk. I am closing this also as 3rd party. |
Thank you all for your time. I hope you don't feel it has been wasted since, at the very least, it confirms an issue in tkinter usage, albeit that the actual cause of the issue is TK itself. |
No problem. While your failing example was 'too long', you *did* search and find the appropriate previous issue, with my inadequate response and incorrect resolution, even though closed. I appreciate having my understanding fixed. IDLE uses several entry boxes in dialogs. I have partially replaced error message boxes with error messages in the dialog. For example, Open Module (Alt-M at least on Windows) opens a dialog to open a module. Enter a bad name and "Error: module not found" is printed in red below, leaving the cursor in the entry box so the user can correct it (or Cancel). As is turn out, this avoids any possibility of running into this bug. I might have run into it before I replaced most old dialog + error box uses, but I now know to watch out in those remaining. |
Fair point about being "too" long. Having seen a "short" example that, unfortunately, didn't actually exhibit the problem, I thought that providing a "smallish" example that definitely did exhibit the issue was quicker than, potentially, spending more time than necessary cutting it down. However, for the record, hope the below example is better. As before, changing: self.createWidgets() to: self.after_idle(self.createWidgets()) avoids the issue. ----- import tkinter as tk
from tkinter import messagebox
class Application(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.master = master
self.pack()
self.createWidgets()
def createWidgets(self):
tk.messagebox.askquestion("Use An Existing File", "Do you want to load and use an existing file?")
tk.Entry(self.master, width = 20).pack()
if __name__ == "__main__":
root = tk.Tk()
app = Application(root)
app.mainloop() |
Apologies, I couldn't find an edit button! That last comment should've said: "As before, changing: self.createWidgets() to: self.after_idle(self.createWidgets) avoids the issue." |
With a truly minimal but reproducible example, I was able to understand and experiment. As is, widget creation is done without the event loop running. Displaying the message box shifts grabs the internal focus but also displays the unfocused tk window. When the message box is dismissed and mainloop() is called, the root window is displayed apparently with focus but it is unresponsive to mouse click on the entry box, hence the entry box never gets focus and never gets keypresses. This is a bug somewhere between tk and Windows window manager and focus management. Adding after_idle delays the message box and entry creation until after the mainloop call, and clicking on the entry now works. In this case, the (unfocused) root window is *not* displayed. If the entry box is the proper default focused widget when the message box is dismissed, an even better fix, to me, is to bind the entry box and explicitly give it the focus. According to the tk docs, focus_set should work. But it does not on Windows (and I suspect that this is related to clicking not working). focus_force does work. Another solution is to add master.update() before createWidgets. Clicking on then works. Putting self.master.update() after the messagebox call does not work. |
Thank you for that information and analysis Terry. As you can see, at the end of the addNew() function in the original example, I'd added: --- if initialLeft is not None:
rightBox.insert(tk.END, initialLeft)
self.leftRightEntries.append((leftBox, rightBox))
leftBox.focus_set() and, as you've noticed, that makes no difference on Windows. I hadn't tried focus_force(), but will try to bear that in mind. |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: