首页 > 其他分享 >GraphicsView框架实战:图形项的使用

GraphicsView框架实战:图形项的使用

时间:2024-08-16 16:07:17浏览次数:14  
标签:实战 center void GraphicsView Item edge 图形 CenterAndEdgePointItem

GraphicsView框架实战:图形项的使用

作者:Aderversa

声明:本文章和代码可以理解为完全开源的。

简单介绍

本篇文章在学习完一下三篇文章后而来:

一些设计参考了上面的文章,一些是自己后续做出的优化。

接下来我会详细介绍一下我参考上面文章后,自己写出的项目。

通过完成该项目,我学会了基本的图形项编写和使用方式。

需求分析

  1. 应用程序可以按照用户需求添加图形项。

  2. 用户添加图形项可以在屏幕上被移动。

  3. 用户可以通过在屏幕上进行操作,对图形项的具体形状进行微调,比如:对于矩形来说,用户通过屏幕的按压移动鼠标来调整矩形的长和宽。用户只能在图形项处于选中状态下,对图形项执行该操作。

  4. 图形项的选中和未选中状态需要有区分度,且选中状态下需要显示更详细的按键以便用户对选中的图形项进行操作。

  5. 应用程序代码需要有好的可扩展性,能支持多种图形项的扩展,比如:多边形,圆角矩形等等东西。

  6. 应用程序代码需要有好的可维护性,对于本项目来说,就是需要进行对图形项之间的解耦合。在参考的文章中,我认为有些地方的耦合是没有必要的。解决的方法也很简单:采用Qt的信号和槽机制。

    信号和槽机制虽然一定程度上会损失一些GraphicsView框架的性能。

    但是对于我们这些初学者而言,在很长一段时间内基本遇不到这种性能瓶颈。

  7. 用户可以删除处于选中状态的图形项。(编写这篇文章的时候我并没有实现该功能)

以上就是本项目基础的需求分析,以我自己为用户,自己写出原型应用而得出的需求列表。而原型是我在学习开头那几篇文章的过程中搭建起来的。

读者如果本身有更多的需求,可以在学习完本篇文章之后,自行修改或添加一些自己的需求,然后实现它。本篇文章旨在引导大家学习GraphicsView这个框架的基本使用。

总体设计

根据需求和原型的代码,我搭建出了以下的UML图,方便后续理解代码的总体结构:

该UML图使用Visual Paradigm Community Edition编写完成。

顶层Class

你可以将MyPaintBoard​理解成最顶层的类,它掌管了一个QGraphicsScene​和一个QGraphicsView​,并提供了一些QPushButton​,让用户可以完成:

  • 添加各种图形项,比如:RectangleItem​,EllipseItem​,SquareItem​和CircleItem​(图形项被添加到QGraphicsScene​中了)。
  • 删除图形项。(本功能暂时未完成)

图形项的定义

如何实现各种各样的图形项,并且保证它们的可扩展性和可维护性?

图形项的实现其实不难,就是继承QGraphcisItem​然后重写出各种各样的功能。

OK,按照这个思路,你去写完RectangleItem​,SquareItem​你会发现它们有很多的地方都是相同的。如果在重写其他的CircleItem​,EllipseItem​你会发现重复的代码越来越多,这样是非常不好的,因为后续你每扩展一个新的图形项类,你就会重复之前你做过的操作。

所以,我们需要通过一些面向对象思想来解决这个问题。


这时候,我们思考一个问题:一个图形项可以由什么构成?

学过QGraphicItem​你就知道,图形项其实就是在一个边界矩阵(boundingRect()​的返回值)中使用QPainter​画出的图形。

我们知道一个矩形的坐标,接下来其实就是利用这些坐标在边界矩阵中进行绘图操作而已。

