0

Is there a way to auto add dashes in a phone number when the person types their phone number like for say phone number is 5551111234, but when they type it in the entry box the number should appear with hyphen b/w the number automatically like 555-1111234.

Delrius Euphoria
  • 14,910
  • 3
  • 15
  • 46

3 Answers3

1

I used a combination of both this example of tracing tkinter variables and combined it with this answer, I am not 100% sure if this is the correct American formatting because I live in the UK and we format things differently here, but this is the rough example of how that would work:


# 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"\1-", "%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()

Alternative

# 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"\1-", "%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()

This was my solution, using phonenumbers from PyPi, which seemed to make it work.

10 Rep
  • 2,217
  • 7
  • 19
  • 33
jimbob88
  • 697
  • 5
  • 20
1

This is a complex example, but it handles more than just phone numbers. It is commented to death.

#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()
OneMadGypsy
  • 4,640
  • 3
  • 10
  • 26
  • Thanks for this amazing answer and your time on this, but is there any way without classes? As my GUI has no classes and implementing classes just for the purpose of this is a bit overkill right? – Delrius Euphoria Aug 27 '20 at 21:49
  • @CoolCloud ~ every single widget you use in `tkinter` is a class, including `Tk` and `Toplevel`. If implementing a class was overkill for your project, then you couldn't use `tkinter`, at all. Also, I edited my answer since your comment. I found 2 tiny bugs and fixed them. – OneMadGypsy Aug 27 '20 at 21:59
  • 1
    @CoolCloud ~ I updated my example with 2 example `main.py`. One is OOP style and the other is procedural. – OneMadGypsy Aug 27 '20 at 22:16
  • Thanks for your time,I'll check it up soon and accept it – Delrius Euphoria Aug 27 '20 at 23:09
  • Wow, this works like a CHARM, but how do i get the data from the entry box? `get()` method seems to not work – Delrius Euphoria Aug 28 '20 at 13:04
  • 1
    @CoolCloud ~ I didn't make references in my example. You have to start by making a reference (ex:) `time = ctk.FormatEntry(root, tformat)` and then you can use `time.input`. `.input` is a read-only `@property` so you call it like a var, not a method/function. – OneMadGypsy Aug 28 '20 at 13:08
  • okays, got that up, anyway to change the format a bit, like currently it is 123-102-1712 and make it 101-1203215 – Delrius Euphoria Aug 28 '20 at 13:12
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/220620/discussion-between-michael-guidry-and-cool-cloud). – OneMadGypsy Aug 28 '20 at 13:13
  • 1
    @CoolCloud ~ everyhing is fixed – OneMadGypsy Aug 29 '20 at 15:58
  • https://stackoverflow.com/questions/63651586/adding-placeholders-to-tkinter-entry-widget-in-a-procedural-way here you go ;) – Delrius Euphoria Aug 29 '20 at 20:41
  • Hey, hi, can you actually tell me what `FormEntryFormat_dc(re.compile('^(\d{1,3}(-(\d{1,7})?)?)?$'), '-' , [3] , True)` would be for a number supposed to insert `-` in the format `12345-12345-12345` – Delrius Euphoria Oct 27 '20 at 14:08
  • @CoolCloud ~ `FormEntryFormat_dc(re.compile('^(\d{1,5}(-(\d{1,5}(-(\d{1,5})?)?)?)?)?$'), '-' , [5, 11], True)`. It's pretty simple. You want 5 characters, dash, 5 characters, dash, 5 characters. So just turn the max number to 5 in the regex ranges, and customize the position list for the proper dash positions ([5, 11]). Keep in mind that this regex is just validating as you type. It doesn't care what the final result is. That's why there are ranges from 1 to max. It has to do that to allow you to type. – OneMadGypsy Oct 27 '20 at 18:48
  • Thanks man!!! Can you help me out once more? Too small to ask as a Q, any idea on how to allow just 5 numbers on an entry field? without using regex or something? – Delrius Euphoria Oct 27 '20 at 18:50
  • @CoolCloud ~ simply capture typing into the field and only allow it to add new characters if the current string has a length less than 5. Use `vcmd`. That way you can simply `return len(entry.get()) < 5)`, which if false, will not type the character. – OneMadGypsy Oct 27 '20 at 18:56
  • @CoolCloud ~ However, you will want to make sure that the keypress was an actual character before you make that return. For instance, if `entry.get()` has a length of 5 and you press `backspace` it will still return false and disallow the backspace from happening. – OneMadGypsy Oct 27 '20 at 19:17
1

Here is a procedural example. The example is very heavily commented.

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()
OneMadGypsy
  • 4,640
  • 3
  • 10
  • 26