【Qt6 QML Book 基础】15:常规模式 3 - 文档窗口(附完整可运行代码)

引言

在桌面应用开发中,文档窗口模式是处理多文档场景的经典交互范式。用户通过独立窗口管理不同文档,支持新建、打开、保存等操作,并在关闭时自动提示未保存修改。Qt Quick 提供的 ApplicationWindow 与原生对话框组件,为实现这一模式提供了完整的解决方案。本文将结合实战代码,详解如何构建支持多文档管理的桌面应用界面,涵盖窗口创建、菜单交互、消息处理等核心功能。

一、运行效果图

1.1 文档窗口界面

  • 核心布局
    • 每个文档独立为一个窗口,标题栏显示文件名及修改状态(* 表示未保存);
    • 顶部菜单栏包含 “文件” 菜单,提供新建、打开、保存等标准操作;
    • 关闭未保存文档时弹出警告对话框,支持保存、放弃或取消操作。

1.2 关键交互场景

  1. 新建文档:点击 “新建” 创建空白窗口,标题为 “文档”;
  2. 打开文件:通过文件对话框选择文件,动态创建窗口并显示文件名;
  3. 关闭提示:未保存修改时弹出对话框,引导用户选择保存、放弃或取消关闭。

二、文档窗口:多实例管理核心

2.1 交互流程

在后面的内容中我们将说明如何实现面向桌面、以文档为中心的用户界面。这个想法是每个文档都有一个窗口。打开新文档时,将打开一个新窗口。对于用户来说,每个窗口都是一个包含单个文档的自包含世界。

程序的代码从 ApplicationWindow 开始,其中包含 File 菜单,其中包含标准作:新建、打开、保存和 另存

2.2 文档窗口组件:ApplicationWindow 扩展

// DocumentWindow.qml(核心窗口组件)
import QtQuick.Controls
import Qt.labs.platform as NativeDialogs

// 主应用窗口定义
ApplicationWindow {
    id: root
    width: 640
    height: 480
    visible: true
    // 动态窗口标题(显示文件名及修改状态)
    title: (fileName.length === 0 ? qsTr("文档") : fileName) + (isDirty?"*":"") 

    // 标记文档是否有未保存的修改
    property bool isDirty: true
    // 当前文档文件名
    property string fileName
    // 处理窗口关闭时的状态标记
    property bool tryingToClose: false

}
  • 核心属性
    • isDirty:控制标题栏是否显示 *,标记文档修改状态;
    • fileName:记录文档路径,无路径时显示 “文档”。

2.3 菜单栏:标准文件操作入口

menuBar: MenuBar {
    Menu {
        title: qsTr("文件(&F)")  // Alt+F 打开菜单

        // 新建文档(Ctrl+N)
        MenuItem {
            text: qsTr("新建(&N)"); icon.name: "document-new"
            shortcut: Qt.createShortcut("Ctrl+N")
            onTriggered: newDocument()
        }
        MenuSeparator {}

        // 打开文档(Ctrl+O)
        MenuItem {
            text: qsTr("打开(&O)"); icon.name: "document-open"
            shortcut: Qt.createShortcut("Ctrl+O")
            onTriggered: openDocument()
        }

        // 保存文档(Ctrl+S)
        MenuItem {
            text: qsTr("保存(&S)"); icon.name: "document-save"
            shortcut: Qt.createShortcut("Ctrl+S")
            onTriggered: saveDocument()
        }

        // 另存为
        MenuItem {
            text: qsTr("另存(&As)..."); icon.name: "document-save-as"
            onTriggered: saveAsDocument()
        }
    }
}
  • 设计规范
    • 图标使用 Freedesktop 标准名称(如 document-new),自动适配系统主题;
    • 快捷键通过 Qt.createShortcut 统一管理,提升操作效率。

三、消息处理:文档生命周期管理

3.1 新建与打开:动态窗口创建

3.1.1 新建空白文档

// 创建新文档窗口
    function createNewDocument ()
    {
        var component = Qt.createComponent("DocumentWindow.qml");
        var window = component.createObject();
        return window;
    }

    function newDocument ()
    {
        // 显示新窗口(初始fileName为空,isDirty为true)
        var window = createNewDocument();
        window.show();
    }

请注意,在使用 createObject 创建新实例时,我们不提供父元素。这样,我们就可以创建新的顶级元素。如果我们已将当前文档作为下一个文档的父级提供,则父窗口的销毁将导致子窗口的销毁。 

3.1.2 打开现有文件

// 文件打开对话框组件
    NativeDialogs.FileDialog {
        id: openDialog
        title: qsTr("打开")
        folder: NativeDialogs.StandardPaths.writableLocation(NativeDialogs.StandardPaths.DocumentsLocation)
        // 用户选择文件后创建窗口
        onAccepted: {
            var window = root.createNewDocument();
            // 绑定文件名
            window.fileName = openDialog.file;
            // 新窗口标记为已保存(实际项目需加载文件内容)
            window.isDirty = false
            window.show();
        }
    }
  • 关键点
    • 使用 Qt.createComponent 动态创建窗口实例,避免父子窗口依赖;
    • StandardPaths.documentsLocation() 自动定位系统文档目录,提升跨平台兼容性。

