软件工程基础-组队项目-WordCount

WordCount项目实践:文件统计与GUI实现
本文介绍了一个WordCount项目,包括统计文本文件的字符、单词和行数,递归处理目录,以及实现GUI界面。文章详细讨论了解题思路、设计实现,特别是文件处理和GUI模块,并分享了在实现过程中遇到的问题及解决方案。

github链接 :https://github.com/YYFCY/Word-Count

一、项目要求

1. 统计文本文件的字符数、单词数和行数

2. 递归处理目录下符合条件的文件

3. 返回更复杂的数据(代码行/空行/注释行)

4. 实现GUI界面

二、项目计划PSP表格

PSP2.1Personal Software Process Stages预估耗时    (分钟)实际耗时    (分钟)
Planning计划  
Estimate估计这个任务需要多少时间30 
Development开发  
Analysis需求分析(包括学习新技术)200 
Design Spec生成设计文档100 
Design Review设计复审(和同事审核设计文档)100 
Coding Standard代码规范(为目前的开发制定合适的规范)30 
Design具体设计100 
Coding具体编码1000 
Code Review代码复审100 
Test测试(自我测试、修改代码、提交修改)100 
Reporting报告  
Test Report测试报告150 
Size Measurement计算工作量30 
Postmortem & Process Improvement Plan事后总结,并提出过程改进计划60 
 合计2000 

三、解题思路

参数功能
-c返回文件file.c的字符数
-w返回文件file.c的词的数目
-l返回文件file.c的行数
-s递归处理目录下符合条件的文件
-a返回更复杂的数据
-x打开GUI界面

下面对参数读取部分做详细解释:

根据输入的参数检查文件是否可以存在并可以读取,返回-1表示文件不存在,返回-2表示文件存在但是没有读取权限,返回1表示文件可以正常读取。

//检查文件是否存在且可以读取
int check_file_name(char file_name[])
{
	if (access(file_name, 0))//判断该文件/文件夹是否存在;
		return -1;
	if (access(file_name, 2) != 0)//判断该文件/文件夹是否有读权限;
		return -2;
	return 1;
}

在确定文件可以读取之后,还需要对输入参数的前半部分做一检测,判断其是否符合题目要求,返回为1则表明符合要求,返回为-1表明输入参数错误

//检查命令是否正确,命令正确返回1,错误返回-1.
int check_command(char command[])
{
	int len = strlen(command);
	if (len != 2 || command[0] != '-')
		return -1;
	if (command[1] == 'c' || command[1] == 'w' || command[1] == 'l' || command[1] == 'a') 
		return 1;
	else 
		return -1;
}

 

四、设计实现

总体设计(程序流程图)

 

五、各功能模块

1.  统计文件字符数 count_char()函数

//统计文件的字符数
int count_char(char file[])
{
	int char_num = 0;
	char ch;
	freopen(file, "r", stdin);//重定向输入
	while ((ch = getchar()) != EOF)
	{
		//if (ch != ' '&&ch != '\n'&&ch != '\t')
		char_num++;
	}
	fclose(stdin);
	return char_num;
}

 

2.统计文件单词数 count_word()

//统计文件的单词数
int count_word(char file[])
{
	int word_num = 0, flag = 0;
	char ch;
	freopen(file, "r", stdin);
	while ((ch = getchar()) != EOF)
	{
		if ((ch >= 'a'&&ch <= 'z') || (ch >= 'A'&&ch <= 'Z') || ch == '_') flag = 1;
		else
		{
			if (flag)
			{
				word_num++;
				flag = 0;
			}
		}
	}
	fclose(stdin);
	return word_num;
}

 

3. 统计文件的行数 count_line()

//统计文件的行数
int count_line(char file[])
{
	int line_num = 0;
	char ch;
	freopen(file, "r", stdin);
	while ((ch = getchar()) != EOF)
	{
		if (ch == '\n')
		{
			line_num++;
		}
	}
	fclose(stdin);
	return line_num;
}

 4. 统计文件的空行数count_blank_line()

//统计文件的空行数
int count_blank_line(char file[])
{
	int blank_line_num = 0, ch_num = 0;
	char ch;
	freopen(file, "r", stdin);
	while ((ch = getchar()) != EOF)
	{
		if (ch == '\n')
		{
			if (ch_num <= 1)
			{
				blank_line_num++;	
			}
			ch_num = 0;
		}
		else if (ch != ' '&&ch != '\t')
			ch_num++;
	}
	fclose(stdin);
	return blank_line_num;
}

 

5. 统计文件的注释的行数count_comment_line()

该部分算法比较简单,只是在编写程序时需要注意一些细节方法,可以使程序显得简单明了

例如本小组用flag1是注释开头的标志位,current_line记录当前行是单行注释,flag2是多行注释开头的标志,line记录多行注释的行数,flag3是多行注释结尾的标志

//统计文件的注释的行数
int count_comment_line(char file[])
{
	//flag1是注释开头的标志位,current_line记录当前行是单行注释,flag2是多行注释开头的标志,line记录多行注释的行数,flag3是多行注释结尾的标志
	int comment_line_num = 0, ch_num=0, flag1 = 0, flag2 = 0, flag3 = 0, line = 0, current_line = 0;
	char ch;
	freopen(file, "r", stdin);
	while ((ch = getchar()) != EOF)
	{
		if (ch == '\n')
		{
			if (line&&ch_num > 0)//若line>0且字符数大于0表示当前行是多行注释中的一行
			{
				line++;
			}
			ch_num = flag1 = current_line = 0;
		}
		if (current_line == 1) continue;//当前行是单行注释,直接跳过对字符的判断直到读到换行符
		if (ch != ' '&&ch != '\n'&&ch != '\t') 
			ch_num++;
		if (flag2)//当前读到多行注释
		{
			if (ch != ' '&&ch != '\n'&&ch != '\t')
				ch_num++;
			if (ch == '*')//读到多行注释的结尾'*/'的符号'*'
				flag3 = 1;
			else if (ch == '/'&&flag3)//读到多行注释结尾的'/',必须连续读'*'和'/',否则flag3置0
			{
				comment_line_num += line;
				line = 0;
				flag2 = flag3 = 0;
				current_line = 1;
			}
			else
				flag3 = 0;
		}
		else if (ch == '/')
		{
			if (flag1 == 0)
				flag1 = 1;
			else if (flag1 = 1 && ch_num <= 3)//单行注释必须连续读取两个'/',且当前行在注释之前最多只有一个可显示字符
			{
				comment_line_num++;
				current_line = 1;
			}
		}
		else if (ch == '*')//多行注释的开头'/*'
		{
			if (flag1 == 1)
			{
				flag2 = 1;
				line = 1;//进入多行注释
			}
		}
		else
			flag1 = 0;
	}
	fclose(stdin);
	return comment_line_num;
}

6. wchar_t转换为string

编写该部分函数主要是为了使之后的输出和数据之间的传递更为方便

//wchar_t 转换 string 
void Wchar_tToString(std::string& szDst, wchar_t *wchar)
{
	wchar_t * wText = wchar;
	DWORD dwNum = WideCharToMultiByte(CP_OEMCP, NULL, wText, -1, NULL, 0, NULL, FALSE);// WideCharToMultiByte的运用
	char *psText;// psText为char*的临时数组,作为赋值给std::string的中间变量
	psText = new char[dwNum];
	WideCharToMultiByte(CP_OEMCP, NULL, wText, -1, psText, dwNum, NULL, FALSE);// WideCharToMultiByte的再次运用
	szDst = psText;// std::string赋值
	delete[]psText;// psText的清除
}

该部分代码参考博客:https://blog.youkuaiyun.com/chinahaerbin/article/details/7764483

7. 递归处理文件模块 search_file()

//递归处理文件模块
void search_file(string path, int idx)
{
	struct _finddata_t filefind;
	string cur = path + "*.*";
	int done = 0, handle;
	if ((handle = _findfirst(cur.c_str(), &filefind)) != -1)
	{
		while (!(done = _findnext(handle, &filefind)))
		{
			if (strcmp(filefind.name, "..") == 0)
				continue;
			if ((_A_SUBDIR == filefind.attrib)) 
			{  //是目录
				cur = path + filefind.name + '\\';
				search_file(cur, idx);    //递归处理
			}
			else //不是目录,是文件
			{
				int len = strlen(filefind.name);
				for (int i = 0; i < len; i++) 
				{
					if (filefind.name[i] == '.') 
					{
						len = i;
						break;
					}
				}
				if (strcmp(filefind.name + len, para[idx] + 1) == 0) 
				{
					cur = path + filefind.name;
					printf("%s:\n", filefind.name);
					for (int i = 1; i < idx; i++)
						basic_command(para[i], &cur[0]);
				}
			}
		}
		_findclose(handle);
	}
}

遇到的问题:

一开始编写该部分的函数时有点无从下手,之后会参考了某博主的写的博客,才有了较为清晰的思路。

传送门:https://fushengfei.iteye.com/blog/1100241

8.GUI界面

 

 

核心代码如下:

        //设置进程的重定向输出
        public void ProcessOutDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (this.Output.InvokeRequired)
            {
                this.Output.Invoke(new Action(() =>
                {
                    this.Output.AppendText(e.Data + "\r\n");
                }));
            }
            else
            {
                this.Output.AppendText(e.Data + "\r\n");
            }
        }
        //C#启动EXE文件
        public bool StartProcess(string filename, string[] args)
        {
            try
            {
                string s = "";
                foreach (string arg in args)
                {
                    s = s + arg + " ";
                }
                s = s.Trim();
                Process myprocess = new Process();
                myprocess.OutputDataReceived -= new DataReceivedEventHandler(ProcessOutDataReceived);
                ProcessStartInfo startInfo = new ProcessStartInfo(filename, s);
                startInfo.FileName = "WordCount.exe";
                startInfo.UseShellExecute = false;
                startInfo.WindowStyle = ProcessWindowStyle.Hidden;
                startInfo.CreateNoWindow = true;
                startInfo.RedirectStandardError = true;
                startInfo.RedirectStandardInput = true;//重定向输入
                startInfo.RedirectStandardOutput = true;//重定向输出
                myprocess.StartInfo = startInfo;
                myprocess.StartInfo.UseShellExecute = false;
                myprocess.Start();
                myprocess.BeginOutputReadLine();
                myprocess.OutputDataReceived += new DataReceivedEventHandler(ProcessOutDataReceived);
                return true;
            }
            catch (Exception ex)
            {
                MessageBox.Show("启动应用程序时出错!原因:" + ex.Message);
            }
            return false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            OpenFileDialog openFileDialog1 = new OpenFileDialog();   //显示选择文件对话框 
            openFileDialog1.InitialDirectory = "c:\\";
            openFileDialog1.Filter = "All files (*.*)|*.*";
            openFileDialog1.FilterIndex = 2;
            openFileDialog1.RestoreDirectory = true;

            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                this.FilePath.Text = openFileDialog1.FileName;     //显示文件路径 
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(this.FilePath.Text.Trim()))
            {
                MessageBox.Show("请选择文件");
            }
            string[] arg = new string[10];
            arg[0] = "-c"; arg[1] = "-w"; arg[2] = "-l"; arg[3] = "-a";
            arg[4] = this.FilePath.Text.Trim();
            StartProcess(@"WordCount.exe", arg);
        }

遇到的问题:

这一部分的进程重定向输出参考了博客:https://www.cnblogs.com/hsiang/p/6596276.html

启动exe文件参考了博客:http://www.cnblogs.com/HappyEDay/p/5498884.html

9. 主函数

int main(int argc, char* argv[])
{
	if (strcmp(argv[1], "-s") == 0) // 递归处理目录下符合条件的文件  
	{		
		TCHAR szPath[MAX_PATH] = { 0 };
		if (GetModuleFileName(NULL, szPath, MAX_PATH))//通过函数GetModuleFileName获取执行程序的绝对路径。
		{
			(_tcsrchr(szPath, _T('\\')))[1] = 0;//这里需要用到_tcsrchr函数来将获取到的执行程序的绝对路径中的执行程序的名称去掉
		}
		string Path;
		Wchar_tToString(Path, szPath);
		for (int i = 0; i < argc; i++)
			strcpy(para[i], argv[i]);
		search_file(Path, argc - 1);
	}
	else if (strcmp(argv[1], "-x") == 0) //图形界面 
	{	
		ShellExecuteA(NULL, "open", "GUI.exe", NULL, NULL, SW_SHOW);
	}
	else //对单个.c文件的统计 
	{									
		int num_para;
		for (num_para = argc - 1; num_para > 1; num_para--) 
		{
			if (strcmp(argv[num_para], "-s") == 0 || strcmp(argv[num_para], "-x") == 0
				|| strcmp(argv[num_para], "-c") == 0 || strcmp(argv[num_para], "-w") == 0
				|| strcmp(argv[num_para], "-l") == 0 || strcmp(argv[num_para], "-a") == 0)
			{
				break;
			}
		}
		char  filename[500] = { 0 };
		strcat(filename, argv[num_para + 1]);
		for (int i = num_para + 2; i < argc; i++) 
		{
			strcat(filename, " ");
			strcat(filename, argv[i]);
		}
		int flag_3 = check_file_name(filename);
		if (flag_3 == -1) 
		{
			printf("Error:The file does not exist !\n");
			return 0;
		}
		else if (flag_3 == -2) 
		{
			printf("Error:The file has no read permissions !\n");
			return 0;
		}
		printf("%s:\n", filename);
		for (int i = 1; i <= num_para; i++) 
		{
			if (check_command(argv[i]) == 1)
				basic_command(argv[i], filename);
			else
				printf("Command parameter error !\n");
		}
	}
	return 0;
}

 

六、PSP表格总结

PSP2.1Personal Software Process Stages预估耗时    (分钟)实际耗时    (分钟)
Planning计划  
Estimate估计这个任务需要多少时间3050
Development开发  
Analysis需求分析(包括学习新技术)200200
Design Spec生成设计文档100120
Design Review设计复审(和同事审核设计文档)10080
Coding Standard代码规范(为目前的开发制定合适的规范)3030
Design具体设计100150
Coding具体编码10001500
Code Review代码复审100200
Test测试(自我测试、修改代码、提交修改)100100
Reporting报告  
Test Report测试报告150150
Size Measurement计算工作量3050
Postmortem & Process Improvement Plan事后总结,并提出过程改进计划6060
 合计20002690

 

七、总结与反思

       这次项目设计与编码部分主要由我来完成,测试与博客撰写主要由我的队友完成。总的来说,编码部分的完成较为顺利,难点在于递归处理文件以及界面的设计部分。经过这次项目,个人编程能力得到了提升,也让我学会了如何与他人合作开发项目。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值