2011年10月17日月曜日

wpf : マウスドラッグで画像を回転させる

マウスドラッグで画像を回転させるサンプルコードを作りました。 コードはブログには長いので下の方に載せます。

サンプルに使っている絵は前に投稿したsvg画像をpngにしたやつです。 ってサンプルコードがシンプルすぎてキャプチャーした意味がない...

気を取り直して、画像はScrollViewerに乗せ、回転でウィンドウから外れた部分があってもスクロールすれば全部見れるようにしました。 見かけ上はScrollViewerのビューポートの中心を軸にまわっているようにしていますが、実際は、

  1. 画像の中心でドラッグした分だけ回転。
  2. 回転した画像が全部収まるように、画像を乗せたCanvas(mainView)のサイズを調整。
  3. 見かけ上の回転軸がずれないようにScrollViewerをスクロール。
  4. 画像の端の場合、ScrollViewerを端にスクロールさせても回転軸を一致させることができないことがあるので、そのときはその分だけmainViewのサイズを拡大。

というようにしています。 このため回転にあわせてスクロールバーがグニグニ動きます。 回転角度はScrollViewer上の外側の座標から決めて、見かけ上の回転の中心はImage上の(回転していないときの)座標を基準にしています。

コントロールを回転させるにはRenderTransformとLayoutTransformの2つが使えます。 このコードで使っているのはRenderTransformの方です。 「LayoutTransformはレイアウトの再計算が発生する範囲が広いので注意」と言われてますよね。 でもこのサンプルの場合はScrollViewerの中でサイズ変更とか回転とかしてるから内部の計算量は変わらないかも? まぁ、RenderTransformの方が速いっていうのは基本なので、最初からそっちで作れたんならそれに越したことはないはず。

SetMarkVisibilityというメソッドで、クライアント座標上の点(markA)をマウスから得て、それに一致する画像上の点(markB)を算出しています。 「markAとmarkBが同じ場所にあれば正しく動いている」という確認用です。

以下、コード。 まずはMainWindow.xaml。

<Window x:Class="ImageRotateTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ImageRotateTest"
        Width="525"
        Height="350"
        Loaded="OnLoaded">
    <Grid>
        <ScrollViewer Name="scrollViewer"
                HorizontalScrollBarVisibility="Auto"
                VerticalScrollBarVisibility="Auto"
        >
            <Canvas
                    Name="mainView"
                    Background="Black"
                    PreviewMouseLeftButtonDown="OnMouseLeftButtonDown"
                    PreviewMouseLeftButtonUp="OnMouseLeftButtonUp"
                    PreviewMouseMove="OnMouseMove"
                    MouseLeave="OnMouseLeave"
                    MouseEnter="OnMouseEnter"
            >
                <Canvas
                        Name="imgBase"
                        Width="{Binding ElementName=img, Path=ActualWidth}"
                        Height="{Binding ElementName=img, Path=ActualHeight}"
                >
                    <Image Name="img" Source="test.png"/>
                    <Canvas Name="centerMark" Opacity="0.5" Visibility="Hidden">
                        <Ellipse Canvas.Left="-10" Canvas.Top="-10" Width="20" Height="20" Stroke="Black" Fill="Aqua"/>
                        <Path Fill="Blue" Data="M -2.0, 0.0 v -20.0 h -5.0 l 7.0 -10.0 7.0 10.0 h -5.0 v 20.0 z"/>
                        <Ellipse Canvas.Left="-5" Canvas.Top="-5" Width="10" Height="10" Fill="Blue"/>
                    </Canvas>
                    <Canvas Name="markB">
                        <Line Stroke="Red" X1="-10" Y1="0" X2="10" Y2="0"/>
                        <Line Stroke="Red" X1="0" Y1="-10" X2="0" Y2="10"/>
                    </Canvas>
                </Canvas>
            </Canvas>
        </ScrollViewer>
        <Canvas>
            <Canvas Name="markA">
                <Line Stroke="Blue" X1="-10" Y1="-10" X2="10" Y2="10"/>
                <Line Stroke="Blue" X1="10" Y1="-10" X2="-10" Y2="10"/>
            </Canvas>
        </Canvas>
    </Grid>
</Window>

