WPFSpark:n之3:FluidWrapPanel






4.77/5 (27投票s)
一个可重新排列的WrapPanel
引言
这是WPFSpark系列的第三篇文章。WPFSpark是一个CodePlex上的开源项目,包含一个提供丰富用户体验的用户控件库。
WPFSpark系列的先前文章可在此处访问
在本文中,我将详细介绍我开发的该库中的第三个控件。它称为FluidWrapPanel
控件,它派生自Canvas
并提供丰富的用户体验。它派生自Panel
并提供WrapPanel
的功能,还有一个额外的优点——面板的子元素可以通过简单的拖放轻松重新排列。
灵感
FluidWrapPanel
的灵感来自酷炫优雅的iOS UI。最近,我得到了一个机会把玩第四代iPod Touch。我被主屏幕提供的功能迷住了,该功能允许你重新排列应用程序图标。我想知道是否可以在WPF中实现这种功能。
我想感谢Roger Peters,他的博客提供了关于在Silverlight中实现的类似实现的详细信息。这确实非常有帮助。
更新 - WPFSparkv1.1
感谢WPF大师Sacha Barber的宝贵建议,我已从头开始重写了FluidWrapPanel
类的核心逻辑,使其在各种场景下更加健壮和可用。这些更改是破坏性更改,意味着如果你正在使用最新的WPFSpark库(v1.1),那么你的代码(使用旧的FluidWrapPanel
)将无法编译,除非你更新它。IFluidDrag
接口已被移除。子元素不再需要实现IFluidDrag
接口即可参与拖放交互。相反,我添加了一个新的行为,称为FluidMouseDragBehavior
,它将为子元素提供拖放交互(更多细节请参阅以下部分)。FluidWrapPanel
代码中的这些更改使得代码更快、更优化。
FluidWrapPanel 揭秘
FluidWrapPanel
由三个主要组件组成
FluidMouseDragBehavior
FluidLayoutManager
FluidWrapPanel
FluidMouseDragBehavior
FluidMouseDragBehavior
派生自System.Windows.Interactivity.Behavior<T>
,它是为对象提供可附加状态和命令的基类。此行为取代了IFluidDrag
接口,从而使子元素更容易订阅鼠标事件(按下/移动/释放)。此行为有一个名为DragButton
的依赖项属性,类型为System.Windows.Input.MouseButton
,用户可以设置它来指示必须订阅哪些鼠标按钮事件。
namespace System.Windows.Input
{
// Summary:
// Defines values that specify the buttons on a mouse device.
public enum MouseButton
{
// Summary:
// The left mouse button.
Left = 0,
//
// Summary:
// The middle mouse button.
Middle = 1,
//
// Summary:
// The right mouse button.
Right = 2,
//
// Summary:
// The first extended mouse button.
XButton1 = 3,
//
// Summary:
// The second extended mouse button.
XButton2 = 4,
}
}
DragButton
的默认值为System.Windows.Input.MouseButton.Left
。
这是FluidMouseDragBehavior
的代码
public class FluidMouseDragBehavior : Behavior<UIElement>
{
#region Fields
FluidWrapPanel parentFWPanel = null;
ListBoxItem parentLBItem = null;
#endregion
#region Dependency Properties
#region DragButton
/// <summary>
/// DragButton Dependency Property
/// </summary>
public static readonly DependencyProperty DragButtonProperty =
DependencyProperty.Register("DragButton", typeof(MouseButton),
typeof(FluidMouseDragBehavior),
new FrameworkPropertyMetadata(MouseButton.Left));
/// <summary>
/// Gets or sets the DragButton property. This dependency property
/// indicates which Mouse button should participate in the drag interaction.
/// </summary>
public MouseButton DragButton
{
get { return (MouseButton)GetValue(DragButtonProperty); }
set { SetValue(DragButtonProperty, value); }
}
#endregion
#endregion
#region Overrides
/// <summary>
///
/// </summary>
protected override void OnAttached()
{
// Subscribe to the Loaded event
(this.AssociatedObject as FrameworkElement).Loaded +=
new RoutedEventHandler(OnAssociatedObjectLoaded);
}
void OnAssociatedObjectLoaded(object sender, RoutedEventArgs e)
{
// Get the parent FluidWrapPanel and check if the AssociatedObject is
// hosted inside a ListBoxItem (this scenario will occur if the FluidWrapPanel
// is the ItemsPanel for a ListBox).
GetParentPanel();
// Subscribe to the Mouse down/move/up events
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);
}
}
/// <summary>
/// Get the parent FluidWrapPanel and check if the AssociatedObject is
/// hosted inside a ListBoxItem (this scenario will occur if the FluidWrapPanel
/// is the ItemsPanel for a ListBox).
/// </summary>
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;
// No need to go further up
return;
}
// Find the visual ancestor of the current item
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
}
如果FluidWrapPanel
用作ListBox
中的ItemsPanel
,则ListBoxItems
将被添加到面板的Children
集合中。因此,当子元素(附加了此行为的子元素)加载时,此行为会处理这种情况。为了实现这一点,使用了VisualTreeHelper
类。
Fluid Layout Manager
FluidLayoutManager
充当FluidWrapPanel
的助手,提供各种辅助方法,例如在FluidWrapPanel
显示之前计算子控件的初始位置,在用户交互期间获取子控件在FluidWrapPanel
中的位置,计算子控件的布局变换,以及创建用于动画化子控件的Storyboard
。
internal sealed class FluidLayoutManager
{
#region Fields
private Size panelSize;
private Size cellSize;
private Orientation panelOrientation;
private Int32 cellsPerLine;
#endregion
#region APIs
/// <summary>
/// Calculates the initial location of the child in the FluidWrapPanel
/// when the child is added.
/// </summary>
/// <param name="index">Index of the child in the FluidWrapPanel</param>
/// <returns></returns>
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);
// Even number of rows
if ((maxRows % 2) == 0)
{
isTop = row < halfRows;
}
// Odd number of rows
else
{
if (row == halfRows)
{
isCenterHeight = true;
isTop = false;
}
else
{
isTop = row < halfRows;
}
}
halfCols = (int)((double)maxCols / (double)2);
// Even number of columns
if ((maxCols % 2) == 0)
{
isLeft = column < halfCols;
}
// Odd number of columns
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;
}
/// <summary>
/// Initializes the FluidLayoutManager
/// </summary>
/// <param name="panelWidth">Width of the FluidWrapPanel</param>
/// <param name="panelHeight">Height of the FluidWrapPanel</param>
/// <param name="cellWidth">Width of each child in the FluidWrapPanel</param>
/// <param name="cellHeight">Height of each child in the FluidWrapPanel</param>
/// <param name="orientation">Orientation of the panel - Horizontal or Vertical</param>
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;
// Calculate the number of cells that can be fit in a line
CalculateCellsPerLine();
}
}
/// <summary>
/// Provides the index of the child (in the FluidWrapPanel's children)
/// from the given row and column
/// </summary>
/// <param name="row">Row</param>
/// <param name="column">Column</param>
/// <returns>Index</returns>
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;
}
/// <summary>
/// Provides the index of the child (in the FluidWrapPanel's children)
/// from the given point
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Provides the row and column of the child based on its index in the
/// FluidWrapPanel.Children
/// </summary>
/// <param name="index">Index</param>
/// <param name="row">Row</param>
/// <param name="column">Column</param>
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;
}
}
}
/// <summary>
/// Provides the row and column of the child based on its
/// location in the FluidWrapPanel
/// </summary>
/// <param name="p">Location of the child in the parentFWPanel</param>
/// <param name="row">Row</param>
/// <param name="column">Column</param>
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);
}
/// <summary>
/// Provides the location of the child in the FluidWrapPanel
/// based on the given row and column
/// </summary>
/// <param name="row">Row</param>
/// <param name="column">Column</param>
/// <returns>Location of the child in the panel</returns>
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;
}
/// <summary>
/// Provides the location of the child in the FluidWrapPanel
/// based on the given row and column
/// </summary>
/// <param name="index">Index</param>
/// <returns>Location of the child in the panel</returns>
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;
}
/// <summary>
/// Creates a TransformGroup based on the given Translation, Scale and Rotation
/// </summary>
/// <param name="transX">Translation in the X-axis</param>
/// <param name="transY">Translation in the Y-axis</param>
/// <param name="scaleX">Scale factor in the X-axis</param>
/// <param name="scaleY">Scale factor in the Y-axis</param>
/// <param name="rotAngle">Rotation</param>
/// <returns>TransformGroup</returns>
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;
//RotateTransform rotation = new RotateTransform();
//rotation.Angle = rotAngle;
TransformGroup transform = new TransformGroup();
// THE ORDER OF TRANSFORM IS IMPORTANT
// First, scale, then rotate and finally translate
transform.Children.Add(scale);
//transform.Children.Add(rotation);
transform.Children.Add(translation);
return transform;
}
/// <summary>
/// Creates the storyboard for animating a child from its
/// old location to the new location.
/// The Translation and Scale properties are animated.
/// </summary>
/// <param name="child">UIElement for which the storyboard has to be created</param>
/// <param name="newLocation">New location of the UIElement</param>
/// <param name="period">Duration of animation</param>
/// <param name="easing">Easing function</param>
/// <returns>Storyboard</returns>
internal Storyboard CreateTransition(UIElement element,
Point newLocation, TimeSpan period, EasingFunctionBase easing)
{
Duration duration = new Duration(period);
// Animate X
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)"));
// Animate Y
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)"));
// Animate ScaleX
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)"));
// Animate ScaleY
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;
}
/// <summary>
/// Gets the total size taken up by the children after the Arrange Layout Phase
/// </summary>
/// <param name="childrenCount">Number of children</param>
/// <param name="finalSize">Available size provided by the FluidWrapPanel</param>
/// <returns>Total size</returns>
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
/// <summary>
/// Calculates the number of child items that can be accommodated in a single line
/// </summary>
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
是使用LayoutManager
提供所需功能的主要组件。它派生自Panel
。
在FluidWrapPanel
首次显示之前,其子元素会围绕它排列(在面板外部)。它使用FluidLayoutManager
的GetInitialLocationOfChild
方法计算每个子元素的位置。FluidWrapPanel
加载后,子元素会通过动画过渡到面板内的相应位置。FluidLayoutManager
的CreateTransition
方法提供了动画的Storyboard
。
尽管FluidWrapPanel
继承了Panel
的Children
属性(类型为UIElementCollection
),但它维护着自己的另一个名为fluidElements
的集合。当子元素通过用户交互重新排列时,会使用此集合。
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
/// <summary>
/// Override for the Measure Layout Phase
/// </summary>
/// <param name="availableSize">Available Size</param>
/// <returns>Size required by the panel</returns>
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;
// Iterate through all the UIElements in the Children collection
for (int i = 0; i < InternalChildren.Count; i++)
{
UIElement child = InternalChildren[i];
if (child != null)
{
// Ask the child how much size it needs
child.Measure(availableItemSize);
// Check if the child is already added to the fluidElements collection
if (!fluidElements.Contains(child))
{
AddChildToFluidElements(child);
}
if (this.Orientation == Orientation.Horizontal)
{
// Will the child fit in the current row?
if (rowWidth + child.DesiredSize.Width > availableSize.Width)
{
// Wrap to next row
totalRowHeight += maxRowHeight;
// Is the current row width greater than the previous row widths
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 // Vertical Orientation
{
// Will the child fit in the current column?
if (colHeight + child.DesiredSize.Height > availableSize.Height)
{
// Wrap to next column
totalColumnWidth += maxColWidth;
// Is the current column height greater
// than the previous column heights
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)
{
// Add the height of the last row
totalRowHeight += maxRowHeight;
// If there is only one row, take its width as the total width
if (totalColumnWidth == 0.0)
{
totalColumnWidth = rowWidth;
}
}
else
{
// Add the width of the last column
totalColumnWidth += maxColWidth;
// If there is only one column, take its height as the total height
if (totalRowHeight == 0.0)
{
totalRowHeight = colHeight;
}
}
Size resultSize = new Size(totalColumnWidth, totalRowHeight);
return resultSize;
}
/// <summary>
/// Override for the Arrange Layout Phase
/// </summary>
/// <param name="finalSize">Available size provided by the FluidWrapPanel</param>
/// <returns>Size taken up by the Panel</returns>
protected override Size ArrangeOverride(Size finalSize)
{
if (layoutManager == null)
layoutManager = new FluidLayoutManager();
// Initialize the LayoutManager
layoutManager.Initialize(finalSize.Width, finalSize.Height,
ItemWidth, ItemHeight, Orientation);
bool isEasingRequired = !isInitializeArrangeRequired;
// If the children are newly added, then set their initial
// location before the panel loads
if ((isInitializeArrangeRequired) && (this.Children.Count > 0))
{
InitializeArrange();
isInitializeArrangeRequired = false;
}
// Update the Layout
UpdateFluidLayout(isEasingRequired);
// Return the size taken up by the Panel's Children
return layoutManager.GetArrangedSize(fluidElements.Count, finalSize);
}
#endregion
#region Construction / Initialization
/// <summary>
/// Ctor
/// </summary>
public FluidWrapPanel()
{
fluidElements = new List<UIElement>();
layoutManager = new FluidLayoutManager();
isInitializeArrangeRequired = true;
}
#endregion
#region Helpers
/// <summary>
/// Adds the child to the fluidElements collection and
/// initializes its RenderTransform.
/// </summary>
/// <param name="child">UIElement</param>
private void AddChildToFluidElements(UIElement child)
{
// Add the child to the fluidElements collection
fluidElements.Add(child);
// Initialize its RenderTransform
child.RenderTransform = layoutManager.CreateTransform
(-ItemWidth, -ItemHeight, NORMAL_SCALE, NORMAL_SCALE);
}
/// <summary>
/// Intializes the arrangement of the children
/// </summary>
private void InitializeArrange()
{
foreach (UIElement child in fluidElements)
{
// Get the child's index in the fluidElements
int index = fluidElements.IndexOf(child);
// Get the initial location of the child
Point pos = layoutManager.GetInitialLocationOfChild(index);
// Initialize the appropriate Render Transform for the child
child.RenderTransform = layoutManager.CreateTransform
(pos.X, pos.Y, NORMAL_SCALE, NORMAL_SCALE);
}
}
/// <summary>
/// Iterates through all the fluid elements and animate their
/// movement to their new location.
/// </summary>
private void UpdateFluidLayout(bool showEasing = true)
{
// Iterate through all the fluid elements and animate their
// movement to their new location.
for (int index = 0; index < fluidElements.Count; index++)
{
UIElement element = fluidElements[index];
if (element == null)
continue;
// If an child is currently being dragged, then no need to animate it
if (dragElement != null && index == fluidElements.IndexOf(dragElement))
continue;
element.Arrange(new Rect(0, 0, element.DesiredSize.Width,
element.DesiredSize.Height));
// Get the cell position of the current index
Point pos = layoutManager.GetPointFromIndex(index);
Storyboard transition;
// Is the child being animated the same as the child which was last dragged?
if (element == lastDragElement)
{
if (!showEasing)
{
// Create the Storyboard for the transition
transition = layoutManager.CreateTransition
(element, pos, FIRST_TIME_ANIMATION_DURATION, null);
}
else
{
// Is easing function specified for the animation?
TimeSpan duration = (DragEasing != null) ?
DEFAULT_ANIMATION_TIME_WITH_EASING :
DEFAULT_ANIMATION_TIME_WITHOUT_EASING;
// Create the Storyboard for the transition
transition = layoutManager.CreateTransition
(element, pos, duration, DragEasing);
}
// When the user releases the drag child, it's Z-Index is set to 1 so that
// during the animation it does not go below other elements.
// After the animation has completed set its Z-Index to 0
transition.Completed += (s, e) =>
{
if (lastDragElement != null)
{
lastDragElement.SetValue(Canvas.ZIndexProperty, 0);
lastDragElement = null;
}
};
}
else // It is a non-dragElement
{
if (!showEasing)
{
// Create the Storyboard for the transition
transition = layoutManager.CreateTransition
(element, pos, FIRST_TIME_ANIMATION_DURATION, null);
}
else
{
// Is easing function specified for the animation?
TimeSpan duration = (ElementEasing != null) ?
DEFAULT_ANIMATION_TIME_WITH_EASING :
DEFAULT_ANIMATION_TIME_WITHOUT_EASING;
// Create the Storyboard for the transition
transition = layoutManager.CreateTransition
(element, pos, duration, ElementEasing);
}
}
// Start the animation
transition.Begin();
}
}
/// <summary>
/// Moves the dragElement to the new Index
/// </summary>
/// <param name="newIndex">Index of the new location</param>
/// <returns>True-if dragElement was moved otherwise False</returns>
private bool UpdateDragElementIndex(int newIndex)
{
// Check if the dragElement is being moved to its current place
// If yes, then no need to proceed further. (Improves efficiency!)
int dragCellIndex = fluidElements.IndexOf(dragElement);
if (dragCellIndex == newIndex)
return false;
fluidElements.RemoveAt(dragCellIndex);
fluidElements.Insert(newIndex, dragElement);
return true;
}
/// <summary>
/// Removes all the children from the FluidWrapPanel
/// </summary>
private void ClearItemsSource()
{
fluidElements.Clear();
Children.Clear();
}
#endregion
#region FluidDrag Event Handlers
/// <summary>
/// Handler for the event when the user starts dragging the dragElement.
/// </summary>
/// <param name="child">UIElement being dragged</param>
/// <param name="position">Position in the child where the user clicked</param>
internal void BeginFluidDrag(UIElement child, Point position)
{
if ((child == null) || (!IsComposing))
return;
// Call the event handler core on the Dispatcher. (Improves efficiency!)
Dispatcher.BeginInvoke(new Action(() =>
{
child.Opacity = DragOpacity;
child.SetValue(Canvas.ZIndexProperty, Z_INDEX_DRAG);
// Capture further mouse events
child.CaptureMouse();
dragElement = child;
lastDragElement = null;
// Since we are scaling the dragElement by DragScale,
// the clickPoint also shifts
dragStartPoint = new Point(position.X * DragScale, position.Y * DragScale);
}));
}
/// <summary>
/// Handler for the event when the user drags the dragElement.
/// </summary>
/// <param name="child">UIElement being dragged</param>
/// <param name="position">Position where the user clicked
/// w.r.t. the UIElement being dragged</param>
/// <param name="positionInParent">Position where the user clicked
/// w.r.t. the FluidWrapPanel (the parentFWPanel of the UIElement being dragged</param>
internal void FluidDrag(UIElement child, Point position, Point positionInParent)
{
if ((child == null) || (!IsComposing))
return;
// Call the event handler core on the Dispatcher. (Improves efficiency!)
Dispatcher.BeginInvoke(new Action(() =>
{
if ((dragElement != null) && (layoutManager != null))
{
dragElement.RenderTransform =
layoutManager.CreateTransform(positionInParent.X - dragStartPoint.X,
positionInParent.Y - dragStartPoint.Y,
DragScale,
DragScale);
// Get the index in the fluidElements list corresponding
// to the current mouse location
Point currentPt = positionInParent;
int index = layoutManager.GetIndexFromPoint(currentPt);
// If no valid cell index is obtained, add the child to the end of the
// fluidElements list.
if ((index == -1) || (index >= fluidElements.Count))
{
index = fluidElements.Count - 1;
}
// If the dragElement is moved to a new location, then only
// call the updation of the layout.
if (UpdateDragElementIndex(index))
{
UpdateFluidLayout();
}
}
}));
}
/// <summary>
/// Handler for the event when the user stops dragging the dragElement and releases it.
/// </summary>
/// <param name="child">UIElement being dragged</param>
/// <param name="position">Position where the user clicked w.r.t.
/// the UIElement being dragged</param>
/// <param name="positionInParent">Position where the user clicked
/// w.r.t. the FluidWrapPanel (the parentFWPanel of the UIElement being dragged</param>
internal void EndFluidDrag(UIElement child, Point position, Point positionInParent)
{
if ((child == null) || (!IsComposing))
return;
// Call the event handler core on the Dispatcher. (Improves efficiency!)
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;
// Z-Index is set to 1 so that during the animation it does
// not go below other elements.
child.SetValue(Canvas.ZIndexProperty, Z_INDEX_INTERMEDIATE);
// Release the mouse capture
child.ReleaseMouseCapture();
// Reference used to set the Z-Index to 0 during the UpdateFluidLayout
lastDragElement = dragElement;
dragElement = null;
}
UpdateFluidLayout();
}));
}
#endregion
}
FluidWrapPanel 属性
依赖属性 | 类型 | 描述 | 默认值 |
---|---|---|---|
DragEasing |
EasingFunction | 获取或设置当用户停止拖动并释放元素时,用于为元素设置动画的Easing 函数。 |
null |
DragOpacity |
双精度浮点型 | 获取或设置用户拖动元素时的不透明度。范围:0.1D - 1.0D(包含)。 |
0.6D |
DragScale |
双精度浮点型 | 获取或设置用户拖动元素时的缩放因子。 | 1.3D |
ElementEasing |
EasingFunction | 获取或设置当FluidWrapPanel 中的元素重新排列时,用于为它们设置动画的Easing 函数。 |
null |
IsEditable |
布尔值 | 标志,指示FluidWrapPanel 中的子元素是否可以重新排列。 |
假 |
ItemHeight |
双精度浮点型 | 获取或设置FluidWrapPanel 中每个子元素分配的高度。 |
0 |
ItemsSource |
IEnumerable | 可绑定到集合的属性。 | null |
ItemWidth |
双精度浮点型 | 获取或设置FluidWrapPanel 中每个子元素分配的宽度。 |
0 |
Orientation |
System.Windows.Controls.Orientation | 获取或设置FluidWrapPanel 可以具有的不同方向。可能的值为Horizontal 和Vertical 。 |
Horizontal |
端点
要参与Fluid Drag交互,子元素可以通过XAML或代码添加FluidMouseDragBehavior
。为此,必须添加对System.Windows.Interactivity
的引用。
通过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>
通过代码添加行为
using System.Windows.Interactivity;
public class AppButton : Button
{
public AppButton() : base()
{
var behaviors = Interaction.GetBehaviors(this);
behaviors.Add(new FluidMouseDragBehavior { DragButton = MouseButton.Right });
}
}
FluidWrapPanel
引用了Microsoft.Expression.Interactions
和System.Windows.Interactivity
程序集。如果你的计算机上安装了Expression Blend,那么这些DLL将在GAC中可用。否则,你可以只安装Expression Blend SDK(此处有售),它将为你提供所需的DLL。
历史
- 2012年1月19日:
WPFSpark v1.1
发布 - 2011年12月21日:
WPFSpark v1.0
发布 - 2011年8月23日:
WPFSpark v0.7
发布