zhiyiYo / PyQt-Fluent-Widgets

A fluent design widgets library based on C++ Qt/PyQt/PySide. Make Qt Great Again.
https://qfluentwidgets.com
GNU General Public License v3.0
5.61k stars 541 forks source link

[Bug]: 继承 FlyoutViewBase 类自定义组件,组件中包含LineEdit,系统输入法切换到中文但无法输入中文字符。 #780

Closed keymou closed 7 months ago

keymou commented 7 months ago

What happened?

继承 FlyoutViewBase 类自定义组件,其中包含LineEditBreadcrumbBar

Operation System

Window 7

Python Version

3.8.10 64bit

PyQt/PySide Version

5.15.9

PyQt/PySide-Fluent-Widgets Version

1.5.1

How to Reproduce?

  1. 继承 FlyoutViewBase 类自定义组件AddNodeFlyoutViewCard,组件中包含LineEdit,无法输入中文字符。 image

若将AddNodeFlyoutViewCard基类改为QWidget,则可以正常输入中文字符。 image

  1. BreadcrumbBar 设置 setToolTip,通过 ToolTipFilter设置位置,无法正常显示,注释掉后可以正常显示。 self.nodePathBreadcrumb.installEventFilter(ToolTipFilter(self.nodePathBreadcrumb, 0, ToolTipPosition.TOP)) image

Minimum code

from typing import Optional, List

from PyQt5.QtCore import Qt, QSize, QRegExp, QFile, pyqtSignal as Signal
from PyQt5.QtGui import QIcon, QRegExpValidator
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QFrame, QGridLayout, QLayout, QWidget, QApplication
from qfluentwidgets import (FlyoutViewBase, LineEdit, TransparentToolButton, ComboBox, BreadcrumbBar, PrimaryPushButton,
                            FluentIcon, ToolTipFilter, ToolTipPosition, Flyout, FlyoutAnimationType)

def getStyleSheetFromFile(qss_file: str):
    """ get style sheet from qss file """
    f = QFile(qss_file)
    f.open(QFile.ReadOnly)
    qss = str(f.readAll(), encoding='utf-8')
    f.close()
    return qss

