如何在 leaveEvent 上强制更新小部件的样式?

How to force the style update of a widget on leaveEvent?

我正在尝试使用 PySide6 编写应用程序代码,作为项目的一部分,我想实现自己的标题栏。

问题是,当我点击“最大化”按钮,然后点击“调整大小”按钮时,“最大化”按钮的样式似乎一直存在,就好像鼠标指针悬停在它上面一样.通过隐藏按钮,不会调用 leaveEvent(通常在鼠标指针离开小部件的边界时调用),但即使我手动调用 leaveEvent(使用 QEvent.Leave 有效负载)和更新最大化按钮上的方法,它的样式仍然没有更新。

您将在下面找到一个工作示例:


import sys
from PySide6.QtCore import Slot
from PySide6.QtWidgets import QApplication, QWidget, QFrame, QPushButton, QLabel, QHBoxLayout


def dict_to_stylesheet(properties: dict[str, dict[str, str]]) -> str:
    stylesheet = ""

    for q_object in properties:

        stylesheet += q_object + " { "

        for style_property in properties[q_object]:
            stylesheet += f"{style_property}: {properties[q_object][style_property]}; "

        stylesheet += " } "

    return stylesheet


class MainWindow(QWidget):

    def __init__(self):
        super().__init__()

        # ---------- Attributes ----------
        self.mouse_position = None

        # ---------- Styling attributes ----------
        self.width = 1080
        self.height = 720
        self.minimum_width = 960
        self.minimum_height = 540
        self.background_color = "#EFEFEF"

        self.dict_stylesheet = {
            "QWidget": {
                "background-color": self.background_color
            }
        }

        # ---------- UI elements ----------
        self.title_bar = TitleBar(self)

        # ---------- Layout ----------
        self.layout = QHBoxLayout(self)

        # ---------- Initialize UI ----------
        self.setup_ui()

    def setup_ui(self):
        # ---------- QMainWindow (self) ----------
        self.setMinimumSize(self.minimum_width, self.minimum_height)
        self.resize(self.width, self.height)
        self.setStyleSheet(dict_to_stylesheet(self.dict_stylesheet))

        # ---------- Layout ----------
        self.layout.setSpacing(0)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.addWidget(self.title_bar)


class TitleBar(QFrame):

    def __init__(self, main_window):
        super().__init__()

        # ---------- Attributes ----------
        self.main_window = main_window

        # ---------- Styling attributes ----------
        self.height = 30
        self.background_color = "#AAAAAA"

        self.dict_stylesheet = {
            "QFrame": {
                "background-color": self.background_color,
            },
            "QPushButton": {
                "border": "none",
                "background-color": self.background_color,
                "margin-left": "5px",
                "margin-right": "5px",
                "padding-left": "2px",
                "padding-right": "2px"
            },
            "QPushButton:hover": {
                "background-color": "#888888",
            },
            "QPushButton:pressed": {
                "background-color": "#666666"
            }
        }

        # ---------- UI elements ----------

        # QPushButtons
        self.minimize_button = QPushButton("Minimize")
        self.maximize_button = QPushButton("Maximize")
        self.resize_button = QPushButton("Resize")
        self.close_button = QPushButton("Close")

        # QLabels
        self.title_label = QLabel("A title")

        # ---------- Layout ----------
        self.layout = QHBoxLayout(self)

        # ---------- Event handling ----------
        self.minimize_button.clicked.connect(self.minimize_app)
        self.maximize_button.clicked.connect(self.maximize_app)
        self.resize_button.clicked.connect(self.resize_app)
        self.close_button.clicked.connect(self.close_app)

        # ---------- Initialize UI ----------
        self.setup_ui()

    def setup_ui(self):
        # ---------- QFrame (self) ----------
        self.setFixedHeight(self.height)
        self.setStyleSheet(dict_to_stylesheet(self.dict_stylesheet))

        # ---------- Title QLabel ----------
        self.title_label.setFixedHeight(self.height)
        self.title_label.setStyleSheet("margin-left: 5px")

        # ---------- QPushButtons ----------
        self.minimize_button.setFixedHeight(self.height)
        self.maximize_button.setFixedHeight(self.height)
        self.resize_button.setFixedHeight(self.height)
        self.resize_button.setHidden(True)
        self.close_button.setFixedHeight(self.height)

        # ---------- Layout ----------
        self.layout.setSpacing(0)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.addWidget(self.title_label)
        self.layout.addStretch(1)
        self.layout.addWidget(self.minimize_button)
        self.layout.addWidget(self.maximize_button)
        self.layout.addWidget(self.resize_button)
        self.layout.addWidget(self.close_button)

    @Slot()
    def minimize_app(self):
        self.main_window.showMinimized()

    @Slot()
    def maximize_app(self):
        self.main_window.showMaximized()

        # Update layout
        self.toggle_resize_and_maximize_buttons()

    @Slot()
    def resize_app(self):
        self.main_window.showNormal()

        # Update layout
        self.toggle_resize_and_maximize_buttons()

    @Slot()
    def close_app(self):
        self.main_window.close()

    def toggle_resize_and_maximize_buttons(self) -> None:
        hide_maximize_button = True if self.maximize_button.isVisible() else False
        hide_resize_button = not hide_maximize_button

        self.maximize_button.setHidden(hide_maximize_button)
        self.resize_button.setHidden(hide_resize_button)


