65.9K
CodeProject 正在变化。 阅读更多。
Home

在 WPF 中创建自定义面板

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2009年6月17日

CPOL

2分钟阅读

viewsIcon

91519

downloadIcon

1092

如何在 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 派生并实现两个重写:MeasureOverrideLayoutOverride。这些实现了两遍布局系统,在 Measure 阶段,你的父级会调用你来查看你想要多少空间。你通常会询问你的子元素需要多少空间,然后将结果返回给父级。在第二遍中,有人决定一切的大小,并将最终大小传递给你的 ArrangeOverride 方法,你可以在其中告诉子元素它们的大小并进行布局。请注意,每次你做一些影响布局的事情(例如,调整窗口大小),所有这些都会再次发生,并使用新的大小。

那么我试图通过这篇博文实现什么呢?嗯,我正在做一个爱好项目,我想要一个基于列的面板,当当前列空间不足时,它会换到新列。现在,我可以使用一个包含大量垂直 StackPanelDockPanel,但这违背了我的意图。我希望 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>

最后,这是一张屏幕截图。

37348/image-thumb1.png

这是演示项目.

© . All rights reserved.