目录

  1. 项目概述
  2. 技术栈选择
  3. 架构设计
  4. 核心功能实现
  5. 数据持久化方案
  6. UI设计与实现
  7. 开发过程中遇到的问题与解决方案
  8. 性能优化
  9. 项目构建与部署
  10. 总结与展望

项目概述

在这里插入图片描述
项目地址:https://gitcode.com/szkygc/HarmonyOs_PC-PGC/tree/main/memorandum

项目背景

随着移动应用的快速发展,备忘录应用作为日常工具类应用的重要组成部分,在用户生活中扮演着越来越重要的角色。本项目旨在开发一款基于Qt/QML框架的备忘录应用,运行于HarmonyOS平台,提供简洁、高效、美观的备忘录管理功能。

项目目标

本项目的主要目标包括:

  1. 功能完整性:实现备忘录的创建、查看、编辑、删除等基本CRUD操作
  2. 数据持久化:确保备忘录数据能够可靠地保存和恢复
  3. 用户体验:提供流畅、直观的用户界面,符合现代移动应用的设计规范
  4. 平台兼容性:适配HarmonyOS平台的特殊要求,确保应用稳定运行
  5. 代码质量:保持代码结构清晰,便于维护和扩展

功能特性

  • 创建备忘录:支持标题和内容的输入
  • 查看备忘录列表:按更新时间倒序显示所有备忘录
  • 编辑备忘录:点击列表项进入编辑页面进行修改
  • 删除备忘录:支持删除操作,带确认对话框防止误操作
  • 数据持久化:使用JSON文件存储,确保数据不丢失
  • 美观的UI:Material Design风格,现代化界面设计
  • 错误提示:完善的错误处理和用户提示机制

技术栈选择

前端框架:Qt/QML

选择理由:

  1. 跨平台能力:Qt/QML提供了强大的跨平台支持,可以在HarmonyOS、Android、iOS等多个平台上运行
  2. 声明式UI:QML的声明式语法使得UI开发更加直观和高效
  3. 性能优势:Qt的渲染引擎基于OpenGL ES,性能优异
  4. 丰富的组件库:Qt Quick Controls提供了大量现成的UI组件
  5. C++集成:可以方便地与C++代码集成,实现复杂业务逻辑

后端语言:C++

选择理由:

  1. 性能:C++执行效率高,适合处理数据持久化等性能敏感操作
  2. Qt生态:Qt框架本身就是基于C++的,集成自然
  3. 数据操作:使用Qt的JSON和文件操作API,代码简洁可靠

数据存储:JSON文件

选择理由:

  1. 简单可靠:JSON格式易于理解和调试
  2. 跨平台兼容:不依赖特定数据库驱动
  3. 轻量级:对于小型应用,文件存储足够高效
  4. 易于备份:文件可以直接复制备份

UI框架:Qt Quick Controls 2 + Material主题

选择理由:

  1. Material Design:符合现代移动应用设计规范
  2. 组件丰富:提供了Button、TextField、Dialog等常用组件
  3. 主题支持:Material主题提供了统一的视觉风格
  4. 响应式布局:支持不同屏幕尺寸的自适应

架构设计

整体架构

本项目采用经典的MVC(Model-View-Controller)架构模式,结合Qt/QML的特性,实现了清晰的代码分层:

┌─────────────────────────────────────┐
│          Presentation Layer         │
│         (QML UI Components)         │
│  - ListView (备忘录列表)            │
│  - StackView (页面导航)             │
│  - Dialog (对话框)                  │
└──────────────┬──────────────────────┘
               │
               │ QML Context Properties
               │ Signals & Slots
               │
┌──────────────▼──────────────────────┐
│         Business Logic Layer         │
│         (C++ MemoStorage)            │
│  - loadMemos()                       │
│  - saveMemo()                        │
│  - deleteMemo()                      │
│  - getNextId()                       │
└──────────────┬──────────────────────┘
               │
               │ File I/O
               │ JSON Operations
               │
┌──────────────▼──────────────────────┐
│          Data Layer                  │
│      (JSON File Storage)             │
│  - memos.json                        │
│  - File System                       │
└──────────────────────────────────────┘

模块划分

1. 表示层(Presentation Layer)

职责:

  • 用户界面展示
  • 用户交互处理
  • 数据绑定和显示

主要组件:

  • main.qml:应用主界面,包含列表页和编辑页
  • ListView:备忘录列表展示
  • StackView:页面导航管理
  • Dialog:删除确认对话框
2. 业务逻辑层(Business Logic Layer)

职责:

  • 数据业务逻辑处理
  • 数据验证
  • 与数据层交互

主要类:

  • MemoStorage:备忘录存储管理类

核心方法:

class MemoStorage : public QObject
{
    Q_OBJECT
  
public:
    // 加载所有备忘录
    Q_INVOKABLE QVariantList loadMemos();
  