if __name__ == '__main__':
    app = QApplication()

    main = MainWindow()
    main.show()

    sys.exit(app.exec())

重现不良行为的步骤:

  1. 单击“最大化”按钮
  2. 点击“调整大小”按钮

如果您对如何修复此错误有任何想法,我将不胜感激。

我不能总是重现这个问题,但我明白了。

问题在于改变 window 状态(最大化或“规范化”)不是由 Qt 直接完成的,而是对 OS 完成的请求,因此调整大小可能不会立即发生。结果是,在该过程中,小部件(或者更好的是 Qt)仍然相信小部件在鼠标光标下,即使它不是,那是因为 Qt尚未处理整个调整大小事件 queue。

现在我没有看到直接的解决方案(我怀疑是否会有,因为它完全取决于底层的 window 经理),但是 当小部件收到调整大小事件时,我们 do 知道小部件已调整大小。这意味着我们可以自行检查几何图形和光标位置并更新 Qt.WA_UnderMouse 属性(通常在调整大小完成后由 Qt 设置)。

class TitleBar(QFrame):
    # ...
    def resizeEvent(self, event):
        super().resizeEvent(event)
        cursor = QCursor.pos()
        for button in self.resize_button, self.maximize_button:
            rect = button.rect().translated(button.mapToGlobal(QPoint()))
            if not button.isVisible():
                continue
            underMouse = rect.contains(cursor)
            if button.testAttribute(Qt.WA_UnderMouse) != underMouse:
                button.setAttribute(Qt.WA_UnderMouse, underMouse)
                button.update()

不幸的是,这有一些缺点。调整大小可以从外部源完成,即使按钮实际上被另一个 window.

部分覆盖,它仍然可以虚拟地位于鼠标下方

一种可能的方法是在 window 调整大小时使用 QApplication.widgetAt() 检查鼠标下的 actual 小部件,但是,正如文档还提示,函数可以slow。如果启用了不透明调整大小(当从边缘调整 window 大小时,所有小部件内容都会动态调整大小),该功能实际上 非常 性能方面的成本,即使是单个像素大小的变化可能会检查给定坐标的整个小部件树。

由于您可能只在 window 状态更改时才对更新按钮感兴趣,因此解决方法是为状态更改设置一个内部状态标志,并仅在需要时通过安装事件调用更新在主window(标题栏的parent)上过滤,最终在状态实际改变时触发函数:

class TitleBar(QFrame):
    windowStateChange = False
    def __init__(self, main_window):
        super().__init__()
        main_window.installEventFilter(self)
        # ...

    def eventFilter(self, obj, event):
        if event.type() == QEvent.Type.WindowStateChange and obj.windowState():
            self.windowStateChange = True
        elif event.type() == QEvent.Type.Resize and self.windowStateChange:
            self.windowStateChange = False
            cursor = QCursor.pos()
            widgetUnderMouse = QApplication.widgetAt(cursor)
            for button in self.resize_button, self.maximize_button:
                if button.isVisible():
                    underMouse = widgetUnderMouse == button
                    if underMouse != button.testAttribute(Qt.WA_UnderMouse):
                        button.setAttribute(Qt.WA_UnderMouse, underMouse)
                    button.update()
        return super().eventFilter(obj, event)

请注意,以下行不必要地复杂:

hide_maximize_button = True if self.maximize_button.isVisible() else False

isVisible() 已经 returns 一个布尔值;将其更改为:

hide_maximize_button = self.maximize_button.isVisible()