This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Entry Widget not editable on Windows 10, but is on Linux Ubuntu 16.04
Type: behavior Stage: resolved
Components: Tkinter Versions: Python 3.10, Python 3.9, Python 3.8
process
Status: closed Resolution: third party
Dependencies: Superseder:
Assigned To: Nosy List: christian.heimes, epaine, jmccabe, serhiy.storchaka, terry.reedy
Priority: normal Keywords:

Created on 2021-01-08 12:46 by jmccabe, last changed 2022-04-11 14:59 by admin. This issue is now closed.

Messages (17)
msg384655 - (view) Author: John McCabe (jmccabe) Date: 2021-01-08 12:46
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 #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 #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.

====
#!/usr/bin/python3

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()
msg384657 - (view) Author: Christian Heimes (christian.heimes) * (Python committer) Date: 2021-01-08 12:50
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.
msg384660 - (view) Author: John McCabe (jmccabe) Date: 2021-01-08 13:35
Is behaviour that differs between platforms, using components that are listed in the "classification" -> "Components" section NOT a bug then?
msg384663 - (view) Author: Christian Heimes (christian.heimes) * (Python committer) Date: 2021-01-08 14:04
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.
msg384664 - (view) Author: John McCabe (jmccabe) Date: 2021-01-08 14:09
It's reproducible in both 3.8 and 3.9 on Windows 10.
msg384666 - (view) Author: John McCabe (jmccabe) Date: 2021-01-08 14:13
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.
msg384668 - (view) Author: Christian Heimes (christian.heimes) * (Python committer) Date: 2021-01-08 14:22
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..
msg384672 - (view) Author: John McCabe (jmccabe) Date: 2021-01-08 14:29
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).
msg384678 - (view) Author: E. Paine (epaine) * Date: 2021-01-08 16:06
This is a Tk/Windows issue, not tkinter. I tested the following on Windows 10 using Tk 8.6.9:

# Our entry
pack [entry .e]

# Causes the entry to fail
#tk_messageBox -title Title -message Message
#after 0 tk_messageBox -title Title -message Message

# Does not cause the entry to fail
#after 1 tk_messageBox -title Title -message Message
after idle tk_messageBox -title Title -message Message

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(...)
msg384682 - (view) Author: John McCabe (jmccabe) Date: 2021-01-08 17:41
@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.
msg384702 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2021-01-09 00:44
I added this note to #9673 after rereading the posts and the 2010 tkinter list thread.

"My testing was inadequate.  From #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 #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.
msg384716 - (view) Author: John McCabe (jmccabe) Date: 2021-01-09 09:27
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.
msg384748 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2021-01-10 01:17
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.
msg384778 - (view) Author: John McCabe (jmccabe) Date: 2021-01-10 21:14
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.

-----
#!/usr/bin/python3

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()
msg384779 - (view) Author: John McCabe (jmccabe) Date: 2021-01-10 21:16
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."
msg384785 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2021-01-10 23:20
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.
msg384808 - (view) Author: John McCabe (jmccabe) Date: 2021-01-11 10:27
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.
History
Date User Action Args
2022-04-11 14:59:40adminsetgithub: 87033
2021-01-11 10:27:51jmccabesetmessages: + msg384808
2021-01-10 23:20:31terry.reedysetmessages: + msg384785
2021-01-10 21:16:39jmccabesetmessages: + msg384779
2021-01-10 21:14:18jmccabesetmessages: + msg384778
2021-01-10 01:17:47terry.reedysetmessages: + msg384748
2021-01-09 09:27:41jmccabesetmessages: + msg384716
2021-01-09 00:44:15terry.reedysetstatus: open -> closed
resolution: third party
messages: + msg384702

stage: resolved
2021-01-08 17:41:15jmccabesetmessages: + msg384682
2021-01-08 16:06:50epainesetnosy: + epaine

messages: + msg384678
versions: + Python 3.8, Python 3.10
2021-01-08 14:29:52jmccabesetmessages: + msg384672
2021-01-08 14:22:07christian.heimessetnosy: + terry.reedy, serhiy.storchaka
messages: + msg384668
2021-01-08 14:13:30jmccabesetmessages: + msg384666
2021-01-08 14:09:12jmccabesetmessages: + msg384664
versions: + Python 3.9, - Python 3.6
2021-01-08 14:04:00christian.heimessetmessages: + msg384663
2021-01-08 13:35:18jmccabesetmessages: + msg384660
2021-01-08 12:50:24christian.heimessetnosy: + christian.heimes
messages: + msg384657
2021-01-08 12:46:12jmccabecreate