Creating Custom Panels In WPF

WPF has a number of layout Panels that you could use straight out the box, there is

  • WrapPanel
  • StackPanel
  • Grid
  • Canvas
  • DockPanel

All of which are great, but occasionally you want something a little bit special. Whilst its probably true that you make most creations using a combination of the existing layouts, its sometimes just more convenient to wrap this into a custom Panel .

Now when creating custom Panels, there are just 2 methods that you need to override, these are  :

  • Size MeasureOverride(Size constraint)
  • Size ArrangeOverride(Size arrangeBounds)

One of the best articles I’ve ever seen on creating custom Panels is the article by Paul Tallett over at codeproject, Fisheye Panel, paraphrasing Pauls excellent article.

To get your own custom panel off the ground, you need to derive from System.Windows.Controls.Panel and implement two overrides: MeasureOverride and LayoutOverride. These implement the two-pass layout system where during the Measure phase, you are called by your parent to see how much space you’d like. You normally ask your children how much space they would like, and then pass the result back to the parent. In the second pass, somebody decides on how big everything is going to be, and passes the final size down to your ArrangeOverride method where you tell the children their size and lay them out. Note that every time you do something that affects layout (e.g., resize the window), all this happens again with new sizes.

So what am I trying to achieve with this blog, well I am working on a hobby project where I wanted a column based panel that wrapped to a new column, when it ran out of space in the current column. Now I could have just used a DockPanel, that contained loads of vertical  StackPanels, but that defeats what I am after. I want the Panel to work out how many items are in a column based on the available size.

So I set to work exploring, and I found an excellent start within the superb Pro WPF in C# 2008: Windows Presentation Foundation with .NET 3.5, by Mathew McDonald, so my code is largely based on Mathews book example.

It looks like this:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using System.Windows.Controls;
   5:  using System.Windows;
   6:  using System.Windows.Media;
   7:  
   8:  namespace CustomPanel
   9:  {
  10:      /// <summary>
  11:      /// A column based layout panel, that automatically
  12:      /// wraps to new column when required. The user
  13:      /// may also create a new column before an element
  14:      /// using the 
  15:      /// </summary>
  16:      public class ColumnedPanel : Panel
  17:      {
  18:  
  19:          #region Ctor
  20:          static ColumnedPanel()
  21:          {
  22:              //tell DP sub system, this DP, will affect
  23:              //Arrange and Measure phases
  24:              FrameworkPropertyMetadata metadata =
  25:                  new FrameworkPropertyMetadata();
  26:              metadata.AffectsArrange = true;
  27:              metadata.AffectsMeasure = true;
  28:              ColumnBreakBeforeProperty =
  29:                  DependencyProperty.RegisterAttached(
  30:                  "ColumnBreakBefore",
  31:                  typeof(bool), typeof(ColumnedPanel),
  32:                  metadata);
  33:          }
  34:          #endregion
  35:  
  36:          #region DPs
  37:  
  38:          /// <summary>
  39:          /// Can be used to create a new column with the ColumnedPanel
  40:          /// just before an element
  41:          /// </summary>
  42:          public static DependencyProperty ColumnBreakBeforeProperty;
  43:  
  44:          public static void SetColumnBreakBefore(UIElement element,
  45:              Boolean value)
  46:          {
  47:              element.SetValue(ColumnBreakBeforeProperty, value);
  48:          }
  49:          public static Boolean GetColumnBreakBefore(UIElement element)
  50:          {
  51:              return (bool)element.GetValue(ColumnBreakBeforeProperty);
  52:          }
  53:          #endregion
  54:  
  55:          #region Measure Override
  56:          // From MSDN : When overridden in a derived class, measures the 
  57:          // size in layout required for child elements and determines a
  58:          // size for the FrameworkElement-derived class
  59:          protected override Size MeasureOverride(Size constraint)
  60:          {
  61:              Size currentColumnSize = new Size();
  62:              Size panelSize = new Size();
  63:  
  64:              foreach (UIElement element in base.InternalChildren)
  65:              {
  66:                  element.Measure(constraint);
  67:                  Size desiredSize = element.DesiredSize;
  68:  
  69:                  if (GetColumnBreakBefore(element) ||
  70:                      currentColumnSize.Height + desiredSize.Height >
  71:                      constraint.Height)
  72:                  {
  73:                      // Switch to a new column (either because the 
  74:                      //element has requested it or space has run out).
  75:                      panelSize.Height = Math.Max(currentColumnSize.Height,
  76:                          panelSize.Height);
  77:                      panelSize.Width += currentColumnSize.Width;
  78:                      currentColumnSize = desiredSize;
  79:  
  80:                      // If the element is too high to fit using the 
  81:                      // maximum height of the line,
  82:                      // just give it a separate column.
  83:                      if (desiredSize.Height > constraint.Height)
  84:                      {
  85:                          panelSize.Height = Math.Max(desiredSize.Height,
  86:                              panelSize.Height);
  87:                          panelSize.Width += desiredSize.Width;
  88:                          currentColumnSize = new Size();
  89:                      }
  90:                  }
  91:                  else
  92:                  {
  93:                      // Keep adding to the current column.
  94:                      currentColumnSize.Height += desiredSize.Height;
  95:  
  96:                      // Make sure the line is as wide as its widest element.
  97:                      currentColumnSize.Width =
  98:                          Math.Max(desiredSize.Width,
  99:                          currentColumnSize.Width);
 100:                  }
 101:              }
 102:  
 103:              // Return the size required to fit all elements.
 104:              // Ordinarily, this is the width of the constraint, 
 105:              // and the height is based on the size of the elements.
 106:              // However, if an element is higher than the height given
 107:              // to the panel,
 108:              // the desired width will be the height of that column.
 109:              panelSize.Height = Math.Max(currentColumnSize.Height,
 110:                  panelSize.Height);
 111:              panelSize.Width += currentColumnSize.Width;
 112:              return panelSize;
 113:  
 114:          }
 115:          #endregion
 116:  
 117:          #region Arrange Override
 118:          //From MSDN : When overridden in a derived class, positions child
 119:          //elements and determines a size for a FrameworkElement derived
 120:          //class.
 121:  
 122:          protected override Size ArrangeOverride(Size arrangeBounds)
 123:          {
 124:              int firstInLine = 0;
 125:  
 126:              Size currentColumnSize = new Size();
 127:  
 128:              double accumulatedWidth = 0;
 129:  
 130:              UIElementCollection elements = base.InternalChildren;
 131:              for (int i = 0; i < elements.Count; i++)
 132:              {
 133:  
 134:                  Size desiredSize = elements[i].DesiredSize;
 135:  
 136:                  //need to switch to another column
 137:                  if (GetColumnBreakBefore(elements[i]) ||
 138:                      currentColumnSize.Height +
 139:                      desiredSize.Height >
 140:                      arrangeBounds.Height)
 141:                  {
 142:                      arrangeColumn(accumulatedWidth,
 143:                          currentColumnSize.Width,
 144:                          firstInLine, i, arrangeBounds);
 145:  
 146:                      accumulatedWidth += currentColumnSize.Width;
 147:                      currentColumnSize = desiredSize;
 148:  
 149:                      //the element is higher then the constraint - 
 150:                      //give it a separate column 
 151:                      if (desiredSize.Height > arrangeBounds.Height)
 152:                      {
 153:                          arrangeColumn(accumulatedWidth,
 154:                              desiredSize.Width, i, ++i, arrangeBounds);
 155:                          accumulatedWidth += desiredSize.Width;
 156:                          currentColumnSize = new Size();
 157:                      }
 158:                      firstInLine = i;
 159:                  }
 160:                  else //continue to accumulate a column
 161:                  {
 162:                      currentColumnSize.Height += desiredSize.Height;
 163:                      currentColumnSize.Width =
 164:                          Math.Max(desiredSize.Width,
 165:                          currentColumnSize.Width);
 166:                  }
 167:              }
 168:  
 169:              if (firstInLine < elements.Count)
 170:                  arrangeColumn(accumulatedWidth,
 171:                      currentColumnSize.Width,
 172:                      firstInLine, elements.Count,
 173:                      arrangeBounds);
 174:  
 175:              return arrangeBounds;
 176:          }
 177:          #endregion
 178:  
 179:          #region Private Methods
 180:          /// <summary>
 181:          /// Arranges a single column of elements
 182:          /// </summary>
 183:          private void arrangeColumn(double x,
 184:              double columnWidth, int start,
 185:              int end, Size arrangeBounds)
 186:          {
 187:              double y = 0;
 188:              double totalChildHeight = 0;
 189:              double widestChildWidth = 0;
 190:              double xOffset = 0;
 191:  
 192:              UIElementCollection children = InternalChildren;
 193:              UIElement child;
 194:  
 195:              for (int i = start; i < end; i++)
 196:              {
 197:                  child = children[i];
 198:                  totalChildHeight += child.DesiredSize.Height;
 199:                  if (child.DesiredSize.Width > widestChildWidth)
 200:                      widestChildWidth = child.DesiredSize.Width;
 201:              }
 202:  
 203:              //work out y start offset within a given column
 204:              y = ((arrangeBounds.Height - totalChildHeight) / 2);
 205:  
 206:  
 207:              for (int i = start; i < end; i++)
 208:              {
 209:                  child = children[i];
 210:                  if (child.DesiredSize.Width < widestChildWidth)
 211:                  {
 212:                      xOffset = ((widestChildWidth -
 213:                          child.DesiredSize.Width) / 2);
 214:                  }
 215:  
 216:                  child.Arrange(new Rect(x + xOffset, y,
 217:                      child.DesiredSize.Width, columnWidth));
 218:                  y += child.DesiredSize.Height;
 219:                  xOffset = 0;
 220:              }
 221:          }
 222:          #endregion
 223:  
 224:      }
 225:  
 226:  
 227:  }

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, “Courier New”, courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

