Dragable Canvas 만들기




목차


지난 포스팅(http://crystalcube.co.kr/152)에서 만든 CanvasTreeView 를 가지고 기능을 추가해 보도록 하겠습니다.

먼저 앞선 포스팅을 읽어보시는게 좋을 것 같습니다. :)


추가해 볼 기능은, 마우스로 Drag 하는 기능입니다. 단순히 Canvas 에 그리는 것이 아니라, 마우스로 움직일 수 있도록 하려는 것입니다. 실제로 포토샵, 파워포인트 같은 툴들처럼 마우스로 이리저리 움직이는 그런 흔한 것들 말입니다. :)





개요


이 캔버스의 이름은 DragableCanvas 라고 하겠습니다.

어떻게 하면 좋을까요?


1차 시도.

단순히 DragableCanvas 의 OnMouseLeftButtonDown 등의 이벤트를 override 해서 구현한다.


이 경우 해당 핸들러까지 이벤트가 넘어오지 않습니다 -_-; OnPreviewMouseLeftButtonDown 과 같은 Preview 메소드 들을 사용하면 가능하긴 합니다. 하지만 Canvas 위에 올라가는 각종 UIElement 들과 충돌이 생기고 관리가 지저분해지기 시작합니다. 재양의 시작이라고 해 두죠.


2차 시도.

GragableCanvas 에 Child 가 추가될때마다, 각 Child 에 Mouse 관련 이벤트 핸들러들을 달아서, Drag 가능토록 한다.

말만 들어도 끔직합니다. 매번 핸들러를 추가/삭제 하기도 힘들 뿐더러, 코드도 지저분해지고...메모리 릭도 걱정되고..



위에 언급한 방법들을 이래저래 해 보았는데, 엄청 까다롭습니다.

고민끝에 찾아낸 방법은.


Behavior 를 사용하는 것입니다. Template 안에서 Behavior 를 붙여주면, 핸들러나 메모리 릭에 대한 부담이 적어지죠.

그래서 그렇게 구현해 보았습니다. 그랬러니...

이번엔 이벤트가 문제입니다.

AssociatedObject 에서 MouseMove 를 처리하다보니, 다른 Child 에 가려지는 순간 이벤트가 더이상 넘어오지 않아, Drag 가 멈추어 버립니다. 그리고 마우스를 빠르게 움직여서 해당 AssociatedObject 밖으로 마우스가 순간적으로 빠져나가면... 마찬가지로 Drag 가 끝나버립니다.


이런 이유로, Behavior 에다가 움직일 Target(움직일 UIElement) 과 BaseUIElement(마우스 캡쳐 베이스) 디펜던시 프로퍼티를 추가해 주었습니다. 잘 동작하네요 :)





코드는 다음과 같습니다.



    [DragableCanvas.cs]



using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;


namespace SW.Common.Controls
{
    public class DragableCanvas : TreeView
    {
        static DragableCanvas()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(DragableCanvas), new FrameworkPropertyMetadata(typeof(DragableCanvas)));
        }
    }
}




    [DragableCanvas.xaml]



    <Style TargetType="{x:Type local:DragableCanvas}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type TreeView}">
                    <ControlTemplate.Resources>
                        <Style TargetType="{x:Type TreeViewItem}">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="{x:Type TreeViewItem}">
                                        <Canvas x:Name="PART_Canvas">
                                            <ContentPresenter Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}"
                                                              Canvas.Left="{Binding (Canvas.Left)}"
                                                              Canvas.Right="{Binding (Canvas.Right)}"
                                                              Canvas.Top="{Binding (Canvas.Top)}"
                                                              Canvas.Bottom="{Binding (Canvas.Bottom)}">
                                                <i:Interaction.Behaviors>
                                                    <behaviors:DragOnCanvasBehavior BaseUIElement="{Binding RelativeSource={RelativeSource AncestorType=local:DragableCanvas}}"
                                                                                    Target="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}}"/>
                                                </i:Interaction.Behaviors>
                                            </ContentPresenter>
                                            <ItemsPresenter/>
                                        </Canvas>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>

                            <Setter Property="ItemsPanel">
                                <Setter.Value>
                                    <ItemsPanelTemplate>
                                        <Canvas/>
                                    </ItemsPanelTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </ControlTemplate.Resources>

                    <ScrollViewer Background="{TemplateBinding Background}" x:Name="PART_ScrollViewer"
                              HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" 
                              VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
                              CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}"
                              PanningRatio="{TemplateBinding ScrollViewer.PanningRatio}"
                              PanningMode="{TemplateBinding ScrollViewer.PanningMode}"
                              Focusable="False">
                        <ItemsPresenter/>
                    </ScrollViewer>
                </ControlTemplate>
            </Setter.Value>
        </Setter>

        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <Canvas/>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>



    [DragOnCanvasBehavior]



