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

在实际业务应用程序中使用 Silverlight 动画

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.55/5 (85投票s)

2007年12月26日

CPOL

54分钟阅读

viewsIcon

239334

downloadIcon

2671

一篇关于使用 Silverlight 动画构建实际业务应用的文章。

Drink Mate Standard Imprint Image Viewer Screenshot

目录

引言

本文展示了使用 Silverlight 动画构建实际业务应用所需的详细步骤。它说明了业务相关注意事项和用户界面设计如何与 Silverlight 编码决策重叠。

从技术角度来看,本文解释了

  • 如何在 HTML 页面中包含 Silverlight 内容
  • 如何在 HTML 页面中包含多个 Silverlight 控件
  • 如何为您的应用程序创建自定义按钮
  • 如何将矢量图形转换为 XAML
  • 如何使用 Silverlight Downloader
  • 如何通过使用带有 Downloader 的多个 Zip 文件来提高应用程序的性能
  • 如何将事件处理程序链接到您的 Silverlight 对象
  • 如何使用事件处理程序移动 XAML 图形
  • 如何使用事件处理程序为 XAML 图形着色
  • 如何使用事件处理程序显示或隐藏 HTML 内容
  • 如何使用事件处理程序显示或隐藏 XAML 内容
  • 如何使用 Visual Studio 2008 创建动画
  • 如何使用 Expression Blend 创建动画
  • 如何创建左右移位动画
  • 如何创建淡出和淡入动画
  • 如何构建应用程序的动画介绍
  • 如何允许用户取消动画
  • 如何在 Silverlight 应用程序中使用自定义字体
  • 如何将滑块绑定到 TextBlockFontSize 属性
  • 如何通过拖动重新定位 TextBlock
  • 如何将 TextBlock 自动居中在另一个 TextBlock
  • 如何创建可以动态添加和移除的工具提示
  • 如何理解 Microsoft 不断变化的默认文件名
  • 如何管理 HTML 和 Silverlight 对象之间的交互
  • 如何管理不同 Silverlight 控件中 Silverlight 对象之间的交互
  • 如何为自己的第一个 Silverlight 项目做准备

注意:一位朋友最近指出,我的标题可能被解释为暗示本文与企业业务应用程序(WCF、Web 服务等)中的 Silverlight 动画有关。很遗憾,并非如此。大型企业和小企业都有。Drink Mate 确实是一个小型企业(实际上非常小,或者其他任何同样微小的形容词)。然而,它是一个真实的业务,我很自豪地报告,自 1990 年以来,我们已售出超过 300 万个 Drink Mates。因此,我认为标题是准确的,只要它是按照我的意思来理解的。本节中的列表几乎涵盖了本文讨论的所有内容。

背景

为了准备我在 Foothill College(加利福尼亚州洛斯阿尔托斯山丘)的 Silverlight 课程,我决定构建一个小型演示应用程序。我希望做一些既实用又具有教育意义的事情,所以我选择了一个可以为我的 ASP.NET 2.0 网站(用于我的 Drink Mate 业务)带来好处的应用程序。我的目标是构建一些 Silverlight 动画,将一组 2D 图形叠加在我们的 Drink Mate 产品的一系列.jpg图像上。

Drink Mates 是一种小型塑料夹子,您可以在鸡尾酒会上使用,当您一手拿着食物盘,一手拿着饮料时。它的设计旨在解决当您的双手都拿满东西时,试图吃、喝、握手、签名等问题。

此时,我强烈建议您暂停一下,尝试一下应用程序本身。它以一个简短的介绍开始,应该能帮助您更好地理解本文。此外,如果您实际看到应用程序的工作方式,我的文字引用将更容易与应用程序的任何特定功能或方面相关联。

如果您已经安装了 Silverlight,回顾我的应用程序只需几分钟即可预览本文将讨论的所有功能。由于 Silverlight 是基于 Web 的,如果您已经安装了浏览器插件,则无需进一步下载、解压缩和安装。

如果您尚未安装 Silverlight,恕我直言,您在继续阅读本文之前,不尝试使用我的应用程序 - 或其他同等样本应用程序 - 可能是浪费时间。Silverlight(及其同类 WPF)不是简单的技术,获得某种视觉参考框架来关联其一些晦涩的术语非常重要。

正如我的应用程序介绍中所解释的那样,大多数 Drink Mates 都带有定制的广告印刷品。这些通常是公司标志或个性化文本。

然而,对于某些通用场合(例如,圣诞节或新年派对),如果我们客户没有定制的艺术品但仍希望使用装饰过的 Drink Mate 而不是普通的 Drink Mate,我们有一些标准的图像(剪贴画)。此标准印刷品查看器的目的是允许潜在客户预览我们标准印刷品的集合,并结合任何产品和印刷颜色组合。

当然,准备构建此 Silverlight 应用程序的许多工作与编程关系不大。首先,我必须环游世界才能获得一些不错的照片放入我的页眉图像中。我甚至不得不说服我的侄子结婚,以便他可以为我的页眉拼贴拍照。然后,我必须花费大量时间进行产品摄影,以收集一系列图像作为我的 2D 图形标准印刷品的背景。幸运的是,我从事这项工作已经很多年了,几乎所有这些初步工作在我开始构建我的 Silverlight 应用程序之前就已完成。

尽管在 WPF 方面拥有丰富的经验,但当我开始这个项目时,我对 Silverlight 完全陌生。我开始阅读 Adam Nathan 的整本书《Silverlight 1.0 Unleashed》。

我还阅读了 Laurence Moroney 的书《Introducing Microsoft Silverlight 1.0》的很大一部分,特别是关于如何将 Silverlight 内容机械地插入 HTML 页面的部分。

荣誉提名:Chris Sells 和 Ian Griffiths 的《Programming WPF》的附录 E 也包含对 Silverlight 的精彩总结,值得一读。

考虑到我之前对 JavaScript 的经验几乎为零,我几乎立即发现理解 Silverlight 控件、XAML 内容、HTML 和 JavaScript 之间的关系对于使这个项目能够正常工作至关重要。尽管如此,在处理此领域之前,我首先想描述影响我用户界面设计选择的相关考虑因素。

用户界面设计问题

创建标题和页眉图像

每个应用程序(或网页)都应该有一个清晰陈述的标题或页眉。幸运的是,我有一个大约一年前为另一个项目创建的页眉图形,所以我所要做的就是更改文本,使其显示“Drink Mate Standard Imprints”。

构建元素轮廓

如果 Silverlight 1.0 支持 Border(就像 WPF 一样),我就会用它们来在我的图像控件周围放置轮廓。由于 Border 不是一个选项,我改用了 Line。在某个阶段,我尝试使用 Rectangle,假设一次绘制四条线比一次绘制一条线更容易。但是,由于我希望我的轮廓颜色为深灰色而不是纯黑色,因此在遇到一些 Rectangle 重叠的问题后,我最终切换到了 Line

从布局设计的角度来看,我决定将 Drink Mate 的不同标准产品颜色(白色、黑色、透明和烟灰色)的图像 along the right side of the main image。为了保持对称性,我将每个较小的图像尺寸设置为主图像的四分之一(并考虑了轮廓的厚度)。

在主图像和备用产品颜色图像下方,我放置了五个轮廓,代表 Drink Mate 的印刷区域。通过使每个此类轮廓的大小恰好是主图像印刷区域大小的一半,我能够大大简化确定全尺寸图像的正确 ScaleXScaleY 值的数学计算,因为它们始终是下方缩略图相应值的两倍(顶行)。

设计颜色条

颜色条代表我们用于印刷 Drink Mate 的每种非定制油墨。正如促销产品行业中的标准一样,我们可以通过混合这些标准油墨来匹配几乎任何 PMS 颜色(PMS = Pantone Matching System),但该服务需要混合费。最初,我将颜色布局为与 Drink Mate 网站上的布局相匹配,该网站在图表中列出了可用的标准颜色。后来我得出结论,最好将这些颜色大致从最暗到最亮排列。这种修改布局的优点是,当将鼠标悬停在颜色条上以预览备用印刷颜色时,用户可以更容易地限制其笔触,对于黑色或烟灰色 Drink Mate,选择浅色印刷,对于白色或透明 Drink Mate,选择深色印刷。这样,用户可以更容易地避免预览白色 Drink Mate 上的白色印刷品,或黑色 Drink Mate 上的黑色印刷品的非逻辑组合。

颜色条本身仅由一系列 30x30 的 Rectangle 组成,其 Fill 属性设置为相应的颜色。每个 Rectangle 共享相同的 Canvas.Top 值,并具有一个 Canvas.Left 值,该值比其左侧的相邻对象大 30。

<TextBlock Text="Imprint Colors" Canvas.Top="920" 
    Canvas.Left="170"  FontSize="22" 
    FontFamily="Comic Sans MS"   />
<Rectangle Name="BlackRectangle"  Canvas.Top="920" Canvas.Left="330" 
    Height="30" Width="30" Fill="Black" 
    Stroke="#FFFFFF" StrokeThickness="0" 
    MouseLeftButtonUp="handleMouseUpImprintColors" 
    MouseEnter="handleMouseEnterImprintColors" 
    MouseLeave="handleMouseLeaveImprintColors" />
<Rectangle Name="ReflexBlueRectangle"  Canvas.Top="920" Canvas.Left="360" 
    Height="30" Width="30" Fill="#000099" 
    Stroke="#FFFFFF" StrokeThickness="0" 
    MouseLeftButtonUp="handleMouseUpImprintColors" 
    MouseEnter="handleMouseEnterImprintColors" 
    MouseLeave="handleMouseLeaveImprintColors"  />
    ...
<Rectangle Name="SilverRectangle"  Canvas.Top="920" Canvas.Left="630" 
    Height="30" Width="30" Fill="#CCCCCC" 
    Stroke="#000000" StrokeThickness="0" 
    MouseLeftButtonUp="handleMouseUpImprintColors" 
    MouseEnter="handleMouseEnterImprintColors" 
    MouseLeave="handleMouseLeaveImprintColors"  />
<Rectangle Name="WhiteRectangle"  Canvas.Top="920" Canvas.Left="660" 
    Height="30" Width="30" Fill="White" 
    Stroke="#000000" StrokeThickness="0" 
    MouseLeftButtonUp="handleMouseUpImprintColors" 
    MouseEnter="handleMouseEnterImprintColors" 
    MouseLeave="handleMouseLeaveImprintColors"  />

显示和隐藏文本相关控件

圣诞节、庆祝活动和新年图像集是真正的标准印刷品,而生日和毕业图像集是标准印刷品(剪贴画)和派对荣誉嘉宾姓名的组合。允许用户输入姓名并更改其字体系列和字号的 HTML 控件仅在生日和毕业图像集的情况下才相关。因此,我在创建它们的 HTML 中将它们的 Visibility 属性设置为“hidden”。从技术上讲,此 Visibility 属性属于包含 HTML 控件的 DivSpan

<span id="spnBirthdayName" style="margin-left:15px;  margin-top:5px; margin-right:250px; 
        visibility: hidden; font-family: 'Comic Sans MS'; font-weight: normal">
    Please Enter a Name: 
    <input type="text" id="txtBirthdayName" onkeyup="handleBirthdayNameChange(this);"
        onblur="CenterNameText();"  />
</span>

然后,每次切换图像集时,如果新的图像集是生日或毕业,则将此 Visibility 属性设置为“visible”。

switch(newImageSet)
{
    case "Christmas":
        HideUserTextControls(); 
        ... 
    case "Birthday Party":  
        //Display the controls to let users enter a Name
        //and change the font family and font size
        var spnBirthdayName = document.getElementById("spnBirthdayName");
        spnBirthdayName.style.visibility = "visible"; 
        var divCustomFonts = document.getElementById("divCustomFonts");
        divCustomFonts.style.visibility = "visible";  
        var divResizeFontText = document.getElementById("divResizeFontText");
        divResizeFontText.style.visibility = "visible";  
        var divSliderTrack = document.getElementById("divSliderTrack");
        divSliderTrack.style.visibility = "visible";  
        var divSliderDisplay = document.getElementById("divSliderDisplay");
        divSliderDisplay.style.visibility = "visible";

另一方面,如果新的图像集是圣诞节、新年或庆祝活动,我将通过将其主机容器的 Visibility 属性设置为“hidden”来隐藏这些控件。

function HideUserTextControls()
{
    var spnBirthdayName = document.getElementById("spnBirthdayName");
    spnBirthdayName.style.visibility = "hidden"; 
    var divCustomFonts = document.getElementById("divCustomFonts");
    divCustomFonts.style.visibility = "hidden";  
    var divResizeFontText = document.getElementById("divResizeFontText");
    divResizeFontText.style.visibility = "hidden";  
    var divSliderTrack = document.getElementById("divSliderTrack");
    divSliderTrack.style.visibility = "hidden";  
    var divSliderDisplay = document.getElementById("divSliderDisplay");
    divSliderDisplay.style.visibility = "hidden";
}

选择 Silverlight 1.0 和 Silverlight 2.0

Silverlight 1.0 和 Silverlight 2.0(以前称为 Silverlight 1.1)之间的主要区别是众所周知的。2.0 版本将支持使用 .NET 语言(C# 和 VB.NET)进行事件处理,而 1.0 版本仅支持 JavaScript 进行事件处理。版本 2.0 占优。1.0 版本几乎不支持用户界面控件(没有按钮、列表框或组合框、文本框等)。Microsoft 已宣布将在 2.0 版本中提供大多数这些常用用户界面控件。同样,版本 2.0 占优。2.0 版本预计还将包括对布局和数据绑定的支持。再一次,版本 2.0 占优。游戏、设置、比赛……好吧,不那么快,1.0 版本是已发布的版本,而 2.0 版本目前处于 alpha 阶段。虽然Silverlight Gallery 中包含的 2.0 版本项目集合表明可以使用 2.0 版本创建一些非常令人印象深刻的项目,但除了对 .NET 语言事件处理的支持外,上述大多数优势仍然只是未来的承诺。

最终,我个人在此项目上的决定非常简单,并且基于许多不可泛化的因素。我希望在 2008 年 4 月推出我的 Foothill College Silverlight 课程,远早于 2.0 版本的预期发布。提供基于未发布软件的课程实际上是不切实际的。虽然目前有许多涵盖 Silverlight 1.0 的书籍,但还没有涵盖 Silverlight 2.0 的书籍。此外,作为一名讲师,我认为我必须熟悉 1.0 版本和 2.0 版本,尽管后者显然是 Silverlight 的长期未来。

然而,我选择 1.0 版本给我带来的一个主要遗憾是,在观看 Vertigo 公司 Scott Stanfield 对某个 2.0 版本 C# 代码的演示时。我非常嫉妒地看到他使用Region来折叠他的代码以便于导航。我发现我的项目在 JavaScript 和 XAML 中缺少Region是生产力上的主要障碍。此项目的代码超过 3000 行,即使分成多个文件,有时定位特定代码段也是一项极其令人沮丧的任务。

在 HTML 页面中包含 Silverlight 内容

如果您是 Silverlight 新手,除非您理解本节讨论的内容,否则您将无法在构建自己的 Silverlight 项目方面取得太多进展。另一方面,如果您已经掌握了这些先决条件,您可以跳到下一节(创建自定义按钮)。

HTML 文件

Silverlight 显然是一项基于 Web 的技术,因此每个 Silverlight 应用程序的起点都必须是某种网页。如果您使用 Visual Studio 2008 为您创建新的 Silverlight 1.0 项目,它将创建一个名为Default.html的网页。在该网页的某个位置,您需要放置一个 Silverlight 控件。以下代码显示了文件Default.html的 HTML 内容,如果您使用 Silverlight 1.0 模板开始一个新项目,VS 2008 将为您创建该文件。突出显示的 createSilverlight() 方法(稍后解释)将创建必要的 Silverlight 控件。

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>CodeProjectIllustration</title>
    <script type="text/javascript" src="Silverlight.js"></script>
    <script type="text/javascript" src="Default.html.js"></script>
    <script type="text/javascript" src="Scene.xaml.js"></script>
</head>
<body>
    <div id="SilverlightPlugInHost">
        <script type="text/javascript">
            createSilverlight();
        </script>
    </div>
</body>
</html>

XAML 文件

此 Silverlight 控件将显示的内容通常包含在一个单独的 XAML 文件中(出于良好的代码组织目的),在本例中,Visual Studio 2008 将其命名为Scene.xaml。在这方面,我认为将这种安排概念化为大致类似于一个将包含并显示位图图像(通常是.jpg文件)的图像控件。在这个类比中,Silverlight 控件等同于图像控件,而复合 XAML 内容等同于.jpg图像。

协助用户下载和安装 Silverlight 插件 (Silverlight.js)

Silverlight 的情况比图像控件复杂得多。为了让网页中引用的 Silverlight 控件正常工作,用户的浏览器中必须存在一个浏览器插件。而且,用户第一次访问您的网页时,很可能这个浏览器插件不存在。因此,您的网页必须提醒用户需要此插件,并最好引导他们完成安装过程。幸运的是,Microsoft 编写了一个名为Silverlight.js的脚本,旨在做到这一点。不幸的是,此脚本可以进行一些改进,因为据我观察,对于普通计算机用户(意味着介于中级到高级 .NET 开发者水平以下的所有人)来说,用户体验可能会相当混乱。

如果用户没有在浏览器中安装 Silverlight,我认为用户应该收到的消息的逻辑顺序大致如下:

  1. 您请求的内容需要一个名为 Silverlight 的浏览器插件。
  2. Silverlight 大致上与 Adobe Flash 相当,因此既安全又易于安装。
  3. 如果您想安装 Silverlight 浏览器插件,请点击“确定”。
  4. 许可协议,Silverlight 正在安装,等等……
  5. Silverlight 已安装完毕。单击此处查看您之前请求的内容。

我建议您自己尝试此过程,看看它与我推荐的方案有多接近。我认为您会发现初始消息“Get Microsoft Silverlight”几乎没有向用户解释为什么他想要 Silverlight。诚然,再点击两次就可以让用户进入一个页面,该页面会详细讨论“为什么使用 Silverlight?”这个问题,但对于任何看到“Get Microsoft Silverlight”消息的用户来说,一个更简单、更有说服力的答案应该是“能够看到您请求的内容”。

此外,要求用户先将安装文件保存到磁盘然后再运行它,这对该过程来说必然是一个心理上和物理上的障碍。

尽管如此,由于您自己构建此任务的结构并不切实际,因此最佳解决方案是简单地使用 Microsoft 提供的Silverlight.js文件。(此外,正如我的一位好朋友指出的那样,根据 Silverlight 许可协议,使用Silverlight.js似乎是必需的。)然而,强烈建议将 Silverlight 控件的 inplaceInstallPrompt 属性设置为 true(而不是默认值),以便为需要安装 Silverlight 插件的用户提供更短的路径。此设置直接链接到要下载的文件,而不是链接到 Silverlight 网站上的官方下载页面。

Silverlight.createObjectEx({
    source: 'Scene.xaml',
    parentElement: document.getElementById('SilverlightPlugInHost'),
    id: 'SilverlightPlugIn',
    properties: {
        width: '1000',
        height: '780',
        background:'#7799aa',
        isWindowless: 'false',
        inplaceInstallPrompt: true, //must be added manually
            version: '1.0'
    },

除了协助用户下载和安装 Silverlight 浏览器插件之外,Silverlight.js还有一个重要任务。它包含构建将托管您的 XAML 内容的实际 Silverlight 控件所需的 createObject()createObjectEx() 方法。这两个方法基本相同,仅在它们接受的参数的语法上有所不同。CreateObjectEx 使用流行的 JSON 语法,对于大多数开发人员来说可能更可取。

Visual Studio 2008 在Default.html.js文件中名为 createSilverlight() 的方法中调用 Silverlight.createObjectExcreateObjectEx() 的实现可以在Silverlight.js中找到。这种安排的结构如下图所示。

Default.htmlScene.xaml 的名称只是默认名称,可以(通常也应该)更改以适合您的项目。虽然 JavaScript 可以找到方法,无论它位于哪个.js文件中,但出于良好的代码组织,Scene.xaml 中包含的任何 XAML 对象的 JavaScript 事件处理程序应放在一个名为Scene.xaml.js的文件中。如果您在项目中有一个以上的 XAML 文件(就像我的项目一样),则任何其他 XAML 文件中包含的 XAML 对象的 JavaScript 事件处理程序应放在一个具有相应名称和.js扩展名的单独文件中。这将在您的Default.html文件的 <head> 部分中需要其他引用,如下面的图所示。

从这两个图可以看出,开始您的项目将从以下步骤开始:

  • Default.html添加任何额外的 HTML 内容。
  • Default.html.js中名为 CreateSilverlight() 的方法中设置 Silverlight 控件的各种属性(例如,高度、宽度、版本)。
  • 创建要放在Scene.xaml中的 XAML 内容。

默认文件名

最后,关于默认文件名的说明。Microsoft 在其构建 Silverlight 应用程序的各种工具之间以及随时间推移对默认文件名的使用上并不一致。我的图表中显示的(Default.htmlScene.xaml)文件名是由 Visual Studio 2008 Beta 2 为 Silverlight 1.0 应用程序创建的。如果您使用 Visual Studio 2008 的最终版本为 Silverlight 1.0 应用程序,您仍然会得到相同的默认文件名。另一方面,如果您使用 Expression Blend 创建 Silverlight 1.0 应用程序,HTML 页面也命名为Default.html,但 XAML 文件名为Page.xaml

此外,如果您阅读 Silverlight QuickStart,题为“如何创建 Silverlight 项目”,您会在一个名为“基本 Silverlight 项目包含什么?”的部分中遇到以下引述:“根 HTML 文件:通常,它会被命名为default.html或类似的名称。Visual Studio 模板使用文件名TestPage.html。”(现在,如果有一个默认名称需要在生产应用程序中更改……我想,与密码“ChangeMe”不相上下。)继续阅读会发现以下引述:“CreateSilverlight.js:在 Visual Studio 模板中,此文件名为TestPage.html.js。”这似乎有点像“我想向您介绍我的儿子威廉。我们叫他迈克。”Nathan 在他的书中也提到了一个名为CreateSilverlight.js的文件,他建议该文件“按照约定”使用。不幸的是,随着时间的推移,约定似乎已经发生了很大变化,这可能会在您试图理解不同作者在不同时间编写的不同源代码材料时造成相当大的混淆。下表试图阐明这些默认文件的使用。

尽管此表显示 Silverlight.js 在所有情况下都统一使用,但您也可以将此文件的内容包含在您自己的其他.js文件中。然而,Microsoft 指出,其许可协议要求其未修改的内容存在于每个 Silverlight 应用程序中。

创建自定义按钮

由于 Silverlight 1.0 不包含 Button 作为用户控件,因此有必要使用某种图形工具创建自己的按钮。虽然 Silverlight 中没有 Button 本身,但您可以使用 RectangleEllipseLine 来构建自己的按钮。我对 Expression Graphic 的经验有限,因此我选择使用 Expression Blend 来创建我的按钮。

对于这个项目,我总共需要三个按钮:一个左三角形按钮,一个右三角形按钮,以及一个普通的矩形按钮,其功能是显示应用程序的使用说明。

对于这项特定任务,一个非常有用的资源是 Lee Brimelow 在 Lynda.com 上关于 Expression Blend 的培训视频,题为“创建动画按钮”。目前此视频可以免费观看。

我开始尝试创建我的三角形按钮,并几乎立即遇到了一些困难。虽然 XAML 包含 RectangleEllipseLine,但没有 Triangle 元素本身,并且虽然 Rectangle(或 Ellipse)上有一个 RadiusXRadiusY 属性,但 LinePolyLine 上没有相应的属性。我正在寻找我的三角形按钮的对称圆角,并且找不到一个简单的方法从三角形开始,然后仅仅拖动装饰器来塑造我的圆角。最后,我想到创建两个圆角矩形,并用其中一个来“修剪”另一个。这也不是那么容易,因为以对称方式对齐两个按钮并不容易。最终,我将它们放在了想要的位置,然后从 Blend 菜单中选择了 Object->Combine->Intersect。左边的图像显示了叠加在一起的按钮看起来如何。注意代表下部(蓝色)矩形外部尺寸的微弱蓝色轮廓。右边的图像显示了“修剪”操作的结果。

接下来,我创建了一个比我的按钮稍微小一点的版本,并将其叠加在我的初始按钮上。Blend 在创建渐变方面表现出色。(当然,Blend 无法替代艺术天赋,我的按钮可能也表明了这一点。)还需要创建我的按钮的其他版本来表示 MouseOverDisabled 状态。我的结果如下所示。

一旦我有了正确的三角形按钮,我就简单地使用 Object->Flip->Horizontal 菜单条目来创建等效的左侧按钮。

我使用类似的过程创建了矩形按钮,省略了修剪和翻转步骤。

创建必要的 2D 图形

这个项目的大部分 2D 图形最初都是 CorelDraw 格式,或者至少以某种方式进入了该格式。为了以我想要的方式在应用程序中使用它们,我需要将它们转换为 XAML。(以 XAML 格式显示这些图形将允许我通过更改其 PathFillStroke 属性来简单地更改颜色。)这是一个相对简单的过程,首先使用 CorelDraw 将文件转换为 Adobe Illustrator(.ai)格式,然后使用 Mike Swanson XAML exporter 将图像转换为 XAML。我的博客上有一个关于此过程的视频演示,其中确切地说明了如何完成。

尽管在 CorelDraw 过程的最初,我使用了一个模板(固定大小的矩形)来将每件艺术品的大小调整到大致相同的尺寸,但由于两个不同的原因,我仍然面临一些与大小相关的问题。首先,我需要每个图像的两个副本,一个用于放入印刷区域轮廓的缩略图表示,另一个用于叠加在 Drink Mate 上的全尺寸版本。其次,考虑到这些图像来自各种不同来源,并且它们不共享任何特定的形状甚至常见的纵横比,我能做的最好的就是创建所需大小的粗略近似值。为了微调每个特定图像的大小,在使用 Mike Swanson XAML exporter 创建 XAML 文件后,我手动向每个 XAML 文件添加了一个 ScaleTransform 元素。

<Canvas.RenderTransform>
    <ScaleTransform x:Name="GraduationImageSetScaleTransform" 
                 ScaleX=".16" ScaleY=".16" />
</Canvas.RenderTransform>

这个 ScaleTransform 允许我手动调整图像的大小,以匹配它们将被放置的轮廓。确定 ScaleXScaleY 的最终正确值是通过反复试验确定的。

使用 Silverlight Downloader

我应用程序的当前版本包含五个图像集,每个图像集包含五个图像。每个图像有两个版本,一个缩略图版本和一个全尺寸版本。为可用图像集集合添加额外的五个图像,该应用程序总共需要 55 个图像。这些 XAML 文件总共大约为 4 MB。

为了方便将此类文件从服务器传输到客户端浏览器,Silverlight 提供了一个 Downloader 对象,该对象可以从服务器检索 XAML 文件并自动将它们转换为 Silverlight 对象。更好的是,Downloader 能够检索包含多个 XAML 文件的 Zip 文件,并可以无缝地解压缩它们来构建它们包含的相应 Silverlight 对象。

所以,在这种情况下,我将所有 XAML 文件放入一个 Zip 文件中,并将 Downloader 指向该文件。

var downloader = plugIn.CreateObject("downloader");
//Because the Downloader works asynchronously, we need to monitor its Completed Event
downloader.AddEventListener("Completed", handleCompletedImprintImages);
//Parameter 2: Use a path relative to the current HTML file
downloader.Open("GET", "ImprintImages/ImprintImages.zip"); 
downloader.Send();

从这段代码可以看出,Downloader 对象是通过调用 Silverlight 控件的 CreateObject 方法创建的。在附加一个事件处理程序来响应其 Completed 事件后,我们调用 Open() 方法来指定要检索的文件和检索时要使用的协议(HTTP GET,唯一的有效选项)。Zip 文件实际上在调用 Send() 方法之前不会被检索。

为了获得更好的性能,我将 XAML 文件分成两组,并将仅包含圣诞图像集的第一个 Zip 文件,以及包含所有其余 XAML 文件的第二个 Zip 文件。由于圣诞图像是在显示启动屏幕所必需的唯一图像,因此通过首先仅下载这些图像,应用程序可以给人一种加载速度更快的印象。一旦应用程序完全显示给用户,所有剩余的 XAML 文件可以继续在后台下载,并在它们实际被应用程序需要之前准备好。

实例化任何包含在下载的 XAML 文件中的 Silverlight 对象都需要使用 Content 属性的 CreateFromXaml() 方法或 CreateFromXamlDownloader() 方法。两者都属于 Silverlight 控件的 Content 属性。

function handleCompletedImprintImages(sender, eventArgs)
{
    ///Sender = the Downloader object
    ...
    var controlContent = sender.GetHost().Content;
    var newThumbContent = controlContent.CreateFromXamlDownloader(
        sender, "PeaceOnEarthThumb.xaml");
    controlContent.Root.Children.Add(newThumbContent);
    var newThumbImage = controlContent.Root.FindName("PeaceOnEarthThumb");
    newThumbImage.SetValue("Canvas.Top", 766);
    newThumbImage.SetValue("Canvas.Left", 80);
    ...
}

sender 对象,我们可以通过调用 GetHost() 方法来获取对 Silverlight 控件的引用。从那里,我们可以使用属性语法来获取对 Silverlight 控件的 Content 属性的引用。通过此对 Silverlight 控件的 Content 属性的引用,我们现在可以调用 CreateFromXamlDownloader() 方法。我们必须传递给此方法的两个参数是 Downloader 对象本身以及我们想要提取的文件名。

仅仅从 XAML 创建一个对象并不会使其显示。相反,它必须显式地添加到逻辑树的某个位置。这就是 controlContent.Root.Children.Add(newThumbContent); 这行代码的目的。在这种情况下,PeaceOnEarthThumb 图像(2D 图形)被直接添加为根 Canvas 对象的一个子级。

定位 2D 图形

默认情况下,任何对象的位置都是 0,0,这个位置对于我的大多数图像来说都不太合适。为了将这些图像移动到我希望它们显示的位置,有必要首先获取对每个图像的引用。这可以通过调用属于每个 UI 元素的方法 FindName() 来轻松完成。换句话说,如果您在应用程序中的任何 UI 元素(除了少数例外)都有一个引用,那么您就可以获取对任何其他 UI 元素的引用。

var newThumbImage = controlContent.Root.FindName("PeaceOnEarthThumb");

使用此引用,可以设置图像的任何可写属性。

newThumbImage.SetValue("Canvas.Top", 766);
zewThumbImage.SetValue("Canvas.Left", 80);

也可以使用此备用语法设置这些属性。

newThumbImage["Canvas.Top"] = 766;
newThumbImage["Canvas.Left"] = 80;

为这些位置分配的值是根据反复试验确定的。

串联 Downloader

将 XAML 图像放入两个 Zip 文件中的目标是通过在第一个 Zip 文件中仅下载显示初始屏幕所需的图像来提高应用程序的可感知性能。出于同样的原因,最好推迟下载第二个 Zip 文件,直到第一个 Zip 文件检索完成后,这样两个下载就不会在带宽上相互竞争。通过将第二个 Zip 文件的请求放在第一个 Zip 文件下载的事件处理程序的末尾来完成此操作。

sender.AddEventListener("Completed", handleCompletedImprintImages2);
sender.Open("GET", "ImprintImages/ImprintImages2.zip");  
sender.Send();

这段代码使用相同的 downloader 对象,但将其指向不同的 Zip 文件,并使用不同的事件处理程序来处理包含的 XAML 文件。由于我不清楚的原因,此时尝试删除第一个事件处理程序似乎并未按预期工作。因此,我使用了一个简单的标志来限制第一个事件处理程序只运行一次。在请求第二个 Zip 文件后,我的代码将此标志设置为 false,以防止第二次通过第一个事件处理程序。

m_blnFirstPass = false;

为 2D 图形着色

所有 XAML 图形都由 CanvasPath 元素组合而成。虽然每个此类文件在所有情况下都将有一个 Canvas 作为根元素,但大多数也包含一个或多个子 Canvas,有时会向下延伸多层。当然,Canvas 就像一个钉板,Path 挂在上面,它本身没有可见的组成部分。因此,对 Canvas 着色是不必要的(甚至不可能的)。对 Path 着色很简单,只需将其 FillStroke 属性设置为当前选择的颜色即可。然而,由于构成给定图像的并非所有 Path 都直接位于根 Canvas 上,因此有必要编写一个递归方法来导航到每个子 Canvas,以便访问该图像中的每个 Path

function applyNewImprintColor(elementReference, newImprintColor)
{
    for (var i = 0; i < elementReference.Children.Count; i++)
    {
        var element = elementReference.Children.GetItem(i);
        if (element.ToString() == "Canvas")
        {//Recursive call to this method
            applyNewImprintColor(element, newImprintColor); 
        }
        else if (element.ToString() == "Path")
        {
            element.Fill = newImprintColor;
            element.Stroke = newImprintColor;
        }
    }//Now color our three textblocks as well
    refTxbHappyBirthday = elementReference.FindName("txbHappyBirthday");
    refTxbHappyBirthday.Foreground = currentImprintColorPreference;        
    refTxbCongratulations = elementReference.FindName("txbCongratulations");
    refTxbCongratulations.Foreground = currentImprintColorPreference;         
    refTxbBirthdayBoy = elementReference.findName("txbBirthdayBoy");
    refTxbBirthdayBoy.Foreground = currentImprintColorPreference;
}

处理事件

将 Silverlight 对象链接到 JavaScript 事件处理程序

将 Silverlight 对象链接到 JavaScript 事件处理程序可以通过三种不同的方式完成。最简单的方法是在 XAML 代码中使用属性语法将事件处理程序的名称分配给事件属性。

<Path Name="Outline1"  Canvas.Top="754" Canvas.Left="4" Fill="#7799aa" 
    MouseLeftButtonUp="handleMouseUpImprintOutlines" 
    MouseEnter="handleMouseEnterImprints"  
    MouseLeave="handleMouseLeaveImprints" ...></Path>

然而,虽然这种方法对于在.xaml文件中定义的 Silverlight 对象(即,在设计时)有效,但不能用于下载的 XAML 文件中包含的 Silverlight 对象(即,在运行时)。在这种情况下,有必要使用 JavaScript 的 addEventListener() 方法,该方法可用于在运行时连接在设计时省略的事件处理程序。

function handleCompletedImprintImages(sender, eventArgs)
{
    //Sender = the Downloader object
    if(m_blnFirstPass == false)
    {
        return;
    }
    //Retrieve First Imprint Image (Peace on Earth)
    var controlContent = sender.GetHost().Content;
    var newThumbContent = controlContent.CreateFromXamlDownloader(
        sender, "PeaceOnEarthThumb.xaml");
    controlContent.Root.Children.Add(newThumbContent);
    var newThumbImage = controlContent.Root.FindName("PeaceOnEarthThumb");
    newThumbImage.SetValue("Canvas.Top", 766);
    newThumbImage.SetValue("Canvas.Left", 80);
    newThumbImage.addEventListener("MouseLeftButtonUp", handleMouseUpImprints);
    newThumbImage.addEventListener("MouseEnter", handleMouseEnterImprints);
    newThumbImage.addEventListener("MouseLeave", handleMouseLeaveImprints);
    ...
}

Nathan,第 146 页,指出此事件处理程序可以指定为字符串(例如,“handleMouseUpImprints”)、直接引用(例如,handleMouseUpImprints)甚至作为内联函数。

第三种附加事件处理程序的方法使用了一个名为 Silverlight.createDelegate() 的方法,该方法由 Visual Studio 2008 自动添加到Default.html.js文件中。

onLoad: Silverlight.createDelegate(scene, scene.handleLoad)
...
Silverlight.createDelegate = function(instance, method) {
    return function() {
        return method.apply(instance, arguments);

如果您对委托不太清楚,我强烈推荐 Dan Solis 的书《Illustrated C# 2005》(或即将出版的《Illustrated C# 2008》)。然而,您很可能发现坚持使用上面描述的前两种指定事件处理程序的方法要容易得多。

定位全尺寸图像

在我的应用程序中切换全尺寸图像完全通过事件处理程序实现(即,无需任何动画)。当用户单击缩略图图像或上一行中的某个轮廓时,会触发 handleMouseUpImprintOutlines 事件处理程序。此事件处理程序的第一项职责是通过将现有全尺寸图像移出屏幕远处的来实现将其从视图中移除。

function hideCurrentlyDisplayedFullSizeImprint()
{
    var refSilverlightControl = document.getElementById("SilverlightPlugIn");            
    var refRoot = refSilverlightControl.Content.Root;
    var refFullSizeImprint = refRoot.findName(currentlySelectedImprint); 
    refFullSizeImprint.SetValue("Canvas.Top", 3340);
    refFullSizeImprint.SetValue("Canvas.Left", 110);        
}

然后,通过从当前选定的缩略图图像中删除最后五个字符(“thumb”)来推导出全尺寸图像的名称。

var fullSizeImprint = 
  currentlySelectedThumbImage.substring(0, currentlySelectedThumbImage.length-5);

之后,可以通过设置其 Canvas.LeftCanvas.Top 属性使其与所需位置(最初通过反复试验确定)相对应来将新的全尺寸图像移入到位。

function displayFullSizeImprint(newSelectedImprint)
{  
    currentlySelectedImprint = newSelectedImprint;
    var refSilverlightControl = document.getElementById("SilverlightPlugIn");            
    var refRoot = refSilverlightControl.Content.Root;
    refFullSizeImprint = refRoot.findName(newSelectedImprint);  
    //Position the imprint in the main imprint area
    switch (newSelectedImprint)
    {
        //Christmas Image Set - Display Full Size Images
        case "PeaceOnEarth":
        var imprintClickAnimation = refRoot.findName("PeaceOnEarthClickResponse");
        //This animation provides the expansion / contraction
        //        click effect of the thumbnail image
        imprintClickAnimation.begin(); 
        refFullSizeImprint.SetValue("Canvas.Top", 335);
        refFullSizeImprint.SetValue("Canvas.Left", 160);
        applyNewImprintColor(refFullSizeImprint, currentImprintColorPreference);
        break;
            ...
     }
     ...
}

Silverlight 动画

选择要使用的工具

在此应用程序中使用 Silverlight 的主要原因是为了利用 Silverlight 对动画出色的内置支持。总而言之,这个项目使用了超过 100 种不同类型的动画。

关于动画,一个及早需要解决的问题是应该使用哪种工具来创建它们。如果您仅限于使用 Visual Studio 来创建动画,那么过程基本上是完全手动的。另一方面,如果您有 Expression Blend,您可以让工具为您编写动画。

如果您希望设置动画的属性是 double 数据类型,我可以说,只要您有一些先验经验,就可以手动编写您的动画代码。事实上,这正是我在这个项目中为大多数动画所做的。创建一个 Storyboard 对象,为其分配一个名称,并设置任何其他相关属性。然后包含一个或多个 DoubleAnimation 对象,指定持续时间、目标值、目标对象和目标属性。重复,冲洗,重复。

<Storyboard x:Name="PeaceOnEarthClickResponse"  AutoReverse="True">
    <DoubleAnimation Duration="00:00:00.20" To=".35" 
    Storyboard.TargetName="PeaceOnEarthThumbImprintScaleTransform" 
                    Storyboard.TargetProperty="ScaleX" />
    <DoubleAnimation Duration="00:00:00.20" To=".35" 
    Storyboard.TargetName="PeaceOnEarthThumbImprintScaleTransform" 
                    Storyboard.TargetProperty="ScaleY" />
</Storyboard>

我有近 100 种此类动画,只需通过获取相应 Storyboard 的引用然后调用 Storyboard.Begin() 方法即可调用。

var imprintClickAnimation = refRoot.findName("PeaceOnEarthClickResponse");
imprintClickAnimation.begin();

然而,并非所有动画都像简单地更改 ScaleXCanvas.Left 的值那样简单。使用 Expression Blend 可以更有效地创建用于更改线性渐变填充颜色的动画。甚至使用 Expression Blend 也可以更快、更轻松地调整 DoubleAnimations 的值,只需在 Animation Workspace 中单击“播放”按钮,而无需每次都重新启动整个应用程序。因此,除了最简单的动画之外,我认为 Expression Blend 对于构建任何有意义的 Silverlight 应用程序来说确实是必不可少的工具。

构建淡入/淡出动画

我首先处理的动画类型涉及到切换显示在轮廓顶行的缩略图图像。当 Downloader 读取缩略图图像时,它会将第一组(圣诞节)放入顶行的轮廓中。所有剩余的缩略图图像都放在远离屏幕的地方,放在“临时存储区”。

最初,我认为可能足以简单地将所有缩略图图像放置在适当的轮廓中,然后通过更改它们的 Opacity 来淡出一个集合并淡入另一个集合。虽然这从视觉上看效果很好,但我很快就发现,Opacity 设置为 0 的对象仍然会对鼠标事件做出响应。这被证明是非常混乱的,当点击可见图像附近(但不是正好在上面)时,可能会显示一个不同的(隐藏的)图像。

上图仅为模拟,并非此问题的精确表示。在实践中,底层图像将完全不可见,但此模拟旨在展示如何点击可见图像附近可能会触发与幽灵图像相关联的事件处理程序,该幽灵图像的 Opacity 属性设置为 0,但物理上仍然存在于印刷区域轮廓内。

虽然 Opacity 为 0 的元素仍响应鼠标事件,但 Visibility 属性为 Collapsed 的元素则不响应。起初,我认为我会简单地为每个图像的 Visibility 属性制作动画,根据需要将其从 Visible 移至 Collapsed,反之亦然。这种方法被证明是死胡同,因为 Silverlight 不支持 DiscreteObjectKeyFrame 动画,而这是为 Visibility 属性制作动画所必需的。

最终,我得出的结论是,唯一实用的方法是在隐藏图像时将其移出屏幕,然后在需要时将其移回原位。由于我不希望它们在淡入(或淡出)时像幽灵一样滑过屏幕,所以我先用 0.01 秒将新的图像集移到位,然后开始淡入,持续 0.8 秒。对于要离开的图像集,我直到淡出过程完成后才开始移出屏幕。这是通过在移动动画上设置 BeginTime 属性为 0.9 秒来完成的。实际的淡入和淡出设计为同时发生。

<Storyboard x:Name="PeaceOnEarthFadeOut"  AutoReverse="False">
    <DoubleAnimation Duration="00:00:00.80" To="0" 
         Storyboard.TargetName="PeaceOnEarthThumb" 
         Storyboard.TargetProperty="Opacity"  BeginTime="0:0:0.1" />
    <DoubleAnimation Duration="00:00:00.01" To="2000" 
         Storyboard.TargetName="PeaceOnEarthThumb" 
         Storyboard.TargetProperty="(Canvas.Left)" BeginTime="0:0:0.9"/>
</Storyboard>
...
<Storyboard x:Name="HappyBirthdayCakeFadeIn"  AutoReverse="False">
    <DoubleAnimation Duration="00:00:00.1" To="520" 
        Storyboard.TargetName="HappyBirthdayCakeThumb" 
        Storyboard.TargetProperty="(Canvas.Left)" />
    <DoubleAnimation Duration="00:00:00.80" To="1" 
        Storyboard.TargetName="HappyBirthdayCakeThumb" 
        Storyboard.TargetProperty="Opacity"  BeginTime="0:0:0.1" />
</Storyboard>

构建单击响应动画

为了在用户单击轮廓顶行中的某个标准印刷品时产生可见响应,我决定创建一个动画,该动画将在 0.2 秒内将这些印刷品的尺寸增长约 5%,然后重新缩小。通过设置每个图像的 ScaleXScaleY 属性的新值来实现图像尺寸的增加。通过将 StoryboardAutoReverse 属性设置为“True”来实现图像尺寸的恢复。

<Storyboard x:Name="PeaceOnEarthClickResponse"  AutoReverse="True">
    <DoubleAnimation Duration="00:00:00.20" To=".35" 
    Storyboard.TargetName="PeaceOnEarthThumbImprintScaleTransform" 
                    Storyboard.TargetProperty="ScaleX" />
    <DoubleAnimation Duration="00:00:00.20" To=".35" 
    Storyboard.TargetName="PeaceOnEarthThumbImprintScaleTransform" 
                    Storyboard.TargetProperty="ScaleY" />
</Storyboard>

构建左移和右移动画

我为下行轮廓编写的动画比上行轮廓复杂得多。我想为这些图像及其相关的轮廓和标题实现的关键效果是,在单击相应的(三角形)按钮时,将所有轮廓向右或向左移动一个位置。

我的第一步是将名为“LeftShiftImageSets”和“RightShiftImageSets”的事件处理程序分别分配给左按钮和右按钮。尽管我的应用程序目前只有五个图像集,其中四个显示,任何给定时间只有一个隐藏,但我希望使应用程序尽可能灵活,以适应未来添加的图像集。这是通过一个多步骤过程实现的。当触发 LeftShiftImageSets 事件处理程序时,它首先调用另一个名为 AssignTargetPositionValues 的方法,然后该方法会向一组模块级变量添加 98,这些变量由动画用于设置每个图像集图像和轮廓的 Canvas.Left 属性。自然,当 RightShiftImageSets 事件处理程序调用此相同的 AssignTargetPositionValues 方法时,它不会添加 98,而是从这组模块级变量中减去 98。每个轮廓和每个图像都从某个预先分配的位置开始。由于每个轮廓的宽度恰好是 98 像素,因此通过将 Canvas.Left 属性减少 98 来完成向左移动一个位置,通过将 98 添加到 Canvas.Left 属性来完成向右移动一个位置。

function LeftShiftImageSets()
{//Event handler for the Left Button
    ...
    AssignTargetPositionValues("Left");
    ...
}
...
function AssignTargetPositionValues(strDirection)
{
    switch(strDirection)
    {
        case "Left":
            m_intTargetPositionOutline1 -= 98;
            m_intTargetPositionOutline2 -= 98;
            m_intTargetPositionOutline3 -= 98;
            m_intTargetPositionOutline4 -= 98;
            ...
        case "Right":
            m_intTargetPositionOutline1 += 98;
            m_intTargetPositionOutline2 += 98;
            m_intTargetPositionOutline3 += 98;
            m_intTargetPositionOutline4 += 98;
            ...
     }
     ...
}

一旦这些目标值被分配,我们就可以使用这些新值来设置每个水平移位动画的 To 属性。

var refDoubleAnimationOutline1 = refRoot.findName("HorizontalShiftOutline1");
refDoubleAnimationOutline1.To = m_intTargetPositionOutline1;  
var refDoubleAnimationOutline2 = refRoot.findName("HorizontalShiftOutline2");
refDoubleAnimationOutline2.To = m_intTargetPositionOutline2;

一旦所有这些新目标值都被分配,我们就可以调用动画,该动画会平滑地将所有图像及其轮廓和标题移动一个位置到左边或右边。

refHorizontalShiftImageSetAnimation.begin();
...
<Storyboard x:Name="HorizontalShiftImageSetAnimation" 
          AutoReverse="False" FillBehavior="HoldEnd" >
    <DoubleAnimation x:Name="HorizontalShiftOutline1" 
      Duration="00:00:01" To="0" 
      Storyboard.TargetName="ImageSetOutline1" 
      Storyboard.TargetProperty="(Canvas.Left)" />
    <DoubleAnimation x:Name="HorizontalShiftOutline2" 
      Duration="00:00:01" To="0" 
      Storyboard.TargetName="ImageSetOutline2" 
      Storyboard.TargetProperty="(Canvas.Left)" />
</Storyboard>

为鼠标悬停效果制作动画

接着,对图像集轮廓的鼠标悬停动画也带来了许多挑战。我想要的对此悬停的响应是使轮廓和图像膨胀约 10%,并使背景颜色略微变暗。

我遇到的第一个问题是,如果通过简单地增加 ScaleXScaleY 值来扩展轮廓,它不会从中心点均匀扩展,而是只会向右下方扩展。我的解决方案是同时偏移其 Canvas.LeftCanvas.Top 属性。请注意,扩展动画的 FillBehavior 属性设置为 HoldEnd,以便轮廓和关联的图像将保持其扩展状态,直到触发 MouseLeave 事件。

<Storyboard x:Name="BirthdayImageSetExpansion" 
         AutoReverse="False" FillBehavior="HoldEnd" >
    <DoubleAnimation Duration="00:00:00.20" To=".30" 
      Storyboard.TargetName="BirthdayImageSetScaleTransform" 
      Storyboard.TargetProperty="ScaleX" />
    <DoubleAnimation Duration="00:00:00.20" To=".30" 
      Storyboard.TargetName="BirthdayImageSetScaleTransform" 
      Storyboard.TargetProperty="ScaleY" />
    <DoubleAnimation Duration="00:00:00.20" To=".45" 
       Storyboard.TargetName="ImageSetOutline1ScaleTransform" 
       Storyboard.TargetProperty="ScaleX" />
    <DoubleAnimation Duration="00:00:00.20" To=".45" 
      Storyboard.TargetName="ImageSetOutline1ScaleTransform" 
      Storyboard.TargetProperty="ScaleY" />
    <DoubleAnimation Duration="00:00:00.20" To="23" 
      Storyboard.TargetName="ImageSetOutline1" 
      Storyboard.TargetProperty="(Canvas.Top)" />
    <DoubleAnimation x:Name="ExpansionCanvasLeftOutline1" 
      Duration="00:00:00.20" To="31" 
      Storyboard.TargetName="ImageSetOutline1" 
      Storyboard.TargetProperty="(Canvas.Left)" />
    <DoubleAnimation Duration="00:00:00.20" To="30" 
      Storyboard.TargetName="BirthdayImageSet" 
      Storyboard.TargetProperty="(Canvas.Top)" />
    <DoubleAnimation x:Name="ExpansionCanvasLeftBirthdayImageSet" 
      Duration="00:00:00.20" To="40" 
      Storyboard.TargetName="BirthdayImageSet" 
      Storyboard.TargetProperty="(Canvas.Left)" />
</Storyboard>
...
<Storyboard x:Name="BirthdayImageSetContraction" 
        AutoReverse="False" FillBehavior="HoldEnd" >
    <DoubleAnimation Duration="00:00:00.20" To=".28" 
       Storyboard.TargetName="BirthdayImageSetScaleTransform" 
       Storyboard.TargetProperty="ScaleX" />
    <DoubleAnimation Duration="00:00:00.20" To=".28" 
      Storyboard.TargetName="BirthdayImageSetScaleTransform" 
      Storyboard.TargetProperty="ScaleY" />
    <DoubleAnimation Duration="00:00:00.20" To=".42" 
      Storyboard.TargetName="ImageSetOutline1ScaleTransform" 
      Storyboard.TargetProperty="ScaleX" />
    <DoubleAnimation Duration="00:00:00.20" To=".42" 
      Storyboard.TargetName="ImageSetOutline1ScaleTransform" 
      Storyboard.TargetProperty="ScaleY" />
    <DoubleAnimation Duration="00:00:00.20" To="27" 
      Storyboard.TargetName="ImageSetOutline1" 
      Storyboard.TargetProperty="(Canvas.Top)" />
    <DoubleAnimation x:Name="InitialCanvasLeftOutline1" 
      Duration="00:00:00.20" To="34" 
      Storyboard.TargetName="ImageSetOutline1" 
      Storyboard.TargetProperty="(Canvas.Left)" />
    <DoubleAnimation Duration="00:00:00.20" To="35" 
      Storyboard.TargetName="BirthdayImageSet" 
      Storyboard.TargetProperty="(Canvas.Top)" />
    <DoubleAnimation x:Name="InitialCanvasLeftBirthdayImageSet" 
       Duration="00:00:00.20" To="42" 
       Storyboard.TargetName="BirthdayImageSet" 
       Storyboard.TargetProperty="(Canvas.Left)" />
</Storyboard>

不幸的是,上面显示的轮廓和 BirthdayImageSet 在 BirthdayImageSetContraction Storyboard 中的 Canvas.Left 目标值 34 和 42 仅在图像集位于其原始位置时才有效。一旦图像向右或向左移动,正确的新值就是这个原始值加上或减去 98 的倍数。这通过在 AssignTargetPositionValues() 方法中添加或减去原始值来解决,然后重置每个动画的 To 属性来解决。这就是为什么这些 Canvas.Left 属性的 DoubleAnimations 具有 Name 属性(例如,分别是 InitialCanvasLeftOutline1InitialCanvasLeftBirthdayImageSet)。正是通过这个名称,我们才能够在事件处理程序代码中修改 To 属性。

function AssignTargetPositionValues(strDirection)
{
    switch(strDirection)
    {
        case "Left":
            ...
            m_intInitialCanvasLeftOutline1 -= 98;
            m_intExpansionCanvasLeftOutline1 -= 98;
            ...
            m_intInitialCanvasLeftBirthdayImageSet -= 98;
            m_intExpansionCanvasLeftBirthdayImageSet -= 98;
            ...
     }
     ...
}

...
function HorizontalShiftImageSets()
{
    ....
    var refExpansionCanvasLeftOutline1 = 
        refRoot.findName("ExpansionCanvasLeftOutline1");
    refExpansionCanvasLeftOutline1.To = m_intExpansionCanvasLeftOutline1;   
    var refInitialCanvasLeftOutline1 = refRoot.findName("InitialCanvasLeftOutline1");
    refInitialCanvasLeftOutline1.To = m_intInitialCanvasLeftOutline1;   
    var refExpansionCanvasLeftBirthdayImageSet = 
        refRoot.findName("ExpansionCanvasLeftBirthdayImageSet");
    refExpansionCanvasLeftBirthdayImageSet.To = m_intExpansionCanvasLeftBirthdayImageSet;    
    var refInitialCanvasLeftBirthdayImageSet = 
        refRoot.findName("InitialCanvasLeftBirthdayImageSet");
    refInitialCanvasLeftBirthdayImageSet.To = m_intInitialCanvasLeftBirthdayImageSet; 
    ...
}

我的下一个问题涉及 Z 顺序。仅仅增加 ScaleXScaleY 值不会产生预期的结果,如果扩展的一部分被位于上方展开的轮廓所遮挡。解决方法是在 MouseEnter 事件处理程序中增加轮廓及其关联图像的 ZIndex 属性,然后在 MouseLeave 事件处理程序中将其 ZIndex 属性减回其初始值。这些事件处理程序也是实现背景颜色变化的地方。

function handleMouseEnterImageSets(sender, eventArgs)
{//Changes the background color, expands the
// outline and image to give the expansion effect
    var refSilverlightControl = document.getElementById("SilverlightPlugIn2");            
    var refRoot = refSilverlightControl.Content.Root; 
    switch(sender.Name)
    {
        case "ImageSetOutline1":
        case "BirthdayImageSet":
            var refImageSetOutline1 = refRoot.findName("ImageSetOutline1");
            refImageSetOutline1.SetValue("Canvas.ZIndex", 10);
            var refBirthdayImageSet = refRoot.findName("BirthdayImageSet");
            refBirthdayImageSet.SetValue("Canvas.ZIndex", 11);
            var imprintClickAnimation = refRoot.findName("BirthdayImageSetExpansion");
        //Call the animation which increases the ScaleX and ScaleY values (shown above)
            imprintClickAnimation.begin(); 
            refImageSetOutline1.Fill = "#7780aa";  //Darker color 
            break;
            ...
     }
     ...
}
...
function handleMouseLeaveImageSets(sender, eventArgs)
{
    //Returns to original values for mouseovers
    var refSilverlightControl = document.getElementById("SilverlightPlugIn2");            
    var refRoot = refSilverlightControl.Content.Root; 
    switch(sender.Name)
    {
        case "ImageSetOutline1":
        case "BirthdayImageSet":
            var refImageSetOutline1 = refRoot.findName("ImageSetOutline1");
            refImageSetOutline1.SetValue("Canvas.ZIndex", 0);
            var refBirthdayImageSet = refRoot.findName("BirthdayImageSet");
            refBirthdayImageSet.SetValue("Canvas.ZIndex", 1);
            var imprintClickAnimation = refRoot.findName("BirthdayImageSetContraction");
            imprintClickAnimation.begin(); 
            refImageSetOutline1.Fill = "#7799aa";    //Original color
            break;
            ...
     }
     ...
}

为线性渐变变化制作动画

最后,我按钮上鼠标悬停效果的动画需要更改我的线性渐变填充颜色。这是 Expression Blend 对我来说完全不可或缺的一个动画集合。

Blend 为这些动画提供了两个优点。首先,当我使用 Blend UI 创建动画时,它为我编写了动画的 XAML。其次,它允许我通过颜色图表而不是仅仅通过反复试验来猜测 RGB 值来选择目标颜色。

这些动画的最终结果创建了一个 ColorAnimationUsingKeyFrames,该动画在 0.4 秒的时间内将每个渐变停止的颜色从初始颜色更改为新的目标颜色。虽然我使用 Blend 创建了 MouseEnter 动画,但我通过简单地交换开始和结束颜色值手动编写了 MouseLeave 动画。如果您观看 Lee Brimelow 的视频,您可以看到如何使用 Blend 来完成这种交换。我认为这两种技术付出的努力差不多。

//Animation generated by Expression Blend
<Storyboard x:Name="RightButtonMouseover"
         AutoReverse="False" FillBehavior="HoldEnd" >
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" 
     Storyboard.TargetName="RightInnerButton" 
     Storyboard.TargetProperty=
      "(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FF185C84"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FF3C8CBB"/>
    </ColorAnimationUsingKeyFrames>
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" 
      Storyboard.TargetName="RightInnerButton" 
      Storyboard.TargetProperty=
       "(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FF99CBE8"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FFC4DBE8"/>
    </ColorAnimationUsingKeyFrames>
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="RightOuterButton" 
    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FF79B7DA"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FFF5F9FB"/>
    </ColorAnimationUsingKeyFrames>
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="RightOuterButton" 
    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FF276D95"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FF4993BD"/>
    </ColorAnimationUsingKeyFrames>
</Storyboard>
...
//Symmetrical animation manually created by reversing the starting and ending color values
<Storyboard x:Name="RightButtonMouseLeave" 
        AutoReverse="False" FillBehavior="HoldEnd" >
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" 
              Storyboard.TargetName="RightInnerButton" 
              Storyboard.TargetProperty=
                "(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FF3C8CBB"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FF185C84"/>
    </ColorAnimationUsingKeyFrames>
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" 
            Storyboard.TargetName="RightInnerButton" 
            Storyboard.TargetProperty=
              "(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FFC4DBE8"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FF99CBE8"/>
    </ColorAnimationUsingKeyFrames>
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="RightOuterButton" 
    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FFF5F9FB"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FF79B7DA"/>
    </ColorAnimationUsingKeyFrames>
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="RightOuterButton" 
    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FF4993BD"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FF276D95"/>
    </ColorAnimationUsingKeyFrames>
</Storyboard>

当任一方向没有更多屏幕外的图像集时,禁用左侧或右侧按钮以指示单击它不会从该方向带出任何新图像集是适当的。禁用(以及重新启用)这些按钮的动画与上面描述的 MouseEnter(和 MouseLeave)动画在功能上是相同的。

<Storyboard x:Name="DisableRightButton" 
         AutoReverse="False" FillBehavior="HoldEnd" >
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" 
             Storyboard.TargetName="RightInnerButton" 
             Storyboard.TargetProperty=
               "(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FF185C84"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FF616668"/>
    </ColorAnimationUsingKeyFrames>
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="RightInnerButton" 
    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FF99CBE8"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FFD5E2EA"/>
    </ColorAnimationUsingKeyFrames>
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="RightOuterButton" 
    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FF276D95"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FF939595"/>
    </ColorAnimationUsingKeyFrames>
    <ColorAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="RightOuterButton" 
    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
        <SplineColorKeyFrame KeyTime="00:00:00" Value="#FF276D95"/>
        <SplineColorKeyFrame KeyTime="00:00:00.4000000" Value="#FF171D20"/>
    </ColorAnimationUsingKeyFrames>
</Storyboard>

应用自定义字体

Silverlight 1.0 本机仅支持九种字体:Arial、Arial Black、Comic Sans MS、Courier New、Georgia、Lucinda Sans Unicode、Times New Roman、Trebuchet MS 和 Verdana。如果您想使用其他字体,则必须在应用程序中提供它们。(Nathan p.85 指出,即使目标计算机上安装了其他字体,也不会使用它们。)

除了拥有实际的字体文件外,您还需要知道字体的确切家族名称。例如,Comic Sans 字体的家族名称是“Comic Sans MS”,而不仅仅是“Comic Sans”。此信息无法从字体文件的属性中获取,但可以从字体管理程序(如 Bitstream Font Navigator)中获取。

要将字体文件传输到客户端浏览器,请将其包含在 Downloader 检索的 Zip 文件之一中。与 XAML 图像文件不同,Silverlight 控件不需要创建任何字体对象。相反,您希望能够使用这些字体的每个 Silverlight 对象都必须在字体检索后调用 SetFontSource() 方法。在我的例子中,主图像上方的三个 TextBlock 需要使用这些字体,因此在第二个 Zip 文件的事件处理程序中,我对每个 TextBlock 调用了 SetFontSource() 方法。请注意,此方法对于每个对象仅调用一次以使用下载的字体,而不管 Zip 文件中包含多少字体。SetFontSource() 方法的参数是 Downloader 对象。

refTxbHappyBirthday = controlContent.Root.FindName("txbHappyBirthday");
refTxbHappyBirthday.SetFontSource(sender);
refTxbCongratulations = controlContent.Root.FindName("txbCongratulations");
refTxbCongratulations.SetFontSource(sender);
refTxbBirthdayBoy = controlContent.Root.FindName("txbBirthdayBoy");
refTxbBirthdayBoy.SetFontSource(sender);

字体的选择发生在组合框中。因为我想了解如何与常规 HTML 元素和 Silverlight 元素进行交互,所以我选择使用 HTML 组合框而不是尝试 .NET 版本。(由于 1.0 版本不提供 Microsoft 组合框,因此唯一的 Silverlight 组合框来自第三方。)我将 HTML 选择控件的事件处理程序设置为我的 JavaScript 文件中的一个方法,并将选择控件作为参数传递给该方法。

<select id="cboCustomFonts" onchange="setCustomFont(this)"  >
...
</select>

在每个 TextBlock 上设置字体需要三个步骤:

  • 获取对 TextBlock 的引用
  • 确定用户选择了哪种字体
  • TextBlockFontFamily 属性设置为选定的字体系列

TextBlock 当前是否显示并不重要。最简单、最高效的做法是简单地设置所有三个 TextBlockFontFamily 属性,而不是使用一些条件逻辑来确定给定的 TextBlock 当前是否对用户可见。

function setCustomFont(selectBox)
{//Event handler for Font ComboBox    
    var silverlightControl = document.getElementById("SilverlightPlugIn");            
    var refRoot = silverlightControl.Content.Root;  
    refTxbBirthdayBoy = refRoot.findName("txbBirthdayBoy");
    var customFontPreference = selectBox.options[selectBox.selectedIndex].value;
    refTxbBirthdayBoy.FontFamily = customFontPreference;  
    refTxbCongratulations = refRoot.FindName("txbCongratulations");
    refTxbCongratulations.FontFamily = customFontPreference;
    refTxbHappyBirthday = refRoot.FindName("txbHappyBirthday");
    refTxbHappyBirthday.FontFamily = customFontPreference;
}

调整字体大小

每个 TextBlock 都有一个 FontSize 属性,毫不奇怪,该属性决定了字体的尺寸。然而,即使它们的 FontSize 属性相同,两种不同字体的字符的实际物理尺寸也可能存在显著差异,如下面的两个屏幕截图所示。

Comic Sans - FontSize 36

Linus - FontSize 36

为了补偿这些物理字体尺寸的差异,我决定添加一个滑块,并将该滑块链接到我的三个 TextBlockFontSize 属性。既然已经决定使用 HTML 文本框和 HTML 组合框,我也决定使用 HTML 滑块。在实际操作中,这意味着一个用 JavaScript 创建的滑块。所以我通过 Google 搜索了一个免费的滑块,我可以在我的项目中将其插入到适当的位置。

滑块本身几乎不需要修改。我只是按照其开发者提供的说明,将它的 JavaScript 文件引用添加到我的Default.html文件中的列表中。我将滑块的值范围设置为对应于我想在我的应用程序中允许的字体大小范围(36-50)。

为了更改我的 TextBlockFontSize 属性,我创建了一个名为 SetFontSize() 的方法,该方法接受一个代表目标 FontSizedouble 值的单个参数。在该方法内部,我只是获取对每个 TextBlock 的引用,并将其 FontSize 属性设置为传入参数所代表的值。

function SetFontSize(sliderValue)
{
    //Event handler for HTML Slider            
    var silverlightControl = document.getElementById("SilverlightPlugIn");            
    var refRoot = silverlightControl.Content.Root;
    refTxbHappyBirthday = refRoot.FindName("txbHappyBirthday");
    refTxbHappyBirthday.FontSize = sliderValue; 
    refTxbCongratulations = refRoot.FindName("txbCongratulations");
    refTxbCongratulations.FontSize = sliderValue;    
    refTxbBirthdayBoy = refRoot.findName("txbBirthdayBoy");
    refTxbBirthdayBoy.FontSize = sliderValue;
}

当然,这还要求在处理其移动事件的滑块的事件处理程序中调用我的方法。

SetFontSize(v); //Where v represents the current value of the slider

通过拖动重新定位文本

生日图像集和毕业图像集都可以通过允许用户向标准印刷图像添加姓名来定制。为了实现这一点,我稍微缩小了剪贴画图像,将它们大致放在印刷区域的上半部分,并为生日添加了一个写有“Happy Birthday”的 TextBlock,为毕业添加了一个写有“Congratulations”的 TextBlock

如果“Name”TextBlock是固定的,并且其位置是为了处理“平均”长度的名称,那么对于较短和较长的名称,结果都可能非常不平衡。

幸运的是,我为 Silverlight 课程所做的准备包括观看 Silverlight 网站上的各种操作方法视频。我强烈推荐这些视频,但总的来说,只有在您已经熟悉了 Silverlight 的基本知识之后。从这些视频的标题可以看出,其中许多都涉及非常具体的主题。因此,除非您已经有了一个可以附加这些额外知识的结构,否则它们是一种非常零散的学习方法。有些主题相当高级,有些则相当晦涩。因此,它们对初学者来说用处不大。(想象一下,在刚开始学习 C# 时观看关于委托或属性的视频。)

然而,这有几个重要的例外,主要是题为“Getting Started with Silverlight”的视频。这个视频非常值得观看(可能多次),如果您是“初学者”。

有几位演示者非常出色,特别是 Jesse Liberty 和 Shawn Wildemuth。

在我遇到这个对齐问题之前,我已经观看了这些视频,所以我已经知道有一个简单的解决方案。于是我把 Visual Studio 放在我的左显示器上,把Silverlight 视频放在右显示器上。每次演示者(Jesse Liberty)输入几行代码时,我都会暂停视频,然后将等效的代码输入到我的应用程序中。视频完成后,我构建并运行了我的代码。它运行得很顺利。谢谢 Jesse。非常感谢。

var beginX;
var beginY;
var blnTrackingMouseMove = false;

function handleMouseDownTextBlocks(sender, eventArgs)
{
    beginX = eventArgs.getPosition(null).x;
    beginY = eventArgs.getPosition(null).y;
    blnTrackingMouseMove = true;
    sender.captureMouse();
    //Lock the appropriate TextBlock to later retain the user selected position
    //when the auto-centering function would otherwise apply
    if (sender.Name == "txbHappyBirthday")
    {
        m_blnLockTxbHappyBirthday = true;
    }
    else if (sender.Name == "txbBirthdayBoy")
    {
        m_blnLockTxbBirthdayBoy = true;
    }
    else if (sender.Name == "txbCongratulations")
    {
        m_blnLockTxbCongratulations = true;
    }
}        
 
function handleMouseUpTextBlocks(sender, eventArgs)
{
    //Continue to drag until the user releases the mouse
    sender.releaseMouseCapture();
    blnTrackingMouseMove = false;
}        
 
function handleMouseMoveTextBlocks(sender, eventArgs)
{
    if (blnTrackingMouseMove == true)
    {
        var currentX = eventArgs.getPosition(null).x;
        var currentY = eventArgs.getPosition(null).y;
        sender["Canvas.Left"] += currentX - beginX;
        sender["Canvas.Top"] += currentY - beginY;
        beginX = currentX;
        beginY = currentY;
   }
}

这项功能允许用户使用鼠标抓住其中一个 TextBlock 并将其拖动到他偏好的新位置。然而,由于此功能的可用性对于用户来说并不明显,特别是对于经验较少的用户,我将其中每个 TextBlockCursor 属性设置为“Hand”。(感谢 Robert 的这个建议。)

<TextBlock Name="txbHappyBirthday" Text="Happy Birthday"  ... Cursor="Hand" />

您还会注意到,我在其中一个事件处理程序中设置了一个标志,以在我的自动居中代码运行时(如下文所述)尊重此用户选择的位置。当此标志设置为 true 时,自动居中代码将保持用户首选位置不变。

我曾考虑对用户可以拖动文本的位置范围设置一些限制,但最终我决定这些限制既不必要又过于复杂,不易实现。因此,用户可以将任何三个 TextBlock 移动到托管它们的 Silverlight 控件内的任何位置。这确实允许用户尝试其他布局选项。

文本自动居中

通过为我的三个 TextBlock 中的每一个指定“Hand”光标,如果用户将鼠标移到 TextBlock 上,光标将从正常的指针形状变为手形,从而暗示了拖动能力。然而,一个更好的解决方案是自动为用户居中文本。为了实现这一点,我将一个事件处理程序链接到了 Name 文本框的失去焦点事件(onblur)。

<input type="text" id="txtBirthdayName" ... onblur="CenterNameText();"  />

在这个事件处理程序中,我写了一些代码,将名称 TextBlock 居中在上方的 TextBlock(“Happy Birthday”或“Congratulations”)上。此过程所需的步骤包括:

  • 获取对上方 TextBlock 的引用
  • 确定上方 TextBlock 的中心点
  • 获取对名称 TextBlock 的引用
  • 确定名称 TextBlock 的实际宽度
  • 将名称 TextBlockCanvas.Left 属性设置为上方 TextBlock 的中心点减去其 ActualWidth 的一半。
function CenterNameText()
{
    //Triggered by LostFocus (onblur) of txtBirthdayBoy
    var intCenterPointX;
    var silverlightControl = document.getElementById("SilverlightPlugIn");            
    var refRoot = silverlightControl.Content.Root;  
    //Determine which of Birthday or Graduation is the currently selected Image Set
    switch(currentlySelectedImprintImageSet)
    {//Get the CenterPointX of "Happy Birthday" or "Congratulations"
        case "Birthday Party":
            refTxbHappyBirthday = refRoot.findName("txbHappyBirthday");
            intCenterPointX = refTxbHappyBirthday["Canvas.Left"] 
            + (refTxbHappyBirthday.ActualWidth/2);
            break;
        case "Graduation":
            refTxbCongratulations = refRoot.findName("txbCongratulations");
            intCenterPointX = refTxbCongratulations["Canvas.Left"] 
            + (refTxbCongratulations.ActualWidth/2);
            break;
    }            
    //Get the ActualWidth of "BirthdayBoy" (doubles as the graduate)
    refTxbBirthdayBoy = refRoot.findName("txbBirthdayBoy");
    var dblActualWidth = refTxbBirthdayBoy.ActualWidth;
    //Set the Canvas.Left of BirthdayBoy to CenterPointX - 1/2 ActualWidth
    refTxbBirthdayBoy["Canvas.Left"] =  intCenterPointX - (dblActualWidth/2);
    m_blnLockTxbBirthdayBoy = true;
}

有了这个事件处理程序,每当用户选项卡或单击离开名称 TextBox 时,名称 TextBlock 就会自动居中在上方的 TextBlock(“Happy Birthday”或“Congratulations”)上。

另一个文本对齐问题涉及在不同的生日或毕业图像之间切换。如果显示的图像是第一个此类图像,则有必要将相应的 TextBlock 从“存储”移到主图像前面。然而,如果用户已经输入了姓名并且手动重新定位了它或自动居中了它,那么不应该将其重新定位到默认位置。为了防止这种不希望的结果,我为三个 TextBlock 中的每一个都添加了一个模块级变量(例如,m_blnLockTxbBirthdayBoy)。这些变量最初设置为 false,但如果出于任何原因将 TextBlock 移离其默认位置,则设置为 true。相反,当用户切换图像集时,这些变量会被重置为 false

在将 TextBlock 重新定位到其默认位置之前,会检查此变量以确定是否需要重新定位。

if(m_blnLockTxbHappyBirthday == false)
{
    var refTxbHappyBirthday = refRoot.findName("txbHappyBirthday"); 
    refTxbHappyBirthday.SetValue("Canvas.Top", 440);
    refTxbHappyBirthday.SetValue("Canvas.Left", 100);
}
if(m_blnLockTxbBirthdayBoy == false)
{
    var refTxbBirthdayBoy = refRoot.findName("txbBirthdayBoy"); 
    refTxbBirthdayBoy.SetValue("Canvas.Top", 490);
    refTxbBirthdayBoy.SetValue("Canvas.Left", 130);
}

创建工具提示和说明

在创建工具提示时,我在微软网站上发现了一篇非常有用的文章,标题是“运行时构造对象”。这篇文章解释了如何在 JavaScript 事件处理程序中放置一些预先构建好的 XAML。然后,当这个事件处理程序运行时,XAML 片段会被 Silverlight 控件实例化,并添加到预先存在的 XAML 内容中,无论指定的在哪一个位置。

起初,我遵循了微软文章中的示例,并将工具提示相对于用户鼠标悬停的位置(悬停位置)显示在屏幕上。然而,这样做会导致工具提示可以在指定范围内的任何位置弹出,并且当鼠标依次移过一个图像、包含该图像的轮廓、下一个轮廓、下一个图像等时,工具提示会跳动。这使得工具提示的定位看起来很杂乱,它们随着鼠标移动在屏幕上跳来跳去。

后来我决定,考虑到我的工具提示所提供的所有消息,大多数用户应该已经知道,或者可以猜到,或者至少可以很快学会,所以最好让工具提示的位置不那么显眼。因此,最终我选择将所有工具提示消息放置在相同的位置,如下图所示。

function  handleMouseEnterImprintColors(sender, EventArgs)
{
    //Creates a tooltip for instructions
    if (toolTipImprintColors == null)
    {
        var xamlFragment = '<canvas width=""235"" height=""25"" />';
        xamlFragment += '<rectangle width=""235"" height=""25"" ' + 
          'fill=""#FFFFE1"" stroke=""Black"" ' + 
          'radiusx=""5"" radiusy=""5"" />';
        xamlFragment += '<textblock text=""Click" canvas.left=""10"" ' + 
          'canvas.top=""3"" fontsize=""13"" />';
        xamlFragment += '<</textblock /></rectangle /></canvas />';            
        toolTipImprintColors = sender.GetHost().content.CreateFromXaml(xamlFragment, false);
        //var cursorPosition = EventArgs.getPosition(sender);
        toolTipImprintColors["Canvas.Left"] = 750;
        toolTipImprintColors["Canvas.Top"] = 920;
        toolTipImprintColors["Canvas.ZIndex"] = 10; //unnecessary
        var mainCanvas = sender.FindName("mainCanvas");
        mainCanvas.children.add(toolTipImprintColors);
    } 
    ...
}

创建动画介绍

构建介绍

这是一个令人sad但又简单的事实:我亲密的朋友和家人圈子之外的 99% 的人都不熟悉 Drink Mate。我无法告诉你我母亲有多沮丧,她从未遇到过任何见过或听说过 Drink Mate 的人——即使在我们已经售出超过 300 万个之后。

因此,必须假设,查看此应用程序的用户中,有很高比例的人一开始会想:“我看到的是什么鬼东西?”为了弥补 Drink Mates 经验的不足,我决定创建一个动画介绍来传达以下信息:

  • Drink Mates 是一种小塑料夹子,可以让你单手拿住盘子和杯子。
  • 大多数 Drink Mates 都附带自定义广告信息。
  • 除了自定义印花,我们还可以为大多数基本节日提供一些标准印花。

对于第一条信息,我选择了一张我朋友使用 Drink Mate 拿盘子和杯子的照片。

对于第二条信息,我选择了一张带有自定义印花的 Drink Mate 的代表性照片(在这种情况下是亚利桑那州凤凰城的 Phoenician Hotel)。

最后一条信息,我选择了一个带有标准印花(Joy)的 Drink Mate 的特写。

首先,我创建了三个额外的 Image 控件来放置这三张额外的图片。然后,我将它们定位,第一个显示在顶部,其余的依次排列,最后一个显示在底部。在 XAML 中,每个元素都会显示在所有先前创建的、占据相同位置的元素之上。我曾考虑只使用两个 Image 控件,并在每次显示后重置 Source 属性来显示其他图像,但未能使该方法令人满意。

图像之间的过渡是通过在半秒内将 Opacity 属性从 1 变为 0 来实现的。同时,下一张图像的 Opacity 在相同的过渡期间从 0 变为 1。虽然这效果相当不错,但实际上,这种 Opacity 变化会导致轻微的闪烁。由于 WPF 中相同的动画类型不会闪烁,我怀疑这是 Silverlight 插件的一个 bug。这是我对这个应用程序不太满意的地方之一。

相比之下,任何 2D 图形的淡入淡出效果都是完全没有闪烁的。最后一步,我引入了一个消息,解释该应用程序可用于预览 Drink Mate 标准印花集合。为了说明这一点,我通过移动 Canvas.LeftCanvas.Top 属性,将其中一个全尺寸印花(Joy)放置在 Drink Mate 背景上。

<Storyboard x:Name="DrinkMateIntro1">
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
        Storyboard.TargetName="Alternate1MainImageViewer" 
        Storyboard.TargetProperty="(UIElement.Opacity)">
        <SplineDoubleKeyFrame KeyTime="00:00:00" Value="1"/><!--  Cecilia -->
        <SplineDoubleKeyFrame KeyTime="00:00:08" Value="1"/>
        <SplineDoubleKeyFrame KeyTime="00:00:08.5" Value="0"/>
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
      Storyboard.TargetName="Alternate2MainImageViewer" 
        Storyboard.TargetProperty="(UIElement.Opacity)">
        <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/><!--  Phoenician -->
        <SplineDoubleKeyFrame KeyTime="00:00:08" Value="0"/>
        <SplineDoubleKeyFrame KeyTime="00:00:08.5" Value="1"/>
        <SplineDoubleKeyFrame KeyTime="00:00:14" Value="1"/>
        <SplineDoubleKeyFrame KeyTime="00:00:14.5" Value="0"/>
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
        Storyboard.TargetName="Alternate3MainImageViewer" 
        Storyboard.TargetProperty="(UIElement.Opacity)">
        <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/><!--  Joy -->
        <SplineDoubleKeyFrame KeyTime="00:00:14" Value="0"/>
        <SplineDoubleKeyFrame KeyTime="00:00:14.5" Value="1"/>
        <SplineDoubleKeyFrame KeyTime="00:00:20" Value="1"/>
        <SplineDoubleKeyFrame KeyTime="00:00:20.5" Value="0"/>
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
        Storyboard.TargetName="MainImageViewer" 
        Storyboard.TargetProperty="(UIElement.Opacity)">
        <SplineDoubleKeyFrame KeyTime="00:00:00" 
           Value="0"/><!--  White Background 800 -->
        <SplineDoubleKeyFrame KeyTime="00:00:20" Value="0"/>
        <SplineDoubleKeyFrame KeyTime="00:00:20.5" Value="1"/>
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
        Storyboard.TargetName="txbStandardImprints" 
        Storyboard.TargetProperty="(UIElement.Opacity)">
        <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
        <SplineDoubleKeyFrame KeyTime="00:00:20" Value="0"/>
        <SplineDoubleKeyFrame KeyTime="00:00:20.5" Value="1"/>
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
        Storyboard.TargetName="txbApplicationInstructions" 
        Storyboard.TargetProperty="(UIElement.Opacity)">
        <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
        <SplineDoubleKeyFrame KeyTime="00:00:20" Value="0"/>
        <SplineDoubleKeyFrame KeyTime="00:00:20.5" Value="1"/>
        <SplineDoubleKeyFrame KeyTime="00:00:25" Value="1"/>
        <SplineDoubleKeyFrame KeyTime="00:00:26" Value="0"/>
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
           Storyboard.TargetName="Joy" 
           Storyboard.TargetProperty="(Canvas.Left)">
        <SplineDoubleKeyFrame KeyTime="00:00:24" Value="3000"/>
        <SplineDoubleKeyFrame KeyTime="00:00:25" Value="135"/>
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
           Storyboard.TargetName="Joy" 
           Storyboard.TargetProperty="(Canvas.Top)">
        <SplineDoubleKeyFrame KeyTime="00:00:24" Value="3000"/>
        <SplineDoubleKeyFrame KeyTime="00:00:25" Value="340"/>
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
      Storyboard.TargetName="Joy" 
      Storyboard.TargetProperty="(UIElement.Opacity)">
        <SplineDoubleKeyFrame KeyTime="00:00:23" Value="0"/>
        <SplineDoubleKeyFrame KeyTime="00:00:26" Value="0"/>
        <SplineDoubleKeyFrame KeyTime="00:00:29.5" Value="1"/>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

完成这些动画说明后,我在测试代码时发现,如果用户点击缩略图印花之一,全尺寸印花就会显示出来,即使介绍还没有完成。这可能是一个严重的问题,因为当显示新的全尺寸印花时,它会成为 currentlySelectedImprint。(请记住,当点击每个新的缩略图图像时,第一步是将“currentlySelectedImprint”移开,以便为下一个全尺寸印花腾出空间。)在这种情况下,当介绍完成并将 Joy 印花放置在 Drink Mate 背景上时,它不会是“currentlySelectedImprint”,因此会一直保留在那里,直到用户随后从缩略图列表中选择它。在此期间,将显示两个独立的图像——这将使应用程序明显出错。

我为这个问题提供的解决方案是创建一个名为 m_blnLockImprintImages 的模块级变量。该变量最初为 true,直到介绍结束才设置为 false。但是,在调用 Storyboard.Begin() 之后立即将此变量设置为 true 是无效的,因为介绍是异步运行的,而且在动画完成之前很长一段时间锁就会被释放(从而什么也做不成)。正确的做法是向 Storyboard 添加一个事件处理程序,用于 StoryboardCompleted 事件,然后在该事件处理程序中将锁设置为 false

refDrinkMateIntro1.addEventListener("Completed", onCompletedDrinkMateIntro1);

... 

function onCompletedDrinkMateIntro1()
{
    m_blnLockImprintImages = false;
}

允许用户跳过介绍

对于回访用户或仅仅是没耐心的人,我添加了一个功能,允许用户跳过介绍,立即开始预览 Drink Mate 标准印花。这是通过添加一个文本块(TextBlock)来实现的,该文本块显示“跳过介绍”,并将其链接到一个事件处理程序,该处理程序:

  • 通过调用 [Storyboard].Stop() 停止动画。
  • 将每个 AlternateMainImageViewerOpacity 属性设置为 0。
  • 将“Standard Imprints”TextBlockOpacity 设置为 1。
  • 将 Joy 全尺寸印花放置在白色 Drink Mate 背景图像上。
  • 移除缩略图印花的锁定,以便它们可以响应鼠标点击。
function handleMouseUpSkipIntroduction(sender, eventArgs)
{
    //Used to terminate the introduction if a user chooses
    if(sender.Opacity == 1)
    {
        var refDrinkMateIntro1 = sender.FindName("DrinkMateIntro1");
        refDrinkMateIntro1.Stop();     
        var refAlternate1MainImageViewer = 
            sender.FindName("Alternate1MainImageViewer");
        refAlternate1MainImageViewer.Opacity = "0";     
        var refAlternate2MainImageViewer = 
            sender.FindName("Alternate2MainImageViewer");
        refAlternate2MainImageViewer.Opacity = "0";     
        var refAlternate3MainImageViewer = 
            sender.FindName("Alternate3MainImageViewer");
        refAlternate3MainImageViewer.Opacity = "0"; 
        var refMainImageViewer = sender.FindName("MainImageViewer");
        refMainImageViewer.Opacity = "1"; 
        var reftxbStandardImprints = 
            sender.FindName("txbStandardImprints");
        reftxbStandardImprints.Opacity = "1";      
        var reftxbApplicationInstructions = 
            sender.FindName("txbApplicationInstructions");
        reftxbApplicationInstructions.Opacity = "0";  
        var refJoy = sender.FindName("Joy");
        refJoy["Canvas.Left"] = "135";
        refJoy["Canvas.Top"]  = "340";
        refJoy.Opacity = "1";
        sender.Opacity = "0"
        m_blnLockImprintImages = false;
    }
}

清理代码

由于 XAML 或 JavaScript 代码中缺少 Region,良好的代码组织受到了严重阻碍。(**请联系您当地的微软代表,恳求他们敦促 WPF 和 Silverlight 团队添加 Region。**)

当 Visual Studio 创建 Scene.xaml 文件时,它会在文件中放置一个单独的按钮,为用户提供一些示例代码,说明如何创建自己的自定义按钮。它还在 Scene.xaml.js 中放置了一些示例事件处理代码。最初,我保留了这些代码(尽管我将按钮的 Opacity 设置为 0,以免碍事),认为我将来可能会用到它们。然而,在项目结束时,这些代码仍然未使用,所以我只是删除了它们。

当然,任何代码清理都不可能不添加注释来帮助你记住你在代码中做出的决定以及不同元素的预期用途。

建议

我最显著的观察是,对于我需要执行的几乎所有任务,我都能找到一些资源来准确地解释如何执行该任务。在许多情况下,提供的示例代码只需稍作修改即可实现所需的结果。例如:

  • 如何创建拖放功能以允许用户重新定位 TextBlock:Silverlight 网站上的 Jesse Liberty 视频。
  • 如何从矩形创建按钮:Lynda.com 上的 Lee Brimelow 培训视频。
  • 如何在 HTML 页面中包含 Silverlight 内容:Nathan 和 Moroney 的书籍,Silverlight 网站上的 Wildemuth 视频。
  • 如何使用 Silverlight 下载器:Nathan 的书。
  • 如何将 2D 图形文件转换为 XAML:我的博客(www.WPFLearningExperience.com)。
  • 如何创建工具提示:微软网站上的文章。

基于我在这个项目中的经验,这里是我给你的建议:

  • 在开始项目之前,尽可能多地做好准备。如果可能的话,阅读 Adam Nathan 的整本书。
  • 学习如何使用 Expression Blend。观看 Lynda.com 上 Lee Brimelow 的整个系列。
  • 务必彻底理解项目中各种文件与微软提供的预置代码之间的关系。
  • 在着手自己的项目之前,尝试一些动手练习或循序渐进的课程。

VS 2008 竞赛投稿

本文已提交给 Visual Studio 2008 竞赛。如果您认为本文值得考虑参加竞赛,请花一点时间对其进行评分。如果您一直阅读到这里,我认为您可以理解,这个项目和文章花费了几周的全职努力。感谢您的考虑。

我非常欢迎您的评论,尽管正如您从下面的我的简介和上面的几张照片中可以看到的,我的回应能力经常受到国外旅行的阻碍。

© . All rights reserved.