那么矩形的坐标如何而来?

  • 一种想法是:一个中心点 + 一个边缘点,决定了一个矩形的坐标。你去看看QRectF​的构造函数就可以知道,矩形的构造其实可以通过topLeft​和bottomRight​来完成。那么当我知道了矩形的中心,和矩形的一个顶点的坐标,理论上来说就可以通过数学的方式求出整个矩形的坐标。
  • 可能是存在其他方法的:比如,就多边形而言,其矩形坐标的计算可能并不能由中心点 + 边缘点来完成,因此需要其他管理方法,于是就需要衍生其他的坐标组织形式。

这就是为什么从AbstractShapeItem​中泛化出CenterAndEdgePointItem​和OtherSpecialItem​(该类实际并不存在,只是为了展示可以有额外的扩展图形项类,你可以将它定义为PolygonItem​、RoundedRectangleItem​等等)。

AbstractShapeItem​对选中和未选中状态的切换进行了统一的管理,并通过继承QObject​为其子类提供了信号和槽机制。

CenterAndEdgePointItem

CenterAndEdgePointItem​基于这样的思想而搭建:一个图形项的边界矩阵可以由一个中心点​和一个边缘点​组成,且边界矩阵中绘制的图形可以利用二者通过数学计算得来。这两个点都是AtomPointItem​,它可以告知父Item自身位置的变化、焦点状态的变化等等,将这些变化以信号的方式发出,让父Item有能力对此做出反应。

AtomPointItem​的Atom​前缀是为了说明:这个Item没有子项!

CenterAndEdgePointItem​需要实现对中心点​和边缘点​的信号的处理。在这些处理当中,尤其重要的是AtomPointItem​的位置发生变换时,坐标系的变换问题。

CenterAndEdgePointItem​规定,中心点永远处于父Item坐标系的原点(0,0),如果中心点发生移动,那么实际上中心点不需要移动,而是要让父Item的位置需要在爷坐标系中发出相应移动,中心点就会跟着父Item移动。

注意:中心点检测到移动时不需要真的进行移动,因为中心点的移动并不会导致父Item的原点的位置变化。

如果我们移动中心点后,在爷坐标系中移动了中心点父Item(现在指的是CenterAndEdgePointItem​),那么父Item的坐标系变动其实还会推着中心点移动。

简单理解就是,中心点自己走了10步,然后你告诉中心点的父Item要移动10步,于是这个父Item自己移动了10步,并且还推着子Item走了10步,最终中心点走了20步,偏离了中心点10步。这是你中心点走的越多,偏离原点越远。

而对于边缘点,它可以处于父ItemCenterAndEdgePointItem​坐标系中的任何位置,我们利用它计算边界矩阵。直接看源码可以能比较快理解:

QRectF RectangleItem::boundingRect() const
{
    QPointF center = m_center->m_point;
    QPointF edge   = m_edge->m_point;
    qreal width =  qAbs(center.x() - edge.x());
    qreal height =  qAbs(center.y() - edge.y());
	// 这个QRectF的坐标是相对于RectangleItem坐标系的坐标
    return QRectF(QPointF(-width, -height), QPointF(width, height));
}

OtherSpecialItem

一些不能用CenterAndEdgePointItem​进行编写的图形项,比如多边形这种东西,只监控中心点和边缘点可能根本不能满足操作的需求。

这时候,我们呢就需要继承AbstractShapeItem​,然后自己按需添加AtomPointItem​然后编写新的信号处理逻辑和坐标处理逻辑,以此来编写特殊的图形项。

特殊的图形项由于各自特殊,所以重复度不是很高,可以这样搞。

如果在这里面出现了三个点的抽象,比如:中心点和边缘点负责构造边界矩阵,还有一个点负责调整绘制图形的形状。那么就可以从OtherSpecialItem​中衍生一个三个点的类来管理。

或者继承现有的Item​类,添加一个AtomPointItem​,然后利用这个点和原来的中心点和边缘点,重写绘制的逻辑。

详细设计

AtomPointItem

AtomPointItem​的详细UML图如下:

