Qt for HarmonyOS TextEditorPro 多功能文本编辑器开源鸿蒙开发实践

📋 项目概述

在这里插入图片描述

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

项目地址:https://gitcode.com/szkygc/HarmonyOs_PC-PGC/blob/main/TextEditorPro

✨ 主要功能

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

🛠️ 技术栈

  • 开发框架: 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 的属性
  • 双向绑定CheckBoxchecked 属性绑定到样式属性
  • 自定义样式:使用 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. 查找替换功能实现

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

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 官方文档

HarmonyOS 开发者社区


🎉 总结

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

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

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


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