I think the code is fairly self explanatory, it just keeps adding children to the current column if there is enough space. If there isn’t enough space within the current column or the current children has opted to be in a new column, by using the ColumnBreakBefore DP, the remaining children will start within a new column. This is repeated for all children.

As I just stated the child can opt to be in a new column, using the ColumnBreakBefore DP, this is shown below. Without the ColumnBreakBefore DP declaration the Button would fit within the current column.

   1:  <Window x:Class="CustomPanel.Window1"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:      xmlns:local="clr-namespace:CustomPanel;assembly="
   5:      Title="Window1" Height="300" Width="300">
   6:  
   7:  
   8:      <local:ColumnedPanel Width="auto" Height="200"
   9:                           VerticalAlignment="Center" Background="WhiteSmoke">
  10:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  11:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  12:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  13:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  14:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  15:          <!-- Without the DP ColumnedPanel.ColumnBreakBefore set here, 
  16:               this button would fit in the current column-->
  17:          <Button local:ColumnedPanel.ColumnBreakBefore="True"
  18:                  FontWeight="Bold" Width="80" Height="80">New Column</Button>
  19:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  20:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  21:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  22:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  23:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  24:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  25:      </local:ColumnedPanel>
  26:  </Window>

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, “Courier New”, courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }And finally here is a screen shot.

image

And here is the demo project ColumnedPanel.zip

About these ads

7 thoughts on “Creating Custom Panels In WPF

  1. Emilio says:

    Could a custom panel made in this way decide that a child will be outside of the viewable area, or even clipped in any way? What do you think?

  2. siroman says:

    Hi Sacha,
    I’m a newbie in WPF, and your articles helps me a lot to learn. Just one question (maybe a stupid one): what’s the difference between ColumnedPanel and the standard WrapPanel with Orientation property set to Vertical?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s