Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replacing selected entries in a ttk.Entry widget with `validatecommand` option

Tags:

python

tkinter

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
like image 569
Sun Bear Avatar asked Sep 12 '25 13:09

Sun Bear


2 Answers

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()
like image 147
Bryan Oakley Avatar answered Sep 14 '25 02:09

Bryan Oakley


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()
like image 28
Sun Bear Avatar answered Sep 14 '25 04:09

Sun Bear