서피스 그래프 갤러리

Surface3D 그래프를 사용하는 세 가지 방법이 포함된 갤러리입니다.

Surface 그래프 갤러리에서는 Surface3D 그래프를 사용하여 세 가지 사용자 지정 기능을 보여 줍니다. 각 기능은 애플리케이션에 자체 탭이 있습니다.

다음 섹션에서는 이러한 기능에만 집중하고 기본 기능에 대한 설명은 생략합니다. 자세한 QML 예제 설명서는 간단한 분산형 그래프를 참조하십시오.

예제 실행하기

에서 예제를 실행하려면 Qt Creator에서 Welcome 모드를 열고 Examples 에서 예제를 선택합니다. 자세한 내용은 예제 빌드 및 실행하기를 참조하세요.

높이 맵

Height Map 탭에서 높이 데이터로 표면 그래프를 생성합니다. 사용된 데이터는 뉴질랜드의 루아페후 산과 응우루호 산의 높이 맵입니다.

그래프에 데이터 추가

데이터는 높이 맵 이미지에서 높이 정보를 읽는 HeightMapSurfaceDataProxy 을 사용하여 설정합니다. 프록시 자체는 Surface3DSeries 에 포함되어 있습니다. HeightMapSurfaceDataProxy 내부의 heightMapFile 속성은 높이 데이터가 포함된 이미지 파일을 지정합니다. 프록시의 값 속성은 표면 영역 너비, 깊이 및 높이의 최소값과 최대값을 정의합니다. zx 값은 위도 및 경도(대략 실제 위치)이며 y 값은 미터 단위입니다.

참고: 그래프의 가로 세로 비율은 실제 축척으로 설정되지 않고 대신 높이가 과장되어 있습니다.

Surface3DSeries {
    id: heightSeries
    flatShadingEnabled: false
    drawMode: Surface3DSeries.DrawSurface

    HeightMapSurfaceDataProxy {
        heightMapFile: "://qml/qmlsurfacegallery/heightmap.png"
        // We don't want the default data values set by heightmap proxy, but use
        // actual coordinate and height values instead
        autoScaleY: true
        minYValue: 740
        maxYValue: 2787
        minZValue: -374 // ~ -39.374411"N
        maxZValue: -116 // ~ -39.115971"N
        minXValue: 472  // ~ 175.471767"E
        maxXValue: 781  // ~ 175.780758"E
    }

    onDrawModeChanged: heightMapView.checkState()
}
데이터 표시

main.qml 에서 Surface3D 요소를 설정하여 데이터를 표시합니다.

먼저 서페이스에 사용할 사용자 정의 그라데이션을 정의합니다. ColorGradient 을 사용하여 0.0에서 1.0 사이의 색상을 설정하고 그래프를 더 선명하게 만들기 위해 두 스톱을 추가합니다:

ColorGradient {
    id: surfaceGradient
    ColorGradientStop { position: 0.0; color: "darkgreen"}
    ColorGradientStop { position: 0.15; color: "darkslategray" }
    ColorGradientStop { position: 0.7; color: "peru" }
    ColorGradientStop { position: 1.0; color: "white" }
}

이 요소를 Surface3D 에 사용된 themebaseGradients 속성으로 설정합니다:

theme: Theme3D {
    type: Theme3D.ThemeStoneMoss
    font.family: "STCaiyun"
    font.pointSize: 35
    colorStyle: Theme3D.ColorStyleRangeGradient
    baseGradients: [surfaceGradient] // Use the custom gradient
}

버튼을 사용하여 다른 Surface3D 기능을 제어합니다.

첫 번째 버튼은 서피스 그리드를 켜고 끕니다. 그리기 모드는 완전히 지울 수 없으므로 서피스 자체가 보이지 않는 한 서피스 그리드를 숨길 수 없습니다:

onClicked: {
    if (heightSeries.drawMode & Surface3DSeries.DrawWireframe)
        heightSeries.drawMode &= ~Surface3DSeries.DrawWireframe;
    else
        heightSeries.drawMode |= Surface3DSeries.DrawWireframe;
}

두 번째 버튼은 서피스 그리드 색상을 설정합니다:

onClicked: {
    if (Qt.colorEqual(heightSeries.wireframeColor, "#000000")) {
        heightSeries.wireframeColor = "red";
        text = "Black surface\ngrid color";
    } else {
        heightSeries.wireframeColor = "black";
        text = "Red surface\ngrid color";
    }
}

