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

一个 Silverlight WidgetZone 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (2投票s)

2009年10月21日

Ms-PL

4分钟阅读

viewsIcon

65649

downloadIcon

440

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

引言

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

背景

在我看来,WPF/Silverlight 控件可以分为两类。一类是布局控件,另一类是显示控件。在很多情况下,布局控件本身没有外观,只是用于放置其他控件,例如 Panel(包括 StackPanelDockPanel 等)、GridCanvas。另一方面,大多数显示控件都有自己的外观,用于向用户显示内容,例如 ButtonTextBox 等。

如果您熟悉 WPF/Silverlight 控件开发,您可能会发现这两类控件的开发模式大不相同。要实现一个布局控件,您总是重写 MeasureOverrideArrangeOverride 方法,并且该控件没有默认的 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 将每个子元素放置在正确的列和行中。我们使用 ColumnRow 附加属性来指示元素所在的列和行。附加属性是可以附加到另一个元素的属性。有关附加属性的更多信息,请参阅 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();
    }
}

当元素移动时,它可能会移动到另一列或行,因此我们指示如果现在放置,移动的元素将被放置在哪里。我们通过 GetColumnByPositionGetRowByPosition 来计算当前列和行。

当左键抬起时,我们通过将 ColumnRow 属性附加到占位符来将元素放置在占位符指示的列和行中。

// 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 日:首次发布
© . All rights reserved.