#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.