本文采用我的博文Qt 手册 中设计的模块 xinet
探讨可塑性的矩形框的设计。先载入必备包:
from xinet import QtWidgets, QtGui, QtCore
from xinet.run_qt import run
Qt = QtCore.Qt
QGraphicsItem = QtWidgets.QGraphicsItem
QColor = QtGui.QColor
QRectF = QtCore.QRectF
本文设计可塑性的矩形框,需要一些控制点:
使用代码抽象:
class RectHandle(QtWidgets.QGraphicsRectItem):
# handles 按照顺时针排列
handle_names = ('left_top', 'middle_top', 'right_top', 'right_middle',
'right_bottom', 'middle_bottom', 'left_bottom', 'left_middle')
# 设定在控制点上的光标形状
handle_cursors = {
0: Qt.SizeFDiagCursor,
1: Qt.SizeVerCursor,
2: Qt.SizeBDiagCursor,
3: Qt.SizeHorCursor,
4: Qt.SizeFDiagCursor,
5: Qt.SizeVerCursor,
6: Qt.SizeBDiagCursor,
7: Qt.SizeHorCursor
}
offset = 4.0 # 外边界框相对于内边界框的偏移量
min_size = 8 * offset # 矩形框的最小尺寸
def update_handles_pos(self):
"""
更新控制点的位置
"""
o = self.offset # 偏置量
s = o*2 # handle 的大小
b = self.rect() # 获取内边框
x1, y1 = b.left(), b.top() # 左上角坐标
offset_x = b.width()/2
offset_y = b.height()/2
# 设置 handles 的位置
self.handles[0] = QRectF(x1-o, y1-o, s, s)
self.handles[1] = self.handles[0].adjusted(offset_x, 0, offset_x, 0)
self.handles[2] = self.handles[1].adjusted(offset_x, 0, offset_x, 0)
self.handles[3] = self.handles[2].adjusted(0, offset_y, 0, offset_y)
self.handles[4] = self.handles[3].adjusted(0, offset_y, 0, offset_y)
self.handles[5] = self.handles[4].adjusted(-offset_x, 0, -offset_x, 0)
self.handles[6] = self.handles[5].adjusted(-offset_x, 0, -offset_x, 0)
self.handles[7] = self.handles[6].adjusted(0, -offset_y, 0, -offset_y)
RectHandle
类中定义 handle_names
为控制点的名称,handle_cursors
设定控制上光标的显示形状。update_handles_pos
函数定义了矩形框的控制点所在的位置。
下面便可定制矩形框了:
class RectItem(RectHandle):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.handles = {} # 控制点的字典
self.setAcceptHoverEvents(True) # 设定为接受 hover 事件
self.setFlags(QGraphicsItem.ItemIsSelectable | # 设定矩形框为可选择的
QGraphicsItem.ItemSendsGeometryChanges | # 追踪图元改变的信息
QGraphicsItem.ItemIsFocusable | # 可移动
QGraphicsItem.ItemIsMovable) # 可移动
self.update_handles_pos() # 初始化控制点
self.reset_Ui() # 初始化 UI 变量
def reset_Ui(self):
'''初始化 UI 变量'''
self.handleSelected = None
self.mousePressPos = None
self.mousePressRect = None
def boundingRect(self):
"""
限制图元的可视化区域,且防止出现图元移动留下残影的情况
"""
o = self.offset
# 添加一个间隔为 o 的外边框
return self.rect().adjusted(-o, -o, o, o)
def paint(self, painter, option, widget=None):
"""
Paint the node in the graphic view.
"""
painter.setBrush(QtGui.QBrush(QColor(255, 0, 0, 100)))
painter.setPen(QtGui.QPen(QColor(0, 0, 0), 1.0, Qt.SolidLine))
painter.drawRect(self.rect())
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setBrush(QtGui.QBrush(QColor(255, 255, 0, 200)))
painter.setPen(QtGui.QPen(QColor(0, 0, 0, 255), 1.0,
Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
for shape in self.handles.values():
if self.isSelected():
painter.drawEllipse(shape)
使用该矩形框可以这样:
class MainWindow(QtWidgets.QGraphicsView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 设定视图尺寸
self.resize(600, 600)
# 创建场景
self.scene = QtWidgets.QGraphicsScene()
self.setSceneRect(0, 0, 600, 600) # 设置场景的边界矩形,即可视化区域矩形
# x1, y1, w, h
self.item = RectItem(20, 25, 120, 120)
self.scene.addItem(self.item)
self.scene.addItem(RectItem(200, 250, 120, 120))
# 设定视图的场景
self.setScene(self.scene)
if __name__ == '__main__':
run(MainWindow)
这样需要注意的是 boundingRect
函数,返回 QRectF
。这个纯虚拟函数将图元项目的外部边界定义为矩形;所有绘画都必须限制在图元项目的边界区域内。QtWidgets.QGraphicsView
使用它来确定该图元项目是否需要重绘。尽管图元项目的形状可以是任意的,但边界矩形始终为矩形,并且不受图元项目变换的影响。如果,没有重写该函数,可以出现图元移动留下残影的情况:
而重写 boundingRect
函数后便没有残影了:
此时,已经实现的功能有,选中图元,则会出现控制点,且支持使用鼠标拖动图元。
接着设定鼠标悬停事件:
class RectItem(RectHandle):
...
def handle_at(self, point):
"""
返回给定 point 下的控制点 handle
"""
for k, v, in self.handles.items():
if v.contains(point):
return k
return
def hoverMoveEvent(self, event):
"""
当鼠标移到该 item(未按下)上时执行。
"""
if self.isSelected():
handle = self.handle_at(event.pos())
cursor = self.handle_cursors[handle] if handle in self.handles else Qt.ArrowCursor
self.setCursor(cursor)
super().hoverMoveEvent(event)
def hoverLeaveEvent(self, event):
"""
当鼠标离开该形状(未按下)上时执行。
"""
self.setCursor(Qt.ArrowCursor)
super().hoverLeaveEvent(event)
函数 handle_at
用于判断给定的点是否在控制点上,如果在,则返回控制点的序号。hoverMoveEvent
、hoverLeaveEvent
分别设定鼠标光标进入、离开控制点的形状。
改变矩形的大小:
class RectItem(RectHandle):
...
def mousePressEvent(self, event):
"""
当在 item 上按下鼠标时执行。
"""
self.handleSelected = self.handle_at(event.pos())
if self.handleSelected in self.handles:
self.mousePressPos = event.pos()
super().mousePressEvent(event)
def mouseReleaseEvent(self, event):
"""
Executed when the mouse is released from the item.
"""
super().mouseReleaseEvent(event)
self.update()
self.reset_Ui()
def mouseMoveEvent(self, event):
"""
Executed when the mouse is being moved over the item while being pressed.
"""
if self.handleSelected in self.handles:
self.interactiveResize(event.pos())
else:
super().mouseMoveEvent(event)
def interactiveResize(self, mousePos):
"""
Perform shape interactive resize.
"""
rect = self.rect()
self.prepareGeometryChange()
# movePos = mousePos - self.mousePressPos
# move_x, move_y = movePos.x(), movePos.y()
if self.handleSelected == 0:
rect.setTopLeft(mousePos)
elif self.handleSelected == 1:
rect.setTop(mousePos.y())
elif self.handleSelected == 2:
rect.setTopRight(mousePos)
elif self.handleSelected == 3:
rect.setRight(mousePos.x())
elif self.handleSelected == 4:
rect.setBottomRight(mousePos)
elif self.handleSelected == 5:
rect.setBottom(mousePos.y())
elif self.handleSelected == 6:
rect.setBottomLeft(mousePos)
elif self.handleSelected == 7:
rect.setLeft(mousePos.x())
self.setRect(rect)
self.update_handles_pos()
效果:
一个 Bug
没有解决矩形框过小或者反转坐标的情况下出现残影等问题。
点击查看更多内容
为 TA 点赞
评论
共同学习,写下你的评论
评论加载中...
作者其他优质文章
正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