完成图片框选功能
This commit is contained in:
parent
cc460cfbcd
commit
f2c782a542
|
@ -1,196 +1,308 @@
|
||||||
from PySide6.QtCore import Qt, QPointF, QRectF, Signal
|
from PySide6.QtCore import Qt, QPointF, QRectF, Signal, QSize, QLineF
|
||||||
from PySide6.QtGui import QPixmap, QPainter, QPen, QBrush, QCursor, QColor, QPainterPath
|
from PySide6.QtGui import (QPixmap, QPainter, QPen, QBrush, QCursor, QColor, QImage,
|
||||||
|
QPainterPath, QTransform)
|
||||||
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton,
|
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||||
QGraphicsView, QGraphicsScene, QGraphicsItem,
|
QGraphicsView, QGraphicsScene, QGraphicsItem,
|
||||||
QGraphicsRectItem, QGraphicsEllipseItem,
|
QGraphicsRectItem, QGraphicsEllipseItem,
|
||||||
QGraphicsPixmapItem, QSizePolicy)
|
QGraphicsPixmapItem, QSizePolicy)
|
||||||
|
import math
|
||||||
class ResizableGraphicsItem(QGraphicsRectItem):
|
class ResizableGraphicsItem(QGraphicsRectItem):
|
||||||
def __init__(self, x, y, width, height, parent=None):
|
handle_radius = 12 # 手柄半径(视觉+点击区域)
|
||||||
super().__init__(x, y, width, height, parent)
|
rotate_offset = 0 # 旋转手柄离矩形顶边的距离
|
||||||
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
|
|
||||||
self.setFlag(QGraphicsItem.ItemIsMovable, True)
|
def __init__(self, x, y, w, h, parent=None):
|
||||||
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
|
super().__init__(x, y, w, h, parent)
|
||||||
|
self.setFlags(QGraphicsItem.ItemIsSelectable |
|
||||||
|
QGraphicsItem.ItemIsMovable |
|
||||||
|
QGraphicsItem.ItemSendsGeometryChanges)
|
||||||
self.setAcceptHoverEvents(True)
|
self.setAcceptHoverEvents(True)
|
||||||
|
self.setPen(QPen(Qt.blue, 2))
|
||||||
self.handle_size = 8
|
self.setBrush(QBrush(Qt.transparent))
|
||||||
self.handles = {}
|
|
||||||
self.selected_handle = None
|
self.setTransformOriginPoint(self.rect().center())
|
||||||
self.mouse_press_pos = None
|
|
||||||
self.mouse_press_rect = None
|
self._rotating = False
|
||||||
|
self._resize_corner = None
|
||||||
# 创建调整大小的手柄
|
self._orig_rect = QRectF()
|
||||||
self.create_handles()
|
self._press_scene = QPointF()
|
||||||
|
self._orig_rotation = 0.0
|
||||||
def create_handles(self):
|
|
||||||
"""创建调整大小的手柄"""
|
self._rotate_anchor = QPointF() # 旋转中心(item 坐标)
|
||||||
rect = self.rect()
|
self._last_vec = QPointF() # 上一帧鼠标相对向量
|
||||||
self.handles = {
|
|
||||||
"top_left": QRectF(rect.left(), rect.top(), self.handle_size, self.handle_size),
|
def setRect(self, rect):
|
||||||
"top_right": QRectF(rect.right() - self.handle_size, rect.top(), self.handle_size, self.handle_size),
|
super().setRect(rect)
|
||||||
"bottom_left": QRectF(rect.left(), rect.bottom() - self.handle_size, self.handle_size, self.handle_size),
|
self.setTransformOriginPoint(rect.center())
|
||||||
"bottom_right": QRectF(rect.right() - self.handle_size, rect.bottom() - self.handle_size, self.handle_size, self.handle_size),
|
|
||||||
"top": QRectF(rect.center().x() - self.handle_size/2, rect.top(), self.handle_size, self.handle_size),
|
|
||||||
"bottom": QRectF(rect.center().x() - self.handle_size/2, rect.bottom() - self.handle_size, self.handle_size, self.handle_size),
|
|
||||||
"left": QRectF(rect.left(), rect.center().y() - self.handle_size/2, self.handle_size, self.handle_size),
|
|
||||||
"right": QRectF(rect.right() - self.handle_size, rect.center().y() - self.handle_size/2, self.handle_size, self.handle_size),
|
|
||||||
}
|
|
||||||
|
|
||||||
def update_handles(self):
|
|
||||||
"""更新手柄位置"""
|
|
||||||
rect = self.rect()
|
|
||||||
self.handles["top_left"].moveTopLeft(rect.topLeft())
|
|
||||||
self.handles["top_right"].moveTopRight(rect.topRight())
|
|
||||||
self.handles["bottom_left"].moveBottomLeft(rect.bottomLeft())
|
|
||||||
self.handles["bottom_right"].moveBottomRight(rect.bottomRight())
|
|
||||||
self.handles["top"].moveCenter(QPointF(rect.center().x(), rect.top() + self.handle_size/2))
|
|
||||||
self.handles["bottom"].moveCenter(QPointF(rect.center().x(), rect.bottom() - self.handle_size/2))
|
|
||||||
self.handles["left"].moveCenter(QPointF(rect.left() + self.handle_size/2, rect.center().y()))
|
|
||||||
self.handles["right"].moveCenter(QPointF(rect.right() - self.handle_size/2, rect.center().y()))
|
|
||||||
|
|
||||||
def paint(self, painter, option, widget=None):
|
def paint(self, painter, option, widget=None):
|
||||||
"""绘制项和手柄"""
|
super().paint(painter, option, widget)
|
||||||
# 绘制主矩形
|
if not self.isSelected():
|
||||||
pen = QPen(Qt.blue, 2, Qt.SolidLine)
|
return
|
||||||
if self.isSelected():
|
|
||||||
pen.setStyle(Qt.DashLine)
|
painter.save()
|
||||||
painter.setPen(pen)
|
painter.setRenderHint(QPainter.Antialiasing) # 消除残影
|
||||||
painter.setBrush(QBrush(Qt.transparent))
|
painter.setPen(QPen(Qt.black, 1))
|
||||||
painter.drawRect(self.rect())
|
|
||||||
|
for name, pt in self._handles_local().items():
|
||||||
|
color = Qt.red if name == 'rotate' else Qt.green
|
||||||
|
painter.setBrush(QBrush(color))
|
||||||
|
painter.drawEllipse(pt, self.handle_radius, self.handle_radius)
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
def drawTransformHandles(self, painter):
|
||||||
|
"""绘制变换控制点"""
|
||||||
|
rect = self.rect()
|
||||||
|
handle_size = self.handle_radius * 2
|
||||||
|
|
||||||
# 如果选中,绘制手柄
|
# 四个角的控制点
|
||||||
if self.isSelected():
|
corners = [
|
||||||
painter.setBrush(QBrush(Qt.white))
|
rect.topLeft(),
|
||||||
painter.setPen(QPen(Qt.black, 1))
|
rect.topRight(),
|
||||||
for handle in self.handles.values():
|
rect.bottomLeft(),
|
||||||
painter.drawRect(handle)
|
rect.bottomRight()
|
||||||
|
]
|
||||||
def hoverMoveEvent(self, event):
|
|
||||||
"""鼠标悬停时检查是否在手柄上"""
|
|
||||||
for handle_name, handle_rect in self.handles.items():
|
|
||||||
if handle_rect.contains(event.pos()):
|
|
||||||
# 根据手柄位置设置不同的光标
|
|
||||||
if handle_name in ["top_left", "bottom_right"]:
|
|
||||||
self.setCursor(Qt.SizeFDiagCursor)
|
|
||||||
elif handle_name in ["top_right", "bottom_left"]:
|
|
||||||
self.setCursor(Qt.SizeBDiagCursor)
|
|
||||||
elif handle_name in ["top", "bottom"]:
|
|
||||||
self.setCursor(Qt.SizeVerCursor)
|
|
||||||
elif handle_name in ["left", "right"]:
|
|
||||||
self.setCursor(Qt.SizeHorCursor)
|
|
||||||
self.selected_handle = handle_name
|
|
||||||
return
|
|
||||||
|
|
||||||
self.setCursor(Qt.SizeAllCursor)
|
# 旋转控制点(顶部中间)
|
||||||
self.selected_handle = None
|
rotate_pos = QPointF(rect.center().x(), rect.top() - 20)
|
||||||
super().hoverMoveEvent(event)
|
|
||||||
|
|
||||||
def hoverLeaveEvent(self, event):
|
|
||||||
"""鼠标离开时恢复默认光标"""
|
|
||||||
self.setCursor(Qt.ArrowCursor)
|
|
||||||
self.selected_handle = None
|
|
||||||
super().hoverLeaveEvent(event)
|
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
|
||||||
"""鼠标按下事件"""
|
|
||||||
if event.button() == Qt.LeftButton:
|
|
||||||
self.mouse_press_pos = event.pos()
|
|
||||||
self.mouse_press_rect = self.rect()
|
|
||||||
|
|
||||||
# 如果点击的是手柄,则开始调整大小
|
|
||||||
if self.selected_handle:
|
|
||||||
event.accept()
|
|
||||||
return
|
|
||||||
|
|
||||||
super().mousePressEvent(event)
|
# 绘制控制点
|
||||||
|
painter.setBrush(QBrush(Qt.green))
|
||||||
|
painter.setPen(QPen(Qt.black, 1))
|
||||||
|
for corner in corners:
|
||||||
|
painter.drawEllipse(corner, handle_size, handle_size)
|
||||||
|
|
||||||
|
# 绘制旋转控制点
|
||||||
|
painter.setBrush(QBrush(Qt.red))
|
||||||
|
painter.drawEllipse(rotate_pos, handle_size, handle_size)
|
||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
def mouseMoveEvent(self, event):
|
||||||
"""鼠标移动事件"""
|
if self._rotating:
|
||||||
if event.buttons() & Qt.LeftButton and self.mouse_press_pos:
|
# 当前鼠标向量
|
||||||
if self.selected_handle:
|
cur_vec = event.scenePos() - self._rotate_center
|
||||||
# 调整大小
|
# 计算角度增量
|
||||||
self.resize_item(event.pos())
|
old_angle = math.atan2(self._press_vec.y(), self._press_vec.x())
|
||||||
event.accept()
|
new_angle = math.atan2(cur_vec.y(), cur_vec.x())
|
||||||
return
|
delta = math.degrees(new_angle - old_angle)
|
||||||
else:
|
|
||||||
# 移动项
|
# 直接累加旋转角,Qt 会按 TransformOriginPoint 旋转
|
||||||
super().mouseMoveEvent(event)
|
self.setRotation(self.rotation() + delta)
|
||||||
else:
|
|
||||||
super().mouseMoveEvent(event)
|
self._press_vec = cur_vec
|
||||||
|
event.accept()
|
||||||
def resize_item(self, mouse_pos):
|
return
|
||||||
"""根据鼠标位置调整项大小"""
|
|
||||||
rect = self.mouse_press_rect
|
if self._resize_corner:
|
||||||
pos = mouse_pos
|
delta = event.pos() - self._press_local
|
||||||
new_rect = QRectF(rect)
|
r = self._orig_rect
|
||||||
|
new_r = QRectF(r)
|
||||||
|
if self._resize_corner == 'topLeft':
|
||||||
|
new_r.setTopLeft(r.topLeft() + delta)
|
||||||
|
elif self._resize_corner == 'topRight':
|
||||||
|
new_r.setTopRight(r.topRight() + delta)
|
||||||
|
elif self._resize_corner == 'bottomLeft':
|
||||||
|
new_r.setBottomLeft(r.bottomLeft() + delta)
|
||||||
|
elif self._resize_corner == 'bottomRight':
|
||||||
|
new_r.setBottomRight(r.bottomRight() + delta)
|
||||||
|
|
||||||
|
if new_r.width() >= 10 and new_r.height() >= 10:
|
||||||
|
self.setRect(new_r)
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
super().mouseMoveEvent(event)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
if event.button() != Qt.LeftButton:
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
return
|
||||||
|
|
||||||
|
hit = self._hit_handle(event.pos())
|
||||||
|
if hit == 'rotate':
|
||||||
|
self._rotating = True
|
||||||
|
# 记录旋转中心(场景坐标)
|
||||||
|
self._rotate_center = self.mapToScene(self.rect().center())
|
||||||
|
# 记录鼠标第一帧向量(场景坐标)
|
||||||
|
self._press_vec = event.scenePos() - self._rotate_center
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
if hit in ('topLeft', 'topRight', 'bottomLeft', 'bottomRight'):
|
||||||
|
self._resize_corner = hit
|
||||||
|
self._orig_rect = self.rect()
|
||||||
|
self._press_local = event.pos()
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
|
||||||
# 根据选中的手柄调整矩形
|
if hit == 'rotate':
|
||||||
if self.selected_handle == "top_left":
|
self._rotating = True
|
||||||
new_rect.setTopLeft(pos)
|
self._rotate_anchor = self.rect().center() # 旋转中心
|
||||||
elif self.selected_handle == "top_right":
|
self._last_vec = event.pos() - self._rotate_anchor # 第一帧向量
|
||||||
new_rect.setTopRight(pos)
|
event.accept()
|
||||||
elif self.selected_handle == "bottom_left":
|
return
|
||||||
new_rect.setBottomLeft(pos)
|
|
||||||
elif self.selected_handle == "bottom_right":
|
super().mousePressEvent(event)
|
||||||
new_rect.setBottomRight(pos)
|
|
||||||
elif self.selected_handle == "top":
|
|
||||||
new_rect.setTop(pos.y())
|
|
||||||
elif self.selected_handle == "bottom":
|
|
||||||
new_rect.setBottom(pos.y())
|
|
||||||
elif self.selected_handle == "left":
|
|
||||||
new_rect.setLeft(pos.x())
|
|
||||||
elif self.selected_handle == "right":
|
|
||||||
new_rect.setRight(pos.x())
|
|
||||||
|
|
||||||
# 确保矩形不会太小
|
|
||||||
if new_rect.width() < 10:
|
|
||||||
if self.selected_handle in ["left", "top_left", "bottom_left"]:
|
|
||||||
new_rect.setLeft(new_rect.right() - 10)
|
|
||||||
else:
|
|
||||||
new_rect.setRight(new_rect.left() + 10)
|
|
||||||
|
|
||||||
if new_rect.height() < 10:
|
|
||||||
if self.selected_handle in ["top", "top_left", "top_right"]:
|
|
||||||
new_rect.setTop(new_rect.bottom() - 10)
|
|
||||||
else:
|
|
||||||
new_rect.setBottom(new_rect.top() + 10)
|
|
||||||
|
|
||||||
self.setRect(new_rect)
|
|
||||||
self.update_handles()
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
def mouseReleaseEvent(self, event):
|
||||||
"""鼠标释放事件"""
|
self._rotating = False
|
||||||
self.mouse_press_pos = None
|
self._resize_corner = None
|
||||||
self.mouse_press_rect = None
|
self.setCursor(Qt.ArrowCursor)
|
||||||
self.selected_handle = None
|
|
||||||
super().mouseReleaseEvent(event)
|
super().mouseReleaseEvent(event)
|
||||||
|
|
||||||
def itemChange(self, change, value):
|
def hoverMoveEvent(self, event):
|
||||||
"""项变化时更新手柄位置"""
|
if not self.isSelected():
|
||||||
if change == QGraphicsItem.ItemSelectedChange:
|
super().hoverMoveEvent(event)
|
||||||
self.update_handles()
|
return
|
||||||
elif change == QGraphicsItem.ItemPositionHasChanged or change == QGraphicsItem.ItemTransformHasChanged:
|
hit = self._hit_handle(event.pos())
|
||||||
self.update_handles()
|
self.setCursor(Qt.PointingHandCursor if hit == 'rotate' else
|
||||||
|
Qt.SizeFDiagCursor if hit in ('topLeft', 'bottomRight') else
|
||||||
return super().itemChange(change, value)
|
Qt.SizeBDiagCursor if hit in ('topRight', 'bottomLeft') else
|
||||||
|
Qt.ArrowCursor)
|
||||||
|
super().hoverMoveEvent(event)
|
||||||
|
|
||||||
|
def _handles_local(self):
|
||||||
|
r = self.rect()
|
||||||
|
return {
|
||||||
|
'topLeft' : r.topLeft(),
|
||||||
|
'topRight' : r.topRight(),
|
||||||
|
'bottomLeft' : r.bottomLeft(),
|
||||||
|
'bottomRight': r.bottomRight(),
|
||||||
|
'rotate' : QPointF(r.center().x(), r.top() - self.rotate_offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
def boundingRect(self):
|
||||||
|
m = self.handle_radius + 5
|
||||||
|
return self.rect().adjusted(-m, -m - self.rotate_offset, m, m)
|
||||||
|
|
||||||
|
def _handles(self):
|
||||||
|
"""返回所有手柄的 scene 坐标"""
|
||||||
|
rect = self.rect()
|
||||||
|
center = self.mapToScene(rect.center())
|
||||||
|
top_mid = self.mapToScene(QPointF(rect.center().x(), rect.top()))
|
||||||
|
rotate = top_mid + QPointF(0, -20) # 旋转手柄
|
||||||
|
return {
|
||||||
|
'topLeft' : self.mapToScene(rect.topLeft()),
|
||||||
|
'topRight' : self.mapToScene(rect.topRight()),
|
||||||
|
'bottomLeft' : self.mapToScene(rect.bottomLeft()),
|
||||||
|
'bottomRight': self.mapToScene(rect.bottomRight()),
|
||||||
|
'rotate' : rotate
|
||||||
|
}
|
||||||
|
|
||||||
|
def _hit_handle(self, pos_local):
|
||||||
|
for name, pt in self._handles_local().items():
|
||||||
|
if (pos_local - pt).manhattanLength() <= self.handle_radius:
|
||||||
|
return name
|
||||||
|
return None
|
||||||
class ResizableEllipseItem(ResizableGraphicsItem):
|
class ResizableEllipseItem(ResizableGraphicsItem):
|
||||||
def paint(self, painter, option, widget=None):
|
def paint(self, painter, option, widget=None):
|
||||||
"""绘制椭圆和手柄"""
|
# 先画椭圆
|
||||||
# 绘制主椭圆
|
painter.setPen(QPen(Qt.red, 2))
|
||||||
pen = QPen(Qt.red, 2, Qt.SolidLine)
|
painter.setBrush(Qt.NoBrush)
|
||||||
if self.isSelected():
|
|
||||||
pen.setStyle(Qt.DashLine)
|
|
||||||
painter.setPen(pen)
|
|
||||||
painter.setBrush(QBrush(Qt.transparent))
|
|
||||||
painter.drawEllipse(self.rect())
|
painter.drawEllipse(self.rect())
|
||||||
|
|
||||||
# 如果选中,绘制手柄
|
# 选中时画控制手柄
|
||||||
if self.isSelected():
|
if self.isSelected():
|
||||||
painter.setBrush(QBrush(Qt.white))
|
self.drawTransformHandles(painter)
|
||||||
painter.setPen(QPen(Qt.black, 1))
|
|
||||||
for handle in self.handles.values():
|
class CustomGraphicsView(QGraphicsView):
|
||||||
painter.drawRect(handle)
|
def __init__(self, scene, editor, parent=None):
|
||||||
|
super().__init__(scene, parent)
|
||||||
|
self.editor = editor
|
||||||
|
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
|
||||||
|
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
|
||||||
|
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||||
|
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||||
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
|
self._pan = False
|
||||||
|
self._pan_start = QPointF()
|
||||||
|
self._last_pan_value = QPointF()
|
||||||
|
|
||||||
|
def wheelEvent(self, event):
|
||||||
|
"""鼠标滚轮缩放"""
|
||||||
|
factor = 1.2
|
||||||
|
if event.angleDelta().y() < 0:
|
||||||
|
factor = 1.0 / factor
|
||||||
|
|
||||||
|
self.scale(factor, factor)
|
||||||
|
self.editor.scale_factor *= factor
|
||||||
|
self.limit_view_to_image()
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
if event.button() == Qt.LeftButton:
|
||||||
|
scene_pos = self.mapToScene(event.position().toPoint())
|
||||||
|
items = self.scene().items(scene_pos)
|
||||||
|
|
||||||
|
# 如果点到了框线,让框线处理,且禁止 pan
|
||||||
|
if any(isinstance(it, (ResizableGraphicsItem, ResizableEllipseItem))
|
||||||
|
for it in items):
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 选择模式下点空白,启动 pan
|
||||||
|
if self.editor.mouse_state == "select":
|
||||||
|
self.scene().clearSelection() # 取消所有选中
|
||||||
|
self._pan = True
|
||||||
|
self._pan_start = event.position()
|
||||||
|
self.setCursor(Qt.ClosedHandCursor)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.editor.mouse_state in ["create_rect", "create_ellipse"]:
|
||||||
|
self.editor.start_pos = scene_pos
|
||||||
|
if self.editor.mouse_state == "create_rect":
|
||||||
|
self.editor.current_item = ResizableGraphicsItem(0, 0, 0, 0)
|
||||||
|
else:
|
||||||
|
self.editor.current_item = ResizableEllipseItem(0, 0, 0, 0)
|
||||||
|
self.editor.current_item.setRect(QRectF(scene_pos, scene_pos))
|
||||||
|
self.scene().addItem(self.editor.current_item)
|
||||||
|
self.editor.current_item.setSelected(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
if self._pan:
|
||||||
|
delta = event.position() - self._pan_start
|
||||||
|
self._pan_start = event.position()
|
||||||
|
hs = self.horizontalScrollBar()
|
||||||
|
vs = self.verticalScrollBar()
|
||||||
|
hs.setValue(hs.value() - int(delta.x()))
|
||||||
|
vs.setValue(vs.value() - int(delta.y()))
|
||||||
|
return
|
||||||
|
if self.editor.current_item and self.editor.start_pos:
|
||||||
|
scene_pos = self.mapToScene(event.position().toPoint())
|
||||||
|
rect = QRectF(self.editor.start_pos, scene_pos).normalized()
|
||||||
|
self.editor.current_item.setRect(rect)
|
||||||
|
return
|
||||||
|
|
||||||
|
super().mouseMoveEvent(event)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
if event.button() == Qt.MiddleButton or self._pan:
|
||||||
|
self._pan = False
|
||||||
|
self.setCursor(Qt.ArrowCursor)
|
||||||
|
return
|
||||||
|
if self.editor.current_item:
|
||||||
|
if self.editor.current_item.rect().width() < 10 or self.editor.current_item.rect().height() < 10:
|
||||||
|
self.scene().removeItem(self.editor.current_item)
|
||||||
|
else:
|
||||||
|
self.editor.set_select_mode()
|
||||||
|
self.editor.current_item = None
|
||||||
|
self.editor.start_pos = None
|
||||||
|
return
|
||||||
|
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
|
||||||
|
def limit_view_to_image(self):
|
||||||
|
"""限制视图范围不超过图片边界"""
|
||||||
|
if not self.editor.pixmap_item:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 确保滚动条不会超出范围
|
||||||
|
h_scroll = self.horizontalScrollBar()
|
||||||
|
v_scroll = self.verticalScrollBar()
|
||||||
|
|
||||||
|
h_scroll.setValue(max(0, min(h_scroll.value(), h_scroll.maximum())))
|
||||||
|
v_scroll.setValue(max(0, min(v_scroll.value(), v_scroll.maximum())))
|
||||||
|
|
||||||
class DefectMarkEditor(QDialog):
|
class DefectMarkEditor(QDialog):
|
||||||
def __init__(self, parent=None, image_path="", current_description=""):
|
def __init__(self, parent=None, image_path="", current_description=""):
|
||||||
|
@ -204,7 +316,11 @@ class DefectMarkEditor(QDialog):
|
||||||
self.start_pos = None
|
self.start_pos = None
|
||||||
self.current_item = None
|
self.current_item = None
|
||||||
self.scale_factor = 1.0
|
self.scale_factor = 1.0
|
||||||
|
self.is_panning = False
|
||||||
|
self.pan_start_pos = QPointF()
|
||||||
|
self.max_view_size = QSize(1920, 1080) # 最大视图尺寸
|
||||||
|
self.pixmap_item = None
|
||||||
|
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
self.load_image()
|
self.load_image()
|
||||||
|
|
||||||
|
@ -239,8 +355,8 @@ class DefectMarkEditor(QDialog):
|
||||||
self.scene = QGraphicsScene(self)
|
self.scene = QGraphicsScene(self)
|
||||||
self.view = CustomGraphicsView(self.scene, self)
|
self.view = CustomGraphicsView(self.scene, self)
|
||||||
self.view.setRenderHint(QPainter.Antialiasing)
|
self.view.setRenderHint(QPainter.Antialiasing)
|
||||||
self.view.setDragMode(QGraphicsView.RubberBandDrag)
|
|
||||||
self.view.setMouseTracking(True)
|
self.view.setMouseTracking(True)
|
||||||
|
self.view.setDragMode(QGraphicsView.NoDrag)
|
||||||
|
|
||||||
main_layout.addLayout(toolbar)
|
main_layout.addLayout(toolbar)
|
||||||
main_layout.addWidget(self.view)
|
main_layout.addWidget(self.view)
|
||||||
|
@ -251,13 +367,32 @@ class DefectMarkEditor(QDialog):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.scene.clear()
|
self.scene.clear()
|
||||||
self.pixmap_item = QGraphicsPixmapItem(QPixmap(self.image_path))
|
|
||||||
|
# 加载图片并计算合适的缩放比例
|
||||||
|
image = QImage(self.image_path)
|
||||||
|
if image.isNull():
|
||||||
|
return
|
||||||
|
|
||||||
|
# 计算缩放比例以适应视图
|
||||||
|
img_size = image.size()
|
||||||
|
|
||||||
|
# 如果图片大于最大视图尺寸,则缩放
|
||||||
|
if img_size.width() > self.max_view_size.width() or img_size.height() > self.max_view_size.height():
|
||||||
|
img_size.scale(self.max_view_size, Qt.KeepAspectRatio)
|
||||||
|
scaled_pixmap = QPixmap.fromImage(image.scaled(img_size, Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
||||||
|
else:
|
||||||
|
scaled_pixmap = QPixmap.fromImage(image)
|
||||||
|
|
||||||
|
self.pixmap_item = QGraphicsPixmapItem(scaled_pixmap)
|
||||||
self.pixmap_item.setFlag(QGraphicsItem.ItemIsMovable, False)
|
self.pixmap_item.setFlag(QGraphicsItem.ItemIsMovable, False)
|
||||||
self.pixmap_item.setFlag(QGraphicsItem.ItemIsSelectable, False)
|
self.pixmap_item.setFlag(QGraphicsItem.ItemIsSelectable, False)
|
||||||
self.scene.addItem(self.pixmap_item)
|
self.scene.addItem(self.pixmap_item)
|
||||||
|
|
||||||
# 设置场景大小为图片大小
|
# 设置场景大小为图片大小
|
||||||
self.scene.setSceneRect(self.pixmap_item.boundingRect())
|
self.scene.setSceneRect(self.pixmap_item.boundingRect())
|
||||||
|
|
||||||
|
# 自动调整视图以显示整个图片
|
||||||
|
self.view.fitInView(self.pixmap_item, Qt.KeepAspectRatio)
|
||||||
|
|
||||||
def set_select_mode(self):
|
def set_select_mode(self):
|
||||||
"""设置为选择模式"""
|
"""设置为选择模式"""
|
||||||
|
@ -265,7 +400,7 @@ class DefectMarkEditor(QDialog):
|
||||||
self.select_btn.setChecked(True)
|
self.select_btn.setChecked(True)
|
||||||
self.rect_btn.setChecked(False)
|
self.rect_btn.setChecked(False)
|
||||||
self.ellipse_btn.setChecked(False)
|
self.ellipse_btn.setChecked(False)
|
||||||
self.view.setDragMode(QGraphicsView.RubberBandDrag)
|
self.view.setCursor(Qt.ArrowCursor)
|
||||||
|
|
||||||
def set_create_rect_mode(self):
|
def set_create_rect_mode(self):
|
||||||
"""设置为创建矩形模式"""
|
"""设置为创建矩形模式"""
|
||||||
|
@ -273,7 +408,7 @@ class DefectMarkEditor(QDialog):
|
||||||
self.select_btn.setChecked(False)
|
self.select_btn.setChecked(False)
|
||||||
self.rect_btn.setChecked(True)
|
self.rect_btn.setChecked(True)
|
||||||
self.ellipse_btn.setChecked(False)
|
self.ellipse_btn.setChecked(False)
|
||||||
self.view.setDragMode(QGraphicsView.NoDrag)
|
self.view.setCursor(Qt.CrossCursor)
|
||||||
|
|
||||||
def set_create_ellipse_mode(self):
|
def set_create_ellipse_mode(self):
|
||||||
"""设置为创建圆形模式"""
|
"""设置为创建圆形模式"""
|
||||||
|
@ -281,94 +416,90 @@ class DefectMarkEditor(QDialog):
|
||||||
self.select_btn.setChecked(False)
|
self.select_btn.setChecked(False)
|
||||||
self.rect_btn.setChecked(False)
|
self.rect_btn.setChecked(False)
|
||||||
self.ellipse_btn.setChecked(True)
|
self.ellipse_btn.setChecked(True)
|
||||||
self.view.setDragMode(QGraphicsView.NoDrag)
|
self.view.setCursor(Qt.CrossCursor)
|
||||||
|
|
||||||
def clear_all_items(self):
|
def clear_all_items(self):
|
||||||
"""清除所有标记项"""
|
"""清除所有标记项"""
|
||||||
for item in self.scene.items():
|
for item in self.scene.items():
|
||||||
if isinstance(item, (ResizableGraphicsItem, ResizableEllipseItem)):
|
if isinstance(item, (ResizableGraphicsItem, ResizableEllipseItem)):
|
||||||
self.scene.removeItem(item)
|
self.scene.removeItem(item)
|
||||||
|
|
||||||
def mouse_press_on_scene(self, pos):
|
|
||||||
"""场景鼠标按下事件"""
|
|
||||||
if self.mouse_state == "select":
|
|
||||||
return
|
|
||||||
|
|
||||||
self.start_pos = pos
|
|
||||||
if self.mouse_state == "create_rect":
|
|
||||||
self.current_item = ResizableGraphicsItem(pos.x(), pos.y(), 1, 1)
|
|
||||||
elif self.mouse_state == "create_ellipse":
|
|
||||||
self.current_item = ResizableEllipseItem(pos.x(), pos.y(), 1, 1)
|
|
||||||
|
|
||||||
if self.current_item:
|
|
||||||
self.scene.addItem(self.current_item)
|
|
||||||
|
|
||||||
def mouse_move_on_scene(self, pos):
|
|
||||||
"""场景鼠标移动事件"""
|
|
||||||
if not self.start_pos or not self.current_item:
|
|
||||||
return
|
|
||||||
|
|
||||||
rect = QRectF(self.start_pos, pos).normalized()
|
|
||||||
self.current_item.setRect(rect)
|
|
||||||
|
|
||||||
def mouse_release_on_scene(self, pos):
|
|
||||||
"""场景鼠标释放事件"""
|
|
||||||
if self.current_item and self.mouse_state != "select":
|
|
||||||
# 如果创建的项太小,则删除
|
|
||||||
if self.current_item.rect().width() < 10 or self.current_item.rect().height() < 10:
|
|
||||||
self.scene.removeItem(self.current_item)
|
|
||||||
|
|
||||||
self.start_pos = None
|
|
||||||
self.current_item = None
|
|
||||||
|
|
||||||
# 创建完成后自动回到选择模式
|
|
||||||
self.set_select_mode()
|
|
||||||
|
|
||||||
class CustomGraphicsView(QGraphicsView):
|
def export_annotated_image(self, output_dir):
|
||||||
def __init__(self, scene, editor, parent=None):
|
"""
|
||||||
super().__init__(scene, parent)
|
将当前场景(原图 + 所有标记)按原分辨率导出为 PNG/JPG,
|
||||||
self.editor = editor
|
文件名与原图完全一致,保存到 output_dir。
|
||||||
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
|
"""
|
||||||
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
|
import os
|
||||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
from PySide6.QtGui import QPainter, QImage
|
||||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
|
||||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
if not self.pixmap_item:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 1. 取原图 QImage(保持原始分辨率)
|
||||||
|
source_image = QImage(self.image_path)
|
||||||
|
if source_image.isNull():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2. 创建与原图同尺寸的 QImage 作为画布
|
||||||
|
out_image = QImage(source_image.size(), QImage.Format_ARGB32)
|
||||||
|
out_image.fill(Qt.transparent)
|
||||||
|
|
||||||
|
painter = QPainter(out_image)
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
|
|
||||||
|
# 3. 先把原图完整画上去
|
||||||
|
painter.drawImage(0, 0, source_image)
|
||||||
|
|
||||||
|
# 4. 计算场景 → 原图坐标 的缩放因子
|
||||||
|
scene_rect = self.pixmap_item.sceneBoundingRect()
|
||||||
|
sx = source_image.width() / scene_rect.width()
|
||||||
|
sy = source_image.height() / scene_rect.height()
|
||||||
|
|
||||||
|
# 5. 逐个绘制标记(仅最终形状,不绘制手柄)
|
||||||
|
for item in self.scene.items():
|
||||||
|
if isinstance(item, (ResizableGraphicsItem, ResizableEllipseItem)):
|
||||||
|
# 关闭选中状态,避免手柄被绘制
|
||||||
|
was_selected = item.isSelected()
|
||||||
|
item.setSelected(False)
|
||||||
|
|
||||||
|
# 计算 item 在场景中的实际几何
|
||||||
|
shape_path = item.mapToScene(item.shape()) # 考虑旋转
|
||||||
|
painter.setTransform(QTransform()) # 重置 painter
|
||||||
|
painter.scale(sx, sy) # 映射到原图坐标
|
||||||
|
painter.translate(-scene_rect.x(), -scene_rect.y()) # 对齐偏移
|
||||||
|
|
||||||
|
# 设置画笔
|
||||||
|
pen = item.pen()
|
||||||
|
pen.setCosmetic(False) # 让线宽随缩放
|
||||||
|
painter.setPen(pen)
|
||||||
|
painter.setBrush(Qt.NoBrush)
|
||||||
|
|
||||||
|
# 绘制形状
|
||||||
|
if isinstance(item, ResizableEllipseItem):
|
||||||
|
painter.drawPath(shape_path)
|
||||||
|
else:
|
||||||
|
painter.drawPath(shape_path)
|
||||||
|
|
||||||
|
# 恢复选中状态
|
||||||
|
item.setSelected(was_selected)
|
||||||
|
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
# 6. 保存文件
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
base_name = os.path.basename(self.image_path)
|
||||||
|
save_path = os.path.join(output_dir, base_name)
|
||||||
|
return out_image.save(save_path)
|
||||||
|
|
||||||
def wheelEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""鼠标滚轮缩放"""
|
"""关闭窗口时保存标记"""
|
||||||
factor = 1.2
|
self.export_annotated_image("/home/dtyx/桌面/yhh/ReportGenerator/output")
|
||||||
if event.angleDelta().y() < 0:
|
super().closeEvent(event)
|
||||||
factor = 1.0 / factor
|
|
||||||
|
if __name__ == '__main__':
|
||||||
self.scale(factor, factor)
|
from PySide6.QtWidgets import QApplication
|
||||||
self.editor.scale_factor *= factor
|
import sys
|
||||||
|
app = QApplication(sys.argv)
|
||||||
def mousePressEvent(self, event):
|
editor = DefectMarkEditor(image_path=r"/home/dtyx/桌面/yhh/ReportGenerator/测试数据/山东国华无棣风电场叶片外部数据/A1(31301)/1#(1-GW68.6C-I190809C)/DJI_20250606162745_0005_Z.JPG")
|
||||||
"""鼠标按下事件"""
|
editor.show()
|
||||||
if event.button() == Qt.LeftButton:
|
sys.exit(app.exec())
|
||||||
scene_pos = self.mapToScene(event.pos())
|
|
||||||
items = self.scene().items(scene_pos)
|
|
||||||
|
|
||||||
# 如果点击的是背景或图片,则开始创建
|
|
||||||
if not items or items[-1] == self.editor.pixmap_item:
|
|
||||||
self.editor.mouse_press_on_scene(scene_pos)
|
|
||||||
else:
|
|
||||||
super().mousePressEvent(event)
|
|
||||||
else:
|
|
||||||
super().mousePressEvent(event)
|
|
||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
|
||||||
"""鼠标移动事件"""
|
|
||||||
if self.editor.start_pos and self.editor.current_item:
|
|
||||||
scene_pos = self.mapToScene(event.pos())
|
|
||||||
self.editor.mouse_move_on_scene(scene_pos)
|
|
||||||
else:
|
|
||||||
super().mouseMoveEvent(event)
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
|
||||||
"""鼠标释放事件"""
|
|
||||||
if event.button() == Qt.LeftButton and self.editor.start_pos:
|
|
||||||
scene_pos = self.mapToScene(event.pos())
|
|
||||||
self.editor.mouse_release_on_scene(scene_pos)
|
|
||||||
else:
|
|
||||||
super().mouseReleaseEvent(event)
|
|
||||||
|
|
|
@ -2586,7 +2586,7 @@ class DictionaryBrowser(QWidget):
|
||||||
current_desc = self.current_dict_data.get(key, "") if key in self.current_dict_data else ""
|
current_desc = self.current_dict_data.get(key, "") if key in self.current_dict_data else ""
|
||||||
|
|
||||||
# 创建并显示编辑对话框
|
# 创建并显示编辑对话框
|
||||||
editor = DefectDescriptionEditor(self, current_description=current_desc)
|
editor = DefectDescriptionEditor(self, current_description=current_desc, img_path=img_path)
|
||||||
if editor.exec_() == QDialog.Accepted:
|
if editor.exec_() == QDialog.Accepted:
|
||||||
# 获取新描述
|
# 获取新描述
|
||||||
new_desc = editor.get_description()
|
new_desc = editor.get_description()
|
||||||
|
@ -2648,7 +2648,7 @@ class DefectDescriptionEditor(QDialog):
|
||||||
main_layout.setContentsMargins(5, 5, 5, 5)
|
main_layout.setContentsMargins(5, 5, 5, 5)
|
||||||
|
|
||||||
# 左侧 - 图片编辑器 (使用DefectMarkEditor)
|
# 左侧 - 图片编辑器 (使用DefectMarkEditor)
|
||||||
self.image_editor = DefectMarkEditor(self, self.img_path)
|
self.image_editor = DefectMarkEditor(image_path=self.img_path)
|
||||||
self.image_editor.setMinimumSize(600, 600)
|
self.image_editor.setMinimumSize(600, 600)
|
||||||
main_layout.addWidget(self.image_editor, stretch=2)
|
main_layout.addWidget(self.image_editor, stretch=2)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue