본문 바로가기

silverlight

Make a Deepzoom Solution

Deepzoom 에 관한 기초 지식을 기반으로 간략한 Test 솔루션을 만들어 보겠습니다.

Test 솔루션에서 구현 될 기능 리스트 입니다.
1. Mouse Left Button Click 시 Deepzoom Image 확대.
2. Shift + Mouse Left Button Click 시 Deepzoom Image 축소.
3. MouseWheelHelper를 사용한 Wheel 지원
4. Pan 기능(Mouse Drag를 통한 Deepzoom Image 이동).

Deepzoom Test Solution 을 하나 생성합니다.(Web Project 도 함께 추가합니다.)


Deepzoom Composer 를 이용한 Deepzoom Image 피라미드를 ClientBin 폴더에 복사합니다.

Deepzoom Composer를 통해 산출 된 Image 피라미드는 MultiScaleImage의 Source가 됩니다.

MainPage를 Design하고 MultiScaleImage 를 Layout Panel 안에 배치합니다. Layout Panel 에 배치 한 뒤, MultiScaleImage 의 Source를 지정합니다. MultiScaleImage의 Source는 Deepzoom Composer로 산출 된 dzc_output.xml 파일을 지정하시면 됩니다.
<MultiScaleImage x:Name="myMSI" Grid.Column="1" Grid.Row="1" Source="dzc_output.xml"/>

사실, 이 작업까지 한다면 "심플한" Deepzoom 솔루션을 위한 준비는 끝났습니다. 추가로 작업하실 부분은 이미지 축소, 확대, 이동 등에 대한 기능 구현이겠죠^^ 그럼, 기능 구현을 한 번 해보겠습니다.

1. Mouse Left Button Click 시 Deepzoom Image 확대.
유저 입장에서 Click이란 Mouse를 손가락으로 따닥 하는 것입니다. 하지만 "따닥" 이란 Mouse Button을 Down, Up 하는 과정이겠죠. Click시 Deepzoom Image 확대 라는 기능을 구현 시, Down에 확대 될 것인가 혹은, Up에 확대 될 것인가를 명확히 구분해야 한다는 뜻입니다. (이 예제에서는 Mouse Left Button을 Down 하여, Drag 할 경우 Deepzoom Image가 Mouse를 이동하는 방향과 같이 움직이게 구현 할 것이기 때문에, Down시 확대를 하지 않고, Up이 될 경우에 확대를 해야 합니다.)

확대 혹은 축소는, MultiScaleImage.ZoomAboutLogicalPoint 메소드를 통해서 가능합니다.
ZoomAboutLogicalPoint는 3개의 파라미터를 인자(double ZoomFactor, double LogicalXPoint, double LogicalYPoint) 로 요구합니다. 여기서 ZoomFactor 란, 이미지의 확대/축소를 결정하는 값입니다. 초기 값은 1이며, 1보다 큰 숫자이면 확대됩니다. 1보다 작으면 축소가 됩니다. 0 이하의 값을 사용하면 오류가 반환되고 확대/축소 변경이 적용되지 않습니다.

MultiScaleImage.MouseLeftButtonUp 이벤트의 Argument로 오는 MouseButtonEventArgs 에는 GetPosition 메소드가 있어 UIElement를 인자로 넘겨주면, UIElement 상에서 현재 Mouse 의 위치를 ElementPoint로 가져오게 됩니다. 이 ElementPoint를 LogicalPoint로 변경 한 뒤, ZoomAboutLogicalPoint 함수를 실행하면 됩니다.
<코드 1> 
this.myMSI.MouseLeftButtonUp += (sender, e) =>
{
    Point log_mousePosition = this.myMSI.ElementToLogicalPoint(e.GetPosition(this.myMSI)); 

    if (this.shiftKeyPressed)
    {
        this.myMSI.ZoomAboutLogicalPoint(0.7, log_mousePosition.X, log_mousePosition.Y);
    }
    else
    {
        this.myMSI.ZoomAboutLogicalPoint(1.3, log_mousePosition.X, log_mousePosition.Y);
    }
};


