我正在尝试创建带边框的多边形,并在鼠标光标位于其上方时更改其颜色。当我创建多个多边形并设置大于 4 的边框大小时出现问题。第一个多边形行为正确但对于其他多边形似乎多边形边框的内半部分被视为不属于多边形,因为 hoverLeaveEvent() 是到达那个位时触发。
我可以不画边框,而是在已有的多边形上使用额外的多边形作为边框,或者画线,但这会变得有点乱。我想知道是否有一种方法可以在不创建额外项目的情况下解决该问题。
这里是一个显示问题的小示例代码。如果设置边框宽度 >5
会更好看from PyQt6.QtWidgets import QMainWindow, QGraphicsScene, QGraphicsView, QGraphicsPolygonItem, QApplication
from PyQt6.QtGui import QColor, QPolygonF, QBrush, QPen
from PyQt6.QtCore import QPointF
class Polygon(QGraphicsPolygonItem):
def __init__(self, parent):
super().__init__(parent)
self.setBrush(QBrush(QColor(255, 0, 0, 120)))
self.setPen(QPen(QColor(255, 0, 0), 10))
self.setAcceptHoverEvents(True)
def hoverEnterEvent(self, event):
self.setBrush(QBrush(QColor(255, 0, 0, 250)))
def hoverLeaveEvent(self, event):
self.setBrush(QBrush(QColor(255, 0, 0, 120)))
class MyWindow(QMainWindow):
def __init__(self):
super(MyWindow, self).__init__()
self.setGeometry(200, 0, 500, 600)
self.canvas = QGraphicsView()
self.canvas.setScene(QGraphicsScene(self))
polygon = QPolygonF([
QPointF(0, 0),
QPointF(100, 0),
QPointF(100, 100),
QPointF(0, 100)
])
self.polygon_item = Polygon(polygon)
self.canvas.scene().addItem(self.polygon_item)
polygon = QPolygonF([
QPointF(110, 110),
QPointF(150, 160),
QPointF(200, 250),
QPointF(200, 100)
])
self.polygon_item = Polygon(polygon)
self.canvas.scene().addItem(self.polygon_item)
polygon = QPolygonF([
QPointF(0, 200),
QPointF(0, 300),
QPointF(100, 300),
QPointF(100, 200)
])
self.polygon_item = Polygon(polygon)
self.canvas.scene().addItem(self.polygon_item)
self.setCentralWidget(self.canvas)
if __name__ == '__main__':
app = QApplication([])
win = MyWindow()
win.show()
app.exec()
这实际上看起来像是一个错误,但它还需要进一步检查才能意识到问题的难度。
首先,这个问题不依赖于创建顺序:如果你在任何其他多边形之后移动第一个多边形,问题仍然存在。
问题与以下几个方面有关:
shape()
函数进行碰撞检测(包括鼠标悬停),在 QAbstractGraphicsShapeItem 子类(如 QGraphicsPolygonItem)的情况下,它返回一个复杂的 QPainterPath 从项目的“路径”开始创建并使用其 调整pen()
,使用 QGraphicsPathStroker,创建另一个 QPainterPath,它使用提供的 QPen 具有给定路径的“轮廓”;不幸的是,尽管 QPainterPath 功能强大且智能,但它并不完美:Qt 开发人员必须在功能和性能之间取得平衡,因为 QPainterPaths 在 Qt 中使用很多,因此需要非常轻便和快速。
结果是你的路径的最终
shape()
相当复杂。尝试将以下覆盖添加到您的 Polygon
类中:
def paint(self, qp, opt, widget=None):
super().paint(qp, opt, widget)
qp.save()
qp.setPen(Qt.black)
qp.drawPath(self.shape())
qp.restore()
你会看到
shape()
的结果路径比预期的要复杂得多:
现在,问题出现了,因为QGraphicsScene 使用QPainterPath
contains()
函数来检测鼠标光标是否within 图形项边界。对于如此复杂的路径,默认(“快速”)行为可能会因您设置的非常大的轮廓而失败。
我不会过多地深入数学方面,但考虑到 碰撞检测 是一个众所周知的 困境 并且处理它意味着在处理通用 API 行为时必须做出一些利弊决定。 QPolygons 可能是凸的,甚至有相交的线:例如,如果多边形 like this 及其太粗的线,您将如何处理?
现在,假设您的多边形始终具有一致的顶点(意味着绝对没有交点,包括由笔宽引起的交点),并且您不会使用太多不那么复杂的项目或形状,有一个可能的解决方法:自己提供
shape()
。
诀窍是让默认返回
shape()
,break它在toSubpathPolygons()
并遍历它们以检查它们中的哪些不不包含任何其他。
不幸的是,不确定哪个子路径多边形实际上属于 stroker 的边界:它理论上是第一个,但它可能不是,所以我们需要使用 while 循环仔细遍历所有这些。
注意,如上所述,多边形必须是闭。为了简化事情,我在
__init__
中创建了一个新的 QPolygonF(可变对象永远不应该在另一个对象的初始化中更改),但是如果你想以 correct 的方式做事,你应该添加一个进一步的实现最终返回一个封闭的多边形和仅如果需要。
class Polygon(QGraphicsPolygonItem):
def __init__(self, polygon):
if not polygon.isClosed():
polygon = QPolygonF(polygon)
polygon.append(polygon[0])
super().__init__(polygon)
self.setBrush(QBrush(QColor(255, 0, 0, 120)))
self.setPen(QPen(QColor(255, 0, 0), 10))
self.setAcceptHoverEvents(True)
def shape(self):
shape = super().shape().simplified()
polys = iter(shape.toSubpathPolygons(self.transform()))
outline = next(polys)
while True:
try:
other = next(polys)
except StopIteration:
break
for p in other:
# check if all points of the other polygon are *contained*
# within the current (possible) "outline"
if outline.containsPoint(p, Qt.WindingFill):
# the point is *inside*, meaning that the "other"
# polygon is probably an internal intersection
break
else:
# no intersection found, the "other" polygon is probably the
# *actual* outline of the QPainterPathStroker
outline = other
path = QPainterPath()
path.addPolygon(outline)
return path