请升级到MicrosoftEdge以使用最新的功能、安全更新和技术支持。
“Mango”(芒果)是WindowsPhoneSDK7.1版本的内部代码名称,当然也是一种美味的热带水果的名字。芒果的用途多种多样—例如,用在馅饼、色拉和各种鸡尾酒中。据说芒果对于人体健康还有诸多好处,还有十分有趣的文化史。在本文中,我将讨论Mangolicious,这是一个关于芒果的WindowsPhoneSDK7.1应用程序。该应用程序提供各种芒果食谱、鸡尾酒和小知识,不过我们真正的目的是探讨7.1版本中的主要新增功能,具体而言就是:
图1Mangolicious全景主页
应季集锦部分中的菜单和项都是通往应用程序其他页的链接。大多数页都是简单明了的Silverlight页,其中一页是XNA集成游戏专用的页。构建这个应用程序从头到尾需要完成的任务,总结如下:
对于这个应用程序,我将在VisualStudio中使用WindowsPhoneSilverlightandXNAApplication模板。这样会生成一个包含三个项目的解决方案;重新命名之后,这些项目汇总如图2所示。
图2:WindowsPhoneSilverlightandXNA解决方案中的项目
WindowsPhoneSDK7.1版本引入了对本地数据库的支持。也就是说,应用程序可以将数据存储在手机的本地数据库文件(SDF)中。建议在代码中创建数据库,要么作为应用程序本身的组成部分创建,要么通过单纯为创建数据库而构建的独立帮助应用程序创建。如果只在应用程序运行时才创建大多数或所有数据,则最好在应用程序内创建数据库。至于Mangolicious应用程序,我只有静态数据,可以预先填充数据库。
为此,我将单独创建用于创建数据库的帮助应用程序,首先从简单的WindowsPhoneApplication模板开始。要在代码中创建数据库,我需要一个从DataContext派生的类(在System.Data.Linq程序集的自定义Phone版本中定义)。这个DataContext类既可以在创建数据库的帮助应用程序中使用,也可以在使用数据库的主应用程序中使用。在该帮助应用程序中,必须将数据库位置指定在独立存储中,因为这是唯一可以从手机应用程序写入数据的位置。该类还包含每个数据库表的一组表字段:
publicclassMangoDataContext:DataContext{publicMangoDataContext():base("DataSource=isostore:/Mangolicious.sdf"){}publicTable
图3:定义表类
[Table]publicclassRecipe{privateintid;[Column(IsPrimaryKey=true,IsDbGenerated=true,DbType="INTNOTNULLIdentity",CanBeNull=false,AutoSync=AutoSync.OnInsert)]publicintID{get{returnid;}set{if(id!=value){id=value;}}}privatestringname;[Column(DbType="NVARCHAR(32)")]publicstringName{get{returnname;}set{if(name!=value){name=value;}}}...additionalcolumndefinitionsomittedforbrevity}另外,在帮助应用程序中—使用标准Model-View-ViewModel(MVVM)方法—我现在需要一个ViewModel类,以便使用DataContext类连接视图(UI)与模型(数据)。ViewModel有一个DataContext字段以及一组表数据集合(Recipes、Facts和Cocktails)。这些是静态数据,因此在这里使用List
图4:为ViewModel中的表数据定义集合属性
publicclassMainViewModel{privateMangoDataContextmangoDb;privateList
图5:创建数据库
"C:\ProgramFiles\MicrosoftSDKs\WindowsPhone\v7.1\Tools\IsolatedStorageExplorerTool\ISETool"tsxd{e0e7e3d7-c24b-498e-b88d-d7c2d4077a3b}C:\Temp\IsoDump此命令假定该工具安装在标准位置。参数说明请参见图6。
图6:IsolatedStorageExplorer命令行参数
将SDF文件提取到桌面后,就不需要使用帮助应用程序了,我可以将精力转到将使用此数据库的Mangolicious应用程序。
在Mangolicious应用程序中,将SDF文件添加到项目中,将上面的自定义DataContext类添加到解决方案中,并进行稍许更改。在Mangolicious中,我不需要写入数据库,可从应用程序安装文件夹直接使用数据库。因此,连接字符串略微不同于帮助应用程序中的连接字符串。此外,Mangolicious在代码中定义SeasonalHighlights表。数据库中没有对应的SeasonalHighlight表。此代码表从两个基础数据库表(Recipes和Cocktails)中取出数据,用于填充应季集锦全景项。这两处更改是数据库创建帮助应用程序与Mangolicious数据库使用应用程序中的DataContext类之间仅有的不同:
publicclassMangoDataContext:DataContext{publicMangoDataContext():base("DataSource=appdata:/Mangolicious.sdf;FileMode=readonly;"){}publicTable
图7:启动时加载数据
publicvoidLoadData(){mangoDb=newMangoDataContext();if(!mangoDb.DatabaseExists()){mangoDb.CreateDatabase();}varseasonalRecipes=fromrinmangoDb.Recipeswherer.Season==seasonselectnew{r.ID,r.Name,r.Photo};varseasonalCocktails=fromcinmangoDb.Cocktailswherec.Season==seasonselectnew{c.ID,c.Name,c.Photo};seasonalHighlights=newList
主页包含一个全景,其中有三个全景项。第一项是一个列表框,用于提供应用程序主菜单。如果用户选择其中一个列表框项,则会导航至相应页(即Recipes、Facts和Cocktails的集合页或Game页)。我确保在导航前将相应数据加载到Recipes、Facts或Cocktails集合中:
switch(CategoryList.SelectedIndex){case0:App.ViewModel.LoadRecipes();NavigationService.Navigate(newUri("/RecipesPage.xaml",UriKind.Relative));break;...additionalcasesomittedforbrevity}如果用户在UI中选择应季集锦列表中的某一项,则检查所选项是菜谱还是鸡尾酒,然后导航至菜谱或鸡尾酒页,传入项ID作为导航查询字符串的一部分,如图8所示。
图8:在应季集锦列表中选择
SeasonalHighlightselectedItem=(SeasonalHighlight)SeasonalList.SelectedItem;StringnavigationString=String.Empty;if(selectedItem.SourceTable=="Recipes"){App.ViewModel.LoadRecipes();navigationString=String.Format("/RecipePage.xamlID={0}",selectedItem.ID);}elseif(selectedItem.SourceTable=="Cocktails"){App.ViewModel.LoadCocktails();navigationString=String.Format("/CocktailPage.xamlID={0}",selectedItem.ID);}NavigationService.Navigate(newSystem.Uri(navigationString,UriKind.Relative));用户可以从主页菜单导航至三个列表页中的一个。以下页数据都与ViewModel中的相应集合进行了数据绑定以显示项列表:Recipes、Facts或Cocktails。这些页都提供一个简单的列表框,列表框中的每一项都包含一个Image控件(显示照片)和一个TextBlock(显示项名称)。例如,图9显示的是FactsPage。
图9:FunFacts,集合列表页之一
所有这些页的代码隐藏都很简单。在OnNavigatedTo重写中,我从查询字符串中提取各个项ID,在ViewModel集合中找到相应项,然后进行数据绑定。RecipePage的代码略复杂于其他代码—此页中的附加代码都与页右上角的HyperlinkButton有关。请参见图10。
图10:带图钉按钮的菜谱页
如果用户单击各菜谱页中的“图钉”超链接按钮,会将相应项固定为手机起始页上的辅助图标。固定操作将用户导航至起始页并停用该应用程序。如果以这种方式固定辅助图标,该图标会周期性展示动画效果,不断翻转显示正面和背面,如图11和图12所示。
图11:固定的菜谱辅助图标(正面)
图12:固定的菜谱辅助图标(背面)
之后,如果用户单击这个固定辅助图标,则会直接导航至应用程序内的相应项。到达该页时,“图钉”按钮将显示“解除固定”图像。如果用户解除固定该页,该页则会从起始页中移除,应用程序继续运行。
下面是工作原理。在RecipePage的OnNavigatedTo重写中,在完成用于确定对哪个特定Recipe进行数据绑定的标准操作之后,我构造一个稍后可用作此页URI的字符串:
thisPageUri=String.Format("/RecipePage.xamlID={0}",recipeID);在“图钉”按钮的按钮单击处理程序中,首先检查此页是否已有辅助图标,如果没有,则立即创建。我使用当前Recipe数据创建该辅助图标:图像和名称。我还设置一张静态图像(以及静态文字),供辅助图标背面使用。同时,我利用机会使用“解除固定”图像重新绘制按钮自身。另一方面,如果辅助图标存在,因为用户已选择解除固定辅助图标,则必须在单击处理程序中,在这里,删除辅助图标并使用“图钉”图像重新绘制按钮,如图13所示。
图13:固定和解除固定页
最初,我创建这个应用程序是想实现一个WindowsPhoneSilverlightandXNAApplication解决方案。这样就有了三个项目。我将主要的MangoApp构建成非游戏功能项目。GameLibrary项目充当SilverlightMangoApp与XNAGameContent之间的“桥梁”。MangoApp项目引用该项目,该项目自己则引用GameContent项目。这需要进一步的工作。要将游戏集成到手机应用程序中,需要完成两项主要任务:
在我的改写内容中,有一个由代码中的Player类表示的鸡尾酒调酒器,它瞄准来袭的芒果(敌人)发射炮弹。如果击中芒果,芒果就裂开变成果桨。每次击中芒果都能得到100分。鸡尾酒调酒器每次与芒果相碰,玩家场强都会消弱10。当场强变为零,游戏结束。用户也可以随时按手机的后退键正常终止游戏。图14显示真正运行的游戏。
图14:正在运行的XNA游戏
我不需要对(几乎是空的)GamePage.xaml进行任何更改。所有工作都在代码隐藏中完成。VisualStudio为GamePage类生成起始代码,如图15所示。
图15:GamePage起始代码
首先,必须将Shooter游戏项目文件复制到现有的MangoApp项目中。另外,将ShooterContent内容文件复制到现有的GameContent项目中。图16汇总了Shooter游戏项目中的现有类。
图16:Shooter游戏类
为了将这款游戏集成到手机应用程序中,我需要对GamePage类进行以下更改:
传统的XNA游戏会创建新的GraphicsDeviceManager,而在手机应用程序中已有公开GraphicsDevice属性的SharedGraphicsDeviceManager,这些对我来说已经足够。为了简单起见,我将对GraphicsDevice的引用缓存为GamePage类中的字段。
在标准的XNA游戏中,Update和Draw方法是Microsoft.Xna.Framework.Game基类中虚方法的重写。但是,在集成Silverlight/XNA应用程序中,GamePage类不是派生于XNAGame类,因此,必须从Update和Draw提取代码并插入OnUpdate和OnDraw事件处理程序。请注意,某些游戏对象类(例如Animation、Enemy和Player)、Update和Draw方法,以及Update所调用的某些帮助程序方法都使用GameTime参数。这是在Microsoft.Xna.Framework.Game.dll中定义的,通常,如果Silverlight应用程序包含对该程序集的引用,则会被视为错误。GameTime参数可由传入OnUpdate和OnDraw计时器事件处理程序的GameTimerEventArgs对象所公开的两个Timespan属性(TotalTime和ElapsedTime)完全替代。除GameTime外,我还可以原样移植Draw代码。
原始Update方法测试GamePad状态,并有条件地调用Game.Exit。集成Silverlight/XNA应用程序中不使用此方法,因此不能移植到新方法中:
//if(GamePad.GetState(PlayerIndex.One).Buttons.Back==ButtonState.Pressed)//{////this.Exit();//}现在,新的Update方法差不多是调用其他方法以更新各种游戏对象的工具。即使游戏结束,我也更新视差背景,不过只更新玩家、敌人、撞击、炮弹和爆炸(如果玩家依然存活)。这些帮助程序方法计算各种游戏对象的数目和位置。因为不使用GameTime,所以可以全部原样移植这些方法,但有一个例外:
privatevoidOnUpdate(objectsender,GameTimerEventArgse){backgroundLayer1.Update();backgroundLayer2.Update();if(isPlayerAlive){UpdatePlayer(e.TotalTime,e.ElapsedTime);UpdateEnemies(e.TotalTime,e.ElapsedTime);UpdateCollision();UpdateProjectiles();UpdateExplosions(e.TotalTime,e.ElapsedTime);}}UpdatePlayer方法需要进行细微更改。在游戏的原始版本中,玩家状况在降至0后会重置为100,也就是说游戏可以永久循环。在我改写的游戏中,如果玩家状况降至0,某个标志会设置为False。我在OnUpdate和OnDraw方法中测试该标志。在OnUpdate中,标志值确定是否继续计算对象的更改;在OnDraw中,该值确定是绘制对象还是绘制游戏结束屏幕及最终得分:
privatevoidUpdatePlayer(TimeSpantotalTime,TimeSpanelapsedTime){...unchangedcodeomittedforbrevity.if(player.Health<=0){//player.Health=100;//score=0;gameOverSound.Play();isPlayerAlive=false;}}总结在本文中,我们讨论了如何利用WindowsPhoneSDK7.1中的几个新增功能开发应用程序:本地数据库、LINQtoSQL、辅助图标和深层链接,以及Silverlight/XNA集成。7.1版本还提供很多其他新增功能,以及对现有功能的增强。有关进一步详细信息,请参见以下链接:
AndrewWhitechapel*拥有20多年开发工作经验,目前担任WindowsPhone团队的项目经理,负责应用程序平台的核心部分。*
衷心感谢以下技术专家对本文的审阅:NickGravelyn、BrianHudson和HimadriSarkar