Delphi:模拟按键以实现自动化
我想要改变一个外部应用程序中编辑框的文本。这个应用程序是用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来做。但奇怪的是,我无法通过Windows的API来读写Delphi中的编辑框。以下是我尝试过的一些方法:
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 个回答
奇怪的是,我无法通过Windows API在Delphi中读取或写入编辑控件。
pywinauto使用的是标准的Win32 API,所以它能做的事情,你在Delphi中也能做到。
pywinauto是开源的,你可以查看ctrl.ClickInput()
和ctrl.SetEditText()
是怎么实现的。
ctrl.ClickInput()
调用了SetCursorPos()
和SendInput()
。
ctrl.SetEditText()
发送一个EM_SETSEL
消息来高亮显示编辑控件当前的文本,然后发送一个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, nil);
如果你使用AttachThreadInput()
,你需要附加到拥有编辑控件的线程,所以要使用编辑控件的HWND,而不是它的父HWND。
if AttachThreadInput(GetCurrentThreadID, FocusedThreadID, {Attach=}True) then // 返回False
你使用的是什么版本的Windows?在Vista及以后的版本中,如果AttachThreadInput()
失败,GetLastError()
会返回一个有效的错误代码。
更新:你展示的脚本的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;