In a comment on the original post of this series, Kollen Glynn points out that I could specify a bounding rectangle to the Invalidate() call in the Stroke method. Here's the code for that alteration:
// The event that this method handles is raised whenever
// a stroke is completed (the pen leaves the surface)
private void Stroke( object sender,
InkCollectorStrokeEventArgs e )
{
// Make sure we only invalidate the necessary region
Rectangle bounds = e.Stroke.GetBoundingBox();
// GetBoundingBox uses Ink Space coords, so convert
using( Graphics g = CreateGraphics() )
InkSpaceToPixel( g, ref bounds );
// The false here means that we don't refresh the
// control's children. You might want to use true
// if you are using InkOverlayAttachMode.InFront
Invalidate( bounds, false );
// For some reason, this must come after the call
// to Invalidate. Otherwise the stroke is not redrawn
if ( inkMode == InkMode.Highlight )
e.Stroke.ExtendedProperties.Add( HighlightGuid, inkMode );
}
I also wrote a helper method to deal with converting a Rectangle from Ink space to pixel space:
// Convert an Ink Space Rectangle to Pixel Space
private void InkSpaceToPixel( Graphics g, ref Rectangle rect )
{
// Cannot pass a reference of a property,
// so we need to make copies of each
Point topLeft = rect.Location;
Point bottomRight = rect.Location + rect.Size;
// Convert both temporaries to pixel space
ink.Renderer.InkSpaceToPixel( g, ref topLeft );
ink.Renderer.InkSpaceToPixel( g, ref bottomRight );
// Repackage our new values as a Rectangle
rect.Location = topLeft;
rect.Size = ( Size )bottomRight - ( Size )topLeft;
}
At this point it occurred to me that it would probably be better to only draw those strokes that fall within the clipping rectangle. In order to do this I've changed the painting code quite a lot, moving the actual drawing into two separate methods:
/// <summary>
/// Overrides <see cref="Control"/>.OnPaint.
/// </summary>
protected override void OnPaint( PaintEventArgs e )
{
// Doing all our redrawing here enables us to take
// full advantage of the automatic double buffering
base.OnPaint( e );
// Adjust for any change in the view position
HandleScroll();
// Draw all the strokes, clipped appropriately
DrawInk( e.Graphics, e.ClipRectangle );
// If you aren't selecting you can go home now :)
if ( inkMode != InkMode.Select )
return;
// Draw all selected ink, regardless of clipping
DrawSelectedInk( e.Graphics );
}
The method to draw selected ink strokes uses the same approach as before, except that I no longer bother with the second drawing operation if the stroke is a highlight. This is because the white centre is never visible for selected highlights anyway, so drawing it would be a waste of time.
// Draw all the selected ink. Does not bother with
// testing to see if strokes are in the clipping region
private void DrawSelectedInk( Graphics g )
{
Strokes strokes = ink.Selection;
Renderer render = ink.Renderer;
// Get the amount to increase size of selected lines
Point strokeExtra = new Point( 4, 4 );
render.PixelToInkSpace( g, ref strokeExtra );
// Redraw the selection strokes as they should look
for( int i = 0; i < strokes.Count; ++i )
{
Stroke stroke = strokes[i];
DrawingAttributes da = stroke.DrawingAttributes.Clone();
// Draw the extra thick stroke
da.IgnorePressure = true;
da.Width += strokeExtra.X;
da.Height += strokeExtra.Y;
render.Draw( g, stroke, da );
// The second draw doesn't show for highlights
// so there's no need to bother with it
ExtendedProperties ep = stroke.ExtendedProperties;
if ( ep.DoesPropertyExist( HighlightGuid ) )
continue;
// Draw the white center
da.Width -= strokeExtra.X;
da.Height -= strokeExtra.Y;
da.Color = Color.White;
render.Draw( g, stroke, da );
}
}
There are quite a few changes to the way I'm drawing normal ink strokes. Firstly, I'm not drawing selected strokes here anymore (since they get drawn afterwards anyway). Secondly, each stroke's bounding box is tested against the clipping rectangle to determine if it should be drawn:
// Draw all the non-selected ink. Tests each
// stroke to ensure it should be drawn
private void DrawInk( Graphics g, Rectangle clip )
{
Strokes strokes = ink.Ink.Strokes;
Strokes selection = ink.Selection;
Renderer render = ink.Renderer;
Rectangle bounds;
// Draw all the non-selected strokes
for( int i = 0; i < strokes.Count; ++i )
{
Stroke stroke = strokes[i];
// Don't bother drawing the selection here
if ( selection.Contains( stroke ) )
continue;
// Don't bother drawing if it's not needed
bounds = stroke.GetBoundingBox();
InkSpaceToPixel( g, ref bounds );
if ( !clip.IntersectsWith( bounds ) )
continue;
// Just draw the damn thing already!
render.Draw( g, stroke );
}
}
I have no idea if this testing against the clipping rectangle actually produces a performance increase or not. It certainly doesn't seem to hurt performance on my machine (which is a laptop, not a tablet), but you should probably try benchmarking or profiling to see if you need this yourself. I don't bother doing this for the selected strokes because there are generally fewer of them, and the testing is much harder (you have to compensate for the extra size).
As always, enjoy and let me know if there are problems with it.
EDIT: Corrected a nasty little error that got the size wrong in InkSpaceToPixel.