Tkinter binding question

F

Frederic Rentsch

Hi All,

For most of an afternoon I've had that stuck-in-a-dead-end feeling
probing to no avail all permutations formulating bindings, trying to
make sense of manuals and tutorials. Here are my bindings:

label_frame.bind ('<Enter>', self.color_selected)
label_frame.bind ('<Leave>', self.color_selectable)
label_frame.bind ('<Button-1><ButtonRelease-1>', self.pick_record)
label_frame.bind ('<Button-3><ButtonRelease-3>', self.info_profile)

<Enter> and <Leave> work fine. But when I try to select an entered item,
the moment I push the left or the right button, color_selectable ()
and color_selected () are called again in rapid succession. The same
effect happens even when I push the middle mouse button which is
rather weird, because it has no binding. The behavior suggests that
no event can occur on an entered widget before it is left again and
if an event other that <Leave> comes in, the <Leave> callback gets
called automatically. That can't be right, though. It isn't possible
to click an item without entering it.
I put traces in all of the four callbacks. So I know what gets
called an what doesn't. Traces are:

On <Enter>:
hit list.color_selected ()
On <Leave>:
hit list.color_selectable ()

Fine so far.

<Enter>:
hit list.color_selected () # Still fine
<Button-1> or <Button-2> or <Button-3>:
hit list.color_selectable () # Not so fine!
hit list.color_selected ()
<ButtonRelease-1> (or -2 or -3)
(nothing)


Thanks for any suggestion

Frederic


OS: Ubuntu 10.04 LTS
Python: sys.version: 2.6.5 (r265:79063, Apr 16 2010, 13:09:56) [GCC 4.4.3]
Tkinter: VERSION 73770 (help (Tkinter), last line)
 
R

rantingrickjohnson

Hi All,

For most of an afternoon I've had that stuck-in-a-dead-end feeling
probing to no avail all permutations formulating bindings, trying to
make sense of manuals and tutorials. Here are my bindings:

label_frame.bind ('<Enter>', self.color_selected)
label_frame.bind ('<Leave>', self.color_selectable)
label_frame.bind ('<Button-1><ButtonRelease-1>', self.pick_record)
label_frame.bind ('<Button-3><ButtonRelease-3>', self.info_profile)

This example is useless to me. As a matter of fact, your whole explanation of the problem is ambitious at best. But you do realize that by binding both the click and release of a mouse button to the same callback you will call the callback twice?

Try to create a simplistic and generic example of the problem. Most times you'll find the flaw. Most code should start as template that prints messages to stdout. Large applications are built by taking babysteps. Not by jumping out the fire and into the frying pan!

WARNING: Debugging large chucks of spaghetti code is bad for your brain! Tip of the day: DIVIDE AND CONQUER!

## START CODE ##
from __future__ import print_function
import Tkinter as tk
root = tk.Tk()
root.geometry('200x200+20+20')
root.bind("<Enter>", lambda e:print("Enter"))
root.bind("<Leave>", lambda e:print("Leave"))
root.bind("<ButtonRelease-1>",
lambda e:print("ButtonOneRelease"))
root.bind("<ButtonRelease-3>",
lambda e:print("ButtonThreeRelease"))
root.mainloop()
## END CODE ##

Q1: Now, what exactly is the problem? Hmm???
Q2: Can you modify this code to correct the problem? Hmm???
Q3: Why are there so many inconsistencies in the Tkinter API? Hmm???

Tip: NEVER bind "ButtonClick" to trigger a callback (ESPECIALLY FOR A BUTTON CALLBACK!!!). ONLY bind "ButtonRelease". Why? Well because if the user accidentally clicks the button, he can move his pointer beyond the boundaries of the button and then release the mouse "button" WITHOUT triggering the callback. All good GUI coders follow this design pattern. (Except in certain cases where you want a mouse click to trigger an action quickly; like a Listbox item or menu, or whatever.)

if GUI.ergonomics < optimal:
raise NeophyteError("Do not pass GUI. Do not collect 100 dollars!")
<Enter> and <Leave> work fine. But when I try to select an entered item,
the moment I push the left or the right button, color_selectable ()
and color_selected () are called again in rapid succession.

