写在前面
本篇主要是为前期不成熟的工作买单,因为我对QListView、QTableView、QTreeView的认识不熟悉,所以所有板块无脑全选了QTableView,现在除规划板块之外其他板块都做完了,我们重新审视这个板块,最终决定使用QTreeView,因为QTreeView有多级节点,很适合一个规划第一阶段干什么第二阶段干什么最后自我评价这种结构的展示
归根结底还是我不懂这个控件,"不知道自己不知道",如果我早就知道它展示的时候这么方便,第一次做我就会用了
还有这个板块是在我技术已经成熟,经手了界面优化、客户端增删改查、后端数据传输的各个流程之后重做的,所以这一篇的板块开发流程相对完善,包括单个板块的界面、客户端和服务器开发
技术栈包括:Qt框架下的tcp通信、QTreeView控件、Qt基本操作
界面设计
首先先构思一个表格的视图,还有讨论究竟需不需要三个界面连在一起来实现

这是板块主界面应该有的样子,底部是导航栏按钮,再上一层是功能按钮,最上边是QTreeView控件,上表头写着事项、起始时间、结束时间、完成情况,左表头写着标题

这是一级标题点击新建修改后跳转的界面,主要就是改名字,为了扩充界面加了一个预期(自我评价部分不好加,决定删去)

这是点击新建修改二级标题之后跳转的界面,主要输入子规划的事件、时间和完成情况
前端界面已经做好了,我们再考虑运行时的样子:用户登录成功之后,客户端就向服务器发送读取请求,若有规划就读到规划板块里,所以这一步要先做出来。然后就是新建修改,一方面要在前端能成功展示出来,一方面也要向服务器发送请求
实现方法
前端各个界面数据互传和前几个板块是类似的,唯一不同的就是QTreeView视图的操作方法,然后就是它深度远比前几个板块深,要涉及到子任务的新建修改,就要牵一发而动全身,还有新建修改删除也要做两遍,一遍规划一遍任务,所以这个最好提前声明好信号和令牌,要不然后边做一次声明一次很烦的,还容易漏
再附一个创建数据库的脚本
sql
use tick_task;
create table plan_duration(
Account VARCHAR(30) NOT NULL,
Pname VARCHAR(50) NOT NULL,
StartTime TIMESTAMP NOT NULL,
EndTime TIMESTAMP NOT NULL,
Evaluation VARCHAR(500),
FOREIGN KEY(Account) REFERENCES Account(Account),
CONSTRAINT unique_account_duration UNIQUE (Account,StartTime,EndTime),
INDEX idx_account_pname (Account, PName)
);
create table plan_mission(
Account VARCHAR(30) NOT NULL,
PName VARCHAR(50) NOT NULL,
MName VARCHAR(50) NOT NULL,
MStartTime TIMESTAMP NOT NULL,
MEndTime TIMESTAMP NOT NULL,
IsStandard BOOLEAN DEFAULT 0,
FOREIGN KEY(Account,PName) REFERENCES plan_duration(Account,PName) ON DELETE CASCADE,
CONSTRAINT unique_account_pname_mname UNIQUE (Account, PName, MName)
);
具体实现
(1)前端准备
先按照构思图拖控件,改命名,连带优化



