PyQt5: Resizable ' qgraphicsrectem ':如何正确更新其位置(在场景坐标中)



我创建了一个可调整大小的QGraphicsRectItem,我可以调整它的大小,但是我无法更新新调整大小的项目在其场景中的位置

import typing
import sys
from PyQt5.QtGui import QPen, QBrush, QColor, QResizeEvent
from PyQt5.QtCore import QRectF, QSize
from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsRectItem, QMainWindow, QVBoxLayout, QWidget
class ResizableRect(QGraphicsRectItem):
def __init__(self, *args):
super().__init__(*args)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setPen(QPen(QBrush(QColor('blue')), 5))
self.selected_edge = None
self.click_pos = self.click_rect = None
def mousePressEvent(self, event):
""" The mouse is pressed, start tracking movement. """
self.click_pos = event.pos()
self.newY = self.pos().y()
rect = self.rect()
if abs(rect.left() - self.click_pos.x()) < 5:
self.selected_edge = 'left'
elif abs(rect.right() - self.click_pos.x()) < 5:
self.selected_edge = 'right'
elif abs(rect.top() - self.click_pos.y()) < 5:
self.selected_edge = 'top'
elif abs(rect.bottom() - self.click_pos.y()) < 5:
self.selected_edge = 'bottom'
else:
self.selected_edge = None
self.click_pos = event.pos()
self.click_rect = rect
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
""" Continue tracking movement while the mouse is pressed. """
# Calculate how much the mouse has moved since the click.
pos = event.pos()
x_diff = pos.x() - self.click_pos.x()
y_diff = pos.y() - self.click_pos.y()
# Start with the rectangle as it was when clicked.
rect = QRectF(self.click_rect)
# Then adjust by the distance the mouse moved.
if self.selected_edge is None:
rect.translate(x_diff, y_diff)
elif self.selected_edge == 'top':
rect.adjust(0, y_diff, 0, 0)
# Test when resize rectangle upward; not working properly for now
if y_diff < 0:              
newCenter = (rect.bottom() - pos.y()) / 2
self.newY = self.pos().y() - newCenter
elif self.selected_edge == 'left':
rect.adjust(x_diff, 0, 0, 0)
elif self.selected_edge == 'bottom':
rect.adjust(0, 0, 0, y_diff)
elif self.selected_edge == 'right':
rect.adjust(0, 0, x_diff, 0)

# Also check if the rectangle has been dragged inside out.
if rect.width() < 5:
if self.selected_edge == 'left':
rect.setLeft(rect.right() - 5)
else:
rect.setRight(rect.left() + 5)
if rect.height() < 5:
if self.selected_edge == 'top':
rect.setTop(rect.bottom() - 5)
else:
rect.setBottom(rect.top() + 5)
# Finally, update the rect that is now guaranteed to stay in bounds.
self.setY(self.newY)
self.setRect(rect)

def mouseReleaseEvent(self, event): # for printing only i.e., after resizing
print(f"item.pos(): {self.pos()}")

class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
central = QWidget(self)
self.setCentralWidget(central)
self.rect = ResizableRect(-100, -50, 200, 100)
scene = QGraphicsScene(0, 0, 300, 300)
scene.addItem(self.rect)
self.view = QGraphicsView(central)
self.view.setScene(scene)
layout = QVBoxLayout(central)
layout.addWidget(self.view)

def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

main()

现在,我正在测试更新item.pos()时向上调整大小,它不能正常工作,我需要建议来纠正这个实现。在mouseMoveEvent()中,当self.selected_edge == top时,我计算新矩形的中心。然后我计算newY值,我将使用它来更新道具在场景中的位置,即self.setY(self.newY)。结果是,当我调整大小时,项目一直向上移动。我哪里做错了?

谢谢你的帮助!

一个经常被误解的重要方面是用于图形项的坐标系统。

