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
Related
Is there a simple way to get the right click menu to open on texty only and not the whole window?
This was a quick mashup to illustrate my question. Inheriting from texty on line 25 was a shot in the dark, which didnt work, but it's close to a simple solution, like I am seeking. I was hoping to avoid programming a whole class each time I want to set a right click menu.
from tkinter import *
from tkinter import ttk
def menu_popup(event):
try:
popup.tk_popup(event.x_root, event.y_root, 0)
finally:
popup.grab_release()
win = Tk()
win.geometry("600x550+125+125")
e = Entry(win, width=50, font=('Helvetica', 11))
e.pack()
e.insert(0, "Some text....")
label = Label(win, text="Right-click to see a menu", font= ('Helvetica 18'))
label.pack(pady= 40)
texty=Text(win, height=10)
texty.pack()
popup = Menu(texty, tearoff=0)
popup.add_command(label="New")
popup.add_separator()
popup.add_command(label="Open")
popup.add_separator()
popup.add_command(label="Close")
win.bind("<Button-3>", menu_popup)
button = ttk.Button(win, text="Quit", command=win.destroy)
button.pack()
mainloop()
The widget on which the callback should be executed for the respective event is determined by the widget you call bind on(and the level of bind too*). So if you want the event to be identified within texty, then apply binding to it.
texty.bind("<Button-3>", menu_popup)
* There is bind_all which executes no matter which widget has focus or is called upon. Read 54.1. Levels of binding for more info.
I hope you can help me with a problem I have in python 2.7. I couldn't find a solution online, but I'm honestly unsure what keywords to search for, so I'm sorry if this is redundant.
The code below is an example of my problem.
import Tkinter as tk
root = tk.Tk()
#Widgets.
btn1 = tk.Label(root, text="btn1", bg="gray80")
btn2 = tk.Label(root, text="btn2", bg="gray80")
btn1.pack(side=tk.TOP, fill=tk.X)
btn2.pack(side=tk.TOP, fill=tk.X)
#Widget events.
def onClick1(event):
print "Clicked button 1."
def onRelease1(event):
print "Released button 1."
def onClick2(event):
print "Clicked button 2."
def onRelease2(event):
print "Released button 2."
#Bindings.
btn1.bind("<Button-1>", onClick1, add="+")
btn1.bind("<ButtonRelease-1>", onRelease1, add="+")
btn2.bind("<Button-1>", onClick2, add="+")
btn2.bind("<ButtonRelease-1>", onRelease2, add="+")
root.mainloop()
Whenever I click one button (technically a label) and hold it, the onClick event for it fires, but if I drag the mouse over to the other and release it, I get the same onRelease as the one I clicked, and not the one for the label I have my mouse over currently. This has held me back some time now, and I'd hate to scrap the whole feature in my program I need this for, so any help would be greatly appreciated.
The release event always fires on the same widget that got the press event. Within your handler you can ask tkinter what widget is under the cursor.
Example:
import Tkinter as tk
root = tk.Tk()
btn1 = tk.Label(root, text="btn1", bg="gray80")
btn2 = tk.Label(root, text="btn2", bg="gray80")
btn1.pack(side=tk.TOP, fill=tk.X)
btn2.pack(side=tk.TOP, fill=tk.X)
def onRelease(event):
x,y = event.widget.winfo_pointerxy()
widget = event.widget.winfo_containing(x, y)
print("widget:", widget.cget("text"))
btn1.bind("<ButtonRelease-1>", onRelease)
btn2.bind("<ButtonRelease-1>", onRelease)
root.mainloop()
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)
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.
I am trying to make a GUI in Tkinter and am wondering how to refresh a window, namely if I fill in a rectangle, I want the GUI to delete it a specified time later. How would I go about doing this? Documentation on Tkinter seems to be thin...
Each Tkinter widget has a after method, which you can use to call your rectangle delete function e.g. in the example below first I change a msg using after, and then destruct the window using after
from Tkinter import *
def changeMsg():
label.configure(text="I will self destruct in 2 secs")
label.after(2000, root.destroy)
root = Tk()
mainContainer = Frame(root)
label = Label(mainContainer, text="")
label.configure(text="msg will change in 3 secs")
label.pack(side=LEFT, ipadx=5, ipady=5)
mainContainer.pack()
label.after(3000, changeMsg)
root.title("Timed event")
root.mainloop()