写一个远控

程序的入口点(想让其后台默认.exe进程运行)

也可以不通过vs设置也可以通过定义预处理设置

单例模式

保证系统中一个类只有一个对象被初始化(防止多次构造和析构造成全局流程的崩溃)

将其默认构造函数和拷贝构造函数设计为私有;在单例类内部定义了一个静态对象,作为唯一实例

项目里面的单例模式采用的是很完整的写法

//C:\Users\secqin\program\bianchen\code\lnnk1\promote\study_project\RemoteCtrl\RemoteCtrl\ServerSocket.h
class CserverSocket
{
public:
	static CserverSocket* getInstance() { //外部访问接口,类静态函数,可以直接调用
		if (m_instance == NULL) { //静态函数没有this指针,无法直接访问成员变量
			m_instance = new CserverSocket();
		}
		return m_instance;
	}
private: //单例模式,希望这个类只被构造和析构一次
	CserverSocket& operator=(const CserverSocket& ss) {} //赋值也要设置为私有
	CserverSocket(const CserverSocket&) {} //复制构造函数也要设置为私有
	CserverSocket(){ //构造函数肯定要私有
	}
	~CserverSocket() {//析构函数肯定要私有
		WSACleanup();
	}
static CserverSocket* m_instance; //声明一个静态指针变量,必须类外初始化(),如果是静态成员变量,外面不好初始化,指针赋值NULL就可以了,静态成员变量在类内声明,声明的作用只是限制静态成员变量作用域
}
//C:\Users\secqin\program\bianchen\code\lnnk1\promote\study_project\RemoteCtrl\RemoteCtrl\ServerSocket.cpp
CserverSocket* CserverSocket::m_instance = NULL; //静态指针变量的类外初始化(必须)

CserverSocket* pserver = CserverSocket::getInstance(); //指针是全局的,不是对象没有问题

但是这里有一个问题

我们使用堆创建的对象,new时候调用了构造函数,但是析构函数需要在delete时候调用

接下来我们想办法怎么调用那个析构函数,也就是说什么时候delete掉m_instance指针?

不能自己明确的去调用,需要让这个析构函数和整个程序消亡在附近,这只能通过另一个全局对象消亡时候,顺带着让我这个对象消亡才能做到,所以我们新定义一个类,这就是来完成这个使命的

我们创建一个新类,申请一个类静态对象(和程序同生命周期)

//C:\Users\secqin\program\bianchen\code\lnnk1\promote\study_project\RemoteCtrl\RemoteCtrl\ServerSocket.h
static void releaseInstance() { //静态成员函数只能访问静态成员(因为没有this指针所以不能访问普通成员变量或者函数)
		if (m_instance != NULL) {
			CserverSocket* tmp = m_instance;
			m_instance = NULL;
			delete tmp;
		}
	}	
class CHelper {
		public:
		CHelper() {
			CserverSocket::getInstance();
		}
		~CHelper() {
			CserverSocket::releaseInstance(); //通过类来调用只能是静态成员函数
		}
	};
	static CHelper m_helper;//声明,静态类的目的是为了将其变成全局变量
//C:\Users\secqin\program\bianchen\code\lnnk1\promote\study_project\RemoteCtrl\RemoteCtrl\ServerSocket.cpp
CserverSocket::CHelper m_helper;

静态变量不能在类内赋值,静态变量在全局变量赋值时要声明作用域,静态成员变量在类内声明,声明的作用只是限制静态成员变量作用域

上面的做法其实比较臃肿,下面是自己查资料写出来的一个更简便的方法(析构的问题成功自动解决)

socket操作的封装

封装到CserverSocket类中

初始化操作

	bool InitSocket() {
		//TODO,校验
		if (m_sock == -1) return false;
		sockaddr_in serv_adr;
		memset(&serv_adr, 0, sizeof(serv_adr));
		serv_adr.sin_family = AF_INET;
		serv_adr.sin_addr.s_addr = INADDR_ANY; //可能不是单网卡,可能有很多网卡,多IP
		serv_adr.sin_port = htons(9527);
		//绑定
		if (bind(m_sock, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) {
			return false;
		}//TODO;
		//TODO;
		if (listen(m_sock, 1) == -1) { //1对1的,队列里面存一个就行
			return false;
		}
		return true;
	}

server端的accept操作

	bool AcceptClient() {
		//read文件
		sockaddr_in client_adr;
		
		int cli_sz = sizeof(client_adr);
		m_client = accept(m_sock, (sockaddr*)&client_adr, &cli_sz);
		if (m_client == -1) return false;
		return true;
		//recv(serv_sock, buffer, sizeof(buffer), 0);
		//send(serv_sock, buffer, sizeof(buffer), 0);
	}

提取包中的命令

	int DealCommand() { //无限循环
		if (m_client == -1) return -1; //断开连接了
		//char buffer[1024] = "";
		char* buffer = new char[4096]; //缓冲区
		memset(buffer, 0, 4096); //缓冲区置0
		size_t index = 0;
		while (true) {
			size_t len = recv(m_client, buffer+index, 4096-index, 0); //index是用掉的
			if (len <= 0) {
				return -1;
			}
			index += len; //可能收到2000个字节的包
			len = index;
			m_packet = CPacket ((BYTE*)buffer, index); //将缓冲区包化
            //这个地方出来的index就是处理包获得一段有效包用掉的大小
			if (len > 0) { //其实这段是废话,能到这里len已经大于0了
				memmove(buffer, buffer + len, 4096-len);//从buffer + len复制4096-len个字节到buffer,将下一个包数据往前挪
				index -= len; //可能只用1000个
				return m_packet.sCmd;
			}
		}
		return -1;
	}

这段代码不是很理解,插个flag,这段代码逻辑肯定有问题,后面再来修改

设计的包结构

定义一个定制包类

class CPacket
{
````
public:
	WORD sHead; //FEFF
	DWORD nLength; //包长度(从控制命令开始到和校验结束)
	WORD sCmd; //控制命令
	std::string strData; //要发的数据
	WORD sSum;//和校验
}

将server端的数据封装为包(不带命令参数)(收到缓冲区和长度时候)

CPacket(const BYTE* pData, size_t& nSize) { //一开始传入的这个nSize是传入的一个包总大小
	size_t i = 0;
	for (; i < nSize; i++) {
		if (*(WORD*)(pData + i) == 0xFEFF) {
			sHead = *(WORD*)(pData + i);
			i += 2; //解析成功后,给解析失败那里加逻辑
			break;
		}
	} //有没有一种可能性,就是前面杂音数据是不是字节整数倍的情况
	if (i+4+2+2 > nSize) { //解析失败,怕越界呀,白数据可能不全,或者包头未能全部接收到吗,这个前提是找到头了
		nSize = 0; //用掉了0个字节
		return;
	}
	nLength = *(DWORD*)(pData + i); i += 4;
	if (nLength + i > nSize) { //包未完全接收到,就返回,解析失败,半个包
		nSize = 0;//用掉了0个字节
		return;
	}
	sCmd = *(WORD*)(pData + i); i += 2;
	if (nLength > 4) { //这是构成包的前提
		strData.resize(nLength - 2 - 2);//命令和校验位,长度不包含自身
		memcpy((void*)strData.c_str(), pData + i, nLength - 4); //将pData + i起始地址的nLength - 4个连续的字节复制到包的data区
		i += nLength - 4;
	}
	sSum = *(WORD*)(pData + i); i += 2;
	WORD sum = 0;
	for (size_t j = 0; j < strData.size(); j++) {
		sum += BYTE(strData[j]) & 0xFF;
	}
	if (sum == sSum) {
		nSize = i;//nLength + 2 + 4;//head length data
		return;
	}
	nSize = 0;//解析失败了
}

将server端的数据封装为包(带命令参数)

这个程序在server端运行,然后获取server端的信息,自己想办法要将获取到的信息封装为包

通过构造函数

	CPacket():sHead(0), nLength(0), sCmd(0), sSum(0){}//无参构造函数
	CPacket(WORD nCmd, const BYTE* pData, size_t nSize) { //常量指针,指向的内容不能改变
		sHead = 0xFEFF; //头
		nLength = nSize + 2+2; //数据的长度加上命令长度加上校验的长度
		sCmd = nCmd; //命令
		if (nSize > 0) { //有数据
			strData.resize(nSize); //给包data容器重新设置长度
			memcpy((void*)strData.c_str(), pData, nSize); //给包打他字段设置上
		}
		else { //没有数据了,头字段和命令字段要清空
			strData.clear();
		}
		sSum = 0; //校验和
		for (size_t j = 0; j < strData.size(); j++) {
			sSum += BYTE(strData[j]) & 0xFF;
		}
	}

对于socket操作的封装后面一个个介绍

对于strData.c_str(),必须加c_str(),因为string里面不是单纯存储了字符串,还存了其他信息,所以需要将其转成char数组(c语言字符串类型)

获取server端电脑里面存在哪些盘符

int MakeDriveInfo() { //1==>A 2==>B 3==>C ... 26==>Z
    std::string result;
    for (int i = 1; i < 26; i++) {
        if (_chdrive(i) == 0) //改变当前的驱动,_chdrive函数(c和c++中)应该是封装了win的api函数GetLogicalDrives(说反了)
        {
            if (result.size() > 0) {
                result += ',';
            }
            result += 'A' + i - 1;
            //切换成功
        }
    }
    CPacket pack(1, (BYTE*)result.c_str(), result.size()); //重载了构造函数(如果不重载成员变量需要一个个值赋值太麻烦了),打包用的,抽象
    Dump((BYTE*)pack.Data(), pack.Size()); //自己控制台输出打包数据(debug)
    //CserverSocket::getInstance()->Send(pack);
    return 0;
}

因为client端没有写好,所以我们需要一个函数来调试

void Dump(BYTE* pData, size_t nSize) {
    std::string strOut;
    for (size_t i = 0; i < nSize; i++) {
        char buf[8] = "";//缓冲区,把二进制的0/1当char一个字节来存入(质疑)
        if (i > 0 && (i % 16 == 0)) strOut += "\n";
        snprintf(buf, sizeof(buf), "%02X", pData[i] & 0xFF); //按16进制输出,不足两位补0
        strOut += buf;
    }
    strOut += "\n";
    OutputDebugStringA(strOut.c_str());
}

snprintf

snprintf 是一个 C 标准库函数,用于格式化字符串并将结果写入指定的字符数组中,以及控制最大写入的字符数(通过第二参数size),以防止缓冲区溢出。(snprintf会自动在末尾补上'\0',所以复制的字符长度为size - 1)

snprintf 的工作方式类似于 printf,但是不会将结果输出到标准输出,而是将结果写入到提供的字符数组中。它会根据指定的格式字符串和可变数量的参数生成一个格式化的字符串,并将其复制到指定的缓冲区中,直到达到指定的最大字符数或格式化操作完成为止

OutputDebugString

OutputDebugString属于windows API的,所以只要是包含了window.h这个头文件后就可以使用了。可以把调试信息输出到编译器的输出窗口,还可以用DbgView(本机或TCP远程)这样的工具查看,这样就可以脱离编译器了

调试后控制台出现这个

FFFE 07000000 0100 432C44 B300

432C44转为ASCII码为C,D

但是我们头设计的是sHead = 0xFEFF,这个按小端方式输出了(sCmd同样这样)

查看指定目录(client那端指定)下的文件

int MakeDirectoryInfo() {
    std::string strPath;
    //std::list<FILEINFO> lstFileInfos; //收集信息
    if (CserverSocket::getInstance()->GetFilePath(strPath) == false) {
        OutputDebugString(_T("当前的命令,不是获取文件列表,命令解析错误!!!"));
        return -1;
    }
    if (_chdir(strPath.c_str()) != 0) { //代表失效了,没有打开这个目录
        FILEINFO finfo;
        finfo.IsInvalid = TRUE; //失效
        finfo.IsDirectory = TRUE;
        finfo.HasNext = FALSE;
        memcpy(finfo.szFileName, strPath.c_str(), strPath.size()); //复制进去文件名
        //lstFileInfos.push_back(finfo);
        CPacket pack(2, (BYTE*)&finfo, sizeof(finfo));
        CserverSocket::getInstance()->Send(pack);
        OutputDebugString(_T("没有权限访问目录!!"));
        return -2;
    }
    _finddata_t fdata;
    int hfind = 0; //接下来就是看文件夹下没有有文件
    if((hfind = _findfirst("*", &fdata)) == -1){
        OutputDebugString(_T("没有找到任何文件!!"));
        return -3;
    }
    do { //有文件,默认执行了_findfirst,上面定义的fdata包含第一个文件的信息
        FILEINFO finfo; //定义结构体
        finfo.IsDirectory = (fdata.attrib & _A_SUBDIR) != 0; //看是不是文件夹属性
        finfo.IsInvalid = false; //有效
        memcpy(finfo.szFileName, fdata.name, strlen(fdata.name)); //复制文件名
        //lstFileInfos.push_back(finfo);
        CPacket pack(2, (BYTE*)&finfo, sizeof(finfo));//将这个结构体信息打包
        CserverSocket::getInstance()->Send(pack);//发送
    } while (!_findnext(hfind, &fdata));//因为文件结构是一个树
    //发送信息到控制端
    FILEINFO finfo;
    finfo.HasNext = FALSE; //置为false,收尾
    CPacket pack(2, (BYTE*)&finfo, sizeof(finfo));
    CserverSocket::getInstance()->Send(pack);
    //如果文件夹下有10000个文件怎么办(日志文件夹,临时文件夹等),切片来解决
    return 0;
}

从包里面获取文件路径

	bool GetFilePath(std::string& strPath) {
		if ((m_packet.sCmd <= 4)&& (m_packet.sCmd >= 2)) {
			strPath = m_packet.strData;
			return true;
		}
		return false;
	}

对于获取的文件信息我们需要定义一个结构体

typedef struct file_info{ //struct和class很想在c++中
    file_info() {
        IsInvalid = false;
        IsDirectory = -1;
        HasNext = TRUE;
        memset(szFileName, 0, sizeof(szFileName));
    } //结构体不用析构
    BOOL IsInvalid;//是不是有效的文件(快捷方式需要排除)
    BOOL IsDirectory;//是否为目录 0 否 1 是
    BOOL HasNext;//是否还有后续 0没有 1有
    char szFileName[256];//文件名

}FILEINFO,*PLFILEINFO;

_finddata_t结构体

_finddata_t是用来存储文件各种信息的结构体,使用这个结构体要引用的头文件为“ #include <io.h>”

struct _finddata_t
        {
             unsigned attrib; //文件属性
             time_t time_create; //保存从1970年1月1日0时0分0秒到现在时刻的秒数
             time_t time_access; //文件最后一次被访问的时间
             time_t time_write;//文件最后一次被修改的时间
             _fsize_t size;//文件的大小(字节数为单位)
             char name[_MAX_FNAME];//文件的文件名
        };

对于Send的重构

	bool Send(const char* pData, int nSize) {
		if (m_client == -1) return false;
		return send(m_client, pData, nSize, 0) > 0;
	}
	bool Send(CPacket& pack) {
		if (m_client == -1) return false;
		return send(m_client, pack.Data(), pack.Size(), 0) > 0;
	}

将包解构成char数组好满足缓冲区的需求

	const char* Data() {
		strOut.resize(nLength + 6);
		BYTE* pData = (BYTE*)strOut.c_str();
		*(WORD*)pData = sHead; pData += 2;
		*(DWORD*)(pData) = nLength; pData += 4;
		*(WORD*)pData = sCmd; pData += 2;
		memcpy(pData, strData.c_str(), strData.size()); pData += strData.size();
		*(WORD*)pData = sSum;
		return strOut.c_str();
	}

运行文件

int RunFile() {
    std::string strPath;
    CserverSocket::getInstance()->GetFilePath(strPath);
    ShellExecuteA(NULL, NULL, strPath.c_str(), NULL, NULL, SW_SHOWNORMAL); //相当于双击给出的路径名字
    CPacket pack(3, NULL, 0); //必须要要client知道我这个结束了,就是发0
    CserverSocket::getInstance()->Send(pack);
    return 0;
}

下载文件

int DownloadFile() {
    std::string strPath;
    CserverSocket::getInstance()->GetFilePath(strPath);
    long long data = 0;
    FILE* pFile = NULL;
    errno_t err = fopen_s(&pFile,strPath.c_str(), "rb");//按二进制(可以包含文本也可以二进制)只读打开
    
    if (err != 0) {
        CPacket pack(4, NULL, 0); //文件啥也没有拿到
        CserverSocket::getInstance()->Send(pack);
        return -1;
    }
    if (pFile != NULL) {
        fseek(pFile, 0, SEEK_END);//设置到尾(最后一个字节)
        data = _ftelli64(pFile); //长度,_ftelli64:返回当前文件位置
        CPacket head(4, (BYTE*)&data, 8); //通过8个字节拿到文件的长度
        fseek(pFile, 0, SEEK_SET);//设置到头
        char buffer[1024] = ""; //读1KB发1KB
        size_t rlen = 0;
        do {
            rlen = fread(buffer, 1, 1024, pFile);//从pFile读取数据到buffer 1KB的缓冲区
            CPacket pack(4, (BYTE*)buffer, rlen);
            CserverSocket::getInstance()->Send(pack);
        } while (rlen >= 1024); //还未读到文件尾
        fclose(pFile);
    }
    CPacket pack(4, NULL, 0);
    CserverSocket::getInstance()->Send(pack);
    return 0;
}

流式文件操作和I/O文件操作的区别:

流式文件操作(Stream-based File I/O)和I/O文件操作的主要区别在于数据的读写方式。

  • 流式文件操作:流式文件操作涉及的是字节流或者字符流的读写,它适合处理大量数据或者不知道数据总大小的文件。使用流式操作时,文件内容不会一次性全部加载到内存中,而是按照顺序读取或写入的,每次处理数据块,这样可以避免大量内存消耗。

  • I/O文件操作:I/O操作通常指的是输入/输出操作,它包含了更广泛的文件处理方式,如顺序读取、随机访问等。I/O操作可以一次性读取或写入大量数据到内存中,也可以进行随机访问文件中的任意位置。

流式文件操作更加高级,适合文本处理,而I/O文件操作则更为底层,适用于二进制数据的处理

流式文件操作

这种方式的文件操作有一个重要的结构FILE,FILE在头文件stdio.h中定义如下:

typedef struct {
int level;
unsigned flags;
char fd;
unsigned char hold;
int bsize;
unsigned char _FAR *buffer;
unsigned char _FAR *curp;
unsigned istemp;
short token;
} FILE;

FILE这个结构包含了文件操作的基本属性,对文件的操作都要通过这个结构的指针来进行,此种文件操作常用的函数见下表 函数 功能

fopen() 打开流 fclose() 关闭流 fputc() 写一个字符到流中 fgetc() 从流中读一个字符 fseek() 在流中定位到指定的字符 fputs() 写字符串到流 fgets() 从流中读一行或指定个字符 fprintf() 按格式输出到流 fscanf() 从流中按格式读取 feof() 到达文件尾时返回真值 ferror() 发生错误时返回其值 rewind() 复位文件定位器到文件开始处 remove() 删除文件 fread() 从流中读指定个数的字符 fwrite() 向流中写指定个数的字符 tmpfile() 生成一个临时文件流 tmpnam() 生成一个唯一的文件名

远程截图

源码

int SendScreen() { //先功能实现再错误处理
    CImage screen; //图片适合windowsGDI(全局设备接口)编程
    HDC hScreen = ::GetDC(NULL); //设备上下文,得到句柄
    int nBitperPixel = GetDeviceCaps(hScreen, BITSPIXEL); //我们这个应该返回24(255×255×255)8+8+8,还有带透明度8位的
    int nWidth = GetDeviceCaps(hScreen, HORZRES); //拿到宽是多少个像素点
    int nHeight = GetDeviceCaps(hScreen, VERTRES); //垂直
    screen.Create(nWidth, nHeight, nBitperPixel);
    BitBlt(screen.GetDC(), 0, 0, 1920, 1020,hScreen,0,0,SRCCOPY); //跳过任务栏 MFC基础课里面有
    ReleaseDC(NULL, hScreen);

    //保存到内存中
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, 0);
    if (hMem == NULL) return -1;
    IStream* pStream = NULL; //流(内存缓冲区)
    HRESULT ret = CreateStreamOnHGlobal(hMem,TRUE,&pStream);//全局对象创建流,将流和内存这个句柄绑定
    if (ret == S_OK) {
        screen.Save(pStream, Gdiplus::ImageFormatPNG); //流里面保存的就是PNG的图片
        LARGE_INTEGER bg = { 0 };
        pStream->Seek(bg,STREAM_SEEK_SET,NULL);//从头开始设置
        PBYTE pData = (PBYTE)GlobalLock(hMem); //需要锁定内存
        size_t nSize = GlobalSize(hMem); //看内存的大小
        CPacket pack(6, NULL, nSize);
        CserverSocket::getInstance()->Send(pack);
        GlobalUnlock(hMem); //解锁这个内存
    }
    //screen.Save(_T("test2020.png"), Gdiplus::ImageFormatPNG);
    //screen.Save(_T("test2020.jpg"), Gdiplus::ImageFormatJPEG);
    pStream->Release();
    GlobalFree(hMem); //释放内存
    screen.ReleaseDC();
    return 0;
}

关于CImage

CImage类是基于GDI+的,但是这里为什么要讲来源于GDI?

主要是基于这样的考虑: 在GDI+环境中,我们可以直接使用GDI+ ,没多少必要再使用CImage类 但是,如果再GDI环境中,我们要想使用GDI+,有点麻烦,还得加入头文件,加入启动GDI+的代码和关闭GDI+的代码,显得太罗嗦了,GDI 的CBitmap 处理功能又有局限,只能处理BMP格式的图片。 怎么办?这时,我们便可使用CImage类,因为这个类本身封装了GDI+得使用环境,所以无需我们手动设置,简化了我们的操作。 同时,又可以利用GDI+中强大的图片处理功能,及可以简便的与CBitmap对象进行转换 ,大大方便了在GDI环境下,进行各种图片处理工作 。 其实,将其称作 GDI/ GDI+ 混合编程,这样才更确切些

说人话就是在GDI的环境下封装了GDI+函数,然后功能强大方便处理

CImage是MFC和ATL共享的新类,它能从外部磁盘中调入一个JPEG、GIF、BMP和PNG格式的图像文件加以显示,而且这些文件格式可以相互转换。

CImage是VC.NET中定义的一种MFC/ATL共享类,也是ATL的一种工具类,它提供增强型的(DDB和DIB)位图支持,可以装入、显示、转换和保存多种格式的图像文件,包括BMP、GIF、JPG、PNG、TIF等。CImage是一个独立的类,没有基类。(CImage类是基于GDI+的,从VC.NET起引进,VC 6.0中没有。)

ATL(Active Template Library,活动模板库)是一套基于模板的 C++ 类,用以简化小而快的 COM 对象的编写。

为了在MFC程序中使用CImage类,必须包含ATL的图像头文件atlimage.h:(在VS08 SP1中不用包含)

说人话就是功能强大不讲武德

关于设备上下文

又称为设备描述表。windows提供设备描述表,用于应用程序和物理设备之间进行交互,从而提供了应用程序设计的平台无关性(兼容各大平台,就像驱动层提供的统一接口一样)

定义一组图形对象及其属性、影响输出的图形方式(数据)结构,它包括了一个设备(如显示器和打印机)的绘制属性相关的信息。所有的绘制操作通过设备描述表进行,应用程序不能直接访问设备描述表,只能由各种相关API函数通过设备描述表的句柄间接访问该结构,在windows GDI界面下,它总是相关于某个窗口或这窗口上的某个显示区域

通常意义上窗口的设备描述表,一般指的是窗口的客户区,不包括标题栏菜单栏所占有的区域,而对于整个窗口来说,其设备描述表严格意义上来讲应该称为窗口设备描述表,它包含窗口的全部显示区域。二者的操作方法完全一致,所不同的仅仅是可操作的范围不同而已

windows 窗口一旦创建,它就自动地产生了与之相对应的设备描述表数据结构,用户可运用该结构,实现对窗口显示区域的GDI操作,如划线、写文本、绘制位图、填充等,并且所有这些操作均要通过设备描述表句柄来进行

我们通过设备上下文来处理(DC)

说人话就是程序创建一个窗口后就会创建设备描述表来操作这个窗口(或者整个屏幕)对应的显示设备(显示画图之类的)

HDC GetDC(HWND hWnd);

该函数检索一指定窗口的客户区域或整个屏幕的显示 设备上下文环境的句柄,以后可以在GDI函数中使用该句柄来在设备上下文环境中绘图

hWnd:设备上下文环境被检索的窗口的句柄,如果该值为NULL,GetDC则检索整个屏幕的设备上下文环境

上面代码就是检索整个屏幕,得到的是可以获取整个屏幕图像的句柄

返回值:如果成功,返回指定窗口客户区的设备上下文环境;如果失败,返回值为Null。

对于普通设备上下文环境,GetDC在每次检索的时候部分分配给它缺省特性,对于典型和特有的设备上下文环境,GetDC不改变先前设置的特性。在使用普通设备上下文环境绘图之后,必须调用ReleaseDc函数释放该设备上下文环境,典型和特有设备上下文环境不需要释放,设备上下文环境的个数仅受有效内存的限制

int GetDeviceCaps( [in] HDC hdc, [in] int index);

hdc:设备上下文的句柄,通过GetDC()获得

对于index,从win官网截取这部分

HORZRES屏幕的宽度(以像素为单位)。(拿到宽的像素大小)
VERTRES屏幕的高度(以光栅线为单位)。(拿到高的像素大小)
BITSPIXEL每个像素的相邻颜色位数。(相当于24位真色彩那种,8+8+8)

我们就是为了截图,不是为了写windows的数位板的驱动程序,所以不需要了解那么深入

CImage的Create()成员函数

CImage的对象.Create(图像的像素宽,图像的像素高,位深; //前两个参数的单位是像素,bpp是位深度,jpeg的位深度是24

此时此刻CImage的对象和全局设备上下文关联起来

通过这一步其实已经拿到截图了,但是我们需要跳过任务栏

BOOL BitBlt( [in] HDC hdc, [in] int x, [in] int y, [in] int cx, [in] int cy, [in] HDC hdcSrc, [in] int x1, [in] int y1, [in] DWORD rop );

前面是目标矩形,后面是源矩形

对于rop参数:

SRCCOPY将源矩形直接复制到目标矩形。

CImage的对象和全局设备上下文此刻关联起来了

ReleaseDC(NULL, hScreen);

然后可以释放掉全局设备上下文

CImage的Save()成员函数

HRESULT Save(
    IStream* pStream, //文件流
    REFGUID guidFileType) const throw();

HRESULT Save(
    LPCTSTR pszFileName, //保存下来的文件路径和文件名
    REFGUID guidFileType = GUID_NULL) const throw();

guidFileType
保存图像的文件类型。 可以是以下值之一:

  • ImageFormatBMP 未压缩的位图图像。
  • ImageFormatPNG 可移植网络图形格式 (PNG) 压缩图像。
  • ImageFormatJPEG JPEG 压缩图像。
  • ImageFormatGIF GIF 压缩图像。

screen.Save(_T("test2020.png"), Gdiplus::ImageFormatPNG);

但是我们需要将数据发出去,需要打包,所以需要将这个图片存到内存缓冲区(流)里面

下面就是这个操作的函数

GlobalAlloc(UINT uFLAG,DWORD dwBytes);

我们获取到文件以后,这个文件在磁盘里面,我们需要将其放到内存里面

GlobalAlloc申请的内存分两种,一种是GMEM_FIXED(固定分区分配),另一种是GMEM_MOVEABLE(动态分区分配)。两者的差别只要在于GMEM_MOVEABLE类型的内存操作系统是可以移动的,比如堆中有好几块小内存,当再申请一大块内存时,操作系统会移动GMEM_MOVEABLE类型的内存来合并出一大块(4个算法)。正因为GMEM_MOVEABLE是可移动的,所以要用句柄标识,不能用内存地址标识,在使用时通过GlobalLock由句柄得到内存地址。对于GMEM_FIXED类型的,该函数返回的句柄就是内存指针,可以直接当内存指针使用。

dwBytes

要分配的字节数。 如果此参数为零,并且 uFlags 参数指定 GMEM_MOVEABLE,则函数将返回一个标记为已放弃的内存对象的句柄。

如果函数成功,则返回值是新分配的内存对象的句柄。

如果函数失败,则返回值为 NULL。

Windows 内存管理不提供单独的本地堆和全局堆。 因此, GlobalAllocLocalAlloc 函数本质上是相同的。

除非文档明确指出应使用全局函数,否则新应用程序应使用 [堆函数](https://learn.microsoft.com/zh-cn/windows/desktop/Memory/heap-functions) 来分配和管理内存。

这个函数相当于得到一个数组(我们之前的缓冲区形式,这个默认在内存里面)的地址了

我们需要全局对象创建流,然后将内存和那个流进行绑定,才能保存下来那个截图

CreateStreamOnHGlobal(全局内存的句柄,TRUE,流对象地址)

全局对象创建流,将流和内存这个句柄绑定

pStream->Seek(bg,STREAM_SEEK_SET,NULL);

HRESULT Seek(
  [in]  LARGE_INTEGER  dlibMove,
  [in]  DWORD          dwOrigin,
  [out] ULARGE_INTEGER *plibNewPosition   //一般设置为NULL
);

LARGE_INTEGER是大整数的结构体,dlibMove相当于之前我们常说的offset偏移量,如果 dwOrigin是STREAM_SEEK_SET,则会将其解释为无符号值,而不是有符号值

dlibMove 中指定的位移的原点。 原点可以是文件 (STREAM_SEEK_SET ) 的开头、当前查找指针 (STREAM_SEEK_CUR) ,也可以是文件 (STREAM_SEEK_END) 的末尾。 有关值的详细信息,请参阅 STREAM_SEEK 枚举。

这个函数是

產品版本
Visual Studio SDK2015, 2017, 2019, 2022

这几个vs版本特有的SDK

LPVOID GlobalLock( [in] HGLOBAL hMem );

锁定全局内存对象并返回指向对象内存块的第一个字节的指针。和GlobalAlloc搭配使用

BOOL GlobalUnlock( [in] HGLOBAL hMem );

解锁内存

锁屏操作

很多勒索病毒的原理就是这样,先锁屏,然后鼠标键盘所有的都操作不了,然后再需要联系解锁,网吧计时结束就是采用这个锁屏,让你操作不了

锁机时候需要消息泵,需要while循环来等待键盘的消息,但是我们server端需要不断的监听来自client的请求,所以也需要while循环不断等待,所以我们不能只用一个线程,我们需要多线程了

第一步:我们需要创建一个dialog视图

项目右键,然后添加资源

然后我们右键属性

修改这几个属性,同时将标题栏设置为false

然后在工具箱里面

选择static text添加上文字,然后在dialog属性里面改文字的字体(注意不是在字体属性上面改)

这是为了将控件和我们之前的项目联系起来,注意那个ID

主代码如下

int LockMC() { //锁机
    if (dlg.m_hWnd == NULL || (dlg.m_hWnd == INVALID_HANDLE_VALUE)) { //未初始化的情况
        //_beginthread(threadLockDlg, 0, NULL);
        _beginthreadex(NULL, 0, threadLockDlg, NULL, 0, &threadid);
        TRACE("threadid=%d\r\n",threadid);
    }
    CPacket pack(7, NULL, 0);
    CserverSocket::getInstance()->Send(pack);
    return 0;
}

我们采用了多线程编程

_beginthreadex函数

unsigned long _beginthreadex( 
void * _Security,       //第1个参数:安全属性,NULL为默认安全属性
unsigned  _StackSize,  //第2个参数:指定线程堆栈的大小。如果为0,则线程堆栈大小和创建它的线程的相同。一般用0
unsigned ( __stdcall * _StartAddress )( void * ), //第3个参数:指定线程函数的地址,也就是线程调用执行的函数地址(用函数名称即可,函数名称就表示地址)
void * _Arglist,        //第4个参数:传递给线程的参数的指针,可以通过传入对象的指针,在线程函数中再转化为对应类的指针
unsigned int  _Initflag,    //第5个参数:线程初始状态,0:立即运行;CREATE_SUSPEND:suspended(悬挂)
unsigned int*  _Thrdaddr   //第6个参数:用于记录线程ID的地址,这个参数我们一开始可以设置为0,然后线程运行就会自动改这个值为该有的值,相当于我们记录下来了线程的id
);

子线程里面的代码如下:

unsigned _stdcall threadLockDlg(void* arg) {
    TRACE("%s(%d)\r\n", __FUNCTION__,__LINE__,GetCurrentThreadId());
    //弹出一个消息(请联系管理员解锁)(最顶层显示,无法最小化,无法关闭(我们开发阶段要留关闭接口))
    //使用非模块对话框
    dlg.Create(IDD_DIALOG, NULL);
    dlg.ShowWindow(SW_SHOW);
    //遮蔽后台窗口,让其没办法干其他的事情
    CRect rect;
    rect.left = 0;
    rect.top = 0; //鼠标限制在屏幕的左上角
    rect.right = GetSystemMetrics(SM_CXFULLSCREEN); //获取分辨率长
    rect.bottom = GetSystemMetrics(SM_CYFULLSCREEN) * 1.3;//获取分辨率宽
    TRACE("right = %d bottom = %d\r\n", rect.right, rect.bottom);
    dlg.MoveWindow(rect);//移到全屏的状态
    //窗口置顶
    dlg.SetWindowPos(&dlg.wndTopMost, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
    //限制鼠标功能
    ShowCursor(false); // 鼠标消失
    //隐藏任务栏
    ::ShowWindow(::FindWindow(_T("Shell_TrayWnd"), NULL), SW_HIDE); //遮蔽windows任务栏
    //dlg.GetWindowRect(rect); //获取windows范围
    //rect.right = rect.left + 1; //限制更小一点
    //rect.bottom = rect.top + 1; //限制更小一点
    rect.left = 0;
    rect.top = 0; //鼠标限制在屏幕的左上角
    rect.right = 1;
    rect.bottom = 1;

    ClipCursor(rect); //鼠标限制在窗口范围内
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) { //依赖于线程的,只能拿到我这个线程相关的消息
        TranslateMessage(&msg);
        DispatchMessage(&msg);
        if (msg.message == WM_KEYDOWN) {
            TRACE("msg:%08X wparam:%08X lparam:%08X\r\n", msg.message, msg.wParam, msg.lParam); //追踪按下的是哪个按键
            if (msg.wParam == 0x41) { //摁下A退出
                break;
            }
        }
    } //对话框依赖这个消息循环
    ShowCursor(true); // 鼠标显示
    ::ShowWindow(::FindWindow(_T("Shell_TrayWnd"), NULL), SW_SHOW);
    dlg.DestroyWindow();
    _endthreadex(0);
    return 0;
}

这个函数我们为什么要采用unsigned _stdcall返回值

因为我们看_beginthreadex函数的源码

源码着实牛逼,我居然看不懂,意思到了就行,需要我深入探索

RACE("%s(%d):%d\r\n", __FUNCTION__,__LINE__,GetCurrentThreadId());

_DATE_ 当前日期,一个以 “MMM DD YYYY” 格式表示的字符串常量。

_TIME_ 当前时间,一个以 “HH:MM:SS” 格式表示的字符串常量。

_FILE_ 这会包含当前文件名,一个字符串常量。

_LINE_ 这会包含当前行号,一个十进制常量。

_FUNCTION_:当前函数的名称

STDC 当编译器以 ANSI 标准编译时,则定义为 1;判断该文件是不是标准 C 程序。

dlg.Create(IDD_DIALOG, NULL);

创建无模式对话框

无模式对话框和有模式对话框区别

无模式对话框 - 对话框显示后,不影响其它窗口的使用

有模式对话框 - 对话框显示后,会将其它窗口禁止输入操作.

因为我们这个就一个对话框,所以搞不了有模式对话框

virtual BOOL Create(
    LPCTSTR lpszTemplateName, //创建的dialog名称
    CWnd* pParentWnd = NULL); //父窗口对象句柄,如果为 NULL,则对话框对象的父窗口设置为主应用程序窗口

virtual BOOL Create(
    UINT nIDTemplate, //创建的dialog ID号 
    CWnd* pParentWnd = NULL);

dlg.ShowWindow(SW_SHOW);

创建了就必须show

创建一个全屏的矩形区域

    CRect rect;
    rect.left = 0;
    rect.top = 0; //鼠标限制在屏幕的左上角
    rect.right = GetSystemMetrics(SM_CXFULLSCREEN); //获取分辨率长
    rect.bottom = GetSystemMetrics(SM_CYFULLSCREEN) * 1.3;//获取分辨率宽

然后将这个矩形区域移到dlg窗口上

参考文章:MFC之CRect详解-CSDN博客

dlg.MoveWindow(rect);//移到全屏的状态

然后我们需要将这个窗口置顶

dlg.SetWindowPos(&dlg.wndTopMost, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);

BOOL SetWindowPos(
  [in]           HWND hWnd, //窗口的句柄
  [in, optional] HWND hWndInsertAfter,
  [in]           int  X,
  [in]           int  Y,
  [in]           int  cx,
  [in]           int  cy,
  [in]           UINT uFlags
);

对于uFlags

含义
SWP_ASYNCWINDOWPOS0x4000如果调用线程和拥有窗口的线程附加到不同的输入队列,则系统会将请求发布到拥有窗口的线程。 这可以防止调用线程阻止其执行,而其他线程处理请求。
SWP_DEFERERASE0x2000阻止生成 WM_SYNCPAINT 消息。
SWP_DRAWFRAME0x0020在窗口的类说明) 围绕窗口绘制 (定义的框架。
SWP_FRAMECHANGED0x0020使用 SetWindowLong 函数应用新框架样式集。 向窗口发送 WM_NCCALCSIZE 消息,即使窗口的大小未更改也是如此。 如果未指定此标志,则仅在更改窗口大小时发送 WM_NCCALCSIZE
SWP_HIDEWINDOW0x0080隐藏窗口。
SWP_NOACTIVATE0x0010不激活窗口。 如果未设置此标志,则会激活窗口并将其移动到最顶层或非最顶部组 (的顶部,具体取决于 hWndInsertAfter 参数) 的设置。
SWP_NOCOPYBITS0x0100丢弃工作区的整个内容。 如果未指定此标志,则会保存工作区的有效内容,并在调整窗口大小或重新定位后复制回工作区。
SWP_NOMOVE0x0002保留当前位置 (忽略 XY 参数) 。
SWP_NOOWNERZORDER0x0200不更改所有者窗口在 Z 顺序中的位置。
SWP_NOREDRAW0x0008不重绘更改。 如果设置了此标志,则不执行任何形式的重绘。 这适用于工作区、非工作区 (包括标题栏和滚动条) ,以及由于窗口移动而发现父窗口的任何部分。 设置此标志后,应用程序必须显式使需要重绘的窗口和父窗口的任何部分失效或重绘。
SWP_NOREPOSITION0x0200SWP_NOOWNERZORDER 标志相同。
SWP_NOSENDCHANGING0x0400阻止窗口接收 WM_WINDOWPOSCHANGING 消息。
SWP_NOSIZE0x0001保留当前大小 (忽略 cxcy 参数) 。
SWP_NOZORDER0x0004保留当前 Z 顺序 (忽略 hWndInsertAfter 参数) 。
SWP_SHOWWINDOW0x0040显示“接收端口跟踪选项” 窗口。

这个SetWindowPos和windows文档给出来的不一样,我认为windows文档给出来的是全局那种SetWindowPos,而不是这个dialog类里面的内部函数

我们可以看到pWndInsertAfter有四个参数,置顶是wndTopMost,这个不用指出句柄的原因估计就是因为这个dlg对象自带了

接下来我们需要让鼠标消失

ShowCursor(false);

消失成功

后面要加上ShowCursor(true); // 鼠标显示

虽然消失成功了,但是并不是真的消除了

所以我们需要限制这个隐身的鼠标的活动位置

rect.left = 0;
rect.top = 0; //鼠标限制在屏幕的左上角
rect.right = 1;
rect.bottom = 1;

ClipCursor(rect); //鼠标限制在窗口范围内

然后就是windows编程里面的消息循环

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
        if (msg.message == WM_KEYDOWN) {
            TRACE("msg:%08X wparam:%08X lparam:%08X\r\n", msg.message, msg.wParam, msg.lParam); //追踪按下的是哪个按键
            if (msg.wParam == 0x41) { //摁下A退出
                break;
            }
        }
    } //对话框依赖这个消息循环

如果我们按下a键就退出(也可以改成输入一连串的按键才退出)

我们发现,任务栏没有隐藏,鼠标可以点击到外面

::ShowWindow(::FindWindow(_T(“Shell_TrayWnd”), NULL), SW_HIDE); //遮蔽windows任务栏

后面结尾要加上

::ShowWindow(::FindWindow(_T(“Shell_TrayWnd”), NULL), SW_SHOW);//显示任务栏

然后隐藏任务栏后依然可以按win按键,但是你不管打开什么我这个dialog就是置顶的

锁屏解锁

代码

int UnlockMC() {
    //dlg.SendMessage(WM_KEYDOWN, 0X41,0x01E0001); //模拟按下了这个按键
    //::SendMessage(dlg.m_hWnd, WM_KEYDOWN, 0X41, 0x01E0001);
    //消息机制根据线程来的
    PostThreadMessage(threadid, WM_KEYDOWN, 0x41, 0); //向指定线程发消息,20ms那个线程可以收到,因为消息泵本身需要花费20-30ms
    CPacket pack(7, NULL, 0);
    CserverSocket::getInstance()->Send(pack);
    return 0;
}

前面模拟按下这个按键是不起作用的,因为windows消息传递依赖于线程,只在线程里面传递,不依赖于句柄

所以需要那个dialog线程的id,所以前面我们大费周章的_beginthreadex用这个函数,本质还是为了记录下这个线程的id号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值