写在前面
笔者昨天刚刚收到课设的截止时间要求,距离写这篇博客的时间还有一个月,我从申请自命题课设到今天已经27天了,先用两周时间学Qt,然后就开始做这个项目,现在已经快把基础功能全部实现了。
目前的打算是完成基础功能让学长能下手进行拓展就可以了,至于一些进阶的内容笔者想沉淀完设计模式重构代码之后再加,要不然现在加的内容越多,重构的时候越困难。反正离课设验收还有一个月,时间上绰绰有余。
好了,言归正传,本篇博客记录Tick-Task的笔记板块实现,这一板块在普通的增删改查基础上又多了很多东西:一个文本编辑器肯定不只是能打字,什么文本字体字号段落加粗变颜色下划线都要有,还需要能插入图片,可以说就是一个缩减版的word,而这部分也是笔者没接触过的,所以对我来说还是有点难实现的
需求分析

要在这个界面上加东西,作为一个文本编辑器应该有的功能
笔者计划在这里添加的功能有:
正常支持文本输入;
撤销操作,取消撤销操作;
设置字体的类型、大小、颜色、加粗、倾斜、下划线;
插入图片;
实现思路
(1)辅助控件
首先要在上边那个界面里加控件,这里来介绍几个控件:
QTextEdit:是一个很常见的控件,它已经在图中界面出现了,可以实现文本输入删除光标选中之类的
QFontCombox:这个控件是一个字体选择器,可以选择字体
QSpinBox:这个控件可以调整字号
QColorDialog:这不是一个可视化的控件,但是可以通过和pushButton连接来实现选择字体颜色
(2)核心原理
功能根据选择模式可以分为两种:一种是改掉光标以后的内容,即光标之后的变成用户想要的格式,一种是改变用户选中的内容,即用户用鼠标拖出的一片区域
其实这些功能都可以调用API接口直接实现,需要通过QTextCursor捕获选中范围,再通过QTextCursor::mergeCharFormat()这个函数改变格式就行
(3)梳理顺序
首先,在note.ui里添加需要的控件,包括撤销重做、字体选择器、调整字号、颜色选择器、段落调节
然后,实现新建和修改功能,这一部分和前三个板块思路类似
接着,要把控件的槽函数写出来,核心内容就是把选中文本捕获,然后修改对应文本的格式
具体实现
(1)新增控件