class AddNodeFlyoutViewCard(FlyoutViewBase):
    closed = Signal()
    properties = Signal(dict)

    def __init__(self, node_names: Optional[List[str]] = None, path: Optional[List[str]] = None,
                 serial: Optional[str] = None, parent=None):
        """

        :param node_names: 节点名称集合
        :param path: 节点路径列表
        :param serial: 节点编号
        :param parent:
        """
        super().__init__(parent)
        self.nodeNames = node_names
        self.path = path
        self.serial = serial

        self.viewLayout = QHBoxLayout(self)
        self.verticalLayout = QVBoxLayout()
        self.titleLabel = QLabel(self)
        self.contentView = QFrame(self)
        self.gridLayout = QGridLayout(self.contentView)
        self.nodeNameLabel = QLabel(self.contentView)
        self.nodePathLabel = QLabel(self.contentView)
        self.nodeSerialLabel = QLabel(self.contentView)
        self.nodeTypeLabel = QLabel(self.contentView)
        self.nodeNameLineEdit = LineEdit(self.contentView)
        self.nodeSerial = QFrame(self.contentView)
        self.nodeSerialLayout = QHBoxLayout(self.nodeSerial)
        self.serialLineEdit = LineEdit(self.nodeSerial)
        self.editButton = TransparentToolButton(self.nodeSerial)
        self.nodeTypeComboBox = ComboBox(self.contentView)
        self.nodePathBreadcrumb = BreadcrumbBar(self.contentView)
        self.submitButton = PrimaryPushButton(self.contentView)
        self.closeButton = TransparentToolButton(FluentIcon.CLOSE, self)
        # 初始化界面布局
        self._initLayout()
        # 初始化组件
        self._initWidget()

    def _initWidget(self):
        # 设置控件尺寸
        self.titleLabel.setFixedHeight(58)
        self.nodeTypeComboBox.setMaximumSize(QSize(200, 16777215))
        self.nodeSerial.setFixedHeight(40)
        self.nodeNameLineEdit.setMinimumWidth(300)
        self.submitButton.setMaximumWidth(100)
        self.closeButton.setFixedSize(32, 32)
        self.closeButton.setIconSize(QSize(12, 12))
        self.submitButton.setFixedWidth(100)
        self.editButton.setMinimumHeight(35)
        # 按钮图标
        self.editButton.setIcon(QIcon('../images/icon/EditSerial_black.svg'))
        # 设置节点路径
        for p in self.path:
            self.nodePathBreadcrumb.addItem(p, p)

        self.nodePathBreadcrumb.setToolTip(' > '.join(self.path))
        self.nodePathBreadcrumb.installEventFilter(ToolTipFilter(self.nodePathBreadcrumb, 0, ToolTipPosition.TOP))
        # 格栅布局
        self.gridLayout.setSizeConstraint(QLayout.SetDefaultConstraint)
        # 不允许点击
        self.nodePathBreadcrumb.setMinimumWidth(300)
        self.nodePathBreadcrumb.setEnabled(False)
        # 设置标签文本
        self.titleLabel.setText('新建节点')
        self.nodePathLabel.setText('节点路径')
        self.nodeTypeLabel.setText('节点类型')
        self.nodeSerialLabel.setText('节点编号')
        self.nodeNameLabel.setText('节点标题')
        # 设置按钮文本
        self.submitButton.setText('提交')
        # 初始默认状态
        if self.serial:
            self.serialLineEdit.setEnabled(False)
            self.serialLineEdit.setText(self.serial)
        else:
            self.serialLineEdit.setEnabled(True)
            self.editButton.setVisible(False)
        # 输入框使能清空功能
        self.serialLineEdit.setClearButtonEnabled(True)
        self.nodeNameLineEdit.setClearButtonEnabled(True)
        # 下拉框赋值
        self.nodeTypeComboBox.addItems(['一级模块', '二级模块', '三级模块', '四级模块', '五级模块'])
        if len(self.path) <= 4:
            self.nodeTypeComboBox.setCurrentIndex(len(self.path) - 1)
            self.nodeTypeComboBox.setEnabled(False)
        else:
            self.nodeTypeComboBox.setCurrentIndex(-1)
            # 设置 Placeholder 文本
            self.nodeTypeComboBox.setPlaceholderText('请选择节点类型')
        # 设置输入值检验
        validator = QRegExpValidator(QRegExp('[0-9\\.]+'), self.serialLineEdit)
        self.serialLineEdit.setValidator(validator)
        # 初始默认状态
        self.closeButton.setAutoRepeat(False)
        self.closeButton.setAutoRaise(True)
        self.submitButton.setEnabled(False)
        # 自定义控件样式
        qss = getStyleSheetFromFile('../qss/add_node_flyout_view.qss')
        self.setStyleSheet(qss)

        self._setQss()
        self._connectSignalToSlot()

    def _initLayout(self):
        self.viewLayout.setSpacing(0)
        self.viewLayout.setContentsMargins(10, 6, 6, 0)
        self.verticalLayout.setContentsMargins(20, 0, 16, 16)

        self.nodeSerialLayout.setContentsMargins(0, 0, 0, 0)
        self.nodeSerialLayout.setSpacing(0)
        self.nodeSerialLayout.addWidget(self.serialLineEdit)
        self.nodeSerialLayout.addWidget(self.editButton)

        self.gridLayout.setContentsMargins(0, 0, 0, 0)
        self.gridLayout.setHorizontalSpacing(15)
        self.gridLayout.setVerticalSpacing(12)
        self.gridLayout.addWidget(self.nodePathLabel, 0, 0, 1, 1, Qt.AlignLeft)
        self.gridLayout.addWidget(self.nodePathBreadcrumb, 0, 1, 1, 4)
        self.gridLayout.addWidget(self.nodeTypeLabel, 1, 0, 1, 1, Qt.AlignLeft)
        self.gridLayout.addWidget(self.nodeTypeComboBox, 1, 1, 1, 1)
        self.gridLayout.addWidget(self.nodeSerialLabel, 2, 0, 1, 1, Qt.AlignLeft)
        self.gridLayout.addWidget(self.nodeSerial, 2, 1, 1, 2)
        self.gridLayout.addWidget(self.nodeNameLabel, 3, 0, 1, 1, Qt.AlignLeft)
        self.gridLayout.addWidget(self.nodeNameLineEdit, 3, 1, 1, 3, Qt.AlignLeft)

        self.gridLayout.addWidget(self.submitButton, 4, 1, 1, 1)
        self.gridLayout.setColumnStretch(1, 1)

        self.verticalLayout.addWidget(self.titleLabel, 0, Qt.AlignLeft)
        self.verticalLayout.addSpacing(10)
        self.verticalLayout.addWidget(self.contentView)
        self.viewLayout.addLayout(self.verticalLayout)
        self.viewLayout.addWidget(self.closeButton, 0, Qt.AlignTop | Qt.AlignRight)

    def _setQss(self):
        self.viewLayout.setObjectName('viewLayout')
        self.verticalLayout.setObjectName('verticalLayout')
        self.titleLabel.setObjectName('titleLabel')
        self.contentView.setObjectName('contentView')
        self.gridLayout.setObjectName('gridLayout')
        self.nodeNameLabel.setObjectName('nodeNameLabel')
        self.nodePathLabel.setObjectName('nodePathLabel')
        self.nodeSerialLabel.setObjectName('nodeSerialLabel')
        self.nodeTypeLabel.setObjectName('nodeTypeLabel')
        self.nodeNameLineEdit.setObjectName('nodeNameLineEdit')
        self.nodeSerial.setObjectName("nodeSerial")
        self.nodeSerialLayout.setObjectName('nodeSerialLayout')
        self.serialLineEdit.setObjectName('serialLineEdit')
        self.editButton.setObjectName('editButton')
        self.nodeTypeComboBox.setObjectName('nodeTypeComboBox')
        self.nodePathBreadcrumb.setObjectName('nodePathBreadcrumb')
        self.submitButton.setObjectName('submitButton')
        self.closeButton.setObjectName('closeButton')

    def _connectSignalToSlot(self):
        """槽函数"""
        self.nodeTypeComboBox.currentIndexChanged.connect(self.isValid)
        self.editButton.clicked.connect(lambda: self.serialLineEdit.setEnabled(True))  # type:ignore
        self.serialLineEdit.textEdited.connect(self.isValid)  # type:ignore
        self.nodeNameLineEdit.textEdited.connect(self.isValid)  # type:ignore
        self.submitButton.clicked.connect(self.submit)  # type:ignore
        self.closeButton.clicked.connect(self.close)  # type:ignore

    def isValid(self):
        if self.nodeNameLineEdit.text().strip().upper() in self.nodeNames:
            self.submitButton.setEnabled(False)
            return

        if all([self.nodeTypeComboBox.currentText(), self.serialLineEdit.text(), self.nodeNameLineEdit.text()]):
            self.submitButton.setEnabled(True)
        else:
            self.submitButton.setEnabled(False)

    def submit(self):
        properties = dict(
            name=self.nodeNameLineEdit.text().strip(),
            type=self.nodeTypeComboBox.currentIndex(),
            serial=self.serialLineEdit.text().strip(),
        )
        self.properties.emit(properties)  # type: ignore
        self.close()

    def closeEvent(self, e):
        self.deleteLater()

        super().closeEvent(e)
        self.closed.emit()  # type:ignore

