Wicked Code
Silverlight 2 変換およびクリッピング領域
Jeff Prosise
このコラムは、Silverlight 2 のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。

目次
Silverlight は、機能性と浸透性が高い対話型のブラウザベース アプリケーションを作成するためのマイクロソフトの画期的なプラットフォームです。Silverlight 2 は、この記事の執筆時点ではベータ版ですが、この記事が発行されるまでにはリリースに近いものになっている予定です。Silverlight 2 では、マルチスレッド、ネットワーキング、ブラウザ統合、分離ストレージ、厳密な型指定、リフレクションなどがサポートされます。しかし、Silverlight はその魅力的なグラフィックスが最もよく知られています。
Silverlight 2 は、ベクタベースの XAML レンダリング エンジンをブラウザベース バージョンの CRL および Microsoft .NET Framework 基本クラス ライブラリと組み合わせています。XAML の実用的な知識を備えた開発者およびデザイナは、すばらしい機能を実現できます。Silverlight アプリケーションに搭載された多くの目を見張るビジュアル効果の鍵は、変換およびクリッピング領域です。たとえば、2008 年 5 月号の「Wicked Code」コラム (「
Silverlight のページめくりを簡単に」) で示したページめくりフレームワークは、変換およびクリッピング領域に大きく依存して、本や雑誌をめくるのと同様のページめくり効果をブラウザに作成しています。
これから説明する変換とクリッピングの助けを少々借りて、より優れた手法を実現できます。ただし最初に、ここで示すサンプルは、Silverlight 2 ベータ 2 で構築およびテストしたため、Silverlight 2 RC および RTM リリースで作業するには修正が必要な可能性があることに注意してください。
虫眼鏡で見る
Windows Presentation Foundation (WPF) プログラマは、VisualBrush を使用して図 1 に示すような仮想虫眼鏡を作成することがあります。Silverlight では、VisualBrush がサポートされません。このため、一部の開発者は、Silverlight アプリケーションで同様の効果を作成することが不可能であると信じ、公言さえしています。
図 1 実行中の Silverlight Magnifier (クリックすると拡大画像が表示されます)
さいわいにも、Silverlight では虫眼鏡をシミュレートできます。そのために必要なのは、変換およびクリッピング領域に若干工夫を凝らすことだけです。ここで示す Magnifier アプリケーションで、その方法を実証します。図 2 に、Magnifier の Page.xaml ファイルの簡略バージョンを示します。それぞれ図 1 に示すコンテンツのコピーを含んだ 2 つのほとんど同一のキャンバスを宣言します。最初のキャンバス (MainCanvas) は、通常表示されるキャンバスです。2 つ目のキャンバス (ZoomCanvas) には、最初は同じコンテンツが含まれていますが、4 の倍数ですべてを拡大する ScaleTransform も含まれています。

