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

WPF 的简单 Metro 风格 Panorama 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (59投票s)

2012年4月21日

CPOL

7分钟阅读

viewsIcon

318253

downloadIcon

16869

一个适用于 WPF 的简易 Metro 风格 Panorama 控件。

注意

上面提供的备用演示项目和触摸项目是由 Jonathan Hodges 提交给我的。

备用演示:修复了布局问题,并支持鼠标滚轮滚动。感谢 Jonathan,编辑得很棒。 

触摸演示:支持触摸事件,适用于触摸屏。 

更好的新改进的 Panorama 鼠标处理程序:Marc Jacobi 

引言

附加的代码实际上并没有做太多事情,所以不需要太多额外的说明来描述它的功能。它的功能非常简单,可以很容易地总结为以下几点:

  • 创建一个 Metro 风格的控件。我说风格,是因为它可能与 Win8 Metro 磁贴界面所做的或其功能不完全一致,但说实话,它足够满足我的需求,并且我认为对其他人来说也可能不错。所以请注意,它不是最棒的控件,实际上它非常简单。
  • 允许以直接内容模式使用,或使用 MVVM 通过数据绑定来驱动 UI 元素的创建。
  • 允许用户自定义生成磁贴时应使用的主要/辅助颜色。
  • 允许单个磁贴组回弹动画(你知道当你在 iPhone UI 体验中没有拖动超过一半的距离时,它会回弹到之前的磁贴组)。
  • 适用于 WPF。

我编写这个控件是因为我找不到一个可用的,我知道 Windows7 的 contrib/源代码中有一个,但我想要一个用于 WPF 的,所以不得不自己编写。

所以,总而言之,这就是它所做的。我想这时展示一两张截图会比较好,那就这么做吧。

移动部分的截图

这是所有组件如何组合在一起的(您可以单击此图像获取更大的图像)。

这里需要注意的重要一点是,您可以自己定义磁贴的外观,这完全取决于您。您完全可以控制磁贴的显示方式,这通常是通过为您的磁贴类型使用 `DataTemplate` 来完成的。我们稍后会详细介绍。

深入了解其工作原理

实际上我们只需要关心少数几个控件,下面将对它们进行完整描述:

PanoramaGroup

您可以将 `PanoramaGroup` 视为磁贴的逻辑分组。您提供的 `PanoramaGroup` 对象需要看起来像这样:

/// <summary>
/// Represents a groupig of tiles
/// </summary>
public class PanoramaGroup
{
    public PanoramaGroup(string header, ICollectionView tiles)
    {
        this.Header = header;
        this.Tiles = tiles;
    }

    public string Header { get; private set; }
    public ICollectionView Tiles { get; private set; }
}

Panorama

显然,所有真正的操作都在这里发生。那么这个控件提供了什么呢?它提供了以下功能:

  • 通过 DataTemplates 实现模板化能力(在您自己的代码中……加分!)。
  • 它是一个无样式控件,因此您可以根据需要对其进行模板化/样式化。
  • 易于使用。
  • 两种滚动形式,一种是回弹,另一种是根据当前鼠标坐标使用摩擦力。

以上是它的功能……代码看起来怎么样?好吧,就是这样。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Threading;
using System.ComponentModel;
using System.Windows.Input;
using System.Windows.Media;

namespace PanoramaControl
{
    [TemplatePart(Name = "PART_ScrollViewer", Type = typeof(ScrollViewer))]
    public class Panorama : ItemsControl
    {
        #region Data
        private ScrollViewer sv;
        private Point scrollTarget;
        private Point scrollStartPoint;
        private Point scrollStartOffset;
        private Point previousPoint;
        private Vector velocity;
        private double friction;
        private DispatcherTimer animationTimer = new DispatcherTimer(DispatcherPriority.DataBind);
        private static int PixelsToMoveToBeConsideredScroll = 5;
        private static int PixelsToMoveToBeConsideredClick = 2;
        private Random rand = new Random(DateTime.Now.Millisecond);
        private bool _mouseDownFlag;
        private Cursor _savedCursor;
        #endregion

        #region Ctor
        public Panorama()
        {
            friction = 0.85;

            animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 20);
            animationTimer.Tick += new EventHandler(HandleWorldTimerTick);
            animationTimer.Start();

