How to catch NonClientAreaMouseMove event in QEvent - python

I try to find a way to catch event when user move Qdialog from titleBar.
My goal is to attach a drag event to dock my custom qdialog inside my mainWindow. (on Linux)
In other terms, do what dockwidgets do (I can use dockwidget) I have to do the same with Custom Qdialog (or widget with Qt.Window flags)
I see in c++ Qt source code than for QDockWidget, They use this kind of stuff:
bool QDockWidget::event(QEvent *event)
{ [...]
case QEvent::NonClientAreaMouseMove:
case QEvent::NonClientAreaMouseButtonPress:
case QEvent::NonClientAreaMouseButtonRelease:
case QEvent::NonClientAreaMouseButtonDblClick:
d->nonClientAreaMouseEvent(static_cast<QMouseEvent*>(event));
But when I try to catch this kind of event on pyside, I recieve nothin:
def event(self, e):
print('event %s' % e.type())
return super(myDyalig,self).event(e)
event PySide2.QtCore.QEvent.Type.ActivationChange
event PySide2.QtCore.QEvent.Type.UpdateRequest
event PySide2.QtCore.QEvent.Type.Paint
# I recieve only this move event when user stop moving (when he
# release the button)
event PySide2.QtCore.QEvent.Type.Move
event PySide2.QtCore.QEvent.Type.WindowActivate
event PySide2.QtCore.QEvent.Type.ActivationChange
event PySide2.QtCore.QEvent.Type.UpdateRequest
event PySide2.QtCore.QEvent.Type.Paint
Any idea how to do this ? (or another idea how to realize a drag event with qdialog)
Edit:
a minimal example:
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
class CustomDialog(QDialog):
def __init__(self, parent=None):
super(CustomDialog,self).__init__(parent)
self.setFixedSize(QSize(200,200))
def event(self, e):
print('event %s' % e.type())
return super(CustomDialog,self).event(e)
def main():
import sys
app = QApplication(sys.argv)
dial = CustomDialog()
dial.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

You need to install an event filter:
def __init__(self, parent=None):
super().__init__(parent)
#...
self.installEventFilter(self)
def eventFilter(self, obj, event):
if event.type() in (QEvent.NonClientAreaMouseButtonPress, QEvent.NonClientAreaMouseButtonPress, QEvent.Move):
print(event)
return super().eventFilter(obj, event)
See also: https://doc.qt.io/qt-5/eventsandfilters.html

Related

Get mouse press event from ANY widget

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)

Steal focus in PyQt5/Pyside2

