改造联想Y480的快捷键(跨进程替换窗口过程的实现——远程线程注入)
2013-01-10 18:40
513 查看
前段时间入手了联想Y480N-IFI,在C面顶部有几个快捷按键。一键恢复、一键影音,这两个按键本身的功能对于本人是毫无作用,
我便想着能否改成像多媒体键盘那样有一些快捷键可以打开一些软件。正好这段时间有个9天的假期。我便开始研究。
对windows msg敏感同学应该就会想到,按键按下时就会有windows msg,那么可能是哪些消息呢?
我首先想到既然这些按键不是标准按键,那应该也不是标准的msg了,马上想到非常有可能是自定义消息。于是,便打开spyxx进行分析。如下:
打开spyxx,Spy菜单→Log Messages...弹出message Options。选中Additional Windows里的All Window in system,
然后切换到Messages选项卡,先clear all,然后选中WM_USER,确定之后就开始记录消息了。
可以看到。当按下这两个按键时都会有一条WM_USER + 1002的消息,只是消息参数不同,一键恢复的消息参数是0x0b,而一键影音的则是0x05。
继续分析,可以看到。收到这条消息的窗口是哪个。
还可以得到所属的进程
那么,接下来的要做的事,就是拦截这条消息,自己进行处理。
拦截的方式有很多,例如
1.全局hook这个消息
2.注入目标进程hook这个消息
3.注入目标进程替换窗口过程
最终,我确定下来,采用第三种方案。
程序最终界面,采用了WTL。
程序实现的功能:
一键影音和一键恢复两个按键可以设置成如下功能:
打开我的电脑、库、我的文档、计算器、记事本、任务管理器、cmd、默认音乐播放器、默认视频播放器、默认浏览器、默认邮件客户端、指定的程序。
接下来看看一些关键方法:
远程线程注入Dll原理:通过CreateRemoteThread这个API在目标进程中创建远程线程。
由于LoadLibrary这个API和ThreadProc的原型基本一致。所以可以直接用LoadLibrary作为线程函数,然后把Dll的路径作为线程参数。
LoadLibrary(W/A)这个API在kernel32.dll里导出。系统中每一个进程都会将kernel32.dll映射到同一个地址,
所以我们可以在本地进程里GetProcAddress显式取到LoadLibrary的地址可以作为线程函数地址,
而Dll的路径则需要用VirtualAllocEx在目标进程里分配内存空间。然后通过WriteProcessMemory将dll的路径写入到目标进程。
将VirtualAllocEx返回的地址作为线程参数即可。详细的可以看看《Windows vic C/C++》 第22章。
注入的代码:①
查找指定进程的指定模块基址的代码:
程序里判断当前注入状态也是通过上面这段代码来实现。
取消注入的代码:
程序开机自启的实现:
写入注册表,加上命令行参数,通过参数来判断是手动启动还是自启。
注册表操作用了CReg类。代码如下:
为了方便,将dll作为资源嵌入到exe里。程序运行时会判断是否存在dll。如果不存在则释放出dll。代码如下
程序可以把快捷键自定义为打开某个程序,在使用文件选择对话框时可以选中快捷方式,而程序则需要解析出快捷方式真正的路径。代码如下②
打开某个程序这里都是通过ShellExecute来实现。
接下来说说如何获取一些默认程序。
ShellExcute支持用CLSID作为参数来打开程序,打开我的电脑,我的文档、库、是通过CLSID来打开。
对应的CLSID分别是:
::{20D04FE0-3AEA-1069-A2D8-08002B30309D}
::{031E4825-7B94-4dc3-B131-E946B44C8DD5}
::{450D8FBA-AD25-11D0-98A8-0800361B1103}
这些CLSID是从注册表翻出来的。
打开email是ShellExecute支持mailto协议
其他程序都是通过读取注册表解析关联的程序。
读取的代码如下
第一个参数是扩展名,如.mp3,第二个参数传出参数,需要一个TCHAR sz[MAX_PATH]。第三个参数是一个标志位。
如果是要解析默认浏览器。则将第二个参数设置为"http"第三个参数设置为FALSE,
因为解析默认浏览器比其他的少了一个步骤。
以上是主要代码。详细的可以看附件提供的源码
另外写的过程中有一些暂时用不到的代码,在这里分享下:
一段用来显式加载Dll的宏:
如果这个宏有更好的写法,欢迎指教。
下面代码是用来判断dll是否是64位的
代码里经常出现用来关闭句柄的宏:
本来还打算。用一个32位的程序,嵌入两个dll一个32位一个64.在不同平台选择不同dll注入。
一开始是这么写。后来发现。没法直接用32位程序注入64位dll到64位进程,会出现拒绝访问。
得换思路实现。不过没时间写,就把思路分享如下:
思路一:再写一个64位的injector嵌入到exe。然后在64位系统上就释放这个Injector来注入。
思路二:用native API。得到ntdll.dll的baseaddr。然后定位到LdrLoadDll的addr。然后定位到RtlCreateUserThread,通过RtlCreateUserThread来创建远程线程并用LdrLoadDll来加载dll。这个需要内联汇编实现。比较麻烦。
另外,写这个程序时。一开始是在DLL_PROCESS_ATTACH时处理了很多操作。。。这就出现了有时DllMain里的代码没执行的情况。。。绞尽脑汁。。唯一能想到的原因就是Dllloader出现deadlock。。不过我没去验证。。在微软上看到一份文档,有兴趣可以看看。《Best
Practices for Creating DLLs》
最后我把一些操作封装成类,在构造函数里操作,然后再Dll里定义了一个全局对象。这样就没出现问题了。
=================================华丽的分割线======================================
源码下载:OneKeyMgr_src.7z (IDE采用VS2012)
可执行文件:
OneKeyMgr_for_Win32.7z
OneKeyMgr_for_Win64.7z
mark:
①此段代码借鉴了Jeffrey Richter的InjectLib。
②此处借鉴codeproject上的一篇文章
转载请标明出处,原文地址:/article/5538322.html
如果觉得本文对您有帮助,请支持一下,您的支持是我写作最大的动力,谢谢。
我便想着能否改成像多媒体键盘那样有一些快捷键可以打开一些软件。正好这段时间有个9天的假期。我便开始研究。
对windows msg敏感同学应该就会想到,按键按下时就会有windows msg,那么可能是哪些消息呢?
我首先想到既然这些按键不是标准按键,那应该也不是标准的msg了,马上想到非常有可能是自定义消息。于是,便打开spyxx进行分析。如下:
打开spyxx,Spy菜单→Log Messages...弹出message Options。选中Additional Windows里的All Window in system,
然后切换到Messages选项卡,先clear all,然后选中WM_USER,确定之后就开始记录消息了。
可以看到。当按下这两个按键时都会有一条WM_USER + 1002的消息,只是消息参数不同,一键恢复的消息参数是0x0b,而一键影音的则是0x05。
继续分析,可以看到。收到这条消息的窗口是哪个。
还可以得到所属的进程
那么,接下来的要做的事,就是拦截这条消息,自己进行处理。
拦截的方式有很多,例如
1.全局hook这个消息
2.注入目标进程hook这个消息
3.注入目标进程替换窗口过程
最终,我确定下来,采用第三种方案。
程序最终界面,采用了WTL。
程序实现的功能:
一键影音和一键恢复两个按键可以设置成如下功能:
打开我的电脑、库、我的文档、计算器、记事本、任务管理器、cmd、默认音乐播放器、默认视频播放器、默认浏览器、默认邮件客户端、指定的程序。
接下来看看一些关键方法:
远程线程注入Dll原理:通过CreateRemoteThread这个API在目标进程中创建远程线程。
由于LoadLibrary这个API和ThreadProc的原型基本一致。所以可以直接用LoadLibrary作为线程函数,然后把Dll的路径作为线程参数。
LoadLibrary(W/A)这个API在kernel32.dll里导出。系统中每一个进程都会将kernel32.dll映射到同一个地址,
所以我们可以在本地进程里GetProcAddress显式取到LoadLibrary的地址可以作为线程函数地址,
而Dll的路径则需要用VirtualAllocEx在目标进程里分配内存空间。然后通过WriteProcessMemory将dll的路径写入到目标进程。
将VirtualAllocEx返回的地址作为线程参数即可。详细的可以看看《Windows vic C/C++》 第22章。
注入的代码:①
BOOL InjectLib( DWORD dwProcessId, LPCTSTR pszLibFileName ) { // TODO: Inject dll to process BOOL bOk = FALSE; // Assume that the function fails HANDLE hProcess = NULL; HANDLE hThread = NULL; LPVOID pszLibFileRemote = NULL; __try { // Get a handle for the target process. hProcess = ::OpenProcess( PROCESS_QUERY_INFORMATION | // Required by Alpha PROCESS_CREATE_THREAD | // For CreateRemoteThread PROCESS_VM_OPERATION | // For VirtualAllocEx/VirtualFreeEx PROCESS_VM_WRITE, // For WriteProcessMemory FALSE, dwProcessId); if (hProcess == NULL) __leave; // Calculate the number of bytes needed for the DLL's pathname int cb = (::lstrlen(pszLibFileName) + 1) * sizeof(TCHAR); // Allocate space in the remote process for the pathname pszLibFileRemote = ::VirtualAllocEx(hProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE); if (pszLibFileRemote == NULL) __leave; // Copy the DLL's pathname to the remote process' address space if (!::WriteProcessMemory(hProcess, pszLibFileRemote, pszLibFileName, cb, NULL)) __leave; // Get the real address of LoadLibraryW in Kernel32.dll #ifdef UNICODE PTHREAD_START_ROUTINE pfnThreadRtn = reinterpret_cast<PTHREAD_START_ROUTINE>( ::GetProcAddress(::GetModuleHandle(_T("Kernel32")), "LoadLibraryW")); #else PTHREAD_START_ROUTINE pfnThreadRtn = reinterpret_cast<PTHREAD_START_ROUTINE>( ::GetProcAddress(::GetModuleHandle(_T("Kernel32")), "LoadLibraryA")); #endif if (pfnThreadRtn == NULL) __leave; // Create a remote thread that calls LoadLibraryW(DLLPathname) hThread = ::CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, pszLibFileRemote, 0, NULL); if (hThread == NULL) __leave; // Wait for the remote thread to terminate ::WaitForSingleObject(hThread, INFINITE); bOk = TRUE; // Everything executed successfully } __finally { // Now, we can clean everything up // Free the remote memory that contained the DLL's pathname if (pszLibFileRemote != NULL) ::VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE); CLOSE_HANDLE(hThread); CLOSE_HANDLE(hProcess); } return bOk; }
查找指定进程的指定模块基址的代码:
BOOL FindProcessModule( DWORD dwProcessId, LPCTSTR pszModuleName, PMODULEENTRY32 pMe ) { BOOL bOk = FALSE; // Assume that the function fails HANDLE hthSnapshot = NULL; __try { // Grab a new snapshot of the process hthSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId); if (hthSnapshot == INVALID_HANDLE_VALUE) __leave; MODULEENTRY32 me = {sizeof(MODULEENTRY32)}; if (NULL == pMe) pMe = &me; // Get the HMODULE of the desired library BOOL bFound = FALSE; BOOL bMoreMods = ::Module32First(hthSnapshot, pMe); for (; bMoreMods; bMoreMods = ::Module32Next(hthSnapshot, pMe)) { bFound = (::lstrcmpi(pMe->szModule, pszModuleName) == 0) || (::lstrcmpi(pMe->szExePath, pszModuleName) == 0); if (bFound) break; } if (!bFound) __leave; bOk = TRUE; // Everything executed successfully } __finally { // Now we can clean everything up CLOSE_HANDLE(hthSnapshot); } return bOk; }
程序里判断当前注入状态也是通过上面这段代码来实现。
取消注入的代码:
BOOL EjectLib( DWORD dwProcessId, LPCTSTR pszLibFileName ) { // TODO: Eject dll to process BOOL bOk = FALSE; // Assume that the function fails HANDLE hProcess = NULL; HANDLE hThread = NULL; MODULEENTRY32 me = {sizeof(MODULEENTRY32)}; if (!::FindProcessModule(dwProcessId, pszLibFileName, &me)) return FALSE; __try { // Get a handle for the target process. hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION, // For CreateRemoteThread FALSE, dwProcessId); if (hProcess == NULL) __leave; // Get the real address of FreeLibrary in Kernel32.dll PTHREAD_START_ROUTINE pfnThreadRtn = reinterpret_cast<PTHREAD_START_ROUTINE>( ::GetProcAddress(::GetModuleHandle(_T("Kernel32")), "FreeLibrary")); if (pfnThreadRtn == NULL) __leave; // Create a remote thread that calls FreeLibrary() hThread = ::CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, me.modBaseAddr, 0, NULL); if (hThread == NULL) __leave; // Wait for the remote thread to terminate ::WaitForSingleObject(hThread, INFINITE); bOk = TRUE; // Everything executed successfully } __finally { // Now we can clean everything up CLOSE_HANDLE(hThread); CLOSE_HANDLE(hProcess); } return TRUE; }
程序开机自启的实现:
写入注册表,加上命令行参数,通过参数来判断是手动启动还是自启。
注册表操作用了CReg类。代码如下:
BOOL CMainDlg::SetAutoRun(BOOL bAuto) { CRegKey reg; LONG lRet = reg.Open(HKEY_LOCAL_MACHINE, _T("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"), KEY_WRITE); if (ERROR_SUCCESS == lRet) { if (bAuto) { TCHAR szTmp[MAX_PATH]; wsprintf(szTmp, _T("%s -auto"), m_szAppPath); lRet = reg.SetStringValue(_T("OnKeyMgr"), szTmp); } else { lRet = reg.DeleteValue(_T("OnKeyMgr")); } } else { MessageBox(_T("Access is denied. Plz Run as administrator to retry."), _T("Error"), MB_OK | MB_ICONERROR); } reg.Close(); return lRet == ERROR_SUCCESS; }
为了方便,将dll作为资源嵌入到exe里。程序运行时会判断是否存在dll。如果不存在则释放出dll。代码如下
BOOL CMainDlg::ExpandLib(void) { // Determine whether the file already exists if (::PathFileExists(m_szLibPath)) return TRUE; //Expand res HANDLE hLib = NULL; __try { HRSRC hRes = ::FindResource(NULL, MAKEINTRESOURCE(IDR_BIN_LIB), _T("BIN")); if (hRes == NULL) __leave; HGLOBAL hData = ::LoadResource(NULL, hRes); if (hData == NULL) __leave; LPVOID lpData = ::LockResource(hData); if (NULL == lpData) __leave; DWORD dwResSize = ::SizeofResource(NULL, hRes); hLib = ::CreateFile(m_szLibPath, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hLib == INVALID_HANDLE_VALUE) __leave; DWORD dwWritten; if (!::WriteFile(hLib, lpData, dwResSize, &dwWritten, NULL)) __leave; return TRUE; } __finally { CLOSE_HANDLE(hLib); } return FALSE; }
程序可以把快捷键自定义为打开某个程序,在使用文件选择对话框时可以选中快捷方式,而程序则需要解析出快捷方式真正的路径。代码如下②
HRESULT ResolveShortcut( LPCWSTR lpszShortcutPath, LPWSTR lpszFilePath) { HRESULT hRes = E_FAIL; CComPtr<IShellLink> ipShellLink; // buffer that receives the null-terminated string // for the drive and path WCHAR szPath[MAX_PATH]; // buffer that receives the null-terminated // structure that receives the information about the shortcut WIN32_FIND_DATA wfd; WCHAR wszTemp[MAX_PATH]; lpszFilePath[0] = L'\0'; // Get a pointer to the IShellLink interface hRes = ::CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, reinterpret_cast<LPVOID*>(&ipShellLink)); if (SUCCEEDED(hRes)) { // Get a pointer to the IPersistFile interface CComQIPtr<IPersistFile> ipPersistFile(ipShellLink); // IPersistFile is using LPCOLESTR, ::lstrcpynW(wszTemp, lpszShortcutPath, MAX_PATH); // Open the shortcut file and initialize it from its contents hRes = ipPersistFile->Load(wszTemp, STGM_READ); if (SUCCEEDED(hRes)) { // Try to find the target of a shortcut, // even if it has been moved or renamed hRes = ipShellLink->Resolve(NULL, SLR_UPDATE); if (SUCCEEDED(hRes)) { // Get the path to the shortcut target hRes = ipShellLink->GetPath(szPath, MAX_PATH, &wfd, SLGP_RAWPATH); if (FAILED(hRes)) return hRes; ::lstrcpynW(lpszFilePath, szPath, MAX_PATH); } } } return hRes; }
打开某个程序这里都是通过ShellExecute来实现。
接下来说说如何获取一些默认程序。
ShellExcute支持用CLSID作为参数来打开程序,打开我的电脑,我的文档、库、是通过CLSID来打开。
对应的CLSID分别是:
::{20D04FE0-3AEA-1069-A2D8-08002B30309D}
::{031E4825-7B94-4dc3-B131-E946B44C8DD5}
::{450D8FBA-AD25-11D0-98A8-0800361B1103}
这些CLSID是从注册表翻出来的。
打开email是ShellExecute支持mailto协议
其他程序都是通过读取注册表解析关联的程序。
读取的代码如下
第一个参数是扩展名,如.mp3,第二个参数传出参数,需要一个TCHAR sz[MAX_PATH]。第三个参数是一个标志位。
如果是要解析默认浏览器。则将第二个参数设置为"http"第三个参数设置为FALSE,
因为解析默认浏览器比其他的少了一个步骤。
BOOL CMgr::GetAssociatedApp( LPCTSTR pszExt, LPTSTR pszPath, BOOL bIsExt /*= TRUE*/ ) { HKEY hKey; LONG lRet; DWORD dwData = MAX_PATH; if (bIsExt) { lRet = ::RegOpenKeyEx(HKEY_CLASSES_ROOT, pszExt, 0, KEY_QUERY_VALUE, &hKey); if (ERROR_SUCCESS != lRet) return FALSE; lRet = ::RegQueryValueEx(hKey, NULL, 0, NULL, reinterpret_cast<LPBYTE>(pszPath), &dwData); if (ERROR_SUCCESS != lRet) return FALSE; lRet = ::RegCloseKey(hKey); if (ERROR_SUCCESS != lRet) return FALSE; } TCHAR szSubKey[MAX_PATH]; ::wsprintf(szSubKey, TEXT("%s\\Shell\\Open\\Command"), pszPath); lRet = ::RegOpenKeyEx(HKEY_CLASSES_ROOT, szSubKey, 0, KEY_QUERY_VALUE, &hKey); if (ERROR_SUCCESS != lRet) return FALSE; dwData = MAX_PATH; lRet = ::RegQueryValueEx(hKey, NULL, 0, NULL, reinterpret_cast<LPBYTE>(pszPath), &dwData); if (ERROR_SUCCESS != lRet) return FALSE; lRet = ::RegCloseKey(hKey); if (ERROR_SUCCESS != lRet) return FALSE; ::PathRemoveArgs(pszPath); return TRUE; }
以上是主要代码。详细的可以看附件提供的源码
另外写的过程中有一些暂时用不到的代码,在这里分享下:
一段用来显式加载Dll的宏:
/*********************************************************************************** * ex for decl API : void PFNPathRemoveArgs(PTSTR pszPath) * * typedef PTSTR (WINAPI *PROCTYPE(PathFindExtension))(PTSTR); // proc type decl * PROCTYPE(PathFindExtension) PathFindExtension; // variable decl * GETPROCADDR(hMod, PathFindExtension); * * Expand: * typedef PTSTR (WINAPI *PFNPathFindExtension)(PTSTR); * PFNPathFindExtension PathFindExtension; * PathFindExtension = reinterpret_cast<PFNPathFindExtension>( * ::GetProcAddress(hMod, "PathFindExtensionW")); ***********************************************************************************/ #define PROCNAME(x) #x #define PROCTYPE(x) PFN##x #ifdef UNICODE #define GETPROCADDR(hMod, proc) \ proc = reinterpret_cast<PROCTYPE(proc)> (::GetProcAddress(hMod, PROCNAME(proc##W))) #else #define GETPROCADDR(hMod, proc) \ proc = reinterpret_cast<PROCTYPE(proc)> (::GetProcAddress(hMod, PROCNAME(proc##A))) #endif
如果这个宏有更好的写法,欢迎指教。
下面代码是用来判断dll是否是64位的
BOOL CMainDlg::IsWow64Lib(LPCTSTR pszDll) { HANDLE hFile = NULL; __try { //Open dll file hFile = ::CreateFile(pszDll, GENERIC_READ, 0, NULL, OPEN_EXISTING, NULL, NULL); if (hFile == INVALID_HANDLE_VALUE) __leave; IMAGE_DOS_HEADER imgDosHdr; DWORD dwRead; // Read the IMAGE_DOS_HEADER if (!::ReadFile(hFile, &imgDosHdr, sizeof(IMAGE_DOS_HEADER), &dwRead, NULL)) __leave; // MZ header if (imgDosHdr.e_magic != IMAGE_DOS_SIGNATURE) __leave; IMAGE_NT_HEADERS imgNtHdr; // Offset file pointer to the pe header ::SetFilePointer(hFile, imgDosHdr.e_lfanew, 0, FILE_BEGIN); // Read the IMAGE_NT_HEADER ::ReadFile(hFile, &imgNtHdr, sizeof(IMAGE_NT_HEADERS), &dwRead, NULL); // PE header if (imgNtHdr.Signature != IMAGE_NT_SIGNATURE) __leave; return imgNtHdr.FileHeader.Machine != IMAGE_FILE_MACHINE_I386; } __finally { CLOSE_HANDLE(hFile); } return FALSE; }
代码里经常出现用来关闭句柄的宏:
#define CLOSE_HANDLE(handle) \ do \ { \ CloseHandle(handle); \ handle = NULL; \ } while (FALSE)
本来还打算。用一个32位的程序,嵌入两个dll一个32位一个64.在不同平台选择不同dll注入。
一开始是这么写。后来发现。没法直接用32位程序注入64位dll到64位进程,会出现拒绝访问。
得换思路实现。不过没时间写,就把思路分享如下:
思路一:再写一个64位的injector嵌入到exe。然后在64位系统上就释放这个Injector来注入。
思路二:用native API。得到ntdll.dll的baseaddr。然后定位到LdrLoadDll的addr。然后定位到RtlCreateUserThread,通过RtlCreateUserThread来创建远程线程并用LdrLoadDll来加载dll。这个需要内联汇编实现。比较麻烦。
另外,写这个程序时。一开始是在DLL_PROCESS_ATTACH时处理了很多操作。。。这就出现了有时DllMain里的代码没执行的情况。。。绞尽脑汁。。唯一能想到的原因就是Dllloader出现deadlock。。不过我没去验证。。在微软上看到一份文档,有兴趣可以看看。《Best
Practices for Creating DLLs》
最后我把一些操作封装成类,在构造函数里操作,然后再Dll里定义了一个全局对象。这样就没出现问题了。
=================================华丽的分割线======================================
源码下载:OneKeyMgr_src.7z (IDE采用VS2012)
可执行文件:
OneKeyMgr_for_Win32.7z
OneKeyMgr_for_Win64.7z
mark:
①此段代码借鉴了Jeffrey Richter的InjectLib。
②此处借鉴codeproject上的一篇文章
转载请标明出处,原文地址:/article/5538322.html
如果觉得本文对您有帮助,请支持一下,您的支持是我写作最大的动力,谢谢。
相关文章推荐
- 改造联想Y480的快捷键(跨进程替换窗口过程(子类化)的实现——远程线程注入)
- oracle的存储过程实现字段类型的替换
- 如何在同一窗口打开多个终端并实现快捷键切换
- eclipse如何使用快捷键实现编辑窗口放大还原
- C#获取进程的主窗口句柄的实现方法
- Android6.0 WMS(五) WMS计算Activity窗口大小的过程分析(一)应用进程
- Android双进程保护实现的思考及过程说明
- 窗口过程封装的一些实现
- -----------如何实现开机启动、清缓存、杀进程、悬浮窗口单双击区分,附源码
- 如何实现开机启动、清缓存、杀进程、悬浮窗口单双击区分,附源码
- Qt 实现进程间窗口嵌套(一)
- 前端页面打开新窗口的实现过程
- 进程替换以及简单实现简易shell
- 圆角窗口实现过程
- Linux进程网络流量统计的实现过程
- SDK学习笔记2-一个Win32窗口程序实现过程
- Android Binder实现的进程间IPC过程概要
- 如何在同一窗口打开多个终端并实现快捷键切换
- 微软实现的获取进程主窗口句柄代码
- VC实现A进程窗口嵌入到B进程窗口中显示的方法