Tablet PC Programming under WPF
January 5, 2006
NYC
Avalon Tablet guy Shawn A. Van Ness noted that a certain WPF book already on the streets has nothing to say about Tablet PC programming, so he put together a tic-tac-toe using InkCanvas.
Because it handles rendering and stroke retention, InkCanvas certainly ranks as the high-level approach to collecting ink in WPF programs. WPF also has a facility to use stylus plug-ins, allowing the low-level stylus stuff we can do with the Tablet under WinForms and which I discussed in my MSDN article on Real-Time Stylus programming.
However, WPF also has a middle way that I've come to like very much. The UIElement class defines a whole bunch of stylus events (and related On methods and a few properties) that are very, very similar to mouse events. Thus, StylusEnter, StylusDown, StylusMove, StylusUp, StylusLeave, to name only the most common. These are all routed events, of course, so you have the same flexibility that you have with handling keyboard and mouse events. To use the stylus events, a program must be running on a Tablet PC and (very important) make a call to Stylus.Enable(). (Although the stylus generates mouse events as well as stylus events on the Tablet PC, the opposite is not true. The mouse does not generate stylus events on a non-Tablet PC. InkCanvas works with both the stylus and the mouse because it specifically traps both types of events.)
A year or so ago, I became obsessed with rendering drop-shadow on stylus input and wrote an online article about it. I have now returned to that crazy task for my WPF book and found the whole job much, much easier. As with most WPF graphics jobs, the minimal-redraw retained-graphics system helps out enormously.
The ShadowTheStylus.csproj and ShadowTheStylus.cs files were developed under the December CTP. The program creates a Canvas panel for the window content, and the window overrides the OnStylusDown, OnStylusMove, and OnStylusUp methods rather than installing event handlers. Because these are routed events, the canvas gets notified of the events first, and then the window if the canvas doesn't handle the event. (Actually, after you've drawn on the window, there will likely be various Polyline elements that have first dibs on these events.) And, of course, as with the keyboard and mouse, there are Preview versions of these events that begin at the window and go down the tree.
In response to an OnStylusDown call, the program creates two Polyline objects — one for the foreground stroke and the other for the shadow. These are given rounded ends and joins for smoother rendering. The two elements are then added to the Children collection of the Canvas. But for proper composite rendering, these children must be ordered in a specific manner. Children early in the collection get drawn first, and are possibly obscured by children later in the collection. For this program, the collection must contain all the shadows first, followed by all the foreground strokes.
The program manages this feat by inserting the shadow polyline in the middle of the collection (which will always have an even number of elements at this point):
canv.Children.Insert(canv.Children.Count / 2, polyShadow);
The foreground stroke can then go on the end:
canv.Children.Add(polyStylus);
Once that's done, everything else is a snap. Notice that the program captures the stylus by calling CaptureStylus (so the stylus can drift outside the window) and sets its own isDrawing flag to true. The OnStylusMove method simply adds an additional point each to the foreground and shadow polyline, and lets WPF worry about the flicker-free redraw. I've allowed the stroke to be aborted with a press of the Escape key, and the override of OnLostStylusCapture is then reponsible for removing the two polylines from the Children collection in that case.
And now, a confession: Because I much prefer programming on a 21" monitor with an ergonomic keyboard, I first wrote the program on my desktop machine to use the mouse, and then converted it to the stylus for my Tablet PC. The conversion wasn't quite a search-and-replace from "Mouse" to "Stylus" but it was nearly so, clearly demonstrating the similarity between the mouse and stylus event APIs.