一个 Silverlight WidgetZone 控件
如今,Widget 开发越来越流行,拖放支持是 Widget 平台的基本功能。如果您想开发一个 Silverlight Widget 平台,您可能需要一个支持在其上拖放 UIElement 的 Panel。WidgetZone 就是一个在这种情况下可以使用的 Panel。

引言
如今,Widgets 开发越来越流行,拖放支持是 Widget 平台的基本功能。如果您想开发一个 Silverlight Widget 平台,您可能需要一个支持在其上拖放 UIElement
的 Panel
。WidgetZone
就是一个在这种情况下可以使用的 Panel。它支持在其上拖放元素。
背景
在我看来,WPF/Silverlight 控件可以分为两类。一类是布局控件,另一类是显示控件。在很多情况下,布局控件本身没有外观,只是用于放置其他控件,例如 Panel
(包括 StackPanel
、DockPanel
等)、Grid
和 Canvas
。另一方面,大多数显示控件都有自己的外观,用于向用户显示内容,例如 Button
、TextBox
等。
如果您熟悉 WPF/Silverlight 控件开发,您可能会发现这两类控件的开发模式大不相同。要实现一个布局控件,您总是重写 MeasureOverride
和 ArrangeOverride
方法,并且该控件没有默认的 ControlTemplate
。要开发一个显示控件,您总是为其默认外观提供一个默认的 ControlTemplate
。并且您的代码不应干扰控件的外观。
WidgetZone
是一个布局控件,用于在其上放置一个项目并支持拖放该项目。它在很大程度上类似于 Grid
控件,但支持将元素拖放到其他列或行。
实现 WidgetZone 控件
首先,我们需要决定我们的控件应从哪里(或哪个控件)派生?乍一看,它有点像 Grid
,所以也许从 Grid
派生是正确的。但如果您仔细检查其功能,它比其他控件更像一个 Panel
。
其次,如何定义 WidgetZone
包含的列?如果我们支持 XAML 绑定,我们必须提供一个 TypeConverter
来将 string
转换为列定义类型。
定义列的最简单方法是按比例定义每列的宽度,例如“3:2:1”,这意味着如果总宽度为 600 像素,则第一列为 300 像素,第二列为 200 像素,最后一列为 100 像素。我们在 ColumnPartitions
类中定义这种比例表示,并使用 ColumnPartitionsConverter
将其从字符串表示(在 XAML 文件中)转换为 ColumnPartitions
对象。
Panel 使用 MeasureOverride
来计算每个子元素的大小,并使用 ArrangeOverride
来排列它。因此,布局控件的核心实现就是这两个函数。让我们来看看它们。
/// <summary>
/// Provides the behavior for the "measure" pass of Silverlight layout.
/// </summary>
/// <param name="availableSize">The size that this object
/// should use to measure its child objects. </param>
/// <returns>The actual size used. </returns>
protected sealed override Size MeasureOverride(Size availableSize)
{
// Calc columns width and left
CalcColumnLeftAndWidth(availableSize.Width);
// First of all, create the column based dictionary
BuildDictionary(this.Children);
// The used height of each column
double[] usedHeights = new double[Partitions.ColumnsCount];
// we measure each item based on it's column and row
foreach (var item in columnDictionary)
{
// Calc the column width
double columnWidth = columnWidths[item.Key];
// for each item in a column
foreach (UIElement element in item.Value)
{
// Attach event handle
AttachEventHandle(element);
// Arrange
if (GetIsMoving(element))
{
// If the element in moving
// The moving item dose not consume the height
element.Measure(new Size(columnWidth,
availableSize.Height - usedHeights[item.Key]));
}
else
{
try
{
// We measure the desired size based on each column
element.Measure(new Size(columnWidth, availableSize.Height -
usedHeights[item.Key]));
usedHeights[item.Key] += (element.DesiredSize.Height +
RowSpacing);
}
catch
{
}
}
}
}
// The final desired size
double desiredHeight = 0;
if (this.VerticalAlignment == VerticalAlignment.Stretch)
desiredHeight = availableSize.Height;
// Get Max height of the columns
foreach (double height in usedHeights)
{
desiredHeight = Math.Max(desiredHeight, height);
}
return new Size(availableSize.Width, desiredHeight);
}
MeasureOverride
首先计算每列的宽度和左侧位置,然后根据每个子元素所在的列构建一个字典。然后,它计算每个子元素在所需列和行中的大小。
/// <summary>
/// Provides the behavior for the "arrange" pass of Silverlight layout.
/// </summary>
/// <param name="finalSize">The size that this object should use
/// to arrange its child objects. </param>
/// <returns>The actual size used. </returns>
protected sealed override Size ArrangeOverride(Size finalSize)
{
// The used height of each column
double[] usedHeights = new double[Partitions.ColumnsCount];
// We arrange each item based on it's column then row
foreach (var item in columnDictionary)
{
// for each UIElement in a column
foreach (UIElement element in item.Value)
{
if (GetIsMoving(element))
{
Canvas.SetZIndex(element, 10);
// If the element is in moving state
// we arrange it like Canvas
double left = Canvas.GetLeft(element);
double top = Canvas.GetTop(element);
// The moving item dose not eat height
Rect finalRect = new Rect(left, top,
columnWidths[item.Key], element.DesiredSize.Height);
element.Arrange(finalRect);
}
else
{
// set Canvas left and top for moving
Canvas.SetLeft(element, columnLefts[item.Key]);
Canvas.SetTop(element, usedHeights[item.Key]);
// Calc the final rect
Rect finalRect = new Rect(columnLefts[item.Key],
usedHeights[item.Key],
columnWidths[item.Key], element.DesiredSize.Height);
usedHeights[item.Key] +=
(element.DesiredSize.Height + RowSpacing);
element.Arrange(finalRect);
}
}
}
// The final size
double finalHeight = 0.0;
if (this.VerticalAlignment == VerticalAlignment.Stretch)
finalHeight = finalSize.Height;
// Gets the max height of the columns
foreach (double height in usedHeights)
{
finalHeight = Math.Max(finalHeight, height);
}
return new Size(finalSize.Width, finalHeight);
}
ArrangeOverride
将每个子元素放置在正确的列和行中。我们使用 Column
和 Row
附加属性来指示元素所在的列和行。附加属性是可以附加到另一个元素的属性。有关附加属性的更多信息,请参阅 MSDN。如果一个元素正在移动,这意味着该元素正在被用户拖动,它可以覆盖其他元素。
现在让我们看看如何拖放一个元素。我们通常使用鼠标来拖动一个元素,所以我们需要处理鼠标左键按下和鼠标移动事件。我们附加这些事件并进行处理来实现拖放。
当鼠标左键在一个元素上按下时,我们这样做
// The mouse left button down
void element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (EditMode)
{
// if in edit mode, user can drag move the item from one place to other
UIElement element = sender as UIElement;
currentColumn = GetColumn(element);
currentRow = GetRow(element);
if (placeholder == null)
{
placeholder = CreatePlaceholder();
this.Children.Add(placeholder);
}
// Set placeholder property
placeholder.Height = element.RenderSize.Height + 2;
placeholder.Width = columnWidths[currentColumn];
SetColumn(placeholder, currentColumn);
SetRow(placeholder, currentRow);
placeholder.Visibility = Visibility.Visible;
SetIsMoving(element, true);
orgPoint = e.GetPosition(element);
element.CaptureMouse();
}
}
在代码中,我们创建一个占位符来指示该元素将被移动,并通过将 IsMoving
附加属性附加到该元素来将其设置为移动状态。
当鼠标移动时,选定的元素应该随着鼠标移动。
// The mouse move
void element_MouseMove(object sender, MouseEventArgs e)
{
UIElement element = sender as UIElement;
if (GetIsMoving(element))
{
Point point = e.GetPosition(this);
int columnIndex = GetColumnByPosition(point);
int rowIndex = GetRowByPosition(columnIndex, point);
if (currentColumn != columnIndex)
{
// column changed
orgPoint.X = orgPoint.X * columnWidths[columnIndex] /
columnWidths[currentColumn];
placeholder.Width = columnWidths[columnIndex];
SetColumn(placeholder, columnIndex);
SetColumn(element, columnIndex);
currentColumn = columnIndex;
}
if (currentRow != rowIndex)
{
// Row changed
SetRow(placeholder, rowIndex);
currentRow = rowIndex;
}
Canvas.SetLeft(element, point.X - orgPoint.X);
Canvas.SetTop(element, point.Y - orgPoint.Y);
this.InvalidateArrange();
}
}
当元素移动时,它可能会移动到另一列或行,因此我们指示如果现在放置,移动的元素将被放置在哪里。我们通过 GetColumnByPosition
和 GetRowByPosition
来计算当前列和行。
当左键抬起时,我们通过将 Column
和 Row
属性附加到占位符来将元素放置在占位符指示的列和行中。
// The mouse left button up
void element_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
UIElement element = sender as UIElement;
SetIsMoving(element, false);
if (placeholder != null)
{
// place the moving element to dest
SetColumn(element, currentColumn);
SetRow(element, currentRow);
placeholder.Visibility = Visibility.Collapsed;
}
element.ReleaseMouseCapture();
}
使用 WidgetZone
如果您想在您的 SL 应用程序中使用 WidgetZone
,首先引用 Cokkiy.Widgets.Widget
程序集,然后在您的 XAML 文件中,像这样操作
<UserControl x:Class="WidgetZoneTest.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:widgets="clr-namespace:Cokkiy.Widgets;assembly=Cokkiy.Widgets.Widget"
xmlns:zone="clr-namespace:Cokkiy.Widgets;assembly=Cokkiy.Widgets.WidgetZone"
xmlns:local="clr-namespace:WidgetZoneTest"
Width="Auto" Height="480">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<zone:WidgetZone VerticalAlignment="Stretch" Name="myZone" Partitions="3:2:1">
<Grid Height="40" Background="Yellow" zone:WidgetZone.Column="1"/>
<Grid Background="Red" Height="50" zone:WidgetZone.Column="0">
</Grid>
<Grid Background="Beige" Height="30" zone:WidgetZone.Column="0"
zone:WidgetZone.Row="1"/>
<Grid Background="CadetBlue" Height="40"
zone:WidgetZone.Column="0" zone:WidgetZone.Row="2"/>
<Grid Background="Chartreuse" Height="30"
zone:WidgetZone.Column="1" zone:WidgetZone.Row="1"/>
<Grid Background="DarkMagenta" Height="80"
zone:WidgetZone.Column="1" zone:WidgetZone.Row="2"/>
<widgets:Widget Title="My Widget" ShowTitleBar="False"
zone:WidgetZone.Column="1">
<widgets:Widget.Editor>
<local:MyEditor/>
</widgets:Widget.Editor>
</widgets:Widget>
</zone:WidgetZone>
<Button Grid.Row="1" Content="Goto Edit Mode"
x:Name="editModeButton" Click="editModeButton_Click"></Button>
</Grid>
</UserControl>
历史
- 2009 年 10 月 21 日:首次发布