Chapter 9. Routed Input Events


Under the Windows Presentation Foundation, the three primary forms of user input are the keyboard, the mouse, and the stylus. (Stylus input is available on Tablet PCs and through digitizing tablets.) Previous chapters have shown code that installed event handlers for some keyboard and mouse events, but this chapter examines input events more comprehensively.

Input events are defined with delegates whose second argument is a type that descends from RoutedEventArgs by way of InputEventArgs. The following class hierarchy is complete from InputEventArgs on down.

Object

     EventArgs

           RoutedEventArgs

                      InputEventArgs

                                KeyboardEventArgs

                                       KeyboardFocusChangedEventArgs

                                       KeyEventArgs

                      MouseEventArgs

                                       MouseButtonEventArgs

                                       MouseWheelEventArgs

                                       QueryCursorEventArgs

                      StylusEventArgs

                                       StylusButtonEventArgs

                                       StylusDownEventArgs

                                       StylusSystemGestureEventArgs

                      TextCompositionEventArgs

RoutedEventArgs is used extensively in the Windows Presentation Foundation as part of the support for event routing, which is a mechanism that allows elements to process events in a very flexible manner. Events are said to be routed when they travel up and down the element tree.

The user clicks the mouse button. Who gets the event? The traditional answer is "the visible and enabled control most in the foreground under the mouse pointer." If a button is on a window and the user clicks the button, the button gets the event. Very simple.

But under the Windows Presentation Foundation, this simple approach doesn't work very well. A button is not just a button. A button has content, and this content can consist of a panel that in turn contains shapes and images and text blocks. Each of these elements is capable of receiving mouse events. Sometimes it's proper for these individual elements to process their own mouse events, but not always. If these elements decorate the surface of a button, it makes the most sense for the button to handle the events. It would help if there existed a mechanism to route the events through the shapes, images, text blocks, and panels to the button.

The user presses a key on the keyboard. Who gets the event? The traditional answer is "the control that has the input focus." If a window contains multiple text boxes, for example, only one has the input focus and that's the one that gets the event.

But wouldn't it be nice if there existed a mechanism that allowed the window to examine the keyboard events first? Perhaps the window could decide if the event was something it was interested in processing before the event even got to the text box. (The EditSomeRichText program in Chapter 4 does precisely that to display file dialog boxes when the user types Ctrl+O or Ctrl+S.)

Event routing in the WPF allows both scenarios. There is always an element that is considered the "source" of an event. For mouse and stylus events, the source of the event is generally the element most in the foreground underneath the mouse pointer or stylus. This element must be both visible and enabled. (That is, the element's Visibility property must equal the enumeration member Visibility.Visible, and the IsEnabled property must be true.) If the element is not visible and enabled, the source of the event is the topmost underlying element that is enabled and visible.

The source of a keyboard event is the element that has the input focus. For both keyboard and mouse events, only one element is the source of any particular event, but all ancestors of the source element in the element tree also potentially have access to that event.

The UIElement class defines most of the user-input events. In the Events section of UIElement documentation, you'll see, for example, the MouseDown event, which signals when the user clicks any mouse button. In the Methods section of the documentation of UIElement, you'll see OnMouseDown, which is the protected virtual method that corresponds to that event. Any class that derives from UIElement can override the OnMouseDown method rather than install a MouseDown event handler. The two approaches are functionally identical, but the OnMouseDown call occurs before the MouseDown event is fired.

When overriding a virtual method such as OnMouseDown, it is common to call the method in the base class:

protected override void OnMouseDown(MouseEventArgs args) {     base.OnMouseDown(args);     ... } 


In Windows Forms, calling the OnMouseDown method in the base class was very important because that's where the base class fired the MouseDown event. The OnMouseDown method as implemented in the Control class in Windows Forms probably looks something like this:

// This is Windows Forms, not the Windows Presentation Foundation! protected virtual void OnMouseDown(MouseEventArgs args) {     ...     if (MouseDown != null)         MouseDown(this, args)     ... } 


In Windows Forms, a class called MyControl could derive from Control and override OnMouseDown without calling the method in the base class. This is usually not good, because if the program then created an object of type MyControl and attached a MouseDown event handler to that object, that handler would never get any events.

In the Windows Presentation Foundation, this is not an issue. The firing of the MouseDown event occurs outside of the call to OnMouseDown. As the documentation for OnMouseDown in the UIElement class states, "This method has no default implementation." In other words, the body of the OnMouseDown method in UIElement is empty. In a class that inherits directly from UIElement, you can safely override OnMouseDown without calling the method in the base class and inflict no bad side effects on any class that inherits from your class or instantiates your class. You'll see the same statement about "no default implementation" in the other input-related virtual methods in UIElement.

The WPF documentation indicates not only when a class defines a virtual method, but also when a class overrides that method. Look at the list of methods in FrameworkElement, the class that inherits directly from UIElement. Do you see OnMouseDown? No, FrameworkElement does not override OnMouseDown. Now check Control, which inherits directly from FrameworkElement. Once again, OnMouseDown is not listed. Now check ButtonBase. There you will see an override not of OnMouseDown, but of OnMouseLeftButtonDown, which is a related method that ButtonBase uses to generate its Click event. The OnMouseLeftButtonDown method in ButtonBase is not responsible for firing the MouseLeftButtonDown event, but it is responsible for firing the Click event. If you derive a class from ButtonBase, and if you override OnMouseLeftButtonDown without calling the method in the base class, you will effectively disable the Click event. The documentation for OnMouseLeftButtonDown in ButtonBase does not say that the "method has no default implementation."

Although UIElement does not fire its events in the corresponding On methods, other classes may take a different approach. For example, the ButtonBase class defines an OnClick method. The OnMouseLeftButtonDown override in ButtonBase almost certainly calls OnClick, and the OnClick method is responsible for firing the Click event. As with the OnMouseLeftButtonDown method, if you derive from ButtonBase and override OnClick without calling the method in the base class, you will disable the Click event for any object based on your class.

As a general rule, it's a good idea whenever you override a virtual method to call the method in the base class unless you have a specific reason for not doing so. I tend to do that even for methods where it is documented as unnecessary.

The MouseDown event and the OnMouseDown virtual protected method are joined by other related members of the UIElement class. In the Fields section of the UIElement documentation, you'll also see a static read-only field of type RoutedEvent named MouseDownEvent. As you'll discover in this chapter, these RoutedEvent objects have a role similar to that of dependency properties. MouseDownEvent provides an easy way to refer to the MouseDown event in some method calls, and the RoutedEvent class encapsulates information that governs how the event is routed to other elements in the element tree.

In the UIElement documentation, you'll also find an event named PreviewMouseDown, a method named OnPreviewMouseDown, and a field named PreviewMouseDownEvent. This pattern of names is pervasive throughout most of the user input events defined by UIElement. You'll see shortly how these two sets of events work.

The class hierarchy shown at the beginning of this chapter suggests that RoutedEventArgs is an important class in WPF user events, and many other classes besides InputEventArgs inherit from RoutedEventArgs.

RoutedEventArgs defines just four properties that are often useful in event handling. One of these properties is named RoutedEvent, and it's an object of type RoutedEvent. The RoutedEvent property of RoutedEventArgs identifies the event itself. For a MouseDown event, for example, this property will be equal to MouseDownEvent, which is the static field defined in UIElement. You could use the same event handler for different types of events and identify the events with code like this:

if (args.RoutedEvent == MouseDownEvent) {     ... } else if (args.RoutedEvent == MouseUpEvent) {     ... } 


The Source and OriginalSource properties of RoutedEventArgs are both of type object and indicate (as the names imply) the element that is the source of the event. In general, the Source property is very important, but in some cases you might also need to look at the OriginalSource property.

The Handled property of RoutedEventArgs is initially false, but may be set to true to prevent future routing of the event. The EditSomeRichText program in Chapter 4 used this property to prevent the Ctrl+O and Ctrl+S keystrokes from going to the RichTextBox itself.

To the properties defined by RoutedEventArgs, InputEventArgs adds a Device property of type InputDevice to identify the input device, and a TimeStamp property of type int.

Let's examine some actual routed events. The program below is called ExamineRoutedEvents, and the first thing you should know is that it inherits from Application rather than Window, and then creates an object of type Window and several other elements as well. The original version of the program defined a class named ExamineRoutedEvents that inherited from Window, but (as you'll see) the program displays text with class names such as "TextBlock" and "Button" and "Grid" and it was just too confusing for the program to display a class named "ExamineRoutedEvents" rather than a class simply named "Window."

ExamineRoutedEvents.cs

[View full width]

//---------------------------------------------------- // ExamineRoutedEvents.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ExamineRoutedEvents { public class ExamineRoutedEvents: Application { static readonly FontFamily fontfam = new FontFamily("Lucida Console"); const string strFormat = "{0,-30} {1,-15} {2,-15} {3,-15}"; StackPanel stackOutput; DateTime dtLast; [STAThread] public static void Main() { ExamineRoutedEvents app = new ExamineRoutedEvents(); app.Run(); } protected override void OnStartup (StartupEventArgs args) { base.OnStartup(args); // Create the Window. Window win = new Window(); win.Title = "Examine Routed Events"; // Create the Grid and make it Window content. Grid grid = new Grid(); win.Content = grid; // Make three rows. RowDefinition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); rowdef = new RowDefinition(); rowdef.Height = new GridLength(100, GridUnitType.Star); grid.RowDefinitions.Add(rowdef); // Create the Button & add it to the Grid. Button btn = new Button(); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.Margin = new Thickness(24); btn.Padding = new Thickness(24); grid.Children.Add(btn); // Create the TextBlock & add it to the Button. TextBlock text = new TextBlock(); text.FontSize = 24; text.Text = win.Title; btn.Content = text; // Create headings to display above the ScrollViewer. TextBlock textHeadings = new TextBlock(); textHeadings.FontFamily = fontfam; textHeadings.Inlines.Add(new Underline (new Run( String.Format(strFormat, "Routed Event", "sender", "Source" , "OriginalSource")))); grid.Children.Add(textHeadings); Grid.SetRow(textHeadings, 1); // Create the ScrollViewer. ScrollViewer scroll = new ScrollViewer(); grid.Children.Add(scroll); Grid.SetRow(scroll, 2); // Create the StackPanel for displaying events. stackOutput = new StackPanel(); scroll.Content = stackOutput; // Add event handlers. UIElement[] els = { win, grid, btn, text }; foreach (UIElement el in els) { // Keyboard el.PreviewKeyDown += AllPurposeEventHandler; el.PreviewKeyUp += AllPurposeEventHandler; el.PreviewTextInput += AllPurposeEventHandler; el.KeyDown += AllPurposeEventHandler; el.KeyUp += AllPurposeEventHandler; el.TextInput += AllPurposeEventHandler; // Mouse el.MouseDown += AllPurposeEventHandler; el.MouseUp += AllPurposeEventHandler; el.PreviewMouseDown += AllPurposeEventHandler; el.PreviewMouseUp += AllPurposeEventHandler; // Stylus el.StylusDown += AllPurposeEventHandler; el.StylusUp += AllPurposeEventHandler; el.PreviewStylusDown += AllPurposeEventHandler; el.PreviewStylusUp += AllPurposeEventHandler; // Click el.AddHandler(Button.ClickEvent, new RoutedEventHandler (AllPurposeEventHandler)); } // Show the window. win.Show(); } void AllPurposeEventHandler(object sender, RoutedEventArgs args) { // Add blank line if there's been a time gap. DateTime dtNow = DateTime.Now; if (dtNow - dtLast > TimeSpan .FromMilliseconds(100)) stackOutput.Children.Add(new TextBlock(new Run(" "))); dtLast = dtNow; // Display event information. TextBlock text = new TextBlock(); text.FontFamily = fontfam; text.Text = String.Format(strFormat, args .RoutedEvent.Name, TypeWithoutNamespace(sender), TypeWithoutNamespace(args.Source), TypeWithoutNamespace(args.OriginalSource)); stackOutput.Children.Add(text); (stackOutput.Parent as ScrollViewer) .ScrollToBottom(); } string TypeWithoutNamespace(object obj) { string[] astr = obj.GetType().ToString ().Split('.'); return astr[astr.Length - 1]; } } }



This program creates elements that it assembles into the following tree:

The Window object is considered the "root" of the tree. (Yes, I knowif the Window is the root of the tree, the tree is upside-down. But this is a special kind of tree.) From the Window, all the other elements are considered to be "down" from that root. From the perspective of one of the TextBlock elements, the Window is considered to be "up" in the tree.

The ExamineRoutedEvents program installs various event handlers for the Window, Grid, and Button elements, and also for the TextBlock content of the Button. These event handlers encompass several common keyboard, mouse, and stylus events but all share the same method, AllPurposeEventHandler.

Whenever the AllPurposeEventHandler receives a new event, it creates a new TextBlock object (corresponding to the TextBlock with the ellipsis shown in the tree diagram). This TextBlock object displays information about the event, including the first argument to the event handler (typically called sender) as well as the RoutedEvent, Source, and OriginalSource properties of the RoutedEventArgs argument. The event handler then adds that TextBlock to a StackPanel that is the content of a ScrollViewer. The TextBlock element in the center of the tree diagram simply provides headings for the information the program displays.

To help you read the output, the AllPurposeEventHandler attempts to insert a blank line whenever an event follows another by at least 100 milliseconds. Just be aware that this feature doesn't always work flawlessly if your fingers are very fast or the events are delayed for some reason.

When you run the program, you'll see a big button at the top with the text "Examine Routed Events." I want you to click the text in that button with the right mouse button. You should see the sequence of events listed in the following table:

RoutedEvent

sender

Source

OriginalSource

PreviewMouseDown

Window

TextBlock

TextBlock

PreviewMouseDown

Grid

TextBlock

TextBlock

PreviewMouseDown

Button

TextBlock

TextBlock

PreviewMouseDown

TextBlock

TextBlock

TextBlock

MouseDown

TextBlock

TextBlock

TextBlock

MouseDown

Button

TextBlock

TextBlock

MouseDown

Grid

TextBlock

TextBlock

MouseDown

Window

TextBlock

TextBlock


