如何强制键盘聚焦于特定的 widget/QGroupBox (PyQt5)?

How to force keyboard focus on a specific widget/QGroupBox (PyQt5)?

我正在开发一个 GUI,我遇到了一个问题,有时按下 'enter' 键会使多个小部件发送它们的信号。最奇怪的部分是它有时会发生,有时不会。最主要的是,我不能保证一直只关注一个QGroupBox

这是一个比较简单的例子。如果你 运行 它并输入文本,然后点击 'enter',将执行两个函数(下图)。

# -*- coding: utf-8 -*-

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QComboBox, QStyleFactory, QDialog, QTextEdit,
                QGroupBox, QLabel, QLineEdit, QGridLayout, QPushButton, QVBoxLayout)
import sys


class GrblGUI(QDialog):

    class PositionDescriber:
        """ Label and widget associated for each axis. Save some writing later """
        def __init__(self, labelText, initVal=0.0):
            self.posLabel = QLabel(labelText)
            self.value = initVal
            self.posWidget = QLineEdit(str(self.value))


    def __init__(self, parent=None):
        """ Initializes the GUI and all widgets within.
            Creates the general layout
        """
        super(GrblGUI, self).__init__(parent)
        self.originalPalette = QApplication.palette()

        self.axes = [ self.PositionDescriber("X pos : "),
                     self.PositionDescriber("Y pos : "),
                     self.PositionDescriber("Z pos : "),
                     self.PositionDescriber("A pos : "),
                     self.PositionDescriber("B pos : ")]
        self.size = range(len(self.axes))
        self.ports = ["None"]

        # Creating widget within panels
        self.createConnectToCOM()
        self.createPositionControlPanel()
        self.createPushButtonsPanel()
        self.createMessageHistory()

        mainLayout = QGridLayout()
        mainLayout.addWidget(self.connectToCOM, 0, 0, 1, 2)
        mainLayout.addWidget(self.positionControlPanel, 1, 0)
        mainLayout.addWidget(self.pushButtonsPanel, 0, 2, 2, 1)
        mainLayout.addWidget(self.messageHistory, 1, 1)

        mainLayout.setRowStretch(1, 1)
        self.setLayout(mainLayout)
        self.setWindowTitle("minimal")
        QApplication.setStyle(QStyleFactory.create('Fusion'))
        QApplication.setPalette(QApplication.style().standardPalette())


    """
    Creation of panels, widgets, and associated layouts
    """

    def createConnectToCOM(self):
        self.connectToCOM = QGroupBox()
        self.availableDevicesScroll = QComboBox()
        for item in self.ports:
            self.availableDevicesScroll.addItem(item)

        connectLabel = QLabel("Connect to device :")
        self.updatePushButton = QPushButton("Update")
        self.updatePushButton.setDefault(True)
        self.connectPushButton = QPushButton("Connect")
        self.connectPushButton.setDefault(True)
        self.updatePushButton.clicked.connect(self.updateAvailableCOM)
        self.connectPushButton.clicked.connect(self.connectToPort)

        layout = QGridLayout()
        layout.addWidget(connectLabel, 0, 0)
        layout.addWidget(self.availableDevicesScroll, 1, 0)
        layout.addWidget(self.updatePushButton, 0, 1)
        layout.addWidget(self.connectPushButton, 1, 1)
        layout.setColumnStretch(0, 1)
        self.connectToCOM.setLayout(layout)


    def createPositionControlPanel(self):
        self.positionControlPanel = QGroupBox("Position Control Panel : ")
        for i in self.size:
            self.axes[i].posWidget.returnPressed.connect(self.registerInput)

        layout = QGridLayout()
        for i in self.size:
            layout.addWidget(self.axes[i].posLabel, i, 0)
        for i in self.size:
            layout.addWidget(self.axes[i].posWidget, i, 1)

        sendPushButton = QPushButton("Send to pos")
        sendPushButton.setDefault(True)
        sendPushButton.clicked.connect(self.sendToPos)
        layout.addWidget(sendPushButton, len(self.axes), 2, 1, 2)
        layout.setRowStretch(6, 1)
        self.positionControlPanel.setLayout(layout)


    def createPushButtonsPanel(self):
        self.pushButtonsPanel = QGroupBox("Things you may want to do : ")

        self.homingPushButton = QPushButton("Homing")
        self.homingPushButton.setDefault(True)
        self.homingPushButton.clicked.connect(self.homing)

        recPosPushButton = QPushButton("Record current pos")
        recPosPushButton.setDefault(True)
        recPosPushButton.clicked.connect(self.recordPosition)

        layout = QVBoxLayout()
        layout.setSpacing(20)
        layout.addWidget(self.homingPushButton)
        layout.addWidget(recPosPushButton)

        layout.addStretch(1)
        self.pushButtonsPanel.setLayout(layout)

    def createMessageHistory(self):
        self.messageHistory = QGroupBox("Message history : ")
        self.textEdit = QTextEdit()
        self.textEdit.setReadOnly(True)
        self.textEdit.setPlainText("")
        layout = QVBoxLayout()
        layout.addWidget(self.textEdit)
        self.messageHistory.setLayout(layout)


    """
    Methods to call
    """

    def connectToPort(self):
        self.textEdit.append("connectToPort")

    def updateAvailableCOM(self):
        self.textEdit.append("updateAvailableCOM")

    def registerInput(self):
        self.textEdit.append("registerInput")

    def homing(self):
        self.textEdit.append("homing")

    def recordPosition(self):
        self.textEdit.append("recordPosition")

    def sendToPos(self):
        self.textEdit.append("sendToPos")


