Introduction
This is the third article in the WPFSpark series. WPFSpark is an Open Source project in CodePlex containing a library of user controls providing rich user experience.
The previous articles in the WPFSpark series can be accessed here:
- WPFSpark: 1 of n: SprocketControl
- WPFSpark: 2 of n: ToggleSwitch
In this article, I describe in detail the third control in this library which I have developed. It is called the FluidWrapPanel
control and derives from Canvas
and provides a rich user experience. It derives from Panel
and provides the functionality of a WrapPanel
with an added advantage - the child elements of the panel can be easily rearranged by simple drag and drop.
Inspiration
FluidWrapPanel
is inspired from the cool and elegant iOS UI. Recently, I got a chance to play around with the 4th generation iPod Touch. I was fascinated by the feature provided in the Home Screen which allows you to rearrange the App icons. I wondered if this functionality could be done in WPF.
I wish to thank Roger Peters whose blog provided details about a similar implementation done in Silverlight. It proved very helpful indeed.
Update - WPFSparkv1.1
Thanks to valuable suggestions from the WPF guru, Sacha Barber, I have rewritten the core logic of FluidWrapPanel
class from scratch to make it more robust and usable in various scenarios. These changes are breaking changes, meaning if you are using the latest WPFSpark library (v1.1) then your code, using the old FluidWrapPanel
, will not compile unless you update it. The interface IFluidDrag
has been removed. Child elements no longer need to implement the IFluidDrag
interface to participate in the drag and drop interaction. Instead I have added a new Behavior called FluidMouseDragBehavior
which would facilitate the child element with drag and drop interaction (more details in the following section). These changes in the FluidWrapPanel
code have resulted in a faster, optimized code.
FluidWrapPanel Demystified
FluidWrapPanel
is composed of three major components:
FluidMouseDragBehavior
FluidLayoutManager
FluidWrapPanel
FluidMouseDragBehavior
FluidMouseDragBehavior
derives from System.Windows.Interactivity.Behavior<T>
which is the base class for providing attachable state and commands to an object. This behavior replaces the IFluidDrag
interface, thus making it easier for child elements to subscribe to mouse events (down/move/up). This behavior has a Dependency Property called DragButton
of type System.Windows.Input.MouseButton
which the user can set to indicate which mouse button events must be subscribed to.
namespace System.Windows.Input
{
public enum MouseButton
{
Left = 0,
Middle = 1,
Right = 2,
XButton1 = 3,
XButton2 = 4,
}
}
The default value of DragButton
is System.Windows.Input.MouseButton.Left
.
Here is the code for FluidMouseDragBehavior
:
public class FluidMouseDragBehavior : Behavior<UIElement>
{
#region Fields
FluidWrapPanel parentFWPanel = null;
ListBoxItem parentLBItem = null;
#endregion
#region Dependency Properties
#region DragButton
public static readonly DependencyProperty DragButtonProperty =
DependencyProperty.Register("DragButton", typeof(MouseButton),
typeof(FluidMouseDragBehavior),
new FrameworkPropertyMetadata(MouseButton.Left));
public MouseButton DragButton
{
get { return (MouseButton)GetValue(DragButtonProperty); }
set { SetValue(DragButtonProperty, value); }
}
#endregion
#endregion
#region Overrides
protected override void OnAttached()
{
(this.AssociatedObject as FrameworkElement).Loaded +=
new RoutedEventHandler(OnAssociatedObjectLoaded);
}
void OnAssociatedObjectLoaded(object sender, RoutedEventArgs e)
{
GetParentPanel();
if (parentLBItem != null)
{
parentLBItem.PreviewMouseDown +=
new MouseButtonEventHandler(OnPreviewMouseDown);
parentLBItem.PreviewMouseMove += new MouseEventHandler(OnPreviewMouseMove);
parentLBItem.PreviewMouseUp += new MouseButtonEventHandler(OnPreviewMouseUp);
}
else
{
this.AssociatedObject.PreviewMouseDown +=
new MouseButtonEventHandler(OnPreviewMouseDown);
this.AssociatedObject.PreviewMouseMove +=
new MouseEventHandler(OnPreviewMouseMove);
this.AssociatedObject.PreviewMouseUp +=
new MouseButtonEventHandler(OnPreviewMouseUp);
}
}
private void GetParentPanel()
{
FrameworkElement ancestor = this.AssociatedObject as FrameworkElement;
while (ancestor != null)
{
if (ancestor is ListBoxItem)
{
parentLBItem = ancestor as ListBoxItem;
}
if (ancestor is FluidWrapPanel)
{
parentFWPanel = ancestor as FluidWrapPanel;
return;
}
ancestor = VisualTreeHelper.GetParent(ancestor) as FrameworkElement;
}
}
protected override void OnDetaching()
{
(this.AssociatedObject as FrameworkElement).Loaded -= OnAssociatedObjectLoaded;
if (parentLBItem != null)
{
parentLBItem.PreviewMouseDown -= OnPreviewMouseDown;
parentLBItem.PreviewMouseMove -= OnPreviewMouseMove;
parentLBItem.PreviewMouseUp -= OnPreviewMouseUp;
}
else
{
this.AssociatedObject.PreviewMouseDown -= OnPreviewMouseDown;
this.AssociatedObject.PreviewMouseMove -= OnPreviewMouseMove;
this.AssociatedObject.PreviewMouseUp -= OnPreviewMouseUp;
}
}
#endregion
#region Event Handlers
void OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == DragButton)
{
Point position = parentLBItem != null ?
e.GetPosition(parentLBItem) : e.GetPosition(this.AssociatedObject);
FrameworkElement fElem = this.AssociatedObject as FrameworkElement;
if ((fElem != null) && (parentFWPanel != null))
{
if (parentLBItem != null)
parentFWPanel.BeginFluidDrag(parentLBItem, position);
else
parentFWPanel.BeginFluidDrag(this.AssociatedObject, position);
}
}
}
void OnPreviewMouseMove(object sender, MouseEventArgs e)
{
bool isDragging = false;
switch (DragButton)
{
case MouseButton.Left:
if (e.LeftButton == MouseButtonState.Pressed)
{
isDragging = true;
}
break;
case MouseButton.Middle:
if (e.MiddleButton == MouseButtonState.Pressed)
{
isDragging = true;
}
break;
case MouseButton.Right:
if (e.RightButton == MouseButtonState.Pressed)
{
isDragging = true;
}
break;
case MouseButton.XButton1:
if (e.XButton1 == MouseButtonState.Pressed)
{
isDragging = true;
}
break;
case MouseButton.XButton2:
if (e.XButton2 == MouseButtonState.Pressed)
{
isDragging = true;
}
break;
default:
break;
}
if (isDragging)
{
Point position = parentLBItem != null ?
e.GetPosition(parentLBItem) : e.GetPosition(this.AssociatedObject);
FrameworkElement fElem = this.AssociatedObject as FrameworkElement;
if ((fElem != null) && (parentFWPanel != null))
{
Point positionInParent = e.GetPosition(parentFWPanel);
if (parentLBItem != null)
parentFWPanel.FluidDrag(parentLBItem, position, positionInParent);
else
parentFWPanel.FluidDrag(this.AssociatedObject,
position, positionInParent);
}
}
}
void OnPreviewMouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == DragButton)
{
Point position = parentLBItem != null ?
e.GetPosition(parentLBItem) : e.GetPosition(this.AssociatedObject);
FrameworkElement fElem = this.AssociatedObject as FrameworkElement;
if ((fElem != null) && (parentFWPanel != null))
{
Point positionInParent = e.GetPosition(parentFWPanel);
if (parentLBItem != null)
parentFWPanel.EndFluidDrag(parentLBItem, position, positionInParent);
else
parentFWPanel.EndFluidDrag
(this.AssociatedObject, position, positionInParent);
}
}
}
#endregion
}
If the FluidWrapPanel
is used as an ItemsPanel
in a ListBox
, then the ListBoxItems
are added to the panel's Children
collection. So this scenario is handled in this behavior when the child (to which this behavior is attached) is loaded. To achieve this, the VisualTreeHelper
class is utilized.
Fluid Layout Manager
The FluidLayoutManager
acts as an assistant to the FluidWrapPanel
providing various helper methods like calculating the initial position of a child control before the FluidWrapPanel
is displayed, getting the position of a child in the FluidWrapPanel
during user interaction, calculation the layout transform of the child, and creating the Storyboard
for animating the child controls.
internal sealed class FluidLayoutManager
{
#region Fields
private Size panelSize;
private Size cellSize;
private Orientation panelOrientation;
private Int32 cellsPerLine;
#endregion
#region APIs
internal Point GetInitialLocationOfChild(int index)
{
Point result = new Point();
int row, column;
GetCellFromIndex(index, out row, out column);
int maxRows = (Int32)Math.Floor(panelSize.Height / cellSize.Height);
int maxCols = (Int32)Math.Floor(panelSize.Width / cellSize.Width);
bool isLeft = true;
bool isTop = true;
bool isCenterHeight = false;
bool isCenterWidth = false;
int halfRows = 0;
int halfCols = 0;
halfRows = (int)((double)maxRows / (double)2);
if ((maxRows % 2) == 0)
{
isTop = row < halfRows;
}
else
{
if (row == halfRows)
{
isCenterHeight = true;
isTop = false;
}
else
{
isTop = row < halfRows;
}
}
halfCols = (int)((double)maxCols / (double)2);
if ((maxCols % 2) == 0)
{
isLeft = column < halfCols;
}
else
{
if (column == halfCols)
{
isCenterWidth = true;
isLeft = false;
}
else
{
isLeft = column < halfCols;
}
}
if (isCenterHeight && isCenterWidth)
{
double posX = (halfCols) * cellSize.Width;
double posY = (halfRows + 2) * cellSize.Height;
return new Point(posX, posY);
}
if (isCenterHeight)
{
if (isLeft)
{
double posX = ((halfCols - column) + 1) * cellSize.Width;
double posY = (halfRows) * cellSize.Height;
result = new Point(-posX, posY);
}
else
{
double posX = ((column - halfCols) + 1) * cellSize.Width;
double posY = (halfRows) * cellSize.Height;
result = new Point(panelSize.Width + posX, posY);
}
return result;
}
if (isCenterWidth)
{
if (isTop)
{
double posX = (halfCols) * cellSize.Width;
double posY = ((halfRows - row) + 1) * cellSize.Height;
result = new Point(posX, -posY);
}
else
{
double posX = (halfCols) * cellSize.Width;
double posY = ((row - halfRows) + 1) * cellSize.Height;
result = new Point(posX, panelSize.Height + posY);
}
return result;
}
if (isTop)
{
if (isLeft)
{
double posX = ((halfCols - column) + 1) * cellSize.Width;
double posY = ((halfRows - row) + 1) * cellSize.Height;
result = new Point(-posX, -posY);
}
else
{
double posX = ((column - halfCols) + 1) * cellSize.Width;
double posY = ((halfRows - row) + 1) * cellSize.Height;
result = new Point(posX + panelSize.Width, -posY);
}
}
else
{
if (isLeft)
{
double posX = ((halfCols - column) + 1) * cellSize.Width;
double posY = ((row - halfRows) + 1) * cellSize.Height;
result = new Point(-posX, panelSize.Height + posY);
}
else
{
double posX = ((column - halfCols) + 1) * cellSize.Width;
double posY = ((row - halfRows) + 1) * cellSize.Height;
result = new Point(posX + panelSize.Width, panelSize.Height + posY);
}
}
return result;
}
internal void Initialize(double panelWidth, double panelHeight,
double cellWidth, double cellHeight, Orientation orientation)
{
if (panelWidth <= 0.0d)
panelWidth = cellWidth;
if (panelHeight <= 0.0d)
panelHeight = cellHeight;
if ((cellWidth <= 0.0d) || (cellHeight <= 0.0d))
{
cellsPerLine = 0;
return;
}
if ((panelSize.Width != panelWidth) ||
(panelSize.Height != panelHeight) ||
(cellSize.Width != cellWidth) ||
(cellSize.Height != cellHeight))
{
panelSize = new Size(panelWidth, panelHeight);
cellSize = new Size(cellWidth, cellHeight);
panelOrientation = orientation;
CalculateCellsPerLine();
}
}
internal int GetIndexFromCell(int row, int column)
{
int result = -1;
if ((row >= 0) && (column >= 0))
{
switch (panelOrientation)
{
case Orientation.Horizontal:
result = (cellsPerLine * row) + column;
break;
case Orientation.Vertical:
result = (cellsPerLine * column) + row;
break;
default:
break;
}
}
return result;
}
internal int GetIndexFromPoint(Point p)
{
int result = -1;
if ((p.X > 0.00D) &&
(p.X < panelSize.Width) &&
(p.Y > 0.00D) &&
(p.Y < panelSize.Height))
{
int row;
int column;
GetCellFromPoint(p, out row, out column);
result = GetIndexFromCell(row, column);
}
return result;
}
internal void GetCellFromIndex(int index, out int row, out int column)
{
row = column = -1;
if (index >= 0)
{
switch (panelOrientation)
{
case Orientation.Horizontal:
row = (int)(index / (double)cellsPerLine);
column = (int)(index % (double)cellsPerLine);
break;
case Orientation.Vertical:
column = (int)(index / (double)cellsPerLine);
row = (int)(index % (double)cellsPerLine);
break;
default:
break;
}
}
}
internal void GetCellFromPoint(Point p, out int row, out int column)
{
row = column = -1;
if ((p.X < 0.00D) ||
(p.X > panelSize.Width) ||
(p.Y < 0.00D) ||
(p.Y > panelSize.Height))
{
return;
}
row = (int)(p.Y / cellSize.Height);
column = (int)(p.X / cellSize.Width);
}
internal Point GetPointFromCell(int row, int column)
{
Point result = new Point();
if ((row >= 0) && (column >= 0))
{
result = new Point(cellSize.Width * column, cellSize.Height * row);
}
return result;
}
internal Point GetPointFromIndex(int index)
{
Point result = new Point();
if (index >= 0)
{
int row;
int column;
GetCellFromIndex(index, out row, out column);
result = GetPointFromCell(row, column);
}
return result;
}
internal TransformGroup CreateTransform(double transX, double transY,
double scaleX, double scaleY, double rotAngle = 0.0D)
{
TranslateTransform translation = new TranslateTransform();
translation.X = transX;
translation.Y = transY;
ScaleTransform scale = new ScaleTransform();
scale.ScaleX = scaleX;
scale.ScaleY = scaleY;
TransformGroup transform = new TransformGroup();
transform.Children.Add(scale);
transform.Children.Add(translation);
return transform;
}
internal Storyboard CreateTransition(UIElement element,
Point newLocation, TimeSpan period, EasingFunctionBase easing)
{
Duration duration = new Duration(period);
DoubleAnimation translateAnimationX = new DoubleAnimation();
translateAnimationX.To = newLocation.X;
translateAnimationX.Duration = duration;
if (easing != null)
translateAnimationX.EasingFunction = easing;
Storyboard.SetTarget(translateAnimationX, element);
Storyboard.SetTargetProperty(translateAnimationX,
new PropertyPath("(UIElement.RenderTransform).
(TransformGroup.Children)[1].(TranslateTransform.X)"));
DoubleAnimation translateAnimationY = new DoubleAnimation();
translateAnimationY.To = newLocation.Y;
translateAnimationY.Duration = duration;
if (easing != null)
translateAnimationY.EasingFunction = easing;
Storyboard.SetTarget(translateAnimationY, element);
Storyboard.SetTargetProperty(translateAnimationY,
new PropertyPath("(UIElement.RenderTransform).
(TransformGroup.Children)[1].(TranslateTransform.Y)"));
DoubleAnimation scaleAnimationX = new DoubleAnimation();
scaleAnimationX.To = 1.0D;
scaleAnimationX.Duration = duration;
if (easing != null)
scaleAnimationX.EasingFunction = easing;
Storyboard.SetTarget(scaleAnimationX, element);
Storyboard.SetTargetProperty(scaleAnimationX,
new PropertyPath("(UIElement.RenderTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleX)"));
DoubleAnimation scaleAnimationY = new DoubleAnimation();
scaleAnimationY.To = 1.0D;
scaleAnimationY.Duration = duration;
if (easing != null)
scaleAnimationY.EasingFunction = easing;
Storyboard.SetTarget(scaleAnimationY, element);
Storyboard.SetTargetProperty(scaleAnimationY,
new PropertyPath("(UIElement.RenderTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"));
Storyboard sb = new Storyboard();
sb.Duration = duration;
sb.Children.Add(translateAnimationX);
sb.Children.Add(translateAnimationY);
sb.Children.Add(scaleAnimationX);
sb.Children.Add(scaleAnimationY);
return sb;
}
internal Size GetArrangedSize(int childrenCount, Size finalSize)
{
if ((cellsPerLine == 0.0) || (childrenCount == 0))
return finalSize;
int numLines = (Int32)(childrenCount / (double)cellsPerLine);
int modLines = childrenCount % cellsPerLine;
if (modLines > 0)
numLines++;
if (panelOrientation == Orientation.Horizontal)
{
return new Size(cellsPerLine * cellSize.Width, numLines * cellSize.Height);
}
return new Size(numLines * cellSize.Width, cellsPerLine * cellSize.Height);
}
#endregion
#region Helpers
private void CalculateCellsPerLine()
{
double count = (panelOrientation == Orientation.Horizontal) ?
panelSize.Width / cellSize.Width :
panelSize.Height / cellSize.Height;
cellsPerLine = (Int32)Math.Floor(count);
if ((1.0D + cellsPerLine - count) < Double.Epsilon)
cellsPerLine++;
}
#endregion
}
FluidWrapPanel
FluidWrapPanel
is the main component which uses the LayoutManager
to provide the desired functionality. It derives from Panel
.
Before a FluidWrapPanel
is displayed for the first time, its children are arranged around it (outside the panel). It calculates the location of each child using the FluidLayoutManager
's GetInitialLocationOfChild
method. Just after the FluidWrapPanel
is loaded, the children are transitioned to their respective location within the panel using animation. FluidLayoutManager
's CreateTransition
method provides the Storyboard
for the animation.
Even though FluidWrapPanel
inherits the Children
property (of type UIElementCollection
) of Panel
, it maintains another collection of its own called fluidElements
. This collection is used when the children are rearranged by user interaction.
public class FluidWrapPanel : Panel
{
#region Constants
private const double NORMAL_SCALE = 1.0d;
private const double DRAG_SCALE_DEFAULT = 1.3d;
private const double NORMAL_OPACITY = 1.0d;
private const double DRAG_OPACITY_DEFAULT = 0.6d;
private const double OPACITY_MIN = 0.1d;
private const Int32 Z_INDEX_NORMAL = 0;
private const Int32 Z_INDEX_INTERMEDIATE = 1;
private const Int32 Z_INDEX_DRAG = 10;
private static TimeSpan DEFAULT_ANIMATION_TIME_WITHOUT_EASING =
TimeSpan.FromMilliseconds(200);
private static TimeSpan DEFAULT_ANIMATION_TIME_WITH_EASING =
TimeSpan.FromMilliseconds(400);
private static TimeSpan FIRST_TIME_ANIMATION_DURATION = TimeSpan.FromMilliseconds(320);
#endregion
#region Fields
Point dragStartPoint = new Point();
UIElement dragElement = null;
UIElement lastDragElement = null;
List<UIElement> fluidElements = null;
FluidLayoutManager layoutManager = null;
bool isInitializeArrangeRequired = false;
#endregion
#region Dependency Properties
...
#endregion
#region Overrides
protected override Size MeasureOverride(Size availableSize)
{
Size availableItemSize = new Size
(Double.PositiveInfinity, Double.PositiveInfinity);
double rowWidth = 0.0;
double maxRowHeight = 0.0;
double colHeight = 0.0;
double maxColWidth = 0.0;
double totalColumnWidth = 0.0;
double totalRowHeight = 0.0;
for (int i = 0; i < InternalChildren.Count; i++)
{
UIElement child = InternalChildren[i];
if (child != null)
{
child.Measure(availableItemSize);
if (!fluidElements.Contains(child))
{
AddChildToFluidElements(child);
}
if (this.Orientation == Orientation.Horizontal)
{
if (rowWidth + child.DesiredSize.Width > availableSize.Width)
{
totalRowHeight += maxRowHeight;
if (rowWidth > totalColumnWidth)
totalColumnWidth = rowWidth;
rowWidth = 0.0;
maxRowHeight = 0.0;
}
rowWidth += child.DesiredSize.Width;
if (child.DesiredSize.Height > maxRowHeight)
maxRowHeight = child.DesiredSize.Height;
}
else {
if (colHeight + child.DesiredSize.Height > availableSize.Height)
{
totalColumnWidth += maxColWidth;
if (colHeight > totalRowHeight)
totalRowHeight = colHeight;
colHeight = 0.0;
maxColWidth = 0.0;
}
colHeight += child.DesiredSize.Height;
if (child.DesiredSize.Width > maxColWidth)
maxColWidth = child.DesiredSize.Width;
}
}
}
if (this.Orientation == Orientation.Horizontal)
{
totalRowHeight += maxRowHeight;
if (totalColumnWidth == 0.0)
{
totalColumnWidth = rowWidth;
}
}
else
{
totalColumnWidth += maxColWidth;
if (totalRowHeight == 0.0)
{
totalRowHeight = colHeight;
}
}
Size resultSize = new Size(totalColumnWidth, totalRowHeight);
return resultSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
if (layoutManager == null)
layoutManager = new FluidLayoutManager();
layoutManager.Initialize(finalSize.Width, finalSize.Height,
ItemWidth, ItemHeight, Orientation);
bool isEasingRequired = !isInitializeArrangeRequired;
if ((isInitializeArrangeRequired) && (this.Children.Count > 0))
{
InitializeArrange();
isInitializeArrangeRequired = false;
}
UpdateFluidLayout(isEasingRequired);
return layoutManager.GetArrangedSize(fluidElements.Count, finalSize);
}
#endregion
#region Construction / Initialization
public FluidWrapPanel()
{
fluidElements = new List<UIElement>();
layoutManager = new FluidLayoutManager();
isInitializeArrangeRequired = true;
}
#endregion
#region Helpers
private void AddChildToFluidElements(UIElement child)
{
fluidElements.Add(child);
child.RenderTransform = layoutManager.CreateTransform
(-ItemWidth, -ItemHeight, NORMAL_SCALE, NORMAL_SCALE);
}
private void InitializeArrange()
{
foreach (UIElement child in fluidElements)
{
int index = fluidElements.IndexOf(child);
Point pos = layoutManager.GetInitialLocationOfChild(index);
child.RenderTransform = layoutManager.CreateTransform
(pos.X, pos.Y, NORMAL_SCALE, NORMAL_SCALE);
}
}
private void UpdateFluidLayout(bool showEasing = true)
{
for (int index = 0; index < fluidElements.Count; index++)
{
UIElement element = fluidElements[index];
if (element == null)
continue;
if (dragElement != null && index == fluidElements.IndexOf(dragElement))
continue;
element.Arrange(new Rect(0, 0, element.DesiredSize.Width,
element.DesiredSize.Height));
Point pos = layoutManager.GetPointFromIndex(index);
Storyboard transition;
if (element == lastDragElement)
{
if (!showEasing)
{
transition = layoutManager.CreateTransition
(element, pos, FIRST_TIME_ANIMATION_DURATION, null);
}
else
{
TimeSpan duration = (DragEasing != null) ?
DEFAULT_ANIMATION_TIME_WITH_EASING :
DEFAULT_ANIMATION_TIME_WITHOUT_EASING;
transition = layoutManager.CreateTransition
(element, pos, duration, DragEasing);
}
transition.Completed += (s, e) =>
{
if (lastDragElement != null)
{
lastDragElement.SetValue(Canvas.ZIndexProperty, 0);
lastDragElement = null;
}
};
}
else {
if (!showEasing)
{
transition = layoutManager.CreateTransition
(element, pos, FIRST_TIME_ANIMATION_DURATION, null);
}
else
{
TimeSpan duration = (ElementEasing != null) ?
DEFAULT_ANIMATION_TIME_WITH_EASING :
DEFAULT_ANIMATION_TIME_WITHOUT_EASING;
transition = layoutManager.CreateTransition
(element, pos, duration, ElementEasing);
}
}
transition.Begin();
}
}
private bool UpdateDragElementIndex(int newIndex)
{
int dragCellIndex = fluidElements.IndexOf(dragElement);
if (dragCellIndex == newIndex)
return false;
fluidElements.RemoveAt(dragCellIndex);
fluidElements.Insert(newIndex, dragElement);
return true;
}
private void ClearItemsSource()
{
fluidElements.Clear();
Children.Clear();
}
#endregion
#region FluidDrag Event Handlers
internal void BeginFluidDrag(UIElement child, Point position)
{
if ((child == null) || (!IsComposing))
return;
Dispatcher.BeginInvoke(new Action(() =>
{
child.Opacity = DragOpacity;
child.SetValue(Canvas.ZIndexProperty, Z_INDEX_DRAG);
child.CaptureMouse();
dragElement = child;
lastDragElement = null;
dragStartPoint = new Point(position.X * DragScale, position.Y * DragScale);
}));
}
internal void FluidDrag(UIElement child, Point position, Point positionInParent)
{
if ((child == null) || (!IsComposing))
return;
Dispatcher.BeginInvoke(new Action(() =>
{
if ((dragElement != null) && (layoutManager != null))
{
dragElement.RenderTransform =
layoutManager.CreateTransform(positionInParent.X - dragStartPoint.X,
positionInParent.Y - dragStartPoint.Y,
DragScale,
DragScale);
Point currentPt = positionInParent;
int index = layoutManager.GetIndexFromPoint(currentPt);
if ((index == -1) || (index >= fluidElements.Count))
{
index = fluidElements.Count - 1;
}
if (UpdateDragElementIndex(index))
{
UpdateFluidLayout();
}
}
}));
}
internal void EndFluidDrag(UIElement child, Point position, Point positionInParent)
{
if ((child == null) || (!IsComposing))
return;
Dispatcher.BeginInvoke(new Action(() =>
{
if ((dragElement != null) && (layoutManager != null))
{
dragElement.RenderTransform =
layoutManager.CreateTransform(positionInParent.X - dragStartPoint.X,
positionInParent.Y - dragStartPoint.Y,
DragScale,
DragScale);
child.Opacity = NORMAL_OPACITY;
child.SetValue(Canvas.ZIndexProperty, Z_INDEX_INTERMEDIATE);
child.ReleaseMouseCapture();
lastDragElement = dragElement;
dragElement = null;
}
UpdateFluidLayout();
}));
}
#endregion
}
FluidWrapPanel Properties
Dependency Property |
Type |
Description |
Default Value |
DragEasing |
EasingFunction |
Gets or sets the Easing function to be used, to animate the element, when the user stops dragging and releases the element. |
null |
DragOpacity |
Double |
Gets or sets the Opacity of the element when it is being dragged by the user. Range: 0.1D - 1.0D inclusive. |
0.6D |
DragScale |
Double |
Gets or sets the Scale Factor of the element when it is being dragged by the user. |
1.3D |
ElementEasing |
EasingFunction |
Gets or sets the Easing function to be used, to animate the elements in the FluidWrapPanel , when they are rearranged. |
null |
IsEditable |
Boolean |
Flag to indicate whether the children in the FluidWrapPanel can be rearranged or not. |
False |
ItemHeight |
Double |
Gets or sets the Height to be allotted for each child in the FluidWrapPanel . |
0 |
ItemsSource |
IEnumerable |
Bindable property to which a collection can be bound. |
null |
ItemWidth |
Double |
Gets or sets the Width to be allotted for each child in the FluidWrapPanel . |
0 |
Orientation |
System.Windows.Controls.Orientation |
Gets or sets the different orientations the FluidWrapPanel can have. Possible values are Horizontal and Vertical . |
Horizontal |
EndPoint
To participate in the Fluid Drag interactions, the child can add the FluidMouseDragBehavior
either through XAML or through code. For this purpose, reference to System.Windows.Interactivity
must be added.
Adding behavior through XAML
<UserControl x:Class="WPFSparkClient.ImageIcon"
...
xmlns:i="clr-namespace:System.Windows.Interactivity;
assembly=System.Windows.Interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:wpfspark="clr-namespace:WPFSpark;assembly=WPFSpark">
<i:Interaction.Behaviors>
<wpfspark:FluidMouseDragBehavior DragButton="Left">
</wpfspark:FluidMouseDragBehavior>
</i:Interaction.Behaviors>
<Grid>
...
</Grid>
</UserControl>
Adding behavior through code
using System.Windows.Interactivity;
public class AppButton : Button
{
public AppButton() : base()
{
var behaviors = Interaction.GetBehaviors(this);
behaviors.Add(new FluidMouseDragBehavior { DragButton = MouseButton.Right });
}
}
FluidWrapPanel
references the assemblies Microsoft.Expression.Interactions
and System.Windows.Interactivity
. If you have Expression Blend installed on your machine, then these DLLs will be available in the GAC. Else you can just install the Expression Blend SDK (available here) which will provide you with the required DLLs.
History
- January 19, 2012:
WPFSpark v1.1
released
- December 21, 2011:
WPFSpark v1.0
released
- August 23, 2011:
WPFSpark v0.7
released