Blocking the default listbox scrolling in Python running on Windows - python-2.7

My environment is Python 2.7, running on Windows 7.
When I create a Tkinter window with two scrolling listboxes, the default behavior is that Windows will direct the mousewheel action to whichever of them has the focus. Each increment of the mousewheel moves 4 lines up/down. When the focus leaves the listbox, it stops responding to the mousewheel.
Ultimately I'm looking to completely replace this behavior with my own bindings. I've written the code for that and it yields the new custom response I want, but the problem is that the default behavior still occurs as well, based on the listbox that has the focus. I can't seem to block it no matter what I try.
Here's a simplified code sample to illustrate. I'm expecting that my binding to the <FocusIn> event (with its return 'break' line) would prevent the default mousewheel response, but still occurs. I tried intercepting the <<ListboxSelect>> event as well, but that doesn't block it either. It doesn't even help when I do a bind_all on the <Mousewheel> event; somehow the default response still gets triggered.
import Tkinter as tk
#Root window
root = tk.Tk()
#Widgets
ctrl1 = tk.StringVar()
vsb1 = tk.Scrollbar(root,
orient=tk.VERTICAL)
lst1 = tk.Listbox(root,
width=20,height=10,
listvariable=ctrl1,
activestyle='dotbox',
yscrollcommand=vsb1.set)
vsb1.config(command=lst1.yview)
ctrl2 = tk.StringVar()
vsb2 = tk.Scrollbar(root,
orient=tk.VERTICAL)
lst2 = tk.Listbox(root,
width=20,height=10,
listvariable=ctrl2,
activestyle='dotbox',
yscrollcommand=vsb2.set)
vsb2.config(command=lst2.yview)
#Geometry
lst1.grid(row=0,column=0,sticky=tk.NSEW,padx=(5,0),pady=5)
vsb1.grid(row=0,column=1,sticky=tk.NS,padx=(0,5),pady=5)
lst2.grid(row=0,column=2,sticky=tk.NSEW,padx=(15,0),pady=5)
vsb2.grid(row=0,column=3,sticky=tk.NS,padx=(0,5),pady=5)
#Bindings
def focusIn1(*args):
print 'Focus in 1'
return 'break'
def focusIn2(*args):
print 'Focus in 2'
return 'break'
lst1.bind('<FocusIn>',focusIn1)
lst2.bind('<FocusIn>',focusIn2)
#Dummy listbox content
ctrl1.set('Entry-index-00 Entry-index-01 Entry-index-02 Entry-index-03 '+ \
'Entry-index-04 Entry-index-05 Entry-index-06 Entry-index-07 '+ \
'Entry-index-08 Entry-index-09 Entry-index-10 Entry-index-11 '+ \
'Entry-index-12 Entry-index-13 Entry-index-14 Entry-index-15 '+ \
'Entry-index-16 Entry-index-17 Entry-index-18 Entry-index-19 <end-of-queue>')
ctrl2.set('Entry-index-00 Entry-index-01 Entry-index-02 Entry-index-03 '+ \
'Entry-index-04 Entry-index-05 Entry-index-06 Entry-index-07 '+ \
'Entry-index-08 Entry-index-09 Entry-index-10 Entry-index-11 '+ \
'Entry-index-12 Entry-index-13 Entry-index-14 Entry-index-15 '+ \
'Entry-index-16 Entry-index-17 Entry-index-18 Entry-index-19 <end-of-queue>')
#Begin app
tk.mainloop()
Is there some other event I need to intercept instead? Or is there maybe some different approach I need to use to block the default mousewheel response?
(Just in case it matters... the custom behavior I'm aiming for is to have single-line scrolling of one listbox or the other independently, based solely on whether the mouse pointer is within that listbox. Because of the described issue, if (say) listbox #1 has the focus and the pointer is over listbox #2, they gang-scroll in response to the mousewheel, with #2 scrolling one line at a time while #1 scrolls 4 lines at a time. If I switch and point to #1, it scrolls by itself but it still scrolls 4 lines at a time instead of 1.)
EDIT: Binding that references the <MouseWheel> event, but still fails to block the default scrolling behavior. I put this in place of the <FocusIn> events.
#Bindings
def scrollBlock(*args):
print "Blocking scrolling (except it won't)"
return "break"
root.bind_all('<MouseWheel>',scrollBlock)

Related

PyQt5 QComboBox list items changing postion

I am facing some issue with the display style of Qcombobox items. Currently am hardcoding the data to be shown in the combobox.
here is the code :
self.Dummy = QComboBox(self)
self.Dummy.setGeometry(200,600, 350, 50)
self.Dummy.setStyleSheet("QComboBox {background-color: white;border-style: outset;" border-width: 2px;border-radius: 5px;border-color: #448aff; font: 12px; min-width: 10em; padding: 3px;}")
self.Dummy.addItems(["-Select-", "2", "3","4","5","6","7","8","9","0","11",])
The issue is that the dropdown "list" postion keeps changing after each selection. Here is the image of the issue am facing.
Below is my combobox
The list contains items <-Select->,2,3,4,5,6,7,8,9,0,11 , where <-Select-> will be the first element shown.
Now when I click the box, the box list "down" the elements and suppose I selected '2'. Then, if I try to select another item, the list will be dropped in a "downwards" direction. see below
Now, say if selected the last element from the items, '11'. Now if I try to select a new item by clicking on the box, the list will be popped "up" instead of down. see below
What should be done to fix it ? I don't think its an issue with stylesheet, without it also, this issue is happening. The reason I need this to be fixed is that when the list is popping up, its covering the label above it
What you see is a behavior that is OS and style dependent.
To avoid it, the best way is to subclass QComboBox and overwrite showPopup(), then we call the base class implementation (which is responsible of showing, resizing and positioning the popup view) and move it if necessary.
class Combo(QtWidgets.QComboBox):
def showPopup(self):
super().showPopup()
# find the widget that contains the list; note that this is *not* the view
# that QComboBox.view() returns, but what is used to show it.
popup = self.view().window()
rect = popup.geometry()
if not rect.contains(self.mapToGlobal(self.rect().center())):
# the popup is not over the combo, there's no need to move it
return
# move the popup at the bottom left of the combo
rect.moveTopLeft(self.mapToGlobal(self.rect().bottomLeft()))
# ensure that the popup is always inside the edges of the screen
# we use the center of the popup as a reference, since with multiple
# screens the combo might be between two screens, but that position
# could also be completely outside the screen, so the cursor position
# is used as a fallback to decide on what screen we'll show it
done = False
for i, pos in enumerate((rect.center(), QtGui.QCursor.pos())):
for screen in QtWidgets.QApplication.screens():
if pos in screen.geometry():
screen = screen.geometry()
if rect.x() < screen.x():
rect.moveLeft(screen.x())
elif rect.right() > screen.right():
rect.moveRight(screen.right())
if rect.y() < screen.y():
rect.moveTop(screen.y())
elif rect.bottom() > screen.bottom():
# if the popup goes below the screen, move its bottom
# *over* the combo, so that the its current selected
# item will always be visible
rect.moveBottom(self.mapToGlobal(QtCore.QPoint()).y())
done = True
break
if done:
break
popup.move(rect.topLeft())
This can also be done without subclassing (for example if you have many combos, you created the UI from Designer and don't want to use promoted widgets), but you'll have to remember to change all referencies to the combo.
class MyWindow(QtWidgets.QWidget):
def __init__(self):
# ...
self.combo = QtWidgets.QComboBox()
self.combo.showPopup = self.showPopupAndCheck
def showPopupAndCheck(self):
QtWidgets.QComboBox.showPopup(self.combo)
popup = self.view().window()
rect = popup.geometry()
if not rect.contains(self.combo.mapToGlobal(self.combo.rect().center())):
# the popup is not over the combo, there's no need to move it
return
# change everything from self to self.combo
Alternatively, if you want to keep this behavior consistent through all your program without always using the subclass, you can use some sort of monkey patching hack.
The advantage is that any QComboBox you create (even when loading UI files or creating a combo at runtime) will always use the new behavior.
Important: this MUST be at the very beginning of the main file of your program, possibly just after the import section.
from PyQt5 import QtCore, QtGui, QtWidgets
def customShowPopup(self):
# we can't use super(), because we're not in the class definition, but
# calling the class method with "self" as first argument is practically the
# same thing; note the underscore!
QtWidgets.QComboBox._showPopup(self)
popup = self.view().window()
# ... go on, exactly as above
# create a new reference to the showPopup method, which is the one we've used
# in the function above
QtWidgets.QComboBox._showPopup = QtWidgets.QComboBox.showPopup
# overwrite the original reference with the new function
QtWidgets.QComboBox.showPopup = customShowPopup

Behavioral discrepancy in Tkinter listbox, arrow keys vs. mouse click

My environment is Python 2.7, running on Windows 7.
I'm trying get a Tkinter Listbox to trigger a callback in response to the user changing the 'active' item (i.e. the item with focus). I'm using a binding to the <<ListboxSelect>> event to make this happen, and it's working -- sort of.
The callback itself is supposed to check what the new active item is, and carry out some processing accordingly. This logic operates the way I expect when I change the active item via the up/down arrow keys. But when I point & click on a new item instead, the code mistakenly identifies the prior active item as the current one.
Here's a stripped-down code sample that illustrates the behavior I'm getting:
import Tkinter as tk
#Root window
root = tk.Tk()
#Callback to show focus change
def updateDisplay(*args):
focusIndex = str(lb.index(tk.ACTIVE))
ctrlFI.set('Focus is at index '+focusIndex)
#Control variables
ctrlLB = tk.StringVar()
ctrlFI = tk.StringVar()
#Widgets
lb = tk.Listbox(root,
width=20, height=10,
relief=tk.FLAT,highlightthickness=0,
selectmode=tk.EXTENDED,
activestyle='dotbox',
listvariable=ctrlLB)
lbl = tk.Label(root,
justify=tk.LEFT, anchor=tk.W,
textvariable=ctrlFI)
lb.grid(row=0,column=0,sticky=tk.NW,padx=(5,0),pady=5)
lbl.grid(row=1,column=0,columnspan=2,sticky=tk.NW,padx=5,pady=5)
#Listbox binding to trigger callback
lb.bind('<<ListboxSelect>>',updateDisplay)
#Initializations to prep GUI
ctrlLB.set('Index0-entry Index1-entry Index2-entry Index3-entry Index4-entry')
ctrlFI.set('Ready')
#Begin app
tk.mainloop()
Here are the results when you use the arrow keys:
But here's what you get when you click with the mouse:
The information 'lags' one behind, showing the prior selection instead. (If you click the same item a second time, it 'catches up.')
So my questions are:
What is causing the discrepancy?
How do I fix it so the mouse click gives the right result?
The active item is not necessarily the same as the selected item. When you press the mouse down it changes the selected value but it does not change the active item. The active item only changes once you release the mouse button.
You should be able to see this by clicking and holding the mouse button over an item that is not currently selected. When you do, you'll see something like this:
In the above image, the active item is the one surrounded by a dotted outline. The selected item is in blue. When your code displays the 'focus', it's displaying the active element rather than the selected element.
If you want the selected item, you need to use curselection to get the index of the selected item. It returns a tuple, so in extended mode you need to get the first element that is returned (eg: lb.curselection()[0]). Be sure to handle the case where curselection returns an empty string.

Correct parameter to pass a bound event to treeview.identify/identify_region in Tkinter?

I have a couple of treeviews tkinter widgets. For style purposes i need to prevent a manual resizing of the treeview columns width without totally disableing column resize.
Using a possible solution provided here: How to disable manual resizing of Tkinter's Treeview column? i am catching single click events on separators between the columns and stop the event there. For this purpose every treeview binds a handling function.
Example:
self.tree.bind('<Button-1>', lambda event: handle_treeview_single_click(self.tree, event))
def handle_treeview_single_click(tree, event):
if tree.identify_region(event.x, event.y) == 'separator':
return 'break'
Running the code produce the following error:
File "C:\Program Files (x86)\Python27\ArcGIS10.2\lib\lib-tk\ttk.py", line 1277, in identify
return self.tk.call(self._w, "identify", component, x, y)
TclError: bad component "region": must be row or column
this doesn't change regardless the object i click on (Header, cell or separator). For example the relating parameter look like this:
tree = Treeview: .42424440.47830640.47831440.47831800.47831840
x = 464
y = 14
Not sure what im missing here, even more since im fairly new to python.
There is a comment in the ttk source code that explains why this might not be working:
def identify_region(self, x, y):
"""
...
* Availability: Tk 8.6"""
return self.identify("region", x, y)
Since you're using python 2.7, you probably aren't using tk 8.6. You can verify this by printing out the value of Tkinter.TkVersion

messagebox getting looped

I am displaying a message box on click of option menu when data is not available and is to be acquired. But somehow whenever I change content of the option menu the message box gets looped. Like- first time it vanishes when I press ok. when I change option again second time it displays message box two times and third time three times. Can someone tell me what to change in my code?
#the function where I change option menu value
def module_func(event):
#declarations
if(dateText.get()!="" and driveText.get()!=""):
global module_dir
global selected_module
modules['menu'].delete(0,END)
select="-----Select-----"
modules['menu'].add_command(label=select, command=tk._setit(module,select))
for module_dir in get_immediate_subdirectories(('%s\%s\%s')%(startPath,selected_shot,selected_fec)):
module_dropdown.append(module_dir)
modules['menu'].add_command(label=module_dir, command=tk._setit(module,module_dir))
module.trace('w',setEightText) #think problem is here
else:
errormsg("")
def setEightText(*args):
#declarations
if(os.path.exists(('%s\%s\%s\%s\%s')%(startPath,selected_shot,selected_fec,selected_module,chan))):
#some operations
else:
tkMessageBox.showinfo("Error","Data at given location not available.\n Please Wait until data is acquired.")
module=tk.StringVar(labelframe1)
module.set("-----Select-----")
modules = tk.OptionMenu(labelframe1, module, module_dropdown, command=module_func)
modules.config(width=20, background="SNOW")
modules.grid(row=8, column=2, sticky="W", padx=10, pady=5)
modules.bind('<Button-1>',module_func)
I think the problem is because of trace. But don't know what to add instead of that.

multiple tasks on tkinter canvas?

I am trying to update canvas background at the same time running a binding event.
(from code)In do_popup popup menu will be implemented and conti will continuously change the canvas background color. how can i use popup option while canvas is updating continuously.
Sample code:
from Tkinter import *
root = Tk()
def do_popup(event,w2):
print w2 # inplace of print some popupmenu will be implemented
def conti():
idt=1
while idt==1:
w.config(bg="red") # in place of red it will be a random color
w.update_idletasks()
w= Canvas(root, width=600, height=600)
w.grid(row=0, column=0)
line1 = w.create_line(200,200,300,300, width=10, tags="line1", fill="black")
w.tag_bind(line1, "<Button-3>", lambda e, w2="test1" :do_popup(e,w2))
f = Frame(root)
f.grid(row=0, column=1, sticky=N)
f1=Button(f, text='visual', command=lambda :conti())
f1.grid(row=0, column=1,columnspan=1, sticky=W+N+S+E)
mainloop()
will multiprocessing work?
I am using windows 7 32 bit with python 2.7.3
Thanks in advance
When your script enters the mainloop then the events are executed.
To make reoccurring updates I like to do this:
def conti():
try:
w.config(bg="red") # in place of red it will be a random color
finally:
# start conti after 10 milliseconds,
root.after(10, conti)
# could also be 0ms to handle events
root.after(0, conti)
You can see root.mainloop as
while not (root.quit was called):
root.update()
This way wou can do:
root.quit()
and conti automatically stops.
There is no concurrency as with threads in mainloops.
But you can put a mainloop() somewhere when you create an own dialog box and conti will go on.
If you use the modules tkMessageBox(Python2) or tkinter.messagebox(Python3) then you conti should run while the dialog is open.
Does this answer your questions?
PS: do root.protocol("WM_DELETE_WINDOW", root.quit) to make the mainloop end when you close the window.