如何通过HWND句柄在另一个进程中更改TDateTimePicker控件中当前选择的日期?

我正在编写一个自定义模块来使用专有软件。 (该软件已经停产,我没有源代码。)我的模块将作为一个单独的进程运行。 其目标是通过该专有软件实现操作自动化。 为此,我需要能够在TDateTimePicker控件中选择特定日期。 我知道这是一个Delphi控件,但就我对Delphi / Pascal的了解而言。 我可以找到这个控件的HWND句柄。

所以我的问题 – 有没有办法只在外部进程(使用WinAPI)的句柄中设置该控件的日期?

您可以将DTM_SETSYSTEMTIME消息发送到DTP的HWND 。 但是,该消息将指向SYSTEMTIME记录的指针作为参数,并且该指针必须在拥有DTP控件的进程的地址空间中有效。

当跨过程边界发送时,Windows 不会自动封送DTM_SETSYSTEMTIME ,因此如果您使用指向发送进程所拥有的SYSTEMTIME的指针并将其按原样发送到DTP进程,那么这将无效。 您必须手动将SYSTEMTIME数据SYSTEMTIME送到DTP进程,例如:

 uses ..., CommCtrl; var Wnd: HWND; Pid: DWORD; hProcess: THandle; ST: TSystemTime; PST: PSystemTime; Written: SIZE_T; begin Wnd := ...; // the HWND of the DateTimePicker control DateTimeToSystemTime(..., ST); // the desired date/time value // open a handle to the DTP's owning process... GetWindowThreadProcessId(Wnd, Pid); hProcess := OpenProcess(PROCESS_VM_WRITE or PROCESS_VM_OPERATION, FALSE, Pid); if hProcess = 0 then RaiseLastOSError; try // allocate a SYSTEMTIME record within the address space of the DTP process... PST := PSystemTime(VirtualAllocEx(hProcess, nil, SizeOf(ST), MEM_COMMIT, PAGE_READWRITE)); if PST = nil then RaiseLastOSError; try // copy the SYSTEMTIME data into the DTP process... if not WriteProcessMemory(hProcess, PST, @ST, SizeOf(ST), Written) then RaiseLastOSError; // now send the DTP message, specifying the memory address that belongs to the DTP process... SendMessage(Wnd, DTM_SETSYSTEMTIME, GDT_VALID, LPARAM(PST)); finally // free the SYSTEMTIME memory... VirtualFreeEx(hProcess, PST, SizeOf(ST), MEM_DECOMMIT); end; finally // close the process handle... CloseHandle(hProcess); end; end; 

现在,有了这个说法,还有另一个与TDateTimePicker有关的问题(一般不是DTP控件)。 TDateTimePicker 使用DTM_GETSYSTEMTIME消息来检索当前选择的日期/时间。 其Date / Time属性只返回在以下情况下更新的内部TDateTime变量的当前值:

  1. 最初创建TDateTimePicker ,其中日期/时间设置为Now()

  2. Date / Time属性由应用程序分配,可以是代码或DFM流。

  3. 它接收带有新日期/时间值的DTN_DATETIMECHANGE通知。

在这种情况下,你想要#3发生。 但是, DTN_DATETIMECHANGE (基于WM_NOTIFY )并不是由DTM_SETSYSTEMTIME自动生成的,所以你必须伪造它,但不能跨进程边界发送 WM_NOTIFY (Windows不会允许它–Raymond Chen 解释了一些原因 )。 这在MSDN上有记录:

对于Windows 2000及更高版本的系统,无法在进程之间发送WM_NOTIFY消息。

因此,您必须在DTP拥有的进程中注入一些自定义代码, DTN_DATETIMECHANGE在与DTP相同的进程中发送DTN_DATETIMECHANGE 。 将代码注入另一个进程并不容易实现 。 然而,在这个特殊情况下,有一个相当简单的解决方案,礼貌David Ching:

https://groups.google.com/d/msg/microsoft.public.vc.mfc/QMAHlPpEQyM/Nu9iQycmEykJ

正如其他人所指出的那样,LPARAM中的指针需要与创建hwnd的线程驻留在同一个进程中……我创建了一个SendMessageRemote()API,它使用VirtualAlloc,ReadProcessMemory,WriteProcessMemory和CreateRemoteThread来完成繁重的工作。 ..

http://www.dcsoft.com/private/sendmessageremote.h
http://www.dcsoft.com/private/sendmessageremote.cpp

它基于一篇很棒的CodeProject文章:
http://www.codeproject.com/threads/winspy.asp

这是他的代码的Delphi翻译。 注意,我已经在32位测试它,它的工作原理,但我还没有在64位测试它。 将消息从32位进程发送到64位进程或反之亦然,或者如果目标DTP使用的是Ansi窗口而不是Unicode窗口时,您可能需要调整它:

 const MAX_BUF_SIZE = 512; type LPFN_SENDMESSAGE = function(Wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; PINJDATA = ^INJDATA; INJDATA = record fnSendMessage: LPFN_SENDMESSAGE; // pointer to user32!SendMessage hwnd: HWND; msg: UINT; wParam: WPARAM; arrLPARAM: array[0..MAX_BUF_SIZE-1] of Byte; end; function ThreadFunc(pData: PINJDATA): DWORD; stdcall; begin Result := pData.fnSendMessage(pData.hwnd, pData.msg, pData.wParam, LPARAM(@pData.arrLPARAM)); end; procedure AfterThreadFunc; begin end; function SendMessageRemote(dwProcessId: DWORD; hwnd: HWND; msg: UINT; wParam: WPARAM; pLPARAM: Pointer; sizeLParam: size_t): LRESULT; var hProcess: THandle; // the handle of the remote process hUser32: THandle; DataLocal: INJDATA; pDataRemote: PINJDATA; // the address (in the remote process) where INJDATA will be copied to; pCodeRemote: Pointer; // the address (in the remote process) where ThreadFunc will be copied to; hThread: THandle; // the handle to the thread executing the remote copy of ThreadFunc; dwThreadId: DWORD; dwNumBytesXferred: SIZE_T; // number of bytes written/read to/from the remote process; cbCodeSize: Integer; lSendMessageResult: DWORD; begin Result := $FFFFFFFF; hUser32 := GetModuleHandle('user32'); if hUser32 = 0 then RaiseLastOSError; // Initialize INJDATA @DataLocal.fnSendMessage := GetProcAddress(hUser32, 'SendMessageW'); if not Assigned(DataLocal.fnSendMessage) then RaiseLastOSError; DataLocal.hwnd := hwnd; DataLocal.msg := msg; DataLocal.wParam := wParam; Assert(sizeLParam <= MAX_BUF_SIZE); Move(pLPARAM^, DataLocal.arrLPARAM, sizeLParam); // Copy INJDATA to Remote Process hProcess := OpenProcess(PROCESS_CREATE_THREAD or PROCESS_QUERY_INFORMATION or PROCESS_VM_OPERATION or PROCESS_VM_WRITE or PROCESS_VM_READ, FALSE, dwProcessId); if hProcess = 0 then RaiseLastOSError; try // 1. Allocate memory in the remote process for INJDATA // 2. Write a copy of DataLocal to the allocated memory pDataRemote := PINJDATA(VirtualAllocEx(hProcess, nil, sizeof(INJDATA), MEM_COMMIT, PAGE_READWRITE)); if pDataRemote = nil then RaiseLastOSError; try if not WriteProcessMemory(hProcess, pDataRemote, @DataLocal, sizeof(INJDATA), dwNumBytesXferred) then RaiseLastOSError; // Calculate the number of bytes that ThreadFunc occupies cbCodeSize := Integer(LPBYTE(@AfterThreadFunc) - LPBYTE(@ThreadFunc)); // 1. Allocate memory in the remote process for the injected ThreadFunc // 2. Write a copy of ThreadFunc to the allocated memory pCodeRemote := VirtualAllocEx(hProcess, nil, cbCodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); if pCodeRemote = nil then RaiseLastOSError; try if not WriteProcessMemory(hProcess, pCodeRemote, @ThreadFunc, cbCodeSize, dwNumBytesXferred) then RaiseLastOSError; // Start execution of remote ThreadFunc hThread := CreateRemoteThread(hProcess, nil, 0, pCodeRemote, pDataRemote, 0, dwThreadId); if hThread = 0 then RaiseLastOSError; try WaitForSingleObject(hThread, INFINITE); // Copy LPARAM back (result is in it) if not ReadProcessMemory(hProcess, @pDataRemote.arrLPARAM, pLPARAM, sizeLParam, dwNumBytesXferred) then RaiseLastOSError; finally GetExitCodeThread(hThread, lSendMessageResult); CloseHandle(hThread); Result := lSendMessageResult; end; finally VirtualFreeEx(hProcess, pCodeRemote, 0, MEM_RELEASE); end; finally VirtualFreeEx(hProcess, pDataRemote, 0, MEM_RELEASE); end; finally CloseHandle(hProcess); end; end; 

现在,操作DTP的代码变得更加简单:

 uses ..., CommCtrl; var Wnd: HWND; Pid: DWORD; nm: TNMDateTimeChange; begin Wnd := ...; // the HWND of the DateTimePicker control // get PID of DTP's owning process GetWindowThreadProcessId(Wnd, Pid); // prepare DTP message data nm.nmhdr.hwndFrom := Wnd; nm.nmhdr.idFrom := GetDlgCtrlID(Wnd); // VCL does not use CtrlIDs, but just in case nm.nmhdr.code := DTN_DATETIMECHANGE; nm.dwFlags := GDT_VALID; DateTimeToSystemTime(..., nm.st); // the desired date/time value // now send the DTP messages from within the DTP process... if SendMessageRemote(Pid, Wnd, DTM_SETSYSTEMTIME, GDT_VALID, @nm.st, SizeOf(nm.st)) <> 0 then SendMessageRemote(Pid, GetParent(Wnd), WM_NOTIFY, nm.nmhdr.idFrom, @nm, sizeof(nm)); end; 

如果一切顺利, TDateTimePicker现在将更新其内部TDateTime变量以匹配您发送给它的SYSTEMTIME

只是为了扩展Remy Lebeau的post ,这几乎提供了一个解决方案。

他的ThreadFunc有两个问题,或者在远程进程中调用的线程过程:

  • 大多数情况下, AfterThreadFunc方法将在Release构建之外进行优化,因此ThreadFunc过程的大小将无法正确设置。

  • 许多执行调试器构建的编译器会向方法添加额外的调试器检查,这肯定会使注入的远程进程中的ThreadFunc崩溃。

我想到了解决上面所说的最简单的方法,但遗憾的是除了使用汇编程序之外似乎没有更好的方法。 显然,由于这个原因,以下内容仅适用于32位进程。

这是我对Remy Lebeau解决方案的C实现(抱歉,我不使用Delphi。)

第一个结构定义:

 #define MAX_BUF_SIZE (512) typedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM); struct INJDATA { //IMPORTANT: If ANY of this struct members are changed, you will need to adjust the assembler code below! SENDMESSAGE fnSendMessage; // pointer to user32!SendMessage HWND hwnd; UINT msg; WPARAM wParam; BYTE arrLPARAM[MAX_BUF_SIZE]; }; 

然后在应用程序启动时收集一次静态指针,每次调用我们的方法时都不需要这样做。 为此,将它们全部移动到自己的struct

 struct SENDMSG_INJ_INFO{ SENDMESSAGE fnSendMessageRemote; int ncbSzFnSendMessageRemote; //Size of 'fnSendMessageRemote' in BYTEs HMODULE hUser32; SENDMESSAGE pfnSendMessage; //SendMessage API pointer SENDMSG_INJ_INFO() : fnSendMessageRemote(NULL) , ncbSzFnSendMessageRemote(0) { hUser32 = ::LoadLibrary(L"user32"); pfnSendMessage = hUser32 ? (SENDMESSAGE)GetProcAddress(hUser32, "SendMessageW") : NULL; int ncbSz = 0; SENDMESSAGE pfn = NULL; __asm { //Get sizes & offsets mov eax, lbl_code_begin mov dword ptr [pfn], eax mov eax, lbl_code_after sub eax, lbl_code_begin mov dword ptr [ncbSz], eax jmp lbl_code_after lbl_code_begin: //Thread proc that will be executed in remote process mov eax,dword ptr [esp+4] mov edx,dword ptr [eax+0Ch] lea ecx,[eax+10h] push ecx mov ecx,dword ptr [eax+8] push edx mov edx,dword ptr [eax+4] mov eax,dword ptr [eax] push ecx push edx call eax ret lbl_code_after: } ncbSzFnSendMessageRemote = ncbSz; fnSendMessageRemote = pfn; } ~SENDMSG_INJ_INFO() { if(hUser32) { ::FreeLibrary(hUser32); hUser32 = NULL; } } }; 

现在,不了解汇编程序的人的问题是如何在asm获得该程序。 这实际上非常简单。 将以下方法放入Release构建中(注意Release ,这很重要),然后在prototypeThreadFuncSendMsg调用上设置调试器断点并从中复制asm:

 //.h hile LRESULTDWORD __declspec(noinline) prototypeThreadFuncSendMsg(INJDATA *pData); //.cpp file LRESULT prototypeThreadFuncSendMsg(INJDATA *pData) { // There must be less than a page-worth of local // variables used in this function. return pData->fnSendMessage( pData->hwnd, pData->msg, pData->wParam, (LPARAM) pData->arrLPARAM ); } 

重要的是让编译器不要内联它。 对于Visual Studio,我为此添加了__declspec(noinline)

然后我们需要一个全局变量来存储我们的指针:

 //Define on a global scope SENDMSG_INJ_INFO sii; 

现在调用它的方法(只是原始post中稍微调整过的代码 – 我只是添加了几个错误检查和超时):

 //.h file static BOOL SendMessageTimeoutRemote(DWORD dwProcessId, HWND hwnd, UINT msg, WPARAM wParam, LPVOID pLPARAM, size_t sizeLParam, DWORD dwmsMaxWait = 5 * 1000, LRESULT* plOutSendMessageReturn = NULL); //.cpp file BOOL SendMessageTimeoutRemote(DWORD dwProcessId, HWND hwnd, UINT msg, WPARAM wParam, LPVOID pLPARAM, size_t sizeLParam, DWORD dwmsMaxWait, LRESULT* plOutSendMessageReturn) { //'dwmsMaxWait' = max number of ms to wait for result, or INFINITE to wait for as long as needed //'plOutSendMessageReturn' = if not NULL, will receive the value returned from calling SendMessage API in remote process //RETURN: // = TRUE if message was sent successfully (check returned value in 'plOutSendMessageReturn') BOOL bRes = FALSE; HANDLE hProcess = NULL; // the handle of the remote process HINSTANCE hUser32 = NULL; INJDATA *pDataRemote = NULL; // the address (in the remote process) where INJDATA will be copied to; DWORD *pCodeRemote = NULL; // the address (in the remote process) where ThreadFunc will be copied to; HANDLE hThread = NULL; // the handle to the thread executing the remote copy of ThreadFunc; DWORD dwThreadId = 0; DWORD dwNumBytesXferred = 0; // number of bytes written/read to/from the remote process; LRESULT lSendMessageReturn = 0xFFFFFFFF; __try { if (sii.pfnSendMessage == NULL) __leave; if(sizeLParam < 0 || sizeLParam > MAX_BUF_SIZE) { //Too much data ASSERT(NULL); __leave; } // Initialize INJDATA INJDATA DataLocal = { sii.pfnSendMessage, hwnd, msg, wParam }; memcpy ( DataLocal.arrLPARAM, pLPARAM, sizeLParam ); // Copy INJDATA to Remote Process hProcess = OpenProcess ( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId); if ( !hProcess ) __leave; // 1. Allocate memory in the remote process for INJDATA // 2. Write a copy of DataLocal to the allocated memory pDataRemote = (INJDATA*) VirtualAllocEx( hProcess, 0, sizeof(INJDATA), MEM_COMMIT, PAGE_READWRITE ); if (pDataRemote == NULL) __leave; if(!WriteProcessMemory( hProcess, pDataRemote, &DataLocal, sizeof(INJDATA), (SIZE_T *)&dwNumBytesXferred ) || dwNumBytesXferred != sizeof(INJDATA)) __leave; // Calculate the number of bytes that ThreadFunc occupies int cbCodeSize = sii.ncbSzFnSendMessageRemote; if(cbCodeSize <= 0) __leave; if(!sii.fnSendMessageRemote) __leave; // 1. Allocate memory in the remote process for the injected ThreadFunc // 2. Write a copy of ThreadFunc to the allocated memory pCodeRemote = (PDWORD) VirtualAllocEx( hProcess, 0, cbCodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if (pCodeRemote == NULL) __leave; if(!WriteProcessMemory( hProcess, pCodeRemote, sii.fnSendMessageRemote, cbCodeSize, (SIZE_T *)&dwNumBytesXferred ) || dwNumBytesXferred != cbCodeSize) __leave; // Start execution of remote ThreadFunc hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) pCodeRemote, pDataRemote, 0 , &dwThreadId); if (hThread == NULL) __leave; //Wait for thread to finish DWORD dwR = WaitForSingleObject(hThread, dwmsMaxWait); if(dwR == WAIT_OBJECT_0) { //Get return value if(GetExitCodeThread(hThread, (PDWORD)&lSendMessageReturn)) { // Copy LPARAM back (result is in it) if(ReadProcessMemory( hProcess, pDataRemote->arrLPARAM, pLPARAM, sizeLParam, (SIZE_T *)&dwNumBytesXferred) && dwNumBytesXferred == sizeLParam) { //Done bRes = TRUE; } } } } __finally { //Clean up if ( pDataRemote != 0 ) { VirtualFreeEx( hProcess, pDataRemote, 0, MEM_RELEASE ); pDataRemote = NULL; } if ( pCodeRemote != 0 ) { VirtualFreeEx( hProcess, pCodeRemote, 0, MEM_RELEASE ); pCodeRemote = NULL; } if ( hThread != NULL ) { CloseHandle(hThread); hThread = NULL; } if ( hProcess ) { CloseHandle (hProcess); hProcess = NULL; } } if(plOutSendMessageReturn) *plOutSendMessageReturn = lSendMessageReturn; return bRes; } 

最后我要求的方法来设置日期/时间:

 BOOL SetDateCtrlRemote(HWND hWnd, SYSTEMTIME* pSt) { //Set date/time in the DateTimePicker control with 'hWnd' in another process //'pSt' = local date/time to set //RETURN: // = TRUE if done BOOL bRes = FALSE; NMDATETIMECHANGE dtc = {0}; if(hWnd && pDt && pSt) { memcpy(&dtc.st, pSt, sizeof(*pSt)); //Get process ID for Digi DWORD dwProcID = 0; ::GetWindowThreadProcessId(hWnd, &dwProcID); if(dwProcID) { int nCntID = ::GetDlgCtrlID(hWnd); if(nCntID) { HWND hParentWnd = ::GetParent(hWnd); if(hParentWnd) { dtc.dwFlags = GDT_VALID; dtc.nmhdr.hwndFrom = hWnd; dtc.nmhdr.code = DTN_DATETIMECHANGE; dtc.nmhdr.idFrom = nCntID; LRESULT lRes = 0; //First change the control itself -- use 2 sec timeout if(SendMessageTimeoutRemote(dwProcID, hWnd, DTM_SETSYSTEMTIME, GDT_VALID, &dtc.st, sizeof(dtc.st), 2 * 1000, &lRes) && lRes != 0) { //Then need to send notification to the parent too! if(SendMessageTimeoutRemote(dwProcID, hParentWnd, WM_NOTIFY, dtc.nmhdr.idFrom, &dtc, sizeof(dtc), 2 * 1000)) { //Done bRes = TRUE; } } } } } } return bRes; } 

我知道这是很多代码,但是一旦你做了一次,它将全部工作,你可以重用该方法用于其他调用。

再次感谢Remy Lebeau !