虽然文档中提到了三个"major"坐标系统,重要的是要理解:

  • 子项具有相对于父项的pos();
  • 任何物品坐标(本地)相对于物品位置;

文档在基本函数(如addRect())中解决了这个问题:

项目的几何形状以项目坐标表示,其位置初始化为(0,0)

理解这一点非常重要,特别是在处理鼠标事件时。

假设您创建了一个qgraphicsrectem子类并创建了它们的两个实例:

  • itemA:创建一个简单的myRectItem(100, 50, 300, 200);
  • itemB:用myRectItem(0, 0, 300, 200)创建,然后用itemB.setPos(100, 50)移动;

如果您在该类中实现mousePressEvent()并打印event.pos(),您将看到两个非常不同的结果。假设您在这些项的中心单击:

  • itemA显示QPointF(250, 150);
  • itemB将显示QPointF(150, 100);

这是因为位置在项目坐标:而itemB的矩形总是从(0, 0)(项目的原点)开始,itemB的矩形实际上是"翻译"的;从项的位置,所以你得到相对于矩形的点,加上矩形的位置。

如果你想允许在一个项目的所有边缘上调整大小,你必须考虑这些方面,还要决定是否调整大小实际上应该改变项目的位置矩形的几何形状

最常见和建议的方法是使用第一种方法,因为它通常更一致和直接。
唯一的区别是选择项目的起始点,这取决于你的需要:通常你只需要从项目起始点开始的内容到右下角(类似于窗口的行为),但在某些情况下,内容应该"围绕";项目位置的中心(常用的"控制点")。

最后,矩形的大小调整通常应该考虑它的。一个好的方法不使用字符串来标识"边",而是使用整数值,或者更好的是使用按位值。Qt提供了一些允许OR组合的基本枚举,我们可以根据需要使用Qt.Edges标志。

通过这种方式,我们不仅可以提供从边角调整大小的功能,还可以为项目设置适当的光标,以添加有关大小调整功能的视觉提示。

在下面的代码中,我实现了上述所有功能,并进一步考虑:

  • ItemIsMovable的现有实现(您通过在mouseMoveEvent()中完全覆盖它而忽略了它);
  • 笔大小(边界检测必须基于笔宽度);
  • 可变内容定位(设置在(0, 0)或围绕中心);
  • 鼠标光标移动到项目的边缘或角落时改变;
