Delphi: Simulate key press for automation

2020-07-27 03:28发布

问题:

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.

回答1:

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;