locked
FreshPaint のようにタッチによるズームが可能な Canvas を実現するには RRS feed

  • 質問

  • ズーミングを管理するコントロールである ScrollViewer の中に Canvas コントロールを配置すれば、ズーム可能な Canvas にすることができます。

    <ScrollViewer
        x:Name="CanvasArea"
        Background="Gray"
        HorizontalScrollBarVisibility="Auto">
        <Canvas
            x:Name="ZoomableInkCanvas"
            Width="480"
            Height="360"
            Background="White"/>
    </ScrollViewer>

    この Canvas に Ink 機能を付与し、さらにタッチ操作による描画を許可するには、PointerPressed イベントハンドラで、ポインターがタッチ操作であることを判定し、Canvas の ManipulationMode を None に設定します。

    private void ZoomableInkCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
    {
        var pt = e.GetCurrentPoint(ZoomableInkCanvas);
    
        if (pt.PointerDevice.PointerDeviceType == PointerDeviceType.Touch)
            ZoomableInkCanvas.ManipulationMode = ManipulationModes.None;
    
        // インク機能の記述…
    }
    
    void ZoomableInkCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
    {
        // インク機能の記述...
    }
    
    void ZoomableInkCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
    {
        // インク機能の記述...
    
        if (ZoomableInkCanvas.ManipulationMode == ManipulationModes.None)
            ZoomableInkCanvas.ManipulationMode = ManipulationModes.System;
    }

    ここで、ScrollViewer 内に配置された Canvas に対してピンチ ジェスチャーによるズームを行おうとすると、ジェスチャーが Canvas に対するインク操作としてキャプチャされます。MSDN にあるサンプルコードでは Canvas を複数ポインターによるインク描画に対応させていないために、描画は正常に行われませんが、マルチタッチでの描画が目的ではないので、その点はここでは問題視しません。

    マルチタッチの場合に描画処理を行わせないために、PointerPressed イベントハンドラで、イベントを発生させたポインターがプライマリ ポインターでないことを判定し、Canvas の ManipulationMode を System に設定しました。

    private void ZoomableInkCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
    {
        var pt = e.GetCurrentPoint(ZoomableInkCanvas);
    
        if (pt.Properties.IsPrimary)
        {
            ZoomableInkCanvas.ManipulationMode = ManipulationModes.None;
            _inkManager.ProcessPointerDown(pt);
            _penID = pt.PointerId;
        }
        else
            ZoomableInkCanvas.ManipulationMode = ManipulationModes.System;
    
        e.Handled = true;
    }

    ここまでのコードを実行させてみると、一本指タッチの場合には Canvas への描画が、マルチタッチの場合にはズーム操作が行えます。問題は、二本指によるタッチでズーム操作を行えない点です。

    二本の指だけをタッチさせると、Canvas の ManipulationMode でピンチ ジェスチャーが許可されていてもズームができないようでした。この場合、一本目の指によるインク キャプチャと、二本目の指によるスクロール操作が検出されます。三本目の指をタッチさせると、二本目の指との間でピンチ ジェスチャーが検出され、Canvas をズームさせることができるようです。

    また、PointerMoved イベントハンドラで、ポインターがプライマリ ポインターである場合に Canvas に対するマニピュレーションをキャンセルするコードを試してみました。

    private void ZoomableInkCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
    {
        var pt = e.GetCurrentPoint(ZoomableInkCanvas);
    
        if (pt.Properties.IsPrimary)
        {
            //ZoomableInkCanvas.ManipulationMode = ManipulationModes.None;
            _inkManager.ProcessPointerDown(pt);
            _penID = pt.PointerId;
        }
        //else
            //ZoomableInkCanvas.ManipulationMode = ManipulationModes.System;
    
        e.Handled = true;
    }
    
    void InkCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
    {
        PointerPoint pt = e.GetCurrentPoint(ZoomableInkCanvas);
        if (pt.Properties.IsPrimary)
            ZoomableInkCanvas.CancelDirectManipulations(); // マニピュレーションのキャンセル
    
        // インク機能の記述...
    }
    
    void ZoomableInkCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
    {
        // インク機能の記述...
    
        //if (ZoomableInkCanvas.ManipulationMode == ManipulationModes.None)
            //ZoomableInkCanvas.ManipulationMode = ManipulationModes.System;
    }

    このコードの場合、素直に指でパン ジェスチャーを行っただけでは PointerMoved イベント自体が発生せず、ScrollViewer に対してのスクロール操作であると解釈されます。Canvas 上で指をタップしたまま押さえるなどして、ScrollViewer にパン ジェスチャーとして認識されない程度にわずかなポインターの移動が発生した場合にのみ、PointerMoved イベントが発生します。

    CancelDirectManipulations が実行されたあとは、そのポインターからはジェスチャーの読み取りが行われなくなり、指を素早く動かしても Canvas に対する PointerMoved として解釈されるようになりました。

    US の MSDN フォーラムにあったこちらの質問も参考にしてみました。ManipulationDelta イベントを起点に ScrollViewer に対して ChangeView を行う方法と読めましたが、具体的にどういったコードになるかはいまいちつかめませんでした。

    プライマリ ポインターである一本目の指もタッチ ジェスチャーを行えるポインターとして扱わせる、あるいは適切なタイミングで CancelDirectManipulations を行うためには、どういった記述が必要になるでしょうか。


    • 編集済み asato2001 2015年4月18日 4:38
    2015年4月18日 4:28