class ResizableRect(QGraphicsRectItem):
selected_edge = None
def __init__(self, x, y, width, height, onCenter=False):
if onCenter:
super().__init__(-width / 2, -height / 2, width, height)
else:
super().__init__(0, 0, width, height)
self.setPos(x, y)
self.setFlags(QGraphicsItem.ItemIsMovable)
self.setAcceptHoverEvents(True)
self.setPen(QPen(QBrush(Qt.blue), 5))
# a child item that shows the current position; note that this is only
# provided for explanation purposes, a *proper* implementation should
# use the ItemSendsGeometryChanges flag for *this* item and then
# update the value within an itemChange() override that checks for
# ItemPositionHasChanged changes.
self.posItem = QGraphicsSimpleTextItem(
'{}, {}'.format(self.x(), self.y()), parent=self)
self.posItem.setPos(
self.boundingRect().x(), 
self.boundingRect().y() - self.posItem.boundingRect().height()
)
def getEdges(self, pos):
# return a proper Qt.Edges flag that reflects the possible edge(s) at
# the given position; note that this only works properly as long as the
# shape() override is consistent and for *pure* rectangle items; if you
# are using other shapes (like QGraphicsEllipseItem) or items that have
# a different boundingRect or different implementation of shape(), the
# result might be unexpected.
# Finally, a simple edges = 0 could suffice, but considering the new
# support for Enums in PyQt6, it's usually better to use the empty flag
# as default value.
edges = Qt.Edges()
rect = self.rect()
border = self.pen().width() / 2
if pos.x() < rect.x() + border:
edges |= Qt.LeftEdge
elif pos.x() > rect.right() - border:
edges |= Qt.RightEdge
if pos.y() < rect.y() + border:
edges |= Qt.TopEdge
elif pos.y() > rect.bottom() - border:
edges |= Qt.BottomEdge
return edges
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.selected_edge = self.getEdges(event.pos())
self.offset = QPointF()
else:
self.selected_edge = Qt.Edges()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.selected_edge:
mouse_delta = event.pos() - event.buttonDownPos(Qt.LeftButton)
rect = self.rect()
pos_delta = QPointF()
border = self.pen().width()
if self.selected_edge & Qt.LeftEdge:
# ensure that the width is *always* positive, otherwise limit
# both the delta position and width, based on the border size
diff = min(mouse_delta.x() - self.offset.x(), rect.width() - border)
if rect.x() < 0:
offset = diff / 2
self.offset.setX(self.offset.x() + offset)
pos_delta.setX(offset)
rect.adjust(offset, 0, -offset, 0)
else:
pos_delta.setX(diff)
rect.setWidth(rect.width() - diff)
elif self.selected_edge & Qt.RightEdge:
if rect.x() < 0:
diff = max(mouse_delta.x() - self.offset.x(), border - rect.width())
offset = diff / 2
self.offset.setX(self.offset.x() + offset)
pos_delta.setX(offset)
rect.adjust(-offset, 0, offset, 0)
else:
rect.setWidth(max(border, event.pos().x() - rect.x()))
if self.selected_edge & Qt.TopEdge:
# similarly to what done for LeftEdge, but for the height
diff = min(mouse_delta.y() - self.offset.y(), rect.height() - border)
if rect.y() < 0:
offset = diff / 2
self.offset.setY(self.offset.y() + offset)
pos_delta.setY(offset)
rect.adjust(0, offset, 0, -offset)
else:
pos_delta.setY(diff)
rect.setHeight(rect.height() - diff)
elif self.selected_edge & Qt.BottomEdge:
if rect.y() < 0:
diff = max(mouse_delta.y() - self.offset.y(), border - rect.height())
offset = diff / 2
self.offset.setY(self.offset.y() + offset)
pos_delta.setY(offset)
rect.adjust(0, -offset, 0, offset)
else:
rect.setHeight(max(border, event.pos().y() - rect.y()))
if rect != self.rect():
self.setRect(rect)
if pos_delta:
self.setPos(self.pos() + pos_delta)
else:
# use the default implementation for ItemIsMovable
super().mouseMoveEvent(event)
self.posItem.setText('{},{} ({})'.format(
self.x(), self.y(), self.rect().getRect()))
self.posItem.setPos(
self.boundingRect().x(), 
self.boundingRect().y() - self.posItem.boundingRect().height()
)
def mouseReleaseEvent(self, event):
self.selected_edge = Qt.Edges()
super().mouseReleaseEvent(event)
def hoverMoveEvent(self, event):
edges = self.getEdges(event.pos())
if not edges:
self.unsetCursor()
elif edges in (Qt.TopEdge | Qt.LeftEdge, Qt.BottomEdge | Qt.RightEdge):
self.setCursor(Qt.SizeFDiagCursor)
elif edges in (Qt.BottomEdge | Qt.LeftEdge, Qt.TopEdge | Qt.RightEdge):
self.setCursor(Qt.SizeBDiagCursor)
elif edges in (Qt.LeftEdge, Qt.RightEdge):
self.setCursor(Qt.SizeHorCursor)
else:
self.setCursor(Qt.SizeVerCursor)

class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
scene = QGraphicsScene(0, 0, 300, 300)
self.view = QGraphicsView(scene)
self.rect = ResizableRect(0, 50, 200, 100, True)
scene.addItem(self.rect)
central = QWidget()
layout = QVBoxLayout(central)
layout.addWidget(self.view)
self.setCentralWidget(central)

最新更新