工程效果

具体完整代码可在示例里面找到。工程路径在Qt安装目录下的

Examples\Qt-XX.XX.XX\widgets\graphicsview\collidingmice 目录下,XX.XX.XX为Qt的版本号,如:5.14.1。

可以看到,小老鼠碰撞后耳朵会变红。

工程总体就是多了一个mouse的源文件和头文件,即Mouse类相关文件。在Graphics View框架结构主要包含三个类:场景类(QGraphicsScene)、视图类(QGraphicsView)和图元类(QGraphicsItem),统称为“三要素”。而Mouse类就是继承自QGraphicsItem类的自定义类。

Scene类管理数量众多的Item类,并组织起他们的信息交流。但是它只是起后台管理的功能,并不会让这些Item类展现出现。View类起到将Item类可视化的功能,支持旋转和缩放。有点类似于浏览器的后端与前端,后端负责流程交互,前端负责展示效果。

mouse.h

其实QT本身就提供了Item类,包括矩形、椭圆形、线型等,但是我们一般使用的图案都是几种类型的结合,所以需要自定义类。以下为官方提供的Item类。

class Mouse : public QGraphicsItem
{
public:Mouse();QRectF boundingRect() const override;QPainterPath shape() const override;void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget) override;protected:void advance(int step) override;private:qreal angle = 0;qreal speed = 0;qreal mouseEyeDirection = 0;QColor color;
};

Mouse类是继承自QGraphicsItem类,所以我们在工程里面见到很多并未定义的函数,就是来自QGraphicsItem类。在Graphics View框架当中,QGraphicsItem是所有item的基础类。后三个函数是自定义Item类必须重新定义的纯虚函数。

paint函数是实际的绘制函数,包含老鼠的身体、眼镜等部分都是需要在paint函数里面绘制。

boundingRect 函数返回的是Item重新绘制的区域大小,相当于老鼠的平面面积。因为实际上动画的实现,就是擦除旧的,再画新的,所以好像移动了一样。这个擦除区域的大小就是由boundingRect() 来返回的。

Shape函数主要用于碰撞检测,它会返回老鼠的精确形状,默认情况下它返回的就是boundingRect的值,但有时候我们想要靠近5cm就认为碰撞,就需要重写该函数。

advance函数用来推进Scene,可以很容易实现Item的动画效果。调用Scene的advance函数就会自动调用Scene中所有Item的advance函数,而且Item的advane函数会被分为两个阶段调用两次。第一次phase为0,告诉所有的Items:Scene将要改变;第二次phase为1,在这时才进行具体的操作。

qreal是double数据类型,QColor是Qt的颜色变量,这几个定义的向量在后面会用到。

mouse.cpp

一、paint函数

先来看看它是怎么通过paint函数画出老鼠的。Graphics View框架调用paint函数绘制item的内容,而且是以本地坐标系为基准。

1.身体、眼睛、鼻子

setBrush函数是设置笔刷的函数,主要就是内部填充的颜色,这里的color变量是Mouse类的私有变量,主要是想实现随机颜色的老鼠。

drawEllipse函数是paninter类的画椭圆的函数,共有四个参数,前两个代表对应矩形的左上角的坐标,后两个代表宽度和高度。

setBrush函数是设置笔刷的函数,主要就是内部填充的颜色,这里的color变量是Mouse类的私有变量,主要是想实现随机颜色的老鼠。

drawEllipse函数是paninter类的画椭圆的函数,共有四个参数,前两个代表对应矩形的左上角的坐标,后两个代表宽度和高度。

随机颜色的设定:color是Qt类Qcolor的实例化对象,三个参数代表RGB的值。bounded函数主要用于产生0-256的随机数。帮助手册说明如下:

随机角度的确定:setRotation函数是继承自QGraphicsItem类的一个函数,主要用于设定item出现在sence坐标系时的角度。默认情况下,item一般是以sence的中心(0,0)为自身局部坐标系的中心。通过setRotation函数可以改变老鼠开始移动时的方向。