I am creating a launcher, in the style of Albert, Alfred or uLauncher. My application runs in the background and shows up when a hotkey is pressed. I use pynput to listen to hotkeys. I cannot use PyQt5 hotkey's feature (can't I?) because I need to listen to keyboard events in the system scope, not only the application's scope.
When the shortcut is pressed, it calls the show() method of my widget. The only issue is that I can't get the focus back on my window, despite the use of raise_, setFocus and activateWindow.
I found a (ugly) workaround that consists in openning a QMessageBox (+ tweaking its appearance to make it invisible, but I didn't put that in the example code) and closing it immediately after.
When I was working on Linux, that workaround was doing the job, and I was ready to forget how ugly it is for it does the job. But I switched to Windows (on which my app must run too), and now this cheeky trick seems to cause freeze then crash of my application. Karma? For sure.
Any ways, my application is useless if it cannot catch focus, so I'm asking two questions, and I'd be happy with only one being solved. :)
Do you know why showing the QMessageBox causes a crash?
Do you know any other way to get the focus back on my application?
Here is an example code to play with.
Thank you very much :)
EDIT: I just found out that even with deactivating the QMessageBox workaround, the application eventually crashes (after 5, 20, 30 calls of the hotkey). So the issue might as well be in the way I bind my shortcut to the GUI, I fear a thread issue, but this is way beyond my knowledge :/
import sys
from PyQt5.QtWidgets import QLineEdit, QApplication, QMessageBox
from PyQt5.QtCore import QSize, Qt, QEvent
from pynput import keyboard
class Launcher(QLineEdit):
def __init__(self):
super().__init__()
self.setFixedSize(QSize(600, 50))
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setPlaceholderText('Search...')
self.installEventFilter(self)
self.set_shortcut('<ctrl>+`')
def set_shortcut(self, shortcut):
def for_canonical(f):
return lambda k: f(listener.canonical(k))
hotkey = keyboard.HotKey(
keyboard.HotKey.parse(shortcut),
self.wake_up)
listener = keyboard.Listener(
on_press=for_canonical(hotkey.press),
on_release=for_canonical(hotkey.release))
listener.start()
def wake_up(self):
print('Waking up')
self.show()
self.cheeky_focus_stealer()
def cheeky_focus_stealer(self):
self.setFocus()
self.raise_()
self.activateWindow()
# Working of linux, but causes freeze/crash on Windows 10
message_box = QMessageBox(self)
message_box.show()
message_box.hide()
def eventFilter(self, obj, event):
if obj is self and event.type() == QEvent.KeyPress:
if event.key() == Qt.Key_Escape:
self.hide()
return True
return super().eventFilter(obj, event)
def main():
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
window = Launcher()
window.show()
app.exec_()
if __name__ == "__main__":
main()
I found my error, so I'm posting an updated piece of code here for it could be helpful to anyone trying to bind a global hotkey to a function that affects a GUI, aka two different thread communicating.
My mistake was indeed to bind the hotkey triggered action straight to my show() method, which implies that the pynput listenner thread will attempt to communnicate with the QApplication.
The trick is to use a pyqtSignal() and to ask it to trigger the show() method. The signal itself being trigger by the hotkey.
After doing that in a clean way, my cheeky_focus_stealer works again, because it is ran from the GUI thread.
import sys
from PyQt5.QtWidgets import QLineEdit, QApplication, QMessageBox
from PyQt5.QtCore import QSize, Qt, QEvent, QObject, pyqtSignal
from pynput import keyboard
class Forwarder(QObject):
signal = pyqtSignal()
class Launcher(QLineEdit):
def __init__(self):
super().__init__()
self.setFixedSize(QSize(600, 50))
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setPlaceholderText('Search...')
self.installEventFilter(self)
self.set_shortcut('<ctrl>+`')
def set_shortcut(self, shortcut):
# The forwarder must be parented to the Launcher
forwarder = Forwarder(parent=self)
forwarder.signal.connect(self.wake_up)
def for_canonical(f):
return lambda k: f(listener.canonical(k))
hotkey = keyboard.HotKey(
keyboard.HotKey.parse(shortcut),
forwarder.signal.emit)
listener = keyboard.Listener(
on_press=for_canonical(hotkey.press),
on_release=for_canonical(hotkey.release))
listener.start()
def wake_up(self):
print('Waking up')
self.show()
self.cheeky_focus_stealer()
def cheeky_focus_stealer(self):
self.setFocus()
self.raise_()
self.activateWindow()
# Working of linux, but causes freeze/crash on Windows 10
message_box = QMessageBox(self)
message_box.show()
message_box.hide()
def eventFilter(self, obj, event):
if obj is self and event.type() == QEvent.KeyPress:
if event.key() == Qt.Key_Escape:
self.hide()
return True
return super().eventFilter(obj, event)
def main():
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
window = Launcher()
window.show()
app.exec_()
if __name__ == "__main__":
main()

Pyside uiLoader capture close event signals

