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 Panel
s, 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 Panel
s 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 StackPanel
s, 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:
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: 11: 12: 13: 14: 15: 16: public class ColumnedPanel : Panel
17: {
18:
19: #region Ctor
20: static ColumnedPanel()
21: {
22:
23:
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: 39: 40: 41: 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:
57:
58:
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:
74:
75: panelSize.Height = Math.Max(currentColumnSize.Height,
76: panelSize.Height);
77: panelSize.Width += currentColumnSize.Width;
78: currentColumnSize = desiredSize;
79:
80:
81:
82:
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:
94: currentColumnSize.Height += desiredSize.Height;
95:
96:
97: currentColumnSize.Width =
98: Math.Max(desiredSize.Width,
99: currentColumnSize.Width);
100: }
101: }
102:
103:
104:
105:
106:
107:
108:
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:
119:
120:
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:
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:
150:
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
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: 181: 182: 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:
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:
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.
Here is the demo project.