Plane Spotter (QML)

Plane Spotter 示例演示了如何将位置和定位数据类型紧密集成到 QML 中。

Plane Spotter 示例演示了如何将位置和定位相关的 C++ 数据类型集成到 QML 中,反之亦然。当需要在本地环境中运行 CPU 密集型定位计算,但结果又要用 QML 显示时,这就非常有用了。

示例显示了欧洲地图和横跨欧洲的两条航线上的飞机。第一架飞机往返于奥斯陆和柏林之间,第二架飞机往返于伦敦和柏林之间。每架飞机的位置跟踪都是用 C++ 实现的。奥斯陆至柏林的飞机由 QML 驾驶,伦敦至柏林的飞机由 C++ 驾驶员指挥。

运行示例

要从 Qt Creator,打开Welcome 模式,并从Examples 中选择示例。更多信息,请参阅Qt Creator: Tutorial:构建并运行

概述

本例使用Q_GADGET 功能作为位置控制器实现的一部分。它允许将非基于QObject 的 C++ 值类型直接集成到 QML 中。

PlaneController 类的主要目的是在给定时间内跟踪平面的当前坐标。它通过 position 属性公开位置。

class PlaneController: public QObject
{
    Q_OBJECT
    Q_PROPERTY(QGeoCoordinate position READ position WRITE setPosition NOTIFY positionChanged)
    // ...
};

示例中的main() 函数负责将PlaneController 类实例绑定到 QML 上下文中:

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    PlaneController oslo2berlin;
    PlaneController berlin2london;

    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("oslo2Berlin", &oslo2berlin);
    engine.rootContext()->setContextProperty("berlin2London", &berlin2london);
    engine.load(QUrl(QStringLiteral("qrc:/planespotter.qml")));

    return app.exec();
}

QObject 派生类类似,QGeoCoordinate 无需额外的 QML 封装器即可集成。

操纵飞机

如上所述,PlaneController 类的主要目的是跟踪两架飞机(奥斯陆-柏林和伦敦-柏林)的当前位置,并将其作为属性公布给 QML 层。它的第二个目的是设置飞机并使其沿着给定的飞行路径前进。从某种意义上说,它可以充当飞行员。这一点与CoordinateAnimation 非常相似,后者可以将从一个地理坐标过渡到另一个地理坐标的过程制作成动画。本例演示了 C++ 代码如何利用 PlaneController 自身的驾驶能力以及 QML 代码如何利用CoordinateAnimation 作为飞行员修改PlaneController 的位置属性。奥斯陆-柏林飞机使用 QML 代码进行动画,而伦敦-柏林飞机则使用 C++ 代码进行动画。

无论使用哪种驾驶员,驾驶员的操作结果在 C++ 和 QML 中都是可见的,因此该示例演示了通过 C++/QML 边界无阻碍地直接交换位置数据。

每个Plane 的可视化表示是使用MapQuickItem 类型完成的,该类型允许将任意QtQuick 项嵌入地图:

// Plane.qml
MapQuickItem {
    id: plane
    property string pilotName;
    property int bearing: 0;

    anchorPoint.x: image.width/2
    anchorPoint.y: image.height/2

    sourceItem: Item {
        //...
    }
}
C++ 飞行员

C++ 飞机由 C++ 驾驶。控制器类的fromto 属性设置了起点和终点,飞行员使用它们来计算飞机的方位:

Q_PROPERTY(QGeoCoordinate from READ from WRITE setFrom NOTIFY fromChanged)
Q_PROPERTY(QGeoCoordinate to READ to WRITE setTo NOTIFY toChanged)

飞行员使用QBasicTimerQTimerEvents 不断更新位置。在每次定时器迭代过程中,PlaneController::updatePosition() ,并计算出新的位置。

void updatePosition()
{
    // simple progress animation
    qreal progress;
    QTime current = QTime::currentTime();
    if (current >= finishTime) {
        progress = 1.0;
        timer.stop();
    } else {
        progress = ((qreal)startTime.msecsTo(current) / ANIMATION_DURATION);
    }

    setPosition(coordinateInterpolation(
                      fromCoordinate, toCoordinate, easingCurve.valueForProgress(progress)));

    if (!timer.isActive())
        emit arrived();
}

计算出新位置后,setPosition() 被调用,随后的属性更改通知会将新位置推送到 QML 层。

点击 C++ 平面即可启动:

Plane {
    id: cppPlane
    pilotName: "C++"
    coordinate: berlin2London.position

    TapHandler {
        onTapped: {
            if (cppPlaneAnimation.running || berlin2London.isFlying()) {
                console.log("Plane still in the air.");
                return;
            }

            berlin2London.swapDestinations();
            cppPlaneAnimation.rotationDirection = berlin2London.position.azimuthTo(berlin2London.to)
            cppPlaneAnimation.start();
            cppPlane.departed();
        }
    }
}

azimuthTo() 计算从一个坐标到另一个坐标的度数方位。请注意,上述代码利用 QML 动画将旋转和位置变化绑定到一个动画流程中:

SequentialAnimation {
    id: cppPlaneAnimation
    property real rotationDirection : 0;
    NumberAnimation {
        target: cppPlane; property: "bearing"; duration: 1000
        easing.type: Easing.InOutQuad
        to: cppPlaneAnimation.rotationDirection
    }
    ScriptAction { script: berlin2London.startFlight() }
}

首先,NumberAnimation 将飞机旋转到正确的方向,一旦旋转完成,startFlight() 函数就会开始改变飞机的位置。

public slots:
    void startFlight()
    {
        if (timer.isActive())
            return;

        startTime = QTime::currentTime();
        finishTime = startTime.addMSecs(ANIMATION_DURATION);

        timer.start(15, this);
        emit departed();
    }
QML 飞行器

CoordinateAnimation 类型用于控制从奥斯陆到柏林的飞行,反之亦然。它取代了上面的ScriptAction

CoordinateAnimation {
    id: coordinateAnimation; duration: 5000
    target: oslo2Berlin; property: "position"
    easing.type: Easing.InOutQuad
}

QML 飞机的TapHandler 实现了航线设置逻辑,并在需要时启动动画。

TapHandler {
    onTapped: {
        if (qmlPlaneAnimation.running) {
            console.log("Plane still in the air.");
            return;
        }

        if (oslo2Berlin.position === berlin) {
            coordinateAnimation.from = berlin;
            coordinateAnimation.to = oslo;
        } else if (oslo2Berlin.position === oslo) {
            coordinateAnimation.from = oslo;
            coordinateAnimation.to = berlin;
        }

        qmlPlaneAnimation.rotationDirection = oslo2Berlin.position.azimuthTo(coordinateAnimation.to)
        qmlPlaneAnimation.start()
    }
}

示例项目 @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.