2. Shift + Mouse Left Button Click 시 Deepzoom Image 축소.
모든 UIElement 는 KeyDown, KeyUp 이벤트를 지원합니다. 하지만, 몇 개의 엘리먼트는 KeyDown, KeyUp 이벤트가 지원되지 않는데요, 그 이유는 KeyDown, KeyUp 이벤트는 엘리먼트가 포커싱 되 있는 경우에만 가능하기 때문에, 포커싱 될 수 없는 엘리먼트는 KeyDown, KeyUp 이벤트가 지원되지 않는 것입니다. MultiScaleImage도 그 중 하나입니다. 그래서, Shift+Click 으로 이미지의 축소를 구현하기 위해서는 MultiScaleImage가 배치 된 Layout Panel에 KeyDown, KeyUp 이벤트를 구현하여, Shift가 눌렸는지 확인해야 합니다.
Member변수로 Boolean 타입의 shiftKeyPressed 를 선언 한 뒤, MultiScaleImage가 배치 된 Layout Panel 에서 KeyDown 시 Shift 키가 눌렸는지 확인하여 Shift 가 눌리면 true로, 아니면 false를 할당하고, KeyUp 시에는 false를 할당합니다.
<코드 2> 
this.KeyDown += (sender, e) =>
{
    if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
        this.shiftKeyPressed = true;
    else
        this.shiftKeyPressed = false;
}; 

this.KeyUp += (sender, e) => this.shiftKeyPressed = false;


이렇게 맴버 변수 shiftKeyPressed를 관리하게 되면, MouseLeftButtonUp(이미지 확대, 축소가 구현 될 이벤트)에서 shiftKeyPressed 의 상태에 따라 true면 축소를, false이면 확대를 해 주면 됩니다. (<코드 1> 참조)

3. MouseWheelHelper를 사용한 Wheel 지원
Deepzoom Composer 최신 버전으로 Export 하시게 되면, MouseWheelHelper가 포함 되 있습니다. 혹시, 없으시면 => 다운
MouseWheelHelper를 Silverlight 솔루션에 추가 하신 뒤, Namespace명을 솔루션에 맞게 수정하신 뒤 사용하시면 됩니다.

그런 다음, MultiScaleImage 가 있는 MainPage 비하인드 코드에서, MouseWheelHelper 인스턴스를 생성 한 뒤, Moved 이벤트를 구현하시면 됩니다. Moved 이벤트는 MouseWheel 을 상하로 움직이는 경우에 발생하는 이벤트입니다.
MouseWheelEventArgs에 Deta Property가 있습니다. 이 Delta 값을 이용하여 MouseWheel이 위로 움직여졌는지, 아래로 움직여졌는지 확인 할 수 있습니다. Delta 값이 0보다 크면 위로, 0보다 작으면 아래 방향으로 MouseWheel이 동작 된 것입니다.
<코드 3> 
new MouseWheelHelper(this.myMSI).Moved += (sender, e) =>
{
    Double zoomFactor = 0;
    if (e.Delta > 0)
        zoomFactor = 1.3;
    else
        zoomFactor = 0.7; 

    Point log_mousePosition = this.myMSI.ElementToLogicalPoint(this.currentMousePosition);
    this.myMSI.ZoomAboutLogicalPoint(zoomFactor, log_mousePosition.X, log_mousePosition.Y);
};


<코드 3> 에서 ZoomAboutLogicalPoint 메소드를 실행하기 위해, 맴버 변수인 currentMousePosition을 LogicalPoint로 변환 하였습니다. (currentMousePosition의 관리는 MouseMove 이벤트에서 관리 하면 되겠죠^^)

