Question

I'm trying to change a tab in a page control in another application's window using the WinAPI.

I sent a TCM_SETCURSEL message to the page control, that did change the tab but didn't changed the tab contents. Ex: The Pagecontrol is on tab 0, I send a TCM_SETCURSEL Index: 1 to the page control, the page control is now on tab 1, but keep showing tab 0's contents instead of tab 1's.

I have tried:

  • send a WM_PAINT to tab 1 after the TCM_SETCURSEL.
  • send a WM_NCPAINT to the tab 1 after the TCM_SETCURSEL.
  • to send a WM_NOTIFY + TCN_SELCHANGING before the TCM_SETCURSEL and a WM_NOTIFY + TCN_SELCHANGE after it to the page control.
  • do the above to the page control's parent.

I'm using delphi 2010 and the target application is also a delphi app.

This is the last code iteration, which sends the notifications to the page control's parent:

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;

When I click on tab 1 WinSpy shows that it receives these messages:

<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
Was it helpful?

Solution

Found out that using the TCM_SETCURFOCUS message instead of the TCM_SETCURSEL is enough to change the tab's contents.

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

However this will not work if the page control is in button mode (has the TCS_BUTTONS style), because the buttons can receive focus without changing the contents.

OTHER TIPS

Normally a PageControl itself sends TCN_... notifications to its own parent, thus parameters used for those notifications exist in the same address space that the PageControl and parent are running in. You are sending the notifications from another process, so your TNMHdr pointer is in the address space of the sending app and is not a valid pointer in the address space of the receiving app. And worse, WM_NOTIFY is not allowed to be sent across process boundaries, as documented by MSDN:

For Windows 2000 and later systems, the WM_NOTIFY message cannot be sent between processes.

So, you need to use VirtualAllocEx() and WriteProcessMemory() to allocate and manipulate a TNMHdr record in the receiving app's address space. And you need to inject code into the receiving process in order to send the TCN_... messages.

Try this:

// 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;
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top