How does one properly capture the close event coming out of a PySide QtUiTools.QUiLoader() setup?
I can get the instanced class to connect to widgets and everything else, but I am not sure how to intercept the signals in this setup.
Ideally, I want all close calls to pass through my closeEvent (obviously) so that I can ensure that it's safe to close the window. But since my self.closeEvent() is tied to my View(QtWidgets.QMainWindow) and not the self._qt.closeEvent(), I don't know how to get to the self._qt.closeEvent() method to override it in this case.
Or is there a better way to set this up to capture those window events?
# Compatible enough with Pyside 2
from PySide import QtGui as QtWidgets
from PySide import QtUiTools
from PySide import QtCore
class View(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(View, self).__init__(parent=parent)
self.setup()
def closeEvent(self, event):
# Do things
event.accept()
def setup(self):
loader = QtUiTools.QUiLoader()
fy = QtCore.QFile('example.ui')
fy.open(QtCore.QFile.ReadOnly)
self._qt = loader.load(fy, self)
fy.close()
self._qt.pCanceled.clicked(self._qt.close)
Doesn't apply:
PySide / PyQt detect if user trying to close window
Close, but PySide doesn't use PyQt's uic and appears to run differently (and didn't work):
PyQt: clicking X doesn't trigger closeEvent
closeEvent is not a signal, it is a method that is called when the QCloseEvent event is sent. A signal and an event are different things. Going to the problem, in Qt there are 2 ways to listen to events, the first one is overwriting the fooEvent() methods and the second one using an event filter as I show below:
from PySide import QtGui as QtWidgets
from PySide import QtUiTools
from PySide import QtCore
class View(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(View, self).__init__(parent=parent)
self._qt = None
self.setup()
def closeEvent(self, event):
self.do_things()
super(View, self).closeEvent(event)
def do_things(self):
print("do_things")
def setup(self):
loader = QtUiTools.QUiLoader()
fy = QtCore.QFile('example.ui')
fy.open(QtCore.QFile.ReadOnly)
self._qt = loader.load(fy, self)
fy.close()
self._qt.pCanceled.clicked.connect(self._qt.close)
self._qt.installEventFilter(self)
def eventFilter(self, watched, event):
if watched is self._qt and event.type() == QtCore.QEvent.Close:
self.do_things()
return super(View, self).eventFilter(watched, event)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = View()
w.show()
sys.exit(app.exec_())
Update:
Normally in the eventFilter it is enough to return True for the event to be ignored but in the case of QCloseEvent you must ignore the event and return True as shown below:
def eventFilter(self, watched, event):
if watched is self._qt and event.type() == QtCore.QEvent.Close:
self.do_things()
event.ignore()
return True
return super(View, self).eventFilter(watched, event)

PyQt - Add right click to a widget

I am using PyQt and I want to add a right click to a widget, but I can't find any code on this subject online.
How can you do it ?
You just have to override the methods that take care of it.
In this case you will override the mousePressEvent, have a look on this and see if it makes sense and works for what you need.
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QWidget
class MyWidget(QWidget):
def __init__(self):
super(MyWidget, self).__init__()
def mousePressEvent(self, QMouseEvent):
if QMouseEvent.button() == Qt.LeftButton:
print("Left Button Clicked")
elif QMouseEvent.button() == Qt.RightButton:
#do what you want here
print("Right Button Clicked")
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MyWidget()
mw.show()
sys.exit(app.exec_())
Another good way to do that would be installing a event filter in your object and overriding its eventFilter. Inside that method you would make what you want. Remember you can always make use of pyqtSignal for good practices and call another object to make the job, not overloading the method with a lot of logic.
Here is another small example:
import sys
from PyQt5.QtCore import QEvent
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QWidget
class MyWidget(QWidget):
def __init__(self):
super(MyWidget, self).__init__()
self.installEventFilter(self)
def eventFilter(self, QObject, event):
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.RightButton:
print("Right button clicked")
return False
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MyWidget()
mw.show()
sys.exit(app.exec_())
Note: Remember that this last example will receive ALL KIND OF EVENTS, so you will have to be careful and make sure it's the one you want and not runtime breaking your app calling methods of your event that doesn't exist because it's not of that kind. For example if you call event.button() without making sure before that it is a QEvent.MouseButtonPress your app would break of course.
There are other ways to do that, these are the most known ones.
I have come up with a pretty simple way of doing this and works perfectly. In the ControlMainWindow class add the following to initialise the Context menu policy as CustomeContextMenu where listWidget_extractedmeters will be the name of your QListWidget:
self.listWidget_extractedmeters.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.listWidget_extractedmeters.connect(self.listWidget_extractedmeters,QtCore.SIGNAL("customContextMenuRequested(QPoint)" ), self.listItemRightClicked)
Then in the ControlMainwindow class the following functions allow you to add context menu items and to call a funtion that performs some functionality:
def listItemRightClicked(self, QPos):
self.listMenu= QtGui.QMenu()
menu_item = self.listMenu.addAction("Remove Item")
self.connect(menu_item, QtCore.SIGNAL("triggered()"), self.menuItemClicked)
parentPosition = self.listWidget_extractedmeters.mapToGlobal(QtCore.QPoint(0, 0))
self.listMenu.move(parentPosition + QPos)
self.listMenu.show()
def menuItemClicked(self):
currentItemName=str(self.listWidget_extractedmeters.currentItem().text() )
print(currentItemName)

PyQt: Emit signal when cell entered in QCalendarWidget

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.

Categories

Resources