文章的目的为了记录使用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();
}
三、效果演示
打开二进制文件的效果,可以进行编辑保存。
