Wednesday, January 28, 2009

Creating a ColorFillToy in Silverlight - Part 2

Click here to view a demo of the application after part 2 of the tutorial/
Click here to download the source

In part 1 of this tutorial we focused on the creation of the XAML for the ColorFillToy application. In this part, we will create the code to set the cursor and to detect the FillArea that we need to fill.

Terence Tsang from shinedraw.com did the preparation for us, so we're going to reuse some of his code from his flash-vs-silverlight post on the ColorFillToy.

I'm also going to use the ColorPicker from the SilverlightContrib project. So we'll need a reference in our project for that.
As a little extra I'm going to create a ColorPickerButton control which triggers the ColorPicker and shows the selected color. Page Brooks was pretty excited about this and we're probably going to include it in the SilverlightContrib project as an addition to the ColorPicker control. I'll get around to this in the next and final part of this serie.

Finalizing the XAML

First of all we're going to finish up our XAML in Expression Blend by creating a ColorPickerButtonControl and creating an image which we'll use as our cursor. There is no bucket cursor available in Silverlight so we'll have to create one ourselves.

Using Expression Blend we can create a button looking a lot like the one Terence used in his Flash example.

First create the button on the LayoutRoot of the ColorFillToy. It's just a bunch of rectangles and paths making it look like this:


The most important thing of this control is the Rectangle (second object in our Canvas) named ButtonBackground. ButtonBackground is used to "fill" the button with the currently selected color of the ColorPicker.

Once we're pleased with the looks of the ColorPickerButtonControl we can right-click the container Canvas and select "Make Control...".
Now Blend will do most of the work for us. Create the control, create the reference and and instance of the control where we had our container Canvas.

The new Control will open in a new window in Blend. Close it and return to the Page.xaml.

Now lets not forget to add an image to the LayoutRoot canvas of 16x16 and select the paintbucket image. I placed the image right outside of the canvas so it won't be visible if the application loads.



This rounds up the design part.
Right-click the Page.xaml in Expression Blend and select "Edit in Visual Studio" to continue in VS2008.
Like I said earlier, we're going to use most of the code Terence created for us. You can download his code here: http://www.shinedraw.com/?dl_id=113

Making it work

Most of the important stuff is done in the RootVisual_MouseLeftButtonDown handler:

void RootVisual_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// generate random color
Random r = new Random();
byte red = (byte)(255 * r.NextDouble());
byte green = (byte)(255 * r.NextDouble());
byte blue = (byte)(255 * r.NextDouble());
Color color = Color.FromArgb(255, red, green, blue);

// get the ui
Border border = sender as Border;
Canvas canvas = (Canvas)border.Child;

// create ellipse
Ellipse ellipse = new Ellipse() { Fill = new SolidColorBrush(color), Width = DEFAULT_SIZE, Height = DEFAULT_SIZE };
ellipse.SetValue(Canvas.LeftProperty, e.GetPosition(canvas).X);
ellipse.SetValue(Canvas.TopProperty, e.GetPosition(canvas).Y);

// add to stage
canvas.Children.Add(ellipse);
ScaleTransform scale = new ScaleTransform() { CenterX = DEFAULT_SIZE / 2, CenterY = DEFAULT_SIZE / 2 };
ellipse.RenderTransform = scale;

// expand the ellipse
DoubleAnimation doubleAnimatoin1 = new DoubleAnimation() { From = 1, To = SCALE_MAX };
doubleAnimatoin1.Duration = new Duration(new TimeSpan(0, 0, 3));
Storyboard.SetTarget(doubleAnimatoin1, scale);
Storyboard.SetTargetProperty(doubleAnimatoin1, new PropertyPath("(ScaleX)"));

DoubleAnimation doubleAnimatoin2 = new DoubleAnimation() { From = 1, To = SCALE_MAX };
doubleAnimatoin2.Duration = new Duration(new TimeSpan(0, 0, 3));
Storyboard.SetTarget(doubleAnimatoin2, scale);
Storyboard.SetTargetProperty(doubleAnimatoin2, new PropertyPath("(ScaleY)"));

// start the story board
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(doubleAnimatoin1);
storyboard.Children.Add(doubleAnimatoin2);
storyboard.Begin();
}


All that is actually done in this code is the following:
- Generate a random color
- Get the border object that is clicked on
- Create an Ellipse and add it to the Canvas located inside the Border
- Use a Storyboard to expand the Ellipse

So most of this stuff is reusable. We're going to make the following changes to this code to make it work in our application:
- Instead of Border objects we're going to use a method to find the fillarea that was clicked on.
- The cursor paintbucket needs to be added when hovering over ColorFillToy.
- We need to finish up our ColorPickerButtonControl to expand the ColorPickerControl when it's clicked and set the background of the button to the selected color. This selectedcolor is used to fill the Ellipse instead of the randomized color

Finding the FillArea


To locate the parent FillArea, we have are going to use the MouseLeftButtonDown event of the FillArea Canvas or of the objects on the FillArea Canvas.
We can use a recursive method to go through the collection of user controls from top to bottom. The first FillArea we encounter will be the parent FillArea of the clicked object.
I created an extension method for the FrameWorkElement controls:

public static Panel FindFillArea(this FrameworkElement originalSource)
{
if (originalSource == null) throw new ArgumentNullException("originalSource");
if (originalSource.Name.IndexOf("fillarea") == 0 || originalSource == Application.Current.RootVisual)
return (Panel)originalSource;

FrameworkElement parent = (FrameworkElement)originalSource.Parent;
if (parent != null)
{
if (parent.Name.IndexOf("fillarea") == 0 || parent == Application.Current.RootVisual)
{
return (Panel)parent;
}
return FindFillArea(parent);
}

return (Panel)Application.Current.RootVisual;
}


We can use this extensionmethod in the MouseLeftButtonDownHandler.
Instead of the line:
Canvas canvas = (Canvas)border.Child;

We can use:
Panel panel = originalSource.FindFillArea();


Adding the paintbucket cursor


The add the paintbucket cursor add the following lines to the Loaded event of the control:

LayoutRoot.MouseMove += ColorFillToy_MouseMove;
cursor.IsHitTestVisible = false;
cursor.Visibility = Visibility.Collapsed;

And add the following MouseMove handler:

private void ColorFillToy_MouseMove(object sender, MouseEventArgs e)
{
cursor.Visibility = Visibility.Visible;
this.Cursor = Cursors.None;
Canvas.SetLeft(cursor, e.GetPosition(null).X - 10);
Canvas.SetTop(cursor, e.GetPosition(null).Y - 10);
}


By doing this the image will replace the normal cursor and it will appear to be a real cursor.

In this part we finished up the XAML, created an extension method to find the fillarea we need and filled the area with a random color.
In the next and final part we'll add the ColorPickerButton and the ColorPicker of the SilverlightContrib project and make sure we can pick any color we like.

Click here to view a demo of the application after part 2 of the tutorial/

Click here to download the source

Cheers!

Rob.

2 comments: