Chapter 28. Asynchronous Input Device Polling

 

Why do we never have time to do it right, but always have time to do it over?

 
 --Anonymous

The Principle of Feedback (Chapter 7, “Fundamentals of User Interface Design“) is perhaps the most important concept of application design. This principle entails visual cues that easily describe the state of the application, but this principle also covers responsiveness of the user interface and cues related to waiting periods.

The standard Microsoft Windows message pump typically works well for most applications, but applications that update their state each time the mouse is moved can suffer from performance penalties. This is usually felt by applications that employ the use of 3D real-time graphics technologies like Direct3D.

The Microsoft DirectX library has a technology called DirectInput that communicates directly with device hardware drivers, completely avoiding the Microsoft Windows message pump. DirectInput offers considerable performance boosts over the message pump, and also supports asynchronous polling and data buffering.

This chapter focuses on using DirectInput to asynchronously read the mouse and keyboard devices. The mouse positions are expressed as deltas (difference) between the current and last positions. This chapter also shows how to read the mouse buttons. This is a useful technique for using the mouse to control a 3D camera. You will also learn how to check whether a key is depressed on the keyboard.

Asynchronous Mouse Polling

The first step is to include and reference the appropriate namespaces. You should add a reference to Microsoft.DirectX and Microsoft.DirectX.DirectInput and use the following namespaces.

using System;
using System.Threading;
using Microsoft.DirectX;
using Microsoft.DirectX.DirectInput;

The following class encapsulates all the nitty gritty details of using DirectInput for mouse polling. After instantiation, the Initialize method is executed to create the mouse input device and to spawn the asynchronous polling thread.

public class AsynchronousMouse : IDisposable
{

The Device object is used to manage Microsoft DirectInput devices and associated properties, specify behavior, manage force-feedback effects, interact with the device’s control panel, and perform device initialization.

    private Device _device = null;

    private Thread _threadData = null;
    private AutoResetEvent _eventTrigger = null;

    private byte[] _buttons;
    private int _x;
    private int _y;
    private int _z;

    private System.Windows.Forms.Form _context;

    public delegate void MovementDelegate(int x,
                                          int y,
                                          int z,
                                          bool left,
                                          bool middle,
                                          bool right);

    private delegate void PollTriggerDelegate();

    public event MovementDelegate MouseMovement;
    public byte[] Button
    {
        get { return _buttons; }
    }

    public int X
    {
        get { return _x; }
    }

    public int Y
    {
        get { return _y; }
    }

    public int Z
    {
        get { return _z; }
    }

The initialization method instantiates the device, sets the cooperative level, and sets the notification event that is used to control the polling thread.

The following code shows the initialization method.

    public bool Initialize(System.Windows.Forms.Form context)
    {
        _context = context;

The next two lines create a new thread that asynchronously polls the mouse device for state changes and dispatches them back to the user.

        _threadData = new Thread(new ThreadStart(this.AsynchronousPolling));
        _threadData.Start();

The _eventTrigger is used by DirectInput to notify threads when the mouse state changes. We use this event to control the asynchronous polling thread.

        _eventTrigger = new AutoResetEvent(false);

        try
        {

SystemGuid contains constant identifiers for system devices for use with DirectInput. SystemGuid.Mouse is associated with a mouse that has up to four buttons, or another device that is behaving like a mouse.

            _device = new Device(SystemGuid.Mouse);
            _device.SetDataFormat(DeviceDataFormat.Mouse);
        }
        catch (InputException)
        {
            return false;
        }

You must also specify the cooperative level for DirectInput devices. The different flags are described in Table 28.1.

Table 28.1. DirectInput Device Cooperative Level Flags

Flag

Description

Exclusive

This flag means that we want priority for control of the device.

NonExclusive

This flag means that we do not need priority for control of the device.

Foreground

This flag means that we only want data from the device if the window passed into the SetCooperativeLevel method has focus.

Background

This flag means that we always want data from the device.

NoWindowsKey

This flag is used to ignore the Windows logo key, and is generally used in full screen mode to prevent interruptions.

We always want data and we do not want to stall other applications using the input devices, so we use the NonExclusive | Background level.

        _device.SetCooperativeLevel(_context,
                                    CooperativeLevelFlags.NonExclusive |
                                    CooperativeLevelFlags.Background);
        _device.SetEventNotification(_eventTrigger);
        Acquire();
        return true;
    }

The polling thread runs in a loop, and each cycle is executed when the mouse is moved. A cycle polls the mouse device to update the cached state information, and then the callback method is fired. The callback method is registered by the consumer of the component, and can be used to update the user interface or calculate a 3D camera, for example.

    private void AsynchronousPolling()
    {
        while (_context.Created)
        {

The AutoResetEvent.WaitOne() method is used to pause thread execution until it receives a notification event (in this case the mouse state changing).

            _eventTrigger.WaitOne(-1, false);
            try
            {
                if (_device == null)
                {
                    continue;
                }

The next line retrieves the current mouse state.

                _device.Poll();
            }
            catch (InputException)
            {
                continue;
            }

The next two lines are used to asynchronously execute the trigger method that routes mouse state information back to the client through a registered callback.

            if (_context.Created && !_context.Disposing)
                _context.BeginInvoke(new PollTriggerDelegate(PollTrigger));
        }
    }

The following code shows the trigger logic that gets the current mouse information and sends it to the user event.

    private void PollTrigger()
    {
        if (MouseMovement != null)
        {
            MouseState stateData = _device.CurrentMouseState;

            _buttons = stateData.GetMouseButtons();
            bool left = (_buttons[0] != 0);
            bool right = (_buttons[1] != 0);
            bool middle = (_buttons[2] != 0);

            _x = stateData.X;
            _y = stateData.Y;
            _z = stateData.Z;

            MouseMovement(stateData.X,
                          stateData.Y,
                          stateData.Z,
                          left,
                          middle,
                          right);
        }
    }

The following method is used to gain access to the mouse device. This is a required step before data can be read from the device.

    public void Acquire()
    {
        if (_device != null)
        {
            try
            {
                 _device.Acquire();
            }
            catch
            {
            }
        }
    }

The following methods are used to properly dispose of the device and trigger objects using the IDisposable pattern.

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_eventTrigger != null)
                _eventTrigger.Set();

            if (_device != null)
            {
                _device.Unacquire();
                _device.Dispose();
                _device = null;
            }

            _eventTrigger = null;
        }
    }
}

Asynchronous Keyboard Polling

Just as with the mouse class, you need to include and reference the appropriate namespaces. You should add a reference to Microsoft.DirectX and Microsoft.DirectX.DirectInput and use the following namespaces.

using System;
using System.Threading;
using Microsoft.DirectX;
using Microsoft.DirectX.DirectInput;

The following class encapsulates all the nitty gritty details of using DirectInput for mouse polling. The code is almost identical to the AsynchronousMouse class, so only the new sections will be discussed.

public class AsynchronousKeyboard : IDisposable
{
    private Device _device = null;
    private Thread _threadData = null;
    private AutoResetEvent _eventTrigger = null;

    private System.Windows.Forms.Form _context;

    private bool _acquired = false;

    private delegate void PollTriggerDelegate();
    public delegate void ActionDelegate();
    public event ActionDelegate KeyboardAction;

    private KeyboardState _keyboardState;

    public bool Initialize(System.Windows.Forms.Form context)
    {
        _context = context;

        _threadData = new Thread(new ThreadStart(this.AsynchronousPolling));
        _threadData.Start();

        _eventTrigger = new AutoResetEvent(false);

        try
        {
            _device = new Device(SystemGuid.Keyboard);
            _device.SetDataFormat(DeviceDataFormat.Keyboard);
        }
        catch (InputException)
        {
            return false;
        }

        _device.SetCooperativeLevel(_context,
                                    CooperativeLevelFlags.NonExclusive |
                                    CooperativeLevelFlags.Background);
        _device.SetEventNotification(_eventTrigger);

       Acquire();

       return true;
    }

The following method is used to query the keyboard state to determine whether a particular key is depressed. The keyboard state is cached each time the state changes within the asynchronous polling thread.

    public bool KeyDown(Key key)
    {
        if (_keyboardState != null && _keyboardState[key])
        {
           return true;
        }

        return false;
    }
    private void AsynchronousPolling()
    {
        while (_context.Created)
        {
            _eventTrigger.WaitOne(-1, false);

            try
            {
                if (_device == null)
                {
                    continue;
                }

                Acquire();

                if (_acquired)
                {

Retrieve and cache the current keyboard state so that the KeyDown method can use it.

                    _keyboardState = _device.GetCurrentKeyboardState();
                }
            }
            catch (InputException)
            {
                continue;
            }

            if (_context.Created && !_context.Disposing)
                _context.BeginInvoke(new PollTriggerDelegate(PollTrigger));
            }
       }

       private void PollTrigger()
       {
           if (KeyboardAction != null)
               KeyboardAction();
       }
       public void Acquire()
       {
           if (_device != null)
           {
               try
               {
                    if (!_acquired)
                    {
                         _device.Acquire();
                         _acquired = true;
                    }
               }
               catch
               {
                   _acquired = false;
               }
           }
       }

       public void Dispose()
       {
           Dispose(true);
           GC.SuppressFinalize(this);
       }

       protected virtual void Dispose(bool disposing)
       {
           if (disposing)
           {
               if (_eventTrigger != null)
                   _eventTrigger.Set();
               if (_device != null)
               {
                   _device.Unacquire();
                   _device.Dispose();
                   _device = null;
               }

                   _eventTrigger = null;
               }
         }
   }

Sample Usage

Using the two classes in this chapter is very easy. The following code is from the example for this chapter on the Companion Web site. A callback is used to report keyboard state changes, although AsynchronousKeyboard.KeyDown() can be called at any time outside of the callback. The Buttons, X, Y, Z properties of the AsynchronousMouse class can also be called outside of the callback too.

public partial class MainForm : Form
{
    private AsynchronousMouse _mouse;
    private AsynchronousKeyboard _keyboard;

    public MainForm()
    {
        InitializeComponent();

        _mouse = new AsynchronousMouse();
        _mouse.MouseMovement +=
                   new AsynchronousMouse.MovementDelegate(MouseMovementCallback);

        _keyboard = new AsynchronousKeyboard();
        _keyboard.KeyboardAction +=
                  new AsynchronousKeyboard.ActionDelegate(KeyboardActionCallback);
    }

    void KeyboardActionCallback()
    {
        if (_keyboard.KeyDown(Microsoft.DirectX.DirectInput.Key.UpArrow))
        {
            UpArrowState.Text = "Down";
        }
        else
        {
            UpArrowState.Text = "Up";
        }

        if (_keyboard.KeyDown(Microsoft.DirectX.DirectInput.Key.DownArrow))
        {
            DownArrowState.Text = "Down";
        }
        else
        {
            DownArrowState.Text = "Up";
        }

        if (_keyboard.KeyDown(Microsoft.DirectX.DirectInput.Key.RightArrow))
        {
            RightArrowState.Text = "Down";
        }
        else
        {
            RightArrowState.Text = "Up";
        }

        if (_keyboard.KeyDown(Microsoft.DirectX.DirectInput.Key.LeftArrow))
        {
            LeftArrowState.Text = "Down";
        }
        else
        {
            LeftArrowState.Text = "Up";
        }

        if (_keyboard.KeyDown(Microsoft.DirectX.DirectInput.Key.A))
        {
            AState.Text = "Down";
        }
        else
        {
            AState.Text = "Up";
        }

        if (_keyboard.KeyDown(Microsoft.DirectX.DirectInput.Key.D))
        {
            DState.Text = "Down";
        }
        else
        {
            DState.Text = "Up";
        }
    }

    void MouseMovementCallback(int x,
                               int y,
                               int z,
                               bool left,
                               bool middle,
                               bool right)
    {
        if (left)
        {
            LeftButtonState.BackColor = Color.LimeGreen;
        }
        else
        {
            LeftButtonState.BackColor = Color.Maroon;
        }

        if (middle)
        {
            MiddleButtonState.BackColor = Color.LimeGreen;
        }
        else
        {
            MiddleButtonState.BackColor = Color.Maroon;
        }

        if (right)
        {
            RightButtonState.BackColor = Color.LimeGreen;
        }
        else
        {
            RightButtonState.BackColor = Color.Maroon;
        }

        ListViewItem listItem = new ListViewItem();

        listItem.Text = x.ToString();
        listItem.SubItems.Add(y.ToString());
        listItem.SubItems.Add(z.ToString());

        CoordinateList.Items.Add(listItem);

        Coordinates.Text = String.Format("Coordinates ({0}, {1}, {2})",
                                         x,
                                         y,
                                         z);
    }

    private void MainForm_Load(object sender, EventArgs e)
    {
        if (!_mouse.Initialize(this))
        {
            MessageBox.Show("Error initializing asynchronous mouse. Exiting.");
            Application.Exit();
        }

        if (!_keyboard.Initialize(this))
        {
            MessageBox.Show("Error initializing async device. Exiting.");
            Application.Exit();
        }
    }

    private void MainForm_Activated(object sender, EventArgs e)
    {
        if (_mouse != null)
        {
            _mouse.Acquire();
        }

        if (_keyboard != null)
        {
            _keyboard.Acquire();
        }
    }
}

Conclusion

This chapter briefly discussed DirectInput and some advantages of using DirectInput over the standard Microsoft Windows message pump. A solution was later presented that shows how to read input device data asynchronously from the mouse or keyboard. This technique is very useful for graphic-intensive programs where smooth input is required.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.212.212