4. Pan 기능(Mouse Drag를 통한 Deepzoom Image 이동).
MouseLeftButtonDown 후, Drag 를 통하여 Deepzoom Image를 이동하는 동작을 구현 해 보겠습니다.
우선, MouseMove 시 Image를 이동시켜야 하는데, Drag모드 인지 아닌지를 구별할 필요가 있기 때문에 맴버변수로 Boolean 타입의 dragging 을 선언합니다. Drag는 MouseLeftButton을 Down한 뒤 Move하는 동안 발생하는 것이기 때문에, dragging 변수의 관리는 MouseLeftButtonDown에서 true로 MouseLeftButtonUp에서 false로 해 주면 됩니다.
dragging 변수의 관리가 되 있다면, MouseMove시 dragging 인지를 확인한 뒤, Drag 모드 에서만 Image를 Move 해 주면 되는거죠.
<코드 4>
this.myMSI.MouseMove += (sender, e) =>
{
    this.currentMousePosition = e.GetPosition(this.myMSI);

    if (this.dragging)
    {
        Point newOrigin = new Point();

        // dragOffset = MouseLeftButtonDown 된 지점
        newOrigin.X = currentViewportOrigin.X - (((currentMousePosition.X - dragOffset.X) / this.myMSI.ActualWidth) * this.myMSI.ViewportWidth);

        newOrigin.Y = currentViewportOrigin.Y - (((currentMousePosition.Y - dragOffset.Y) / this.myMSI.ActualHeight) * this.myMSI.ViewportWidth);

        this.myMSI.ViewportOrigin = newOrigin;
    }
};



몇 가지 추가적으로 처리 해 줘야 하는 것이 있습니다. 사용자가 MultiScaleImage 상에서 MouseLeftButton을 Down한 뒤, Move하여 이미지를 이동하다가 Mouse가 MultiScaleImage 밖으로 나가게 되면, 이미지의 이동이 끝나야 하는데도 계속 마우스를 쫓아서 움직이게 됩니다. 더군다나, 그 상태에서 마우스가 MultiScaleImage 안으로 다시 들어오면 Drag 상태가 계속 지속됩니다. 이런 경우를 대비하기 위해서 MouseLeftButtonDown 시 MultiScaleImage.CaptureMouse 함수를 실행하고, MultiScaleImage에서 MouseLeave시 MultiScaleImage.ReleaseMouseCapture 함수를 실행합니다. 그리고 MouseLeave에서 추가적으로 dragging 맴버 변수를 false 로 할당해야 되겠죠.
<코드 5>
this.myMSI.MouseLeftButtonDown += (sender, e) =>
{
    this.myMSI.CaptureMouse();
    .
    .
    .
};

this.myMSI.MouseLeave += (sender, e) =>
{
    this.myMSI.ReleaseMouseCapture();
    if (this.dragging) this.dragging = false;
};


<코드 5> 처럼 처리하면, Drag 모드에서 마우스가 MultiScaleImage 밖으로 나가게 되면, Drag모드에서 벗어나게 되고, MouseCapture도 Release 됩니다. 혼란스러운 동작을 조금이나마 명확하게 없애주게 됩니다^^



이상 "간략한" Deepzoom Solution 예제의 전문입니다.
<Xaml - MainPage.xaml> 
<UserControl x:Class="DeepzoomSolution.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Width="600" Height="500">
    <Grid x:Name="LayoutRoot" Background="#444444">
        <Grid.RowDefinitions>
            <RowDefinition Height="0.08*"/>
            <RowDefinition Height="0.768*"/>
            <RowDefinition Height="0.064*"/>
            <RowDefinition Height="0.088*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.08*"/>
            <ColumnDefinition Width="0.84*"/>
            <ColumnDefinition Width="0.08*"/>
        </Grid.ColumnDefinitions>
        <MultiScaleImage x:Name="myMSI" Grid.Column="1" Grid.Row="1" Source="dzc_output.xml"/>
        <Slider x:Name="mySlider" Margin="8,8,8,8" Grid.Column="1" Grid.Row="2"/>
        <StackPanel Grid.Column="1" Grid.Row="3" VerticalAlignment="Top" Height="47" >
            <TextBlock Text="{Binding Value, ElementName=mySlider}"/>
            <Button x:Name="testButton" Margin="8,0,0,0" Width="50" Content="Click"/>
        </StackPanel>
    </Grid>
</UserControl>

<C# Code - MainPage.xaml.cs>
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Diagnostics;

namespace DeepzoomSolution
{
    public partial class MainPage : UserControl
    {
        private Boolean shiftKeyPressed = false;
        private Point currentMousePosition = new Point();
        private Point currentViewportOrigin = new Point();
        private Point dragOffset = new Point();
        private Boolean dragging = false;
        private Boolean moved = false

