OrdinAry的文章

  • 内存快速搜索遍历

    背景相比很多人都用过内存搜索软件 Cheat Engine 吧,它里面提供了强大进程内存搜索功能,搜索速度很快,搜索结果也很精确。我之前对内存搜索也稍微专研了一下,是因为当时需要写一个小程序,那个小程序的功能就是可以搜索出指定进程指定值的内存地址,这个CE就能做,只不过是要在自己的程序里实现内存的搜索。
    内存的遍历搜索,说难也不难,说容易也不容易。因为你可以做得比较简单,也可以做得比较完美,这主要是在搜索效率上的区别而已。简单的搜索方法就是直接暴力搜索内存,直接从0地址搜索到0x7FFFFFFF地址处,因为低 2GB 进程空间是用户空间。然后,匹配值就可以了。难点的,就是过滤掉一些内存地址,不用搜索。例如,进程加载基址之前的地址空间,就可以不用搜索等等,以此来缩小搜索的范围,提升搜索效率。
    本文就是对内存遍历实现快速搜索,当然,这肯定不会是最快的搜索方式,只是相对的快速。我们也是通过加载基址,以及内存空间地址信息,缩小搜索的范围,提升搜索效率。现在,就把实现过程整理成文档,分享给大家。
    函数介绍VirtualQueryEx 函数
    查询地址空间中内存地址的信息。
    函数声明
    DWORD VirtualQueryEx( HANDLE hProcess, LPCVOID lpAddress, PMEMORY_BASIC_INFORMATION lpBuffer, DWORD dwLength);
    参数

    hProcess:进程句柄。lpAddress:查询内存的地址。lpBuffer:指向MEMORY_BASIC_INFORMATION结构的指针,用于接收内存信息。dwLength:MEMORY_BASIC_INFORMATION结构的大小。
    返回值

    返回值是信息缓冲区中返回的实际字节数。如果函数失败,返回值是 0。为了获得更多的错误信息,调用GetLastError。

    MEMORY_BASIC_INFORMATION 结构体
    结构体声明
    typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; // 区域基地址 PVOID AllocationBase; // 分配基地址 DWORD AllocationProtect; // 区域被初次保留时赋予的保护属性 SIZE_T RegionSize; // 区域大小(以字节为计量单位) DWORD State; // 状态(MEM_FREE、MEM_RESERVE或 MEM_COMMIT) DWORD Protect; // 保护属性 DWORD Type; // 类型} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
    成员

    BaseAddress:与lpAddress参数的值相同,但是四舍五入为页面的边界值。AllocationBas:指明用VirtualAlloc函数分配内存区域的基地址。lpAddress在该区域之内。AllocationProtect:指明该地址空间区域被初次保留时赋予该区域的保护属性。PAGE_READONLY:只读属性,如果试图进行写操作,将引发访问违规。如果系统区分只读、执行两种属性,那么试图在该区域执行代码也将引发访问违规。PAGE_READWRITE:允许读写。PAGE_EXECUTE:只允许执行代码,对该区域试图进行读写操作将引发访问违规。PAGE_EXECUTE_READ:允许执行和读取。PAGE_EXECUTE_READWRITE:允许读写和执行代码。PAGE_EXECUTE_WRITECOPY:对于该地址空间的区域,不管执行什么操作,都不会引发访问违规。如果试图在该页面上的内存中进行写入操作,就会将它自己的私有页面(受页文件的支持)拷贝赋予该进程。PAGE_GUARD:在页面上写入一个字节时使应用程序收到一个通知(通过一个异常条件)。PAGE_NOACCESS:禁止一切访问。PAGE_NOCACHE:停用已提交页面的高速缓存。一般情况下最好不要使用该标志,因为它主要是供需要处理内存缓冲区的硬件设备驱动程序的开发人员使用的。RegionSize 用于指明内存块从基地址即BaseAddress开始的所有页面的大小(以字节为计量单位)这些页面与含有用LpAddress参数设定的地址的页面拥有相同的保护属性、状态和类型。State:用于指明所有相邻页面的状态。MEM_COMMIT:指明已分配物理内存或者系统页文件。MEM_FREE:空闲状态。该区域的虚拟地址不受任何内存的支持。该地址空间没有被保留。该状态下AllocationBase、AllocationProtect、Protect和Type等成员均未定义。MEM_RESERVE:指明页面被保留,但是没有分配任何物理内存。该状态下Protect成员未定。Protect:用于指明所有相邻页面(内存块)的保护属性。这些页面与含有拥有相同的保属性、状态和类型。意义同AllocationProtect。Type:用于指明支持所有相邻页面的物理存储器的类型(MEM_IMAGE,MEM_MAPPED或MEM_PRIVATE)。这些相邻页面拥有相同的保护属性、状态和类型。如果是Windows 98,那么这个成员将总是MEM_PRIVATE 。MEM_IMAGE:指明该区域的虚拟地址原先受内存映射的映像文件(如.exe或DLL文件)的支持,但也许不再受映像文件的支持。例如,当写入模块映像中的全局变量时,“写入时拷贝”的机制将由页文件来支持特定的页面,而不是受原始映像文件的支持。MEM_MAPPED:该区域的虚拟地址原先是受内存映射的数据文件的支持,但也许不再受数据文件的支持。例如,数据文件可以使用“写入时拷贝”的保护属性来映射。对文件的任何写入操作都将导致页文件而不是原始数据支持特定的页面。MEM_PRIVATE:指明该内存区域是私有的。不被其他进程共享。

    ReadProcessMemory 函数
    在指定的进程中从内存区域读取数据。 要读取的整个区域必须可访问或操作失败。
    函数声明
    BOOL WINAPI ReadProcessMemory( _In_ HANDLE hProcess, _In_ LPCVOID lpBaseAddress, _Out_ LPVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesRead);
    参数

    hProcess [in]具有正在读取的内存的进程的句柄。 句柄必须具有进程的PROCESS_VM_READ访问权限。lpBaseAddress [in]指向从中读取的指定进程中的基地址的指针。 在发生任何数据传输之前,系统将验证指定大小的基地址和内存中的所有数据都可访问以进行读取访问,如果不可访问,则该函数将失败。lpBuffer [out]指向从指定进程的地址空间接收内容的缓冲区的指针。nSize [in]要从指定进程读取的字节数。lpNumberOfBytesRead [out]指向一个变量的指针,该变量接收传输到指定缓冲区的字节数。 如果lpNumberOfBytesRead为NULL,则该参数将被忽略。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为0(零)。 要获取扩展错误信息,请调用GetLastError。

    实现原理实现内存的快速搜索,我们主要是缩小搜索的范围,来提升的搜索效率。缩小搜索范围的方法有,一是过滤掉进程加载基址之前的内存地址;二是获取内存空间地址信息,把内存状态不是 MEM_COMMIT 以及保护属性没有读权限的内存区域都过滤掉。
    所以,获取进程加载基址以及获取内存空间地址信息比较关键。
    大概分为以下 7 个步骤:

    首先,我们调用 OpenProcess 函数根据进程 PID 打开进程,并获取进程的句柄,进程句柄的权限为 PROCESS_ALL_ACCESS
    然后,我们根据进程句柄,获取指定进程的加载基址。对于进程加载基址的获取,我们使用的是 EnumProcessModules 函数来获取。其它的的进程基址获取方法,可以参考本站上其他网友写的“获取指定进程的加载基址”这篇文章
    接着,以进程加载基址作为内存搜索的起始地址,调用 VirtualQueryEx 函数查询地址空间中内存地址的信息,然后将内存页面状态不是MEM_COMMIT 过滤掉,即过滤掉没有分配物理内存或者系统页文件。同时,也把没有读权限的页面属性保护都过滤掉
    通过内存地址的信息过滤之后,我们就可以调用 ReadProcessMemory 函数把对应的内存区域读取到自己进程的缓冲区中
    接着,我们就可以匹配内存,搜索指定的内存,并获取制定进程内存地址
    然后,获取下一块内存区域的起始地址,继续重复上面3、4、5步操作,直到满足退出条件
    最后,我们就释放内存,并关闭进程句柄

    这样,就是先了内存的快速遍历搜索。
    编码实现// 搜索内存BOOL SearchMemory(DWORD dwProcessId, PVOID pSearchBuffer, DWORD dwSearchBufferSize){ // 根据PID, 打开进程获取进程句柄 HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } // 获取进程加载基址 HMODULE hModule = NULL; ::EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), NULL); // 把加载基址作为遍历内存的起始地址, 开始遍历 BYTE *pSearchAddress = (BYTE *)hModule; MEMORY_BASIC_INFORMATION mbi = {0}; DWORD dwRet = 0; BOOL bRet = FALSE; BYTE *pTemp = NULL; DWORD i = 0; BYTE *pBuf = NULL; while (TRUE) { // 查询地址空间中内存地址的信息 ::RtlZeroMemory(&mbi, sizeof(mbi)); dwRet = ::VirtualQueryEx(hProcess, pSearchAddress, &mbi, sizeof(mbi)); if (0 == dwRet) { break; } // 过滤内存空间, 根据内存的状态和保护属性进行过滤 if ((MEM_COMMIT == mbi.State) && (PAGE_READONLY == mbi.Protect || PAGE_READWRITE == mbi.Protect || PAGE_EXECUTE_READ == mbi.Protect || PAGE_EXECUTE_READWRITE == mbi.Protect)) { // 申请动态内存 pBuf = new BYTE[mbi.RegionSize]; ::RtlZeroMemory(pBuf, mbi.RegionSize); // 读取整块内存 bRet = ::ReadProcessMemory(hProcess, mbi.BaseAddress, pBuf, mbi.RegionSize, &dwRet); if (FALSE == bRet) { ShowError("ReadProcessMemory"); break; } // 匹配内存 for (i = 0; i < (mbi.RegionSize - dwSearchBufferSize); i++) { pTemp = (BYTE *)pBuf + i; if (RtlEqualMemory(pTemp, pSearchBuffer, dwSearchBufferSize)) { // 显示搜索到的地址 printf("0x%p\n", ((BYTE *)mbi.BaseAddress + i)); } } // 释放内存 delete[]pBuf; pBuf = NULL; } // 继续对下一块内存区域进行遍历 pSearchAddress = pSearchAddress + mbi.RegionSize; } // 释放内存, 关闭句柄 if (pBuf) { delete[]pBuf; pBuf = NULL; } ::CloseHandle(hProcess); return TRUE;}
    程序测试我们直接运行程序,对 520.exe 进程进行搜索,搜索值为 0x00905A4D 的地址都哪些,程序成功列举所有的地址。

    总结这个程序,通过以进程加载基址为搜索起始地址、判断地址内存空间信息过滤一些内存状态不是 MEM_COMMIT 以及保护属性没有读权限的内存区域,以此来缩小搜索的范围,提升搜索的效率。
    大家注意理解 VirtualQueryEx 函数以及 ReadProcessMemory 函数的参数使用方式,同时也要注意不同进程内存空间的转换。
    参考参考自《Windows黑客编程技术详解》一书
    2  留言 2018-12-20 12:16:28
  • 为程序创建任务栏右下角托盘图标

    背景我们用过很多应用程序,无论是功能复杂的大程序还是功能单一的小程序,大都会在计算机右下角显示一个图盘图标,然后我们可以直接把窗口关掉,通过点击托盘图标来控制程序的执行操作或显示窗口。使得程序使用起来比较便捷,不用显示程序主窗口就能完成一些操作,增强了程序的用户体验。
    本文要讲解的正是为程序添加这样的一个右下角托盘,并实现显示程序主窗口、退出程序等功能。本文分别在MFC程序和Windows应用程序中进行演示,其实,原理步骤都是相同的,只是为了方便大家理解,所以MFC程序和Windows程序都各写一个来演示。
    现在,我就把程序实现的过程整理成文档,分享给大家。
    函数介绍Shell_NotifyIcon 函数
    主要用于向任务栏的状态栏发送一个消息。
    函数声明
    BOOL Shell_NotifyIcon( DWORD dwMessage, PNOTIFYICONDATA lpdata);
    参数

    dwMessage为输入参数,传递发送的消息,表明要执行的操作。可选的值如下:NIM_ADD向托盘区域添加一个图标。此时第二个参数lpdata指向的NOTIFYICONDATA结构体中的hWnd和uID成员用来标示这个图标,以便以后再次使用Shell_NotifyIcon对此图标操作。NIM_DELETE删除托盘区域的一个图标。此时第二个参数lpdata指向的NOTIFYICONDATA结构体中的hWnd和uID成员用来标示需要被删除的这个图标。NIM_MODIFY修改托盘区域的一个图标。此时第二个参数lpdata指向的NOTIFYICONDATA结构体中的hWnd和uID成员用来标示需要被修改的这个图标。NIM_SETFOCUSVersion 5.0. 设置焦点。比如当用户操作托盘图标弹出菜单,而有按下ESC键将菜单消除后,程序应该使用此消息来将焦点设置到托盘图标上。NIM_SETVERSIONVersion 5.0. 设置任务栏按照第二个参数lpdata指向的NOTIFYICONDATA结构体中的uVersion成员指定的版本号来工作。此消息可以允许用户设置是否使用基于Windows2000的version 5.0的风格。uVersion的缺省值为0,默认指明了使用原始Windows 95图标消息风格。具体这两者的区别请参考msdn中的Shell_NotifyIcon函数说明的Remarks。lpdata为输入参数,是指向NOTIFYICONDATA结构体的指针,结构体内容用来配合第一个参数wMessage进行图标操作。
    返回值

    如果图标操作成功返回TRUE,否则返回FALSE。

    NOTIFYICONDATA 结构体
    结构体声明
    typedef struct _NOTIFYICONDATA { DWORD cbSize; HWND hWnd; UINT uID; UINT uFlags; UINT uCallbackMessage; HICON hIcon; TCHAR szTip[64]; DWORD dwState; DWORD dwStateMask; TCHAR szInfo[256]; union { UINT uTimeout; UINT uVersion; }; TCHAR szInfoTitle[64]; DWORD dwInfoFlags; GUID guidItem;} NOTIFYICONDATA, *PNOTIFYICONDATA;
    成员

    cbSize:结构体的大小,以字节为单位。hWnd:窗口的句柄。窗口用来接收与托盘图标相关的消息。Shell_NotifyIcon函数调用时,hWnd和uID成员用来标示具体要操作的图标。uID:应用程序定义的任务栏图标的标识符。Shell_NotifyIcon函数调用时,hWnd和uID成员用来标示具体要操作的图标。通过将多次调用,你可以使用不同的uID将多个图标关联到一个窗口。hWnd。uFlags:此成员表明具体哪些其他成员为合法数据(即哪些成员起作用)。此成员可以为以下值的组合:NIF_ICON:hIcon成员起作用。NIF_MESSAGE:uCallbackMessage成员起作用。NIF_TIP:szTip成员起作用。NIF_STATE:dwState和dwStateMask成员起作用。NIF_INFO:使用气球提示代替普通的工具提示框。szInfo, uTimeout, szInfoTitle和dwInfoFlags成员起作用。NIF_GUID:保留。uCallbackMessage:应用程序定义的消息标示。当托盘图标区域发生鼠标事件或者使用键盘选择或激活图标时,系统将使用此标示向由hWnd成员标示的窗口发送消息。消息响应函数的wParam参数标示了消息事件发生的任务栏图标,lParam参数根据事件的不同,包含了鼠标或键盘的具体消息,例如当鼠标指针移过托盘图标时,lParam将为WM_MOUSEMOVE。hIcon:增加、修改或删除的图标的句柄。注意,windows不同版本对于图标有不同要求。Windows XP可支持32位。szTip:指向一个以\0结束的字符串的指针。字符串的内容为标准工具提示的信息。包含最后的\0字符,szTip最多含有64个字符。对于Version 5.0 和以后版本,szTip最多含有128个字符(包含最后的\0字符)。dwState:Version 5.0,图标的状态,有两个可选值,如下:NIS_HIDDEN:图标隐藏NIS_SHAREDICON:图标共享dwStateMask:Version 5.0. 指明dwState成员的那些位可以被设置或者访问。比如设置此成员为NIS_HIDDEN,将导致只有hidden状态可以被获取。szInfo:Version 5.0. 指向一个以\0结束的字符串的指针。字符串的内容为气球提示内容。最多含有255个字符。如果要移除已经存在的气球提示信息,设置uFlags成员为NIF_INFO,同时将szInfo设为空。uTimeout:和uVersion成员为联合体。uTimeout表示气球提示超时的时间,单位为毫秒,此时间后气球提示将消失。系统默认气球提示的超时时间最小值为10秒,最大值为30秒。如果设置的uTimeout的值小于10将设置最小值,如果大于30将设置最大值。将超时时间分为最大最小两种,是因为解决不同图标的气球提示同时弹出的问题,详细内容请参考MSDN中NOTIFYICONDATA结构体说明的remarks。uVersion:Version 5.0. 和uTimeout成员为联合体。用来设置使用Windows 95 还是 Windows 2000风格的图标消息接口。szInfoTitle:Version 5.0. 指向一个以\0结束的字符串的指针。字符串的内容为气球提示的标题。此标题出现在气球提示框的上部,最多含有63个字符。dwInfoFlags:Version 5.0. 设置此成员用来给气球提示框增加一个图标。增加的图标出现在气球提示标题的左侧,注意如果szInfoTitle成员设为空字符串,则图标也不会显示。guidItem:Version 6.0. 保留。

    实现过程无论是对MFC程序还是Windows应用程序,实现的步骤和原理都是一样的。
    1. 设置 NOTIFYICONDATA首先,我们需要对托盘图标结构体 NOTIFYICONDATA 进行设置,设置的内容,就是我们想要托盘图标显示的内容。我们主要是对 NOTIFYICONDATA 的cbSize、hWnd、uID、uFlags、uCallbackMessage、hIcon以及szTip成员进行设置。
    其中,cbSize表示 NOTIFYICONDATA 结构体的大小;hWnd表示和托盘图标相关联的程序窗体的句柄,这个值需要被指定,因为窗口用来接收与托盘图标相关的消息;uID表示应用程序定义的任务栏图标的标识符,可以使用不同的uID将多个图标关联到一个窗口;uFlags表示哪些结构体成员起作用,NIF_ICON则hIcon成员起作用,NIF_MESSAGE则uCallbackMessage成员起作用,NIF_TIP则szTip成员起作用,所以下面我们还需要对hIcon、uCallbackMessage以及szTip成员赋值;hIcon表示托盘显示的图标句柄;uCallbackMessage表示托盘的消息类型;szTip表示托盘的提示信息。
    NOTIFYICONDATA notifyIconData = {0};::RtlZeroMemory(&notifyIconData, sizeof(notifyIconData));notifyIconData.cbSize = sizeof(NOTIFYICONDATA);notifyIconData.hWnd = hWnd;notifyIconData.uID = IDI_ICON1;notifyIconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;notifyIconData.uCallbackMessage = WM_MYTASK;notifyIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), (LPCSTR)IDI_ICON1);::lstrcpy(notifyIconData.szTip, "This Is A Notify Tip.");
    2. 增加托盘显示然后,我们开始调用 Shell_NotifyIcon 函数按照上面 NOTIFYICONDATA 设置新增托盘。其中,NIM_ADD表示向托盘区域添加一个图标。notifyIconData就是上面设置的 NOTIFYICONDATA 结构体变量。
    // 增加托盘::Shell_NotifyIcon(NIM_ADD, &notifyIconData);
    3. 托盘消息处理函数经过上面两步操作,我们就可以成功为程序在窗口右下角增加一个托盘图标显示。但是,我们还需要让托盘图标对一些操作做出响应,而不是只是显示而已。例如,我们鼠标右击托盘图标的时候,显示菜单栏。
    根据上面对 NOTIFYICONDATA 结构体成员 uCallbackMessage 介绍中知道 ,当托盘图标区域发生鼠标事件或者使用键盘选择或激活图标时,系统将使用此标示向由hWnd成员标示的窗口发送消息。消息响应函数的wParam参数标示了消息事件发生的任务栏图标,lParam参数根据事件的不同,包含了鼠标或键盘的具体消息,例如当鼠标指针移过托盘图标时,lParam将为WM_MOUSEMOVE。所以,我们要响应鼠标右键的操作,需要判断 lParam 是否为 WM_RBUTTONUP ,若是,弹出菜单,否则忽略操作。
    switch (lParam){// 鼠标右键弹起时case WM_RBUTTONUP: { // 弹出菜单 PopupMyMenu(); break;}default: break;}
    就这样,菜单弹出来后,我们直接响应菜单选项的消息响应函数,在对应的选项里执行相应的操作就好。
    为程序添加托盘显示的原理就是上面 3 个步骤,剩下来的就是编码实现了。
    编码实现Windows应用程序实现首先我们把对NOTIFYICONDATA结构体的设置以及添加托盘图标的操作代码都放在程序初始化WM_INITDIALOG操作里。
    然后,我们就开始在窗口消息过程函数中添加托盘消息类型WM_MYTASK,并对实现托盘消息处理响应函数。这样,在右击托盘的时候,就可以显示菜单栏了。
    窗口消息过程函数BOOL CALLBACK ProgMainDlg(HWND hWnd, UINT uiMsg, WPARAM wParam, LPARAM lParam){ if (WM_INITDIALOG == uiMsg) { // 设置托盘显示 SetNotifyIcon(hWnd); } else if (WM_CLOSE == uiMsg) { // 隐藏窗口 ::ShowWindow(hWnd, SW_HIDE); } else if (WM_MYTASK == uiMsg) { // 处理操作托盘的消息 OnTaskMsg(hWnd, wParam, lParam); } else if (WM_COMMAND == uiMsg) { if (ID_EXIT == wParam) { // 托盘菜单 退出 ::EndDialog(hWnd, NULL); } else if (ID_SHOW == wParam) { // 托盘菜单 显示主窗口 ::ShowWindow(hWnd, SW_SHOW); } } return FALSE;}
    设置托盘显示// 设置托盘显示void SetNotifyIcon(HWND hWnd){ NOTIFYICONDATA notifyIconData = {0}; ::RtlZeroMemory(&notifyIconData, sizeof(notifyIconData)); notifyIconData.cbSize = sizeof(NOTIFYICONDATA); notifyIconData.hWnd = hWnd; notifyIconData.uID = IDI_ICON1; notifyIconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; notifyIconData.uCallbackMessage = WM_MYTASK; notifyIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), (LPCSTR)IDI_ICON1); ::lstrcpy(notifyIconData.szTip, "This Is A Notify Tip."); // 增加托盘 ::Shell_NotifyIcon(NIM_ADD, &notifyIconData);}
    托盘消息处理函数// 处理操作托盘的消息LRESULT OnTaskMsg(HWND hWnd, WPARAM wParam, LPARAM lParam){ switch (lParam) { // 鼠标右键弹起时,弹出菜单 case WM_RBUTTONUP: { // 弹出菜单 PopupMyMenu(hWnd); break; } default: break; } return 0;}
    弹出菜单栏// 弹出菜单栏void PopupMyMenu(HWND hWnd){ POINT p; ::GetCursorPos(&p); HMENU hMenu = ::LoadMenu(::GetModuleHandle(NULL), (LPCSTR)IDR_MENU1); HMENU hSubMenu = ::GetSubMenu(hMenu, 0); ::TrackPopupMenu(hSubMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, p.x, p.y, 0, hWnd, NULL); ::DestroyMenu(hSubMenu);}
    MFC程序实现和上面的Windows应用程序一样,首先我们把对NOTIFYICONDATA结构体的设置以及添加托盘图标的操作代码都放在程序初始化,对于MFC来说,初始化消息响应函数是主对话框类的OnInitDialog函数。我们把托盘设置这部分代码,放在函数OnInitDialog中即可。
    然后,我们开始对为自定义消息类型 WM_MYTASK 创建自定义消息响应函数 OnTaskMsg。创建自定义消息响应函数操作如下:

    声明消息类型 WM_MYTASK。
    #define WM_MYTASK (WM_USER + 100)
    声明消息响应函数,函数名称可以任意,但是返回值类型和参数类型是固定的。
    // 托盘消息处理函数 LRESULT OnTaskMsg(WPARAM wParam, LPARAM lParam);
    在主对话框的窗口消息响应列表中,为上述自定义的消息类型和消息响应函数进行关联。
    BEGIN_MESSAGE_MAP(CNotifyIcon_MFC_TestDlg, CDialogEx) … …(省略) ON_MESSAGE(WM_MYTASK, OnTaskMsg) END_MESSAGE_MAP()

    这样,我们就可以直接定义响应函数OnTaskMsg就可以了。
    接下来的操作和代码,基本上和Windows应用程序中的代码是一样的,所以,就给出代码了。若有什么问题,可以直接参考本文对应的程序代码即可。
    程序测试我们直接运行程序,便可以看到窗口右下角有托盘图标显示,然后我们鼠标右击图标,便成功弹出菜单栏,点击菜单栏选项,成功实现相应的操作。

    总结这个程序原理上不是很复杂,但是实现上面,由于Windows应用程序和MFC程序框架上不同,所以,要注意它们的区别,特别是MFC的自定义消息响应函数。
    参考参考自《Windows黑客编程技术详解》一书
    2  留言 2018-12-20 12:32:31

发送私信

有没有那样一种永远,永远永远不改变

12
文章数
32
评论数
eject