図 2 Page.xaml
<UserControl x:Class="Magnifier.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid x:Name="LayoutRoot" Background="Black"
MouseLeftButtonDown="OnMouseLeftButtonDown" MouseMove="OnMouseMove"
MouseLeftButtonUp="OnMouseLeftButtonUp">
<Canvas x:Name="RootCanvas" Width="800" Height="800">
<!-- Main canvas -->
<Canvas x:Name="MainCanvas" Canvas.Left="0" Canvas.Top="0"
Width="800" Height="900" Background="Black">
<Canvas Canvas.Left="90" Canvas.Top="30" Width="620" Height="470">
<Rectangle Canvas.Left="0" Canvas.Top="0" Width="620"
Height="470" Fill="White" />
<Image Canvas.Left="10" Canvas.Top="10" Width="600" Height="450"
Source="Images/BobCat.jpg" />
</Canvas>
<Canvas Canvas.Left="90" Canvas.Top="540">
<Line Canvas.Left="0" Canvas.Top="0" X1="0" Y1="0" X2="620"
Y2="0" Stroke="#808080" StrokeThickness="3"
StrokeDashArray="1,1" />
<TextBlock Canvas.Left="0" Canvas.Top="10" Foreground="White"
FontSize="36" Text="BVM BobCat" />
<TextBlock Canvas.Left="0" Canvas.Top="70" Foreground="White"
FontSize="12" TextWrapping="Wrap" Width="620" Text="..." />
<Line Canvas.Left="0" Canvas.Top="180" X1="0" Y1="0" X2="620"
Y2="0" Stroke="#808080" StrokeThickness="3"
StrokeDashArray="1,1" />
</Canvas>
</Canvas>
<!-- Zoom canvas -->
<Canvas x:Name="ZoomCanvas" Canvas.Left="0" Canvas.Top="0"
Width="800" Height="900" Background="Black" Visibility="Collapsed">
<Canvas.RenderTransform>
<ScaleTransform CenterX="0" CenterY="0" ScaleX="4" ScaleY="4"/>
</Canvas.RenderTransform>
<Canvas.Clip>
<EllipseGeometry x:Name="Lens" Center="0,0"
RadiusX="40" RadiusY="40" />
</Canvas.Clip>
...
<Path Canvas.Left="0" Canvas.Top="0" Stroke="#808080"
StrokeThickness="1">
<Path.Data>
<EllipseGeometry x:Name="LensBorder" Center="0,0"
RadiusX="40" RadiusY="40" />
</Path.Data>
</Path>
</Canvas>
</Canvas>
</Grid>
</UserControl>
ZoomCanvas は、通常は非表示になっていますが、マウスの左ボタンを押すと、OnMouseLeftButtonDown (図 3 を参照) が Visibility プロパティを切り替えることで ZoomCanvas を表示します。ZoomCanvas の全体が表示されることはありません。一方、その Clip プロパティは、事実上虫眼鏡として表示される完全な円の中にコンテンツを表示する EllipseGeometry で初期化されます。虫眼鏡 (実際にはクリッピング領域) のサイズを変更するには、EllipseGeometry の RadiusX プロパティと RadiusY プロパティを変更します。その場合、LensBorder という名前の EllipseGeometry の同じプロパティも一致するように変更する必要があります (EllipseGeometry は虫眼鏡の周りに枠を描画します)。

