【Qt6 QML Book 基础】12:图像查看器(附完整可运行代码)

一、运行效果图

1.1 桌面版(Fusion 风格)

  • 核心布局
    • 顶部菜单栏(File/Help)与工具栏(Open 按钮);
    • 中心区域显示图片,支持拖拽缩放与右键菜单;
    • 深灰色背景衬托图片,符合桌面应用视觉规范。

1.2 安卓版(Material 风格)

  • 移动适配
    • 左侧抽屉式导航(Drawer)替代传统菜单;
    • 顶部工具栏包含汉堡菜单按钮与标题标签;
    • 橙色主题色符合 Material Design 规范,适配触摸交互。

二、桌面版:经典桌面应用架构

2.1 ApplicationWindow 核心结构

ApplicationWindow 由四个主要区域组成,如下所示。菜单栏、工具栏和状态栏通常由 MenuBarToolBar 或 TabBar 控件的实例填充,而内容区域是窗口的子项所在的位置。请注意,图像查看器应用程序没有状态栏;这就是为什么这里的代码显示以及上图中都没有它的原因。

然后,我们通过添加 Image 元素作为内容,开始在 main.qml 中构建用户界面。当用户打开图像时,此元素将保存图像,因此目前它只是一个占位符。background 属性用于为窗口提供一个元素,以放置在内容后面。当没有加载图像时,将显示此图像,如果纵横比不允许图像填充窗口的内容区域,则会显示图像周围的边框。 

// 图片显示组件
    Image {
        id: image
        anchors.fill: parent  // 充满父窗口
        fillMode: Image.PreserveAspectFit  // 保持宽高比显示
        asynchronous: true     // 启用异步加载(避免界面冻结)
    }

2.2 界面布局:从菜单栏到内容区域

2.2.1 工具栏(ToolBar)设计

我们添加 ToolBar。这是使用窗口的 toolBar 属性完成的。在工具栏内,我们添加了一个 Flow 元素,该元素将允许内容在溢出到新行之前填充控件的宽度。在流中,我们放置了一个 ToolButton

