如何通过其 HWND 句柄在另一个进程中更改 TDateTimePicker 控件中的当前选定日期
How to change currently selected date in TDateTimePicker control in another process by its HWND handle?
我正在编写一个自定义模块来使用专有软件。(该软件已停产,我没有其源代码。我的模块将作为一个单独的进程运行。它的目标是通过此专有软件使操作自动化。为此,我需要能够在TDateTimePicker
控件中选择特定日期。我知道这是一个德尔福控件,但就我对德尔福/帕斯卡的了解而言。不过,我可以找到此控件的HWND
句柄。
所以我的问题 - 有没有办法仅通过外部进程(使用 WinAPI)的句柄在该控件中设置日期?
您可以向 DTP 的HWND
发送DTM_SETSYSTEMTIME
消息。 但是,该消息将指向SYSTEMTIME
记录的指针作为参数,并且该指针在拥有 DTP 控件的进程的地址空间中必须有效。
DTM_SETSYSTEMTIME
在跨进程边界发送时不会由 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
属性只是返回内部 TDateTime
变量的当前值,该变量在以下情况下更新:
最初创建
TDateTimePicker
,其中日期/时间设置为Now()
。它的
Date
/Time
属性由应用程序在代码或 DFM 流中分配。它接收具有新日期/时间值的
DTN_DATETIMECHANGE
通知。
在这种情况下,您希望发生 #3。 但是,DTN_DATETIMECHANGE
(基于 WM_NOTIFY
)不是由 DTM_SETSYSTEMTIME
自动生成的,所以你必须伪造它,但WM_NOTIFY
不能跨进程边界发送(Windows 不允许这样做 - 雷蒙德·陈解释了一点原因)。 这记录在 MSDN 上:
对于 Windows 2000 及更高版本的系统,无法在进程之间发送WM_NOTIFY消息。
因此,您必须将一些自定义代码注入到 DTP 的拥有进程中,以便在与 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。
这是他的代码的德尔菲翻译。 请注意,我已经在 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的帖子,这几乎给出了一个解决方案。
他的ThreadFunc
或将在远程进程中调用的线程过程有两个问题:
-
最肯定的是
AfterThreadFunc
方法将在发布版本之外进行优化,因此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;
现在调用它的方法(只是原始帖子中略微调整的代码 - 我只是添加了一些错误检查和超时):
//.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;
}
我知道这是很多代码,但是一旦你做了一次,它就会全部工作,你可以将该方法重用于其他调用。
再次感谢雷米·勒博!
- 在createdialog创建的窗口中捕获用于编辑控件的OnMouseMove消息
- WinAPI 在单击第一个对话框上的按钮控件并销毁第一个对话框后创建第二个对话框
- 在编译时,C++项目抛出错误 C2228,这是预期的,因为控件在运行时未达到该点
- 如何更改窗体上所有控件的标题?[C++生成器]
- 双击更改 mfc 中列表控件中的行的颜色
- 派生的 wxPanel 控件如何访问其中包含 wxDialog 中的数据?
- 如何从代码本身向 wxwidgets 中的文本控件插入字符?
- 如何在MFC中的静态文本控件上插入图标?
- 我的主窗口在创建时或单击更新区域时是否会收到编辑控件?
- 如何在Qt C++中向自定义控件添加属性?
- C/C++ 检测双击 TVItem 的常用控件
- 从C++标头中导入常量而不是硬编码它们:扩展 .net 控件?
- 控件不会在选择函数旁边移动
- MFC:我们能否扩展CEditView中存在的CEdit控件类行为
- 通过嵌入式 IWebBrowser2 控件中的链接打开 youtube 搜索失败
- 查找素数:错误:控件到达非void函数的末尾
- C++ 获取"控件可能会到达约翰逊-特罗特代码上的非空函数的末尾
- C++WIN32-将RTF数据加载到Rich Edit控件
- 由非托管(C++)COM服务器实例化的托管(C#)控件在Windows更新后损坏
- QVideo探针控件在仍被引用时被破坏