用`validatecommand`选项替换ttk.Entry小部件中的选定条目

时间:2018-10-01 16:33:01

标签: python tkinter

以下脚本创建一个ttk.Entry小部件,该小部件仅接受可以转换为浮点类型的条目。当我使用鼠标指针选择键入的条目,然后按新的数字条目时,我希望新的数字条目替换所选的条目。目前,该行为不会发生。而是,新的数字条目将出现在所选数字的左侧。如何获得所需的替换行为?

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()

更新:在.bind事件上使用'<ButtonRelease>'方法,我发现ttk.Entry小部件中的选定类型条目可以通过{{ 1}}方法。但是,我还没有弄清楚如何链接这些方法以获得所需的行为。

将这些句子附加到.selection_get()方法的末尾。

__init__()

2 个答案:

答案 0 :(得分:1)

我想分享一下我在下面提出的解决方案。我发现,在ttk.Entry文本字段中选择一个字符串/子字符串时,tkinter将默认执行两步验证。 (1)将选择的字符串/子字符串视为要进行的第一次编辑。 (2)将按键输入作为要进行的第二次编辑。因此,validatecommand将被调用两次。我还在脚本中添加了一些注释。

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()

更新: 下面的脚本显示了一种改进的算法,该算法仅允许在tkinter Entry小部件中使用浮点类型的条目(包括具有指数的条目)。请使用这个。

优势:

  1. 此算法允许将浮点类型数字及其指数输入到tkinter Entry小部件。
  2. 鉴于以下原因,该算法避免了.selection_present()方法 它使用%d,它是 validatecommand%d的值指示与删除(或选择),插入和其他情况相关的场景(即“聚焦于”,“聚焦于”或“文本变量值的变化”。
  3. 与我的第一个算法相比,该算法中考虑的场景具有更大的涵盖性。 (如果您发现任何相关情况遗漏,请提醒我。谢谢。)

改进的算法:

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()

答案 1 :(得分:1)

这是怎么回事:当您选择一定范围的文本然后按一个键时,tkinter必须做两件事:必须删除所选的文本,然后必须插入新的文本。

首先调用处理程序进行删除。因为删除操作导致条目窗口小部件完全为空,并且您不能将空字符串转换为浮点型,所以处理程序将返回False。这样可以防止删除。

接下来,将调用您的处理程序进行插入。旧文本仍然存在。您允许插入,因此最终结果是未删除所选文本,而是在其之前插入了新文本。

最简单的解决方案是允许使用空字符串。然后,您可以简单地验证可以将非空字符串转换为浮点数。

示例:

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()