図 3 Page.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Magnifier
{
public partial class Page : UserControl
{
private bool _dragging = false;
private const double _scale = 4.0;
public Page()
{
InitializeComponent();
}
private void OnMouseLeftButtonDown(object sender,
MouseButtonEventArgs e)
{
double x = e.GetPosition(RootCanvas).X;
double y = e.GetPosition(RootCanvas).Y;
PositionLens(x, y);
ZoomCanvas.Visibility = Visibility.Visible;
((FrameworkElement)sender).CaptureMouse();
_dragging = true;
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (_dragging)
{
double x = e.GetPosition(MainCanvas).X;
double y = e.GetPosition(MainCanvas).Y;
PositionLens(x, y);
}
}
private void OnMouseLeftButtonUp(object sender,
MouseButtonEventArgs e)
{
if (_dragging)
{
ZoomCanvas.Visibility = Visibility.Collapsed;
((FrameworkElement)sender).ReleaseMouseCapture();
_dragging = false; }
}
private void PositionLens(double x, double y)
{
Lens.Center = LensBorder.Center = new Point(x, y);
ZoomCanvas.SetValue(Canvas.LeftProperty, (1 - _scale) * x);
ZoomCanvas.SetValue(Canvas.TopProperty, (1 - _scale) * y);
}
}
}
アプリケーションを実行し、シーンのどこかで (ページの下部にあるテキストの上でもかまいません) マウスの左ボタンをクリックしたままにすると、虫眼鏡の動作を確認できます。また、ボタンを押したままマウスを動かすと、虫眼鏡も移動します。これは、2 つの EllipseGeometry の Center プロパティを変更してレンズを再配置し、クリッピング領域に表示されているコンテンツが MainCanvas 内のカーソルの下にあるコンテンツと一致するように ZoomCanvas の Canvas.Left プロパティと Canvas.Top プロパティを調整することで実現できます。その厳密な動作を確認するには、OnMouseMove メソッドと PositionLens メソッドを参照してください。
アプリケーションを再生する場合、虫眼鏡はイメージ上に配置されたときにピクセルを拡大しないことに注意してください。より大きなイメージ詳細が表示されます。その理由は、イメージのネイティブ解像度が 2,400 x 1,800 ピクセルであるためです。イメージは、MainCanvas および ZoomCanvas で 600 の幅と 450 の高さで宣言されます。つまり、ネイティブ解像度のちょうど 4 分の 1 です。ただし、ZoomCanvas が ScaleTransform を使用してイメージを 4 倍に拡大すると、イメージがネイティブのサイズに戻ります。したがって、通常表示されるイメージは、実際の幅と高さの 4 分の 1 に圧縮されていますが、虫眼鏡を通して表示されるイメージは、ネイティブ解像度で表示されます。イメージのネイティブ サイズが 600 x 450 だった場合、拡大したビューはピクセル化されます。
回転する (3D)
Silverlight と WPF のもう 1 つの違いは、WPF が 3D グラフィックスをサポートすることです。ただし、それでも Silverlight 開発者がアプリケーションに 3D 効果を採用するのを止める理由にはなりません。3D サポートの欠落は、3D 効果を作成するために Silverlight プログラマの仕事が若干増えることを意味するだけです。このような効果を次の例で示します。
最初に表示されるときに、SpinAndZoom アプリケーションでは溶岩石から透明な海に飛び込む筆者の末娘の写真が表示されます。しかし、マウスの左ボタンを押したまま写真上でドラッグすると、写真が垂直軸で回転し、同じ溶岩構造の土台の海底にいる別の娘の写真が表示されます (図 4)。ところで、娘を常に水中に入れておくのは、男たちから隠しておくためのすばらしい方法です。
図 4 垂直軸で写真を回転する (3D) (クリックすると拡大画像が表示されます)
図 5 に、3D 回転の基礎となる XAML を示します。前面の写真とそのリフレクション用に 2 つ、背面の写真とそのリフレクション用に 2 つ、合計 4 つのイメージを宣言します。リフレクションは、ScaleTransform を使用してイメージの上下をひっくり返し、OpacityMask を LinearGradientBrush と組み合わせて、リフレクション イメージを距離の関数としてフェードすることで生成されます。回転は、ScaleTransform (SpinScaleTransform) および SkewTransform (SpinSkewTransform) を操作することでプログラムで実現できます。回転の角度が増加すると、SkewTransform は上下のエッジの角度を増やし、ScaleTransform はイメージを水平に圧縮します。正しく構成すると、結果は 3D 回転の模倣として納得のいくものになります。

図 5 Page.xaml
<UserControl x:Class="SpinAndZoom.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid x:Name="LayoutRoot" Background="Black"
MouseLeftButtonDown="OnMouseLeftButtonDown" MouseMove="OnMouseMove"
MouseLeftButtonUp="OnMouseLeftButtonUp">
<Canvas x:Name="SpinCanvas" Width="400" Height="300">
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="SpinScaleTransform" CenterX="200" />
<SkewTransform x:Name="SpinSkewTransform" CenterX="200" />
<ScaleTransform x:Name="ZoomScaleTransform" CenterX="200"
CenterY="150" />
</TransformGroup>
</Canvas.RenderTransform>
<!-- Front -->
<Image x:Name="Front" Source="Images/Abby.jpg" Canvas.Left="0"
Canvas.Top="0" Width="400" Height="300" Visibility="Visible"
Stretch="Fill" />
<!-- Front reflection -->
<Image x:Name="FrontReflection" Source="Images/Abby.jpg"
Canvas.Left="0" Canvas.Top="500" Width="400" Height="200"
Visibility="Visible" Stretch="Fill">
<Image.RenderTransform>
<ScaleTransform ScaleY="-1" />
</Image.RenderTransform>
<Image.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0.5" Color="#00000000" />
<GradientStop Offset="1" Color="#80000000" />
</LinearGradientBrush>
</Image.OpacityMask>
</Image>
<!-- Back -->
<Image x:Name="Back" Source="Images/Amy.jpg" Canvas.Left="0"
Canvas.Top="0" Width="400" Height="300" Visibility="Collapsed"
Stretch="Fill" />
<!-- Back reflection -->
<Image x:Name="BackReflection" Source="Images/Amy.jpg" Canvas.Left="0"
Canvas.Top="500" Width="400" Height="200" Visibility="Collapsed"
Stretch="Fill">
<Image.RenderTransform>
<ScaleTransform ScaleY="-1" />
</Image.RenderTransform>
<Image.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0.5" Color="#00000000" />
<GradientStop Offset="1" Color="#80000000" />
</LinearGradientBrush>
</Image.OpacityMask>
</Image>
</Canvas>
</Grid>
</UserControl>
該当するコードはダウンロードできますが、図 6 にその抜粋を示します。主要メソッドである RotateTo は、MouseMove ハンドラによって呼び出され、マウス カーソルがシーン上を移動するときに回転の角度を増減します。RotateTo では、若干の三角法を使用して、図 7 のジオメトリに基づいて SkewTransform パラメータと ScaleTransform パラメータを計算します。回転の角度が指定されると、RotateTo は紫色で強調された三角形の幅と内角 (中心に最も近い角度) を計算します。幅は、ScaleTransform の ScaleX プロパティに設定する値を RotateTo に示し、角度は、SkewTransform の AngleY プロパティに設定する値を示します。

