如何在 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())
重现不良行为的步骤:
- 单击“最大化”按钮
- 点击“调整大小”按钮
如果您对如何修复此错误有任何想法,我将不胜感激。
我不能总是重现这个问题,但我明白了。
问题在于改变 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()
我正在尝试使用 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())
重现不良行为的步骤:
- 单击“最大化”按钮
- 点击“调整大小”按钮
如果您对如何修复此错误有任何想法,我将不胜感激。
我不能总是重现这个问题,但我明白了。
问题在于改变 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()