在 PyQt5/Pyside2 中窃取焦点

Steal focus in PyQt5/Pyside2

我正在创建一个 Albert, Alfred or uLauncher 风格的启动器。我的应用程序 运行 在后台运行,并在按下热键时显示。我使用 pynput 来听热键。我不能使用 PyQt5 热键的功能(不是吗?)因为我需要在系统范围内监听键盘事件,而不仅仅是应用程序的范围。

按下快捷方式时,它会调用我的小部件的 show() 方法。唯一的问题是,尽管使用了 raise_setFocusactivateWindow.

,但我无法将焦点重新放在 window 上

我找到了一个(丑陋的)解决方法,包括打开一个 QMessageBox(+ 调整它的外观使其不可见,但我没有把它放在示例代码中)并在之后立即关闭它。

当我在 Linux 上工作时,该解决方法正在完成工作,我准备好忘记它是多么丑陋,因为它完成了工作。但是我切换到 Windows(我的应用程序也必须在 运行 上),现在这个厚颜无耻的把戏似乎导致我的应用程序冻结然后崩溃。因果报应?当然可以。

无论如何,如果我的应用程序不能吸引焦点,它就毫无用处,所以我问了两个问题,我很高兴只解决一个问题。 :)

这是一个示例代码。

非常感谢:)

编辑:我刚刚发现即使停用 QMessageBox 解决方法,应用程序最终也会崩溃(在调用热键 5、20、30 次之后)。所以问题也可能出在我将快捷方式绑定到 GUI 的方式上,我担心线程问题,但这超出了我的知识范围:/

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()

我发现了我的错误,所以我在这里发布了一段更新的代码,因为它可能对任何试图将全局热键绑定到影响 GUI 的函数(也就是两个不同的线程通信)的人有所帮助。

我的错误确实是将热键触发的操作直接绑定到我的 show() 方法,这意味着 pynput 侦听器线程将尝试与 QApplication 进行通信。

诀窍是使用 pyqtSignal() 并要求它触发 show() 方法。信号本身由热键触发。

以干净的方式完成后,我的 cheeky_focus_stealer 再次工作,因为它是来自 GUI 线程的 运行。

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()