简体   繁体   中英

How to create circular image using pyqt4?

Here I wrote this code but did not work:

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

One possible solution is to overwrite the paint() method of the QGraphicsPixmapItem and use setClipPath to restrict the painting region:

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

在此处输入图像描述

Update:

# ...
view = QtGui.QGraphicsView(
    scene, 
)
# ...
view.show()

sys.exit(app.exec_())

Second possible solution:

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

在此处输入图像描述

Another approach, slightly different from the one provided by eyllanesc . While this might seem much more complicated than that, I believe that it offers a better implementation and interface, with the addition of better performance.

In this case, instead of overriding the paint method (that is run everytime the item is painted, which happens very often), I'm using the shape() function along with the QGraphicsItem.ItemClipsToShape flag, that allows to limit the painting only within the boundaries of the path shape.

What shape() does is to return a QPainterPath that includes only the "opaque" portions of an item that will react to mouse events and collision detection (with the scene boundaries and its other items). In the case of a QGraphicsPixmapItem this also considers the possible mask (for example, a PNG based pixmap with transparent areas, or an SVG image). By setting the ItemClipsToShape we can ensure that the painting will only cover the parts of the image that are within that shape.

The main advantage of this approach is that mouse interaction and collision detection with other items honors the actual circle shape of the item.

This means that if you click outside the circle (but still within the rectangle area of the full image), the item will not receive the event. Also, if the image supports masking (a PNG with transparent areas) which by default would not be part of the shape, this method will take that into account.

Also, by "caching" the shape we are also speeding up the painting process a bit (since Qt will take care of it, without any processing done using 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

Keep in mind that there's a catch about both methods: if the aspect ratio of the image differs very much from 1:1, you'll always end up with some positioning issues. With my image, for example, it will always be shown 60 pixel right from the actual item position. If you want to avoid that, the updateRect function will be slightly different and, unfortunately, you'll have to override the paint() function (while still keeping it a bit faster than other options):

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

This will make the boundingRect (and resulting internal shape) position the whole item at the top-left of the item position.


The following image shows the differences between the two approaches; I've used a PNG with transparent areas to better explain the whole concept.
On the top there is the source image, in the middle the paint() override approach, and finally the shape() implementation at the bottom.

剪辑示例

While there seems to be no difference between the two methods, as shown on the examples on the left, on the right I've highlighted the actual boundaries of each item, by showing their boundingRect (in blue), shape (in red), which will be used for mouse events, collision detection and paint clipping; the green circle shows the overall circle used for both shape and painting.
The examples in the middle show the positioning based on the original image size, while on the right you can see the absolute positioning based on the effective circle size as explained above.

Drawing a circle around the image

Unfortunately, the ItemClipsToShape flag doesn't support antialiasing for clipping: if we just draw a circle after painting the image the result will be ugly. On the left you can see that the circle is very pixellated and does not overlap perfectly on the image. On the right the correct painting.

剪贴画方法差异

To support that, the flag must not be set, and the paint function will be a bit different.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM