如何使用pyqt4创建圆形图像?

How to create circular image using pyqt4?

我在这里写了这段代码但是没有用:

import sys
from PyQt4 import QtGui, QtCore

class CricleImage(QtCore.QObject):
    def __init__(self):
        super(CricleImage, self).__init__()

        self.pix = QtGui.QGraphicsPixmapItem(QtGui.QPixmap("bird(01).jpg"))

        #drawRoundCircle
        rect = self.pix.boundingRect()
        self.gri = QtGui.QGraphicsRectItem(rect)
        self.gri.setPen(QtGui.QColor('red'))


if __name__ == '__main__':

    myQApplication = QtGui.QApplication(sys.argv)

    IMG = CricleImage()
    #scene
    scene = QtGui.QGraphicsScene(0, 0, 400, 300)
    scene.addItem(IMG.pix)
    #view
    view = QtGui.QGraphicsView(scene)
    view.show()
    sys.exit(myQApplication.exec_())

一种可能的解决方法是覆盖QGraphicsPixmapItem的paint()方法,使用setClipPath来限制绘画区域:

from PyQt4 import QtCore, QtGui


class CirclePixmapItem(QtGui.QGraphicsPixmapItem):
    @property
    def radius(self):
        if not hasattr(self, "_radius"):
            self._radius = 0
        return self._radius

    @radius.setter
    def radius(self, value):
        if value >= 0:
            self._radius = value
            self.update()

    def paint(self, painter, option, widget=None):
        painter.save()
        rect = QtCore.QRectF(QtCore.QPointF(), 2 * self.radius * QtCore.QSizeF(1, 1))
        rect.moveCenter(self.boundingRect().center())
        path = QtGui.QPainterPath()
        path.addEllipse(rect)
        painter.setClipPath(path)
        super().paint(painter, option, widget)
        painter.restore()


if __name__ == "__main__":
    import sys

    app = QtGui.QApplication(sys.argv)

    pixmap = QtGui.QPixmap("logo.jpg")

    scene = QtGui.QGraphicsScene()
    view = QtGui.QGraphicsView(scene)
    view.setRenderHints(
        QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform
    )

    it = CirclePixmapItem(pixmap)
    scene.addItem(it)

    it.radius = pixmap.width() / 2

    view.show()
    sys.exit(app.exec_())

更新:

# ...
view = QtGui.QGraphicsView(
    scene, <b>alignment=QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft</b>
)
# ...
view.show()
<b>it.setPos(80, 80)</b>
sys.exit(app.exec_())

第二种可能的解决方案:

import sys
#from PyQt4 import QtCore, QtGui

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


class Label(QLabel):
    def __init__(self, *args, antialiasing=True, **kwargs):
        super(Label, self).__init__(*args, **kwargs)
        self.Antialiasing = antialiasing
        self.setMaximumSize(200, 200)
        self.setMinimumSize(200, 200)
        self.radius = 100 

        self.target = QPixmap(self.size())  
        self.target.fill(Qt.transparent)    # Fill the background with transparent

        # Upload image and zoom to control level
        p = QPixmap("head2.jpg").scaled(  
            200, 200, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)

        painter = QPainter(self.target)
        if self.Antialiasing:
            # antialiasing
            painter.setRenderHint(QPainter.Antialiasing, True)
            painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
            painter.setRenderHint(QPainter.SmoothPixmapTransform, True)

        path = QPainterPath()
        path.addRoundedRect(
            0, 0, self.width(), self.height(), self.radius, self.radius)

        # pruning
        painter.setClipPath(path)
        painter.drawPixmap(0, 0, p)
        self.setPixmap(self.target)


class Window(QWidget):
    def __init__(self, *args, **kwargs):
        super(Window, self).__init__(*args, **kwargs)
        layout = QHBoxLayout(self)
        layout.addWidget(Label(self))
        self.setStyleSheet("background: green;")           


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

另一种方法,与 略有不同。虽然这看起来可能比那复杂得多,但我相信它提供了更好的实现和接口,并增加了更好的性能。

在这种情况下,我使用shape() 函数连同 QGraphicsItem.ItemClipsToShape 标志,允许将绘画限制在 路径形状的边界内

shape() 所做的是 return 一个 QPainterPath,该路径仅包含一个项目的 "opaque" 部分,该部分将对鼠标事件和碰撞检测(具有场景边界及其其他项目)。在 QGraphicsPixmapItem 的情况下,这也考虑了可能的掩码(例如,具有透明区域的基于 PNG 的像素图,或 SVG 图像)。通过设置 ItemClipsToShape,我们可以确保绘画只会覆盖该形状内的图像部分。

这种方法的主要优点是鼠标交互和与其他项目的碰撞检测遵循项目的实际圆形。

