📋 项目概述

在这里插入图片描述

本文档基于一个完整的 TextEditorPro 项目,详细介绍了如何在 HarmonyOS 平台上使用 Qt 开发多功能文本编辑器应用程序。项目整合了文本编辑、样式控制、查找替换、格式化等功能,展示了 Qt Quick Controls 2.15 在 HarmonyOS 平台上的实际应用,为开发者提供了完整的文本编辑器开发参考。

✨ 主要功能

  • ✅ 文本编辑:支持多行文本编辑,自动换行
  • ✅ 样式控制:下划线、斜体、粗体三种文本样式
  • ✅ 颜色控制:黑色、红色、蓝色三种文本颜色
  • ✅ 查找替换:支持查找、替换、全部替换功能
  • ✅ 文本操作:追加行、逐行读取、全部读取、清空文本
  • ✅ 文本格式化:转大写、转小写、首字母大写、去除空格、插入时间
  • ✅ 文本统计:实时统计字符数、行数、词数
  • ✅ 对话框:查找结果对话框、全部读取对话框
  • ✅ 响应式布局:适配不同屏幕尺寸
  • ✅ 完整的触摸交互支持

欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/

仓库地址:https://atomgit.com/szkygc/HarmonyOs_PC-PGC/tree/main/Qt/TextEditorPro
环境搭建参考
Mac构建Qt鸿蒙项目:https://blog.csdn.net/qq_46169531/article/details/155111784
官方仓库:https://atomgit.com/openharmony-sig/qt
官方环境搭建参考:https://atomgit.com/openharmony-sig/qt/wiki/Home.md

🛠️ 技术栈

  • 开发框架: Qt 5.15+ for HarmonyOS
  • 编程语言: C++ / QML / JavaScript
  • 界面框架: Qt Quick Controls 2
  • 文本组件: TextArea / TextInput
  • 构建工具: CMake
  • 目标平台: HarmonyOS (OpenHarmony) / PC

🏗️ 项目架构

目录结构

TextEditorPro/
├── entry/src/main/
│   ├── cpp/
│   │   ├── main.cpp              # 应用入口(HarmonyOS适配)
│   │   ├── main.qml              # 主界面(多功能文本编辑器)
│   │   ├── CMakeLists.txt        # 构建配置
│   │   └── qml.qrc               # QML资源文件
│   ├── module.json5              # 模块配置
│   └── resources/                # 资源文件
└── image/
    └── 运行截图.png              # 运行截图

组件层次结构

ApplicationWindow (main.qml)
├── Rectangle (内容容器)
│   └── ScrollView (滚动视图)
│       └── Column (主列布局)
│           ├── Row (标题和统计信息)
│           ├── Row (样式控制 - 三个CheckBox)
│           ├── Row (颜色控制 - 三个RadioButton)
│           ├── Rectangle (文本编辑区域)
│           │   └── ScrollView
│           │       └── TextArea (文本编辑器)
│           ├── Column (查找替换区域)
│           │   ├── Text (标题)
│           │   └── Row
│           │       ├── Column (查找输入框)
│           │       ├── Column (替换输入框)
│           │       └── Column (操作按钮)
│           ├── Column (文本操作区域)
│           │   ├── Text (标题)
│           │   └── Row (操作按钮 + ComboBox)
│           ├── Column (文本格式化区域)
│           │   ├── Text (标题)
│           │   └── Row (格式化按钮)
│           └── Row (确定/取消/退出按钮)
├── Dialog (全部读取对话框)
│   ├── ScrollView
│   │   └── TextArea (只读文本显示)
│   └── DialogButtonBox (确定/取消按钮)
└── Dialog (查找结果对话框)
    ├── Text (结果显示)
    └── DialogButtonBox (确定按钮)

📝 核心功能实现

1. HarmonyOS 入口函数:qtmain()

⚠️ 关键要点:HarmonyOS 真机上必须使用 qtmain() 而不是 main()!

// ✅ 正确写法
extern "C" int qtmain(int argc, char **argv)
{
    // Qt 应用作为共享库加载,生命周期由 HarmonyOS 管理
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    return app.exec();  // ⚠️ 重要:必须调用 exec() 启动事件循环
}

// ❌ 错误写法(桌面应用方式)
int main(int argc, char *argv[])
{
    // 这种方式在 HarmonyOS 上会导致应用无法正常启动
}

