이 칼럼은 Silverlight 2 시험판 버전을 기준으로 합니다. 여기에 포함된 모든 정보는 변경될 수 있습니다.
Silverlight는 고도로 기능적이고 몰입도가 높은 대화형 브라우저 기반 응용 프로그램을 만들기 위한 Microsoft의 혁신적인 플랫폼입니다. 기사 작성 시점을 기준으로 베타 단계에 있지만 곧 출시될 Silverlight 2는 다중 스레딩, 네트워킹, 브라우저 통합, 격리된 저장소, 강력한 형식, 리플렉션 등 풍부한 기능을 지원합니다. 그러나 Silverlight에 대해 가장 잘 알려진 부분은 멋진 그래픽입니다.
Silverlight 2는 벡터 기반 XAML 렌더링 엔진을 브라우저 기반 버전의 CLR 및 Microsoft .NET Framework 기본 클래스 라이브러리와 결합합니다. XAML에 대한 지식을 갖춘 개발자와 디자이너는 멋진 그래픽을 구현할 수 있습니다. Silverlight 응용 프로그램의 멋진 시각 효과 중에는 변형과 클리핑 영역을 활용한 것이 많습니다. 예를 들어 2008년 5월호 Wicked Code 칼럼("Silverlight를 사용한 간단한 페이지 넘기기 구현")에서 소개한 페이지 넘기기 프레임워크에서는 책이나 잡지를 넘기는 것과 같은 페이지 넘기기 효과를 브라우저에서 구현하기 위해 변형과 클리핑 영역을 주로 사용했습니다.
지금부터 살펴보겠지만 변형과 클리핑을 활용하면 더욱 다양하고 멋진 효과를 만들 수 있습니다. 여기에서 소개하는 샘플은 Silverlight 2 베타 2로 작성 및 테스트되었으므로 Silverlight 2 RC 및 RTM 릴리스를 사용하는 경우에는 수정이 필요할 수 있습니다.
돋보기 효과 만들기
WPF(Windows Presentation Foundation) 프로그래머는 종종 VisualBrush를 사용하여 그림 1에 나와 있는 것과 같은 가상 돋보기를 만듭니다. Silverlight는 VisualBrush를 지원하지 않기 때문에 일부 개발자는 Silverlight 응용 프로그램에서 이러한 효과를 만드는 것이 불가능하다고 생각하고 그렇게 단언하기도 합니다.
그림 1 Silverlight 돋보기 실행 화면 (더 크게 보려면 이미지를 클릭하십시오.)
그러나 약간의 재간만 있으면 변형과 클리핑 영역을 활용하여 Silverlight에서도 돋보기 효과를 모방할 수 있습니다. 여기에 나온 Magnifier 응용 프로그램을 통해 그 방법을 볼 수 있습니다. 그림 2는 Magnifier의 Page.xaml 파일을 요약한 형태입니다. 이 파일은 각각 그림 1에 나온 콘텐츠의 복사본을 포함하는 거의 동일한 두 개의 캔버스를 선언합니다. 첫 번째 캔버스인 MainCanvas는 사용자가 일반적으로 보는 캔버스입니다. 두 번째 캔버스인 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>
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 참조)에 의해 ZoomCanvas의 Visibility 속성이 전환되어 ZoomCanvas가 표시됩니다. 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);
}
}
}
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);
}
}
}
응용 프로그램을 실행하고 화면 아무 곳에서나(페이지 아래쪽의 텍스트도 포함) 왼쪽 마우스 단추를 누르고 있으면 돋보기가 작동하는 모습을 확인할 수 있습니다. 마우스 단추를 누른 상태로 마우스를 움직이면 돋보기가 움직이는 효과도 확인할 수 있습니다. 이러한 효과는 두 EllipseGeometry의 Center 속성을 수정하여 렌즈 위치를 변경하고, ZoomCanvas의 Canvas.Left와 Canvas.Top 속성을 조정하여 클리핑 영역을 통해 노출되는 콘텐츠가 MainCanvas의 커서 아래에 있는 콘텐츠와 일치하도록 함으로써 구현됩니다. OnMouseMove와 PositionLens 메서드를 보면 정확한 작동 원리를 알 수 있습니다.
응용 프로그램을 실행해 보면 돋보기가 이미지의 픽셀을 확대하는 것이 아니라, 이미지를 더 높은 해상도로 보여 준다는 것을 알 수 있습니다. 이것은 이미지의 원래 해상도가 2,400 x 1,800 픽셀이기 때문입니다. MainCanvas와 ZoomCanvas에서 이미지는 원래 해상도의 정확히 1/4인 가로 600 픽셀과 세로 450 픽셀로 선언됩니다. 그러나 ZoomCanvas가 ScaleTransform을 사용하여 이미지를 4배로 확장하면 이미지는 원래의 크기로 되돌아갑니다. 따라서 사용자가 통상적으로 보는 이미지는 실제 너비와 높이의 1/4로 압축된 이미지이며 돋보기를 통해 보이는 이미지는 원래 해상도로 표시됩니다. 이미지의 원래 크기가 600 x 450이라면 확대된 이미지는 픽셀을 잡아 늘린 형태가 될 것입니다.
3D로 사진 회전시키기
Silverlight와 WPF의 또 다른 차이는 후자가 3D 그래픽을 지원한다는 데 있습니다. 그러나 Silverlight 개발자도 응용 프로그램에 3D 효과를 사용할 수 있습니다. 3D는 지원되지 않지만 이 부분은 Silverlight 프로그래머가 약간의 추가 작업으로 보완할 수 있습니다. 이러한 효과 중 하나를 다음 예에서 살펴보겠습니다.
SpinAndZoom 응용 프로그램을 처음 실행하면 필자의 막내딸이 바위에서 맑은 바다로 뛰어드는 사진이 표시됩니다. 왼쪽 단추를 누른 상태로 마우스를 끌면 사진이 세로축을 따라 회전하면서 필자의 다른 딸이 같은 화산암 지대의 바다 밑바닥을 유영하는 사진이 표시됩니다(그림 4 참조). 참고로 바닷속에서 놀게 하는 것은 남자애들로부터 딸들을 감추는 좋은 방법입니다.
그림 4 세로축을 따라 3D로 사진 회전시키기 (더 크게 보려면 이미지를 클릭하십시오.)
그림 5에는 3D 회전을 수행하는 XAML이 나와 있습니다. 이 XAML은 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>
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 속성에 설정할 값을, 각도는 SkewTransform의 AngleY 속성에 설정할 값을 각각 RotateTo에 알려 줍니다.
그림 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;
}
}
{
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);
}
}
{
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를 사용하여 라이브 이벤트를 스트리밍하는 Microsoft 고객을 만났습니다. 이 고객은 Silverlight의 플랫폼 호환성을 활용하여 Windows 이외의 사용자까지 고객 기반을 확대하는 방법을 모색하고 있었습니다. 이 고객은 필자에게 Windows Media Server에서 비디오 피드에 라이브 오버레이를 삽입할 수 있는지를 물었습니다. 그는 텔레비전 뉴스 채널의 화면 아래쪽을 가로지르는 자막 뉴스 효과를 원했습니다.
필자는 Silverlight를 사용하면 서버에 고가의 하드웨어나 소프트웨어를 추가하지 않고도 비디오 오버레이를 구현할 수 있다는 사실을 설명했습니다. Silverlight의 네트워킹 스택을 사용하여 비디오 스트림이 포함된 대역 외에서 피드를 가져온 다음 XAML을 사용하여 Silverlight MediaElement 위에 피드를 표시할 수 있습니다. 그림 9의 응용 프로그램이 이러한 방법을 보여 줍니다.
그림 9 MediaElement 위에 표시되는 스크롤되는 헤드라인
이 응용 프로그램은 서버에서 다운로드한 WMV(Windows Media Video) 파일을 MediaElement를 사용하여 재생합니다. 응용 프로그램은 비디오 대역 외에서 WebClient 개체를 사용하여 FeedBurner.com(여기에는 도메인 간 액세스를 허용하는 XML 정책 파일이 있음)으로부터 뉴스 피드를 가져오고 SyndicationFeed 개체를 사용하여 피드를 구문 분석한 다음 뉴스 헤드라인 문자열을 생성합니다. 그런 다음 오버레이가 그 아래쪽의 비디오를 완전히 가리지 않도록 불투명도 0.5를 사용하여 TextBlock과 Rectangle로 구성된 오버레이에 문자열을 표시합니다. 간단한 애니메이션이 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>
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>
그림 11에는 XAML 코드 숨김의 일부가 나와 있습니다. 전체 코드는 코드 다운로드에 포함되어 있습니다. Page 생성자는 WebClient.OpenReadAsync를 호출하여 뉴스 피드에 대한 비동기 요청을 시작하며, 완료 이벤트 처리기는 피드에서 TextBlock으로 헤드라인을 복사합니다. 그런 다음 Storyboard.Begin을 호출하여 TextBlock 스크롤을 시작하며 5분마다 Tick 이벤트를 발생시키도록 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);
}
}
...
}
}
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에게 질문이나 의견이 있으면 wicked@microsoft.com으로 보내시기 바랍니다.
Jeff Prosise는 MSDN Magazine 편집자이며 Programming Microsoft .NET을 비롯한 여러 권의 책을 집필했습니다. 또한 Microsoft .NET을 전문적으로 다루는 소프트웨어 컨설팅 및 교육 업체인 Wintellect(www.wintellect.com)의 공동 설립자이기도 합니다.
[출처] MSDN Magazine
[출처] MSDN Magazine