        public MainPage()
        {
            InitializeComponent(); 

            this.KeyDown += (sender, e) =>
            {
                if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
                    this.shiftKeyPressed = true;
                else
                    this.shiftKeyPressed = false;
            }; 

            this.KeyUp += (sender, e) => this.shiftKeyPressed = false

            new MouseWheelHelper(this.myMSI).Moved += (sender, e) =>
            {
                Double zoomFactor = 0;
                if (e.Delta > 0)
                    zoomFactor = 1.3;
                else
                    zoomFactor = 0.7; 

                Point log_mousePosition = this.myMSI.ElementToLogicalPoint(this.currentMousePosition);

                this.myMSI.ZoomAboutLogicalPoint(zoomFactor, log_mousePosition.X, log_mousePosition.Y);
            }; 

            this.myMSI.MouseMove += (sender, e) =>
            {
                this.currentMousePosition = e.GetPosition(this.myMSI);
                this.moved = true

                if (this.dragging)
                {
                    Point newOrigin = new Point();

                    // dragOffset = MouseLeftButtonDown 된 지점
                    newOrigin.X = currentViewportOrigin.X - (((currentMousePosition.X - dragOffset.X) / this.myMSI.ActualWidth) * this.myMSI.ViewportWidth);

                    newOrigin.Y = currentViewportOrigin.Y - (((currentMousePosition.Y - dragOffset.Y) / this.myMSI.ActualHeight) * this.myMSI.ViewportWidth);

                    this.myMSI.ViewportOrigin = newOrigin;
                }
            }; 

            this.myMSI.MouseLeftButtonDown += (sender, e) =>
            {
                this.myMSI.CaptureMouse();
                this.dragOffset = e.GetPosition(this.myMSI);
                this.currentViewportOrigin = this.myMSI.ViewportOrigin;
                this.dragging = true;
            }; 

            this.myMSI.MouseLeftButtonUp += (sender, e) =>
            {
                this.dragging = false
                Point log_mousePosition = this.myMSI.ElementToLogicalPoint(e.GetPosition(this.myMSI)); 

                if (!moved)
                {
                    if (this.shiftKeyPressed)
                    {
                        this.myMSI.ZoomAboutLogicalPoint(0.7, log_mousePosition.X, log_mousePosition.Y);
                    }
                    else
                    {
                        this.myMSI.ZoomAboutLogicalPoint(1.3, log_mousePosition.X, log_mousePosition.Y);
                    }
                }

                this.moved = false;
            }; 

            this.myMSI.MouseLeave += (sender, e) =>
            {
                this.myMSI.ReleaseMouseCapture();

                if (this.dragging) this.dragging = false;
            };

            this.testButton.Click += (sender, e) =>
            {
                this.myMSI.ViewportOrigin = new Point(-0.3, -0.3);
            };

            Boolean viewPortChanged = false;
            Boolean sliderValueChanged = false

            this.myMSI.ViewportChanged += (sender, e) =>
            {
                if (sliderValueChanged) return;
                viewPortChanged = true;
                this.mySlider.Value = this.myMSI.ViewportWidth;
            };

             this.myMSI.MotionFinished += (s, e) =>
            {
                viewPortChanged = false;
                sliderValueChanged = false;
            }; 

            this.mySlider.ValueChanged += (sender, e) =>
            {
                if (viewPortChanged) return;
                sliderValueChanged = true;
                this.myMSI.ViewportWidth = this.mySlider.Value;
            };

            this.testButton.Click += (sender, e) =>
            {
                this.myMSI.ViewportOrigin = new Point(0, 0);
                this.myMSI.ViewportWidth = 1;
            };
        }
    }
}


(사실, 전문에는 Slider로 ViewportWidth 를 Binding 해 보는 코드가 포함 되 있습니다. 첨엔 Element To Element Binding으로 구현 해 봤는데, Slider의 Value에 Minium 값을 지정 해 줘야만 하는데다 -ViewportWidth가 0이 되면 다시는 Image를 볼 수 없더라고요-, TwoWay Mode로 해도 Zoom In/Out 시 ViewportWidth가 Slider에 제대로 Binding 되지 않아서 억지로 구현 해 봤는데요.. 코드도 명확하지 못하고, 구현도 정확히 작동하지 않아서 본문에는 포함시키지 않았습니다 ㅠ.ㅠ)