原因说明:

  • HarmonyOS 将 Qt 应用作为共享库(.so)加载
  • 应用生命周期由 HarmonyOS 的 Ability 管理
  • qtmain() 是 HarmonyOS Qt 插件的标准入口点

2. OpenGL ES 表面格式配置

⚠️ 关键要点:必须在创建 QGuiApplication 之前配置 QSurfaceFormat!

// Step 1: 配置 OpenGL ES 表面格式(必须在创建应用之前!)
QSurfaceFormat format;

// 设置 Alpha 通道(透明度)
format.setAlphaBufferSize(8);      // 8 位 Alpha 通道

// 设置颜色通道(RGBA 32 位真彩色)
format.setRedBufferSize(8);        // 8 位红色通道
format.setGreenBufferSize(8);      // 8 位绿色通道
format.setBlueBufferSize(8);       // 8 位蓝色通道

// 设置深度和模板缓冲区
format.setDepthBufferSize(24);     // 24 位深度缓冲
format.setStencilBufferSize(8);    // 8 位模板缓冲

// 指定渲染类型为 OpenGL ES(HarmonyOS要求)
format.setRenderableType(QSurfaceFormat::OpenGLES);

// 指定 OpenGL ES 版本为 3.0(推荐)
format.setVersion(3, 0);

// ⚠️ 关键:必须在创建 QGuiApplication 之前设置默认格式!
QSurfaceFormat::setDefaultFormat(format);

// Step 2: 创建 Qt 应用实例(必须在设置格式之后)
QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
QGuiApplication app(argc, argv);

配置说明:

  • OpenGL ES 3.0:HarmonyOS 推荐使用 OpenGL ES 3.0
  • RGBA 8-8-8-8:32 位真彩色,支持透明度
  • 深度缓冲 24 位:用于 3D 渲染和层级管理
  • 模板缓冲 8 位:用于复杂图形效果

3. 文本样式控制实现

核心思想:使用 CheckBox 控制文本的样式属性(下划线、斜体、粗体)。

ApplicationWindow {
    id: root
    
    // 文本样式属性
    property bool textUnderline: false
    property bool textItalic: false
    property bool textBold: false
    
    CheckBox {
        id: underlineCheckBox
        text: "下划线"
        font.pixelSize: 36
        checked: textUnderline
        
        // 自定义 indicator 样式
        indicator: Rectangle {
            implicitWidth: 56
            implicitHeight: 56
            x: underlineCheckBox.leftPadding
            y: parent.height / 2 - height / 2
            radius: 8
            border.color: underlineCheckBox.checked ? "#0078D4" : "#999999"
            border.width: underlineCheckBox.checked ? 4 : 3
            color: underlineCheckBox.checked ? "#0078D4" : "white"
            
            Text {
                anchors.centerIn: parent
                text: "✓"
                color: "white"
                font.pixelSize: 40
                font.bold: true
                visible: underlineCheckBox.checked
            }
        }
        
        onCheckedChanged: {
            textUnderline = checked
        }
    }
    
    TextArea {
        id: textEdit
        font.underline: textUnderline  // 绑定到样式属性
        font.italic: textItalic
        font.bold: textBold
    }
}

关键点:

  • 属性绑定:TextArea 的样式属性绑定到 ApplicationWindow 的属性
  • 双向绑定:CheckBox 的 checked 属性绑定到样式属性
  • 自定义样式:使用 indicator 自定义 CheckBox 的外观

4. 文本颜色控制实现

核心思想:使用 RadioButton 控制文本的颜色。

ApplicationWindow {
    id: root
    
    // 文本颜色属性(0: 黑色, 1: 红色, 2: 蓝色)
    property int textColorIndex: 0
    
    // 颜色映射
    property color textColor: {
        if (textColorIndex === 0) return "black"
        else if (textColorIndex === 1) return "red"
        else return "blue"
    }
    
    Row {
        spacing: 60
        
        RadioButton {
            id: blackRadioButton
            text: "黑色"
            font.pixelSize: 36
            checked: textColorIndex === 0
            
            onCheckedChanged: {
                if (checked) {
                    textColorIndex = 0
                }
            }
        }
        
        RadioButton {
            id: redRadioButton
            text: "红色"
            font.pixelSize: 36
            checked: textColorIndex === 1
            
            onCheckedChanged: {
                if (checked) {
                    textColorIndex = 1
                }
            }
        }
        
        RadioButton {
            id: blueRadioButton
            text: "蓝色"
            font.pixelSize: 36
            checked: textColorIndex === 2
            
            onCheckedChanged: {
                if (checked) {
                    textColorIndex = 2
                }
            }
        }
    }
    
    TextArea {
        id: textEdit
        color: textColor  // 绑定到颜色属性
    }
}