接下来要做的事情是:初始化TreeView控件,给它设置表头,设置第二个界面输入框的提示词
cpp
//初始化QTreeView控件的函数
void Plan::getTreeView()
{
// 获取界面中的树状视图对象
QTreeView *planShow = ui->planShowTree;
// 设置TreeView的表头
model->setHorizontalHeaderLabels({"事项", "起始时间", "结束时间", "完成情况"});
// 设置树形视图的点击模式
planShow->setSelectionBehavior(QAbstractItemView::SelectRows);
planShow->setSelectionMode(QAbstractItemView::SingleSelection);
// 设置焦点策略
planShow->setFocusPolicy(Qt::NoFocus);
// 设置模型
planShow->setModel(model);
// 配置视图样式
planShow->setIndentation(20); // 设置缩进距离
planShow->setHeaderHidden(false); // 确保表头可见
// 调整列宽策略
planShow->header()->setSectionResizeMode(QHeaderView::Stretch); // 列自动拉伸
}
然后把不需要动脑子的取消按钮给搞定,就是直接返回主界面
(2)大标题新建修改的实现
新建和修改存在大量的一致代码,并且函数也是根据模式变量进入了不同的if分支,所以就一起写了
新建修改前端的第一步都是:跳转界面并设置模式变量,新建的时候界面里数据全空,修改的时候需要把要修改的内容放到第二个界面中
cpp
//新建按钮的槽函数,实现界面跳转和模式选择
void Plan::on_newPlanButton_clicked()
{
ui->stackedWidget->setCurrentIndex(1);
addOrRevise=1;
}
cpp
void Plan::on_revisePlanButton_clicked()
{
ui->stackedWidget->setCurrentIndex(1);
if(model->rowCount() == 0)
{
QMessageBox::warning(this, "提示", "目前暂时没有规划!");
return;
}
else if(currentRow == -1)
{
QMessageBox::warning(this, "提示", "请先选择一行数据!");
return;
}
//不能修改名称
ui->planTitleInput->setReadOnly(1);
//这段代码的作用是将表格中被选中的行的内容依次存进中介数组
if (currentRow != -1)
{
for (int col = 0; col < 4; col++)
{
QModelIndex index = model->index(currentRow, col);
QVariant data = model->data(index);
shiftPlanList.append(data.toString());
}
}
//这段代码的作用是在界面中设置中介数组中的内容
QString format = "yyyy/MM/dd HH:mm";
QDateTime startTime = QDateTime::fromString(shiftPlanList[1],format);
QDateTime endTime = QDateTime::fromString(shiftPlanList[2],format);
ui->planTitleInput->setText(shiftPlanList[0]);
if (startTime.isValid())
{
ui->planStartTime->setDateTime(startTime);
}
else
{
qDebug() << "开始时间转换失败,原始字符串:" << shiftPlanList[1];
}
if (endTime.isValid())
{
ui->planEndTime->setDateTime(endTime);
}
else
{
qDebug() << "开始时间转换失败,原始字符串:" << shiftPlanList[2];
}
ui->planEvaluationInput->setText(shiftPlanList[3]);
//要把中介数组清空,方便下次使用
shiftPlanList.clear();
}
//这个函数是选中对应行
void Plan::on_planShowTree_clicked(const QModelIndex &index)
{
currentRow=index.row();
}
前端界面跳转已经做完了,接下来就是点击应用按钮之后的事情,这个地方本质上都是获取全部内容然后改掉或者建一个,所以只有最后一步是不同的,这个地方感兴趣的可以看看我之前写的方式,实际上两个分支代码重复率很高,这里也算一个小重构吧
cpp
//第一个子界面的应用按钮的槽函数,实现保存第一个子界面控件内容并传回第一个界面
void Plan::on_plannext_applyButton_clicked()
{
ui->stackedWidget->setCurrentIndex(0);
QString planTitle=ui->planTitleInput->text();
QString evaluation=ui->planEvaluationInput->toPlainText();
QDateTime starttime = ui->planStartTime->dateTime();
QDateTime endtime = ui->planEndTime->dateTime();
if (starttime > endtime) {
QMessageBox::warning(this, "提示", "开始时间不能先于结束时间!");
return;
}
pendingplan.pName=planTitle;
pendingplan.Evaluation=evaluation;
pendingplan.StartTime=starttime;
pendingplan.EndTime=endtime;
if(addOrRevise==1)
{
upLoadPlan(pendingplan);
}
else
{
modifyPlan(pendingplan);
}
}
在上述函数发射了信号之后需要在TcpClient类中专门来接收,之后处理
cpp
void TcpClient::SendPlanRequest(const PlanClass &plan, unsigned char istoken)
{
QByteArray data;
int plancount = 1;
QDataStream stream(&data, QIODevice::WriteOnly);
stream << currentuser << plancount;
stream << plan.pName;
stream << plan.Evaluation;
stream << plan.StartTime;
stream << plan.EndTime;
QByteArray sendData;
PreSendData(sendData, data, istoken);
sendResponse(sendData);
}
//这个函数的作用是发送新建的规划
void TcpClient::SendNewPlan(const PlanClass &plan)
{
SendPlanRequest(plan,CLIENT_PLAN_UPLOAD);
}
void TcpClient::SendModifyPlan(const PlanClass &plan)
{
SendPlanRequest(plan,CLIENT_PLAN_MODIFY);
}
这种把传数据统一进一个函数的写法也算一个小重构了,以后可以多用一用,这其实是个很基本的事情
接下来是服务器方面的接收函数
cpp
//这个函数的作用是处理新建规划请求
void ClientWorker::upLoadPlan(QByteArray packdata)
{
PlanDbWorker *pworker = new PlanDbWorker; //一开始就创建数据库对象
QDataStream stream(&packdata,QIODevice::ReadOnly); //规划的处理方式和待办的处理方式相同
QVector<PlanClass> recPlans; //创建存规划的数组
QString account; //账号
int plancount; //规划数量
stream>>account>>plancount; //从数据流中取出账号和规划数
{
qDebug() << "account:" << account << ", plancount:" << plancount;
}
for(int i = 0; i < plancount;i++) //一个规划一个规划取出数据
{
PlanClass plan;
stream >> plan.pName;
stream >> plan.Evaluation;
stream >> plan.StartTime;
stream >> plan.EndTime;
recPlans.append(plan);
}
QVector<int> res = pworker->SavePlanData(account,recPlans,1); //调用数据库操作对象的函数,将账号、任务、模式都传进去,然后返回一个存传输失败任务序号的数组
if(res.empty()) //如果返回值是空数组,说明所有东西都很完美的存进去了
{
QByteArray data = "All plans upload successfully.";
unsigned char istoken = CLIENT_PLAN_UPLOAD_SUCCESSFUL;
QByteArray sendData;
PreSendData(sendData,data,istoken); //这个就还是老一套操作了
sendResponse(sendData);
emit PlanUploadSuccess(account,plancount);
}
else
{
QByteArray data;
QDataStream stream(&data,QIODevice::WriteOnly);
stream << res; //如果有没保存成功的就返回哪个没成功,处理基本也是老一套
QByteArray sendData;
unsigned char istoken = CLIENT_PLAN_UPLOAD_FAILED;
PreSendData(sendData,data,istoken);
sendResponse(sendData);
emit TaskUploadFailed(account,plancount,data);
}
}
新建与上边这个函数几乎完全一样,区别就是传的信号、返回的令牌和调用的数据库函数传的第三个参数不同,这里笔者也是很遗憾的,因为完全可以重构,后续应该会做
接下来就是上边这个函数调用的数据库函数的具体内容,其实我也不是很懂原理,都是照抄前边板块的板子
cpp
QVector<int> PlanDbWorker::SavePlanData(QString account, QVector<PlanClass> plans, int method)
{
qDebug() << "[PlanDbWorker] Requesting DB connection, thread:" << QThread::currentThreadId();
QSqlDatabase db = DbConnectionPool::instance().getConnection();
QString connName = db.connectionName();
qDebug() << "connecttionname" << connName;
QSqlQuery query(db);
QVector<int> failedIndices;
// 开启事务
if (!db.transaction()) {
qWarning() << "[PlanDbWorker] Failed to start transaction";
DbConnectionPool::instance().releaseConnection(connName);
return { -1 }; // 事务开启失败
}
if (method == 1) { // 插入新规划
for (int i = 0; i < plans.size(); i++) {
PlanClass plan = plans[i];
// 准备插入语句
query.prepare(R"(
INSERT INTO plan_duration
(Account, Pname, StartTime, EndTime, Evaluation)
VALUES
(:account, :pname, :startTime, :endTime, :evaluation)
)");
// 绑定参数
query.bindValue(":account", account);
query.bindValue(":pname", plan.pName);
query.bindValue(":startTime", plan.StartTime);
query.bindValue(":endTime", plan.EndTime);
query.bindValue(":evaluation", plan.Evaluation);
// 执行插入
if (!query.exec()) {
qWarning() << "[PlanDbWorker] Failed to insert plan:"
<< query.lastError().text()
<< " at index:" << i;
failedIndices.push_back(i + 1); // 记录失败的索引(从1开始)
// 回滚事务
if (!db.rollback()) {
qWarning() << "[PlanDbWorker] Failed to rollback transaction";
}
continue; // 跳过当前规划,继续处理下一个
}
}
}
else if (method == 2) { // 更新规划
// 更新逻辑实现
for (int i = 0; i < plans.size(); i++) {
PlanClass plan = plans[i];
query.prepare(R"(
UPDATE plan_duration
SET
StartTime = :startTime,
EndTime = :endTime,
Evaluation = :evaluation
WHERE Account = :account AND Pname = :pname
)");
query.bindValue(":account", account);
query.bindValue(":pname", plan.pName);
query.bindValue(":startTime", plan.StartTime);
query.bindValue(":endTime", plan.EndTime);
query.bindValue(":evaluation", plan.Evaluation);
if (!query.exec()) {
qWarning() << "[PlanDbWorker] Failed to update plan:"
<< query.lastError().text()
<< " at index:" << i;
failedIndices.push_back(i + 1);
}
}
}
// 提交事务
if (failedIndices.isEmpty()) {
if (!db.commit()) {
qWarning() << "[PlanDbWorker] Failed to commit transaction";
failedIndices.push_back(-1); // 提交失败
}
}
DbConnectionPool::instance().releaseConnection(connName);
return failedIndices;
}
数据库操作函数看起来和前端方面的应用函数风格差不多了,都是先提取数据然后分别处理,提取数据这个部分就可以写在一起
然后是客户端接收反馈之后的处理
在TcpClient类的分类函数中分出这几个分支,发射信号,然后被Plan接收,再调用handle函数处理前端
cpp
case CLIENT_PLAN_UPLOAD_SUCCESSFUL:
emit planUpLoadSuccess();
break;
case CLIENT_PLAN_UPLOAD_FAILED:
emit planUpLoadFailed();
break;
case CLIENT_PLAN_MODIFY_SUCCESSFUL:
emit planModifySuccess();
break;
case CLIENT_PLAN_MODIFY_FAILED:
emit planModifyFailed();
break;
cpp
void Plan::handleMissionUpLoadModifySuccess()
{
shiftMissionList.append(pendingmission.mName);
shiftMissionList.append(pendingmission.StartTime.toString("yyyy/MM/dd HH:mm"));
shiftMissionList.append(pendingmission.EndTime.toString("yyyy/MM/dd HH:mm"));
qDebug()<<pendingmission.StartTime;
qDebug()<<pendingmission.EndTime;
for(int i=0;i<4;i++)
{
qDebug()<<shiftMissionList[i];
}
if(pendingmission.isStandard==false)
{
shiftMissionList.append("未完成");
}
else
{
shiftMissionList.append("已完成");
}
// 确保当前有选中的规划行
if (currentRow == -1) {
QMessageBox::warning(this, "提示", "请先选择一个规划!");
shiftMissionList.clear();
return;
}
if(addOrRevise==1)
{
// 获取当前规划的模型项
QStandardItem* planItem = model->item(currentRow, 0);
if (!planItem) {
QMessageBox::warning(this, "提示", "未找到对应的规划项!");
shiftMissionList.clear();
return;
}
// 创建任务的子节点项
QList<QStandardItem*> missionItems;
for (int col = 0; col < 4; ++col) {
QStandardItem* item = new QStandardItem(shiftMissionList.at(col));
missionItems.append(item);
}
// 将任务添加为规划的子节点
planItem->appendRow(missionItems);
// 更新数据模型中的任务数据
if (currentRow < allPlans.size()) {
allPlans[currentRow].allmissions.append(pendingmission);
}
QMessageBox::about(this, "提示", "任务添加成功!");
}
else
{
if(addOrRevise==2)
{
// 确保当前有选中的任务行
if (parentOrChild != 2) {
QMessageBox::warning(this, "提示", "请先选择要修改的任务!");
shiftMissionList.clear();
return;
}
// 获取当前选中的任务索引
QModelIndex currentIndex = ui->planShowTree->currentIndex();
if (!currentIndex.isValid()) {
QMessageBox::warning(this, "提示", "未选中任何任务!");
shiftMissionList.clear();
return;
}
// 获取任务所属的规划节点
QModelIndex parentIndex = currentIndex.parent();
if (!parentIndex.isValid()) {
QMessageBox::warning(this, "提示", "任务所属规划节点无效!");
shiftMissionList.clear();
return;
}
// 获取规划项和任务项
QStandardItem* planItem = model->item(parentIndex.row(), 0);
QStandardItem* missionItem = planItem->child(currentIndex.row(), 0);
if (!missionItem) {
QMessageBox::warning(this, "提示", "未找到对应的任务项!");
shiftMissionList.clear();
return;
}
// 更新任务节点数据
for (int col = 0; col < 4; ++col) {
QStandardItem* item = planItem->child(currentIndex.row(), col);
if (item) {
item->setText(shiftMissionList.at(col));
}
}
// 通知视图数据已更改
QModelIndex topLeft = model->index(currentIndex.row(), 0, parentIndex);
QModelIndex bottomRight = model->index(currentIndex.row(), 3, parentIndex);
emit model->dataChanged(topLeft, bottomRight, {Qt::DisplayRole});
// 更新数据模型中的任务数据
if (currentRow < allPlans.size() && currentIndex.row() < allPlans[currentRow].allmissions.size()) {
allPlans[currentRow].allmissions[currentIndex.row()] = pendingmission;
}
QMessageBox::about(this, "提示", "任务修改成功!");
}
}
clearSecondForm();
shiftMissionList.clear();
addOrRevise=0;
}
这样就可以实现了
(3)大标题删除的实现
删除这个功能其实我一直没做,都是学长做的,现在我第一次做,也是无脑照着task的实现方法抄
不过这里有一个和QTreeView相关的点,就是父节点被删,他带着的子节点都得被删,在前端函数中要专门来处理这个事情,数据库方面在开数据库的时候就已经确定了这种形式
cpp
void Plan::handlePlanDelete()
{
if(parentOrChild==1)
{
// 处理父节点删除(需级联删除所有子节点)
if (currentRow == -1)
return;
// 获取当前选中的父节点模型项
QStandardItem* parentItem = model->item(currentRow, 0);
if (!parentItem)
return;
// 先删除所有子节点(从后往前删除,避免索引变化问题)
int childCount = parentItem->rowCount();
for (int i = childCount - 1; i >= 0; i--) {
// 从模型中删除子节点
parentItem->removeRow(i);
// 从allPlans中删除对应的子规划数据
// 假设子规划存储在父规划的childPlans列表中
if (currentRow < allPlans.size() && i < allPlans[currentRow].allmissions.size()) {
allPlans[currentRow].allmissions.removeAt(i);
}
}
// 最后删除父节点
model->removeRow(currentRow);
allPlans.removeAt(currentRow);
// 更新currentRow
int rowCount = model->rowCount();
if (rowCount > 0)
currentRow = 0;
else
currentRow = -1;
clearFirstForm();
parentOrChild=0;
}
}
(4)子节点的处理思路分析
树状视图的优势和难点在子节点这里才能体现出来。首先前边的函数细看的话是可以看出区别的。因为父子节点的关系是:父节点被删了子节点全要被删,子节点删一个或者建一个对父节点来说不痛不痒。父节点修改的时候还不能直接替换,必须要以"父节点"这个身份来替换同一行的数据。
子节点操作的时候首先要在选中行时判断选中的是子节点还是父节点,然后针对这点再做不同的处理,这里我的实现思路是设置一个类型变量,选中父节点就是1,选中子节点就是0
接下来就又变成了数据传输的业务了
(5)子标题新建修改的实现
客户端服务器的数据传输在子标题的处理步骤中就不说了,都是重复的内容
这个步骤中新东西就是对QTreeView的操作
cpp
void Plan::handleMissionUpLoadModifySuccess()
{
shiftMissionList.append(pendingmission.mName);
shiftMissionList.append(pendingmission.StartTime.toString("yyyy/MM/dd HH:mm"));
shiftMissionList.append(pendingmission.EndTime.toString("yyyy/MM/dd HH:mm"));
qDebug()<<pendingmission.StartTime;
qDebug()<<pendingmission.EndTime;
for(int i=0;i<4;i++)
{
qDebug()<<shiftMissionList[i];
}
if(pendingmission.isStandard==false)
{
shiftMissionList.append("未完成");
}
else
{
shiftMissionList.append("已完成");
}
// 确保当前有选中的规划行
if (currentRow == -1) {
QMessageBox::warning(this, "提示", "请先选择一个规划!");
shiftMissionList.clear();
return;
}
if(addOrRevise==1)
{
// 获取当前规划的模型项
QStandardItem* planItem = model->item(currentRow, 0);
if (!planItem) {
QMessageBox::warning(this, "提示", "未找到对应的规划项!");
shiftMissionList.clear();
return;
}
// 创建任务的子节点项
QList<QStandardItem*> missionItems;
for (int col = 0; col < 4; ++col) {
QStandardItem* item = new QStandardItem(shiftMissionList.at(col));
missionItems.append(item);
}
// 将任务添加为规划的子节点
planItem->appendRow(missionItems);
// 更新数据模型中的任务数据
if (currentRow < allPlans.size()) {
allPlans[currentRow].allmissions.append(pendingmission);
}
QMessageBox::about(this, "提示", "任务添加成功!");
}
else
{
if(addOrRevise==2)
{
// 确保当前有选中的任务行
if (parentOrChild != 2) {
QMessageBox::warning(this, "提示", "请先选择要修改的任务!");
shiftMissionList.clear();
return;
}
// 获取当前选中的任务索引
QModelIndex currentIndex = ui->planShowTree->currentIndex();
if (!currentIndex.isValid()) {
QMessageBox::warning(this, "提示", "未选中任何任务!");
shiftMissionList.clear();
return;
}
// 获取任务所属的规划节点
QModelIndex parentIndex = currentIndex.parent();
if (!parentIndex.isValid()) {
QMessageBox::warning(this, "提示", "任务所属规划节点无效!");
shiftMissionList.clear();
return;
}
// 获取规划项和任务项
QStandardItem* planItem = model->item(parentIndex.row(), 0);
QStandardItem* missionItem = planItem->child(currentIndex.row(), 0);
if (!missionItem) {
QMessageBox::warning(this, "提示", "未找到对应的任务项!");
shiftMissionList.clear();
return;
}
// 更新任务节点数据
for (int col = 0; col < 4; ++col) {
QStandardItem* item = planItem->child(currentIndex.row(), col);
if (item) {
item->setText(shiftMissionList.at(col));
}
}
// 通知视图数据已更改
QModelIndex topLeft = model->index(currentIndex.row(), 0, parentIndex);
QModelIndex bottomRight = model->index(currentIndex.row(), 3, parentIndex);
emit model->dataChanged(topLeft, bottomRight, {Qt::DisplayRole});
// 更新数据模型中的任务数据
if (currentRow < allPlans.size() && currentIndex.row() < allPlans[currentRow].allmissions.size()) {
allPlans[currentRow].allmissions[currentIndex.row()] = pendingmission;
}
QMessageBox::about(this, "提示", "任务修改成功!");
}
}
clearSecondForm();
shiftMissionList.clear();
addOrRevise=0;
}
重点看这个函数就可以了,这个函数是负责向父标题下的子节点加内容的,用了appendRow这个库函数,还要区分父节点和子节点
(6)子标题删除的实现
这一部分比父节点的删除还要简单,因为它甚至不需要考虑会不会影响到父节点,流程也和前边一样,就不多说了
(7)规划板块的数据读取
这一部分相当重要,即在用户登录成功之后要向服务器发送读取数据请求,然后把数据传回客户端,并在界面中展示出来
我也按照完整流程附一遍代码
cpp
connect(Client,&TcpClient::LoginSuccessful,this,[=](){emit ReadPlan();});
这是Plan类的构造函数连接的内容
cpp
void TcpClient::SendPlanReadRequest()
{
QByteArray data = currentuser.toUtf8();
QByteArray sendData;
PreSendData(sendData, data, CLIENT_PLAN_READ);
sendResponse(sendData);
}
这是TcpClient类的处理函数
cpp
//这个函数的作用是处理读取规划请求
void ClientWorker::DoPlanRead(QByteArray packdata)
{
PlanDbWorker *pworker = new PlanDbWorker;
QString account = QString::fromUtf8(packdata);
// 调用数据库对象的读取函数,返回规划数据
QVector<PlanClass> sendPlans = pworker->ReadPlanData(account);
QByteArray PlanData;
QDataStream stream(&PlanData, QIODevice::WriteOnly);
int size = sendPlans.size();
stream << size;
qDebug() << "[DoPlanRead] sendPlans.size:" << size;
if(sendPlans.size() == 0)
{
// 数据库中对应账号没有数据
QByteArray data = "No Plan Data Record in Database";
qDebug() << "No Plan Data Record in Database";
unsigned char istoken = CLIENT_PLAN_READ_FAILED;
QByteArray SendData;
PreSendData(SendData, data, istoken);
sendResponse(SendData);
emit PlanReadFailed(account);
}
else
{
qDebug() << "Read Plan Data successful";
for(const PlanClass &plan : sendPlans)
{
qDebug() << "plan name : " << plan.pName;
// 写入规划基本信息
stream << plan.pName;
stream << plan.StartTime;
stream << plan.EndTime;
stream << plan.Evaluation;
// 写入任务数量
int missionCount = plan.allmissions.size();
stream << missionCount;
// 写入每个任务信息
for(const Mission &mission : plan.allmissions)
{
stream << mission.mName;
stream << mission.pName;
stream << mission.StartTime;
stream << mission.EndTime;
stream << mission.isStandard;
}
}
unsigned char istoken = CLIENT_PLAN_READ_SUCCESSFUL;
QByteArray SendData;
PreSendData(SendData, PlanData, istoken);
sendResponse(SendData);
emit PlanReadSuccess(account);
}
}
这是服务器方面的接收函数
cpp
QVector<PlanClass> PlanDbWorker::ReadPlanData(QString account)
{
qDebug() << "[PlanDBWorker] Requesting DB connection, thread:" << QThread::currentThreadId();
QSqlDatabase db = DbConnectionPool::instance().getConnection();
QString connName = db.connectionName();
qDebug() << "connecttionname" << connName;
QSqlQuery query(db);
QVector<PlanClass> res;
// 开启事务
if (!db.transaction()) {
qWarning() << "[PlanDBWorker] Failed to start transaction for reading";
DbConnectionPool::instance().releaseConnection(connName);
return res;
}
// 先查询所有规划
query.prepare(R"(
SELECT Account, Pname, StartTime, EndTime, Evaluation
FROM plan_duration
WHERE Account = :account
)");
query.bindValue(":account", account);
if(!query.exec())
{
qWarning() << "[PlanDBWorker] Failed to read plan data:" << query.lastError().text();
db.rollback();
DbConnectionPool::instance().releaseConnection(connName);
return res;
}
while(query.next())
{
PlanClass plan;
plan.pName = query.value("Pname").toString();
plan.StartTime = query.value("StartTime").toDateTime();
plan.EndTime = query.value("EndTime").toDateTime();
plan.Evaluation = query.value("Evaluation").toString();
// 查询该规划下的所有任务
QSqlQuery missionQuery(db);
missionQuery.prepare(R"(
SELECT MName, MStartTime, MEndTime, IsStandard
FROM plan_mission
WHERE Account = :account AND PName = :pname
)");
missionQuery.bindValue(":account", account);
missionQuery.bindValue(":pname", plan.pName);
if(missionQuery.exec())
{
while(missionQuery.next())
{
Mission mission;
mission.mName = missionQuery.value("MName").toString();
mission.pName = plan.pName; // 任务所属的规划名称
mission.StartTime = missionQuery.value("MStartTime").toDateTime();
mission.EndTime = missionQuery.value("MEndTime").toDateTime();
mission.isStandard = missionQuery.value("IsStandard").toBool();
plan.allmissions.append(mission);
}
}
else
{
qWarning() << "[PlanDBWorker] Failed to read mission data for plan:" << plan.pName;
}
res.append(plan);
}
if (!db.commit()) {
qWarning() << "[PlanDBWorker] Failed to commit transaction after reading";
} else {
}
DbConnectionPool::instance().releaseConnection(connName);
return res;
}
这是数据库操作函数
cpp
void TcpClient::DoReadPlan(const QByteArray &data)
{
QDataStream stream(data);
QVector<PlanClass> recPlans;
int recsize;
// 读取规划总数
stream >> recsize;
for (int i = 0; i < recsize; ++i) {
PlanClass plan;
// 读取规划基本信息
stream >> plan.pName;
stream >> plan.StartTime;
stream >> plan.EndTime;
stream >> plan.Evaluation;
int missionCount;
// 读取该规划下的任务数量
stream >> missionCount;
for (int j = 0; j < missionCount; ++j) {
Mission mission;
// 读取任务信息
stream >> mission.mName;
stream >> mission.pName;
stream >> mission.StartTime;
stream >> mission.EndTime;
stream >> mission.isStandard;
plan.allmissions.append(mission);
}
recPlans.append(plan);
}
// 转发解析好的规划数据,供其他模块处理
emit handleReadPlan(recPlans);
// 通知规划总数
emit sendTotalPlan(recsize);
}
这是客户端接收后的处理函数
cpp
void Plan::handleReadPlan(QVector<PlanClass> recPlans)
{
allPlans=recPlans;
// 遍历规划数据,构建树形结构
for (const PlanClass& plan : recPlans) {
// 创建规划父节点(大标题),显示规划名称
QStandardItem* planParentItem = new QStandardItem(plan.pName);
// 规划父节点的其他列填默认空字符串或你想展示的规划级信息
QStandardItem* planCol2 = new QStandardItem(plan.StartTime.toString("yyyy/MM/dd HH:mm"));
QStandardItem* planCol3 = new QStandardItem(plan.EndTime.toString("yyyy/MM/dd HH:mm"));
QStandardItem* planCol1 = new QStandardItem(plan.Evaluation);
QList<QStandardItem*> planRow;
planRow << planParentItem << planCol2 << planCol3 << planCol1;
model->appendRow(planRow);
// 遍历该规划下的任务,创建子节点(子标题)
for (const Mission& mission : plan.allmissions) {
QStandardItem* missionChildItem = new QStandardItem(mission.mName);
// 绑定完整任务对象到节点
// 任务子节点填充对应列数据,与表头"事项""起始时间""结束时间""完成情况"匹配
QStandardItem* missionCol2 = new QStandardItem(mission.StartTime.toString("yyyy/MM/dd HH:mm"));
QStandardItem* missionCol3 = new QStandardItem(mission.EndTime.toString("yyyy/MM/dd HH:mm"));
QString missionStatus = mission.isStandard ? "已完成" : "未完成";
QStandardItem* missionCol4 = new QStandardItem(missionStatus);
QList<QStandardItem*> missionRow;
missionRow << missionChildItem << missionCol2 << missionCol3 << missionCol4;
// 将任务行作为规划父节点的子行
planParentItem->appendRow(missionRow);
}
}
// 自动展开所有规划父节点,方便默认查看子任务
QTreeView* planShow = ui->planShowTree;
planShow->expandAll();
}
这是前端展示函数
读取操作全流程函数已经附上
篇末总结
哪怕接上数据库和计算机网络,也只是加长了代码开发的长度,本质上还是没什么难度
这个项目重复的东西还是太多了,每个板块几乎都在重复做同一件事,全部交给AI还不是很放心,只能说项目的结构是非常重要的,在项目还没有开始写的时候就要想好项目结构,不过毕竟是笔者的处女作,没有经验也是正常的,只能后期重构了
做到这里,笔者在整个项目的工作也全部做完了,后续会更一篇重构博客和一篇总结博客,最后再把我的处女作开源出去