            TileColors = new Brush[] {
                new SolidColorBrush(Color.FromRgb((byte)111,(byte)189,(byte)69)),
                new SolidColorBrush(Color.FromRgb((byte)75,(byte)179,(byte)221)),
                new SolidColorBrush(Color.FromRgb((byte)65,(byte)100,(byte)165)),
                new SolidColorBrush(Color.FromRgb((byte)225,(byte)32,(byte)38)),
                new SolidColorBrush(Color.FromRgb((byte)128,(byte)0,(byte)128)),
                new SolidColorBrush(Color.FromRgb((byte)0,(byte)128,(byte)64)),
                new SolidColorBrush(Color.FromRgb((byte)0,(byte)148,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)0,(byte)199)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)135,(byte)15)),
                new SolidColorBrush(Color.FromRgb((byte)45,(byte)255,(byte)87)),
                new SolidColorBrush(Color.FromRgb((byte)127,(byte)0,(byte)55))
    
            };

            ComplimentaryTileColors = new Brush[] {
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255))
            };

        }

        static Panorama()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Panorama), new FrameworkPropertyMetadata(typeof(Panorama)));
        }
        #endregion

        #region Properties
        public double Friction
        {
            get { return 1.0 - friction; }
            set { friction = Math.Min(Math.Max(1.0 - value, 0), 1.0); }
        }

        public List<Brush> TileColorPair
        {
            get 
            {
                int idx = rand.Next(TileColors.Length);
                return new List<Brush>() { TileColors[idx], ComplimentaryTileColors[idx] };
            }
        }

        #region DPs


        #region ItemBox

        public static readonly DependencyProperty ItemBoxProperty =
            DependencyProperty.Register("ItemHeight", typeof(double), typeof(Panorama),
                new FrameworkPropertyMetadata((double)120.0));


        public double ItemBox
        {
            get { return (double)GetValue(ItemBoxProperty); }
            set { SetValue(ItemBoxProperty, value); }
        }

        #endregion

        #region GroupHeight

        public static readonly DependencyProperty GroupHeightProperty =
            DependencyProperty.Register("GroupHeight", typeof(double), typeof(Panorama),
                new FrameworkPropertyMetadata((double)640.0));


        public double GroupHeight
        {
            get { return (double)GetValue(GroupHeightProperty); }
            set { SetValue(GroupHeightProperty, value); }
        }

        #endregion



        #region HeaderFontSize

        public static readonly DependencyProperty HeaderFontSizeProperty =
            DependencyProperty.Register("HeaderFontSize", typeof(double), typeof(Panorama),
                new FrameworkPropertyMetadata((double)30.0));

        public double HeaderFontSize
        {
            get { return (double)GetValue(HeaderFontSizeProperty); }
            set { SetValue(HeaderFontSizeProperty, value); }
        }

        #endregion


 
        #region HeaderFontColor

        public static readonly DependencyProperty HeaderFontColorProperty =
            DependencyProperty.Register("HeaderFontColor", typeof(Brush), typeof(Panorama),
                new FrameworkPropertyMetadata((Brush)Brushes.White));

        public Brush HeaderFontColor
        {
            get { return (Brush)GetValue(HeaderFontColorProperty); }
            set { SetValue(HeaderFontColorProperty, value); }
        }

        #endregion

        #region HeaderFontFamily

        public static readonly DependencyProperty HeaderFontFamilyProperty =
            DependencyProperty.Register("HeaderFontFamily", typeof(FontFamily), typeof(Panorama),
                new FrameworkPropertyMetadata((FontFamily)new FontFamily("Segoe UI")));

        public FontFamily HeaderFontFamily
        {
            get { return (FontFamily)GetValue(HeaderFontFamilyProperty); }
            set { SetValue(HeaderFontFamilyProperty, value); }
        }

        #endregion

        #region TileColors

        public static readonly DependencyProperty TileColorsProperty =
            DependencyProperty.Register("TileColors", typeof(Brush[]), typeof(Panorama),
                new FrameworkPropertyMetadata((Brush[])null));

        public Brush[] TileColors
        {
            get { return (Brush[])GetValue(TileColorsProperty); }
            set { SetValue(TileColorsProperty, value); }
        }

        #endregion

        #region ComplimentaryTileColors

        public static readonly DependencyProperty ComplimentaryTileColorsProperty =
            DependencyProperty.Register("ComplimentaryTileColors", typeof(Brush[]), typeof(Panorama),
                new FrameworkPropertyMetadata((Brush[])null));

        public Brush[] ComplimentaryTileColors
        {
            get { return (Brush[])GetValue(ComplimentaryTileColorsProperty); }
            set { SetValue(ComplimentaryTileColorsProperty, value); }
        }

        #endregion

        #region UseSnapBackScrolling

        public static readonly DependencyProperty UseSnapBackScrollingProperty =
            DependencyProperty.Register("UseSnapBackScrolling", typeof(bool), typeof(Panorama),
                new FrameworkPropertyMetadata((bool)true));

        public bool UseSnapBackScrolling
        {
            get { return (bool)GetValue(UseSnapBackScrollingProperty); }
            set { SetValue(UseSnapBackScrollingProperty, value); }
        }

        #endregion

        #endregion

        #endregion

        #region Private Methods

        private void DoStandardScrolling()
        {
            sv.ScrollToHorizontalOffset(scrollTarget.X);
            sv.ScrollToVerticalOffset(scrollTarget.Y);
            scrollTarget.X += velocity.X;
            scrollTarget.Y += velocity.Y;
            velocity *= friction;
        }


        private void HandleWorldTimerTick(object sender, EventArgs e)
        {
            var prop = DesignerProperties.IsInDesignModeProperty;
            bool isInDesignMode = (bool)DependencyPropertyDescriptor.FromProperty(prop,
                typeof(FrameworkElement)).Metadata.DefaultValue;

            if (isInDesignMode)
                return;


            if (IsMouseCaptured)
            {
                Point currentPoint = Mouse.GetPosition(this);
                velocity = previousPoint - currentPoint;
                previousPoint = currentPoint;
            }
            else
            {
                if (velocity.Length > 1)
                {
                    DoStandardScrolling();
                }
                else
                {
                    if (UseSnapBackScrolling)
                    {
                        int mx = (int)sv.HorizontalOffset % (int)ActualWidth;
                        if (mx == 0)
                            return;
                        int ix = (int)sv.HorizontalOffset / (int)ActualWidth;
                        double snapBackX = mx > ActualWidth / 2 ? (ix + 1) * ActualWidth : ix * ActualWidth;
                        sv.ScrollToHorizontalOffset(sv.HorizontalOffset + (snapBackX - sv.HorizontalOffset) / 4.0);
                    }
                    else
                    {
                        DoStandardScrolling();
                    }
                }
            }
        }
        #endregion

        #region Overrides


        public override void OnApplyTemplate()
        {
            sv = (ScrollViewer)Template.FindName("PART_ScrollViewer", this);
            base.OnApplyTemplate();
        }


        protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            if (sv.IsMouseOver)
            {
                _mouseDownFlag = true;

                // Save starting point, used later when determining how much to scroll.
                scrollStartPoint = e.GetPosition(this);
                scrollStartOffset.X = sv.HorizontalOffset;
                scrollStartOffset.Y = sv.VerticalOffset;
            }

            base.OnPreviewMouseLeftButtonDown(e);
        }

        protected override void OnPreviewMouseMove(MouseEventArgs e)
        {
            if (_mouseDownFlag)
            {
                Point currentPoint = e.GetPosition(this);

                // Determine the new amount to scroll.
                Point delta = new Point(scrollStartPoint.X - currentPoint.X, scrollStartPoint.Y - currentPoint.Y);

                if (Math.Abs(delta.X) > PixelsToMoveToBeConsideredScroll ||
                    Math.Abs(delta.Y) > PixelsToMoveToBeConsideredScroll)
                {
                    scrollTarget.X = scrollStartOffset.X + delta.X;
                    scrollTarget.Y = scrollStartOffset.Y + delta.Y;

                    // Scroll to the new position.
                    sv.ScrollToHorizontalOffset(scrollTarget.X);
                    sv.ScrollToVerticalOffset(scrollTarget.Y);

                    if (!this.IsMouseCaptured)
                    {
                        if ((sv.ExtentWidth > sv.ViewportWidth) ||
                            (sv.ExtentHeight > sv.ViewportHeight))
                        {
                            _savedCursor = this.Cursor;
                            this.Cursor = Cursors.ScrollWE;
                        }

                        this.CaptureMouse();
                    }
                }
            }

            base.OnPreviewMouseMove(e);
        }

        protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
        {
            bool mouseDownFlag = _mouseDownFlag;
            // mouse move events may trigger while inside this handler.
            _mouseDownFlag = false;

            if (this.IsMouseCaptured)
            {
                // scroll action stopped
                this.Cursor = _savedCursor;
                this.ReleaseMouseCapture();
            }
            else if (mouseDownFlag)
            {
                // click action stopped
            }

            _savedCursor = null;

            base.OnPreviewMouseLeftButtonUp(e);
        }
        #endregion

    }
}

