0

I'm trying to create a custom Tkinter Entry widget that will convert lowercase characters to uppercase before the characters are inserted into the widget. I know there are solutions that bind key release events to a widget with a function to get the widget text and then convert to uppercase and reinsert the text. But those solutions display the lowercase character before the conversion occurs, which I prefer not to do.

I'm trying to use a custom Entry widget based on code shared by Bryan Oakley - Tkinter adding line number to text widget

This code works great for a custom Text widget and I have used it in other applications.

Now I'm trying to use the same approach to create a custom Entry widget. Here is sample code that incorporates both a custom Text widget and custom Entry widget:

from tkinter import *

class CustomText(Text):
    def __init__(self, *args, **kwargs):
        Text.__init__(self, *args, **kwargs)
        self._orig = self._w + "_orig"
        self.tk.call("rename", self._w, self._orig)
        self.tk.createcommand(self._w, self._proxy)

    def _proxy(self, *args):

        if args[0] == "insert" and args[1] == "insert":
           args =('insert', 'insert', args[2].upper())

        cmd = (self._orig,) + args
        result = self.tk.call(cmd)
        return result

class CustomEntry(Entry):
    def __init__(self, *args, **kwargs):
        Entry.__init__(self, *args, **kwargs)

        self._orig = self._w + "_orig"
        self.tk.call("rename", self._w, self._orig)
        self.tk.createcommand(self._w, self._proxy)

    def _proxy(self, *args):

        if args[0] == "insert" and args[1] == "insert":
           args =('insert', 'insert', args[2].upper())

        cmd = (self._orig,) + args
        result = self.tk.call(cmd)
        return result

def get_text():
    print("TEXT widget text = ", mytext.get("1.0", "end"))
    print("ENTRY widget text = " ,entryvar.get())

root = Tk()

entryvar = StringVar()

root.geometry("300x300")
myframe = Frame(root, width = 300, height = 300)
Label(myframe, text="TEXT WIDGET").pack(side = TOP)
myframe.pack(side = TOP)
mytext = CustomText(myframe, width = 30, height=1, wrap="none")
mytext.pack(side = TOP)
Label(myframe, text="ENTRY WIDGET").pack(side = TOP)
myentry = CustomEntry(myframe,  textvariable=entryvar, width = 30)
myentry.pack(side = TOP)

Button(myframe, text="Get Entry Widget Text", command=get_text).pack(side=TOP)

root.mainloop()

The solutions works for the CustomText widget. As you can see the characters are converted before insertion and the result is as if the user is entering the characters with the shift key pressed.

But I get an exception when entering data into the CustomEntry widget I get the following exception form the result = self.tk.call(cmd):

Traceback (most recent call last):
  File "C:/Users/rhkea/AppData/Roaming/JetBrains/PyCharmCE2020.1/scratches/custom_entry_box.py", line 57, in <module>
    root.mainloop()
  File "C:\Users\rhkea\AppData\Local\Programs\Python\Python37\lib\tkinter\__init__.py", line 1283, in mainloop
    self.tk.mainloop(n)
  File "C:/Users/rhkea/AppData/Roaming/JetBrains/PyCharmCE2020.1/scratches/custom_entry_box.py", line 34, in _proxy
    result = self.tk.call(cmd)
_tkinter.TclError: selection isn't in widget .!frame.!customentry

Any ideas on why this might be happening?

j_4321
  • 15,431
  • 3
  • 34
  • 61
rkinca
  • 73
  • 7

1 Answers1

2

After playing around with the code, I have found out that after inserting text in the entry, the method index sel.first is called. But there is no selection so an error is raised "selection isn't in widget .!frame.!customentry". I don't know how this is handled in a regular Entry but you can catch this error:

class CustomEntry(Entry):
    def __init__(self, *args, **kwargs):
        Entry.__init__(self, *args, **kwargs)

        self._orig = self._w + "_orig"
        self.tk.call("rename", self._w, self._orig)
        self.tk.createcommand(self._w, self._proxy)

    def _proxy(self, *args):
        if args[0] == "insert" and args[1] == "insert":
            args = ('insert', 'insert', args[2].upper())
        cmd = (self._orig,) + args

        try:
            return self.tk.call(cmd)
        except TclError as e:
            if args[0] == 'index' and args[1] == 'sel.first':
                pass
            else:
                raise TclError(str(e))

Otherwise, you can use an altogether different method to change the case of the inserted text: use a trace on the textvariable of the entry.

class CustomEntry(Entry):
    def __init__(self, *args, **kwargs):
        Entry.__init__(self, *args, **kwargs)
        self._var = StringVar(self)
        self.configure(textvariable=self._var)
        self._var.trace_add('write', self._uppercase)

    def _uppercase(self, *args):
        self._var.set(self._var.get().upper())

With self._var.trace_add('write', self._uppercase), the method _uppercase() is called each time the content of the StringVar changes.

j_4321
  • 15,431
  • 3
  • 34
  • 61