classDiagram class AtomPointItem { +m_point : QPointF -m_type : PointType #mousePressEvent() void #mouseMoveEvent() void #mouseRelease() void +pointMoved(QPoint) void +focusIn() void +focusOut() void +boudingRect() QRectF +paint() void }
signals:
	void pointMoved(QPointF difference);
	void focusIn();
	void focusOut();

以上三个函数都是作为信号存在的。

  • AtomPointItem​检测到边界矩阵中发生了鼠标点击事件(即mousePressEvent()​)发生之后,会发出focusIn()​信号。通过连接这个信号我们的父Item可以得知AtomPointItem​什么时候开始被用户操纵,从而做出相应的反应。
  • 如果AtomPointItem​检测到在其边界矩阵中鼠标发生了移动,AtomPointItem​不会将这个移动立马应用到AtomPointItem​本身,而是通过发出pointMoved(difference)​信号,将鼠标移动的(dx, dy)​发送出去,让连接这个信号的父Item进行处理(具体怎么处理根据不同图形项的功能需求自行定义)。
  • 如果AtomPointItem​检测到在其边界矩阵中鼠标释放了,说明用户对于AtomPointItem​的操作结束了,父Item需要给出相应的反应。

boundingRect()​和paint()​作为一个图形项是必须实现的。

AbstractShapeItem

AbstractShapeItem​的UML类图设计如下:

classDiagram class AbstractShapeItem { -m_pen_isSelected : QPen -m_pen_noSelected : QPen #rectPen : QPen #focusInEvent() void #focusOutEvent() void #drawBoundingRect() void }
  • focusInEvent()​:处理焦点进入事件,该方法用于确定图形项处于选中状态,需要切换绘画图形项的QPen(选中和未选中的Pen颜色不一样),用于区分图形项是否被选中。
  • focusOutEvent()​:处理焦点丢失事件,该方式明确图形项处于未选中状态,切换绘图所用的pen。
  • drawBoundingRect()​:提供了一种绘制虚线边界矩形框的方法。由于有些不规则图形项有需要绘制出边界矩形框让用户知道自己操作图形项的范围,所以提供统一绘画矩形框的方法。子类可以通过设置rectPen​来设置该方法绘制的矩形框的样式。

CenterAndEdgePointItem

CenterAndEdgePointItem​的UML类图定义如下:

classDiagram class CenterAndEdgePointItem { #m_center : AtomPointItem #m_edge : AtomPointItem #connectedMutex : QMutex #isConnected : bool #centerMove(QPointF) void #centerFocusIn() void #centerFocusOut() void #edgeMove(QPointF) void #edgeFocusIn() void #edgeFocusOut() void #focusChanged() void #focusInEvent() void }
  • centerMove()​,centerFocusIn()​,centerFocusOut()​分别对应m_center​发出的pointMoved()​,focusIn()​,focusOut()​信号的处理槽函数。
  • edgeMove()​,edgeFocusIn()​,edgeFocusOut()​分别对应m_edge​发出的pointMoved()​,focusIn()​,focusOut()​信号的处理槽函数。
  • focusChanged()​是检测CenterAndEdgePointItem​所在的Scene​发出的焦点改变信号,需要查看这个信号以确定自己和自己所管理的AtomPointItem​们是否处于焦点状态,以确定处理逻辑。
  • m_center​和m_edge​是默认隐藏的两个Item,需要通过CenterAndEdgePointItem​获得焦点状态后才能展示出来。展示出来的逻辑构成由上面的槽函数来完成。focusInEvent()​负责获得焦点状态后将m_center​和m_edge​显示出来。

centerMove

virtual void centerMove(QPointF difference);

m_center​检测到的鼠标移动difference = (dx, dy)​,转换成父Item在爷坐标系上的移动。

edgeMove

virtual void edgeMove(QPointF difference);

m_edge​检测到的鼠标移动difference = (dx, dy)​,反应成m_edge​在其父Item坐标系上的移动。

focusEvent

void CenterAndEdgePointItem::focusInEvent(QFocusEvent* event);

获得焦点后,将m_center​和m_edge​从默认的隐藏状态中显示出来,让用户可以对图形项进行操作。在某些图形项中,获得焦点状态的图形项会渲染虚线边界矩形框。

centerFocusIn

void centerFocusIn();

m_center​获得焦点状态发出focusIn()​信号,然后触发该槽函数。

它将使CenterAndEdgePointItem​中其他AtomPointItem​隐藏起来。

centerFocusOut

void centerFocusOut();

m_center​失去焦点状态后发出focusOut()​信号,然后触发该槽函数。

它将使CenterAndEdgePointItem​中由于centerFocusIn()​而隐藏的AtomPointItem​重现,并将CenterAndEdgePointItem​重新置于焦点状态。

edgeFocusIn

void edgeFocusIn();

其作用和centerFocusIn()​类似。

edgeFocusOut

void edgeFocusOut();

其作用和centerFocusOut()​类似。

focusChanged

void focusChanged(QGraphicsItem* newFocusItem, QGraphicsItem* oldFocusItem, Qt::FocusReason reason);

我们需要在Focus不再处于CenterAndEdgePointItem​或者m_center​或者m_edge​之后,将m_center​和m_edge​隐藏起来,回归图形项的初始样貌。

你可以想使用focusOutEvent()​来检测焦点是否这项任务,但是这存在以下几个问题:

  • 首先你要知道:由于焦点状态同一时间只能由一个Item获取,子Item获取焦点状态,并不会导致父Item获得焦点状态,因为它们是两个Item即使它们存在父子关系。
  • 你可能会想到:在focusOutEvent()​中使用AtomPointItem​从QGraphicsItem​中继承而来的hasFocus()​方法来判断某个子项是否拥有焦点状态,来确定是不是m_center​和m_edge​是否具有焦点。但这样是行不通的,因为在下一个Item获得焦点之前,GraphicsView框架似乎会先执行上一个焦点Item的focusOutEvent()​,所以即使你知道你的子Item下一个瞬间就能获取焦点,但是程序不知道。这是我Debug多次的结果。
  • 最终的结果就是:你可能通过hasFocus来判断子项是否处于焦点状态,但是由于上面解释的原因,子项不可能在本focusOutEvent()执行完之前获得焦点状态,所以导致m_center或者m_edge在获得焦点之前就被hide起来了。

最终我的解决方案就是:处理QGraphicsScene::focusChanged()​这个信号。

我们可以通过检测oldFocusItem看看是不是CenterAndEdgePointItem本身,newFocusItem是不是m_centerm_edge来决定要进行何种操作。当焦点不再CenterAndEdgePointItem或者其子Item时,将m_centerm_edge进行隐藏。


这样操作有一个新的问题:何时将QGraphicsScene::focusChanged()​绑定到CenterAndEdgePointItem::focusChanged()​上呢?

通过this->scene()​我们可以获取QGraphcisScene​,但是因为当Item创建时,它不一定被添加到QGraphicsScene​当中。这样的话this->scene()​就是空指针,没有任何信号能够触发我们的槽。

我的解决方案是:第一次触发CenterAndEdgePointItem::focusInEvent()​的时候,对上述的信号和槽进行连接。

由于我们都已经出发的了focus事件,那么Item肯定存在于一个Scene中,也就肯定可以连接成功。

由于focus事件可能多次触发,可能会导致多次连接,所以需要使用一个isConnected​的布尔值来标志是否连接成功。

这里由于害怕Qt的信号和槽等消息机制可能是多线程状态下进行工作的,isConnected​的读写可能会有同步问题,这里使用了一个QMutex​来保证其状态。

RectangleItem

RectangleItem​的UML类图如下:

classDiagram class RectangleItem { + boundingRect() QRectF + paint() void }

RectangleItem​的大多数方法和变量通过继承的方式获得,这里我们只要关注矩形的绘制,关于m_center​和m_edge​的状态管理由CenterAndPointItem​来进行。

具体坐标计算如下:

QRectF RectangleItem::boundingRect() const
{
    QPointF center = m_center->m_point;
    QPointF edge   = m_edge->m_point;
    qreal width =  qAbs(center.x() - edge.x());
    qreal height =  qAbs(center.y() - edge.y());
    return QRectF(QPointF(-width, -height), QPointF(width, height));
}

绘制方法非常简单,就是利用QPainter​将这个边界矩形绘制出来即可,使用到的QPen和QBrush都来自其祖先类:

void RectangleItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(option);
    Q_UNUSED(widget);
    painter->setPen(this->pen());
    painter->setBrush(this->brush());
    painter->drawRect(this->boundingRect());
}

EllipseItem

EllipseItem​的UML类图如下:

classDiagram class EllipseItem { + boundingRect() QRectF + paint() void }

boundingRect()​与RectangleItem​的类似,而paint()​无非是将drawRect()​变为了drawEllipsse()​。

并且添加了在焦点状态下绘制矩形框的逻辑。

SquareItem

SquareItem​的UML类图如下:

classDiagram class SquareItem { + boundingRect() QRectF + paint() void # edgeMove() void }

该类重写了来自CenterAndEdgePointItem​的edgeMove()​方法,因为在SquareItem​,m_edge​不再可以在父Item的坐标系中随意移动了。

想要画出不“斜”的正方形,你就必须要保证m_edge​的坐标在y = x​这条线上。

每当m_edge​不在这条线上,我们就必须要把它重新移回y = x​上。

这里我采用的方法是:

m_edge​移动后的点(a, b)​和斜率-1​做一条直线,该直线为:y = -x + a + b​。

则其与y = x​的交点:((a + b) / 2, (a+b) / 2)​即为m_edge​的新坐标。

这样可以保证,m_edge​的移动大致反馈到y = x​上。

如果你数学很好,可以找出更为精确的算法,那么可以替换掉我的算法,使用你自己的算法。大学已经上两年的我基本上把高中的数学,甚至是高等数学忘得差不多了。

限制完移动之后,绘图就想对简单很多了:

QRectF SquareItem::boundingRect() const
{
    QPointF edge   = m_edge->m_point;
    QPointF bottomRight(qAbs(edge.x()), qAbs(edge.y()));
    QPointF topLeft(-qAbs(edge.x()), -qAbs(edge.y()));
    return QRectF(topLeft, bottomRight);
}

void SquareItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(option);
    Q_UNUSED(widget);
    painter->setPen(this->pen());
    painter->setBrush(this->brush());
    painter->drawRect(this->boundingRect());
}

CircleItem

CircleItem​的UML类图如下:

classDiagram class CircleItem { + boundingRect() QRectF + paint() void # edgeMove() void }

其实它与SquareItem​类似,只不过将paint()​中的drawRect()​变为了drawEllipse()​。同时添加了在焦点状态下绘制矩形框的逻辑。

MyPaintBoard

该类需要搭建的控件布局如上所示,我们为具体按键添加上相应的功能即可。

MyPaintBoard​的UML类图如下:

classDiagram class MyPaintBoard { -addCircleBtn : QPushButton -addSquareBtn : QpushButton -addRectangleBtn : QPushButton -addEllipseBtn : QPushButton -scene : QGraphicsScene -view : QGraphicsView -buttonsLayout : QHBoxLayout -primeLayout : QVBoxLayout -addCircle() void -addSquare() void -addRectangle() void -addEllipse() void }
  • addCircleBtn​的clicked()​与addCircle()​连接,添加CircleItem​到scene​中。
  • addSquareBtn​的clicked()​与addSquare()​连接,添加SquareItem​到scene​中。
  • addRectangleBtn​的clicked()​与addRectangle()​连接,添加RectangleItem​到scene​中。
  • addEllipseBtn​的clicked()​与addEllipse()​连接,添加EllipseItem​到scene​中。

剩下的就是进行布局管理的搭建,我这里默认读者已经将这些东西掌握了。这些知识网上一抓一大把,可以自行去了解。

编码实现

源码已经发布到GitHub上,有兴趣的可以自行阅读:

Aderversa/MyPaintBoard (github.com)

Qt的版本是:6.6.3

编译器使用的是:MinGW-64bithttps://github.com/Aderversa/MyPaintBoard

标签:实战,center,void,GraphicsView,Item,edge,图形,CenterAndEdgePointItem
From: https://www.cnblogs.com/Aderversa/p/18363041

相关文章

  • 公众号实战
     ===============================公众号运营工具推荐排版:壹伴插件、135编辑器写文:内容神器设计:canva、创客贴热点追踪工具:今日热榜、飞瓜数据、百度指数、微博热搜、知乎热搜数据分析:新榜、西瓜数据、清博指数FashionBeans.comStreetwill|FreeHi-ResPhotospixa......
  • 解锁企业数据分析瓶颈:数据可视化与报表自动化的实战策略
    Hi~这里是ProXiao文章参考:晓观点 《数据可视化与报表自动化:如何破解企业数据分析的痛点?》在数字化时代背景下,企业面临着日益增长的数据量和复杂性,这对数据处理、分析和应用提出了更高要求。企业决策过程中如何高效利用数据资源,已成为一个关键议题。本文旨在分析企业在数据......
  • 4、zabbix-图形监控的使用-监控某个主机、应用
    修改界面语言: ==============================================1、创建主机群组-用来放置管理监控对象=======================================================  ===================================================2、创建主机--》添加监控对象===================......
  • 打卡信奥刷题(568)用Scratch图形化工具信奥B2082[普及组/提高] B2083 画矩形
    画矩形题目描述根据输入的四个参数:a,b,c,f......
  • Linux shell脚本实战案例
    文章目录1.基础案例:显示系统信息2.文件备份案例3.自动安装软件案例4.批量重命名文件案例5.监控磁盘空间案例6.定时任务案例:定期清理日志文件7.错误处理和日志记录案例:安全地运行命令8.备份数据库案例:定期备份MySQL数据库9.系统监控案例:CPU和内存使用率10.用户......
  • Xinference实战指南:全面解析LLM大模型部署流程,携手Dify打造高效AI应用实践案例,加速AI
    Xinference实战指南:全面解析LLM大模型部署流程,携手Dify打造高效AI应用实践案例,加速AI项目落地进程XorbitsInference(Xinference)是一个开源平台,用于简化各种AI模型的运行和集成。借助Xinference,您可以使用任何开源LLM、嵌入模型和多模态模型在云端或本地环境中运行推理,并......
  • ECMAScript性能调优艺术:深度挖掘与实战避坑
    ECMAScript性能调优艺术:深度挖掘与实战避坑在Web开发的广阔天地中,ECMAScript(即JavaScript的标准化版本)的性能调优不仅是技术层面的挑战,更是艺术层面的追求。本文旨在深入探讨ECMAScript性能优化的各种技巧,并揭示隐藏在日常编码中的性能陷阱,帮助开发者在追求高性能的道路上......
  • 均线通道与K线中值突破程序化交易实战策略
    //策略说明://基于平移的高点和低点均线通道与K线中值突破进行判断//系统要素://1.MyRangeLeader是个当前K线的中点在之前K线的最高点上,且当前K线的振幅大于之前K线的振幅的K线//2.计算高点和低点的移动平均线//入场条件://1、上根K线为RangeLead,并且上一根收盘......
  • R语言基于日期范围筛选数据实战:日期范围之内的数据、日期范围之外的数据、日期之后的
    R语言基于日期范围筛选数据实战:日期范围之内的数据、日期范围之外的数据、日期之后的数据、日期之前的数据目录R语言基于日期范围筛选数据实战(SubsetbyaDateRange)#基于日期范围筛选数据语法#基于日期范围筛选数据(日期范围内的数据)#基于日期范围筛选数据(日期范围外的......
  • R语言ggplot2可视化实战:将可视化图像的标题(title)放置在图像的左下角
     R语言ggplot2可视化实战:将可视化图像的标题(title)放置在图像的左下角(customizetitlepositoninbottomleftofggplot2graph)目录R语言ggplot2可视化:将可视化图像的标题(title)放置在图像的左下角(customizetitlepositoninbottomleftofggplot2graph)#仿真数据......