This sequence is followed by a similar pattern for the PreviewMouseUp event and then for the MouseUp event. (If you see an OriginalSource of ButtonChrome, you didn't right-click the text in the button.)

The first argument to the event handlertypically called senderis always the object on which you attached the event handler. This argument indicates the object that is sending the event to your application. The Source property of RoutedEventArgs is the element that actually raised the event.

The sequence shown in the table is the essence of event routing. The PreviewMouseDown event is an example of a tunneling event. The event begins at the root of the visual tree and "tunnels down" to the element directly underneath the mouse cursor. The MouseDown event is a bubbling event. The event "bubbles up" through visual parents to the root element. (This terminology of "down" and "up" depends on the tree being visualized with its root at the top.)

Event routing provides an extremely flexible way to handle events. If any element higher in the tree wants first dibs on the mouse-down action, it can do so by handling the PreviewMouseDown event. If any of these elements wants only those mouse-down events ignored by elements lower in the tree, it can receive those as well by handling MouseDown.

Now move the mouse cursor slightly away from the button text but still within the confines of the button and click again with the right mouse button. Here's what you'll see:

RoutedEvent

sender

Source

OriginalSource

PreviewMouseDown

Window

Button

ButtonChrome

PreviewMouseDown

Grid

Button

ButtonChrome

PreviewMouseDown

Button

Button

ButtonChrome

MouseDown

Button

Button

ButtonChrome

MouseDown

Grid

Button

ButtonChrome

MouseDown

Window

Button

ButtonChrome


You'll also see an identical pattern for PreviewMouseUp and MouseUp. The TextBlock is not involved in this event because it's no longer under the mouse pointer. Instead, the Button object is raising the event as indicated by the Source property, but the OriginalSource property indicates that the foreground object actually underneath the mouse pointer is an object of type ButtonChrome. Have you ever heard of ButtonChrome? Neither did I before I wrote and ran this program. ButtonChrome inherits from Decorator (which in turn derives from FrameworkElement) and can be found in the Microsoft.Windows.Themes namespace. Button uses ButtonChrome to draw its surface.

The lesson here is simple: The OriginalSource property is often something you can safely ignore. You'll usually focus on the Source property. (However, if Source is not giving you what you want and OriginalSource is, then obviously use that instead.)

I've been telling you to click the button with the right mouse button. There's a reason for that. If you click the button TextBlock with the left mouse button, you'll see the following sequence:

RoutedEvent

sender

Source

OriginalSource

PreviewMouseDown

Window

TextBlock

TextBlock

PreviewMouseDown

Grid

TextBlock

TextBlock

PreviewMouseDown

Button

TextBlock

TextBlock

PreviewMouseDown

TextBlock

TextBlock

TextBlock

MouseDown

TextBlock

TextBlock

TextBlock


And then the MouseDown events end. There are no more. What happened to them?

The Button class happens to be very interested in the left mouse button, because that's what it uses to generate Click events. As I mentioned earlier, ButtonBase overrides the OnMouseLeftButtonDown method. When that method is called, ButtonBase begins the work that will eventually result in a Click event. Because ButtonBase is, in effect, handling the left mouse button and putting it to work, it sets the Handled flag of RoutedEventArgs to true. That flag halts a call to the MouseDown event handlers, as well as all further routing of the event through the tree.

As you release the left mouse button, you'll see the following sequence of events:

RoutedEvent

sender

Source

OriginalSource

PreviewMouseUp

Window

Button

Button

PreviewMouseUp

Grid

Button

Button

PreviewMouseUp

Button

Button

Button

Click

Button

Button

Button

Click

Grid

Button

Button

Click

Window

Button

Button


The TextBlock is no longer involved. The Button has "captured the mouse" (a technique I'll discuss shortly) and has become the source of this event. During the call to the OnLeftMouseButtonUp method, ButtonBase sets the Handled property of the event arguments to true, and generates a Click event, which continues through the tree back up to the window. (The Click event exists only in a bubbling version. There is no tunneling PreviewClick event.)

The Click event is defined by ButtonBase and inherited by Button. The Window class knows nothing about Click events and neither does Grid. It would normally be impossible to install a Click event handler on a Window or Grid object. But UIElement defines an AddHandler method (and a corresponding RemoveHandler method) that accepts a first argument of type RoutedEvent and lets you install an event handler for any routed event of any other element in the same tree. Here's how the ExamineRoutedEvents program uses AddHandler to install a Click event handler on the other elements:

el.AddHandler(Button.ClickEvent,     new RoutedEventHandler(AllPurposeEventHandler)); 


Installing a handler on a panel (for example) is a very useful technique for consolidating the handling of particular events coming from a number of child elements.

If you run this program on a Tablet PC, you'll want to tap the program's button with the stylus. First you'll see a PreviewStylusDown event tunnel down the tree from Window to Grid to Button to TextBlock, and then you'll see the StylusDown event bubble up the tree from TextBlock to Button to Grid to Window. At that point, PreviewMouseDown and MouseDown events are generated just as if you had clicked with the left mouse button. When you release the stylus from the screen, the PreviewStylusUp event tunnels down from the Window to the Button and the StylusUp event bubbles up from the Button to the Window. (The TextBlock is out of the picture.) Then, PreviewMouseUp and MouseUp events are generated. The stylus always generates mouse events as well as stylus events.

Because you've been clicking the button with the mouse or stylus, it has the input focus. Press a function key. You'll see a PreviewKeyDown event tunnel down the tree from the Window to the Grid to the Button, and then a KeyDown event bubble up the tree from the Button to the Grid to the Window. Obviously the Button has the input focus and not the TextBlock. As you release the key you'll see a PreviewKeyUp tunnel down the tree and KeyUp events bubble up the tree in the same pattern.

Now press any letter key. You'll see three PreviewKeyDown events and three KeyDown events, followed by three PreviewTextInput events and three TextInput events. The TextInput events indicate actual text input generated from the keyboard. As you release the key, you'll see three PreviewKeyUp events and three KeyUp events.

Now press the spacebar. As with the left mouse button, the Button control is very interested in the spacebar. Here's what you'll see:

RoutedEvent

sender

Source

OriginalSource

PreviewKeyDown

Window

Button

Button

PreviewKeyDown

Grid

Button

Button

PreviewKeyDown

Button

Button

Button

PreviewKeyUp

Window

Button

Button

PreviewKeyUp

Grid

Button

Button

PreviewKeyUp

Button

Button

Button

Click

Button

Button

Button

Click

Grid

Button

Button

Click

Window

Button

Button


The Button stops further processing of the KeyDown event and then turns the KeyUp event into a Click. You'll see something similar for the Enter key.

Defining a routed event in your own class is similar to defining a dependency property. Suppose you have a control in which you need a Click-like event that you'd rather call Knock. You first define a static read-only field of type RoutedEvent for Knock (and optionally PreviewKnock):

public static readonly RoutedEvent KnockEvent; public static readonly RoutedEvent PreviewKnockEvent; 


By convention, the field names consist of the name of the event followed by the word Event.

In the field definition itself or in a static constructor, you call the static EventManager.RegisterRoutedEvent method. The first argument is the text name of the event:

KnockEvent =     EventManager.RegisterRoutedEvent("Knock", RoutingStrategy.Bubble,         typeof(RoutedEventHandler), typeof(YourClass)); PreviewKnockEvent =     EventManager.RegisterRoutedEvent("PreviewKnock", RoutingStrategy.Tunnel,         typeof(RoutedEventHandler), typeof(YourClass)); 


Notice that the second argument is RoutingStrategy.Bubble for the Knock event and RoutingStrategy.Tunnel for PreviewKnock. The only other member of the RoutingStrategy enumeration is Direct, in which case the event is not routed. The third argument is shown as RoutedEventHandler here, but it can be any delegate with an argument of RoutedEventArgs or a descendent. The fourth argument is the type of the class in which you're defining this event.

You must also define the actual Knock and PreviewKnock events. The first line of this block of code looks similar to the way you define a regular .NET event using the event keyword and the delegate associated with the event:

public event RoutedEventHandler Knock {     add { AddHandler(KnockEvent, value); }     remove { RemoveHandler(KnockEvent, value); } } 


But this event definition also includes add and remove accessors that refer to the static routed event. AddHandler and RemoveHandler are defined by DependencyObject. The value argument is the event handler. An event definition for PreviewKnock would be similar but refer to the PreviewKnockEvent event.

When it comes time for your class to actually raise these events (most likely in the OnMouseUp method), you create an object of type RoutedEventArgs (or a descendent):

RoutedEventArgs argsEvent = new RoutedEventArgs(); argsEvent.RoutedEvent = YourClass.PreviewKnockEvent; argsEvent.Source = this; RaiseEvent(argsEvent); argsEvent = new RoutedEventArgs(); argsEvent.RoutedEvent = YourClass.KnockEvent; argsEvent.Source = this; RaiseEvent(argsEvent); 


You'll recognize the RoutedEvent and Source properties of RoutedEventArgs, of course. RoutedEventArgs also defines a constructor that accepts these two values. If you don't explicitly set OriginalSource, the OriginalSource is set by default from Source. The RaiseEvent method is defined by UIElement. Notice that you call RaiseEvent for the tunneling event first, followed by the bubbling event.

A complete program using the Knock and PreviewKnock events is presented in Chapter 10.

Although I've been discussing user input events as implemented in the UIElement class, these events are implemented in ContentElement as well. This is the class from which TextElement, FixedDocument, and FlowDocument derive. The ContentElement objects are elements that cannot render themselves on the screen, but are rendered by other elements. Both UIElement and ContentElement implement the IInputElement interface, which also includes most (but not all) of the user input events defined by UIElement and ContentElement.

Both UIElement and ContentElement define 10 events that begin with the word Mouse and 8 that begin with PreviewMouse.

Event handlers for the MouseMove, PreviewMouseMove, MouseEnter, and MouseLeave events are all of type MouseEventHandler, and the event is accompanied by an object of type MouseEventArgs. (There are no PreviewMouseEnter or PreviewMouseLeave events.) The MouseMove event occurs a multitude of times when the mouse cursor is moved across the surface of an element. The MouseEnter and MouseLeave events occur when the mouse cursor enters and leaves the area occupied by the element. UIElement and ContentElement also define two related read-only properties. IsMouseOver is true if the mouse cursor is anywhere over the element. IsMouseDirectlyOver is true if the mouse cursor is over the element but not over any child elements.

When handling a MouseMove, MouseEnter, or MouseLeave event, you might want to know which mouse buttons, if any, are currently pressed. MouseEventArgs has separate read-only properties for all the types of buttons found on a mouse: LeftButton, MiddleButton, and RightButton, as well as the two extended buttons, XButton1 and XButton2. Each of these properties is set to a member of the MouseButtonState enumeration, which has just two members: Pressed and Released.

Particularly for a MouseMove event, your program may need to know the location of the mouse cursor. The GetPosition method defined by MouseEventArgs requires an argument of any type that implements the IInputElement interface and returns a Point object in device-independent coordinates relative to the upper-left corner of that element.

The MouseDown, MouseUp, PreviewMouseDown, and PreviewMouseUp events occur when the user presses and releases a mouse button over an element in your program. These events are accompanied by objects of type MouseButtonEventArgs. You can identify what button triggered the event by examining the ChangedButton property. This is a member of the MouseButton enumeration, which has members Left, Middle, Right, XButton1, and XButton2. If the user presses two buttons simultaneously, each generates its own MouseDown event. The MouseButtonEventArgs class also defines a property named ButtonState, which is a member of the MouseButtonState enumeration. Obviously, for a MouseDown event, the ButtonState property will equal MouseButtonState.Pressed, and for a MouseUp event, it will be MouseButtonState.Released, but the presence of this property lets you distinguish the two events if you use the same event handler for both MouseDown and MouseUp.

The UIElement and ContentElement classes also define events specifically for the left and right mouse buttons named MouseLeftButtonDown, MouseRightButtonDown, MouseLeftButtonUp, and MouseRightButtonUp, all with Preview versions. Think of these as "convenience" events. There is really no difference between processing a MouseLeftButtonDown event and processing a MouseDown event by enclosing your logic in an if statement:

if (args.ChangedButton == MouseButton.Left) {     ... } 


Just to get an idea of how these left button and right button events fit in with everything else, you may want to insert the following code in the foreach loop in ExamineRoutedEvents:

el.MouseLeftButtonDown += AllPurposeEventHandler; el.MouseLeftButtonUp += AllPurposeEventHandler; el.PreviewMouseLeftButtonDown += AllPurposeEventHandler; el.PreviewMouseLeftButtonUp += AllPurposeEventHandler; el.MouseRightButtonDown += AllPurposeEventHandler; el.MouseRightButtonUp += AllPurposeEventHandler; el.PreviewMouseRightButtonDown += AllPurposeEventHandler; el.PreviewMouseRightButtonUp += AllPurposeEventHandler; 


You'll see these events interleaved between the "down" and "up" events. Each element in the tree generates either a MouseLeftButtonUp or MouseRightButtonUp event just prior to the MouseUp event, and similarly for the "down" and "preview" versions.

The MouseWheel and PreviewMouseWheel events report on turns of the wheel located on some modern mouse devices. The wheel doesn't turn smoothly but has tactile (and sometimes audible) ticks. Today's wheel mouse devices associate a value of 120 with each tick. Perhaps wheel mouse devices of the future will have finer ticks and be associated with smaller numbers, but for now the Delta property of MouseWheelEventArgs is 120 per tick if the wheel is turned away from the user, and 120 if the wheel is turned toward the user. A program can determine if a mouse wheel is present by the SystemParameters.IsMouseWheelPresent value.

A program can also get information about the current mouse position and the state of mouse buttons through static properties in the Mouse class. This class also has static methods to attach and remove mouse event handlers. The MouseDevice class has instance methods for mouse position and button state.

The Control class defines an additional mouse event named MouseDoubleClick and a corresponding OnMouseDoubleClick method that are accompanied by objects of type MouseButtonEventHandler.

The mouse is represented on screen by a bitmap called the mouse cursor. In the Windows Presentation Foundation, this is an object of type Cursor, most conveniently made available in the Cursors class. To associate a particular cursor with an element, you can set the Cursor object to the Cursor property defined by FrameworkElement. Or, for more flexibility, you can attach a handler for the QueryCursor event (or override the OnQueryCursor method). This event is triggered whenever the mouse is moved. The QueryCursorEventArgs object that accompanies this event includes a property named Cursor that you set to the appropriate Cursor object.

It is tempting to believe that MouseDown and MouseUp events occur in pairs. From the perspective of your application, they definitely do not. For example, suppose the user moves the mouse cursor to your application's window and presses a button. Your window gets a MouseDown event. Now, with the button still pressed, the user moves the cursor away from your window and releases the button. Your application's window has no knowledge of that event.

Sometimes this normal mouse behavior is not desirable. Sometimes it is convenient for a program to know what's happening with the mouse outside of its window. For example, consider a drawing program where the user presses the mouse to begin drawing a graphical object, and then the mouse drifts outside the window, perhaps only momentarily. It would be preferable for the application to keep receiving MouseMove events during the time the mouse is outside the borders of the window.

Or look at a standard Button control. The Button generates a Click event when the left mouse button is released, but the processing preceding that Click event is obviously more complex: If you press the left mouse button while the mouse is positioned over the button, the button surface turns darker to indicate that it's been depressed. Now, with the mouse button still pressed, move the mouse pointer away from the Button object. You'll see the surface turn normal again. If you release the mouse button and then move the pointer back to the button, the Button knows that the mouse button has been released.

This behavior is possible through a process called capturing the mouse. To capture the mouse, an element calls the CaptureMouse method defined by UIElement or ContentElement (or the static Capture method of the Mouse class). The method returns bool if the capture is successful. It's not a good idea to try to capture the mouse other than during a MouseDown event. It might confuse the user and it's just not very useful.

After an element calls CaptureMouse, it will continue to receive MouseMove and MouseUp events even if the mouse pointer is moved away from the element. The read-only IsMouseCaptured property will be true to indicate the capture. When the element gets a MouseUp event, it is proper to release the mouse capture with a call to ReleaseMouseCapture or to the static Mouse.Capture method with a null argument.

Mouse capturing can potentially interfere with the proper operation of Windows. For that reason, Windows can unilaterally free the mouse from your program's capture. If you don't release the mouse on receipt of a MouseUp event, and the user clicks another window, your program will lose the mouse capture. Even if your program abides by all the rules and Windows suddenly needs to put a system-modal dialog on the screen, your program will lose the mouse capture.

If you have a need to capture the mouse, you should also install an event handler for the LostMouseCapture event, and use that occasion to perform any necessary clean-up. The LostMouseCapture event is also triggered when your program calls Mouse.Capture with a null argument, so you may need to distinguish between a normal loss of mouse capture and one that's being imposed on you.

Mouse capturing and many of the mouse events are demonstrated in the DrawCircles program. You may want to try using the program before you look at the source code. Press the left mouse button over the window and drag the mouse to draw circles in the window's client area. The initial point that you click becomes the circle's center. When you release the left mouse button, the circle is drawn with a blue perimeter and a red interior.

If you press the right mouse button over one of the circles, you can drag it to a new location. If you click the middle mouse button over a circle, you can toggle the interior brush between red and transparent. Pressing the Escape key aborts any drawing or dragging operation.

DrawCircles.cs

[View full width]

//-------------------------------------------- // DrawCircles.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.DrawCircles { public class DrawCircles : Window { Canvas canv; // Drawing-Related fields. bool isDrawing; Ellipse elips; Point ptCenter; // Dragging-Related fields. bool isDragging; FrameworkElement elDragging; Point ptMouseStart, ptElementStart; [STAThread] public static void Main() { Application app = new Application(); app.Run(new DrawCircles()); } public DrawCircles() { Title = "Draw Circles"; Content = canv = new Canvas(); } protected override void OnMouseLeftButtonDown(MouseButtonEventArgs args) { base.OnMouseLeftButtonDown(args); if (isDragging) return; // Create a new Ellipse object and add it to canvas. ptCenter = args.GetPosition(canv); elips = new Ellipse(); elips.Stroke = SystemColors .WindowTextBrush; elips.StrokeThickness = 1; elips.Width = 0; elips.Height = 0; canv.Children.Add(elips); Canvas.SetLeft(elips, ptCenter.X); Canvas.SetTop(elips, ptCenter.Y); // Capture the mouse and prepare for future events. CaptureMouse(); isDrawing = true; } protected override void OnMouseRightButtonDown(MouseButtonEventArgs args) { base.OnMouseRightButtonDown(args); if (isDrawing) return; // Get the clicked element and prepare for future events. ptMouseStart = args.GetPosition(canv); elDragging = canv.InputHitTest (ptMouseStart) as FrameworkElement; if (elDragging != null) { ptElementStart = new Point(Canvas .GetLeft(elDragging), Canvas .GetTop(elDragging)); isDragging = true; } } protected override void OnMouseDown (MouseButtonEventArgs args) { base.OnMouseDown(args); if (args.ChangedButton == MouseButton .Middle) { Shape shape = canv.InputHitTest (args.GetPosition(canv)) as Shape; if (shape != null) shape.Fill = (shape.Fill == Brushes.Red ? Brushes .Transparent : Brushes.Red); } } protected override void OnMouseMove (MouseEventArgs args) { base.OnMouseMove(args); Point ptMouse = args.GetPosition(canv); // Move and resize the Ellipse. if (isDrawing) { double dRadius = Math.Sqrt(Math .Pow(ptCenter.X - ptMouse.X, 2) + Math .Pow(ptCenter.Y - ptMouse.Y, 2)); Canvas.SetLeft(elips, ptCenter.X - dRadius); Canvas.SetTop(elips, ptCenter.Y - dRadius); elips.Width = 2 * dRadius; elips.Height = 2 * dRadius; } // Move the Ellipse. else if (isDragging) { Canvas.SetLeft(elDragging, ptElementStart.X + ptMouse.X - ptMouseStart.X); Canvas.SetTop(elDragging, ptElementStart.Y + ptMouse.Y - ptMouseStart.Y); } } protected override void OnMouseUp (MouseButtonEventArgs args) { base.OnMouseUp(args); // End the drawing operation. if (isDrawing && args.ChangedButton == MouseButton.Left) { elips.Stroke = Brushes.Blue; elips.StrokeThickness = Math.Min (24, elips.Width / 2); elips.Fill = Brushes.Red; isDrawing = false; ReleaseMouseCapture(); } // End the capture operation. else if (isDragging && args .ChangedButton == MouseButton.Right) { isDragging = false; } } protected override void OnTextInput (TextCompositionEventArgs args) { base.OnTextInput(args); // End drawing or dragging with press of Escape key. if (args.Text.IndexOf('\x1B') != -1) { if (isDrawing) ReleaseMouseCapture(); else if (isDragging) { Canvas.SetLeft(elDragging, ptElementStart.X); Canvas.SetTop(elDragging, ptElementStart.Y); isDragging = false; } } } protected override void OnLostMouseCapture (MouseEventArgs args) { base.OnLostMouseCapture(args); // Abnormal end of drawing: Remove child Ellipse. if (isDrawing) { canv.Children.Remove(elips); isDrawing = false; } } } }



The window covers its client area with a Canvas panel. Almost always when you're drawing based on mouse input, you want to use Canvas. Rather than installing event handlers, the window class overrides seven mouse-related methods. DrawCircles strives less for consistency than for demonstrating different mouse-handling techniques. For example, the program has separate OnLeftMouseButtonDown and OnRightMouseButtonDown methods for initiating drawing and dragging, but combines the end of these operations in a single OnMouseUp method. The window captures the mouse for drawing, but not for dragging.

During the OnLeftMouseButtonDown method, the program creates an Ellipse object, adds it to the Canvas child collection, and captures the mouse. The method also sets the isDrawing field to true so that all future event handlers know exactly what's going on.

Further processing occurs during the OnMouseMove override. I wanted the original mouse click to indicate the center of the circle, and for the perimeter to track the mouse cursor position as it's moved. This required recalculating the Width and Height properties as well as the Canvas.LeftProperty and Canvas.RightProperty attached properties. An alternativemore suitable for a rectangle than for a circlewould have been to assign the initial mouse position to one corner of the bounding rectangle and the current mouse position to the opposite corner. These are not necessarily the upper-left and lower-right corners because the user could move the mouse above or to the left of the original position.

In the OnMouseUp override, the first section applies to the drawing operation. The method now gives the ellipse a blue perimeter and a red interior. It sets isDrawing to false and releases the mouse capture. Releasing the mouse capture generates a call to the OnLostMouseCapture, but because isDrawing is already false, no further work is done here. But notice the OnTextInput override. If the user presses the Escape key and isDrawing is true, the method calls ReleaseMouseCapture. The resultant call to OnLostMouseCapture now has a little cleanup work to do in removing the Ellipse object from the Canvas child collection.

Pressing the right mouse button initiates a dragging operation. The OnRightMouseButtonDown method calls InputHitTest on the Canvas object to determine which element (if any) is underneath the mouse cursor. The method saves the mouse position and that element's position in fields. During subsequent calls to OnMouseMove, the program moves the ellipse. Because the mouse is not captured during this operation, if you move the mouse outside the window, the ellipse will stop moving. You can then release the mouse button and move the mouse back into the window, and the ellipse will resume following the mouse around even though the button is not pressed. It's not a serious problem, however. All you need to do is click with the right mouse button and the dragging operation concludes. In a real-world application, you'll want to capture the mouse during such a dragging operation.

Finally, clicking the middle button toggles the interior brush between Brushes.Red and Brushes.Transparent. I originally wrote this logic to set the Fill property of the Ellipse to null rather than Brushes.Transparent. The default Fill value is null anyway, so I knew it wouldn't cause a problem. However, with a null interior, you need to click the ellipse on the perimeter to get it to respond. You can no longer click in the interior to move it or even subsequently toggle the Fill brush.

This program is a good example of why it was necessary for the designers of the Windows Presentation Foundation to implement routed event handling. When you first start using the program, you're actually triggering events from the Canvas rather than from the Window. While it would have been easy enough to install event handlers for the Canvas, what happens after you've drawn a few circles? If you click an existing circle, the Ellipse object is now the source of the mouse event. In a nonrouted system, you'd need to force mouse events to the canvas, perhaps by setting the IsEnabled flag of the Ellipse to false. Routed events give you much more flexibility in how you structure your program.

If you're using a Tablet PC, all stylus input is converted to mouse events, so you can use the stylus with the DrawCircles program. But you may want to write code specifically for the Tablet PC. There are three general programming interfaces you can use.

The highest-level WPF programming interface to the Tablet PC is the InkCanvas, which sounds like a type of panel, but it's not. InkCanvas inherits from FrameworkElement, and implements ink rendering and retention. I mentioned earlier that stylus input is always converted to mouse input. InkCanvas goes in the other direction as well: It will respond to mouse input and treat it like stylus input. When you draw in the InkCanvas using the stylus or mouse, the InkCanvas draws the actual lines on its surface and saves them in its Strokes property as a collection of Stroke objects, which are basically polylines with drawing attributes attached. Each stroke consists of touching the stylus to the screen, moving it, and lifting the stylus from the screen. With the mouse, each stroke begins when the left mouse button is pressed and ends when the button is released. Chapter 22 has a sample program using InkCanvas.

The lowest-level WPF programming interface to the Tablet PC can be found in the System-Windows.Input.StylusPlugins namespace. Stylus plug-ins are classes that you write to render, modify, or store stylus input.

Between these two extremes is a collection of events defined by UIElement and ContentElement that begin with Stylus and PreviewStylus. These events are very similar to the mouse events, although the actual activity with the input device may be a little different. For example, a MouseDown event occurs when you press a mouse button; a StylusDown event occurs when you touch the stylus to the screen. The StylusButtonDown event refers not to pressing the stylus to the screen, but to the button on the side of the stylus that you press to change the meaning of the StylusDown event. (Perhaps with the stylus button depressed the stylus invokes a context menu.)

Here's a program that runs on the Tablet PC only. It creates two objects of type Polyline on receipt of a StylusDown event and adds these polylines to the Canvas child collection. (Like Ellipse, Polyline inherits from the Shape class.) The program captures the stylus and adds additional points to each of the two polylines during StylusMove events. The lines are terminated with a StylusUp event or a press of the Escape key.

The gimmick in this program is that the second polyline is drawn at an offset from the first one, resulting in a real-time drop-shadow effect. Notice that the Points collection of the foreground polyline always gets ptStylus while the shadow polyline gets ptStylus plus vectShadow.

ShadowTheStylus.cs

[View full width]

//------------------------------------------------ // ShadowTheStylus.cs (c) 2006 by Charles Petzold //------------------------------------------------ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.ShadowTheStylus { public class ShadowTheStylus : Window { // Define some constants for the stylus polylines. static readonly SolidColorBrush brushStylus = Brushes.Blue; static readonly SolidColorBrush brushShadow = Brushes.LightBlue; static readonly double widthStroke = 96 / 2.54; // 1 cm static readonly Vector vectShadow = new Vector(widthStroke / 4 , widthStroke / 4); // More fields for stylus-move operations. Canvas canv; Polyline polyStylus, polyShadow; bool isDrawing; [STAThread] public static void Main() { Application app = new Application(); app.Run(new ShadowTheStylus()); } public ShadowTheStylus() { Title = "Shadow the Stylus"; // Create a Canvas for window content. canv = new Canvas(); Content = canv; } protected override void OnStylusDown (StylusDownEventArgs args) { base.OnStylusDown(args); Point ptStylus = args.GetPosition(canv); // Create a Polyline with rounded ends and joins for the foreground. polyStylus = new Polyline(); polyStylus.Stroke = brushStylus; polyStylus.StrokeThickness = widthStroke; polyStylus.StrokeStartLineCap = PenLineCap.Round; polyStylus.StrokeEndLineCap = PenLineCap.Round; polyStylus.StrokeLineJoin = PenLineJoin.Round; polyStylus.Points = new PointCollection(); polyStylus.Points.Add(ptStylus); // Another Polyline for the shadow. polyShadow = new Polyline(); polyShadow.Stroke = brushShadow; polyShadow.StrokeThickness = widthStroke; polyShadow.StrokeStartLineCap = PenLineCap.Round; polyShadow.StrokeEndLineCap = PenLineCap.Round; polyShadow.StrokeLineJoin = PenLineJoin.Round; polyShadow.Points = new PointCollection(); polyShadow.Points.Add(ptStylus + vectShadow); // Insert shadow before all foreground polylines. canv.Children.Insert(canv.Children .Count / 2, polyShadow); // Foreground can go at end. canv.Children.Add(polyStylus); CaptureStylus(); isDrawing = true; args.Handled = true; } protected override void OnStylusMove (StylusEventArgs args) { base.OnStylusMove(args); if (isDrawing) { Point ptStylus = args.GetPosition (canv); polyStylus.Points.Add(ptStylus); polyShadow.Points.Add(ptStylus + vectShadow); args.Handled = true; } } protected override void OnStylusUp (StylusEventArgs args) { base.OnStylusUp(args); if (isDrawing) { isDrawing = false; ReleaseStylusCapture(); args.Handled = true; } } protected override void OnTextInput (TextCompositionEventArgs args) { base.OnTextInput(args); // End drawing with press of Escape key. if (isDrawing && args.Text.IndexOf('\ x1B') != -1) { ReleaseStylusCapture(); args.Handled = true; } } protected override void OnLostStylusCapture(StylusEventArgs args) { base.OnLostStylusCapture(args); // Abnormal end of drawing: Remove child shapes. if (isDrawing) { canv.Children.Remove(polyStylus); canv.Children.Remove(polyShadow); isDrawing = false; } } } }



One of the key statements in this program occurs towards the end of the OnStylusDown method when the shadow polyline is added in the middle of the Children collection of the Canvas:

canv.Children.Insert(canv.Children.Count / 2, polyShadow); 


In contrast, the foreground polyline is just added at the end:

canv.Children.Add(polyStylus); 


Keep in mind that the Children collection is rendered in order, so that all the shadows are drawn first, followed by all the foreground lines. This is an excellent example of the convenience of retained graphics. Readers interested in how programmers managed similar jobs back in the dark ages of Windows Forms and the Tablet PC API might want to look at my online article "In Search of the Real-Time Drop Shadow" (http://www.charlespetzold.com/etc/RealTimeDropShadow).

If you want to handle both mouse and stylus events such as InkCanvas, you'll probably need to distinguish whether a mouse event comes from actual mouse activity or is generated from the stylus. MouseEventArgs has a property named StylusDevice. If this property is null, the event came from the mouse. If it's an object of type StylusDevice, the event was generated by the stylus. StylusEventArgs has an identical property, so you can even identify a particular stylus device if the computer has more than one.

Much routine keyboard handling in a Windows Presentation Foundation application will probably be relegated to the TextBox and RichTextBox controls, or to controls such as ScrollViewer that can respond to some cursor movement keys. But there will be times when you'll need to look at keyboard input yourself.

An element or control is the source of a keyboard input event only if it has keyboard focusthat is, the IsKeyboardFocused property is true. Keyboard events are routed events, so all the ancestors in the tree of the focused element can participate in keyboard input. For these elements, the IsKeyboardFocusedWithin property is true.

To obtain keyboard focus, an element must have its Focusable property set to true. (Unlike the other keyboard-related properties, methods, and events, Focusable is defined by the FrameworkElement class.) The shift of input focus through the element tree is generally governed by the user clicking with the mouse or pressing the Tab or cursor-movement keys. (The Control class defines IsTabStop and TabIndex properties in connection with Tab navigation.) A program can set focus to a particular element by calling the Focus method for that element. A program can get more involved with focus navigation with the MoveFocus method defined by FrameworkElement.

When elements gain and lose keyboard focus, they generate GotKeyboardFocus and LostKeyboardFocus events. These are routed events, and both events also come in tunneling versions (PreviewGotKeyboardFocus and PreviewLostKeyboardFocus) so that all elements up the tree can become aware of a change in focus. The Source property of the event arguments always indicates the element gaining or losing the keyboard focus. The KeyboardFocusedChangedEventArgs class also defines two properties of type IInputElement named OldFocus and NewFocus.

Keyboard input is associated with three types of events that generally occur in this order:

  • KeyDown: event arguments of type KeyEventArgs

  • TextInput: event arguments of type TextCompositionEventArgs

  • KeyUp: event argument of type KeyEventArgs

If you press a shift key or a function key or a cursor movement key, no TextInput event is generated. If you type a capital A by pressing the Shift key and then the A key, you'll get two KeyDown events, a TextInput event, and two more KeyUp events as you lift your fingers from the keys.

KeyEventArgs includes a property named Key that identifies the key involved in the event. Key is a member of the massive Key enumeration, which contains more than 200 members, many of them referring to keys I've never seen and hope to never see.

The following program can help you understand keyboard events by displaying the properties of the event arguments accompanying KeyDown, TextInput, and KeyUp.

ExamineKeystrokes.cs

[View full width]

//-------------------------------------------------- // ExamineKeystrokes.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ExamineKeystrokes { class ExamineKeystrokes : Window { StackPanel stack; ScrollViewer scroll; string strHeader = "Event Key Sys-Key Text " + "Ctrl-Text Sys-Text Ime KeyStates " + "IsDown IsUp IsToggled IsRepeat "; string strFormatKey = "{0,-10}{1,-20}{2 ,-10} " + " {3,-10}{4,-15 }{5,-8}{6,-7}{7,-10}{8,-10}"; string strFormatText = "{0,-10} " + "{1,-10}{2,-10}{3 ,-10}"; [STAThread] public static void Main() { Application app = new Application(); app.Run(new ExamineKeystrokes()); } public ExamineKeystrokes() { Title = "Examine Keystrokes"; FontFamily = new FontFamily("Courier New"); Grid grid = new Grid(); Content = grid; // Make one row "auto" and the other fill the remaining space. RowDefinition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); grid.RowDefinitions.Add(new RowDefinition()); // Display header text. TextBlock textHeader = new TextBlock(); textHeader.FontWeight = FontWeights.Bold; textHeader.Text = strHeader; grid.Children.Add(textHeader); // Create StackPanel as child of ScrollViewer for displaying events. scroll = new ScrollViewer(); grid.Children.Add(scroll); Grid.SetRow(scroll, 1); stack = new StackPanel(); scroll.Content = stack; } protected override void OnKeyDown (KeyEventArgs args) { base.OnKeyDown(args); DisplayKeyInfo(args); } protected override void OnKeyUp (KeyEventArgs args) { base.OnKeyUp(args); DisplayKeyInfo(args); } protected override void OnTextInput (TextCompositionEventArgs args) { base.OnTextInput(args); string str = String.Format(strFormatText, args .RoutedEvent.Name, args.Text, args .ControlText, args.SystemText); DisplayInfo(str); } void DisplayKeyInfo(KeyEventArgs args) { string str = String.Format(strFormatKey, args .RoutedEvent.Name, args.Key, args.SystemKey, args .ImeProcessedKey, args.KeyStates, args.IsDown, args.IsUp, args .IsToggled, args.IsRepeat); DisplayInfo(str); } void DisplayInfo(string str) { TextBlock text = new TextBlock(); text.Text = str; stack.Children.Add(text); scroll.ScrollToBottom(); } } }



It's also possible to get keyboard information apart from the events. The Keyboard class has a static property named PrimaryDevice that is of type KeyboardDevice, and KeyboardDevice has methods to get the state of every key on the keyboard.

Several of the programs in the following two chapters show keyboard and mouse handling in the context of actual classes. The MedievalButton class in Chapter 10 and the RoundedButton class in Chapter 11 mimic the handling of mouse and keyboard input in normal buttons. The CalculateInHex program and ColorGrid control in Chapter 11 both illustrate common keyboard and mouse handling.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net