使用 Silverlight 2 创建可尽情涂鸦的 Web 应用程序
2008-10-26 11:49:18 来源:WEB开发网本文将介绍以下内容:
Silverlight InkPresenter 控件
Web 应用程序中的手写内容
手写识别
创建透明背景和图像背景
本文使用以下技术:
Silverlight 2、Expression Blend 和 Visual Studio 2008
这篇文章基于 Visual Studio 2008 SP1、Silverlight 2 和 Expression Blend 的预发布版而撰写。文中的所有信息均有可能发生变更。
目录
InkPresenter 简介
不需要 Tablet PC
InkPresenter 101
添加事件和背景
更时尚的 InkPresenter
获得 Silverlight 式外观
向 InkPresenter 添加背景图像或视频
笔划设计基础知识
手写识别
InkAnalyzer 类
服务器端分析
将服务挂接到 Silverlight 客户端
使用服务保留注释
存储和检索注释
总结
Silverlight 是 Microsoft 在过去几年中开发出的最令人兴奋的新 Web 技术之一。这项技术非常重要,现在几乎每年的 MIX 会议都是以 Silverlight™ 及其诸多功能为中心。Silverlight 1.0 最初发布于 2007 年,Silverlight 2 最终版本之前的每个新版本都增加了令人印象深刻的新功能。Silverlight 的超炫功能之一是 InkPresenter 控件,只是此功能尚未得到自己应得的关注。使用 InkPresenter 控件,Internet 用户能够直接从其浏览器在 Silverlight 应用程序中进行绘制。
由于 Silverlight 适用于各种操作系统和浏览器,所以 InkPresenter 也是如此,这就去除了浏览器、操作系统和硬件这另一组限制。我将在此处介绍的示例应用程序结合使用了 Silverlight 2 的 Microsoft® .NET Framework 编程功能与 InkPresenter,允许用户为预定义的图像集添加注释、执行手写识别、将注释和已识别的文本保存到服务器端数据库中、检索所选图像的注释以及根据与图像关联的文本筛选图像。数据库和手写识别功能由 Windows® Communication Foundation (WCF) 服务提供。图 1 显示了已完成的应用程序。
图 1 已完成的应用程序
需要说明一下,本文介绍的所有功能也可以在 Silverlight 1.0 中完成。实际上,在 Silverlight 2 问世之前,我在 Silverlight 1.0 中编写过一个类似的应用程序,其中包含了完全相同的功能。但是,如果客户端上没有可供您使用的以 .NET 为目标的代码,您就需要利用 .NET Web 服务来获得更多功能。
InkPresenter 简介
使此应用程序得以实现的 InkPresenter 控件是一个笔划集合容器。每个笔划都由一组 StylusPoints 组成。请注意,Silverlight 中还存在另一个 Stroke 属性,它是 Shape 类的成员,注意不要将两者混淆。
以笔和纸为例,每次用笔接触纸时,就开始了一个笔划,然后继续来回移动笔可以绘制一个圆或写一个字。将笔从纸上提起则表示该笔划结束。再次放下笔就会开始一个新笔划。
笔划具有一些 DrawingAttributes,可定义笔划的颜色、宽度和其他几个属性等特征。笔划中的各个点也具有属性:代表位置的 X 坐标和 Y 坐标以及 PressureFactor。在计算机中使用数字化器 PressureFactor,它允许您以编程方式根据用户将笔针按入数字化器的力度影响笔划。图 2 说明了该类层次结构。
图 2 InkPresenter 类
与 Silverlight 中的其他可视化元素一样,InkPresenter 对象及其子项可以表示为 XAML。图 3 显示了 InkPresenter 中的三个小笔划,图 4 显示了 StrokeCollection 中某个笔划的 XAML 表示形式。尽管笔划很小,数字化器同样能够收集到大量数据。如果使用鼠标进行相同测试,则收集到的数据量就会明显减少,因为笔针点只是成对的,而且点的收集时间间隔加大。
图 3 一些笔划
图 4 InkPresenter 控件中的第一个笔划
<StrokeCollection>
<StrokeCollection xmlns="http://schemas.microsoft.com/client/2007">
<Stroke>
<Stroke.DrawingAttributes>
<DrawingAttributes Color="#FF000000" OutlineColor="#00000000" Width="3" Height="3" />
</Stroke.DrawingAttributes>
<Stroke.StylusPoints>
<StylusPoint X="81.4583358764648" Y="96.5833282470703" />
<StylusPoint X="81.4583358764648" Y="96.5833282470703" />
<StylusPoint X="81.0833358764648" Y="96.4166717529297" />
<StylusPoint X="81.0833358764648" Y="96.4166717529297" />
<StylusPoint X="81.0833358764648" Y="96.4166717529297" />
<StylusPoint X="81.0833358764648" Y="96.4166717529297" />
<StylusPoint X="81.0833358764648" Y="96.4166717529297" />
<StylusPoint X="80.4583358764648" Y="96.8333282470703" />
<StylusPoint X="80.4583358764648" Y="96.8333282470703" />
<StylusPoint X="80" Y="97.2916717529297" />
<StylusPoint X="80" Y="97.2916717529297" />
<StylusPoint X="79.625" Y="97.75" />
<StylusPoint X="79.625" Y="97.75" />
<StylusPoint X="79.625" Y="97.75" />
<StylusPoint X="79.625" Y="97.75" />
<StylusPoint X="79.625" Y="96.5416717529297" />
<StylusPoint X="79.8333358764648" Y="95.7083358764648" />
<StylusPoint X="80.25" Y="94.7916641235352" />
<StylusPoint X="80.7916641235352" Y="93.5416641235352" />
<StylusPoint X="81.5" Y="92.125" />
<StylusPoint X="82.4166641235352" Y="90.4583358764648" />
<StylusPoint X="83.4583358764648" Y="88.5833358764648" />
<StylusPoint X="84.75" Y="86.5416641235352" />
<StylusPoint X="86.1666641235352" Y="84.3333358764648" />
<StylusPoint X="87.7083358764648" Y="82.1666641235352" />
<StylusPoint X="89.25" Y="79.9166641235352" />
<StylusPoint X="90.75" Y="77.9583358764648" />
<StylusPoint X="92" Y="76.0833358764648" />
<StylusPoint X="93.1666641235352" Y="74.8333358764648" />
<StylusPoint X="94" Y="73.625" />
<StylusPoint X="94.7083358764648" Y="73.1666641235352" />
<StylusPoint X="95.125" Y="73.1666641235352" />
<StylusPoint X="95.125" Y="73.1666641235352" />
<StylusPoint X="95.125" Y="73.1666641235352" />
<StylusPoint X="94.7083358764648" Y="73.5" />
</Stroke.StylusPoints>
</Stroke>
...
</StrokeCollection>
使用 InkPresenter 实际上也就是创建其笔划并与笔划进行交互。但默认情况下,InkPresenter 并不执行这些操作。它会提供一些事件和方法;允许您添加和删除 StrokeCollection,还允许您访问 Stroke 以便与其进行交互。但是,是否以编程方式跟踪 InkPresenter 控件边界内的鼠标或笔针活动并生成笔划,决定权在您。
不需要 Tablet PC
在 Windows Presentation Foundation (WPF) 和 Silverlight 问世之前,开发人员是依靠 Tablet PC SDK 创建自定义程序来利用 Tablet PC 绘制功能的。SDK 是一组带有 .NET 包装的 COM API,能使用 .NET、Visual Basic® 6.0 和 C++ 进行开发。必需使用 Windows XP Tablet PC Edition 操作系统。
从 Tablet PC SDK 1.7 版本开始,重要控件 InkOverlay 的运行便不再需要对系统有“完全信任”权限。您可以创建启用了手写功能的 Windows 窗体控件并将其嵌入到网页中,就像使用了任一 ActiveX® 控件一样。尽管 ActiveX 控件仅限于在 Internet Explorer® 中使用,但这也在开发人员将绘制和其他手写功能引入到 Web 的方向上迈出了第一步。
在 Windows Vista® 中,Tablet PC 功能是作为一流成员内置到操作系统中的,并且用于 Tablet 功能的开发 API 已并入 WPF InkCanvas 对象。这就意味着,任何安装了 .NET Framework 3.0 的计算机都支持 Tablet 功能,即使该计算机不是 Tablet PC 也无妨。但一定要认识到,在 Tablet 的数字化器上使用笔针得到的分辨率比使用鼠标所得到的分辨率要高得多(相差多个数量级)。
Silverlight 中的手写功能是 WPF 中提供的功能的子集。但是,两者之间有一个重要区别,即 WPF InkCanvas 有一个 InkPresenter 属性,可在 InkCanvas 上显示手写内容,但在 Silverlight 中没有 InkCanvas,您必须直接使用 InkPresenter。
最重要的是,Silverlight 不限于 Windows 或 Internet Explorer,因此许多环境都能够使用启用手写功能的网站。
InkPresenter 101
适用于 Visual Studio® 2008 的 Silverlight 工具将 Silverlight XAML 设计器嵌入到了 Visual Studio 2008 中,但是设计图面本身是只读的,因此您可能会发现在 Expression Blend™ 2.5 中执行某些 InkPresenter 设计工作将更加方便。这两个应用程序可以很好地集成,因此这实现起来相当简单,熟悉 Expression Blend 之后,它也会给您带来很多乐趣。
我喜欢在 Visual Studio 中开始我的项目,因为默认项目模板会为我提供很大的帮助。然后,当我想在 XAML 中开展某些设计工作时,我会在 Expression Blend 中打开该项目。通过在 Visual Studio IDE 的解决方案资源管理器中右键单击 XAML 文件,然后选择相应的选项在 Expression Blend 中打开该文件,您就可以轻松实现此操作。如果没有 Expression Blend 2.5,您可以直接在 Visual Studio 2008 中手动编辑 XAML,这样就可以立即看到更改结果。
创建 Silverlight 项目时,您可以选择使用 Visual Basic 或 C#。尽管我的 Visual Basic 技能不错,但对于此项目,我选择使用 C#,因为我在 Silverlight 1.0 中工作时存储过一段 JavaScript 代码,随时可以移植到 C#。
在 Visual Studio 中创建项目后,第一个任务是在 Expression Blend 中打开该项目,以便开始设计。在 Expression Blend 设计器中打开 XAML 文件后,您可以将 InkPresenter 控件添加到设计图面中。若要查找 InkPresenter 控件,您将需要通过单击工具箱底部的 >> 图标打开 Expression Blend 资源库。然后,必须单击“显示所有内容”复选框才能查看 InkPresenter 以及其他一些不是很常用的控件。或者,您还可以使用搜索框搜索控件。
将 InkPresenter 拖动到 XAML 设计图面上;然后,使用其属性窗口为此控件命名。我选择了“inkP”,本文稍后部分的代码将频繁用到此名称。
在设计图面上选中 InkPresenter 后,您可以看到它的边界,但在未选中时,它看起来似乎消失了。InkPresenter 控件是一个能够呈现手写内容的容器,尽管它确实具有背景属性,但没有 Fill、Stroke(对于边框)或其他控件提供的许多其他属性。因此,您需要另一个控件来提供可视化边界。
举一个简单的示例,向画布添加一个与 InkPresenter 具有相同位置和维度的矩形。默认情况下,矩形具有黑色边框(Stroke 属性),其中 StrokeThickness 为 1。该矩形(我称之为“inkBorder”)和 InkPresenter 在画布中应该属于同级。InkPresenter 的 Z 次序必须高于矩形(即位于其上方),否则矩形将覆盖 InkPresenter。
由于在设计图面上查找 InkPresenter 控件比较困难,因此当需要选择它时,最简单的方法就是在“对象和时间线”面板中进行选择。此操作会在设计图面上突出显示该控件。到目前为止,如果您要测试解决方案(通过按 F5),就会看到矩形边框,但是,如果您尝试使用鼠标或笔针在边框内部绘图,则不会有任何效果。
添加事件和背景
如上文所述,InkPresenter 仅仅是一个笔划集容器,本身并不能创建笔划。您必须通过响应 InkPresenter 上的事件以编程方式创建笔划。捕获笔划的关键事件是 MouseLeftButtonDown、MouseMove 和 MouseLeftButtonUp。当 InkPresenter 收到 MouseLeftButtonDown 事件时,您需要在内存中创建一个新笔划并将其添加到 InkPresenter 的 StrokeCollection 中。在 InkPresenter 内部来回移动鼠标以创建 MouseMove 事件时,您需要向该笔划添加 StylusPoints。只要用户触发了 MouseLeftButtonUp 事件,那么无论是通过将笔针从数字化器上提起还是通过释放鼠标,您都需要完成该笔划。
借助 Expression Blend 2.5,就可以轻松使用“属性”窗口来关联事件与控件了。选择 inkP 控件,然后单击“属性”窗口顶部的事件图标来查看控件的事件。接下来,分别双击上述三个事件处理程序的文本框。对于每个事件处理程序,会将相应的方法添加到 Visual Studio 中 XAML 控件的代码隐藏中。Visual Studio 与 Expression Blend 之间可以双向集成。每次做出更改后,Visual Studio 都会要求您确认更改,因此请注意任务栏上闪烁的 Visual Studio 图标。
创建了这三个处理程序后,切换到 Visual Studio 以添加图 5 中的代码,这些代码允许 InkPresenter 在用户与控件进行交互时收集和显示笔划数据。此代码执行先前列出的任务:创建新笔划并添加笔针点。笔针点可通过 MouseLeftButtonDown 事件和 MouseMove 事件的 MouseEventArgs 进行访问。
图 5 收集和显示笔划数据
System.Windows.Ink.Stroke newstroke;
void inkP_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
inkP.CaptureMouse();
newStroke = new System.Windows.Ink.Stroke();
newStroke.StylusPoints.Add(e.StylusDevice.GetStylusPoints(inkP));
inkP.Strokes.Add(newStroke);
}
void inkP_MouseMove(object sender, MouseEventArgs e)
{
if (newStroke != null)
{
newStroke.StylusPoints.Add(e.StylusDevice.GetStylusPoints(inkP));
}
}
void inkP_MouseLeftButtonUp (object sender, MouseEventArgs e)
{
newStroke = null;
inkP.ReleaseMoustCapture();
}
再有一块这个拼图就要完成了。InkPresenter 必须包含 Background 属性才能接收鼠标事件。Background 属性与其他控件的 Fill 属性类似,但不具备您可以对 Fill 执行的其他自定义项。根据您的设计方案,您可以对背景进行一些创造性设计,但是现在只需将 Background 属性设置为“透明”即可。
您可以在 Expression Blend 中使用 InkPresenter 控件的“属性”窗口设置此背景,方法是为该背景选择纯色画笔,然后将其 Alpha 值设置为 0。另一种方法是直接在 XAML 中键入 Background="Transparent"。
以下是在关联事件并指定 Background 属性后,这两个控件的 XAML 代码:
<Rectangle Margin="20,30,35,24"
x:Name="inkBorder" Stroke="#FF000000"/>
<InkPresenter Margin="20,30,35,24"
x:Name="inkP"
MouseLeftButtonDown=
"inkP_MouseLeftButtonDown"
MouseLeftButtonUp=
"inkP_ MouseLeftButtonUp"
MouseMove="inkP_MouseMove"
Background="Transparent"
Opacity="1"/>
现在,在 Expression Blend 或 Visual Studio 中运行该项目时,您就可以在 InkPresenter 上即时看到所绘的笔划(请参见图 6)。
图 6 InkPresenter 上的笔划
更时尚的 InkPresenter
正如您所看到的,虽然 InkPresenter 是一个容器,但与其他可视化元素(如矩形)相比,它更像是一个画布。您需要将 InkPresenter 与其他元素相结合,才能使其看起来更加有趣;否则对 Silverlight 不公平。因此,请在 Expression Blend 中打开现有的 XAML,以便使用先前为 InkPresenter 创建边框时添加的矩形来调整外观,使其更加整洁。
首先,我们来圆化边框矩形的角。选择该矩形,将其 RadiusX 和 RadiusY 属性设置为 25。现在,矩形已经拥有了美观的圆化边;但是,InkPresenter 的各角延伸到了可视边框以外,并将接受手写内容。解决方案是更改或剪切 InkPresenter 的边界,使其与可视边框相吻合。这可以通过使用 Silverlight 剪切功能重塑 InkPresenter 的形状来完成。
使用 Expression Blend 后,剪切某个元素使其与另一个元素的形状相吻合就比较容易了。但在此之前,请先为 inkBorder 矩形创建一个副本,以供稍后使用。在“对象和时间线”窗口中,右键单击这个新矩形。从其上下文菜单中,选择“路径”,然后选择“生成剪切路径”。然后,Expression Blend 将弹出一个窗口,要求您选择要按路径剪切哪个对象,也就是说,哪个对象将采用矩形的形状。选择 InkPresenter。这会产生两个结果:InkPresenter 现在采用矩形的形状;新矩形消失。当矩形成为 InkPresenter 的剪切路径时,它就不再是一个对象了。至此,您一定明白复制矩形的原因了。
生成的 XAML 如图 7 所示。运行该项目以测试 InkPresenter 的新边缘。它将如图 8 所示。在 Silverlight 中,可以使用任何形状作为剪切路径,因此您可以根据自己的喜好为 InkPresenter 创建任何形状。图 9 显示了一个用于剪切 InkPresenter 的随机形状示例。
图 7 已剪切对象的 XAML
<Rectangle x:Name="inkBorder" Width="346" Height="234"
Stroke="#FF000000" Canvas.Top="25" Canvas.Left="25"
RadiusX="25" RadiusY="25"/>
<InkPresenter x:Name="inkP"
Width="607" Height="408" Canvas.Left="25" Canvas.Top="34"
MouseLeftButtonDown="inkP_MouseLeftButtonDown"
MouseLeftButtonUp="inkP_MouseLeftButtonUp"
MouseMove="inkP_MouseMove"
Background="Transparent"
Clip="M0.5,25.5 C0.5,11.692881 11.692881,0.5 25.5,0.5 L581.5, 0.5 C595.30712,0.5 606.5,11.692881 606.5,25.5 L606.5, 382.5 C606.5, 396.30712 595.30712,407.5 581.5,407.5 L25.5,407.5 C11.692881, 407.5 0.5,396.30712 0.5,382.5 z" >
</InkPresenter>
图 8 剪切 InkPresenter 的边缘
图 9 将这个随机绘制的形状用于剪切 InkPresenter
获得该 Silverlight 外观
通过 Silverlight,您可以使用透明度创建有趣的可视层。您的 InkPresenter 也可以采用半透明图面的外观。若要实现此效果,首先应将背景图像添加到画布中。我使用的是 Silverlight.net 网站中提供的背景(请访问 silverlight.net)。通过将图像拖动到画布中并将其 Stretch 属性设置为 Fill,即可轻松设置背景。请务必确保图像的 Z 次序是设计图面中列出的第一个控件。否则,它会位于矩形和 InkPresenter 的顶部,从而覆盖它们。
接下来,修改矩形为其赋予黑色背景。一种方法是选择“属性”窗口中的“填充画笔”,然后将其 R、G 和 B 值都设置为 0。将矩形的 Opacity 更改为 10%,使其呈现半透明效果。
虽然可以在 InkPresenter 上设置背景色并为其设置透明度,但此透明度还会影响手写内容。我喜欢将 InkPresenter 的背景设置为完全透明,并使用一些其他控件提供这种效果。您可能会发现,亲自更改控件背景色的 Alpha 值,并将其效果与更改控件 Opacity 的效果进行比较,这一过程很有趣。您还可以使用 DrawingAttribute 的 Color 和 OutlineColor 属性的 Alpha 属性,直接更改手写内容的透明度。此效果与 WPF InkCanvas 和 Tablet PC SDK 中的 DrawingAttribute.Transparency 属性的效果完全相同。图 10 显示了与半透明矩形相结合的 InkPresenter,为您的绘图背景提供了美观的视觉效果。
图 10 创建半透明的背景
向 InkPresenter 添加背景图像或视频
在某些情况下,半透明背景是一个非常吸引人的解决方案,但您还可以使用图像甚至是视频作为背景。若要创建此类背景,InkPresenter 的实际 Background 属性不会更改。图像或视频将作为 InkPresenter 对象的子元素添加。如果子元素没有 Height、Width、Left 或 Top 属性,它将继承父级 InkPresenter 的属性。
尝试一下 — 使用 Expression Blend 向 XAML 设计图面添加图像元素,然后将该图像拖动到 InkPresenter。或者,您可以直接添加 XAML:
<InkPresenter x:Name="inkP"
Width="607" Height="426" Canvas.Left="25" Canvas.Top="34"
MouseLeftButtonDown="inkP_MouseLeftButtonDown"
MouseLeftButtonUp="inkP_MouseLeftButtonUp"
MouseMove="inkP_MouseMove"
Background="Transparent">
<Image Source="Assets/Leaves.jpg" Stretch="Fill" />
</InkPresenter>
现在,可以在图像上直接绘图了。或者,可以减小图像的 Opacity 值,也使其成为半透明的,如图 11 所示。
图 11 使用 Opacity 属性创建半透明图像
添加视频也很容易。您要使用 MediaElement,而不是 Image。Expression Blend 处理视频的方式与处理图像的方式不同。虽然可以将视频从 Silverlight 项目拖放到 XAML 设计图面上,但在运行项目时找不到该文件。因此,您需要将视频放置到主机 Web 项目内部。然后,MediaElement 的源属性需要引用该文件的 URL。Media Element 不会像 Image 一样自动填充 InkPresenter。您必须手动调整大小或直接在 XAML 中添加 Stretch="Fill"。以下示例将在 InkPresenter 的背景中播放视频:
<InkPresenter x:Name="inkP" . . . >
<MediaElement Height="246" x:Name="Butterfly_wmv" Width="345"
Source="http://localhost:52476/MSDNMagAnnotationClient_Web/Assets/Butterfly.wmv"
Stretch="Fill"/>
</InkPresenter>
有关如何使用 MediaElements 以及与其交互的详细信息,请参阅 Silverlight 文档。
笔划设计基础知识
默认情况下,笔划的默认绘制属性将创建高度和宽度都为 3 的黑色笔划。此值表示与设备无关的像素 (DIP),不能设置为小于 2 的值。
在代码中,您可以创建一些方法和事件处理程序来影响各种笔划属性,如 penWidth 和 penColor。例如,currentColor 变量可以通过控件的单击事件更改其值,接着在创建新笔划后,该变量便可用于 inkP_MouseLeftButtonDown 事件中。
若要进行尝试,请在类中添加变量声明,将默认值设置为 black,如下所示:
System.Windows.Media.Color currentColor = Colors.Black;
在以下示例中,我创建了一个方法,可用作任意数量的彩色矩形的 MouseLeftButtonDown 事件。此方法可确定矩形的颜色,然后使用该颜色作为 currentColor 变量的值:
private void ChangeColor(object sender, MouseButtonEventArgs e)
{
Rectangle rec = (Rectangle)sender;
SolidColorBrush scb = (SolidColorBrush)rec.Fill;
currColor = scb.Color;
}
在 inkP_MouseLeftButtonDown 方法中,添加以下代码可将 DrawingAttributes 属性设置为 currentColor 变量:
newStroke.DrawingAttributes.Color = currentColor;
最后,您还需要一种方法来触发更改。添加两个矩形元素,将一个元素的 Fill 设置为 black,将另一个元素的 Fill 设置为 red。每个矩形都需要一个 MouseLeftButtonDown 事件来调用 ChangeColor 方法。以下 XAML 会创建两个显示为圆形的矩形对象:
<Rectangle MouseLeftButtonDown="ChangeColor"
Width="24" Height="22" Fill="#FF000000"
Stroke="#FF000000" RadiusX="25" RadiusY="25"
Canvas.Left="-10" Canvas.Top="282"/>
<Rectangle MouseLeftButtonDown="RedInk"
Width="24" Height="22"
Fill="#FFCE0C0C" Stroke="#FF000000"
RadiusX="25" RadiusY="25"
Canvas.Left="25" Canvas.Top="282"/>
或者,您还可以编写代码深入到 StrokeCollection 内部,以更改现有笔划的颜色。
OutlineColor 是 Stroke 对象的 DefaultDrawingAttributes 之一。如果您计划在多色背景(如图像)中进行绘制,则使用一致的轮廓颜色绘制手写内容将会很有帮助。您可以将以下代码添加到 inkP_MouseLeftButtonDown 事件中,以设置 newStroke 的 OutlineColor:
newStroke.DrawingAttributes.OutlineColor =
Colors.White;
图 12 说明了这一概念。
图 12 有框的墨迹
手写识别
应用程序中存在众多超炫的手写功能,其中之一就是手写识别。虽然此功能最初就是 Tablet PC SDK 的一部分,也是 WPF 的一项功能,但并未包含在 Silverlight 中。不过,通过将笔划数据发送到将执行识别并返回结果的 ASP .NET Web 服务或 WCF 服务中,您也可以在 Silverlight 应用程序中使用手写识别功能。
Microsoft Research 收集了一百多万用户的手写示例,用于创建手写识别的算法。当您发现手写体字母比印刷体字母更易于识别时,您可能会感到很吃惊。另外,还针对多种语言提供了手写识别引擎,其中包括多种亚洲语言。
该引擎可以分析笔划集合,还可以确定某一组笔划是代表字、句子、段落还是绘图。识别引擎可以从包含多个字的一组笔划中识别出各个字,然后将这个字视为一个整体。接着,识别引擎将使用其示例和算法提供出一组可能的字供您选择。如果您发送的是一组字(如句子),它就会返回一组字以及一系列替代选项。过去,只有 Tablet PC 上可实现识别功能。而现在,任何支持 Silverlight 的计算机/浏览器组合都可以实现识别功能!
InkAnalyzer 类
System.Windows.Ink.InkAnalyzer 可以执行手写识别。此类使用起来非常简单。只需将笔划集传递到 InkAnalyzer 对象,调用它的 Analyze 方法,然后使用名为 Successful 的布尔属性确定 Analyze 是否成功完成。如果 Successful 为 true,则 GetRecognizedString 方法将返回最佳推断值,而 GetAlternates 方法将返回一个备选字符串数组。
虽然 Silverlight API 并不包含 InkAnalyzer 类,但是您仍可以使用 Web 或 WCF 服务为 Silverlight 应用程序提供手写识别。Web 服务器可以托管 WPF API 并提供执行识别的功能。但是,这需要进行一些转换。
首先,您需要在要执行手写识别功能的所有组件中引用图 13 中的 API。前两个 API 可通过 Visual Studio 中的“添加引用”界面随时使用。最后两个 API 与 Tablet PC SDK 一同安装,位于 Program FilesReference AssembliesMicrosoftTablet PCv1.7 目录下。当进行编码时,您将需要了解 IACore 与 IAWinFX 命名空间之间的模糊引用。最后,还需要引用随 Tablet PC SDK 一同安装的 IALoader.dll。如果您使用的是 Windows Vista,则可以在 C:Program FilesMicrosoft SDKsWindowsv6.0Bin 下找到此文件。
图 13 WPF API
API | 功能 |
PresentationCore.dll | 包含 System.Windows.Ink API。 |
WindowsBase.dll | 包含供 Collection 使用的功能。 |
IAWinFX.dll | 将 InkAnalyzer 类和功能添加到 System.Windows.Ink 中。 |
IACore.dll | 通过 System.Windows.Ink.AnalysisCore 提供 InkAnalyzerBase 类和功能。 |
为了通过线路将 Stroke 从 InkPresenter 传输到服务中,需要对笔划进行序列化。但这些对象是无法序列化的。因此,您需要为 StrokeCollection 创建一个基于字符串的 XAML 表示形式。这样,就可以对字符串进行序列化并将其发送到服务了。
在 Silverlight 1.0 中,需要使用 JavaScript 才能创建此字符串表示形式。而在 Silverlight 2 中,您能够使用 LINQ to XML,这是一个优势。(如果您使用的是 Visual Basic,还可以获得使用 XML 文本这一优势。)
图 14 中显示的代码可深入到 StrokeCollection 对象内部(能够读取 DrawingAttributes、StylusPoints 及其详细信息),并使用 LINQ to XML 创建其 XAML 表示形式。此代码有多种用途,包括手写识别,但识别功能会忽略 DrawingAttributes。如果您创建此方法只是为了进行识别,则可以省略收集 DrawingAttributes 的代码。
图 14 基于笔划创建 XAML 表示形式
public XElement StrokestoXAML(StrokeCollection mystrokes)
{
//this method uses LINQ to XML
//be sure to add the namespace to each element in order to load back
//into a new StrokeCollection later with the XAMLReader
string xmlnsString = "http://schemas.microsoft.com/client/2007";
XNamespace xmlns = xmlnsString;
XElement XMLStrokes = new XElement(xmlns + "StrokeCollection",
new XAttribute("xmlns", xmlnsString));
//create stroke, then add to collection element
XElement mystroke;
foreach (Stroke s in mystrokes)
{
mystroke = new XElement(xmlns + "Stroke",
new XElement(xmlns + "Stroke.DrawingAttributes",
new XElement(xmlns + "DrawingAttributes",
new XAttribute("Color", s.DrawingAttributes.Color),
new XAttribute("OutlineColor",
s.DrawingAttributes.OutlineColor),
new XAttribute("Width", s.DrawingAttributes.Width),
new XAttribute("Height", s.DrawingAttributes.Height))));
//create points separately then add to mystroke XElement
XElement myPoints = new XElement(xmlns + "Stroke.StylusPoints");
foreach (StylusPoint sp in s.StylusPoints)
{
XElement mypoint = new XElement(xmlns + "StylusPoint",
new XAttribute("X", sp.X.ToString()),
new XAttribute("Y", sp.Y.ToString()));
//add the new point to the points collection of the stroke
myPoints.Add(mypoint);
}
//add the new points collection to the stroke
mystroke.Add(myPoints);
//add the stroke to the collection
XMLStrokes.Add(mystroke);
}
return XMLStrokes;
}
注意如何使用命名空间构建 XAML。Silverlight 2 XMLReader 要求在 XAML 元素的根目录下使用此命名空间,以便稍后将 XAML 加载到对象中。
服务器端分析
现在,可以将字符串作为参数传递给服务操作,从而返回字符串形式的结果。此操作所使用的方法必须基于 XAML 字符串重新创建一个 StrokeCollection。然后,可以将该 StrokeCollection 发送到 InkAnalyzer 中。在服务器上,您不再能够访问 Silverlight API,而将使用 WPF API。
虽然 WPF 也包含 XAMLReader.Load 方法,但 WPF StrokeCollection 与 Silverlight StrokeCollection 之间存在一些细微差别,因此将无法识别架构,并且 XAMLReader.Load 也将失败。但是,您可以使用 LINQ to XML 轻松实现对 XAML 字符串的深入研究并读取各个 Stroke 元素中的数据,然后构建新的 WPF Stroke 对象。
WPF 与 Silverlight StrokeCollection 之间的差别极小。例如,WPF Stroke.DrawingAtrributes 没有 Outline Color,并且 WPF 笔划中的 StylusPoints 基于与设备无关的坐标系,而非 Silverlight 中使用的像素值。虽然存在这些差别,但在 Silverlight 和 WPF 中,Stroke、StrokeCollection 和 StylusPoint 类位于相同的命名空间中。
图 15 中的 CreateWPFStrokeCollectionfromXAML 方法使用 LINQ to XML 来创建 Stroke 元素集合,然后遍历这些元素,为每个元素创建一个新的 Stroke 对象。该方法将再次使用 LINQ 创建 StylusPoints 集合,然后为每个笔划创建 StylusPointsCollection。请注意,由于执行识别时不需要使用 Color 等 DrawingAttributes,因此此处并非完全重新创建 StrokeCollection。这两个方法依赖于之前讨论的 PresentationCore 和 WindowsBase API。创建 StrokeCollection 后,便可将其传递到将执行分析的方法中。
图 15 CreateWPFStrokeCollectionfromXAML 方法
private StrokeCollection CreateWPFStrokeCollectionfromXAML
(string XAMLStrokes)
{
//because namespace was used to create this
// (for Silverlight to reuse the XAML),
//you need to insert the namesace into the Descendent's parameter
var xmlElem = XElement.Parse(XAMLStrokes);
XNamespace xmlns = xmlElem.GetDefaultNamespace();
StrokeCollection objStrokes = new StrokeCollection();
//Query the XAML to extract the Strokes
var strokes = from s in xmlElem.Descendants(xmlns+ "Stroke") select s;
foreach (XElement strokeNodeElement in strokes)
{
//query the stroke to extract its StylusPoints
var points = from p
in strokeNodeElement.Descendants(xmlns + "StylusPoint") select p;
//create Stylus points collection from point element values
StylusPointCollection pointData =
new System.Windows.Input.StylusPointCollection();
foreach (XElement pointElement in points)
{
double Xpoint = Convert.ToDouble(pointElement.Attribute("X").Value);
double Ypoint = Convert.ToDouble(pointElement.Attribute("Y").Value);
pointData.Add(new StylusPoint(Xpoint, Ypoint));
}
//create a new Stroke from the StylusPointCollection
System.Windows.Ink.Stroke newstroke = new
System.Windows.Ink.Stroke(pointData);
//add the new stroke to the StrokeCollection
objStrokes.Add(newstroke);
}
return objStrokes;
}
可以在 WCF 或 ASMX Web 服务中使用图 16 中的方法接受来自 InkPresenter 的 StrokeCollection,并返回代表最佳推断结果的字符串。此功能依赖于 IAWinFX 和 IACore 程序集。
图 16 将字符串传递给识别器
public string RecognizeStrokes(string XAMLStrokes)
{
try
{
//custom method to create WPF StrokeCollection from the string-based XAML
var strokeColl = CreateWPFStrokeCollectionfromXAML(XAMLStrokes);
var IA = new System.Windows.Ink.InkAnalyzer();
IA.AddStrokes(strokeColl);
var status = IA.Analyze();
if (status.Successful)
return IA.GetRecognizedString();
else
return "Not Recognized";
}
catch (Exception ex)
{
//trap and display errors at design time, not in production code
return "error:" + ex.Message;
}
}
将服务挂接到 Silverlight 客户端
RecognizeStrokes 方法封装在 ASMX 或 WCF 服务的内部,因此在 Silverlight 应用程序中可轻松调用 Web 服务方法。我在解决方案中使用了 WCF 服务来提供手写识别功能。如需有关 WCF 的任何帮助,请参阅 Silverlight.net 网站上的简单的快速入门指南,该指南介绍了如何创建可使用 Silverlight 访问的 WCF 服务。
虽然在基于 Windows 的应用程序中通常是随着手写内容的绘制来动态进行识别,但是在分布式应用程序中,还是让用户显式请求识别比较合理。因此,您需要在设计图面上提供一个控件来启动此过程。创建一个控件,如 XAML 中的按钮。您需要针对该按钮的 Click 事件提供一个事件处理程序,用于触发 GetXAMLfromStrokes 方法;然后将生成的 XAML 发送到 Web 服务进行识别。在我的 Web 服务中,此操作称为“Recognize”。您还需要一个 TextBlock 控件来显示返回的字符串。我将其称为“RecoText”。
将 WCF 服务引用添加到 Silverlight 应用程序后,代理将只实现对服务操作的异步调用。因此,您需要一个用于处理 RecognizeCompleted 的方法(请参见图 17)。如果注释以多行形式写出,则可能会通过硬回车符加以识别,因此我没有使用 RecoText 的 TextBox 控件,而是改用了 ScrollViewer 控件,如图 18 所示。
图 17 识别操作
private void RecognizeButtonHandler(object sender, RoutedEventArgs e)
{
StrokeCollection sc = inkP.Strokes;
XElement sXML = StrokestoXAML(sc,true);
string ss = sXML.ToString();
GetRecoString(ss);
//the next two lines are to test the validity of XAML created
//(this would make a great unit test for this application)
//StrokeCollection sc2 = new StrokeCollection();
//sc2 = (StrokeCollection)System.Windows.Markup.XamlReader.Load(ss);
}
private void GetRecoString(string inkStrokes)
{
Binding binding = new BasicHttpBinding();
EndpointAddress endpoint = new
EndpointAddress("http://myserver/SilverlightInkService.svc");
var svc = new ServiceReference1.SilverlightInkServiceClient(binding,
endpoint);
svc.RecognizeCompleted += new
EventHandler<ServiceReference1.RecognizeCompletedEventArgs>
(svc_RecognizeCompleted);
svc.RecognizeAsync(inkStrokes);
}
private void svc_RecognizeCompleted(object sender,
ServiceReference1.RecognizeCompletedEventArgs e)
{
RecoText.Text = e.Result.ToString();
}
图 18 对已识别的文本使用 ScrollViewer 控件
为了获得更好的测试体验,您可能希望在 XAML 页上再添加一个按钮,用以清除手写内容和已识别的文本,以方便您体验各种注释。顾名思义,InkPresenter.Strokes.Clear 可以删除 InkPresenter 中的笔划。
使用服务保留注释
在此解决方案中,Web 服务的另一重要用途是保留注释。无论 StrokeCollection 是手写文本、绘图还是其他标记,在多数情况下,您都希望将其保留在数据库中或其他某种类型的数据存储中。您还可以使用 Silverlight 中提供的 IsolatedFileStorage 将注释数据保存在用户的计算机上。但在本文中,我将着重介绍服务器端的持久性。
就持久性而言,通常您会保存整个 StrokeCollection,并可以随时将其添加到另一 InkPresenter 中。我已经提到,因为必须对 StrokeCollection 对象进行序列化,所以最简单的方法是使用 GetXAMLfromStrokes 方法创建 XAML 字符串表示形式。
创建了 XAML 字符串之后,其他的工作就与在数据库中存储其他任何文本没什么两样了。请记住,如果是针对绘图,XAML 就可能会变得非常庞大,因此,当定义用于存储 XAML 的数据库字段以及客户端和服务设置时,您必须考虑这一可能性,以适应庞大数据量的传输。
对于 WCF,您可能还会考虑使用 JavaScript Object Notation (JSON) 序列化,此格式比 XML 序列化数据的压缩程度更高。虽然可以使用 SQL Server® 2005 及其更高版本中的 XML 数据类型,但除非您打算利用 XML 数据类型的优势(可以对其进行查询和编制索引,具有架构支持并允许修改数据),否则一般不使用,因为执行此类任务使用 SQL Server 类型(如 nvarchar,甚至是针对预期的大型绘图的 nvarchar(MAX))即可。
在示例应用程序中,用户可以从中选择各种图像。每个图像的注释都将使用图像的唯一文件名存储数据库中。选中某个图像后,便可从数据库中检索到其注释并显示该注释。
存储和检索注释
就识别而言,只能在服务中传递单个字符串。但是,在存储和检索数据时,您将传入 XAML,也可能传入与注释相关的其他元数据,如创建日期、用户,或者对该注释所属的图像或视频的引用。WCF 使用 DataContracts 管理消息的上述各种组件。借助属于图像文件的注释的示例,我将创建一个操作,使其能够同时存储图像路径和 XAML,然后检索特定文件路径的 XAML。
此 WCF 服务结合使用接口和属性来定义操作和数据约定。您可在图 19 中看出定义了三个独立的服务操作:StoreImageXAML、RetrieveImageXAML 和 Recognize。StoreImageXAML 采用 ImageXAMLComposite 类型的参数,该参数由 ImageXAMLComposite 类定义为 DataContract。另外两个操作只接受和返回单个字符串,因此要简单得多。服务类实现了这三个方法,用于调用可执行识别和数据库交互的辅助方法。这些与数据库协作的方法使用 LINQ to SQL(请参见图 20)。
图 19 定义 OperationContracts
namespace SilverlightInkWCFService
{
[ServiceContract]
public interface ISLInk
{
[OperationContract]
void StoreImageXAML(ImageXAMLComposite value);
[OperationContract]
string RetrieveImageXAML(string imageName);
[OperationContract]
string Recognize(string XAMLString);
}
[DataContract]
public class ImageXAMLComposite
{
string imagePath;
string xamlString;
[DataMember]
public string XAMLString
{
get { return xamlString; }
set { xamlString = value; }
}
[DataMember]
public string ImagePath
{
get { return ImagePath; }
set { ImagePath = value; }
}
}
}
图 20 调用识别辅助方法
public void StoreImageXAML(ImageXAMLComposite value)
{
//ExistingRows, InsertRow and updateRow use LINQ to the SQL
//SubmitChanges
if (ExistingRows(value.ImagePath) == true)
insertRow(value.ImagePath, value.XAMLString);
else
updateRow(value.ImagePath, value.XAMLString);
}
public ImageXAMLComposite RetrieveImageXAML(string imagePath)
{
//getXAML method performs a LINQ to SQL query
return getXAML(imagePath);
}
public string Recognize(string XAMLString)
{
return RecognizeStrokes(XAMLString);
}
总结
至此,我已介绍了有关创建 InkPresenter 和与其进行交互、执行手写识别功能以及通过服务存储和检索注释的 XAML 表示形式的所有重要内容。即使没有 Tablet PC 或者并未使用 Windows Vista,您仍然可以使用本应用程序(甚至可通过鼠标使用),但解决方案的效果不会很好。
另一个有趣的可用方法是使用视频。虽然可以使用 Silverlight 动画和触发器来播放笔划,但协调计时和视频却是一项挑战,不过也充满了乐趣。
可以从 go.microsoft.com/fwlink/?LinkId=122132 中下载 Silverlight、SDK 以及适用于 Visual Studio 2008 和 Expression Blend 2.5 2008 年 6 月预览版的 Silverlight Tools Beta2。
特别要感谢来自 Microsoft 的 Stefan Wick,他在笔记本、Tablet PC 和 UMPC 开发 MSDN® 论坛中提供了很大帮助,并为本文提供了重要指导。
Julia Lerman 是一位 .NET 顾问,她在软件构建方面有 20 余年的经验。她还是 .NET 社区中有名的会议发言人、作者、Microsoft .NET MVP 和 Vermont .NET 用户组的负责人。她即将出版的新书名为《Programming Entity Framework》。Julia 的博客是 thedatafarm.com/blog。
Tags:使用 Silverlight 创建
编辑录入:爽爽 [复制链接] [打 印]赞助商链接