Discussion:
[Tkinter-discuss] tkinter Entry validation: insert instead of replace
lansman
2013-02-22 09:50:45 UTC
Permalink
Here is simple Python/Tkinter program with single Entry widget that i want
automatically check is number entered or not.

http://pastebin.com/WkLy2Csb <http://pastebin.com/WkLy2Csb>

print statement in _validate() function only for debugging. The problem is
in this case visual editing of Entry is incorrect. For example i doing those
steps:

1. Launch program.
2. See '111' value in the Entry
3. Select '111' by left mouse button
4. Press "9" on keyboard
5. Instead of full replace '111' to '9' insert happens and i see '9111'!

Debug log (i numerated steps for convinience):
1. OnValidate: d='-1' i='-1' P='111' s='' S='' v='all' V='forced'
W='.37125736'
2. OnValidate: d='-1' i='-1' P='111' s='111' S='' v='all' V='focusin'
W='.37125736'
3. OnValidate: d='0' i='0' P='' s='111' S='111' v='all' V='key'
W='.37125736'
4. OnValidate: d='1' i='0' *P='9111'* s='111' S='9' v='all' V='key'
W='.37125736'
5. OnValidate: d='0' i='1' P='9' s='9111' S='111' v='all' V='key'
W='.37125736'
6. OnValidate: d='1' i='1' P='99' s='9' S='9' v='all' V='key' W='.37125736'
7. OnValidate: d='1' i='2' P='999' s='99' S='9' v='all' V='key'
W='.37125736'

Pay attention to step 4. It is strange additional step with unwanted Entry
text state ('9111')

But if i change
/return P.isdigit()/
to
/return True/

Everything becomes ok! Entry works like any entry in other programs.

1. OnValidate: d='-1' i='-1' P='111' s='' S='' v='all' V='forced'
W='.37650024'
2. OnValidate: d='-1' i='-1' P='111' s='111' S='' v='all' V='focusin'
W='.37650024'
3. OnValidate: d='0' i='0' P='' s='111' S='111' v='all' V='key'
W='.37650024'
4. OnValidate: d='1' i='0' P='9' s='' S='9' v='all' V='key' W='.37650024'
5. OnValidate: d='1' i='1' P='99' s='9' S='9' v='all' V='key' W='.37650024'
6. OnValidate: d='1' i='2' P='999' s='99' S='9' v='all' V='key'
W='.37650024'

I work on Windows 7, python 2.7.
Anybody know why it happens?



--
View this message in context: http://python.6.n6.nabble.com/tkinter-Entry-validation-insert-instead-of-replace-tp5006800.html
Sent from the Python - tkinter-discuss mailing list archive at Nabble.com.
Michael Lange
2013-02-22 14:44:34 UTC
Permalink
Hi,

On Fri, 22 Feb 2013 01:50:45 -0800 (PST)
Post by lansman
Anybody know why it happens?
this is a common issue. To illustrate what's going on, I changed your
validate callback a little into:

def _validate(self, d, i, P, s, S, v, V, W):
print '"%s"' % P, P.isdigit()
return P.isdigit()

then I ran the program and got:

$ python test8.py
"111" True # -> program start
"111" True # -> selected the 111 with the mouse
"" False # -> pressed the 9 key
"9111" True

So we see, what happens is that when the selected "111" is overwritten
with "9" the Entry is *emptied first*, the empty string validates to
False and is therefore not accepted by the widget and so the 111 remains
in the widget when the 9 is inserted.

So the trick here is to let the widget accept the empty string, as in:

def _validate(self, d, i, P, s, S, v, V, W):
if P == '':
return True
return P.isdigit()

Regards

Michael

.-.. .. ...- . .-.. --- -. --. .- -. -.. .--. .-. --- ... .--. . .-.

Lots of people drink from the wrong bottle sometimes.
-- Edith Keeler, "The City on the Edge of Forever",
stardate unknown
Teodor the Thinker
2013-02-22 15:59:50 UTC
Permalink
Thank you. It was so simple! I changed function return to
return P.isdigit() or (P == "")
and it seems work.

But it allows empty string not only during editing. I want to make: when i
clear the Entry and switch to another GUI element Entry rollbacks to last
valid value.

But obvious code to achieve this goal doesnt work:
if P == "" and V == 'focusout':
return False
else:
return (P.isdigit() or P == "")

