关于Qt4的图形视图框架的理解(一)——碰撞的小鼠

时间:2023-02-01 18:24:40

Ø 碰撞的小鼠的例子:

该例子展示了如何通过图形视图框架来实现图元动画和图元相互间的碰撞检测。

图形视图提供了QGraphicsScene类来管理和与大量定制的2d图形项通过QGraphicsItem类派生的Item进行交互,并且通过QGraphicsView类来进行Item的可视化,支持缩放和旋转等。

该例子包含一个Item类和一个主函数:这个小鼠类继承自QGraphicsItem类,主函数提供了主窗口。

首先检查小鼠类图看看如何实现图元动画和图元相互间的碰撞检测,然后检查主函数,如何把图元放入场景,并通过QGraphicsView类来进行可视化。

 

小鼠类定义:

小鼠类继承自QGraphicsItem类,QGraphicsItem类在图形视图框架中是所有图形图元的基类,并提供了一个轻量级的基类去重新自定义自己的图元类。

 class Mouse : public QGraphicsItem

{

public:

    Mouse();

    QRectF boundingRect() const;

    QPainterPath shape() const;

    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget);

protected:

    void advance(int step);

private:

    qreal angle;

    qreal speed;

    qreal mouseEyeDirection;

    QColor color;

};

当自定义图元类时,你必须重写QGraphicsItem类的两个纯虚函数:boundingRect(),返回一个绘图区域,paint()函数实现实际的绘制。另外,我们重写了shape()advance()函数,重写shape函数返回一个小鼠图元的形状;默认的返回一个矩形区域。重写advance函数去处理当发生变化时进行更新,推进场景。

当构造一个小鼠类时,我们首先确保所有的图元的私有变量被正确的初始化:

 Mouse::Mouse()

    : angle(0), speed(0), mouseEyeDirection(0),

      color(qrand() % 256, qrand() % 256, qrand() % 256)

{

    setRotation(qrand() % (360 * 16));

}

计算小鼠各个组件的颜色,用全局函数qrand,他是线程安全的标准的C++rand函数。

然后调用继承自QGraphicsItem类的setRotation函数。图元位于自己的坐标系中。他们的坐标系通常是以(0,0)为中心点,并且也是矩阵转换的中心。通过调用setRotation函数,当小鼠开始移动时,我们改变它的移动位置。

QGraphicsScene类决定推进场景一帧时,会调用advance函数在每个图元上。使我们能够使用我们自定义的advance函数。

void Mouse::advance(int step)

{

    if (!step)

        return;

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

    if (lineToCenter.length() > 150) {

        qreal angleToCenter = ::acos(lineToCenter.dx() / lineToCenter.length());

        if (lineToCenter.dy() < 0)

            angleToCenter = TwoPi - angleToCenter;

        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函数会被调用两次,第一次显示图元,第二次才是推进场景帧。我们也应确保小鼠在一个半径为150像素的圆内。

 

注意QGraphicsItem类有mapFromScene函数,该函数映射一个位置从图元坐标系到场景坐标系中。

 QList<QGraphicsItem *> dangerMice = scene()->items(QPolygonF()

                                                       << mapToScene(0, 0)

                                                       << mapToScene(-30, -50)

                                                       << mapToScene(30, -50));

    foreach (QGraphicsItem *item, dangerMice) {

        if (item == this)

            continue;

 

        QLineF lineToMouse(QPointF(0, 0), mapFromItem(item, 0, 0));

        qreal angleToMouse = ::acos(lineToMouse.dx() / lineToMouse.length());

        if (lineToMouse.dy() < 0)

            angleToMouse = TwoPi - angleToMouse;

        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 && (qrand() % 10) == 0) {

        if (qrand() % 1)

            angle += (qrand() % 100) / 500.0;

        else

            angle -= (qrand() % 100) / 500.0;

    }

然后我们试着去躲避其他老鼠的碰撞。

speed += (-50 + qrand() % 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)));

}

最后,我们计算老鼠的速度和眼睛的位置。然后设置新的位置。