接下来画白色的眼睛和黑色的鼻子,如果drawEllipse的后两个参数是相同的,画出来就是圆形。至于这个QRectF是强制转换函数,就是将里面四个参数先转为QRectF,然后再作为drawEllipse函数的参数。这个叫函数的重载。不管是直接四个参数,还是传入QRectF参数都是可以的,QT都有提供对应的函数。

painter->setBrush(Qt::black);
painter->drawEllipse(QRectF(-2, -22, 4, 4));

2.瞳孔

这里存在一个变量就是矩形左上角的x,这个mouseEyeDirection变量是在advance函数里面进行更新的,主要实现的效果就是,当老鼠往左,那瞳孔就左移一点;当老鼠往右,那瞳孔就右移一点。

3.耳朵

耳朵是用来表示碰撞的,正常情况下耳朵是灰黄色,当老鼠之间发生碰撞,耳朵会变成红色。这里使用了scene类的collidingItems函数进行判断是否发生碰撞。这个this代表的就是类本身,就是某一个对象(老鼠),函数会返回与该老鼠碰撞的其他item的清单。因为此工程只需要判断碰撞与否,而不需要判断与谁发生碰撞,所以用isEmpty函数判断是否为空就行了。

使用手册相关说明如下:

从By default, all items whose shape intersects item or is contained inside item's shape are returned.可以得知,碰撞的检测是以shape交叉或者包含为准的,这就是shape函数实现的。

4.尾巴

drawPath函数用于绘制给定的路径曲线。

cubicTo函数是功能是画贝塞尔曲线的。使用c1和c2指定的控制点在当前位置和给定端点之间添加三次Bezier曲线。添加曲线后,将当前位置更新为曲线的端点。所以例程中存在6个参数,其实对应的就是c1、c2、endPoint三个点的坐标点。而实例化对象时用的(0,20)就是起点。

因为path.cubicTo(-5, 22, -5, 22, 0, 25);中的c1和c2是一样的,相当于曲线一定要从这一点经过(贝塞尔曲线的一个特性是:控制点之间的连线必定和曲线相交,而这两个点坐标相同,在这两个控制点就是交点,即在曲线上)。而path.cubicTo(5, 27, 5, 32, 0, 30);则表示曲线得从(5,27)和(5,32)这两个点中间经过,然后到达(0,30)这个点。

至此,老鼠已经画完了。关于贝塞尔曲线的简单介绍,请参见《贝塞尔曲线简单介绍》。

二、advance函数

当Scene准备刷新场景的时候,就会调用每一个Item类的advance函数,完成场景的刷新。通过advance函数我们可以完成老鼠的移动。

  void Mouse::advance(int step){if (!step)return;QLineF lineToCenter(QPointF(0, 0), mapFromScene(0, 0));if (lineToCenter.length() > 150) {qreal angleToCenter = std::atan2(lineToCenter.dy(), lineToCenter.dx());angleToCenter = normalizeAngle((Pi - angleToCenter) + Pi / 2);if (angleToCenter < Pi && angleToCenter > Pi / 4) {// Rotate leftangle += (angle < -Pi / 2) ? 0.25 : -0.25;} else if (angleToCenter >= Pi && angleToCenter < (Pi + Pi / 2 + Pi / 4)) {// Rotate rightangle += (angle < Pi / 2) ? 0.25 : -0.25;}} else if (::sin(angle) < 0) {angle += 0.25;} else if (::sin(angle) > 0) {angle -= 0.25;}

首先,如果step为0,那老鼠不做任何的前进,直接return。因为scene会调用两次advance函数,第一次调用step为0,表明item即将更新;第二次调用step等于1,这次是实际的更新。所以我们要保证step等于0时,item不发生更改。

GraphicsView坐标系统

Item坐标系是自身的局部坐标系,Scene坐标系是全局坐标系。之前我们通过paint画图就是在Item类坐标系上画的,但老鼠的移动时是需要在Scene类坐标系中移动的,所以存在一个坐标映射的问题,也可以称之为坐标转化。比如图中Item类坐标系的原点(0,0)转换为Scene类坐标系就是大概为(15,-10)这样。

QLineF lineToCenter(QPointF(0, 0), mapFromScene(0, 0));

QLineF是表示一条线,两个参数,一个起点和一个终点。mapFromScene函数是继承自QGraphicsItem类的函数,主要是将Scene的坐标转换为Item类的坐标。这句代码其实就是将Scene类坐标(0,0)转换为Item坐标系中的点。这条线就是如下图所示的红色直线。

如果这条直线的长度length大于150,就进行方向变化,这是为了保证老鼠待在一个150像素的圆内。如果超过150,就掉头。通过QLineF类的dx函数和dy函数,结合atan2函数可以算出这条直线的角度,公式为float angle = atan2( y2-y1, x2-x1 ),即终点减去起点。atan2函数与atan函数的取值范围不同,是-π到π。如果对atan、atan2不懂,请参考

《C++中反正切atan2(y,x)与atan(x)的区别》博文。

          qreal angleToCenter = std::atan2(lineToCenter.dy(), lineToCenter.dx());angleToCenter = normalizeAngle((Pi - angleToCenter) + Pi / 2);

因为atan2的范围为[-PI, PI], 运用数学不等式知识,则:

-PI<= - angleToCenter <= PI

0 <= PI - angleToCenter <= 2*PI

PI / 2 <= PI - angleToCenter + PI / 2 <= 2*PI + PI / 2

即从Y轴正方向绕了360°即转了一个圆后又回到Y轴正方向。例程中通过atan2函数计算得出方位角后,还经过了normalizeAngle函数去调整角度,而这个normalizeAngle函数是例程自定义的角度处理函数。函数功能不难理解,就是将PI /2 ~ 5* PI / 2角度范围限定在0-2π之间。

static qreal normalizeAngle(qreal angle)
{while (angle < 0)angle += TwoPi;while (angle > TwoPi)angle -= TwoPi;return angle;
}

为什么通过atan2角度之后,还需要经过(Pi - angleToCenter) + Pi / 2计算呢?这是我思考很久才弄明白的,因为说明手册也没说。

以下图为例,角度不代表真实角度,就是为了表示他们之间的换算关系。老鼠的头部是朝上的,我们习惯以老鼠头部为基准,然后去判断方向,如下图我们可以认为Scene坐标系原点在老鼠头部的左前方。

但是tan2函数算出来的角度是以Item坐标系为准的,比如下图的-120°就是通过atan2函数计算得出,这代表直线角度从x正方向逆时针转动了120°。通过(Pi - angleToCenter) + Pi / 2计算,得到的角度是390°,以这个参数代入normalizeAngle函数,得到30°,这个就是我们想要的角度。

所以我们可以推断出这个angleToCenter角度是以Item坐标系的y轴负方向为基准,然后逆时针旋转的角度。

再看一个例子:

当然在程序中得到的数值和计算的数值都是以弧度制表示的,上面为了讲解用了角度值。原理是一样的。

如果angleToCenter处于左下角区域,老鼠可能往左调头返回,也可能往右调头返回,这个由angle决定。angleToCenter表示的是Scenne坐标系原点和Item坐标系原点的角度,即确定老鼠中心所处的位置,那angle又是表示什么?

angle能推测出是老鼠前进的方向,通过说明手册能得知,这个角度是Item沿Z轴旋转的方向,以顺时针为正,默认情况下为0.也就是两个坐标系x、y轴方向一致的时候是0.

下面这张图,此时老鼠的angle就是0度,没有发生旋转。

根据angleToCenter值不同,angle增减值也不一样。弧度制0.25大约相当于角度制是14度。

        if (angleToCenter < Pi && angleToCenter > Pi / 4) {// Rotate leftangle += (angle < -Pi / 2) ? 0.25 : -0.25;

下图以箭头方向表示老鼠方向,判断标准是-Pi/2,即原始老鼠沿逆时针旋转90°。通过下图可以看到,这个判断就是:让老鼠往-Pi/2方向偏转,调头往Scene坐标系中心移动,不至于跑出圈外。其实现原理是:

下面几个判断的原理差不多,都是让不同位置的老鼠掉头往中心移动。

 避免碰撞

上面这一段代码是为了尝试不让老鼠撞在一起。QList是QT的链表数据结构,类似于数组。这个列表里面的数据类型是Item类指针,就是那一只只小老鼠。scene函数继承于Item类,返回这个Item目前所处的scene。items函数是QGraphicsScene类的函数,返回该scene中所有的Item的顺序列表。

mapToScene函数是把Item坐标系的点映射到Scene坐标系,是mapFromScene函数的反操作。

QPoltgonF函数是生成多边形的函数,以三个点生成多边形,就是三角形。(0,0)是老鼠的中心位置,(-30,-50)和(30,-50)是老鼠的两个耳朵左上角和右上角边缘。这三个点生成的三角形,相当于半个老鼠大小的三角形。

计算得到angleToMouse和angleToCenter是一样的,都是以y负半轴为0°,逆时针旋转。angle是以z轴顺时针旋转的角度,加上0.5就是顺时针旋转大约28°,减去0.5就是逆时针旋转大约28°。

如下图所示,蓝色老鼠就位于橙色老鼠0-Pi/2的区域内,根据判断条件,增加angle增加0.5,向右旋转28°,避开蓝色老鼠。

 增加随机运动

dangerMice就是那个装着全部老鼠的队列,如果队列里面的老鼠数量size大于1,且产生的随机数等于0。因为bounded产生的随机数位于0-9之间,所以等于0的概率是十分之一。后面又增加或者减少一个随机的角度。

小鼠移动
角度angle由三个方式确定,一是与中心点的距离和位置,二是与其他老鼠的位置关系,三是随机运动产生的角度。speed也是一个由bounded产生的随机数,结合整条式子,产生的speed值为-0.05至+0.49。dx范围为-10至10之间。

rotation函数表示的是当前item绕Z轴顺时针旋转的角度,默认值是0(如下图所示)。

setRotation设置的就是Item的旋转角度,范围是-360°至360°。setRotation(rotation() + dx);就是在原来的基础上,旋转dx度。但是例程通过qreal dx = ::sin(angle) * 10;把弧度制的angle转换成了角度值dx,这个转换关系我也没搞懂,可能就是大致范围的转换吧。因为正常来说弧度制转角度值,是/Pi*180。

setRotation决定了老鼠的方向,而setPos就是让老鼠运动起来。根据说明手册,设定item在父坐标系中的位置,如果它没有父类,那就以scene坐标系为准。这个例程的老鼠就是单独的item,它没有父类。 item的位置描述,指的是item坐标系的原点在scene坐标系的位置。

setPos(mapToParent(0, -(3 + sin(speed) * 3)));

boundingRect函数
boundingRect函数以一个矩形定义了item的外部界限。Graphice View框架使用这个矩形来决定item是否需要重新绘画,所以所有的绘制都需要在矩形框中进行。如果绘制超过了这个矩形框,Graphice View框架就不会将超出的那部分擦除。

通过下图就能看到上面几个数据是如何得到的,基本就是返回一个比item还要大一点的矩形。

Shape函数

Graphice View框架使用shape函数返回了形状来确定两个item是否相撞,所以shape函数返回的形状会更加精确点。代码所示,这个矩形相当于就是老鼠的椭圆大小,不考虑尾巴多出来那部分。

Main函数

首先创建了一个scene,之前讲到的scene坐标系就是这里创建的。scene左上角在(-300,-300),长和宽都是600像素。

      QGraphicsScene scene;scene.setSceneRect(-300, -300, 600, 600);

QGraphicsScene类是作为QGraphicsItems类的容器,它可以高效地决定每个item的位置和哪个item是可见的。

如下所示,Scene默认情况是使用索引算法,这种算法可以加速搜索item,比较适用于静态场景。但是如果场景有很多动画,我们可以设置为NoIndex,这样会比较快点。

scene.setItemIndexMethod(QGraphicsScene::NoIndex);

通过addItem给scene添加老鼠。

      for (int i = 0; i < MouseCount; ++i) {Mouse *mouse = new Mouse;mouse->setPos(::sin((i * 6.28) / MouseCount) * 200,::cos((i * 6.28) / MouseCount) * 200);scene.addItem(mouse);}

scene是为了管理item,但是不能让item可视化。可视化需要新建QGraphicsView类。
setRenderHin函数:设置了反走样\反锯齿(Antialiasing),主要是让线条更柔顺,不会边缘出现锯齿。
setBackgroundBrush函数:设置了view的背景图----奶酪图。
setCacheMode函数:设置缓存模式,加速渲染背景图。
setViewportUpdateMode函数:ViewportUpdateMode属性描述了View类在场景发生变化时是如何更新它的视图的,存在五种模式。
QGraphicsView::FullViewportUpdate:更新整个视图;
QGraphicsView::MinimalViewportUpdate:这是默认模式,寻求最小化区域进行更新;
QGraphicsView::SmartViewportUpdate:分析重绘区域,然后试图寻找最佳的模式;
QGraphicsView::BoundingRectViewportUpdate:(该例程选择模式),只更改界限以内的内容。这个模式缺点是就算其他item未发生改变,它的界限内也会发生重绘。
QGraphicsView::NoViewportUpdate:不更新试图,这种用户想要自己控制更新时选择的模式;

setDragMode函数:这个DragMode属性定义了当用户点击scene背景并且拖拽鼠标时会发生什么,存在三种模式。
QGraphicsView::NoDrag :忽略鼠标事件,不可以拖动。
QGraphicsView::ScrollHandDrag :光标变为手型,可以拖动场景进行移动。(本例程采用)
QGraphicsView::RubberBandDrag :进行区域选择,可以选中一个区域内的所有图形项。

    QGraphicsView view(&scene);view.setRenderHint(QPainter::Antialiasing);view.setBackgroundBrush(QPixmap(":/images/cheese.jpg"));view.setCacheMode(QGraphicsView::CacheBackground);view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);view.setDragMode(QGraphicsView::ScrollHandDrag);

**setWindowTitle函数:设置窗口标题;

    view.setWindowTitle(QT_TRANSLATE_NOOP(QGraphicsView, "Colliding Mice"));view.resize(400, 300);view.show();

创建一个定时器,连接定时器的timeout信号,到scene的advance槽函数。每一次定时器到了时间,就激活scene刷新场景。定时器的实际是1000/33毫秒。所以相当于动画一秒钟会刷新30帧。对于大多数动画来说,这个帧速率足够了。

    QTimer timer;QObject::connect(&timer, &QTimer::timeout, &scene, &QGraphicsScene::advance);timer.start(1000 / 33);

本文转自:Qt Creator Colliding Mice碰撞老鼠例程解析【1.5W字数长文!详细!】_研究僧-彬彬的博客-CSDN博客

Colliding Mice碰撞老鼠工程分析相关推荐

  1. Qt Creator Colliding Mice碰撞老鼠例程解析【1.5W字数长文!详细!】

    工程效果 可以看到,小老鼠碰撞后耳朵会变红.具体完整代码可在示例里面找到. 工程总体就是多了一个mouse的源文件和头文件,即Mouse类相关文件.在Graphics View框架结构主要包含三个类: ...

  2. Qt 视图框架示例 Colliding Mice 的翻译

    目录名字 Qt 视图框架示例 Colliding Mice 的翻译 简介: Mouse Class 定义 Mouse Class 定义 The Main() 函数 Qt 视图框架示例 Collidin ...

  3. 机器学习cae_CAE工程分析技术年会记

    读书使人充实,讨论使人机智,笔记使人准确,读史使人明智,读诗使人灵秀,数学使人周密,科学使人深刻,伦理使人庄重,逻辑修辞使人善辩.凡有所学,皆成性格.---- (英国)培根 可能是会议热度不够,非常遗 ...

  4. MATLAB数学计算与工程分析范例教程,MATLAB数学计算与工程分析范例教程

    基本信息 书名:MATLAB数学计算与工程分析范例教程 定价:28.00元 作者:石博强,赵金 编著 出版社:中国铁道出版社 出版日期:2005-05-01 ISBN:9787#113057596 字 ...

  5. ML之FE:利用FE特征工程(分析两两数值型特征之间的相关性)对AllstateClaimsSeverity(Kaggle2016竞赛)数据集实现索赔成本值的回归预测

    ML之FE:利用FE特征工程(分析两两数值型特征之间的相关性)对AllstateClaimsSeverity(Kaggle2016竞赛)数据集实现索赔成本值的回归预测 目录 输出结果 设计思路 核心代 ...

  6. 03-instancing 工程分析详解

    opengl编程指南第8版源码怎么下载.编译,请参考<opengl编程指南第8版源码编译详细说明> 1. 程序启动 请参考<03-drawcommands工程分析详解> 2. ...

  7. python与金融工程的区别_科研进阶 | 纽约大学 | 金融工程、量化金融、商业分析:Python金融工程分析...

    科研进阶 | 纽约大学 | 金融工程.量化金融.商业分析:Python金融工程分析(2021.2.6开课)​mp.weixin.qq.com 课题名称 = Python金融工程分析 = 项目配景 大数 ...

  8. MATLAB数学计算与工程分析范例教程,MATLAB 2016数学计算与工程分析从入门到精通...

    全书通过近400个实例讲解了利用MATLAB 2016进行数学计算和工程分析的方法和技巧,涵盖了MATLAB的五大功能:1)数值计算功能:2)符号计算功能:3)图形与数据可视化功能:4)可视化建模与仿 ...

  9. 计算机辅助工程分析课程论文,教学大纲—计算机辅助工程分析.doc

    <计算机辅助工程分析>课程教学大纲 英文课程名Computer Aided Engineering总 学 时32学 分2课程编码202725理论教学学时8适用专业机械工程.过程装备与控制工 ...

