I want to change text of an edit control of an external application. The application is written in Delphi. It has several forms. I started with Python libraries pywinauto
+ sendkeys
to test the first form TLoginForm
. It works perfectly. Here is the pseudo code:
helper = pywinauto.application.Application()
hwnd = pywinauto.findwindows.find_windows(class_name='TLoginForm')[0]
window = helper.window_(handle=hwnd)
ctrl = window[2] # the second control is the edit control I want to access
ctrl.ClickInput() # focus the control
ctrl.SetEditText('Hello world') # text can be changed expectedly
As the second step, I want make an UI for the automation tool. But due to lack knowledge of Python UI and considering of the complexity to distribute binary in Python, I want to do it Delphi. But the weird thing is I am not able to read/write the edit control in Delphi by using Windows apis. Here are some attempts:
SetForegroundWindow(EditControlHandle); // Works, the application will be brought to front, the edit control will be focused
// Attempt 1: Nothing happens
SetFocus(AnotherEditControlHandle);
// Attempt 2: Nothing happens
SetWindowText(EditControlHandle, 'Hello world');
// Attempt 3: Nothing happens
SendKeys32.SendKey('Hello world', {Wait=}True);
// Attempt 4: Nothing happens
SendMessage(EditControlHandle, Ord('H'), WM_KEYDOWN, 0);
SendMessage(EditControlHandle, Ord('H'), WM_KEYUP, 0);
// Attempt 5: AttachThreadInput will return False, the reason is "Access Denied"
FocusedThreadID := GetWindowThreadProcessID(ExternalAppMainWindowHandle, nil);
if AttachThreadInput(GetCurrentThreadID, FocusedThreadID, {Attach=}True) then
As it works in Python, I think I must have missed something very basic and very important. But I am now so blind to find the problem. Any hints are very much appreciated.
But the weird thing is I am not able to read/write the edit control in Delphi by using Windows apis.
pywinauto uses standard Win32 API, so anything it can do, you can do in Delphi.
pywinauto is open source, so you can see how ctrl.ClickInput()
and ctrl.SetEditText()
are implemented.
ctrl.ClickInput()
calls SetCursorPos()
and SendInput()
.
ctrl.SetEditText()
sends an EM_SETSEL
message to highlight the Edit control's current text, and then sends an EM_REPLACESEL
message to replace the highlighted text with new text. My guess would be that the Edit control's "anti-input protection" might not be blocking those messages.
Something else to note, pywinauto tends to call WaitForInputIdle()
and Sleep()
after performing actions in other windows/processes, to give the target some time to process the actions. That may be a factor in "anti-input protection" trying to weed out automated code but allow user activity.
SetForegroundWindow(EditControlHandle); // Works, the application will be brought to front, the edit control will be focused
I have never heard of SetForegroundWindow()
bringing a child control into the foreground. Even if it does, SetForegroundWindow()
has many restrictions that will likely prevent your app from setting a foreground window anyway.
SetFocus(EditControlHandle); // Nothing happens, if it is focused at another edit control of the form currently
If you want to change the input focus to a window in another process, you have to attach your calling thread to the target window's thread using AttachThreadInput()
. This is clearly stated in the SetFocus()
documentation.
SetText(EditControlHandle, 'Hello world'); // Nothing happens
SetText()
is not a standard Win32 API function. Do you mean SetWindowText()
instead? SetWindowText()
cannot set the text of a window in another process, the documentation says as much.
Or is SetText()
a wrapper for WM_SETTEXT
? A control that has "anti-input protection" is likely to block WM_SETTEXT
messages that it does not generate itself.
SendKeys32.SendKey('Hello world', {Wait=}True); // Nothing happens
SendKeys merely puts keystrokes into the system's keyboard queue, letting Windows deliver them to the focused window. This should work, since an app cannot differentiate between user entered keystrokes and SendKeys-injected keystrokes. Unless the target app is hooking SendKeys()
and keybd_event()
to detect injected keystrokes, that is.
Have you tried this code yet?
http://www.experts-exchange.com/Programming/Languages/Pascal/Delphi/Q_27432926.html
SendMessage(EditControlHandle, Ord('H'), WM_KEYDOWN, 0); // Nothing happens
SendMessage(EditControlHandle, Ord('H'), WM_KEYUP, 0);
You have the Msg
and wParam
parameter values backwards. Ord('H')
is 72, which is the WM_POWER
message. Edit controls do not care about electricity state changes.
You also need to include some flags when sending these messages:
var
ScanCode: UINT;
ScanCode := MapVirtualKey(Ord('H'), MAPVK_VK_TO_VSC);
SendMessage(EditControlHandle, WM_KEYDOWN, Ord('H'), ScanCode shl 16);
SendMessage(EditControlHandle, WM_KEYUP, Ord('H'), (ScanCode shl 16) or $C0000001);
FocusedThreadID := GetWindowThreadProcessID(ExternalAppMainWindowHandle, nil);
If you use AttachThreadInput()
, you need to attach to the thread that owns the Edit control, so use the Edit control's HWND, not its parent HWND.
if AttachThreadInput(GetCurrentThreadID, FocusedThreadID, {Attach=}True) then // Returns False
What version of Windows are you using? On Vista and later, GetLastError()
returns a valid error code if AttachThreadInput()
fails.
Update: a rough translation of the pywinauto source code for the script you showed would look something like this in Delphi:
uses
..., Windows;
procedure WaitGuiThreadIdle(wnd: HWND);
var
process_id: DWORD;
hprocess: THandle;
begin
GetWindowThreadProcessId(wnd, process_id);
hprocess := OpenProcess(PROCESS_QUERY_INFORMATION, 0, process_id);
WaitForInputIdle(hprocess, 1000);
CloseHandle(hprocess);
end;
function SndMsgTimeout(wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): DWORD_PTR;
begin
SendMessageTimeout(wnd, Msg, wParam, lParam, SMTO_NORMAL, 1, @Result);
end;
var
wnd, ctrl, cur_foreground: HWND;
cur_fore_thread, control_thread: DWORD;
r: TRect;
input: array[0..1] of TInput;
i: Integer;
begin
// hwnd = pywinauto.findwindows.find_windows(class_name='TLoginForm')[0]
wnd := FindWindow('TLoginForm', nil);
// window = helper.window_(handle=hwnd)
// ctrl = window[2] # the second control is the edit control I want to access
wnd := GetWindow(wnd, GW_CHILD);
ctrl := GetWindow(wnd, GW_HWNDNEXT);
// ctrl.ClickInput() # focus the control
cur_foreground := GetForegroundWindow();
if ctrl <> cur_foreground then
begin
cur_fore_thread := GetWindowThreadProcessId(cur_foreground, nil);
control_thread := GetWindowThreadProcessId(ctrl, nil);
if cur_fore_thread <> control_thread then
begin
AttachThreadInput(cur_fore_thread, control_thread, True);
SetForegroundWindow(ctrl);
AttachThreadInput(cur_fore_thread, control_thread, False);
end
else
SetForegroundWindow(ctrl);
WaitGuiThreadIdle(ctrl);
Sleep(60);
end;
GetWindowRect(ctrl, r);
SetCursorPos((r.Width div 2) + r.Left, (r.Height div 2) + r.Top);
Sleep(10);
for I := 0 to 1 do
begin
input[I].Itype := INPUT_MOUSE;
input[I].mi.dx := 0;
input[I].mi.dy := 0;
input[I].mi.mouseData := 0;
input[I].mi.dwFlags := 0;
input[I].mi.time := 0;
input[I].mi.dwExtraInfo := 0;
end;
if GetSystemMetrics(SM_SWAPBUTTON) = 0 then
begin
input[0].mi.dwFlags := MOUSEEVENTF_LEFTDOWN;
input[1].mi.dwFlags := MOUSEEVENTF_LEFTUP;
end else
begin
input[0].mi.dwFlags := MOUSEEVENTF_RIGHTDOWN;
input[1].mi.dwFlags := MOUSEEVENTF_RIGHTUP;
end;
for I := 0 to 1 do
begin
SendInput(1, @input[I], Sizeof(TInput));
Sleep(10);
end;
// ctrl.SetEditText('Hello world') # text can be changed expectedly
SndMsgTimeout(ctrl, EM_SETSEL, 0, -1);
WaitGuiThreadIdle(ctrl);
Sleep(0);
SndMsgTimeout(ctrl, EM_REPLACESEL, 1, LPARAM(PChar('Hello world')));
WaitGuiThreadIdle(ctrl);
Sleep(0);
end;