4

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!).
ICW
  • 4,875
  • 5
  • 27
  • 33
  • Could you use `` and use the (x, y) coordinates the Event contains? – PM 2Ring Jul 16 '18 at 21:12
  • This question has been asked before [here](https://stackoverflow.com/questions/47944322/how-to-make-tkinter-enter-event-work-when-button-1-is-pressed) and [here](https://stackoverflow.com/questions/48029920/tkinter-widget-does-not-detect-entry-of-mouse-when-the-mouse-is-held-down-befo) and the concenses seems to be it can't be done. Holding down the mouse blocks events from other widgets. You will have to use the events from the initial click widget to determine what widget the mouse is over. – Novel Jul 16 '18 at 21:31
  • I don't understand what you are trying to do. Where does the "scrollable" part comes in? – Novel Jul 16 '18 at 21:32
  • You could take some clues from [drag and drop](https://stackoverflow.com/questions/44887576/how-make-drag-and-drop-interface) code. https://github.com/python/cpython/blob/master/Lib/tkinter/dnd.py#L151 – Novel Jul 16 '18 at 21:37
  • @Novel The scrolling logic isn't implemented in my example code, since it's irrelevant to the question. – ICW Jul 17 '18 at 00:08
  • @PM2Ring I think this is a possible solution. I am going to try it. – ICW Jul 17 '18 at 00:09

1 Answers1

6

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()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Spot on. I like the solution as it does exactly what I'm asking, although it's unfortunately a workaround. Is there a chance the constant calls to the motion event slow down my application on any level? It is not currently CPU intensive and I'd like it to remain that way. Anyways, for my case, I think I will be able to get away with simply changing my bind to the event and check for the containing widget. Since I've named my table widgets in a certain way, I can simply use the motion event and check if the name is a table row widget, if so do the scrolling. Thanks @BryanOakley – ICW Jul 17 '18 at 00:12
  • 2
    @YungGun it depends on how much work you do in the callback as to whether or not it will slow down the program. The best thing to do is turn the tracking on with the button click, and back off on a button release. – Bryan Oakley Jul 17 '18 at 00:56
  • Ah that's a good idea and exactly what I'll do. Are you aware whether binding or unbinding keys an expensive operation? Since I'd be binding and unbinding on every single click down/up and my application is meant to be clicked a lot that could be problematic. Are you saying it's cheaper to bind/unbind all keys every click than running the motion event bound function on every motion event? The motion event in question would be performing a single boolean check that returns false in the case of the mouse not currently being clicked down. – ICW Jul 17 '18 at 02:07
  • 1
    @YungGun: not expensive at all. I'm not sure what you mean by "bind/unbind all keys". This question is about binding the mouse, and there's just a couple of bindings. The easiest way to learn about performance is to do it and measure it – Bryan Oakley Jul 17 '18 at 02:11
  • sorry that wasn't the best way to say that. In my case I'm binding about 30 widgets to the mouse 1 motion event, so if I was disabling input whenever the mouse is up I'd have to unbind each widgets mouse 1 motion event right? – ICW Jul 17 '18 at 20:22
  • @YungGun: you don't need to bind 30 widgets to the motion event. Just do one `bind_all`. It's relatively harmless since it really only does something when the user is moving the mouse when the button is down. – Bryan Oakley Jul 17 '18 at 21:10
  • Well I only want it to happen when the mouse is over the table, and in its current state that is exactly what's happening. It's doing closer to what I want in it's current state, but it is unideal in that it takes many bindings. My classes are currently set up so that's its really easy to bind all the widgets I want too within the table, so for now I think I'll keep it as is. It's doing what I want to and there's no potential for any significant slow down AFAIK, so I'd really just be refactoring. – ICW Jul 18 '18 at 01:40
  • @BryanOakley - According to the quick blurb I read about bindings and events, it looks like there should be a valid event sequence named '', since 'B1' is a valid Modifier, and 'Enter' is a valid Action. But it sounds like (according to what you say and my testing) that event sequence will never fire. Is this correct? – GaryMBloom Jan 22 '20 at 19:33
  • Update: Actually, as Bryan said, it looks like it fires for the widget that got the button push, but only that one widget... – GaryMBloom Jan 22 '20 at 20:33