1.问题背景
本课程设计中旨在构建一个项目工期成本分析程序,该程序的核心功能是解析和处理项目活动数据。这些数据包括但不限于活动编号、名称、正常完成所需天数、加班完成所需天数、加班费用、前驱活动编号以及后驱活动编号。程序将从文本文件中读取至少20个活动的数据,以确保分析的全面性和实用性。通过这一过程,程序将能够计算并输出在正常工作条件下完成项目所需的总天数,以及在考虑加班情况下完成项目所需的总天数和由此产生的额外加班费用。
2.主要运行界面
2.1菜单页面
界面顶部有一个标题 “项目工期分析”,明确了软件的主要功能。界面中央有四个功能选项,分别是:(1)工期查询(2)时间对照(3)甘特图显示(4)关键路径
图4.1 项目工期分析菜单界面
4.1.2 点击“工期查询”后
如图所示,点击工期查询,便会让用户输入想要查询的节点(如图4.3)。
图4.2 工期查询菜单界面
图4.3 查询节点菜单界面
点击确定后便会输出距离最早最晚完工时间(如图4.4)
图4.4 查询节点距离完工时间的界面
4.1.3 点击“时间对照”后
所有节点最早和最晚的开始时间,并将最早时间等于最晚时间的节点进行标红处理。这些节点为关键路径上的点。
图4.5 查询所有节点最早最晚时间的界面
4.1.4 点击“工期查询”后
如图所示,点击甘特图显示,便会出现这张用project画出来的甘特图(如图4.6)。
图4.6 显示甘特图的界面
4.1.5 点击“关键路径”后
如图所示,点击关键路径后,也会出现想要查询的节点(如图4.7)。输入节点后,会出现弹窗显示是否为关键节点(如图4.8与4.9)。
图4.7 显示想要查询节点的界面
图4.8 是关键节点的界面
图4.9 不是关键节点的界面
若为关键节点(图4.8),则会弹出图4.10的界面,输出加班成本和加班后需要天数。
图4.10 显示节点加班成本和天数的界面
然后,会弹出一张带有所有节点的路径图,显示出关键路径(标红部分),让用户可以进行更仔细地分析。
图4.11 显示完整路径图的界面
完整代码:
#define _CRT_SECURE_NO_WARNINGS //一些老函数报错
#include<stdio.h>
#include<stdlib.h>
#include<graphics.h> //Easyx的图形库头文件
#include<string.h>
#include<ctype.h>//检测字符
int flag = 0;//全局变量,用来判断是否有关键路径
int* t, * ve, * vl; //t:记录拓扑序列结果,ve:最早开始时间,vl:最迟开始时间
typedef struct arc/*边表节点*/
{
int index; //编号
int weight; //正常工作时间
int plusweight;//加班完成时间
int cost; //加班费用
struct arc* next; //指向下一个节点
}AR;
typedef struct MyGraph
{
int type;//0表示无向网,1表示有向网
int arcnum; //边的数量
int vexnum; //点的个数
char** vexname; //点的名字
AR* N;//邻接表
}GH;
GH G;
int findVertexIndex(GH* G, char* s);//确定顶点s对应的编号号
void creatgraph(GH* G);//以邻接表的形式创建图
void printgraph(GH* G);//以邻接表的形式显示图
int topologicalsort(GH* G, int* t);//获得拓扑排序
void keypath(GH* G); //获得关键路径
void allkeypath(GH* G, int head, int end, int* path, int n, int length, int* visit, int* isequl);//显示所有关键路径(不止一条)
void show1();
void show2();
void show3();
void show4();
int chainitAndAnalyze();
int findNode();
void initAndAnalyze();
void PrintUI();
//定义按钮的大小数组
int R[] = { 150,50,650,150 };
int r1[] = { 200,340,600,390 };
int r2[] = { 200,410,600,460 };
int r3[] = { 200,480,600,530 };
int r6[] = { 200,270,600,320 };
void initAndAnalyze()
{
printf("\n--------------项目成本分析课程设计--------------\n\n"); //说明该代码的实现功能
printf("\n原图为:\n");
creatgraph(&G);//创建图
printgraph(&G);//显示图
keypath(&G);
}
int findVertexIndex(GH* G, char* s)
{
int i;
for (i = 0; i < G->vexnum; i++)
{
if (strcmp(G->vexname[i], s) == 0)//判断字符串是否一样
return i;
}
printf("Error!\n");
exit(0);
}
void creatgraph(GH* G)
{
FILE* fp;
int i, j, n;
int k, l, m;//记录权重
char s1[20], s2[20];
AR* p;
errno_t err = fopen_s(&fp, "D:\\课设\\数据结构课设数据.txt", "r");
//"D:\\课设\\有回路.txt"
//"D:\\课设\\数据结构课设数据.txt"
if (err != 0 || !fp) {
printf("Can not open file!!!\n");
exit(0);
}
fscanf_s(fp, "%d", &n);
G->vexnum = n; //点的数量
G->type = 1; //有向图
G->N = (AR*)malloc(n * sizeof(AR));
G->vexname = (char**)malloc(n * sizeof(char*));
G->arcnum = 0; //边的数量初始化为0
for (i = 0; i < n; i++)//初始化
{
fscanf_s(fp, "%s", s1, sizeof(s1));
G->vexname[i] = (char*)malloc(strlen(s1) + 1);
strcpy_s(G->vexname[i], strlen(s1) + 1, s1);
G->N[i].next = NULL;
}
while (fscanf_s(fp, "%19s %19s %d %d %d", s1, (unsigned)_countof(s1), s2, (unsigned)_countof(s2), &k, &l, &m) != EOF)//读入边的两个顶点和权重
{
p = (AR*)malloc(sizeof(AR));
i = findVertexIndex(G, s1);
j = findVertexIndex(G, s2);
(G->arcnum)++;
p->index = j;
p->weight = k;
p->plusweight = l;
p->cost = m;
p->next = G->N[i].next;
G->N[i].next = p;
}
fclose(fp);//关闭文件
}
void printgraph(GH* G)//用邻接表显示图
{
int i;
AR* p;
for (i = 0; i < G->vexnum; i++)
{
printf("\n%s", G->vexname[i]);
p = G->N[i].next;
while (p)
{
printf("--->%s", G->vexname[p->index]);
p = p->next;
}
printf("\n");
}
printf("\n");
}
int topologicalsort(GH* G, int* t)
{
int* stack, * indegree;//stack是栈,将入度为0的点入栈,indegree是记录每个点入度的数组,用来寻找入度为0的点
int top = -1, count = 0, i;//top为栈顶,count表示
AR* p;
stack = (int*)malloc(G->vexnum * sizeof(int));
indegree = (int*)malloc(G->vexnum * sizeof(int));
memset(indegree, 0, G->vexnum * sizeof(int));//初始化,将所有点的入度初始化为0
for (i = 0; i < G->vexnum; i++)//求每个点的入度
{
p = G->N[i].next;
while (p)
{
indegree[p->index]++;
p = p->next;
}
}
for (i = 0; i < G->vexnum; i++)//将入度为0的点入栈
{
if (indegree[i] == 0)
{
top++;
stack[top] = i;
}
}
while (top >= 0)
{
i = stack[top];
top--;
t[count] = i;
count++;
p = G->N[i].next;
while (p)//将所有该点连接的点的入度都减一
{
indegree[p->index]--;
if (indegree[p->index] == 0)//发现减完后,有点入度为0,入栈
{
top++;
stack[top] = p->index;
}
p = p->next;
}
}
free(stack);
free(indegree);
if (count == G->vexnum)//判断是否所有点都在拓扑排序里面,是的话返回1,否则因为没有所有点进入排序,说明排序失败:返回0
return 1;
else
return 0;
}
void keypath(GH* G)
{
int* visit, * path, * isequl;//visit记录每个点是否被访问,path记录路径上的点,isequl记录每个点最早开始时间和最迟是否相等,1代表相等,0代表不等
int i;
int max;//用来寻找最早开始时间的最大值
AR* p;
t = (int*)malloc(G->vexnum * sizeof(int));
ve = (int*)malloc(G->vexnum * sizeof(int));
vl = (int*)malloc(G->vexnum * sizeof(int));
visit = (int*)malloc(G->vexnum * sizeof(int));
path = (int*)malloc(G->vexnum * sizeof(int));
isequl = (int*)malloc(G->vexnum * sizeof(int));
memset(ve, 0, G->vexnum * sizeof(int));//初始化,将所有点的最早开始时间初始化为0
memset(visit, 0, G->vexnum * sizeof(int)); //初始化,将所有点的访问状态初始化为未访问
memset(vl, 0, G->vexnum * sizeof(int));//初始化,将所有点的最迟开始时间初始化为0
memset(isequl, 0, G->vexnum * sizeof(int));
if (!topologicalsort(G, t))//拓扑排序并输出拓扑序列
{
printf("图中有回路,不存在关键路径!\n");
exit(0);
}
for (i = 0; i < G->vexnum; i++)//求每个点的最早开始时间并输出,从前往后
{
p = G->N[t[i]].next;// 获取拓扑排序中第i个顶点的所有邻接点链表的头结点
while (p)
{
if (ve[p->index] < (ve[t[i]] + p->weight))//判断更新最小的时间,ve[p->index] 是邻接点的当前最早开始时间,ve[t[i]] + p->weight 是通过当前顶点到邻接点的边计算出的最早开始时间。
ve[p->index] = ve[t[i]] + p->weight;
p = p->next;
}
}
printf("\n最早发生时间:\n");
max = ve[0];// 初始化最大值为第一个顶点的最早开始时间
for (i = 0; i < G->vexnum; i++)
{
if (ve[i] > max)
max = ve[i];
printf("%s: %d\n", G->vexname[i], ve[i]);
}
printf("\n");
for (i = 0; i < G->vexnum; i++)//给每个点的最迟开始时间赋初值,为最早开始时间
vl[i] = max;
for (i = G->vexnum - 1; i >= 0; i--)//求每个点的最迟开始时间并输出
{
p = G->N[t[i]].next;// 获取当前顶点的所有邻接点链表的头结点
while (p)
{
if (vl[t[i]] > (vl[p->index] - p->weight))//思路同上,vl[t[i]] 是邻接点的当前最迟开始时间,vl[p->index] - p->weight 是通过当前顶点到邻接点的边计算出的最迟开始时间。
vl[t[i]] = vl[p->index] - p->weight;
p = p->next;
}
}
printf("最迟发生时间:\n");
for (i = 0; i < G->vexnum; i++)
{
printf("%s: %d\n", G->vexname[i], vl[i]);
if (vl[i] == ve[i])//将最早开始时间和最迟开始时间相等的点的isequal记为1,寻找关键路径
isequl[i] = 1;
}
printf("\n");
path[0] = t[0];
allkeypath(G, t[0], t[G->vexnum - 1], path, 1, 0, visit, isequl);//递归调用allkeypath函数显示所有关键路径
}
void allkeypath(GH* G, int head, int end, int* path, int n, int length, int* visit, int* isequl)//path用来存储路径,这个仿照了之前迷宫的路径输出方式
{
if (head == end)//到达终点
{
int i;
flag++;//全局变量flag用来记录关键路径的数量
printf("关键路径:");
for (i = 0; i < n - 1; i++)
printf("%s-->", G->vexname[path[i]]);
printf("%s 路径长度为:%d\n\n", G->vexname[path[i]], length);
}
else
{
AR* p;
p = G->N[head].next;
while (p)//遍历每个点的所有后继
{
if (!visit[p->index] && isequl[p->index])
{
path[n] = p->index;
visit[p->index] = 1;
allkeypath(G, p->index, end, path, n + 1, length + p->weight, visit, isequl);//递归调用自己,显示所有关键路径
visit[p->index] = 0;//回溯
}
p = p->next; 移动到下一个后继顶点
}
}
}
//主菜单
void menu()
{
//进入程序就读取数据,获取当前nowstu_num
initAndAnalyze();
initgraph(800, 620);//界面大小
//鼠标操作1
ExMessage m1;
cleardevice();
//背景图
IMAGE img; // 定义一个 IMAGE 结构体来存储图像数据
loadimage(&img, "D:\\课设\\最新调试\\res\\背景.jpg"); // 加载图像
putimage(0, 0, &img); // 将图像绘制在窗口的 (0, 0) 位置
setbkmode(TRANSPARENT);
setfillcolor(WHITE);
fillrectangle(r1[0], r1[1], r1[2], r1[3]);//按钮
fillrectangle(r2[0], r2[1], r2[2], r2[3]);
fillrectangle(r3[0], r3[1], r3[2], r3[3]);
fillrectangle(r6[0], r6[1], r6[2], r6[3]);
RECT R1 = { R[0],R[1],R[2],R[3] };//矩形指针R1,方便后面在里面写字,不用计算字的位置
RECT R4 = { r1[0],r1[1],r1[2],r1[3] };
RECT R5 = { r2[0],r2[1],r2[2],r2[3] };
RECT R6 = { r3[0],r3[1],r3[2],r3[3] };
RECT R7 = { r6[0],r6[1],r6[2],r6[3] };
LOGFONT f; //字体样式指针
gettextstyle(&f); //获取字体样式
f.lfHeight = 60;
_tcscpy(f.lfFaceName, _T("宋体")); //宋体
f.lfQuality = ANTIALIASED_QUALITY; // 抗锯齿
settextstyle(&f); // 设置字体样式
setbkmode(TRANSPARENT); //字体背景透明
settextcolor(BLACK);
drawtext("项目工期分析", &R1, DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R1内输入文字,水平居中,垂直居中,单行显示
drawtext("时间对照", &R4, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
drawtext("甘特图显示", &R5, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
drawtext("关键路径", &R6, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
drawtext("工期查询", &R7, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
//进入主界面选项操作界面
while (1)
{
m1 = getmessage();//获取鼠标操作
if (m1.x > r1[0] && m1.x < r1[2] && m1.y>r1[1] && m1.y < r1[3]) {
setlinecolor(RED);
rectangle(r1[0], r1[1], r1[2], r1[3]);
if (m1.message == WM_LBUTTONDOWN)
{
show1();//打印时间
}
}
else if (m1.x >= r2[0] && m1.x <= r2[2] && m1.y >= r2[1] && m1.y <= r2[3]) {
setlinecolor(RED);
rectangle(r2[0], r2[1], r2[2], r2[3]);
if (m1.message == WM_LBUTTONDOWN)
{
show2();
}
}
else if (m1.x >= r3[0] && m1.x <= r3[2] && m1.y >= r3[1] && m1.y <= r3[3]) {
setlinecolor(RED);
rectangle(r3[0], r3[1], r3[2], r3[3]);
if (m1.message == WM_LBUTTONDOWN)
{
show3();//修改
}
}
else if (m1.x >= r6[0] && m1.x <= r6[2] && m1.y >= r6[1] && m1.y <= r6[3]) {
setlinecolor(RED);
rectangle(r6[0], r6[1], r6[2], r6[3]);
if (m1.message == WM_LBUTTONDOWN)
{
show4();//修改
}
}
else {
setlinecolor(WHITE);
rectangle(r1[0], r1[1], r1[2], r1[3]);
rectangle(r2[0], r2[1], r2[2], r2[3]);
rectangle(r3[0], r3[1], r3[2], r3[3]);
rectangle(r6[0], r6[1], r6[2], r6[3]);
}
}
}
void show1() //打印时间
{
ExMessage m3;
initgraph(800, 620);
IMAGE img2;
loadimage(&img2, "D:\\课设\\最新调试\\res\\背景.jpg", 800, 620);
putimage(0, 0, &img2);
setfillcolor(WHITE);
fillrectangle(680, 490, 760, 520);
setbkmode(TRANSPARENT);
settextcolor(BLACK);
settextstyle(20, 0, "黑体");
outtextxy(40, 20, "名称");
outtextxy(200, 20, "最早时间");
outtextxy(300, 20, "最晚时间");
outtextxy(700, 500, "返回");//返回按钮
char ve_str[20];
char vl_str[20];
for (int q = 0; q < G.vexnum; q++) {
sprintf(ve_str, "%d", ve[q]);
sprintf(vl_str, "%d", vl[q]);
// 根据ve_str和vl_str是否相等来设置不同的文本颜色
if (strcmp(ve_str, vl_str) == 0) {
settextcolor(RED);
}
else {
settextcolor(BLACK);
}
outtextxy(40, 60 + q * 20, G.vexname[q]);
outtextxy(200, 60 + q * 20, ve_str);
outtextxy(300, 60 + q * 20, vl_str);
// 输出完当前这组数据后,将文本颜色恢复为默认的黑色(如果后续有其他需要统一颜色的情况)
settextcolor(BLACK);
}
while (1) {
m3 = getmessage();
if (m3.x > 680 && m3.x < 760 && m3.y > 490 && m3.y < 520) {
setlinecolor(RED);
rectangle(680, 490, 760, 520);
}
else {
setlinecolor(WHITE);
rectangle(680, 490, 760, 520);
}
if (m3.message == WM_LBUTTONDOWN) {
menu();//返回主菜单
}
}
}
void show2() //打印时间
{
ExMessage m4;
initgraph(800, 620);
IMAGE img3;
loadimage(&img3, "D:\\课设\\res\\屏幕截图 2024-12-20 234937.jpg", 800, 620);
putimage(0, 0, &img3);
fillrectangle(680, 490, 760, 520);
outtextxy(700, 500, "返回");//返回按钮
while (1) {
m4 = getmessage();
if (m4.x > 680 && m4.x < 760 && m4.y > 490 && m4.y < 520) {
setlinecolor(RED);
rectangle(680, 490, 760, 520);
}
else {
setlinecolor(WHITE);
rectangle(680, 490, 760, 520);
}
if (m4.message == WM_LBUTTONDOWN) {
menu();//返回主菜单
}
}
}
void show3() //打印时间
{
int fan1 = findNode();//返回值
if (fan1 == -1)//没找到
{
//获取窗口句柄
HWND hndtipsF = GetHWnd();
int isok = MessageBox(hndtipsF, "查无此节点!", "提示", MB_OK);
}
else
{
if (ve[fan1] == vl[fan1])//判断是否为关键节点
{
// 先弹出是否为关键节点的提示框
HWND hndtipsF = GetHWnd();
int isok = MessageBox(hndtipsF, "此节点为关键节点,加班可以缩短工期!", "提示", MB_OK);
// 获取关键节点的cost和plusweight信息
AR* p = G.N[fan1].next;
if (p != NULL) // 确保存在出边
{
int costOfCriticalNode = p->cost;
int plusweightOfCriticalNode = p->plusweight;
// 显示关键节点信息的单独白框界面
ExMessage m4;
initgraph(800, 620);
setbkcolor(WHITE); // 设置背景颜色为白色
cleardevice(); // 清屏使背景生效
setbkmode(TRANSPARENT);
settextcolor(BLACK);
settextstyle(20, 0, "黑体");
// 输出关键节点的cost信息
char cost_str[20];
sprintf(cost_str, "关键节点加班成本: %d", costOfCriticalNode);
outtextxy(40, 140, cost_str);
// 输出关键节点的加班天数信息
char plusweight_str[20];
sprintf(plusweight_str, "关键节点加班天数: %d", plusweightOfCriticalNode);
outtextxy(40, 180, plusweight_str);
fillrectangle(680, 490, 760, 520);
outtextxy(700, 500, "返回");// 返回按钮,提示可查看详情
while (1)
{
m4 = getmessage();
if (m4.x > 680 && m4.x < 760 && m4.y > 490 && m4.y < 520)
{
setlinecolor(RED);
rectangle(680, 490, 760, 520);
}
else
{
setlinecolor(WHITE);
rectangle(680, 490, 760, 520);
}
if (m4.message == WM_LBUTTONDOWN)
{
// 点击“返回(查看详情)”按钮后,进入下一阶段,显示图片界面
initgraph(800, 620);
IMAGE img3;
loadimage(&img3, "D:\\课设\\游戏\\游戏\\路线图.jpg", 800, 620);
putimage(0, 0, &img3);
fillrectangle(680, 490, 760, 520);
outtextxy(700, 500, "返回");// 返回主菜单按钮
while (1)
{
m4 = getmessage();
if (m4.x > 680 && m4.x < 760 && m4.y > 490 && m4.y < 520)
{
setlinecolor(RED);
rectangle(680, 490, 760, 520);
}
else
{
setlinecolor(WHITE);
rectangle(680, 490, 760, 520);
}
if (m4.message == WM_LBUTTONDOWN)
{
menu();// 返回主菜单
}
}
}
}
}
else
{
// 若不存在出边(这种情况可能不符合预期逻辑,但做个错误提示处理)
ExMessage m4;
initgraph(800, 620);
setbkcolor(WHITE);
cleardevice();
setbkmode(TRANSPARENT);
settextcolor(BLACK);
settextstyle(20, 0, "黑体");
outtextxy(40, 140, "关键节点无有效出边,无法获取加班成本和加班天数信息");
fillrectangle(680, 490, 760, 520);
outtextxy(700, 500, "返回");//返回按钮
while (1)
{
m4 = getmessage();
if (m4.x > 680 && m4.x < 760 && m4.y > 490 && m4.y < 520)
{
setlinecolor(RED);
rectangle(680, 490, 760, 520);
}
else
{
setlinecolor(WHITE);
rectangle(680, 490, 760, 520);
}
if (m4.message == WM_LBUTTONDOWN)
{
menu();//返回主菜单
}
}
}
}
else
{
HWND hndtipsF = GetHWnd();
int isok = MessageBox(hndtipsF, "此节点不是关键节点,没有必要加班!", "提示", MB_OK);
ExMessage m4;
initgraph(800, 620);
IMAGE img3;
loadimage(&img3, "D:\\课设\\游戏\\游戏\\路线图.jpg", 800, 620);
putimage(0, 0, &img3);
fillrectangle(680, 490, 760, 520);
outtextxy(700, 500, "返回");//返回按钮
while (1)
{
m4 = getmessage();
if (m4.x > 680 && m4.x < 760 && m4.y > 490 && m4.y < 520)
{
setlinecolor(RED);
rectangle(680, 490, 760, 520);
}
else
{
setlinecolor(WHITE);
rectangle(680, 490, 760, 520);
}
if (m4.message == WM_LBUTTONDOWN)
{
menu();//返回主菜单
}
}
}
}
}
int findNode()//查找
{
char Input[30];
InputBox(Input, 10, "请输入想查询的节点");
for (int k = 0; k < 20; k++)
{
if (strcmp(G.vexname[k], Input) == 0)//两个字符串相等
{
return k;
}
}
return -1;
}
void PrintUI(int q)//输出界面
{
ExMessage m3;
initgraph(800, 620);
IMAGE img2;
loadimage(&img2, "D:\\课设\\最新调试\\res\\背景.jpg", 800, 620);
putimage(0, 0, &img2);
setfillcolor(WHITE);
fillrectangle(680, 490, 760, 520);
setbkmode(TRANSPARENT);
settextcolor(BLACK);
settextstyle(20, 0, "黑体");
outtextxy(40, 20, "名称");
outtextxy(200, 20, "距离最早完工时间还有");
outtextxy(500, 20, "距离最晚完工时间还有");
outtextxy(700, 500, "返回");//返回按钮
char ve_str[20];
char vl_str[20];
sprintf(ve_str, "%d", ve[19] - ve[q]);
sprintf(vl_str, "%d", vl[19] - vl[q]);
outtextxy(40, 140, G.vexname[q]);
outtextxy(200, 140, vl_str);
outtextxy(500, 140, ve_str);
while (1) {
m3 = getmessage();
if (m3.x > 680 && m3.x < 760 && m3.y > 490 && m3.y < 520) {
setlinecolor(RED);
rectangle(680, 490, 760, 520);
}
else {
setlinecolor(WHITE);
rectangle(680, 490, 760, 520);
}
if (m3.message == WM_LBUTTONDOWN) {
menu();//返回主菜单
}
}
}
void show4()//调用findNode函数,输出成绩界面
{
int fan1 = findNode();//返回值
if (fan1 == -1)//没找到
{
//获取窗口句柄
HWND hndtipsF = GetHWnd();
int isok = MessageBox(hndtipsF, "查无此节点!", "提示", MB_OK);
}
else
{
PrintUI(fan1);
}
}
int main(void)//主函数调用
{
menu();
return 0;
}