Skip to content

Instantly share code, notes, and snippets.

@MaxMorais
Last active October 24, 2023 12:06
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save MaxMorais/5388218 to your computer and use it in GitHub Desktop.
Save MaxMorais/5388218 to your computer and use it in GitHub Desktop.
Masked Entry Widget for TkinterFormWidget for Tkinter - Like web2py FormWidget, with label, entry and tooltipTo future, ensure validation.
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#-------------------------------------------------------------------------------
# Name: maskedit.py
# Purpose:
#
# Author: Maxwell Morais (max.morais.dmm@gmail.com)
#
# Created: 10/04/2013
# Copyright: (c) Maxwell Morais 2013
# Licence: MIT
#-------------------------------------------------------------------------------
import sys
import re
import calendar
if sys.version[0]=='2':
import Tkinter as tk
import ttk
import tkFont
elif sys.version[0]=='3':
import tkinter as tk
import tkinter.font as tkFont
from tkinter import ttk
basestring = type(u'')
else:
print("Python version not supported, try 2 or 3 versions")
definitions = {
'9': '[0-9]',
'a': '[a-zA-Z]',
'x': '[a-zA-z0-9]'
}
class ToolTip(tk.Toplevel):
def __init__(self, root, **kw):
self.fields = {
'tooltip': None,
'variable': None,
'showdelay': 1,
'hidedelay': 10,
'root': root,
}
self.__hide_id = None
self.__show_id = None
for k in list(kw.keys()):
if k in self.fields:
self.fields[k]=kw.pop(k)
if not 'variable' in kw and not self.fields['variable']:
self.fields['variable']=tk.StringVar(root)
tk.Toplevel.__init__(self, root, **kw)
self.withdraw()
self.overrideredirect(True)
self.setup()
def setup(self):
self._tooltip()
self._bind()
self._grid()
def set_text(self, value):
self.fields['variable'].set(value)
def get_text(self):
return self.fields['variable'].get()
def _tooltip(self):
if not isinstance(self.fields.get('variable', None), tk.StringVar):
self.fields['variable'] = tk.StringVar(self)
tooltiptext = self.fields['tooltip'] or self.fields['variable'].get()
self.fields['variable'].set(tooltiptext)
self._widget = ttk.Label(self,
textvariable=self.fields['variable'],
#aspect=1000,
background='#ffffcc'
)
def _grid(self):
self._widget.grid(row=0, column=0)
def _bind(self):
self.fields['root'].bind('<Enter>', self._init_tooltip, True)
self.fields['root'].bind('<Leave>', self._hide_tooltip, True)
def _init_tooltip(self, event):
self.__show_id = self.after(
int(self.fields['showdelay']*1000),
self._show_tooltip
)
def _show_tooltip(self, event=None):
self.__show_id = None
x = self.fields['root'].winfo_rootx()
y = self.fields['root'].winfo_rooty()
height = self.fields['root'].winfo_height()
width = self.fields['root'].winfo_width()
lx = x + self.winfo_width() + 5
if lx < self.fields['root'].winfo_screenwidth() - 5:
x += 5
else:
x += self.fields['root'].winfo_screenwidth() - lx
ly = (y + height + self.winfo_height() + 5)
if ly < self.fields['root'].winfo_height() - 5:
y += height + 5
else:
y += -(self.winfo_height()+5)
self.wm_geometry('+%d+%d'%(x,y))
self.deiconify()
self.__hide_id = self.after(
int(self.fields['hidedelay'])*1000,
self._hide_tooltip
)
def _hide_tooltip(self, event):
if self.__show_id is not None:
self.after_cancel( self.__show_id )
self.__show_id = None
if self.__hide_id is not None:
self.after_cancel( self.__hide_id )
self.__hide_id = None
self.withdraw()
class FormWidget(ttk.Frame):
_fields= {
'field': None,
'label': None,
'help': None,
'variable': None,
'labelvariable': None,
'helpvariable': None
}
_factory = {
'variable': tk.StringVar,
'widget': tk.Entry
}
def __init__(self, root, **kw):
self.fields = self._fields.copy()
for k in list(kw.keys()):
if k in self.fields:
self.fields[k]=kw.pop(k)
if not 'variable' in kw:
self.fields['variable']=self._factory.get(
'variable',
tk.StringVar)(root)
ttk.Frame.__init__(self, root, **kw)
self.setup()
def setup(self):
self._label()
self._widget()
self._help()
self._grid()
def set(self, value): self.fields['variable'].set(value)
def get(self): return self.fields['variable'].get()
def set_label(self, value): self.fields['labelvariable'].set(value)
def get_label(self): return self.fields['labelvariable'].get()
def set_help(self, value): self.fields['helpvariable'].set(value)
def get_help(self): return self.fields['helpvariable'].get()
def _label(self):
labeltext = self.fields['label'] or self.fields['field']['label'] or ''
if not isinstance(self.fields.get('labelvariable', None), tk.StringVar):
self.fields['labelvariable'] = tk.StringVar(self)
self.fields['labelvariable'].set(labeltext)
self._wgt_label = ttk.Label(
self,
textvariable=self.fields['labelvariable']
)
def _widget(self):
self._widget = self._factory.get('widget', ttk.Entry)(
self,
textvariable=self.fields['variable']
)
def _help(self):
helptext = self.fields['help'] or ''
if not helptext: return
if not isinstance(self.fields.get('helpvariable', None), tk.StringVar):
self.fields['helpvariable'] = tk.StringVar(self)
self.fields['helpvariable'].set(helptext)
self._wgt_help = ToolTip(
self._widget,
variable=self.fields['helpvariable']
)
def _grid(self):
self._wgt_label.grid(row=0, column=0)
self._widget.grid(row=1, column=0)
class StringWidget(ttk.Entry):
@property
def variable(self):
return self.fields['textvariable']
class MaskedWidget(ttk.Entry):
def __init__(self, master, format_type, **kw):
self.fields = {
'type': format_type,
'mask': None,
'monetary': False,
'dec_places': 2,
'dec_sep': '.',
'tho_places': 3,
'tho_sep': ',',
'symbol': '',
'fmt_neg': '-%(symbol)s%(amount)s',
'fmt_pos': '%(symbol)s%(amount)s',
'placeholder': '_',
'textvariable': None,
}
if str(format_type).lower() == 'fixed':
assert 'mask' in kw, 'the fixed mask, is not present'
self.fields['mask'] = kw.pop('mask', '').lower()
for k in list(kw.keys()):
if k in self.fields:
if k!='textvariable':
self.fields[k]=kw.pop(k)
else:
self.fields[k]=kw[k]
if not 'textvariable' in kw:
self.fields['textvariable']=tk.StringVar(master)
kw['textvariable'] = self.fields['textvariable']
ttk.Entry.__init__(self, master, **kw)
self.defs = definitions
self.tests = []
self.partialPosition = None
self.firstNonMaskPosition = None
self.len = len(self.fields['mask'])
for i,c in enumerate(self.fields['mask'].lower()):
if c == '?':
self.len -= 1
self.partialPosition = i
atom = self.defs.get(c, None)
self.tests.append(re.compile('(%s)'%atom) if atom else atom)
if not atom and self.firstNonMaskPosition==None:
self.firstNonMaskPosition = len(self.tests)-1
self.writeBuffer()
if str(self.cget("state")).upper()!="DISABLED":
self.bind('<KeyPress>', self._onKeyPress, True)
self.bind('<KeyRelease>', lambda e: 'break', True)
self.bind('<FocusIn>', self._onFocusIn, True)
def clean(self):
self.fields['textvariable'].set('')
self.writeBuffer()
def clean_numeric(self, string):
if not isinstance(string, basestring): string = str(string)
string = string.replace(self.fields['symbol']+' ', '')\
.replace(self.fields['tho_sep'], '')\
.replace(self.fields['dec_sep'], '.')
if not '.' in string:
string = list(string)
string.insert(-2, '.')
string = ''.join(string)
return string.partition('.')
def fmt_numeric( self, amount):
temp = '00' if not '.' in str(amount) \
else str(amount).split('.')[1]
l = []
amount = amount.split('.')[0]
try:
minus = float(''.join(self.clean_numeric(amount)))<0
except ValueError:
minus = 0
if len(amount)> self.fields['tho_places']:
nn = amount[-self.fields['tho_places']:]
l.append(nn)
amount = amount[:len(amount)-self.fields['tho_places']]
while len(amount) > self.fields['tho_places']:
nn = amount[len(amount)-self.fields['tho_places']:]
l.insert(0, nn)
amount = amount[0:len(amount)-self.fields['tho_places']]
if len(''.join(self.clean_numeric(amount)))>0: l.insert(0, amount)
amount = self.fields['tho_sep'].join(l)+self.fields['dec_sep']+temp
if minus:
amount = self.fields['fmt_neg']%{
'symbol':self.fields['symbol'],
'amount': amount
}
else:
amount = self.fields['fmt_pos']%{
'symbol': (self.fields['symbol']+' ') if self.fields['symbol'] else '',
'amount': amount
}
return amount
def seekNext(self, pos):
if 0 <= pos+1<self.len:
if self.tests[pos+1]:
return pos+1
return self.seekNext(pos+1)
return pos
def seekPrev(self, pos):
if 0 <= pos-1 < self.len:
if self.tests[pos-1]:
return pos-1
return self.seekPrev(pos-1)
return pos
def shiftL(self, begin, end):
if begin < 0: return
for i in range(self.len):
j = self.seekNext(begin)
if self.tests[i]:
if j < self.len and self.tests[i].match(self.buffer[i]):
self.buffer[i] = self.buffer[j]
self.buffer[j] = self.fields['placeholder']
else:
break
def shiftR(self, pos, c):
if pos in range(len(self.len)):
j = self.seekNext(pos)
t = self.buffer[pos]
if not t == c and j < self.len and t == self.fields['placeholder']:
self.buffer[pos] = c
def writeBuffer(self):
self.fields['textvariable'].set(
''.join(
filter(
lambda x: x!=None,
map(
lambda c, self=self:
(self.fields['placeholder']
if self.defs.get(c, None)
else c)
if c!='?' else None, self.fields['mask'])
)
)
)
return self.get()
def _onFocusIn(self, event):
if self.len>0 and self.tests[0]:
self.icursor(0)
else:
self.icursor(self.seekNext(0))
def _onKeyPress(self, event):
if event.keysym == 'Tab':
return
elif event.keysym == 'Escape':
if self.fields['type'] == 'fixed':
self.writeBuffer()
else:
self.delete(0, len(event.widget.get()))
widget = event.widget
val = widget.get()
idx = widget.index(tk.INSERT)
if event.keysym == 'Left':
if 0 <= idx < self.len:
if idx < self.firstNonMaskPosition:
return 'break'
elif not self.tests[idx]:
widget.icursor(self.seekPrev(idx))
elif event.keysym == 'Right':
if 0 <= idx < self.len:
if idx >= self.len:
return 'break'
elif not self.tests[idx]:
widget.icursor(self.seekNext(idx))
elif event.keysym == 'BackSpace' and self.fields['type'] != 'numeric':
def repl_or_stop(cls, wget, pos):
if 0 <= pos <= cls.len:
if not cls.tests[pos]:
pos = cls.seekPrev(pos)
cls._write_char(pos, cls.fields['placeholder'], -1)
return 'break'
repl_or_stop(self, widget, idx - 1)
return 'break'
else:
if self.fields['type'] == 'fixed':
if self._write_char(idx, event.char) == 'break':
return 'break'
elif self.fields['type'] == 'numeric' and event.char.isdigit():
if val:
widget.delete(0, len(val))
head, sep, tail = self.clean_numeric(val)
else:
head, sep, tail = '0', '.', '00'
if not head:
head = '0'
if len(tail) < 2:
tail = '0' + tail
if tail and len(tail + event.char) <= 2 and (int(tail+event.char))<99:
tail = tail[1:] + event.char
else:
if not int(head):
head = tail[0] if tail else '0'
else:
head += tail[0]
tail = tail[1:] + event.char
widget.insert(0, ''.join([head, sep, tail]))
return 'break'
elif self.fields['type'] == 'numeric' and event.keysym == 'BackSpace':
if val:
widget.delete(0, len(val))
head, sep, tail = self.clean_numeric(val[:-1])
else:
head, sep, tail = '0', '.', '00'
widget.insert(0, ''.join([head, sep, tail]))
return 'break'
else:
self.bell()
return 'break'
def insert(self, index, value):
if self.fields['type']=='numeric':
ttk.Entry.insert(self, index, self.fmt_numeric(value))
else:
for c in str(value):
while (not self.tests[index] or not self.tests[index].match(c)):
index += 1
self._write_char(index,c)
index += 1
def _write_char(self, idx, char, direction=+1):
if 0<=idx<self.len and self.tests[idx]:
if char != self.fields['placeholder'] and not self.tests[idx].match(char):
self.bell()
return 'break'
self.delete(idx)
ttk.Entry.insert(self, idx, char)
if direction == +1:
if idx + 1 < self.len and not self.tests[idx+1]:
idx = self.seekNext(idx)
else:
idx += 1
elif direction == -1 and \
idx - 1 >= 0 and \
not self.tests[idx]:
idx = self.seekPrev(idx)
self.icursor(idx)
return 'break'
else:
self.bell()
return 'break'
def get_calendar(locale, fwday):
# instantiate proper calendar class
if locale is None:
return calendar.TextCalendar(fwday)
else:
return calendar.LocaleTextCalendar(fwday, locale)
class Calendar(ttk.Frame):
# XXX ToDo: cget and configure
datetime = calendar.datetime.datetime
timedelta = calendar.datetime.timedelta
def __init__(self, master=None, **kw):
"""
WIDGET-SPECIFIC OPTIONS
locale, firstweekday, year, month, selectbackground,
selectforeground
"""
# remove custom options from kw before initializating ttk.Frame
fwday = kw.pop('firstweekday', calendar.SUNDAY)
year = kw.pop('year', self.datetime.now().year)
month = kw.pop('month', self.datetime.now().month)
locale = kw.pop('locale', None)
sel_bg = kw.pop('selectbackground', '#ecffc4')
sel_fg = kw.pop('selectforeground', '#05640e')
self._date = self.datetime(year, month, 1)
self._selection = None # no date selected
ttk.Frame.__init__(self, master, **kw)
self._cal = get_calendar(locale, fwday)
self.__setup_styles() # creates custom styles
self.__place_widgets() # pack/grid used widgets
self.__config_calendar() # adjust calendar columns and setup tags
# configure a canvas, and proper bindings, for selecting dates
self.__setup_selection(sel_bg, sel_fg)
# store items ids, used for insertion later
self._items = [self._calendar.insert('', 'end', values='')
for _ in range(6)]
# insert dates in the currently empty calendar
self._build_calendar()
# set the minimal size for the widget
self._calendar.bind('<Map>', self.__minsize)
def __setitem__(self, item, value):
if item in ('year', 'month'):
raise AttributeError("attribute '%s' is not writeable" % item)
elif item == 'selectbackground':
self._canvas['background'] = value
elif item == 'selectforeground':
self._canvas.itemconfigure(self._canvas.text, item=value)
else:
ttk.Frame.__setitem__(self, item, value)
def __getitem__(self, item):
if item in ('year', 'month'):
return getattr(self._date, item)
elif item == 'selectbackground':
return self._canvas['background']
elif item == 'selectforeground':
return self._canvas.itemcget(self._canvas.text, 'fill')
else:
r = ttk.tclobjs_to_py({item: ttk.Frame.__getitem__(self, item)})
return r[item]
def __setup_styles(self):
# custom ttk styles
style = ttk.Style(self.master)
arrow_layout = lambda dir: (
[('Button.focus', {'children': [('Button.%sarrow' % dir, None)]})]
)
style.layout('L.TButton', arrow_layout('left'))
style.layout('R.TButton', arrow_layout('right'))
def __place_widgets(self):
# header frame and its widgets
hframe = ttk.Frame(self)
lbtn = ttk.Button(hframe, style='L.TButton', command=self._prev_month)
rbtn = ttk.Button(hframe, style='R.TButton', command=self._next_month)
self._header = ttk.Label(hframe, width=15, anchor='center')
# the calendar
self._calendar = ttk.Treeview(show='', selectmode='none', height=7)
# pack the widgets
hframe.pack(in_=self, side='top', pady=4, anchor='center')
lbtn.grid(in_=hframe)
self._header.grid(in_=hframe, column=1, row=0, padx=12)
rbtn.grid(in_=hframe, column=2, row=0)
self._calendar.pack(in_=self, expand=1, fill='both', side='bottom')
def __config_calendar(self):
cols = self._cal.formatweekheader(3).split()
self._calendar['columns'] = cols
self._calendar.tag_configure('header', background='grey90')
self._calendar.insert('', 'end', values=cols, tag='header')
# adjust its columns width
font = tkFont.Font()
maxwidth = max(font.measure(col) for col in cols)
for col in cols:
self._calendar.column(col, width=maxwidth, minwidth=maxwidth,
anchor='e')
def __setup_selection(self, sel_bg, sel_fg):
self._font = tkFont.Font()
self._canvas = canvas = tk.Canvas(self._calendar,
background=sel_bg, borderwidth=0, highlightthickness=0)
canvas.text = canvas.create_text(0, 0, fill=sel_fg, anchor='w')
canvas.bind('<ButtonPress-1>', lambda evt: canvas.place_forget())
self._calendar.bind('<Configure>', lambda evt: canvas.place_forget())
self._calendar.bind('<ButtonPress-1>', self._pressed)
def __minsize(self, evt):
width, height = self._calendar.master.geometry().split('x')
height = height[:height.index('+')]
self._calendar.master.minsize(width, height)
def _build_calendar(self):
year, month = self._date.year, self._date.month
# update header text (Month, YEAR)
header = self._cal.formatmonthname(year, month, 0)
self._header['text'] = header.title()
# update calendar shown dates
cal = self._cal.monthdayscalendar(year, month)
for indx, item in enumerate(self._items):
week = cal[indx] if indx < len(cal) else []
fmt_week = [('%02d' % day) if day else '' for day in week]
self._calendar.item(item, values=fmt_week)
def _show_selection(self, text, bbox):
"""Configure canvas for a new selection."""
x, y, width, height = bbox
textw = self._font.measure(text)
canvas = self._canvas
canvas.configure(width=width, height=height)
canvas.coords(canvas.text, width - textw, height / 2 - 1)
canvas.itemconfigure(canvas.text, text=text)
canvas.place(in_=self._calendar, x=x, y=y)
# Callbacks
def _pressed(self, evt):
"""Clicked somewhere in the calendar."""
x, y, widget = evt.x, evt.y, evt.widget
item = widget.identify_row(y)
column = widget.identify_column(x)
if not column or not item in self._items:
# clicked in the weekdays row or just outside the columns
return
item_values = widget.item(item)['values']
if not len(item_values): # row is empty for this month
return
text = item_values[int(column[1]) - 1]
if not text: # date is empty
return
bbox = widget.bbox(item, column)
if not bbox: # calendar not visible yet
return
# update and then show selection
text = '%02d' % text
self._selection = (text, item, column)
self._show_selection(text, bbox)
def _prev_month(self):
"""Updated calendar to show the previous month."""
self._canvas.place_forget()
self._date = self._date - self.timedelta(days=1)
self._date = self.datetime(self._date.year, self._date.month, 1)
self._build_calendar() # reconstuct calendar
def _next_month(self):
"""Update calendar to show the next month."""
self._canvas.place_forget()
year, month = self._date.year, self._date.month
self._date = self._date + self.timedelta(
days=calendar.monthrange(year, month)[1] + 1)
self._date = self.datetime(self._date.year, self._date.month, 1)
self._build_calendar() # reconstruct calendar
# Properties
@property
def selection(self):
"""Return a datetime representing the current selected date."""
if not self._selection:
return None
year, month = self._date.year, self._date.month
return self.datetime(year, month, int(self._selection[0]))
def main():
root = tk.Tk()
page = tk.Frame(root)
c = MaskedWidget(page, 'fixed', mask='+99 (99) 999-999-999', width=20)
c.insert(0, '6611122266647')
c.pack()
c = MaskedWidget(page, 'fixed', mask='99/99/9999')
c.insert(0, '29091991')
c.pack()
MaskedWidget(page, 'numeric').pack()
c = MaskedWidget(page, 'numeric', dec_sep=",", tho_sep='.', symbol="R$")
c.insert(0, '12659.96')
c.pack()
# Cleaning text
c = MaskedWidget(page, 'numeric', dec_sep=",", tho_sep='.', symbol="R$")
c.insert(0, '12659.96')
c.pack()
c.clean()
c = Calendar(page)
c.pack()
FormWidget(page,
label=u'Olá',
help='Seu Nome'
).pack()
page.pack()
root.mainloop()
if __name__ == '__main__':
main()
@MaxMorais
Copy link
Author

Fixed a delete error using BackSpace key, reported by Juan Carvalho

@MaxMorais
Copy link
Author

Various improvements regarding the BackSpace key issue, and the formatting of numeric fields

@bernardesrodrigo
Copy link

Max, tudo bem?

Implementei o seu código em uma aplicação que estou desenvolvendo.
O único problema é que quando preciso limpar os campos usando o método ".delete(0,END)" ele limpa também a máscara inserida, e transforma o widget em um entry normal.

Alguma sugestão? Tentei usar o método ".select_range(0,END)" e depois ".select_clear()" mas não estão funcionando não sei porque. Eles nao executam nada.

Pesquisei várias alternativas e a que me deixou mais perto do que quero foi o seu código.

Desde já muito obrigado.

@MaxMorais
Copy link
Author

@bernardesrodrigo eu só vi sua mensagem agora, para limpar vc pode usar

widgets.fields['textvariable'].set("")

@wesleygurgel
Copy link

Max, poderia dar uma exemplificada melhor na dúvida do Bernardes, por favor!

Estava tentando aqui com o que você repassou - widgets.fields['textvariable'].set("") - porém não tá dando certo :/

@MaxMorais
Copy link
Author

MaxMorais commented Sep 28, 2020

@wesleygurgel vou implementar um método dedicado para isso, assim que estiver em ordem, te aviso, e ao @bernardesrodrigo também