关键点:

  • 索引映射:使用 textColorIndex 作为颜色索引
  • 颜色计算:使用 JavaScript 表达式计算颜色值
  • 单选组:RadioButton 自动形成单选组

5. TextArea 文本编辑器实现

核心思想:使用 TextArea 实现多行文本编辑,支持样式和颜色绑定。

Rectangle {
    width: parent.width
    height: 500
    color: "white"
    border.color: "#333333"
    border.width: 5
    radius: 12
    
    ScrollView {
        anchors.fill: parent
        anchors.margins: 5
        
        TextArea {
            id: textEdit
            width: parent.width
            height: Math.max(500, implicitHeight)
            placeholderText: "在此输入文本..."
            text: "Hello world\n\nIt is my demo."
            font.pixelSize: 38
            font.underline: textUnderline
            font.italic: textItalic
            font.bold: textBold
            color: textColor
            wrapMode: TextArea.Wrap
            selectByMouse: true
            padding: 20
            selectionColor: "#0078D4"
            selectedTextColor: "white"
            placeholderTextColor: "#999999"
            
            onTextChanged: {
                updateTextStats()  // 文本变化时更新统计
            }
            
            // 背景样式
            background: Rectangle {
                color: "white"
                border.color: textEdit.activeFocus ? "#0078D4" : "transparent"
                border.width: 2
                radius: 8
            }
        }
    }
}

关键点:

  • 多行编辑:TextArea 支持多行文本编辑
  • 自动换行:使用 wrapMode: TextArea.Wrap 实现自动换行
  • 文本选择:使用 selectByMouse: true 支持鼠标选择
  • 焦点样式:使用 activeFocus 实现焦点时的边框高亮

6. 文本统计功能实现

核心思想:实时统计文本的字符数、行数、词数。

ApplicationWindow {
    id: root
    
    // 文本统计属性
    property int textLength: 0
    property int textLineCount: 0
    property int textWordCount: 0
    
    // 更新文本统计
    function updateTextStats() {
        var text = textEdit.text
        textLength = text.length
        var lines = text.split("\n")
        textLineCount = lines.length
        var words = text.trim().split(/\s+/).filter(function(word) { return word.length > 0 })
        textWordCount = words.length
    }
    
    Row {
        Text {
            text: "字符: " + textLength + " | 行: " + textLineCount + " | 词: " + textWordCount
            font.pixelSize: 26
            color: "#666666"
        }
    }
    
    TextArea {
        id: textEdit
        onTextChanged: {
            updateTextStats()  // 文本变化时更新统计
        }
    }
}

关键点:

  • 字符统计:使用 text.length 获取字符数
  • 行数统计:使用 split("\n") 分割文本获取行数
  • 词数统计:使用正则表达式 /\s+/ 分割文本获取词数
  • 实时更新:在 onTextChanged 中调用统计函数

7. 查找替换功能实现

核心思想:使用 indexOf 和 replace 方法实现文本查找和替换。

