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

类似手机的控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (80投票s)

2010 年 10 月 29 日

CPOL

13分钟阅读

viewsIcon

201944

downloadIcon

2084

部分模仿一款流行手机的控件。

引言

我离开 CodeProject 写文章已经有一段时间了,但这并不意味着我无所事事。恰恰相反,我业余时间正在做一个基于 ASP.NET MVC 的大型项目,我希望它能成为大多数开发者的有用工具。问题是,网络并不是我真正热爱的地方,我厌倦了在火车上写所有这些网络代码,并处理那些跨浏览器怪异问题(在这个时代我们居然还要关心这些……真是的)。长话短说,我需要一点休息,于是我花了一个星期的火车旅程(我离公司 50 英里,每天有几个小时可以在火车上玩)来回到我真正热爱的地方,那就是 WPF 和美好的时光。大约在那时,我的同事也得到了一款知名手机的新版本,它有一个炫酷的导航和分组系统,我想,啊,也许我可以尝试做些类似的东西。本文介绍了一个 WPF 控件库,它模仿了这款知名手机导航和分组系统的一部分。我应该提一下,它不像那款知名手机的功能那么多,而且我已经完成了这个项目,因为我已经准备好回到我的网站项目了,而且我还要感谢那位知名手机制造商,他们一开始就创造了如此出色的系统;它太棒了,而且要完全模仿它确实是一项艰巨的任务。

所以,总结一下,本文**只**提供了这款知名手机厂商系统中的一小部分功能,但我仍然认为本文中有一些对各位有价值的东西,可以对大家有用。

视频演示

由于这是一个非常视觉化的东西,我认为演示它的最好方式就是给你们看一个小视频;点击下面的图片即可观看完成控件的视频

应用程序的整体结构是什么

现在你们已经看过了视频,让我们来看一些图表,这些图表可能会进一步加深你们对控件集是如何构建的理解。

让我们从这个图表开始

我认为这张图是一个很好的起点。你们在上面的图中可以看到,有一个宿主窗口,它包含了一个看起来像可滚动区域的东西,该区域只显示了一个充满块的整个画布的一部分。这句话基本上准确地描述了代码的工作方式。确实有一个可滚动区域,它是一个名为 ScrollContainer 的控件,其中包含多个 BlockContainer 控件。每个 BlockContainer 还包含一些 GroupedBlockControlExpandedBlockControl 控件。

哦,而且你还可以展开一个 GroupedBlockControl(前提是还没有其他已展开的),它看起来会是这样

为了了解所有这些控件类型如何与上面的图表相关联,请考虑下面的图表,我已经对其进行了注释,以显示不同的控件类型

那么它是如何工作的呢?

以下子节将概述这些五种控件的相关部分是如何工作的。

ScrollContainer

该控件功能描述

本文的核心是一个自定义的 ScrollContainer 控件,它只是包含一个支持摩擦力的特殊 ScrollViewer,我已经在我的多篇文章中使用过它,所以我就不详细介绍它的工作原理了;请看代码。

我认为更好的做法是整体讨论 ScrollContainer 的功能,然后给你们看一些精简的代码。

ScrollContainer 作为多个 BlockContainer 控件的容器,当 ScrollContainerBlocks 属性被赋值一个新的 List<BlockItemBase> 时创建这些控件。本质上,发生的事情是检查传入的 List<BlockItemBase> 块,如果该项是 GroupedBlockItem,则会创建一个新的 GroupedBlockControl 并将其添加到当前的 BlockContainer。如果该项是 BlockWorkItem,则会创建一个新的 ExpandedBlockControl 并将其添加到当前的 BlockContainer

注意到我提到了一个当前的 BlockContainer?那么,“当前”究竟是什么意思呢?

ScrollContainer 类持有一个变量,该变量决定了一个 BlockContainer 中应该容纳多少个块,所以只需要创建足够的 BlockContainer 来容纳 ScrollViewer.Blocks 属性中的项目数量即可。

