可配置的 Silverlight 图像旋转器






4.92/5 (16投票s)
使用 Silverlight 2.0 和 C#/VB.NET 构建一个具有有用基本功能且易于设置和部署的图片旋转器。
引言
使用 Silverlight 2.0 和 C#/VB.NET,我们将构建一个具有有用基本功能且易于设置和部署的图片旋转器。
特点
- 可以链接到应用程序部署的本地图片,或者链接到互联网上托管的图片。
- 图片路径和属性存储在 XML 文件中,方便更改,无需重新编译应用程序。
- 自动播放可以开关。
- 每张图片显示的时长可调。
- 图片过渡效果可以开关。
- 图片编号、上一张和下一张按钮可以隐藏/显示。
- 选择编号按钮时可以暂停自动播放,选择其他按钮时可以重新开始。
- 图片边框可以显示/隐藏。
- 图片边框的宽度和颜色/透明度可以更改。
- 所有配置更改都可以进行,而无需重新编译 ImageRotator。
使用代码
在构建此图片查看器时,我最注重的是使其易于在网站(HTML 或 ASP.NET)上实现,同时包含一些有用的功能。我还想确保可以在不重新编译应用程序或重启 Web 服务器的情况下更改、删除和添加图片。由于我将许多配置复杂性抽象化了,所以对于我认为是简单应用程序的代码量来说,它有点长。因此,我将不详细介绍构成此控件的每个方法。相反,我将简要概述其工作原理,然后花更多时间介绍引起我最大麻烦的几个问题,并为那些不关心它如何工作但只想使用它的人提供良好的部署说明。
概述
此应用程序分为两个项目。第一个是网站项目 (ImageRotator.Web),另一个是 Silverlight 项目 (ImageRotator)。网站项目有一个名为 ClientBin 的文件夹,Silverlight 应用程序在运行时可以访问它。我们将在此处存储实际图片和 SlideShowImages.xml 文件。
该网站还包含两个使用 ImageRotator
控件的网页。第一个是 ImageRotatorTestPage.aspx。此页面不仅包含控件,还包含一些用于更改 ImageRotator
属性并在页面刷新时刷新控件的控件,以便您可以看到更改。显然,这是为了演示目的,您的页面很可能不包含这些其他控件。此页面的代码隐藏也显示了如何从 web.config 的 appSettings
或从页面刷新时的网页控件值加载所需的 InitParams
,并将它们传递到 ImageRotator
控件。包含 Silverlight 控件的第二个页面是 ImageRotatorTestPage.html。此页面不包含用于更改 InitParams
和在运行时刷新控件的所有控件。但是,它展示了如何使用 param
标签(名称为“initParams
”)和逗号分隔的名称/值对列表来加载 InitParams
的另一个示例。
请注意,ImageRotator
要求 *必须* 传递所有这些参数。还有其他传递 InitParams
的方法我没有展示,例如通过 URL 字符串传递。我将这些其他方法留给您自行研究。
转到第二个项目 ImageRotator,我们有构成 Silverlight 控件并赋予其功能的类。我将简要介绍每个文件。
- SlideShowImage.cs:此类代表一张图片,并包含与单张图片相关的属性。
- PictureAlbum.cs:此类包含
SlideShowImages
的集合以及集合中所有图片的通用属性。此类还包含异步调用,该调用从 SlideShowImages.xml 文件加载图片到集合中,并触发pictureAlbumLoadedHandler
来通知监听器图片集已加载。 - PictureAlbumEventArgs.cs:此类包含自定义事件的支持代码,当图片集完成异步加载要由
ImageRotator
使用的图片集合(从网站上的 SlideShowImages.xml 文件)时,会触发这些事件。 - App.xaml.cs:当应用程序启动时,会触发
Application_Startup
事件。在此处,我们验证是否正在传递ImageRotator
所需的所有参数。我们没有对参数进行任何健全的检查,只是验证传递的数量是否正确。如果任何参数格式不正确,我们稍后将引发异常。 - Page.xaml:包含
ImageRotator
控件的一些标记,其余部分在控件加载时动态创建。但是,您可以在此处看到环绕整个图片的图像边框。此外,还包括将在运行时动态添加图像控件的imageStackPanel
(每个图片一个)。最后,此处还可以看到navigationStackPanel
,其中包含多边形(箭头)按钮,以及numberedItemsStackPanel
,这是一个用于在运行时添加的编号按钮的容器。
<UserControl x:Class="ImageRotator_CSharp.Page"
xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
xmlns :local="clr-namespace:ImageRotator_CSharp"
x:Name="imageRotatorControl">
<Border x:Name="imageBorder" Cursor="Hand">
<Canvas x:Name="layoutRoot" Background="White" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" Margin="0">
<StackPanel x:Name="imageStackPanel" />
<Border x:Name="navigationBorder" Background="White" Opacity=".5"
CornerRadius="10" BorderBrush="White" BorderThickness="0"
Canvas.Left="10"
Canvas.Top="325">
<StackPanel x:Name="navigationStackPanel" Orientation="Horizontal"
Margin="2,1,2,1" >
<Polygon x:Name="arrowLeft"
MouseLeftButtonDown="LeftArrow_MouseLeftButtonDown"
MouseEnter="Arrow_MouseEnter"
MouseLeave="Arrow_MouseLeave"
Points="0,6,10,0,10,12"
Fill="Black"
VerticalAlignment="Center" Margin="2,2,8,2" />
<StackPanel x:Name="numberedItemsStackPanel"
Orientation="Horizontal" />
<Polygon x:Name="arrowRight"
MouseLeftButtonDown="RightArrow_MouseLeftButtonDown"
MouseEnter="Arrow_MouseEnter"
MouseLeave="Arrow_MouseLeave"
Points="0,0,10,6,0,12"
Fill="Black"
VerticalAlignment="Center" Margin="2" />
</StackPanel>
</Border>
</Canvas>
</Border>
</UserControl>
ImageRotator
及其功能得以实现。再次重申,我不会在此处介绍所有方法,但您可以随时下载并自行查看此代码。我已尽力为所有内容添加了良好的注释;但是,如果您有任何疑问,请随时与我联系。我将介绍在开发此应用程序期间给我带来最大困难的区域。无需重新编译即可更改图片
我最初构建应用程序时,将图片放在 ImageRotator Silverlight 库内的图片目录中。这没什么问题,除了一个地方。当调用应用程序时,此库会被编译成一个 .xap 文件并放置在网站的 ClientBin 文件夹中。这包括该项目中的所有内容,甚至包括图片。但是,通过一些研究,我发现了网站 ClientBin 文件夹的真正魔力。底线是,文件可以存储在此目录中,并在运行时由 Silverlight 控件访问。因此,我创建了一个 images 子目录,并将一些我想显示的图片放在此目录中。我还将 SlideShowImages.xml 文件放在这里。此文件包含您希望部署的图片以及每张图片的属性。
<?xml version="1.0" encoding="utf-8"?>
<!--
ImageUri: Absolute or Relative path and name of image.
RedirectLink (optional): Uri to redirect to if image is clicked.
Order (optional): Image order expression.
-->
<PictureAlbum ImageWidth="480" ImageHeight="360">
<SlideShowImage>
<ImageUri>../Images/a.jpg</ImageUri>
<RedirectLink>http://www.tasteofstepford.com</RedirectLink>
<Order>1</Order>
<DisplayImage>False</DisplayImage>
</SlideShowImage>
<SlideShowImage>
<ImageUri>../Images/b.jpg</ImageUri>
<RedirectLink>http://www.tasteofstepford.com</RedirectLink>
<Order>2</Order>
<DisplayImage>True</DisplayImage>
</SlideShowImage>
<SlideShowImage>
<ImageUri>../Images/c.jpg</ImageUri>
<RedirectLink>http://www.tasteofstepford.com</RedirectLink>
<Order>3</Order>
<DisplayImage>True</DisplayImage>
</SlideShowImage>
</PictureAlbum>
将配置/设置值从网站传递到 Silverlight 应用程序
步骤 #1:第一步是实际传递网站中的名称/值对。这可以通过几种不同的方式完成。我从我的 ImageRotatorTestPage.aspx 页面传递它们的方式是通过代码隐藏。我构建了一个名称值对的字符串(从 web.config 获取初始加载值,并从页面控件刷新值)。然后,我将 InitParams
字符串设置到 ImageRotator
实例的 InitParamaters
属性中。
protected void refresh_Click(object sender, EventArgs e)
{
string browserWindowOptions = "resizable=1|scrollbars=1|menubar=1|status=1|"
browserWindowOptions += "toolbar=1|titlebar=1|width=900|height=725|left=5|top=5";
string initParams = "autoPlay=" + autoPlay.Checked.ToString();
initParams += ",autoPlayInterval=" + autoPlayInterval.Text;
initParams += ",numberedNavigation=" + numberedNavigation.Checked.ToString();
initParams += ",arrowNavigation=" + arrowNavigation.Checked.ToString();
initParams += ",stopStartAutoPlay=" + stopStartAutoPlay.Checked.ToString();
initParams += ",animation=" + animation.Checked.ToString();
initParams += ",border=" + border.Checked.ToString();
initParams += ",borderThickness=" + borderThickness.SelectedValue;
initParams += ",argb=" + a.Text + "|" + r.Text +
"|" + g.Text + "|" + b.Text;
initParams += ",imageRotatorDiv=" + imageRotatorDiv;
initParams += ",browserWindowOptions=" + browserWindowOptions;
imageRotator.InitParameters = initParams;
}
请注意,InitParams
的两个名称/值对包含一个管道分隔的列表。我最初使用了逗号分隔,但这破坏了 initParams
的分隔符。为了解决这个问题,我编码了逗号。然而,我觉得这很难阅读和维护。最终,我决定使用管道作为分隔符。由于 ImageRotatorTestPage.html 没有代码隐藏,我必须找到另一种方法从它传递 InitParams
。事实证明,这可以通过 param
标签来实现,其 name
属性设置为 initParams
,其 value
属性设置为属性=值对的逗号分隔列表。
<object data="data:application/x-silverlight-2,"
type="application/x-silverlight-2" width="100%" height="100%">
<param name="source" value="ClientBin/ImageRotator_CSharp.xap"/>
<param name="onerror" value="onSilverlightError" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="2.0.31005.0" />
<param name="autoUpgrade" value="true" />
<!-- Begin: initParams for ImageRotator -->
<param name="initParams"
value="autoPlay=true,
autoPlayInterval=7,
numberedNavigation=true,
arrowNavigation=true,
stopStartAutoPlay=true,
animation=true,
border=true,
borderThickness=2,
imageRotatorDiv=silverlightControlHost,
browserWindowOptions=resizable=1|scrollbars=1|menubar=1|
status=1|toolbar=1|titlebar=1|width=1000|height=725|left=5|top=5,
argb=255|0|0|0" />
<!-- End: initParams for ImageRotator -->
<a href=http://go.microsoft.com/fwlink/?LinkID=124807 style="text-decoration: none;">
<img src=http://go.microsoft.com/fwlink/?LinkId=108181
alt="Get Silverlight" style="border-style: none"/>
</a>
</object>
步骤 #2:参数必须传递到 Silverlight 页面 (Page.xaml)。这不是直接完成的,也不是默认设置为传递 InitParams
。它们必须首先传递到应用程序类 (App.xaml.cs),然后由应用程序类将它们传递到您想要的任何用户控件(在我这里是 Page.xaml.cs)。更具体地说,每次加载 Silverlight 应用程序时(首次加载或通过页面刷新),都会调用 App.xaml.cs 的默认构造函数 (App()
)。这会连接 Application_Startup
事件,然后该事件将被调用。默认情况下,此方法仅调用 Page.xaml.cs 中的默认构造函数。我们需要它做的是调用一个重载的构造函数(我们将需要创建它),该构造函数将 InitParams
作为参数。以下是调用重载的 Page
类构造函数的更新后的 Application_Startup
方法。
private void Application_Startup(object sender, StartupEventArgs e)
{
if (e.InitParams.Count == 11)
this.RootVisual = new Page(e.InitParams);
else
{
string exc = "Must pass the required InitParams " +
"to the Page.xaml.cs constructor"
throw new Exception(exc);
}
}
步骤 #3:这是接受 InitParams
并将值放入一些私有成员变量以供以后使用的重载构造函数的示例。
public Page(IDictionary<string, > parameters)
{
InitializeComponent();
// retrieve the initParams, convert back any ASCII characters
foreach (var item in parameters)
{
switch (item.Key)
{
case "autoPlay":
enableAutoPlay = Convert.ToBoolean(item.Value);
break;
case "autoPlayInterval":
autoPlayIntervalSeconds = Convert.ToInt32(item.Value);
break;
case "numberedNavigation":
displayNumberedButtons = Convert.ToBoolean(item.Value);
break;
case "arrowNavigation":
displayArrows = Convert.ToBoolean(item.Value);
break;
case "stopStartAutoPlay":
buttonsStopStartAutoPlay = Convert.ToBoolean(item.Value);
break;
case "animation":
enableAnimations = Convert.ToBoolean(item.Value);
break;
case "border":
displayImageBorder = Convert.ToBoolean(item.Value);
break;
case "borderThickness":
imageBorderThickness = Convert.ToInt32(item.Value);
break;
case "argb":
argbBorderColor = item.Value.ToString().Replace("|", ",");
break;
case "imageRotatorDiv":
imageRotatorDiv = item.Value.ToString();
break;
case "browserWindowOptions":
browserWindowOptions = item.Value.ToString().Replace("|", ",");
break;
}
}
LoadPictureAlbum();
}
异步从网站将 XML 文件加载到 PictureAlbum 类的实例中,并在完成后通知自定义事件侦听器
这里有两点需要注意。首先,我们希望异步访问网站并获取 SlideShowImages.xml 文件。此 XML 文件包含每个图片的 <SlideShowImage>
元素。每个 SlideShowImage
元素包含几个标识该图片属性的元素。我选择使用元素而不是属性,是因为我希望此应用程序对非开发人员也易于使用,我认为元素比属性更易于阅读(对我来说也是如此)。我们需要这样做才能加载 PictureAlbum
类,该类包含一个 SlideShowImages
集合,用于驱动 ImageRotator
。我们还需要让 ImageRotator
知道此过程已完成,以便它可以随后使用 SlideShowImages
实例来动态创建控件上的图片和按钮。
这可能是我花费时间最多的地方。我的第一个想法是使用某种等待句柄来阻止我的 Page.xaml.cs 类中的调用线程,直到 XML 文件加载完毕,然后发出完成信号,以便创建图片控件。根据我创建新线程的方式以及使用等待句柄,我遇到了各种问题。要么等待句柄似乎没有阻塞,要么它会到达阻塞控件并永远旋转而从未到达我的回调方法进行信号。相信我,我尝试了所有方法。然后,我做了一些研究(我早就应该做的),并发现了问题。在 Silverlight 中使用 WaitHandle
s、CallBackMethod
s 或 ManualResetEvent
s 没有问题。但是,*您不能在 Silverlight 应用程序中阻塞主 UI 线程*。我的替代方法,几乎毫不费力地奏效,是创建一个自定义事件,在 XML 文件加载完成后触发该事件。
首先,在 PictureAlbum
类中,我为异步调用创建了一个委托,并创建了一个自定义事件,在完成时触发该事件。
public delegate void PictureAlbumLoaded(object sender, PictureAlbumLoadedEventArgs a);
public event PictureAlbumLoaded pictureAlbumLoaded;
接下来,我创建了一个方法 (LoadPictureAlbumXMLFile
),该方法在页面类构造期间被调用。此方法创建一个 WebClient
实例,我们将使用它通过其 DownloadStringAsync
方法通过网络获取 XML 文件。我们还将连接到 WebClient
实例的 DownloadStringCompleted
事件。此方法接受一个回调方法作为参数,我已将其提供(XMLFileLoaded
)。一旦 WebClient
完成了获取文件的异步调用,它就会触发其 DownloadStringCompleted
事件。在我们的例子中,这将导致调用 XMLFileLoaded
。此方法将返回的 XML 文件作为字符串,并将其解析为 XDocument
。然后,我们使用一些 LINQ to XML 从中提取图片详细信息,并将其放入 PictureAlbum
的图片集合中。最后,触发 pictureAlbumLoaded
事件,以通知任何监听器此方法调用的成功或失败。
public void LoadPictureAlbumXMLFile()
{
WebClient xmlClient = new WebClient();
xmlClient.DownloadStringCompleted +=
new DownloadStringCompletedEventHandler(XMLFileLoaded);
xmlClient.DownloadStringAsync(new Uri("SlideShowImages.xml",
UriKind.RelativeOrAbsolute));
}
private void XMLFileLoaded(object sender, DownloadStringCompletedEventArgs e)
{
if (e.Error == null)
{
Int32 buttonNumber = 0;
slideShowImages = new List<SlideShowImage>();
PictureAlbumLoadedEventArgs pictureAlbumLoadedCompletedSuccessfully = null;
try
{
xdoc = XDocument.Parse(e.Result);
imageWidth = Convert.ToDouble(xdoc.Element("PictureAlbum")
.Attribute("ImageWidth").Value);
imageHeight = Convert.ToDouble(xdoc.Element("PictureAlbum")
.Attribute("ImageHeight").Value);
foreach (XElement item in xdoc.Elements("PictureAlbum")
.Elements("SlideShowImage"))
{
if (item.Element("DisplayImage").Value.ToLower() == "true")
{
SlideShowImage slideShowImage = new SlideShowImage();
if (item.Element("ImageUri").Value.Contains("http"))
slideShowImage.ImageUri =
new Uri(item.Element("ImageUri").Value, UriKind.Absolute);
else
{
slideShowImage.ImageUri =
new Uri(item.Element("ImageUri").Value, UriKind.Relative);
}
slideShowImage.RedirectLink = item.Element("RedirectLink").Value;
slideShowImage.Order = Convert.ToInt32(item.Element("Order").Value);
slideShowImage.ButtonNumber = buttonNumber++;
slideShowImages.Add(slideShowImage);
}
}
pictureAlbumLoadedCompletedSuccessfully = new PictureAlbumLoadedEventArgs(true);
}
catch
{
pictureAlbumLoadedCompletedSuccessfully =
new PictureAlbumLoadedEventArgs(false);
}
finally
{
// Fire off pictureAlbumLoadedHandler so the listeners can respond.
pictureAlbumLoaded(this, pictureAlbumLoadedCompletedSuccessfully);
}
}
}
调用所有这些的方法 (LoadPictureAlbum
) 位于 Page
类中,它也有一些异步逻辑。它首先连接一个 PictureAlbumLoaded
事件的监听器,并将 PictureAlbumLoaded
方法作为回调传递。然后它调用 LoadPictureAlbumXMLFile
,如我们之前所见,它将触发 PictureAlbumLoaded
监听器。一旦调用此监听器,就会调用 PictureAlbumLoaded
。它首先检查 PictureAlbumLoadedEventArgs
以验证加载是否成功,如果不成功,则引发异常。如果加载成功,它将继续为集合中的每个 image
动态生成一个 Image 控件。
private void LoadPictureAlbum()
{
pictureAlbum.pictureAlbumLoaded +=
new PictureAlbum.PictureAlbumLoaded(PictureAlbumLoaded);
pictureAlbum.LoadPictureAlbumXMLFile();
}
private void PictureAlbumLoaded(object sender,
PictureAlbumLoadedEventArgs args)
{
try
{
if (args.PictureAlbumLoadedSuccessfully)
{
imageWidth = pictureAlbum.ImageWidth;
imageHeight = pictureAlbum.ImageHeight;
pictures = pictureAlbum.SlideShowImages;
imageCount = pictures.Count;
// create first image
AddNewImage(0, Visibility.Visible);
imagesToLoadCount = imageCount - 1;
// create rest of images asynchronously
for (int i = 1; (i < pictures.Count); i++)
ThreadPool.QueueUserWorkItem(ProcessSlideShowImage, i.ToString());
}
else
{
string exc = "The SlideShowImages.xml file failed to load the "
exc += "PictureAlbum class successfully."
throw new Exception(exc);
}
}
finally
{
pictureAlbum.pictureAlbumLoaded -=
new PictureAlbum.PictureAlbumLoaded(PictureAlbumLoaded);
}
}
淡出旧图片并淡入新图片
这个不是太难,但确实需要一些试错。基本上,我创建了一个带有动画 (DoubleAnimation
) 的方法来淡出当前 Image
控件。在该方法中,我连接了 storyboard 的 Completed
事件。当 storyboard 动画完成时,图片将淡出,并将触发事件。此事件与一个回调方法相关联,该方法然后用新 Image
控件替换旧 Image
控件,然后调用 FadeIn
。FadeIn
是另一个动画 (DoubleAnimation
),顾名思义,它会淡入新的 Image
控件。
private void FadeOut()
{
DoubleAnimation fadeOutDoubleAnimation = new DoubleAnimation();
fadeOutDoubleAnimation.From = 1;
fadeOutDoubleAnimation.To = 0;
fadeOutDoubleAnimation.Duration = new Duration(TimeSpan.FromSeconds(1));
fadeOutStoryboard = new Storyboard();
fadeOutStoryboard.Children.Add(fadeOutDoubleAnimation);
fadeOutStoryboard.Completed += new EventHandler(FadeOutStoryboard_Completed);
Storyboard.SetTarget(fadeOutDoubleAnimation,
((Image)imageStackPanel.Children.ElementAt(lastImageIndex)));
Storyboard.SetTargetProperty(fadeOutDoubleAnimation,
new PropertyPath(ListBox.OpacityProperty));
layoutRoot.Resources.Add("fadeOutStoryboard", fadeOutStoryboard);
fadeOutStoryboard.Begin();
layoutRoot.Resources.Remove("fadeOutStoryboard");
}
private void FadeOutStoryboard_Completed(object sender, EventArgs e)
{
((Image)imageStackPanel.Children.ElementAt(lastImageIndex)).Visibility =
Visibility.Collapsed;
((Image)imageStackPanel.Children.ElementAt(nextImageIndex)).Visibility =
Visibility.Visible;
lastImageIndex = nextImageIndex;
FadeIn();
if ((enableAutoPlay || buttonsStopStartAutoPlay) && leavePaused == false)
autoPlayTimer.Start();
}
private void FadeIn()
{
DoubleAnimation fadeInDoubleAnimation = new DoubleAnimation();
fadeInDoubleAnimation.From = 0;
fadeInDoubleAnimation.To = 1;
fadeInDoubleAnimation.Duration = new Duration(TimeSpan.FromSeconds(1));
fadeInStoryboard = new Storyboard();
fadeInStoryboard.Children.Add(fadeInDoubleAnimation);
Storyboard.SetTarget(fadeInDoubleAnimation,
((Image)imageStackPanel.Children.ElementAt(nextImageIndex)));
Storyboard.SetTargetProperty(fadeInDoubleAnimation,
new PropertyPath(ListBox.OpacityProperty));
layoutRoot.Resources.Add("fadeInStoryboard", fadeInStoryboard);
fadeInStoryboard.Begin();
layoutRoot.Resources.Remove("fadeInStoryboard");
}
历史
- 2009 年 2 月 23 日 - 添加了 VB.NET 的源代码。