制作高度定制的、可悬停的、可重叠的、小部件

Making Highly Customized, Hoverable, Overlappable, Widgets

我想使用 PyQt5 在 UI 设计中提升我的游戏水平。感觉PyQt5中UI自定义的资源不好找。可以尝试制作个性化的widget,但是整体方法好像non-standardized.

我需要构建一个可悬停、可与其他小部件重叠且高度自定义的箭头小部件。正如我在 this 教程和其他一些帖子中所读到的,使用 paintEvent 可以完全满足您的需要。因此,这就是我尝试的方法,但总的来说,我觉得该方法非常混乱,我想要一些关于构建复杂的定制通用小部件的指南。这是我拥有的:

自定义形状: 我根据 this

构建了我的代码

可悬停 属性: 我到处都读到修改项目 styleSheet 通常是可行的方法,特别是如果你想让你的 Widget 通用并适应颜色,问题是我无法找到如何正确使用 self.palette 来获取 QApplication 样式表的当前颜色。我觉得我可能不得不使用 enterEventleaveEvent,但我试图在这些函数中用画家重绘整个小部件,它说

QPainter::begin: Painter already active
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::setRenderHint: Painter must be active to set rendering hints

Overlappable 属性: 我找到了一个之前的 似乎找到了解决方案:创建第二个小部件 children的主要小部件,以便能够移动 children。我试过了,但它似乎不想移动,无论我给小部件的位置如何。

这是我的代码:

import sys
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QGraphicsDropShadowEffect, QApplication, QFrame, QPushButton
from PyQt5.QtCore import Qt, QPoint, QLine
from PyQt5.QtGui import QPainter, QPen, QColor, QPalette


class MainWidget(QWidget):
    def __init__(self):
        super(MainWidget, self).__init__()
        self.resize(500, 500)
        self.layout = QHBoxLayout()
        self.setLayout(self.layout)
        self.myPush = QPushButton()
        self.layout.addWidget(self.myPush)

        self.arrow = ArrowWidget(self)
        position = QPoint(-40, 0)

        self.layout.addWidget(self.arrow)
        self.arrow.move(position)