应用的默认样式

由于这是一个无样式控件,为完整起见,这里是应用于 `Panorama` 控件的默认 `Style`:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:PanoramaControl">
    <local:PanoramaGroupWidthConverter x:Key="conv" />

    <Style x:Key="headerLabelStyle" TargetType="Label">
        <Setter Property="FontSize"
                Value="{Binding RelativeSource={RelativeSource 
                    AncestorType={x:Type local:Panorama}, Mode=FindAncestor}, Path=HeaderFontSize}" />
        <Setter Property="Foreground"
                Value="{Binding RelativeSource={RelativeSource 
                    AncestorType={x:Type local:Panorama}, Mode=FindAncestor}, Path=HeaderFontColor}" />
        <Setter Property="FontFamily"
                Value="{Binding RelativeSource={RelativeSource 
                    AncestorType={x:Type local:Panorama}, Mode=FindAncestor}, Path=HeaderFontFamily}" />
        <Setter Property="FontWeight"
                Value="Normal" />
        <Setter Property="HorizontalAlignment"
                Value="Left" />
        <Setter Property="HorizontalContentAlignment"
                Value="Left" />
        <Setter Property="VerticalAlignment"
                Value="Center" />
        <Setter Property="VerticalContentAlignment"
                Value="Center" />
        <Setter Property="Margin"
                Value="10,0,0,20" />
    </Style>

    <DataTemplate DataType="{x:Type local:PanoramaGroup}">
        <DataTemplate.Resources>
            <Style x:Key="transparentListBoxItemStyle"
                   TargetType="{x:Type ListBoxItem}">
                <Style.Resources>
                    <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}"
                                     Color="Transparent" />
                </Style.Resources>
                <Setter Property="Padding"
                        Value="0" />
                <Setter Property="Margin"
                        Value="0" />
            </Style>
        </DataTemplate.Resources>

        <DockPanel LastChildFill="True" Background="Transparent">
            <Label Style="{StaticResource headerLabelStyle}"
                   Content="{Binding Header}"
                   DockPanel.Dock="Top" />
            <ListBox ItemsSource="{Binding Tiles}" 
                     SelectionMode="Single"
                     BorderThickness="0"
                     BorderBrush="Transparent"
                     Background="Transparent"
                     IsSynchronizedWithCurrentItem="True"
                     ItemContainerStyle="{StaticResource transparentListBoxItemStyle}">

                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Height="{Binding 
                                RelativeSource={RelativeSource 
                                AncestorType={x:Type local:Panorama}, 
                                Mode=FindAncestor}, 
                                Path=GroupHeight}">
                            <WrapPanel.Width>
                                <MultiBinding Converter="{StaticResource conv}">
                                    <Binding Path="ItemBox" 
                                             RelativeSource="{RelativeSource 
                                                    AncestorType={x:Type local:Panorama}, 
                                                    Mode=FindAncestor}" />
                                    <Binding Path="GroupHeight" 
                                             RelativeSource="{RelativeSource 
                                                    AncestorType={x:Type local:Panorama}, 
                                                    Mode=FindAncestor}" />
                                    <Binding RelativeSource="{RelativeSource 
                                                    AncestorType={x:Type ListBox}, 
                                                    Mode=FindAncestor}" />
                                </MultiBinding>
                            </WrapPanel.Width>
                        </WrapPanel>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ListBox>
        </DockPanel>
    </DataTemplate>


    <Style TargetType="{x:Type local:Panorama}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ItemsControl}">
                    <ControlTemplate.Resources>
                        <Style TargetType="{x:Type ScrollViewer}">
                            <Setter Property="HorizontalScrollBarVisibility"
                                    Value="Hidden" />
                            <Setter Property="VerticalScrollBarVisibility"
                                    Value="Hidden" />
                        </Style>
                    </ControlTemplate.Resources>

                    <ScrollViewer x:Name="PART_ScrollViewer"
                                  Background="{TemplateBinding Background}"
                                  Padding="{TemplateBinding Padding}"
                                  SnapsToDevicePixels="true">
                        <ItemsPresenter Margin="0" 
                           SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                    </ScrollViewer>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

