- Qt 5.5
- Qt Quick
- Qt Quick Demo - Photo Surface
- Qt 5.5.1 Reference Documentation

# Qt Quick Demo - Photo Surface

Photo Surface demonstrates how to use a Repeater with a FolderListModel and a FileDialog to access images from a folder selected by a user and how to handle dragging, rotation and pinch zooming within the same item using a PinchArea that contains a MouseArea.

All the app code is contained in one QML file, photosurface.qml. Inline JavaScript code is used to place, rotate, and scale images on the photo surface.

## 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.

## Creating the Main Window

To create the main window for the Photo Surface app, we use the Window QML type as the root item. It automatically sets up the window for use with Qt Quick graphical types:

    Window {
        id: root
        visible: true
        width: 1024; height: 600
        color: "black"
        property int highestZ: 0
        property real defaultSize: 200
        property var currentFrame: undefined

To use the Window type, we must import it:

    import QtQuick.Window 2.1

## Accessing Folder Contents

We use a Repeater QML type together with the FolderListModel to display GIF, JPG, and PNG images located in a folder:

            Repeater {
                model: FolderListModel {
                    id: folderModel
                    objectName: "folderModel"
                    showDirs: false
                    nameFilters: ["*.png", "*.jpg", "*.gif"]
                }

To use the FolderListModel type, we must import it:

    import Qt.labs.folderlistmodel 1.0

We use a FileDialog to enable users to select the folder that contains the images:

        FileDialog {
            id: fileDialog
            title: "Choose a folder with some images"
            selectFolder: true
            onAccepted: folderModel.folder = fileUrl + "/"
        }

To use the FileDialog type, we must import Qt Quick Dialogs:

    import QtQuick.Dialogs 1.0

We use the `fileDialog.open()` function to open the file dialog when the app starts:

    Component.onCompleted: fileDialog.open()

Users can also click the file dialog icon to open the file dialog. We use an Image QML type to display the icon. Inside the Image type, we use a MouseArea with the `onClicked` signal handler to call the `fileDialog.open()` function:

        Image {
            anchors.top: parent.top
            anchors.left: parent.left
            anchors.margins: 10
            source: "resources/folder.png"
            MouseArea {
                anchors.fill: parent
                anchors.margins: -10
                onClicked: fileDialog.open()
            }
        }

## Displaying Images on the Photo Surface

We use a Rectangle as a delegate for a Repeater to provide a frame for each image that the FolderListModel finds in the selected folder. We use JavaScript `Math()` methods to place the frames randomly on the photo surface and to rotate them at random angles, as well as to scale the images:

                Rectangle {
                    id: photoFrame
                    width: image.width * (1 + 0.10 * image.height / image.width)
                    height: image.height * 1.10
                    scale: defaultSize / Math.max(image.sourceSize.width, image.sourceSize.height)
                    Behavior on scale { NumberAnimation { duration: 200 } }

## Handling Pinch Gestures

We use a PinchArea that contains a MouseArea in the photo frames to handle dragging, rotation and pinch zooming of the frame:

                    Behavior on x { NumberAnimation { duration: 200 } }
                    Behavior on y { NumberAnimation { duration: 200 } }
                    border.color: "black"
                    border.width: 2
                    smooth: true
                    antialiasing: true
                    Component.onCompleted: {
                        x = Math.random() * root.width - width / 2
                        y = Math.random() * root.height - height / 2
                        rotation = Math.random() * 13 - 6
                    }
                    Image {
                        id: image
                        anchors.centerIn: parent
                        fillMode: Image.PreserveAspectFit
                        source: folderModel.folder + fileName
                        antialiasing: true
                    }
                    PinchArea {
                        anchors.fill: parent
                        pinch.target: photoFrame
                        pinch.minimumRotation: -360
                        pinch.maximumRotation: 360
                        pinch.minimumScale: 0.1
                        pinch.maximumScale: 10
                        pinch.dragAxis: Pinch.XAndYAxis
                        onPinchStarted: setFrameColor();

We use the `pinch` group property to control how the photo frames react to pinch gestures. The `pinch.target` sets `photoFrame` as the item to manipulate. The rotation properties specify that the frames can be rotated at all angles and the scale properties specify that they can be scaled between `0.1` and `10`.

In the MouseArea's `onPressed` signal handler, we raise the selected photo frame to the top by increasing the value of its `z` property. The root item stores the z value of the top-most frame. The border color of the photo frame is controlled in the `onEntered` signal handler to highlight the selected image:

                        property real zRestore: 0
                        onSmartZoom: {
                            if (pinch.scale > 0) {
                                photoFrame.rotation = 0;
                                photoFrame.scale = Math.min(root.width, root.height) / Math.max(image.sourceSize.width, image.sourceSize.height) * 0.85
                                photoFrame.x = flick.contentX + (flick.width - photoFrame.width) / 2
                                photoFrame.y = flick.contentY + (flick.height - photoFrame.height) / 2
                                zRestore = photoFrame.z
                                photoFrame.z = ++root.highestZ;
                            } else {
                                photoFrame.rotation = pinch.previousAngle
                                photoFrame.scale = pinch.previousScale
                                photoFrame.x = pinch.previousCenter.x - photoFrame.width / 2
                                photoFrame.y = pinch.previousCenter.y - photoFrame.height / 2
                                photoFrame.z = zRestore
                                --root.highestZ
                            }
                        }

                        MouseArea {
                            id: dragArea
                            hoverEnabled: true
                            anchors.fill: parent
                            drag.target: photoFrame
                            scrollGestureEnabled: false  // 2-finger-flick gesture should pass through to the Flickable
                            onPressed: {
                                photoFrame.z = ++root.highestZ;
                                parent.setFrameColor();
                            }
                            onEntered: parent.setFrameColor();

To enable you to test the example on the desktop, we use the MouseArea's `onWheel` signal handler to simulate pinch gestures by using a mouse:

                            onWheel: {
                                if (wheel.modifiers & Qt.ControlModifier) {
                                    photoFrame.rotation += wheel.angleDelta.y / 120 * 5;
                                    if (Math.abs(photoFrame.rotation) < 4)
                                        photoFrame.rotation = 0;
                                } else {
                                    photoFrame.rotation += wheel.angleDelta.x / 120;
                                    if (Math.abs(photoFrame.rotation) < 0.6)
                                        photoFrame.rotation = 0;
                                    var scaleBefore = photoFrame.scale;
                                    photoFrame.scale += photoFrame.scale * wheel.angleDelta.y / 120 / 10;
                                }
                            }
                        }
                        function setFrameColor() {
                            if (currentFrame)
                                currentFrame.border.color = "black";
                            currentFrame = photoFrame;
                            currentFrame.border.color = "red";
                        }
                    }
                }
            }
        }

        Rectangle {
            id: verticalScrollDecorator
            anchors.right: parent.right
            anchors.margins: 2
            color: "cyan"
            border.color: "black"
            border.width: 1
            width: 5
            radius: 2
            antialiasing: true
            height: flick.height * (flick.height / flick.contentHeight) - (width - anchors.margins) * 2
            y:  flick.contentY * (flick.height / flick.contentHeight)
            NumberAnimation on opacity { id: vfade; to: 0; duration: 500 }
            onYChanged: { opacity = 1.0; fadeTimer.restart() }
        }

        Rectangle {
            id: horizontalScrollDecorator
            anchors.bottom: parent.bottom
            anchors.margins: 2
            color: "cyan"
            border.color: "black"
            border.width: 1
            height: 5
            radius: 2
            antialiasing: true
            width: flick.width * (flick.width / flick.contentWidth) - (height - anchors.margins) * 2
            x:  flick.contentX * (flick.width / flick.contentWidth)
            NumberAnimation on opacity { id: hfade; to: 0; duration: 500 }
            onXChanged: { opacity = 1.0; fadeTimer.restart() }
        }

        Timer { id: fadeTimer; interval: 1000; onTriggered: { hfade.start(); vfade.start() } }

        Image {
            anchors.top: parent.top
            anchors.left: parent.left
            anchors.margins: 10
            source: "resources/folder.png"
            MouseArea {
                anchors.fill: parent
                anchors.margins: -10
                onClicked: fileDialog.open()
            }
        }

        Text {
            anchors.bottom: parent.bottom
            anchors.left: parent.left
            anchors.right: parent.right
            anchors.margins: 10
            color: "darkgrey"
            wrapMode: Text.WordWrap
            font.pointSize: 8
            text: "On a touchscreen: use two fingers to zoom and rotate, one finger to drag\n" +
                  "With a mouse: drag normally, use the vertical wheel to zoom, horizontal wheel to rotate, or hold Ctrl while using the vertical wheel to rotate"
        }

        Component.onCompleted: fileDialog.open()
    }

The `onWheel` signal handler is called in response to mouse wheel gestures. Use the vertical wheel to zoom and Ctrl and the vertical wheel to rotate frames. If the mouse has a horizontal wheel, use it to rotate frames.

Files:

- demos/photosurface/photosurface.qml
- demos/photosurface/main.cpp
- demos/photosurface/photosurface.pro
- demos/photosurface/photosurface.qmlproject
- demos/photosurface/photosurface.qrc

See also QML Applications.