这意味着如果您点击圆圈外(但仍在完整图像的矩形区域内),该项目将不会收到事件。此外,如果图像支持默认情况下不会成为形状一部分的遮罩(具有透明区域的 PNG),此方法将考虑到这一点。

此外,通过 "caching" 形状,我们也稍微加快了绘画过程(因为 Qt 会处理它,而无需使用 python 完成任何处理)。

class CircleClipPixmapItem(QtGui.QGraphicsPixmapItem):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setFlag(self.ItemClipsToShape)
        self.updateRect()

    def updateRect(self):
        baseRect = super().boundingRect()
        minSize = min(baseRect.width(), baseRect.height())
        self._boundingRect = QtCore.QRectF(0, 0, minSize, minSize)
        self._boundingRect.moveCenter(baseRect.center())
        self._shape = QtGui.QPainterPath()
        self._shape.addEllipse(self._boundingRect)
        # the shape might include transparent areas, using the & operator
        # I'm ensuring that _shape only includes the areas that intersect
        # the shape provided by the base implementation
        self._shape &= super().shape()

    def setPixmap(self, pm):
        super().setPixmap(pm)
        # update the shape to reflect the new image size
        self.updateRect()

    def setShapeMode(self, mode):
        super().setShapeMode(mode)
        # update the shape with the new mode
        self.updateRect()

    def boundingRect(self):
        return self._boundingRect

    def shape(self):
        return self._shape

请记住,这两种 方法都有一个问题:如果图像的纵横比与 1:1 相差很大,您最终会得到一些定位问题。例如,对于我的图片,它总是显示在距实际项目位置 60 像素的位置。如果你想避免这种情况,updateRect 函数会略有不同,不幸的是,你必须覆盖 paint() 函数(同时仍然保持它比其他选项快一点):

    def updateRect(self):
        baseRect = super().boundingRect()
        minSize = min(baseRect.width(), baseRect.height())
        self._boundingRect = QtCore.QRectF(0, 0, minSize, minSize)
        # the _boundingRect is *not* centered anymore, but a new rect is created
        # as a reference for both shape intersection and painting
        refRect= QtCore.QRectF(self._boundingRect)
        refRect.moveCenter(baseRect.center())
        # note the minus sign!
        self._reference = -refRect.topLeft()
        self._shape = QtGui.QPainterPath()
        self._shape.addEllipse(self._boundingRect)
        self._shape &= super().shape().translated(self._reference)

    # ...

    def paint(self, painter, option, widget):
        # we are going to translate the painter to the "reference" position,
        # let's save its state before that
        painter.save()
        painter.translate(self._reference)
        super().paint(painter, option, widget)
        painter.restore()

这将使 boundingRect(和生成的内部形状)将整个项目定位在项目位置的左上角。


下图显示了两种方法之间的差异;我使用了带有透明区域的 PNG 来更好地解释整个概念。
顶部是源图像,中间是 paint() 覆盖方法,最后是底部的 shape() 实现。

虽然这两种方法之间似乎没有区别,如左侧示例所示,但在右侧,我通过显示它们的 boundingRect(在蓝色),shape(红色),用于鼠标事件、碰撞检测和油漆剪裁;绿色圆圈表示用于形状和绘画的整体圆圈。
中间的示例显示了基于原始图像大小的定位,而在右侧您可以看到基于有效圆大小的绝对定位,如上所述。

在图像周围画一个圆圈

不幸的是,ItemClipsToShape 标志不支持裁剪的抗锯齿:如果我们在绘制图像后只画一个圆圈,结果会很难看。在左侧,您可以看到圆圈像素化程度很高,并且在图像上没有完美重叠。右边是正确的画。

为了支持它,必须不设置标志,并且绘制功能会有点不同。

class CircleClipPixmapItem(QtGui.QGraphicsPixmapItem):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # we don't need this anymore:
        # self.setFlag(self.ItemClipsToShape)

        # always set the shapeMode to the bounding rect without any masking:
        # if the image has transparent areas they will be clickable anyway
        self.setShapeMode(self.BoundingRectShape)
        self.updateRect()
        self.pen = QtGui.QPen(QtCore.Qt.red, 2)

    # ...

    def setPen(self, pen):
        self.pen = pen
        self.update()

    def paint(self, painter, option, widget):
        # we are going to translate the painter to the "reference" position,
        # and we are also changing the pen, let's save the state before that
        painter.save()
        painter.translate(.5, .5)
        painter.setRenderHints(painter.Antialiasing)
        # another painter save "level"
        painter.save()
        # apply the clipping to the painter
        painter.setClipPath(self._shape)
        painter.translate(self._reference)
        super().paint(painter, option, widget)
        painter.restore()

        painter.setPen(self.pen)
        # adjust the rectangle to precisely match the circle to the image
        painter.drawEllipse(self._boundingRect.adjusted(.5, .5, -.5, -.5))
        painter.restore()
        # restore the state of the painter