照片表面

这是一款用于触摸设备的 QML 应用程序,它使用带有FolderListModel 的 Repeater 来访问文件夹中的内容,并使用PinchHandler 来处理获取内容上的捏合手势。

Photo Surface演示了如何使用带有FolderListModelFolderDialogRepeater 访问用户所选文件夹中的图片,以及如何使用Qt Quick 输入处理程序在同一项目中处理拖动、旋转和捏合缩放。

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

运行示例

要从 Qt Creator,打开Welcome 模式,然后从Examples 中选择示例。更多信息,请参阅Qt Creator: 教程:构建并运行

创建主窗口

要创建 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 类型来显示图标。在图像类型中,我们使用TapHandleronTapped 信号处理器来调用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 在选定文件夹中找到的每张图片提供一个框架。我们使用 JavaScriptMath() 方法在照片表面随机放置边框,并以随机角度旋转边框和缩放图片。边框颜色表示交互状态:

            delegate: Rectangle {
                required property date fileModified
                required property string fileName
                required property url fileUrl
                id: photoFrame
                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
                }

处理拖动、捏合手势和鼠标

我们在每个相框中使用DragHandlerPinchHandler 来处理拖动、缩放和旋转:

                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 是在矩形内部声明的,因此PinchHandler.target 属性是隐式设置的,因此捏合手势可操作矩形。旋转属性指定相框可以旋转到所有角度,缩放属性指定相框可以在0.110 之间缩放。捏合手势在触摸屏或多点触控触摸板上同样有效。变换框架可变换其内容(图像)。

DragHandler.target 属性也是隐式设置,因此您可以用一根手指在触摸屏或触摸板上拖动照片,也可以用鼠标拖动照片。在DragHandleronActiveChanged 信号处理程序中,我们通过增加z 属性的值,将选中的相框提升到顶部(而共享的highestZ 属性则保存迄今为止使用过的最大z 值)。拖动结束后,我们会开始播放一段动画,让相框沿同一方向移动一会儿,然后放慢速度并停止。如果将照片 "甩 "过曲面的边缘,曲面就会展开以容纳新的位置。您可以通过包含中继器及其填充的所有相框的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 应用程序

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