最近做SonySource项目时实现了几个很小的Silverlight程序,分别是Clock、HomePeoplePicker和ManageMentPeoplePicker。实际上这三个silverlight程序都非常简单,主要特点有以下几个方面:
1. Silverlight程序和页面上的HTML元素混合在一起,且在特定事件触发后要动态改变Silverlight程序在页面中占的位置及大小,但给用户的感觉是无缝连接;
2. Javascript和Silverlight相互调用;
3. 简单的探照灯遮照效果;
下面就分别对我认为比较不好处理的地方或者一些我费了很多周折才实现的地方做一简要说明:
一、使Silverlight浮动在Html元素中,并动态改变大小
或许我这个小标题描述得还不是很准确,不能直观表达我的意思。举个例子,假设我们要用Silverlight做个下拉菜单,并将他放在html页面上使用。我们希望这个silverlight菜单所占的大小只是980px宽和30px高,因为在紧挨菜单的上面和下面的地方我们要放置一起其他的html元素。但当用户点击某个菜单项时,这个子菜单就展开,假设子菜单的大小是100px款和200px高,那就要求Siverlight所占的位置至少高为230px。由于Silverlight菜单只有30px高,所以下拉菜单就被截断而不能完整显示。我做的这个项目里三个Silverlight都遇到类似问题。例如PeoplePicker是在一个表格框里显示很多人的图像,当用户点击一个人的图像的时候弹出一个窗口以显示人的详细信息,在某种情况下,这个弹出窗口会超出包含所有人物图像的表格从而部分被截断。在《Silverlight嵌入到HTML之windowless属性及运用AjaxControlToolKit时出现虚线边框的问题》所描述的问题就是基于这种需求。
上述问题是否可以简单的描述为:Silverlight程序在页面上只在指定的Silverlight plug in(<object/>元素)中显示,当超过Silverlight Plug in时就会被截除;当Silverlight程序的宽和高在运行时不确定时,就要求Silverlight Plug in的大小和位置随之改变以使所有silverlight内容都能完整正确的显示出来。
我在这个项目里的解决办法就是基于以上的描述,动态改变Silverlight plug in(object元素)的大小,并时silverlight plug in以绝对定位的方式浮动于其他元素之上,且让silverlight plug in的背景色为透明以不至于让他遮盖所有的底层元素。
首先,我们在页面上定义一个<DIV>元素,我们的silverlight程序就放在这个<DIV>里,并以它作为silverlight的定位基准。即正常情况下silverlight和包含它的<div>的位置和大小完全一致。当需要改变silverlight的大小和位置时,也以该<DIV>为参考。在页面布局时,我们只用关注这个<DIV>应该放到哪就行了。HTML代码大致如下:
<div id="silverlightHomePeoplePickerHost" style="width:275px;height:324px;background-color:transparent;float:left"> <object data="data:application/x-silverlight-2," type="application/x-silverlight-2" style="width:100%;height:100%;position:absolute"> <param name="source" value="../ClientBin/SEL.SonySource.Silverlight.HomePeoplePicker.xap"/> <param name="onerror" value="onSilverlightError" /> <param name="onload" value="onSilverlightHomePeoplePickerLoaded" /> <param name="background" value="transparent" /> <param name="windowless" value="true" /> <param name="minRuntimeVersion" value="2.0.31005.0" /> <param name="autoUpgrade" value="true" /> <a href="http://go.microsoft.com/fwlink/?LinkID=124807" mce_href="http://go.microsoft.com/fwlink/?LinkID=124807" style="text-decoration: none;" mce_style="text-decoration: none;"> <img src="http://go.microsoft.com/fwlink/?LinkId=108181" mce_src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style: none" mce_style="border-style: none"/> </a> </object> <iframe style='visibility:hidden;height:0;width:0;border:0px'></iframe> </div>
那么怎么初始化silverlight的位置和大小呢?就在onload事件里处理:
var silverlightHomePeoplePickerInstance = null; // When loaded the silverlight HomePeoplePicke in html page function onSilverlightHomePeoplePickerLoaded(sender, args) { silverlightHomePeoplePickerInstance = new SilverlightHomePeoplePicker(sender); }
在onSilverlightHomePeoplePickerLoaded事件中,首先通过参数sender创建一个SilverlightHomePeoplePicker的实例,SilverlightHomePeoplePicker对象的代码大致如下:
// the class of SilverlightHomePeoplePicker function SilverlightHomePeoplePicker(sender) { this.slApp = sender; this.objElement = this.slApp.getHost(); this.divContainer = this.objElement.parentNode; this.page = this.objElement.Content.Page; // the left and top offset to the directly parent element of silverlight object tag this.leftOffsetToContainer = 0; this.topOffsetToContainer = 0; this.page.addEventListener("SilverlightScopeChanged", SilverlightHomePeoplePicker.createDelegate(this, this.handleSilverlightScopeChangedEvent)); addEvent(window, "onresize", SilverlightHomePeoplePicker.createDelegate(this, this.setPosition)); with (this.objElement.style) { position = "absolute"; zIndex = 999; width = this.divContainer.offsetWidth + "px"; height = this.divContainer.offsetHeight + "px"; } this.setPosition(); } SilverlightHomePeoplePicker.prototype = { // change scope of silverlight object tag handleSilverlightScopeChangedEvent: function(s, e) { this.leftOffsetToContainer = e.Left; this.topOffsetToContainer = e.Top; this.setPosition(); with (this.objElement.style) { width = e.Width + "px"; height = e.Height + "px"; } }, // set left and top positions setPosition: function() { setSilverlightObjectPosition(this.objElement, this.divContainer, this.leftOffsetToContainer, this.topOffsetToContainer); } } SilverlightHomePeoplePicker.createDelegate = function(instance, method) { return function() { return method.apply(instance, arguments); } }
可以看到,在构造SilverlightHomePeoplePicker的实例时就设置了<object/>元素的位置和大小。
leftOffsetToContainer和topOffsetToContainer是指silverlight plug in ( object 元素 )左上相对于包含它的<DIV>的左上角的偏移量,一般正常情况下,这个偏移量为0,即silverlight与包含它的<div>左上角位置重叠。初始化时同时设置了silveright插件的宽和高,即等于包含它的<DIV>的宽和高。
那么当Silverlight的位置和大小需要改变时怎么办呢?谁来负责处理这个变化呢?首先,在Silverlight程序里应该最先知道这个Silverlight程序的范围是否改变了,是否需要改变silverlight plug in的位置和大小来正确显示整个Silverlight的内容。例如,当弹出或关闭详细信息窗口时,Silverlight程序应该做这个检查,如果需要改变silverlight plugin的位置和大小,就通知javascript程序。
Silverlight的启动xmal文件Page.xaml大概是这个样子:
<UserControl x:Class="SEL.SonySource.Silverlight.HomePeoplePicker.Page" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="275" Height="324"> <Grid x:Name="LayoutRoot" RenderTransformOrigin="0,0"> <!-- 主要内容。。。。。。 --> <!-- To move the positon of the grid to show Detail Window if the detail overstep left side or top side --> <Grid.RenderTransform> <TranslateTransform x:Name="LayoutRootTranslate" X="0" Y="0" /> </Grid.RenderTransform> </Grid> </UserControl>
然后定义一个SilverlightScopeChangeHandler:
/// <summary> /// When the scope of the app changed, call the handler to invoke js to resize the position of the plug-in object /// Most time, this will be triggered by popup or close the detail window /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public delegate void SilverlightScopeChangedHandler(Object sender, ScopeEventArgs e); /// <summary> /// Indicate the scope of the silverlight app /// </summary> public class ScopeEventArgs : EventArgs { /// <summary> /// Relative to the orginal left position, it will always less than 0 /// </summary> [ScriptableMember] public double Left { get; internal set; } /// <summary> /// Relative to the orginal top position, it will always less than 0 /// </summary> [ScriptableMember] public double Top { get; internal set; } /// <summary> /// The actual width of the app /// </summary> [ScriptableMember] public double Width { get; internal set; } /// <summary> /// The height width of the app /// </summary> [ScriptableMember] public double Height { get; internal set; } public ScopeEventArgs(double left, double top, double width, double height) : base() { this.Left = left; this.Top = top; this.Width = width; this.Height = height; } public static bool operator ==(ScopeEventArgs e1, ScopeEventArgs e2) { return Object.Equals(e1, e2); } public static bool operator !=(ScopeEventArgs e1, ScopeEventArgs e2) { return !Object.Equals(e1, e2); } public override bool Equals(object obj) { if (obj == null) return false; if (GetType() != obj.GetType()) return false; ScopeEventArgs e = (ScopeEventArgs)obj; return (this.Left==e.Left && this.Top == e.Top && this.Width == e.Width && this.Height == e.Height ); } }
在Page.cs页面中,当捕获到需要改变silverlight plugin(object元素)的位置和大小时,就触发相应的SilverlightScopeChange事件:
// scope changed event [ScriptableMember] public event SilverlightScopeChangedHandler SilverlightScopeChanged; // save current silverlight scope private ScopeEventArgs silverlightScope = null; /// <summary> /// detect if the size of silverlight app is larged than orgianl size or if it's return to the origianl size /// This will invoke the js to reposition the silverlight plug-in on the html page /// </summary> private void DetectSilverlightScopeChange() { ........ ScopeEventArgs se = new ScopeEventArgs(left, top, width - left, height - top ); if (silverlightScope != se) { silverlightScope = se; LayoutRootTranslate.X = -silverlightScope.Left; LayoutRootTranslate.Y = -silverlightScope.Top; if (SilverlightScopeChanged != null) SilverlightScopeChanged(this, silverlightScope); } }
当Silverlight应用程序检查到需要改变<object>元素的大小时,就触发SilverlightScopeChanged事件,告知javascript来处理,同时SilverlightScopeChangeEventArgs参数还告诉了silverlight plug in需要的宽、高以及相对于上级<div>的偏移量。
到这里,大家就会看到上述SilverlightHomePeoplePicker代码中
this.page.addEventListener("SilverlightScopeChanged", SilverlightHomePeoplePicker.createDelegate(this, this.handleSilverlightScopeChangedEvent));
的意义所在了。
二、Silverlight程序和Javascript程序的相互调用
1. javascript注册silverlight的事件
其实上面的代码已经体现了这点,即通过javascript的代码
this.page.addEventListener("SilverlightScopeChanged", SilverlightHomePeoplePicker.createDelegate(this, this.handleSilverlightScopeChangedEvent));
来注册silverlight端的事件。这里要注意几点:
a. Silverlight事件必须是Scriptable的,即事件的声明上加上[ScriptableMember];
b. 事件原型必须有两个参数, sender和e。sender是object类型的,e需要时继承自EventArgs类型的。我在这里走了些弯路。
c. 必须在Silverlight程序中注册整个类以供javascript访问,例如:HtmlPage.RegisterScriptableObject("Page", this);
d. silverlight程序在LayoutUpdated事件中是无法访问或触发任何javascript方法的。
2. silverlight注册html element的事件
在silverlight中可以注册html元素的事件。例如在clock程序中,要求当用户点击网页上任何位置时都要关闭timezonelist下拉列表。这时可以在silverligh中注册document.click事件:
HtmlPage.Document.AttachEvent("onclick", Document_Click); ... private void Document_Click(object sender, HtmlEventArgs e) { if (ListBoxMaskLayer.Visibility == Visibility.Visible) HideTimezoneList(); }
但后来由于firefox下点击silverlight时也触发document.click事件,所以没有采用这种方式。
3. Javascript调用Silverlight的方法9
由于没有采用第2点所示的方法,所以document.click事件还是由javascript来处理: 但是,在handleClickDocumentEvent事件里,javascript调用了silverlight的方法:
this.page.HideTimezoneList();
与第1条所述类似,silverlight中HideTimezoneList这个方法必须标记为[ScriptableMember],且要注册Page类到js:
HtmlPage.RegisterScriptableObject("Page", this);
// the class of SilverlightClock function SilverlightClock(slClock) { ............... this.page = this.objElement.Content.Page addEvent(document, "onclick", SilverlightClock.createDelegate(this, this.handleClickDocumentEvent)); ................. } SilverlightClock.prototype = { // when click on the document, shrink the timezone listbox handleClickDocumentEvent: function(e) { e = window.event || e; var srcE = e.srcElement || e.target; if (!srcE.source || srcE.source.indexOf("SEL.SonySource.Silverlight.Clock.xap") == -1) { if (this.objElement.style.zIndex > 0) { try{ //BUG: there will be an error in FF this.page.HideTimezoneList(); } catch(err){} } } } }
三、简单探照灯效果的实现
这里的探照灯的实现,主要运用了RadialGradientBrush。在整个Silverlight的内容上面放一个Rectangle,且将其Fill为RadialGradientBrush:
<Grid x:Name="LayoutRoot" RenderTransformOrigin="0,0" MouseMove="LayoutRoot_MouseMove" MouseEnter="LayoutRoot_MouseEnter"> <!-- the uniformGrid contain all peoples --> <local:UniformGrid x:Name="PicturesContainerGrid" Width="275" Height="324" Columns="5" Rows="4" MinWidth="55" MinHeight="81" /> <!-- the lamp light masker --> <Canvas x:Name="LamplightCanvas" Width="275" Height="324" MouseLeftButtonDown="LamplightCanvas_MouseLeftButtonDown" Cursor="Hand"> <Rectangle x:Name="LamplightRect" Width="700" Height="644" Stretch="Fill" RenderTransformOrigin="0.5,0.5" Canvas.Left="-208" Canvas.Top="-199" > <Rectangle.Fill> <RadialGradientBrush x:Name="LamplightGradient" RadiusX="0.5" RadiusY="0.666667" Center="0,0" GradientOrigin="0,0"> <GradientStop Color="#00000000" Offset="0.08"/> <GradientStop Color="#7F000000" Offset="0.143"/> <GradientStop Color="#7F000000" Offset="1"/> <GradientStop Color="#00FFFFFF" Offset="0"/> </RadialGradientBrush> </Rectangle.Fill> </Rectangle> <Canvas.Clip> <GeometryGroup x:Name="LamplightClipGroup"> <RectangleGeometry Rect="0,0,275,324" /> <RectangleGeometry Rect="0,0,0,0" x:Name="MouseOverRectGemo" /> </GeometryGroup> </Canvas.Clip> </Canvas> </Grid>
当鼠标移动时,就动态改变RadialGradientBrush的Center和GradientOrigin属性。大致代码如下:
private void LayoutRoot_MouseMove(object sender, MouseEventArgs e) { HanleMouseEventOnLayoutRoot(e); } private void HanleMouseEventOnLayoutRoot(MouseEventArgs e) { // if the mouse move in the Main Grid if (IsCaptureMouse(PicturesContainerGrid, e)) { // if no people has been selected ,move the lamplight if (dicPersonDetails.Count == 0) { // move lamplight SetLamplight(e); // open a small rect to enable triger the mouseover and click of HomePerson control Point ptMouse = e.GetPosition(LamplightCanvas); MouseOverRectGemo.Rect = new Rect(ptMouse.X - 2, ptMouse.Y - 2, 4, 4); } else { // Don't move lamplight because one HomePerson has alreay get the lamplight, // but a big rect should open to trigger MouseOver of HomePerson Control HomePerson homePerson = GetHomePerson(e); if (homePerson != null) { // check if click on any detail window foreach (KeyValuePair<HomePerson, HomeDetails> kv in dicPersonDetails) { if (kv.Value.IsOpened && IsCaptureMouse(kv.Value, e)) { MouseOverRectGemo.Rect = new Rect(-1, -1, 0, 0); return; } } RemoveLamplightMask(homePerson); } } } } private void SetLamplight(Point pointRelativeToLamplightRec) { Point ptCenter = new Point(pointRelativeToLamplightRec.X / LamplightRect.ActualWidth, pointRelativeToLamplightRec.Y / LamplightRect.ActualHeight); LamplightGradient.Center = LamplightGradient.GradientOrigin = ptCenter; } /// <summary> /// set the focus lamplight to the mouse position /// </summary> /// <param name="pointRelativeToLamplightRec"></param> private void SetLamplight(MouseEventArgs e) { Point pointRelativeToLamplightRec = e.GetPosition(LamplightRect); SetLamplight(pointRelativeToLamplightRec); }
另外还有一个问题,由于在所有内容上遮照了一个Rectangle,那么当鼠标移到某个位置时,虽然灯亮了,那怎么触发Rectangle下层元素的事件呢?我这里主要运用了包含Rectangle的Canvas的Clip属性,在Clip里我定义了两个Geometry,动态改变Geometry的位置,就相当于在Rectangle上动态打一些窟窿。
四、其他问题
当然还有一些需要改进的地方,例如Clock的点"edit"按钮后弹出的下拉框的宽和高应该动态自适应,可以在TimezoneList的Curstom Control的MeasureOverride事件中获取应该分配的宽和高,进而计算其他数据达到自适应的效果,简单伪代码如下:
public event RequestMeasuredSizeHandler RequestMeasuredSize; protected override Size MeasureOverride(Size availableSize) { Size desireSize = base.MeasureOverride(new Size(double.MaxValue,double.MaxValue)); if (RequestMeasuredSize != null) RequestMeasuredSize(desireSize); this.Width = desireSize.Width; this.Height = desireSize.Height; return desireSize; }
还有,在探照灯已经固定在某一个人的头像上后,当鼠标滑动时还可以使效果更加柔和。
五、程序效果演示
HomePeoplePicker
ManagementPeoplePicker
Clock