为了账号安全,请及时绑定邮箱和手机立即绑定

Qt 设计可塑性的矩形框(Python)

标签:
Python

本文采用我的博文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

本文设计可塑性的矩形框,需要一些控制点:

图1 8 个控制点操作矩形框

使用代码抽象:

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 使用它来确定该图元项目是否需要重绘。尽管图元项目的形状可以是任意的,但边界矩形始终为矩形,并且不受图元项目变换的影响。如果,没有重写该函数,可以出现图元移动留下残影的情况:

图2 图元移动留下残影

而重写 boundingRect 函数后便没有残影了:

图3 可移动的矩形框

此时,已经实现的功能有,选中图元,则会出现控制点,且支持使用鼠标拖动图元。

接着设定鼠标悬停事件:

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 用于判断给定的点是否在控制点上,如果在,则返回控制点的序号。hoverMoveEventhoverLeaveEvent 分别设定鼠标光标进入、离开控制点的形状。

改变矩形的大小:

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()

效果:

图4 可塑性矩形

一个 Bug

没有解决矩形框过小或者反转坐标的情况下出现残影等问题。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消