这个控件还有什么其他功能?嗯,大致来说,这个控件的任务是执行以下任务

  1. 即使在用户抬起鼠标后,该控件仍会进行摩擦滚动。如果摩擦力不够或者用户在未达到新块的一半时松开了鼠标,该控件会处理恢复到前一个位置。
  2. ExpandedBlocksContainer 被展开时,该控件将不会滚动。
  3. 如果控件被认为被点击,该控件还会调用 ExpandedBlock.DoWork() 方法。
  4. 该控件还将展开一个 GroupedBlocksControl,只要没有其他 GroupedBlocksControl 已展开。

好了,这听起来可能不错,但当你有一个应该一直滚动但有时又不应该滚动的可滚动控件时,你必须使用一些技巧,而且如何确定一个控件是否被点击?实际上,这比说起来要难。我们如何做到这些呢?

滚动与否可以通过一个简单的 ViewState 枚举轻松实现,该枚举具有 BlockExpanded/BlockCollapsed 值。只有当 ScrollContainerViewStateBlockExpanded 时,我们才允许滚动。

如何确定一个控件是否被点击?嗯,这是一个两步过程:首先,我们需要在首次 MouseDown 时查看鼠标下方是否有控件;这可以通过以下方式实现

protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
    ....
    ....
    ....
    //store Control if one was found, so we can call
    //its Expand() method/DoWork() on MouseUp()
    groupedBlockControl = 
      TreeHelper.TryFindFromPoint<GroupedBlockControl>(this, scrollStartPoint);
    expandedBlockControl = 
      TreeHelper.TryFindFromPoint<ExpandedBlockControl>(this, scrollStartPoint);
    ....
    ....
    ....
}

我们如何使用 VisualTree(通过演示应用程序中的 TreeHelper 类)来找出我们是否有一个 Point 位于 GroupedBlockControlExpandedBlockControl 之上?基本上,稍后,如果我们认为点击了一个 GroupedBlockControl,它将被展开(只要没有其他 GroupedBlockControl 已展开),如果我们认为点击了一个 ExpandedBlockControl,它的 BlockWorkItemClickedWork 回调 Action<BlockWorkItem> 委托将被调用。

但是,在任何这些发生之前,我们需要找到是否有什么东西被点击了;否则,我们只是在美妙地滚动。确定点击是通过测量移动的像素量来完成的,如果它们在某个限制范围内,则原始控件(我们在 MouseDown 时存储的)将被视为被点击。像素量存储在 ScrollContainer 变量 PixelsToMoveToBeConsideredClick 中。

好了,言归正传,这是 ScrollContainer 中最相关的代码;XAML 什么都没有,就是一个 ScrollViewer,仅此而已。

public partial class ScrollContainer : UserControl, IScrollContainer
{
    #region Data
    ....
    ....
    private GroupedBlockControl groupedBlockControl;
    private ExpandedBlockControl expandedBlockControl;
    private List<BlockItemBase> blocks;
    #endregion

    #region Ctor
    public ScrollContainer()
    {
        InitializeComponent();
        friction = 0.85;
    ....
    ....
    ....
        ContainerViewState = ViewState.BlockCollapsed;
    }
    #endregion
 
    #region Public Properties

    public ViewState ContainerViewState { get; set; }

    public List<BlockItemBase> Blocks
    {
        get { return blocks; }
        set 
        {
            blocks = value;
            CreateItems();
        }
    }
    #endregion

    #region Overrides
    protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
    {
        if (scroller.IsMouseOver)
        {
            ......
            ......
            ......
            ......
            //store Control if one was found, so we can
            //call its Expand() method/DoWork() on MouseUp()
            groupedBlockControl = 
              TreeHelper.TryFindFromPoint<GroupedBlockControl>(this, scrollStartPoint);
            expandedBlockControl = 
              TreeHelper.TryFindFromPoint<ExpandedBlockControl>(this, scrollStartPoint);
            
            this.CaptureMouse();
        }

        base.OnPreviewMouseDown(e);
    }

    protected override void OnPreviewMouseMove(MouseEventArgs e)
    {
        if (this.IsMouseCaptured)
        {
            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) < ScrollContainer.PixelsToMoveToBeConsideredScroll &&
                   Math.Abs(delta.Y) < ScrollContainer.PixelsToMoveToBeConsideredScroll)
                return;

            scrollTarget.X = scrollStartOffset.X + delta.X;
            scrollTarget.Y = scrollStartOffset.Y + delta.Y;

