我创建了一个可调整大小的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
的矩形实际上是"翻译"的;从项的位置,所以你得到相对于矩形的点,加上矩形的位置。
如果你想允许在一个项目的所有边缘上调整大小,你必须考虑这些方面,还要决定是否调整大小实际上应该改变项目的位置或矩形的几何形状
最常见和建议的方法是使用第一种方法,因为它通常更一致和直接。
唯一的区别是选择项目的起始点,这取决于你的需要:通常你只需要从项目起始点开始的内容到右下角(类似于窗口的行为),但在某些情况下,内容应该"围绕";项目位置的中心(常用的"控制点")。
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)