Sunday, September 21, 2008

Scale, Move and rotate controls in your GUI - WPF

In this blog post I will describe and share a custom control in WPF (Windows Presentation Foundation) that you can use to encapsulate other WPF-controls. This control will add functionality so that you can move, rotate and scale your WPF-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 a control that will raise a click-event. The outer gray squares are for rotation and the inner ones are for scaling. In the middle the control can be moved (translated).

Background
In a previous post I did this control in Silverlight. This is the WPF version and I have made some adjustments and used some other solutions in this version. These will be described below.

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?
Feel free to compare this solution with the Silverligh version. To start with, this is a control that can be used as a container for other controls. This means that you do not have to modify any other code in your existing controls. This control just adds the ability to interact (move, scale and rotate) with your control.
To do this we need a bock of Transforms in our custom control:

<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Scale}" ScaleY="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Scale}" />
<RotateTransform Angle="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Angle}"/>
<TranslateTransform X="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=X}" Y="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Y}"/>
</TransformGroup>
</Grid.RenderTransform>

This is the first different to the Silverlight version. In Silverlight we used the PART_-convension and set the ScaleX, Angle etc. in code behind through these variables. We still have dependency properties in code behind but we don't need to have the reference to the Transforms. Instead we use a binding to our properties. The RelativeSource property is something that doesn't exist in Silverlight and therefore we need the PART_-solution in that example. In WPF we can solve the problem by using a binding and in that way we doesn't restrict us as much in the template. It also results in less code in code behind (a lot of code/logic is removed) and that will make this control simpler and easier to maintain.

The templates are much the same in WPF/Silverlight. One other difference than the binding is that the VisualStateManager doen't exist in WPF (but I think it will be implmeneted in future versions). I love the VisualStateManager and its separation between control and template. In WPF we instead put all the interaction controls in a Panel and then set the Visibility of that panel in code behind.

To get all the interaction working we hook up on mouse events in both our interaction controls (panels) and also in our base the custom control (inherited from ContentControl). When I first translated from Silverlight to WPF the whole control froze after the first interaction. The listeners for mouse event looks like this:
private void OnScaleControlsMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_previousPosition
= e.GetPosition(this);
this.CaptureMouse();
_iteractionMode
= InteractionMode.Scaling;
}

We call CaptureMouse() and in Silverlight this will be automatically released then the mouse button is released. In WPF we need to do this manually by setting:
private void OnControlMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_iteractionMode
= InteractionMode.None;
Mouse.Capture(
null);
}

With Mouse.Capture(null) the mouse will be released.

WPF also have more Cursors that we can use. We use more common cursors for moving and sizing which makes the interaction more intuitive.

image 
Code and sample project can be found here.

4 comments:

Anonymous said...

Very nice, thank you for sharing. Any way to make the wrappers background transparent instead of pink? And, can we still use the buttons as buttons after we have wrapped them with this control, as I can't seem to fire off events from my button now (but I am only 10 hours into WPF!)

Anders Bursjöö said...

Hi, yes you can restyle your transforminteraction control to pass throug event to the underlying control. I have made an example of that in the source code here: http://www.codeplex.com/dotwaywpf/SourceControl/ListDownloadableCommits.aspx.
There where another request about this that you can read about here: http://www.codeplex.com/dotwaywpf/Thread/View.aspx?ThreadId=38343
I hope this will help you!
/Anders

Unknown said...

How can you make the keyboard arrow keys use the MouseMove event? I want to be able to use both the mouse and the keyboard to move the image around when the cross hairs are visable?

Unknown said...

Hi Anders
The article is fantastic.Using your article if i place a line control and resize the same at runtime can i get the latest coordinates of the line control.
Any help greatly appreciated