最新文章

  1. STM32 UART2程序--端口重映射
  2. EJS学习(五)之EJS的CommonJs规范版本
  3. Javascript实现MD5加密
  4. 数学--数论--HDU6919 Senior PanⅡ【2017多校第九场】
  5. linux proc进程,linux 下 /proc/进程号/ 重要进程文件的内容解析
  6. MiniProfiler.EF6监控调试MVC5和EF6的性能
  7. Python3.7.2版本出现ModuleNotFoundError: No module named 'paramiko'解决办法
  8. HTTP所承载的货物(图像、文本、软件等)要满足的条件
  9. Scrapy 导出的 cvs 文件,双击打开乱码问题
  10. 大项目之网上书城(一)——注册页面
  11. c语言入门if语句(嵌套)
  12. 需求分析与原型设计———记账软件
  13. 摄影基础1 : 135相机
  14. 图形学进阶——移动端TB(D)R架构基础
  15. 网友评选最好玩实用的二十大良心网站,You Know?!
  16. android手机网速,简单一步让你的手机网速至少提升2倍!
  17. html输入文本颜色,Input输入字体颜色改变js(兼容IE)
  18. 【Maven】基础概念、仓库、构建与部属
  19. 唐常杰--一篇 它引 上万的大牛论文 与 数据血统论-- 趣味数据挖掘之三
  20. 帝国CMS审计-后台模板注入导致getshell

热门文章

  1. FL Studio电音编曲软件V21中文完整版 安装下载教程
  2. 明天终于要到公司开工了
  3. matlab实现密堆立方体,LAMMPS如何定义六角密堆结构HCP
  4. 前端开发神器VS Code安装教程
  5. javascript 判断 flash 插件是否安装
  6. style-component中引入icon-font步骤以及出现方块问题的解决
  7. 水漆哪个品牌好?十大品牌水漆排行榜
  8. 清华大学 博士后 原来入的计算机科学与技术 现在能入软件工程吗,清华大学软件学院...
  9. 服务器上MySQL数据库密码忘了
  10. 谷歌又闹大乌龙!Jeff Dean参与的新模型竟搞错Hinton生日