实际上这并不难。本质上,`Panorama` 控件是一个专门的 `ItemsControl`,它将它的项(`PanoramaGroup`)托管在一个 `ScrollViewer` 中,而每个 `PanoramaGroup` 又在一个 `ListBox` 中托管多个自定义磁贴(您可以将其声明为 ViewModel,并实现一个 `DataTemplate` 来显示它们),这个 `ListBox` 是 `PanoramaGroup` 默认样式的一部分。磁贴 ViewModel 的外观和感觉由您决定,可以通过您自己的 `DataTemplate` 来应用。

至于滚动,这全部在 `Panorama` 控件内部处理,而且都是鼠标操作代码。唯一值得一提的是,您可以通过设置 `UseSnapBackScrolling` 来选择您的滚动类型,这个选项可以设置在 `Panorama` 控件上。

如何在您自己的应用程序中使用它

在您的应用程序中使用它只需要做几件事,下面将概述这些步骤。还有一个附加的演示应用程序,展示了如何创建本文所示的 `Panorama`。

1. 创建自定义 ViewModel 

这非常简单,您几乎可以完全控制您的实现类的外观。 

这是一个非常简单的磁贴示例:

public class PanoramaTileViewModel : INPCBase
{
    private IMessageBoxService messageBoxService;
    private Timer liveUpdateTileTimer = new Timer();