ApplicationWindow {
    id: root
    
    // 查找替换相关
    property string searchText: ""
    property string replaceText: ""
    
    // 查找文本
    function findText() {
        if (searchText.trim() === "") {
            findResultDialog.text = "请输入要查找的文本"
            findResultDialog.open()
            return
        }
        
        var text = textEdit.text
        var index = text.indexOf(searchText)
        if (index >= 0) {
            // 选中找到的文本
            textEdit.select(index, index + searchText.length)
            textEdit.cursorPosition = index + searchText.length
            findResultDialog.text = "找到文本: \"" + searchText + "\"\n位置: " + index
        } else {
            findResultDialog.text = "未找到文本: \"" + searchText + "\""
        }
        findResultDialog.open()
    }
    
    // 替换文本
    function replaceText() {
        if (searchText.trim() === "") {
            findResultDialog.text = "请输入要替换的文本"
            findResultDialog.open()
            return
        }
        
        var text = textEdit.text
        if (text.indexOf(searchText) >= 0) {
            textEdit.text = text.replace(searchText, replaceText)
            updateTextStats()
            findResultDialog.text = "已替换: \"" + searchText + "\" -> \"" + replaceText + "\""
        } else {
            findResultDialog.text = "未找到要替换的文本: \"" + searchText + "\""
        }
        findResultDialog.open()
    }
    
    // 全部替换
    function replaceAll() {
        if (searchText.trim() === "") {
            findResultDialog.text = "请输入要替换的文本"
            findResultDialog.open()
            return
        }
        
        var text = textEdit.text
        var count = 0
        while (text.indexOf(searchText) >= 0) {
            text = text.replace(searchText, replaceText)
            count++
        }
        
        if (count > 0) {
            textEdit.text = text
            updateTextStats()
            findResultDialog.text = "已全部替换 " + count + " 处: \"" + searchText + "\" -> \"" + replaceText + "\""
        } else {
            findResultDialog.text = "未找到要替换的文本: \"" + searchText + "\""
        }
        findResultDialog.open()
    }
}

关键点:

  • 查找算法:使用 indexOf 查找文本位置
  • 文本选择:使用 select() 方法选中找到的文本
  • 单次替换:使用 replace() 方法替换第一个匹配项
  • 全部替换:使用 while 循环替换所有匹配项

8. 文本操作功能实现

核心思想:实现追加行、逐行读取、全部读取、清空等文本操作。

ApplicationWindow {
    id: root
    
    // QPlainTextEditDemo 相关属性
    property int rowCount: 0  // 行号计数器
    
    // 追加一行文本
    function appendLine() {
        rowCount++
        var newLine = "第" + rowCount + "行"
        textEdit.text = textEdit.text + "\n" + newLine
        updateTextStats()
    }
    
    // 逐行读取,添加到 ComboBox
    function readLines() {
        lineComboBox.model.clear()
        var lines = textEdit.text.split("\n")
        for (var i = 0; i < lines.length; i++) {
            if (lines[i].trim() !== "") {  // 忽略空行
                lineComboBox.model.append({text: lines[i]})
            }
        }
    }
    
    // 全部读取,显示对话框
    function readAll() {
        var allText = textEdit.text
        if (allText.trim() === "") {
            allText = "文本区域为空"
        }
        readAllDialog.text = allText
        readAllDialog.open()
    }
    
    // 清空文本
    function clearText() {
        textEdit.text = ""
        rowCount = 0
        lineComboBox.model.clear()
        updateTextStats()
    }
    
    ComboBox {
        id: lineComboBox
        width: 280
        height: 80
        
        model: ListModel {
            id: lineModel
        }
    }
}

关键点:

  • 追加行:使用字符串拼接追加新行
  • 逐行读取:使用 split("\n") 分割文本,添加到 ListModel
  • 全部读取:获取全部文本,显示在对话框中
  • 清空文本:重置文本和计数器

9. 文本格式化功能实现

核心思想:实现转大写、转小写、首字母大写、去除空格、插入时间等格式化功能。

ApplicationWindow {
    id: root
    
    // 文本格式化:转大写
    function toUpperCase() {
        textEdit.text = textEdit.text.toUpperCase()
        updateTextStats()
    }
    
    // 文本格式化:转小写
    function toLowerCase() {
        textEdit.text = textEdit.text.toLowerCase()
        updateTextStats()
    }
    
    // 文本格式化:首字母大写
    function toTitleCase() {
        var lines = textEdit.text.split("\n")
        var result = []
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i]
            if (line.length > 0) {
                line = line.charAt(0).toUpperCase() + line.slice(1).toLowerCase()
            }
            result.push(line)
        }
        textEdit.text = result.join("\n")
        updateTextStats()
    }
    
    // 去除首尾空格
    function trimText() {
        var lines = textEdit.text.split("\n")
        var result = []
        for (var i = 0; i < lines.length; i++) {
            result.push(lines[i].trim())
        }
        textEdit.text = result.join("\n")
        updateTextStats()
    }
    
    // 插入当前时间
    function insertDateTime() {
        var now = new Date()
        var dateStr = now.toLocaleString(Qt.locale("zh_CN"), "yyyy-MM-dd hh:mm:ss")
        textEdit.text = textEdit.text + "\n" + dateStr
        updateTextStats()
    }
}

关键点:

  • 大小写转换:使用 toUpperCase() 和 toLowerCase() 方法
  • 首字母大写:使用 charAt(0) 和 slice(1) 实现
  • 去除空格:使用 trim() 方法去除首尾空格
  • 插入时间:使用 Date 对象和 toLocaleString() 格式化时间

10. 对话框实现

核心思想:使用 Dialog 组件实现查找结果和全部读取对话框。

ApplicationWindow {
    id: root
    
    // 全部读取对话框
    Dialog {
        id: readAllDialog
        x: (root.width - width) / 2
        y: (root.height - height) / 2
        width: Math.min(root.width - 100, 600)
        height: Math.min(root.height - 100, 500)
        modal: true
        title: "文本内容"
        
        property string text: ""
        
        background: Rectangle {
            color: "white"
            radius: 15
            border.color: "#CCCCCC"
            border.width: 2
        }
        
        contentItem: ScrollView {
            width: readAllDialog.width - 40
            height: readAllDialog.height - 120
            
            TextArea {
                id: dialogTextArea
                width: parent.width
                readOnly: true
                text: readAllDialog.text
                font.pixelSize: 24
                wrapMode: TextArea.Wrap
                selectByMouse: true
            }
        }
        
        footer: DialogButtonBox {
            Button {
                text: "确定"
                DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
                onClicked: {
                    clearText()
                    readAllDialog.close()
                }
            }
            
            Button {
                text: "取消"
                DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
                onClicked: {
                    readAllDialog.close()
                }
            }
        }
    }
    
    // 查找结果对话框
    Dialog {
        id: findResultDialog
        x: (root.width - width) / 2
        y: (root.height - height) / 2
        width: Math.min(root.width - 100, 500)
        height: 200
        modal: true
        title: "操作结果"
        
        property string text: ""
        
        contentItem: Text {
            text: findResultDialog.text
            font.pixelSize: 22
            wrapMode: Text.Wrap
            color: "#333333"
        }
        
        footer: DialogButtonBox {
            Button {
                text: "确定"
                DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
                onClicked: {
                    findResultDialog.close()
                }
            }
        }
    }
}

关键点:

  • 居中显示:使用 (root.width - width) / 2 计算居中位置
  • 模态对话框:使用 modal: true 实现模态对话框
  • 自定义样式:使用 background 自定义对话框背景
  • 按钮角色:使用 DialogButtonBox.buttonRole 设置按钮角色

11. ⚠️ 关键配置:deviceTypes 必须包含 “2in1”

这是本文档最重要的发现!

在 entry/src/main/module.json5 文件中,deviceTypes 必须包含 "2in1":

{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": [
      "default",
      "tablet",
      "2in1"  // ⚠️ 必须添加!否则打包会失败
    ],
    // ...
  }
}

错误信息:

hvigor ERROR: Failed :entry:default@PackageHap...
Ohos BundleTool [Error]: 10011001 Parse and check args invalid in hap mode.
Error Message: --json-path must be the config.json file or module.json file.

原因分析:

  • HarmonyOS PC 设备(如 MateBook)被识别为 "2in1" 设备类型
  • 如果 deviceTypes 中缺少 "2in1",打包工具无法正确识别配置文件路径
  • 这会导致打包失败,即使应用能在真机上运行

最佳实践:

"deviceTypes": [
  "default",   // 手机
  "tablet",    // 平板
  "2in1"       // ⚠️ PC/2合1设备(必须添加!)
]

🐛 常见问题与解决方案

问题 1:文本样式不生效

症状:勾选样式复选框后,文本样式没有变化。

原因:

  1. TextArea 的样式属性没有绑定到全局属性
  2. 属性绑定路径错误

解决方案:

// ✅ 正确:绑定到全局属性
TextArea {
    id: textEdit
    font.underline: textUnderline  // 绑定到 root.textUnderline
    font.italic: textItalic
    font.bold: textBold
    color: textColor
}

// ❌ 错误:没有绑定
TextArea {
    id: textEdit
    font.underline: false  // 硬编码,不会变化
}

问题 2:查找功能找不到文本

症状:输入查找文本后,提示找不到。