図 6 Page.xaml.cs からの RotateTo
private void RotateTo(double angle)
{
double radians = (angle * Math.PI) / 180;
double scaleX = Math.Abs(Math.Cos(radians));
// Update the ScaleTransform
// Avoid ScaleX == 0 to prevent images from disappearing!
SpinScaleTransform.ScaleX = Math.Max(0.005, scaleX);
// Update the SkewTransform
if (angle != 90 && angle != 270) // Tangent undefined
{
double h = Math.Sin(radians) * _aspect;
double r = Math.Atan(h / scaleX);
if (angle > 90 && angle < 270)
r = -r;
SpinSkewTransform.AngleY = (r * 180) / Math.PI;
}
}
図 7 3D 回転のジオメトリ (クリックすると拡大画像が表示されます)
表示の効果を高めるための鍵は、回転イメージの頂点が架空の円柱で定義された軌道をたどっていることです。円柱がユーザーに対してどの程度傾けられるかは、_aspect という名前のプライベート フィールドで制御されます。_aspect の値を増やすことで、円柱の度数を上げることができます。
また、SpinAndZoom は、Silverlight アプリケーションに見られる別の一般的なビジュアル効果の実装方法も示します。それは対話型ズームです。図 8 に示す OnMouseWheelTurned というイベント ハンドラは、ZoomScaleTransform という名前の ScaleTransform を操作することでマウスホイールの移動に応答します。この変換は、シーン内のすべての XAML オブジェクトを拡大/縮小します (試してみてください。シーンのどこかにマウス カーソルを置き、マウス ホイールを前後に回転します)。

