从Silverlight从1.0版本开始,就提供了InkPresenter控件。很多人在第一次了解到这个控件时非常兴奋,纷纷打算做“手写识别”“网络共享白板”等应用程序。可惜Silverlight的InkPresenter只是WPF中InkPresenter的阉割版,想要扩展它不是说不可以,而是一件相当伤脑筋的麻烦事——至少,到目前为止,我都没有见到过真正的Silverlight网络共享白板。
博客园的webabcd的在4个月前的一篇文章的评论中,也说到了打算做Silverlight 网络白板的问题。
而WXWinter(冬)在3个月前用Silverlight+WCF完成了一个“近似”的“网络共享白板”。(围观连接:http://www.cnblogs.com/foundation/archive/2008/12/02/1345506.html)
为什么我说他是“近似”的呢?
这得先从InkPresenter的原理说起:用户用鼠标画在InkPresenter上的笔迹,都被保存为
StrokeCollection类型的inkPresenter.Strokes里。顾名思义,StrokeCollection是Stroke的集合。 Stroke 可以通俗地理解成“一笔”。而这“一笔”是“一条线”,它是包由很多“点”构成的, Stroke 把关键的点(有转折的点)保存在 Stroke. StylusPoints里, StylusPoint 则是具体点每个点。
可惜的是,StylusPoint里除了X和Y坐标外,几乎没有提供任何可供编程的接口和方法,连Visible这种属性都没有提供。Stroke稍好一些,但也提供得不多。
WXWinter(冬)的“网络共享白板”是以Stroke为单位的。当用户画完“一整笔”后,Silverlight程序将描述这”一整笔”的Stroke通过WCF发送到服务器,同时通过Timer定时取得服务器上最新版本的所有Stroke。
用过基于socket的“网络白板”的人都知道,WXWinter(冬)的方法只是一个近似的方法。第一,它没有真正的“实时”,这个问题不大,就算是不直接使用socket,Shareach也示范了使用WCF的解决方式;第二,它的数据是以“线”为单位的,在实际使用时,对方看不到你画线的过程,只是会突然发现自己的屏幕上出现一条别人画的线,这是一件比较囧的事。
说了这么多,终于进入正题了:我最近一直在思考以上提到的第二个问题,如何直播或回放用户画线的过程,而不是让那些笔迹一整条一整条地跳出来呢?我认为,首先要把“点”从“线”中分离出来,对“点”编程而不是操作“线”;其次记录用户画每个点的准确时间;第三,使用动画。本文展示一个回放用户在InkPresenter上涂鸦过程的Demo。
效果图:
现场Demo (需要安装Silverlight 3.0控件,在这里安装:http://download.microsoft.com/download/0/D/7/0D76C405-E0E5-43CC-89D3-18243A4FCA86/Silverlight.3.0_Developer.exe )
【使用说明】
1.等待数据加载完,
2.点击“开始录制”,
3.音乐响起,你可以随便涂写.
4.画完后点击“停止录制”.
5.点击”回放预览”查看你的杰作。
(如果你看不到下面的Silverlight对象,可以到这里查看:http://azuredrive.blob.core.windows.net/netdrive1/file_98a1cf05-94e2-4918-a6ef-29e791c8e327.html)
实现步骤草图:
1.InkPresenter的XAML代码及基本操作
MouseLeftButtonDown='onInkPresenterDown' MouseMove='onInkPresenterMove' MouseLeftButtonUp='onInkPresenterUp'>
<InkPresenter.Resources>
<Storyboard Duration="0:0:0" Completed="onStrokePlaybackTimerTick" x:Name="strokePlaybackTimer" />
</InkPresenter.Resources>
<MediaElement Name='mediaElement' Source="http://azuredrive.blob.core.windows.net/netdrive1/file_c6184705-b9f7-49e9-a2e9-1e76a01a4565.wmv"Width='720' Height='480'
AutoPlay='False' MediaEnded="onMediaEnded"/>
</InkPresenter>
InkPresenter的基本操作请参考webabcd的这篇文章。
2.用视频(或音频)的时间轴来作为涂鸦事件的时间轴,记录每一笔的开始时间
仔细看看上文的InkPresenter的XAML代码。它的Resources里是动画信息,它的内容仅仅是一个WMV媒体文件。我们之前提到了要保存每一个笔画的时间,就可以直接使用这个媒体文件的时间轴。
具体操作是这样的:
{
//捕获鼠标
inkPresenter.CaptureMouse();
newStroke = new Stroke();
//设置该笔画的属性。本Demo中全部使用默认属性。
newStroke.DrawingAttributes = defaultDrawingAttributes;
//记录该笔的第一个点的信息
newStroke.StylusPoints.Add(e.StylusDevice.GetStylusPoints(inkPresenter));
inkPresenter.Strokes.Add(newStroke);
//记录该笔第一个点画下的时间
strokeStartTimes.Add(mediaElement.Position.Seconds);
}
3.考虑到可能与服务器或其他网络用户的交互,我们用单一的string保存“点”的信息,
用int数组保存时间信息
string inkStringForPlayback = null ;
//用于保存每笔的开始时间
List<int> strokeStartTimes = new List<int>();
同时提供string与Strokes互相转换的两个函数:
{
string serializedStylusPoints = "";
if (strokes != null)
{
int strokeCount = strokes.Count;
for ( int i = 0 ; i < strokeCount; i++)
{
Stroke stroke = strokes[i];
int packetCount = stroke.StylusPoints.Count;
for ( int j = 0 ; j < packetCount; j++)
{
StylusPoint stylusPoint = stroke.StylusPoints[j];
serializedStylusPoints += stylusPoint.X.ToString();
serializedStylusPoints += " ,";
serializedStylusPoints += stylusPoint.Y.ToString();
if (j != packetCount - 1)
{
serializedStylusPoints += " ,";
}
else
{
serializedStylusPoints += ";";
}
}
}
}
return serializedStylusPoints;
}
private StrokeCollection ConvertStringToInk(string serializedStylusPoints)
{
StrokeCollection strokes = new StrokeCollection();
string [] strokeStrings = serializedStylusPoints.Split( ' ;');
for (var i = 0 ; i < strokeStrings.Length - 1 ; i++)
{
Stroke stroke = new Stroke();
stroke.DrawingAttributes = defaultDrawingAttributes;
string [] stylusPoints = strokeStrings[i].Split( ' ,');
for (var j = 0 ; j < stylusPoints.Length / 2 ; j++)
{
StylusPoint stylusPoint = new StylusPoint();
stylusPoint.X = double .Parse(stylusPoints[ 2 * j]);
stylusPoint.Y = double .Parse(stylusPoints[ 2 * j + 1]);
stroke.StylusPoints.Add(stylusPoint);
}
strokes.Add(stroke);
}
return strokes;
}
4.根据时间轴,动态画出每一笔、每个点。
{
if (strokesForPlayback.Count == 0) return;
Stroke currentStroke = strokesForPlayback[playbackStrokeIndex];
if (playbackPointIndex == 0)
{
if (mediaElement.Position.Seconds < strokeStartTimes[playbackStrokeIndex])
{
strokePlaybackTimer.Begin();
return;
}
strokeToPlayback = new Stroke();
inkPresenter.Strokes.Add(strokeToPlayback);
strokeToPlayback.DrawingAttributes = currentStroke.DrawingAttributes;
}
strokeToPlayback.StylusPoints.Add(currentStroke.StylusPoints[playbackPointIndex]);
playbackPointIndex++;
if (playbackPointIndex < currentStroke.StylusPoints.Count)
{
strokeToPlayback.StylusPoints.Add(currentStroke.StylusPoints[playbackPointIndex]);
playbackPointIndex++;
}
if (playbackPointIndex == currentStroke.StylusPoints.Count)
{
playbackPointIndex = 0;
playbackStrokeIndex++;
if (playbackStrokeIndex == strokesForPlayback.Count)
{
return;
}
}
strokePlaybackTimer.Begin();
}