原因:

  1. searchText 属性没有正确更新
  2. 文本内容为空或格式不匹配

解决方案:

// ✅ 正确:在输入框的 onTextChanged 中更新
TextInput {
    id: searchInput
    onTextChanged: {
        searchText = text  // 实时更新
    }
}

// ✅ 正确:查找前检查
function findText() {
    if (searchText.trim() === "") {
        findResultDialog.text = "请输入要查找的文本"
        findResultDialog.open()
        return
    }
    // ...
}

问题 3:文本统计不准确

症状:文本统计的字符数、行数、词数不准确。

原因:

  1. 统计函数没有在文本变化时调用
  2. 统计算法有误

解决方案:

// ✅ 正确:在 onTextChanged 中更新统计
TextArea {
    id: textEdit
    onTextChanged: {
        updateTextStats()  // 文本变化时更新
    }
}

// ✅ 正确:使用正确的统计算法
function updateTextStats() {
    var text = textEdit.text
    textLength = text.length  // 字符数
    var lines = text.split("\n")
    textLineCount = lines.length  // 行数
    var words = text.trim().split(/\s+/).filter(function(word) { return word.length > 0 })
    textWordCount = words.length  // 词数
}

问题 4:对话框不显示

症状:调用 dialog.open() 后,对话框不显示。

原因:

  1. 对话框的 modal 属性设置错误
  2. 对话框的父窗口没有正确设置

解决方案:

// ✅ 正确:设置 modal 和父窗口
Dialog {
    id: myDialog
    modal: true  // 设置为模态对话框
    parent: root  // 设置父窗口
    
    onOpened: {
        console.log("对话框已打开")
    }
}

// ✅ 正确:使用 open() 方法
function showDialog() {
    myDialog.open()
}

问题 5:TextArea 无法滚动

症状:文本内容超出 TextArea 高度,但无法滚动。

原因:

  1. TextArea 没有放在 ScrollView 中
  2. height 设置不正确

解决方案:

// ✅ 正确:使用 ScrollView 包裹 TextArea
ScrollView {
    anchors.fill: parent
    
    TextArea {
        id: textEdit
        width: parent.width
        height: Math.max(500, implicitHeight)  // 使用 implicitHeight
        wrapMode: TextArea.Wrap
    }
}

// ❌ 错误:直接使用 TextArea,没有 ScrollView
TextArea {
    id: textEdit
    height: 500  // 固定高度,无法滚动
}

问题 6:打包失败 - json-path 错误

症状:

hvigor ERROR: Failed :entry:default@PackageHap...
Error Message: --json-path must be the config.json file or module.json file.

原因:module.json5 中的 deviceTypes 缺少 "2in1"。

解决方案:

// entry/src/main/module.json5
{
  "module": {
    "deviceTypes": [
      "default",
      "tablet",
      "2in1"  // ⚠️ 必须添加!
    ]
  }
}

💡 最佳实践

1. 文本样式控制

ApplicationWindow {
    id: root
    
    // 定义样式属性
    property bool textUnderline: false
    property bool textItalic: false
    property bool textBold: false
    
    // 使用 CheckBox 控制样式
    CheckBox {
        checked: textUnderline
        onCheckedChanged: textUnderline = checked
    }
    
    // 绑定到 TextArea
    TextArea {
        font.underline: textUnderline
        font.italic: textItalic
        font.bold: textBold
    }
}

2. 文本颜色控制

ApplicationWindow {
    id: root
    
    // 使用索引映射颜色
    property int textColorIndex: 0
    property color textColor: {
        if (textColorIndex === 0) return "black"
        else if (textColorIndex === 1) return "red"
        else return "blue"
    }
    
    // 使用 RadioButton 控制颜色
    RadioButton {
        checked: textColorIndex === 0
        onCheckedChanged: if (checked) textColorIndex = 0
    }
    
    // 绑定到 TextArea
    TextArea {
        color: textColor
    }
}

3. 文本统计

// 实时更新统计
TextArea {
    id: textEdit
    onTextChanged: {
        updateTextStats()
    }
}

function updateTextStats() {
    var text = textEdit.text
    textLength = text.length
    textLineCount = text.split("\n").length
    textWordCount = text.trim().split(/\s+/).filter(function(word) { return word.length > 0 }).length
}

4. 查找替换

