Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Creating Custom Panels In WPF

5.00/5 (12 votes)
17 Jun 2009CPOL2 min read 89.9K   1.1K  
How to create 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 it's probably true that you make most creations using a combination of the existing layouts, it's 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 Paul's 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 Mathew's book example.

It looks like this:

C#
  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:  }

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.

C#
 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.

37348/image-thumb1.png

Here is the demo project.

License

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