制作高度自定义,可悬停,可重叠的小部件

时间:2019-07-30 11:10:48

标签: python pyqt pyqt5 customization

我想使用PyQt5在UI设计中完善自己的游戏。我觉得PyQt5中用于UI定制的资源不容易找到。可以尝试制作个性化的小部件,但总体方法似乎不规范。

我需要构建一个可悬停,可与其他小部件重叠并且高度定制的箭头小部件。正如我在this教程和其他文章中所读到的,可以使用paintEvent来完成您需要做的事情。因此,这就是我尝试过的方法,但是总的来说,我觉得该方法非常凌乱,并且我希望了解有关构建复杂的定制通用小部件的一些准则。这是我所拥有的:

自定义形状:我基于this

构建了代码

可悬停属性:我到处都读到,修改项目styleSheet通常是可行的方法,特别是如果您想使Widget具有通用性并适应颜色,则问题是我找不到正确使用self.palette来获取QApplication styleSheet当前颜色的方法。我觉得我可能不得不使用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

可重叠的属性:我发现了以前的post,它似乎已经找到了解决方案:创建第二个作为主窗口小部件的子窗口小部件,以便能够移动周围的孩子。我尝试过,但是无论我给小部件什么位置,它似乎都不想移动。

这是我的代码:

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_())

Failed custom, overlapable and hoverable widget

如果有人可以帮助我完成这项工作,并一路解释以帮助我们更好地了解自定义窗口小部件的结构,并解释一种更好的方法,而这种方法不会像这样杂乱无章,那么我认为这对初学者是一个加分像我一样,使用PyQt5作为UI制作的主要框架。

1 个答案:

答案 0 :(得分:1)

自定义窗口小部件没有“标准”方法,但通常是 paintEvent覆盖。

您的示例中存在其他问题,我将尝试解决。

重叠

如果您希望窗口小部件“可重叠”,则不得将其添加到布局中。将小部件添加到布局将意味着它将在布局内具有其“插槽”,这将依次尝试计算其大小(基于其包含的小部件);同样,一个布局通常每个“布局插槽”只有一个小部件,几乎不可能使小部件重叠。 QGridLayout是一种特殊情况,它允许(仅通过代码,不使用Designer)将更多的窗口小部件添加到同一插槽,或使某些窗口小部件重叠。最后,小部件一旦成为布局的一部分,就不能自由移动或调整大小(除非您设置了fixedSize)。
唯一真正的解决方案是使用父项创建窗口小部件。这样将可以使用move()resize(),但在父级边界内。

悬停

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

绘画

收到这些警告的原因是因为您在声明使用绘画设备作为参数的QPainter之后调用begin:创建后它会自动使用设备参数调用begin。另外,通常不需要调用end(),因为QPainter销毁时会自动调用它,因为它是局部变量,所以在paintEvent返回时会发生这种情况。

示例

screenshot of the running example
我根据您的问题创建了一个小示例。它在QGridLayout中创建一个带有按钮和标签的窗口,并在它们的下方 使用一个QFrame集(因为它首先被添加了),显示了我之前写的“重叠”布局。然后是您的箭头小部件,它以主窗口为父窗口创建,可以通过单击并拖动它来移动。

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_())