I have a QMainWindow application that has multiple widgets (buttons, labels, etc.) inside it.
How can I get an event when the user presses ANYWHERE of the app?
I tried to customize mousePressEvent() function, but this doesn't accept the event when other widgets (buttons, labels, etc.) are pressed.
Explanation:
The handling of mouse events between the widgets goes from children to parents, that is, if the child does not accept the event (it does not use it) then it will pass the event to the parent. For example, if you press on the QPushButton, it accepts the event and the parent is not notified, unlike QLabel that does not consume it, so the event passes to the parent.
Tools:
On the other hand, there are several methods to listen to events in general, such as:
Override any method like mousePressEvent, keyPressEvent, etc or the event or customEvent method.
Use an eventFilter.
Solution:
An alternative is to apply some previous method to all the widgets but you can discard all of them for the current objective, for example:
In the first and second method it would involve detecting when a widget is added or removed (using QEvent::ChildAdded or QEvent::ChildRemoved).
In the first method it would imply override the methods that many times is impossible.
With the above, the problem is attacked on the widgets side, but there are other alternatives:
Override the notify() method of Q{Core, GUi,}Application, verify that it is a widget and that it belongs to the window, it also implies discriminating if the event has already been consumed.
Listen to the mouse event associated with the window (QWindow).
In this case the most reasonable is the second method.
import sys
from PyQt5 import QtCore, QtWidgets
class MouseObserver(QtCore.QObject):
pressed = QtCore.pyqtSignal(QtCore.QPoint)
released = QtCore.pyqtSignal(QtCore.QPoint)
moved = QtCore.pyqtSignal(QtCore.QPoint)
def __init__(self, window):
super().__init__(window)
self._window = window
self.window.installEventFilter(self)
#property
def window(self):
return self._window
def eventFilter(self, obj, event):
if self.window is obj:
if event.type() == QtCore.QEvent.MouseButtonPress:
self.pressed.emit(event.pos())
elif event.type() == QtCore.QEvent.MouseMove:
self.moved.emit(event.pos())
elif event.type() == QtCore.QEvent.MouseButtonRelease:
self.released.emit(event.pos())
return super().eventFilter(obj, event)
class MainWindow(QtWidgets.QMainWindow):
pass
def main(args):
app = QtWidgets.QApplication(args)
w = MainWindow()
w.show()
mouse_observer = MouseObserver(w.window().windowHandle())
mouse_observer.pressed.connect(lambda pos: print(f"pressed: {pos}"))
mouse_observer.released.connect(lambda pos: print(f"released: {pos}"))
mouse_observer.moved.connect(lambda pos: print(f"moved: {pos}"))
app.exec_()
if __name__ == "__main__":
main(sys.argv)
Related
I have a small GUI app made using pyqt5.
I found a strange problem while using eventFilter...
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.KeyPress:
# RETURN
if event.key() == QtCore.Qt.Key_Return:
if source is self.userLineEdit:
self.pswLineEdit.setFocus()
elif source is self.pswLineEdit:
self.LoginButton.click()
# TAB
elif event.key() == QtCore.Qt.Key_Tab:
if source is self.userLineEdit:
self.pswLineEdit.setFocus()
return super().eventFilter(source, event)
While pressing enter key just behave normally, tab key does not.
I don't know where the problem could be. I'm going to link a video to show the exact problem as I'm not able to describe how this is not working
Link to video
I know it's pixelated (sorry) but the important thing is the behavior of the cursor
SMALL EXAMPLE
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLineEdit
from PyQt5 import QtCore
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'Hello, world!'
self.left = 10
self.top = 10
self.width = 640
self.height = 480
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.userEdit = QLineEdit(self)
self.pswEdit = QLineEdit(self)
self.userEdit.setPlaceholderText("Username")
self.pswEdit.setPlaceholderText("Password")
self.userEdit.installEventFilter(self)
self.pswEdit.installEventFilter(self)
mainLayout = QVBoxLayout()
mainLayout.addWidget(self.userEdit)
mainLayout.addWidget(self.pswEdit)
self.setLayout(mainLayout)
self.show()
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.KeyPress:
# RETURN
if event.key() == QtCore.Qt.Key_Return:
if source is self.userEdit:
self.pswEdit.setFocus()
# TAB
elif event.key() == QtCore.Qt.Key_Tab:
if source is self.userEdit:
self.pswEdit.setFocus()
return super().eventFilter(source, event)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
If you don't filter events, they are processed by the object and eventually propagated back to the parent.
By default, QWidget will try to focus the next (or previous, for Shift+Tab) child widget in the focus chain, by calling its focusNextPrevChild(). If it can do it, it will actually set the focus on that widget, otherwise the event is ignored and propagated to the parent.
Since most widgets (including QLineEdit) don't handle the tab keys for focus changes on their own, as they don't have children, their parent will receive it, which will call focusNextPrevChild() looking for another child widget, and so on, up to the object tree, until a widget finally can handle the key, eventually changing the focus.
In your case, this is what's happening:
you check events and find that the tab key event is received by the first line edit;
you set the focus on the other line edit, the password field;
you let the event be handled anyway by the widget, since you're not ignoring or filtering it out;
the first line edit calls focusNextPrevChild() but is not able to do anything with it;
the event is propagated to the parent, which then calls its own focusNextPrevChild();
the function checks the current focused child widget, which is the password field you just focused, and finds the next, which coincidentally is the first line edit, which gets focused again;
The simple solution is to just add return True after changing the focus, so that the event doesn't get propagated to the parent causing a further focus change:
if event.key() == QtCore.Qt.Key_Tab:
if source is self.userEdit:
self.pswEdit.setFocus()
return True
Note that overriding the focus behavior is quite complex, and you have to be very careful about how focus and events are handled, especially for specific widgets that might deal with events in particular ways (studying the Qt sources is quite useful for this), otherwise you'll get unexpected behavior or even fatal recursion.
For instance, there's normally no need for an event filter for the return key, as QLineEdit already provides the returnPressed signal:
self.userEdit.returnPressed.connect(self.pswEdit.setFocus)
Qt already has a quite thorough focus management system, if you just want more control over the way the tab chain works use existing functions like setTabOrder() on the parent or top level window, and if you want to have more control over how (or if) they get it, use setFocusPolicy().
I have a context menu with a checkbox and a spinbox. Both work as expected, except that when I am typing a number in the spinbox, I tend to hit enter to close the menu, but that also toggles the checkbox. Does anyone know how to prevent that?
minimal example:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QFrame, QMenu, QAction, QWidgetAction, QFormLayout, QSpinBox
from PyQt5.QtCore import Qt
t = True
v = 0
class SpinAction(QWidgetAction):
def __init__(self, parent):
super(SpinAction, self).__init__(parent)
w = QWidget()
layout = QFormLayout(w)
self.spin = QSpinBox()
layout.addRow('value', self.spin)
w.setLayout(layout)
self.setDefaultWidget(w)
def _menu(position):
menu = QMenu()
test_action = QAction(text="Test", parent=menu, checkable=True)
test_action.setChecked(t)
test_action.toggled.connect(toggle_test)
spin_action = SpinAction(menu)
spin_action.spin.setValue(v)
spin_action.spin.valueChanged.connect(spin_changed)
menu.addAction(test_action)
menu.addAction(spin_action)
action = menu.exec_(w.mapToGlobal(position))
def toggle_test(val):
global t
t = val
def spin_changed(val):
global v
v = val
app = QApplication(sys.argv)
w = QFrame()
w.setContextMenuPolicy(Qt.CustomContextMenu)
w.customContextMenuRequested.connect(_menu)
w.show()
sys.exit(app.exec_())
The widget must accept focus, so a proper focus policy (StrongFocus or WheelFocus) must be set, but since it's a container you should also set the focus proxy to the spin, so that it will handle the focus for it.
In this way the keyboard navigation of the menu will also work properly by using arrows or Tab to focus the spinbox.
The action also has to be set as the active action of the menu in order to prevent the menu to accept events that may trigger other actions, and a way to achieve that is to install an event filter on the spin and check for events that should make the widget action as the active one, like FocusIn and Enter event.
Note that this results in some unintuitive results from the UX perspective: by default, hovering a normal QAction makes it the active one, but the user might want to move the mouse away from the spinbox in order to edit it without having the mouse on it, and by doing it another action could become active; in the meantime the spin should still keep the focus for obvious reasons. The result is that the other action might seem visually selected while the spinbox has focus. There is no obvious solution for this, and that's why widgets used for QWidgetActions should always be chosen with care.
class SpinAction(QWidgetAction):
FocusEvents = QtCore.QEvent.FocusIn, QtCore.QEvent.Enter
ActivateEvents = QtCore.QEvent.KeyPress, QtCore.QEvent.Wheel)
WatchedEvents = FocusEvents + ActivateEvents
def __init__(self, parent):
# ...
w.setFocusPolicy(self.spin.focusPolicy())
w.setFocusProxy(self.spin)
self.spin.installEventFilter(self)
def eventFilter(self, obj, event):
if obj == self.spin and event.type() in self.WatchedEvents:
if isinstance(self.parent(), QtWidgets.QMenu):
self.parent().setActiveAction(self)
if event.type() in self.FocusEvents:
self.spin.setFocus()
return super().eventFilter(obj, event)
Note: the obj == self.spin condition is required since QWidgetAction already installs an event filter. A more elegant solution is to create a QObject subclass intended for that purpose only, and only overriding eventFilter().
Using PyQt5 I am viewing an image in a QGraphicsView. I want to be able to zoom in/out while pressing ctrl and using the mouse wheel. I have this working, however if the image is too large, and there are scroll bars, it ignores the zoom functionality until you scroll to the top or bottom.
How can I fix this to where it does not scroll when ctrl is pressed, while allowing it to zoom in/out.
from PyQt5.QtWidgets import QFileDialog, QLineEdit, QWidget, QPushButton, QApplication, QVBoxLayout, QLabel, QGraphicsView, QGraphicsPixmapItem, QGraphicsScene
from PyQt5.QtCore import pyqtSignal, Qt
from pdf2image import convert_from_path
from PIL import ImageQt
import sys
class step1(QWidget):
changeViewSignal = pyqtSignal()
def __init__(self, parent=None):
super(step1, self).__init__(parent)
self.name = QLineEdit(self)
self.fileBtn = QPushButton("Select file", self)
self.nextBtn = QPushButton("Next", self)
self.graphicsView = QGraphicsView()
# self.graphicsView.setFrameShadow(QFrame.Raised)
# self.graphicsView.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContentsOnFirstShow)
self.graphicsView.setHorizontalScrollBarPolicy()
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.name)
self.layout.addWidget(self.fileBtn)
self.layout.addWidget(self.nextBtn)
self.layout.addWidget(self.graphicsView)
self.fileBtn.clicked.connect(self.convert_file)
def wheelEvent(self, event):
modifiers = QApplication.keyboardModifiers()
if modifiers == Qt.ControlModifier:
self.graphicsView.scrollContentsBy(0,0)
x = event.angleDelta().y() / 120
if x > 0:
self.graphicsView.scale(1.05, 1.05)
elif x < 0:
self.graphicsView.scale(.95, .95)
def convert_file(self):
fname = QFileDialog.getOpenFileName(self, 'Open File', 'c:\\', "PDF Files (*.pdf)")
if len(fname[0]) > 0:
pages = convert_from_path(fname[0])
images = []
qimage = ImageQt.toqpixmap(pages[0])
item = QGraphicsPixmapItem(qimage)
scene = QGraphicsScene(self)
scene.addItem(item)
self.graphicsView.setScene(scene)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = step1()
ex.show()
sys.exit(app.exec_())
The scrolling is first handled by the QGraphicsView before it would be propagated up to the parent widget where you are reimplementing the wheelEvent. This is why the scrolling occurs according to the normal QGraphicsView behavior when it has space to scroll.
A solution is to subclass QGraphicsView and reimplement the wheelEvent there instead.
class GraphicsView(QGraphicsView):
def wheelEvent(self, event):
if event.modifiers() & Qt.ControlModifier:
x = event.angleDelta().y() / 120
if x > 0:
self.scale(1.05, 1.05)
elif x < 0:
self.scale(.95, .95)
else:
super().wheelEvent(event)
Then use the subclass name here:
self.graphicsView = GraphicsView()
Besides the proper solution proposed by alec, there's also the option of using an event filter, which can be useful for UIs created in Designer without the need of using promoted widgets.
The important aspect to keep in mind is that the event filter must be installed on the view's viewport() (the widget in which the contents of the scene are actually rendered and, possibly, scrolled), because that is the widget that will receive the wheel event: input events are always sent to the widget under the mouse (or has keyboard focus)[1], and possibly propagated to their parents if the event is not handled[2].
class step1(QWidget):
def __init__(self, parent=None):
# ...
self.graphicsView.viewport().installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == event.Wheel and event.modifiers() & Qt.ControlModifier:
x = event.angleDelta().y() / 120
if x > 0:
self.graphicsView.scale(1.05, 1.05)
elif x < 0:
self.graphicsView.scale(.95, .95)
return True
return super().eventFilter(source, event)
Returning True means that the viewport has handled the event, and it should not be propagated to the parent; QGraphicsView is based on QAbstractScrollArea, and if a wheel event is not handled by the viewport it will call the base wheelEvent implementation of the viewport's parent (the graphics view), which by default will post the event to the scroll bars. If the filter returns True, it will avoid that propagation, thus preventing scrolling.
Note that you should not use scrollContentsBy unless you really know what you're doing; as the documentation explains: «Calling this function in order to scroll programmatically is an error, use the scroll bars instead».
[1] Mouse events are always sent to the topmost widget under the mouse, unless a modal child window is active, or there is a mouse grabber, which is a widget that has received a mouse button press event but didn't receive a mouse button release event yet, or a widget on which grabMouse() was explicitly called. Keyboard events are always sent to the widget of the active window that has current focus, or the widget on which grabKeyboard() has been explicitly called.
[2] "handled event" can be a confusing concept: it doesn't mean that the widget actually "does" something with the event, nor that it doesn't, no matter if the event then becomes accepted or ignored. A widget could "ignore" an event and still react to it in some way: for instance, you might need to notify the user that the event has been received, but let the parent manage it anyway.
I'm creating a launcher application (like Spotlight/Albert/Gnome-Do). I'm using Python 2.7 and Pyside. Made and used on Windows 10.
It is running in the background and listening to a shortcut with the keyboard (pip install keyboard). When the shortcut is called, a QObject signal calls the show method of my main widget.
My issue is that when the main widget gets hidden by pressing escape or return, next time the widget is shown, the focus will be in the QlineEdit and the user will be able to type its query straight away.
But when the widget is hidden by clicking outside widget (handled by filtering the QEvent WindowDeactivate), the focus won't be on my QLineEdit at next call, which ruins the user experience.
I've tried playing with activateWindow() or raise_(), but it doesn't change anything.
Heree here a simplified example code that shows my problem:
import sys
import keyboard
from PySide.QtCore import *
from PySide.QtGui import *
SHORTCUT = 'Ctrl+space'
class ShortcutThread(QObject):
signal = Signal()
class Launcher(QMainWindow):
def __init__(self, parent=None):
super(Launcher, self).__init__()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Popup)
self.resize(500, 50)
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout_ = QHBoxLayout()
self.central_widget.setLayout(self.layout_)
self.search = QLineEdit()
self.layout_.addWidget(self.search)
def eventFilter(self, obj, event):
# Hide dialog when losing focus
if event.type() == QEvent.WindowDeactivate:
self.hide()
return super(Launcher, self).eventFilter(obj, event)
def keyPressEvent(self, e):
# Hide dialog when pressing escape or return
if e.key() in [Qt.Key_Escape, Qt.Key_Return]:
self.hide()
def main():
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
launcher = Launcher()
shortcut = ShortcutThread()
shortcut.signal.connect(launcher.show)
keyboard.add_hotkey(SHORTCUT, shortcut.signal.emit, args=[])
launcher.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
When I call the shortcut (Ctrl+Space here) and click elsewhere, next time I'll call the shortcut, the focus won't be set to the QLineEdit widget.
When the launcher is hidden by hitting return or escape, it does work as expected.
In my Qt application I'm using the QCalendarWidget and I would like to get notified when the mouse enters a new cell of the calendar. I know that the QCalendarWidget is using a QTableView internally which inherits from QAbstractItemView and this has an entered signal:
This signal is emitted when the mouse cursor enters the item specified
by index. Mouse tracking needs to be enabled for this feature to work.
I tried to receive the signal with following code:
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyCalendar:
def __init__(self):
app = QApplication(sys.argv)
window = QMainWindow()
cal = QCalendarWidget(window)
window.resize(320, 240)
cal.resize(320, 240)
table = cal.findChild(QTableView)
table.setMouseTracking(True)
table.entered.connect(self.onCellEntered)
window.show()
sys.exit(app.exec_())
def onCellEntered(self, index):
print("CellEntered")
if __name__ == "__main__":
window = MyCalendar()
But my callback function is never called. Do you have any ideas why?
The QCalendarWidget class uses a custom table-view which bypasses the normal mouse-event handlers - so the enetered signal never gets emitted. However, it is possible to work-around that by using an event-filter to emit a custom signal that does the same thing.
Here is a demo script that implements that:
import sys
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
cellEntered = QtCore.pyqtSignal(object)
def __init__(self):
super(Window, self).__init__()
self.calendar = QtGui.QCalendarWidget(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.calendar)
self._table = self.calendar.findChild(QtGui.QTableView)
self._table.setMouseTracking(True)
self._table.installEventFilter(self)
self._index = None
self.cellEntered.connect(self.handleCellEntered)
def eventFilter(self, source, event):
if source is self._table:
if event.type() == QtCore.QEvent.MouseMove:
index = QtCore.QPersistentModelIndex(
source.indexAt(event.pos()))
if index != self._index:
self._index = index
self.cellEntered.emit(QtCore.QModelIndex(index))
elif event.type() == QtCore.QEvent.Leave:
self._index = None
return super(Window, self).eventFilter(source, event)
def handleCellEntered(self, index):
print(index.row(), index.column())
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(600, 100, 300, 200)
window.show()
sys.exit(app.exec_())
I've investigated a bit and I think I know why this happens.
QCalendarWidget creates a private subclass of QTableView called QCalendarView and instantiates it as a child of itself. (This instance is called qt_calendar_calendarview.)
If you look at QCalendarView's code (Qt 5), you'll see this:
void QCalendarView::mouseMoveEvent(QMouseEvent *event)
{
[...]
if (!calendarModel) {
QTableView::mouseMoveEvent(event);
return;
}
[...]
}
This means only if there is no calendarModel, the superclasses mouseMoveEventis called which is responsible for emitting the entered signal. All of this is an implementation detail of `QCalendarWidget, so it's probably best not to rely on any of this anyway.
So for a way around this, I'm not sure what the best approach is. You'll have to catch the event before it gets to the table. This can be done using QObject.installEventFilter() or re-implementing QWidget.mouseMoveEvent(), but then you don't get the model index directly. You could probably use QAbstractItemView.indexAt() for this purpose.