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

FishEyePanel/FanPanel - WPF 自定义布局面板示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (65投票s)

2006年9月25日

Ms-PL

6分钟阅读

viewsIcon

309203

downloadIcon

11725

本文介绍了如何实现自己的WPF布局面板,类似于Grid和StackPanel。

FishEye demo

FishEye demo

引言

最近,我需要编写一些自定义面板(类似于GridStackPanel,但布局方式不同)。这些面板效果相当不错,似乎是关于如何编写自定义面板的好例子,因此我想与大家分享。这些面板是作为一个概念验证(Proof-Of-Concept)的一部分编写的,而不是生产质量的代码,所以,当然,在代码清理以提交时,我最终进行了重写,但这很正常!

有两个不同的面板 - FishEyePanelFanPanel。它们都是由Martin Grayson设计的,他绘制了线框图 - 然后我编写了代码。

FishEyePanel相当完整,它实现了一种变体,类似于Expression Interactive Designer示例中的Hyperbar示例,以及MAC任务栏上的功能。这个控件的区别在于子元素的有趣增长,而其他子元素则收缩以腾出空间 - 与Hyperbar和MAC不同。这意味着面板的宽度保持不变。

FanPanel将它的子元素排列成一个堆栈,当鼠标悬停在上面时会展开。然后,当您单击时,它会展开到全屏视图。该面板需要更多的工作,因为将子元素的大小设置为大约300平方像素以外的值会导致效果不正确。

我为微软工作并不意味着这些示例是正确的。我不是产品组的成员,也没有任何内部信息。我采用了将子元素放置在 (0,0) 位置重叠的策略,然后使用附加到子元素的RenderTransforms来将它们移动到我想要的位置。我不知道这样做是否是“正规”的做法,但它似乎效果很好。

使用代码

要编译代码,您需要VS2005、.NET Framework 3.0 (RC1)以及Visual Studio "Orcas" 扩展。如果您没有这些扩展,VS将无法识别项目类型。

我包含了预编译的EXE文件,因此您只需使用RC1 .NET Framework即可进行体验。代码只需重新编译即可用于后续版本的框架。

编写自定义面板

要让自己的自定义面板启动并运行,您需要从System.Windows.Controls.Panel派生并实现两个重载:MeasureOverrideLayoutOverride。它们实现了两阶段布局系统,在Measure阶段,父容器会调用您来了解您需要多少空间。您通常会询问子元素它们需要多少空间,然后将结果返回给父容器。在第二阶段,有人会决定所有元素的大小,并将最终尺寸传递给您的ArrangeOverride方法,您在该方法中告诉子元素它们的大小并进行布局。请注意,每次执行影响布局的操作时(例如,调整窗口大小),都会以新的尺寸重新执行所有这些操作。

protected override Size MeasureOverride(Size availableSize)
{
    Size idealSize = new Size(0, 0);

    // Allow children as much room as they want - then scale them
    Size size = new Size(Double.PositiveInfinity, Double.PositiveInfinity);
    foreach (UIElement child in Children)
    {
        child.Measure(size);
        idealSize.Width += child.DesiredSize.Width;
        idealSize.Height = Math.Max(idealSize.Height, 
                           child.DesiredSize.Height);
    }

    // EID calls us with infinity, but framework
    // doesn't like us to return infinity
    if (double.IsInfinity(availableSize.Height) || 
        double.IsInfinity(availableSize.Width))
        return idealSize;
    else
        return availableSize;
}

在我们的MeasureOverride中,我们不顾一切,让我们的子元素拥有它们想要的所有空间。然后,我们将我们的理想尺寸告知父容器,在我们的例子中,父容器会忽略它。子元素通过在Measure调用期间设置的child.DesiredSize属性告诉我们它们想要的大小。我们将根据施加给我们的任何尺寸来缩放我们的子元素。

