如何通过其 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_SETSYSTEMTIMENOT 在跨进程边界发送时由 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 变量的当前值,该变量在以下时间更新:

  1. 最初创建TDateTimePicker,其中date/time设置为Now()

  2. 它的 Date/Time 属性 由应用分配,在代码或 DFM 流中。

  3. 它收到一个带有新 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!