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

时间:2015-03-06 20:45:34

标签: c++ c delphi winapi

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

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

2 个答案:

答案 0 :(得分:8)

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

Windows在跨进程边界发送时

DTM_SETSYSTEMTIME NOT 自动编组,因此如果您指向发送进程拥有的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变量的当前值:

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

  2. Date / Time属性由应用分配,代码或DFM流式传输。

  3. 它会收到DTN_DATETIMECHANGE通知,其中包含新的日期/时间值。

  4. 在这种情况下,你想要#3发生。但是,DTN_DATETIMECHANGE(基于WM_NOTIFYDTM_SETSYSTEMTIME并非由WM_NOTIFY自动生成,因此您必须假冒它,但DTN_DATETIMECHANGE 无法跨进程发送边界(Windows不允许它 - Raymond Chen explains a bit why)。这在MSDN上记录:

      

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

    因此,您必须在DTP的拥有过程中注入一些自定义代码,以便在与DTP相同的进程中发送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; 。并将代码注入另一个进程is not trivial to implement。然而,在这个特殊情况下,有一个相当简单的解决方案,礼貌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窗口时,您可能需要调整它:

    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;
    

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

    TDateTimePicker

    如果一切顺利,TDateTime现在会更新其内部SYSTEMTIME变量,以匹配您发送给它的{{1}}。

答案 1 :(得分:0)

只需扩展Remy Lebeau's post,这几乎可以提供解决方案。

他的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;

现在调用它的方法(只是来自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;
}

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

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