    public PanoramaTileViewModel(IMessageBoxService messageBoxService, string text, string imageUrl, bool isDoubleWidth)
    {
        if (isDoubleWidth)
        {
            liveUpdateTileTimer.Interval = 1000;
            liveUpdateTileTimer.Elapsed += LiveUpdateTileTimer_Elapsed;
            liveUpdateTileTimer.Start();
        }


        this.messageBoxService = messageBoxService;
        this.Text = text;
        this.ImageUrl = imageUrl;
        this.IsDoubleWidth = isDoubleWidth;
        this.TileClickedCommand = new SimpleCommand<object, object>(ExecuteTileClickedCommand);
    }

    void LiveUpdateTileTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
        if (Counter < 10)
            Counter++;
        else
            Counter = 0;
        NotifyPropertyChanged("Counter");
    }

    public int Counter { get; set; }
    public string Text { get; private set; }
    public string ImageUrl { get; private set; }
    public bool IsDoubleWidth { get; private set; }
    public ICommand TileClickedCommand { get; private set; }

    public void ExecuteTileClickedCommand(object parameter)
    {
        messageBoxService.ShowMessage(string.Format("you clicked {0}", this.Text));
    }
}

这个简单的演示 ViewModel 类提供了以下能力:

  • 显示图像。
  • 拉伸为双倍宽度的磁贴。
  • 显示一些文本(如果您愿意,此演示将此文本显示为 `ToolTip`)。
  • 响应点击事件。 
  • 通过 `INotifyPropertyChanged` 绑定允许磁贴实时更新(我在演示应用程序中这样做的方式不太典型,但这不是您通常会做的事情,您通常会连接到 Web 服务/数据库/WCF 服务等,而不是使用 `Timer`,这只是为了演示您可以通过 `INotifyPropertyChanged` 绑定以任何您想要的方式实时更新磁贴)。

您会注意到,我的磁贴示例为了简洁而相当简单,但您可以让它进行动画/流式传输数据,您喜欢什么都可以,`Panorama` 控件只会显示这些数据。基本上,这一切都是标准的 XAML,所以 XAML 提供的任何功能都应该能正常工作。

2. 定义一个适合您的 ViewModel 类的 DataTemplate

接下来您需要做的是创建您自己的 `DataTemplate`。这**必须**是**您**想要看到的,我在这方面帮不了您,但既然您已经创建了自己的 ViewModel 类,那么根据当前的数据值设计您想要的磁贴外观和感觉就只是一个设计问题了。

这是演示应用程序的 `PanoramaTileViewModel` 类的 `DataTemplate`:

<DataTemplate DataType="{x:Type local:PanoramaTileViewModel}">
    <Border x:Name="bord" 
            BorderThickness="2"
            BorderBrush="{Binding RelativeSource={RelativeSource Mode=Self}, 
                Path=Background}"
            Background="{Binding RelativeSource={RelativeSource 
                AncestorType={x:Type pan:Panorama}, 
                Mode=FindAncestor}, 
                Path=TileColorPair[0]}"
            Width="120" Height="120" Margin="0">
        <StackPanel Orientation="Horizontal">

            <Button Command="{Binding TileClickedCommand}">
                <Button.Template>
                    <ControlTemplate>
                        <Image x:Name="img"
                            Source="{Binding ImageUrl}"
                            Width="100"
                            Height="100"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center"
                            ToolTip="{Binding Text}" >
                        </Image>
                        <ControlTemplate.Triggers>
                            <DataTrigger Binding="{Binding IsDoubleWidth}" Value="True">
                                <Setter TargetName="img"
                                    Property="HorizontalAlignment"
                                    Value="Left" />
                                <Setter TargetName="img"
                                    Property="Margin"
                                    Value="10,0,0,0" />
                            </DataTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Button.Template>
            </Button>
                    

            <Grid  Margin="30,0,0,0"
                    HorizontalAlignment="Left"
                    VerticalAlignment="Center">
                        
            <Ellipse Stroke="White"
                        StrokeThickness="2"
                        Width="50"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        Height="50" Fill="Transparent"/>
                    
                <Label x:Name="liveUpdate"
                    Content="{Binding Counter}"
                    Visibility="Collapsed"
                    HorizontalAlignment="Center"
                    HorizontalContentAlignment="Center"
                    VerticalAlignment="Center"
                    VerticalContentAlignment="Center"
                    Foreground="White"
                    FontFamily="Segoe UI"
                    FontSize="30"
                    FontWeight="DemiBold"/>
            </Grid>

        </StackPanel>

    </Border>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource 
                        AncestorType={x:Type ListBoxItem}, Mode=FindAncestor}, 
                        Path=IsSelected}"
                        Value="True">
            <Setter TargetName="bord"
                    Property="BorderBrush"
                    Value="{Binding RelativeSource={RelativeSource 
                        AncestorType={x:Type pan:Panorama}, Mode=FindAncestor}, 
                        Path=TileColorPair[1]}"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding IsDoubleWidth}"
                        Value="True">
            <Setter TargetName="bord"
                    Property="Width"
                    Value="240" />
            <Setter TargetName="liveUpdate"
                    Property="Visibility"
                    Value="Visible" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

3. 创建一个 IEnumerable<PanoramaGroup> 并将其与 Panorama 控件一起使用

信不信由你,这几乎就够了,您现在需要做的就是创建一些手动 UI 元素(哎呀,没人想要那个,我们希望 MVVM 来为我们做),或者创建一个 ViewModel 来为 `Panorama` 控件的实例提供项。让我们看一个非常简单的演示 ViewModel,它为 `Panorama` 控件提供项。

public class MainWindowViewModel : INPCBase
{
    private Random rand = new Random(DateTime.Now.Millisecond);
    private List<DummyTileData> dummyData = new List<DummyTileData>();
    private IMessageBoxService messageBoxService;



