이 페이지에서

축 처리

QML에서 사용자 정의 입력 핸들러를 사용하여 축 드래그 기능을 구현하고, 사용자 정의 축 포맷터를 생성하는 방법.

‘축 처리’에서는 축과 관련된 두 가지 사용자 정의 기능을 보여줍니다. 이 기능들은 애플리케이션 내에서 각각 별도의 탭을 가지고 있습니다.

다음 섹션에서는 해당 기능에만 집중하고 기본 기능에 대한 설명은 생략합니다. 더 자세한 QML 예제 문서는 ‘간단한 산점도’를 참조하십시오.

구형 데이터와 날짜 기반 축이 포함된 3D 산점도

예제 실행

다음 경로에서 예제를 실행할 수 있습니다:

축 드래그

Axis Dragging 탭에서, 축 레이블을 드래그하여 축 범위를 변경할 수 있도록 하는 사용자 정의 입력 핸들러를 QML로 구현합니다. 또한, 직교 투영을 사용하고 사용자 정의 항목의 속성을 동적으로 업데이트합니다.

기본 입력 처리 재정의

기본 입력 처리 메커니즘을 비활성화하려면 Scatter3D 의 기본 입력 핸들러 설정을 해제하십시오:

unsetDefaultInputHandler();
    ...

그런 다음, ` MouseArea `를 추가하고, 이를 부모 요소(즉, ` scatterGraph `가 포함된 ` Item `)를 채우도록 설정합니다. 또한, 이 예제에서는 다른 버튼이 필요하지 않으므로 마우스 왼쪽 버튼 클릭만 허용하도록 설정합니다:

