1.前言
本文将带领大家来写一个U盘小偷的项目。
事实上,我已经写过一篇关于控制台版本U盘小偷的文章,可以参考这篇文章:手写一个U盘小偷
这篇文章里面涉及到的很多东西可能目前还没有讲到,所以推荐先学习一下本文再去看控制台版本的U盘小偷。
同时为了兼顾学习MFC编程、以及让程序更加好看,所以本文将打算用MFC
写一个带界面的U盘小偷。
这里先介绍一下U盘小偷的运行原理:
- 监听U盘消息。
- 遍历U盘中的所有文件。
- 将U盘中的所有文件拷贝到计算机硬盘中。
考虑到小偷这个属性,我们就还需要隐藏我们的程序,还有考虑到拷贝文件的效率,还需要采用多线程等等。
2.前置工作
这次的项目名称为day13--UThief
,建立MFC项目,选用基于对话框和使用静态库,其它选项都随意,默认都行,影响不大。
然后将默认的控件都删除掉:
这里注意一下,对话框的里面还有一圈蓝色的虚线,这个虚线是控件能摆放的范围,即控件只能放在这个虚线之内。
所以一般我都会将这个虚线拖大到窗口大小,这样整个窗口都可以摆放控件了。
既然这个项目的主要功能是拷贝文件,那么我们就得先有一个控件用于显示拷贝的过程吧。
这里就用到了前面提到的编辑框了,它除了可以用于输入,当然也可以用于输出信息。
拖一个编辑框控件,然后将它拉大,可以通过下图箭头所示的按钮预览效果:
这里先放一个编辑框吧,之后如果有其它的功能需要添加再说,然后是它的属性调整:
Auto VScroll
:auto
为自动,V为垂直(Vertical
),Scroll
为滚动,即自动垂直滚动,就是当你在最后一行按换行后,就会自动行上滚。- 垂直滚动:就是出现右侧的滚动条。
- 多行:很好理解,因为其默认只显示为1行。
- 想要返回:也就是我们按的
Enter
键,用于换行,不将它置为true
,我们就无法换行。 ID
:控件识别名,相当于我们的身份证,用于识别控件的。
然后为它绑定一个控件变量:
自此,控件方面暂时就这样了,后面到了有需要的地方,我们再一步一步添加。
然后我们还需要添加消息,即我们得监听U盘的插入与拔出等消息。
但如果你在网上搜:MFC U盘消息
会发现基本大部分文章内容都是需要提前注册设备号,但我们是U盘小偷啊,怎么可能提前知道设备号?
所以这种方法行不通,即MFC没有处理陌生设备消息的能力,那就没办法了,只能我们自己在比较原始的回调函数中处理U盘消息了。
回调函数不会不记得了吧?在windows编程入门章节详解介绍过,就是用于处理各种消息的。
当然,它也只是稍微原始一点,因为windows
消息的回调函数也是已经被MFC封装过了,我们只需要用类向导,自动生成虚函数来处理即可:
生成之后我们就能看到:
这个函数的参数与最原始的消息回调函数是不是特别相似!只是将第一个参数封装了而已,原始的回调函数如下:
对于我们不需要处理的消息,都通过调用基类的函数进行默认处理,自此前置工作我们就做完了!
3.U盘消息
上述步骤主要作用是接收消息,我们想要接收的消息是U盘消息。
但这个回调函数会收到所有的窗口消息,所以我们还需要通过回调函数的第一个参数来判断是否为U盘消息。
因此我们需要知道U盘消息是什么,U盘只是设备的一种,更加统一的叫法其实是设备管理消息。
所有的Windows消息都可以在官方文档中找到:windows消息。
我们主要在意的是最后一个项目,即窗口消息(WM
:windows message):
窗口消息里面也有很多种类的消息,我们现在需要的是U盘消息,即设备管理消息(Device Management Messages)。
点进去我们就能看到消息的名称:
所以代码中我们就可以这样写:
但U盘消息也应该有不同种类啊,比如插入消息,拔出消息等等,所以点进这个消息看一看!
可以看到第三个参数,即MFC虚函数的第二个参数介绍如下:
首先是它所在的头文件,然后下面是一些消息,我们只关注两个,一个是插入、一个是拔出:
所以后面我们就是根据这个参数的值来判断设备的不同状态,然后我们就可以将上面的代码改造如下:
至此,我们完成了消息处理,接下来就是书写处理这两个消息的代码了,代码如下:
if (message == WM_DEVICECHANGE) {
switch (wParam)
{
case DBT_DEVICEARRIVAL: //设备插入的消息
break;
case DBT_DEVICEREMOVECOMPLETE: //设备拔出的消息
break;
default:
break;
}
}
4.U盘插入处理
首先来看我们应该如何处理插入U盘的消息。
现在是我们知道了电脑目前已经有U盘插入了,但我们并不知道插入的U盘名称,所以第一步就是找到U盘的名称。
为了方便,我们就写一个成员函数好了!
4.1 FindDriver
函数名就叫FindDriver
,目的是查找当前电脑所有U盘后,通过链表将其名称返回:
前面提到过链表的用法,因为U盘可能不止一个,所以我们可以通过这个函数来返回所有可用的U盘名称。
而链表声明为了CString
类型,因为CString
是MFC中通用的字符串类,可以自动适应宽窄字符集,窄字符集是多字符集的另一种叫法。
这里只写了函数的声明,前面提过一嘴如何快速用VS生成函数定义,不知道大家尝试过没有。
方法很简单,一般只写了函数声明,VS会在其名字下面划绿线,然后我们用鼠标点一下这个函数名,将光标移过去,按快捷键Ctrl+Enter
,就会出现下图:
点击它即可快速生成相应的代码,然后看这个函数内部的代码实现:
list<CString> Cday13UThiefDlg::FindDriver()
{
int len = GetLogicalDriveStrings(0, 0); //存储所有U盘字符需要的缓存区大小
TCHAR* dev = new TCHAR[len + 1];
GetLogicalDriveStrings(len+1, dev);//找到所有盘符
list<CString> uDrive;
for (int i = 0; i < len - 1; i++) {
if (dev[i] == _T('\0') && dev[i + 1] == _T('\0')) break; //到结尾,退出
if (dev[i] != '\0') continue; //不为盘符名分界,继续下一次循环
i += 1;
if (GetDriveType(dev+i) == DRIVE_REMOVABLE) uDrive.push_back(dev+i); //如果为U盘则添加到返回链表中
}
delete[] dev;
return uDrive;
}
这里获取盘符的函数为GetLogicalDriveStrings,它的作用很简单,就是返回当前电脑所有的盘符名称,即我们常见的C盘,D盘,E盘等等,具体细节参考官方文档。
注意,这里的函数同样只是一个宏,真正调用的是宽字符函数,这取决于我们当前MFC使用的多字节字符集还是宽字节字符集,只要在VS中字体颜色为紫色,就是宏,后面不再赘述了。
这里调用了它两次,第一次参数全部为0,目的是我们得先知道返回的字符串长度是多少。
int len = GetLogicalDriveStrings(0, 0);
TCHAR* dev = new TCHAR[len + 1];
然后分配空间,使用的通用类型TCHAR
,然后再次调用这个函数,传入缓存区与长度,就能得到所有的盘符。
需要注意的是,它所有的盘符,比如C盘与D盘,两个名称之间使用的0
分割的,即写在代码中就是\0
。
所以我们就可以利用这个特性,进行解析。
来到循环中,我们首先判断是不是连续的两个 0
,如果是就说明到了字符串结尾:
if(dev[i] == _T('\0') && dev[i + 1] == _T('\0')) break; //到结尾,退出
然后再进行判断当前字符的值是不是0
,不是则跳过,因为如果当前字符为0,且前面已经判断了这里不是连续的两个0,那么紧跟其后的就必然是盘符名称了。
所以只要当前字符为0,我们就将i++
,来到下一个字符,即盘符名称的首个字符位置。
因为dev
为字符串首地址,即第一个字符的地址,加i
之后就是第i
个字符的地址,又由于该字符从之间都是由0分割与结尾的。
所以正好符合C/C++
字符串的规则:以0
作为字符串结尾。
我们只需要传入盘符名称首个字符地址,就能完整读出盘符字符串!这就需要对字符串有深刻的理解!不然这里很可能你就想不明白。
然后我们又调用了GetDriveType函数,因为设备有很多,我们得判断这个设备为U盘才行!
判断是否为U盘的方法如上,更多细节请参考官方文档,这里不再赘述。
如果为U盘,我们就可以将这个U盘的名字推入链表中,最后delete[]
内存,将结果进行返回。
4.2 FindAllFile
上面我们已经可以获得U盘名称,那么下一步肯定就是看这个U盘里面有哪些文件。
所以我们还需要写一个找出U盘中所有文件的函数。
在写出一个函数前,我们需要先来考虑这样一些情况,那就是拷贝文件需要些什么?
随便在网上一搜就能发现,win API
中有一个拷贝文件函数为CopyFile:
BOOL CopyFile(
[in] LPCTSTR lpExistingFileName, //要进行拷贝的文件路径(包含文件名)
[in] LPCTSTR lpNewFileName, //目的地路径(包含文件名)
[in] BOOL bFailIfExists //文件如果已经存在,是否不覆盖,True表示不覆盖
);
可以看到,这个函数很简单,主要就是需要两个路径,即源文件路径和目的地的文件路径。
所以我们的需求就产生了,我们需要遍历U盘中所有文件,得到完整路径,并且还需要拼接出目的地文件路径,才能完成拷贝的任务。
所以我们就需要一个控件用于用户输入保存文件的文件夹路径,那自然就想到用编辑框控件了,然后还需要一个静态文本控件来显示它是干嘛的:
编辑框的属性只需要改下ID即可:
然后还要为这个控件绑定一个变量,为了稳妥起见,我们就绑定控件变量吧:
完成了这一步,我们就可以来思索一下我们函数的样子了。
它需要的参数应该就是两个,即源文件夹和目的文件夹,然后这个函数就可以遍历源文件夹下的文件,复制到目标文件夹,所以就有了下面的代码:
保存文件路径变量是我们绑定控件时,自动生成的,并且还写了一个函数的声明,他需要我们传入两个参数,第一个参数为源文件夹,第二个参数为目的文件夹。
/**
* @brief 将所有源文件夹中的内容复制到目标文件夹中
* @param srcDir 源文件夹
* @param desDir 目标文件夹
*/
void FindAllFile(CString srcDir,CString desDir);
是不是觉得这个函数名字有点词不达意了?这暂时不用管,因为这样写肯定是有问题的,我们后面再一步一步优化。
然后函数中的内容为:
void Cday13UThiefDlg::FindAllFile(CString srcDir, CString desDir)
{
if (srcDir[srcDir.GetLength() - 1] == _T('\\')) { //去除最后的\符号
srcDir.Delete(srcDir.GetLength() - 1, 1);
}
if (desDir[desDir.GetLength() - 1] == _T('\\')) { //去除最后的\符号
desDir.Delete(desDir.GetLength() - 1, 1);
}
if (!PathIsDirectory(desDir))
{
CreateDirectory(desDir, 0);
}
WIN32_FIND_DATA fileData{};
HANDLE hFile = FindFirstFile(srcDir+ _T("\\*"), &fileData);
if (hFile == INVALID_HANDLE_VALUE) {
return;
}
do {
CString fileName = fileData.cFileName;
if (fileName.Compare(_T(".")) == 0 || fileName.Compare(_T("..")) == 0) { //如果为.或..目录,直接跳过
continue;
}
if (fileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { //如果为目录,进行递归
continue;
}
CopyFile(srcDir + _T('\\') + fileData.cFileName, desDir + _T('\\') + fileData.cFileName, TRUE);
} while (FindNextFile(hFile, &fileData));
FindClose(hFile); //关闭句柄
}
首先进行了两个判断,看传入的路径是否存在斜杆\
,如果有就把它们去除掉,这里调用的Delete
函数,目的是删除最后一个字符:
srcDir.Delete(srcDir.GetLength() - 1, 1);
这个Delete
函数第一个参数是要删除的位置,第二个为要删除的个数。
因为要删除最后一个,所以第二个参数为1,第一个参数通过它的本身的GetLength
函数来获得,减一个1
,就是最后一个字符的下标位置了。