自动将破折号添加到 tkinter 中的输入框

Auto add dashes to entryboxes in tkinter

当人们输入他们的 phone 号码时,有没有一种方法可以在 phone 号码中自动添加破折号,比如说 phone 号码是 5551111234,但是当他们输入输入框的数字应显示连字符 b/w 数字自动像 555-1111234.

我结合使用了两者 this example of tracing tkinter variables and combined it with this answer,我不能 100% 确定这是否是正确的美国格式,因为我住在英国,我们这里的格式不同,但这是粗略的示例这将如何运作:


# Python program to trace
# variable in tkinter


from tkinter import *
import re

root = Tk()

my_var = StringVar()

# defining the callback function (observer)


def phone_format(phone_number):
    try:
        clean_phone_number = re.sub('[^0-9]+', '', phone_number)
        formatted_phone_number = re.sub(
            r"(\d)(?=(\d{3})+(?!\d))", r"-", "%d" % int(clean_phone_number[:-1])) + clean_phone_number[-1]
        return formatted_phone_number
    except ValueError:
        return phone_number


def my_callback(var, indx, mode):
    my_var.set(phone_format(my_var.get()))
    label.configure(text=my_var.get())


my_var.trace_add('write', my_callback)

label = Label(root)
label.pack(padx=5, pady=5)

Entry(root, textvariable=my_var).pack(padx=5, pady=5)

root.mainloop()

备选

# Python program to trace
# variable in tkinter


from tkinter import *
import phonenumbers
import re

root = Tk()

my_var = StringVar()

# defining the callback function (observer)


# def phone_format(phone_number):
#     try:
#         clean_phone_number = re.sub('[^0-9]+', '', phone_number)
#         formatted_phone_number = re.sub(
#             r"(\d)(?=(\d{3})+(?!\d))", r"-", "%d" % int(clean_phone_number[:-1])) + clean_phone_number[-1]
#         return formatted_phone_number
#     except ValueError:
#         return phone_number

def phone_format(n):                                                                                                                                  
    # return format(int(n[:-1]), ",").replace(",", "-") + n[-1]   
    # return phonenumbers.format_number(n, phonenumbers.PhoneNumberFormat.NATIONAL)
    formatter = phonenumbers.AsYouTypeFormatter("US")
    for digit in re.findall(r'\d', n)[:-1]:
        formatter.input_digit(digit)
    return formatter.input_digit(re.findall(r'\d', n)[-1])



def my_callback(var, indx, mode):
    print(my_var.get())
    my_var.set(phone_format(my_var.get()))
    label.configure(text=my_var.get())

def callback(event):
    entry.icursor(END)


my_var.trace_add('write', my_callback)

label = Label(root)
label.pack(padx=5, pady=5)

entry = Entry(root, textvariable=my_var)
entry.bind("<Key>", callback)
entry.pack(padx=5, pady=5)


root.mainloop()

这是我的解决方案,使用 PyPi 中的 phonenumbers,似乎可以正常工作。

这是一个复杂的示例,但它处理的不仅仅是 phone 个数字。被评论死了

#widgets.py

import tkinter as tk, re
from dataclasses import dataclass, field
from typing import List, Pattern, Iterable
from copy import deepcopy

Char: Pattern = re.compile('[a-z0-9]', re.I)


''' FormEntryFormat_dc
    this serves as a configuration for the behavior of FormEntry
'''
@dataclass
class FormEntryFormat_dc:
    valid      :Pattern        = None                        #pattern to validate text by
    separator  :str            = None                        #the separator to use
    marks      :List           = field(default_factory=list) #list of positions to apply separator
    strict     :bool           = False                       #True|False strict typing
        
    def config(self, ascopy:bool=True, **data):
        c = deepcopy(self) if ascopy else self
        for key in c.__dict__:
            if key in data:
                c.__dict__[key] = data[key]                  #assign new value
        return c
    
    
