65.9K
CodeProject 正在变化。 阅读更多。
Home

WPFSpark:n之3:FluidWrapPanel

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (27投票s)

2011年8月22日

Ms-PL

5分钟阅读

viewsIcon

90583

downloadIcon

2098

一个可重新排列的WrapPanel

引言

这是WPFSpark系列的第三篇文章。WPFSpark是一个CodePlex上的开源项目,包含一个提供丰富用户体验的用户控件库。

WPFSpark系列的先前文章可在此处访问

  1. WPFSpark:n之1:SprocketControl
  2. WPFSpark:n之2:ToggleSwitch

在本文中,我将详细介绍我开发的该库中的第三个控件。它称为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首次显示之前,其子元素会围绕它排列(在面板外部)。它使用FluidLayoutManagerGetInitialLocationOfChild方法计算每个子元素的位置。FluidWrapPanel加载后,子元素会通过动画过渡到面板内的相应位置。FluidLayoutManagerCreateTransition方法提供了动画的Storyboard

尽管FluidWrapPanel继承了PanelChildren属性(类型为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可以具有的不同方向。可能的值为HorizontalVertical 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.InteractionsSystem.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发布
© . All rights reserved.