本页内容

Photo Surface

这是一个面向触控设备的 QML 应用程序,它使用带FolderListModel 的 Repeater 来访问文件夹中的内容,并使用PinchHandler 来处理对已获取内容的双指缩放手势。

一组照片

“照片界面”演示了如何将Repeater 与FolderListModel 及FolderDialog 结合使用,以访问用户选定文件夹中的图片。该示例还展示了如何利用Qt Quick 输入处理程序,在同一项目内处理拖动、旋转和捏合缩放操作。

所有应用程序代码都包含在一个 QML 文件中:photosurface.qml 。内联 JavaScript 代码负责在照片表面上放置、旋转和缩放图像。

运行示例

您可以通过以下方式运行该示例:

创建主窗口

要创建“Photo Surface”应用的主窗口,请将Window QML类型用作根项。它会自动配置窗口以支持 Qt Quick 图形化类型:

Window {
    id: root
    visible: true
    width: 1024; height: 600
    color: "black"
    title: Application.displayName + " : " + folderModel.folder
    property real defaultSize: 200
    property real surfaceViewportRatio: 1.5
    property var imageNameFilters: ["*.png", "*.jpg", "*.gif"] // overridden in main.cpp

访问文件夹内容

将Repeater QML类型与FolderListModel 结合使用,以显示文件夹中的GIF、JPG和PNG图像(尽管main.cpp可能会扩展支持的图像类型列表):

        Repeater {
            model: FolderListModel {
                id: folderModel
                objectName: "folderModel"
                showDirs: false
                nameFilters: root.imageNameFilters
            }

导入FolderListModel 类型:

import Qt.labs.folderlistmodel

FolderDialog 允许用户选择包含图像的文件夹:

    FolderDialog {
        id: folderDialog
        title: qsTr("Choose a folder with some images")
        onAccepted: folderModel.folder = selectedFolder
    }

要使用 FolderDialog 类型,请添加以下导入语句:

import QtQuick.Dialogs

folderDialog.open() 函数会在初始幻灯片放映结束后打开文件对话框,除非已通过命令行参数指定了文件夹路径:

        Component.onDestruction: {
            folderIcon.visible = true
            const lastArg = Application.arguments.slice(-1)[0]
            const standardPicturesLocations = StandardPaths.standardLocations(StandardPaths.PicturesLocation)
            const hasValidPicturesLocation = standardPicturesLocations.length > 0
            if (hasValidPicturesLocation)
                folderDialog.currentFolder = standardPicturesLocations[0]
            if (/.*hotosurface.*|--+/.test(lastArg)) {
                if (hasValidPicturesLocation)
                    folderModel.folder = standardPicturesLocations[0]
                else
                    folderDialog.open()
            }

用户还可以点击文件夹对话框图标来打开它。在此,一个ImageQML 类型负责显示该图标。在Image类型内部,一个带有onTapped 信号处理程序的TapHandler 会调用folderDialog.open() 函数:

    Image {
        id: folderIcon
        visible: false
        anchors.top: parent.top
        anchors.left: parent.left
        anchors.margins: 10
        source: "resources/folder.png"

        TapHandler { onTapped: folderDialog.open() }

        HoverHandler { id: folderMouse }

        ToolTip.visible: folderMouse.hovered
        ToolTip.text: qsTr(`Open an image directory (${openShortcut.nativeText})`)
        ToolTip.delay: 1000

        Shortcut {
            id: openShortcut
            sequence: StandardKey.Open
            onActivated: folderDialog.open()
        }
    }

在“照片”表面上显示图像

使用Rectangle 作为Repeater 的委托,为FolderListModel 在所选文件夹中找到的每张图像提供一个画布。JavaScript的Math() 方法会将画布随机放置在照片表面上,以随机角度旋转它们,并对图像进行缩放。边框颜色表示交互状态:

            delegate: Rectangle {
                id: photoFrame
                required property date fileModified
                required property string fileName
                required property url fileUrl
                objectName: "frame-" + fileName
                width: image.width * (1 + 0.10 * image.height / image.width)
                height: image.height * 1.10
                scale: root.defaultSize / Math.max(image.sourceSize.width, image.sourceSize.height)
                border.color: pinchHandler.active || dragHandler.active ? "darkturquoise"
                                                                        : mouse.hovered ? "darkseagreen"
                                                                                        : "saddlebrown"
                border.width: 3 / scale
                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: photoFrame.fileUrl
                    antialiasing: true
                }

处理拖拽、捏合手势及鼠标操作

每个照片画框中的DragHandler 和PinchHandler 负责处理拖动、捏合缩放和旋转操作:

                PinchHandler {
                    id: pinchHandler
                    minimumRotation: -360
                    maximumRotation: 360
                    minimumScale: 0.1
                    maximumScale: 10
                    grabPermissions: PointerHandler.CanTakeOverFromAnything // and never gonna give it up
                    onActiveChanged: if (active) photoFrame.z = ++flick.highestZ
                }

                DragHandler {
                    id: dragHandler
                    onActiveChanged: {
                        if (active)
                            photoFrame.z = ++flick.highestZ
                        else
                            anim.restart(centroid.velocity)
                    }
                }

由于PinchHandler 是在Rectangle内部声明的,因此PinchHandler.target 属性会被隐式设置,使得捏合手势能够操作该Rectangle。旋转属性指定相框可旋转至任意角度,缩放属性则指定其缩放比例可在0.1 与10 之间变化。该捏合手势在触摸屏或多点触控触控板上均可正常工作。 变换画框会同时变换其内容(即Image)。

DragHandler.target 属性同样被隐式设置,因此您可以在触摸屏或触控板上用单指拖动照片,也可以使用鼠标进行拖动。在DragHandler 的onActiveChanged 信号处理程序中,选中的照片框架通过增加其z 属性的值来升至最前(同时,共享的highestZ 属性保存了迄今为止使用过的最大z 值)。 当拖动结束时,会开始一个动画效果,使其沿原方向继续移动片刻,随后逐渐减速并最终停止。如果你将照片“甩”到界面的边缘之外,界面会自动扩展以适应其新位置。 你可以通过包含 Repeater 及其填充的所有照片画框的ScrollView 在界面中移动,以查看界面的不同部分。

由于你可以用两根手指通过 DragHandlers 拖动两张照片,也可以用两根手指对单个PinchHandler 进行捏合操作,而且照片集合往往会堆叠在一起,因此你需要调整grabPermissions ,以确保PinchHandler 具有优先级:当捏合手势开始时,它不允许 DragHandlers 再次接管对触摸点的抓取。

为了使该示例在没有触控设备的计算机上更具交互性,请添加上述 border.color 所依赖的HoverHandler ,以及两个WheelHandlers 。 其中一个控件允许您按住 Ctrl 键并使用鼠标滚轮围绕鼠标光标旋转照片;另一个控件则允许您按住 Shift 键并使用鼠标滚轮对光标所在位置进行放大或缩小。这两个控件均会像上文的 `DragHandler ` 一样将照片抬高:

                HoverHandler { id: mouse }

                WheelHandler {
                    acceptedModifiers: Qt.ControlModifier
                    property: "rotation"
                    onActiveChanged: if (active) photoFrame.z = ++flick.highestZ
                }

                WheelHandler {
                    acceptedModifiers: Qt.ShiftModifier
                    property: "scale"
                    onActiveChanged: if (active) photoFrame.z = ++flick.highestZ
                }

源文件

示例项目 @ code.qt.io

另请参阅 《QML 应用程序》以及《Qt Quick 示例与教程》。

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