我可以将Neumorphism效果应用于QWidget吗? [英] Can I apply a Neumorphism effect to a QWidget?

查看:88
本文介绍了我可以将Neumorphism效果应用于QWidget吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

尽管Qt提供了QGraphicsDropShadowEffect,但没有" Neumorphism "效果可用:

While Qt provides the QGraphicsDropShadowEffect, there's no "Neumorphism" effect available:

在CSS中,有一个box-shadow属性(在上图中是这样的),它可以有多种颜色,但是Qt缺少对该属性的支持,并且不可能一次应用多个图形效果

In css there is the box-shadow property (that's how it's done in the image above), which can have multiple colors, but Qt lacks support for that property, and it's not possible to apply more than one graphics effect at once.

可以做到吗?

推荐答案

解决方案是创建QGraphicsEffect的自定义子类并使用渐变.

The solution is to create a custom subclass of QGraphicsEffect and using gradients.

起初,我考虑过使用与CSS相同的概念,将QGraphicsDropShadowEffect子类化,并在内部使用另一个概念绘制其他"阴影,但我不喜欢这种结果:在某些情况下(通常当半径和对比度为太大),这是行不通的:

At first I thought about following the same concept used for CSS, subclassing QGraphicsDropShadowEffect and using another one internally to draw the "other" shadow, but I didn't like the result: in certain situations (usually when radius and contrast are too big) it just doesn't work:

如果仔细观察,您会发现结果与阴影太相似,就像对象是漂浮的,而它应该是挤压"的.

If you look closely, you'll find out that the result is too similar to the drop shadow, like the object is floating, while it should be "extruding".

我发现的唯一有效解决方案是手动绘制所有内容,对边界使用线性渐变,对角使用复合渐变.尽管第一个非常合理,但是第二个通过使用QPainter的复合模式需要一些独创性:Qt仅具有径向和圆锥形渐变,但是它们之间没有混合".

The only effective solution I found was to manually draw everything, using linear gradients for borders and composite gradients for the corners. While the first is pretty logical, the second required a bit of ingenuity by using composite modes of QPainter: Qt only has radials and conical gradients, but there is no "mix" between them.

然后,诀窍是为浅"颜色创建一个径向渐变,中间为全色,边界为0,然后使用相同的颜色,然后为暗"颜色叠加一个圆锥形渐变(使用起始处为深色",而90度处为浅"),这将使用第一个渐变的alpha分量进行绘制.

The trick then was to create a radial gradient for the "light" color with the full color at the center and the same color at the border with 0 alpha, then superimpose a conical gradient for the "dark" color (with the "dark" color on start and the "light" at 90°), which will be painted using the alpha component of the first gradient.

然后只需要创建函数来更新每个属性即可:距离(效果的程度),颜色(用于渐变,默认为应用程序的QPalette.Window颜色角色),原点(角落)用作光源的源"),以及用于圆角边框的可选clipRadius.

Then it's just a matter of creating functions to update each one of the properties: distance (the extent of the effect), color (used for the gradients, defaults to the application's QPalette.Window color role), origin (the corner used as the "source" for the light) and an optional clipRadius for rounded borders.

一些重要说明:

  • 由于它是QGraphicsEffect,因此只能应用于父"窗口小部件:子代不能对其应用其他效果,这意味着如果您具有QGroupBox或QTabWidget之类的容器,则必须选择是否要将其应用于父母或每个孩子;
  • 由于其简单"性质,它仅支持矩形形状:如果小部件具有蒙版,则效果形状仍将基于矩形;
  • 布局边距和间距应予以考虑,因为如果使用它们的小部件太窄,则多个效果可能会重叠;我建议使用QProxyStyle并为PM_Layout [*] Margin和PM_Layout [*] Spacing设置一个最小默认值,并根据length属性返回一个值;
  • clipRadius属性允许四舍五入的边框裁剪,但是它不是完美的,因为QPainter的裁剪不支持抗锯齿.我看看以后是否可以解决这个问题;
  • 当应用于QGraphicsScene项时,类似于QGraphicsDropShadowEffect,其效果是在设备坐标中,因此将不应用变换(旋转,缩放,剪切).每当我也能够解决此问题时,我都会更新此答案;
  • since it's a QGraphicsEffect, it can only be applied to a "parent" widget: children cannot have another effect applied on them, which means that if you have a container like QGroupBox or QTabWidget, you have to choose if you want to apply it to the parent or to each of the children;
  • due to its "simple" nature, it only supports rectangular shapes: if a widget has a mask, the effect shape will still be based on a rectangle;
  • layout margins and spacings should be taken into account, as multiple effects could overlap if the widgets that use them are too narrow; I'd suggest using a QProxyStyle and set a minimum default for both PM_Layout[*]Margin and PM_Layout[*]Spacing, and return a value according to the length property;
  • the clipRadius property allows rounded border clipping, but it's not perfect, since QPainter's clipping doesn't support antialiasing; I'll see if I can address this issue in the future;
  • when applied to QGraphicsScene items, similarly to QGraphicsDropShadowEffect, the effect is in device coordinates, so transformations (rotation, scale, shearing) won't be applied; I'll update this answer whenever I'll be able to solve this issue too;

