Click here to Skip to main content
15,868,016 members
Articles / Desktop Programming / WPF

WPF Custom Card Panel

Rate me:
Please Sign up or sign in to vote.
4.20/5 (4 votes)
10 Jul 2010CPOL3 min read 34.7K   672   13   3
A custom panel that simulates choosing cards from a deck.

Introduction

This is a custom implementation of a WPF panel that simulates the action of choosing a card from a deck. The selected card jumps to the front of the deck. Some animations and transformations are performed, and the other cards are subsequently moved to the back of the deck, maintaining their initial order. The card animations are essentially random, which creates a unique look each time a card is selected. Henceforth, each card selection precipitates a different animation speed, resulting in a life-like look and feel to the entire event. This custom card panel can be used with any UIElement object, and is not purely limited to images.

Background

In the past, I have implemented Windows Controls, and ultimately became very fond of the numerous techniques that had to be employed. I was forced to use the GDI or GDI+ APIs, and was ultimately a very happy man even with the Immediate Mode rendering engine. I took pride in my controls because they were not trivial to implement. Then came WPF (a moment of silence for Windows Controls...). WPF made it almost trivial to modify and create professional looking controls. While creating and modifying Windows Controls were a lot of fun, WPF, in my opinion, has replaced the hours of coding with hours of creativity. It did not take me long to learn WPF. I would say, it naturally fell into my lap. I wanted to create this Card Panel to show developers how relatively easy it has now become to create controls and animations. While we are using DirectX under the hood, for example, most, if not all, of these implementation details are under the hood. (I do feel that developers should still get intimate with these technologies just to walk the walk and talk the talk, so to speak.)

Using the code

Firstly, the Card Panel actually is not as complicated as one would initially assume. I wanted to be able to provide a custom behavior that the WPF panels did not possess. In order to create our own custom layout, we have the option to derive from the WPF Panel class and override the MeasureOverride() and ArrangeOverride() methods. WPF has a two step layout process that is responsible for sizing and arranging children. The first stage is dubbed the measure phase. In this phase, the children tell their parent how large (or small) they would like to be. The second phase is dubbed the layout phase. This phase is very important as the children are actually laid out and given their bounds.

During the measure phase, we have to iterate through all of the children in the panel and call the Measure() method on each one. At this point, the children get to tell their parent their desired size. After the call to Measure(), each child's DesiredSize property will contain the size they want. At the end of the MeasureOverride() method, the panel will know how much space its children need altogether and returns it.

C#
protected override Size MeasureOverride(Size availableSize)
{
    Size idealSize = new Size(0, 0);

    Size size = new Size(this.ItemWidth, this.ItemHeight);

    // Allow children as much room as they want
    foreach (UIElement child in Children)
    {
        child.Measure(size);
        idealSize.Width = 
           Math.Max(child.DesiredSize.Width, idealSize.Width);
        idealSize.Height += child.DesiredSize.Height + childPadding;
    }

    totalChildHeight = idealSize.Height;
  
    forMoving.Clear();

    if (double.IsInfinity(availableSize.Height) || 
        double.IsInfinity(availableSize.Width))
    {
        return idealSize;
    }
    else
    {
        return availableSize;
    }
}

During the layout phase, the ArrangeOverride() method is called. At this point, each of the panel's children has already been measured in the MeasureOverride() procedure. By calling the Arrange() method of each child, you can set their size and position in the panel.

C#
protected override Size ArrangeOverride(Size finalSize)
{
    if (this.Children == null || this.Children.Count == 0)
    {
        return finalSize;
    }

    ourSize = finalSize;
    totalChildWidth = 0;

    foreach (UIElement child in this.Children)
    {
        if (child.RenderTransform as TransformGroup == null)
        {
            child.RenderTransformOrigin = 
                  new Point(transformOriginX, transformOriginY);
            TransformGroup group = new TransformGroup();
            child.RenderTransform = group;

            group.Children.Add(new TranslateTransform());
            group.Children.Add(new ScaleTransform());
        }

        //Start by putting all children
        //in the upper left corner of the cell
        child.Arrange(new Rect(10, 0, 
              child.DesiredSize.Width, child.DesiredSize.Height));
        totalChildWidth += child.DesiredSize.Width;
    }

    forMoving.Clear();
    AnimateChildren();

    return finalSize;
}

You can disregard all of the animation code in the methods. They are not at all involved in the layout process.

Because each child, when clicked, needs to pop-up, I used a scaling transformation to increase their size. This simulates the pop up behavior. Subsequently, the selected child needs to 'pop-down' into the deck. Again, I employed the ScaleTranform, however, to scale down rather than up. To actually move the card up and down the deck, I employed the use of another transformed called a TranslateTransform.

Here is the complete code for the WPF Card Panel:

C#
[Serializable]
public class CardPanel : Panel
{
    #region Private Members

    private double totalChildHeight;
    private double totalChildWidth;
    private Size ourSize;
    private double transformOriginY = 1;
    private double transformOriginX = 0.8;
    private double duration = 700;
    private double childPadding = 5;
    private bool childClicked;
    private int clickedIndex;
    private UIElement clickedChild;

    private const int RAND_FROM = 700;
    private const int RAND_TO = 4000;

    private IDictionary<uielement,> elementLocations = 
            new Dictionary<uielement,>();
    private ICollection<uielement> forMoving = 
            new Collection<uielement>();
    private Collection<uielement> list = 
            new Collection<uielement>();

    public static readonly DependencyProperty ItemHeightProperty = 
           DependencyProperty.Register("ItemHeight", typeof(double), 
           typeof(CardPanel), new FrameworkPropertyMetadata((double)50, 
           FrameworkPropertyMetadataOptions.AffectsMeasure));

    #endregion

    #region Public Properties

    public double ItemWidth
    {
        get { return (double)GetValue(ItemWidthProperty); }
        set { SetValue(ItemWidthProperty, value); }
    }

    public static readonly DependencyProperty ItemWidthProperty = 
           DependencyProperty.Register("ItemWidth", 
           typeof(double),typeof(CardPanel), 
           new FrameworkPropertyMetadata((double)50, 
           FrameworkPropertyMetadataOptions.AffectsMeasure));

    public double ItemHeight
    {
        get { return (double)GetValue(ItemHeightProperty); }
        set { SetValue(ItemHeightProperty, value); }
    }

    #endregion

    #region Constructors

    /// <summary>
    /// Initializes a new instance of the <see cref="CardPanel"> class.
    /// </see>
    public CardPanel()
    {
    }

    #endregion

    #region Helper Procedures

    /// <summary>
    /// Adds the element to locations.
    /// </summary>
    /// <param name="element" />The element.
    /// <param name="x" />The x.
    /// <param name="y" />The y.
    private void AddElementToLocations(UIElement element, double x, double y)
    {
        if (!elementLocations.ContainsKey(element))
        {
            elementLocations.Add(element, new Point(x, y));
        }
        else
        {
            elementLocations[element] = new Point(x, y);
        }
    }

    /// <summary>
    /// Gets the index of the clicked element
    /// </summary>
    /// <param name="child" />The child.
    /// <returns>
    private int GetClickedIndex(object child)
    {
        int i = 0;

        foreach (UIElement element in list)
        {
            if (element == child)
            {
                return i;
            }
            else
            {
                i++;
            }
        }

        return i;
    }

    /// <summary>
    /// Gets the element.
    /// </summary>
    /// <param name="index" />The index.
    /// <returns>
    private UIElement GetElement(int index)
    {
        if (index > list.Count || list.Count == 0)
        {
            InvalidateMeasure();
            return null;
        }

        return list[index];
    }

    /// <summary>
    /// Re-Added children to panel.
    /// </summary>
    /// <param name="childrenToAdd" />The children to add.
    private void ReAddChildrenToPanel(IEnumerable<uielement> childrenToAdd)
    {
        int i = 0;

        foreach (UIElement element in list)
        {
            CardPanel.SetZIndex(element, i);
            i++;
        }
    }

    /// <summary>
    /// Gets the separation.
    /// </summary>
    /// <param name="childHeight" />Height of the child.
    /// <returns>
    private double GetSeparation(double childHeight)
    {
        double relativeSeparation = 0;

        if (totalChildHeight < ourSize.Height)
        {
            relativeSeparation = childHeight + childPadding;
        }
        else
        {
            relativeSeparation = (ourSize.Height - 
                    childHeight - 10) / (Children.Count - 1);
        }

        return relativeSeparation;
    }

    #endregion

    #region Overrides

    /// <summary>
    /// Invoked when the <see
    /// cref="T:System.Windows.Media.VisualCollection">
    /// of a visual object is modified.
    /// </see>
    /// <param name="visualAdded" />The
    /// <see cref="T:System.Windows.Media.Visual">
    /// that was added to the collection.
    /// <param name="visualRemoved" />
    /// The <see cref="T:System.Windows.Media.Visual">
    /// that was removed from the collection.
    protected override void OnVisualChildrenChanged(
              DependencyObject visualAdded, DependencyObject visualRemoved)
    {
        base.OnVisualChildrenChanged(visualAdded, visualRemoved);

        //For every element added 
        //add an event handler to know when it is clicked
        UIElement element = visualAdded as UIElement;
        if (element != null)
        {
            element.AddHandler(UIElement.MouseLeftButtonDownEvent, 
                    new RoutedEventHandler(element_MouseLeftButtonDown), true);
        }
    }

    /// <summary>
    /// When overridden in a derived class, measures the size in layout
    /// required for child elements and determines a size for
    /// the <see cref="T:System.Windows.FrameworkElement">-derived class.
    /// </see>
    /// <param name="availableSize" />The available size that this element
    /// can give to child elements. Infinity can be specified as a value
    /// to indicate that the element will size to whatever content is available.
    /// <returns>
    /// The size that this element determines it needs during layout,
    /// based on its calculations of child element sizes.
    /// </returns>
    protected override Size MeasureOverride(Size availableSize)
    {
        Size idealSize = new Size(0, 0);

        Size size = new Size(this.ItemWidth, this.ItemHeight);

        // Allow children as much room as they want
        foreach (UIElement child in Children)
        {
            child.Measure(size);
            idealSize.Width = Math.Max(child.DesiredSize.Width, idealSize.Width);
            idealSize.Height += child.DesiredSize.Height + childPadding;
        }

        totalChildHeight = idealSize.Height;
      
        forMoving.Clear();

        if (double.IsInfinity(availableSize.Height) || 
            double.IsInfinity(availableSize.Width))
        {
            return idealSize;
        }
        else
        {
            return availableSize;
        }
    }

    /// <summary>
    /// When overridden in a derived class, positions child elements
    /// and determines a size for
    /// a <see cref="T:System.Windows.FrameworkElement"> derived class.
    /// </see>
    /// <param name="finalSize" />The final area within the parent
    /// that this element should use to arrange itself and its children.
    /// <returns>The actual size used.</returns>
    protected override Size ArrangeOverride(Size finalSize)
    {
        if (this.Children == null || this.Children.Count == 0)
        {
            return finalSize;
        }

        ourSize = finalSize;
        totalChildWidth = 0;

        foreach (UIElement child in this.Children)
        {
            if (child.RenderTransform as TransformGroup == null)
            {
                child.RenderTransformOrigin = 
                      new Point(transformOriginX, transformOriginY);
                TransformGroup group = new TransformGroup();
                child.RenderTransform = group;

                group.Children.Add(new TranslateTransform());
                group.Children.Add(new ScaleTransform());
            }

            //Start by putting all children in the upper left corner of the cell
            child.Arrange(new Rect(10, 0, 
                  child.DesiredSize.Width, child.DesiredSize.Height));
            totalChildWidth += child.DesiredSize.Width;
        }

        forMoving.Clear();
        AnimateChildren();

        return finalSize;
    }

    #endregion

    #region Element Events

    /// <summary>
    /// Handles the MouseLeftButtonDown event of the element control.
    /// </summary>
    /// <param name="sender" />The source of the event.
    /// <param name="e" />The <see cref="System.Windows.RoutedEventArgs">
    /// instance containing the event data.
    private void element_MouseLeftButtonDown(object sender, RoutedEventArgs e)
    {
        if (!childClicked)
        {
            childClicked = true;
            clickedIndex = GetClickedIndex(sender);

            this.InvalidateArrange();
        }
    }
    #endregion

    #region Animation procedures

    /// <summary>
    /// Animates the children.
    /// </summary>
    private void AnimateChildren()
    {
        if (this.Children == null || this.Children.Count == 0)
            return;

        double x = 10;
        double y = 0;

        if (childClicked)
        {
            clickedChild = GetElement(clickedIndex);

            if (clickedChild == null) return;

            if (Children.Count > 0)
            {
                //Move selected element to the bottom
                UIElement bottomChild = list[list.Count - 1];

                //Don't animate if the clicked child is already the bottom child
                if (clickedChild != bottomChild)
                {
                    AnimateToScaleUp(clickedChild, duration);
                }

                Point point = elementLocations[bottomChild];
                AnimateTo(clickedChild, point.X, point.Y, duration);

                elementLocations[clickedChild] = new Point(point.X, point.Y);

                // collect elements that come after the clicked index
                for (int i = clickedIndex + 1; i < list.Count; i++)
                {
                    UIElement element = GetElement(i);
                    if (element != null)
                        forMoving.Add(element);
                }

                x = 10;
                y = 0;

                Random rand = new Random();
                int currentAddIndex = 0;

                foreach (UIElement element in forMoving)
                {
                    elementLocations[element] = new Point(x, y + childPadding);
                    list.Remove(element);

                    list.Insert(currentAddIndex, element);

                    AnimateTo(element, x, y, rand.Next(RAND_FROM, RAND_TO));

                    y += GetSeparation(element.DesiredSize.Height);

                    currentAddIndex++;
                }

                foreach (UIElement element in Children)
                {
                    if (!forMoving.Contains(element) && element != clickedChild)
                    {
                        elementLocations[element] = new Point(x, y + 10);
                        list.Remove(element);
                        list.Insert(currentAddIndex, element);

                        AnimateTo(element, x, y, rand.Next(RAND_FROM, RAND_TO));
                        y += GetSeparation(element.DesiredSize.Height);

                        currentAddIndex++;
                    }
                }

                ReAddChildrenToPanel(list);

                childClicked = false;
            }
        }
        else
        {
            list.Clear();
            elementLocations.Clear();
            foreach (UIElement element in base.InternalChildren)
            {
                AddElementToLocations(element, x, y + childPadding);
                list.Add(element);
                AnimateTo(element, x, y + childPadding, duration);

                y += GetSeparation(element.DesiredSize.Height);
            }
        }
    }

    /// <summary>
    /// Animates to scale up.
    /// </summary>
    /// <param name="child" />The child.
    /// <param name="duration" />The duration.
    private void AnimateToScaleUp(UIElement child, double duration)
    {
        if (child == null) return;

        TransformGroup group = (TransformGroup)child.RenderTransform;
        ScaleTransform scale = (ScaleTransform)group.Children[1];
        {
            scale.BeginAnimation(ScaleTransform.ScaleXProperty, 
                  MakeAnimation(1.2, duration / 2, animation_Completed));
            scale.BeginAnimation(ScaleTransform.ScaleYProperty, 
                  MakeAnimation(1.2, duration / 2));
        }
    }

    /// <summary>
    /// Animates to scale down.
    /// </summary>
    /// <param name="child" />The child.
    /// <param name="duration" />The duration.
    private void ScaleDown(UIElement child, double duration)
    {
        if (child == null) return;

        TransformGroup group = (TransformGroup)child.RenderTransform;
        ScaleTransform scale = (ScaleTransform)group.Children[1];

        scale.CenterX = -0.5;
        scale.CenterY = 0;

        scale.BeginAnimation(ScaleTransform.ScaleXProperty, 
                             MakeAnimation(1, duration / 3));
        scale.BeginAnimation(ScaleTransform.ScaleYProperty, 
                             MakeAnimation(1, duration / 3));
    }

