WinRT - 自定义 WrapPanel






4.33/5 (4投票s)
如何为 Windows RT / 8 创建自定义(Wrap)面板控件。
引言
这一切都开始得很平常。 随着 Windows 8 的推出,我被要求(或者说被分配了一项任务)为我工作的公司创建一个新闻应用程序。 在整理了所有有趣的东西(如连接到 API、文章的序列化/反序列化、会话管理)之后,我开始将数据呈现给用户。
我的基本概念是将文章显示为文章元素(图像、段落、视频、图表等)的集合,并以相同宽度的多列显示。 类似于这样(随机谷歌图片)

WrapPanel 非常适合完成这项工作,但正如我所发现的那样,WinRT 中没有任何 WrapPanel(感谢 MACrosoft,好像 Vista 还不够。)
在谷歌上搜索之后,我找到了一个移植到 WinRT 的 Silverlight WrapPanel 控件,但它有一些问题。 例如,它无法自动调整大小以适应其内容,所以我决定自己动手。 既然您正在阅读本文,希望也能节省一些时间。
创建 WrapPanel
我的第一个想法是创建一个自定义面板控件,该控件将在水平堆叠面板中包含多个垂直堆叠面板,并在向控件添加和删除项目时更新内部面板的内容和计数。 最终的解决方案变得更简单,不需要任何额外的内部面板。
所以让我们开始吧。
首先,我们需要创建一个新的面板控件作为面板的后代,并声明一些有用的属性
public class WrapPanel : Panel
{
//Gets or sets whether elements are stacked vertically or horizontally.
public Orientation Orientation { get; set; }
//Gets or sets the fixed dimension size of block.
//Vertical orientation => BlockWidth
//Horizontal orientation => BlockHeight
public double BlockSize { get; set; }
//Gets or sets the amount of space in pixels between blocks.
public double BlockSpacing { get; set; }
}
稍后我们将把这些“标准”属性转换为依赖属性。 这样就可以在 XAML 中设置它们。 但首先,让我们来处理在新面板中调整控件的大小和布局。
为此,我们需要覆盖父 Panel 控件的两个方法。 即 MeasureOverride
和 ArrangeOverride
。
我不会详细描述这些方法/*这就是我们使用 msdn 的地方*/
http://msdn.microsoft.com/zh-cn/library/system.windows.frameworkelement.measureoverride.aspx
http://msdn.microsoft.com/zh-cn/library/system.windows.frameworkelement.arrangeoverride.aspx
微软基本上想告诉我们的是
使用 measure override 来报告(给父控件)您将使用的空间量。 如果父控件根据其内容自动调整大小,它将自动调整为此报告的大小(加上其他可能控件的大小)。 请注意,您报告的大小将在 ArrangeOverride 方法中传回给您。
使用 arrange override 实际在面板内布局子控件。
为了简单起见,我将 Measure/Arrange override 方法拆分为垂直和水平方向的单独方法。 您可以将它们合并在一起,但我觉得这样更易于阅读。 由于代码非常相似,我只展示垂直版本。
Measure override
protected override Size MeasureOverride(Size availableSize)
{
switch (Orientation)
{
case Orientation.Horizontal:
return MeasureOverrideHorizontal(availableSize);
case Orientation.Vertical:
default:
return MeasureOverrideVertical(availableSize);
}
}
以及垂直的 MeasureOverride
private Size MeasureOverrideVertical(Size availableSize)
{
//Create available size for child control
//In Vertical orientation child control can have a maximum width of BlockSize
//And it's height can be "unlimited" - I want to know what height would control like to have at given width
Size childAvailableSize = new Size(BlockSize, double.PositiveInfinity);
//Next, i want to stack my child controls under each other (i call it block), until i reach my available height.
//From that point i want to begin another block of controls next to the current one.
int blockCount = 0;
if (Children.Count > 0) //If i have any child controls, than i will have at least one block.
blockCount = 1;
var remainingSpace = availableSize.Height; //Set my limit as my available height.
foreach (var item in Children)
{
item.Measure(childAvailableSize); //Let the child measure itself (result of this will be in item.DesiredSize
if (item.DesiredSize.Height > remainingSpace) //If there is not enough space for this control
{
//Then we will start a new block, but only if the current block is not empty
//if its empty, then remaining space will be equal to available height.
if (remainingSpace != availableSize.Height)
{
remainingSpace = availableSize.Height;
blockCount++; //Reset remaining space and increase block count.
}
}
//In any case, decrease remaining space by desired height of control.
remainingSpace -= item.DesiredSize.Height;
}
//Now we need to report back how much size we want,
//thats number of blocks * their width, plus spaces between blocks
//And for height, we will take what ever we can get.
Size desiredSize = new Size();
if (blockCount > 0)
desiredSize.Width = (blockCount * BlockSize) + ((blockCount - 1) * BlockSpacing);
else desiredSize.Width = 0;
desiredSize.Height = availableSize.Height;
return desiredSize;
}
Arrange Override
protected override Size ArrangeOverride(Size finalSize)
{
switch (Orientation)
{
case Orientation.Horizontal:
return ArrangeOverrideHorizontal(finalSize);
case Orientation.Vertical:
default:
return ArrangeOverrideVertical(finalSize);
}
}
再一次,是 Arrange
垂直方法
private Size ArrangeOverrideVertical(Size finalSize)
{
//Each child control will be placed in rectangle with width of BlockSize
//and height of child controls desired height.
//Upper left corner of first controls rectangle will initialy start at 0,0 relative to this control
//and move down by height of control and more to the left by BlockSize once the block runs out of free space
double offsetX = 0;
double offsetY = 0;
foreach (var item in Children)
{
//If item will fit into remaining space, ....
if ((finalSize.Height - offsetY) < item.DesiredSize.Height)
{
if (offsetY != 0) //and the current block is not empty. (same rules as in measureoverride)
{
offsetX += BlockSpacing; //We will increse offset from left by the block size
offsetX += BlockSize; //and spacing between blocks
offsetY = 0; //and finally reset offset from top
}
}
//Create rectangle for child control
Rect rect = new Rect(new Point(offsetX, offsetY), new Size(BlockSize, item.DesiredSize.Height));
//And make it arrange within the rectangle, ...
item.Arrange(rect);
//Increment the offset by height.
offsetY += item.DesiredSize.Height;
}
return base.ArrangeOverride(finalSize);
}
使我们的属性对 XAML 友好
之后,剩下的就是将我们的属性转换为依赖属性。 请注意,我们需要在更改它们后更新我们的布局。 为此,我们不能仅仅将更新逻辑放入属性设置器中(这仅在运行时有效),而是需要在属性注册期间在属性元数据中注册 OnChangeHandler
。
Orientation
public Orientation Orientation
{
get { return (Orientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register("Orientation", typeof(Orientation), typeof(WrapPanel), new PropertyMetadata(Orientation.Vertical, OnOrientationPropertyChanged));
private static void OnOrientationPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
(source as WrapPanel).InvalidateMeasure();
}
BlockSize
public double BlockSize
{
get { return (double)GetValue(BlockSizeProperty); }
set { SetValue(BlockSizeProperty, value); }
}
public static readonly DependencyProperty BlockSizeProperty =
DependencyProperty.Register("BlockSize", typeof(double), typeof(WrapPanel), new PropertyMetadata(100.0, OnBlockSizePropertyChanged));
private static void OnBlockSizePropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
(source as WrapPanel).InvalidateMeasure();
}
BlockSpacing
public double BlockSpacing
{
get { return (double)GetValue(BlockSpacingProperty); }
set { SetValue(BlockSpacingProperty, value); }
}
public static readonly DependencyProperty BlockSpacingProperty =
DependencyProperty.Register("BlockSpacing", typeof(double), typeof(WrapPanel), new PropertyMetadata(0.0, OnBlockSpacingPropertyChanged));
private static void OnBlockSpacingPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
(source as WrapPanel).InvalidateMeasure();
}
注意:使用 invalidate measure 进行无效化,因为更改例如块大小将导致 Wrap 面板的尺寸发生变化。 调用 invalidate arrange 将导致重新排列控件,但 Wrap 面板的大小将保持不变。 控件将被裁剪或会留下空余空间(无论大小是增加还是减少)
使用控件
构建项目后,您应该能够将此控件拖放到设计器表面上,并像使用任何其他控件一样使用它。
最基本的使用是用于同构控件(为此,WrapGrid
应该足够了)示例 Xaml 和图像
<Grid Name="grid" Width="600" Height="160" Background="{StaticResource ApplicationPageBackgroundThemeBrush}" HorizontalAlignment="Left">
<local:WrapPanel Background="Brown" VerticalAlignment="Stretch" HorizontalAlignment="Left" MinWidth="100" BlockSize="250" BlockSpacing="10" Orientation="Vertical">
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 1" />
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 2" />
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 3" />
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 4" />
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 5" />
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 6" />
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 7" />
</local:WrapPanel>
</Grid>
但它也可以很好地用于不同大小的控件:
<Grid Name="grid" Width="550" Height="200" Background="{StaticResource ApplicationPageBackgroundThemeBrush}" HorizontalAlignment="Left">
<local:WrapPanel Background="Brown" VerticalAlignment="Stretch" HorizontalAlignment="Left" MinWidth="100" BlockSize="150" BlockSpacing="10" Orientation="Vertical">
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 1" />
<TextBlock FontSize="18" TextWrapping="Wrap" TextAlignment="Justify">Hello world, this is a test of long textblock</TextBlock>
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="90" Content="Test 2 - Bigger" />
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 3" />
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 4" />
<Button HorizontalAlignment="Stretch" Margin="0,22" VerticalAlignment="Stretch" Content="Test 5 - Margin" />
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 6" />
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Test 7" />
</local:WrapPanel>
</Grid>
注意:您看到的黑色边框来自底层网格,以使 Wrap 面板的大小可见,删除网格的固定宽度,它将自动调整为我们的面板的大小。
进一步改进提示
嗯,我只能想到一个,如果您想添加具有固定大小的控件并使块自动调整到其中最大的控件,您所要做的就是记住在 measure pass 期间找到的最大宽度,并将其设为您的 BlockSize
。 但那不是我所需要的,所以由你决定。