using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;


namespace SW.Common.Behaviors
{
    public class DragOnCanvasBehavior : Behavior<UIElement>
    {
        public UIElement BaseUIElement
        {
            get { return (UIElement)GetValue(BaseUIElementProperty); }
            set { SetValue(BaseUIElementProperty, value); }
        }

        // Using a DependencyProperty as the backing store for BaseUIElement.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty BaseUIElementProperty =
            DependencyProperty.Register("BaseUIElement", typeof(UIElement), typeof(DragOnCanvasBehavior), new PropertyMetadata(null));




        public UIElement Target
        {
            get { return (UIElement)GetValue(TargetProperty); }
            set { SetValue(TargetProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Target.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TargetProperty =
            DependencyProperty.Register("Target", typeof(UIElement), typeof(DragOnCanvasBehavior), new PropertyMetadata(null));




        protected override void OnAttached()
        {
            this.AssociatedObject.MouseLeftButtonDown += OnMouseLeftButtonDown;

            base.OnAttached();
        }



        protected override void OnDetaching()
        {
            this.AssociatedObject.MouseLeftButtonDown -= OnMouseLeftButtonDown;

            base.OnDetaching();
        }


        private Point _initMousePosition;
        private Point _initItemPosition;
        private bool _onDrag = false;

        private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (this._onDrag == false)
            {
                UIElement captureBase = (this.BaseUIElement ?? this.AssociatedObject);
                captureBase.MouseMove += OnMouseMove;
                captureBase.MouseLeftButtonUp += OnMouseLeftButtonUp;

                UIElement target = (this.Target ?? this.AssociatedObject);

                this._onDrag = true;
                this._initMousePosition = e.GetPosition(captureBase);
                double x = Canvas.GetLeft(target);
                double y = Canvas.GetTop(target);
                x = double.IsNaN(x) ? 0 : x;
                y = double.IsNaN(y) ? 0 : y;

                this._initItemPosition = new Point(x, y);
                captureBase.CaptureMouse();
            }
        }


        private void OnMouseMove(object sender, MouseEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed && this._onDrag == true)
            {
                UIElement captureBase = (this.BaseUIElement ?? this.AssociatedObject);
                UIElement target = (this.Target ?? this.AssociatedObject);

                Vector delta = e.GetPosition(captureBase) - this._initMousePosition;
                Canvas.SetLeft(target, this._initItemPosition.X + delta.X);
                Canvas.SetTop(target, this._initItemPosition.Y + delta.Y);
            }
        }



        private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            this._onDrag = false;

            UIElement captureBase = (this.BaseUIElement ?? this.AssociatedObject);
            captureBase.ReleaseMouseCapture();

            captureBase.MouseMove -= OnMouseMove;
            captureBase.MouseLeftButtonUp -= OnMouseLeftButtonUp;
        }
    }
}




참, Behavior 를 사용하기 위해서는, System.Windows.Interactivity 를 참조추가 해 주어야 한다는걸 잊지 않으시길 바랍니다. :)





'Microsoft > WPF' 카테고리의 다른 글

격자 배경 그리기  (0) 2014.11.14
Expanding Canvas 만들기  (2) 2014.11.14
TreeView Template 을 통한 Canvas 만들기  (1) 2014.11.12
MEF(Managed Extensibility Framework)  (2) 2014.05.19
화면에 노출되는 프로퍼티는?  (1) 2014.05.13