![]() |
Platforms, Frameworks & Libraries »
Windows Presentation Foundation »
Controls
Intermediate
License: The Code Project Open License (CPOL)
WPFDesignerBy Victor HurdugaciA WPF UserControl on which you can add elements and move/resize them |
C# (C# 1.0, C# 2.0, C# 3.0)WinXP, Vista, WPF, Dev, Design
|
||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
This article describes how to create a Windows Presentation Foundation (WPF) UserControl
on which you can add elements and move/resize them.

Because WPF doesn't have a designer component by default - something that allows to add controls on it and move/resize them - one must be created. There are many solutions for this problem but as far as I know there is no one that provides the ability to add/remove controls without manually wrapping them in custom control.
WPF Designer is basically a canvas on which you can add anything that derives from
FrameworkElement.
When a control is added, it is not placed directly on the canvas but instead is
wrapped in another container. The container has all the handles and logic for moving
and resizing.
The end is not disturbed by this control because the designer exposes all the controls
in it as a FrameworkElement collection.
If an element that is added in the designer has a fixed dimension (width/height) there will be no handles for that one. Min/max limits are also treated so you cannot resize a control more that its initial specifications.
The inner control is placed in a ContentPresenter holder.

One of the big questions I had to answer when developing this control was "should I use tunneling of bubbling when clicking the control?". Tunneling is when an event is routed from parent to child and bubbling is the opposite.
Because in WPF an event can be marked as Handled I've decided to use tunneling (events that contain "Preview" in their names). For example when the user clicks an element on the canvas, the first one who receives the event is the canvas and after that is sent to the element. This is good because there is no need of another event on element click. So when the canvas receives this event, it deselects all elements. If one element also receives it than it selects itself.
This solutions uses a few converters so they need to be explained before going deeper in the code. The two converter below are used on visibility binding of the resize handles. For a better understanding the resize handles will be numbered from 1 to 8.
1 2 3 8 4 7 6 5
They also have names related to they position.
1 = TopLeft
2 = Top
3 = TopRight
...
8 = Left
VisibilityConverter
This is a one way converter that converts a boolean value to a visibility specificator
like this: true = Visible and false = Collapsed.
class VisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
bool b = (bool)value;
return b ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
MultiVisibilityConverter
This is a one way multiconverter that converts a boolean value to a visibility
specificator, just like the one above, but also takes in account other values. It
is needed because you cannot bind the "ConvertParameter" field of a converter to
something. So this is like a converter with more parameter that are binded to something.
It takes two or three values: the first one is the visibility value that can be
true or false and the other two specify if the control
can resize vertically and/or horizontally.
For example resize handle 1 will be displayed only if the element can resize both
horizontally and vertically. Let's say that the first parameter is true
- this means that we should display the handle - and the other two parameter are
binded on two properties called CanHResize and CanVResize
which return true and false. Theoretically we should display
the handle because the first parameter is true but when we check the
other two we see that the element cannot resize vertically so handle 1 is not needed.
class MultiVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if ((bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
return Visibility.Visible;
bool b1 = (bool)values[0];
bool b2 = false;
if (values.Length >= 2)
b2 = (bool)values[1];
if (values.Length == 3)
b2 = b2 && (bool)values[2];
if (!b2)
return Visibility.Collapsed;
return b1 ? Visibility.Visible : Visibility.Collapsed;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
The list below shows what parameters (only the second and third) are needed for
each handle:
1 - CanHResize | CanVResize
2 - CanVResize | -
3 - CanHResize | CanVResize
4 - CanHResize | -
5 - CanHResize | CanVResize
6 - CanVResize | -
7 - CanHResize | CanVResize
8 - CanHResize | -
This component wraps any element from the canvas. It provides support for resize and movement of elements.
The XAML part provides the aspect. There are three styles SelectionThumbStyle,
code RoundThumbStyle and SquareThumbStyle and nine thumbs
(eight for resize handles and one for movement).
SelectionThumbStyle
This style defines the aspect of the selection thumb. It is the blue border that surrounds the selected element.
<Style x:Key="SelectionThumbStyle" TargetType="{x:Type Thumb}"> <Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Thumb}"> <Border BorderBrush="#FF385D8A" Background="#00000000" BorderThickness="2,2,2,2"/> </ControlTemplate> </Setter.Value> </Setter> </Style>
RoundThumbStyle
This style defines the aspect of the round resize thumbs (the ones from corners). It is a round border with stroke and gradient fill.
<Style x:Key="RoundThumbStyle" TargetType="{x:Type Thumb}"> <Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Thumb}"> <Border BorderThickness="1,1,1,1" CornerRadius="4,4,4,4" BorderBrush="#FF7D8289"> <Border.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FFF6FFFF" Offset="0"/> <GradientStop Color="#FFCAEAED" Offset="0.45"/> <GradientStop Color="#FFCAEAED" Offset="0.5"/> <GradientStop Color="#FFCAEAED" Offset="0.55"/> <GradientStop Color="#FFF6FFFF" Offset="1"/> </LinearGradientBrush> </Border.Background> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
SquareThumbStyle
Same as RoundThumbStyle just for the square buttons.
<Style x:Key="SquareThumbStyle" TargetType="{x:Type Thumb}"> <Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Thumb}"> <Border BorderThickness="1,1,1,1" CornerRadius="0,0,0,0" BorderBrush="#FF7D8289"> <Border.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FFF6FFFF" Offset="0"/> <GradientStop Color="#FFCAEAED" Offset="0.45"/> <GradientStop Color="#FFCAEAED" Offset="0.5"/> <GradientStop Color="#FFCAEAED" Offset="0.55"/> <GradientStop Color="#FFF6FFFF" Offset="1"/> </LinearGradientBrush> </Border.Background> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
Thumbs
There are nine thumbs. Eight for resize, one for every corner and side, and one for movement - as big as the inner control.
The SelectionThumb is the thumb responsible for movement. The movement
offset is provided by the DeltaDrag event. It's Visibility
property is binded on the IsSelected property from the user control
that contains the thumb - that's why the ancestor is searched. Also the visibility
converter is used for the return value of the property.
<Thumb x:Name="SelectionThumb" Style="{DynamicResource SelectionThumbStyle}" DragDelta="SelectionThumb_DragDelta" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Cursor="SizeAll" Visibility="{Binding Path=IsSelected, Converter={StaticResource visibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
The resize handle thumbs are basically the same. Even though similar, they have a major difference: the visibility binding. As previously explained we pass more arguments to the converter. Because this is handle no. 1 (TopLeft) it takes three arguments, values from properties in the ancestor UserControl.
<Thumb x:Name="TopLeftThumb" Style="{DynamicResource RoundThumbStyle}" DragDelta="Thumb_DragDelta" HorizontalAlignment="Left" VerticalAlignment="Top" Width="10" Height="10" Margin="-5,-5,0,0" Cursor="SizeNWSE" > <Thumb.Visibility> <MultiBinding Converter="{StaticResource multiVisibilityConverter}"> <Binding Path="IsSelected" RelativeSource="{RelativeSource AncestorType={x:Type UserControl}}" /> <Binding Path="CanHResize" RelativeSource="{RelativeSource AncestorType={x:Type UserControl}}" /> <Binding Path="CanVResize" RelativeSource="{RelativeSource AncestorType={x:Type UserControl}}" /> </MultiBinding> </Thumb.Visibility> </Thumb>
The code behind - DesignerComponent.xaml.cs
There is one dependency property, IsSelected. This needs to be dependency
property in order to reflect changes on value change.
private static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected", typeof(bool), typeof(DesignerComponent), new FrameworkPropertyMetadata(false));
public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
The minHeight,minWidth,maxHeight and maxWidth
keep the minimum/maximum values of the control. If not specified by the content
they are set to 5.0 for minimum and double.PositiveInfinity. The 5.0
value is necessary because on less than 5 pixels the control is unusable. The two
properties, CanVResize and CanHResize, tell wether the
control can resize on a particular axis.
private readonly double minHeight;
private readonly double minWidth;
private readonly double maxHeight;
private readonly double maxWidth;
public bool CanVResize { get; private set; }
public bool CanHResize { get; private set; }
The constructor is just one. It takes as only parameter the FrameworkElement
that will be displayed on canvas. Before setting the content, the FrameworkElement's
properties are check to determine if it can resize and/or has a minimum/maximum.
Also the position on canvas is determined. size limit.
public DesignerComponent(FrameworkElement content)
{
this.InitializeComponent();
if (!double.IsNaN(content.Width))
{
CanHResize = false;
this.Width = content.Width;
}
else
{
CanHResize = true;
this.Width = 23.0;
}
if (!double.IsNaN(content.Height))
{
CanVResize = false;
this.Height = content.Height; ;
}
else
{
CanVResize = true;
this.Height = 23.0;
}
minWidth = content.MinWidth < 10.0 ? 10.0 : content.MinWidth;
minHeight = content.MinHeight < 10.0 ? 10.0 : content.MinHeight;
maxWidth = content.MaxWidth;
maxHeight = content.MaxHeight;
double top = (double)content.GetValue(Canvas.TopProperty);
if (double.IsNaN(top))
top = 0.0;
double left = (double)content.GetValue(Canvas.LeftProperty);
if (double.IsNaN(left))
left = 0.0;
SetValue(Canvas.TopProperty, top);
SetValue(Canvas.LeftProperty, left);
//Set the actual content. Note that "Content" property is a new property. See below
this.Content = content;
}
The original Content property is hidden behind a new one. This is because we need to expose the content of the ContentPresenter not the one belonging to the UserControl.
public new object Content { get { return this.ContentComponent.Content; } protected set { this.ContentComponent.Content = value; } }
The DragDelta event of the SelectionThumb is handled by
SelectionThumb_DragDelta method. Basically it moves the UserControl
on canvas with the offset specified by the DragDeltaEventArgs argument.
private void SelectionThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
SetValue(Canvas.LeftProperty, (double)GetValue(Canvas.LeftProperty) + e.HorizontalChange);
SetValue(Canvas.TopProperty, (double)GetValue(Canvas.TopProperty) + e.VerticalChange);
}
The DragDelta event of the resize thumbs is handled by Thumb_DragDelta.
Based on the name of the thumb a specified opperation is performed.
private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
{
string name = ((Thumb)sender).Name;
if (name.Contains("Top"))
{
double newHeight = this.Height - e.VerticalChange;
if (newHeight >= minHeight && newHeight <= maxHeight)
{
this.Height = newHeight;
SetValue(Canvas.TopProperty, (double)GetValue(Canvas.TopProperty) + e.VerticalChange);
}
}
if (name.Contains("Right"))
{
double newWidth = this.Width + e.HorizontalChange;
if (newWidth >= minWidth && newWidth <= maxWidth)
this.Width = newWidth;
}
if (name.Contains("Bottom"))
{
double newHeight = this.Height + e.VerticalChange;
if (newHeight >= minHeight && newHeight <= maxHeight)
this.Height = newHeight;
}
if (name.Contains("Left"))
{
double newWidth = this.Width - e.HorizontalChange;
if (newWidth >= minWidth && newWidth <= maxWidth)
{
this.Width = newWidth;
SetValue(Canvas.LeftProperty, (double)GetValue(Canvas.LeftProperty) + e.HorizontalChange);
}
}
}
In a previous section called "Tunelling or bubbleing?" I wrote about tunelling.
In practice it is used on the PreviewMouseDown event of DesignerComponent.
private void DesignerComponent_PreviewMouseDown(object sender, MouseButtonEventArgs e) { this.IsSelected = true; }
Designer is just an UserControl with a canvas on it. The XAML part
doesn't need to much attention just notice the PreviewMouseDown event.
<UserControl x:Class="WPFDesigner.Designer" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="Auto" Width="Auto"> <Border BorderBrush="#FF000000" BorderThickness="1,1,1,1" x:Name="OuterBorder"> <Canvas ClipToBounds="True" PreviewMouseDown="DesignArea_PreviewMouseDown" x:Name="DesignArea" Background="#FFFFFFFF" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto"/> </Border> </UserControl>
The interesting part here is the code behind. The Designer acts like
a collection and implements the ICollection interface. Because this
collection is of FrameworkElement type the user is not aware that the
element goes in a container (DesignerComponenrt) which is then placed
on canvas.
Because another List of FrameworkElement is kept internally
it is easy to create a mapping between a control on the canvas and one in this list.
When a control is added in Designer it is actually added in two lists.
protected List<FrameworkElement> frameworkElements = new List<FrameworkElement>();
public void Add(FrameworkElement item)
{
this.frameworkElements.Add(item);
this.DesignArea.Children.Add(new DesignerComponent(item));
}
The same thing happens for all methods from the ICollection interface.
Using the code is extremly simple. All you have to do is create a new instance of
WPFDesigner.Designer and add it to a form. Afterwards you can add control
in it.
WPFDesigner.Designer designer = new WPFDesigner.Designer();
public Window1()
{
InitializeComponent();
designer.Width = 300;
designer.Height = 300;
MyGrid.Children.Add(designer);
Button b = new Button();
b.SetValue(Canvas.TopProperty, 200.0);
b.MaxHeight = 100.0;
b.Content = "ABCDE";
designer.Add(b);
}
If you add a control on WPFDesigner that has fixed height the vertical resize handles will not be visible. The same thing is valid for width but you will not see the horizontal resize handles.
Setting the minimum/maximim width/height on a control and adding it to WPFDesigner will make the control resize only in that interval.
| You must Sign In to use this message board. | ||||||||
|
||||||||
|
||||||||
|
||||||||
|
||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 30 Aug 2008 Editor: |
Copyright 2008 by Victor Hurdugaci Everything else Copyright © CodeProject, 1999-2009 Web09 | Advertise on the Code Project |