2

我正在尝试使用 WinAPI 在另一个应用程序的窗口中更改页面控件中的选项卡。

我向页面控件发送了一条 TCM_SETCURSEL 消息,该消息确实更改了选项卡,但没有更改选项卡内容。例如:页面控件在选项卡 0 上,我将 TCM_SETCURSEL Index: 1 发送到页面控件,页面控件现在在选项卡 1 上,但继续显示选项卡 0 的内容而不是选项卡 1 的内容。

我努力了:

  • 在 TCM_SETCURSEL 之后将 WM_PAINT 发送到选项卡 1。
  • 在 TCM_SETCURSEL 之后将 WM_NCPAINT 发送到选项卡 1。
  • 将 TCM_SETCURSEL 之前的 WM_NOTIFY + TCN_SELCHANGING 和之后的 WM_NOTIFY + TCN_SELCHANGE 发送到页面控件。
  • 对页面控件的父级执行上述操作。

我正在使用 delphi 2010,目标应用程序也是一个 delphi 应用程序。

这是最后一次代码迭代,它将通知发送到页面控件的父级:

procedure ChangeTab(PageControlHandle: HWND; TabIndex: Integer);
var
  Info: TNMHdr;
begin
  Info.hwndFrom := PageControlHandle;
  Info.idFrom := GetWindowLongPtr(PageControlHandle, GWL_ID);
  Info.code := TCN_SELCHANGING;
  if SendMessage(GetParent(PageControlHandle), WM_NOTIFY, PageControlHandle, lParam(@Info)) <> 0 then
    raise Exception.Create('Page control didn''t allow tab to change.');

  if SendMessage(PageControlHandle, TCM_SETCURSEL, TabIndex, 0) = -1 then
    raise Exception.Create('Failed to change tab.');

  Info.code := TCN_SELCHANGE;
  SendMessage(GetParent(PageControlHandle), WM_NOTIFY, PageControlHandle, lParam(@Info))
end;

当我单击选项卡 1 WinSpy 显示它收到这些消息:

<000001> 001D0774 S WM_WINDOWPOSCHANGING lpwp:0018F308
<000002> 001D0774 R WM_WINDOWPOSCHANGING
<000003> 001D0774 S WM_CHILDACTIVATE
<000004> 001D0774 R WM_CHILDACTIVATE
<000005> 001D0774 S WM_WINDOWPOSCHANGED lpwp:0018F308
<000006> 001D0774 R WM_WINDOWPOSCHANGED
<000007> 001D0774 S WM_WINDOWPOSCHANGING lpwp:0018EF7C
<000008> 001D0774 R WM_WINDOWPOSCHANGING
<000009> 001D0774 S WM_NCPAINT hrgn:00000001
<000010> 001D0774 R WM_NCPAINT
<000011> 001D0774 S WM_ERASEBKGND hdc:33011920
<000012> 001D0774 R WM_ERASEBKGND fErased:True
<000013> 001D0774 S WM_WINDOWPOSCHANGED lpwp:0018EF7C
<000014> 001D0774 R WM_WINDOWPOSCHANGED
<000015> 001D0774 P WM_PAINT hdc:00000000
<000016> 001D0774 S WM_CTLCOLORSTATIC hdcStatic:FB01097B hwndStatic:001507D0
<000017> 001D0774 R WM_CTLCOLORSTATIC hBrush:261011F7
<000018> 001D0774 S WM_CTLCOLORSTATIC hdcStatic:FB01097B hwndStatic:001507D0
<000019> 001D0774 R WM_CTLCOLORSTATIC hBrush:261011F7
<000020> 001D0774 S WM_CTLCOLORSTATIC hdcStatic:530112DB hwndStatic:000608C2
<000021> 001D0774 R WM_CTLCOLORSTATIC hBrush:261011F7
<000022> 001D0774 S WM_CTLCOLORSTATIC hdcStatic:530112DB hwndStatic:000608C2
<000023> 001D0774 R WM_CTLCOLORSTATIC hBrush:261011F7
<000024> 001D0774 S WM_DRAWITEM idCtl:395458 lpdis:0018F728
<000025> 001D0774 R WM_DRAWITEM fProcessed:False
<000026> 001D0774 S WM_CTLCOLOREDIT hdcEdit:FB01097B hwndEdit:000808A8
<000027> 001D0774 R WM_CTLCOLOREDIT hBrush:3810149A
<000028> 001D0774 S WM_CTLCOLOREDIT hdcEdit:FB01097B hwndEdit:000808A8
<000029> 001D0774 R WM_CTLCOLOREDIT hBrush:3810149A
<000030> 001D0774 S WM_DRAWITEM idCtl:526504 lpdis:0018F728
<000031> 001D0774 R WM_DRAWITEM fProcessed:False
<000032> 001D0774 S WM_CTLCOLORSTATIC hdcStatic:530112DB hwndStatic:001A06F2
<000033> 001D0774 R WM_CTLCOLORSTATIC hBrush:261011F7
<000034> 001D0774 S WM_CTLCOLORSTATIC hdcStatic:530112DB hwndStatic:001A06F2
<000035> 001D0774 R WM_CTLCOLORSTATIC hBrush:261011F7
4