暂时先根据这个界面来实现功能
注意要修改新建出来的控件的命名
(2)实现新建修改
这一部分因为文本内容太多,而且是富文本,用表格视图有些不美观,所以表格里只展示笔记的标题,笔记的具体内容应该和存储联系起来之后再做
新建可以做,就是点击新建按钮然后跳到第二个界面,然后开始输入内容,存储不能实现,这个要连接数据库,然后把标题传回去,文本内容清空,等待下一次点击
修改也可以粗略地做一下,点击对应行,然后连接数据库把标题对应的内容传到第二个界面里,然后保存之后再把文本框的内容传到数据库
cpp
//这个函数的作用是获取并初始化TableView控件
void Note::GetNoteDecideTableView()
{
//获取界面中的表格对象
QTableView *noteDecide = ui->noteDecideTableView;
//设置表格的点击模式为行单选
ui->noteDecideTableView->setSelectionBehavior(QAbstractItemView::SelectRows); // 选择整行
ui->noteDecideTableView->setSelectionMode(QAbstractItemView::SingleSelection); // 单选模式
noteDecide->setModel(model);
}
先把表格初始化一下
cpp
//这个函数的作用是保存已经新建或修改的笔记-------------------未完成
void Note::on_note_applyButton_clicked()
{
ui->stackedWidget->setCurrentIndex(0);
if(addOrRevise==1)
{
//这行代码的作用是获取第二个界面输入框里的文本
QString title=ui->notenext_nameInput->text();
//这几行代码的作用是向tableView里添加一行
int row = model->rowCount();
model->insertRow(row);
QStandardItem *item = new QStandardItem(title);
model->setItem(row, item);
ui->noteDecideTableView->setColumnWidth(row, 381);
//textEdit中的内容存储过程-----------------------------------------------------------------------------未完成
//这句代码的作用是清空这个数组以及第二个界面的内容,方便下次使用
ui->notenext_nameInput->setText("");
ui->noteTextEdit->setText("");
addOrRevise=0;
}
}
这个是新建模式的保存代码 ,核心内容就是往表格插入一行标题,而富文本输入框的存储没有实现
cpp
//这个函数的作用是修改已存在的笔记-------------------------未完成
void Note::on_note_reviseButton_clicked()
{
ui->stackedWidget->setCurrentIndex(1);
//这段代码的作用是将表格中被选中的行的标题传到第二个界面里
if (currentRow != -1)
{
QModelIndex index = model->index(currentRow,0);
QVariant data = model->data(index);
QString title=(data.toString());
ui->notenext_nameInput->setText(title);
}
//把数据库中对应的文本传到第二个界面的textEdit中-------------------------------------------------------未完成
ui->stackedWidget->setCurrentIndex(1);
addOrRevise=2;
}
点击修改之后把文本内容传过去
cpp
else if(addOrRevise==2)
{
//这行代码的作用是获取第二个界面输入框里的文本
QString title=ui->notenext_nameInput->text();
//这几行代码的作用是向tableView选中的一行修改
QStandardItem *item = new QStandardItem(title);
model->setItem(currentRow, item);
ui->noteDecideTableView->setColumnWidth(currentRow, 381);
//textEdit中的内容存储过程-----------------------------------------------------------------------------未完成
//这句代码的作用是清空这个数组以及第二个界面的内容,方便下次使用
ui->notenext_nameInput->setText("");
ui->noteTextEdit->setText("");
addOrRevise=0;
}
点击保存再改对应行的内容
在不连接数据库的情况下,这个新建修改是四个板块中最好实现的,但是事实上连接数据库反而更麻烦 ,那是之后的事情
(3)实现文本编辑
先实现撤销和重做功能,这两个直接调用QTextEdit的库函数就行
cpp
//撤销按钮的槽函数
void Note::on_undoButton_clicked()
{
ui->noteTextEdit->undo();
}
//重做按钮的槽函数
void Note::on_redoButton_clicked()
{
ui->noteTextEdit->redo();
}
经测试是可以正常使用的,而且自带快捷键(Ctrl+Z)
再实现字体的加粗,注意这里需要把是否选中文本的情况都要考虑到
给加粗这个pushButton控件勾上checkable属性,然后识别它有没有被点就可以选择是细体还是粗体
注意:这里应该是双向传导信息的,选中文本之后再看按钮是什么状态,如果有粗体按钮就变成选中状态,如果没有粗体按钮就是默认状态
实现逻辑:写一个QTextEdit的槽函数,这个槽函数检测文本选中情况的改变,在这个槽函数里写判断是否包含粗体进而修改加粗按钮的选中形式,那这样的话自自然每次加粗之后加粗按钮都要变回默认状态了
cpp
//这个函数的作用是加粗字体和取消加粗字体
void Note::on_boldButton_clicked()
{
//这句的作用是捕获光标选中的位置
QTextCursor currentCursor = ui->noteTextEdit->textCursor();
//这两句的作用是确定加粗是被选中状态还是未选中状态
QTextCharFormat format;
format.setFontWeight(ui->boldButton->isChecked() ? QFont::Bold : QFont::Normal);
//判断是是否有选中的文本,如果有就改变选中的文本,没有就改变光标后的内容
if (currentCursor.hasSelection())
{
//对选中的文本应用格式
currentCursor.mergeCharFormat(format);
ui->noteTextEdit->setTextCursor(currentCursor);
}
else
{
//对后续输入的文本应用格式
ui->noteTextEdit->mergeCurrentCharFormat(format);
}
}
这是加粗按钮的槽函数,修改逻辑是根据按钮被选中状态来实现的
cpp
//这个函数的作用是判断选中文本的状态来决定按钮显示类型--------------------------------------------------------------未实现
void Note::on_noteTextEdit_selectionChanged()
{
//捕获当前选中内容
QTextCursor cursor = ui->noteTextEdit->textCursor();
//判断是否有选中内容
if (!cursor.hasSelection())
{
// 如果没有选中文本,使用当前光标位置的格式
QTextCharFormat format = ui->noteTextEdit->currentCharFormat();
// 安全检查:确保格式对象有效
if (!format.isEmpty())
{
ui->boldButton->setChecked(format.fontWeight() == QFont::Bold);
ui->italicButton->setChecked(format.fontItalic());
}
else
{
//格式对象无效,重置按钮状态
ui->boldButton->setChecked(false);
ui->italicButton->setChecked(false);
}
}
else //这个判断的作用是:如果选中文本有加粗或倾斜,那么按钮就显示选中状态,如果一个加粗或倾斜的字都没有,就显示未选中状态
{
//两个标记变量,初始化都是false
bool hasBold = false;
bool hasItalic = false;
//定一个临时光标,用于遍历操作
QTextCursor tempCursor = cursor;
//把这个临时光标移到选中区域的第一个字符处,准备开始遍历
tempCursor.setPosition(cursor.selectionStart());
//循环:作用是寻找加粗、倾斜字符
while (!tempCursor.atEnd() && tempCursor.position() < cursor.selectionEnd())
{
//临时光标移动操作
tempCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1);
//设一个变量存当前光标指向的字符
QTextCharFormat tempFormat = tempCursor.charFormat();
//只要发现一个加粗字符,就标记为存在加粗
if (tempFormat.fontWeight() == QFont::Bold)
hasBold = true;
//只要发现一个倾斜字符,就标记为存在倾斜
if (tempFormat.fontItalic())
hasItalic = true;
//如果两者都已找到,可以提前结束循环
if (hasBold && hasItalic)
break;
tempCursor.setPosition(tempCursor.selectionEnd());
}
//根据检查结果设置按钮状态,如果有加粗或倾斜按钮就变成加粗或倾斜
ui->boldButton->setChecked(hasBold);
ui->italicButton->setChecked(hasItalic);
}
}
这段代码是实现加粗和倾斜的核心,主要功能是选中文本,然后根据文本状态改变按钮的状态,按钮的槽函数再根据自己的状态修改文本状态
代码逻辑是利用QTextEdit的selectionChanged信号的槽函数,捕获选中区域然后遍历,如果有加粗和倾斜就改变按钮状态
现在实现字体倾斜操作,和加粗差不多方法
cpp
// 这个函数的作用是字体倾斜和取消倾斜
void Note::on_italicButton_clicked()
{
// 捕获光标选中的位置
QTextCursor currentCursor = ui->noteTextEdit->textCursor();
// 确定倾斜按钮是被选中状态还是未选中状态
QTextCharFormat format;
format.setFontItalic(ui->italicButton->isChecked()); // 设置字体倾斜状态
// 判断是否有选中的文本,如果有就改变选中的文本,没有就改变光标后的内容
if (currentCursor.hasSelection())
{
// 对选中的文本应用格式
currentCursor.mergeCharFormat(format);
ui->noteTextEdit->setTextCursor(currentCursor);
}
else
{
// 对后续输入的文本应用格式
ui->noteTextEdit->mergeCurrentCharFormat(format);
}
}
这个函数只有一处地方和加粗函数不同,就是判断是否被选中时的设置函数不同
下面再实现下划线,这个需要在TextEdit的槽函数中添加类似加粗和倾斜的遍历,然后再实现类似的槽函数就行
这里不好附向TextEdit槽函数添加的代码,就附一个下划线按钮的槽函数吧
cpp
//这个函数的作用是字体加下划线和取消下划线
void Note::on_underlineButton_clicked()
{
//捕获光标选中的位置
QTextCursor currentCursor = ui->noteTextEdit->textCursor();
//确定倾斜按钮是被选中状态还是未选中状态
QTextCharFormat format;
format.setFontUnderline(ui->underlineButton->isChecked()); // 设置字体下划线状态
//判断是否有选中的文本,如果有就改变选中的文本,没有就改变光标后的内容
if (currentCursor.hasSelection())
{
//对选中的文本应用格式
currentCursor.mergeCharFormat(format);
ui->noteTextEdit->setTextCursor(currentCursor);
}
else
{
//对后续输入的文本应用格式
ui->noteTextEdit->mergeCurrentCharFormat(format);
}
}
其实,加粗、倾斜、下划线这三个代码重复性的有很多,我觉得这样写一点都不好,不过我一时半会儿想不到什么更好的办法,只能先以出功能为主,做完再考虑重构
接下来再实现字号的改变,这个和前三个有一些区别,前三个是非黑即白类型,但是字号可以有很多,所以实现起来也要比前三个难一点
cpp
//这个函数的作用是根据字号选择框中的内容来改变文本的字号
void Note::on_spinChoice_valueChanged(int value)
{
QTextCursor cursor = ui->noteTextEdit->textCursor();
if (cursor.hasSelection())
{
//选中文本的情况:修改选中部分的字号
QTextCharFormat format;
format.setFontPointSize(value);
cursor.mergeCharFormat(format);
ui->noteTextEdit->setTextCursor(cursor);
}
else
{
//无选中内容:设置后续输入的字号
QTextCharFormat format;
format.setFontPointSize(value);
ui->noteTextEdit->mergeCurrentCharFormat(format);
}
}
这个函数是改变字号输入框的槽函数,核心思路就是传入当前输入框的数值,然后把数值设置为选中文本的字号
在textEdit的selectionChanged的槽函数中也需要加东西,遍历选中文本判断其字号并显示在字号输入框中,如果有字号不统一的情况就清空字号输入框
笔者感觉修改字体的方式和字号的方式差不多,所以接下来再做修改字体的操作
先在QTextEdit的槽函数中向字体的输入框中传递信息,再在输入框的槽函数中修改选中文本的字体,这些都有很强的重复性,就不附代码了
接下来实现文本颜色的改变,这个与前几个功能有些不一样,因为它要弹出一个对话框让用户选择颜色
梳理一下实现逻辑:给颜色按钮设置一个槽函数,这个槽函数的作用是打开选择颜色的对话框,然后用户去选择颜色,改变选中文本的颜色,注意这里也有一个判断,因为打开对话框之后要把当前颜色给设置好,因为这个是打开对话框才显示颜色,所以不需要像前几个功能那样在QTextEdit里改实时变化
这里不附遍历代码了,逻辑和前几个功能一样没什么意思
cpp
// 打开颜色对话框,使用检测到的颜色作为默认值
QColor selectedColor = QColorDialog::getColor(currentColor, this, "选择颜色");
if (selectedColor.isValid())
{
QTextCharFormat format;
format.setForeground(QBrush(selectedColor));
if (cursor.hasSelection())
{
cursor.mergeCharFormat(format);
ui->noteTextEdit->setTextCursor(cursor);
}
else
{
ui->noteTextEdit->mergeCurrentCharFormat(format);
}
}
(4)实现插图功能
直接在插图按钮的槽函数里调用一堆API就可以
cpp
//这个函数的作用是插入图片
void Note::on_insertPictureButton_clicked()
{
//打开文件对话框选择图片,这是个固定传参类型:(父类窗口指针,窗口标题,文件路径,文件过滤器)
QString filePath = QFileDialog::getOpenFileName(
this,
"选择图片",
"",
"图片文件 (*.png *.jpg *.jpeg *.bmp *.gif);;所有文件 (*)"
);
if (!filePath.isEmpty())
{
//获取当前光标位置
QTextCursor cursor = ui->noteTextEdit->textCursor();
//检查文件是否存在且可读
QFile file(filePath);
if (file.exists() && file.open(QIODevice::ReadOnly))
{
//生成唯一的图片名称(避免重复)操作是获取当前时间的毫秒数然后加上文件后缀
QString imageName = "image_" + QString::number(QDateTime::currentMSecsSinceEpoch()) + "." +
QFileInfo(filePath).suffix();
//将图片添加到文档资源中
QImage image(filePath);
if (!image.isNull())
{
//缩放图片(可选,避免过大图片)
if (image.width() > 200)
{ //设置最大宽度
image = image.scaledToWidth(200, Qt::SmoothTransformation);
}
//将图片添加到文档资源中
ui->noteTextEdit->document()->addResource(QTextDocument::ImageResource,
QUrl("file:///" + imageName),
image);
//插入图片到文本中
QTextImageFormat imageFormat;
imageFormat.setName("file:///" + imageName);
imageFormat.setWidth(image.width());
imageFormat.setHeight(image.height());
cursor.insertImage(imageFormat);
ui->noteTextEdit->setTextCursor(cursor);
}
file.close();
}
}
}
简单解释一下这个函数,就是打开选择图片的窗口,然后设置这个窗口的一些属性,接着就对打开的文件路径(注意这里只是路径)操作,这里的逻辑是把文件路径传进QTextEdit自己的文件系统里,然后给这张图设置东西,再把它放到QTextEdit里
篇末总结
至此,Tick-Task课设所有基础功能已经全部实现,剩下的是数据向数据库的存储和取出,以及打卡板块时间的更新。笔者在做这个项目的过程中也有了很多感悟:
-
做项目也是一个学习的过程,很多东西都是在做项目的过程中学会的,经验也会在一次次修改过程中累积
-
调用早就出现的API接口,使用AI是一个很好很有效率的方法,AI不是洪水猛兽,用AI写代码也不丢人
-
饭要一点一点吃,路要一步一步走,程序也要一个小功能一个小功能一步步实现
还有一些反思:
-
QTextEdit控件的selection的槽函数包括的内容实在太多了,这不是一个好事情,事后应该重构
-
使用框架的时候操作最多的就是调用库函数,一直调用库函数,活脱脱一个API工程师,但是这样其实不好,搬砖是没啥技术含量的,一定要多做一些有技术含量的操作才行