这是我之前遇到的一个问题,但我总是放弃解决问题并制定出一个解决方法。不是今天(希望如此)。
我正在尝试为经典的Doom II制作机器人。我希望我的机器人可以访问通过转义键访问的主菜单。当然我试过了:
sendkeys.send("{ESC}")
没有运气。但后来发生了一些奇怪的事情。当我已经在菜单上时,我意外地运行了代码......它关闭了菜单(如果你在菜单上按下escape,这是正常的)。很明显,Doom II听取了Sendkeys。
我已经尝试过sendinput,postmessage和simulateinput。没有工作(它们都具有与sendkeys描述的相同的行为)。
如果有人可以骑上白马并给我代码来解决这个问题,那会很棒,但除此之外,任何人都可以向我解释这种行为吗?
答案 0 :(得分:2)
似乎Zandronum在游戏运行时(不暂停)不接受发送给它的虚拟键。我不确定,但似乎虚拟键实际上可能是窗口消息,就像Andrew Morton所说的那样(或者它们至少是类似的......)。解决方法是发送hardware scan code而不是virtual key code。
硬件扫描代码似乎是按键时实际键盘发送的代码,而虚拟键代码是系统从中解释的密钥扫描代码(reference)。
所以我设法使用一些WinAPI函数向Zandronum发送击键(全屏和窗口):
SendInput()
,用于发送实际的键盘输入。MapVirtualKeyEx()
,用于将密钥代码转换为扫描代码,反之亦然。GetKeyboardLayout()
用于获取用户当前的键盘布局(例如,我有瑞典语键盘)。通过使用我构建的下面的帮助程序类(或更正确的:包装程序),您现在可以以简单的方式发送击键(硬件与否),键的种类比SendKeys.Send()
包含的更多。您可以使用System.Windows.Forms.Keys
enumeration中的任意键。
这是用Zandronum测试的,完全可以使用:
InputHelper.PressKey(Keys.Escape, True) 'True = Send key as hardware scan code.
<强> InputHelper.vb:强>
Imports System.Runtime.InteropServices
Public NotInheritable Class InputHelper
Private Sub New()
End Sub
#Region "Methods"
#Region "PressKey()"
''' <summary>
''' Virtually presses a key.
''' </summary>
''' <param name="Key">The key to press.</param>
''' <param name="HardwareKey">Whether or not to press the key using its hardware scan code.</param>
''' <remarks></remarks>
Public Shared Sub PressKey(ByVal Key As Keys, Optional ByVal HardwareKey As Boolean = False)
If HardwareKey = False Then
InputHelper.SetKeyState(Key, False)
InputHelper.SetKeyState(Key, True)
Else
InputHelper.SetHardwareKeyState(Key, False)
InputHelper.SetHardwareKeyState(Key, True)
End If
End Sub
#End Region
#Region "SetKeyState()"
''' <summary>
''' Virtually sends a key event.
''' </summary>
''' <param name="Key">The key of the event to send.</param>
''' <param name="KeyUp">Whether to push down or release the key.</param>
''' <remarks></remarks>
Public Shared Sub SetKeyState(ByVal Key As Keys, ByVal KeyUp As Boolean)
Key = ReplaceBadKeys(Key)
Dim KeyboardInput As New KEYBDINPUT With {
.wVk = Key,
.wScan = 0,
.time = 0,
.dwFlags = If(KeyUp, KEYEVENTF.KEYUP, 0),
.dwExtraInfo = IntPtr.Zero
}
Dim Union As New INPUTUNION With {.ki = KeyboardInput}
Dim Input As New INPUT With {
.type = INPUTTYPE.KEYBOARD,
.U = Union
}
SendInput(1, New INPUT() {Input}, Marshal.SizeOf(GetType(INPUT)))
End Sub
#End Region
#Region "SetHardwareKeyState()"
''' <summary>
''' Virtually sends a key event using the key's scan code.
''' </summary>
''' <param name="Key">The key of the event to send.</param>
''' <param name="KeyUp">Whether to push down or release the key.</param>
''' <remarks></remarks>
Public Shared Sub SetHardwareKeyState(ByVal Key As Keys, ByVal KeyUp As Boolean)
Key = ReplaceBadKeys(Key)
Dim KeyboardInput As New KEYBDINPUT With {
.wVk = 0,
.wScan = MapVirtualKeyEx(CUInt(Key), 0, GetKeyboardLayout(0)),
.time = 0,
.dwFlags = KEYEVENTF.SCANCODE Or If(KeyUp, KEYEVENTF.KEYUP, 0),
.dwExtraInfo = IntPtr.Zero
}
Dim Union As New INPUTUNION With {.ki = KeyboardInput}
Dim Input As New INPUT With {
.type = INPUTTYPE.KEYBOARD,
.U = Union
}
SendInput(1, New INPUT() {Input}, Marshal.SizeOf(GetType(INPUT)))
End Sub
#End Region
#Region "ReplaceBadKeys()"
''' <summary>
''' Replaces bad keys with their corresponding VK_* value.
''' </summary>
''' <remarks></remarks>
Private Shared Function ReplaceBadKeys(ByVal Key As Keys) As Keys
Dim ReturnValue As Keys = Key
If ReturnValue.HasFlag(Keys.Control) Then
ReturnValue = (ReturnValue And Not Keys.Control) Or Keys.ControlKey 'Replace Keys.Control with Keys.ControlKey.
End If
If ReturnValue.HasFlag(Keys.Shift) Then
ReturnValue = (ReturnValue And Not Keys.Shift) Or Keys.ShiftKey 'Replace Keys.Shift with Keys.ShiftKey.
End If
If ReturnValue.HasFlag(Keys.Alt) Then
ReturnValue = (ReturnValue And Not Keys.Alt) Or Keys.Menu 'Replace Keys.Alt with Keys.Menu.
End If
Return ReturnValue
End Function
#End Region
#End Region
#Region "WinAPI P/Invokes"
<DllImport("user32.dll", SetLastError:=True)>
Private Shared Function SendInput(ByVal nInputs As UInteger, <MarshalAs(UnmanagedType.LPArray)> ByVal pInputs() As INPUT, ByVal cbSize As Integer) As UInteger
End Function
<DllImport("user32.dll")> _
Private Shared Function MapVirtualKeyEx(uCode As UInteger, uMapType As UInteger, dwhkl As IntPtr) As UInteger
End Function
<DllImport("user32.dll")> _
Private Shared Function GetKeyboardLayout(idThread As UInteger) As IntPtr
End Function
#Region "Enumerations"
Private Enum INPUTTYPE As UInteger
MOUSE = 0
KEYBOARD = 1
HARDWARE = 2
End Enum
<Flags()> _
Private Enum KEYEVENTF As UInteger
EXTENDEDKEY = &H1
KEYUP = &H2
SCANCODE = &H8
UNICODE = &H4
End Enum
#End Region
#Region "Structures"
<StructLayout(LayoutKind.Explicit)> _
Private Structure INPUTUNION
<FieldOffset(0)> Public mi As MOUSEINPUT
<FieldOffset(0)> Public ki As KEYBDINPUT
<FieldOffset(0)> Public hi As HARDWAREINPUT
End Structure
Private Structure INPUT
Public type As Integer
Public U As INPUTUNION
End Structure
Private Structure MOUSEINPUT
Public dx As Integer
Public dy As Integer
Public mouseData As Integer
Public dwFlags As Integer
Public time As Integer
Public dwExtraInfo As IntPtr
End Structure
Private Structure KEYBDINPUT
Public wVk As UShort
Public wScan As Short
Public dwFlags As UInteger
Public time As Integer
Public dwExtraInfo As IntPtr
End Structure
Private Structure HARDWAREINPUT
Public uMsg As Integer
Public wParamL As Short
Public wParamH As Short
End Structure
#End Region
#End Region
End Class
为了好玩,我还设法在MSDN上找到了扫描代码列表:https://msdn.microsoft.com/en-us/library/aa299374(v=vs.60).aspx
由于我自己是一个Doom粉丝而且熟悉它是如何工作的,也许你应该(根据你的旧问题)确保你在菜单中选择New Game
然后按Enter键? / p>
Zandronum知道菜单项的名称,所以你只需要给它第一个字母,它就会跳转到以它开头的项目:
InputHelper.PressKey(Keys.Escape, True) 'Open the menu.
System.Threading.Thread.Sleep(100) 'Small delay to let the menu open.
InputHelper.PressKey(Keys.N, True) 'Jump to the "New Game" menu item.
InputHelper.PressKey(Keys.Enter, True) 'Go into the "New Game" menu.
InputHelper.PressKey(Keys.Enter, True) 'Start a new game.
我在游戏中测试了上述代码,以全屏模式运行。像魅力一样。