I found when "return False" is executed, *s* variable is empty. So i need
to rollback not for last but to *before last* value. Is there a nice bultin
Tkinter way to achieve that or i should declare some variable with "before
last value" by my own?


2013/2/22 Michael Lange <klappnase at web.de>
Post by Michael Lange
Hi,
On Fri, 22 Feb 2013 01:50:45 -0800 (PST)
Post by lansman
Anybody know why it happens?
this is a common issue. To illustrate what's going on, I changed your
print '"%s"' % P, P.isdigit()
return P.isdigit()
$ python test8.py
"111" True # -> program start
"111" True # -> selected the 111 with the mouse
"" False # -> pressed the 9 key
"9111" True
So we see, what happens is that when the selected "111" is overwritten
with "9" the Entry is *emptied first*, the empty string validates to
False and is therefore not accepted by the widget and so the 111 remains
in the widget when the 9 is inserted.
return True
return P.isdigit()
Regards
Michael
.-.. .. ...- . .-.. --- -. --. .- -. -.. .--. .-. --- ... .--. . .-.
Lots of people drink from the wrong bottle sometimes.
-- Edith Keeler, "The City on the Edge of Forever",
stardate unknown
_______________________________________________
Tkinter-discuss mailing list
Tkinter-discuss at python.org
http://mail.python.org/mailman/listinfo/tkinter-discuss
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/tkinter-discuss/attachments/20130222/374c893e/attachment.html>
Michael Lange
2013-02-22 19:24:01 UTC
Permalink
On Fri, 22 Feb 2013 21:59:50 +0600
Post by Teodor the Thinker
Thank you. It was so simple! I changed function return to
return P.isdigit() or (P == "")
and it seems work.
when i clear the Entry and switch to another GUI element Entry
rollbacks to last valid value.
return False
return (P.isdigit() or P == "")
I found when "return False" is executed, *s* variable is empty. So i
need to rollback not for last but to *before last* value. Is there a
nice bultin Tkinter way to achieve that or i should declare some
variable with "before last value" by my own?
There is no (or at least I don't know of) any built-in way to do this.

One possible way to work around this is to store the old value and restore
it if required, as in this example:

##############################################################
import Tkinter

class IntEntry(Tkinter.Entry):
def __init__(self, master=None, value=0, **kw):
Tkinter.Entry.__init__(self, master, **kw)
self._oldvalue = value
self.insert('end', value)
vcmd = (self.register(self._validate), '%s', '%P')
self.configure(validate='all', vcmd=vcmd)
self.bind('<FocusOut>', self._focus_out)

def _focus_out(self, event):
self.get()

def _validate(self, old, new):
if old != '':
self._oldvalue = old
if new == '':
return True
return new.isdigit()

def get(self):
value = Tkinter.Entry.get(self)
if value == '':
self.insert('end', self._oldvalue)
value = self._oldvalue
return int(value)

def set(self, value):
self.delete(0, 'end')
self.insert('end', value)

root = Tkinter.Tk()
e1 = IntEntry(root, value=111)
e1.pack(side='top', padx=100, pady=20)
e2 = IntEntry(root, value=222)
e2.pack(side='bottom', padx=100, pady=20)
def test(event):
print 'test'
e2.bind('<FocusOut>', test)
root.mainloop()
##############################################################

This appears to work rather robust, it takes even care that get()
will always return a decent value.
However you see the weak point of this approach in the second Entry where
a new binding to <FocusOut> events stops the validation from working
properly.
Using the "%V" substitution in the _validate() callback seems not to be
exactly an alternative, because calling insert() from within the
validation callback will again start validation and the results are hard
to predict; sooner or later it may happen that Tk decides to turn off
validation at all (at least I never managed to get such setups working).

An alternative may (or may not ;) be to use the modified get() as above
but to leave the empty Entries alone until some procedure is started by
the user that calls get() which will automagically restore the last sane
value (that's in fact what I did in one of my programs where I took the
above example from).

Regards

Michael

.-.. .. ...- . .-.. --- -. --. .- -. -.. .--. .-. --- ... .--. . .-.

Another war ... must it always be so? How many comrades have we lost
in this way? ... Obedience. Duty. Death, and more death ...
-- Romulan Commander, "Balance of Terror", stardate 1709.2
Doc Lans
2013-02-22 23:35:24 UTC
Permalink
I removed your focus bind and all Entries work fine for me. I cannot insert
something incorrect and cannot leave Entries blank.

import Tkinter

class IntEntry(Tkinter.Entry):
def __init__(self, master=None, value=0, **kw):
Tkinter.Entry.__init__(self, master, **kw)
self._oldvalue = value
self.insert('end', value)
vcmd = (self.register(self._validate), '%s', '%P')
self.configure(validate='all', vcmd=vcmd)
self.bind('<FocusOut>', self._focus_out)

def _focus_out(self, event):
self.get()

def _validate(self, old, new):
if old != '':
self._oldvalue = old
if new == '':
return True
return new.isdigit()

def get(self):
value = Tkinter.Entry.get(self)
if value == '':
self.insert('end', self._oldvalue)
value = self._oldvalue
return int(value)

def set(self, value):
self.delete(0, 'end')
self.insert('end', value)

def printer():
for i in vals:
print i.get(),
print

root = Tkinter.Tk()
vals = []
for i in range(5):
e = IntEntry(root, value=(i + 1) * 111)
e.pack(side='top', padx=5, pady=5)
vals.append(e)
Tkinter.Button(text='print all', command=printer).pack(side='top')
root.mainloop()

2013/2/23 Michael Lange <klappnase at web.de>
Post by Michael Lange
On Fri, 22 Feb 2013 21:59:50 +0600
Post by Teodor the Thinker
Thank you. It was so simple! I changed function return to
return P.isdigit() or (P == "")
and it seems work.
when i clear the Entry and switch to another GUI element Entry
rollbacks to last valid value.
return False
return (P.isdigit() or P == "")
I found when "return False" is executed, *s* variable is empty. So i
need to rollback not for last but to *before last* value. Is there a
nice bultin Tkinter way to achieve that or i should declare some
variable with "before last value" by my own?
There is no (or at least I don't know of) any built-in way to do this.
One possible way to work around this is to store the old value and restore
##############################################################
import Tkinter
Tkinter.Entry.__init__(self, master, **kw)
self._oldvalue = value
self.insert('end', value)
vcmd = (self.register(self._validate), '%s', '%P')
self.configure(validate='all', vcmd=vcmd)
self.bind('<FocusOut>', self._focus_out)
self.get()
self._oldvalue = old
return True
return new.isdigit()
value = Tkinter.Entry.get(self)
self.insert('end', self._oldvalue)
value = self._oldvalue
return int(value)
self.delete(0, 'end')
self.insert('end', value)
root = Tkinter.Tk()
e1 = IntEntry(root, value=111)
e1.pack(side='top', padx=100, pady=20)
e2 = IntEntry(root, value=222)
e2.pack(side='bottom', padx=100, pady=20)
print 'test'
e2.bind('<FocusOut>', test)
root.mainloop()
##############################################################
This appears to work rather robust, it takes even care that get()
will always return a decent value.
However you see the weak point of this approach in the second Entry where
a new binding to <FocusOut> events stops the validation from working
properly.
Using the "%V" substitution in the _validate() callback seems not to be
exactly an alternative, because calling insert() from within the
validation callback will again start validation and the results are hard
to predict; sooner or later it may happen that Tk decides to turn off
validation at all (at least I never managed to get such setups working).
An alternative may (or may not ;) be to use the modified get() as above
but to leave the empty Entries alone until some procedure is started by
the user that calls get() which will automagically restore the last sane
value (that's in fact what I did in one of my programs where I took the
above example from).
Regards
Michael
.-.. .. ...- . .-.. --- -. --. .- -. -.. .--. .-. --- ... .--. . .-.
Another war ... must it always be so? How many comrades have we lost
in this way? ... Obedience. Duty. Death, and more death ...
-- Romulan Commander, "Balance of Terror", stardate 1709.2
_______________________________________________
Tkinter-discuss mailing list
Tkinter-discuss at python.org
http://mail.python.org/mailman/listinfo/tkinter-discuss
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/tkinter-discuss/attachments/20130223/8828e807/attachment-0001.html>
Michael Lange
2013-02-22 23:55:24 UTC
Permalink
On Sat, 23 Feb 2013 05:35:24 +0600
Post by Doc Lans
I removed your focus bind and all Entries work fine for me. I cannot
insert something incorrect and cannot leave Entries blank.
I'm glad I could help.

Regards

Michael


.-.. .. ...- . .-.. --- -. --. .- -. -.. .--. .-. --- ... .--. . .-.

Men of peace usually are [brave].
-- Spock, "The Savage Curtain", stardate 5906.5

Loading...