#prepare a few formats        
TimeFormat   = FormEntryFormat_dc(re.compile('^(\d{1,2}(:(\d{1,2}(:(\d{1,2})?)?)?)?)?$'      ), ':' , [2, 5])
DateFormat   = FormEntryFormat_dc(re.compile('^(\d{1,2}(\\(\d{1,2}(\\(\d{1,4})?)?)?)?)?$'), '\', [2, 5])
PhoneFormat  = FormEntryFormat_dc(re.compile('^(\d{1,3}(-(\d{1,3}(-(\d{1,4})?)?)?)?)?$'      ), '-' , [3, 7], True)   
PhoneFormat2 = FormEntryFormat_dc(re.compile('^(\d{1,3}(-(\d{1,7})?)?)?$'                    ), '-' , [3]   , True)   


''' FormEntry
    an entry with format behavior
'''
class FormEntry(tk.Entry):
    @property
    def input(self) -> str:
        return self.get()
        
    def offset(self, separator:str, marks:Iterable):
        sep_marks = [] #cache for positions of already inserted separators
        offset    = 0  #the overall offset between inserted and expected separator marks
        
        #get a mark for every current separator
        for i, c in enumerate(self.input):
            if c == separator:
                sep_marks.append(i)
        
        #if any sep_marks ~ subtract the value of sep_marks last index 
        #~from the value of the corresponding index in marks
        n = len(sep_marks)
        if n:       
            offset = max(0, marks[n-1]-sep_marks[-1])
            
        return offset
    
    def __init__(self, master, frmt:FormEntryFormat_dc, **kwargs):
        tk.Entry.__init__(self, master, **kwargs)
        
        self.valid = frmt.valid
        if self.valid:
            #register validatecommand and assign to options
            vcmd = self.register(self.validate)
            self.configure(validate="all", validatecommand=(vcmd, '%P'))
            
        if frmt.marks and frmt.separator:
            #bind every key to formatting
            self.bind('<Key>', lambda e: self.format(e, frmt.separator, frmt.marks, frmt.strict))
        
    def validate(self, text:str):      
        return not (self.valid.match(text) is None) #validate with regex

    def format(self, event, separator:str, marks:Iterable, strict:bool):
        if event.keysym != 'BackSpace':             #allow backspace to function normally
            i = self.index('insert')                #get current index
            if Char.match(event.char) is None and (i in marks or not strict):
                event.char = separator              #overwrite with proper separator
            else:
                #automatically add separator
                if i+self.offset(separator, marks) in marks:
                    event.char = f'{separator}{event.char}'
                    
            self.insert(i, event.char)              #validation will check if this is allowed
            
            return 'break'

#main.py (OOP style)

import widgets as ctk #custom tk
import tkinter as tk

class Main(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        
        self.title("Formatted Entry")

        self.grid_columnconfigure(2, weight=1)

        #create labels
        self.labels = ['time', 'date', 'phone', 'phone2']
        for n, label in enumerate(self.labels):
            tk.Label(self, text=f'{label}: ', width=14, font='consolas 12 bold', anchor='w').grid(row=n, column=0, sticky='w')

        #create entries
        self.entries = []
        for n, format in enumerate([ctk.TimeFormat, ctk.DateFormat, ctk.PhoneFormat, ctk.PhoneFormat2]):
            self.entries.append(ctk.FormEntry(self, format, width=14, font='consolas 12 bold'))
            self.entries[-1].grid(row=n, column=1, sticky='w')
        
        #form submit button        
        tk.Button(self, text='submit', command=self.submit).grid(column=1, sticky='e')
            
    def submit(self):
        for l, e in zip(self.labels, self.entries):
            print(f'{l}: {e.input}')
        


Main().mainloop() if __name__ == "__main__" else None

#main.py (procedural style)

import widgets as ctk #custom tk
import tkinter as tk

if __name__ == "__main__":
    root = tk.Tk()
    root.title("Formatted Entry")
    root.grid_columnconfigure(2, weight=1)

    #create labels
    labels = ['time', 'date', 'phone', 'phone2']
    for n, label in enumerate(labels):
        tk.Label(root, text=f'{label}: ', width=14, font='consolas 12 bold', anchor='w').grid(row=n, column=0, sticky='w')

    #create entries
    entries = []
    for n, format in enumerate([ctk.TimeFormat, ctk.DateFormat, ctk.PhoneFormat, ctk.PhoneFormat2]):
        entries.append(ctk.FormEntry(root, format, width=14, font='consolas 12 bold'))
        entries[-1].grid(row=n, column=1, sticky='w')
     
    def submit():
        for l, e in zip(labels, entries):
            print(f'{l}: {e.input}')
            
    #form submit button        
    tk.Button(root, text='submit', command=submit).grid(column=1, sticky='e')
    
    root.mainloop()

这是一个程序示例。这个例子有很多评论。

import tkinter as tk, re
from dataclasses import dataclass, field
from typing import List, Pattern, Iterable
from copy import deepcopy

Char: Pattern = re.compile('[a-z0-9]', re.I)


''' FormField_dc
    this serves as a configuration for the behavior of form_field
'''
@dataclass
class FormEntryFormat_dc:
    valid      :Pattern        = None                        #pattern to validate text by
    separator  :str            = None                        #the separator to use
    marks      :List           = field(default_factory=list) #list of positions to apply separator
    strict     :bool           = False                       #True|False strict typing
        
    def config(self, ascopy:bool=True, **data):
        c = deepcopy(self) if ascopy else self
        for key in c.__dict__:
            if key in data:
                c.__dict__[key] = data[key]                  #assign new value
        return c
    
    
#prepare a few formats        
TimeFormat   = FormEntryFormat_dc(re.compile('^(\d{1,2}(:(\d{1,2}(:(\d{1,2})?)?)?)?)?$'      ), ':' , [2, 5])
DateFormat   = FormEntryFormat_dc(re.compile('^(\d{1,2}(\\(\d{1,2}(\\(\d{1,4})?)?)?)?)?$'), '\', [2, 5])
PhoneFormat  = FormEntryFormat_dc(re.compile('^(\d{1,3}(-(\d{1,3}(-(\d{1,4})?)?)?)?)?$'      ), '-' , [3, 7], True)   
PhoneFormat2 = FormEntryFormat_dc(re.compile('^(\d{1,3}(-(\d{1,7})?)?)?$'                    ), '-' , [3]   , True)   


''' FormField
    An entry field intended to force a specific format while the user types
'''
def form_field(master, f:FormEntryFormat_dc, **kwargs) -> tk.Entry:
    entry = tk.Entry(master, **kwargs)
    
    def offset(separator:str, marks:Iterable):
        sep_marks = [] #cache for positions of already inserted separators
        offset    = 0  #the overall offset between inserted and expected separator marks
        
        #get a mark for every current separator
        for i, c in enumerate(entry.get()):
            if c == separator:
                sep_marks.append(i)
        
        #if any sep_marks ~ subtract the value of sep_marks last index 
        #~from the value of the corresponding index in marks
        n = len(sep_marks)
        if n:       
            offset = max(0, marks[n-1]-sep_marks[-1])
            
        return offset
        
    #test text against validity conditions
    def validate(text):
        #if numeric check is True and len(text) > 0 
        return not (f.valid.match(text) is None) #validate with regex
        
    if f.valid:
        #register validatecommand and assign to options
        vcmd = entry.register(validate)
        entry.configure(validate="all", validatecommand=(vcmd, '%P'))
            
    #add separators when entry "insert" index equals a mark  
    #~and separator isn't already present
    def format(event, separator:str, marks:Iterable, strict:bool):
        #allow backspace to function normally
        if event.keysym != 'BackSpace':
            i = entry.index('insert')                #get current index
            if Char.match(event.char) is None and (i in marks or not strict):
                event.char = separator              #overwrite with proper separator
            else:
                #automatically add separator
                if i+offset(separator, marks) in marks:
                    event.char = f'{separator}{event.char}'
                    
            entry.insert(i, event.char)              #validation will check if this is allowed
            
            return 'break'
                
    
    if f.marks and f.separator:           
        #bind every keypress to formatting
        entry.bind('<Key>', lambda e: format(e, f.separator, f.marks, f.strict))
    
    
    return entry
    

##USAGE EXAMPLE
if __name__ == "__main__":
    root = tk.Tk()
    root.title("Formatted Entry")
    root.grid_columnconfigure(2, weight=1)

    #create labels
    labels = ['time', 'date', 'phone', 'phone2']
    for n, label in enumerate(labels):
        tk.Label(root, text=f'{label}: ', width=14, font='consolas 12 bold', anchor='w').grid(row=n, column=0, sticky='w')

    #create entries
    entries = []
    for n, format in enumerate([TimeFormat, DateFormat, PhoneFormat, PhoneFormat2]):
        entries.append(form_field(root, format, width=14, font='consolas 12 bold'))
        entries[-1].grid(row=n, column=1, sticky='w')
     
    def submit():
        for l, e in zip(labels, entries):
            print(f'{l}: {e.get()}')
            
    #form submit button        
    tk.Button(root, text='submit', command=submit).grid(column=1, sticky='e')
    
    root.mainloop()