最近为了做课程的大作业自学了VS2010 MFC编程,选的题目非常简单——文本编辑器。控制台程序仅仅花了一个多小时就完成了,但为了使作业看起来更高端霸气一些,最后还是决定用MFC做个窗体界面的程序出来。
本人半年前开始接触C++,学了一学期,编控制台程序基本凑合,但为了这次这个小东西,前前后后还是花了一周有余的。先谈一点学习语言的经验教训,刚开始的几天我根本没有半点头绪,就是自己从图书馆借书看,知道有MFC这个东西,就是不知道怎么用,白看了三四天的书和视频。后来,经过同学的指点,我才熟悉了VS2010 MFC的界面,正式开始学习,其实现在回过头看,学习一门语言,一定得先熟悉它的编译器,熟悉了编译器之后,才能和计算机有更好的互动。实际操作才是学习语言最好的办法,这次的学习,到后期基本没看书,基本就是编译器+百度的搭配搞定了所有问题。
好了,废话不多说,先上程序的最终界面图
这个编辑器共实现了5个功能,打开、保存、统计、查找和删除。因为使用了编辑框,所以这个文本编辑器比DOS下的编辑器要好用的多得多。下面给出各个函数的代码,并且介绍编程时遇到的一些问题和心得。其中一些变量名下文都会介绍,如果疏忽了自己读一遍应该也能懂,基本不影响理解。
一、打开文件
前半段我调用了CFileDialog类,使得打开时能够跳出我们常见的文件打开窗体。值得一说的是,CFileDialog类中,只有获取文件路径的GetPathName函数(网上教程都只教到这儿就没了)。我做的时候还想,有没有这样一个函数,能直接打开我选择的文件,结果我找了半天,硬是没找到、、汗。。所以,为了实现点击文件即在编辑框(其变量为IDC_EDIT1)中打开文件内容的功能,最后还是采用文件流的形式。因为可以用GetPathName函数获取文件地址,所以使用CFile.Read函数,通过字符数组将文本内容导入cstring类里面,再通过SetDlgItemText在编辑器中显示文本。打开功能就此实现。
在这里我也有一个问题,大家可以看到,我使用的是char a[10000]这种静态分配方式(因为还没打开文件,无法获知文件长度),但真的文档编辑器绝对不可能这么做,一定是动态分配空间的,但我想到现在也没想到比较好的办法解决这个问题,如果有大鸟出现的话顺手帮我解决一下,感激不尽~
void CMy005Dlg::OnBnClickedButton1() { // TODO: 在此添加控件通知处理程序代码 char a [10000]; // 设置过滤器 TCHAR szFilter[] = _T("文本文件(*.txt)|*.txt|所有文件(*.*)|*.*||"); // 构造打开文件对话框 CFileDialog fileDlg(TRUE, _T("txt"), NULL, 0, szFilter, this); CString strFile,strFilePath; // 显示打开文件对话框 if (IDOK == fileDlg.DoModal()) { // 如果点击了文件对话框上的“打开”按钮,则将选择的文件显示到编辑框里 strFilePath=fileDlg.GetPathName(); CFile file(strFilePath, CFile::modeReadWrite); // 将数据写入到文件中 file.Read(a,10000); a[file.GetLength()]='\0'; CString str(a); SetDlgItemText(IDC_EDIT1,str); } }
二、保存
void CMy005Dlg::OnBnClickedButton2() { // TODO: Add your control notification handler code here // 设置过滤器 TCHAR szFilter[] = _T("文本文件(*.txt)|*.txt|Word文件(*.doc)|*.doc|所有文件(*.*)|*.*||"); // 构造保存文件对话框 CFileDialog fileDlg(FALSE, _T("doc"), _T("my"), OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, szFilter, this); CString strFilePath; // 显示保存文件对话框 if (IDOK == fileDlg.DoModal()) { // 如果点击了文件对话框上的“保存”按钮,则将选择的文件路径显示到编辑框里 strFilePath = fileDlg.GetPathName(); CString strText(_T("")); GetDlgItemText(IDC_EDIT1, strText); //获取edit box中的数据 try { CStdioFile file; file.Open(strFilePath, CFile::modeCreate | CFile::modeWrite | CFile::typeText); //打开D盘的txt文件 file.WriteString(strText); //写入edit box中的数据 file.Close(); } catch (CFileException* e) { e->ReportError(); e->Delete(); } } else return; }
保存功能的实现基本与打开类似,使用了WriteString函数,代码与其他网络版本大同小异,不赘述。
三、统计
这一部分在DOS中是最简单的,但到了MFC中,由于对字符串格式CString和char[]之间的转化问题不够了解,成了花费我最多时间的一块内容。
先谈一下字符串转化的机制:在MFC中,如果想要使控件中的内容传递到变量中去,要给控件添加相关变量,在此例中,我们应该给编辑框添加CString变量。作为数据结构的作业,要实现统计功能不能直接用类库函数,而cstring类又不支持str[i]形式的统计法,所以只能用数组定位这种方法实现统计功能,这就牵扯到字符串格式的转换问题。
首先,利用UpdateData函数将编辑框中内容传到其变量cstr中后,我们需要将其转化为字符数组。
最开始,我使用的是memcpy函数,代码段为memcpy(s,cstr,cstr.GetLength()),但鬼知道怎么回事,最后做统计的时候,所有统计都只统计恰好前一半的文本内容,一开始我以为是算法问题,后来,我检验了半天才发现是字符串转化时的问题,利用memcpy函数转化时,文本内容只转换了一半,但文本长度一样,被传递字符串的数组中每两个字符中有一个空字符,即若我传递的str=“abcdef”,数组s[0]=a,s[1]内容为空,s[2]=b,s[3]内容又为空,依次类推。。。所以最后只统计出一半的结果。这应该是函数的BUG,在此也请教各位高手。
后来的话,我就换了如下的方式,采用循环语句赋值(虽然从数据结构角度上讲时间复杂度上升了),但起码这个方案能够正确转换。再通过CString.Format函数与Messagebox实现统计结果的输出。
void CMy005Dlg::OnBnClickedButton3() { // TODO: Add your control notification handler code here INT_PTR nRes; UpdateData(TRUE); int len=cstr.GetLength(); char *s=new char[len+1]; int i; for(i=0;i<len;i++) { s[i]=cstr[i]; } s[len]='\0'; int a,b,c; a=b=c=0; for(i=0;i<len;i++) { if(s[i]>='0' && s[i]<='9') { a++;continue; } else if(s[i]>='A' && s[i]<='Z') { b++;continue; } else if(s[i]>='a' && s[i]<='z') { b++;continue; } else if(s[i]==' ') { c++;continue; } } CString str2; str2.Format(_T("文本中共有:\n数字个数:%d \n字母数:%d \n空格数:%d \n文章总字数:%d \n"),a,b,c,len); nRes = MessageBox(str2, _T("统计结果"), MB_OK); // 判断消息对话框返回值。如果为IDOK就return if (IDOK == nRes) return; }
四、查询
查询算法是数据结构作业的核心,在该查询算法中我使用了KMP算法,利用代码将其实现时,主要是分两步:
1,计算查询串NEXT值,存入数组
2,查询
这里next[0]设定为-1而不是书上的0,是为了更方便的实现字符的比较,设定为-1后,next值即对应其移动比较的位置,不用再做处理。dstr1为查询框文本变量
void CMy005Dlg::OnBnClickedButton4() { // TODO: Add your control notification handler code here INT_PTR nRes; UpdateData(TRUE); //将编辑框中的字符串赋给字符数组 int slen=cstr.GetLength(); char *s=new char[slen+1]; int i; for(i=0;i<slen;i++) { s[i]=cstr[i]; } s[slen]='\0'; int tlen=dstr1.GetLength(); char *t=new char[tlen+1]; for(i=0;i<tlen;i++) { t[i]=dstr1[i]; } t[tlen]='\0'; //KMP算法,求next值 int *next = new int[tlen]; i = 0; int j = -1; next[0] = -1; while(i<tlen-1) { if(j==-1 || t[i]==t[j]) { ++i; ++j; next[i] = j; } else { j = -1; } } //查找 int flag=0; i = 0; j = 0; while(i<slen && j<tlen) { if(j==-1 || s[i]==t[j]) { ++i; ++j; } else { j = next[j]; } if(j==tlen) { flag++;j=0; } } delete[] next; CString str; str.Format(_T("字符串共出现%d次 \n"),flag); nRes = MessageBox(str, _T("查询结果"), MB_OK); if (IDOK == nRes) return; }
五、删除
删除与查询的实现类似,也利用了KMP算法,所不同的是,为了实现删除功能,我们还得另外定义一个和文本内容一样长的int数组(原先使用的是bool数组,但无法实现动态分配空间,只得改用int),给其赋初值均为0,若查询到相同的字符串,首先将该段字符串所对应int数组均改为1,最后,再将int数组所有0对应的字符拼接起来,达到删除的效果。
void CMy005Dlg::OnBnClickedButton5() { // TODO: Add your control notification handler code here INT_PTR nRes; UpdateData(TRUE); int slen=cstr.GetLength(); char *s=new char[slen+1]; int i,j,k=0; int dlen=dstr2.GetLength(); int *visit=new int[slen]; memset(visit,0,sizeof(visit)); //将编辑框中的字符串赋给字符数组 for(i=0;i<slen;i++) { s[i]=cstr[i]; } s[slen]='\0'; char *d=new char[dlen+1]; for(i=0;i<dlen;i++) { d[i]=dstr2[i]; } d[dlen]='\0'; //KMP算法,求next值 int *next = new int[dlen]; i = 0; j = -1; next[0] = -1; while(i<dlen-1) { if(j==-1 || d[i]==d[j]) { ++i; ++j; next[i] = j; } else { j = -1; } } //查找删除 int flag=0; i = 0; j = 0; while(i<slen && j<dlen) { if(j==-1 || s[i]==d[j]) { ++i; ++j; } else { j = next[j]; } if(j==dlen) { for(k=i-dlen;k<i;k++) visit[k]=1; j=0; } } i=j=0; while(j<slen) { while(j<slen && visit[j]==1) j++; if(j==slen) break; s[i++]=s[j++]; } s[i]='\0'; delete[] next; delete[] visit; cstr=CA2CT(s); UpdateData(FALSE); }
趁热打铁,总算写完了,尽管写代码的时候千万种煎熬,发各种誓今后不当程序员,但做出程序的感觉真的还是很不错的,通过这次大作业也算是对MFC入了门(虽说MFC已经差不多被半淘汰了。。。),总之学习了很多内容,希望对大家有所帮助。