In tkinter, how to make custom child widget receive mouse scroll event? - python

I can't sort out how to make a custom widget receive mouse scroll events. If I bind to the root window, then notifications occur. And if I bind to a child of root window other than my widget (here a simple Listbox), the notifications also occur (evidenced by watching the list move when I move the wheel). What am I overlooking?
Example code where roll() never gets called:
#!/usr/bin/python3
from tkinter import *
from tkinter.ttk import *
class CustomWidget(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.width = 200
self.height = 200
self.canvas = Canvas(self, width=200, height=200)
self.canvas.config(background='red')
self.canvas.pack()
self.bind('<MouseWheel>', self.roll)
self.bind('<Button-4>', self.roll)
self.bind('<Button-5>', self.roll)
def roll(self, event):
print("detected mouse roll!");
if __name__ == "__main__":
root = Tk()
root.wm_title("TestRoot")
sb = Scrollbar(root, orient=VERTICAL)
lb = Listbox(root, yscrollcommand=sb.set)
sb.config(command=lb.yview)
cw = CustomWidget(root)
for char in list("abcdefghijklmnopqrstuvwxyz"):
lb.insert(END, char)
cw.pack()
lb.pack()
sb.pack()
root.update()
root.mainloop()

So in order for a frame to receive events, it needs to have focus. You can call frame.set_focus() on it, but as soon as you give another widget focus it won't work. To get around that we could bind <Button-1> to the frame and have that set the focus to the frame, but your canvas takes up the entire size of the frame, so You will need to bind the <Button-1> events to that instead.
Adding:
self.canvas.bind("<Button-1>", lambda _: self.focus_set())
after your other bindings in CustomWidget.__init__ will make your bindings work as long as the widget has focus, which it will when the user clicks it (similar how to the listbox works). If the canvas is never as large as it's frame, you may need to add another <Button-1> binding to the frame.

Related

Python Tkinter: Unbinding Mouse Scroll Wheel on ComboBox

I have a combobox within a scrollable canvas frame- when I open the combobox and attempt to scroll through the options, the combobox and the entire window both scroll together. It would be nice to pause canvas scrolling while the combobox is open, but unbinding the mousewheel scroll from the combobox would also work.
Here is the scrollable canvas code:
root = Tk()
width=800
height=1020
root.geometry(str(width)+"x"+str(height)+"+10+10")
main_frame = Frame(root,width=width,height=height)
main_frame.place(x=0,y=0)
canvas = Canvas(main_frame, width=width, height=height)
canvas.place(x=0,y=0)
scrolly = ttk.Scrollbar(main_frame, orient=VERTICAL, command=canvas.yview)
scrolly.place(x=width-15,y=0,height=height)
canvas.configure(yscrollcommand=scrolly.set)
canvas.bind('<Configure>', lambda e: canvas.configure(scrollregion = canvas.bbox("all")))
def _on_mouse_wheel(event):
canvas.yview_scroll(-1 * int((event.delta / 120)), "units")
canvas.bind_all("<MouseWheel>", _on_mouse_wheel)
w = Frame(canvas,width=width,height=height)
w.place(x=0,y=0)
canvas.create_window((0,0), window=w, anchor="nw")
w.configure(height=3000)
Here is the combobox initialization:
sel = Combobox(w, values=data)
sel.place(x=xval, y=yval)
I have tried unbinding the mousewheel for the combobox
sel.unbind_class("TCombobox", "<MouseWheel>") # windows
as well as rebinding it to an empty function
def dontscroll(event):
return 'break'
sel.bind('<MouseWheel>', dontscroll)
but neither method worked.
I also attempted both methods in a separate test file (complete code):
from tkinter import *
from tkinter import ttk
from tkinter.ttk import Combobox
root = Tk()
root.geometry(str(300)+"x"+str(300)+"+10+10")
def dontscroll(event):
return 'break'
sel = Combobox(root, values=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20])
sel.place(x=10, y=10)
sel.unbind_class("TCombobox", "<MouseWheel>") # on windows
sel.bind('<MouseWheel>', dontscroll)
This still didn't work. Any help is appreciated, thanks.
The reason is that you are binding all "<MouseWheel>" events with bind_all, you can simple just change it to bind, but then you will notice that it doesn't work with scrolls on the canvas, that is because you are now binding to canvas but the scroll is actually(pretty counter intuitive) happening to w which is a Frame, so just bind to that widget instead:
w.bind("<MouseWheel>", _on_mouse_wheel)
And you can also remove all the unbind and bind related to sel as it is no longer needed.
On an unrelated note, the trick I used to find out which widget triggered the "<MouseWheel>" event could be useful in the future:
def _on_mouse_wheel(event):
print(event.widget) # Print the widget that triggered the event
canvas.yview_scroll(-1 * int((event.delta / 120)), "units")
Edit: This seems to work with multiple widget
def _on_mouse_wheel(event):
if isinstance(event.widget, str): # String because it does not have an actual reference
if event.widget.endswith('.!combobox.popdown.f.l'): # If it is the listbox
return 'break'
canvas.yview_scroll(-1 * int((event.delta / 120)), "units") # Else scroll the canvas
w.event_generate('<Escape>') # Close combobox
root.bind_all("<MouseWheel>", _on_mouse_wheel)
I've come up with a solution which will hopefully be helpful for anyone running into a similar issue:
Basically, comboboxes are composed of two subcomponents: an entry and a listbox. When binding to a combobox, it appears to bind the command solely to the entry and not to the listbox, which caused the issue at hand. I don't know of way to access the subcomponents of a combobox instance and modify the listbox from there (drop an answer if you know how to do that), but I figured that I could bind the entire listbox class like so:
w.bind_class('Listbox', '<MouseWheel>', dontscroll)
And now, the canvas scrolls while the listboxes don't. But we can do better:
Instead of binding the listbox class to an antiscrolling method, I decided to bind it to two other methods that work together to pause frame scrolling while the cursor is hovering over the listbox.
def dontscroll(e):
return "dontscroll"
def on_enter(e):
w.bind_all("<MouseWheel>", dontscroll)
def on_leave(e):
w.bind_all("<MouseWheel>", _on_mouse_wheel)
w.event_generate('<Escape>')
w.bind_class('Listbox', '<Enter>',
on_enter)
w.bind_class('Listbox', '<Leave>',
on_leave)
Now, the canvas only scrolls when the mouse is not hovering over a listbox widget.
complete code here

