有没有办法堆叠小部件并忽略重叠?

Is there a way to stack widgets and ignore overlapping?

我正在使用 PyQt5 制作 GUI,当您将鼠标悬停在小部件上时,我让小部件的背景颜色发生变化。但是,当我将鼠标悬停在一个小部件上时,它下面的小部件重叠并且背景在底部被切断。

这是因为我制作背景的方式,我不得不 space 小部件非常靠近并增加边框大小以实现背景效果,但它会像我说的那样扰乱悬停前。有什么方法可以忽略这种重叠,或者我可以使用一种不同的方法来绘制背景而不必重叠小部件?

这是我想要的截图,以防我的解释不好(底部的小部件工作正常,因为它下面没有任何东西可以重叠)

我也会在这里附上我的代码,这样你就可以看到我做了什么

from PyQt5 import QtGui
from PyQt5.QtWidgets import * 
from PyQt5.QtGui import * 
from PyQt5.QtCore import * 
import sys

class MainWindow(QWidget):

    def __init__(self):
        super(MainWindow, self).__init__()
        self.layout = QVBoxLayout()
        self.layout.addWidget(MyBar(self))
        self.setLayout(self.layout)
        self.layout.setContentsMargins(0,0,0,0)
        self.setFixedSize(450,550)
        self.setWindowFlags(Qt.FramelessWindowHint)
        self.setStyleSheet('background-color: #121A2B;')

        self.checkbox_style='''
        QCheckBox
        {
            background-color : rgb(25,34,52);
            border-radius : 5px;
            spacing : 10px;
            padding : 15px;
            min-height : 15px;
        }
        QCheckBox::unchecked
        {
            color : rgb(159,172,168);
        }
        QCheckBox::checked
        {
            color : rgb(217,223,227);
        }
        QCheckBox::indicator
        {
            border : 2px solid rgb(105, 139, 194);
            width : 12px;
            height : 12px;
            border-radius : 8px;
        }
        QCheckBox::indicator:checked
        {
            image : url(red.png);
            border : 2px solid rgb(221, 54, 77);
        }
        QCheckBox::hover
        {
            background-color: #263450
        }
        '''

        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(11)
        font.setBold(True)

        self.checkbox_layout = QVBoxLayout()

        self.checkbox1 = QCheckBox('checkbox1')
        self.checkbox_layout.addWidget(self.checkbox1)
        self.checkbox1.setFont(font)
        self.checkbox1.setStyleSheet(self.checkbox_style)

        self.checkbox2 = QCheckBox('checkbox2')
        self.checkbox_layout.addWidget(self.checkbox2)
        self.checkbox2.setFont(font)
        self.checkbox2.setStyleSheet(self.checkbox_style)
        
        self.checkbox3 = QCheckBox('checkbox3')
        self.checkbox_layout.addWidget(self.checkbox3)
        self.checkbox3.setFont(font)
        self.checkbox3.setStyleSheet(self.checkbox_style)

        self.checkbox3 = QCheckBox('checkbox4')
        self.checkbox_layout.addWidget(self.checkbox3)
        self.checkbox3.setFont(font)
        self.checkbox3.setStyleSheet(self.checkbox_style)

        self.layout.addLayout(self.checkbox_layout)
        self.checkbox_layout.setContentsMargins(15,7,290,350)
        self.setLayout(self.layout)
        self.layout.setAlignment(Qt.AlignTop)

class MyBar(QWidget):

    def __init__(self, parent):
        super(MyBar, self).__init__()
        self.parent = parent
        self.layout = QHBoxLayout()
        self.layout.setContentsMargins(0,0,0,0)
        self.title = QLabel("")

        font = QtGui.QFont()
        font.setFamily("Bebas Neue")
        font.setPointSize(25)
        font.setBold(False)
        self.title.setFont(font)

        self.btn_close = QPushButton()
        self.btn_close.clicked.connect(self.btn_close_clicked)
        self.btn_close.setFixedSize(40, 35)
        self.btn_close.setStyleSheet("QPushButton::hover"
        "{"
            "background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,stop: 0 #fc0703, stop: 1 #a10303);"
            "border : none;"
        "}"
        "QPushButton"
        "{"
        "background-color : rgb(25, 34, 52)"
        "}")
        self.btn_close.setFlat(True)
        self.btn_close.setIcon(QIcon("C:/Users/User/Documents/GitHub/guirebuild/close.png"))
        self.btn_close.setIconSize(QSize(15, 15))

        self.title.setFixedHeight(53)
        self.title.setAlignment(Qt.AlignTop)
        self.layout.addWidget(self.title)
        
        self.layout.addWidget(self.btn_close,alignment=Qt.AlignTop)

        self.title.setStyleSheet("""
            background-color: rgb(25, 34, 52);
            color: white;
        """)

        self.setLayout(self.layout)

        self.start = QPoint(0, 0)
        self.pressing = False

    def resizeEvent(self, QResizeEvent):
        super(MyBar, self).resizeEvent(QResizeEvent)
        self.title.setFixedWidth(self.parent.width())

    def mousePressEvent(self, event):
        self.start = self.mapToGlobal(event.pos())
        self.pressing = True

    def mouseMoveEvent(self, event):
        if self.pressing:
            self.end = self.mapToGlobal(event.pos())
            self.movement = self.end-self.start
            self.parent.setGeometry(self.mapToGlobal(self.movement).x(),
                                self.mapToGlobal(self.movement).y(),
                                self.parent.width(),
                                self.parent.height())
            self.start = self.end

    def mouseReleaseEvent(self, QMouseEvent):
        self.pressing = False

    def btn_close_clicked(self):
        self.parent.close()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    mw = MainWindow()
    mw.show()
    sys.exit(app.exec_())