세 번째는 서페이스 그리기 모드에서 서페이스를 켜거나 끕니다. 그리기 모드는 완전히 지울 수 없으므로 서페이스 격자가 보이지 않는 한 서페이스 자체를 숨길 수 없습니다:

onClicked: {
    if (heightSeries.drawMode & Surface3DSeries.DrawSurface)
        heightSeries.drawMode &= ~Surface3DSeries.DrawSurface;
    else
        heightSeries.drawMode |= Surface3DSeries.DrawSurface;
}

네 번째는 음영 모드를 설정합니다. OpenGL ES 시스템에서 예제를 실행하는 경우 플랫 셰이딩을 사용할 수 없습니다:

onClicked: {
    if (heightSeries.flatShadingEnabled) {
        heightSeries.flatShadingEnabled = false;
        text = "Show\nFlat"
    } else {
        heightSeries.flatShadingEnabled = true;
        text = "Show\nSmooth"
    }
}

나머지 버튼은 그래프 배경 기능을 제어합니다.

스펙트로그램

Spectrogram 탭에서 극좌표 및 직교 스펙트로그램을 표시하고 직교 투영을 사용하여 2D로 표시합니다.

스펙트로그램은 다양한 값을 강조하는 데 사용되는 범위 그라데이션이 있는 표면 그래프입니다. 일반적으로 스펙트로그램은 2차원 표면으로 표시되며, 그래프의 하향식 직교 투영으로 시뮬레이션됩니다. 2D 효과를 적용하려면 직교 모드에서 마우스 또는 터치를 통해 그래프 회전을 비활성화합니다.

스펙트로그램 만들기

2D 스펙트로그램을 만들려면 Surface3DSeries 에 제공된 데이터로 Surface3D 항목을 ItemModelSurfaceDataProxy 으로 정의합니다:

