Delphi: имитация нажатия клавиши для автоматизации

#python #delphi #keyboard #ui-automation #pywinauto

#питон #дельфи #клавиатура #автоматизация пользовательского интерфейса #pywinauto

Вопрос:

Я хочу изменить текст элемента управления редактированием внешнего приложения. Приложение написано на Delphi. Она имеет несколько форм. Я начал с библиотек Python pywinauto sendkeys , чтобы протестировать первую форму TLoginForm . Это работает идеально. Вот псевдокод:

 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
 

В качестве второго шага я хочу создать пользовательский интерфейс для инструмента автоматизации. Но из-за недостатка знаний пользовательского интерфейса Python и учитывая сложность распространения двоичных файлов на Python, я хочу сделать это на Delphi. Но самое странное, что я не могу читать / записывать элемент управления редактированием в Delphi с помощью Windows API. Вот несколько попыток:

 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
 

Поскольку это работает на Python, я думаю, что я, должно быть, пропустил что-то очень простое и очень важное. Но теперь я настолько слеп, чтобы найти проблему. Любые намеки очень ценятся.

Комментарии:

1. Почему бы вам не использовать автоматизацию вместо того, чтобы подделывать ввод.

2. А также, если вы настаиваете на этом фальшивом подходе к вводу, вы должны, по крайней мере, включить проверку ошибок. Почему вы игнорируете возвращаемые значения?

3. @DavidHeffernan Каков правильный подход к автоматизации? Если это легко реализовать, я обязательно им воспользуюсь. И да, вы правы, я бы проверил возвращаемые значения для обработки ошибок. Я сделаю это.

4. Автоматизация пользовательского интерфейса — это API автоматизации

5. Кто знает? Возможно. Возможно, нет. Еще какие-нибудь подробности?

Ответ №1:

Но самое странное, что я не могу читать / записывать элемент управления редактированием в Delphi с помощью Windows API.

pywinauto использует стандартный Win32 API, поэтому все, что он может сделать, вы можете сделать в Delphi.

pywinauto имеет открытый исходный код, поэтому вы можете видеть, как ctrl.ClickInput() и ctrl.SetEditText() реализованы.

ctrl.ClickInput() звонки SetCursorPos() и. SendInput()

ctrl.SetEditText() отправляет EM_SETSEL сообщение для выделения текущего текста элемента управления Edit, а затем отправляет EM_REPLACESEL сообщение для замены выделенного текста новым текстом. Я предполагаю, что «защита от ввода» элемента управления редактированием, возможно, не блокирует эти сообщения.

Что-то еще, что следует отметить, pywinauto имеет тенденцию вызывать WaitForInputIdle() и Sleep() после выполнения действий в других окнах / процессах, чтобы дать цели некоторое время для обработки действий. Это может быть фактором «защиты от ввода», пытающимся отсеять автоматизированный код, но разрешить активность пользователя.

SetForegroundWindow(EditControlHandle); // Работает, приложение будет выведено на передний план, управление редактированием будет сфокусировано

Я никогда не слышал о SetForegroundWindow() выводе дочернего элемента управления на передний план. Даже если это произойдет, SetForegroundWindow() имеет много ограничений, которые, вероятно, не позволят вашему приложению установить окно переднего плана в любом случае.

SetFocus(EditControlHandle); // Ничего не происходит, если он сфокусирован на другом элементе управления редактированием формы в данный момент

Если вы хотите изменить фокус ввода на окно в другом процессе, вам необходимо присоединить вызывающий поток к потоку целевого окна с помощью AttachThreadInput() . Это четко указано в SetFocus() документации.

setText(EditControlHandle, ‘Hello world’); // Ничего не происходит

SetText() не является стандартной функцией Win32 API. Вы имеете в виду SetWindowText() вместо этого? SetWindowText() невозможно установить текст окна в другом процессе, об этом говорится в документации.

Или это SetText() оболочка для WM_SETTEXT ? Элемент управления, который имеет «защиту от ввода», вероятно, будет блокировать WM_SETTEXT сообщения, которые он не генерирует сам.

SendKeys32.SendKey(‘Hello world’, {Wait=}True); // Ничего не происходит

SendKeys просто помещает нажатия клавиш в очередь клавиатуры системы, позволяя Windows доставлять их в сфокусированное окно. Это должно сработать, поскольку приложение не может отличить введенные пользователем нажатия клавиш от нажатий клавиш, введенных SendKeys. Если только целевое приложение не подключается SendKeys() и keybd_event() не обнаруживает введенные нажатия клавиш, то есть.

Вы уже пробовали этот код?

http://www.experts-exchange.com/Programming/Languages/Pascal/Delphi/Q_27432926.html

SendMessage(EditControlHandle, Ord(‘H’), WM_KEYDOWN, 0); // Ничего не происходит
SendMessage(EditControlHandle, Ord(‘H’), WM_KEYUP, 0);

У вас есть Msg wParam значения параметров и наоборот. Ord('H') равно 72, что и есть WM_POWER послание. Элементы управления редактированием не заботятся об изменениях состояния электричества.

Вам также необходимо включить некоторые флаги при отправке этих сообщений:

 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, ноль);

Если вы используете AttachThreadInput() , вам нужно подключиться к потоку, которому принадлежит элемент управления Edit, поэтому используйте HWND элемента управления Edit, а не его родительский HWND.

если AttachThreadInput(GetCurrentThreadId, FocusedThreadID, {Attach=}True), то // Возвращает False

Какую версию Windows вы используете? В Vista и более поздних GetLastError() версиях возвращает допустимый код ошибки в случае AttachThreadInput() сбоя.

Обновление: приблизительный перевод исходного кода pywinauto для скрипта, который вы показали, будет выглядеть примерно так в 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;
 

Комментарии:

1. О, да, у них нет блока EM_REPLACESEL . Ты сделал мой день. Спасибо за предоставленную ценную информацию. Я пробовал AttachThreadInput() на Win7 (x64). Я использую FindWindow(‘TLoginForm’, nil), чтобы получить дескриптор окна.

2. Я добавил для вас перевод кода скрипта на Delphi.