            if (ContainerViewState == ViewState.BlockExpanded)
                return;


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

        base.OnPreviewMouseMove(e);
    }

    protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
    {
        if (this.IsMouseCaptured)
        {
            this.Cursor = Cursors.Arrow;
            this.ReleaseMouseCapture();
        }

        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) < ScrollContainer.PixelsToMoveToBeConsideredClick &&
            Math.Abs(delta.Y) < ScrollContainer.PixelsToMoveToBeConsideredClick)
        {

            switch (ContainerViewState)
            {
                case ViewState.BlockExpanded:
                    if (expandedBlockControl == null)
                    {

                        blockContainers.ForEach((x) => x.CollapseAll());
                        ContainerViewState = ViewState.BlockCollapsed;
                    }
                    else
                    {
                        if (expandedBlockControl.IsPartOfGroup)
                            expandedBlockControl.DoWork();
                        else
                        {
                            blockContainers.ForEach((x) => x.CollapseAll());
                            ContainerViewState = ViewState.BlockCollapsed;
                        }
                    }
                    break;
                case ViewState.BlockCollapsed:
                    if (groupedBlockControl != null)
                        groupedBlockControl.ExpandItem();

                    if (expandedBlockControl != null)
                        expandedBlockControl.DoWork();
                    break;
            }

        }

        base.OnPreviewMouseUp(e);
    }
    #endregion
}

BlockContainer

该控件功能描述

这实际上是一个非常简单的控件,它的主要目的是只显示一组 BlockExpandedBlockControlGroupedBlockControl)。它的另一个工作是响应 GroupedBlockControl 的展开,此时该控件将通过运行标准的 StoryBoard 动画来创建一些空间来显示展开的 GroupedBlockControl。唯一奇怪的是,BlockContainer 需要由展开的 ExpandedBlocksContainer 告知需要多少空间,当它被告知这些信息后,BlockContainer 会调整其 Show/Hide StoryBoards,以确保它能为展开的控件增长/收缩所需的数量。BlockContainer 还会告诉其父 ScrollContainer 其新状态应为 Expanded,这将阻止用户滚动,直到他们关闭当前已展开的 GroupedBlockControl

从下面的图表可以看出,ScrollContainer 可以包含多个 BlockContainer 对象。这取决于源项目的数量和 ScrollContainer blocksInBlocksContainer 变量。

BlockContainer 类最重要的部分如下所示。XAML 不值得一提。

public partial class BlockContainer : UserControl, IBlockContainer
{
    #region Data
    private List<BlockItemBase> blocks;
    private IScrollContainer scrollContainer;
    private ExpandedBlocksContainer expanderToExpand;
    private ExpandedBlocksContainer expanderToCollapse;
       #endregion

    #region Ctor
    public BlockContainer(IScrollContainer scrollContainer)
    {
        InitializeComponent();
        showStory = this.Resources["OnShow"] as Storyboard;
        hideStory = this.Resources["OnHide"] as Storyboard;
        this.scrollContainer = scrollContainer;
    }
    #endregion

    #region Public Methods

    public void StartExpandAnimation()
    {
        showStory.Begin(); 
    }

    public void StartCollapseAnimation()
    {
        hideStory.Begin();
    }

 
    public void CollapseAll()
    {
        expanderToExpand = null;
        expanderToCollapse = null;
        isFirstTime = true;

        foreach (Object child in mainStack.Children)
        {
            if (child is ExpandedBlocksContainer)
            {
                ExpandedBlocksContainer temp = (ExpandedBlocksContainer)child;
                temp.Hide();
            }
        }
        SetOpacityForAllGroups(1.0);
    }


    public void ExpandAndShowBlocks(GroupedBlockControl controlToExpand)
    {
        if (expanderToExpand != null)
            expanderToCollapse = expanderToExpand;

        int rowOfExpandedGroup = controlToExpand.Row;
        int columnOfExpandedGroup = controlToExpand.Column;

        expanderToExpand=null;
        foreach (Object child in mainStack.Children)
        {
            if (child is ExpandedBlocksContainer)
            {
                ExpandedBlocksContainer temp = (ExpandedBlocksContainer)child;
                if ((int)temp.RowNumber ==  (rowOfExpandedGroup + 1))
                {
                    expanderToExpand = (ExpandedBlocksContainer)child;
                    break;
                }
            }
        }
        expanderToExpand.GroupedBlockItem = controlToExpand.GroupedBlockItem;
        expanderToExpand.ColumnBeingShown = columnOfExpandedGroup + 1;

        if (expanderToCollapse != null)
        {
            expanderToCollapse.Hide();
        }

        if (isFirstTime)
        {
            isFirstTime = false;
            expanderToExpand.Show();
            scrollContainer.ContainerViewState = ViewState.BlockExpanded;
            SetOpacityForAllGroups(lowerOpacity);
        }
    }

    public List<BlockItemBase> Blocks
    {
        get { return blocks; }
        set 
        {
            blocks = value;
            CreateItems();
        }
    }
    #endregion
}

GroupBlockControl

该控件功能描述

表示一组项目的分组,这些项目由小图像表示,并在网格中按行/列排列。该控件还可以检测到鼠标点击,当发生这种情况时,如果没有任何其他组已展开,它将向父 BlockContainer 发出信号,要求展开显示该控件中项目行的 ExpandedBlocksContainer。基本上,展开的所有实际工作都在父 BlockContainer 中完成,我们在上面已经讨论过。

此类中最相关部分如下所示

public partial class GroupedBlockControl : UserControl
{
    #region Ctor
    public GroupedBlockControl(IBlockContainer blockContainer,
        GroupedBlockItem groupedBlockItem, 
        int rowPositionInParent, int columnPositionInParent)
    {
        this.blockContainer = blockContainer;
        this.groupedBlockItem = groupedBlockItem;
        this.rowPositionInParent = rowPositionInParent;
        this.columnPositionInParent = columnPositionInParent;
        InitializeComponent();

        ....
        ....
        ....

    }

    ....
    ....
    ....
    #endregion

    #region Public Methods
    public void ExpandItem()
    {
        blockContainer.ExpandAndShowBlocks(this);
    }
    #endregion
}

这里真正值得注意的是,在上面显示的 ExpandItem() 方法中如何调用父 BlockContainer,其中调用了父 BlockContainer.ExpandAndShowBlocks() 方法,并将 this 对象作为参数传递。这使得父 BlockContainer 能够展开这个新被点击的 GroupedBlockControl 对象,也就是调用父 BlockContainerGroupedBlockControl。当我们讨论 BlockContainer 控件时,之前已经解释过这种展开机制。

此控件的 XAML 非常基础;它有一个 Grid,具有行/列来容纳分组项目的图像,还有一个标签。最终成品看起来像这样

ExpandedBlocksContainer

该控件功能描述

此控件包含多个 ExpandedBlockControl 项目,这些项目是通过迭代 IEnumerable<BlockWorkItem> 创建的,该迭代由父 BlockContainerBlockItems 属性上提供给此控件。

BlockContainer 将响应展开 GroupedBlockControl 的请求(该 GroupedBlockControl 也归 BlockContainer 所有)来提供这些项目。

基本上,如果你考虑 GroupedBlockControl 中的单行,它可能包含 GroupedBlockControlExpandedBlockControl 的混合。当一个 GroupedBlockControl 被点击并成功展开时,父 BlockContainer 控件将确定原始项目列表中哪些 GroupedBlockItem 与正在展开的 GroupedBlockControl 相关联,并且此 GroupedBlockItem 将用于填充 ExpandedBlocksContainerGroupedBlockItem 属性,以供正在请求展开的 GroupedBlockControl 使用。

然后 ExpandedBlocksContainer 将被请求展开,当展开 StoryBoard 完成时,ExpandedBlocksContainer 将创建所有单独的 ExpandedBlockControl 项目(每个项目对应于所提供的 GroupedBlockItemIEnumerable<BlockWorkItem> 的一个 BlockWorkItem)。还应该注意的是,当 ExpandedBlocksContainer 展开/隐藏时,它还将在父 BlockContainer 中启动一个展开/隐藏 StoryBoard,以创建足够的空间来容纳新展开的 ExpandedBlocksContainer