回答

  • ManipulationModes.Systemになっている状態で受け付けた最初のポイントと次のポイントを使って拡大縮小処理が行われるようです。
    そのため、Noneになっている1ポイント目は使われず、切り替えた後の2点目と3点目でようやく拡縮が行われるのだと考えられます。

    using System;
    using System.Collections.Generic;
    using Windows.Foundation;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Input;
    using Windows.UI.Xaml.Media;
    using Windows.UI.Input;
    using Windows.UI.Input.Inking;
    using Windows.UI;
    using Windows.UI.Xaml.Shapes;
    using Windows.Devices.Input;
    namespace App1
    {
        //<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        //    <ScrollViewer Margin="100" x:Name="sv" >
        //        <Canvas x:Name="ZoomableInkCanvas" Width="480" Height="360" Background="White"/>
        //    </ScrollViewer>
        //</Grid>
        public sealed partial class MainPage : Page
        {
            const double STROKETHICKNESS = 5;
            Point _previousContactPt;
            PointerPoint _ppLast;
            uint _touchID = 0;
            InkManager _inkManager = new Windows.UI.Input.Inking.InkManager();
    
            public MainPage()
            {
                this.InitializeComponent();
    
                //ManipulationModes.Systemは処理が取られてどうしようもないので使わない
                ZoomableInkCanvas.ManipulationMode = ManipulationModes.Scale; //| ManipulationModes.TranslateX | ManipulationModes.TranslateY;//InkManagerを使わないならPointerMovedの代わりにTranslateでもポインタ移動を検出できる
    
                ZoomableInkCanvas.ManipulationStarted += ZoomableInkCanvas_ManipulationStarted;
                ZoomableInkCanvas.ManipulationDelta += ZoomableInkCanvas_ManipulationDelta;
                ZoomableInkCanvas.ManipulationCompleted += ZoomableInkCanvas_ManipulationCompleted;
    
                ZoomableInkCanvas.PointerPressed+=ZoomableInkCanvas_PointerPressed;
                ZoomableInkCanvas.PointerMoved+=ZoomableInkCanvas_PointerMoved;
                ZoomableInkCanvas.PointerReleased +=ZoomableInkCanvas_PointerReleased;
    
                //Canvasの外側かつScrollViewerの内側でも反応するように
                sv.ManipulationMode = ManipulationModes.Scale;
                sv.ManipulationStarted += ZoomableInkCanvas_ManipulationStarted;
                sv.ManipulationDelta += ZoomableInkCanvas_ManipulationDelta;
                sv.ManipulationCompleted += ZoomableInkCanvas_ManipulationCompleted;
            }
    
            #region 拡縮処理
    
            float zoomOriginal;
            bool zooming;
            bool mooving;
    
            void ZoomableInkCanvas_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
            {
                zoomOriginal = sv.ZoomFactor;
                zooming = false;
                mooving = false;
                e.Handled = true;
            }
    
            void ZoomableInkCanvas_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
            {
                if (zooming || e.Delta.Scale != 1) //拡縮が開始されるとScaleが1以外になる
                {
                    if(mooving)
                    {
                        mooving = false;
                        Point p = ToCanvasPoint(sender, e.Position);
                        OnEndMovePointer(p);
                    }
                    if(!zooming)
                    {
                        zooming = true;
                        OnBeginZoom();
                    }
                    
                    float newzoom = zoomOriginal * e.Cumulative.Scale;
                    newzoom = Math.Min(Math.Max(sv.MinZoomFactor, newzoom), sv.MaxZoomFactor);
                    sv.ChangeView(null, null, newzoom); //ScrollViewerの拡大率を変更
                    e.Handled = true;
                }
                else if (e.Delta.Translation.X != 0 && e.Delta.Translation.X != 0 && e.Delta.Translation.Y != 0)
                {//Tranlateも使う場合のみ
                    Point p = ToCanvasPoint(sender, e.Position);
                    if (!mooving)
                    {
                        mooving = true;
                        OnBeginMovePointer(p);
                    }
                    else
                    {
                        OnContinueMovePointer(p);
                    }
                }
                e.Handled = true;
            }
    
            void ZoomableInkCanvas_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
            {
                if (mooving)
                {
                    mooving = false;
                    Point p = ToCanvasPoint(sender, e.Position);
                    OnEndMovePointer(p);
                }
                if(zooming)
                {
                    zooming = false;
                    OnEndZoom();
                }
                e.Handled = true;
            }
    
            private Point ToCanvasPoint(object sender , Point p)
            {
                ScrollViewer sv = sender as ScrollViewer;
                if (sv != null)
                {
                    var transform = ZoomableInkCanvas.TransformToVisual(sv);
                    p=transform.TransformPoint(new Point(0,0));
                }
                return p;
            }
            #endregion
    
            #region
    
            private void OnBeginZoom()
            {
                if(_ppLast != null)
                {   //線を描いているときに拡大縮小すると変な線になるので、線の描画を終わらせる
                    _inkManager.ProcessPointerUp(_ppLast);
                    _ppLast = null;
                    _touchID = 0;
                    RenderAllStrokes();
                }
            }
            private void OnEndZoom() { }
            private void OnBeginMovePointer(Point p){}
            private void OnContinueMovePointer(Point p){}
            private void OnEndMovePointer(Point p){}
    
            #endregion
    
    //あとはMSDN(https://msdn.microsoft.com/ja-jp/library/windows/apps/xaml/hh974457.aspx)のサンプルとほぼ同じ
    
            private void ZoomableInkCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
            {
                PointerPoint pp = e.GetCurrentPoint(ZoomableInkCanvas);
                _previousContactPt = pp.Position;
    
                PointerDeviceType pointerDevType = e.Pointer.PointerDeviceType;
                if (pointerDevType == PointerDeviceType.Touch && pp.Properties.IsPrimary)
                {
                    _inkManager.ProcessPointerDown(pp);
                    _touchID = pp.PointerId;
                    _ppLast = pp;
                    e.Handled = true;
                }
            }
    
            private void ZoomableInkCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
            {
                if (e.Pointer.PointerId == _touchID)
                {
                    PointerPoint pp = e.GetCurrentPoint(ZoomableInkCanvas);
                    Point currentContactPt = pp.Position;
                    if (Distance(currentContactPt, _previousContactPt) > 2)
                    {
                        Line line = new Line()
                        {
                            X1 = _previousContactPt.X,
                            Y1 = _previousContactPt.Y,
                            X2 = currentContactPt.X,
                            Y2 = currentContactPt.Y,
                            StrokeThickness = STROKETHICKNESS,
                            Stroke = new SolidColorBrush(Windows.UI.Colors.Red)
                        };
    
                        _previousContactPt = currentContactPt;
                        ZoomableInkCanvas.Children.Add(line);
                        _inkManager.ProcessPointerUpdate(pp);
                    }
                }
                e.Handled = true;
            }
    
            private void ZoomableInkCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
            {
                if (e.Pointer.PointerId == _touchID)
                {
                    PointerPoint pp = e.GetCurrentPoint(ZoomableInkCanvas);
                    _inkManager.ProcessPointerUp(pp);
                    _touchID = 0;
                    _ppLast = null;
                }
                RenderAllStrokes();
                e.Handled = true;
            }
    
            private void RenderAllStrokes()
            {
                ZoomableInkCanvas.Children.Clear();
                IReadOnlyList<InkStroke> inkStrokes = _inkManager.GetStrokes();
                foreach (InkStroke inkStroke in inkStrokes)
                {
                    PathGeometry pathGeometry = new PathGeometry();
                    PathFigureCollection pathFigures = new PathFigureCollection();
                    PathFigure pathFigure = new PathFigure();
                    PathSegmentCollection pathSegments = new PathSegmentCollection();
                    Windows.UI.Xaml.Shapes.Path path = new Windows.UI.Xaml.Shapes.Path();
                    path.Stroke = new SolidColorBrush(Colors.Red);
                    path.StrokeThickness = STROKETHICKNESS;
                    IReadOnlyList<InkStrokeRenderingSegment> segments;
                    segments = inkStroke.GetRenderingSegments();
                    bool first = true;
                    foreach (InkStrokeRenderingSegment segment in segments)
                    {
                        if (first)
                        {
                            pathFigure.StartPoint = segment.BezierControlPoint1;
                            first = false;
                        }
                        BezierSegment bezSegment = new BezierSegment();
                        bezSegment.Point1 = segment.BezierControlPoint1;
                        bezSegment.Point2 = segment.BezierControlPoint2;
                        bezSegment.Point3 = segment.Position;
                        pathSegments.Add(bezSegment);
                    }
                    pathFigure.Segments = pathSegments;
                    pathFigures.Add(pathFigure);
                    pathGeometry.Figures = pathFigures;
                    path.Data = pathGeometry;
                    ZoomableInkCanvas.Children.Add(path);
                }
            }
    
            private double Distance(Point currentContact, Point previousContact)
            {
                return Math.Sqrt(Math.Pow(currentContact.X - previousContact.X, 2) + Math.Pow(currentContact.Y - previousContact.Y, 2));
            }
        }
    }

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 編集済み gekkaMVP 2015年4月18日 13:42
    • 回答の候補に設定 星 睦美 2015年4月20日 0:26
    • 回答としてマーク 星 睦美 2015年4月23日 2:43
    2015年4月18日 13:39