That does not make sense. Are you calling the method "w.update()" anywhere in the callback; or as a consequence of the callback?
The same
effect happens even when I push the middle mouse button which is
rather weird, because it has no binding.

Not necessarily. Many of the Tk widgets come prepackaged with annoying little default bindings that would the test the patience of a saint. Like, for example, the Tkinter.Text widget. It has a nasty little default binding to paste the cut buffer contents when you press MouseButtonTwo. And since it also has a default binding of using MouseButtonTwo+MouseMotion to scroll vertically, you find yourself inserting cut buffers everywhere!
The behavior suggests that
no event can occur on an entered widget before it is left again and
if an event other that <Leave> comes in, the <Leave> callback gets
called automatically. That can't be right, though. It isn't possible
to click an item without entering it.

Interesting observation. I would say your code is too complicated to properly debug. You need to divide and conquer this spaghetti mess.
 
R

rantingrickjohnson

Hi All,

For most of an afternoon I've had that stuck-in-a-dead-end feeling
probing to no avail all permutations formulating bindings, trying to
make sense of manuals and tutorials. Here are my bindings:

label_frame.bind ('<Enter>', self.color_selected)
label_frame.bind ('<Leave>', self.color_selectable)
label_frame.bind ('<Button-1><ButtonRelease-1>', self.pick_record)
label_frame.bind ('<Button-3><ButtonRelease-3>', self.info_profile)

This example is useless to me. As a matter of fact, your whole explanation of the problem is ambitious at best. But you do realize that by binding both the click and release of a mouse button to the same callback you will call the callback twice?

Try to create a simplistic and generic example of the problem. Most times you'll find the flaw. Most code should start as template that prints messages to stdout. Large applications are built by taking babysteps. Not by jumping out the fire and into the frying pan!

WARNING: Debugging large chucks of spaghetti code is bad for your brain! Tip of the day: DIVIDE AND CONQUER!

## START CODE ##
from __future__ import print_function
import Tkinter as tk
root = tk.Tk()
root.geometry('200x200+20+20')
root.bind("<Enter>", lambda e:print("Enter"))
root.bind("<Leave>", lambda e:print("Leave"))
root.bind("<ButtonRelease-1>",
lambda e:print("ButtonOneRelease"))
root.bind("<ButtonRelease-3>",
lambda e:print("ButtonThreeRelease"))
root.mainloop()
## END CODE ##

Q1: Now, what exactly is the problem? Hmm???
Q2: Can you modify this code to correct the problem? Hmm???
Q3: Why are there so many inconsistencies in the Tkinter API? Hmm???

Tip: NEVER bind "ButtonClick" to trigger a callback (ESPECIALLY FOR A BUTTON CALLBACK!!!). ONLY bind "ButtonRelease". Why? Well because if the user accidentally clicks the button, he can move his pointer beyond the boundaries of the button and then release the mouse "button" WITHOUT triggering the callback. All good GUI coders follow this design pattern. (Except in certain cases where you want a mouse click to trigger an action quickly; like a Listbox item or menu, or whatever.)

if GUI.ergonomics < optimal:
raise NeophyteError("Do not pass GUI. Do not collect 100 dollars!")
<Enter> and <Leave> work fine. But when I try to select an entered item,
the moment I push the left or the right button, color_selectable ()
and color_selected () are called again in rapid succession.

That does not make sense. Are you calling the method "w.update()" anywhere in the callback; or as a consequence of the callback?
The same
effect happens even when I push the middle mouse button which is
rather weird, because it has no binding.

Not necessarily. Many of the Tk widgets come prepackaged with annoying little default bindings that would the test the patience of a saint. Like, for example, the Tkinter.Text widget. It has a nasty little default binding to paste the cut buffer contents when you press MouseButtonTwo. And since it also has a default binding of using MouseButtonTwo+MouseMotion to scroll vertically, you find yourself inserting cut buffers everywhere!
The behavior suggests that
no event can occur on an entered widget before it is left again and
if an event other that <Leave> comes in, the <Leave> callback gets
called automatically. That can't be right, though. It isn't possible
to click an item without entering it.

Interesting observation. I would say your code is too complicated to properly debug. You need to divide and conquer this spaghetti mess.
 
