如何检测边缘透明的两个QGraphicsPixmapItem的非透明区域碰撞?
哈哈,这个问题我太熟了!Qt默认的collidingItems()确实只会盯着边界矩形做检测,完全不管透明像素的事。要实现非透明区域的精准碰撞,得自己给QGraphicsPixmapItem加些“定制化”的逻辑,这里给你三个实用的方案,按需选就行:
方案1:重写shape()方法(首推)
Qt的碰撞检测核心依据是shape()方法返回的QPainterPath,默认情况下这个路径就是item的边界矩形。我们可以把它替换成基于图片alpha通道的路径,这样collidingItems()就会自动用这个精准路径做碰撞判断了。
步骤很简单:
- 从QPixmap中提取alpha通道的遮罩(
mask()方法) - 把遮罩转换成
QPainterPath - 考虑item的变换(比如缩放、旋转),把路径做对应的变换后返回
代码示例:
#include <QGraphicsPixmapItem> #include <QPainterPath> #include <QRegion> class CustomPixmapItem : public QGraphicsPixmapItem { public: using QGraphicsPixmapItem::QGraphicsPixmapItem; QPainterPath shape() const override { const QPixmap& pix = pixmap(); if (pix.isNull()) return QGraphicsPixmapItem::shape(); // 获取alpha通道的遮罩 QBitmap mask = pix.mask(); // 将遮罩转换为QPainterPath QPainterPath path; path.addRegion(QRegion(mask)); // 应用item的变换(比如缩放、旋转) return transform().map(path); } };
注意:这个方案的优势是完全复用Qt内置的碰撞检测逻辑,性能比自己逐像素检测好很多,而且所有依赖shape()的方法(比如collidingItems()、contains())都会自动生效。唯一需要注意的是,如果你的图片特别大,生成的QPainterPath会比较复杂,可能会有轻微的性能损耗,但大部分场景下完全够用。
方案2:自定义像素级碰撞检测函数
如果不想修改shape(),或者需要更灵活的判断逻辑(比如允许一定透明度的像素也算碰撞),可以自己写一个函数,先通过边界矩形过滤候选item,再在重叠区域做逐像素检测。
代码示例:
QList<QGraphicsItem*> CustomPixmapItem::transparentCollidingItems() { QList<QGraphicsItem*> result; // 先获取边界矩形碰撞的候选,快速排除不可能的情况 QList<QGraphicsItem*> candidates = collidingItems(); for (QGraphicsItem* item : candidates) { CustomPixmapItem* pixItem = qgraphicsitem_cast<CustomPixmapItem*>(item); if (!pixItem) continue; // 计算两个item在全局坐标系下的重叠区域 QRectF globalRect1 = mapToScene(boundingRect()).boundingRect(); QRectF globalRect2 = pixItem->mapToScene(pixItem->boundingRect()).boundingRect(); QRectF overlap = globalRect1.intersected(globalRect2); if (overlap.isEmpty()) continue; // 转换为各自本地坐标系的矩形 QRectF localRect1 = mapFromScene(overlap).boundingRect().toAlignedRect(); QRectF localRect2 = pixItem->mapFromScene(overlap).boundingRect().toAlignedRect(); QImage img1 = pixmap().toImage(); QImage img2 = pixItem->pixmap().toImage(); bool hasCollision = false; // 遍历重叠区域的每个像素 for (int x = localRect1.left(); x < localRect1.right(); ++x) { for (int y = localRect1.top(); y < localRect1.bottom(); ++y) { if (!img1.rect().contains(x, y)) continue; // 检查当前像素是否非透明 if (img1.pixelColor(x, y).alpha() <= 0) continue; // 计算该像素在另一个item中的坐标 QPointF scenePos = mapToScene(x, y); QPoint posInItem2 = pixItem->mapFromScene(scenePos).toPoint(); if (!img2.rect().contains(posInItem2)) continue; // 检查另一个item的对应像素是否非透明 if (img2.pixelColor(posInItem2).alpha() > 0) { hasCollision = true; goto endCheck; // 找到碰撞点就退出循环 } } } endCheck: if (hasCollision) result.append(item); } return result; }
这个方案的优点是灵活,可以自定义碰撞的判断规则(比如设置alpha阈值),缺点是逐像素检测的性能较差,适合item数量少、图片不大的场景。
方案3:重写collidesWithItem()方法
如果你希望调用默认的collidingItems()时自动使用像素级检测,可以重写collidesWithItem()方法,在里面实现自定义的碰撞逻辑。
代码示例:
bool CustomPixmapItem::collidesWithItem(const QGraphicsItem *other, Qt::ItemSelectionMode mode) const { // 先调用默认的边界矩形检测,快速排除不可能的情况 if (!QGraphicsPixmapItem::collidesWithItem(other, mode)) return false; const CustomPixmapItem* pixItem = qgraphicsitem_cast<const CustomPixmapItem*>(other); if (!pixItem) // 如果不是自定义的pixmap item,就用默认逻辑 return QGraphicsPixmapItem::collidesWithItem(other, mode); // 计算全局坐标系下的重叠区域 QRectF globalOverlap = mapToScene(boundingRect()).intersected(pixItem->mapToScene(pixItem->boundingRect())); if (globalOverlap.isEmpty()) return false; QImage img1 = pixmap().toImage(); QImage img2 = pixItem->pixmap().toImage(); QPoint topLeft1 = mapFromScene(globalOverlap.topLeft()).toPoint(); QPoint topLeft2 = pixItem->mapFromScene(globalOverlap.topLeft()).toPoint(); // 遍历重叠区域的像素 for (int x = 0; x < qRound(globalOverlap.width()); ++x) { for (int y = 0; y < qRound(globalOverlap.height()); ++y) { QPoint pos1(topLeft1.x() + x, topLeft1.y() + y); QPoint pos2(topLeft2.x() + x, topLeft2.y() + y); if (img1.rect().contains(pos1) && img2.rect().contains(pos2)) { if (img1.pixelColor(pos1).alpha() > 0 && img2.pixelColor(pos2).alpha() > 0) { return true; } } } } return false; }
这个方案相当于把方案2的逻辑整合到Qt的碰撞检测流程里,调用collidingItems()时会自动使用这个自定义逻辑,不需要单独调用自己写的函数。
内容的提问来源于stack exchange,提问作者Skipper HUN