// 查找文本
function findText() {
    var index = textEdit.text.indexOf(searchText)
    if (index >= 0) {
        textEdit.select(index, index + searchText.length)
        textEdit.cursorPosition = index + searchText.length
    }
}

// 替换文本
function replaceText() {
    var text = textEdit.text
    if (text.indexOf(searchText) >= 0) {
        textEdit.text = text.replace(searchText, replaceText)
    }
}

// 全部替换
function replaceAll() {
    var text = textEdit.text
    while (text.indexOf(searchText) >= 0) {
        text = text.replace(searchText, replaceText)
    }
    textEdit.text = text
}

5. 文本格式化

// 转大写
function toUpperCase() {
    textEdit.text = textEdit.text.toUpperCase()
}

// 转小写
function toLowerCase() {
    textEdit.text = textEdit.text.toLowerCase()
}

// 首字母大写
function toTitleCase() {
    var lines = textEdit.text.split("\n")
    var result = []
    for (var i = 0; i < lines.length; i++) {
        var line = lines[i]
        if (line.length > 0) {
            line = line.charAt(0).toUpperCase() + line.slice(1).toLowerCase()
        }
        result.push(line)
    }
    textEdit.text = result.join("\n")
}

6. 对话框使用

Dialog {
    id: myDialog
    modal: true
    title: "标题"
    
    property string text: ""
    
    contentItem: Text {
        text: myDialog.text
        wrapMode: Text.Wrap
    }
    
    footer: DialogButtonBox {
        Button {
            text: "确定"
            DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
            onClicked: myDialog.close()
        }
    }
}

// 使用对话框
function showDialog() {
    myDialog.text = "对话框内容"
    myDialog.open()
}

📊 项目结构

TextEditorPro/
├── AppScope/
│   └── app.json5              # 应用配置
├── entry/
│   ├── build-profile.json5    # 构建配置
│   ├── src/
│   │   ├── main/
│   │   │   ├── cpp/
│   │   │   │   ├── main.cpp   # C++ 入口(qtmain)
│   │   │   │   ├── main.qml   # QML UI(多功能文本编辑器)
│   │   │   │   ├── qml.qrc    # 资源文件
│   │   │   │   └── CMakeLists.txt
│   │   │   ├── module.json5   # ⚠️ 必须包含 "2in1"
│   │   │   └── ets/           # ArkTS 代码
│   │   └── ohosTest/
│   └── libs/                   # Qt 库文件
├── image/
│   └── 运行截图.png           # 运行截图
└── build-profile.json5        # 根构建配置

🔧 构建配置要点

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 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  # HarmonyOS 插件
)

module.json5(关键配置)

{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": [
      "default",
      "tablet",
      "2in1"  // ⚠️ 必须添加!否则打包会失败
    ],
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceType": [
      "default",
      "tablet",
      "2in1"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ]
  }
}

📚 参考资源

Qt 官方文档


🎉 总结

通过本项目的开发实践,我们总结了以下关键要点:

  1. ✅ 必须使用 qtmain() 作为入口函数,而不是 main()
  2. ✅ OpenGL ES 配置必须在创建应用之前完成
  3. ✅ deviceTypes 必须包含 "2in1",否则打包会失败
  4. ✅ 使用属性绑定实现文本样式控制,通过 CheckBox 控制样式属性
  5. ✅ 使用索引映射实现文本颜色控制,通过 RadioButton 控制颜色索引
  6. ✅ 使用 TextArea 实现多行文本编辑,支持样式和颜色绑定
  7. ✅ 实时更新文本统计,在 onTextChanged 中调用统计函数
  8. ✅ 使用 indexOf 和 replace 实现查找替换,支持单次和全部替换
  9. ✅ 使用 Dialog 组件实现对话框,支持模态和自定义样式
  10. ✅ 使用 ScrollView 包裹 TextArea,实现文本滚动
  11. ✅ 使用 JavaScript 实现文本格式化,支持大小写转换、去除空格等
  12. ✅ 正确处理文本操作,支持追加行、逐行读取、全部读取、清空等

这些经验对于在 HarmonyOS 平台上开发 Qt 应用至关重要,特别是涉及文本编辑的场景。希望本文档能帮助开发者避免常见陷阱,快速上手 Qt for HarmonyOS 开发,并创建出功能完善的文本编辑器应用。


Logo

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

更多推荐