Search code examples
c#winformsqr-codebarcode-scannerimessagefilter

How to create a UTF8 string using WM_KEYDOWN key codes for barcode/QR code scanner


I'm having an issue decoding a string using WM_KEYDOWN messages intercepted by an Application MessageFilter. Customers using my production app can use any standard USB barcode scanner (e.g. this model) to search for items and regardless of make or model these devices send messages to a focused WinForms control (e.g. TextBox) that are identical to typing the same characters on a keyboard only faster than most humans can type.

For example, go into Notepad or WordPad or Word or any WinForms text box, scan this QR code and it just works. No code required because a "USB scanner" is just an input device that behaves like a keyboard.

qr code

The next release adds QR code support and now I'm having issues because the incoming strings are more complex.


Even if there is no currently focused text box to receive the key messages, my app still needs to detect a barcode scan and reliably perform the query. For this reason, I've installed a MessageFilter for the main form in order to intercept WM_KEYDOWN and append the key code to a Buffer class so that I can detect the rate of incoming keystrokes. The rate detection scheme works great but my problem is decoding the messages properly because the QR code renders in raw format.As a hack, I can feed those characters into a dummy control that exists solely for this purpose and it knows exactly what to do with that key sequence.

hack before-and-after

Of course, I'd like to understand what the text box is doing internally so I can construct the UTF8 string directly without having to resort to that. What's the secret?


Here's my code, which also measures the rate at which the key messages are happening in order to distinguish whether the input is coming from human or a scanner:

  • Typing "01234566789" echoes the string to the text box.
  • Scanning "0123456789" should make a popup message instead.

Another requirement is that upon detection, the scanned text will be removed from the focused control (if there is one).

public partial class BarcodeScannerForm : Form, IMessageFilter
{
    public BarcodeScannerForm()
    {
        InitializeComponent();
        // Add message filter to hook WM_KEYDOWN events.
        Application.AddMessageFilter(this);
        Disposed += (sender, e) => Application.RemoveMessageFilter(this);
    }
    const int WM_KEYDOWN = 0x0100;
    public bool PreFilterMessage(ref Message m)
    {
        if(m.Msg.Equals(WM_KEYDOWN)) detectScan((char)m.WParam);
        return false;
    }
    private void detectScan(char @char)
    {
        if(_keyCount == 0) _buffer.Clear();
        int charCountCapture = ++_keyCount;
        _buffer.Append(@char);
        Task
            .Delay(TimeSpan.FromSeconds(SECONDS_PER_CHARACTER_MIN_PERIOD))
            .GetAwaiter()
            .OnCompleted(() => 
            {
                if (charCountCapture.Equals(_keyCount))
                {
                    _keyCount = 0;
                    if(_buffer.Length > SCAN_MIN_LENGTH)
                    {
                        BeginInvoke(()=>MessageBox.Show(_buffer.Text));
                    }
                }
            });
    }
}

This is the version of Buffer that has the problem:

class Buffer
{
    public Buffer(Form owner) { }
    StringBuilder _sbScan = new StringBuilder();
    public void Append(char @char)
    {
        _sbScan.Append(@char);
    }
    public string Text => _sbScan.ToString();
    public int Length => _sbScan.Length;
    public void Clear() => _sbScan.Clear();
}

This is the hack version that gets the correct result by feeding characters through a TextBox, but doing this seems likely to have a negative impact on the rate detector.

class Buffer : TextBox
{
    public Buffer(Form owner)
    {
        // Must be visible and owned by main form to work
        Size = new Size();
        owner.Controls.Add(this);
    }
    public void Append(char @char) => Text.Append(@char);
    public int Length => TextLength;
}

Solution

  • Thank you Jimi for showing me the answer that eluded me by being too darned simple.


    Scan detector requirements:

    • Works for both barcodes and QR codes
    • Works whether or not there is a focused TextBox to receive incoming key events.

    SOLUTION

    Install a MessageFilter for the main form in order to intercept WM_CHAR and append the char code to StringBuilder class.

    public partial class BarcodeScannerForm : Form, IMessageFilter
    {        
        public BarcodeScannerForm()
        {
            InitializeComponent();
            // Add message filter to hook WM_KEYDOWN events.
            Application.AddMessageFilter(this);
            Disposed += (sender, e) => Application.RemoveMessageFilter(this);
        }
        private readonly StringBuilder _buffer = new StringBuilder();
        const int WM_CHAR = 0x0102;
        public bool PreFilterMessage(ref Message m)
        {
            // SOLUTION DO THIS (Thanks Jimi!)
            if (m.Msg.Equals(WM_CHAR)) detectScan((char)m.WParam);
            // NOT THIS
            // if(m.Msg.Equals(WM_KEYDOWN)) detectScan((char)m.WParam);
            return false;
        }
    

    The detector algorithm appends each new character to the buffer and restarts a 100 ms watchdog timer. If the WDT expires without a new keystroke, it checks to see how many characters have been received. If the count is > 8 it's considered a scan.

        private void detectScan(char @char)
        {
            Debug.WriteLine(@char);
            if(_keyCount == 0) _buffer.Clear();
            int charCountCapture = ++_keyCount;
            _buffer.Append(@char);
            Task
                .Delay(TimeSpan.FromSeconds(SECONDS_PER_CHARACTER_MIN_PERIOD))
                .GetAwaiter()
                .OnCompleted(() => 
                {
                    if (charCountCapture.Equals(_keyCount))
                    {
                        _keyCount = 0;
                        if(_buffer.Length > SCAN_MIN_LENGTH)
                        {
                            BeginInvoke(()=>MessageBox.Show(_buffer.Text));
                        }
                    }
                });
        }
        int _keyCount = 0;
        const int SCAN_MIN_LENGTH = 8;
        const double SECONDS_PER_CHARACTER_MIN_PERIOD = 0.1;
    }