MouseArea {
    anchors.fill: parent
    hoverEnabled: true
    acceptedButtons: Qt.LeftButton
    ...

그런 다음 마우스 클릭 이벤트를 수신하고, 이벤트가 포착되면 그래프에 선택 쿼리를 전송합니다:

onPressed: (mouse)=> {
               scatterGraph.doPicking(Qt.point(mouse.x, mouse.y));
           }

onPositionChanged 신호 핸들러는 이동 거리 계산에 필요한 현재 마우스 위치를 포착합니다:

onPositionChanged: (mouse)=> {
                       currentMouseX = mouse.x;
                       currentMouseY = mouse.y;
    ...

onPositionChanged 의 마지막 부분에서, 나중에 소개될 이동 거리 계산을 위해 이전 마우스 위치를 저장합니다:

...
    previousMouseX = currentMouseX;
    previousMouseY = currentMouseY;
}
마우스 움직임을 축 범위 변화로 변환하기

` scatterGraph`에서 ` onSelectedElementChanged` 신호를 감지합니다. 이 신호는 ` inputArea`의 ` onPressed `에서 선택 쿼리가 수행된 후 발생합니다. 해당 요소의 유형은 관심 대상이므로, 메인 컴포넌트에서 정의한 속성(property int selectedAxisLabel: -1)으로 설정합니다:

onSelectedElementChanged: {
    if (selectedElement >= Graphs3D.ElementType.AxisXLabel
            && selectedElement <= Graphs3D.ElementType.AxisZLabel) {
        selectedAxisLabel = selectedElement;
    } else {
        selectedAxisLabel = -1;
    }
}

그런 다음, inputArea 의 onPositionChanged 로 돌아가서 마우스 버튼이 눌려 있는지, 그리고 현재 축 레이블이 선택되어 있는지 확인하십시오. 조건이 충족되면, 마우스 움직임을 축 범위 업데이트로 변환하는 함수를 호출하십시오:

...
if (pressed && selectedAxisLabel != -1)
    axisDragView.dragAxis();
...

이 경우 카메라 회전이 고정되어 있으므로 변환이 간단합니다. 미리 계산된 값을 사용하고, 마우스 이동 거리를 계산한 다음, 해당 값을 선택된 축 범위에 적용하면 됩니다:

function dragAxis() {
    // Do nothing if previous mouse position is uninitialized
    if (previousMouseX === -1)
        return;

    // Directional drag multipliers based on rotation. Camera is locked to 45 degrees, so we
    // can use one precalculated value instead of calculating xx, xy, zx and zy individually
    var cameraMultiplier = 0.70710678;

    // Calculate the mouse move amount
    var moveX = currentMouseX - previousMouseX;
    var moveY = currentMouseY - previousMouseY;

    // Adjust axes
    switch (selectedAxisLabel) {
    case Graphs3D.ElementType.AxisXLabel:
        var distance = ((moveX - moveY) * cameraMultiplier) / dragSpeedModifier;
        // Check if we need to change min or max first to avoid invalid ranges
        if (distance > 0) {
            scatterGraph.axisX.min -= distance;
            scatterGraph.axisX.max -= distance;
        } else {
            scatterGraph.axisX.max -= distance;
            scatterGraph.axisX.min -= distance;
        }
        break;
    case Graphs3D.ElementType.AxisYLabel:
        distance = moveY / dragSpeedModifier;
        // Check if we need to change min or max first to avoid invalid ranges
        if (distance > 0) {
            scatterGraph.axisY.max += distance;
            scatterGraph.axisY.min += distance;
        } else {
            scatterGraph.axisY.min += distance;
            scatterGraph.axisY.max += distance;
        }
        break;
    case Graphs3D.ElementType.AxisZLabel:
        distance = ((moveX + moveY) * cameraMultiplier) / dragSpeedModifier;
        // Check if we need to change min or max first to avoid invalid ranges
        if (distance > 0) {
            scatterGraph.axisZ.max += distance;
            scatterGraph.axisZ.min += distance;
        } else {
            scatterGraph.axisZ.min += distance;
            scatterGraph.axisZ.max += distance;
        }
        break;
    }
}

마우스 움직임에서 축 범위 업데이트로의 보다 정교한 변환 방법은 Graph Gallery를 참조하십시오.

기타 기능

이 예제는 정투영을 사용하는 방법과 사용자 정의 항목의 속성을 실시간으로 업데이트하는 방법도 보여줍니다.

정사투영은 매우 간단합니다. scatterGraph 의 orthoProjection 속성을 변경하기만 하면 됩니다. 예제에는 이 기능을 켜고 끄는 버튼이 있습니다:

Button {
    id: orthoToggle
    width: axisDragView.portraitMode ? parent.width : parent.width / 3
    text: "Display Orthographic"
    anchors.left: axisDragView.portraitMode ? parent.left : rangeToggle.right
    anchors.top: axisDragView.portraitMode ? rangeToggle.bottom : parent.top
    onClicked: {
        if (scatterGraph.orthoProjection) {
            text = "Display Orthographic";
            scatterGraph.orthoProjection = false;
            // Orthographic projection disables shadows, so we need to switch them back on
            scatterGraph.shadowQuality = Graphs3D.ShadowQuality.Medium
        } else {
            text = "Display Perspective";
            scatterGraph.orthoProjection = true;
        }
    }
}

사용자 정의 항목의 경우, scatterGraph 의 customItemList 에 값을 하나 추가하면 됩니다:

customItemList: [
    Custom3DItem {
        id: qtCube
        meshFile: ":/qml/axishandling/cube.mesh"
        textureFile: ":/qml/axishandling/cubetexture.png"
        position: Qt.vector3d(0.65, 0.35, 0.65)
        scaling: Qt.vector3d(0.3, 0.3, 0.3)
    }
]

그래프 내의 모든 항목을 추가, 제거 및 순환시키기 위해 타이머를 구현하고, 사용자 정의 항목을 순환시키는 데에도 동일한 타이머를 사용합니다:

onTriggered: {
    rotationAngle = rotationAngle + 1;
    qtCube.setRotationAxisAndAngle(Qt.vector3d(1, 0, 1), rotationAngle);
    ...

축 포맷터

Axis Formatter 탭에서 사용자 정의 축 포맷터를 생성합니다. 또한 미리 정의된 축 포맷터를 사용하는 방법도 설명합니다.

사용자 정의 축 포맷터

축 포맷터를 사용자 정의하려면 ` QValue3DAxisFormatter`을 상속받은 서브클래스를 만들어야 하며, 이는 QML 코드만으로는 수행할 수 없습니다. 이 예제에서 축은 부동 소수점 값을 타임스탬프로 해석하여 축 레이블에 날짜를 표시합니다. 이를 구현하기 위해 ` QValue3DAxisFormatter`을 상속받은 ` CustomFormatter`이라는 새로운 클래스를 도입합니다:

class CustomFormatter : public QValue3DAxisFormatter
{
...

QScatter3DSeries 의 float 값은 데이터 폭의 차이로 인해 QDateTime 값으로 직접 형변환할 수 없으므로, 두 값 간의 일종의 매핑이 필요합니다. 매핑을 수행하려면 포맷터에 기준 날짜를 지정하고, QScatter3DSeries 의 float 값을 해당 기준 값을 기준으로 한 날짜 오프셋으로 해석합니다. 기준 날짜는 속성으로 지정됩니다:

Q_PROPERTY(QDate originDate READ originDate WRITE setOriginDate NOTIFY originDateChanged)

값을 QDateTime 로 매핑하려면 valueToDateTime() 메서드를 사용합니다:

QDateTime CustomFormatter::valueToDateTime(qreal value) const
{
    return m_originDate.startOfDay().addMSecs(qint64(oneDayMs * value));
}

축 포맷터로 기능하기 위해 ` CustomFormatter `은 몇 가지 가상 메서드를 재구현해야 합니다:

QValue3DAxisFormatter *createNewInstance() const override;
void populateCopy(QValue3DAxisFormatter &copy) override;
void recalculate() override;
QString stringForValue(qreal value, const QString &format) override;

처음 두 메서드는 간단합니다. CustomFormatter 의 새 인스턴스를 생성하고 필요한 데이터를 해당 인스턴스로 복사하기만 하면 됩니다. 이 두 메서드를 사용하여 렌더링을 위한 포맷터 캐시를 생성하고 업데이트하십시오. populateCopy() 의 슈퍼클래스 구현을 호출하는 것을 잊지 마십시오:

QValue3DAxisFormatter *CustomFormatter::createNewInstance() const
{
    return new CustomFormatter();
}

void CustomFormatter::populateCopy(QValue3DAxisFormatter &copy)
{
    QValue3DAxisFormatter::populateCopy(copy);

    CustomFormatter *customFormatter = static_cast<CustomFormatter *>(&copy);
    customFormatter->m_originDate = m_originDate;
    customFormatter->m_selectionFormat = m_selectionFormat;
}

CustomFormatter recalculate() 메서드에서 대부분의 작업이 수행되며, 여기서 포맷터는 그리드, 서브그리드 및 레이블 위치를 계산하고 레이블 문자열의 서식을 지정합니다. 사용자 정의 포맷터에서는 축의 세그먼트 수를 무시하고 그리드 선을 항상 자정에 그립니다. 하위 세그먼트 수와 레이블 위치는 평소와 같이 처리됩니다:

void CustomFormatter::recalculate()
{
    // We want our axis to always have gridlines at date breaks

    // Convert range into QDateTimes
    QDateTime minTime = valueToDateTime(qreal(axis()->min()));
    QDateTime maxTime = valueToDateTime(qreal(axis()->max()));

    // Find out the grid counts
    QTime midnight(0, 0);
    QDateTime minFullDate(minTime.date(), midnight);
    int gridCount = 0;
    if (minFullDate != minTime)
        minFullDate = minFullDate.addDays(1);
    QDateTime maxFullDate(maxTime.date(), midnight);

    gridCount += minFullDate.daysTo(maxFullDate) + 1;
    int subGridCount = axis()->subSegmentCount() - 1;

    QList<float> gridPositions;
    QList<float> subGridPositions;
    QList<float> labelPositions;
    QStringList labelStrings;

    // Reserve space for position arrays and label strings
    gridPositions.resize(gridCount);
    subGridPositions.resize((gridCount + 1) * subGridCount);
    labelPositions.resize(gridCount);
    labelStrings.reserve(gridCount);

    // Calculate positions and format labels
    qint64 startMs = minTime.toMSecsSinceEpoch();
    qint64 endMs = maxTime.toMSecsSinceEpoch();
    qreal dateNormalizer = endMs - startMs;
    qreal firstLineOffset = (minFullDate.toMSecsSinceEpoch() - startMs) / dateNormalizer;
    qreal segmentStep = oneDayMs / dateNormalizer;
    qreal subSegmentStep = 0;
    if (subGridCount > 0)
        subSegmentStep = segmentStep / qreal(subGridCount + 1);

    for (int i = 0; i < gridCount; i++) {
        qreal gridValue = firstLineOffset + (segmentStep * qreal(i));
        gridPositions[i] = float(gridValue);
        labelPositions[i] = float(gridValue);
        labelStrings << minFullDate.addDays(i).toString(axis()->labelFormat());
    }

    for (int i = 0; i <= gridCount; i++) {
        if (subGridPositions.size()) {
            for (int j = 0; j < subGridCount; j++) {
                float position;
                if (i)
                    position = gridPositions.at(i - 1) + subSegmentStep * (j + 1);
                else
                    position = gridPositions.at(0) - segmentStep + subSegmentStep * (j + 1);
                if (position > 1.0f || position < 0.0f)
                    position = gridPositions.at(0);
                subGridPositions[i * subGridCount + j] = position;
            }
        }
    }
    setGridPoitions(gridPositions);
    setSubGridPositions(subGridPositions);
    setlabelPositions(labelPositions);
    setLabelStrings(labelStrings);
}

축 레이블은 날짜만 표시되도록 서식이 지정됩니다. 그러나 선택 레이블의 타임스탬프 해상도를 높이려면, 사용자가 이를 사용자 정의할 수 있도록 사용자 정의 포맷터에 다른 속성을 지정하십시오:

Q_PROPERTY(QString selectionFormat READ selectionFormat WRITE setSelectionFormat NOTIFY
               selectionFormatChanged)

이 선택 형식 속성은 재구현된 ` stringToValue ` 메서드에서 사용되며, 이 메서드에서는 전달된 형식이 무시되고 사용자 정의 선택 형식이 대신 적용됩니다.

QString CustomFormatter::stringForValue(qreal value, const QString &format)
{
    Q_UNUSED(format);

    return valueToDateTime(value).toString(m_selectionFormat);
}

새로운 사용자 정의 포맷터를 QML에서 사용할 수 있게 하려면, 이를 선언하고 QML 모듈로 지정해야 합니다. 이에 대한 자세한 내용은 Surface Graph Gallery를 참조하십시오.

QML

QML 코드에서 각 차원마다 별도의 축을 정의합니다:

axisZ: valueAxis
axisY: logAxis
axisX: dateAxis

Z축은 일반적인 ` Value3DAxis`입니다:

Value3DAxis {
    id: valueAxis
    segmentCount: 5
    subSegmentCount: 2
    labelFormat: "%.2f"
    min: 0
    max: 10
}

Y축의 경우 로그 축을 정의합니다. ` Value3DAxis `가 로그 눈금을 표시하도록 하려면 축의 ` formatter ` 속성에 ` LogValue3DAxisFormatter `를 지정합니다:

Value3DAxis {
    id: logAxis
    formatter: LogValue3DAxisFormatter {
        id: logAxisFormatter
        base: 10
        autoSubGrid: true
        edgeLabelsVisible: true
    }
    labelFormat: "%.2f"
}

마지막으로, X축에는 새로운 CustomFormatter 를 사용합니다:

Value3DAxis {
    id: dateAxis
    formatter: CustomFormatter {
        originDate: "2023-01-01"
        selectionFormat: "yyyy-MM-dd HH:mm:ss"
    }
    subSegmentCount: 2
    labelFormat: "yyyy-MM-dd"
    min: 0
    max: 14
}

애플리케이션의 나머지 부분은 축을 수정하고 그래프를 표시하는, 설명이 필요 없을 정도로 직관적인 로직으로 구성되어 있습니다.

예제 내용

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