3
\$\begingroup\$

I have a handheld Windows Embedded Compact Edition bluetooth device submimtting data to PC application that needs to type the information into a Windows 8.1 computer. The longest strings are about 128 characters long. In an ideal world I would get the device to just simulate a Bluetooth keyboard, but working out how to do that is beyond me for now.

As much as I consider it a crime to use SendKeys, I have had to use SendKeys.SendWait to transmit the data to keyboard. I was unable to use SendKeys.Send as this program runs in system notification area, and I get error message:

SendKeys cannot run inside this application because the application is not handling Windows messages.
Either change the application to handle messages, or use the SendKeys.SendWait method.

Unfortunately for long strings SendKeys.SendWait is quite slow, you can see the characters being typed out gradually.

It is a requirement to simulate the keyboard input faster.

It is OK if we destroy user clipboard in this scenario; as these are special purpose machines not used for any other task. In my testing I found clipboard much faster. Unfortunately sometimes the data sends special keyboard controls (i.e. Ctrl/Alt/F2 etc) which do not work via Clipboard.

So what I currently do is send bulk of data via clipboard, and separately send any special characters via SendKeys, typically there is only 4 of these special characters at most per data sent.

In my testing this is improving speeds from ~200 ms to type the text, to ~50ms, a noticeable improvement.

Unfortunately the code got way more complex and ugly.

While this works, I am concerned I may not have thought of all the scenarios that will break it.

Some points:

The following must be sent by SendKeys:

  • Special keypress is +, ^, % followed by either one character or {string of characters}
  • Special keypress can also be defined as {}

Anything else is being sent via Clipboard.

  • There is never any escaped { or } being sent

Code example:

private static void QuickSend(string message)
        {
            // these "SPECIAL" characters can be sent via clipboard, so replace them with clipboard version
            message = message.Replace("{ENTER}", "\r\n");
            message = message.Replace("{TAB}", "\t");

            // stores messages in order to be sent to keyboard
            List<string> messages = new List<string>();

            // these characters are special keypresses that can't be sent via clipboard
            char[] specialChars = { '+', '^', 
'%', 

'~', '{' };
            for (int i = 0; i < message.Length; i++)
            {
                int nextSpecialCharIndex = message.IndexOfAny(specialChars, i);

                // no more special characters, use remaining string as is
                if (nextSpecialCharIndex == -1)
                {
                    messages.Add(message.Substring(i));
                    i = message.Length;
                }
                else
                {
                    // if the current character is not the special character
                    // add text up until this special character occurs
                    if (nextSpecialCharIndex != i)
                    {
                        messages.Add(message.Substring(i, nextSpecialCharIndex - i));
                    }

                    // check we are not end of string
                    if (nextSpecialCharIndex < message.Length - 1)
                    {
                        // if special character is brace, send text from current brace to end brace
                        if (message.Substring(nextSpecialCharIndex, 1) == "{")
                        {
                            int endBraceIndex = message.IndexOf('}', i);
                            if (endBraceIndex == -1)
                            {
                                // no end brace - just send remaining text
                                messages.Add(message.Substring(nextSpecialCharIndex));
                                i = message.Length;
                            }
                            else
                            {
                                messages.Add(message.Substring(nextSpecialCharIndex, endBraceIndex - nextSpecialCharIndex + 1));
                                i = endBraceIndex;
                            }
                        }
                        else
                        {
                            // if string following special character is a brace, include that text as welll
                            if (message.Substring(nextSpecialCharIndex + 1, 1) == "{")
                            {
                                int endBraceIndex = message.IndexOf('}', i);
                                if (endBraceIndex == -1)
                                {
                                    messages.Add(message.Substring(nextSpecialCharIndex));
                                    i = message.Length;
                                }
                                else
                                {
                                    messages.Add(message.Substring(nextSpecialCharIndex, endBraceIndex - nextSpecialCharIndex + 1));
                                    i = endBraceIndex;
                                }

                            }
                            else
                            {
                                // Just submimt special character + next character
                                messages.Add(message.Substring(nextSpecialCharIndex, 2));
                                i = nextSpecialCharIndex + 1;
                            }
                        }
                    }
                }
            }
        #if DEBUG
            StringBuilder checkResult = new StringBuilder();
        #endif

            // need STA thread for Clipboard
            Thread thread = new Thread(() => {
                    for (int i = 0; i < messages.Count; i++)
                    {
        #if DEBUG
                    Trace.WriteLine($"{i}: '{messages[i]}'");
                    checkResult.Append(messages[i]);
        #endif

                        // check if message has special character
                        if (messages[i].IndexOfAny(specialChars) == -1)
                        {
                            // no...send via "FAST" clipboard method
                            try
                            {
                                Clipboard.SetText(messages[i]);
                                SendKeys.SendWait("^V");
                            }
                            catch (Exception ex)
                            {
        #if DEBUG
                                Trace.WriteLine("Clipboard Failure {ex.Message}");
        #endif
                                // clipboard failure, revert to SendKeys...
                                SendKeys.SendWait(messages[i]);
                            }
                        }
                        else
                        {
                            // yes...send via SENDKEYS
                            SendKeys.SendWait(messages[i]);
                        }
                    }
                });

                thread.SetApartmentState(ApartmentState.STA);
                thread.Start();
                thread.Join();

        #if DEBUG
            Trace.WriteLine($"Original: {message}");
            Trace.WriteLine($"CHECK   : {checkResult.ToString()}");
            Debug.Assert(checkResult.ToString() == message, "Messages do not match!");
        #endif

        }
\$\endgroup\$
  • \$\begingroup\$ You say you can't use SendKeys, but you're using it all over the place. Parts of your question are unclear. Could you try to clarify? \$\endgroup\$ – Mast Sep 19 '17 at 14:17
  • \$\begingroup\$ I can't use SendKeys.Send, SendKeys.SendWait works. I'd prefer not to rely on SendKeys at all due to performance/reliability concerns, but I think it's only option right now to send keyboard input for me. \$\endgroup\$ – Malcolm McCaffery Sep 19 '17 at 14:19
  • \$\begingroup\$ Right. So the current code works, but you want to get rid of the entire SendKeys dependency. Excellent premise, I'm sure we can work with that :-) \$\endgroup\$ – Mast Sep 19 '17 at 14:24
  • \$\begingroup\$ Yes, if it's possible. Or at least simplify/improve the performance. Currently I am using the hyprid copy/paste+SendKeys.SendWait to improve performance vs SendKeys.SendWait on its own. Looking at this code I feel there are so many potential unexpected places for this to go wrong ;) \$\endgroup\$ – Malcolm McCaffery Sep 19 '17 at 14:28
  • 1
    \$\begingroup\$ I'm not familiar with this construct: message = message.sak;jmas;kjdReplace("{ENTER}x", "\r\n");, is there a typo in your code? \$\endgroup\$ – Mast Sep 19 '17 at 14:39
3
\$\begingroup\$

Conditional code in Debug

There is an alternative for using this pattern:

#if DEBUG
    Trace.WriteLine($"Clipboard Failure {ex.Message}");
#endif

Use Debug.WriteLine instead. Much of you code could be simplified replacing Trace.WriteLine with Debug.WriteLine so you don't require the preprocessor directive #if DEBUG.

Debug.WriteLine($"Clipboard Failure {ex.Message}");
\$\endgroup\$
  • \$\begingroup\$ I think it also worth mentioning that Trace.WriteLine is decorated with [System.Diagnostics.Conditional("TRACE")] so it depends on what kind of output is required but I admit, wrapping TRACE by DEBUG is definitely wrong. \$\endgroup\$ – t3chb0t Oct 3 at 17:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.