Qt Quick 2 Axis Dragging Example

Implementing axis dragging in QML.

The Qt Quick 2 axis dragging example concentrates on showing how to implement axis range changing by dragging axis labels in QML. It also gives a quick peek to two other new features in Qt Data Visualization 1.1: orthographic projection and dynamic custom item handling.

Running the Example

To run the example from Qt Creator, open the Welcome mode and select the example from Examples. For more information, visit Building and Running an Example.

Overriding Default Input Handling

First we deactivate the default input handling mechanism by setting the active input handler of Scatter3D graph to null:

 Scatter3D {
     id: scatterGraph
     inputHandler: null
     ...

Then we add a MouseArea and set it to fill the parent, which is the same Item our scatterGraph is contained in. We also set it to accept only left mouse button presses, as in this example we are not interested in other buttons:

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

Then we need to listen to mouse presses, and when caught, send a selection query to the graph:

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

Current mouse position, that will be needed for move distance calculation, is caught in onPositionChanged:

 onPositionChanged: {
     currentMouseX = mouse.x;
     currentMouseY = mouse.y;
     ...

At the end of onPositionChanged, we'll save the previous mouse position for move distance calculation that will be introduced later:

 ...
 previousMouseX = currentMouseX;
 previousMouseY = currentMouseY;
 }

Translating Mouse Movement to Axis Range Change

in scatterGraph we will need to listen to onSelectedElementChanged signal. The signal is emitted after the selection query has been made in the onPressed of inputArea. We set the element type into a property we defined (property int selectedAxisLabel: -1) in our main component, since it is of a type we are interested in:

 onSelectedElementChanged: {
     if (selectedElement >= AbstractGraph3D.ElementAxisXLabel
             && selectedElement <= AbstractGraph3D.ElementAxisZLabel)
         selectedAxisLabel = selectedElement
     else
         selectedAxisLabel = -1
 }

Then, back in the onPositionChanged of inputArea, we check if a mouse button is pressed and if we have a current axis label selection. If the conditions are met, we'll call the function that does the conversion from mouse movement to axis range update:

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

The conversion is easy in this case, as we have a fixed camera rotation. We can use some precalculated values, calculate mouse move distance, and apply the values to the selected axis range:

 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 AbstractGraph3D.ElementAxisXLabel:
         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 AbstractGraph3D.ElementAxisYLabel:
         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 AbstractGraph3D.ElementAxisZLabel:
         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
     }
 }

For a more sophisticated conversion from mouse movement to axis range update, see this example.

Other Features

The example also demonstrates how to use orthographic projection and how to update properties of a custom item on the fly.

Orthographic projection is very simple. You'll just need to change orthoProjection property of scatterGraph. In this example we have a button for toggling it on and off:

 NewButton {
     id: orthoToggle
     width: parent.width / 3
     text: "Display Orthographic"
     anchors.left: rangeToggle.right
     onClicked: {
         if (scatterGraph.orthoProjection) {
             text = "Display Orthographic";
             scatterGraph.orthoProjection = false
             // Orthographic projection disables shadows, so we need to switch them back on
             scatterGraph.shadowQuality = AbstractGraph3D.ShadowQualityLow
         } else {
             text = "Display Perspective";
             scatterGraph.orthoProjection = true
         }
     }
 }

For custom items, first we'll add one in the customItemList of scatterGraph:

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

We have implemented a timer to add, remove, and rotate all the items in the graph, and we'll use the same timer for rotating the custom item:

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

Example Contents

Example project @ code.qt.io