写在前面
笔者的Tick-Task还需要拓展一个功能,即文件上传,还有接上一篇博客中笔记板块的TextEdit内容未实现存储,需要将内容修改为HTML格式方便存储
本篇博客主要涉及到的新技术有:将QTextEdit中的内容转为HTML格式、Qt中的打开选择文件对话框、传入文件路径
需求分析
这次的需求很简单,只有两个:
1. 新增一个“资料上传”板块,然后把文件路径传进去,后续的存储功能是学长实现
2. 将QTextEdit中的内容转为HTML格式
思路梳理
很明显新增一个板块牵扯的东西比较多,因为前期设计界面切换的时候把所有切换逻辑都放在了主界面的类里,这是一个很不好的事情,现在好了,因为耦合性太高导致我还要改动主界面类
不过要加的东西也可以梳理清楚:新建一套文件,类是Document,在MainWindow类里创建这个类的对象,然后就是其他类对象要有的它都要有;然后就是设计它的界面,完成它上传文件路径部分的具体逻辑
HTML格式就先学一下,然后再调用API了,这个比较简单,先实现这个
具体实现
(1)富文本转HTML格式
为了增加这篇博客的技术含量,还有让篇幅长一点,笔者在这里解释一下什么叫做HTML:
HTML是一种标记语言,在CSDN写作的工具栏里可以选择这种语言插入,所以应该正视HTML,它也是一个正儿八经的语言。
那什么是标记语言呢?标记语言和编程语言有什么区别呢?标记语言是一种展示性语言,它没有逻辑,就是通过代码来展示页面,简单来说就是用代码的方式拖控件。标记语言不能实现逻辑,编程语言可以实现逻辑。
回到HTML,它通过“标签”和“元素”来描述结构和内容,告诉富文本编辑器或浏览器如何显示元素,实现布局。它的本质就是一串纯文本,只是可以被解析。具体语法我就不说明了,想细说的话我也要学很久,前端笔者肯定是要学一点的,但不是近期。
笔者笔记板块的QTextEdit控件里的文本有图片,有加粗下划线倾斜,有不同的字体字号,所以转HTML很有必要
其实真正要转是很简单的,只要一句调用API就行
//textEdit中的内容存储过程-----------------------------------------------------------------------------未完成
QString htmlContent = ui->noteTextEdit->toHtml();
这么写笔者心里也没底,所以我要测试一下对不对。具体思路就是把这个传出去的html格式qDebug到输出栏,然后把这串html文本复制到浏览器看看能不能打开
以下是测试环节:
//textEdit中的内容存储过程-----------------------------------------------------------------------------未完成
QString htmlContent = ui->noteTextEdit->toHtml();
qDebug()<<htmlContent;
在转换语句后写一个输出语句,然后运行代码跑一下笔记板块,整一堆富文本和图片
这是随便打的测试样例,记下它现在的样子,很明显看到倾斜加粗下划线字体图片都有了
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html>
<head>
<meta name="qrichtext" content="1" />
<meta charset="utf-8" />
<style type="text/css">
p, li { white-space: pre-wrap; }
hr { height: 1px; border-width: 0; }
li.unchecked::marker { content: "\2610"; }
li.checked::marker { content: "\2612"; }
</style>
</head>
<body style=" font-family:'Microsoft YaHei UI'; font-size:9pt; font-weight:400; font-style:normal;">
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">
<span style=" font-size:15pt; font-style:italic;">爱的</span>
<span style=" font-size:15pt;">色</span>
<span style=" font-size:21pt;">放后来卡号</span>
<span style=" font-size:21pt; text-decoration: underline;">第三方</span>
<span style=" font-size:21pt;">库啊大师</span>
<span style=" font-size:21pt; color:#55aa7f;">傅立刻</span>
<span style=" font-size:21pt;">工</span>
<span style=" font-size:21pt; font-weight:700;">卡很多</span>
<span style=" font-size:21pt;">顾几</span>
<span style=" font-family:'楷体'; font-size:21pt;">号发</span>
<span style=" font-size:21pt;">客户是否健康和</span>
</p>
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">
<img src="file:///image_1748055300640.jpg" width="200" height="115" />
</p>
</body>
</html>
这是qDebug出的内容,当然我肯定是看不懂什么意思的,不过这并不妨碍我用它,
那么在浏览器里打开html文件需要怎么做呢?首先我们需要把这段代码存进一个后缀是html的文件里,然后直接拖到浏览器里就行了
非常悲惨,图片是不能正常打开的
既然这样,那就需要修改文件路径了,只用一句代码转换看来还是不太行
笔者这个时候和学长沟通了一下,我才发现我一直没弄懂这里的逻辑,其实数据库存的不是html文件,而是本机存储的html文件的路径,所以这些html文本我需要在本机开一个文件夹来存储
那这样的话就是需要把传进去的图片文件放到和html文件一起的文件夹里
//确定保存路径(使用时间戳生成唯一文件名,保存到固定目录)
QDateTime currentTime = QDateTime::currentDateTime();
QString timeStamp = currentTime.toString("yyyyMMdd_hhmmss");
QString fileName = timeStamp + ".html";
QString savePath = "E:/Tick-Task/my_html/" + fileName;
//创建保存目录(如果不存在)
QDir dir(QFileInfo(savePath).absolutePath());
if (!dir.exists())
{
if (!dir.mkpath("."))
{
qDebug() << "创建目录失败:" << dir.absolutePath();
return;
}
}
//保存HTML内容到文件
QFile file(savePath);
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream stream(&file);
stream.setEncoding(QStringConverter::Utf8);
stream << htmlContent;
file.close();
}
else
{
qDebug() << "无法保存文件:" << savePath;
}
这段代码只需关注思想,因为在下面几行为了代码的简洁把它移到了一个函数里
解释一下这段代码:这段代码的作用是把html文件保存到我提前开好的文件夹里
当然这段代码是AI写的,因为笔者不知道怎么调库函数,可以说调库一点逻辑都没有,全凭记忆,记住调那个库函数直接调就行了,程序员主要的难点还是实现逻辑。
核心思路是用当前时间来当文件名防止重复,转一个固定格式加后缀名,然后保存目录就是提前开好的文件夹。先开文件再传数据,这是核心思路,以只写形式打开文件,然后把这个html文本用utf-8格式传进文件就行了
接下来再处理图片的问题,笔者在寻找正确方法的过程中经历了无数的失败,归根结底是因为tohtml()这个库函数的局限性,它只能转换为和html相同文件夹的内容,所以需要在插图功能的函数里把插入的图存到html的文件夹,然后html直接用就行
//这几行代码的作用是组合出与html相同路径的存图片的文件夹路径
QString htmlDir = "E:\\Tick-Task\\my_html"; // HTML文件的保存目录(固定路径)
QDir().mkpath(htmlDir + "/images"); // 创建images子目录(如果不存在)
QString uniqueName = "image_" + QUuid::createUuid().toString(QUuid::WithoutBraces) + "." +
QFileInfo(filePath).suffix().toLower(); // 生成唯一的图片名称(使用UUID确保唯一性)
QString targetPath = htmlDir + "/images/" + uniqueName; // 目标路径:images/目录下的唯一文件名
// 复制图片到目标路径
if (QFile::copy(filePath, targetPath))
{
qDebug() << "图片复制成功:" << uniqueName;
}
else
{
qDebug() << "图片复制失败:" << filePath << "→" << targetPath;
}
这段代码是插入功能的函数里加的,作用是向html所在文件夹里开一个存图片的文件夹并把要插入的图片复制一份过去,再给图片起一个名字
//这个函数是辅助函数,把html内容存进给定的文件夹里
void Note::saveHtml()
{
QString htmlContent = ui->noteTextEdit->toHtml();
//用正则表达式暴力替换toHtml()函数生成的残缺文本,把路径改成相对路径
static QRegularExpression regex(R"(src=["']file:///(?:E:/Tick-Task/my_html/images/)?([^"']+)["'])");
htmlContent.replace(regex, "src=\"images/\\1\"");
//调试输出,确认替换是否正确
qDebug() << "替换成功";
//保存文件
QString savePath = "E:/Tick-Task/my_html/" + QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + ".html";
QFile file(savePath);
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream stream(&file);
stream.setEncoding(QStringConverter::Utf8);
stream << htmlContent;
file.close();
qDebug() << "HTML保存成功:" << savePath;
}
else
{
qDebug() << "文件保存失败:" << savePath;
}
}
在保存按钮的槽函数html存储过程中调用这个函数,这个函数的作用是强制把html里不规范的“file:///”格式的路径改成提前开好的绝对路径,然后思想接上边html的存储代码就行了
(2)新增“资料上传”板块
先梳理一下实现顺序:新增一套docupload文件->在docupload.ui里画界面->把其他界面改一下,加一个按钮->把DocUpload对象和MainWindow类联系起来实现界面切换->实现文件路径的保存
表格负责存文件索引,然后可以支持新建删除打开选中文件,第二个界面是上传文件并显示属性,这里有一个需要注意的地方,这个文件的属性肯定不可能让用户随便修改的,所以应该把这个文件的属性输入框设定为只读模式
//这四行代码的作用是设置界面中的资料属性,表示只能由程序运行改变值,用户不能修改值
ui->nameInput->setReadOnly(true);
ui->classInput->setReadOnly(true);
ui->memoryInput->setReadOnly(true);
ui->upTimeInput->setReadOnly(true);
把这四行代码写到构造函数里
写到这里笔者突然想到一些事情:学长那边在同步做多线程的功能,自然会修改其他板块的代码,到时候他们发了新文件,那我自然是要继承的,所以这时修改其他板块毫无意义,到时候还要改
所以笔者决定暂时不弄界面切换,先完成资料上传这个板块应该有的功能
具体思路:先给表格开个表头存储文件名、文件类型、文件大小、文件上传时间->然后实现界面跳转,跳到第二个界面之后上传文件,再把选中文件的属性输出到输出框,点应用就把这个文件的属性存到表格中,这些与前几个板块相似度很高,不同的地方就是对文件属性的提取和文件路径的保存
由于不做界面切换功能,所以为了测试该板块功能,就在主函数里输出一下这个窗口
DocUpload d;
d.show();
由于这个板块的前期准备和前几个板块都很类似,这里就不写了,只附这个板块的特色功能函数
//这个函数的作用是打开文件选择对话框然后选择文件并把文件属性输出出去
void DocUpload::on_uploadButton_clicked()
{
//打开文件选择对话框
QString filePath = QFileDialog::getOpenFileName(
this, //父窗口指针
"选择文件", //对话框标题
QDir::homePath(), //用户主目录
"所有文件 (*);;文本文件 (*.txt);;图像文件 (*.png *.jpg)" //文件过滤器
);
if (!filePath.isEmpty()) {
//用户选择了文件,获取文件信息
QFileInfo fileInfo(filePath);
//获取文件名(带扩展名)
QString name = fileInfo.fileName(); //例如: "example.txt"
//获取文件类型(通过扩展名判断)
QString suffix = fileInfo.suffix(); //例如: "txt"
//获取文件大小(单位:字节)
qint64 fileSize = fileInfo.size(); //例如: 1024
QString endSize = formatFileSize(fileSize); //单位友好化
//获取用户点击"打开"按钮时的系统当前时间
QDateTime selectionTime = QDateTime::currentDateTime();
QString timeStr = selectionTime.toString("yyyy-MM-dd hh:mm:ss");
//以下代码是将获取到的属性存到界面里
ui->nameInput->setText(name);
ui->classInput->setText(suffix);
ui->memoryInput->setText(endSize);
ui->upTimeInput->setText(timeStr);
}
}
//字节转换为可读性友好的内存函数
QString DocUpload::formatFileSize(qint64 bytes)
{
if (bytes < 1024) return QString("%1 B").arg(bytes);
else if (bytes < 1024 * 1024) return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 1);
else if (bytes < 1024 * 1024 * 1024) return QString("%1 MB").arg(bytes / (1024.0 * 1024), 0, 'f', 1);
else return QString("%1 GB").arg(bytes / (1024.0 * 1024 * 1024), 0, 'f', 1);
}
这是上传按钮的槽函数,实现顺序是:打开文件选择框,然后选中文件路径对应的那个文件,用库函数把这个文件开盒,然后把开出来的属性放在界面里
其他的部分都是前几个板块提到好几次的,也没什么好说的了
篇末总结
这一专栏会完整记录笔者的app开发全过程,包括开发和修改的过程,一方面是为了做内容输出提升笔者的技术博客写作能力,另一方面也是记录笔者做项目的过程,防止后续忘记,所以不论难度,只要是一个阶段的任务我都会写一篇博客
说实话笔者刚开始写的时候觉得这篇会很简单,事实上这篇耗时也挺长的,只能说预期和实际的区别还是很大的,要多积累开发经验
今天再附一点感悟:
1. “高内聚低耦合”真的很重要,我前期界面切换耦合性太高,导致新增板块的时候还要动MainWindow里已经写好了的内容
2. 代码复用真的也很重要,如果当时我设计了多态的话,直接调我开好的接口就行,但是现在每次做功能都需要建一遍数据成员和函数
3. 测试的时候运行结果和预期不同是很正常的,计算机是最公正的,只要没跑出正确结果说明有问题的一定是开发者,如果逻辑对了不可能出错
4. AI很好用,没有AI绝对要大大降低开发效率,一是库函数查找不方便,二是代码不好改动,不过要对AI说的话有一个基本的判断,加入自己的逻辑思考之后再问AI
5. 对文件的处理过程中最重要的就是文件路径,文件路径一定要清楚
6. 做项目最怕的是不知道自己不知道,前期我误解了学长的思路,以为是要把html直接存到数据库中,其实和文件相关的内容都是在本机中的,除非上传云,可是我们没开发这个功能