Working with User Input Devices in the Windows Runtime
In earlier chapters, you saw that although the built-in controls you can use in your Windows 8.1 apps include extensive support for touch-based interactions, input from mouse and keyboard input devices continues to be fully supported. The Windows Runtime also features extensive support for gathering information from other inputs, including sensors. The information these sensors provide includes details about a device’s location, as well as knowledge about its position and motion within its immediate environment. Having the capability to incorporate this information into your apps means you can consider giving your users new kinds of interactivity and immersion.
In this chapter, you see how the WinRT APIs provide a common model for working with the various kinds of input pointer devices. This model provides a range of access, allowing you not only to obtain information about raw pointer events, but also to work with higher-level abstract gestures, depending on the needs of your app. You also see how you can access keyboard events from your code and obtain information about the user’s key presses.
In addition, you learn about the WinRT APIs for working with location information, including the capability to set up geographic fences that can result in automatic notifications to your app when your device crosses a fence boundary. Furthermore, you learn how to work with the WinRT APIs that provide access to sensors that can give you information about your device’s interactions with the physical world around it, including details about its orientation, its heading, the rate and direction of its motion, and even the amount of light currently shining on it.
Working with Input Devices
In Chapter 2, “Windows Store Apps and WinRT Components,” you saw how the built-in controls that the Windows Runtime provides are designed to support first-class interactions through touch, as well as keyboard and mouse combinations. Although access to touch input is becoming more common in modern computers and devices, it is not yet available everywhere. Attached keyboards, mouse devices, and pens continue to be important tools for application interaction, not only when touch input is unavailable, but also in addition to touch input when certain interactions are simply easier and more natural using these other input mechanisms.
For touch, mouse, and pen inputs, the Windows Runtime API provides several different kinds of methods and events for working with these devices and responding to user interaction with them. In addition to the APIs for working with these devices, a set of methods and events are available for responding to user interactions with their keyboards.
The Example App
The InputsExample project illustrates several kinds of input device API integration that you can add to your apps. The app enables the user to add shapes to the application canvas, which are then animated to move around the canvas area. The app also detects what input devices are available and shows information about these connected devices, and it provides options for configuring what device types the app will listen to for input and which of the screen or keyboard events the app will respond to. Shapes can be added through buttons provided on the user interface or by pressing predefined keyboard buttons. The shapes themselves are configured to respond in several ways to interaction with pointer input devices. When a pointer intersects the edge of a shape, the shape is highlighted and stops moving. The shapes can also be manipulated to change position, degree of rotation, and size, with or without inertia. Finally, the shapes respond to gestures by changing color when tapped, changing direction when double-tapped, and resetting to their initial size, color, and rotation when they are held or right-clicked.
Identifying Connected Input Devices
You can determine which touch input devices are connected and what their capabilities are in a couple ways. One approach is to use the information that the PointerDevice class provides to obtain detailed information about available touch, mouse, or pen devices. Alternatively, higher-level classes can garner more general information about the current mouse and touch capabilities.
The PointerDevice class can obtain detailed information about one or more connected pointer devices. It provides a static GetPointerDevices method that returns a list of available devices as PointerDevice object instances, as well as a static GetPointerDevice method that can retrieve a specific device based on a pointer ID value (the “Pointer Events” section, later in this chapter, explains how to obtain a pointer ID). Properties of particular interest that the PointerDevice type exposes include the PointerDeviceType, which shows whether the device is a Mouse, Touch, or Pen device, and the IsIntegrated flag, to indicate whether the device is considered to be integrated into the current machine or has been connected externally. It also includes a SupportedUsages collection that lists Human Interface Device (HID) “usages” as PointerDeviceUsage objects. These usages are defined by Usage Page and Usage Id values that are part of the USB HID specification1 and expose value ranges that the pointer device supports.
Listing 13.1 shows how the example application uses device information to determine whether touch, mouse, or pen devices are available. A list of available devices is obtained depending on whether the list should include only integrated devices. The resulting values are then queried to see if any of the desired device types are present.
LISTING 13.1 Determining Device Availability
var devices = PointerDevice.GetPointerDevices(); if (PointerIntegratedDevicesOnly) { devices = devices.Where(x => x.IsIntegrated).ToList(); } IsTouchAvailable = devices.Any(x => x.PointerDeviceType == PointerDeviceType.Touch); IsMouseAvailable = devices.Any(x => x.PointerDeviceType == PointerDeviceType.Mouse); IsPenAvailable = devices.Any(x => x.PointerDeviceType == PointerDeviceType.Pen);
The MouseCapabilities and TouchCapabilities classes obtain higher-level system-wide information about the available mouse and touch device support. When an instance of one of these types is created, its properties provide access to information about the respective device availability.
For MouseCapabilities:
- The MousePresent property is set to a value of 1 if one or more mouse devices are currently available.
- The NumberOfButtons value indicates the highest value available for any given device.
- The VerticalWheelPresent or HorizontalWheelPresent properties is set to a value of 1 to indicate whether a device is connected that has each respective feature.
- The SwapButtons property is set to 1 if the mouse buttons have been swapped in the system settings.
For TouchCapabilities:
- The TouchPresent property returns a value of 1 if a touch digitizer is present.
- The Contacts property indicates the highest number of concurrent contacts that are supported.
The example application uses these values to populate the message boxes that display when the user clicks the Details buttons next to the check boxes that it provides to enable or disable mouse and touch input (see Listings 13.2 and 13.3).
LISTING 13.2 Displaying Mouse Capabilities
var capabilities = new MouseCapabilities(); String message; if (capabilities.MousePresent == 1) { var rawMessage = "There is a mouse present. " + "The connected mice have a max of {0} buttons. " + "There {1} a vertical wheel present. " + "There {2} a horizontal wheel present. " + "Mouse buttons {3} been swapped."; message = String.Format(rawMessage , capabilities.NumberOfButtons , capabilities.VerticalWheelPresent == 1 ? "is" : "is not" , capabilities.HorizontalWheelPresent == 1 ? "is" : "is not" , capabilities.SwapButtons == 1 ? "have" : "have not" ); } else { message = "There are no mice present."; } ShowMessage(message, "Mouse Properties");
LISTING 13.3 Displaying Touch Capabilities
var capabilities = new TouchCapabilities(); String message; if (capabilities.TouchPresent == 1) { var rawMessage = "Touch support is available. " + "Up to {0} touch points are supported."; message = String.Format(rawMessage, capabilities.Contacts); } else { message = "Touch support is not available."; } ShowMessage(message, "Touch Properties");
Pointer, Manipulation, and Gesture Events
Instead of having a separate set of input events for touch, mouse, and pen inputs, the Windows Runtime API combines input from these devices and provides several distinct tiers of events that can be raised in response to input from any of these devices. At the lowest tier are the pointer events, which are raised for each press, move, release, or other simple interaction. Next are the manipulation events, which track and consolidate actions from one or more pointers into higher-level events related to motion, scale, rotation, and inertia. Finally, the gesture events consolidate pointer actions into even higher-level gesture abstractions, such as tapping, double-tapping, and holding.
In the example application, all the support for working with input device pointer, manipulation, and gesture events has been consolidated into a single InputEventHandler class. This class handles the subscriptions to the desired events and provides the event handler implementations for these subscriptions.
Pointer Events
The Windows Runtime combines input from touch, mouse, or stylus devices into the abstract concept of a pointer. Each contact point from each device is represented by a unique pointer instance. For example, imagine an app running on a touch-enabled tablet that supports multiple touch points, and imagine that multiple fingers are pressing the screen simultaneously. In this case, each finger touching the screen is treated as a unique pointer. The same holds true if the touch actions include a combination of several fingers, as well as a click by a mouse or screen contact with a stylus. The mouse and/or stylus inputs are treated as additional unique pointers.
In Windows 8 XAML apps, the most common way to subscribe to pointer events is through events that individual UIElement objects expose. An alternative approach involves subscribing to similar events exposed by an ICoreWindow instance, which can be obtained through the Window.Current.CoreWindow property. This latter approach is primarily used by DirectX WinRT games when UIElement objects aren’t readily available. Table 13.1 summarizes the pointer events that are available when a UIElement is used.
TABLE 13.1 Pointer Events
Event |
Description |
PointerEntered |
A pointer has moved into the item’s bounding area. For mouse and stylus input, this does not require a press. For touch input, because there is no “hover” support, an actual touch is required; it results in an immediate subsequent PointerPressed event, unless cancelled in this event’s handler. |
PointerExited |
A pointer that was in an element’s bounding area has left that area. For touch input, this event immediately follows a PointerReleased event. |
PointerPressed |
A pointer has been pressed while within the bounding area for an item. Note that a PointerPressed is not always terminated by a PointerRelased event, but it can instead be ended by PointerCanceled or PointerCaptureLost events. |
PointerMoved |
A pointer that has entered an item’s bounding area is being moved within that area, or a pointer that has been captured by an item is moving, even if its position is beyond the item’s bounding area. |
PointerReleased |
A pointer that was pressed has been released, usually within an item’s bounding area. This occurs if the pointer was pressed while inside the item’s bounding area; a corresponding PointerPressed event then has been raised, or if the pointer was already pressed when it moved into the item’s bounding area, the PointerPressed event might have occurred elsewhere. If the pointer is currently captured by an item, this event can also be raised when the pointer is released outside the item’s boundary. |
PointerCanceled |
A pointer has lost contact with an item in an unexpected way. This event can fire instead of the PointerReleased event. Potential reasons for unexpected contact loss include changes in an app’s display size, the user logging off, or the depletion of available contact points. Note that this event is only part of the UIElement events, and the ICoreWindow interface does not provide or raise it. |
PointerCaptureLost |
A pointer capture that the event source item obtained has been released either programmatically or because a corresponding PointerPressed has been released. |
Several of the pointer events in Table 13.1 either are directly related to or have side effects that are related to the idea of a pointer being captured. When a pointer is captured, only the element that captured it receives any of the input events related to that pointer until the capture has been released. Typically, a pointer is captured within the handler for a PointerPressed event because a pointer must be pressed to be captured. To capture a pointer, the UIElement class includes a CapturePointer method that takes a Pointer class instance that identifies the pointer to capture. It just so happens that the PointerRoutedEventArgs that are passed to the UIElement pointer event handlers include this pointer object, as the following code illustrates:
private void HandlePointerPressed(Object sender, PointerRoutedEventArgs args) { _eventSourceElement.CapturePointer(args.Pointer); }
The Pointer object includes a PointerId, which is simply a unique integer that is assigned to the current pointer and identifies it throughout the various subsequent pointer events. It also includes a PointerDeviceType property that returns a value of the PointerDeviceType enumeration and indicates whether the current pointer is related to input from a touch device, a mouse device, or a pen device. In the example project, this value is used to ignore processing in the pointer events when a particular device type is deselected in the user interface.
if (!IsValidDevice(args.Pointer.PointerDeviceType)) return;
The Pointer object also includes a pair of flags to indicate the position of the pointer relative to the touch sensor. IsInContact indicates whether the device is actually contacting the sensor, such as whether a stylus is in direct contact with the screen when using a touchscreen tablet. In the case of a mouse device, this is true when one of its buttons is being pressed. IsInRange indicates whether the device is within detection range but not touching; it is primarily meant for pen devices because, unlike touch devices, they can usually be detected before they make physical contact. Generally, mouse devices always return True for this value, and touch devices return True only when a touch is actually occurring.
In addition to the Pointer object, the arguments passed to the pointer events include a KeyModifiers property that indicates whether one or more of the Control, Menu, Shift, or Windows special keyboard keys was pressed at the time of the event.
Finally, the event arguments include a pair of methods that obtain additional information about the input pointer associated with the current interaction. The GetCurrentPoint and GetIntermediatePoints methods both accept a UIElement to provide a frame of reference for any of the coordinate properties included in the method results. If this value is null, the coordinate values that are returned are relative to the app itself. Whereas GetCurrentPoint returns a single PointerPoint instance, the GetIntermediatePoints returns a collection of PointerPoint instances from the last pointer event through the current one. In addition to being able to obtain PointerPoint information from the pointer event arguments, the PointerPoint class itself includes static methods that accept a PointerId value and return the current or intermediate PointerPoint values, with coordinates relative to the app.
The PointerPoint class includes a lot of information about the current interaction. At the root, it includes the PointerId value, a Position value indicating the Point where the pointer event occurred, and a PointerDevice property that provides the same PointerDevice value discussed in the earlier section “Identifying Connected Input Devices.” It also includes a Properties value that provides access to significantly more detailed information. Among the properties provided, this value includes touch information, such as the contact rectangle value; mouse information, such as whether the left, middle, right, first extended, or second extended buttons are pressed; and pen information, including several values that describe the physical position of the pen, whether it is inverted, and the amount of pressure being applied to its tip. Furthermore, the HasUsage and GetUsage methods are useful in obtaining HID value information from the device for the current interaction. These are the same HID values that can be enumerated with the SupportedUsages method that PointerDevice class instances mentioned earlier provide. The following code shows how to request the amount of tip pressure (usageId value 0x30) applied to a digitizer stylus device (usagePage value 0x0D).
if (pointerDetails.Properties.HasUsage(0x0D, 0x30)) { pressure = pointerDetails.Properties.GetUsageValue(0x0D, 0x30); }
Although the amount of detail provided by the pointer events can harness a lot of power, the information provided is at a very low level. For most application needs, this information needs to be synthesized into more abstract concepts. Examples might include recognizing a pair of PointerPressed and PointerReleased events potentially as either a single tap or a hold action, depending on how much time elapses between the two pointer actions, or perhaps tracking multiple pointer actions to determine whether pinch or rotation actions are occurring. Fortunately, you will most likely not need to write and maintain the state-tracking code required to achieve this level of abstraction; these kinds of events are already calculated and provided for you in the form of the manipulation events and gesture events.
Manipulation Events
Manipulation events are the result of grouping and translating several pointer events associated to an item that originate from either one or several pointers. During a manipulation, changes to translation (position), scale (size), and rotation are computed, tracked, and made available via the event argument parameters provided by these events. A manipulation also tracks the velocities with which these changes are occurring and includes the capability to optionally calculate and apply inertia based on these velocities when the pointer events complete.
In Windows 8.1 XAML apps, the most common way you subscribe to manipulation events is through the events that individual UIElement objects expose. For a UIElement to generate manipulation events, the element needs to have its ManipulationMode property set to a value of the ManipulationModes enumeration other than None or System. The default value for most controls is System, and it enables the UIElement to process manipulations internally, whereas a value of None suppresses all manipulations. Other significant values include TranslateX and TranslateY to track movement on the x- and y-axis, Rotate to track rotation, and Scale to track stretching or pinching. Values for TranslateInertia, RotateInertia, and ScaleInertia are also available to indicate that these manipulations should trigger inertia calculations. Table 13.2 summarizes the manipulation events exposed by the UIElement class.
TABLE 13.2 Manipulation Events
Event |
Description |
ManipulationStarting |
A PointerPressed event has occurred, and manipulation processing starts looking for the pointer to move, to actually start tracking a manipulation. |
ManipulationStarted |
A pressed pointer has moved. This marks the beginning of the manipulation, which contains some number of ManipulationDelta events and is concluded with a ManipulationCompleted event. |
ManipulationDelta |
One or more of the pressed pointers have moved or inertia is being applied. |
ManipulationInertiaStarting |
The manipulation has been configured to support inertia, and the last pointer was released while the manipulation still had a velocity. ManipulationDelta events are raised until velocity falls below the inertiadefined threshold. |
ManipulationCompleted |
The last pointer is no longer pressed, and any inertia calculations have completed. |
The first event received during a manipulation is the ManipulationStarting event. This event includes a Mode property that initially matches the ManipulationMode value set on the UIElement object. It allows the types of manipulations that will be tracked to be modified one last time before the manipulation tracking actually starts. If a pressed pointer is moved, the ManipulationStarted event is fired, followed by one or more ManipulationDelta events as the pointer continues to move.
The arguments provided to the ManipulationDelta event handler provide the information that can be used to react to the manipulation. The arguments contain some general-purpose informational properties that include the PointerDeviceType, which is the same as it was for the pointer events (note that this implies that a manipulation cannot span device types, such as a pinch occurring with both a finger and a mouse); a Container value that indicates the UIElement on which the manipulation is occurring; and an IsInertial flag that specifies whether the ManipulationDelta event is a result of inertia that occurs after pointers have been released. Of particular interest, however, are the Delta, Cumulative, and Velocity values.
The Delta property provides the changes in the values for Translation, Expansion, Scale, and Rotation that have occurred since the last ManipulationDelta event occurred. Translation indicates how much movement occurred on the x- and y-axis. Expansion specifies how far the distance grew or shrank between touch contacts. Scale is similar to Expansion, but it specifies the change in distance as a percentage. Finally, Rotation specifies the change in the rotation degrees. The Cumulative property returns the same items, except that the values returned are the overall changes that have occurred since the manipulation started instead of since the previous ManipulationDelta event. Finally, the Velocity provides a Linear property that contains the x and y velocities specified in pixels/milliseconds, an Expansion property that specifies the scaling change in pixels/ milliseconds, and an Angular property that specifies the rotational velocity in degrees/milliseconds.
In the example application, the delta values are applied to the shape being manipulated to move it onscreen, resize it, or rotate it (rotation is better seen with the square shape than the circular one). Listing 13.4 shows the event handler in the InputEventHandler class for the ManipulationDelta event.
LISTING 13.4 Handling Manipulation Changes
private void HandleManipulationDelta (Object sender, ManipulationDeltaRoutedEventArgs args) { // Check to see if this kind of device is being ignored if (!IsValidDevice(args.PointerDeviceType)) return; // Update the shape display based on the delta values var delta = args.Delta; _shapeModel.MoveShape(delta.Translation.X, delta.Translation.Y); _shapeModel.ResizeShape(delta.Scale); _shapeModel.RotateShape(delta.Rotation); }
The processing in the ShapeModel class is fairly straightforward. The MoveShape method simply makes sure that adding the offset values to the current position doesn’t move the shape beyond the current borders and adjusts the resulting position value accordingly. ResizeShape multiplies the current shape scale by the provided percentage and then makes sure the resulting shape size is within the minimum and maximum boundaries established for a shape. RotateShape simply adds the degree value to the current Rotation property. A TranslateTransform is bound to the shape position values. A RotateTransform has its Angle value bound to the rotation angle, as well as its CenterX and CenterY values bound to the position of the shape. Finally, a ScaleTransform has its ScaleX and ScaleY values bound to the scale of the shape, with the CenterX and CenterY values also bound to the shape position.
The final manipulation concept to be discussed is inertia. If one or more of the inertia ManipulationMode values is specified, the manipulation processing can include the application of inertia, depending on whether the last pointer involved in the manipulation was removed following an action that had a velocity. In the example app, this occurs when a shape is being dragged from one side of the screen to another and, halfway through, the finger/mouse/pen is suddenly released. In the physical world, the object would tend to continue to slide along until slowed by friction. With manipulation support for inertia, your app can include similar behavior without any extra work on your part.
When inertia starts, the ManipulationInertiaStarting event is raised. The arguments for this event include the arguments that were discussed for the ManipulationDelta event, as well as TranslationBehavior, ExpansionBehavior, and RotationBehavior arguments to control the behavior of the inertia effect. Each of these values includes a value called DesiredDeceleration that defines the deceleration rate, as well as a value to indicate the final desired value for each property, respectively named DesiredDisplacement, DesiredExpansion, and DesiredRotation. You can either leave the default values in place or replace them with your own value for more control over the inertia behavior. After the handler for this event has completed, the manipulation processor automatically raises ManipulationDelta events with values based on the application of inertia to the current state until either the desired value is reached (if specified) or deceleration results in a velocity of zero.
When the last pointer has been released, or when inertia has completed (when specified through the ManipulationMode setting), the ManipulationCompleted event is raised, signaling that the manipulation is now complete. The arguments to this event include the general-purpose informational properties that were discussed previously, as well as the Cumulative and Velocities information that was also provided to the ManipulationDelta event.
Gesture Events
Gesture events are similar to manipulation events, in that they are the result of grouping and interpreting several pointer events. However, a few key differences set them apart. First, gesture events communicate more abstract and discrete concepts than manipulation events. Manipulation events communicate information about the beginning, middle, and end of a manipulation and include arguments that provide information about the different kind of changes that have occurred. Gesture events each relay information about the occurrence of a single, isolated event, such as a tap or a double-tap. Second, manipulation events provide information that synthesizes input from several pointers, whereas gesture events are concerned with the action of only one pointer at a given time.
As with manipulation events, the UIElement class provides the most commonly used access to gesture events and related configuration settings. Table 13.3 summarizes the gesture events made available by UIElement instances.
TABLE 13.3 Gesture Events Defined in UIElement
Event |
Description |
Tapped |
A tap has occurred, defined by a quick pointer press and release (where a long press followed by a release results in Holding and RightTapped events). This is equivalent to a mouse Click event. |
DoubleTapped |
A second tap has occurred after a first tap event, within a system-setting defined time. This is equivalent to a mouse DoubleClick event. |
Holding |
A long-duration press is occurring or has completed. The event is raised when the long-press is initially detected, and once again when the long-press is either completed or cancelled. Mouse devices generally do not raise this event. |
RightTapped |
A right-tap has occurred, defined by either the completion of a holding gesture (for touch and pen devices) or a click with the right button (for mouse devices). This is equivalent to a mouse RightClick event. |
All the gesture events include a PointerDeviceType property that indicates the type of device that generated the event, as well as a GetPosition method that returns the coordinates of the action that led to the event, relative to the UIElement argument in the method call. If a null value is provided to GetPosition, the coordinates returned are relative to the app itself. The Holding event also includes a HoldingState property that is discussed shortly. Note that the Tapped and Holding events are mutually exclusive. Also, when a double-tap occurs, a Tapped event is raised for the first interaction, but the second one generates only the DoubleTapped event.
The UIElement class also provides the IsTapEnabled, IsDoubleTapEnabled, IsHoldingEnabled, and IsRightTapEnabled properties. By default, they are all set to true; setting them to false prevents the corresponding event from being raised.
The Tapped, DoubleTapped, and RightTapped events are similar, but the Holding event behaves a little differently. As Table 13.3 mentioned, the Tapped event is usually generated only by interaction with touch and stylus devices, not by mouse devices. It is also the only event that is raised when the pointer involved in the event is in a pressed state. When a pointer is pressed and held steady, and after the initial hold time interval has passed, the Holding event is raised with its HoldingState property set to a value of Started. After the hold has begun, if the pointer is moved or the same element captures another pointer, the hold is considered to have been cancelled and the Holding event is raised once again, with the HoldingState property set to a value of Cancelled. Otherwise, when the pressed pointer is lifted, the Holding event is raised again with a HoldingState property set to a value of Completed. If the hold was successfully completed, the RightTapped event follows.
In the example application, the tap-related gesture events cause different actions to happen to the shapes they occur on. The Tapped event changes the shape color to a random value, the DoubleTapped event causes the shape to take a new randomly calculated direction, and the RightTapped event causes the shape to be reset to its original color, size, and rotation. The code in Listing 13.5 illustrates this interaction for a Tapped event.
LISTING 13.5 Processing a Gesture Event
private void HandleTapped(Object sender, TappedRoutedEventArgs args) { // Check to see if this kind of device is being ignored if (!IsValidDevice(args.PointerDeviceType)) return; // Examine the current position var position = args.GetPosition(_eventSourceElement); Debug.WriteLine("Tapped at X={0}, Y={1}", position.X, position.Y); // Alter the shape based on the gesture performed _shapeModel.SetRandomColor(); }
Keyboard Input
In addition to the pointer-based input devices, the Windows Runtime includes support for working with input gathered from keyboards. To obtain information about the available keyboard support, you can use the KeyboardCapabilities class. Similar to the MouseCapabilities and TouchCapabilities counterparts, it includes a KeyboardPresent property that is set to a value of 1 if one or more keyboards are currently available. The example application uses this value to provide the text for a message box that displays when the user clicks the Details button next to the Keyboard header, as in Listing 13.6.
LISTING 13.6 Displaying Keyboard Capabilities
var keyboardCapabilities = new KeyboardCapabilities(); var message = keyboardCapabilities.KeyboardPresent == 1 ? "There is a keyboard present." : "There is no keyboard present."; ShowMessage(message, "Keyboard Properties");
The UIElement class provides two available keyboard events. The KeyDown event is raised when a key is pressed, and the KeyUp event is raised when a pressed key is released. These events are raised by a control only when the control has the input focus, either when the user taps inside the control or uses the Tab key to rotate focus to that control, or when the control’s Focus method has been called programmatically.
As an alternative, the CoreWindow class provides three events related to keyboard interactions. Similar to the UIElement, it provides KeyDown and KeyUp events. However, these events are raised regardless of which control currently has input focus. The CoreWindow class also includes a CharacterReceived event, which is discussed in more detail shortly.
In the case of the UIElement, both the KeyDown and KeyUp events provide KeyRoutedEventArgs arguments; for the CoreWindow class, the KeyDown and KeyUp events provide KeyEventArgs arguments. The most significant difference between these argument types is the naming of the property used to identify the key involved in the action that led to the event being raised. KeyRoutedEventArgs provides a property named Key that returns a value of the VirtualKey enumeration indicating the specific key on the keyboard that was pressed or released. In the KeyEventArgs class, the corresponding property is named VirtualKey.
In either case, the KeyStatus property contains additional information about the key event. For KeyDown events, its WasKeyDown property is particularly interesting because it indicates whether the event is being raised in response to a key being held down. In this case, several KeyDown events usually are raised, followed by a single KeyUp event. The first KeyDown event has its WasKeyDown value set to false, with the subsequent KeyDown events setting the value to true.
The CharacterReceived event of the CoreWindow class was previously mentioned. This event is fired between the KeyDown and KeyUp events and provides access to the actual interpreted character resulting from the current key combination. This value is returned as an unsigned integer in the CharacterReceivedEventArgs KeyCode property. It can be converted to the corresponding Char character using the Convert.ToChar function:
var interpretedChar = Convert.ToChar(args.KeyCode);
To put this in perspective, with a standard U.S. keyboard, pressing the equals (=) key while the Shift key is also pressed is interpreted to result in the plus (+) character. The KeyDown and KeyUp events understand this key only as VirtualKey 187, regardless of whether the Shift key is pressed. However, the KeyCode value provided in the arguments to the CharacterReceived event provides either a value of 61 for the equals key or a value of 43 for the plus key.
To illustrate the use of the keyboard input events, the main page in the example application listens for KeyUp events via the CoreWindow class to add either a new ball or a square shape whenever the B or S keys are pressed, respectively. The following code illustrates this:
if (args.VirtualKey == VirtualKey.B) CreateShape(ShapeModel.ShapeType.Ball);
Note that if you are interested in key combinations in which a “modifier key,” such as one or more of the Shift, Control, or Alt keys pressed in concert with another key, you have two options. First, you can track the individual key down and key up events to determine which keys are up or down at any given instant. Second, you can actively interrogate the state of a given key by using the GetKeyState method that the CoreWindow class provides. Because the result of GetKeyState returns a flag value, it is a best practice to mask the result value before comparing it with the desired value. Also note that the Alt key corresponds to the Menu member of the VirtualKey enumeration. Listing 13.7 shows this approach.
LISTING 13.7 Checking for Modifier Keys
// Check for shift, control, alt (AKA VirtualKey.Menu) var currentWindow = CoreWindow.GetForCurrentThread(); var ctrlState = currentWindow.GetKeyState(VirtualKey.Control); var shftState = currentWindow.GetKeyState(VirtualKey.Shift); var altState = currentWindow.GetKeyState(VirtualKey.Menu); var isControlKeyPressed = (ctrlState & CoreVirtualKeyStates.Down) == CoreVirtualKeyStates.Down; var isShiftKeyPressed = (shftState & CoreVirtualKeyStates.Down) == CoreVirtualKeyStates.Down; var isAltKeyPressed = (altState & CoreVirtualKeyStates.Down) == CoreVirtualKeyStates.Down;