    // 保存备忘录(新建或更新)
    Q_INVOKABLE bool saveMemo(int id, const QString &title, const QString &content);
  
    // 删除备忘录
    Q_INVOKABLE bool deleteMemo(int id);
  
    // 获取下一个可用ID
    Q_INVOKABLE int getNextId();
};
3. 数据层(Data Layer)

职责:

  • 数据持久化存储
  • 文件读写操作
  • JSON数据序列化/反序列化

存储格式:

[
  {
    "id": 1,
    "title": "备忘录标题",
    "content": "备忘录内容",
    "createdAt": "2025-11-12T09:00:00",
    "updatedAt": "2025-11-12T09:30:00"
  }
]

数据流设计

读取流程
用户打开应用
    ↓
QML调用 memoStorage.loadMemos()
    ↓
MemoStorage::loadJsonArray() 读取JSON文件
    ↓
解析JSON数组,转换为QVariantList
    ↓
按更新时间倒序排序
    ↓
返回给QML
    ↓
ListView显示数据
保存流程
用户在编辑页点击"保存"
    ↓
QML验证标题不为空
    ↓
调用 memoStorage.saveMemo(id, title, content)
    ↓
MemoStorage加载现有数据
    ↓
查找是否存在相同ID的记录
    ↓
存在:更新记录
不存在:创建新记录
    ↓
更新updatedAt时间戳
    ↓
保存JSON数组到文件
    ↓
返回成功/失败
    ↓
QML刷新列表并返回列表页
删除流程
用户点击删除按钮
    ↓
显示确认对话框
    ↓
用户确认删除
    ↓
调用 memoStorage.deleteMemo(id)
    ↓
MemoStorage加载现有数据
    ↓
过滤掉指定ID的记录
    ↓
保存更新后的JSON数组
    ↓
返回成功/失败
    ↓
QML刷新列表

核心功能实现

1. 应用入口与初始化

main.cpp 实现

应用入口文件负责初始化Qt环境、配置OpenGL ES、注册C++类到QML、创建QML引擎等核心工作。

extern "C" int qtmain(int argc, char **argv)
{
    // HarmonyOS平台适配
    QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
    QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
  
    // 配置OpenGL ES
    QSurfaceFormat format;
    format.setRenderableType(QSurfaceFormat::OpenGLES);
    format.setVersion(2, 0);
    QSurfaceFormat::setDefaultFormat(format);
  
    QGuiApplication app(argc, argv);
  
    // 注册C++类到QML
    qmlRegisterType<MemoStorage>("MemoApp", 1, 0, "MemoStorage");
  
    QQmlApplicationEngine engine;
    MemoStorage *memoStorage = new MemoStorage(&app);
    engine.rootContext()->setContextProperty("memoStorage", memoStorage);
  
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    return app.exec();
}

extern "C" int main(int argc, char **argv)
{
    return qtmain(argc, argv);
}

关键点说明:

  1. HarmonyOS平台适配

    • 使用 qtmain()作为入口函数,这是HarmonyOS平台的要求
    • 配置OpenGL ES表面格式,确保渲染正常
    • 设置软件渲染后端作为兜底方案
  2. C++与QML集成

    • 使用 qmlRegisterType注册C++类型
    • 使用 setContextProperty设置全局上下文属性
    • 这样QML可以直接访问 memoStorage对象
  3. 错误处理

    • 监听QML警告信号,便于调试
    • 提供备用界面加载机制,避免应用完全崩溃

2. 数据存储层实现

MemoStorage类设计

MemoStorage类是整个应用的数据管理核心,负责所有与数据持久化相关的操作。

头文件(memostorage.h):

#ifndef MEMOSTORAGE_H
#define MEMOSTORAGE_H

#include <QObject>
#include <QString>
#include <QVariantMap>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QFile>
#include <QStandardPaths>
#include <QDir>
#include <QDebug>

class MemoStorage : public QObject
{
    Q_OBJECT
  
public:
    explicit MemoStorage(QObject *parent = nullptr);
  
    // 加载所有备忘录
    Q_INVOKABLE QVariantList loadMemos();
  
    // 保存备忘录(新建或更新)
    Q_INVOKABLE bool saveMemo(int id, const QString &title, const QString &content);
  
    // 删除备忘录
    Q_INVOKABLE bool deleteMemo(int id);
  
    // 获取下一个可用ID
    Q_INVOKABLE int getNextId();
  
private:
    // 获取存储文件路径
    QString getStoragePath();
  
    // 确保存储目录存在
    void ensureStorageDir();
  
    // 从文件加载JSON数组
    QJsonArray loadJsonArray();
  
    // 保存JSON数组到文件
    bool saveJsonArray(const QJsonArray &array);
};

#endif // MEMOSTORAGE_H

关键实现代码:

// 获取存储路径
QString MemoStorage::getStoragePath()
{
    QString dataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
    QDir dir;
    if (!dir.exists(dataPath)) {
        dir.mkpath(dataPath);
    }
    return dataPath + "/memos.json";
}