protected override Size ArrangeOverride(Size finalSize)
{
    if (this.Children == null || this.Children.Count == 0)
        return finalSize;

    ourSize = finalSize;
    totalWidth = 0;

    foreach (UIElement child in this.Children)
    {
        // If this is the first time
        // we've seen this child, add our transforms
        if (child.RenderTransform as TransformGroup == null)
        {
            child.RenderTransformOrigin = new Point(0, 0.5);
            TransformGroup group = new TransformGroup();
            child.RenderTransform = group;
            group.Children.Add(new ScaleTransform());
            group.Children.Add(new TranslateTransform());
//                    group.Children.Add(new RotateTransform());
        }

        child.Arrange(new Rect(0, 0, child.DesiredSize.Width, 
                      child.DesiredSize.Height));

        totalWidth += child.DesiredSize.Width;
    }

    AnimateAll();

    return finalSize;
}

在我们的ArrangeOverride中,我们向每个子元素添加缩放和平移变换,并计算它们总共想要的大小。该面板可以处理不同大小的子元素,您可以选择单独缩放它们使它们宽度相同,或者让它们以不同的尺寸显示。这由ScaleToFit属性控制。注意:我们只是将所有子元素堆叠在 (0,0) 位置,稍后我们会使用RenderTransforms来移动它们。

FishEyePanel的核心代码如下:

// These next few lines took two of us hours to write!
double mag = Magnification;
double extra = 0;
if (theChild != null)
    extra += mag - 1;

if (prevChild == null)
    extra += ratio * (mag - 1);
else if (nextChild == null)
    extra += ((mag - 1 ) * (1 - ratio));
else
    extra += mag - 1;

double prevScale = this.Children.Count * (1 + ((mag - 1) * 
                  (1 - ratio))) / (this.Children.Count + extra);
double theScale = (mag * this.Children.Count) / 
                  (this.Children.Count + extra);
double nextScale = this.Children.Count * (1 + ((mag - 1) * ratio)) / 
                                      (this.Children.Count + extra);
double otherScale = this.Children.Count / 
                   (this.Children.Count + extra);
                   // Applied to all non-interesting children

这是我写过的最难的代码之一,我们两个人花了半天时间,拿着笔和纸才弄清楚!我不会解释数学原理 - 只需要知道它会根据鼠标的位置将三个子元素缩放得比其他元素大,并调整其他所有元素的大小以占据剩余的空间。

FanPanel的核心代码如下:

if (!IsWrapPanel)
{
    if (!this.IsMouseOver)
    {
        // Rotate all the children into a stack
        double r = 0;
        int sign = +1;
        foreach (UIElement child in this.Children)
        {
            if (foundNewChildren)
                child.SetValue(Panel.ZIndexProperty, 0);

            AnimateTo(child, r, 0, 0, scaleFactor);
            r += sign * 15;         // +-15 degree intervals
            if (Math.Abs(r) > 90)
            {
                r = 0;
                sign = -sign;
            }
        }
    }
    else
    {
        // On mouse over explode out the children and don't rotate them
        Random rand = new Random();
        foreach (UIElement child in this.Children)
        {
            child.SetValue(Panel.ZIndexProperty, 
                           rand.Next(this.Children.Count));
            double x = (rand.Next(16) - 8) * ourSize.Width / 32;
            double y = (rand.Next(16) - 8) * ourSize.Height / 32;
            AnimateTo(child, 0, x, y, scaleFactor);
        }
    }
}
else
{
    // Pretend to be a wrap panel
    double maxHeight = 0, x = 0, y = 0;
    foreach (UIElement child in this.Children)
    {
        if (child.DesiredSize.Height > maxHeight)
        // Row height
            maxHeight = child.DesiredSize.Height;
        if (x + child.DesiredSize.Width > this.ourSize.Width)
        {
            x = 0;
            y += maxHeight;
        }

        if (y > this.ourSize.Height - maxHeight)
            child.Visibility = Visibility.Hidden;
        else
            child.Visibility = Visibility.Visible;

        AnimateTo(child, 0, x, y, 1);
        x += child.DesiredSize.Width;
    }
}

同样,这会缩放/旋转/变换所有子元素,使它们有三种可能的排列方式:堆叠、展开或流式面板风格。请注意,我们在不同状态之间进行动画处理,因此添加子元素看起来也很酷(演示中未显示)。

