PyQt5 добавляет графический эффект в QMenu - PullRequest
0 голосов
/ 26 апреля 2020

У меня есть QMenu объект, и я применяю к нему эффект нейморфизма. Графический эффект для эффекта нейморфизма (ниже) является модифицированной версией, которую предоставил @musicamante.

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

import inspect
import sys

class QNeumorphismEffect(QGraphicsEffect):
  originChanged = pyqtSignal(Qt.Corner)
  distanceChanged = pyqtSignal(float)
  colorChanged = pyqtSignal(QColor)
  clipRadiusChanged = pyqtSignal(int)

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

  def __init__(self,color,darkShadow,lightShadow,distance=6,origin=Qt.TopLeftCorner,clipRadius=0,*args,**kwargs):
    QGraphicsEffect.__init__(self,*args,**kwargs)
    distance = max(distance,0)

    self._leftGradient   = QLinearGradient(1,0,0,0)
    self._topGradient    = QLinearGradient(0,1,0,0)
    self._rightGradient  = QLinearGradient(0,0,1,0)
    self._bottomGradient = QLinearGradient(0,0,0,1)
    self._radial         = QRadialGradient(0.5,0.5,0.5)
    self._conical        = QConicalGradient(0.5,0.5,0)

    self._leftGradient.setCoordinateMode(QGradient.ObjectBoundingMode)
    self._topGradient.setCoordinateMode(QGradient.ObjectBoundingMode)
    self._rightGradient.setCoordinateMode(QGradient.ObjectBoundingMode)
    self._bottomGradient.setCoordinateMode(QGradient.ObjectBoundingMode)
    self._radial.setCoordinateMode(QGradient.ObjectBoundingMode)
    self._conical.setCoordinateMode(QGradient.ObjectBoundingMode)

    self._origin = origin
    self._clipRadius = max(0,clipRadius)
    self._setColor(color or QColor("#ff0000"),darkShadow or QColor("#00ff00"),lightShadow or QColor("#0000ff"))
    self._setDistance(distance)

  ##### COLOR MANAGEMENT #####
  def color(self):
    return self._color

  @pyqtSlot(QColor)
  @pyqtSlot(Qt.GlobalColor)
  def setColor(self,color,dark,light):
    if isinstance(color,Qt.GlobalColor):
      color = QColor(color)

    if color == self._color:
      return

    self._setColor(color,dark,light)
    self._setDistance(self._distance)
    self.update()
    self.colorChanged.emit(self._color)

  def _setColor(self,color,dark,light): #use custom colors here for shadows, bg etc...
    self._color = color
    self._baseStart = light
    self._baseStop = QColor(self._baseStart)
    self._baseStop.setAlpha(0)
    self._shadowStart = dark
    self._shadowStop = 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),(0.25,self._shadowStop),(0.75,self._shadowStop),(1,self._shadowStart)]
    self._setOrigin(self._origin)

  ##### DISTANCE MANAGEMENT #####
  def distance(self):
    return seld._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 _setDistance(self,distance):
    distance = max(1,distance)
    self._distance = distance

    #if self._clipRadius > distance:
      #self._clipRadius = distance
    distance += self._clipRadius
    r = 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])

    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)

    images = topLeft,topRight,bottomRight,bottomLeft
    shift = self._cornerShift.index(self._origin)
    if shift:
      transform = QTransform().rotate(shift * 90)
      for img in images:
        img.swap(img.transformed(transform,Qt.SmoothTransformation))

    self.topLeft,self.topRight,self.bottomRight,self.bottomLeft = images[-shift:] + images[:-shift]

  def _getCornerPixmap(self,rect,grad1,grad2=None):
    pm = QPixmap(self._distance + self._clipRadius,
                 self._distance + self._clipRadius)
    pm.fill(Qt.transparent)
    qp = QPainter(pm)
    qp.setRenderHints(qp.Antialiasing | qp.SmoothPixmapTransform)

    if self._clipRadius > 1:
      path = QPainterPath()
      path.addRect(rect)
      size = self._clipRadius * 2 - 1
      mask = 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

  ##### ORIGIN MANAGEMENT #####
  def origin(self):
    return self._origin

  @pyqtSlot(Qt.Corner)
  def setOrigin(self,origin):
    origin = 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

    shift = self._cornerShift.index(self._origin)
    for grad,stops in zip(gradients,stops[-shift:] + stops[:-shift]):
      grad.setStops(stops)

  ##### CLIP RADIUS MANAGEMENT #####
  def clipRadius(self):
    return self.clipRadius

  @pyqtSlot(int)
  @pyqtSlot(float)
  def setClipRadius(self,radius):
    if radius == self._clipRadius: return

    oldRadius = self._clipRadius
    self.set_clipRadius(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)

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

    qp.setRenderHints(qp.Antialiasing | qp.SmoothPixmapTransform)
    qp.setPen(Qt.NoPen)

    x,y,width,height = self.sourceBoundingRect(Qt.DeviceCoordinates).getRect()
    right = x + width
    bottom = y + height

    clip = self._clipRadius
    doubleClip = clip * 2

    qp.setWorldTransform(QTransform())

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

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

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

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

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

    qp.setWorldTransform(restoreTransform)
    if self._clipRadius:
      path = QPainterPath()
      source,offset = self.sourcePixmap(Qt.DeviceCoordinates,mode=QGraphicsEffect.PadToTransparentBorder)

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

Я применяю эффект к QMenu, как это.

app = QApplication(sys.argv)
menu = QMenu()
menu.setGraphicsEffect(QNeumorphismEffect(color=QColor("#1E1E1E"),darkShadow=QColor("#000000"),lightShadow=QColor("#454545"),clipRadius=8)
menu.setStyleSheet(self.styleSheet() + "QMenu {background-color: #1E1E1E; padding: 2px; menu-scrollable: 1; border: none;}")
menu.setStyleSheet(self.styleSheet() + "QMenu:item {background-color: #1E1E1E; border-radius: 8px; padding: 5px 20px; margin: 1px; height: 16px; width: 250px; color: #AAAAAA;}")
menu.setStyleSheet(self.styleSheet() + "QMenu:item:selected {background-color: #050505;}")
menu.setStyleSheet(self.styleSheet() + "QMenu:separator {background-color: #FCCE03;}")
menu.setStyleSheet(self.styleSheet() + "QMenu:icon {padding: 5px; height: 16px}")
menu.setStyleSheet(self.styleSheet() + "QMenu:icon:checked {flat: true;}")
menu.show()
app.quit()

Я могу видеть эффект в углах, но не где-нибудь еще вокруг меню - я знаю, что это эффект, который я могу видеть в углах, потому что в нижних углах я вижу более темную тень. Тем не менее, он прекрасно работает с кнопками и виджетами:

enter image description here

Я пробовал:

  • Заполнение в таблице стилей меню
  • Поля в таблице стилей меню
  • Попытка расширить прямоугольник рисования или меню

Я не уверен, почему эффект не показывает по меню, кто-нибудь еще знает?

Другая информация:

  • PyQt 5.14.2
  • Python 3.8.0
  • Windows 10
...