Click here to Skip to main content
Click here to Skip to main content

Creating Custom Panels In WPF

, 17 Jun 2009
Rate this:
Please Sign up or sign in to vote.
WPF has a number of layout Panels that you could use straight out the box, there isWrapPanelStackPanelGridCanvasDockPanelAll 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

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:      /// <span class="code-SummaryComment"><summary></span>
  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:      /// <span class="code-SummaryComment"></summary></span>
  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:          /// <span class="code-SummaryComment"><summary></span>
  39:          /// Can be used to create a new column with the ColumnedPanel
  40:          /// just before an element
  41:          /// <span class="code-SummaryComment"></summary></span>
  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:          /// <span class="code-SummaryComment"><summary></span>
 181:          /// Arranges a single column of elements
 182:          /// <span class="code-SummaryComment"></summary></span>
 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:  }

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>

And finally here is a screen shot.

image-thumb1.png

And here is the demo project custompanel.zip

License

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

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
QuestionGreat example. PinmemberSchnizit10-Feb-12 5:00 
QuestionI have a question about Measure and Arrange. PinmemberPaulo Zemek18-Aug-11 14:18 
AnswerRe: I have a question about Measure and Arrange. PinmvpSacha Barber18-Aug-11 21:43 
GeneralRe: I have a question about Measure and Arrange. [modified] PinmemberPaulo Zemek19-Aug-11 2:47 
GeneralRe: I have a question about Measure and Arrange. PinmvpSacha Barber19-Aug-11 3:29 
GeneralRe: I have a question about Measure and Arrange. PinmemberPaulo Zemek19-Aug-11 7:00 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 17 Jun 2009
Article Copyright 2009 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid