MSDN Magazine > Home > Issues > 2008 > 8 月 >  塗鴉萬歲!:使用 Silverlight 2 建立可直接繪圖的 Web 應用程式
塗鴉萬歲!
使用 Silverlight 2 建立可直接繪圖的 Web 應用程式
Julia Lerman

本文討論下列主題:
  • Silverlight InkPresenter 控制項
  • Web 應用程式中的筆墨 (Ink)
  • 手寫辨識
  • 建立透明及影像背景
本文使用下列技術:
Silverlight 2、Expression Blend、Visual Studio 2008
本文是根據 Visual Studio 2008 SP1、Silverlight 2 及 Expression Blend 的搶鮮版所撰寫。本文包含的所有資訊均有可能變更。
Silverlight 是這幾年來 Microsoft 所推出的最受矚目 Web 新技術。就連 MIX 年度大會都圍繞著 Silverlight™ 和它的許多新功能打轉,由此可見 Silverlight 的重要性。Silverlight 1.0 是在 2007 年問世,最終版本發行前的各個新 Silverlight 2 版本,也都充滿讓人印象深刻的新功能。其中有一項 Silverlight 功能很酷,但尚未廣受注意,那就是 InkPresenter 控制項。InkPresenter 控制項可讓網際網路使用者透過瀏覽器,直接在 Silverlight 應用程式中進行繪圖。
由於 Silverlight 可以在各種 OS 和各式各樣的瀏覽器中使用,因此 InkPresenter 也可以,使 InkPresenter 能夠擺脫瀏覽器、OS 及硬體的限制。我在此介紹的範例應用程式,是使用 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 可定義的特性包括筆劃色彩、寬度及其他屬性 (Attribute)。筆劃中的各點也有屬性 (Property):代表位置的 X 與 Y 座標以及 PressureFactor。電腦會透過數位板來解譯 PressureFactor,根據使用者在數位板上按壓手寫筆的力量,就能夠以程式設計的方式來影響筆劃。[圖 2] 顯示類別階層。
圖 2 InkPresenter 類別 (按一下影像以放大圖片)
就像 Silverlight 中的其他 Visual 元素,InkPresenter 物件和它的子系都能夠以 XAML 表示。[圖 3] 顯示 InkPresenter 中的三個小筆劃,[圖 4] 顯示 StrokeCollection 中以 XAML 表示的一個筆劃。雖然筆劃都很小,但數位板還是要收集很多資料。如果您使用滑鼠來執行相同的測試,收集的資料會少很多,因為手寫筆的點只會是成對的,而且點的收集頻率會比較低。
圖 3 一些筆劃
<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 在預設情況下並不會執行這些動作。InkPresenter 會提供事件 (Event) 和方法 (Method);它可以讓您新增及移除 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 是必要的 OS。
從 Tablet PC SDK 的 1.7 版起,關鍵的 InkOverlay 控制項不再需要系統上的完全信任 (Full Trust) 才能執行。使用者開始能夠建立具備筆墨功能的 Windows Form 控制項,並將這些控制項內嵌到網頁中,就像使用任何 ActiveX® 控制項一樣。雖然 ActiveX 控制項僅限於在 Internet Explorer® 中使用,但這項改變至少可讓開發人員將繪圖與其他筆墨功能引入 Web 中。
在 Windows Vista® 中,Tablet PC 的功能是以一等公民的身分內建在 OS 中,而且 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
Silverlight Tools for Visual Studio® 2008 可將 Silverlight XAML 設計工具內嵌到 Visual Studio 2008 中,但是設計介面本身是唯讀的,因此針對某些 InkPresenter 設計工作,使用 Expression Blend™ 2.5 可能會更方便。這兩個應用程式的整合度很高,因此使用起來很簡單,一旦您熟悉 Expression Blend,就會感到樂趣無窮。
我習慣用 Visual Studio 來建立專案,因為預設的專案範本很好用。接下來,當我想要使用 XAML 進行一些設計工作時,就會在 Expression Blend 中開啟專案。您只要在 [方案總管] 中以滑鼠右鍵按一下 XAML 檔案,並選取 [於 Expression Blend 中開啟] 的選項,就能夠輕鬆透過 Visual Studio IDE 在 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 就會消失。InkPresenter 控制項是能夠呈現筆墨的容器,雖然這個控制項確實有背景屬性,但是不具有 Fill、Stroke (用於框線) 或其他控制項中常見的許多屬性。因此,您需要另一個控制項來提供視覺界限。
舉個簡單的例子,在畫布中加入一個矩形 (Rectangle),並使用與 InkPresenter 相同的放置位置和維度。根據預設,矩形 (Rectangle) 會包含黑色框線 (Stroke 屬性) 且 StrokeThickness 為 1。我命名為「inkBorder」的矩形和 InkPresenter 應該位於畫布中的同層級。InkPresenter 的 z-Order 必須高於矩形 (在矩形上方),否則矩形 (Rectangle) 會導致 InkPresenter 變成隱藏。
在設計介面上尋找 InkPresenter 控制項很困難,因此當您需要選取此控制項時,最簡單的方法就是在 [物件與時間軸] 面版中選取它。這個動作便會在設計介面上反白顯示該控制項。若您測試這個階段的解決方案 (藉由按下 F5),就會看到矩形 (Rectangle) 框線,不過如果您試著使用滑鼠或手寫筆在矩形內塗鴉,這時不會發生任何事情。

加入事件與背景
如上所述,InkPresenter 只是筆劃集合 (Stroke Collection) 的容器。InkPresenter 本身並沒有建立筆劃的能力。您必須在 InkPresenter 上,以程式設計的方式回應事件,才能建立筆劃。擷取筆劃的主要事件分別是 MouseLeftButtonDown、MouseMove 及 MouseLeftButtonUp。當 InkPresenter 收到 MouseLeftButtonDown 事件時,您必須在記憶體內建立新筆劃,然後將新筆劃加入到 InkPresenter 的 StrokeCollection 中。當滑鼠在 InkPresenter 內移動來建立 MouseMove 事件時,您必須將 StylusPoints 加入到該 Stroke 中。當使用者引發 MouseLeftButtonUp 事件 (藉由從數位板提起手寫筆或放開滑鼠按鈕) 時,您就必須完成筆劃。
Expression Blend 2.5 可以使用 [屬性] 視窗,讓事件和控制項的配對變得很容易。選取 inkP 控制項,然後按一下 [屬性] 視窗上方的事件圖示,即可檢視控制項的事件。接著,針對先前提到的三個事件處理常式,分別按兩下文字方塊。每個事件處理常式都有相對應的方法,會在 Visual Studio 中加入 XAML 控制項的程式碼後置 (Codebehind)。Visual Studio 與 Expression Blend 之間的整合是雙向的。Visual Studio 會要求您確認每次的變更;因此請留意在工作列上閃爍的 Visual Studio 圖示。
建立好這三個處理常式之後,請切換到 Visual Studio 來加入 [圖 5] 中的程式碼,這段程式碼可讓 InkPresenter 在使用者與控制項互動時,收集和顯示筆劃資料。此程式碼會執行先前安排的工作:建立新筆劃並加入手寫筆的點。手寫筆的點是透過 MouseLeftButtonDown 與 MouseMove 這兩個事件的 MouseEventArgs 來存取。
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 屬性,才能接收滑鼠事件。背景類似其他控制項的 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 是一個容器,但比較像畫布而非其他視覺元素 (例如矩形 (Rectangle))。您必須結合 InkPresenter 與其他元素,才能創造豐富的視覺效果;否則就無法發揮 Silverlight 的優勢。因此,請在 Expression Blend 中開啟現有的 XAML,如此一來,您就可以使用稍早加入的矩形 (Rectangle) 來建立 InkPresenter 的框線,好好妝點一番。
首先,我們要使矩形 (Rectangle) 框線的四個角變圓。選取矩形,將 RadiusX 和 RadiusY 屬性變更為 25。如此一來,矩形 (Rectangle) 的四個角就會變成滑順的圓角;但是,InkPresenter 的四個角會突出視覺框線外,而且可接受筆墨。解決方法是變更 (亦即裁剪) InkPresenter 的框線,以配合視覺框線。您可以使用 Silverlight 的裁剪功能,來調整 InkPresenter 的形狀。
Expression Blend 可以簡化元素的裁剪作業,以符合另一個元素的形狀。不過,在這麼做之前,請先建立 inkBorder 矩形 (Rectangle) 的副本,以供稍後使用。在 [物件與時間軸] 視窗中,以滑鼠右鍵按一下新的矩形 (Rectangle)。從它的內容功能表中,依序選取 [路徑] 和 [製作裁剪路徑]。Expression Blend 接著會顯示視窗,要求您選擇路徑所要裁剪的物件,換言之,就是選擇要採用矩形之形狀的物件。選取 InkPresenter。結果會發生兩件事:InkPresenter 現在會採用矩形 (Rectangle) 的形狀,而且新的矩形 (Rectangle) 會消失。當矩形 (Rectangle) 變成 InkPresenter 的裁剪路徑時,就不再是物件。現在您知道剛才為什麼要複製矩形 (Rectangle) 了吧。
產生的 XAML 看起來類似 [圖 7]。執行專案來測試 InkPresenter 的新邊緣。看起來會如 [圖 8] 所示。在 Silverlight 中可以使用任何形狀來當做裁剪路徑,因此您可以為 InkPresenter 建立任何所需的形狀。[圖 9] 顯示使用隨機形狀裁剪 InkPresenter 的範例。
<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-Order 是列在設計介面中的第一個控制項。否則它會放在 Rectangle 與 InkPresenter 上方,使兩者都隱藏起來。
接下來,請修改矩形 (Rectangle),為其指定黑色的背景。若要這麼做,其中一個方法是選取 [屬性] 視窗中的 [填滿筆刷],並將 R、G 及 B 值都設為 0。將矩形的不透明度變更為 10 %,藉此達到半透明的效果。
雖然您也可以設定 InkPresenter 上的背景色彩並指定透明度,但這種透明度也會影響到筆墨。我比較偏好讓 InkPresenter 的背景完全透明,並使用其他一些控制項來提供此效果。您可以變更控制項之背景色彩的 Alpha 值來實驗看看,然後與變更控制項之不透明度的效果互相比較,這麼做應該會很有趣。若使用 DrawingAttribute 的 Color 和 OutlineColor 屬性的 Alpha 屬性,也可以直接影響筆墨的透明度。這麼做的效果與 WPF InkCanvas 和 Tablet PC SDK 中的 DrawingAttribute.Transparency 完全一樣。[圖 10] 顯示 InkPresenter 與半透明矩形 (Rectangle) 的結合,這樣可以為您的繪圖背景提供不錯的視覺效果。
圖 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。MediaElement 不會像 Image 一樣地自動填入 InkPresenter。您必須手動調整大小,或將 Stretch="Fill" 直接加入 XAML。以下是在 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 的黑色筆劃。此值代表著與裝置無關像素 (Device Independent Pixel,DIP),而且無法設定成小於 2 的值。
在程式碼中,您可以建立方法和事件處理常式來影響各種筆劃屬性,例如 penWidth 及 penColor。比方說,稱為 currentColor 的變數可以讓控制項的 Click 事件變更它的值,然後 currentColor 就可以在建立新筆劃時,用於 inkP_MouseLeftButtonDown 事件中。
若要嘗試這麼做,請將變數宣告加入到類別中,並將預設值設為黑色,如下所示:
System.Windows.Media.Color currentColor =        Colors.Black;
在下列範例中,我建立了單一方法,這個方法可以當做數量不限之彩色矩形 (Rectangle) 的 MouseLeftButtonDown 事件。此方法會決定矩形 (Rectangle) 的色彩,接著使用該色彩來當做 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;
最後,您需要設法觸發變更。加入兩個 Rectangle 元素,將其中一個的 Fill 設為黑色,另一個的 Fill 設為紅色。每個 Rectangle 都需要 MouseLeftButtonDown 事件來呼叫 ChangeColor 方法。以下的 XAML 會建立兩個呈現為圓形的 Rectangle 物件:
<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,以便變更現有筆劃的色彩。
Stroke 物件的其中一個 DefaultDrawingAttributes 是 OutlineColor。如果您打算在彩色的背景 (例如影像) 上塗鴉,所描繪的筆墨如果有一致的外框色彩,會比較理想。您可以將程式碼加入到 inkP_MouseLeftButtonDown 事件中,藉此設定 newStroke 的 OutlineColor:
newStroke.DrawingAttributes.OutlineColor = 
  Colors.White;
[圖 12] 展示此概念。
圖 12 具有框線的筆墨 (Ink) (按一下影像以放大圖片)

手寫辨識
筆墨 (Ink) 在應用程式中有一個很棒的功能,就是手寫辨識。雖然手寫辨識打從一開始就是 Tablet PC SDK 的一部分,而且也是 WPF 的一項功能,但並非 Silverlight 的一部分。不過您還是可以在 Silverlight 應用程式中使用手寫辨識,方法是傳送筆劃資料到 ASP.NET Web 服務或 WCF 服務,這些服務將會執行辨識動作並傳回結果。
Microsoft Research 使用手寫範例來建立手寫辨識的演算法,而這些範例是取樣自一百萬名以上的民眾。您可能會很訝異,其實連字體字母比印刷體字母的辨識效果更好。許多其他語言 (包括各種亞洲語系) 也有提供手寫辨識引擎。
引擎會分析筆劃集合,來判斷筆劃代表的是單字、句子、段落或繪圖。辨識引擎可以辨別包含多個字詞的一組筆劃內的個別字詞,然後將字詞識別成一個單元。接著,引擎會使用它的範例與演算法,回應該字詞可能的選項範圍。如果您傳送一組字詞 (例如一個句子),那麼引擎將傳回一組字詞,還有一系列的替代選項。過去,辨識功能僅限於 Tablet PC。現在卻可以在支援 Silverlight 的任何電腦/瀏覽器組合上執行!

InkAnalyzer 類別
手寫辨識會由 System.Windows.Ink.InkAnalyzer 執行。此類別的用法出人意料的簡單。您只要傳遞筆劃集合給 InkAnalyzer 物件,呼叫其 Analyze 方法,然後使用稱為 Successful 的 Boolean 屬性來決定 Analyze 是否成功即可。如果 Successful 是 True,GetRecognizedString 方法便會傳回最佳的猜測結果,而 GetAlternates 方法則會傳回替代字串的陣列。
雖然 InkAnalyzer 類別並非 Silverlight API 的一部分,您還是可以使用 Web 服務或 WCF 服務,為您的 Silverlight 應用程式提供手寫辨識功能。Web 伺服器可以裝載 WPF API,並提供執行辨識所需的功能。不過這需要一些轉換。
首先,您必須在執行筆墨辨識 (Ink Recognition) 的所有元件中參考 [圖 13] 的 API。前兩個 API 已經可以從 Visual Studio 的 [加入參考] 介面使用。後兩個 API 則會與 Tablet PC SDK 一起安裝,您可以在 Program Files\Reference Assemblies\Microsoft\Tablet PC\v1.7 中找到。撰寫程式碼時,您需要避免 IACore 與 IAWinFX 命名空間之間的模擬兩可參考。最後,您還必須參考隨著 Tablet PC SDK 安裝的 IALoader.dll。若您執行的是 Windows Vista,可以在 C:\Program Files\Microsoft SDKs\Windows\v6.0\Bin 中找到此檔案。
API 功能
PresentationCore.dll 包含 System.Windows.Ink API。
WindowsBase.dll 包含 Collections 使用的功能。
IAWinFX.dll 將 InkAnalyzer 類別和功能加入到 System.Windows.Ink。
IACore.dll 經由 System.Windows.Ink.AnalysisCore 提供 InkAnalyzerBase 類別和功能。
為了要從 InkPresenter 經由網路傳輸 Stroke 給服務,筆劃必須先加以序列化。但是這些物件無法序列化。因此,您需要為 StrokeCollection 建立以字串為基礎的 XAML 表示。此字串接著就可以序列化並傳送給服務。
在 Silverlight 1.0 中,您必須使用 JavaScript 來建立此字串表示。在 Silverlight 2 中,您可以使用 LINQ to XML 的優勢 (如果您使用 Visual Basic,此處還能利用 XML 常值)。
[圖 14] 中顯示的程式碼可以切入 StrokeCollection 物件、讀取 DrawingAttributes、StylusPoints 以及其詳細資料,然後使用 LINQ to XML 來建立 XAML 表示。這段程式碼可用於數個用途,包括手寫辨識,即使辨識功能將忽略 DrawingAttributes。如果您是專為辨識用途而建立此方法,就應該排除收集 DrawingAttributes 的程式碼。
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;
}
請注意使用 Namespace 來建置 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 所用的像素值為基礎。即使存在這些差異,Stroke、StrokeCollection 及 StylusPoint 類別都位於 Silverlight 的相同名稱空間中,就像在 WPF 中的情況一樣。
[圖 15] 中的 CreateWPFStrokeCollectionfromXAML 會使用 LINQ to XML 來建立 Stroke 元素的集合,接著重複處理這些元素,以便為每個元素建立新的 Stroke 物件。此方法會再次使用 LINQ 來建立 StylusPoints 的集合,然後為每個筆劃建立 StylusPointsCollection。請注意,這並非 StrokeCollection 的完整重建,因為您不需要 DrawingAttributes (例如色彩) 來執行辨識功能。這兩個方法都依賴先前討論的 PresentationCore 與 WindowsBase API。StrokeCollection 一旦建立之後,就可以傳遞給執行分析的方法。
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;
}
[圖 16] 中的方法可以在 WCF 或 ASMX Web 服務中使用,藉此接受來自 InkPresenter 的 StrokeCollection,並傳回代表最佳猜測結果的字串。這項功能需要依賴 IAWinFX 和 IACore 組件。
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 用戶端
藉由包裝在 ASMX 或 WCF 服務裡的 RecognizeStrokes 方法,就可以輕易地從 Silverlight 應用程式內部呼叫 Web 服務方法。我在解決方案中,使用了 WCF 服務來提供手寫辨識功能。如果您需要關於 WCF 的協助,Silverlight.net 網站上有提供一個簡易的 QuickStart,其中會示範如何建立由 Silverlight 存取的 WCF 服務。
雖然在 Windows 的應用程式中,在繪製筆墨的同時即時執行辨識的做法很常見,不過在分散式應用程式中,更合理的做法是讓使用者明確要求辨識。因此,在設計介面上需要有一個控制項來啟動程序。在 XAML 中建立一個控制項,例如按鈕。您需要為按鈕的 Click 事件準備事件處理常式,以便觸發 GetXAMLfromStrokes 方法;然後傳送產生的 XAML 給 Web 服務以進行辨識。在我的 Web 服務中,這項作業稱為 Recognize。您還需要有 TextBlock 控制項來顯示傳回的字串。我將它稱為 RecoText。
將 WCF 服務參考加入到 Silverlight 應用程式時,Proxy 只會實作服務作業的非同步呼叫。因此,您需要一個方法來處理 RecognizeCompleted (請參閱 [圖 17])。如果註解寫成多行,就會以段落分隔符號來辨識註解,因此我使用 ScrollViewer 控制項來代替 RecoText 的 TextBox,如 [圖 18] 所示。
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 資料型別的優點 (亦即可以進行查詢與建立索引、支援結構描述,以及允許資料的修改),否則針對大型繪圖,nvarchar 或是 nvarchar(MAX) 等 SQL Server 型別就已經綽綽有餘。
在範例應用程式中,使用者可選擇各式各樣的影像。各個影像的註解都會儲存在使用影像唯一檔案名稱的資料庫中。選取影像時,即可從資料庫擷取並顯示其註解。

儲存和擷取註解
進行辨識時,只有單一字串會對服務來回傳遞。不過,在儲存和擷取資料時,您會傳遞 XAML 和其他與註解相關的中繼資料,例如建立日期、使用者,或註解所屬的影像及視訊參考。WCF 會使用 DataContracts 來管理訊息的這些不同元件。以屬於影像檔案的註解為例,我要建立一項作業來儲存影像路徑與 XAML,然後擷取特定檔案路徑的 XAML。
這個 WCF 服務會結合介面與屬性,來定義作業和資料合約。在 [圖 19] 中,您可以看到三項已定義的個別服務作業:StoreImageXAML、RetrieveImageXAML 及 Recognize。StoreImageXAML 會接受 ImageXAMLComposite 型別的參數,這已由 ImageXAMLComposite 類別定義成 DataContract。另外兩項作業比較單純,只會接收和傳回單一字串。服務類別會實作這三個方法,來呼叫 Helper 方法以進行辨識以及與資料庫互動。處理資料庫的這些方法會使用 LINQ to SQL (請參閱 [圖 20])。
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; }
    }
  }
}
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 以及與 InkPresenter 互動、如何執行手寫辨識,以及如何透過服務來儲存和擷取註解的 XAML 表示。即使您沒有 Tablet PC 或執行的並非 Windows Vista,仍然能夠使用此應用程式,而且可以透過滑鼠使用,只不過解析度會比較低。
另一個有趣的做法就是使用視訊。您可以使用 Silverlight 動畫和觸發程序來播放筆劃,不過要針對視訊來協調時間,會是一件具娛樂性的挑戰。
您可以從下列網址下載 Silverlight、SDK、Silverlight Tools Beta2 for Visual Studio 2008 以及 Expression Blend 2.5 June 2008 Preview:go.microsoft.com/fwlink/?LinkId=122132
特別感謝 Microsoft 的 Stefan Wick 在 Notebook、Tablet PC 及 UMPC Development MSDN® 論壇中提供的協助。他為本文提供了重要的指引。


Julia Lerman 是一位 .NET 顧問,有 20 年以上的軟體建置經驗。她在 .NET 社群中有相當的知名度,經常在會議中發表演說,也有出書,還是 Microsoft.NET MVP 以及 Vermont .NET User Group 的領導人。她即將出版的書籍標題為:《Programming Entity Framework》。Julia 的部落格位於 thedatafarm.com/blog

Page view tracker