if __name__ == '__main__':
    app = QApplication(sys.argv)

    gallery = GrblGUI()
    gallery.show()
    app.exec()
    # sys.exit(appctxt.app.exec())

输入文字后的结果:

我尝试了不同的方法,例如 setFocusPolicy(Qt.NoFocus)setFocus(),但没有用。有什么想法吗?

@musicamante 发现,这与

密切相关

最简单的答案是继承 QWidget 而不是 QDialog,因为不再有默认按钮。

问题来自default and autoDefault properties of QPushButton in combination with the usage of QDialog,结果如下

  • 如果 autoDefaultTrue,一个按钮成为可能的 default 按钮;
  • QPushButton 的 autoDefault 属性 是 False,除非它 is/becomes QDialog 的子级(甚至是间接的);

接受的小部件接收的事件会自动传播到其父级,在父级层次结构中向上,直到一个小部件接受 接受它或到达顶级小部件。

QLineEdit 默认 处理 return 按键,但 接受它,这意味着它知道键已被按下(因为它可以发出 returnPressed 信号)但不会处理,因此将其传播给父级。

考虑到上述情况,不需要的行为是按键事件也被 QDialog 接收,因此有多种可能性,具体取决于要求。

使用 QWidget 而不是 QDialog

这是最简单的选择,但您可能仍然需要 QDialog 来实现其功能:exec 事件循环、accepted/rejected 信号和结果接口,或者只是为了简化模态

所有按钮的default属性设置为False

显然,您可以将每个按钮的 autoDefault 属性 设置为 False,但更简单的解决方案是使用一个循环遍历所有 QPushButton 实例的循环,这应该是在我们确定会调用的覆盖中实现,例如 exec 或者,也许更好,showEvent:

class Dialog(QtWidgets.QDialog)
    # ...
    def showEvent(self, event):
        super().showEvent(event)
        if not event.spontaneous():
            for btn in self.findChildren(QtWidgets.QPushButton):
                if btn.default():
                    btn.setDefault(False)
                if btn.autoDefault():
                    btn.setAutoDefault(False)

不过,这可能会成为一个问题,因为有时您可能仍然想使用该功能:例如,如果您有一个 tab/stacked 小部件,并且您想避免使用 return 功能在一个有行编辑的页面中,但在另一个只有一个按钮(如向导)的页面中没有。

忽略对话框中的 Return

重写 keyPressEvent,如果键是 not return,则仅调用基础实现 或输入(这与上面有同样的问题,因为它完全禁用了该功能):

class Dialog(QtWidgets.QDialog)
    # ...
    def keyPressEvent(self, event):
        if event.key() not in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
            super().keyPressEvent(event)

在行编辑中接受按键事件

这可能是一种更合适的方法,因为它解决了 来源 的问题:如果按下 return/enter 键,则认为事件在 QLineEdit 中被接受.这只能在子类中完成:

class LineEdit(QtWidgets.QLineEdit):
    def keyPressEvent(self, event):
        super().keyPressEvent(event)
        if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
            event.accept()

可能对整个表单使用 eventFilter 是您的解决方案:

  1. from QtCore import QEvent
  2. class GrblGUI 中定义 eventFilter(self, obj, event) 并在 Qt.Key_Enter 分支下放置您想要的回车键按下。
  3. 注册事件过滤器app.installEventFilter(gallery)

下面修改示例(小心,我在示例中切换到 PySide2,您可以轻松地再次切换回 PyQt5):


# -*- coding: utf-8 -*-

from PySide2.QtCore import Qt, QEvent # Step 1 - import QEvent
from PySide2.QtWidgets import QApplication, QComboBox, QStyleFactory, QDialog, QTextEdit
from PySide2.QtWidgets import QGroupBox, QLabel, QLineEdit, QGridLayout, QPushButton, QVBoxLayout
import sys