MainWindow.xaml.cs。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace ImageRotateTest
{
    public partial class MainWindow : Window
    {
        private double _imgMargin = 20.0;

        private double _imgTheta; // 回転角度(ラジアン)

        private double _rotateCenterImgX; // img上の回転の見かけの中心
        private double _RotateCenterImgX
        {
            set
            {
                Canvas.SetLeft(centerMark, value);
                _rotateCenterImgX = value;
            }
            get
            {
                return _rotateCenterImgX;
            }
        }
        private double _rotateCenterImgY; // img上の回転の見かけの中心
        private double _RotateCenterImgY
        {
            set
            {
                Canvas.SetTop(centerMark, value);
                _rotateCenterImgY = value;
            }
            get
            {
                return _rotateCenterImgY;
            }
        }

        private double _dragStartViewX; // scrollViewer上のドラッグ開始座標(中心は常にscrollViewerのViewportの中心)
        private double _dragStartViewY;
        private double _dragStartTheta; // ドラッグ開始座標の水平からの角度
        private double _dragLastTheta;  // ドラッグ中、最後に計算された回転角度

        private bool _dragging;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            InitImageParam();
        }

        private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Point p = e.GetPosition(scrollViewer);
            _dragStartViewX = p.X;
            _dragStartViewY = p.Y;
            double dragCenterViewX = scrollViewer.ViewportWidth / 2.0;
            double dragCenterViewY = scrollViewer.ViewportHeight / 2.0;

            Point centerImgPos = ClientPos2ImgPos(new Point(dragCenterViewX, dragCenterViewY));
            _RotateCenterImgX = centerImgPos.X;
            _RotateCenterImgY = centerImgPos.Y;

            double tx = dragCenterViewX - p.X;
            double ty = dragCenterViewY - p.Y;
            _dragStartTheta = Math.Atan2(ty, tx);

            centerMark.Visibility = Visibility.Visible;

            _dragging = true;

            SetMarkVisibility(false, null);
        }

        private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (!_dragging)
                return;

            EndDragging();

            SetMarkVisibility(true, e);
        }

        private void OnMouseMove(object sender, MouseEventArgs e)
        {
            if (!_dragging)
                return;

            Point p = e.GetPosition(scrollViewer);
            double dragCenterViewX = scrollViewer.ViewportWidth / 2.0;
            double dragCenterViewY = scrollViewer.ViewportHeight / 2.0;

            double tx = dragCenterViewX - p.X;
            double ty = dragCenterViewY - p.Y;
            double theta = Math.Atan2(ty, tx);
            _dragLastTheta = _imgTheta + theta - _dragStartTheta;
            if (_dragLastTheta <= -Math.PI)
                _dragLastTheta += Math.PI * 2.0;
            if (Math.PI < _dragLastTheta)
                _dragLastTheta -= Math.PI * 2.0;

            RotateImage(_dragLastTheta);
        }

        private void OnMouseLeave(object sender, MouseEventArgs e)
        {
            if (!_dragging)
                return;

            EndDragging();
        }

        private void OnMouseEnter(object sender, MouseEventArgs e)
        {
            if (!_dragging)
                return;

            EndDragging();
        }

        private void EndDragging()
        {
            _dragging = false;
            _imgTheta = _dragLastTheta;

            centerMark.Visibility = Visibility.Hidden;
        }

        private void InitImageParam()
        {
            _RotateCenterImgX = img.ActualWidth / 2.0;
            _RotateCenterImgY = img.ActualHeight / 2.0;
            _imgTheta = 0.0;

            RotateImage(_imgTheta);
        }

        private void RotateImage(double theta)
        {
            double imgWidth = img.ActualWidth;
            double imgHeight = img.ActualHeight;
            double viewWidth = Math.Abs(imgWidth * Math.Cos(theta)) + Math.Abs(imgHeight * Math.Sin(theta));
            if (viewWidth < scrollViewer.ViewportWidth)
                viewWidth = scrollViewer.ViewportWidth;
            viewWidth += _imgMargin * 2.0;
            double viewHeight = Math.Abs(imgWidth * Math.Sin(theta)) + Math.Abs(imgHeight * Math.Cos(theta));
            if (viewHeight < scrollViewer.ViewportHeight)
                viewHeight = scrollViewer.ViewportHeight;
            viewHeight += _imgMargin * 2.0;

            Matrix mat = new Matrix();
            mat.Translate(-imgWidth / 2.0, -imgHeight / 2.0);
            mat.Rotate(theta * 180.0 / Math.PI);
            mat.Translate(viewWidth / 2.0, viewHeight / 2.0);

            Point rotateCenterView = mat.Transform(new Point(_RotateCenterImgX, _RotateCenterImgY));
            double ViewWidthHalf = scrollViewer.ViewportWidth / 2.0;
            double ViewHeightHalf = scrollViewer.ViewportHeight / 2.0;
            double svOffsetX = rotateCenterView.X - ViewWidthHalf;
            double svOffsetY = rotateCenterView.Y - ViewHeightHalf;

            // mainViewの端に近くてスクロールバーの移動だけでは中心をとらえられない場合
            // mainViewのサイズを変更しスクロール位置を調整
            if (rotateCenterView.X < ViewWidthHalf)
            {
                double adjustWidth = ViewWidthHalf - rotateCenterView.X;
                mat.Translate(adjustWidth, 0.0);
                viewWidth += adjustWidth;
                svOffsetX = 0.0;
            }
            else if (viewWidth - ViewWidthHalf < rotateCenterView.X)
            {
                viewWidth = rotateCenterView.X + ViewWidthHalf;
            }

            if (rotateCenterView.Y < ViewHeightHalf)
            {
                double adjustHeight = ViewHeightHalf - rotateCenterView.Y;
                mat.Translate(0.0, adjustHeight);
                viewHeight += adjustHeight;
                svOffsetY = 0.0;
            }
            else if (viewHeight - ViewHeightHalf < rotateCenterView.Y)
            {
                viewHeight = rotateCenterView.Y + ViewHeightHalf;
            }

            mainView.Width = viewWidth;
            mainView.Height = viewHeight;
            imgBase.RenderTransform = new MatrixTransform(mat);
            scrollViewer.ScrollToHorizontalOffset(svOffsetX);
            scrollViewer.ScrollToVerticalOffset(svOffsetY);
        }

        private Point ClientPos2ImgPos(Point p)
        {
            return img.PointFromScreen(scrollViewer.PointToScreen(p));
        }

        private void SetMarkVisibility(bool v, MouseEventArgs e)
        {
            if (v)
            {
                markA.Visibility = Visibility.Visible;
                markB.Visibility = Visibility.Visible;

                // 座標変換確認
                Point p = e.GetPosition(scrollViewer);
                Canvas.SetLeft(markA, p.X);
                Canvas.SetTop(markA, p.Y);

                Point ip = ClientPos2ImgPos(p);
                Canvas.SetLeft(markB, ip.X);
                Canvas.SetTop(markB, ip.Y);
            }
            else
            {
                markA.Visibility = Visibility.Hidden;
                markB.Visibility = Visibility.Hidden;
            }
        }

        /*
        private Point ImgPos2ViewPos(Point p)
        {
            GeneralTransform trans = img.TransformToAncestor(mainView);
            return trans.Transform(p);
        }*/
    }
}