Introduction
Recently I entered the Tablet PC contest hoping to win the big prize, but alas I had to settle for a lower placing. However, I did win a magazine subscription and Visual Studio .NET Enterprise so perhaps it was worth the effort. The application that I will talk about in this article is not one of my submitted entries from the contest, but one I thought of as I walked along some of my favorite restaurants on the streets of Manhattan, a dynamic menu white board. It is rather a simple application that allows you to list such things as lunch specials and have the colors on the board change randomly to produce an interesting flashing sign effect. The application has three modes: static, random, and sweep mode. Static mode lets you draw your menu just as you would see it at your local NY cafe. You can also choose different pen colors and blackboard background. Random mode chooses different colors for each stroke of the pen and sweep mode sweeps the board vertically with random color changes. I'm not sure its a practical application, because the tablet PC would need to be a slightly larger version to be propped up outside the busy sidewalks of Manhattan. Also, I'm not so sure I'd want to stick my $2000 tablet pc out in the street.
Figure 1 - Restaurant Signs
Design
The design of the restaurant sign consists of a simple Windows Form containing an InkPicture component. Also added to this design is a custom color combo box to allow you to choose a color without the effort of bringing up a dialog. Most of the activity in the board is carried out in a timer event handler. Every 1/2 second, the strokes in the InkPicture are updated with a new color if the program is in the random or sweep mode. If the application is in static mode, the color of the stroke is changed in the inkPicture_Stroke event handler which occurs after a stroke is drawn. In static mode the color is changed according to the color displayed in the ColorComboBox.
Figure 2 - UML Design Reverse Engineered using WithClass
The Application
The Restaurant Sign App let's you control its mode of behavior from the toolbar. The user can choose any one of the different modes: random, sweep, or static from the toolbar. Also the user has the ability to clear the board or only erase the last stroke. The Board button allows you to alter the background color of the board, so you can have a blackboard or a whiteboard.
Figure 3 - Menu at my Restaurant (You can eat there if you can read it!)
Coding for Ink
The Ink SDK contains several objects that makes coding with ink very easy. There is the Ink object itself which contains the strokes. These strokes can be created by the pen (or mouse) you use to draw the strokes or they can be created programmatically with the Ink object. Either way the Ink object holds the strokes. The Strokes collection in the Ink object contains individual strokes that you can manipulate through properties and methods. You can add and delete strokes from the Strokes collection and they will be added or deleted to your ink respectively.
You have pretty fine control over each individual stroke you draw through collections in the Ink. For example, if you want to change all the strokes in the InkPicture to red, you would do the following:
Listing 1- Changing all strokes to Red
foreach (Stroke stroke in inkPicture1.Ink.Strokes)
{
stroke.DrawingAttributes.Color = Color.Red; // Make it Red
}
inkPicture1.Refresh();
The Ink SDK allows you control over shape, position, transformation, hit testing, curvature, color, individual points, and every other aspect of the stroke that you drew. The SDK gives you this control by providing you with a slew of methods that are comprehensive and easy to utilize. Below are a few useful ones:
Method |
Description |
Rectangle GetBoundingBox() |
Gets the rectangle frame surrounding the stroke in ink coordinates |
Point[] GetPoints |
Gets the set of points making up the stroke |
Move(float xoffset, float yoffset) |
Moves a stroke by a particular offset |
Scale(float xscale, float yscale) |
Scales the stroke in 2 dimensions by a pair of factors |
Transform(Matrix m) |
Transforms a stroke (scales, rotates, moves a stroke) |
float NearestPoint(Point referencePoint, out distance) |
Returns the nearest interpolated stroke index on the stroke to a reference point. Also returns the shortest distance to the point on the storke |
bool HitTest(Point centerPoint, float radius) |
Returns true if the stroke falls within or intersects with a given circle specified by the center and radius |
SetPoint(int index, Point p) |
Sets a specific point in the stroke |
float[] FindIntersections(Strokes strokes) |
Finds the collection of indexes in the stroke that intersects all the strokes that are passed. If the parameter is null, it looks for all intersections in the Ink |
Table 1 - Useful methods in a stroke
Keep in mind that you are working in InkSpace (which is in HIMETRIC coordinates). If you want to bring your ink into the GDI+ Form coordinate world, you need to use the Renderer object. You can use the Renderer class in conjunction with a graphics surface to change a pixel from ink coordinates to Windows Form coordinates:
Listing 2 - Using the Renderer class to convert from Ink Space to Pixel Space inside a Form
Graphics graphics = CreateGraphics(); // get a graphics object from the form
inkPicture1.Renderer.InkSpaceToPixel(graphics, ref point); // convert the point from ink to pixel coordinates
g.Dispose(); // release the graphics resource
The Restaurant Sign Code
The restaurant sign uses the timer to execute one of its existing dynamic modes. Inside the timer event handler, we determine the mode we are in and call the appropriate mode function to change the color accordingly. The only mode we ignore in the timer event handler is the static mode, since this mode does not depend at all on the timer.
Listing 3 - The timer event handler for changing our stroke colors dynamically
private void timer1_Tick(object sender, System.EventArgs e)
{
switch (_mode)
{
case Mode.Random: // we are in random mode, change the color of all strokes randomly
ExecuteRandomMode();
break;
case Mode.Sweep: // we are in sweep mode, sweep colors horizontally
ExecuteSweepRandomMode();
break;
case Mode.Static: // don't need to do anything here
break;
default:
break;
}
if (_mode != Mode.Static) // don't need to refresh static mode
inkPicture1.Refresh();
}
ExecuteRandomMode is called when the program is in Random mode. This function behaves very much like Listing 1, except it randomly assigns a color to each stroke every 1/2 second.
Listing 4 - Creating random colored strokes
void ExecuteRandomMode()
{
// cycle through all strokes in the ink and assign a random color to the stroke
foreach (Stroke s in inkPicture1.Ink.Strokes)
{
Color aColor = FormRandomColor();
s.DrawingAttributes.Color = aColor;
}
}
Sweep Mode
Sweep mode is a little more sophisticated because it looks each time at the vertical counter and sees if the stroke falls within the boundaries of the sweep before it decides to change the color of the stroke:
Listing 5 - Using Sweep mode to determine the color of the stroke
int _sweepCounter;
void ExecuteSweepRandomMode()
{
_sweepCounter += 200; // increment the sweep counter
Color aColor = FormSweepColor();
foreach (Stroke s in inkPicture1.Ink.Strokes)
{
if (SweepingStroke(s, _sweepCounter)) // check if the stroke falls within the vertical position of the sweep
{
s.DrawingAttributes.Color = aColor;
}
}
}
bool SweepingStroke(Stroke s, int counter)
{
Rectangle rtStroke = s.GetBoundingBox();
Rectangle rtAll = inkPicture1.Ink.GetBoundingBox(); // See if stroke falls within the bounding box of the sweep
if ((counter % rtAll.Height) + rtAll.Top > rtStroke.Top)
return true; // it does, return true
return false; // it doesn't, return false
}
Static Mode
In Static mode we need a way to change the color stroke after it is drawn. We do this by intercepting the Stroke event. After the stroke is drawn by the pen, the stroke event is triggered and we can do our color changing in the stroke event handler. In the event handler, we simply assign the stroke drawing attribute color to the chosen combo color and refresh the stroke.
Listing 6 - Assigning the stroke a color from the color combo box in the stroke event handler
private void inkPicture1_Stroke(object sender, Microsoft.Ink.InkCollectorStrokeEventArgs e)
{
// only muck with the stroke event if we are in static mode.
// In the dynamic modes, we change colors on the timer event
if (_mode == Mode.Static)
{
_drawingColor = colorComboBox1.ChosenColor; //assign the color to the stroke from our owner drawn combo box
e.Stroke.DrawingAttributes.Color = _drawingColor;
inkPicture1.Refresh(); // refresh the stroke
}
}
Conclusion
The Tablet PC Ink SDK opens the PC up to a possible new paradigm. It probably won't be long before we are taking notes in meetings and classrooms with this cool device. Also, I suspect the tablet pc will serve as a useful device for everything from architecting buildings to designing cartoons. Time to get in on the ground floor and ink-up some new ideas with a # pen!