Drawing a RubberBand in WPF

In C#, WPF by timfanelli1 Comment

Overview

Rubber-banding is a very simple and familiar concept in most graphical applications where the outline of a shape to be drawn is painted to the screen, following the mouse cursor so the end user can visualize exactly where the shape will be placed.

This tutorial will guide you through implementing a very simple WPF window containing a canvas, with rubber-banding rectangles. Adding additional shapes is then a trivial matter, which I’ll address in a follow up to this post.

Main Window

The first thing we’ll do is set up a simple window structure with a toolbar containing buttons for the various shapes, and a canvas on which we’ll paint our rubberband. (For now, we’re only addressing rectangles, and will ignore the toolbar all together… but I’m putting in place for the follow up post) The XAML to describe this structure looks like this:

<Window x:Class="Rubber_Band_Tutorial.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Rubber_Band_Tutorial" Height="300" Width="300"
    >
	<DockPanel LastChildFill="True">
		<ToolBarTray DockPanel.Dock="Top" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
			<ToolBarTray.Resources>
				<Style TargetType="{x:Type Button}">
					<Setter Property="Margin" Value="1,0,1,0"/>
				</Style>
			</ToolBarTray.Resources>

			<ToolBar Padding="2">
				<GroupBox Padding="2,5,2,0" Margin="0,0,3,0">
					<ToolBarPanel Orientation="Horizontal">
						<Button x:Name="shapeButtonSelect">
							Select
						</Button>
						<Button x:Name="shapeButtonRectangle">
							<Rectangle Margin="2" Width="13" Height="9" Stroke="Black"/>
						</Button>
						<Button x:Name="shapeButtonSquare">
							<Rectangle Margin="2" Width="13" Height="13" Stroke="Black"/>
						</Button>
						<Button x:Name="shapeButtonEllipse">
							<Ellipse Margin="2" Width="13" Height="9" Stroke="Black"/>
						</Button>
						<Button x:Name="shapeButtonCircle">
							<Ellipse Margin="2" Width="13" Height="13" Stroke="Black"/>
						</Button>
						<Button x:Name="shapeButtonLine">
							<Line Margin="2" X1="0" Y1="0" X2="13" Y2="13" Stroke="Black"/>
						</Button>
					</ToolBarPanel>
				</GroupBox>
			</ToolBar>
		</ToolBarTray>

		<Canvas Background="White" x:Name="canvas"/>
	</DockPanel>
</Window>

IMPORTANT: For some strange reason, if you do not set the background of your canvas, the event handlers we register with it in the following section do not work. It’s almost as if the canvas disables it’s mouse events in the absence of an explicit Background color… this makes no sense to me, if someone knows why this would be, please post it in the comments.

Functionality Overview

Once we have our main window structure, we’ll need to attach several mouse handlers to paint the rubberband. We’ll need to retain the point at which the left mouse button is depressed, track the mouse cursor as it moves, and finally remove the rubber band from the canvas when the mouse button is released. Therefore, we’ll implement handlers for the canvas’ MouseLeftButtonDown, MouseMove, and MouseLeftButtonUp events.

To get setup, we’ll create some event handlers and register them with our canvas object in the main window’s code-behind file.

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Shapes;

namespace TimFanelli.WPF.Tutorials
{
    public partial class Window1 : System.Windows.Window
    {
        public Window1()
        {
            InitializeComponent();

            canvas.MouseLeftButtonDown += OnLeftDown;
            canvas.MouseLeftButtonUp += OnLeftUp;
            canvas.MouseMove += OnMouseMove;
        }

        protected void OnLeftDown(object sender, MouseEventArgs args)
        {
        }

        protected void OnLeftUp(object sender, MouseEventArgs args)
        {
        }

        protected void OnMouseMove(object sender, MouseEventArgs args)
        {
        }
    }
}

Mouse Left Button Down

The canvas object raises its MouseLeftButtonDown event when the user presses the left mouse button over it. In this handler, we’ll want to store the point at which this event occurred, as it will be used as one corner of the rectangle to which our shape is bound… so we’ll add a private Point member to our window class, and implement the OnLeftUp function as follows:

        private Point mouseLeftDownPoint;
        protected void OnLeftDown(object sender, MouseEventArgs args)
        {
            if (!canvas.IsMouseCaptured)
            {
                mouseLeftDownPoint = args.GetPosition(canvas);
                canvas.CaptureMouse();
            }
        }

This is the simplest of the handlers we’ll write… however it does several very important things… first and foremost, it obtains the point of the event on the canvas. The call to args.GetPosition() takes the UI element we’d like our Point in relation to – so by passing in the canvas, we obtain the X/Y coordinates in relation to (0,0) being the top-left corner of the canvas itself (as opposed to the containing dock panel, or window, or even the screen as a whole). Second, and equally as important, it causes the canvas to capture the mouse. This is done so that any mouse events that occur will be directed to our canvas, regardless of the mouse cursor’s position on the screen. Notice that this is all done only if the canvas does not currently have mouse capture – we perform this check simply as a precaution, if somehow MouseLeftButtonDown happens twice, we don’t want to perform these steps twice. We’ll release mouse capture in OnLeftUp.

Mouse Move

The Mouse Move handler is where we’ll actually create, draw and update our rubberband shape.

One very powerful feature of WPF is that when a UI element is added to a content container, you can affect it’s appearance simply by modifying that element’s properties… there’s no need to redraw the element or post paint events to an event-queue, all this is done transparently for you. So we’ll add a private Shape member to the window class and implement our OnMouseMove handler:

        private Shape rubberBand = null;
        protected void OnMouseMove(object sender, MouseEventArgs args)
        {
            if (canvas.IsMouseCaptured)
            {
                Point currentPoint = args.GetPosition(canvas);

                if (rubberBand == null)
                {
                    rubberBand = new Rectangle();
                    rubberBand.Stroke = new SolidColorBrush(Colors.LightGray);
                    canvas.Children.Add(rubberBand);
                }

                double width = Math.Abs(mouseLeftDownPoint.X - currentPoint.X);
                double height = Math.Abs(mouseLeftDownPoint.Y - currentPoint.Y);
                double left = Math.Min(mouseLeftDownPoint.X, currentPoint.X);
                double top = Math.Min(mouseLeftDownPoint.Y, currentPoint.Y);

                rubberBand.Width = width;
                rubberBand.Height = height;
                Canvas.SetLeft(rubberBand, left);
                Canvas.SetTop(rubberBand, top);
            }
        }

Notice we only handle mouse move if the canvas has mouse capture. This is effectively our way of asking “did the user press the left mouse button down” – and since we’ll release mouse capture in OnLeftUp, it’s more specifically “does the user have the left button held down?”

To place the shape properly, we calculate it’s width and height, as well as the position for it’s top-left corner.

The width and height calculations are the absoluate value of the difference between the left-down and current point’s x and y coordinates. Remember that since (0,0) is the top left corner of the canvas, if the user drags a shape out to the right, then the difference between left-down’s X and current point’s X would be negative. Similarly, the top left corner is placed at the minimum X and minimum Y coordinates of the rectangle defined by the two mouse points.

Notice that the shape instance is only aware of it’s width and height. The placement on the canvas is done using two static methods of the Canvas class, SetLeft and SetTop.

Mouse Left Button Up

The final step is to remove the rubberband when we release the left mouse button. This is also relatively simple:

        protected void OnLeftUp(object sender, MouseEventArgs args)
        {
            if (canvas.IsMouseCaptured && rubberBand != null)
            {
                canvas.Children.Remove(rubberBand);
                rubberBand = null;

                canvas.ReleaseMouseCapture();
            }
        }

All this handler does is remove the rubberband shape from the canvas, null out the rubber band so it’ll properly be recreated later, and release the mouse capture. Effectively, we’re just putting the world back the way we found it.

If this were a more full-fledged application, your mouse left button up would also have code that placed the a new shape instance on your canvas in the same position as the rubberband.

In my next post, we’ll refactor this slightly and then add support for rubberbanding elipses and lines as well. The third part of this tutorial will address circles and squares

Comments

  1. Pingback: Adding Custom Controls to XAML | Zero Byte, LLC

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.