在 WPF 中创建自定义面板





5.00/5 (11投票s)
如何在 WPF 中创建自定义面板。
WPF 提供了许多可以直接使用的布局面板,例如
WrapPanel
StackPanel
Grid
画布
DockPanel
这些都很好用,但有时你想要一些更特别的东西。虽然你可能大部分的创建都使用现有布局的组合,但有时将这些封装到自定义 Panel
中会更方便。
现在,在创建自定义 Panel
时,你只需要重写两个方法,它们是
Size MeasureOverride(Size constraint)
Size ArrangeOverride(Size arrangeBounds)
关于创建自定义 Panel
的最佳文章之一是 Paul Tallett 在 CodeProject 上的文章,鱼眼面板, 简而言之,借鉴了 Paul 的优秀文章。
要启动你自己的自定义面板,你需要从 System.Windows.Controls.Panel
派生并实现两个重写:MeasureOverride
和 LayoutOverride
。这些实现了两遍布局系统,在 Measure
阶段,你的父级会调用你来查看你想要多少空间。你通常会询问你的子元素需要多少空间,然后将结果返回给父级。在第二遍中,有人决定一切的大小,并将最终大小传递给你的 ArrangeOverride
方法,你可以在其中告诉子元素它们的大小并进行布局。请注意,每次你做一些影响布局的事情(例如,调整窗口大小),所有这些都会再次发生,并使用新的大小。
那么我试图通过这篇博文实现什么呢?嗯,我正在做一个爱好项目,我想要一个基于列的面板,当当前列空间不足时,它会换到新列。现在,我可以使用一个包含大量垂直 StackPanel
的 DockPanel
,但这违背了我的意图。我希望 Panel
根据可用大小计算列中的项目数量。
所以我开始探索,并在出色的 Pro WPF in C# 2008: Windows Presentation Foundation with .NET 3.5, by Mathew McDonald 中找到一个很好的起点,所以我的代码很大程度上基于 Mathew 的书中的示例。
看起来是这样的:
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: }
我认为代码相当容易理解,它只是在有足够空间的情况下将子元素添加到当前列。如果当前列没有足够的空间,或者当前子元素选择位于新列中(通过使用 ColumnBreakBefore
DP),则剩余的子元素将从新列开始。对所有子元素重复此操作。
正如我刚才所说,子元素可以使用 ColumnBreakBefore
DP 选择位于新列中,如下所示。如果没有 ColumnBreakBefore
DP 声明,该 Button
将适合当前列。
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>
最后,这是一张屏幕截图。