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

Posted

技术标签:

【中文标题】如何在 leaveEvent 上强制更新小部件的样式?【英文标题】:How to force the style update of a widget on leaveEvent? 【发布时间】:2022-01-16 13:42:54 【问题描述】:

我正在尝试使用 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】:

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

问题在于改变窗口状态(最大化或“标准化”)不是由 Qt 直接完成的,而是对操作系统的请求,因此调整大小可能不会立即发生。结果是,在该过程中,小部件(或者更好的是 Qt)仍然相信小部件在鼠标光标下,即使它不是,这是因为 Qt 没有尚未处理整个调整大小事件队列。

现在我没有看到直接的解决方案(我怀疑是否会有,因为它完全取决于底层的窗口管理器),但是当小部件收到调整大小事件时我们确实知道小部件已调整大小。这意味着我们可以自行检查几何和光标位置并更新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()

不幸的是,这有一些缺点。调整大小可以从外部源完成,即使按钮实际上部分被另一个窗口覆盖,该按钮仍然可以虚拟地位于鼠标下方。

一种可能的方法是在调整窗口大小时使用QApplication.widgetAt() 检查鼠标下的实际小部件,但是,正如文档还建议的那样,该功能可能缓慢。如果启用了不透明调整大小(从边缘调整窗口大小时所有小部件内容都会动态调整大小),该函数实际上在性能方面非常代价高昂,因为即使是单个像素的大小变化也可能会检查给定坐标的整个小部件树。

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

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() 已经返回一个布尔值;将其更改为:

hide_maximize_button = self.maximize_button.isVisible()

【讨论】:

谢谢!这似乎有点矫枉过正,但它成功了。您是否知道为什么手动调用按钮上的 leaveEvent 如下:button.leaveEvent(QEvent(QEvent.Leave))button.leaveEvent(QEvent(QEvent.HoverLeave)) 后跟 button.update() 不能用于更新小部件样式? @Pauuuuuuul 默认情况下,QPushButton(以及继承的类 QAbstractButton 和 QWidget)的 enterEventleaveEvent 什么都不做。它们仅提供给需要处理这些事件的小部件(如项目视图)。向这些事件处理程序发送错误的事件类型也是错误的:leaveEvent 需要 QEvent.Leave,因此您实际上应该通过应用程序 post 事件:QApplication.postEvent(self.resize_button, QEvent(QEvent.HoverLeave))。但这不会有太大变化,因为默认实现只调用 update() 并且不会设置/取消设置 WA_UnderMouse 标志。 @Pauuuuuuul 那是Qt的责任,它又回到了原来的问题:状态改变由系统处理:Qt发送请求,系统调整窗口outside Qt 事件队列,Qt 接收窗口的调整大小并将所需的调整大小事件中继到受其影响的小部件,但调整大小不会更改 WA_UnderMouse 标志。虽然它可能会导致“错误”,但我相信这是意料之中的:正如所解释的,不透明的大小调整可能非常密集,而widgetAt 是一个可能要求很高的递归函数,因此出于性能原因可能不会对其进行更新。 感谢您的宝贵时间!你的回答非常有用:)

以上是关于如何在 leaveEvent 上强制更新小部件的样式?的主要内容,如果未能解决你的问题,请参考以下文章

强制 Android 小部件更新

强制 Android 小部件更新

电池高效的 android 小部件,每分钟更新一次

强制在 contentsRect 而不是 rect 上绘制小部件

如何强制 tkinter 文本小部件保持在一行

如何在状态更改后强制小部件重新创建自己?