这是Qt QGraphicsDropShadowEffect,css仿真和我的NeumorphismEffect(最后两个具有圆角边框:css版本使用border-radius属性,而我的设置为clipRadius)之间的比较:

And here is a comparison between the Qt QGraphicsDropShadowEffect, the css emulation, and my NeumorphismEffect (the last two have rounded borders: the css version uses the border-radius property while mine is set with clipRadius):

class NeumorphismEffect(QtWidgets.QGraphicsEffect):
    originChanged = QtCore.pyqtSignal(QtCore.Qt.Corner)
    distanceChanged = QtCore.pyqtSignal(float)
    colorChanged = QtCore.pyqtSignal(QtGui.QColor)
    clipRadiusChanged = QtCore.pyqtSignal(int)

    _cornerShift = (QtCore.Qt.TopLeftCorner, QtCore.Qt.TopRightCorner, 
        QtCore.Qt.BottomRightCorner, QtCore.Qt.BottomLeftCorner)

    def __init__(self, distance=4, color=None, origin=QtCore.Qt.TopLeftCorner, clipRadius=0):
        super().__init__()

        self._leftGradient = QtGui.QLinearGradient(1, 0, 0, 0)
        self._leftGradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)
        self._topGradient = QtGui.QLinearGradient(0, 1, 0, 0)
        self._topGradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)

        self._rightGradient = QtGui.QLinearGradient(0, 0, 1, 0)
        self._rightGradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)
        self._bottomGradient = QtGui.QLinearGradient(0, 0, 0, 1)
        self._bottomGradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)

        self._radial = QtGui.QRadialGradient(.5, .5, .5)
        self._radial.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)
        self._conical = QtGui.QConicalGradient(.5, .5, 0)
        self._conical.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)

        self._origin = origin
        distance = max(0, distance)
        self._clipRadius = min(distance, max(0, clipRadius))
        self._setColor(color or QtWidgets.QApplication.palette().color(QtGui.QPalette.Window))
        self._setDistance(distance)

    def color(self):
        return self._color

    @QtCore.pyqtSlot(QtGui.QColor)
    @QtCore.pyqtSlot(QtCore.Qt.GlobalColor)
    def setColor(self, color):
        if isinstance(color, QtCore.Qt.GlobalColor):
            color = QtGui.QColor(color)
        if color == self._color:
            return
        self._setColor(color)
        self._setDistance(self._distance)
        self.update()
        self.colorChanged.emit(self._color)

    def _setColor(self, color):
        self._color = color
        self._baseStart = color.lighter(125)
        self._baseStop = QtGui.QColor(self._baseStart)
        self._baseStop.setAlpha(0)
        self._shadowStart = self._baseStart.darker(125)
        self._shadowStop = QtGui.QColor(self._shadowStart)
        self._shadowStop.setAlpha(0)

        self.lightSideStops = [(0, self._baseStart), (1, self._baseStop)]
        self.shadowSideStops = [(0, self._shadowStart), (1, self._shadowStop)]
        self.cornerStops = [(0, self._shadowStart), (.25, self._shadowStop), 
            (.75, self._shadowStop), (1, self._shadowStart)]

        self._setOrigin(self._origin)

    def distance(self):
        return self._distance

    def setDistance(self, distance):
        if distance == self._distance:
            return
        oldRadius = self._clipRadius
        self._setDistance(distance)
        self.updateBoundingRect()
        self.distanceChanged.emit(self._distance)
        if oldRadius != self._clipRadius:
            self.clipRadiusChanged.emit(self._clipRadius)

    def _getCornerPixmap(self, rect, grad1, grad2=None):
        pm = QtGui.QPixmap(self._distance + self._clipRadius, self._distance + self._clipRadius)
        pm.fill(QtCore.Qt.transparent)
        qp = QtGui.QPainter(pm)
        if self._clipRadius > 1:
            path = QtGui.QPainterPath()
            path.addRect(rect)
            size = self._clipRadius * 2 - 1
            mask = QtCore.QRectF(0, 0, size, size)
            mask.moveCenter(rect.center())
            path.addEllipse(mask)
            qp.setClipPath(path)
        qp.fillRect(rect, grad1)
        if grad2:
            qp.setCompositionMode(qp.CompositionMode_SourceAtop)
            qp.fillRect(rect, grad2)
        qp.end()
        return pm

    def _setDistance(self, distance):
        distance = max(1, distance)
        self._distance = distance
        if self._clipRadius > distance:
            self._clipRadius = distance
        distance += self._clipRadius
        r = QtCore.QRectF(0, 0, distance * 2, distance * 2)

        lightSideStops = self.lightSideStops[:]
        shadowSideStops = self.shadowSideStops[:]
        if self._clipRadius:
            gradStart = self._clipRadius / (self._distance + self._clipRadius)
            lightSideStops[0] = (gradStart, lightSideStops[0][1])
            shadowSideStops[0] = (gradStart, shadowSideStops[0][1])

        # create the 4 corners as if the light source was top-left
        self._radial.setStops(lightSideStops)
        topLeft = self._getCornerPixmap(r, self._radial)

        self._conical.setAngle(359.9)
        self._conical.setStops(self.cornerStops)
        topRight = self._getCornerPixmap(r.translated(-distance, 0), self._radial, self._conical)

        self._conical.setAngle(270)
        self._conical.setStops(self.cornerStops)
        bottomLeft = self._getCornerPixmap(r.translated(0, -distance), self._radial, self._conical)

        self._radial.setStops(shadowSideStops)
        bottomRight = self._getCornerPixmap(r.translated(-distance, -distance), self._radial)

        # rotate the images according to the actual light source
        images = topLeft, topRight, bottomRight, bottomLeft
        shift = self._cornerShift.index(self._origin)
        if shift:
            transform = QtGui.QTransform().rotate(shift * 90)
            for img in images:
                img.swap(img.transformed(transform, QtCore.Qt.SmoothTransformation))

        # and reorder them if required
        self.topLeft, self.topRight, self.bottomRight, self.bottomLeft = images[-shift:] + images[:-shift]

    def origin(self):
        return self._origin

    @QtCore.pyqtSlot(QtCore.Qt.Corner)
    def setOrigin(self, origin):
        origin = QtCore.Qt.Corner(origin)
        if origin == self._origin:
            return
        self._setOrigin(origin)
        self._setDistance(self._distance)
        self.update()
        self.originChanged.emit(self._origin)

    def _setOrigin(self, origin):
        self._origin = origin

        gradients = self._leftGradient, self._topGradient, self._rightGradient, self._bottomGradient
        stops = self.lightSideStops, self.lightSideStops, self.shadowSideStops, self.shadowSideStops

        # assign color stops to gradients based on the light source position
        shift = self._cornerShift.index(self._origin)
        for grad, stops in zip(gradients, stops[-shift:] + stops[:-shift]):
            grad.setStops(stops)

    def clipRadius(self):
        return self._clipRadius

    @QtCore.pyqtSlot(int)
    @QtCore.pyqtSlot(float)
    def setClipRadius(self, radius):
        if radius == self._clipRadius:
            return
        oldRadius = self._clipRadius
        self._setClipRadius(radius)
        self.update()
        if oldRadius != self._clipRadius:
            self.clipRadiusChanged.emit(self._clipRadius)

    def _setClipRadius(self, radius):
        radius = min(self._distance, max(0, int(radius)))
        self._clipRadius = radius
        self._setDistance(self._distance)

    def boundingRectFor(self, rect):
        d = self._distance + 1
        return rect.adjusted(-d, -d, d, d)

    def draw(self, qp):
        restoreTransform = qp.worldTransform()

        qp.setPen(QtCore.Qt.NoPen)
        x, y, width, height = self.sourceBoundingRect(QtCore.Qt.DeviceCoordinates).getRect()
        right = x + width
        bottom = y + height
        clip = self._clipRadius
        doubleClip = clip * 2

        qp.setWorldTransform(QtGui.QTransform())
        leftRect = QtCore.QRectF(x - self._distance, y + clip, self._distance, height - doubleClip)
        qp.setBrush(self._leftGradient)
        qp.drawRect(leftRect)

        topRect = QtCore.QRectF(x + clip, y - self._distance, width - doubleClip, self._distance)
        qp.setBrush(self._topGradient)
        qp.drawRect(topRect)

        rightRect = QtCore.QRectF(right, y + clip, self._distance, height - doubleClip)
        qp.setBrush(self._rightGradient)
        qp.drawRect(rightRect)

        bottomRect = QtCore.QRectF(x + clip, bottom, width - doubleClip, self._distance)
        qp.setBrush(self._bottomGradient)
        qp.drawRect(bottomRect)

        qp.drawPixmap(x - self._distance, y - self._distance, self.topLeft)
        qp.drawPixmap(right - clip, y - self._distance, self.topRight)
        qp.drawPixmap(right - clip, bottom - clip, self.bottomRight)
        qp.drawPixmap(x - self._distance, bottom - clip, self.bottomLeft)

        qp.setWorldTransform(restoreTransform)
        if self._clipRadius:
            path = QtGui.QPainterPath()
            source, offset = self.sourcePixmap(QtCore.Qt.DeviceCoordinates)

            sourceBoundingRect = self.sourceBoundingRect(QtCore.Qt.DeviceCoordinates)
            qp.save()
            qp.setTransform(QtGui.QTransform())
            path.addRoundedRect(sourceBoundingRect, self._clipRadius, self._clipRadius)
            qp.setClipPath(path)
            qp.drawPixmap(source.rect().translated(offset), source)
            qp.restore()
        else:
            self.drawSource(qp)

这篇关于我可以将Neumorphism效果应用于QWidget吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