I'm trying to write a PyQt5 GUI that captures all keyboard keys that are currently being pressed. Based on this answer, I've tried the following minimal code:
import sys
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import QEvent
class MainWindow(QWidget):
def __init__(self):
super().__init__()
QApplication.instance().installEventFilter(self)
self.pressedKeys = []
def eventFilter(self, source, event):
if event.type() == QEvent.KeyPress:
if int(event.key()) not in self.pressedKeys:
self.pressedKeys.append(int(event.key()))
print(self.pressedKeys)
elif event.type() == QEvent.KeyRelease:
if int(event.key()) in self.pressedKeys:
self.pressedKeys.remove(int(event.key()))
print(self.pressedKeys)
return super().eventFilter(source, event)
if __name__ == "__main__":
app = QApplication(sys.argv)
demo = MainWindow()
demo.show()
sys.exit(app.exec_())
When I run this, if I hold down a key the output list keeps flipping back and forth between one containing the key value and being empty. Similarly, holding down multiple keys adds the keys to the list, but alternates back and forth between containing and removing the final key that I have pressed. It seems that if I hold down keys the KeyRelease event still keeps getting triggered for the last key I pressed.
Is there are way to hold all current key presses in PyQt5, or should I use a different package (e.g., using one or other of the packages suggested in this question)?
Note, I've also tried:
import sys
from PyQt5.QtWidgets import QApplication, QWidget
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.pressedKeys = []
def keyPressEvent(self, event):
if int(event.key()) not in self.pressedKeys:
self.pressedKeys.append(int(event.key()))
print(self.pressedKeys)
def keyReleaseEvent(self, event):
if int(event.key()) in self.pressedKeys:
self.pressedKeys.remove(int(event.key()))
print(self.pressedKeys)
if __name__ == "__main__":
app = QApplication(sys.argv)
demo = MainWindow()
demo.show()
sys.exit(app.exec_())
which results in pretty much the same behaviour.
Physical keyboards have the "autorepeat" feature, which allows simulating multiple pressure of a key while keeping it pressed.
This results in having multiple press/release events while a standard key (usually, not modifiers) is down and until it's physically released, with a rate normally set for the system.
You can check if the event is actually a physical press/release or if it isAutoRepeat():
def eventFilter(self, source, event):
if event.type() == QEvent.KeyPress:
if not event.isAutoRepeat() and int(event.key()) not in self.pressedKeys:
self.pressedKeys.append(int(event.key()))
print(self.pressedKeys)
elif event.type() == QEvent.KeyRelease:
if not event.isAutoRepeat() and int(event.key()) in self.pressedKeys:
self.pressedKeys.remove(int(event.key()))
print(self.pressedKeys)
return super().eventFilter(source, event)
Related
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()
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
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)
I am trying to capture all of key pressed in my program
def keyPressEvent(self, event):
event = event.key()
if (event == QtCore.Qt.Key_Q):
print ('Q!.')
That function works fine when i am trying to capture keys in my window. (in this case Q_Key)
But if the key was pressed in a Text Widget (for example in a: QListView, QlistWidget, QLineEdit, and so many more ) it don't work. The function prints nothing. I am doing something wrong...
What can i do to fix it?
You will need to install an event-filter on the application object to get all key-press events:
class Window(QMainWindow):
def __init__(self):
...
QApplication.instance().installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.KeyPress:
print('KeyPress: %s [%r]' % (event.key(), source))
return super().eventFilter(source, event)
This is a follow up question to this answer: https://stackoverflow.com/a/11939294/406686:
Consider the following code, which embeds mplayer in a QWidget. The problem is that it doesn't react to any mplayer keyboard shortcuts such as right arrow for seek forward and so on.
It's clear that I can reimplement every shortcut manually. However is there a way to pipe all keyboard sequences automatically to mplayer as long as a modifier key, say ALT or Win-Key is pressed?
For example: Press ALT + → = seek forward...
import mpylayer
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.container = QtGui.QWidget(self)
self.container.setStyleSheet('background: black')
self.button = QtGui.QPushButton('Open', self)
self.button.clicked.connect(self.handleButton)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.container)
layout.addWidget(self.button)
self.mplayer = mpylayer.MPlayerControl(
'mplayer', ['-wid', str(self.container.winId())])
def handleButton(self):
path = QtGui.QFileDialog.getOpenFileName()
if not path.isEmpty():
self.mplayer.loadfile(unicode(path))
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.resize(640, 480)
window.show()
sys.exit(app.exec_())
I am not sure, if I got your problem right. You could easily add the keyPressEvent and keyReleaseEvent methods to your Window Class:
class Window(QtGui.QWidget):
def __init__(self):
# same code as above
self.setFocus()
self.__modifier_pressed = False
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Alt:
self.__modifier_pressed = True
elif self.__modifier_pressed:
self.mplayer.run_command("key_down_event", event.nativeVirtualKey())
def keyReleaseEvent(self, event):
if event.key() == QtCore.Qt.Key_Alt:
self.__modifier_pressed = False
This example would only work with Modifier+ONE other key. If you also need this for more complex shortcuts, such as Alt+Ctrl+Shift+→, you might need lists to save the currently pressed keys, but the basic idea should be clear.
On my computer, the pressed key of python and the received one from mplayer are different, but I use a very uncommon keyboard layout (Neo-Layout), so this might be the reason for this.