文章的目的为了记录使用QT QML开发学习的经历。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。
相关链接:
开源 C++ QT QML 开发(四)复杂控件--Listview
开源 C++ QT QML 开发(五)复杂控件--Gridview
推荐链接:
开源 C# 快速开发(十六)数据库--sqlserver增删改查
本章节主要内容是:实现了一个简单文件编辑器应用程序,可以对文本文件和二进制文件打开和保存。
1.代码分析
2.所有源码
3.效果演示
一、代码分析
- FileHandler 头文件分析
 
1.1 类声明和属性定义
class FileHandler : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString fileName READ fileName NOTIFY fileNameChanged)
    Q_PROPERTY(bool isBinary READ isBinary NOTIFY isBinaryChanged)
    Q_PROPERTY(bool fileModified READ fileModified NOTIFY fileModifiedChanged)
    Q_PROPERTY(QVariantList hexData READ hexData NOTIFY hexDataChanged)
    Q_PROPERTY(QString fileContent READ fileContent NOTIFY fileContentChanged)
        分析:
使用 Qt 属性系统,所有属性都是只读的(只有 READ,没有 WRITE)
NOTIFY 信号确保属性变化时 QML 能自动更新
QVariantList 用于在 C++ 和 QML 之间传递复杂数据
1.2 信号定义
signals:
    void fileNameChanged();
    void isBinaryChanged();
    void fileModifiedChanged();
    void hexDataChanged();
    void fileContentChanged();
    void fileOpened(const QString &fileName, bool isBinary);
    void fileSaved(const QString &fileName);
    void fileClosed();
    void errorOccurred(const QString &errorMessage);
        分析:
前5个信号对应属性变化通知
后4个信号是业务逻辑事件通知
fileOpened 和 errorOccurred 携带参数传递详细信息
- FileHandler 实现文件分析
 
2.1 构造函数
FileHandler::FileHandler(QObject *parent)
    : QObject(parent)
    , m_isBinary(false)
    , m_fileModified(false)
{
}
        分析:
初始化成员变量,默认文件类型为文本,未修改状态
2.2 openFile 函数
bool FileHandler::openFile(const QString &filePath)
{
    QFile file(filePath);
    if (!file.exists()) {
        emit errorOccurred(tr("文件不存在: %1").arg(filePath));
        return false;
    }
        文件存在性检查:
使用 QFile::exists() 检查文件是否存在
如果不存在,发射错误信号并返回 false
 if (!file.open(QIODevice::ReadOnly)) {
        emit errorOccurred(tr("无法打开文件: %1").arg(filePath));
        return false;
    }
        文件打开检查:
尝试以只读模式打开文件
如果打开失败(权限问题、文件被占用等),发射错误信号
    m_fileName = filePath;
    m_isBinary = isBinaryFile(filePath);
    if (m_isBinary) {
        // 读取二进制文件
        QByteArray data = file.readAll();
        m_originalData = data;
        m_fileContent.clear();
        processBinaryData(data);
    } else {
        // 读取文本文件
        QTextStream stream(&file);
        stream.setCodec("UTF-8");
        m_fileContent = stream.readAll();
        m_originalData = m_fileContent.toUtf8();
        // 清空二进制数据
        m_hexData.clear();
        emit hexDataChanged();
        emit fileContentChanged();
        emit fileOpened(filePath, false);
    }
        文件内容处理:
二进制文件:直接读取原始数据,调用 processBinaryData 处理
文本文件:使用 QTextStream 以 UTF-8 编码读取,保存内容和原始数据
清理不相关的数据并发射相应信号
file.close();
    m_fileModified = false;
    emit fileNameChanged();
    emit isBinaryChanged();
    emit fileModifiedChanged();
    return true;
}
        收尾工作:
关闭文件,重置修改状态
发射属性变化信号通知界面更新
2.3 processBinaryData 函数
void FileHandler::processBinaryData(const QByteArray &data)
{
    m_hexData.clear();
    // 预分配空间,提高性能
    int lineCount = (data.size() + 15) / 16; // 计算总行数
    m_hexData.reserve(lineCount);
    const uchar *bytes = reinterpret_cast<const uchar*>(data.constData());
    int dataSize = data.size();
        性能优化:
预计算行数:每行16字节,计算需要的行数
预分配 QVariantList 容量避免重复分配
使用原始指针访问提高效率
    for (int i = 0; i < dataSize; i += 16) {
        QString hexLine;
        hexLine.reserve(48); // 预分配空间:16字节 * 3字符
        for (int j = 0; j < 16 && i + j < dataSize; j++) {
            uchar byte = bytes[i + j];
            QString hexByte = QString("%1 ").arg(byte, 2, 16, QChar('0')).toUpper();
            hexLine.append(hexByte);
        }
        // 移除最后一个空格
        if (!hexLine.isEmpty() && hexLine.endsWith(' ')) {
            hexLine.chop(1);
        }
        十六进制格式化:
每行处理最多16个字节
使用 QString::arg() 格式化为2位十六进制,不足补零
移除行末多余的空格
        QVariantMap lineData;
        lineData["address"] = QString("%1").arg(i, 8, 16, QChar('0')).toUpper();
        lineData["hex"] = hexLine;
        m_hexData.append(lineData);
    }
    emit hexDataChanged();
    emit fileOpened(m_fileName, true);
}
        数据结构构建:
使用 QVariantMap 存储每行的地址和十六进制数据
地址格式化为8位十六进制数
发射信号通知数据就绪2.4 saveFile 函数
bool FileHandler::saveFile(const QString &content)
{
    if (m_fileName.isEmpty()) {
        emit errorOccurred(tr("没有指定文件名"));
        return false;
    }
    QFile file(m_fileName);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        emit errorOccurred(tr("无法保存文件: %1").arg(m_fileName));
        return false;
    }
    QTextStream stream(&file);
    stream.setCodec("UTF-8");
    stream << content;
    file.close();
        文本文件保存:
检查文件名有效性
以文本模式写入,使用 UTF-8 编码
使用 QTextStream 确保编码正确
    m_fileContent = content;
    m_originalData = content.toUtf8();
    m_fileModified = false;
    emit fileContentChanged();
    emit fileSaved(m_fileName);
    emit fileModifiedChanged();
    return true;
}
        状态更新:
更新内存中的内容和原始数据
重置修改标志
发射保存成功信号2.5 saveBinaryFile 函数
bool FileHandler::saveBinaryFile(const QVariantList &hexData)
{
    // ... 文件名和文件打开检查类似 saveFile ...
    // 将十六进制数据转换回二进制
    QByteArray binaryData;
    for (const QVariant &line : hexData) {
        QString hexLine = line.toString();
        QByteArray lineData = hexStringToByteArray(hexLine);
        binaryData.append(lineData);
    }
    file.write(binaryData);
    file.close();
        二进制数据重建:
遍历十六进制数据列表
调用 hexStringToByteArray 转换每行数据
直接写入二进制数据2.6 hexStringToByteArray 函数
QByteArray FileHandler::hexStringToByteArray(const QString &hexString)
{
    QByteArray byteArray;
    QStringList hexBytes = hexString.split(' ', Qt::SkipEmptyParts);
    byteArray.reserve(hexBytes.size());
    for (const QString &hexByte : hexBytes) {
        bool ok;
        char byte = static_cast<char>(hexByte.toInt(&ok, 16));
        if (ok) {
            byteArray.append(byte);
        }
    }
    return byteArray;
}
        十六进制解析:
按空格分割十六进制字符串
使用 toInt(&ok, 16) 将十六进制字符串转换为整数
只添加成功转换的字节2.7 isBinaryFile 函数
bool FileHandler::isBinaryFile(const QString &filePath)
{
    QFileInfo fileInfo(filePath);
    QString extension = fileInfo.suffix().toLower();
    static const QStringList binaryExtensions = {
        "exe", "dll", "bin", "dat", "img", "so", "dylib",
        "jpg", "jpeg", "png", "gif", "bmp", "ico",
        "pdf", "doc", "docx", "xls", "xlsx",
        "zip", "rar", "7z", "tar", "gz"
    };
    return binaryExtensions.contains(extension);
}
        文件类型判断:
基于文件扩展名的简单判断
使用 static const 避免重复构造列表
包含常见二进制文件格式
- QML 界面分析
 
3.1 二进制显示更新函数
function updateBinaryDisplay() {
    var displayText = ""
    for (var i = 0; i < fileHandler.hexData.length; i++) {
        var line = fileHandler.hexData[i]
        displayText += line.address + "    " + line.hex + "\n"
    }
    textArea.text = displayText
}
        显示格式化:
遍历十六进制数据,构建显示字符串
格式:地址 十六进制数据
每行以换行符结束3.2 保存二进制文件逻辑
var lines = textArea.text.split('\n')
var hexData = []
for (var i = 0; i < lines.length; i++) {
    if (lines[i].trim() === "") continue
    var lineParts = lines[i].split('    ')
    if (lineParts.length >= 2) {
        hexData.push(lineParts[1].trim())
    }
}
        fileHandler.saveBinaryFile(hexData)
数据提取:
从显示的文本中解析出十六进制数据
跳过空行,按分隔符分割地址和十六进制数据
只提取十六进制部分传递给 C++
二、所有源码
FileHandler.h文件源码
#ifndef FILEHANDLER_H
#define FILEHANDLER_H
#include <QObject>
#include <QString>
#include <QByteArray>
#include <QFile>
#include <QTextStream>
#include <QDataStream>
#include <QVector>
#include <QVariantList>
#include <QFileInfo>
class FileHandler : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString fileName READ fileName NOTIFY fileNameChanged)
    Q_PROPERTY(bool isBinary READ isBinary NOTIFY isBinaryChanged)
    Q_PROPERTY(bool fileModified READ fileModified NOTIFY fileModifiedChanged)
    Q_PROPERTY(QVariantList hexData READ hexData NOTIFY hexDataChanged)
    Q_PROPERTY(QString fileContent READ fileContent NOTIFY fileContentChanged)
public:
    explicit FileHandler(QObject *parent = nullptr);
    QString fileName() const { return m_fileName; }
    bool isBinary() const { return m_isBinary; }
    bool fileModified() const { return m_fileModified; }
    QVariantList hexData() const { return m_hexData; }
    QString fileContent() const { return m_fileContent; }
    Q_INVOKABLE bool openFile(const QString &filePath);
    Q_INVOKABLE bool saveFile(const QString &content);
    Q_INVOKABLE bool saveBinaryFile(const QVariantList &hexData);
    Q_INVOKABLE void closeFile();
    Q_INVOKABLE bool isBinaryFile(const QString &filePath);
signals:
    void fileNameChanged();
    void isBinaryChanged();
    void fileModifiedChanged();
    void hexDataChanged();
    void fileContentChanged();
    void fileOpened(const QString &fileName, bool isBinary);
    void fileSaved(const QString &fileName);
    void fileClosed();
    void errorOccurred(const QString &errorMessage);
private:
    void processBinaryData(const QByteArray &data);
    QByteArray hexStringToByteArray(const QString &hexString);
    QString m_fileName;
    bool m_isBinary;
    bool m_fileModified;
    QByteArray m_originalData;
    QVariantList m_hexData;
    QString m_fileContent;
};
#endif // FILEHANDLER_H
        FileHandler.cpp文件源码