FanPanel不如FishEyePanel干净,因为我先写的它。不过,大部分的“脏活”都在Favourites.xamlFavourites.xaml.cs文件中,而不是在控件本身。我不太喜欢展开时需要改变面板大小的方式,而且要让两组动画看起来协调一致很棘手。不过,效果看起来很酷,所以我还是决定提交它。如果您想在自己的面板基础上使用其中一个控件,请选择FishEyePanel

关注点

有两个很棒的功能,花了些时间才弄清楚,并且在两个示例中都使用了。

如果您需要获取ItemsControl中使用的面板的引用,这并不容易。您不能仅仅给它命名,因为它在模板中,而那不是真正的控件。我最终想出了一个方法,即挂载Loaded事件并将发送者强制转换为相关的Panel类型。

我测试数据的方式也很酷。Martin Grayson想出了这个方法 - 看一下TestData.xaml。它使用了一个XmlDataProvider。我发现XPath和Binding语句很难弄对,但这是一个在等待中层提供真实数据时快速模拟数据的绝佳方式。

<XmlDataProvider x:Key="Things" XPath="Things/Thing">
 <x:XData>
  <Things xmlns="">
    <Thing Image="Aquarium.jpg"/>
    <Thing Image="Ascent.jpg"/>
    <Thing Image="Autumn.jpg"/>
    <Thing Image="Crystal.jpg"/>
    <Thing Image="DaVinci.jpg"/>
    <Thing Image="Follow.jpg"/>
    <Thing Image="Friend.jpg"/>
    <Thing Image="Home.jpg"/>
    <Thing Image="Moon flower.jpg"/>
  </Things>
 </x:XData>
</XmlDataProvider>

另外,请注意如何在App.xaml中引用资源,以便它们全局可用。

另一个非常出色的属性是设置IsHitTestVisible = false。这意味着对于所有鼠标命中测试,它都是不可见的,所有事件都会传递给父容器,就好像它不存在一样。在实现拖放时,当您移动一个项目,使其跟随鼠标移动时,这尤其有用。您可以设置此属性,鼠标移动会传递给下面的父容器。这花了两天时间才发现!

另一个很酷的功能是ImagePathConverter。它允许您指定图像的相对路径 - 转换器中的代码会查找“Images”文件夹并重新映射引用。

public class ImagePathConverter : IValueConverter
{
    #region IValueConverter Members

    private static string path;

    public object Convert(object value, Type targetType, 
                  object parameter, 
                  System.Globalization.CultureInfo culture)
    {
        if (path == null)
        {
            path = Path.GetDirectoryName(Path.GetDirectoryName(
                   Path.GetDirectoryName(
                   Assembly.GetExecutingAssembly().Location))) + 
                   "\\Images";
            if (!Directory.Exists(path))
            {
                path = Path.GetDirectoryName(
                       Assembly.GetExecutingAssembly().Location) + 
                       "\\Images";
                if (!Directory.Exists(path))
                    throw new FileNotFoundException("Can't " + 
                                  "find images folder", path);
            }
            path += "\\";
        }
        return string.Format("{0}{1}", path, (string)value);
    }

    public object ConvertBack(object value, Type targetType, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        throw new Exception("The method or operation is not implemented.");
    }

    #endregion
}

最后一点需要注意:在FishEyePanel中,向其中填充宽度相同的子元素,与要求它将子元素缩放到相同宽度之间存在差异 - 请注意,在顶行末尾的粉色房子上,它的边距比其他房子稍大。这是因为它最初是一个较小的子元素,然后被缩放了,包括边距。为了获得最佳效果,请向其中填充相同大小的子元素。

摘要

实现自己的面板会很有趣,但布局代码可能真的很难编写。您需要解决联立方程之类的难题。

请随意使用此代码。如果您愿意,甚至可以将其包含在您销售的产品中。我的目标是推动WPF的应用,这也是我发布示例代码的原因。

历史

已修复,以处理不同大小的子元素 - 添加了ScaleToFit属性。

© . All rights reserved.