Surface3D {
    id: surfaceGraph
    anchors.fill: parent

    Surface3DSeries {
        id: surfaceSeries
        flatShadingEnabled: false
        drawMode: Surface3DSeries.DrawSurface
        baseGradient: surfaceGradient
        colorStyle: Theme3D.ColorStyleRangeGradient
        itemLabelFormat: "(@xLabel, @zLabel): @yLabel"

        ItemModelSurfaceDataProxy {
            itemModel: surfaceData.model
            rowRole: "radius"
            columnRole: "angle"
            yPosRole: "value"
        }
    }

2D 효과를 활성화하는 주요 속성은 orthoProjectionscene.activeCamera.cameraPreset 입니다. 그래프에 직교 투영을 활성화하여 원근을 제거하고 그래프를 위에서 직접 보도록 하여 Y 차원을 제거합니다:

// Remove the perspective and view the graph from top down to achieve 2D effect
orthoProjection: true
scene.activeCamera.cameraPreset: Camera3D.CameraPresetDirectlyAbove

이 시점을 사용하면 가로축 격자가 대부분 표면에 가려지므로 가로 격자를 뒤집어 그래프 위에 그립니다:

flipHorizontalGrid: true
극좌표 스펙트로그램

데이터에 따라 직교 그래프 대신 극좌표 그래프를 사용하는 것이 더 자연스러운 경우가 있습니다. 이는 polar 속성을 통해 지원됩니다.

극좌표 모드와 데카르트 모드 간에 전환하는 버튼을 추가합니다:

Button {
    id: polarToggle
    anchors.margins: 5
    anchors.left: parent.left
    anchors.top: parent.top
    width: spectrogramView.buttonWidth // Calculated elsewhere based on screen orientation
    text: "Switch to\n" + (surfaceGraph.polar ? "cartesian" : "polar")
    onClicked: surfaceGraph.polar = !surfaceGraph.polar;
}

극좌표 모드에서는 X축이 각 극좌표 축으로 변환되고 Z축이 방사형 극좌표 축으로 변환됩니다. 새 축에 따라 서페이스 포인트가 다시 계산됩니다.

방사형 축 레이블은 기본적으로 그래프 외부에 그려집니다. 그래프 내부의 0도 각도 축 바로 옆에 그리려면 작은 오프셋만 정의하면 됩니다:

radialLabelOffset: 0.01

2D 효과를 적용하려면 투영 모드에 따라 rotationEnabled 속성을 자동으로 전환하는 사용자 지정 입력 핸들러로 기본 입력 핸들러를 재정의하여 직교 모드에서 그래프 회전을 비활성화합니다:

inputHandler: TouchInputHandler3D {
    rotationEnabled: !surfaceGraph.orthoProjection
}

오실로스코프

Oscilloscope 탭에서 애플리케이션에서 C++와 QML을 결합하고 동적으로 변경되는 데이터를 표시합니다.

C++의 데이터 소스

항목 모델 기반 프록시는 단순하거나 정적인 그래프에 적합하지만 실시간으로 변화하는 데이터를 표시할 때는 기본 프록시를 사용하면 최상의 성능을 얻을 수 있습니다. 이러한 프록시는 저장하는 데이터 항목이 QObject 을 상속하지 않으므로 QML 코드에서 직접 조작할 수 없으므로 QML에서는 지원되지 않습니다. 이 제한을 극복하려면 C++로 간단한 DataSource 클래스를 구현하여 시리즈의 데이터 프록시를 채우세요.

DataSource 클래스를 생성하여 QML에서 호출할 수 있는 두 가지 메서드를 제공합니다:

class DataSource : public QObject
{
    Q_OBJECT
    ...
Q_INVOKABLE void generateData(int cacheCount, int rowCount, int columnCount,
                              float xMin, float xMax,
                              float yMin, float yMax,
                              float zMin, float zMax);

Q_INVOKABLE void update(QSurface3DSeries *series);

첫 번째 메서드인 generateData() 는 표시할 시뮬레이션된 오실로스코프 데이터의 캐시를 생성합니다. 데이터는 QSurfaceDataProxy 에서 허용하는 형식으로 캐시됩니다:

// Populate caches
auto *generator = QRandomGenerator::global();
for (int i = 0; i < cacheCount; ++i) {
    QSurfaceDataArray &cache = m_data[i];
    float cacheXAdjustment = cacheStep * i;
    float cacheIndexAdjustment = cacheIndexStep * i;
    for (int j = 0; j < rowCount; ++j) {
        QSurfaceDataRow &row = *(cache[j]);
        float rowMod = (float(j)) / float(rowCount);
        float yRangeMod = yRange * rowMod;
        float zRangeMod = zRange * rowMod;
        float z = zRangeMod + zMin;
        qreal rowColWaveAngleMul = M_PI * M_PI * rowMod;
        float rowColWaveMul = yRangeMod * 0.2f;
        for (int k = 0; k < columnCount; k++) {
            float colMod = (float(k)) / float(columnCount);
            float xRangeMod = xRange * colMod;
            float x = xRangeMod + xMin + cacheXAdjustment;
            float colWave = float(qSin((2.0 * M_PI * colMod) - (1.0 / 2.0 * M_PI)) + 1.0);
            float y = (colWave * ((float(qSin(rowColWaveAngleMul * colMod) + 1.0))))
                    * rowColWaveMul
                    + generator->bounded(0.15f) * yRangeMod;

            int index = k + cacheIndexAdjustment;
            if (index >= columnCount) {
                // Wrap over
                index -= columnCount;
                x -= xRange;
            }
            row[index] = QVector3D(x, y, z);
        }
    }
}

두 번째 메서드인 update() 는 캐시된 데이터의 한 세트를 다른 배열로 복사하며, 이 배열은 QSurfaceDataProxy::resetArray()를 호출하여 시리즈의 데이터 프록시로 설정됩니다. 오버헤드를 최소화하려면 배열 차원이 변경되지 않은 경우 동일한 배열을 재사용하세요:

// Each iteration uses data from a different cached array
if (++m_index >= m_data.size())
    m_index = 0;

const QSurfaceDataArray &array = m_data.at(m_index);
int newRowCount = array.size();
int newColumnCount = array.at(0)->size();

// If the first time or the dimensions of the cache array have changed,
// reconstruct the reset array
if (!m_resetArray || series->dataProxy()->rowCount() != newRowCount
        || series->dataProxy()->columnCount() != newColumnCount) {
    m_resetArray = new QSurfaceDataArray();
    m_resetArray->reserve(newRowCount);
    for (int i = 0; i < newRowCount; ++i)
        m_resetArray->append(new QSurfaceDataRow(newColumnCount));
}

// Copy items from our cache to the reset array
for (int i = 0; i < newRowCount; ++i) {
    const QSurfaceDataRow &sourceRow = *(array.at(i));
    QSurfaceDataRow &row = *(*m_resetArray)[i];
    std::copy(sourceRow.cbegin(), sourceRow.cend(), row.begin());
}

// Notify the proxy that data has changed
series->dataProxy()->resetArray(m_resetArray);

이전에 프록시로 설정된 배열 포인터에서 작업하고 있더라도 그래프에 데이터를 렌더링하라는 메시지를 표시하려면 데이터를 변경한 후 QSurfaceDataProxy::resetArray()를 호출해야 합니다.

QML에서 DataSource 메서드에 액세스할 수 있도록 하려면 데이터 소스를 QML_ELEMENT 으로 만들어 데이터 소스를 노출하세요:

class DataSource : public QObject
{
    Q_OBJECT
    QML_ELEMENT

또한 CMakeLists.txt에서 QML 모듈로 선언합니다:

qt6_add_qml_module(qmlsurfacegallery
    URI SurfaceGallery
    VERSION 1.0
    NO_RESOURCE_TARGET_PATH
    SOURCES
        datasource.cpp datasource.h
    ...
)

모든 환경 및 빌드에서 DataSource 클래스 메서드의 파라미터로 QSurface3DSeries 포인터를 사용하려면 메타 유형이 등록되어 있는지 확인하세요:

qRegisterMetaType<QSurface3DSeries *>();
QML 애플리케이션

DataSource 를 사용하려면 QML 모듈을 임포트하고 사용할 DataSource 인스턴스를 생성합니다:

import SurfaceGallery
...
DataSource {
    id: dataSource
}

Surface3D 그래프를 정의하고 Surface3DSeries:

Surface3D {
    id: surfaceGraph
    anchors.fill: parent

    Surface3DSeries {
        id: surfaceSeries
        drawMode: Surface3DSeries.DrawSurfaceAndWireframe
        itemLabelFormat: "@xLabel, @zLabel: @yLabel"

그래프에 첨부하는 Surface3DSeries 에 프록시를 지정하지 마세요. 이렇게 하면 시리즈가 기본값인 QSurfaceDataProxy 을 사용합니다.

itemLabelVisible 로 항목 레이블을 숨깁니다. 동적이고 빠르게 변화하는 데이터의 경우 선택 레이블이 떠다니면 산만하고 읽기 어려울 수 있습니다.

itemLabelVisible: false

선택 포인터 위의 기본 부동 레이블 대신 Text 요소에 선택한 항목 정보를 표시할 수 있습니다:

onItemLabelChanged: {
    if (surfaceSeries.selectedPoint == surfaceSeries.invalidSelectionPosition)
        selectionText.text = "No selection";
    else
        selectionText.text = surfaceSeries.itemLabel;
}

그래프가 완성되면 DataSource 에서 같은 이름의 메서드를 호출하는 도우미 함수 generateData() 를 호출하여 DataSource 캐시를 초기화합니다:

Component.onCompleted: oscilloscopeView.generateData();
...
function generateData() {
    dataSource.generateData(oscilloscopeView.sampleCache, oscilloscopeView.sampleRows,
                            oscilloscopeView.sampleColumns,
                            surfaceGraph.axisX.min, surfaceGraph.axisX.max,
                            surfaceGraph.axisY.min, surfaceGraph.axisY.max,
                            surfaceGraph.axisZ.min, surfaceGraph.axisZ.max);
}

데이터 업데이트를 트리거하려면 요청된 간격으로 DataSource 에서 update() 메서드를 호출하는 Timer 를 정의합니다:

Timer {
    id: refreshTimer
    interval: 1000 / frequencySlider.value
    running: true
    repeat: true
    onTriggered: dataSource.update(surfaceSeries);
}
직접 렌더링 활성화

이 애플리케이션은 빠르게 변화하는 많은 데이터를 처리할 가능성이 있으므로 성능을 위해 직접 렌더링 모드를 사용합니다. 이 모드에서 앤티앨리어싱을 활성화하려면 애플리케이션 창의 표면 형식을 변경하세요. QQuickView 에서 사용하는 기본 형식은 앤티앨리어싱을 지원하지 않습니다. main.cpp 에서 제공되는 유틸리티 기능을 사용하여 표면 형식을 변경하세요:

#include <QtDataVisualization/qutils.h>
...
// Enable antialiasing in direct rendering mode
viewer.setFormat(qDefaultSurfaceFormat(true));

예제 내용

예제 프로젝트 @ 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.