类似手机的控件






4.95/5 (80投票s)
部分模仿一款流行手机的控件。
引言
我离开 CodeProject 写文章已经有一段时间了,但这并不意味着我无所事事。恰恰相反,我业余时间正在做一个基于 ASP.NET MVC 的大型项目,我希望它能成为大多数开发者的有用工具。问题是,网络并不是我真正热爱的地方,我厌倦了在火车上写所有这些网络代码,并处理那些跨浏览器怪异问题(在这个时代我们居然还要关心这些……真是的)。长话短说,我需要一点休息,于是我花了一个星期的火车旅程(我离公司 50 英里,每天有几个小时可以在火车上玩)来回到我真正热爱的地方,那就是 WPF 和美好的时光。大约在那时,我的同事也得到了一款知名手机的新版本,它有一个炫酷的导航和分组系统,我想,啊,也许我可以尝试做些类似的东西。本文介绍了一个 WPF 控件库,它模仿了这款知名手机导航和分组系统的一部分。我应该提一下,它不像那款知名手机的功能那么多,而且我已经完成了这个项目,因为我已经准备好回到我的网站项目了,而且我还要感谢那位知名手机制造商,他们一开始就创造了如此出色的系统;它太棒了,而且要完全模仿它确实是一项艰巨的任务。
所以,总结一下,本文**只**提供了这款知名手机厂商系统中的一小部分功能,但我仍然认为本文中有一些对各位有价值的东西,可以对大家有用。
视频演示
由于这是一个非常视觉化的东西,我认为演示它的最好方式就是给你们看一个小视频;点击下面的图片即可观看完成控件的视频
应用程序的整体结构是什么
现在你们已经看过了视频,让我们来看一些图表,这些图表可能会进一步加深你们对控件集是如何构建的理解。
让我们从这个图表开始
我认为这张图是一个很好的起点。你们在上面的图中可以看到,有一个宿主窗口,它包含了一个看起来像可滚动区域的东西,该区域只显示了一个充满块的整个画布的一部分。这句话基本上准确地描述了代码的工作方式。确实有一个可滚动区域,它是一个名为 ScrollContainer
的控件,其中包含多个 BlockContainer
控件。每个 BlockContainer
还包含一些 GroupedBlockControl
或 ExpandedBlockControl
控件。
哦,而且你还可以展开一个 GroupedBlockControl
(前提是还没有其他已展开的),它看起来会是这样
为了了解所有这些控件类型如何与上面的图表相关联,请考虑下面的图表,我已经对其进行了注释,以显示不同的控件类型
那么它是如何工作的呢?
以下子节将概述这些五种控件的相关部分是如何工作的。
ScrollContainer
该控件功能描述
本文的核心是一个自定义的 ScrollContainer
控件,它只是包含一个支持摩擦力的特殊 ScrollViewer
,我已经在我的多篇文章中使用过它,所以我就不详细介绍它的工作原理了;请看代码。
我认为更好的做法是整体讨论 ScrollContainer
的功能,然后给你们看一些精简的代码。
ScrollContainer
作为多个 BlockContainer
控件的容器,当 ScrollContainer
的 Blocks
属性被赋值一个新的 List<BlockItemBase>
时创建这些控件。本质上,发生的事情是检查传入的 List<BlockItemBase>
块,如果该项是 GroupedBlockItem
,则会创建一个新的 GroupedBlockControl
并将其添加到当前的 BlockContainer
。如果该项是 BlockWorkItem
,则会创建一个新的 ExpandedBlockControl
并将其添加到当前的 BlockContainer
。
注意到我提到了一个当前的 BlockContainer
?那么,“当前”究竟是什么意思呢?
ScrollContainer
类持有一个变量,该变量决定了一个 BlockContainer
中应该容纳多少个块,所以只需要创建足够的 BlockContainer
来容纳 ScrollViewer.Blocks
属性中的项目数量即可。
这个控件还有什么其他功能?嗯,大致来说,这个控件的任务是执行以下任务
- 即使在用户抬起鼠标后,该控件仍会进行摩擦滚动。如果摩擦力不够或者用户在未达到新块的一半时松开了鼠标,该控件会处理恢复到前一个位置。
- 当
ExpandedBlocksContainer
被展开时,该控件将不会滚动。 - 如果控件被认为被点击,该控件还会调用
ExpandedBlock.DoWork()
方法。 - 该控件还将展开一个
GroupedBlocksControl
,只要没有其他GroupedBlocksControl
已展开。
好了,这听起来可能不错,但当你有一个应该一直滚动但有时又不应该滚动的可滚动控件时,你必须使用一些技巧,而且如何确定一个控件是否被点击?实际上,这比说起来要难。我们如何做到这些呢?
滚动与否可以通过一个简单的 ViewState
枚举轻松实现,该枚举具有 BlockExpanded
/BlockCollapsed
值。只有当 ScrollContainer
的 ViewState
为 BlockExpanded
时,我们才允许滚动。
如何确定一个控件是否被点击?嗯,这是一个两步过程:首先,我们需要在首次 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
位于 GroupedBlockControl
或 ExpandedBlockControl
之上?基本上,稍后,如果我们认为点击了一个 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
该控件功能描述
这实际上是一个非常简单的控件,它的主要目的是只显示一组 Block
(ExpandedBlockControl
或 GroupedBlockControl
)。它的另一个工作是响应 GroupedBlockControl
的展开,此时该控件将通过运行标准的 StoryBoard
动画来创建一些空间来显示展开的 GroupedBlockControl
。唯一奇怪的是,BlockContainer
需要由展开的 ExpandedBlocksContainer
告知需要多少空间,当它被告知这些信息后,BlockContainer
会调整其 Show/Hide StoryBoard
s,以确保它能为展开的控件增长/收缩所需的数量。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
对象,也就是调用父 BlockContainer
的 GroupedBlockControl
。当我们讨论 BlockContainer
控件时,之前已经解释过这种展开机制。
此控件的 XAML 非常基础;它有一个 Grid
,具有行/列来容纳分组项目的图像,还有一个标签。最终成品看起来像这样
ExpandedBlocksContainer
该控件功能描述
此控件包含多个 ExpandedBlockControl
项目,这些项目是通过迭代 IEnumerable<BlockWorkItem>
创建的,该迭代由父 BlockContainer
在 BlockItems
属性上提供给此控件。
父 BlockContainer
将响应展开 GroupedBlockControl
的请求(该 GroupedBlockControl
也归 BlockContainer
所有)来提供这些项目。
基本上,如果你考虑 GroupedBlockControl
中的单行,它可能包含 GroupedBlockControl
和 ExpandedBlockControl
的混合。当一个 GroupedBlockControl
被点击并成功展开时,父 BlockContainer
控件将确定原始项目列表中哪些 GroupedBlockItem
与正在展开的 GroupedBlockControl
相关联,并且此 GroupedBlockItem
将用于填充 ExpandedBlocksContainer
的 GroupedBlockItem
属性,以供正在请求展开的 GroupedBlockControl
使用。
然后 ExpandedBlocksContainer
将被请求展开,当展开 StoryBoard
完成时,ExpandedBlocksContainer
将创建所有单独的 ExpandedBlockControl
项目(每个项目对应于所提供的 GroupedBlockItem
中 IEnumerable<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
类,它由另外两个类实现:
BlockWorkItem
(表示单个块),它有一个BitmapImage
和一个在ExpandedBlockControl
被点击时调用的Action<BlockWorkItem>
委托。当我点击ExpandedBlockControl
时,我所做的就是调用Action<BlockWorkItem>
委托,传递原始的BlockWorkItem
并显示一个MessageBox
,但我确信你们大家可以在自己的应用程序中想出一些更好的方法(提示:也许可以启动某个应用程序,或者导航窗格)。ExpandedBlockControl
是由ScrollContainer
在处理其Blocks
属性(记住,这是一个List<BlockItemBase>
列表,可以包含BlockWorkItem
和GroupedBlockItem
项)时创建的。GroupedBlockItem
(表示 1 到GroupedBlockControl.BlocksPerGroup
之间的块的分组),其中每个块由一个单独的BlockWorkItem
表示(见上面第 1 项)。
所以您真正需要关心的就是创建一个 List<BlockItemBase>
,其中包含任何满足您要求的 BlockWorkItem
或 GroupedBlockItem
的混合。
下面展示了一个如何创建一些分组/非分组块和图片的完整示例,但请注意这**只是模拟**数据,您的实际数据很可能来自数据库或其他来源。
这是演示应用程序的 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());
}
}
}
就是这样
像往常一样,我想问一下,如果您喜欢这篇文章,并重视我写的文章,能否请您投个赞成票和/或评论?当人们发现您的文章有用时,听到这些总是很高兴的。谢谢!