En esta página

Manejo de ejes

Implementación del arrastre de ejes con un manejador de entrada personalizado en QML, y creación de un formateador de ejes personalizado.

Manejo de Ejes demuestra dos características personalizadas diferentes con ejes. Las funciones tienen sus propias pestañas en la aplicación.

Las siguientes secciones se concentran sólo en esas características y omiten la explicación de la funcionalidad básica - para una documentación más detallada del ejemplo QML, vea Simple Scatter Graph.

Ejecutar el ejemplo

Para ejecutar el ejemplo desde Qt Creatorabra el modo Welcome y seleccione el ejemplo de Examples. Para más información, consulte Qt Creator: Tutorial: Construir y ejecutar.

Arrastre de Ejes

En la pestaña Axis Dragging, implemente un manejador de entrada personalizado en QML que le permita arrastrar las etiquetas de los ejes para cambiar los rangos de los ejes. Además, utilice la proyección ortográfica y actualice dinámicamente las propiedades de un elemento personalizado.

Anulación del manejo de entrada por defecto

Para desactivar el mecanismo de manejo de entrada por defecto, desactive el manejador de entrada por defecto de Scatter3D:

unsetDefaultInputHandler();
    ...

A continuación, añada un MouseArea y configúrelo para que rellene el padre, que es el mismo Item que contiene nuestro scatterGraph. También, configúralo para que sólo acepte pulsaciones del botón izquierdo del ratón, ya que en este ejemplo los otros botones no son necesarios:

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

Entonces, escucha las pulsaciones del ratón, y cuando las atrapa, envía una consulta de selección al gráfico:

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

El manejador de señales onPositionChanged captura la posición actual del ratón que será necesaria para el cálculo de la distancia de movimiento:

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

Al final de onPositionChanged, guarda la posición anterior del ratón para el cálculo de la distancia de movimiento que se introducirá más adelante:

...
    previousMouseX = currentMouseX;
    previousMouseY = currentMouseY;
}
Traduciendo el Movimiento del Ratón al Cambio de Rango del Eje

En scatterGraph, escuche onSelectedElementChanged. La señal se emite después de que se haya realizado la consulta de selección en onPressed del inputArea. Establezca el tipo de elemento en una propiedad que haya definido (property int selectedAxisLabel: -1) en el componente principal, ya que es de un tipo que le interesa:

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

A continuación, de vuelta en el onPositionChanged de inputArea, compruebe si se pulsa un botón del ratón y si tiene una selección de etiqueta de eje actual. Si se cumplen las condiciones, llama a la función que hace la conversión de movimiento del ratón a actualización del rango de ejes:

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

La conversión es fácil en este caso, ya que la rotación de la cámara es fija. Puede utilizar algunos valores precalculados, calcular la distancia del movimiento del ratón y aplicar los valores al rango de ejes seleccionado:

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;
    }
}

Para una conversión más sofisticada del movimiento del ratón a la actualización del rango de ejes, consulte Galería de gráficos.

Otras funciones

El ejemplo también muestra cómo utilizar la proyección ortográfica y cómo actualizar las propiedades de un elemento personalizado sobre la marcha.

La proyección ortográfica es muy sencilla. Sólo tendrá que cambiar la propiedad orthoProjection de scatterGraph. El ejemplo tiene un botón para activarla y desactivarla:

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;
        }
    }
}

Para elementos personalizados, añade uno a la customItemList de scatterGraph:

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)
    }
]

Implementa un temporizador para añadir, eliminar y rotar todos los elementos del gráfico, y utiliza el mismo temporizador para rotar el elemento personalizado:

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

Formateadores de ejes

En la pestaña Axis Formatter, crea un formateador de ejes personalizado. También ilustra cómo utilizar los formateadores de ejes predefinidos.

Formateador de Ejes Personalizado

Personalizar los formateadores de eje requiere subclasificar el QValue3DAxisFormatter, lo que no puede hacerse sólo en código QML. En este ejemplo, el eje interpreta los valores flotantes como una marca de tiempo y muestra la fecha en las etiquetas del eje. Para conseguirlo, introduzca una nueva clase llamada CustomFormatter, que subclasea a la QValue3DAxisFormatter:

class CustomFormatter : public QValue3DAxisFormatter
{
...

Dado que los valores flotantes de QScatter3DSeries no pueden convertirse directamente en valores de QDateTime debido a la diferencia en la anchura de los datos, se necesita algún tipo de correspondencia entre ambos. Para ello, especifique una fecha de origen para el formateador e interprete los valores flotantes de QScatter3DSeries como desplazamientos de fecha respecto a ese valor de origen. La fecha de origen se da como una propiedad:

Q_PROPERTY(QDate originDate READ originDate WRITE setOriginDate NOTIFY originDateChanged)

Para el mapeo de valor a QDateTime, utilice el método valueToDateTime():

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

Para funcionar como formateador de ejes, CustomFormatter necesita reimplementar algunos métodos virtuales:

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

Los dos primeros métodos son sencillos, basta con crear una nueva instancia de CustomFormatter y copiarle los datos necesarios. Usa estos dos métodos para crear y actualizar una caché de formateadores para propósitos de renderizado. Recuerda llamar a la implementación de la superclase 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 hace la mayor parte de su trabajo en el método recalculate(), donde el formateador calcula la rejilla, la sub-rejilla y las posiciones de las etiquetas, así como formatea las cadenas de las etiquetas. En el formateador personalizado, ignora el recuento de segmentos del eje y dibuja una línea de rejilla siempre a medianoche. El recuento de subsegmentos y la posición de las etiquetas se manejan normalmente:

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);
}

Las etiquetas del eje se formatean para mostrar sólo la fecha. Sin embargo, para aumentar la resolución de la marca de tiempo de la etiqueta de selección, especifique otra propiedad para el formateador personalizado que permita al usuario personalizarlo:

Q_PROPERTY(QString selectionFormat READ selectionFormat WRITE setSelectionFormat NOTIFY
               selectionFormatChanged)

Esta propiedad de formato de selección se utiliza en el método reimplementado stringToValue, donde se ignora el formato enviado y se sustituye por el formato de selección personalizado:

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

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

Para exponer nuestro nuevo formateador personalizado al QML, declárelo y conviértalo en un módulo QML. Para obtener información sobre cómo hacerlo, consulte Galería de gráficos de superficie.

QML

En el código QML, defina un eje diferente para cada dimensión:

axisZ: valueAxis
axisY: logAxis
axisX: dateAxis

El eje Z es simplemente un Value3DAxis normal:

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

Para el eje Y, defina un eje logarítmico. Para que Value3DAxis muestre una escala logarítmica, especifique LogValue3DAxisFormatter para la propiedad formatter del eje:

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

Y finalmente, para el eje X utilice el nuevo 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
}

El resto de la aplicación consiste en una lógica bastante autoexplicativa para modificar los ejes y mostrar el gráfico.

Contenido del ejemplo

Proyecto de ejemplo @ 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.