    /// <summary>
    /// Animates to.
    /// </summary>
    /// <param name="child" />The child.
    /// <param name="x" />The x.
    /// <param name="y" />The y.
    /// <param name="duration" />The duration.
    private void AnimateTo(UIElement child, double x, 
                 double y, double duration)
    {
        if (child == null) return;

        TransformGroup group = (TransformGroup)child.RenderTransform;
        TranslateTransform trans = (TranslateTransform)group.Children[0];
        ScaleTransform scale = (ScaleTransform)group.Children[1];

        trans.BeginAnimation(TranslateTransform.XProperty, 
              MakeAnimation(x, duration, animation_Completed));
        trans.BeginAnimation(TranslateTransform.YProperty, 
              MakeAnimation(y, duration));
    }

    /// <summary>
    /// Makes the animation.
    /// </summary>
    /// <param name="to" />To.
    /// <param name="duration" />The duration.
    /// <returns>
    private DoubleAnimation MakeAnimation(double to, double duration)
    {
        return MakeAnimation(to, duration, null);
    }

    /// <summary>
    /// Makes the animation.
    /// </summary>
    /// <param name="to" />To.
    /// <param name="duration" />The duration.
    /// <param name="endEvent" />The end event.
    /// <returns>
    private DoubleAnimation MakeAnimation(double to, 
            double duration, EventHandler endEvent)
    {
        DoubleAnimation anim = 
           new DoubleAnimation(to, TimeSpan.FromMilliseconds(duration));
        anim.AccelerationRatio = 0.2;
        anim.DecelerationRatio = 0.7;

        if (endEvent != null)
        {
            anim.Completed += endEvent;
        }

        return anim;
    }

    /// <summary>
    /// Handles the Completed event of the animation control.
    /// </summary>
    /// <param name="sender" />The source of the event.
    /// <param name="e" />The <see cref="System.EventArgs">
    ///           instance containing the event data.
    private void animation_Completed(object sender, EventArgs e)
    {
        ScaleDown(clickedChild, duration);
    }

    #endregion
}

Conclusion

I had quite a bit of fun creating this custom panel. I have not fully tested it yet. I appreciate your feedback.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Finance Industry
United States United States
Currently pursuing 'Programming Nirvana' (The ineffable ultimate in which one has attained disinterested wisdom and compassion as it relates to programming)

Respected Technologies
1. Confusor (https://confuser.codeplex.com/)
2. Power Threading (http://www.wintellect.com/Resources/visit-the-power-threading-library)
3. EDI Parsers (http://www.rdpcrystal.com)


Acknowledgements:

Microsoft Certified Technologist for WPF and .Net 3.5 (MCTS)
Microsoft Certified Technologist for WCF and .Net 3.5 (MCTS)
Microsoft Certified Application Developer for .Net (MCAD)
Microsoft Certified Systems Engineer (MCSE)
Microsoft Certified Professional (MCP)

Sun Certified Developer for Java 2 Platform (SCD)
Sun Certified Programmer for Java 2 Platform (SCP)
Sun Certified Web Component Developer (SCWCD)

CompTIA A+ Certified Professional

Registered Business School Teacher for Computer Programming and Computer Applications (2004)
(University of the State of New York Education Department)

Graduated from University At Stony Brook

Comments and Discussions

 
GeneralMy vote of 4 Pin
db_cooper195013-Jul-10 13:29
db_cooper195013-Jul-10 13:29 
GeneralRe: My vote of 4 Pin
FatCatProgrammer13-Jul-10 15:43
FatCatProgrammer13-Jul-10 15:43 
GeneralThe article looks good Pin
Abhishek Sur10-Jul-10 12:56
professionalAbhishek Sur10-Jul-10 12:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.