How to trigger tkinter's "<Enter>" event with mouse down? - python

In tkinter with Python 3.7, the default behavior for an event binding is that an "<Enter>" event will not be triggered after the mouse has been clicked down before it has been released. I was intending to implement a scrollable table such that it detects "<Button-1>" (Mouse left-click down) and "<ButtonRelease-1>" (Mouse left-click up) events as well as having each table-row's widgets "<Enter>" event bound to detect when the mouse pointer enters a different table row. In this way I could scroll my table by clicking an row and dragging through a table. My assumption was that "<Enter>" events would be triggered even while the mouse button is held down, which was incorrect. So, my entire scrolling implementation hit a brick wall. I need these events to be triggered while the mouse is down or it just won't work. I'm doing something like:
from tkinter import *
class App:
def __init__(self):
self.root = Tk()
# The name kwarg is used to infer the index of the row in the event handlers.
self.labels = [Label(text=f"Label #{i}", name=f"row-{i}") for i in range(5)]
for row, label in enumerate(self.labels):
label.bind("<Button-1>", self.mouse_down)
label.bind("<ButtonRelease-1>", self.mouse_up)
label.bind("<Enter>", self.mouse_enter)
label.grid(row=row, column=0)
mainloop()
def mouse_up(self, event):
idx = self.index_from_event(event)
# Do some with the row with the passed index
def mouse_down(self, event):
idx = self.index_from_event(event)
# Do some with the row with the passed index
def mouse_enter(self, event):
# I would like for this to be triggered even when the mouse is pressed down.
# However, by default tkinter doesn't allow this.
pass
def index_from_event(self, event):
# Get the index of the row from the labels name string.
return str(event.widget).split('-')[-1]
Any way to enable mouse enter events while the mouse button 1 is held down in tkinter? All the docs on effbot (http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm) say about the enter event is:
<Enter>
The mouse pointer entered the widget (this event doesn’t mean that the user pressed the Enter key!).

No, there is no direct way to bind to the enter and leave events when the button is down, except for the widget which first gets the click. It is fairly easy to add that ability, however,
You can bind <B1-Motion> to all widgets which will call a function whenever the mouse moves. You can then use the winfo_containing method to figure out which widget is under the cursor. With that information you could generate a virtual event which you can bind to (or you can skip the virtual events and add your code in the handler for the motion event).
Here's a bit of a contrived example. When you click and move the mouse, show_info will be called. It keeps track of the current widget and compares it to the widget under the cursor. If it's different, it generates a <<B1-Leave>> to the previous widget and a <<B1-Enter>> on the new widget. Those bindings will display "Hello, cursor!" when the cursor is over the label.
import tkinter as tk
current_widget = None
def show_info(event):
global current_widget
widget = event.widget.winfo_containing(event.x_root, event.y_root)
if current_widget != widget:
if current_widget:
current_widget.event_generate("<<B1-Leave>>")
current_widget = widget
current_widget.event_generate("<<B1-Enter>>")
def on_enter(event):
event.widget.configure(text="Hello, cursor!")
def on_leave(event):
event.widget.configure(text="")
root = tk.Tk()
label = tk.Label(root, bd=1, relief="raised")
l1 = tk.Label(root, text="", width=20, bd=1, relief="raised")
l2 = tk.Label(root, text="", width=20, bd=1, relief="raised")
label.pack(side="top", fill="x")
l1.pack(fill="both", expand=True, padx=20, pady=20)
l2.pack(fill="both", expand=True, padx=20, pady=20)
root.bind_all("<B1-Motion>", show_info)
l1.bind("<<B1-Enter>>", on_enter)
l1.bind("<<B1-Leave>>", on_leave)
l2.bind("<<B1-Enter>>", on_enter)
l2.bind("<<B1-Leave>>", on_leave)
tk.mainloop()

Related

Tkinter Enter and Motion bindings

Is it possible to track what widgets I enter with my mouse into while it's pressed?
I want to create a chain-like effect that the background of the label\button change while click and drag the mouse and moving from widget to widget.
Thanks :)
You can bind to the <B1-Motion> event, and then use winfo_containing to get the widget under the cursor.
Here's a simple example:
import tkinter as tk
root = tk.Tk()
current_label = tk.Label(root, text="", anchor="w", width=100)
current_label.pack(side="top", fill="x")
def show_widget(event):
widget = event.widget.winfo_containing(event.x_root, event.y_root)
current_label.configure(text=f"widget: {str(widget)}")
for x in range(10):
name = f"Label #{x+1}"
label = tk.Label(root, text=name)
label.pack(padx=10, pady=10)
label.bind("<B1-Motion>", show_widget)
root.mainloop()
I've done stuff like tracking entry/exit for a specific widget:
widget.bind("<Enter>", enter_func)
widget.bind("<Leave>", exit_func)
you may be able to do something cute with that

Tkinter - On which widget is mouse pointer currently?

I want to ask u guys if there is a way of getting name or some id of widget which is mouse pointer currently on. Is there a way of doing this? Thanks for any response.
Normally you get this information from a binding. However, if you want to poll the system at any point to find out which widget is under the mouse, you can use winfo_pointerxy to get the coordinates of the mouse, and then pass those to winfo_containing to get the widget under those coordinates.
Here's an example program that continuously prints out the widget under the mouse:
import tkinter as tk
def print_widget_under_mouse(root):
x,y = root.winfo_pointerxy()
widget = root.winfo_containing(x,y)
print("widget:", widget)
root.after(1000, print_widget_under_mouse, root)
root = tk.Tk()
label_foo = tk.Label(root, text="Foo", name="label_foo")
label_bar = tk.Label(root, text="Bar", name="label_bar")
button = tk.Button(root, text="Button", name="button")
button.pack(side="bottom")
label_foo.pack(fill="both", expand=True)
label_bar.pack(fill="both", expand=True)
print_widget_under_mouse(root)
root.mainloop()

CTRL + a select all in entry widget tkinter python

How I can select all text like block using click+drug left mouse in Entry widget tkinter python.
e1 = tk.Entry(bop, width = 50, font = "Helvetica 13")
e1.grid(row=1,column=1, padx=15, pady=15)
e1.bind_class("Entry","<Control-a>", select_all(e1))
here is the function of select_all():
def select_all(e):
a = e.select_range(0,tk.END)
There was so many similar examples on SO
import tkinter as tk
def callback(event):
print('e.get():', e.get())
# or more universal
print('event.widget.get():', event.widget.get())
# select text after 50ms
root.after(50, select_all, event.widget)
def select_all(widget):
# select text
widget.select_range(0, 'end')
# move cursor to the end
widget.icursor('end')
root = tk.Tk()
e = tk.Entry(root)
e.pack()
e.bind('<Control-a>', callback)
root.mainloop()
bind expects filename without () and arguments (callback). But also bind executes this function always with one argument event which gives access to entry which executed this function event.widget so you can use it with many different entries. And finally Entry has .get() to get all text.
EDIT:
Because after releasing keys <Control-a> selection is removed so I use after() to execute selection after 50ms. It selects all text (but it moves cursor to the beginning) and moves cursor to the end. (see code above)
EDIT:
Before I couldn't find correct combination with Release but it has to be <Control-KeyRelease-a> and now it doesn't need after()
import tkinter as tk
def callback(event):
print('e.get():', e.get())
# or more universal
print('event.widget.get():', event.widget.get())
# select text
event.widget.select_range(0, 'end')
# move cursor to the end
event.widget.icursor('end')
root = tk.Tk()
e = tk.Entry(root)
e.pack()
e.bind('<Control-KeyRelease-a>', callback)
root.mainloop()
furas' answer is great but still does not work as a perfect analogue of windows Ctrl+A behavior. The event only fires after releasing the 'a' key, but the event should fire on the 'a' key press.
Taking from Python tkinter: stopping event propagation in text widgets tags , stopping the event propagation is what we need. Returning 'break' stops whatever following event is breaking the ctrl+a behavior, and also allows us to shorten our bind to '<Control-A>'
def callback(event):
# select text
event.widget.select_range(0, 'end')
# move cursor to the end
event.widget.icursor('end')
#stop propagation
return 'break'
root = tk.Tk()
e = tk.Entry(root)
e.pack()
e.bind('<Control-a>', callback)
root.mainloop()

How to make a Button using the tkinter Canvas widget?