// 加载备忘录(关键逻辑)
QVariantList MemoStorage::loadMemos()
{
    QJsonArray array = loadJsonArray();
    QVariantList result;
  
    // 转换为QVariantMap并排序
    for (int i = 0; i < array.size(); i++) {
        QJsonObject obj = array[i].toObject();
        QVariantMap map;
        map["id"] = obj["id"].toInt();
        map["title"] = obj["title"].toString();
        map["content"] = obj["content"].toString();
        map["updatedAt"] = obj["updatedAt"].toString();
        result.append(map);
    }
  
    // 按更新时间倒序排序
    std::sort(result.begin(), result.end(), [](const QVariant &a, const QVariant &b) {
        return a.toMap()["updatedAt"].toString() > b.toMap()["updatedAt"].toString();
    });
  
    return result;
}

// 保存备忘录(关键逻辑)
bool MemoStorage::saveMemo(int id, const QString &title, const QString &content)
{
    if (title.trimmed().isEmpty()) return false;
  
    QJsonArray array = loadJsonArray();
    QString nowStr = QDateTime::currentDateTime().toString(Qt::ISODate);
  
    // 查找是否存在,存在则更新,不存在则新建
    bool found = false;
    for (int i = 0; i < array.size(); i++) {
        if (array[i].toObject()["id"].toInt() == id) {
            QJsonObject obj = array[i].toObject();
            obj["title"] = title;
            obj["content"] = content;
            obj["updatedAt"] = nowStr;
            array[i] = obj;
            found = true;
            break;
        }
    }
  
    if (!found) {
        QJsonObject obj;
        obj["id"] = id;
        obj["title"] = title;
        obj["content"] = content;
        obj["createdAt"] = nowStr;
        obj["updatedAt"] = nowStr;
        array.append(obj);
    }
  
    return saveJsonArray(array);
}

设计要点:

  1. Q_INVOKABLE宏:标记可以在QML中调用的C++方法
  2. QStandardPaths:使用Qt标准路径API,确保跨平台兼容性
  3. 错误处理:文件操作都有错误检查和日志输出
  4. 数据排序:加载时按更新时间倒序排序,最新内容在前
  5. ID管理:自动递增ID,确保唯一性

3. UI界面实现

主界面结构

应用采用StackView实现页面导航,包含列表页和编辑页两个主要页面。

主窗口结构(main.qml关键部分):

ApplicationWindow {
    id: window
    width: 480
    height: 800
  
    // 主题色定义
    readonly property color primaryColor: "#6366f1"
    readonly property color backgroundColor: "#f8fafc"
  
    // 备忘录列表模型
    ListModel {
        id: memoListModel
    }
  
    // 加载数据函数
    function loadMemos() {
        memoListModel.clear()
        var memos = memoStorage.loadMemos()
        for (var i = 0; i < memos.length; i++) {
            memoListModel.append({
                memoId: memos[i].id,
                title: memos[i].title,
                content: memos[i].content,
                updatedAt: memos[i].updatedAt
            })
        }
    }
  
    Component.onCompleted: {
        loadMemos()
    }
  
    // 页面堆栈
    StackView {
        id: stackView
        anchors.fill: parent
        initialItem: listPage
    }
}
列表页实现

列表页的关键代码:

// 列表项delegate
delegate: Rectangle {
    id: memoCard
    height: 120
  
    property int memoId: model.memoId
    property string memoTitle: model.title
    property string memoContent: model.content
  
    // 点击进入编辑页
    MouseArea {
        anchors.fill: parent
        onClicked: {
            var page = editPage.createObject(stackView, {
                memoId: memoCard.memoId,
                memoTitle: memoCard.memoTitle,
                memoContent: memoCard.memoContent
            })
            stackView.push(page)
        }
    }
  
    // 列表项内容布局
    RowLayout {
        anchors.fill: parent
        ColumnLayout {
            Text {
                text: memoTitle || "无标题"
                font.pixelSize: 28
                font.bold: true
            }
            Text {
                text: memoContent || "无内容"
                font.pixelSize: 22
                maximumLineCount: 2
            }
        }
        Button {
            text: "🗑️"
            onClicked: deleteConfirmDialog.open()
        }
    }
}

设计要点:

  1. Material Design风格:使用紫色主题色(#6366f1),符合Material规范
  2. 响应式布局:使用ColumnLayout和RowLayout实现自适应布局
  3. 交互反馈:点击列表项时有视觉反馈效果
  4. 空状态处理:当没有数据时显示提示信息
  5. 字体大小优化:标题28px,内容22px,时间18px,确保可读性
编辑页实现

编辑页的关键代码:

Component {
    id: editPage
  
    Rectangle {
        id: editPageRoot
        property int memoId: -1
        property string memoTitle: ""
        property string memoContent: ""
      
        // 初始化数据
        Component.onCompleted: {
            titleField.text = memoTitle || ""
            contentArea.text = memoContent || ""
        }
      
        ColumnLayout {
            // 标题栏:返回按钮 + 标题 + 保存按钮
            Rectangle {
                height: 70
                color: primaryColor
                // ... 标题栏布局
            }
          
            // 编辑区域
            ColumnLayout {
                // 标题输入
                TextField {
                    id: titleField
                    font.pixelSize: 32
                    placeholderText: "标题"
                }
              
                // 分割线
                Rectangle { height: 2; color: "#e2e8f0" }
              
                // 内容输入(全屏)
                ScrollView {
                    Layout.fillHeight: true
                    TextArea {
                        id: contentArea
                        font.pixelSize: 26
                        placeholderText: "内容..."
                    }
                }
            }
        }
      
        // 保存按钮点击处理
        Button {
            onClicked: {
                if (titleField.text.trim() === "") {
                    errorToast.show("标题不能为空")
                    return
                }
                var id = memoId === -1 ? memoStorage.getNextId() : memoId
                if (memoStorage.saveMemo(id, titleField.text, contentArea.text)) {
                    loadMemos()
                    stackView.pop()
                }
            }
        }
    }
}

设计要点:

  1. 全屏编辑:内容区域占据剩余所有空间,提供良好的编辑体验
  2. 标题与内容分离:使用分割线清晰区分标题和内容区域
  3. 数据初始化:在Component.onCompleted中初始化数据,确保编辑时能正确显示
  4. 输入验证:保存前检查标题是否为空
  5. 字体大小:标题32px,内容26px,确保输入时清晰可见

数据持久化方案

存储方案选择

在开发过程中,我们经历了多次存储方案的调整:

方案一:SQLite数据库(初始方案)

优点:

  • 关系型数据库,支持复杂查询
  • 性能优秀,适合大量数据
  • 支持事务,数据一致性有保障

缺点:

  • 需要SQLite驱动,HarmonyOS平台可能不支持
  • 配置复杂,需要额外的CMake配置
  • 对于简单应用来说过于重量级
方案二:QML LocalStorage(尝试方案)

优点:

  • Qt Quick内置,无需额外驱动
  • 纯QML实现,代码简洁
  • 跨平台兼容性好

缺点:

  • 在HarmonyOS平台上仍然出现驱动加载问题
  • 错误信息:“Driver not loaded”
  • 无法正常使用
方案三:JSON文件存储(最终方案)

优点:

  • 简单可靠,不依赖数据库驱动
  • 易于调试,可以直接查看文件内容
  • 跨平台兼容性最好
  • 轻量级,适合小型应用
  • 易于备份和迁移

缺点:

  • 不适合大量数据(本项目场景足够)
  • 没有事务支持(本项目不需要)
  • 查询能力有限(本项目不需要复杂查询)

最终选择: JSON文件存储方案

JSON文件格式设计

[
  {
    "id": 1,
    "title": "会议记录",
    "content": "今天下午3点开会讨论项目进度",
    "createdAt": "2025-11-12T09:00:00",
    "updatedAt": "2025-11-12T09:30:00"
  },
  {
    "id": 2,
    "title": "购物清单",
    "content": "1. 牛奶\n2. 面包\n3. 鸡蛋",
    "createdAt": "2025-11-12T10:00:00",
    "updatedAt": "2025-11-12T10:15:00"
  }
]

字段说明:

  • id:唯一标识符,整数类型
  • title:备忘录标题,字符串类型
  • content:备忘录内容,字符串类型
  • createdAt:创建时间,ISO 8601格式字符串
  • updatedAt:更新时间,ISO 8601格式字符串

文件存储位置

使用Qt的 QStandardPaths::AppDataLocation获取应用数据目录:

  • HarmonyOS平台/data/storage/el2/base/files/memos.json
  • 其他平台:遵循各平台的标准数据目录规范

数据操作实现

核心数据操作

读取操作:

QVariantList MemoStorage::loadMemos()
{
    QJsonArray array = loadJsonArray();
    QVariantList result;
  
    // 转换为QVariantMap
    for (int i = 0; i < array.size(); i++) {
        QJsonObject obj = array[i].toObject();
        QVariantMap map;
        map["id"] = obj["id"].toInt();
        map["title"] = obj["title"].toString();
        map["content"] = obj["content"].toString();
        map["updatedAt"] = obj["updatedAt"].toString();
        result.append(map);
    }
  
    // 按更新时间倒序排序
    std::sort(result.begin(), result.end(), [](const QVariant &a, const QVariant &b) {
        return a.toMap()["updatedAt"].toString() > b.toMap()["updatedAt"].toString();
    });
  
    return result;
}

保存操作:

bool MemoStorage::saveMemo(int id, const QString &title, const QString &content)
{
    if (title.trimmed().isEmpty()) return false;
  
    QJsonArray array = loadJsonArray();
    QString nowStr = QDateTime::currentDateTime().toString(Qt::ISODate);
  
    // 查找并更新或新建
    bool found = false;
    for (int i = 0; i < array.size(); i++) {
        if (array[i].toObject()["id"].toInt() == id) {
            // 更新现有记录
            QJsonObject obj = array[i].toObject();
            obj["title"] = title;
            obj["content"] = content;
            obj["updatedAt"] = nowStr;
            array[i] = obj;
            found = true;
            break;
        }
    }
  
    if (!found) {
        // 新建记录
        QJsonObject obj;
        obj["id"] = id;
        obj["title"] = title;
        obj["content"] = content;
        obj["createdAt"] = nowStr;
        obj["updatedAt"] = nowStr;
        array.append(obj);
    }
  
    return saveJsonArray(array);
}

删除操作:

bool MemoStorage::deleteMemo(int id)
{
    QJsonArray array = loadJsonArray();
    QJsonArray newArray;
  
    // 过滤掉指定ID的记录
    for (int i = 0; i < array.size(); i++) {
        if (array[i].toObject()["id"].toInt() != id) {
            newArray.append(array[i]);
        }
    }
  
    return saveJsonArray(newArray);
}

UI设计与实现

设计原则

  1. Material Design规范:遵循Google Material Design设计指南
  2. 简洁明了:界面简洁,功能清晰
  3. 易于操作:按钮大小适中,交互反馈明确
  4. 视觉层次:通过颜色、字体大小、间距建立清晰的视觉层次
  5. 响应式设计:适配不同屏幕尺寸

颜色方案

// 主题色定义
readonly property color primaryColor: "#6366f1"      // 主色调(紫色)
readonly property color backgroundColor: "#f8fafc"  // 背景色(浅灰)
readonly property color cardColor: "#ffffff"        // 卡片色(白色)
readonly property color textColor: "#1e293b"        // 主文本色(深灰)
readonly property color textSecondaryColor: "#64748b" // 次要文本色(中灰)

颜色选择理由:

  • 紫色(#6366f1)作为主色调,现代感强,符合Material Design规范
  • 浅灰背景(#f8fafc)提供舒适的视觉体验
  • 白色卡片(#ffffff)与背景形成对比,突出内容
  • 深灰文本(#1e293b)确保良好的可读性

字体大小规范

经过多次优化,最终确定的字体大小:

  • 列表页标题:36px
  • 列表项标题:28px
  • 列表项内容:22px(最多显示2行)
  • 列表项时间:18px
  • 编辑页标题栏:28px
  • 标题输入框:32px
  • 内容输入框:26px
  • 按钮文字:22-28px

这些字体大小确保了在各种设备上都有良好的可读性。

布局设计

列表页布局
┌─────────────────────────────┐
│  标题栏(紫色,70px高)      │
│  📝 备忘录          ➕      │
├─────────────────────────────┤
│                             │
│  备忘录列表(可滚动)        │
│  ┌─────────────────────┐   │
│  │ 标题(28px)         │   │
│  │ 内容预览(22px)     │   │
│  │ 时间(18px)    🗑️  │   │
│  └─────────────────────┘   │
│  ┌─────────────────────┐   │
│  │ ...                 │   │
│  └─────────────────────┘   │
│                             │
└─────────────────────────────┘
编辑页布局
┌─────────────────────────────┐
│  标题栏(紫色,70px高)      │
│  ← 新建备忘录        保存    │
├─────────────────────────────┤
│  标题输入区域(100px高)     │
│  ┌─────────────────────┐   │
│  │ 标题                │   │
│  └─────────────────────┘   │
├─────────────────────────────┤
│  分割线(2px)              │
├─────────────────────────────┤
│  内容输入区域(全屏)        │
│  ┌─────────────────────┐   │
│  │                     │   │
│  │  内容...            │   │
│  │                     │   │
│  │  (可滚动)          │   │
│  └─────────────────────┘   │
└─────────────────────────────┘

交互设计

点击反馈

列表项点击时有视觉反馈:

// 点击效果
Rectangle {
    anchors.fill: parent
    color: "#f0f0f0"
    opacity: mouseArea.pressed ? 0.3 : 0
    Behavior on opacity {
        NumberAnimation { duration: 100 }
    }
}
页面导航

使用StackView实现页面导航,通过 createObject创建页面实例并推入堆栈。

错误提示

使用Toast消息提示错误,3秒后自动消失。


开发过程中遇到的问题与解决方案

问题一:SQLite驱动未加载

问题描述

在HarmonyOS平台上运行时,应用出现以下错误:

QSqlDatabase: QSQLITE driver not loaded
QSqlDatabase: available drivers: 
SQLite驱动不可用

应用无法连接数据库,导致数据持久化功能完全不可用。

问题分析
  1. 根本原因

    • HarmonyOS平台的Qt构建可能没有包含SQLite插件
    • SQLite插件路径配置不正确
    • Qt SQL模块在HarmonyOS平台上的支持不完整
  2. 尝试的解决方案

    • 在CMakeLists.txt中添加SQL模块依赖
    • 检查SQLite插件路径
    • 尝试使用QML LocalStorage API
  3. LocalStorage也失败

    • 尝试使用 QtQuick.LocalStorage模块
    • 仍然出现"Driver not loaded"错误
    • 说明底层SQLite驱动确实不可用
最终解决方案

采用JSON文件存储方案

  1. 创建MemoStorage类

    • 使用Qt的JSON API进行数据序列化/反序列化
    • 使用QFile进行文件读写操作
    • 使用QStandardPaths获取应用数据目录
  2. 实现要点

    // 文件路径获取
    QString MemoStorage::getStoragePath()
    {
        QString dataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
        QDir dir;
        if (!dir.exists(dataPath)) {
            dir.mkpath(dataPath);
        }
        return dataPath + "/memos.json";
    }
    
    // JSON数组加载
    QJsonArray MemoStorage::loadJsonArray()
    {
        QString filePath = getStoragePath();
        QFile file(filePath);
    
        if (!file.exists()) {
            return QJsonArray();
        }
    
        if (!file.open(QIODevice::ReadOnly)) {
            qWarning() << "无法打开文件:" << filePath;
            return QJsonArray();
        }
    
        QByteArray data = file.readAll();
        file.close();
    
        QJsonDocument doc = QJsonDocument::fromJson(data);
        if (doc.isNull() || !doc.isArray()) {
            return QJsonArray();
        }
    
        return doc.array();
    }
    
  3. 优势

    • 不依赖任何数据库驱动
    • 跨平台兼容性最好
    • 易于调试(可以直接查看JSON文件)
    • 轻量级,适合小型应用
经验总结
  1. 平台兼容性:在跨平台开发中,要特别注意平台特定的限制
  2. 备选方案:始终准备备选方案,当主要方案不可用时能够快速切换
  3. 简单优先:对于简单应用,文件存储可能比数据库更合适

问题二:QML component关键字语法错误

问题描述

QML文件编译时出现语法错误:

qrc:/main.qml:199:1: Syntax error

错误位置是使用 component关键字定义组件的地方:

component MemoCard: Rectangle {
    // ...
}
问题分析
  1. 根本原因

    • component关键字是Qt 6.2+才引入的特性
    • HarmonyOS平台使用的Qt版本可能是5.x或6.0/6.1
    • 这些版本不支持 component关键字
  2. Qt版本差异

    • Qt 5.x:不支持 component关键字
    • Qt 6.0/6.1:不支持 component关键字
    • Qt 6.2+:支持 component关键字
解决方案

将component改为内联定义

修改前:

component MemoCard: Rectangle {
    property int memoId: -1
    property string title: ""
    property string content: ""
    // ...
}

ListView {
    delegate: MemoCard {
        memoId: model.memoId
        title: model.title
        content: model.content
    }
}

修改后:

ListView {
    delegate: Rectangle {
        id: memoCard
        property int memoId: model.memoId
        property string memoTitle: model.title
        property string memoContent: model.content
    
        // 直接在delegate中定义所有内容
        ColumnLayout {
            anchors.fill: parent
            // ...
        }
    }
}
经验总结
  1. 版本兼容性:使用新特性前要检查Qt版本支持情况
  2. 降级方案:准备兼容旧版本的实现方案
  3. 代码组织:内联定义虽然代码更长,但兼容性更好

问题三:StackView页面参数传递问题

问题描述

点击列表项进入编辑页时,编辑页中的标题和内容字段为空,无法显示已有数据。

问题分析
  1. 问题原因

    • 使用 StackView.push(component, properties)方式传递参数
    • 参数传递时机不对,组件创建时属性还未设置
    • QML组件的属性绑定机制导致数据无法正确初始化
  2. 错误代码

    // 错误的做法:直接push component
    stackView.push(editPage, { memoId: ..., title: ..., content: ... })
    
解决方案

使用createObject显式创建对象

// 正确的做法:使用createObject创建页面实例
onClicked: {
    var page = editPage.createObject(stackView, {
        memoId: memoCard.memoId,
        memoTitle: memoCard.memoTitle,
        memoContent: memoCard.memoContent
    })
    stackView.push(page)
}

// 在Component.onCompleted中初始化数据
Component.onCompleted: {
    titleField.text = memoTitle || ""
    contentArea.text = memoContent || ""
}
经验总结
  1. 对象创建:使用 createObject可以更精确地控制对象创建和属性设置
  2. 生命周期:利用 Component.onCompleted确保在组件完全初始化后再设置数据
  3. 属性命名:使用 memoTitle而不是 title,避免与QML内置属性冲突

问题四:HarmonyOS平台入口函数问题

问题描述

应用在HarmonyOS平台上无法正常启动,出现闪退。

问题分析
  1. 根本原因

    • HarmonyOS平台要求使用 qtmain()而不是 main()作为入口函数
    • Qt应用作为共享库加载,生命周期由HarmonyOS管理
    • 需要特殊的入口函数签名
  2. 错误代码

    int main(int argc, char *argv[])
    {
        QGuiApplication app(argc, argv);
        // ...
    }
    
解决方案

使用qtmain作为主入口

extern "C" int qtmain(int argc, char **argv)
{
    // HarmonyOS平台要求:使用qtmain作为入口
    QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
    QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
  
    // 配置OpenGL ES
    QSurfaceFormat format;
    format.setRenderableType(QSurfaceFormat::OpenGLES);
    format.setVersion(2, 0);
    QSurfaceFormat::setDefaultFormat(format);
  
    QGuiApplication app(argc, argv);
  
    // 注册C++类到QML
    qmlRegisterType<MemoStorage>("MemoApp", 1, 0, "MemoStorage");
  
    QQmlApplicationEngine engine;
    MemoStorage *memoStorage = new MemoStorage(&app);
    engine.rootContext()->setContextProperty("memoStorage", memoStorage);
  
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    return app.exec();
}

// HarmonyOS兼容性入口
extern "C" int main(int argc, char **argv)
{
    return qtmain(argc, argv);
}

关键配置

// OpenGL ES设置
QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);

// 表面格式配置
QSurfaceFormat format;
format.setRenderableType(QSurfaceFormat::OpenGLES);
format.setVersion(2, 0);
QSurfaceFormat::setDefaultFormat(format);

// 软件渲染兜底
qputenv("QT_QUICK_BACKEND", "software");
qputenv("QSG_RENDER_LOOP", "basic");
经验总结
  1. 平台特定要求:不同平台可能有特殊的入口函数要求
  2. 错误处理:提供兜底方案,确保应用在各种环境下都能运行
  3. 日志输出:添加详细的日志,便于调试和问题定位

问题五:字体大小和UI优化

问题描述

用户反馈字体太小,看不清;UI需要优化。

问题分析
  1. 字体问题

    • 初始字体大小偏小(14-20px)
    • 在移动设备上可读性差
    • 需要根据内容重要性调整字体大小
  2. UI问题

    • 列表项高度不够,内容显示不完整
    • 编辑页布局不够清晰
    • 缺少视觉反馈
解决方案

字体大小优化

  • 列表页标题:24px → 36px
  • 列表项标题:20px → 28px
  • 列表项内容:16px → 22px
  • 编辑页标题输入:22px → 32px
  • 编辑页内容输入:18px → 26px

UI优化

// 列表项:增加高度和内边距
delegate: Rectangle {
    height: 120  // 从80px增加到120px
    anchors.leftMargin: 20
    anchors.rightMargin: 20
}

// 编辑页:标题区域独立,分割线加粗,内容全屏
Rectangle {
    Layout.preferredHeight: 100  // 标题区域
}
Rectangle { height: 2 }  // 分割线
ScrollView { Layout.fillHeight: true }  // 内容全屏

// 点击反馈效果
Rectangle {
    opacity: mouseArea.pressed ? 0.3 : 0
    Behavior on opacity { NumberAnimation { duration: 100 } }
}
经验总结
  1. 用户体验优先:字体大小要以用户可读性为准
  2. 迭代优化:根据用户反馈不断调整UI
  3. 视觉反馈:提供清晰的交互反馈,提升用户体验

性能优化

1. 数据加载优化

延迟加载

应用启动时只加载必要的UI,数据加载在 Component.onCompleted中进行。

数据排序优化

使用C++的 std::sort进行排序,性能优于QML中的JavaScript排序:

std::sort(result.begin(), result.end(), [](const QVariant &a, const QVariant &b) {
    return a.toMap()["updatedAt"].toString() > b.toMap()["updatedAt"].toString();
});

2. UI渲染优化

ListView优化
  • 使用 spacing属性控制列表项间距,避免重叠
  • 使用 clip: true确保超出部分不渲染
  • 列表项高度固定,避免动态计算
动画优化

点击反馈动画使用 NumberAnimation,持续时间100ms,确保流畅:

Behavior on opacity {
    NumberAnimation { duration: 100 }
}

3. 内存管理

对象生命周期
  • MemoStorage对象在main.cpp中创建,父对象为QGuiApplication,确保应用退出时自动释放
  • StackView管理的页面在pop时自动销毁,避免内存泄漏
数据缓存
  • 列表数据缓存在ListModel中,避免频繁读取文件
  • 只在保存或删除后刷新列表,减少IO操作

4. 文件操作优化

批量操作

保存和删除操作都是先加载全部数据,修改后再一次性保存,避免频繁的文件IO。

错误处理

文件操作都有完善的错误处理,避免因文件问题导致应用崩溃。


项目构建与部署

CMakeLists.txt配置

cmake_minimum_required(VERSION 3.5.0)
project(QtForHOSample)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

list(APPEND CMAKE_FIND_ROOT_PATH ${QT_PREFIX})
include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

find_package(QT NAMES Qt5 Qt6 REQUIRED COMPONENTS Core Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS 
    Concurrent 
    Gui 
    Network 
    Qml 
    Quick 
    QuickControls2 
    Widgets 
    QuickTemplates2 
    QmlWorkerScript
)

add_library(entry SHARED 
    main.cpp 
    memostorage.cpp 
    memostorage.h 
    qml.qrc
)

target_link_libraries(entry PRIVATE 
    Qt${QT_VERSION_MAJOR}::Concurrent
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Gui
    Qt${QT_VERSION_MAJOR}::Network
    Qt${QT_VERSION_MAJOR}::Qml
    Qt${QT_VERSION_MAJOR}::Quick
    Qt${QT_VERSION_MAJOR}::Widgets
    Qt${QT_VERSION_MAJOR}::QuickControls2
    Qt${QT_VERSION_MAJOR}::QuickTemplates2
    Qt${QT_VERSION_MAJOR}::QmlWorkerScript
    Qt${QT_VERSION_MAJOR}::QOpenHarmonyPlatformIntegrationPlugin
)

资源文件配置(qml.qrc)

<?xml version="1.0" encoding="UTF-8"?>
<RCC>
    <qresource prefix="/">
        <file>main.qml</file>
    </qresource>
</RCC>

构建步骤

  1. 环境准备

    • 安装Qt for HarmonyOS SDK
    • 配置Qt环境变量
    • 准备HarmonyOS开发环境
  2. 编译

    mkdir build
    cd build
    cmake .. -DQT_PREFIX=/path/to/qt
    make
    
  3. 部署

    • 将生成的 libentry.so文件部署到HarmonyOS设备
    • 配置HarmonyOS应用的EntryAbility
    • 运行应用

调试技巧

  1. 日志输出

    • 使用 qDebug()输出调试信息
    • 使用 qWarning()输出警告信息
    • 使用 qCritical()输出错误信息
  2. QML调试

    • 监听 QQmlApplicationEngine::warnings信号
    • 检查QML语法错误
    • 使用Qt Creator的QML调试器
  3. 性能分析

    • 使用Qt Creator的性能分析工具
    • 监控内存使用情况
    • 检查文件IO操作频率

总结与展望

项目总结

本项目成功开发了一款基于Qt/QML的备忘录应用,运行于HarmonyOS平台。项目实现了以下目标:

  1. 功能完整性:实现了备忘录的完整CRUD操作
  2. 数据持久化:使用JSON文件实现了可靠的数据存储
  3. 用户体验:提供了美观、流畅的用户界面
  4. 平台兼容性:成功适配HarmonyOS平台的特殊要求
  5. 代码质量:代码结构清晰,易于维护和扩展

技术收获

  1. Qt/QML开发

    • 掌握了Qt/QML的基本开发流程
    • 理解了C++与QML的交互机制
    • 学会了使用Qt Quick Controls组件
  2. HarmonyOS平台适配

    • 了解了HarmonyOS平台的特殊要求
    • 掌握了OpenGL ES配置方法
    • 学会了处理平台特定的问题
  3. 数据持久化

    • 了解了不同存储方案的优缺点
    • 掌握了JSON文件操作
    • 学会了根据场景选择合适的存储方案
  4. 问题解决能力

    • 学会了分析问题根源
    • 掌握了多种解决方案的对比和选择
    • 提升了调试和问题定位能力

项目亮点

  1. 架构清晰:采用MVC架构,代码分层明确
  2. 跨平台兼容:使用标准Qt API,易于移植到其他平台
  3. 用户体验优秀:Material Design风格,交互流畅
  4. 代码质量高:错误处理完善,代码注释清晰
  5. 可扩展性强:架构设计便于后续功能扩展

未来改进方向

  1. 功能增强

    • 添加搜索功能
    • 支持分类和标签
    • 添加富文本编辑
    • 支持图片附件
    • 添加数据导出功能
  2. 性能优化

    • 实现数据分页加载
    • 优化大数据量时的性能
    • 添加数据缓存机制
  3. 用户体验

    • 添加暗色主题
    • 支持多语言
    • 添加手势操作
    • 优化动画效果
  4. 技术升级

    • 考虑迁移到Qt 6.x
    • 使用更现代的QML特性
    • 优化代码结构

结语

本项目是一个完整的Qt/QML应用开发实践,涵盖了从需求分析、架构设计、功能实现到问题解决的完整开发流程。通过这个项目,我们不仅掌握了Qt/QML开发技术,更重要的是学会了如何在实际项目中解决问题、优化代码、提升用户体验。

希望这篇技术博客能够帮助其他开发者更好地理解Qt/QML开发,特别是HarmonyOS平台上的应用开发。同时也希望读者能够从我们遇到的问题和解决方案中获得启发,在自己的项目中避免类似的问题。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