@MaxMorais
Copy link
Author

Added clean as dedicated method to cleanup the field

@MaxMorais
Copy link
Author

@wesleygurgel agora basta utilizar o método clean para limpar o widget

@wesleygurgel
Copy link

@wesleygurgel agora basta utilizar o método clean para limpar o widget

@MaxMorais tu é brabo, obg de verdade

@vitortitz
Copy link

bom dia, estou com duvida em relação a atribuir a data selecionada no Calendar à uma variavel.

@MaxMorais
Copy link
Author

@vitortitz, qual a sua pergunta?

@vitortitz
Copy link

após fazer a seleção no calendário como faço para poder atribuir a data selecionada à uma variável? Desculpa a pergunta, sou novato em python

@MaxMorais
Copy link
Author

@vitortitz utilize algo assim no teu codigo

minha_var = meu_calendar.selection

@vitortitz
Copy link

@MaxMorais desculpa a demora, obrigado pela ajuda, irei fazer o teste.

@vitortitz
Copy link

@MaxMorais desculpa incomodar novamente, mas existe alguma forma de selecionar o ano tbm, sem ser voltando de mês em mês?

@jhonpablo5100
Copy link

como faço para adicionar maskara cpf no Entry do Tkinter?

@TIAGOLL
Copy link

TIAGOLL commented Nov 4, 2022

Me ajudou muito mesmo, parabéns pela sua iniciativa!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment