如何通过其 HWND 句柄更改另一个进程中 TDateTimePicker 控件中当前选定的日期?
How to change currently selected date in TDateTimePicker control in another process by its HWND handle?
我正在编写一个自定义模块来使用专有软件。 (该软件已经停产,我没有它的源代码。)我的模块将 运行 作为一个单独的进程。它的目标是通过这个专有软件自动化操作。为此,我需要能够 select TDateTimePicker
控件中的特定日期。我知道这是一个 Delphi 控件,但就我对 Delphi/Pascal 的了解而言。不过,我可以找到此控件的 HWND
句柄。
所以我的问题 -- 有没有一种方法可以仅通过来自外部进程的句柄在该控件中设置日期(使用 WinAPI)?
您可以向 DTP 的 HWND
发送 DTM_SETSYSTEMTIME
消息。但是,该消息将指向 SYSTEMTIME
记录的指针作为参数,并且该指针必须在拥有 DTP 控制的进程的地址 space 中有效。
DTM_SETSYSTEMTIME
是 NOT 在跨进程边界发送时由 Windows 自动编组,所以如果你拿一个指向 SYSTEMTIME
的指针由发送进程拥有并将其按原样发送到 DTP 进程,这将不起作用。您必须手动将 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。它的 Date
/Time
属性只是 return 内部 TDateTime
变量的当前值,该变量在以下时间更新:
最初创建TDateTimePicker
,其中date/time设置为Now()
。
它的 Date
/Time
属性 由应用分配,在代码或 DFM 流中。
它收到一个带有新 date/time 值的 DTN_DATETIMECHANGE
通知。
在这种情况下,您希望#3 发生。但是,DTN_DATETIMECHANGE
(基于WM_NOTIFY
) is not generated automatically by DTM_SETSYSTEMTIME
, so you have to fake it, but WM_NOTIFY
cannot be sent across process boundaries (Windows will not allow it - Raymond Chen explains a bit why)。这记录在 MSDN 上:
For Windows 2000 and later systems, the WM_NOTIFY message cannot be sent between processes.
因此,您必须将一些自定义代码注入 DTP 的所属进程才能在与 DTP 相同的进程中发送 DTN_DATETIMECHANGE
。并将代码注入另一个进程 is not trivial to implement。然而,在这种特殊情况下,有一个相当简单的解决方案,由 David Ching 提供:
https://groups.google.com/d/msg/microsoft.public.vc.mfc/QMAHlPpEQyM/Nu9iQycmEykJ
As others have pointed out, the pointer in LPARAM needs to reside in the same process as the thread that created hwnd ... I have created a SendMessageRemote() API which uses VirtualAlloc, ReadProcessMemory, WriteProcessMemory, and CreateRemoteThread to do the heavy lifting ...
http://www.dcsoft.com/private/sendmessageremote.h
http://www.dcsoft.com/private/sendmessageremote.cpp
It is based on a great CodeProject article:
http://www.codeproject.com/threads/winspy.asp.
这是他的代码的 Delphi 翻译。请注意,我已经在 32 位中测试过它并且它可以工作,但我没有在 64 位中测试过它。当从 32 位进程向 64 位进程发送消息或从 64 位进程向 64 位进程发送消息时,或者如果目标 DTP 使用 Ansi window 而不是 Unicode window:
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
。
只是扩展 ,这几乎给出了解决方案。
他的 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;
现在是调用它的方法(只是 original 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;
}
最后是我要求的方法来设置 date/time:
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!
我正在编写一个自定义模块来使用专有软件。 (该软件已经停产,我没有它的源代码。)我的模块将 运行 作为一个单独的进程。它的目标是通过这个专有软件自动化操作。为此,我需要能够 select TDateTimePicker
控件中的特定日期。我知道这是一个 Delphi 控件,但就我对 Delphi/Pascal 的了解而言。不过,我可以找到此控件的 HWND
句柄。
所以我的问题 -- 有没有一种方法可以仅通过来自外部进程的句柄在该控件中设置日期(使用 WinAPI)?
您可以向 DTP 的 HWND
发送 DTM_SETSYSTEMTIME
消息。但是,该消息将指向 SYSTEMTIME
记录的指针作为参数,并且该指针必须在拥有 DTP 控制的进程的地址 space 中有效。
DTM_SETSYSTEMTIME
是 NOT 在跨进程边界发送时由 Windows 自动编组,所以如果你拿一个指向 SYSTEMTIME
的指针由发送进程拥有并将其按原样发送到 DTP 进程,这将不起作用。您必须手动将 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。它的 Date
/Time
属性只是 return 内部 TDateTime
变量的当前值,该变量在以下时间更新:
最初创建
TDateTimePicker
,其中date/time设置为Now()
。它的
Date
/Time
属性 由应用分配,在代码或 DFM 流中。它收到一个带有新 date/time 值的
DTN_DATETIMECHANGE
通知。
在这种情况下,您希望#3 发生。但是,DTN_DATETIMECHANGE
(基于WM_NOTIFY
) is not generated automatically by DTM_SETSYSTEMTIME
, so you have to fake it, but WM_NOTIFY
cannot be sent across process boundaries (Windows will not allow it - Raymond Chen explains a bit why)。这记录在 MSDN 上:
For Windows 2000 and later systems, the WM_NOTIFY message cannot be sent between processes.
因此,您必须将一些自定义代码注入 DTP 的所属进程才能在与 DTP 相同的进程中发送 DTN_DATETIMECHANGE
。并将代码注入另一个进程 is not trivial to implement。然而,在这种特殊情况下,有一个相当简单的解决方案,由 David Ching 提供:
https://groups.google.com/d/msg/microsoft.public.vc.mfc/QMAHlPpEQyM/Nu9iQycmEykJ
As others have pointed out, the pointer in LPARAM needs to reside in the same process as the thread that created hwnd ... I have created a SendMessageRemote() API which uses VirtualAlloc, ReadProcessMemory, WriteProcessMemory, and CreateRemoteThread to do the heavy lifting ...
http://www.dcsoft.com/private/sendmessageremote.h
http://www.dcsoft.com/private/sendmessageremote.cppIt is based on a great CodeProject article:
http://www.codeproject.com/threads/winspy.asp.
这是他的代码的 Delphi 翻译。请注意,我已经在 32 位中测试过它并且它可以工作,但我没有在 64 位中测试过它。当从 32 位进程向 64 位进程发送消息或从 64 位进程向 64 位进程发送消息时,或者如果目标 DTP 使用 Ansi window 而不是 Unicode window:
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
。
只是扩展
他的 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;
现在是调用它的方法(只是 original 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;
}
最后是我要求的方法来设置 date/time:
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!