How to disable the movement of the Tkinter Window without removing the title bar

I have been creating an application for taking the test. So, for that, I have to do two things.
First, disable the drag of the Tkinter window and don't let the user focus on other windows rather than my application window. This means I wanted to make my application such that, No other application can be used while my application is in use.
Try this:
import tkinter as tk
class FocusedWindow(tk.Tk):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Force it to be unminimisable
super().overrideredirect(True)
# Force it to always be on the top
super().attributes("-topmost", True)
# Even if the user unfoceses it, focus it
super().bind("<FocusOut>", lambda event: self.focus_force())
# Take over the whole screen
width = super().winfo_screenwidth()
height = super().winfo_screenheight()
super().geometry("%ix%i+0+0" % (width, height))
root = FocusedWindow()
# You can use it as if it is a normal `tk.Tk()`
button = tk.Button(root, text="Exit", command=root.destroy)
button.pack()
root.mainloop()
That removed the title bar but you can always create your own one by using tkinter.Labels and tkinter.Buttons. I tried making it work with the title bar but I can't refocus the window for some reason.
One way to do this is by the following, another could be to overwrite the .geometry() method of tkinter.
In the following code I simply had get the position by using winfo_rootx and winfo_rooty. After this you can force the window by calling the geometry method via binding the event every time the window is configured.
import tkinter as tk
def get_pos():
global x,y
x = root.winfo_rootx()
y = root.winfo_rooty()
def fix_pos():
root.bind('<Configure>', stay_at)
def stay_at(event):
root.geometry('+%s+%s' % (x,y))
root = tk.Tk()
button1 = tk.Button(root, text='get_pos', command=get_pos)
button2 = tk.Button(root, text='fix_pos', command=fix_pos)
button1.pack()
button2.pack()
root.mainloop()

Tkinter: Keep from moving focus to window