class ArrowWidget(QWidget):
    def __init__(self, parent=None):
        super(ArrowWidget, self).__init__(parent)
        self.setWindowFlag(Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.w = 200
        self.h = 200
        self.blurRadius = 20
        self.xO = 0
        self.yO = 20
        self.resize(self.w, self.h)
        self.layout = QHBoxLayout()
        # myFrame = QFrame()
        # self.layout.addWidget(myFrame)
        self.setLayout(self.layout)
        self.setStyleSheet("QWidget:hover{border-color: rgb(255,0,0);background-color: rgb(255,50,0);}")
        shadow = QGraphicsDropShadowEffect(blurRadius=self.blurRadius, xOffset=self.xO, yOffset=self.yO)
        self.setGraphicsEffect(shadow)

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.begin(self)
        # painter.setBrush(self.palette().window())
        # painter.setPen(QPen(QPalette, 5))

        ok = self.frameGeometry().width()/2-self.blurRadius/2-self.xO/2
        oky = self.frameGeometry().height()/2-self.blurRadius/2-self.yO/2

        painter.drawEllipse(QPoint(self.frameGeometry().width()/2-self.blurRadius/2-self.xO/2, self.frameGeometry().height()/2-self.blurRadius/2-self.yO/2), self.w/2-self.blurRadius/2-self.yO/2-self.xO/2, self.h/2-self.blurRadius/2-self.yO/2-self.xO/2)
        painter.drawLines(QLine(ok-25, oky-50, ok+25, oky), QLine(ok+25, oky, ok-25, oky+50))
        painter.end()



if __name__ == '__main__':
    app = QApplication(sys.argv)
    testWidget = MainWidget()
    testWidget.show()
    sys.exit(app.exec_())

如果有人可以帮助我完成这项工作并一路解释以帮助我们更好地理解自定义小部件的结构并解释一个不像这个那样混乱的更好方法,我相信这将是一个加号像我这样的初学者使用 PyQt5 作为 UI 制作的主要框架。

自定义小部件没有 "standard" 方法,但通常 paintEvent 需要覆盖。

您的示例中存在不同的问题,我会尝试解决这些问题。

重叠

如果您希望小部件 "overlappable",则不得将其添加到布局中。将小部件添加到布局将意味着它将在布局中具有 "slot",这反过来将尝试计算其大小(基于它包含的小部件);此外,通常一个布局每个 "layout slot" 只有一个小部件,因此几乎不可能使小部件重叠; QGridLayout 是一种特殊情况,它允许(仅通过代码,不使用设计器)将更多小部件添加到相同的插槽中,或者使一些小部件与其他小部件重叠。最后,一旦小部件成为布局的一部分,它就不能自由移动或调整大小(除非您设置固定大小)。
唯一真正的解决方案是创建具有父级的小部件。这将使使用 move()resize() 成为可能,但 在父级的边界内。

悬停

虽然大多数小部件确实可以在样式表中使用 :hover 选择器,但它仅适用于标准小部件,它们自己完成大部分绘制(通过 QStyle 函数)。关于这一点,虽然可以使用样式表进行一些自定义绘制,但它通常用于非常特殊的情况,即使在这种情况下也没有简单的方法来访问样式表属性。
在你的情况下,不需要使用样式表,只需覆盖 enterEventleaveEvent,在那里设置你需要绘制的任何颜色,然后在最后调用 self.update()

绘画

你收到这些警告的原因是因为你在用绘画设备作为参数声明 QPainter 之后调用 begin:一旦它被创建它会自动调用 begin 与设备参数.此外,通常不需要调用 end(),因为它会在 QPainter 被销毁时自动调用,这发生在 paintEvent returns 时,因为它是一个局部变量。

例子


我根据你的问题创建了一个小例子。它在 QGridLayout 中创建一个带有按钮和标签的 window,并且还使用 QFrame 集 它们下面(因为它是首先添加的),显示 "overlapping" 我之前写过的布局。然后是您的箭头小部件,它以主 window 作为父项创建,可以通过单击并拖动它来移动。

import sys
from PyQt5 import QtCore, QtGui, QtWidgets

class ArrowWidget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        # since the widget will not be added to a layout, ensure
        # that it has a fixed size (otherwise it'll use QWidget default size)
        self.setFixedSize(200, 200)
        self.blurRadius = 20
        self.xO = 0
        self.yO = 20
        shadow = QtWidgets.QGraphicsDropShadowEffect(blurRadius=self.blurRadius, xOffset=self.xO, yOffset=self.yO)
        self.setGraphicsEffect(shadow)
        # create pen and brush colors for painting
        self.currentPen = self.normalPen = QtGui.QPen(QtCore.Qt.black)
        self.hoverPen = QtGui.QPen(QtCore.Qt.darkGray)
        self.currentBrush = self.normalBrush = QtGui.QColor(QtCore.Qt.transparent)
        self.hoverBrush = QtGui.QColor(128, 192, 192, 128)

    def mousePressEvent(self, event):
        if event.buttons() == QtCore.Qt.LeftButton:
            self.mousePos = event.pos()

    def mouseMoveEvent(self, event):
        # move the widget based on its position and "delta" of the coordinates
        # where it was clicked. Be careful to use button*s* and not button
        # within mouseMoveEvent
        if event.buttons() == QtCore.Qt.LeftButton:
            self.move(self.pos() + event.pos() - self.mousePos)

    def enterEvent(self, event):
        self.currentPen = self.hoverPen
        self.currentBrush = self.hoverBrush
        self.update()

    def leaveEvent(self, event):
        self.currentPen = self.normalPen
        self.currentBrush = self.normalBrush
        self.update()

    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        qp.setRenderHints(qp.Antialiasing)
        # painting is not based on "pixels", to get accurate results
        # translation of .5 is required, expecially when using 1 pixel lines
        qp.translate(.5, .5)
        # painting rectangle is always 1px smaller than the actual size
        rect = self.rect().adjusted(0, 0, -1, -1)
        qp.setPen(self.currentPen)
        qp.setBrush(self.currentBrush)
        # draw an ellipse smaller than the widget
        qp.drawEllipse(rect.adjusted(25, 25, -25, -25))
        # draw arrow lines based on the center; since a QRect center is a QPoint
        # we can add or subtract another QPoint to get the new positions for
        # top-left, right and bottom left corners
        qp.drawLine(rect.center() + QtCore.QPoint(-25, -50), rect.center() + QtCore.QPoint(25, 0))
        qp.drawLine(rect.center() + QtCore.QPoint(25, 0), rect.center() + QtCore.QPoint(-25, 50))


class MainWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        layout = QtWidgets.QGridLayout()
        self.setLayout(layout)
        self.button = QtWidgets.QPushButton('button')
        layout.addWidget(self.button, 0, 0)
        self.label = QtWidgets.QLabel('label')
        self.label.setAlignment(QtCore.Qt.AlignCenter)
        layout.addWidget(self.label, 0, 1)
        # create a frame that uses as much space as possible
        self.frame = QtWidgets.QFrame()
        self.frame.setFrameShape(self.frame.StyledPanel|self.frame.Raised)
        self.frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        # add it to the layout, ensuring it spans all rows and columns
        layout.addWidget(self.frame, 0, 0, layout.rowCount(), layout.columnCount())
        # "lower" the frame to the bottom of the widget's stack, otherwise
        # it will be "over" the other widgets, preventing them to receive
        # mouse events
        self.frame.lower()
        self.resize(640, 480)
        # finally, create your widget with a parent, *without* adding to a layout
        self.arrowWidget = ArrowWidget(self)
        # now you can place it wherever you want
        self.arrowWidget.move(220, 140)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    testWidget = MainWidget()
    testWidget.show()
    sys.exit(app.exec_())