XAML 中实际上并没有太多内容,但我认为这次我应该完整地展示出来。这是它的全部内容。这里只有几点需要注意:

  • 使用了 2 个 StoryBoard,它们处理展开/折叠动画。这些动画在所有 ExpandedBlockControl 项目添加完毕后,通过代码隐藏进行调整,并且我们知道需要多少空间。
  • 使用了 Grid (blocksContainerGrid),用于托管添加的 ExpandedBlockControl 项目。
  • 使用了 Expression Blend:Microsoft.Expression.Drawing.Dll,以允许我们使用本机形状,例如下面的 RegularPolygon
<UserControl x:Class="PhoneLikeScrollControl.ExpandedBlocksContainer"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing" 
         mc:Ignorable="d" 
         d:DesignHeight="300" 
         d:DesignWidth="300" Margin="0">
    
    <UserControl.Resources>
        <Storyboard x:Key="OnShow" Duration="0:0:0.3" 
                    Completed="OnShowStoryboard_Completed">
            <DoubleAnimationUsingKeyFrames 
                      Storyboard.TargetProperty="(FrameworkElement.Height)" 
                      Storyboard.TargetName="bord">
                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="60"/>
            </DoubleAnimationUsingKeyFrames>

        </Storyboard>
        <Storyboard x:Key="OnHide" Duration="0:0:0.1" 
                  Completed="OnHideStoryboard_Completed">
            <DoubleAnimationUsingKeyFrames 
                       Storyboard.TargetProperty="(FrameworkElement.Height)" 
                       Storyboard.TargetName="bord">
                <EasingDoubleKeyFrame KeyTime="0" Value="60"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="0"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>        
    </UserControl.Resources>

    <StackPanel x:Name="blocksStack" 
             Orientation="Vertical" 
             Background="Transparent">
        <StackPanel.Effect>
            
            <DropShadowEffect Color="White" 
                 Opacity="0.2" Direction="270"
                 ShadowDepth="5" BlurRadius="12" />
        </StackPanel.Effect>

        <ed:RegularPolygon x:Name="polygon"
            
            HorizontalAlignment="Left"
            VerticalAlignment="Center"
            InnerRadius="1"
            PointCount="3"
            Stretch="Fill"
            Stroke="White"
            Fill="White"
            Width="10"
            Height="5"
            Visibility="Collapsed"
            StrokeThickness="1" />

        <Border BorderThickness="0,2,0,2" 
                BorderBrush="White" x:Name="bord" 
                Margin="-1,0,-1,-6" 
                Visibility="Collapsed" 
                RenderTransformOrigin="0.5,0.5">
            <Border.RenderTransform>
                <TransformGroup>
                    <ScaleTransform/>
                    <SkewTransform/>
                    <RotateTransform/>
                    <TranslateTransform/>
                </TransformGroup>
            </Border.RenderTransform>

            <Grid Background="Black">
                  <StackPanel Orientation="Vertical">
                    <Canvas x:Name="canv" 
                            HorizontalAlignment="Stretch" Height="20" 
                            VerticalAlignment="Bottom" 
                        Margin="0,0,0,0">
                        <Canvas.Background>
                            <LinearGradientBrush EndPoint="0.5,1" 
                                           StartPoint="0.5,0">
                                <GradientStop Color="Black" Offset="0.66"/>
                                <GradientStop Color="#FF686868"/>
                            </LinearGradientBrush>
                        </Canvas.Background>

                        <Label x:Name="lblGroup" Foreground="White" 
                           FontFamily="Arial" FontWeight="Bold"
                           FontSize="18"  Width="auto" Margin="0,-3,0,0" 
                           VerticalAlignment="Center" VerticalContentAlignment="Center"
                           HorizontalAlignment="Center" 
                           HorizontalContentAlignment="Center">
                        </Label>

                    </Canvas>
                    <Grid x:Name="blocksContainerGrid" Margin="6,0,0,0"/>
                </StackPanel>
            </Grid>
        </Border>

    </StackPanel>

</UserControl>

现在,让我们考虑代码隐藏中最重要的一些部分,它们如下所示

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Media.Animation;

namespace PhoneLikeScrollControl
{
    public partial class ExpandedBlocksContainer : UserControl
    {
        #region Data
        private GroupedBlockItem groupedBlockItem;
        private Storyboard showStory;
        private Storyboard hideStory;
        private bool isExpanded = false;
        private IBlockContainer blockContainer;
        #endregion

