I want to implement a drag and drop functionality between two custom widgets. My understanding from this question is that I cannot use the built-in drag and drop functionality that e.g. buttons would provide, but that I need to implement mouseReleaseEvent and mousePressEvent handlers manually.
My specific goal is as follows: I have widgets of type Widget1 and Widget2, and I want that when Widget1 is dragged onto Widget2, a certain action is performed. I therefore implemented the mousePressEvent of Widget1 and the mouseReleaseEvent of Widget2.
class Widget1(QWidget):
...
def mousePressEvent(self, event):
print("Press")
class Widget2(QWidget):
...
def mouseReleaseEvent(self, event):
print("Release")
While "Press" is printed whenever I click on Widget1, "Release" is printed only after clicking on Widget2. That is, the release event is called for the same item that was clicked. How do I execute an action for the widget that the mouse was over at release?
Related
The situation is simple. I have a main window with a Help - About menu.
When this menu item is clicked, a modal window is opened (let's say it's an About-window).
With self.grab_set() I disabled the main-window (although the modal window does flicker when you click the main title bar).
So far, so good.
Here is the question: I really like to sound a bell when the user clicks outside the modal window on the main window.
This is what I could find about grab_set(), really not that much:
[effbot] ...a method called grab_set, which makes sure that no mouse or keyboard
events are sent to the wrong window.
[effbot] Routes all events for this application to this widget.
[kite.com] A grab directs all events to this and descendant widgets in the application.
[google books] grab_set() ensures that all of the application's events are sent to w until a corresponding call is made to grab_release ([Me:] or till the window is destroyed?)
I'm not quite sure how to understand this: does it mean you can handle an event on the main window within the modal window (like sounding my bell)?
So I tried things like:
self.bind('<Button-1>', self.bell) Exception in Tkinter callback: _tkinter.TclError: bad window path name
parent.bind('<Button-1>', self.bell) Nothing happens
So, how to sound a bell like when clicked outside the modal window on the main window, like in so many other applications?
Derived questions:
Is it still possible to cature events from the main window after using
grab_set for the modal window?
Is there a way to prevent the flickering?
I really like to understand this mysterious grab_set() method.
Stripped code:
import tkinter as tk
class About(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.geometry('200x150')
#--- OK button
btn_ok = tk.Button(self, text='OK', command=self.destroy) # destroy with OK
btn_ok.pack(side=tk.TOP)
btn_ok.focus() # destroy with spacebar
#--- Make window modal
self.grab_set()
# self.wait_window() # is this necessary in this case?
# self.bind('<Button-1>', self.bell) ??? The question
class MenuBar(tk.Menu):
def __init__(self, parent):
tk.Menu.__init__(self)
helpmenu = tk.Menu(self, tearoff=0)
helpmenu.add_command(label='About', command=lambda: About(parent))
self.add_cascade(label='Help', menu=helpmenu)
class MainApp():
def __init__(self, parent):
parent.configure(background='#000000')
parent.geometry('800x600')
menubar = MenuBar(parent)
parent.configure(menu=menubar)
if __name__ == '__main__':
root = tk.Tk()
MainApp(root)
root.mainloop()
When you set a grab, all button clicks will go to the window with the grab. You capture them the way you capture any other event. In the case of a button click you do that by binding a function to <1>.
It's important to know that a binding on a root window or a Toplevel window will apply to all widgets in that window. For example, binding to self in your code will fire even when you click on the "Ok" button. Therefore, the callback should probably only do work when the widget associated with the event is the same as the toplevel.
Example:
class About(tk.Toplevel):
def __init__(self, parent):
...
self.bind("<1>", self.capture_click)
...
def capture_click(self, event):
if event.widget == self:
<your logic here>
In the case of wanting to know if the user clicked outside the window, you can use the coordinates of the event object to compare against the window to see if the click is inside or outside.
def on_click(self, event):
if event.widget == self:
if (event.x < 0 or event.x > self.winfo_width() or
event.y < 0 or event.y > self.winfo_height()):
self.bell()
I found a second solution. Though my question was explicitly about using grab_set(), this method does the same for me: making the window as modal as possible and sound a bell.
Instead of using self.grab(), you can also disable the parent window:
parent.attributes('-disabled', True)
Of course it needs to be enabled again when the OK button is clicked (and when the windows is closed with the [X] close control. However, my original About-window has no window decoration). The command for the OK-button becomes:
btn_ok = tk.Button(self, text='OK', command=lambda: self.closeme(parent))
...which calls the closeme function:
def closeme(self, parent):
parent.attributes('-disabled', False)
self.destroy()
The bell sounds automatically when clicking a disabled window.
Method 1: Keeps you in full control of the main window but does not 'freeze' the main window: you can still move it around.
Method 2: Completely freezes the main window, but if it happens to be (partially) covered by another window (not of this application), then you can only bring back to top using Alt+Tab (windows).
I'm sure I will use both techniques in the future depending on my needs.
I'm making a user-interface in PYQT 5. It includes a QGroupBox containing several QRadioButton. When there is a toggled event I check the buttons and do some stuff. The problem is that there are two toggled events because one button is toggled on and one off so my code is always running twice.
I have been looking for an event from the QGroupBox instead. That should only happen once instead of twice when i toggle a radiobutton.
def __init__(self):
self.radioButton1.toggled.connect(self.update_stimulus)
self.radioButton2.toggled.connect(self.update_stimulus)
self.radioButton3.toggled.connect(self.update_stimulus)
def update_stimulus(self):
if self.radioButton1.isChecked():
print('1')
if self.radioButton2.isChecked():
print('2')
if self.radioButton3.isChecked():
print('3')
# Do stuff based on button positions
I tried using
self.groupBox.toggled.connect(self.update_stimulus)
But that only works if the groupbox itself is toggled. Is there any way to get a signal from the groupbox when one of the radiobuttons changes or do I have to find some way of doing it with the indivdual signals from the radiobuttons?
As #ekhumoro explains, you can add the checked parameter to the function and do your processing only if it's True:
def update_stimulus(self, checked):
if not checked:
return
if self.radioButton1.isChecked():
print('1')
if self.radioButton2.isChecked():
print('2')
if self.radioButton3.isChecked():
print('3')
Keep in mind, though, that if you want to have different radio button "groups" within the same groupbox, none of this will work properly as they will all be considered as part of the same single group: all Qt buttons (widgets that inherit QAbstractButton: QPushButton, QToolButton, QCheckBox and QRadioButton) have an autoExclusive property which is off by default except from radio buttons. This property makes all button that belong to the same parent widget automatically exclusive.
If you need different groups within the same parent, the solution is to use a QButtonGroup, which extends the exclusive functionality by limiting the membership of each button to a specific group.
def __init__(self):
# ...
self.buttonGroupA = QtWidgets.QButtonGroup()
self.buttonGroupA.addButton(self.radioButton1)
self.buttonGroupA.addButton(self.radioButton2)
self.buttonGroupA.addButton(self.radioButton3)
self.buttonGroupA.buttonToggled[QtWidgets.QAbstractButton, bool].connect(self.update_stimulusA)
self.buttonGroupB = QtWidgets.QButtonGroup()
self.buttonGroupB.addButton(self.radioButton4)
self.buttonGroupB.addButton(self.radioButton5)
self.buttonGroupB.addButton(self.radioButton6)
self.buttonGroupB.buttonToggled[QtWidgets.QAbstractButton, bool].connect(self.update_stimulusB)
def update_stimulusA(self, btn, checked):
if not checked:
return
# do something with group A
def update_stimulusB(self, btn, checked):
if not checked:
return
# do something with group B
Creation of a button group is also possible from Designer: just select at least two buttons that will be members of the same group, right click on one of them, go to the "Assign to button group" sub menu and select "New button group". To add a button to an existing group, just use the same context menu and choose the group you want to add that button to.
I'm dynamically creating creating a list of QPushButtons in a vertical layout. I'm currently using the "click" signal for a function. But I would like to use the right mouse button for an additional function. Ex. print the tooltip of that QPushButton that the mouse is over by right clicking.
but = QtGui.QPushButton()
but.setText(cur);but.setCheckable(True)
but.setStyleSheet(_fromUtf8("text-align:right;background-color: rgb(50, 50, 50);"))
but.setToolTip(longName + "." + cur)
I'm looking over at the "QMouseEvent", "setMouseTracking", "mousePressEvents". But I'm not sure how to properly use them to get my desired result.
I would also be open to a custom signal for a QPushButton on "right-click".
Usually, the right mouse click is connected to the context menu. With
but.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
but.customContextMenuRequested.connect(handle_right_click)
you can connect a handler, which could be used for the right mouse click.
If you'd like to have a pure right mouse click action, you should extend QPushButton and override mousePressEvent(self, event) and mouseReleaseEvent(self, event). Then you need to set the button's ContextMenuPolicy to QtGui.Qt.PreventContextMenu to ensure their delivery:
The widget does not feature a context menu, and in contrast to NoContextMenu, the handling is not deferred to the widget's parent. This means that all right mouse button events are guaranteed to be delivered to the widget itself through QWidget::mousePressEvent(), and QWidget::mouseReleaseEvent().
You can check the mouse buttons in your handler for the clicked signal:
but.clicked.connect(self.buttonClicked)
...
def buttonClicked(self):
if QtGui.qApp.mouseButtons() & QtCore.Qt.RightButton:
print(self.sender().toolTip())
The mouse button constants can be OR'd together, allowing you to test for different combinations of them. So the above code will detect that the right button is held down while the left button is clicked.
You can also do a similar thing with the keyboard modifiers:
if QtGui.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier:
print('ctrl+click')
I have a QMainWindow with a menu bar, including menu items for Save, Open and Quit with the usual shortcuts. It creates a QTableWidget that lists a bunch of different categories from which the user can choose (at his option).
If the user clicks into the QTableWidget to change categories, the widget takes the focus. That's mostly what I want, but unfortunately it also seems to steal the menu shortcuts, so that pressing Ctrl+S no longer triggers the save.
I experimented with keyPressEvent to solve this, but it seems like overkill even if I do get it working. Isn't there a way to delegate all the control/menu keys back to the QMainWindow ?
There must be an issue with how you are creating your QMenuBar. Here is an example that works just fine for me. The Save continues to function regardless of focus being in the table:
class Window(QtGui.QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.resize(640,480)
menuBar = self.menuBar()
menu = menuBar.addMenu("&File")
action = menu.addAction("&Save", self.doAction)
action.setShortcuts(QtGui.QKeySequence.Save)
self.view = QtGui.QTableWidget(5,5)
self.setCentralWidget(self.view)
def doAction(self):
print "Save"
I've got a QComboBox with a custom list object.
The custom list object has a custom mousePressEvent so that when the user click on one of the circles with a +/- (a twisty), the list is expanded/collapsed.
When I use the list with the combo box, when the user clicks on a twisty, the list is expanded/collapsed, but the selection is changed, and the list is hidden. How can I filter this so that when the user click on a twisty, the selection is not changed, and the list not hidden.
Additional screenshots
All of the nodes collapsed:
List hidden:
QT has a eventFilter that "captures" QEvent.MouseButtonRelease. So what I have done is installed my own eventFilter that filters QEvent.MouseButtonRelease events if the user click on a node.
In my list object I have the following method:
def mousePressEvent (self, e):
self.colapse_expand_click = False
if <user clicked node>:
colapse_expand_node()
e.accept ()
self.colapse_expand_click = True
The mousePressEvent runs before mouseReleaseEvent.
Then in the custom combobox, I filter the event:
class RevisionSelectorWidget(QtGui.QComboBox):
def __init__(self, parent = None):
QtGui.QComboBox.__init__(self, parent)
self.log_list = RevisionSelectorLogList(self)
self.setView(self.log_list)
self.log_list.installEventFilter(self)
self.log_list.viewport().installEventFilter(self)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.MouseButtonRelease:
if self.log_list.colapse_expand_click:
return True
return False
Off the top of my head, you could subclass QComboBox and override hideEvent(QHideEvent) (inherited from QWidget)
def hideEvent(self, event):
if self.OkToHide():
event.accept()
else:
event.ignore()
Your screenshot looks like an interesting use of a combo box, I'm curious as to why you haven't used a TreeView style control instead of a list?
Edit (Mar 14 2009):
I looked at the Qt source code and it looks like when the keyboard and mouse events are captured, that as soon as qt has decided to emit the "activated(int index)" signal, "hidePopup()" has been called.
So apart from rewriting their event filter code, another option is to connect the "activated(int index)" or "highlighted(int index)" signal to a slot that can call "showPopup()" which would re-raise the list items. If you get a nasty disappear/appear paint issue you may have to get Qt to delay the paint events while the popup is visible.
Hope that helps!