问题是由于新小部件的堆叠,最简单的解决方案是为这些小部件设置 透明 背景,如果您确定它们不会悬停时有不同的背景:

        self.checkbox_style='''
        QCheckBox
        {
            background-color : rgb(0, 0, 0, 0);
            ...

不幸的是,这不会真正解决您的问题,因为您的实施中存在严重的逻辑问题。

首先,基于整体布局设置布局的硬编码内容边距是一个严重的问题,原因有很多:

  • 它会强制您根据菜单内容更改那些硬编码的边距;
  • 它基于你的关于当前可用字体的假设,这可能会使 UI 元素部分(或完全)隐藏;
  • 它不允许您向布局正确添加更多对象;

经过认真考虑以上事项(非常重要,应该永远不会被低估),问题是,虽然 Qt 允许通过 QSS 填充将项目布置在它们可能位置的“外部”,但您需要考虑小部件 z 级别,它默认堆叠一个新小部件高于任何其他先前创建的 sibling 小部件(具有共同祖先的小部件),这就是为什么您会看到部分隐藏的复选框。

基本解决方案是安装一个事件过滤器,检查它是否是 HoverMove 事件类型,然后调用 raise_() 以确保它位于 之上 任何其他兄弟姐妹:

class MainWindow(QWidget):
    def __init__(self):
        # ...
        for check in self.findChildren(QCheckBox):
            check.installEventFilter(self)

    def eventFilter(self, obj, event):
        if event.type() == event.HoverMove:
            obj.raise_()
        return super().eventFilter(obj, event)

同样,这只能部分解决您的问题,因为您很快就会发现您的硬编码尺寸和位置 不会运行良好(请记住,您在屏幕上看到的是 永远不会 其他人在他们屏幕上看到的)。

一个可能的解决方案是为菜单创建一个单独的 QWidget 子类,在其中添加复选框,并始终在考虑填充的情况下计算最大高度。然后,为确保小部件框正确放置在应放置的位置,您应该添加一个 addStretch() 或至少一个“主小部件”(通常用于“主 window”)。

class Menu(QWidget):
    padding = 15
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout(self)
        self.setMaximumHeight(0)
        layout.setContentsMargins(0, 15, 0, 15)

        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(11)
        font.setBold(True)
        self.setFont(font)

        self.setStyleSheet('''
        QCheckBox
        {
            background-color : rgb(25,34,52);
            border-radius : 5px;
            spacing : 10px;
            padding : 15px;
            min-height : 15px;
        }
        QCheckBox::unchecked
        {
            color : rgb(159,172,168);
        }
        QCheckBox::checked
        {
            color : rgb(217,223,227);
        }
        QCheckBox::indicator
        {
            border : 2px solid rgb(105, 139, 194);
            width : 12px;
            height : 12px;
            border-radius : 8px;
        }
        QCheckBox::indicator:checked
        {
            image : url(red.png);
            border : 2px solid rgb(221, 54, 77);
        }
        QCheckBox::hover
        {
            background-color: #263450
        }
        ''')
        self.checks = []

    def addOption(self, option):
        checkBox = QCheckBox(option)
        self.checks.append(checkBox)
        checkBox.installEventFilter(self)
        self.layout().addWidget(checkBox)
        count = len(self.checks)
        self.setMaximumHeight(count * checkBox.sizeHint().height() - 15)

    def eventFilter(self, obj, event):
        if event.type() == event.HoverMove:
            obj.raise_()
        return super().eventFilter(obj, event)


class MainWindow(QWidget):
    def __init__(self):
        # ...
        self.menu = Menu()
        self.layout.addWidget(self.menu, alignment=Qt.AlignTop)

        for i in range(4):
            self.menu.addOption('checkbox{}'.format(i + 1))

        self.layout.addStretch()

请注意,在上面的代码中,我仍然使用事件过滤器,同时仍然使用显式 opaque 背景颜色,如果您对父背景没问题,则可以避免安装和使用事件过滤器,仅将默认背景设置为透明 rgb(0, 0, 0, 0).