I'm currently working on image manipulation software in Qt, and I've encountered a specific requirement from the client. In my application, I have a customized QGraphicsScene that holds several images, each represented as a custom QGraphicsPixmapItem. The client's request is that when two of these images overlap, the overlapping region should be semi-transparent, with a 50% transparency level.
I've explored a couple of approaches but ran into a few roadblocks. Initially, I tried to make use of Qt's composition modes, but none of them provided the exact effect I'm aiming for – that is, achieving that 50% transparency in the overlapping region.
First attempt:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
{
QGraphicsPixmapItem::paint(painter, option, widget);
painter->setRenderHint(QPainter::Antialiasing);
QList<QGraphicsItem *> collidingItems = this->collidingItems();
for (auto item : qAsConst(collidingItems)) {
if (item == this)
continue;
if (QGraphicsPixmapItem *otherItem = qgraphicsitem_cast<QGraphicsPixmapItem *>(item)) {
// Evaluate intersection between two items in local coordinates
QPainterPath path = this->shape().intersected(this->mapFromItem(otherItem, otherItem->shape()));
QPainterPath otherPath = otherItem->shape().intersected(otherItem->mapFromItem(this, this->shape()));
if (!path.isEmpty() && !otherPath.isEmpty()) {
QRectF thisBoundingRect = path.boundingRect();
QRectF otherBoundingRect = otherPath.boundingRect();
// Create two pixmap of the overlapping section
QPixmap thisPixmap = this->pixmap().copy(thisBoundingRect.toRect());
QPixmap otherPixmap = otherItem->pixmap().copy(otherBoundingRect.toRect());
// Clear overlapping section
painter->save();
painter->fillPath(path, Qt::black);
painter->setClipPath(path);
// Redraw both the pixmaps with opacity at 0.5
painter->setOpacity(0.65);
painter->drawPixmap(path.boundingRect().topLeft(), thisPixmap);
painter->drawPixmap(path.boundingRect().topLeft(), otherPixmap);
painter->restore();
}
}
}
}
Result when not rotated (which is exactly what I want):
Result when rotations are involved:
The above code works as expected when the images are not rotated, but things get tricky when rotations come into play.
To avoid issues related to transformations, especially when images are rotated, I decided to revise my approach:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
{
QGraphicsPixmapItem::paint(painter, option, widget);
painter->setRenderHint(QPainter::Antialiasing);
QList<QGraphicsItem *> collidingItems = this->collidingItems();
for (auto item : qAsConst(collidingItems)) {
if (item == this)
continue;
if (CustomGraphicsPixmapItem *otherItem = qgraphicsitem_cast<CustomGraphicsPixmapItem *>(item)) {
// Evaluate intersection between two items in local coordinates
QPainterPath path = this->shape().intersected(this->mapFromItem(otherItem, otherItem->shape()));
if (!path.isEmpty()) {
QRectF thisBoundingRect = path.boundingRect();
// Create two pixmap of the overlapping section
QPixmap thisPixmap = this->pixmap().copy(thisBoundingRect.toRect());
// Clear overlapping section
painter->save();
// Set the composition mode to clear and then draw with SourceOver
painter->setCompositionMode(QPainter::CompositionMode_Clear);
painter->fillPath(path, Qt::transparent);
painter->setCompositionMode(QPainter::CompositionMode_SourceOver);
painter->setOpacity(0.5);
painter->drawPixmap(thisBoundingRect.topLeft(), thisPixmap);
painter->restore();
}
}
}
}
However, with this revised approach, the problem I'm facing is that when the two images overlap, the CompositionMode_Clear
of the second image not only clears the region of the second image but also clears the region of the underlying image, resulting in a black background for the second image, like so:
How do I efficiently achieve the desired effect? Especially when images are rotated.
Only using the
mapFromItem()
may create some issues for accumulating transforms, my suggestion is to always map everything to the scene and then remap back the result to the item.The concept is to create a QPainterPath that is the result of all scene polygons of all colliding items, using the full path of the bounding rect of the current item (also mapped to the scene), then draw the pixmap in two passes:
This is a basic example in PyQt, but I'm sure you can easily convert it to C++.
Here is a result with two images, both of which are also rotated:
There is a relatively small issue with the boundaries of the clipping (magnify the image and check the aliased edges between the images), but I believe that it shouldn't be an important issue for your case.
I also noted some strange (but not persisting) flickering while moving the items whenever one of them is rotated. Unfortunately, I'm afraid that there's little to do with that, unless you're ready to completely rewrite the whole implementation of QPixmap displaying within the graphics scene.