Large group of women exercising in the parkOne of the basic tenets of WYSIWYG is to be able to create a rubber band region using the mouse to give the user the ability to create a selectable region. This article demonstrates how to do that in any version of Silverlight and the user is given the tools to create such a bounding rectangle in any circumstances via the usage of some basic building blocks in Silverlight.

The below picture (not the one on the left) shows our goal, the dynamic creation of a bounding rubber band (a rectangle control for this demonstration) in a canvas. The below canvas is drawn in black (visually grey due to the opacity set) while the bounding rectangle shows itself in red.

RubberBanding

The user starts the process with an initial click which designates a start location of an upper left point for our bounding box with a mouse click.  Once the click happens the mouse cursor changes to a hand (if moving to the lower right) as a visible indicator that the process has started. While user continues to move to the lower right the band grows and while the mouse button has yet to be let go or released. For the demo a bounded number is shown is which relates the actual dynamic change in the X (horizontal) position. That is done for this article only and the above picture shows the rectangle with an X size of 123 pixels and can still be grown as shown by the hand icon.

Initial Xaml

In the page’s xaml we add a grid and a canvas as such this:

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
        <RowDefinition Height="33*" />
        <RowDefinition Height="267*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="32*" />
        <ColumnDefinition Width="568*" />
    </Grid.ColumnDefinitions>
    <Canvas x:Name="cMain"
            MouseLeftButtonDown="cMain_MouseLeftButtonDown"
            MouseLeftButtonUp="cMain_MouseLeftButtonUp"
            MouseMove="cMain_MouseMove"
            Grid.Row="1"
            Grid.Column="1">
        <TextBlock Width="100"
                    Height="30"
                    Text="{Binding XValue, ElementName=userControl}"
                    />
        <Rectangle Fill="Black"
                    Width="550"
                    Height="200"
                    Opacity=".25"/>
    </Canvas>
</Grid>

The things to note are that the canvas is in a grid which is offset from the initial by 30 pixels in both axis. We then place a rectangle on the canvas which is our area which will be our strike zone where we will intercept the messages mouse events. We have a strike zone because individual controls in a canvas, such as texblocks have whitespace between them and the canvas will not get the mouse clicks and we want to be able to capture the process at all locations on the canvas; hence we fill up the canvas with our strike zone as to be ensured that the events are consumed properly by the code. 

When the user clicks within the strike zone the process starts as signified by a cursor change to the hand, as the user moves within the zone the rubber banding increases and when the mouse button is let up the process ends and the cursor is returned to its previous state.

Initial Properties

Here are the three variables we will use during this process:

private int _xValue;

public int XValue
{
    get { return _xValue; }
    set
    {
        _xValue = value;
        OnPropertyChanged( "XValue" );
    }
}

private Point OriginatingPostionOnCanvas { get; set; }

private Rectangle RubberBandBox { get; set; }

Since the XValue property is reflected on the screen in a text box it reports all changes by using  INotifyPropertyChanged which our class handles in a standard way for notify property (not shown). Followed by that is our initial click location named OriginatingPostionOnCanvas which will dictate the upper left (or lower right) location of the rubber band rectangle which we will dynamically create. The last variable is the actual dynamic rectangle control named RubberBandBox which will be created and modified during the process.

Event Mouse Left Button Down

We will handle three events during this process. The first is the when the user clicks and begins to hold down the mouse button:

private void cMain_MouseLeftButtonDown( object sender, MouseButtonEventArgs e )
{
    OriginatingPostionOnCanvas = e.GetPosition( cMain );

    RubberBandBox = new Rectangle() { Width = 1, 
                                      Height = 1, 
                                      Fill = new SolidColorBrush( Colors.Red ), 
                                      Opacity = .1
                                    };

    RubberBandBox.SetValue( Canvas.LeftProperty, OriginatingPostionOnCanvas.X);
    RubberBandBox.SetValue( Canvas.TopProperty,  OriginatingPostionOnCanvas.Y );

    cMain.Children.Add( RubberBandBox );
}

Our goals here are simple:

  1. Get and store the location where the user clicked in relation to the canvas.
  2. Create  and store the rectangle in red and with an opacity which does allows the user to see what is being selected.
  3. Setting the initial location of the rectangle and adding it to the children of the canvas.

Event Mouse Move

The user is moving and we must adjust the rectangle as appropriate for the target direction. Note we also have to handle when the user moves to the upper left instead of the lower right…. So when that happens we will change the cursor to a Stylus (the dot) for visual indication that we are getting a reverse band situation.

private void cMain_MouseMove(object sender, MouseEventArgs e)
{
    if (RubberBandBox == null)
        return;

    Point pointMovedTo = e.GetPosition( cMain );

    double xDelta = pointMovedTo.X - OriginatingPostionOnCanvas.X;
    double yDelta = pointMovedTo.Y - OriginatingPostionOnCanvas.Y;

    LayoutRoot.Cursor = ((xDelta > 0) && (yDelta > 0)) ? Cursors.Hand : Cursors.Stylus;

    if (LayoutRoot.Cursor == Cursors.Hand)
    {
        RubberBandBox.Height = yDelta;
        RubberBandBox.Width  = xDelta;
    }
    else if (LayoutRoot.Cursor == Cursors.Stylus)
    {
        RubberBandBox.Height = Math.Abs( yDelta );
        RubberBandBox.Width = Math.Abs( xDelta );

        if (xDelta < 0)
            RubberBandBox.SetValue( Canvas.LeftProperty, pointMovedTo.X );

        if (yDelta < 0)
            RubberBandBox.SetValue( Canvas.TopProperty, pointMovedTo.Y );
    }

    XValue = (int) (e.GetPosition( cMain ).X - OriginatingPostionOnCanvas.X);
}
Explanation
Line 3 The mouse can move through the canvas during times we are not processing. We need to check that and only process when we have an actual rectangle on the canvas.
Line 6 Extract the current location in relation to the canvas.
Line 8-9 Get the change differences for x and y as named deltas.
Line 11 If the deltas retrieved are in the positive we have a drag to the lower right and that is designated by or specifying the cursor to a hand. If not, the deltas indicate that movement is upwards and to the left; regardless change the  cursor to the stylus.
Line 13 If we are in the hand state, we want to grow (or decrease) the rectangle in that direction towards the lower right.
Line 18 If we are in the stylus state, there is a negative growth either in the x or y axis. Handle the negatives while changing the height and width. Depending on which delta is negative, handle the new location position of the upper left hand position of our bounding rectangle which will follow the mouse.
Line 30 Inform the user of the current X axis location of the mouse whether positive or negative.

Event Mouse Left Button Up

This is our final event we have to handle. We do a safety check on whether we are actually in operations, and if we are then we simply return the cursor to its original state and remove the rectangle from the canvas’ children.

private void cMain_MouseLeftButtonUp( object sender, MouseButtonEventArgs e )
{
    if (RubberBandBox == null)
        return;

    // Return to the previous state; whatever it is, the OS handles it.
    LayoutRoot.Cursor = null; 

    cMain.Children.Remove( RubberBandBox );

    // Show that we are not processing by making this null.
    RubberBandBox = null; 
}
Share

Tags: , , ,