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;