For all the VB people out there...

| | Comments (13)

Unfortunately, I get asked rather frequently how to port an application which uses SendKeys. This horrible little hack is used to emulate keyboard input in an application (I've usually seen it used to do horrible things like dismiss dialogs automatically).

Well, RB doesn't have the API. However, there is still hope. I think this function should behave the same as the VB SendKeys function. You see, I've started a new section to the Windows Functionality Suite -- VB compatibility functions. I'm sure they'll be rolled into the VB Project Converter some day, but I figured this would be a fun little start. So you should see this addition in the next WFS release. But since I have no idea when I'll get around to releasing it, I'm giving the function to everyone here.

What I need from you is to point out the nuances I've missed. I followed the VB spec to the letter, but there's almost always hidden "gotchas" with this sort of thing. So if you find any, feel free to let me know (or even fix them for me!).

All of the truly fun code is at the bottom of the code listing. It's where I wrote a very simple parser to loop over the keys and do something with them. I put all of this into a module called VB, so if you're following along at home, you may want to do the same.

[rbcode]Private Function ASCIIToVirtualKey(char as String) As Integer
#if TargetWin32
Declare Function VkKeyScanA Lib "User32" ( ch as Integer ) as Short

dim theAscVal as Integer = Asc( char )
dim scan as Integer = VkKeyScanA( theAscVal )
return Bitwise.BitAnd( scan, &hFF )
#endif
End Function

Private Sub FillKeyMap(ByRef map as Dictionary)
map.Value( "BACKSPACE" ) = &h8
map.Value( "BS" ) = &h8
map.Value( "BKSP" ) = &h8
map.Value( "BREAK" ) = &h3
map.Value( "CAPSLOCK" ) = &h14
map.Value( "DELETE" ) = &h2E
map.Value( "DEL" ) = &h2E
map.Value( "DOWN" ) = &h28
map.Value( "END" ) = &h23
map.Value( "ENTER" ) = &h0D
map.Value( "ESC" ) = &h1B
map.Value( "HELP" ) = &h2F
map.Value( "HOME" ) = &h24
map.Value( "INSERT" ) = &h2D
map.Value( "INS" ) = &h2D
map.Value( "LEFT" ) = &h25
map.Value( "NUMLOCK" ) = &h90
map.Value( "PGDN" ) = &h22
map.Value( "PGUP" ) = &h21
map.Value( "PRTSC" ) = &h2C
map.Value( "RIGHT" ) = &h27
map.Value( "SCROLLLOCK" ) = &h91
map.Value( "TAB" ) = &h09
map.Value( "UP" ) = &h26

map.Value( "+" ) = ASCIIToVirtualKey( "+" )
map.Value( "^" ) = ASCIIToVirtualKey( "^" )
map.Value( "%" ) = ASCIIToVirtualKey( "%" )
map.Value( "~" ) = ASCIIToVirtualKey( "~" )
map.Value( "(" ) = ASCIIToVirtualKey( "(" )
map.Value( ")" ) = ASCIIToVirtualKey( ")" )
map.Value( "{" ) = ASCIIToVirtualKey( "{" )
map.Value( "}" ) = ASCIIToVirtualKey( "}" )
map.Value( "[" ) = ASCIIToVirtualKey( "[" )
map.Value( "]" ) = ASCIIToVirtualKey( "]" )

for i as Integer = 1 to 16
map.Value( "F" + Str( i ) ) = &h70 + (i - 1)
next i

End Sub

Private Sub KeyDown(virtualKeyCode as Integer, extendedKey as Boolean = false)
#if TargetWin32
Declare Sub keybd_event Lib "User32" ( keyCode as Integer, scanCode as Integer, _
flags as Integer, extraData as Integer )

dim flags as Integer
Const KEYEVENTF_EXTENDEDKEY = &h1
if extendedKey then
flags = KEYEVENTF_EXTENDEDKEY
end

' Press the key
keybd_event( virtualKeyCode, 0, flags, 0 )
#endif
End Sub

Private Sub KeyUp(virtualKeyCode as Integer, extendedKey as Boolean = false)
#if TargetWin32
Declare Sub keybd_event Lib "User32" ( keyCode as Integer, scanCode as Integer, _
flags as Integer, extraData as Integer )

dim flags as Integer
Const KEYEVENTF_EXTENDEDKEY = &h1
if extendedKey then
flags = KEYEVENTF_EXTENDEDKEY
end

Const KEYEVENTF_KEYUP = &h2
flags = BitwiseOr( flags, KEYEVENTF_KEYUP )
keybd_event( virtualKeyCode, 0, flags, 0 )
#endif
End Sub

Protected Sub SendKeys(keys as String)
// We want to initialize all of our virtual keys. Some of them
// are going to be constants, others will be figured out while
// we parse, and still others will reside in a map.
Const VK_SHIFT = &h10
Const VK_CONTROL = &h11
Const VK_MENU = &h12
Const VK_ENTER = &h0D

Static sMap as Dictionary

if sMap = nil then
// Create our map
sMap = new Dictionary

// Fill the map out (note that this is ByRef)
FillKeyMap( sMap )
end if

// We have to write a very simple parser to parse
// the keys string that's passed in.

// Split the entire string into a set of tokens. Each token
// is a single character.
dim chars( -1 ) as String = Split( keys, "" )

// This holds the current virtual key (so we don't have to
// process the ASCII character multiple times).
dim virtKey as Integer

// This holds the current set of depressed modifier keys.
// As we press the keys, we should be adding to this list,
// and when we need to release the keys, we know which
// ones to generate a key up for.
dim modifierList( -1 ) as Integer

// Sometimes we want to hold the modifiers for a while, for
// instance, if the user enters +(EC), then we want to hold
// the shift key for E and C. But other times, we only want the
// modifier held for one key press, such as +EC, in which case
// there would be a Shift+E, and then a C.
dim holdModifiers as Boolean

// We may need to do some special processing for things inside
// of a {} tag, such as {TAB} or {LEFT 42}
dim specialProcessing as Boolean
dim specialProcessingStr as String

// Loop over every token and do the appropriate
// action.
for each token as String in chars
// If we're doing special processing, then we
// should be doing that instead of the normal
// processing.
if specialProcessing then
// Check to see whether we have a { or
// not. If we have one, and we've already
// processed at least one character, then we
// are done with our special processing. The
// reason we check to see whether we have a
// character or not is because of the string {}},
// which should print a } character.
if token = "}" and Len( specialProcessingStr ) > 0 then
specialProcessing = false
else
// Add the character to our processing string
specialProcessingStr = specialProcessingStr + token
end if
end if

// If we're still doing special processing, then we want
// to continue doing it. But since the state may have changed
// we need to check again
if specialProcessing then continue

select case token
case "("
// We're starting a token group. The group
// should have the current modifiers applied
// to it.
holdModifiers = true

case ")"
// If we're not holding modifies, then
// that means we've never gotten a ( and
// something is wrong
if not holdModifiers then return

// We're ending a token group. This means
// that we can clear the modifiers.
for each modifier as Integer in modifierList
// Release the key
KeyUp( modifier )
next modifier

// Now clear our list
Redim modifierList( -1 )

// We're no longer holding the modifiers
holdModifiers = false

case "{"
// We have a special token to parse, such as
// {TAB} or {F1}. We should search until we
// get to } and figure out what key to press
// from there.
//
// This could also be a repeat modifier that is
// in the form {key number}. If we find a space
// while parsing, then we know we have this form.

// So note that we need to parse until we hit the }.
// Once we have that, we can do the processing.
specialProcessing = true

case "}"
// Our special processing is done now, so we should
// check the data we have in the string. It could be
// in the form "key number", or it could just be "key".

// First, get the key from our map. If we don't have
// the key in the map, then we should try a regular
// ASCII key.
dim firstField as String = NthField( specialProcessingStr, " ", 1 )
dim keyToken as Integer = sMap.Lookup( firstField, -1 )
if keyToken = -1 then
keyToken = ASCIIToVirtualKey( firstField )
end if

// If we're still in a bad state, then we should
// bail out.
if keyToken = 0 then return

// We want our repeats to always be at least one, but
// possibly more, if that field exists.
dim numRepeats as Integer = Max( Val( NthField( specialProcessingStr, " ", 2 ) ), 1 )
for count as Integer = 1 to numRepeats
// Press the key and then release it
KeyDown( keyToken )
KeyUp( keyToken )
next count

// Clear out our special processing data
specialProcessingStr = ""
case "~"
// We want to send an enter key
KeyDown( VK_ENTER )
KeyUp( VK_ENTER )

case "+"
// The shift key modifier should be pressed
if modifierList.IndexOf( VK_SHIFT ) = -1 then
KeyDown( VK_SHIFT )
modifierList.Append( VK_SHIFT )
end if

case "^"
// The control key modifier should be pressed
if modifierList.IndexOf( VK_CONTROL ) = -1 then
KeyDown( VK_CONTROL )
modifierList.Append( VK_CONTROL )
end if

case "%"
// The Alt key modifier should be pressed
if modifierList.IndexOf( VK_MENU ) = -1 then
KeyDown( VK_MENU )
modifierList.Append( VK_MENU )
end if

else
// We have a regular key press, such as A or 4.
virtKey = ASCIIToVirtualKey( token )

// Press the key and then release it
KeyDown( virtKey )
KeyUp( virtKey )

// If we aren't holding modifiers, then we
// should release them all here.
if not holdModifiers then
for each modifier as Integer in modifierList
// Release the key
KeyUp( modifier )
next modifier

// Now clear our list
Redim modifierList( -1 )
end if
end select
next token
End Sub
[/rbcode]
To test this out, I put two EditFields onto a window, and a PushButton. The first EditField is for entering key strings, and the second is for showing the output. The PushButton sets the focus to the second editfield (so that it gets the key strokes), and then calls SendKeys.

I had some really fun test cases, such as: "a{ENTER}b~{c 5}{left 4}+(ab)" and "%fx" and all my tests worked wonderfully.

Enjoy!

13 Comments

[...] It’s been a while since I’ve done an RB Gem, so here’s one for you. In VB, there’s a built-in function called Shell which launches a command line process and then returns a process identifier for you to use. A lot of times this is used in conjunction with the AppActivate to bring the app to the front, and the SendKeys to modify the application. [...]

Will this work ok in 2005R4 ? For some reason my test application is halting
with a bug on: ‘ Press the key. Of course I may not have done this correctly at all, I tried to paste the above code into a module (which I also named VB), and to do a similar type of test project as the one described.

Hrm ok that didnt post correctly - please update to one post if possible :)
its the line in the section:

& # 8216; Press the key

Private Sub KeyDown(virtualKeyCode as Integer, extendedKey as Boolean = false)
#if TargetWin32
Declare Sub keybd_event Lib "User32" ( keyCode as Integer, scanCode as Integer, _
flags as Integer, extraData as Integer )

dim flags as Integer
Const KEYEVENTF_EXTENDEDKEY = &h1
if extendedKey then
flags = KEYEVENTF_EXTENDEDKEY
end
******
‘ Press the key
******
keybd_event( virtualKeyCode, 0, flags, 0 )
#endif
End Sub

The problem is that the HTML got munged into an entity that's not being displayed. Change the ‘ into a // and it should work fine.

Hrm I'm missing something, I deleted that line entirely, and typed in

& # 8216 (without the spaces)

It still but it still stops here .. ?

Yeah, RB doesn't grok HTML entities -- the problem stems from my RB-code-formatting code for my blog.

Just type:

// Press the key

and it should work fine for you.

omg sorry its only a comment ? Ahaa I thought it was some magic thing I wasnt aware of .. hrm I must not be ready for modules .. now when I try to run it comes back to the IDE with the cursor just sitting at the end of the

next token

line at the bottom .. ?

Not certain what the issue might be -- sounds like you aren't showing any UI so the app starts and quits.

It works ! I am definitely motived to check out WFS now - I wasn't pasting the code into the "VB" module correctly - for the uninitiated - you will end up with five "methods":
ASCIIToVirtualKey
FillKeyMap
KeyDown
KeyUp
and of course
SendKeys

=) !

This is a very useful module to have, Aaron ;)

Does it work with colons (":")? It seems to send semicolons (";") when I want to send colons.

Yes and no -- yes, it can send it, but it's a bug that it's behaving the way it is. The issue is that you need to send a shift key in order to get the :, otherwise it just does the non-shifted version of a :.

OK, thanks Aaron, I understand: I did a replaceall(astring,":","+;") to filter the string before I sent it to the SendKeys function.

I've got a fix done, but I'm not going to bother re-posting all of the code. The function will be included in the next WFS release, and anyone who needs it can email me.

The fix is to not mask off the data in ASCIIToVirtualKey, and then test the data returned to see what the state of the modifier keys is as appropriate.

Leave a comment

Disclaimer

I'm currently an employee of REAL Software. My blog is mine. The opinions represented in this blog are mine as well and may not represent my employer's opinions. All original material is copyrighted and property of the author.

REALbasic® is a registered trademark of REAL Software, Inc. REAL SQL Server™ and Lingua™ are pending trademarks of REAL Software, Inc. All rights reserved.