13. MFC实现U盘小偷程序:多线程与界面开发

1.前言

本文将带领大家来写一个U盘小偷的项目。

事实上,我已经写过一篇关于控制台版本U盘小偷的文章,可以参考这篇文章:手写一个U盘小偷

这篇文章里面涉及到的很多东西可能目前还没有讲到,所以推荐先学习一下本文再去看控制台版本的U盘小偷。

同时为了兼顾学习MFC编程、以及让程序更加好看,所以本文将打算用MFC写一个带界面的U盘小偷。

这里先介绍一下U盘小偷的运行原理:

  1. 监听U盘消息
  2. 遍历U盘中的所有文件
  3. 将U盘中的所有文件拷贝到计算机硬盘中

考虑到小偷这个属性,我们就还需要隐藏我们的程序,还有考虑到拷贝文件的效率,还需要采用多线程等等。

2.前置工作

这次的项目名称为day13--UThief,建立MFC项目,选用基于对话框使用静态库,其它选项都随意,默认都行,影响不大。

然后将默认的控件都删除掉:

image-20231209202309193

这里注意一下,对话框的里面还有一圈蓝色的虚线,这个虚线是控件能摆放的范围,即控件只能放在这个虚线之内。

所以一般我都会将这个虚线拖大到窗口大小,这样整个窗口都可以摆放控件了。

既然这个项目的主要功能是拷贝文件,那么我们就得先有一个控件用于显示拷贝的过程吧。

这里就用到了前面提到的编辑框了,它除了可以用于输入,当然也可以用于输出信息。

拖一个编辑框控件,然后将它拉大,可以通过下图箭头所示的按钮预览效果:

image-20231209202452153

这里先放一个编辑框吧,之后如果有其它的功能需要添加再说,然后是它的属性调整:

image-20231209202715812

  1. Auto VScrollauto为自动,V为垂直(Vertical),Scroll为滚动,即自动垂直滚动,就是当你在最后一行按换行后,就会自动行上滚。
  2. 垂直滚动:就是出现右侧的滚动条。
  3. 多行:很好理解,因为其默认只显示为1行。
  4. 想要返回:也就是我们按的Enter键,用于换行,不将它置为true,我们就无法换行。
  5. ID:控件识别名,相当于我们的身份证,用于识别控件的。

然后为它绑定一个控件变量:

image-20231209202816859

自此,控件方面暂时就这样了,后面到了有需要的地方,我们再一步一步添加。

然后我们还需要添加消息,即我们得监听U盘的插入与拔出等消息。

但如果你在网上搜:MFC U盘消息

会发现基本大部分文章内容都是需要提前注册设备号,但我们是U盘小偷啊,怎么可能提前知道设备号?

所以这种方法行不通,即MFC没有处理陌生设备消息的能力,那就没办法了,只能我们自己在比较原始的回调函数中处理U盘消息了。

回调函数不会不记得了吧?在windows编程入门章节详解介绍过,就是用于处理各种消息的。

当然,它也只是稍微原始一点,因为windows消息的回调函数也是已经被MFC封装过了,我们只需要用类向导,自动生成虚函数来处理即可:

image-20231209203149029

生成之后我们就能看到:

image-20231209203218438

这个函数的参数与最原始的消息回调函数是不是特别相似!只是将第一个参数封装了而已,原始的回调函数如下:

在这里插入图片描述

对于我们不需要处理的消息,都通过调用基类的函数进行默认处理,自此前置工作我们就做完了!

3.U盘消息

上述步骤主要作用是接收消息,我们想要接收的消息是U盘消息。

但这个回调函数会收到所有的窗口消息,所以我们还需要通过回调函数的第一个参数来判断是否为U盘消息。

因此我们需要知道U盘消息是什么,U盘只是设备的一种,更加统一的叫法其实是设备管理消息

所有的Windows消息都可以在官方文档中找到:windows消息

我们主要在意的是最后一个项目,即窗口消息(WMwindows message):

image-20231209203454744

窗口消息里面也有很多种类的消息,我们现在需要的是U盘消息,即设备管理消息(Device Management Messages)。

点进去我们就能看到消息的名称:

image-20231209203534034

所以代码中我们就可以这样写:

image-20231209203618190

但U盘消息也应该有不同种类啊,比如插入消息,拔出消息等等,所以点进这个消息看一看!

可以看到第三个参数,即MFC虚函数的第二个参数介绍如下:

image-20231209203728251

首先是它所在的头文件,然后下面是一些消息,我们只关注两个,一个是插入、一个是拔出:

image-20231209203846200

所以后面我们就是根据这个参数的值来判断设备的不同状态,然后我们就可以将上面的代码改造如下:

image-20231209204011971

至此,我们完成了消息处理,接下来就是书写处理这两个消息的代码了,代码如下:

	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盘后,通过链表将其名称返回:

image-20231209204141744

前面提到过链表的用法,因为U盘可能不止一个,所以我们可以通过这个函数来返回所有可用的U盘名称。

而链表声明为了CString类型,因为CString是MFC中通用的字符串类,可以自动适应宽窄字符集,窄字符集是多字符集的另一种叫法。

这里只写了函数的声明,前面提过一嘴如何快速用VS生成函数定义,不知道大家尝试过没有。

方法很简单,一般只写了函数声明,VS会在其名字下面划绿线,然后我们用鼠标点一下这个函数名,将光标移过去,按快捷键Ctrl+Enter,就会出现下图:

image-20231209204329681

点击它即可快速生成相应的代码,然后看这个函数内部的代码实现:

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盘中所有文件,得到完整路径,并且还需要拼接出目的地文件路径,才能完成拷贝的任务。

所以我们就需要一个控件用于用户输入保存文件的文件夹路径,那自然就想到用编辑框控件了,然后还需要一个静态文本控件来显示它是干嘛的:

image-20231209204641858

编辑框的属性只需要改下ID即可:

image-20231209204721547

然后还要为这个控件绑定一个变量,为了稳妥起见,我们就绑定控件变量吧:

image-20231209204821958

完成了这一步,我们就可以来思索一下我们函数的样子了。

它需要的参数应该就是两个,即源文件夹目的文件夹,然后这个函数就可以遍历源文件夹下的文件,复制到目标文件夹,所以就有了下面的代码:

image-20231209205514592

保存文件路径变量是我们绑定控件时,自动生成的,并且还写了一个函数的声明,他需要我们传入两个参数,第一个参数为源文件夹,第二个参数为目的文件夹。

	/**
	 * @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,就是最后一个字符的下标位置了。

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux