4

The following script creates a ttk.Entry widget that only accepts entries that can be converted to a float type. When I use the mouse pointer to select the typed entries, followed by pressing new numeric entries, I would like the new numeric entries to replace the selected entries. Presently that behavior does not occur. Instead, the new numeric entries will appear on the left of the selected numbers. How do I get the replacement behavior I require?

import tkinter as tk  # python 3.x
import tkinter.ttk as ttk  # python 3.x

class Example(ttk.Frame):

    def __init__(self, parent):
        super().__init__(parent)

        # %P = value of the entry if the edit is allowed
        # %S = the text string being inserted or deleted, if any    
        vcmd = (self.register(self.onValidate),'%P', '%S')
        self.entry = ttk.Entry(self, validate="key", validatecommand=vcmd)
        self.entry.pack(side="top", fill="x")

    def onValidate(self, P, S):
        # Disallow anything but '0123456789.+-'
        if S in '0123456789.+-':
            try:
                float(P)
                print('float(P) True')
                return True
            except ValueError:
                print('float(P) False')
                return False
        else:
            print('S in 0123456789.+- False')
            return False

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()

Update: Using the .bind method on '<ButtonRelease>' event, I discovered that the selected typed entries in the ttk.Entry widget can be obtained with the .selection_get() method. However, I have yet to figure out how to link these approaches to get the desired behavior.

Append these sentences to the end of the __init__() method.

        self.entry.bind( '<ButtonRelease>', self.selecttext )

    def selecttext(self, event):
        try:
            print( 'selection = ', self.entry.selection_get() )
        except tk.TclError:
            pass
Sun Bear
  • 7,594
  • 11
  • 56
  • 102
  • Are you wanting to just select the field and when you start typing to delete what was there? – Mike - SMT Oct 01 '18 at 21:08
  • Hum... There is a way to tell if text is selected in a text box but not in an entry field. Sadly you cannot valid in a text box like you can in an entry field. This is an interesting problem. – Mike - SMT Oct 01 '18 at 21:38
  • @Mike-SMT I discovered that a binding can be used to get the selected text. See update given in my question. But I have not found a way to use it to get the desire behaviour I need. What do you think? – Sun Bear Oct 02 '18 at 02:23

2 Answers2

2

I would like to share the solution I have arrived at below. I discovered that when a string/substring in the ttk.Entry textfield is selected, tkinter will default to performing the validation in 2 steps. (1) Treat the selected string/substring as the first edit to be done. (2) Treat the keypressed entry as the second edit that is to be done. So, validatecommand will be called twice. I have also added some comments in the script.

import tkinter as tk  # python 3.x
import tkinter.ttk as ttk  # python 3.x

class Example(ttk.Frame):

    def __init__(self, parent):
        super().__init__(parent)

        # %P = value of the entry if the edit is allowed
        # %S = the text string being inserted or deleted, if any
        # %s = value of entry prior to editing
        vcmd = (self.register(self.onValidate),'%P', '%S', '%s')
        self.text = tk.StringVar()
        self.entry = ttk.Entry(self, validate="key", validatecommand=vcmd)
        self.entry.pack(side="top", fill="x")

    def onValidate(self, P, S, s):
        # Disallow anything but '0123456789.+-'
        selected = None
        print('\nP={}, S={}, s={}'.format(P, S, s) )

        try:

            if S in '0123456789.+-' or float(S):
                if self.entry.selection_present():
                    print('With Selection')
                    selected = self.entry.selection_get()
                    print('selected = ', selected )
                    # Notes:
                    # - When .selection_present()=True, I discovered that 
                    #   tkinter will return by default:
                    #    P = s w/o 'selected'
                    #    S = 'selected' and not the keypressed
                    #    s = value of entry prior to editing.
                    # - I should "return True" so that tkinter will trigger method
                    #   self.onValidate() again. This time,
                    #    P = value of the entry if the keypress edit is allowed.
                    #    S = the key pressed
                    #    s = P from previous attempt.
                    #   As such satisfy .selection_present()=False.
                    return True
                else:
                    print('No Selection')
                    try:
                        float(P); print('True')
                        return True
                    except ValueError:
                        print(ValueError, 'float({}) False'.format(P))
                        return False
            else:
                print('S in 0123456789.+- False')
                return False

        except ValueError:
            print('Try with Exception')
            print(ValueError, 'float({}) False'.format(P))
            return False