        #region Ctor
        public ExpandedBlocksContainer(IBlockContainer 
        int adjustedRowNumber, int rowNumber)
        {
            InitializeComponent();
            this.RowNumber = rowNumber;
            this.AdjustedRowNumber = adjustedRowNumber;
            this.blockContainer = blockContainer;
            showStory = this.Resources["OnShow"] as Storyboard;
            hideStory = this.Resources["OnHide"] as Storyboard;
        }
        #endregion

        #region Public Properties

        public bool IsExpanded
        {
            get { return isExpanded; }
        }

        public GroupedBlockItem GroupedBlockItem 
        {
            get { return groupedBlockItem; }
            set 
            {
                groupedBlockItem = value;
                AdjustHeightForItems();
                SetupGrid();
                CreateAnimations();
            } 
        }
        #endregion

        #region Public Methods
        public void Show()
        {
            this.Visibility = Visibility.Visible;
            bord.Visibility = Visibility.Visible;
            blockContainer.StartExpandAnimation();
            showStory.Begin();
        }

        public void Hide()
        {
            polygon.Visibility = Visibility.Collapsed;
            blockContainer.StartCollapseAnimation();
            hideStory.Begin();
        }

        public event EventHandler<EventArgs> ShowCompleted;
        public event EventHandler<EventArgs> HideCompleted;

        protected virtual void OnShowCompleted(EventArgs e)
        {
            polygon.Visibility = Visibility.Visible;
            CreateItems();
            if (ShowCompleted != null)
            {
                ShowCompleted(this, e);
            }
        }

        protected virtual void OnHideCompleted(EventArgs e)
        {
            bord.Visibility = Visibility.Collapsed;
            if (HideCompleted != null)
            {
                this.Visibility = Visibility.Collapsed;
                HideCompleted(this, e);
            }
        }
        #endregion

        #region Private Methods
        private void OnShowStoryboard_Completed(object sender, EventArgs e)
        {
            isExpanded = true;
            OnShowCompleted(e);
        }

        private void OnHideStoryboard_Completed(object sender, EventArgs e)
        {
            isExpanded = false;
            OnHideCompleted(e);
        }

        /// <summary>
        /// Adjust show/hide storyboards for this controls current height
        /// and also create offsets for parent BlockContainer to that it
        /// can animate to correct positions in unison with this control
        /// </summary>
        private void CreateAnimations()
        {
            ....
            ....
            ....
            ....

        }

        private void CreateItems()
        {
            lblGroup.Content = groupedBlockItem.BlockName;
            
            int row = 0;
            int col = 0;
            
            foreach (BlockWorkItem blockWorkItem in groupedBlockItem.BlockItems)
            {
                 ExpandedBlockControl singleBlock = 
                      new ExpandedBlockControl(blockWorkItem, true,false);
                singleBlock.SetValue(Grid.RowProperty, row);
                singleBlock.SetValue(Grid.ColumnProperty, col++);
                blocksContainerGrid.Children.Add(singleBlock);
                if (col == ScrollContainer.BlocksPerRow)
                {
                    row++;
                    col = 0;
                }
            }
        }
        #endregion
    }
}

我认为这在控件描述的开头已经讲过了,但主要部分是

  • 当设置 GroupedBlockItem 属性时,高度会进行调整以适应项目,并且 2 个 Storyboard 会针对所有项目所需的高度进行调整。
  • 当调用 Show() 方法时,它将触发显示 StoryBoard,并同时在父 BlockContainer 中触发一个 StoryBoard 以在控件展开的同时进行展开。
  • 当显示 StoryBoard 完成时,所有 ExpandedBlockControl 项目都将被创建并分配给 Grid 行/列。
  • 当调用 Hide() 方法时,它将触发隐藏 StoryBoard,并同时在父 BlockContainer 中触发一个 StoryBoard 以在控件展开的同时进行隐藏。

ExpandedBlockControl

该控件功能描述

