本页内容

Plane Spotter (QML)

Plane Spotter 示例展示了位置和定位数据类型与QML的紧密集成。

一张北欧地图,上面标有两个标注为“QML”和“C++”的飞机图标

Plane Spotter 示例演示了如何将位置和定位相关的C++数据类型集成到QML中,反之亦然。当需要在原生环境中运行耗费大量CPU资源的位置计算,但又希望使用QML显示结果时,此方法非常有用。

该示例展示了一张欧洲地图,以及两架飞越欧洲的飞机。第一架飞机往返于奥斯陆和柏林之间,第二架飞机往返于伦敦和柏林之间。每架飞机的位置追踪均由C++实现。奥斯陆-柏林航线的飞机由QML控制,而伦敦-柏林航线的飞机则由C++飞行员控制。

运行示例

您可以通过以下方式运行该示例:

概述

本示例在实现位置控制器时,利用了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 非常相似,后者可以实现从一个地理坐标到另一个地理坐标的过渡动画。本示例演示了PlaneController 的位置属性如何通过C++代码利用PlaneController自身的导航能力进行修改,以及如何通过QML代码使用CoordinateAnimation 作为导航器进行修改。 奥斯陆至柏林的航班通过 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++进行操控。控制器类的from 和to 属性分别设置起点和终点,飞行员据此计算飞机的航向:

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

飞行员利用QBasicTimer 和QTimerEvents 持续更新位置。在每次定时器迭代期间,都会调用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

© 2026 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.