図 8 Page.xaml.cs からの OnMouseWheelTurned
private void OnMouseWheelTurned(Object sender, HtmlEventArgs args)
{
double delta = 0;
ScriptObject e = args.EventObject;
if (e.GetProperty("wheelDelta") != null) // IE and Opera
{
delta = ((double)e.GetProperty("wheelDelta"));
if (HtmlPage.Window.GetProperty("opera") != null)
delta = -delta;
}
else if (e.GetProperty("detail") != null) // Mozilla and Safari
{
delta = -((double)e.GetProperty("detail"));
}
if (delta > 0)
{
if (ZoomScaleTransform.ScaleX < _max)
{
// Zoom in
ZoomScaleTransform.ScaleX += 0.1;
ZoomScaleTransform.ScaleY += 0.1;
}
}
else if (delta < 0)
{
if (ZoomScaleTransform.ScaleX > _min)
{
// Zoom out
ZoomScaleTransform.ScaleX -= 0.1;
ZoomScaleTransform.ScaleY -= 0.1;
}
}
if (delta != 0)
{
args.PreventDefault();
e.SetProperty("returnValue", false);
}
}
Silverlight はマウスホイール イベントを起動しないため、SpinAndZoom は、Silverlight 2 ではアンマネージ ブラウザ DOM イベントに対してマネージ イベント ハンドラを登録できるという事実を利用して、ブラウザのマウスホイール イベントに応答します。登録は Page コンストラクタで行われ、同じハンドラが 3 回登録されてブラウザ間の違いに対応します。ブラウザによってマウスホイール イベントのレポート方法が大きく異なるため、OnMouseWheelTurned には、マウスホイールの移動方向を検出するための独自の機能があります。
ライブ ビデオを簡単にオーバーレイする
最後の例は、他の例ほど派手ではありませんが、並外れた手段を必要とする可能性のある問題に対する単純でコストの低いソリューションを示します。
筆者は最近、Windows Media Player を使用してライブ イベントをストリーミングしているマイクロソフトの顧客に会いました。この顧客は、Silverlight のクロスプラットフォーム機能を使用して、自分の顧客基盤を Windows 以外のユーザーに拡大することに関心を持っていました。余談ですが、Windows Media Server を使用すれば、ビデオ画像にライブ オーバーレイを埋め込むことができるかという質問を彼から受けました。彼は、テレビのニュース チャンネルの画面の下に水平にスクロール表示されるティッカー行のようなものを望んでいたのです。
Silverlight を使用すれば、ビデオ オーバーレイを実行するために、サーバー上に高価なハードウェアやソフトウェアは必要ないことを彼に説明しました。代わりに、Silverlight のネットワーキング スタックを使用して、ビデオ ストリームの帯域外でフィードを取得し、XAML を使用して Silverlight MediaElement の上にフィードを表示できます。図 9 のアプリケーションで、その方法を示します。
図 9 MediaElement 上に配置されたヘッドラインをスクロール表示する
このアプリケーションでは、MediaElement を使用して、サーバーからダウンロードした WMV (Windows Media Video) ファイルを再生します。ビデオの帯域外で、WebClient オブジェクトを使用して FeedBurner.com からニュース フィードをフェッチし (ドメイン間アクセスを可能にする XML ポリシー ファイルがあります)、SyndicationFeed オブジェクトを使用してフィードを解析し、ニュース ヘッドラインの文字列を生成します。次に、TextBlock と Rectangle から構成されるオーバーレイに文字列を表示します。0.5 の透明度を使用して、オーバーレイによって下のビデオが完全に隠されるのを防ぎます。単純なアニメーションで TextBlock を水平にスクロールし、クリッピング領域で Rectangle の外部のテキストをすべて切り取ります (図 10 を参照)。

図 10 Page.xaml
<UserControl x:Class="VideoOverlay.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid x:Name="LayoutRoot" Background="Black">
<Canvas Width="720" Height="480">
<MediaElement x:Name="Player" Source="Videos/CRCC Jet Fly.wmv"
Width="720" Height="480" MediaEnded="MediaElement_MediaEnded" />
<Canvas Canvas.Top="448" Width="720" Height="32" Opacity="0.5">
<Canvas.Clip>
<RectangleGeometry Rect="0,0,720,32" />
</Canvas.Clip>
<Rectangle x:Name="Marquee" Width="720" Height="32" Fill="Black" />
<TextBlock x:Name="Headlines" Canvas.Left="800" Canvas.Top="8"
Foreground="White" FontSize="14">
<TextBlock.Resources>
<Storyboard x:Name="TickerStoryBoard"
Completed="TickerStoryBoard_Completed">
<DoubleAnimation x:Name="TickerAnimation"
Storyboard.TargetName="Headlines"
Storyboard.TargetProperty="(Canvas.Left)" />
...
</UserControl>
XAML 分離コードの各部分を
図 11 に示します (
コード ダウンロードには完全なコードが含まれています)。Page コンストラクタは WebClient.OpenReadAsync を呼び出して、ニュース フィードの非同期要求を起動し、完了イベント ハンドラはフィードから TextBlock にヘッドラインをコピーします。次に、Storyboard.Begin を呼び出して TextBlock のスクロールを開始し、Tick イベントを 5 分おきに起動するように DispatcherTimer をプログラムします。Tick イベント ハンドラは、ニュース フィードの更新要求を発行します。これは最終的にプライベート フィールドに格納されます。