class Demo(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.view = QVBoxLayout(self)
        self.btn = PrimaryPushButton('示例', self)
        self.view.addWidget(self.btn, 0, Qt.AlignCenter)
        self.btn.clicked.connect(self.showCard)

    def showCard(self):
        view = AddNodeFlyoutViewCard(['1', '2', '3'], ['abc', 'efd'], '20.1.5')
        Flyout.make(view, self.btn, self, aniType=FlyoutAnimationType.SLIDE_RIGHT)

if __name__ == "__main__":
    app = QApplication([])
    window = Demo()
    window.show()
    app.exec_()
zhiyiYo commented 7 months ago

这个大概率是 qt 的 bug

zhiyiYo commented 7 months ago

把 LineEdit 换成 QLineEdit 看看会不会有此问题

keymou commented 7 months ago

把 LineEdit 换成 QLineEdit 看看会不会有此问题

换成QLineEdit也有问题

keymou commented 7 months ago

ToolTipFilter 对 组件 BreadcrumbBar不适用,是吗

zhiyiYo commented 7 months ago

可能是打开flyout之前是英文输入法导致的,先切换至中文输入法再打开flyout就正常了

zhiyiYo commented 7 months ago

工具提示的问题还不确定是怎么回事,可能是弹出位置计算错了

keymou commented 7 months ago

可能是打开flyout之前是英文输入法导致的,先切换至中文输入法再打开flyout就正常了

本地试了下打开Flyout前就切换到中文输入法还是不行 :frowning:

keymou commented 7 months ago

修改成

class AddNodeFlyoutViewCard(FlyoutViewBase):
    closed = Signal()
    properties = Signal(dict)

    def __init__(self, node_names: Optional[List[str]] = None, path: Optional[List[str]] = None,
                 serial: Optional[str] = None, parent=None):
        """

        :param node_names: 节点名称集合
        :param path: 节点路径列表
        :param serial: 节点编号
        :param parent:
        """
        super().__init__(parent)
        self.nodeNames = node_names
        self.path = path
        self.serial = serial

        self.viewLayout = QHBoxLayout(self)
        self.verticalLayout = QVBoxLayout()
        self.titleLabel = QLabel(self)
        self.contentView = QFrame(self)
        self.gridLayout = QGridLayout(self.contentView)
        self.nodeNameLabel = QLabel(self.contentView)
        self.nodePathLabel = QLabel(self.contentView)
        self.nodeSerialLabel = QLabel(self.contentView)
        self.nodeTypeLabel = QLabel(self.contentView)
        self.nodeNameLineEdit = LineEdit(self.contentView)
        self.nodeSerial = QFrame(self.contentView)
        self.nodeSerialLayout = QHBoxLayout(self.nodeSerial)
        self.serialLineEdit = LineEdit(self.nodeSerial)
        self.editButton = TransparentToolButton(self.nodeSerial)
        self.nodeTypeComboBox = ComboBox(self.contentView)
        self.nodePathBreadcrumb = BreadcrumbBar(self.contentView)
        self.submitButton = PrimaryPushButton(self.contentView)
        self.closeButton = TransparentToolButton(FluentIcon.CLOSE, self)
        # 初始化界面布局
        self._initLayout()
        # 初始化组件
        self._initWidget()
        self.setShadowEffect()

        self.setAttribute(Qt.WA_TranslucentBackground)
        # self.setWindowFlags(Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint | Qt.Popup)
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint)

Demo

     def showCard(self):
        view = AddNodeFlyoutViewCard(['1', '2', '3'], ['abc', 'efd'], '20.1.5')
        view.show()  

不使用Flyout,就可以正常输入中文字符。

image

但其中改为: self.setWindowFlags(Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint | Qt.Popup) 后,就无法输入中文了 :confused:

AlexZhu2001 commented 7 months ago

本地试了下打开Flyout前就切换到中文输入法还是不行 😦

这个确实是Qt的bug,但是Qt官方认为这样做是正确的。Qt官方的bug追踪器显示这个bug自Qt4就有了,原因是Qt官方认为由于输入法本身使用了弹出窗口,所以弹出窗口不应当继续激活输入法以免造成递归,所以设置了Popup的窗口均不会调用processActivatedEvent(),也就不会调用_q_updateFocusObject(),而输入法是在这个函数中启用的。目前看到的解决方法是在showEvent里调用一下activateWindow方法来激活窗口,这样就会调用到_q_updateFocusObject(),但不确定这一做法会不会带来其他的负面影响。

工具提示的问题还不确定是怎么回事,可能是弹出位置计算错了

关于这个部分,如果你使用ToolTipFilter后把BreadcrumbBar的Enabled设置为False(在你的最小示例里是这么做的),那么ToolTip就不会显示出来,因为_canShowToolTip会判断这个条件(见下图)。不过我认为Disabled的控件应该也可以显示tooltip吧,不如把这里改成isVisible,毕竟不可显示的控件没必要显示tooltip 图片1 不过即使你设置了Enabled,显示位置好像也不太对,这个应该是位置计算的问题了 图片2

keymou commented 7 months ago

本地试了下打开Flyout前就切换到中文输入法还是不行 😦

这个确实是Qt的bug,但是Qt官方认为这样做是正确的。Qt官方的bug追踪器显示这个bug自Qt4就有了,原因是Qt官方认为由于输入法本身使用了弹出窗口,所以弹出窗口不应当继续激活输入法以免造成递归,所以设置了Popup的窗口均不会调用processActivatedEvent(),也就不会调用_q_updateFocusObject(),而输入法是在这个函数中启用的。目前看到的解决方法是在showEvent里调用一下activateWindow方法来激活窗口,这样就会调用到_q_updateFocusObject(),但不确定这一做法会不会带来其他的负面影响。

工具提示的问题还不确定是怎么回事,可能是弹出位置计算错了

关于这个部分,如果你使用ToolTipFilter后把BreadcrumbBar的Enabled设置为False(在你的最小示例里是这么做的),那么ToolTip就不会显示出来,因为_canShowToolTip会判断这个条件(见下图)。不过我认为Disabled的控件应该也可以显示tooltip吧,不如把这里改成isVisible,毕竟不可显示的控件没必要显示tooltip 图片1 不过即使你设置了Enabled,显示位置好像也不太对,这个应该是位置计算的问题了 图片2

暂时添加了你说的解决方法,谢谢 :smile:!

    def showEvent(self, a0) -> None:
        # 激活窗口,解决输入框无法输入中文的 bug
        self.activateWindow()
        super().showEvent(a0)
zhiyiYo commented 7 months ago

下个版本将修复此bug