Trigger a function on clicking QFrame - python

I am working on a project with PyQt5 which has QFrames. I am using mouse press event to trigger a function on clicking frame as below :
frame.mousePressEvent = lambda x: print_name(x, name)
Above line is not executed at the start, It is executed after user has done some work in UI.
I am getting the behaviour I want but here is the problem:
If the user clicks the frame after the above line of code is executed, it works fine but if the user clicks on the frame before the above line of the code is executed and later again clicks the frame (after code is executed), I am not getting the same behaviour. Basically nothing happens.
I want to know where is the problem and how do I solve it?

The problem is caused because PyQt5 caches the methods so if the method is assigned then it cannot be changed. Instead of following the bad practice of assigning methods to mousePressEvent there are other better alternatives such as:
Implement inheritance
class Frame(QFrame):
def mousePressEvent(self, event):
super().mousePressEvent(event)
print(event)
Use an event filter
class MouseObserver(QObject):
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, obj, event):
if obj is self.widget and event.type() == QEvent.MouseButtonPress:
print(event)
return super().eventFilter(obj, event)
Then
observer = MouseObserver(frame)
The second seems the most appropriate for your case.

Related

PyQt5 QWidget event blocks other events. How to avoid it?

I have the next problem:
A created a custom widget (simple derived QWidget class). When I middle-mouse click on it - it creates another QWidget class (sort of context menu). When I release middle-mouse button - that context widget disappears. So that works. What does not work is - that context widget also has some content added, like other small widgets, icons, etc and they all have their own custom events (simple example - enterEvent and leaveEvent with prints indicating those events). But those inner widget events are not working, they are blocked while I keep middle-mouse pressed. When I release it - context widget disappears. Would like to know if there is any solution to let inner widgets'events work as expected.
Here is a minimal example where inner widget does not run mouse events as they are blocked by MainWidget event:
from PyQt5 import QtWidgets, QtGui, QtCore
class SomeSmallWidget(QtWidgets.QWidget):
def __init__(self, increment=1, globalValue=0):
super(SomeSmallWidget, self).__init__()
# UI
self.setMinimumWidth(40)
self.setMaximumWidth(40)
self.setMinimumHeight(40)
self.setMaximumHeight(40)
self.mainLayout = QtWidgets.QVBoxLayout()
self.setLayout(self.mainLayout)
def enterEvent(self, event):
print('Entered') # NOT WORKING
super(SomeSmallWidget, self).enterEvent(event)
def leaveEvent(self, event):
print('Leaved') # NOT WORKING
super(SomeSmallWidget, self).leaveEvent(event)
class ContextWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ContextWidget, self).__init__()
self.setMouseTracking(True)
# position
point = parent.rect().topLeft()
global_point = parent.mapToGlobal(point)
self.move(global_point - QtCore.QPoint(0, 0))
self.innerWidget = SomeSmallWidget() # Here is that small widget, which event does not work
self.mainLayout = QtWidgets.QVBoxLayout()
self.setLayout(self.mainLayout)
self.mainLayout.addWidget(self.innerWidget)
class MainWidget(QtWidgets.QLineEdit):
def __init__(self, value='0'):
super(MainWidget, self).__init__()
self.setMouseTracking(True)
self.popMenu = None
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton: # while we keep MMB pressed - we see context widget
self.popMenu = ContextWidget(parent=self)
self.popMenu.show()
super(MainWidget, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton:
if self.popMenu:
self.popMenu = None
Events are not blocked, all mouse events are sent to widget that was under cursor when mouse button was pressed. Usually (always) it makes more sense. Imagine two buttons next to each other. Suppose user pressed one, moved cursor and released over second button. What was his intention? He probably changed his mind. If button triggers action on mouse press - this options will not be available and it's probably too soon, if button triggers action on mouse release, which button should recieve mouserelease event? If we send mouseevent to second button - that was not pressed it will trigger action that used didn't want. If we dont send mouserelease event to first button - it will stay in sunken mode. Imagine user is selecting text in lineedit, and while selecting he leaves lineedit and goes to other widgets, should they react somehow, and should focus be switched? Probably not. So there is only one active window and only one focused widget at a time and it receives keyboard and mouse input and reacts to it. Most of menus are shown after mouserelease and closes on next mouseclick, providing better user experience.
However, If you still want your widget to receive mouse events, you can achive this by translating it from ContextWidget to SomeSmallWidget like this:
class SomeSmallWidget(QtWidgets.QWidget):
...
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.fillRect(self.rect(), QtCore.Qt.blue)
def onMouseEnter(self):
print('onMouseEnter')
def onMouseLeave(self):
print('onMouseLeave')
class MainWidget(QtWidgets.QLineEdit):
...
def mouseMoveEvent(self, event):
if self.popMenu:
self.popMenu.mouseTest(event.globalPos())
class ContextWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
...
self._inside = False
def mouseTest(self, p):
widget = self.innerWidget
rect = QtCore.QRect(widget.mapToGlobal(QtCore.QPoint(0,0)), widget.size())
inside = rect.contains(p)
if inside != self._inside:
if inside:
widget.onMouseEnter()
else:
widget.onMouseLeave()
self._inside = inside
Notice I added paintEvent to see the widget bounds.

Qt ShortcutOverride default action

I understand that QEvent::ShortcutOverride occurs when there is a shortcut registered in the parent and the child wants to "go against the rule". The example given on the Qt Wiki is of a media player which pauses with Space but a QLineEdit might want to use the Space, which makes a lot of sense.
Furthermore, if the event is accepted then a QEvent::KeyPress is generated to the child widget so you can treat your particular case.
Now, my question is, why does it seem that the default action is to actually accept the QEvent::ShortcutOverride when a standard shortcut is used? This seems to me like the opposite of what the name suggest, i.e., it's overriden by default and you have to treat the event to let the shortcut pass.
In the code below, if you don't install the event filter you don't see the message.
from PySide.QtGui import QApplication
from PySide import QtGui, QtCore
app = QApplication([])
class Test(QtGui.QWidget):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
self.setLayout(QtGui.QVBoxLayout())
self.w_edit = QtGui.QLineEdit(parent=self)
self.layout().addWidget(self.w_edit)
# If we install the event filter and ignore() the ShortcutOverride
# then the shortcut works
self.w_edit.installEventFilter(self)
# Ctrl+Left is already in use (jump to previous word)
shortcut = QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Left'), self)
shortcut.setContext(QtCore.Qt.ApplicationShortcut)
shortcut.activated.connect(self.test_slot)
def test_slot(self):
print('ctrl+left pressed!')
def eventFilter(self, obj, event):
if obj is self.w_edit and event.type() == QtCore.QEvent.ShortcutOverride:
# Send the event up the hierarchy
event.ignore()
# Stop obj from treating the event itself
return True
# Events which don't concern us get forwarded
return super(Test, self).eventFilter(obj, event)
widget = Test()
widget.show()
if __name__ == '__main__':
app.exec_()
My actual scenario is a tab widget for which I want to use Ctrl+Left/Right to cycle through the tabs, which works unless something like a QLineEdit has focus. I feel that there should be a better way other than calling event->ignore(); return true on all QLineEdits and any other widgets which could use the key combo, am I missing something here?
Thanks!
You can set one event filter on the application instance and then filter accordingly:
QtGui.qApp.installEventFilter(self)
# self.w_edit.installEventFilter(self)
...
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ShortcutOverride:
# filter by source object, source.parent(), or whatever...
if isinstance(source, QtGui.QLineEdit):
event.ignore()
return True
return super(Test, self).eventFilter(source, event)

How to read double click with eventFilter?

I have an eventFilter on my custom label and I would like to sniff double clicks with it. Is this possible?
self.installEventFilter(self)
# Handles mouse events
def eventFilter(self, object, event):
try:
if event.buttons() == QtCore.Qt.LeftButton:
#LeftButton event
else:
# nothing is being pressed
except:
pass
Yes it is possible but for some strange reason it is not that simple. Surely you never know if a single click might be followed by another single click effectively resulting in a double click. That's why there must be some inbuilt waiting time. Qt does it and delivers events for double clicks (QEvent.MouseButtonDblClick). On the other hand Qt still delivers events for single clicks (QEvent.MouseButtonPress) even in the case of a double click, but only one. This might not be the best design.
We must differentiate them correctly. I do it with an additional timer that needs to be a bit longer than the inbuilt Qt timer for detecting double clicks. The code then goes:
from PySide import QtCore, QtGui
class MyLabel(QtGui.QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.single_click_timer = QtCore.QTimer()
self.single_click_timer.setInterval(200)
self.single_click_timer.timeout.connect(self.single_click)
def single_click(self):
self.single_click_timer.stop()
print('timeout, must be single click')
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
self.single_click_timer.start()
return True
elif event.type() == QtCore.QEvent.MouseButtonDblClick:
self.single_click_timer.stop()
print('double click')
return True
return False
app = QtGui.QApplication([])
window = MyLabel('Click me')
window.resize(200, 200)
window.show()
app.exec_()
See also Distinguish between single and double click events in Qt.

How to set a "read-only checkbox" in PySide/PyQt

I am trying to display the value of a Boolean variable using a QCheckBox widget, and render the user unable to change the displayed value. I don't want to disable it, as the resulting graying doesn't look good. I have tried to approximate the effect by changing the new value back to its previous value when the user clicks the QCheckBox. However, the problem is compounded by the fact that the state of the widget is described by the "checked" properties of the QAbstractButton parent class, and the "state" properties of the QCheckBox class itself. This gives rise to a combinatorial exercise of signals and slots, of which I have been unable to obtain any good result.
var_ctrl = QtGui.QCheckBox( 'some name' )
def rdslot1(state):
if state == QtCore.Qt.Checked:
var_ctrl.setCheckState( QtCore.Qt.Unchecked )
else:
var_ctrl.setCheckState( QtCore.Qt.Checked )
def rdslot2(state):
if var_ctrl.isChecked():
var_ctrl.setChecked(False)
else:
var_ctrl.setChecked(True)
# Signal/Slot combinations (only one should be active)
var_ctrl.stateChanged.connect( rdslot1 )
var_ctrl.toggled.connect( rdslot2 )
var_ctrl.stateChanged.connect( rdslot2 )
var_ctrl.toggled.connect( rdslot1 )
I'm late to the party - it seems like you got a solution that works. For future reference tho, another way you can do it would be to consume the mouse events - which keeps all of your signals working the way they should:
from PyQt4 import QtGui, QtCore
class MyCheckBox(QtGui.QCheckBox):
def __init__( self, *args ):
super(MyCheckBox, self).__init__(*args) # will fail if passing **kwargs
self._readOnly = False
def isReadOnly( self ):
return self._readOnly
def mousePressEvent( self, event ):
if ( self.isReadOnly() ):
event.accept()
else:
super(MyCheckBox, self).mousePressEvent(event)
def mouseMoveEvent( self, event ):
if ( self.isReadOnly() ):
event.accept()
else:
super(MyCheckBox, self).mouseMoveEvent(event)
def mouseReleaseEvent( self, event ):
if ( self.isReadOnly() ):
event.accept()
else:
super(MyCheckBox, self).mouseReleaseEvent(event)
# Handle event in which the widget has focus and the spacebar is pressed.
def keyPressEvent( self, event ):
if ( self.isReadOnly() ):
event.accept()
else:
super(MyCheckBox, self).keyPressEvent(event)
#QtCore.pyqtSlot(bool)
def setReadOnly( self, state ):
self._readOnly = state
readOnly = QtCore.pyqtProperty(bool, isReadOnly, setReadOnly)
Setting the code up this way gets you a few things (which you may or may not care about) but can be useful when developing custom Qt widgets:
Consuming the event blocks the signal emission, so you can still connect other slots to things like clicked & toggled. If you're looking for those signals and then just switching the value on/off - then other widgets listening for those signals will be triggered incorrectly
Using isReadOnly/setReadOnly keeps the class following the Qt coding style
Creating pyqtSignals & pyqtSlots will help if you expose the plugin to Qt's Designer
Well later on I came up with a shortcut, which simply catches the clicks of the user and handles them according to a specifiable 'Modifiable' property. I have made this class:
class MyQCheckBox(QtGui.QCheckBox):
def __init__(self, *args, **kwargs):
QtGui.QCheckBox.__init__(self, *args, **kwargs)
self.is_modifiable = True
self.clicked.connect( self.value_change_slot )
def value_change_slot(self):
if self.isChecked():
self.setChecked(self.is_modifiable)
else:
self.setChecked(not self.is_modifiable)
def setModifiable(self, flag):
self.is_modifiable = flag
def isModifiable(self):
return self.is_modifiable
It behaves just like a normal QCheckBox, being modifiable by default. However, when you call setModifiable(False), everytime you click it, it keeps the current state of the widget. The trick was to catch the clicked signal, not toggled neither stateChanged.
Try to disable the checkbox widget, but override its look using widget palette or style

Correct way to use the signal closeEditor in QStyledItemDelegate?

I am overriding the QStyledItemDelegate class and reimplementing the eventFilter function so I can customize the editor behavior when a Tab press is detected. However, the following is not working. What is the correct way to invoke the closeEditor signal?
class CustomDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super(CustomDelegate, self).__init__(parent)
def eventFilter(self, editor, event):
if (event.type() == QEvent.KeyPress and
event.key() == Qt.Key_Tab):
print "Tab captured in editor"
self.commitData.emit(editor) #This is working
self.closeEditor.emit(editor) #This does not seem to do anything??
return True
return QStyledItemDelegate.eventFilter(self,editor,event)
This is an old question, but I just ran into the same issue and found this question.
I solved it by changing the
self.closeEditor.emit(editor)
line to
self.closeEditor.emit(editor, QAbstractItemDelegate.NoHint).
The commitData call will setModelData. If you don't call closeEditor, setModelData will be called again as the editor itself will close.

Categories