Saturday, September 20, 2008

Scale, Move and rotate controls in your GUI - Silverlight

In this blog post I will describe and share a custom control in Silverlight 2 that you can use to encapsulate other Silverlight-controls. This control will add functionality so that you can move, rotate and scale your Silverlight-controls with your mouse. It also adds a control that will rasie an event (Click-event) when clicked. You can download a sample project with the control at the bottom of the page.
image
In the image above the red square is the are that will raise a click-event when pressed. The outer squares at the other corners is controls for rotation and the inner squares are controls for scaling.

Problem
Many times when you design an application you do not only want to present information to a user, you also want the user to interact with your program and your controls. Sometimes this interaction involves moving, scaling and rotating an object. I missed a simple control to wrap other controls in that would add this.

How does it work?
A custom control in Silverlight and WPF is a control that contains some functionality and it is look-less. This means that you can change and switch the presentation of a control by changing its template. The base class chosen for this control is ContentControl and this base class is extremely powerful for encapsulating other controls (the content).
The functionality we want in this control is logic for clicking, moving, rotating and scaling. This includes listening to mouse-events of different kinds and the acting on those. What we need is some way of catching these events for the different type of transforms that we want to do.

<Grid x:Name="PART_TranslateControls">
<Rectangle Fill="Transparent" Cursor="Hand" />
</Grid>
<Grid x:Name="PART_ClickControls">
<Rectangle Fill="Red" Stroke="Black" Cursor="Arrow"/>
</Grid>
<Grid x:Name="PART_RotateControls">
<Rectangle Fill="DimGray" Stroke="Black"/>
<Rectangle Fill="DimGray" Stroke="Black"/>
<Rectangle Fill="DimGray" Stroke="Black"/>
</Grid>
<Grid x:Name="PART_ScaleControls">
<Rectangle Fill="LightGray" Stroke="DimGray"/>
<Rectangle Fill="LightGray" Stroke="DimGray"/>
<Rectangle Fill="LightGray" Stroke="DimGray"/>
</Grid>



The rectangles in the snippet above is only the appearance of the control and since we are listening to events from the panels we can set any look and feel of the clickable areas. Also not that we use the PART_ convention for creating custom controls.

In code behind, we add listeners for mousedown events for the different panels and calculates new position, rotation or size when the mouse is moved. The ContentControl contains a TransformGroup with a ScaleTransform, RotateTransform and a TranslateTransform which is the values that we are adjusting.

<TransformGroup>
<ScaleTransform x:Name="PART_Scale"/>
<RotateTransform x:Name="PART_Rotation" />
<TranslateTransform x:Name="PART_Translation" />
</TransformGroup>

We also have dependency properties that we can bind to in this control. The most beautiful solution for this would be to skip using PART_ for the transform and bind to the properties instead. The problem is that you can not bind to ScaleX, Angle, X etc. and therefore we need to get these parts in code-behind and then set the properties manually.

In the sample project below you can see all the mechanisms for this control.

image
Update 1: The sample code is now updated to Silverlight 2 RTW and I also added an sample of a transformable TextBox and functionality for ZIndex.
Update 2: The sample code has been updated. Hal9000Lives pointed out that the calculations for the rotation used the upper left corner as its center point. This has now been changed to use the middple point as center.
Code and sample project can be found here.

WPF-version of this control can be found here.

12 comments:

phempton said...

This does not work in Silverlight 2. Any thoughts as to why?

phempton said...

Found it...the generic.xaml file now belongs in a Themes folder off of the root directory. this is a change in Silverlight 2.0

Anders Bursjöö said...

The sample code is now updated (and with some extra goodies)! Thank you for the notice! /A

sad said...

This sample works great! One question I had was what if I wanted to create the new interaction control dynamically in the Page code-behind? The code below will create an area that can be transformed, but it's not really a button. Can I also create a new Button control and add it as a property to the new aButton object?

Example:
TransformInteraction.TransformInteractionControl aButton = new TransformInteraction.TransformInteractionControl();
aButton.Width = 80;
aButton.Height = 60;
aButton.Content = "Test";

Thanks!

Anders Bursjöö said...

Yepp, that is easy!
TransformInteraction.TransformInteractionControl aButton = new TransformInteraction.TransformInteractionControl();
aButton.Width = 80;
aButton.Height = 60;
Button myButton = new Button();
TextBlock myTextBlock = new TextBlock();
myTextBlock.Text = "Button text";
myButton.Content = myTextBlock;
aButton.Content = myButton;

Hal9000Lives said...

Nice example.

I have been trying to solve a similar rotation gizmo problem, so was pleased to see your example, however your rotation always appears to be about the top left and not around the centre point. To see this start a rotate then move the cursor around the original position of the top-left corner.

This seems to be because _rotation.CenterX/CenterY in GetDeltaAngle() are always zero.

Does anyone know can you correctly get the centre offset relative to an object's transformed position?

Anders Bursjöö said...

Hi, this sample is now a little bit old. You don't need to use the center calculation. Instead set RenderTransformOrigin="0.5,0.5" and that will make the control always rotate around its center. This controls is used in one of my sample project on codeplex (www.codeplex.com/greetingcreator) maybe that code is updated? Or maybe I will have some time to update this project :). /Anders

Anders Bursjöö said...

Maybe I answered a little bit too quickly. I will try to look into the code this weekend and try to find and solve the problem. /A

Hal9000Lives said...

Maybe a little too quick, but I realise I did not explain it too clearly :)

Your rather nice Greeting card project has the same problem. The point of rotation used by the ATAN2 calls is not the centre of the selected object, but rather the top left. CenterX/Y of a rotate transform do not work the way we might have expected.

I am looking at the various transform functions at the moment to find a consistent way to locate the object centre (relative to its parent object). If I find a fix I will post it here.

Thanks, Dave

Anders Bursjöö said...

Now the source code has been updated. Hal9000Lives, you were right that the calculation of the center always returned 0, this has been fixed. Thanks for the feedback!

Anonymous said...

this one is very nice example but when i am using add control in dynamic time like media element and image then create one problem is my medai element is show in behaind image so please give me soluation.

i am using silverlight 2.0

Peter said...

Hello,

I realize this is already an older project of yours, but would it be possible to turn scaling into sizing? Would it be possible to drag the corners and actually make the object larger without scaling it (so changing width and height) and how would one go about that?

Best Regards

Peter