F

Frederic Rentsch

Rick,

Thank you for your thorough discussion. I tried your little program.
Enter and leave work as expected. Pushing mouse buttons call
leave-enter, exactly as it happened with my code. So that seems to be a
default behavior. No big deal. Without the tracing messages it would go
unnoticed. Releasing either of the bound mouse buttons displays the
corresponding messages. So all works fine.
If I copy your event descriptors into my program, the button-release
callback still fails. It works in your code, not in mine. Here is what
my code now looks like. It is somewhat more complicated than yours,
because I bind Frames holding each a line (line_frame) and each frame
contains a few Labels side by side. The idea is to achieve a table with
vertically aligning columns each line of which I can click-select. (Is
there a better way?)

for line_frame in ...:
line_frame.bind ('<Enter>', self.color_selected)
line_frame.bind ('<Leave>', self.color_selectable)
line_frame.bind ('<ButtonRelease-1>', self.pick_record)
line_frame.bind ('<ButtonRelease-3>', self.info_profile)
line_frame.grid (row = n+1, column = 0)
for i in self.range_n_fields:
field = Label (line_frame, width = ..., text = ...
field.grid (row = 0, column = i, sticky = W)
...

def color_selected (self, event):
print 'hit list.color_selected ()'

def color_selectable (self, event):
print 'hit list.color_selectable ()'

def pick_record (self, event): # Nver gets called
print 'hit list.pick_record ()'

def info_profile (self, event): # Never gets called
print 'hit list.info_profile ()'

I admit that I don't have an accurate conception of the inner workings.
It's something the traveler on the learning curve has to acquire a feel
for by trial and error. In this case the differing behavior should
logically have to do with the structural difference: I bind Labels that
contain Labels. If I click this nested assembly, who gets the event? The
contained widget, the containing widget or both?

Frederic


Incidentally, my source of inspiration for chaining event descriptors
was the New Mexico Tech Tkinter 8.4 reference, which says: ... In
general, an event sequence is a string containing one or more event
patterns. Each event pattern describes one thing that can happen. If
there is more than one event pattern in a sequence, the handler will be
called only when all the patterns happen in that same sequence ...

Again, predicting the precedence with overlaps is much like solving a
murder case: finding suspects and let the innocent ones off the hook.
The only reference I have found on that topic is in effbot.org's
tkinterbook, which says that precedence goes to the "closest match", but
doesn't explain how one evaluates "closeness".
 
R

rantingrickjohnson

If I copy your event descriptors into my program, the button-release
callback still fails. It works in your code, not in mine. Here is what
my code now looks like. It is somewhat more complicated than yours,
because I bind Frames holding each a line (line_frame) and each frame
contains a few Labels side by side. The idea is to achieve a table with
vertically aligning columns each line of which I can click-select. (Is
there a better way?)

for line_frame in ...:
line_frame.bind ('<Enter>', self.color_selected)
line_frame.bind ('<Leave>', self.color_selectable)
line_frame.bind ('<ButtonRelease-1>', self.pick_record)
line_frame.bind ('<ButtonRelease-3>', self.info_profile)
line_frame.grid (row = n+1, column = 0)
for i in self.range_n_fields:
field = Label (line_frame, width = ..., text = ...
field.grid (row = 0, column = i, sticky = W)
...

def color_selected (self, event):
print 'hit list.color_selected ()'

def color_selectable (self, event):
print 'hit list.color_selectable ()'

def pick_record (self, event): # Nver gets called
print 'hit list.pick_record ()'

def info_profile (self, event): # Never gets called
print 'hit list.info_profile ()'

Events only fire for the widget that currently has "focus". Frames, labels,and other widgets do not receive focus simply by hovering over them. You can set the focus manually by calling "w.focus_set()" -- where "w" is any Tkinter widget. I can't be sure because i don't have enough of your code to analyze, but I think you should bind (either globally or by class type) all "Enter" events to a callback that sets the focus of the current widget under the mouse. Experiment with this code and see if it is what you need:

## START CODE ##
from __future__ import print_function
import Tkinter as tk

def cb(event):
print(event.widget.winfo_class())
event.widget.focus_set()

root = tk.Tk()
root.geometry('200x200+20+20')
for x in range(10):
w = tk.Frame(root, width=20, height=20,bg='red')
w.grid(row=x, column=0, padx=5, pady=5)
w = tk.Frame(root, width=20, height=20,bg='green', highlightthickness=1)
w.grid(row=x, column=1, padx=5, pady=5)
w = tk.Button(root, text=str(x))
w.grid(row=x, column=2, padx=5, pady=5)
root.bind_all("<Enter>", cb)
root.mainloop()
## END CODE ##

You will see that the first column of frames are recieving focus but you have no visual cues of that focus (due to a default setting). In the second column you get the visual cue since i set "highlightthicness=1". The thirdcolumn is a button widget which by default has visual focus cues.

Is this the problem?

PS: Also check out the "w.bind_class()" method.
Incidentally, my source of inspiration for chaining event descriptors
was the New Mexico Tech Tkinter 8.4 reference,

That's an excellent reference BTW. Keep it under your pillow. Effbot also has a great tutorial.
 
R

rantingrickjohnson

If I copy your event descriptors into my program, the button-release
callback still fails. It works in your code, not in mine. Here is what
my code now looks like. It is somewhat more complicated than yours,
because I bind Frames holding each a line (line_frame) and each frame
contains a few Labels side by side. The idea is to achieve a table with
vertically aligning columns each line of which I can click-select. (Is
there a better way?)

for line_frame in ...:
line_frame.bind ('<Enter>', self.color_selected)
line_frame.bind ('<Leave>', self.color_selectable)
line_frame.bind ('<ButtonRelease-1>', self.pick_record)
line_frame.bind ('<ButtonRelease-3>', self.info_profile)
line_frame.grid (row = n+1, column = 0)
for i in self.range_n_fields:
field = Label (line_frame, width = ..., text = ...
field.grid (row = 0, column = i, sticky = W)
...

def color_selected (self, event):
print 'hit list.color_selected ()'

def color_selectable (self, event):
print 'hit list.color_selectable ()'

def pick_record (self, event): # Nver gets called
print 'hit list.pick_record ()'

def info_profile (self, event): # Never gets called
print 'hit list.info_profile ()'

Events only fire for the widget that currently has "focus". Frames, labels,and other widgets do not receive focus simply by hovering over them. You can set the focus manually by calling "w.focus_set()" -- where "w" is any Tkinter widget. I can't be sure because i don't have enough of your code to analyze, but I think you should bind (either globally or by class type) all "Enter" events to a callback that sets the focus of the current widget under the mouse. Experiment with this code and see if it is what you need:

## START CODE ##
from __future__ import print_function
import Tkinter as tk

def cb(event):
print(event.widget.winfo_class())
event.widget.focus_set()

root = tk.Tk()
root.geometry('200x200+20+20')
for x in range(10):
w = tk.Frame(root, width=20, height=20,bg='red')
w.grid(row=x, column=0, padx=5, pady=5)
w = tk.Frame(root, width=20, height=20,bg='green', highlightthickness=1)
w.grid(row=x, column=1, padx=5, pady=5)
w = tk.Button(root, text=str(x))
w.grid(row=x, column=2, padx=5, pady=5)
root.bind_all("<Enter>", cb)
root.mainloop()
## END CODE ##

You will see that the first column of frames are recieving focus but you have no visual cues of that focus (due to a default setting). In the second column you get the visual cue since i set "highlightthicness=1". The thirdcolumn is a button widget which by default has visual focus cues.

Is this the problem?

PS: Also check out the "w.bind_class()" method.
Incidentally, my source of inspiration for chaining event descriptors
was the New Mexico Tech Tkinter 8.4 reference,

That's an excellent reference BTW. Keep it under your pillow. Effbot also has a great tutorial.
 
F

Frederic Rentsch

Events only fire for the widget that currently has "focus". Frames, labels, and other widgets do not receive focus simply by hovering over them. You can set the focus manually by calling "w.focus_set()" -- where "w" is any Tkinter widget. I can't be sure because i don't have enough of your code to analyze, but I think you should bind (either globally or by class type) all "Enter" events to a callback that sets the focus of the current widget under the mouse. Experiment with this code and see if it is what you need:

## START CODE ##
from __future__ import print_function
import Tkinter as tk

def cb(event):
print(event.widget.winfo_class())
event.widget.focus_set()

root = tk.Tk()
root.geometry('200x200+20+20')
for x in range(10):
w = tk.Frame(root, width=20, height=20,bg='red')
w.grid(row=x, column=0, padx=5, pady=5)
w = tk.Frame(root, width=20, height=20,bg='green', highlightthickness=1)
w.grid(row=x, column=1, padx=5, pady=5)
w = tk.Button(root, text=str(x))
w.grid(row=x, column=2, padx=5, pady=5)
root.bind_all("<Enter>", cb)
root.mainloop()
## END CODE ##

You will see that the first column of frames are recieving focus but you have no visual cues of that focus (due to a default setting). In the second column you get the visual cue since i set "highlightthicness=1". The third column is a button widget which by default has visual focus cues.

Is this the problem?

Yes, I was unaware of focus control. I understand that it is set either
by a left mouse button click or the method focus_set ().
PS: Also check out the "w.bind_class()" method.


That's an excellent reference BTW. Keep it under your pillow. Effbot also has a great tutorial.


Thanks for this additional load af advice.

Googling I chanced on an excellent introduction "Thinking in Tkinter" by
Stephen Ferg.
(http://www.ferg.org/thinking_in_tkinter/all_programs.html). He sets out
identifying a common problem with tutorials: The problem is that the
authors of the books want to rush into telling me about all of the
widgets in the Tkinter toolbox, but never really pause to explain basic
concepts. They don't explain how to "think in Tkinter".
He then explains seventeen functionalities, one at a time, and
illustrates them with a little piece of code ready to run. Working
through the examples is a good way to acquire a basic understanding
without falling victim to "brain clutter". I shall go through the
examples very attentively and go on from there.

Thanks again

Frederic
 
R

rantingrickjohnson

[...]
Googling I chanced on an excellent introduction "Thinking in
Tkinter" [...] He sets out identifying a common problem with
tutorials: The problem is that the authors of the books want to rush
into telling me about all of the widgets in the Tkinter toolbox, but
never really pause to explain basic concepts. They don't explain how
to "think in Tkinter".

Well. I have not analyzed this tutorial myself but i can tell you one thingfor sure; most of the confusion regarding Tkinter comes NOT from improperly written tutorials NO, it is a direct result of the poorly designed Tkinter API itself!

=================================
Some of the main problems follow:
=================================

*## 1. Failure to constrain event sequences:*
Tkinter allows users to bind a virtual cornucopia of possible event sequences. Some say this is a good thing because it allows freedom, i say it is contributing to unreadable and unmaintainable code bases. There are literallythousands of possible combinations a user could bind. Sure, this "freedom"might make the user feel good, but i can guarantee his code will be a spaghetti mess!

It is my firm belief, that only two Key Events and four Mouse Events shouldbe allowed binding. Then, the callback should handle the dirty details -- which could include delegating details to one or many lower worker functions. In the real world you only need six possible events to process user input:

* KeyPress(keyName)
* KeyRelease(keyName)
* MousePress(x, y, bNum)
* MouseRelease(x, y, bNum)
* MouseMotion(x, y, bNum)
* MouseWheel(direction)

Checking the state of modifiers like Control, Shift, or Alt should be handled via an event object. Actually, all events should be a subclass of an EventHandler object.

*## Explicit parent/child relationships needs to be mandatory:*
Tkinter allows users to be lazy and omit the parent of a widget to save a few keystrokes on tiny little throw away scripts. In the real world, and even in small GUI applications, you would never want to do such a thing. I believe this methodology does irreversible damage to a new users brain. He fails to see the app->subwin->widget hierarchy from day one.

*## 2. Inconsistencies when constructing widgets:*
Most widgets expect a "parent" argument as the first argument, but not the case for Dialogs and other classes which allow you to pass "parent=blah" as a kw option. Without a parent, a dialog can not set up a proper transient relationship OR position itself properly over the correct window. Allowing this slothful developer behavior contributes to many frustrating user experiences.

*## 3. Inconsistencies between configuring widgets and configuring windows:*
Windows don't have a config method for things like: "title", "size", "etc".Instead they have methods like win.title("Title") or win.geometry("wxh+x+y"); which again breaks the consistency that is so important in any API! Notto mention the unessesaryily cryptic nature of a few of these methods.

*## 4. Failure to remove old tutorials from the web that are still teachingpeople to code Tkinter GUI's like it's 1991:*
This is a sad result of a dead community. And the few people who are remaining have become so partisan that nothing can get accomplish except, well, nothing.

Congratulations Guido, are you proud of what you have created? Or rather, or you proud of what this community had degenerated into? (BTW those are rhetorical questions.)

I am still hoping beyond hope that some new blood will enter this communitysoon and inject some hope for the future. Guido is an old man. He has no passion anymore. I long for some new blood with a passion for consistency. People with the intuition required to create intuitive APIs. But most of all.. People who have a "can do" community spirit.

A boy can dream.
 
R

rantingrickjohnson

[...]
Googling I chanced on an excellent introduction "Thinking in
Tkinter" [...] He sets out identifying a common problem with
tutorials: The problem is that the authors of the books want to rush
into telling me about all of the widgets in the Tkinter toolbox, but
never really pause to explain basic concepts. They don't explain how
to "think in Tkinter".

Well. I have not analyzed this tutorial myself but i can tell you one thingfor sure; most of the confusion regarding Tkinter comes NOT from improperly written tutorials NO, it is a direct result of the poorly designed Tkinter API itself!

=================================
Some of the main problems follow:
=================================

*## 1. Failure to constrain event sequences:*
Tkinter allows users to bind a virtual cornucopia of possible event sequences. Some say this is a good thing because it allows freedom, i say it is contributing to unreadable and unmaintainable code bases. There are literallythousands of possible combinations a user could bind. Sure, this "freedom"might make the user feel good, but i can guarantee his code will be a spaghetti mess!

It is my firm belief, that only two Key Events and four Mouse Events shouldbe allowed binding. Then, the callback should handle the dirty details -- which could include delegating details to one or many lower worker functions. In the real world you only need six possible events to process user input:

* KeyPress(keyName)
* KeyRelease(keyName)
* MousePress(x, y, bNum)
* MouseRelease(x, y, bNum)
* MouseMotion(x, y, bNum)
* MouseWheel(direction)

Checking the state of modifiers like Control, Shift, or Alt should be handled via an event object. Actually, all events should be a subclass of an EventHandler object.

*## Explicit parent/child relationships needs to be mandatory:*
Tkinter allows users to be lazy and omit the parent of a widget to save a few keystrokes on tiny little throw away scripts. In the real world, and even in small GUI applications, you would never want to do such a thing. I believe this methodology does irreversible damage to a new users brain. He fails to see the app->subwin->widget hierarchy from day one.

*## 2. Inconsistencies when constructing widgets:*
Most widgets expect a "parent" argument as the first argument, but not the case for Dialogs and other classes which allow you to pass "parent=blah" as a kw option. Without a parent, a dialog can not set up a proper transient relationship OR position itself properly over the correct window. Allowing this slothful developer behavior contributes to many frustrating user experiences.

*## 3. Inconsistencies between configuring widgets and configuring windows:*
Windows don't have a config method for things like: "title", "size", "etc".Instead they have methods like win.title("Title") or win.geometry("wxh+x+y"); which again breaks the consistency that is so important in any API! Notto mention the unessesaryily cryptic nature of a few of these methods.

*## 4. Failure to remove old tutorials from the web that are still teachingpeople to code Tkinter GUI's like it's 1991:*
This is a sad result of a dead community. And the few people who are remaining have become so partisan that nothing can get accomplish except, well, nothing.

Congratulations Guido, are you proud of what you have created? Or rather, or you proud of what this community had degenerated into? (BTW those are rhetorical questions.)

I am still hoping beyond hope that some new blood will enter this communitysoon and inject some hope for the future. Guido is an old man. He has no passion anymore. I long for some new blood with a passion for consistency. People with the intuition required to create intuitive APIs. But most of all.. People who have a "can do" community spirit.

A boy can dream.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
474,430
Messages
2,571,676
Members
48,796
Latest member
Greg L.

Latest Threads

Top