I am trying to make an on screen keyboard. It works for widgets in the window but when I press a button it moves focus from the window I am trying to type in to the window that has the buttons. How do I prevent python from moving?
from tkinter import *
from pynput.keyboard import Key, Controller
keyboard = Controller()
class App:
def __init__(self, master):
self.entry = Entry()
self.buttonOne = Button(text='1')
self.buttonTwo = Button(text='2')
self.buttonThree = Button(text='3')
self.buttonOne.bind("<Button-1>", lambda event, keyPressed='1': self.pressed(event, keyPressed))
self.buttonTwo.bind("<Button-1>", lambda event, keyPressed='2': self.pressed(event, keyPressed))
self.buttonThree.bind("<Button-1>", lambda event, keyPressed='3': self.pressed(event, keyPressed))
self.entry.grid(row=0, column=0, columnspan=3)
self.buttonOne.grid(row=1, column=0)
self.buttonTwo.grid(row=1, column=1)
self.buttonThree.grid(row=1, column=2)
def pressed(self, event, keyPressed):
keyboard.press(keyPressed)
keyboard.release(keyPressed)
root = Tk()
app = App(root)
root.mainloop()
I would suggest using withdraw() and deiconify(). This will make it so the window with the button is invisible once you call it on that window. Once you use deiconify() it reverses this and makes it visible again.
More information can be found here.
Question: Keep from moving focus to window
On X11, you can set the -type attribute:
self.wm_attributes("-type", 'dock')
'dock' will working for me not to grab the focus, but are not supported by all window managers.
Reference:
wm attributes
Communicate with window manager
A list of types
-type
Requests that the window should be interpreted by the window manager as being of the specified type(s). This may cause the window to be decorated in a different way or otherwise managed differently, though exactly what happens is entirely up to the window manager.
'dock'
indicates a dock/panel feature,
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.wm_attributes("-type", 'dock')
for n in range(64, 68):
btn = tk.Button(self, text=chr(n), takefocus=0)
btn.bind("<Button-1>", self.on_click)
btn.pack()
def on_click(self, event):
w = event.widget
print(w['text'])
if __name__ == '__main__':
App().mainloop()

Make Toplevel window pop up to the top when root window clicked

I have a UI root window where two other Toplevel windows get created on separate button clicks. These Toplevel windows are anchored to the root window and drag along the screen with the root window.
My problem is if i have another window open and my UI is hiding behind it, if i click on my UI from the taskbar or the little i can see on the screen, only the root Tk window pops up and the other Toplevel windows are still hiding behind the other window.
I tried toplevel.lift() and toplevel.wm_attributes("-topmost", 1) but neither give me what i want.
How can I tie the Toplevel windows so that if they are open and I click on the root window the Toplevel window also pops to the top?
Here is a simple example that will open 2 windows and disable everything on the root window while also binding any interaction with that root window to lift all the top windows above it.
I have also bound the top level close event to first remove the root binding and then destroy the top levels then re-enable all the widgets in the root window. this should serve to be an antiquity example of what you are trying to do.
Let me know if you have any questions.
import tkinter as tk
class ExampleApp(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
self.master.geometry("400x150")
self.main_frame = tk.Frame(self.master)
self.main_frame.pack(expand=tk.YES, fill=tk.BOTH)
self.master.protocol('<WM_LBUTTONDBLCLK>', self.motion)
tk.Label(self.main_frame, text = "This is the main window").pack()
tk.Button(self.main_frame, text = "Open 2 top level windows!", command = self.open_windows).pack()
def motion(self, event):
x, y = event.x, event.y
print('{}, {}'.format(x, y))
def open_windows(self):
self.top1 = tk.Toplevel(self.master)
self.top2 = tk.Toplevel(self.master)
self.top1.geometry("100x100")
self.top2.geometry("100x100")
# ties the window close event to our customer close method for toplevel
self.top1.protocol("WM_DELETE_WINDOW", self.close_toplevels)
self.top2.protocol("WM_DELETE_WINDOW", self.close_toplevels)
self.master.bind("<Unmap>", self.icon_all)
self.top1.bind("<Unmap>", self.icon_all)
self.top2.bind("<Unmap>", self.icon_all)
self.master.bind("<Map>", self.de_icon_all)
self.top1.bind("<Map>", self.de_icon_all)
self.top2.bind("<Map>", self.de_icon_all)
for child in self.main_frame.winfo_children():
child.configure(state='disable')
tk.Label(self.top1, text ="Topwindow 1").pack()
tk.Label(self.top2, text ="Topwindow 2").pack()
# sets the top windows to their initial locations
self.lock_top_to_root()
#keeps the top windows in the specified locations compared to root window
self.master.bind("<Configure>", self.lock_top_to_root)
def withdraw_tops(self, event=None):
self.top1.withdraw()
self.top2.withdraw()
def de_icon_tops(self, event=None):
self.top1.deiconify()
self.top2.deiconify()
def icon_all(self, event=None):
self.withdraw_tops()
self.master.iconify()
def de_icon_all(self, event=None):
self.de_icon_tops()
self.master.deiconify()
self.lock_top_to_root()
def lock_top_to_root(self, event=None):
self.top1.lift() # lift both toplevel windows about root
self.top2.lift()
# places each top level at each side
# this is not set up to compensate for the root being resized but can be if you need it to.
self.top1.geometry('+{}+{}'.format(self.master.winfo_x()+10, self.master.winfo_y()+30))
self.top2.geometry('+{}+{}'.format(self.master.winfo_x()+275, self.master.winfo_y()+30))
def close_toplevels(self):
# customer close method to reset everything
self.master.unbind('<Configure>')
self.master.unbind("<Unmap>")
self.master.unbind("<Map>")
self.top1.destroy()
self.top2.destroy()
for child in self.main_frame.winfo_children():
child.configure(state='active')
root = tk.Tk()
my_example = ExampleApp(root)
root.mainloop()

Tkinter active state

What I'm wanting to do is detect if the mouse has moved over an object on the screen and if it has I want to display the data about the object in a separate frame. No mouse click allowed, only mouse movement, just like activewidth. I don't see and reference anywhere showing a built in feature that will allow what I'm trying to do. Am I wrong and if so what have I not seen yet. Can't research further that which I don't know about.
Edit: I did use the bad word Object when I was referring to drawn shapes on a canvas. These are lines, aka a mapping program, placed on a canvas. When I scroll over a line/road I want it pop up the name of the road on a separate frame. When I scroll over the symbol for a business I want it to bring up the name of the business and other pertinent information in the separate frame. Hence why I said activewidth is what I'm basically trying to mimic as its capturing the mouse location and then automatically recognizing something is on the screen underneath where the mouse pointer is located. If I could capture that same pointer reference...Right now as I'm editting this I'm thinking I would have to create each line with its own separate reference name...
z[0] = self.canvas.create_line()
z[1] = self.canvas.create_line()
z[2] = self.canvas.create_line()
etc. Am I wrong on this thought? Is there an easier way of doing this?
You can bind to the <Enter> and <Leave> events, which will fire whenever the mouse enters or leaves a widget.
Example:
import tkinter as tk
root = tk.Tk()
l1 = tk.Label(root, text="Hover over me",
width=40, bd=2, relief="groove",
background="lightblue")
l2 = tk.Label(root)
l1.pack(side="top", fill="x", padx=10, pady=10)
l2.pack(side="top", fill="both", expand=True)
def handle_enter(event):
event.widget.configure(background="pink")
l2.configure(text="you entered the widget")
def handle_leave(event):
event.widget.configure(background="lightblue")
l2.configure(text="")
l1.bind("<Enter>", handle_enter)
l1.bind("<Leave>", handle_leave)
root.mainloop()
If the object is a tkinter widget, bind to "<Motion>". It will only trigger when the mouse moves over the object that you bound to (or it's children). The event object will contain information about which object you were over and even the object itself.
import Tkinter as tk
class GUI(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
lbl = tk.Label(text='Label 1')
lbl.pack()
lbl.bind('<Motion>', self.motion_detected)
lbl = tk.Label(text='Thing B')
lbl.pack()
lbl.bind('<Motion>', self.motion_detected)
def motion_detected(self, event):
print('motion detected in {} at {},{}'.format(event.widget['text'], event.x, event.y))
def main():
root = tk.Tk()
root.geometry('200x200')
win = GUI(root)
win.pack()
root.mainloop()
if __name__ == '__main__':
main()
with the CURRENT tag, you can match the canvas element under the mouse
CURRENT (or “current”) matches the item under the mouse pointer, if
any. This can be used inside mouse event bindings to refer to the item
that triggered the callback.
source
so,
first you pass your information through the tags option
self.canvas.create_line(.... tags="road1")
then you bind to the <Motion> event and inside the handler you get the tags of the current ellement
ellement_id = canvas.find_withtag(CURRENT)
ellement_tags = canvas.gettags(ellement_id)

Categories

Resources