在 ToolButton 的 onClicked 信号处理程序中是最后一段代码。它调用 fileOpenDialog 元素上的 open 方法。

 // 工具栏配置
    header: ToolBar {
        Flow {  // 流式布局容器
            anchors.fill: parent
            ToolButton {
                text: qsTr("Open")
                icon.name: "document-open"
                onClicked: window.openFileDialog()  // 点击触发文件对话框
            }
        }
    }

    2.2.2 显示图片

    onAccepted 信号处理程序,其中保存窗口内容的 Image 元素被设置为显示所选文件。还有一个 onRejected 信号,但我们不需要在图像查看器应用程序中处理它。

        // 平台文件对话框(桌面端专用)
        Platform.FileDialog {
            id: fileOpenDialog
            title: "Select an image file"  // 对话框标题
            folder: Platform.StandardPaths.writableLocation(Platform.StandardPaths.DocumentsLocation) // 默认打开文档目录
            nameFilters: [ "Image files (*.png *.jpeg *.jpg)" ]  // 文件类型过滤
            onAccepted: image.source = fileOpenDialog.file;  // 文件选择确认时加载图片
        }

    注意:

    • fileOpenDialog 元素是 Qt.labs.platform 模块中的 FileDialog 控件。
    • 文件对话框可用于打开或保存文件。我们将 Qt.labs.platform 导入为 Platform,以避免与 QtQuick.Controls 导入发生命名冲突,因此我们将其称为 Platform.FileDialog

     2.2.2 菜单栏(MenuBar)实现

    我们继续使用 MenuBar。要创建菜单,请将 Menu 元素放在菜单栏内,然后用 MenuItem 元素填充每个 Menu

    在下面的代码中,我们创建两个菜单:File 和 Help。在 File (文件) 下,我们将 Open (打开) 置于与工具栏中的工具按钮相同的图标和作。在 Help (帮助) 下,您可以找到 About (关于),这将触发对 aboutDialog 元素的 open 方法的调用。

    请注意,Menu 的 title 属性中的 & 符号 (“&”) 和 MenuItem 的 text 属性将以下字符转换为键盘快捷方式;例如,按 Alt+F,然后按 Alt+O 来触发打开的项目,从而进入文件菜单。

    // 菜单栏配置
        menuBar: MenuBar {
            // 文件菜单
            Menu {
                title: qsTr("&File")  // &表示快捷键(Alt+F)
                MenuItem {
                    text: qsTr("&Open...")
                    icon.name: "document-open"  // 使用系统图标
                    onTriggered: window.openFileDialog()  // 触发文件对话框
                }
            }
    
            // 帮助菜单
            Menu {
                title: qsTr("&Help")
                MenuItem {
                    text: qsTr("&About...")
                    onTriggered: window.openAboutDialog()  // 触发关于对话框
                }
            }
        }

    2.2.3 关于对话框

    aboutDialog 元素基于 QtQuick.Controls 模块中的 Dialog 控件,该模块是自定义对话框的基础。我们将要创建的对话框如下图所示。

    aboutDialog 的代码可以分为三个部分。首先,我们使用标题设置对话框窗口。然后,我们为对话框提供一些内容 – 在本例中为 Label 控件。最后,我们选择使用标准的 Ok 按钮来关闭对话框。

    // 关于对话框
        Dialog {
            id: aboutDialog
            title: qsTr("About")  // 国际化标题
            standardButtons: Dialog.Ok  // 仅包含确定按钮
    
            Label {
                anchors.fill: parent
                text: qsTr("QML Image Viewer\nA part of the QmlBook\nhttp://qmlbook.org")  // 多行文本
                horizontalAlignment: Text.AlignHCenter  // 水平居中
            }
        }

    三、安卓适配:移动交互范式转换 

    3.1 界面重构:从菜单到抽屉导航

    与桌面应用程序相比,用户界面在移动设备上的外观和行为存在许多差异。我们的应用程序最大的区别在于访问作的方式。我们将使用一个抽屉,而不是菜单栏和工具栏,用户可以从中选择作。抽屉可以从侧面滑入,但我们在标题中也提供了一个汉堡按钮。抽屉打开后的结果应用程序如下所示。

    // 侧滑抽屉导航
        Drawer {
            id: drawer
            width: Math.min(window.width, window.height) / 3 * 2  // 动态计算宽度(屏幕宽高的2/3)
            height: window.height
            edge: Qt.LeftEdge  // 从左侧滑出
    
            // 导航列表视图
            ListView {
                focus: true
                currentIndex: -1  // 初始无选中项
                anchors.fill: parent
    
                // 列表项代理
                delegate: ItemDelegate {
                    width: parent.width
                    text: model.text  // 显示模型文本
                    highlighted: ListView.isCurrentItem
                    onClicked: {
                        drawer.close()  // 点击后关闭抽屉
                        model.triggered()  // 执行模型定义的操作
                    }
                }
    
                // 导航数据模型
                model: ListModel {
                    ListElement {
                        text: qsTr("Open...")
                        triggered: function(){ window.openFileDialog(); }
                    }
                    ListElement {
                        text: qsTr("About...")
                        triggered: function(){ window.openAboutDialog(); }
                    }
                }
    
                ScrollIndicator.vertical: ScrollIndicator { }  // 垂直滚动条
            }
        }

    代码详解:

    1. Drawer 组件作为子组件添加到 ApplicationWindow 中。
    2. 在抽屉中,我们放置了一个包含 ItemDelegate 实例的 ListView。它还包含一个 ScrollIndicator,用于显示长列表的哪一部分。由于我们的列表仅包含两个项目,因此该指标在此示例中不可见。 
    3. 抽屉的 ListView 是从 ListModel 填充的,其中每个 ListItem 对应于一个菜单项。每次单击一个项目时,在 onClicked 方法中,都会调用相应 ListItem 的triggered方法。这样,我们就可以使用单个委托来触发不同的作。

    3.2 ToolBar改造

    这里我们不使用桌面样式的工具栏,重新添加了一个用于打开抽屉的按钮和一个用于应用程序标题的标签。

     // Material风格工具栏
        header: ToolBar {
            Material.background: Material.Orange  // 使用Material橙色主题
    
            // 菜单按钮
            ToolButton {
                id: menuButton
                anchors.left: parent.left
                anchors.verticalCenter: parent.verticalCenter
                icon.source: "images/baseline-menu-24px.svg"  // 使用矢量图标
                onClicked: drawer.open()  // 点击打开侧滑菜单
            }
    
            // 标题标签
            Label {
                id: titleLabel
                anchors.centerIn: parent
                text: "Image Viewer"
                font.pixelSize: 20  // 字号设置
                elide: Label.ElideRight  // 文本过长时右侧省略
            }
        }

     通过上面的更改,我们已将桌面图像查看器转换为适合移动设备的版本。

    四、公共代码

    查看上面实现的代码,我们可以看到桌面和移动端大部分代码是相同的。相同的部分大多与应用程序的文档(即图像)相关联,这些变化分别考虑了桌面和移动设备的不同交互模式。

    4.1 文件选择器

    对于上面说的相同的代码,我们希望创建统一的代码库。QML 通过使用文件选择器来支持这一点。

    文件选择器的工作原理是,如果存在选择器,则用替代文件替换文件。

    通过在与要替换的文件相同的目录中创建名为 +selector 的目录(其中 selector 表示选择器的名称),可以将与要替换的文件同名的文件放置在该目录中。当选择器存在时,将选取目录中的文件,而不是原始文件。

    选择器基于平台:例如 android、ios、osx、linux、qnx 等。它们还可以包括所使用的 Linux 发行版的名称(如果已标识),例如 debian、ubuntu、fedora。最后,它们还包括 locale,例如 en_US、sv_SE 等。

    4.2 ImageViewerWindow组件

    先来提取公共代码为一个组件,为此,我们创建了 ImageViewerWindow 元素,该元素将用于两个变体,而不是 ApplicationWindow。这将由对话框、Image 元素和背景组成。为了使对话框的 open 方法可用于特定于平台的代码,我们需要通过函数 openFileDialog 和 openAboutDialog 公开它们。

    // ImageViewerWindow.qml
    import QtQuick
    import QtQuick.Controls
    import Qt.labs.platform as Platform  // 使用实验室平台的组件(文件对话框)
    
    ApplicationWindow {
        // 窗口功能函数定义
        function openFileDialog() { fileOpenDialog.open(); }  // 打开文件选择对话框
        function openAboutDialog() { aboutDialog.open(); }    // 打开关于对话框
    
        visible: true  // 窗口可见性
        title: qsTr("Image Viewer")  // 国际化窗口标题
    
        // 窗口背景设置
        background: Rectangle {
            color: "darkGray"  // 深灰色背景
        }
    
        // 图片显示组件
        Image {
            id: image
            anchors.fill: parent  // 充满父窗口
            fillMode: Image.PreserveAspectFit  // 保持宽高比显示
            asynchronous: true     // 启用异步加载(避免界面冻结)
        }
    
        // 平台文件对话框(桌面端专用)
        Platform.FileDialog {
            id: fileOpenDialog
            title: "Select an image file"  // 对话框标题
            folder: Platform.StandardPaths.writableLocation(Platform.StandardPaths.DocumentsLocation) // 默认打开文档目录
            nameFilters: [ "Image files (*.png *.jpeg *.jpg)" ]  // 文件类型过滤
            onAccepted: image.source = fileOpenDialog.file;  // 文件选择确认时加载图片
        }
    
        // 关于对话框
        Dialog {
            id: aboutDialog
            title: qsTr("About")  // 国际化标题
            standardButtons: Dialog.Ok  // 仅包含确定按钮
    
            Label {
                anchors.fill: parent
                text: qsTr("QML Image Viewer\nA part of the QmlBook\nhttp://qmlbook.org")  // 多行文本
                horizontalAlignment: Text.AlignHCenter  // 水平居中
            }
        }
    }

    4.3 桌面版和安卓版适配

    我们将用户界面基于 ImageViewerWindow 而不是 ApplicationWindow。然后我们向其添加特定于平台的部分,例如 MenuBar 和 ToolBar。唯一的变化是,打开相应对话框的调用是针对新功能进行的,而不是直接针对对话框控件进行的。

    我们为默认样式 Fusion(即用户界面的桌面版本)创建一个新的 main.qml

    // ================================================
    // desktop (win) main.qml - 桌面端主界面
    // ================================================
    import QtQuick
    import QtQuick.Controls
    
    ImageViewerWindow {
        id: window
        width: 640  // 初始宽度
        height: 480 // 初始高度
    
        // 菜单栏配置
        menuBar: MenuBar {
            // 文件菜单
            Menu {
                title: qsTr("&File")  // &表示快捷键(Alt+F)
                MenuItem {
                    text: qsTr("&Open...")
                    icon.name: "document-open"  // 使用系统图标
                    onTriggered: window.openFileDialog()  // 触发文件对话框
                }
            }
    
            // 帮助菜单
            Menu {
                title: qsTr("&Help")
                MenuItem {
                    text: qsTr("&About...")
                    onTriggered: window.openAboutDialog()  // 触发关于对话框
                }
            }
        }
    
        // 工具栏配置
        header: ToolBar {
            Flow {  // 流式布局容器
                anchors.fill: parent
                ToolButton {
                    text: qsTr("Open")
                    icon.name: "document-open"
                    onClicked: window.openFileDialog()  // 点击触发文件对话框
                }
            }
        }
    }
    

    接下来,我们必须创建一个特定于移动设备的 main.qml。这将基于 Material 主题。在这里,我们保留了 Drawer 和特定于移动设备的工具栏。同样,唯一的变化是对话框的打开方式。

    // ================================================
    // android main.qml - 移动端主界面(Material设计)
    // ================================================
    import QtQuick
    import QtQuick.Controls
    import QtQuick.Controls.Material 2.1  // Material设计库
    
    ImageViewerWindow {
        id: window
        width: 360   // 移动端适配宽度
        height: 520  // 移动端适配高度
    
        // 侧滑抽屉导航
        Drawer {
            id: drawer
            width: Math.min(window.width, window.height) / 3 * 2  // 动态计算宽度(屏幕宽高的2/3)
            height: window.height
            edge: Qt.LeftEdge  // 从左侧滑出
    
            // 导航列表视图
            ListView {
                focus: true
                currentIndex: -1  // 初始无选中项
                anchors.fill: parent
    
                // 列表项代理
                delegate: ItemDelegate {
                    width: parent.width
                    text: model.text  // 显示模型文本
                    highlighted: ListView.isCurrentItem
                    onClicked: {
                        drawer.close()  // 点击后关闭抽屉
                        model.triggered()  // 执行模型定义的操作
                    }
                }
    
                // 导航数据模型
                model: ListModel {
                    ListElement {
                        text: qsTr("Open...")
                        triggered: function(){ window.openFileDialog(); }
                    }
                    ListElement {
                        text: qsTr("About...")
                        triggered: function(){ window.openAboutDialog(); }
                    }
                }
    
                ScrollIndicator.vertical: ScrollIndicator { }  // 垂直滚动条
            }
        }
    
        // Material风格工具栏
        header: ToolBar {
            Material.background: Material.Orange  // 使用Material橙色主题
    
            // 菜单按钮
            ToolButton {
                id: menuButton
                anchors.left: parent.left
                anchors.verticalCenter: parent.verticalCenter
                icon.source: "images/baseline-menu-24px.svg"  // 使用矢量图标
                onClicked: drawer.open()  // 点击打开侧滑菜单
            }
    
            // 标题标签
            Label {
                id: titleLabel
                anchors.centerIn: parent
                text: "Image Viewer"
                font.pixelSize: 20  // 字号设置
                elide: Label.ElideRight  // 文本过长时右侧省略
            }
        }
    }
    

     总结

    通过本文的介绍,我们学习了如何使用 Qt6 和 QML 创建一个简单的图像查看器,并适配桌面和安卓平台。我们了解了ApplicationWindowToolBarMenuBarDialog等常用 UI 组件的使用方法,以及如何使用文件选择器和原生对话框来提高应用程序的兼容性和用户体验。希望本文能对你的 Qt 开发之旅有所帮助。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包

    打赏作者

    binary0010

    你的鼓励将是我创作的最大动力

    ¥1 ¥2 ¥4 ¥6 ¥10 ¥20
    扫码支付:¥1
    获取中
    扫码支付

    您的余额不足,请更换扫码支付或充值

    打赏作者

    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值