虚拟实验室引擎的开发和实现(七、UIItem)
和UIPlace类似,UIItem继承自UIObject,为虚拟实验室中物品(VItem)提供一个可渲染的对象。其继承关系如下:
配置文件
前文说过,在虚拟实验室的引擎中,我使用两类XML文件来传递数据,这两个XML文件分别对应“灵魂”(V开头)和“外表”(UI开头)。关于VItem的属性文件,为以下形式:
<person name='周慊' centerX='73' centerY='144' x='1100' y='780' file='engine1/michael.xml' id='80d76e6b-08d6-4225-bdcc-bf68ca19ca58' actionid='default' />
而UIItem所用的配置文件,为以下形式:
<?xml version="1.0" encoding="utf-8"?> <item image="girl.xml.png" width="147" height="144"> <action name="facerightdown" fps="10" next="facerightdown"> <rectangle x="1176" y="0" /> <rectangle x="1323" y="0" /> <rectangle x="1470" y="0" /> <rectangle x="1617" y="0" /> <rectangle x="1764" y="0" /> <rectangle x="1911" y="0" /> <rectangle x="2058" y="0" /> <rectangle x="2205" y="0" /> </actions> <action name="faceright" fps="10" next="faceright"> <rectangle x="0" y="0" /> <rectangle x="147" y="0" /> <rectangle x="294" y="0" /> <rectangle x="441" y="0" /> <rectangle x="588" y="0" /> <rectangle x="735" y="0" /> <rectangle x="882" y="0" /> <rectangle x="1029" y="0" /> </actions> </item>
这两个配置文件和VObject、UIObject的关系如下图所示:
素材
和UIPlace所用的分块素材不同,我用一整张图片作为UIItem的素材,该图片的相对地址在UIItem的配置文件制定:
item image="girl.xml.png"。(注意,在设计中,该地址是相对于配置文件的)
上图中,红色实线框住的部分表示一个Action所需用到的图片,而绿色实线框住的部分表示一个Action中的一帧所用的图片。
动画
Silverlight和Flex不同,Flex可以使用SWFLoader控件调用Flash制作的逐帧动画,并和SWF中的ActionScript交互,而Silverlight不具备类似的能力,因此只能通过编程的方式来实线同样的效果,Blogcn上有一篇文章介绍如何在Silverlight中制作逐帧动画,这里借用其思想。
导演、剧本、演员、舞台
让我们把UIItem想象成一幕包括导演、剧本、演员和舞台的舞台剧,看看这幕系该如何演出:
导演在舞台剧上的作用为告诉演员在表演的时候该如何执行剧本。在UIItem里我使用一个Storyboard对象和两个DoubleAnimationUsingKeyFrames对象来模拟导演:
protected Storyboard ActionBoard { get; set; } protected DoubleAnimationUsingKeyFrames ActionXAnimation { get; set; } protected DoubleAnimationUsingKeyFrames ActionYAnimation { get; set; }
剧本是告诉演员在表演的时候究竟是执行哪种动作,UIItem中的剧本,就是VItem属性的Action集合(this.VItem.Actions):
public VItem VItem { get { return VObject as VItem; } set { VObject = value; } }
演员是动作的表演者,在UIItem中对应一个Image对象:
protected Image Image { get; set; }
舞台:演员演出的场所。在UIItem中,舞台相当于UIItem的Clip属性(该属性来源于Canvas,Canvas的Clip属性类似Flash中的遮罩层,只有在该属性区域下的内容,才会被显示出来):
public RectangleGeometry RectangleClip { get; set; }
初始化
在UIItem的构造函数里,初始化上面提及的对象:
public UIItem(VItem source) { //添加演员 Image = new Image(); this.Children.Add(Image); Canvas.SetLeft(Image, 0); Canvas.SetTop(Image, 0); //添加舞台 RectangleClip = new RectangleGeometry(); this.Clip = RectangleClip; //添加导演 ActionBoard = new Storyboard(); ActionBoard.Completed += new EventHandler(OnActionCompleted); ActionXAnimation = new DoubleAnimationUsingKeyFrames(); Storyboard.SetTarget(ActionXAnimation, this.Image); Storyboard.SetTargetProperty(ActionXAnimation, new PropertyPath("(Canvas.Left)")); ActionBoard.Children.Add(ActionXAnimation); ActionYAnimation = new DoubleAnimationUsingKeyFrames(); Storyboard.SetTarget(ActionYAnimation, this.Image); Storyboard.SetTargetProperty(ActionYAnimation, new PropertyPath("(Canvas.Top)")); ActionBoard.Children.Add(ActionYAnimation); //添加剧本载体 VItem = source; }
读取剧本
剧本有UIItem的配置文件制定,通过覆盖OnConfigureFileDownload方法来读取剧本
protected override void OnConfigureFileDownload(object sender, DownloadStringCompletedEventArgs e) { if (String.IsNullOrEmpty(e.Result)) return; //开始读取剧本 XElement data = XElement.Parse(e.Result); this.VItem.Actions.Clear(); this.VItem.ActionNames.Clear(); //VItem的大小 this.VItem.Width = data.Attribute<Int32>("width", 0, XElementExtensions.Int32Parser); this.VItem.Height = data.Attribute<Int32>("height", 0, XElementExtensions.Int32Parser); if (data.Elements("action") != null) { //从配置文件中读取每一个Action foreach (var xAction in data.Elements("action")) { //读取一个Action中的每一个Rectangle帧 var rects = xAction.Elements("rectangle").Select(x => new VRectangle() { X = x.Attribute<Int32>("x", 0, XElementExtensions.Int32Parser), Y = x.Attribute<Int32>("y", 0, XElementExtensions.Int32Parser), Width = this.VItem.Width, Height = this.VItem.Height }).ToArray(); //读取该Action的名字,一个Action可以有多个名字,以逗号分割 var names = xAction.Attribute("name").Value.Split(','); //设置Action,包括名字、下一动作、帧率,并添加到Actions集合 foreach (var actionName in names) { VAction action = new VAction( actionName, data.Attribute("next", "default"), data.Attribute<Int32>("fps", 10, XElementExtensions.Int32Parser), rects ); this.VItem.Actions.Add(action.Name, action); this.VItem.ActionNames.Add(action.Name); } } } //读取演员 LoadImage(data.Attribute("image").Value); }
读取演员
演员及素材,是一张PNG图片(也可以是JPG格式或其他Silverlight支持的图片格式)。素材的地址为配置文件的image属性,我在设计的时候,把该地址相对于配置文件(而不是XAP文件,这点和UIPlace不同)。因此,在地址转换的时候,先要通过XAP文件的地址得到配置文件的地址,然后再通过得到的配置文件的地址得到素材的地址(有点乱)。
重载以下方法:
public static Uri GetLocalURI(String path1, String path2) { if (path2.ToLower().StartsWith("http://")) { return new Uri(path2); } else { return new Uri(GetLocalURI(path1), path2); } }
添加UIItem的LoadImage方法:
protected virtual void LoadImage(String path) { if (!String.IsNullOrEmpty(path)) { BitmapImage bm = new BitmapImage(); bm.DownloadProgress += new EventHandler<DownloadProgressEventArgs>(OnImageDownload); Image.Source = bm; bm.UriSource = URIToolkit.GetLocalURI(VItem.ConfigFile, path); } }
最后,处理一下回调函数,当素材装载完毕后,开始有VItem的ActionID制定的动作,为了方便子类覆盖,我把这个函数设置成虚(Virtual)的:
protected virtual void OnImageDownload(object sender, DownloadProgressEventArgs e) { if (e.Progress == 100) { DoAction(VItem.ActionID); } }
设置舞台大小
为了在每个时刻只显示一帧图片,需要设置Clip属性的大小,把Clip设置成和VItem一样大,由于RectangleClip不是DependcyObject类型的,因此不能用绑定的形式,而需要通过覆盖UIItem的OnSourcePropertyChanged方法来达到和绑定类似的效果:
protected override void OnSourcePropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnSourcePropertyChanged(sender, e); if (e.PropertyName == "ActionID") { DoAction(VItem.ActionID); } else if (e.PropertyName == "Width" || e.PropertyName == "Height") { RectangleClip.Rect = new Rect(0, 0, VItem.Width, VItem.Height); } }
执行指定Action
在代码中,可以通过设置VItem的ActionID属性来修改当前UIItem的Action,通过DoAction函数实现:
protected virtual void DoAction(string actionID) { //如果VItem的Action列表中有名为actionID的Action if (VItem.Actions.ContainsKey(actionID)) { VAction vAction = VItem.Actions[actionID]; //导演说:停止上一个动作,各单位准备 ActionBoard.Stop(); ActionXAnimation.KeyFrames.Clear(); ActionYAnimation.KeyFrames.Clear(); double i = 0; //计算Action一帧所需的时间 double t = 1.0 / vAction.FPS; //开始工作,在关键帧中添加每一个Rectangle foreach (var rect in vAction.Rectangle) { ActionXAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(i)), Value = 0 - rect.X }); ActionYAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(i)), Value = 0 - rect.Y }); i += t; } //用一个全局属性保存当前Action CurrentAction = vAction; //导演说,开始! ActionBoard.Begin(); } }