图元的(0,0)点在父坐标系的位置。函数setPos设置了在父坐标系中图元的位置。如果图元没有父类,该函数设置的是其在场景中的位置。QGraphicsItem也提供了mapToPaint函数映射一个位置到父坐标系中。如果图元没有父类,该位置将映射到场景类中。

 

然后实现继承自QGraphicsItem类的虚函数,首先是boundingRect函数

QRectF Mouse::boundingRect() const

{

    qreal adjust = 0.5;

    return QRectF(-18 - adjust, -22 - adjust,

                  36 + adjust, 60 + adjust);

}

该函数定义了图元的外界矩形区域。注意:图形视图框架通过该函数决定是否图元需要重绘,所以所有的重绘将被限制在该矩形区域内。

 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函数去绘制包含的图元;这个在图元坐标系中进行绘制。

注意,画小鼠的耳朵:当一个小鼠碰到其他小鼠时,耳朵被填充为红色;否则填充为暗黄色。我们用collidingItems函数去检测碰撞。实际碰撞检测是由图形视图框架用shape-shape交叉进行检测的。所以我么必须确保shape函数返回我们图元的一个精确的形状:

 QPainterPath Mouse::shape() const

{

    QPainterPath path;

    path.addRect(-10, -20, 20, 40);

    return path;

}

因为随着图形的复杂性,该检测呈现出数量级的递增,该操作耗费大量的时间。另一个方法是重写collidesWithItem函数去提供自定义的图元和形状检测规则。

自此完成了小鼠类的实现,准备开始使用它,看看主函数中如何在场景中使用小鼠类,并看看如何通过View来展示场景中的图元。

主函数:

该例子中通过主函数提供一个主窗口,场景一个图元类对象和场景类对象,把图元放到场景中并展示。

 int main(int argc, char **argv)

{

    QApplication app(argc, argv);

qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));

首先,创建一个应用对象,调用全局函数qsrand函数指定随机数。

然后创建场景对象

 QGraphicsScene scene;

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

场景类作为图元类的容器。使你能够高效的决定图元的位置和指定的可现实的场景中的位置一样。

 

当创建一个场景时建议场景一个场景的矩形区域。该矩形区域指定了场景的范围。主要用在QGraphicsView类决定视图的默认滚动区域,通过QGraphicsScene去管理图元索引。如果未明确指定,那么场景默认的矩形区域为所有的图元在场景中初始创建的区域。

scene.setItemIndexMethod(QGraphicsScene::NoIndex);

该函数常用来搜索图元项。这种方法非常适合处理动态的场景,当许多场景被添加、移动、删除时。该函数用BSP树去进行索引查找。

 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类对象。该对象在一个可滚动视口中显示场景。同时我们应确保绘图上下文通过反锯齿渲染,并且添加了一个奶酪背景画刷。

 

该背景图片作为一个二进制文件被存储在Qt的资源系统中。QPixmap类构造时接受文件名和文件路径。

 view.setCacheMode(QGraphicsView::CacheBackground);

    view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);

    view.setDragMode(QGraphicsView::ScrollHandDrag);

然后设置缓存模式;QGraphicsView类能够在一个像素映射上预缓存内容,然后绘制到视口中。该种方法能够在渲染时快速的绘制到视口中。这个CacheMode属性用于视图的缓冲,并且确保视图背景的缓存。

 

设置拖拽模式属性我们能够定义当用户拖拽鼠标时应该发生什么动作。

 view.setWindowTitle(QT_TRANSLATE_NOOP(QGraphicsView, "Colliding Mice"));

#if defined(Q_WS_S60) || defined(Q_WS_MAEMO_5) || defined(Q_WS_SIMULATOR)

    view.showMaximized();

#else

    view.resize(400, 300);

    view.show();

#endif

 

    QTimer timer;

    QObject::connect(&timer, SIGNAL(timeout()), &scene, SLOT(advance()));

    timer.start(1000 / 33);

 

    return app.exec();

}

然后定义了应用窗口的标题,尺寸在事件循环之前。

最后定义一个定时器,定时发生timeout信号,来调用场景的advace函数来推进场景,同时不断调整小鼠的移动位置等等,速率是30帧每秒。


                                              欢迎各位交流,如有不足望指点一二,Email:qixiaoyu718@163.com