Qt 视图框架示例 Colliding Mice 的翻译
简介:
该示例程序介绍了怎样在Qt的视图框架里创建动画以及冲突检测。
视图框架提供了QGraphicsScene class 场景类来管理2D图形对象.一个QGraphicsView widget 来是显示这些图形项,并且支持视图的缩放和旋转。
在该示例中, Mouse class 自定义了一些Mice 的图形项,main() 函数提供了应用程序的窗体。
首先我们来学习Mouse 类是如何实现动画和冲突检测的。然后我们学习main()函数是如何将mice 图形项添加到场景中。
Mouse Class 定义
The mouse class继承自QGraphicsItem. QGraphicsItem是视图框架中所有图形项的基类。它提供了一个轻量级的框架来创建用户自定义的一些图形项。
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;
};
当我们创建自定义图形项的时候,我们必须要重写两个虚函数
1、 boundingRect(), 绘图的边界
2、 paint(), 绘图
我们还重写了两个函数shape()和advance().
1、shape():mice 图形项的准确形状。默认情况下,返回的是bounding rectangle.
2、advance():实现动画
Mouse Class 定义
构造函数:初始化一些私有变量
Mouse::Mouse() : color(QRandomGenerator::global()->bounded(256),
QRandomGenerator::global()->bounded(256),
QRandomGenerator::global()->bounded(256))
{
setRotation(QRandomGenerator::global()->bounded(360 * 16));
}
这里我们用QRandomGenerator. 来实现Mice的不同肤色。
setRotation()来改变Mice 的方向。
QGraphicsScene 会在帧的推进中会调用每一图形项的advance()功能,这里我们重写了这个advance()方法,于是Mice会 一帧帧的动起来了。
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 left
angle += (angle < -Pi / 2) ? 0.25 : -0.25;
} else if (angleToCenter >= Pi && angleToCenter < (Pi + Pi / 2 + Pi / 4)) {
// Rotate right
angle += (angle < Pi / 2) ? 0.25 : -0.25;
}
} else if (::sin(angle) < 0) {
angle += 0.25;
} else if (::sin(angle) > 0) {
angle -= 0.25;
}
因为advance()被调用两次,第一次是step=0;然后是step=1;前一次不作任何动作。同时,我们要保证mice待在一个半径是150像素的圆内。
mapFromScene() 是完成从图形项坐标到场景坐标的映射。
const QList<QGraphicsItem *> dangerMice = scene()->items(QPolygonF()
<< mapToScene(0, 0)
<< mapToScene(-30, -50)
<< mapToScene(30, -50));
for (const QGraphicsItem *item : dangerMice) {
if (item == this)
continue;
QLineF lineToMouse(QPointF(0, 0), mapFromItem(item, 0, 0));
qreal angleToMouse = std::atan2(lineToMouse.dy(), lineToMouse.dx());
angleToMouse = normalizeAngle((Pi - angleToMouse) + Pi / 2);
if (angleToMouse >= 0 && angleToMouse < Pi / 2) {
// Rotate right
angle += 0.5;
} else if (angleToMouse <= TwoPi && angleToMouse > (TwoPi - Pi / 2)) {
// Rotate left
angle -= 0.5;
}
}
if (dangerMice.size() > 1 && QRandomGenerator::global()->bounded(10) == 0) {
if (QRandomGenerator::global()->bounded(1))
angle += QRandomGenerator::global()->bounded(1 / 500.0);
else
angle -= QRandomGenerator::global()->bounded(1 / 500.0);
}
然后我们要实现老鼠的碰撞的处理
speed += (-50 + QRandomGenerator::global()->bounded(100)) / 100.0;
qreal dx = ::sin(angle) * 10;
mouseEyeDirection = (qAbs(dx / 5) < 1) ? 0 : dx / 5;
setRotation(rotation() + dx);
setPos(mapToParent(0, -(3 + sin(speed) * 3)));
}
最后,我们计算mice的速度,和方向然后设定一个新的坐标。
QGraphicsItem::setPos() 函数会设定一个父窗体的坐标系统的位置。
应为图形项是没有父对象的,所以需要mapToParent() 将坐标映射到需要设置的父窗体的坐标系中。如果没有设定父窗体,默认是映射到场景的坐标系中。
QRectF Mouse::boundingRect() const
{
qreal adjust = 0.5;
return QRectF(-18 - adjust, -22 - adjust,
36 + adjust, 60 + adjust);
}
boundingRect() 定义了图形项的矩形边界。Qt中的视图框架是通过该函数的返回值类决定是否需要重画图形项。
void Mouse::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
// Body
painter->setBrush(color);
painter->drawEllipse(-10, -20, 20, 40);
// Eyes
painter->setBrush(Qt::white);
painter->drawEllipse(-10, -17, 8, 8);
painter->drawEllipse(2, -17, 8, 8);
// Nose
painter->setBrush(Qt::black);
painter->drawEllipse(QRectF(-2, -22, 4, 4));
// Pupils
painter->drawEllipse(QRectF(-8.0 + mouseEyeDirection, -17, 4, 4));
painter->drawEllipse(QRectF(4.0 + mouseEyeDirection, -17, 4, 4));
// Ears
painter->setBrush(scene()->collidingItems(this).isEmpty() ? Qt::darkYellow : Qt::red);
painter->drawEllipse(-17, -12, 16, 16);
painter->drawEllipse(1, -12, 16, 16);
// Tail
QPainterPath path(QPointF(0, 20));
path.cubicTo(-5, 22, -5, 22, 0, 25);
path.cubicTo(5, 27, 5, 32, 0, 30);
path.cubicTo(-5, 32, -5, 42, 0, 35);
painter->setBrush(Qt::NoBrush);
painter->drawPath(path);
}
paint()函数执行实际的图形项的绘制。绘制时的坐标系都是图形项自身的坐标系。
mice 的耳朵在碰撞是会显示红色。没有碰撞时是灰黑色。
在Qt的视图框架里通过shape-shape 交互来检测识别碰撞冲突,所以shape()函数里返回的shape值必须要准确。
QPainterPath Mouse::shape() const
{
QPainterPath path;
path.addRect(-10, -20, 20, 40);
return path;
}
当shapes 变得复杂的时候,shape-shape 重叠检测也会变得复杂,这会耗费计算的时间。重写collidesWithItem()该函数可以自定义冲突的算法。
接着我们来看main()函数
The Main() 函数
int main(int argc, char **argv)
{
QApplication app(argc, argv);
首先我们创建一个应用程序,并且创建一个场景。
QGraphicsScene scene;
scene.setSceneRect(-300, -300, 600, 600);
QGraphicsScene提供了放置QGraphicsItems 的一个容器。该容器提供了一些函数方法来更好的显示,管理图形项。
当我们创建了一个场景,我们推介设定该场景的大小。若过没有设定大小,场景的大小会随着图形项的增加,而不断增大。
场景会给每一个图形项一个索引号,也可以不设定索引号。索引号可以快速的定位到图形项。
scene.setItemIndexMethod(QGraphicsScene::NoIndex);
QgrapbhicsScene使用下标来高效的管理item的位置,默认的使用BSP树,适用于一个大型的场景,其中的item都是静止不变的,可以选择调用setItemIndexMethod().来禁用下标,可是查看itemIndexMethod来获取更多的信息
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);
}
//创建mice 图形项
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);
}
//然后将这些图形项添加到场景中
QGraphicsView view(&scene);
view.setRenderHint(QPainter::Antialiasing);
view.setBackgroundBrush(QPixmap(":/images/cheese.jpg"));
为了能够欧看到场景,我们需要创建一个场景的窗体,QGraphicsView 类 可以提供一个自带滚动条的窗体。我们确保图形的渲染是抗锯齿的。通过背景刷绘制一个背景。
view.setCacheMode(QGraphicsView::CacheBackground);
view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
view.setDragMode(QGraphicsView::ScrollHandDrag);
设置缓存机制。QGraphicsView 可以预渲染像素化的图形。
设定拖拽模式。ScrollHandDrag 该模式下,当鼠标拖拽时会变为手型,同时滚动条会随之变化。
view.setWindowTitle(QT_TRANSLATE_NOOP(QGraphicsView, "Colliding Mice"));
view.resize(400, 300);
view.show();
QTimer timer;
QObject::connect(&timer, &QTimer::timeout, &scene, &QGraphicsScene::advance);
timer.start(1000 / 33);
return app.exec();
}
最后设定窗体的标题,创建一个定时器,设定定时器的超时事件来调用场景的帧的步进。