I want to obtain a button out of a Canvas. I've tried to pack the canvas in the button widget, but that didn't work. Googling a bit, I've found (here: How do you create a Button on a tkinter Canvas?) that the Canvas method create_window might help. But there should be something wrong in the way I'm using it.
import Tkinter
DIM = 100
root = Tkinter.Tk()
frame = Tkinter.Frame(root)
button = Tkinter.Button(None, width=DIM, height=DIM, command=root.quit)
circle = Tkinter.Canvas(frame, width=DIM, height=DIM)
circle.create_oval(5, 5, DIM-5, DIM-5, fill="red")
circle.create_window(0, 0, window=button)
frame.grid()
circle.grid(row=1, column=1)
root.mainloop()
If I erase the create_window line, I can se my painting but I can't (obviously) click on it. But in this way, the button widget cover my circle and shows a sad empty button.
Basically, I want to create a button with a red circle painted inside.
Tkinter doesn't allow you to directly draw on widgets other than the canvas, and canvas drawings will always be below embedded widgets.
The simple solution is to create the effect of a button using just the canvas. There's really nothing special about doing this: just create a canvas, then add bindings for ButtonPress and ButtonRelease to simulate a button being pressed.
Here's a rough idea:
class CustomButton(tk.Canvas):
def __init__(self, parent, width, height, color, command=None):
tk.Canvas.__init__(self, parent, borderwidth=1,
relief="raised", highlightthickness=0)
self.command = command
padding = 4
id = self.create_oval((padding,padding,
width+padding, height+padding), outline=color, fill=color)
(x0,y0,x1,y1) = self.bbox("all")
width = (x1-x0) + padding
height = (y1-y0) + padding
self.configure(width=width, height=height)
self.bind("<ButtonPress-1>", self._on_press)
self.bind("<ButtonRelease-1>", self._on_release)
def _on_press(self, event):
self.configure(relief="sunken")
def _on_release(self, event):
self.configure(relief="raised")
if self.command is not None:
self.command()
To complete the illusion you'll want to set a binding on <Enter> and <Leave> (to simulate the active state), and also make sure that the cursor is over the button on a button release -- notice how real buttons don't do anything if you move the mouse away before releasing.
What you can do is bind the canvas to the mouse:
import Tkinter
DIM = 100
root = Tkinter.Tk()
frame = Tkinter.Frame(root)
circle = Tkinter.Canvas(frame)
circle.create_oval(5, 5, DIM-5, DIM-5, fill="red")
frame.grid()
circle.grid(row=1, column=1)
##################################
def click(event):
root.quit()
circle.bind("<Button-1>", click)
##################################
root.mainloop()
Now, if a user clicks inside the canvas, function click will be called (essentially, the canvas has now been made a button).
Notice though that function click will be called if a user clicks anywhere in the canvas. If you want to make it so that click is only called when a user clicks in the circle, you can use event.x and event.y to get a hold of the x and y coordinates of the click. Once you have those, you can run a calculation to determine whether those coordinates are within the circle. Here is a reference on that.

Buttons have their own coordinate system according to the "grid_location" method?

I'm trying to use the grid_location method, from the Grid Geometry Manager, in Tkinter, but it seems that I'm doing something wrong.
Here's my code:
from tkinter import *
root = Tk()
b=Button(root, text="00")
b.grid(row=0, column=0)
b2=Button(root, text="11")
b2.grid(row=1, column=1)
b3=Button(root, text="22")
b3.grid(row=2, column=2)
b4=Button(root, text="33")
b4.grid(row=3, column=3)
b5=Button(root, text="44")
b5.grid(row=4, column=4)
def mouse(event):
print(event.x, event.y)
print(root.grid_location(event.x, event.y))
root.bind("<Button-1>", mouse)
root.mainloop()
When I click outside the Buttons, it works, but when I click inside of any Button, it seems that each button has its own coordinate system. So, each button is on the (0, 0) cell, despite that in the code, they are on a regular grid.
You are correct that each button "has it's own coordinate system". More accurately, though, the event.x and event.y values are relative to the widget associated with the event rather than the widget's parent or the root window.
If you really do need the row and column that the widget is in you can use grid_info to get the row and column of the widget associated with the event. For example:
def mouse(event):
grid_info = event.widget.grid_info()
print("row:", grid_info["row"], "column:", grid_info["column"])

Categories

Resources