3.1.3 保存与另存为:状态同步逻辑

尝试保存没有名称的文档时,将调用 saveAsDocument。这会导致在 saveAsDialog 上往返,该对话框会设置文件名,然后尝试在 onAccepted 处理程序中再次保存。

保存文档后,在 saveDocument 函数中,选中 tryingToClose 属性。如果保存是用户希望在窗口关闭时保存文档的结果,则设置此标志。因此,在执行 save作后,窗口将关闭。同样,此示例中没有进行实际的保存。

    // 文件另存为对话框组件
    function saveAsDocument ()
    {
        saveAsDialog.open();
    }

    // 处理文档保存核心逻辑
    function saveDocument ()
    {
        // 无文件名时触发另存为
        if (fileName.length === 0)
        {
            root.saveAsDocument();
        }
        else
        {
            // 实际项目中此处应实现文件写入逻辑
            console.log("saving document");
            // 清除未保存标记
            root.isDirty = false;
            // 若因关闭触发保存,保存后关闭窗口
            if (root.tryingToClose)
            {
                root.close();
            }
        }
    }

    // 文件另存为对话框组件
    NativeDialogs.FileDialog {
        id: saveAsDialog
        title: qsTr("另存为")
        folder: NativeDialogs.StandardPaths.writableLocation(NativeDialogs.StandardPaths.DocumentsLocation)
        onAccepted: {
             // 更新文件路径
            root.fileName = saveAsDialog.file;
             // 调用保存逻辑
            saveDocument();
        }
        onRejected: {
            root.tryingToClose = false;
        }
    }

注意:

  • 我们将 Qt.labs.platform 模块作为 NativeDialogs 导入。这是因为它提供了一个 MenuItem 与 QtQuick.Controls 模块提供的 MenuItem 冲突。 

3.3 关闭窗口:未保存修改处理

  1. 关闭窗口时,将调用 onClosing 处理程序。在这里,代码可以选择不接受关闭请求。如果文档有未保存的更改,我们将打开 closeWarningDialog 并拒绝 close 请求。
  2. closeWarningDialog 询问用户是否应保存更改,但用户也可以选择取消关闭作。在 onRejected 中处理的取消是最简单的情况,因为我们在对话框打开时拒绝了关闭。
  3. 用户不想保存更改时,即在 onNoClicked 中,isDirty 标志设置为 false,窗口再次关闭。这一次,onClosing 将接受闭包,因为 isDirty 为 false。
  4. 最后,当用户想要保存更改时,我们在调用 save 之前将 tryingToClose 标志设置为 true。这让我们进入了 save/save as 逻辑。

// 窗口关闭事件处理
    onClosing: function(close) {
        if (root.isDirty) {
            // 弹出未保存修改提示
            closeWarningDialog.open()
            close.accepted = false
        }
    }

    // 关闭确认对话框事件处理
    NativeDialogs.MessageDialog {
        onYesClicked: {
            // 保存后关闭窗口流程
            root.tryingToClose = true
            root.saveDocument()
        }
        onNoClicked: {
            // 放弃修改直接关闭
            root.isDirty = false
            root.close()
        }
    }

// 关闭确认对话框组件
    NativeDialogs.MessageDialog {
        id: closeWarningDialog
        title: qsTr("关闭文档")
        text: qsTr("有修改未保存,是否需要保存修改")
        buttons: NativeDialogs.MessageDialog.Yes | NativeDialogs.MessageDialog.No | NativeDialogs.MessageDialog.Cancel
        onYesClicked: {
            root.tryingToClose = true;
            root.saveDocument();
        }
        onNoClicked: {
            root.isDirty = false;
            root.close();
        }

        onRejected:  {
            //nothing to do
        }
    }
  • 状态机核心
    • 通过 tryingToClose 标记区分主动保存与关闭流程中的保存,避免逻辑混乱;
    • 对话框非阻塞特性要求通过状态标记(如 isDirty)延续操作上下文。

四、总结

核心技术点

  1. 多窗口管理
    • 动态创建 ApplicationWindow 实例,每个文档独立为一个窗口;
    • 通过 Qt.createComponent 实现窗口实例的动态加载与销毁。
  2. 状态驱动界面
    • isDirty 控制标题栏修改标记,fileName 动态更新窗口标题;
    • 利用 onClosing 事件拦截关闭操作,实现未保存修改提示。
  3. 原生对话框集成
    • Qt.labs.platform 提供跨平台文件对话框与消息对话框,适配不同系统风格;
    • 非阻塞对话框通过回调函数与状态标记实现流程控制。

扩展方向

  • 文档内容管理:添加文本编辑组件(如 TextEdit),实现文档内容的实际加载与保存;
  • 窗口列表管理:引入 TabWindow 或 DockWindow 组件,支持多文档标签页或停靠布局;
  • 国际化与主题:通过 qsTr 实现多语言支持,结合 QtQuick.Controls.Material 实现主题切换。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

binary0010

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

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

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

打赏作者

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

抵扣说明:

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

余额充值