Posted Dec 3, 2006 7:19:05 PM
It's often useful to either extend WPF controls or create custom controls to supplement to functionality provided to you by the WPF classes. In this brief tutorial, we'll refactor the code from the first part of the Drawing a RubberBand in WPF tutorial and see how we can add use a custom class in an XAML document.
Refactoring
We've already built a simple window class that contains a System.Windows.Controls.Canvas instance, and registered several mouse handlers with it to draw the rubber band. Instead of instantiating a base Canvas instance though and telling it how to behave (i.e., by registing external handlers), we'll extend the Canvas class, and move the handlers inside it so that'll it'll know how to behave.
The first step is to create a new User Control (WPF) type to our project. This will create an XAML document and associated code-behind file for a class that extends UserControl. Simply changing references to UserControl to Canvas will result in a class that extends Canvas, like so:
RubberBandingCanvas.xaml:<Canvas x:Class="Rubber_Band_Tutorial.RubberBandingCanvas" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Background="White"> </Canvas>RubberBandingCanvas.xaml.cs:public partial class RubberBandingCanvas : Canvas { public RubberBandingCanvas() { InitializeComponent(); } }
As you can see, the XML root element in our XAML is a Canvas type, which matches the base-class type specified in our partial class definition in the code-behind. Also notice the addition of the Background="White" attribute to the canvas in the XAML... recall from part 1 that a canvas with no background does not respond to mouse event handlers (still haven't figured that one out, btw).
The next thing to do is move our mouse handlers out of the Window class, and into our new Canvas-derived RubberBandingCanvas. I'll also update the methods to be static, which'll help minimize the application footprint if I were to allow use of multiple canvases.
RubberBandingCanvas.xaml.cs:
public partial class RubberBandingCanvas : Canvas
{
public RubberBandingCanvas()
{
InitializeComponent();
this.MouseLeftButtonDown += OnLeftDown;
this.MouseLeftButtonUp += OnLeftUp;
this.MouseMove += OnMouseMove;
}
private Point mouseLeftDownPoint;
private Shape rubberBand = null;
protected static void OnLeftDown(object sender, MouseEventArgs args)
{
RubberBandingCanvas canvas = sender as RubberBandingCanvas;
if (!canvas.IsMouseCaptured)
{
canvas.mouseLeftDownPoint = args.GetPosition(canvas);
canvas.CaptureMouse();
}
}
protected static void OnLeftUp(object sender, MouseEventArgs args)
{
RubberBandingCanvas canvas = sender as RubberBandingCanvas;
Shape rubberBand = canvas.rubberBand;
if (canvas.IsMouseCaptured && rubberBand != null)
{
canvas.Children.Remove(rubberBand);
rubberBand = null;
canvas.ReleaseMouseCapture();
}
}
protected static void OnMouseMove(object sender, MouseEventArgs args)
{
RubberBandingCanvas canvas = sender as RubberBandingCanvas;
if (canvas.IsMouseCaptured)
{
Point currentPoint = args.GetPosition(canvas);
Point mouseLeftDownPoint = canvas.mouseLeftDownPoint;
if (canvas.rubberBand == null)
{
canvas.rubberBand = new Rectangle();
canvas.rubberBand.Stroke = new SolidColorBrush(Colors.LightGray);
canvas.Children.Add(canvas.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);
canvas.rubberBand.Width = width;
canvas.rubberBand.Height = height;
Canvas.SetLeft(canvas.rubberBand, left);
Canvas.SetTop(canvas.rubberBand, top);
}
}
}
The code is almost identical to the handlers we wrote last time, with the exception that member variables are resolved against the canvas object that raised the event (i.e., the sender paramter of the handler method).
Adding a Custom Class to a XAML
The only thing left to do is to update my main window's XAML to use the new RubberBandingCanvas class instead of the WPF's base Canvas class.
This is a simple two step process... the first is to add an XML Namespace declaration that will map my CLR-Namespace to an XML namespace, effectively giving me access to my class names as valid XML element names.
Window1.xaml:
<Window x:Class="TimFanelli.WPF.Tutorials.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Rubber_Band_Tutorial;assembly="
Title="Rubber_Band_Tutorial" Height="300" Width="300"
>
<DockPanel LastChildFill="True">
<ToolBarTray DockPanel.Dock="Top" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
Removed for brevity...
</ToolBarTray>
<local:RubberBandingCanvas/>
</DockPanel>
</Window>
The first bold line give me access to public members of the Rubber_Band_Tutorial clr-namespace via the local xml namespace. Then to add a RubberBandingCanvas to the windows dock-panel, I simply add it the same way I would any other element.
Conclusion
Most of this post was concerned with setting up the derived class we used, however the real goal was to add my custom class to the Window1 XAML document, which you can see if a very simple task. This method will work for not only any custom or derived controls you write, but indeed any CLR object with a default constructor.
It's also worth noting, since we didn't use the feature explicitly, any public CLR property (or dependency property) you expose in your objects can be set using standard XML syntax as well, with the property name used as an xml-attribute name on the element in question.
Posted Nov 24, 2006 5:54:55 PM
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.
Posted Nov 7, 2006 7:15:33 PM
Microsoft has officially released the .NET Framework 3.0! This release comes with a handful of new technologies that I'm pretty pumped about, particularly WPF - The Windows Presentation Foundation. WPF is, to greatly oversimplify it, their new framework for developing Windows GUI applications.
I've been teaching WPF this semester at Clarkson and am really thrilled with its capabilities. It's by far the most well thought out and put together GUI API I've had the pleasure of using. Now that I've got my website back up and running, I do plan to post several articles and projects in WPF so you all can get started too.
You can find download links for the framework, SDKs, and visual Studio extensions on this blog post on the .NET FX3 website.
add to del.icio.us