#include "filehandler.h"
#include <QDebug>
#include <QFileInfo>
FileHandler::FileHandler(QObject *parent)
    : QObject(parent)
    , m_isBinary(false)
    , m_fileModified(false)
{
}
bool FileHandler::openFile(const QString &filePath)
{
    QFile file(filePath);
    if (!file.exists()) {
        emit errorOccurred(tr("文件不存在: %1").arg(filePath));
        return false;
    }
    if (!file.open(QIODevice::ReadOnly)) {
        emit errorOccurred(tr("无法打开文件: %1").arg(filePath));
        return false;
    }
    m_fileName = filePath;
    m_isBinary = isBinaryFile(filePath);
    if (m_isBinary) {
        // 读取二进制文件 - 使用更高效的方式
        QByteArray data = file.readAll();
        m_originalData = data;
        m_fileContent.clear();
        processBinaryData(data);
    } else {
        // 读取文本文件
        QTextStream stream(&file);
        stream.setCodec("UTF-8");
        m_fileContent = stream.readAll();
        m_originalData = m_fileContent.toUtf8();
        // 清空二进制数据
        m_hexData.clear();
        emit hexDataChanged();
        // 发送文件内容变化信号
        emit fileContentChanged();
        // 发送信号通知文件已打开
        emit fileOpened(filePath, false);
    }
    file.close();
    m_fileModified = false;
    emit fileNameChanged();
    emit isBinaryChanged();
    emit fileModifiedChanged();
    return true;
}
bool FileHandler::saveFile(const QString &content)
{
    if (m_fileName.isEmpty()) {
        emit errorOccurred(tr("没有指定文件名"));
        return false;
    }
    QFile file(m_fileName);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        emit errorOccurred(tr("无法保存文件: %1").arg(m_fileName));
        return false;
    }
    QTextStream stream(&file);
    stream.setCodec("UTF-8");
    stream << content;
    file.close();
    m_fileContent = content;
    m_originalData = content.toUtf8();
    m_fileModified = false;
    emit fileContentChanged();
    emit fileSaved(m_fileName);
    emit fileModifiedChanged();
    return true;
}
bool FileHandler::saveBinaryFile(const QVariantList &hexData)
{
    if (m_fileName.isEmpty()) {
        emit errorOccurred(tr("没有指定文件名"));
        return false;
    }
    QFile file(m_fileName);
    if (!file.open(QIODevice::WriteOnly)) {
        emit errorOccurred(tr("无法保存文件: %1").arg(m_fileName));
        return false;
    }
    // 将十六进制数据转换回二进制
    QByteArray binaryData;
    for (const QVariant &line : hexData) {
        QString hexLine = line.toString();
        QByteArray lineData = hexStringToByteArray(hexLine);
        binaryData.append(lineData);
    }
    file.write(binaryData);
    file.close();
    m_originalData = binaryData;
    m_fileModified = false;
    emit fileSaved(m_fileName);
    emit fileModifiedChanged();
    return true;
}
void FileHandler::closeFile()
{
    m_fileName.clear();
    m_isBinary = false;
    m_fileModified = false;
    m_originalData.clear();
    m_hexData.clear();
    m_fileContent.clear();
    emit fileNameChanged();
    emit isBinaryChanged();
    emit fileModifiedChanged();
    emit hexDataChanged();
    emit fileContentChanged();
    emit fileClosed();
}
bool FileHandler::isBinaryFile(const QString &filePath)
{
    QFileInfo fileInfo(filePath);
    QString extension = fileInfo.suffix().toLower();
    static const QStringList binaryExtensions = {
        "exe", "dll", "bin", "dat", "img", "so", "dylib",
        "jpg", "jpeg", "png", "gif", "bmp", "ico",
        "pdf", "doc", "docx", "xls", "xlsx",
        "zip", "rar", "7z", "tar", "gz"
    };
    return binaryExtensions.contains(extension);
}
void FileHandler::processBinaryData(const QByteArray &data)
{
    m_hexData.clear();
    // 预分配空间,提高性能
    int lineCount = (data.size() + 15) / 16; // 计算总行数
    m_hexData.reserve(lineCount);
    // 使用更高效的处理方式
    const uchar *bytes = reinterpret_cast<const uchar*>(data.constData());
    int dataSize = data.size();
    for (int i = 0; i < dataSize; i += 16) {
        QString hexLine;
        hexLine.reserve(48); // 预分配空间:16字节 * 3字符(2位十六进制+1空格)
        for (int j = 0; j < 16 && i + j < dataSize; j++) {
            uchar byte = bytes[i + j];
            // 构建十六进制字符串 - 修复的方法
            QString hexByte = QString("%1 ").arg(byte, 2, 16, QChar('0')).toUpper();
            hexLine.append(hexByte);
        }
        // 移除最后一个空格
        if (!hexLine.isEmpty() && hexLine.endsWith(' ')) {
            hexLine.chop(1);
        }
        // 创建行数据 - 只包含地址和十六进制数据
        QVariantMap lineData;
        lineData["address"] = QString("%1").arg(i, 8, 16, QChar('0')).toUpper();
        lineData["hex"] = hexLine;
        m_hexData.append(lineData);
    }
    emit hexDataChanged();
    emit fileOpened(m_fileName, true);
}
QByteArray FileHandler::hexStringToByteArray(const QString &hexString)
{
    QByteArray byteArray;
    QStringList hexBytes = hexString.split(' ', Qt::SkipEmptyParts);
    byteArray.reserve(hexBytes.size());
    for (const QString &hexByte : hexBytes) {
        bool ok;
        char byte = static_cast<char>(hexByte.toInt(&ok, 16));
        if (ok) {
            byteArray.append(byte);
        }
    }
    return byteArray;
}
        main.qml文件源码
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtQuick.Dialogs 1.3
import QtQuick.Layouts 1.12
import FileEditor 1.0
ApplicationWindow {
    id: mainWindow
    width: 800
    height: 600
    title: "文件编辑器 - " + (fileHandler.fileName ? fileHandler.fileName : "未命名")
    visible: true
    FileHandler {
        id: fileHandler
        onFileOpened: {
            console.log("文件已打开:", fileName, "二进制:", isBinary)
            if (isBinary) {
                updateBinaryDisplay()
            } else {
                // 文本文件:直接将内容设置到 TextArea
                textArea.text = fileHandler.fileContent
            }
            mainWindow.title = "文件编辑器 - " + fileName
        }
        onFileContentChanged: {
            // 当文件内容变化时更新显示(对于文本文件)
            if (!fileHandler.isBinary && fileHandler.fileName) {
                textArea.text = fileHandler.fileContent
            }
        }
        onFileSaved: {
            console.log("文件已保存:", fileName)
            infoDialog.text = "文件已保存: " + fileName
            infoDialog.open()
        }
        onFileClosed: {
            console.log("文件已关闭")
            textArea.text = ""
            mainWindow.title = "文件编辑器 - 未命名"
        }
        onErrorOccurred: {
            console.error("错误:", errorMessage)
            errorDialog.text = errorMessage
            errorDialog.open()
        }
        onHexDataChanged: {
            if (fileHandler.isBinary) {
                updateBinaryDisplay()
            }
        }
    }
    // 更新二进制显示的函数
    function updateBinaryDisplay() {
        var displayText = ""
        for (var i = 0; i < fileHandler.hexData.length; i++) {
            var line = fileHandler.hexData[i]
            // 只显示地址和十六进制数据,移除 ASCII 部分
            displayText += line.address + "    " + line.hex + "\n"
        }
        textArea.text = displayText
    }
    // 菜单栏
    menuBar: MenuBar {
        Menu {
            title: "文件"
            MenuItem {
                text: "打开"
                onTriggered: openDialog.open()
            }
            MenuItem {
                text: "保存"
                onTriggered: {
                    if (fileHandler.isBinary) {
                        // 对于二进制文件,需要从显示的文本中提取十六进制数据
                        var lines = textArea.text.split('\n')
                        var hexData = []
                        for (var i = 0; i < lines.length; i++) {
                            if (lines[i].trim() === "") continue
                            var lineParts = lines[i].split('    ')
                            if (lineParts.length >= 2) {
                                hexData.push(lineParts[1].trim())
                            }
                        }
                        fileHandler.saveBinaryFile(hexData)
                    } else {
                        fileHandler.saveFile(textArea.text)
                    }
                }
            }
            MenuSeparator {}
            MenuItem {
                text: "关闭"
                onTriggered: fileHandler.closeFile()
            }
            MenuSeparator {}
            MenuItem {
                text: "退出"
                onTriggered: Qt.quit()
            }
        }
        Menu {
            title: "查看"
            MenuItem {
                text: "文本模式"
                enabled: fileHandler.fileName && fileHandler.isBinary
                onTriggered: {
                    // 重新以文本模式打开文件
                    var currentFile = fileHandler.fileName
                    fileHandler.closeFile()
                    fileHandler.openFile(currentFile)
                }
            }
            MenuItem {
                text: "十六进制模式"
                enabled: fileHandler.fileName && !fileHandler.isBinary
                onTriggered: {
                    // 重新以二进制模式打开文件
                    var currentFile = fileHandler.fileName
                    fileHandler.closeFile()
                    fileHandler.openFile(currentFile)
                }
            }
        }
    }
    // 主编辑区域
    ScrollView {
        anchors.fill: parent
        anchors.margins: 10
        TextArea {
            id: textArea
            width: parent.width
            height: parent.height
            font.family: fileHandler.isBinary ? "Courier New" : "Arial"
            font.pointSize: 10
            wrapMode: TextArea.Wrap
            selectByMouse: true
            placeholderText: "请打开一个文件开始编辑..."
            // 监听文本变化,用于设置修改状态
            onTextChanged: {
                if (fileHandler.fileName && !fileHandler.isBinary) {
                    // 可以在这里添加修改状态的逻辑
                }
            }
        }
    }
    // 状态栏
    footer: ToolBar {
        RowLayout {
            anchors.fill: parent
            Label {
                text: {
                    if (fileHandler.fileName) {
                        if (fileHandler.isBinary) {
                            return "十六进制模式 - " + fileHandler.fileName
                        } else {
                            return "文本模式 - " + fileHandler.fileName
                        }
                    } else {
                        return "就绪"
                    }
                }
                Layout.fillWidth: true
            }
            Label {
                text: "字符数: " + textArea.length
            }
        }
    }
    // 文件对话框
    FileDialog {
        id: openDialog
        title: "选择文件"
        selectMultiple: false
        nameFilters: ["所有文件 (*)"]
        onAccepted: {
            var filePath = openDialog.fileUrl.toString().replace("file:///", "")
            fileHandler.openFile(filePath)
        }
    }
    // 信息对话框
    MessageDialog {
        id: infoDialog
        title: "信息"
        icon: StandardIcon.Information
    }
    // 错误对话框
    MessageDialog {
        id: errorDialog
        title: "错误"
        icon: StandardIcon.Critical
    }
    // 键盘快捷键
    Shortcut {
        sequence: "Ctrl+O"
        onActivated: openDialog.open()
    }
    Shortcut {
        sequence: "Ctrl+S"
        onActivated: {
            if (fileHandler.isBinary) {
                var lines = textArea.text.split('\n')
                var hexData = []
                for (var i = 0; i < lines.length; i++) {
                    if (lines[i].trim() === "") continue
                    var lineParts = lines[i].split('    ')
                    if (lineParts.length >= 2) {
                        hexData.push(lineParts[1].trim())
                    }
                }
                fileHandler.saveBinaryFile(hexData)
            } else {
                fileHandler.saveFile(textArea.text)
            }
        }
    }
    Shortcut {
        sequence: "Ctrl+W"
        onActivated: fileHandler.closeFile()
    }
}
        main.cpp文件源码
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "filehandler.h"
int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);
    // 设置应用信息
    app.setApplicationName("文件编辑器");
    app.setApplicationVersion("1.0");
    app.setOrganizationName("MyCompany");
    // 注册 C++ 类型到 QML
    qmlRegisterType<FileHandler>("FileEditor", 1, 0, "FileHandler");
    QQmlApplicationEngine engine;
    // 加载 QML 文件
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);
    return app.exec();
}
        三、效果演示
打开二进制文件的效果,可以进行编辑保存。
