Автоматически добавлять тире в поля ввода в tkinter

Есть ли способ автоматически добавлять тире в номер телефона, когда человек вводит свой номер телефона, например, например, номер телефона 5551111234, но когда он вводит его в поле ввода, номер должен автоматически отображаться с дефисом ч/б номер, например < сильный>555-1111234.


person Cool Cloud    schedule 31.05.2020    source источник


Ответы (3)


Это сложный пример, но он обрабатывает не только телефонные номера. Это закомментировано до смерти.

#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()
person Michael Guidry    schedule 27.08.2020
comment
Спасибо за этот удивительный ответ и ваше время на это, но есть ли способ без классов? Поскольку в моем графическом интерфейсе нет классов, и реализация классов только для этой цели немного излишняя, верно? - person Cool Cloud; 28.08.2020
comment
@CoolCloud ~ каждый виджет, который вы используете в tkinter, является классом, включая Tk и Toplevel. Если реализация класса была излишним для вашего проекта, то вы вообще не могли использовать tkinter. Кроме того, я отредактировал свой ответ после вашего комментария. Я нашел 2 маленькие ошибки и исправил их. - person Michael Guidry; 28.08.2020
comment
@CoolCloud ~ Я обновил свой пример двумя примерами main.py. Один в стиле ООП, а другой процедурный. - person Michael Guidry; 28.08.2020
comment
Спасибо за ваше время, я скоро проверю и приму его. - person Cool Cloud; 28.08.2020
comment
Вау, это работает как CHARM, но как мне получить данные из поля ввода? get() метод не работает - person Cool Cloud; 28.08.2020
comment
@CoolCloud ~ Я не делал ссылок в своем примере. Вы должны начать с создания ссылки (например:) time = ctk.FormatEntry(root, tformat), а затем вы можете использовать time.input. .input - это @property только для чтения, поэтому вы называете его как переменную, а не метод/функцию. - person Michael Guidry; 28.08.2020
comment
хорошо, понял, в любом случае немного изменить формат, например, сейчас это 123-102-1712 и сделать его 101-1203215 - person Cool Cloud; 28.08.2020
comment
Давайте продолжим обсуждение в чате. - person Michael Guidry; 28.08.2020
comment
@CoolCloud ~ все исправлено - person Michael Guidry; 29.08.2020
comment
stackoverflow.com/questions/63651586/ вот, пожалуйста;) - person Cool Cloud; 29.08.2020
comment
Эй, привет, не могли бы вы сказать мне, что FormEntryFormat_dc(re.compile('^(\d{1,3}(-(\d{1,7})?)?)?$'), '-' , [3] , True) будет для числа, которое должно вставлять - в формате 12345-12345-12345 - person Cool Cloud; 27.10.2020
comment
@CoolCloud ~ FormEntryFormat_dc(re.compile('^(\d{1,5}(-(\d{1,5}(-(\d{1,5})?)?)?)?)?$'), '-' , [5, 11], True). Это довольно просто. Вы хотите 5 символов, тире, 5 символов, тире, 5 символов. Поэтому просто измените максимальное число на 5 в диапазонах регулярных выражений и настройте список позиций для правильных позиций тире ([5, 11]). Имейте в виду, что это регулярное выражение просто проверяется при вводе. Неважно, каков будет конечный результат. Вот почему существуют диапазоны от 1 до макс. Он должен сделать это, чтобы позволить вам печатать. - person Michael Guidry; 27.10.2020
comment
Спасибо чувак!!! Вы можете помочь мне еще раз? Слишком мало, чтобы спросить в качестве вопроса, есть идеи о том, как разрешить только 5 чисел в поле ввода? без использования регулярных выражений или чего-то еще? - person Cool Cloud; 27.10.2020
comment
@CoolCloud ~ просто запишите ввод в поле и разрешите добавлять новые символы только в том случае, если текущая строка имеет длину менее 5. Используйте vcmd. Таким образом, вы можете просто return len(entry.get()) < 5), что, если оно ложно, не будет вводить символ. - person Michael Guidry; 27.10.2020
comment
@CoolCloud ~ Тем не менее, вы должны убедиться, что нажатие клавиши было фактическим символом, прежде чем выполнять этот возврат. Например, если entry.get() имеет длину 5, и вы нажмете backspace, он все равно вернет false и запретит выполнение возврата. - person Michael Guidry; 27.10.2020

Я использовал комбинацию обоих этого примера трассировки переменных tkinter и объединил его с этим ответом, я не уверен на 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"\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()

Альтернатива

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

Это было мое решение с использованием phonenumbers из PyPi, которое, казалось, заставило его работать.

person jimbob88    schedule 31.05.2020
comment
ценю это, но форматирование довольно испорчено, вы пробовали это? - person Cool Cloud; 31.05.2020
comment
Извините, я запутался, просто скопировал и вставил это, и это сработало в моей системе? В чем проблема? - person jimbob88; 31.05.2020
comment
метка не такая, как мы вводим в поле ввода - person Cool Cloud; 31.05.2020
comment
@CoolCloud Это должно быть исправлено сейчас! Прости - person jimbob88; 31.05.2020
comment
такая же ошибка, попробуй ввести 055 на ул, может и заметишь - person Cool Cloud; 31.05.2020

Вот процедурный пример. Пример очень сильно прокомментирован.

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()
person Michael Guidry    schedule 27.08.2020