2 回答 2

2

通常,PageControl 本身TCN_...会向其自己的父级发送通知,因此用于这些通知的参数存在于 PageControl 和父级正在运行的同一地址空间中。您正在从另一个进程发送通知,因此您的TNMHdr指针位于发送应用程序,并且不是接收应用程序地址空间中的有效指针。更糟糕的WM_NOTIFY是,不允许跨进程边界发送,如MSDN 所述

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

因此,您需要使用VirtualAllocEx()WriteProcessMemory()分配和操作TNMHdr接收应用程序地址空间中的记录。您需要在接收进程中注入代码才能发送TCN_...消息。

尝试这个:

// this is a Delphi translation of code written by David Ching:
//
// https://groups.google.com/d/msg/microsoft.public.vc.mfc/QMAHlPpEQyM/Nu9iQycmEykJ
//
// http://www.dcsoft.com/private/sendmessageremote.h
// http://www.dcsoft.com/private/sendmessageremote.cpp

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;

procedure ChangeTab(PageControlHandle: HWND; TabIndex: Integer);
var
  dwProcessId: DWORD;
  hParent: HWND;
  Info: TNMHdr;
begin
  GetWindowThreadProcessId(PageControlHandle, @dwProcessId);
  hParent := GetParent(PageControlHandle);

  Info.hwndFrom := PageControlHandle;
  Info.idFrom := GetWindowLongPtr(PageControlHandle, GWL_ID);
  Info.code := TCN_SELCHANGING;

  if SendMessageRemote(dwProcessId, hParent, WM_NOTIFY, WPARAM(PageControlHandle), @Info, SizeOf(TNMHdr)) <> 0 then
    raise Exception.Create('Page control didn''t allow tab to change.');

  if SendMessage(PageControlHandle, TCM_SETCURSEL, TabIndex, 0) = -1 then
    raise Exception.Create('Failed to change tab.');

  Info.code := TCN_SELCHANGE;
  SendMessageRemote(dwProcessId, hParent, WM_NOTIFY, WPARAM(PageControlHandle), @Info, SizeOf(TNMHdr));
end;
于 2013-04-17T20:07:09.390 回答
2

发现使用 TCM_SETCURFOCUS 消息而不是 TCM_SETCURSEL 足以更改选项卡的内容。

procedure ChangeTab(PageControlHandle: HWND; TabIndex: Integer);
begin
  SendMessage(PageControlHandle, TCM_SETCURFOCUS, TabIndex, 0);
end;

但是,如果页面控件处于按钮模式(具有 TCS_BUTTONS 样式),这将不起作用,因为按钮可以在不更改内容的情况下接收焦点。

于 2013-04-18T13:59:35.023 回答