if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()

Update: The script below shows an improved algorithm to ONLY allow float type entries (including those with exponent) in a tkinter Entry widget. Please use this.

Advantages:

  1. This algorithm allows float type numbers and its exponents to be entered to a tkinter Entry widget.
  2. This algorithm avoids the .selection_present() method given that it uses %d which is an inherent callback substitution code of validatecommand. %d has values to indicate scenarios associated to deletion(or selection), insertion, and others (i.e. "focus in", "focus out", or "changes in textvariable values".
  3. The scenarios considered in this algorithm are more encompassing than my first algorithm. (Do alert me if you notice any relevant scenario being left out. Thank you.).

Improved Algorithm:

import tkinter as tk  # python 3.x
import tkinter.ttk as ttk  # python 3.x

class Example(ttk.Frame):

    def __init__(self, parent):
        super().__init__(parent)

        # %d = Type of action (1=insert, 0=delete, -1 for others)
        # %P = value of the entry if the edit is allowed
        # %S = the text string being inserted or deleted, if any
        vcmd = (self.register(self.onValidate_Float), '%d','%P','%S')
        self.entry = ttk.Entry(self, validate="key", validatecommand=vcmd)
        self.entry.pack(side="top", fill="x")

    def onValidate_Float(self, d, P, S):
        '''Allow only float type insertions (including exponents).

        Notes: 1. The culminated insertions can fail to convert to a float.
                  This scenario occurs ONLY when the exponent entry is not 
                  completed, i.e. when 'e-' and 'e+' are supplied only.
               2. User of this method should remember that when they bind a
                  handle and '<KeyPress-Return>' to the tkinter Entry widget,
                  the handle can encounter error associated with processing
                  "float(numeric)" when numeric=1.4e- or 1.4e+ (as example).
               3. I discovered that validatecommand uses %d to determine
                  the type of actions it take. As such, it is useful to
                  structure validatecommand instructions according to scenarios
                  d='0', d='1' and d='-1'. 
        '''

        def _checkDecimal(P):
            '''Return True when decimal does not occur in exponent.'''
            decimal_index = P.find('.')
            exponent_index = P.find('e')
            if exponent_index > decimal_index:
                return True
            else:
                return False

        print('\nd={}, P={}, S={}'.format(d, P, S) )

        if d == '0': #Delete Selection or keypress "Delete"/"Backspace"
            print('Allow delete action regardless of keypress.')
            return True

        elif d == '1': #Insert keypress
            print('d==1, Insert keypress.')
            try:
                if S in '0123456789e.+-':
                    float(P); print('True')
                    return True
                else:
                    print('False')
                    return False
            except ValueError:
                print('float({}) ValueError.'.format(P))
                if P.count('e')>1: return False
                if P.count('e.')>0: return False
                if P.count('-e')>0: return False
                if P.count('+e')>0: return False
                if P.find('e') == 0: return False
                if P.count('.')>1: return False

                if P[0]=="-":
                    print('P[0]=="-"')
                    if P.count("e-")>=1:
                        print('P.count("e-")>=1')
                        if P.count("-")>2: return False
                        if P.count("+")>0: return False
                        if not _checkDecimal(P): return False
                    elif P.count("e+")>=1:
                        print('P.count("e+")>=1')
                        if P.count("+")>1: return False
                        if P.count("-")>1: return False
                        if not _checkDecimal(P): return False
                    else:
                        print('no e- or e+')
                        if P.find('.') == 1: return False #disallow '-.'
                        if P.find('+') >= 1: return False #disallow '-+'
                        if P.find('-') >= 1: return False #disallow '--'
                        if P.count("-")>1: return False
                        if P.count("+")>1: return False

                elif P[0]=="+":
                    print('P[0]=="+"')
                    if P.count("e-")>=1:
                        print('P.count("e-")>=1')
                        if P.count("-")>1: return False
                        if P.count("+")>1: return False
                        if not _checkDecimal(P): return False
                    elif P.count("e+")>=1:
                        print('P.count("e+")>=1')
                        if P.count("+")>2: return False
                        if P.count("-")>0: return False
                        if not _checkDecimal(P): return False
                    else:
                        print('no e- or e+')
                        if P.find('.') == 1: return False #disallow '+.'
                        if P.find('+') >= 1: return False #disallow '++'
                        if P.find('-') >= 1: return False #disallow '+-'
                        if P.count("-")>1: return False
                        if P.count("+")>1: return False

                else:
                    print('P[0] is a number') 
                    if P.count("e-")>=1:
                        print('P.count("e-")>=1')
                        if P.count("-")>1: return False
                        if P.count("+")>0 : return False
                        if not _checkDecimal(P): return False
                    elif P.count("e+")>=1:
                        print('P.count("e+")>=1')
                        if P.count("+")>1: return False
                        if P.count("-")>0: return False
                        if not _checkDecimal(P): return False
                    else:
                        print('no e- or e+')
                        if P.count("-")>0: return False
                        if P.count("+")>0: return False

                return True #True for all other insertion exceptions.

        elif d == '-1': #During focus in, focus out, or textvariable changes
            print('d==-1, During focus in, focus out, or textvariable changes')
            return True


if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()
Sun Bear
  • 7,594
  • 11
  • 56
  • 102
  • Nice work. I found the same selection_get() method for text but read it would not work with entry. Glad it worked for you. Bryan Oakley made a good point about selection_get() in the comments of [this post here](https://stackoverflow.com/a/4073612/7475225). – Mike - SMT Oct 02 '18 at 11:44
  • @Mike-SMT Thanks. The `.selection_get()` method isn't necessary. I had used it to visualise what was happening. I just tried `exportselection=0`, found that Bryan Oakley wrote also applied to Entry widget. Fortunately, it did not affect the `.selection_present()` method. :) I am still testing out the script to see if it is general enough. – Sun Bear Oct 02 '18 at 11:57
  • @Mike-SMT I managed to work out a more encompassing algorithm that allows float type entries with exponents. I have also added comments to help user gain understanding. Found a way to just use `validatecommand` callback substitution code `'%d'` instead of `.selection_get()` method. – Sun Bear Oct 04 '18 at 04:37
2

What is happening is this: when you select a range of text and then press a key, tkinter must do two things: it must delete the selected text and then it must insert the new text.

The handler is first called for the delete. Because the delete causes the entry widget to be completely empty, and you can't convert an empty string to a float, your handler returns False. This prevents the delete from happening.

Next, your handler is called for the insert. The old text is still there. You allow the insert, so you end up with the result that the selected text is not removed and the new text is inserted right before it.

The simplest solution is to allow for an empty string. You can then simply validate that a non-empty string can be converted to a float.

Example:

import tkinter as tk  # python 3.x
import tkinter.ttk as ttk  # python 3.x

class Example(ttk.Frame):

    def __init__(self, parent):
        super().__init__(parent)

        # %P = value of the entry if the edit is allowed
        vcmd = (self.register(self.onValidate),'%P')
        self.entry = ttk.Entry(self, validate="key", validatecommand=vcmd)
        self.entry.pack(side="top", fill="x")

    def onValidate(self, P):
        if P.strip() == "":
            # allow blank string
            return True
        try:
            float(P)
            return True
        except:
            return False

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Thank you for your explanations. I have improved my answer to allow float type entry with exponent to be entered into the tkinter `Entry` widget, and better structured my algorithm. – Sun Bear Oct 04 '18 at 04:49