此控件直接接受一个类型为 BlockWorkItem 的构造函数参数,其中包含一个回调委托(Action<T>),当此控件被点击时会被调用,无论是没有展开的组被展开时(当它直接是 BlocksContainer 的一部分时),还是当它是一个已展开组的一部分时(当它用作 ExpandedBlocksControl 的一部分时)。

从上图可以看出,此控件既用在 BlockContainer 中,也用作 ExpandedBlocksControl 组的一部分。

这是构成本文源代码的五种控件中最简单的。此控件的 XAML 仅包含一个 Border 和一个 Image,没什么特别的。

这是 ExpandedBlockControl 的大部分代码,唯一值得注意的是 DoWork() 方法,它只是调用原始的 BlockWorkItem.BlockWorkItemClickedWorkcallback Action<BlockWorkItem> 委托。这就是宿主应用程序如何能够响应块被点击并对其执行有用的操作。基本上,宿主应用程序应该在创建初始 List<BlockItemBase> ScrollableContainer.Blocks 属性值时为回调委托提供一个载荷。

/// <summary>
/// Represents a single block that can be used inside of a <c>BlockContainer</c>
/// or a <c>ExpandedBlocksContainer</c>
/// </summary>
public partial class ExpandedBlockControl : UserControl
{
    #region Data
    private BlockWorkItem blockWorkItem;
    private bool isPartOfGroup = false;
    #endregion

    #region Ctor
    public ExpandedBlockControl(BlockWorkItem blockWorkItem, 
           bool isPartOfGroup, bool addOnLabelHeight)
    {
        InitializeComponent();

        ....
        ....
            
        this.blockWorkItem = blockWorkItem;
    }
    #endregion

    #region Public Methods
    /// <summary>
    /// Calls the Action delegate which allows the users of this control
    /// to do somework based on this control being clicked
    /// </summary>    
    public void DoWork()
    {
        blockWorkItem.BlockWorkItemClickedWork(this.blockWorkItem);
    }
    #endregion

    #region Public Properties
    public bool IsPartOfGroup
    {
        get { return isPartOfGroup;  }
    }
    #endregion
}

如何在我的应用程序中使用它?

使用这套控件非常简单,您真正需要做的就是:

步骤 1:获取一些图片

在您自己的应用程序中添加一些您想用于块的图片。在附件的演示应用程序中,这些图片位于 DemoApp/Images 文件夹中。

步骤 2:托管 ScrollContainer

在您自己的应用程序中托管 ScrollableContainer。在演示应用程序中,这是通过在宿主 Window 中托管 ScrollableContainer 来完成的。如下所示

<Window x:Class="PhoneLikeScrollControl.Window1"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:local="clr-namespace:PhoneLikeScrollControl;assembly=PhoneLikeScrollControl">
    <local:ScrollContainer x:Name="scrollContainer" 
                           MaxHeight="400" MaxWidth="290"
                           HorizontalAlignment="Left"/>
</Window>

步骤 3:为 ScrollContainer 创建一些项目

现在您已经有一些图片,并且已经托管了 ScrollableContainer,您只需要创建一些块传递给 ScrollableContainer.Blocks 属性。如文章前面所述,有两种类型的块控件:分组的(GroupedBlockControl)和单个块的(ExpandedBlockControl)。这两种不同类型的控件是 ScrollableContainer 创建的内部对象;您永远不会自己创建这些控件。

ScrollableContainer 根据检查传入的 List<BlockItemBase> 来创建这些内部控件,该列表应传递给 ScrollableContainer.Blocks 属性。技巧在于 BlockItemBase 是一个 abstract 类,它由另外两个类实现:

  1. BlockWorkItem(表示单个块),它有一个 BitmapImage 和一个在 ExpandedBlockControl 被点击时调用的 Action<BlockWorkItem> 委托。当我点击 ExpandedBlockControl 时,我所做的就是调用 Action<BlockWorkItem> 委托,传递原始的 BlockWorkItem 并显示一个 MessageBox,但我确信你们大家可以在自己的应用程序中想出一些更好的方法(提示:也许可以启动某个应用程序,或者导航窗格)。ExpandedBlockControl 是由 ScrollContainer 在处理其 Blocks 属性(记住,这是一个 List<BlockItemBase> 列表,可以包含 BlockWorkItemGroupedBlockItem 项)时创建的。
  2. GroupedBlockItem(表示 1 到 GroupedBlockControl.BlocksPerGroup 之间的块的分组),其中每个块由一个单独的 BlockWorkItem 表示(见上面第 1 项)。

所以您真正需要关心的就是创建一个 List<BlockItemBase>,其中包含任何满足您要求的 BlockWorkItemGroupedBlockItem 的混合。

下面展示了一个如何创建一些分组/非分组块和图片的完整示例,但请注意这**只是模拟**数据,您的实际数据很可能来自数据库或其他来源。

这是演示应用程序的 Window1.xaml.cs 中的完整模拟代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Diagnostics;

namespace PhoneLikeScrollControl
{

    public partial class Window1 : Window
    {
        //some randomness for simulated data
        private Random rand = new Random();

        //Setup images for Blocks
        private string[] imageUrls = new string[] 
        {
            "/Images/square1.png",
            "/Images/square2.png",
            "/Images/square3.png",
            "/Images/square4.png",
            "/Images/square5.png",
            "/Images/square6.png",
            "/Images/square7.png",
            "/Images/square8.png",
            "/Images/square9.png",
            "/Images/square10.png",
            "/Images/square11.png",
            "/Images/square12.png",
            "/Images/square13.png",
            "/Images/square14.png",
            "/Images/square15.png",
            "/Images/square16.png"
        };

        public Window1()
        {
            InitializeComponent();
            this.Loaded += new RoutedEventHandler(Window1_Loaded);
        }

        /// <summary>
        /// On Load create a simulation mixture of single/grouped blocks using
        /// the 2 helper methods CreateNewBlock() and CreateNewGroupBlock() and add
        /// these to the hosted ScrollContainer
        /// </summary>
        private void Window1_Loaded(object sender, RoutedEventArgs e)
        {
            List<BlockItemBase> items = new List<BlockItemBase>();
            for (int i = 0; i < 72; i++)
            {
                if (rand.NextDouble() > 0.5)
                    items.Add(CreateNewGroupBlock((i+1)));
                else
                    items.Add(CreateNewBlock((i + 1)));
            }
            //add blocks to the ScrollContainer, and it will
            //create the actual Controls required
            //based on this incoming Blocks List<BlockItemBase>
            scrollContainer.Blocks = items;
        }


        /// <summary>
        /// Gets a random BitmapImage from the available images
        /// </summary>
        private BitmapImage GetRandomImagePath()
        {
            return new BitmapImage(
                new Uri(string.Format(
                    "pack://application:,,,/DemoApp;component/{0}", 
                        imageUrls[rand.Next(0, 6)])));
        }

        /// <summary>
        /// Creates a new grouped block data object, to be used as part
        /// of List<BlockItemBase> that will be used to pass to 
        /// ScrollContainer.Blocks property
        /// </summary>
        private GroupedBlockItem CreateNewGroupBlock(int blockNum)
        {
            List<BlockWorkItem> blockItems = new List<BlockWorkItem>();
            for (int i = 0; i < rand.Next(1, GroupedBlockControl.BlocksPerGroup); i++)
            {
                string blockName = string.Format("title_{0}", (i + 1).ToString());

                blockItems.Add(new BlockWorkItem(blockName, (x) =>
                    {
                        MessageBox.Show(string.Format("you clicked : {0}", x.BlockName));
                    },
                    GetRandomImagePath()));
            }

            return new GroupedBlockItem(string.Format("group_{0}", blockNum), blockItems); 
        }

        /// <summary>
        /// Creates a new single block data object, to be used as part
        /// of List<BlockItemBase> that will be used to pass to 
        /// ScrollContainer.Blocks property
        /// </summary>       
        private BlockWorkItem CreateNewBlock(int blockNum)
        {
            string blockName = string.Format("title_{0}", blockNum.ToString());
            return new BlockWorkItem(blockName, (x) =>
                {
                    MessageBox.Show(string.Format("you clicked : {0}", x.BlockName));
                },
                GetRandomImagePath());
        }
    }
}

就是这样

像往常一样,我想问一下,如果您喜欢这篇文章,并重视我写的文章,能否请您投个赞成票和/或评论?当人们发现您的文章有用时,听到这些总是很高兴的。谢谢!

© . All rights reserved.