class GrblGUI(QDialog):

    class PositionDescriber:
        """ Label and widget associated for each axis. Save some writing later """
        def __init__(self, labelText, initVal=0.0):
            self.posLabel = QLabel(labelText)
            self.value = initVal
            self.posWidget = QLineEdit(str(self.value))

    def __init__(self, parent=None):
        """ Initializes the GUI and all widgets within.
            Creates the general layout
        """
        super(GrblGUI, self).__init__(parent)
        self.originalPalette = QApplication.palette()

        self.axes = [ self.PositionDescriber("X pos : "),
                     self.PositionDescriber("Y pos : "),
                     self.PositionDescriber("Z pos : "),
                     self.PositionDescriber("A pos : "),
                     self.PositionDescriber("B pos : ")]
        self.size = range(len(self.axes))
        self.ports = ["None"]

        # Creating widget within panels
        self.createConnectToCOM()
        self.createPositionControlPanel()
        self.createPushButtonsPanel()
        self.createMessageHistory()

        mainLayout = QGridLayout()
        mainLayout.addWidget(self.connectToCOM, 0, 0, 1, 2)
        mainLayout.addWidget(self.positionControlPanel, 1, 0)
        mainLayout.addWidget(self.pushButtonsPanel, 0, 2, 2, 1)
        mainLayout.addWidget(self.messageHistory, 1, 1)

        mainLayout.setRowStretch(1, 1)
        self.setLayout(mainLayout)
        self.setWindowTitle("minimal")
        QApplication.setStyle(QStyleFactory.create('Fusion'))
        QApplication.setPalette(QApplication.style().standardPalette())

    """
    Creation of panels, widgets, and associated layouts
    """

    def eventFilter(self, obj, event): # Step 2 - declare QEvent filter
        if event.type() == QEvent.KeyPress:
            if (event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return):
                self.textEdit.append("***DEBUG: Enter pressed")
                # Step 2 Place callback you want to process key press
                event.accept() # block event propagation to other widgets
                return True
        return False

    def createConnectToCOM(self):
        self.connectToCOM = QGroupBox()
        self.availableDevicesScroll = QComboBox()
        for item in self.ports:
            self.availableDevicesScroll.addItem(item)

        connectLabel = QLabel("Connect to device :")
        self.updatePushButton = QPushButton("Update")
        self.updatePushButton.setDefault(True)
        self.connectPushButton = QPushButton("Connect")
        self.connectPushButton.setDefault(True)
        self.updatePushButton.clicked.connect(self.updateAvailableCOM)
        self.connectPushButton.clicked.connect(self.connectToPort)

        layout = QGridLayout()
        layout.addWidget(connectLabel, 0, 0)
        layout.addWidget(self.availableDevicesScroll, 1, 0)
        layout.addWidget(self.updatePushButton, 0, 1)
        layout.addWidget(self.connectPushButton, 1, 1)
        layout.setColumnStretch(0, 1)
        self.connectToCOM.setLayout(layout)

    def createPositionControlPanel(self):
        self.positionControlPanel = QGroupBox("Position Control Panel : ")
        for i in self.size:
            self.axes[i].posWidget.returnPressed.connect(self.registerInput)

        layout = QGridLayout()
        for i in self.size:
            layout.addWidget(self.axes[i].posLabel, i, 0)
        for i in self.size:
            layout.addWidget(self.axes[i].posWidget, i, 1)

        sendPushButton = QPushButton("Send to pos")
        sendPushButton.setDefault(True)
        sendPushButton.clicked.connect(self.sendToPos)
        layout.addWidget(sendPushButton, len(self.axes), 2, 1, 2)
        layout.setRowStretch(6, 1)
        self.positionControlPanel.setLayout(layout)

    def createPushButtonsPanel(self):
        self.pushButtonsPanel = QGroupBox("Things you may want to do : ")

        self.homingPushButton = QPushButton("Homing")
        #self.homingPushButton.setFocusPolicy(Qt.StrongFocus); #!!!

        self.homingPushButton.setDefault(True)
        self.homingPushButton.clicked.connect(self.homing)

        recPosPushButton = QPushButton("Record current pos")
        recPosPushButton.setDefault(True)
        recPosPushButton.clicked.connect(self.recordPosition)

        layout = QVBoxLayout()
        layout.setSpacing(20)
        layout.addWidget(self.homingPushButton)
        layout.addWidget(recPosPushButton)

        layout.addStretch(1)
        self.pushButtonsPanel.setLayout(layout)

    def createMessageHistory(self):
        self.messageHistory = QGroupBox("Message history : ")
        self.textEdit = QTextEdit()
        self.textEdit.setReadOnly(True)
        self.textEdit.setPlainText("")
        layout = QVBoxLayout()
        layout.addWidget(self.textEdit)
        self.messageHistory.setLayout(layout)

    """
    Methods to call
    """

    def connectToPort(self):
        self.textEdit.append("connectToPort")

    def updateAvailableCOM(self):
        self.textEdit.append("updateAvailableCOM")

    def registerInput(self):
        self.textEdit.append("registerInput")

    def homing(self):
        self.textEdit.append("homing")

    def recordPosition(self):
        self.textEdit.append("recordPosition")

    def sendToPos(self):
        self.textEdit.append("sendToPos")

if __name__ == '__main__':
    app = QApplication(sys.argv)

    gallery = GrblGUI()
    gallery.show()
    app.installEventFilter(gallery) # Step 4 - register event filter in app
    app.exec_()