図 11 Page.xaml.cs
using System;
namespace VideoOverlay
{
public partial class Page : UserControl
{
private const double _offset = 20.0;
private const double _secondsPerFrame = 10.0;
private const string _separator = " ? ";
private readonly Uri _uri =
new Uri("http://feeds.feedburner.com/AbcNews_TopStories");
private DispatcherTimer _timer = new DispatcherTimer();
private string _text = null;
public Page()
{
InitializeComponent();
// Launch an async request for current news headlines
WebClient wc = new WebClient();
wc.OpenReadCompleted += new
OpenReadCompletedEventHandler(OnInitialDownloadCompleted);
wc.OpenReadAsync(_uri);
}
private void OnInitialDownloadCompleted(object sender,
OpenReadCompletedEventArgs e)
{
if (e.Error == null)
{
// Convert the news content into a string
Headlines.Text = GetHeadlinesFromSyndicationStream(e.Result);
// Begin scrolling headlines
StartTicker();
// Start the refresh timer
_timer.Tick += new EventHandler(OnTimerTick);
_timer.Interval = new TimeSpan(0, 5, 0); // 5 minutes
_timer.Start();
}
}
private void OnTimerTick(object sender, EventArgs e)
{
// Launch an async request for current news headlines
WebClient wc = new WebClient();
wc.OpenReadCompleted += new
OpenReadCompletedEventHandler(OnRefreshDownloadCompleted);
wc.OpenReadAsync(_uri);
}
private void OnRefreshDownloadCompleted(object sender,
OpenReadCompletedEventArgs e)
{
if (e.Error == null)
{
// Convert the news content into a string and store it away
_text = GetHeadlinesFromSyndicationStream(e.Result);
}
}
...
}
}
ヘッドラインをスクロールするアニメーションが終了するたびに、Storyboard.Completed イベント ハンドラはそのフィールドで最新のニュース フィードをチェックし、新しいコンテンツが使用可能な場合は TextBlock のコンテンツを更新します。次に、アニメーションの実行を再び開始します。続いて、ヘッドラインが連続ループで画面にスクロール表示され、5 分おきにヘッドラインが更新されます。重要なのは、スレッド同期ロジックやアプリケーションの UI スレッドへのコールバックのマーシャリングが不要なことです。その理由は、DispatcherTimer.Tick イベント ハンドラと Storyboard.Completed イベント ハンドラがどちらも UI スレッドで実行されるためです。
このアプローチの欠点は、ニュース フィードのフェッチと更新によって追加のネットワーク トラフィックが生成されることです。ただし、トラフィックはビデオ ストリーム自体に比べるとわずかであり、ライブ ビデオ ストリームを変更するためにサーバー上に高価なハードウェアやソフトウェアは不要です。さらに、クライアントにレンダリングされるニュース フィードは、ヘッドラインに関する追加情報を表示するマウスオーバーや、ヘッドラインをニュース ページにリンクするハイパーリンクなどの追加の UX 効果で拡張できます。ユーザーに提示されるコンテンツが単なるビデオ ストリームのビットではなく XAML DOM の一部である場合は、可能性は無限です。
変換およびクリッピング領域は、XAML で自由に使用できる最も強力なツールです。これらのツールがないと、Silverlight のグラフィックス サブシステムの魅力は大幅に低下します。筆者は、回転木馬、ローロデックススタイルのページング、対話型チャートなどの効果を実装する方法について最近多くの質問を受けます。今後の Wicked Code のコラムで、より魅力的な Silverlight のグラフィカル機能を探してください。
Jeff Prosise は、MSDN Magazine に寄稿している編集者で、『プログラミング Microsoft .NET』など数冊の著書があります。また、ソフトウェアのコンサルティングおよび Microsoft .NET 専門の教育機関である Wintellect (
www.wintellect.com) の共同創立者でもあります。