    public MainWindowViewModel(IMessageBoxService messageBoxService)
    {
        this.messageBoxService = messageBoxService;

        //create some dummy data
        dummyData.Add(new DummyTileData("Add", @"Images/Add.png"));
        dummyData.Add(new DummyTileData("Adobe", @"Images/Adobe.png"));
        dummyData.Add(new DummyTileData("Android", @"Images/Android.png"));
        dummyData.Add(new DummyTileData("Author", @"Images/Author.png"));
        dummyData.Add(new DummyTileData("Blogger", @"Images/Blogger.png"));
        dummyData.Add(new DummyTileData("Copy", @"Images/Copy.png"));
        dummyData.Add(new DummyTileData("Delete", @"Images/Delete.png"));
        dummyData.Add(new DummyTileData("Digg", @"Images/Digg.png"));
        dummyData.Add(new DummyTileData("Edit", @"Images/Edit.png"));
        dummyData.Add(new DummyTileData("Facebook", @"Images/Facebook.png"));
        dummyData.Add(new DummyTileData("GMail", @"Images/GMail.png"));
        dummyData.Add(new DummyTileData("RSS", @"Images/RSS.png"));
        dummyData.Add(new DummyTileData("Save", @"Images/Save.png"));
        dummyData.Add(new DummyTileData("Search", @"Images/Search.png"));
        dummyData.Add(new DummyTileData("Trash", @"Images/Trash.png"));
        dummyData.Add(new DummyTileData("Twitter", @"Images/Twitter.png"));
        dummyData.Add(new DummyTileData("VisualStudio", @"Images/VisualStudio.png"));
        dummyData.Add(new DummyTileData("Wordpress", @"Images/Wordpress.png"));
        dummyData.Add(new DummyTileData("Yahoo", @"Images/Yahoo.png"));
        dummyData.Add(new DummyTileData("YouTube", @"Images/YouTube.png"));

        //Great some dummy groups
        List<PanoramaGroup> data = new List<PanoramaGroup>();
        List<PanoramaTileViewModel> tiles = new List<PanoramaTileViewModel>();

        for (int i = 0; i < 4; i++)
        {
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(true));

            tiles.Add(CreateTile(true));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));

            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));

            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
        }

        data.Add(new PanoramaGroup("Settings",
            CollectionViewSource.GetDefaultView(tiles)));

        PanoramaItems = data;

    }


    private PanoramaTileViewModel CreateTile(bool isDoubleWidth)
    {
        DummyTileData dummyTileData = dummyData[rand.Next(dummyData.Count)];
        return new PanoramaTileViewModel(messageBoxService, 
            dummyTileData.Text, dummyTileData.ImageUrl, isDoubleWidth);
    }


    private IEnumerable<PanoramaGroup> panoramaItems;

    public IEnumerable<PanoramaGroup> PanoramaItems
    {
        get { return this.panoramaItems; }

        set
        {
            if (value != this.panoramaItems)
            {
                this.panoramaItems = value;
                NotifyPropertyChanged("CompanyName");
            }
        }
    }
}




public class DummyTileData
{
    public string Text { get; private set; }
    public string ImageUrl { get; private set; }

    public DummyTileData(string text, string imageUrl)
    {
        this.Text = text;
        this.ImageUrl = imageUrl;
    }
}

这是它在 XAML 中使用这个演示 ViewModel 的一个例子:

<Controls:MetroWindow
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:pan="clr-namespace:PanoramaControl;assembly=PanoramaControl"
    Width="960" MinWidth="960" Height="540">
    
        <pan:Panorama Grid.Row="1" x:Name="pan"
                      UseSnapBackScrolling="{Binding ElementName=chkUseSNapBackScrolling, 
				Path=IsChecked, Mode=OneWay}"
                      ItemsSource="{Binding PanoramaItems}"
                      ItemBox="120"
                      GroupHeight="360"
                      Background="Transparent" />
	
</Controls:MetroWindow>

您可以看到,`Panorama` 控件的设置非常简单。在使用它时,您可能需要做的一件事是设置您自己的 `Brush` 数组,以指定您的主要和辅助磁贴颜色(当然,如果您愿意,也可以完全忽略这一点,最终磁贴的生成/颜色/外观和感觉都取决于您)。

但是,如果您想使用该控件提供的功能,可以使用两个 `DependencyProperty` 值来设置一个 `Brush` 对象数组,这些对象可用于磁贴和辅助颜色画笔。它们可以通过以下 `Panorama` 控件 DPs 访问,这些 DPs 期望类型为 `Brush[]`:

  • TileColors
  • `ComplimentaryTileColors`(您可以在 `DataTemplate` 中使用它来为边框等着色,如果您愿意使用的话,就像我说的,这取决于您)

功劳归于应得的人

对于整体 `Window` 样式,我使用了一些在这里找到的代码:http://mahapps.com/MahApps.Metro/,奇怪的是,它也提供了一个 Panorama 控件,看起来很棒,但在我尝试时,它的滚动功能似乎不太好用。也许是我用错了,但我完全搞不懂它是如何实现滚动的。

最初的代码包含了捕获鼠标的代码,因此不允许 Button 等输入控件成为磁贴 `DataTemplate` 的一部分,但感谢 Marc Jacobi 提供的一些更好的鼠标处理程序,磁贴内的 Button 现在完全没有问题了,正如新的演示代码中可以看到的DataTemplate 

 

© . All rights reserved.