てけとーぶろぐ。

ソフトウェアの開発と、お絵かきと、雑記と。

マウスホイールでの拡大縮小を考える。(3)

前回の続き。

WPFを使って実験用アプリを作ってみた。

ImageZooming.zip

f:id:kurimayoshida:20150320153914p:plain

試してみるとズームアウトがどうもじれったい。

実際現実世界もこんなかんじかもと思いつつも使いづらいのはよろしくない。
そこで、ズームアウト時の拡大率にはズームイン時の拡大率の逆数使うようにしてみた。
それが上のチェックボックス

いいような気もするが、そんなことするならなんでモデリングしたんだっけか…。

以下ソースコード

MainWindow.xaml

<Window x:Class="ImageZooming.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        Loaded="window_Loaded">
    <DockPanel Height="Auto" Width="Auto" LastChildFill="True">
        <CheckBox DockPanel.Dock="Top" Name="checkBox" Content="縮小率に拡大率の逆数を使う" Canvas.Left="109" Canvas.Top="59" Checked="checkBox_Checked" Unchecked="checkBox_Unchecked"/>
        <ScrollViewer DockPanel.Dock="Bottom" x:Name="scrollViewer" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
        	PreviewMouseWheel="scrollViewer_PreviewMouseWheel">
            <Canvas x:Name="canvas">
                <Image x:Name="image" Stretch="Fill" Width="{Binding Source.PixelWidth, RelativeSource={RelativeSource Self}}" Height="{Binding Source.PixelHeight, RelativeSource={RelativeSource Self}}"/>
            </Canvas>
        </ScrollViewer>
    </DockPanel>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace ImageZooming
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジックs
    /// </summary>
    public partial class MainWindow : Window
    {
        const double MAGNIFICATION_LEVEL_1 = 1.1;
        const double VIEW_ANGLE = Math.PI / 4;

        private TransformGroup _canvasTransformGroup = new TransformGroup();
        private int _zoomLevel = 0;
        private double _canvasOriginalWidth;
        private double _canvasOriginalHeight;


        public MainWindow()
        {
            InitializeComponent();
        }


        private void window_Loaded(object sender, RoutedEventArgs e)
        {
            string currentDirectory = System.IO.Directory.GetCurrentDirectory();
            string filePath = System.IO.Path.Combine(currentDirectory, "image.jpg");

            BitmapImage bi = new BitmapImage();
            bi.BeginInit();
            bi.UriSource = new Uri(filePath);
            bi.EndInit();

            image.Source = bi;

            // キャンバスのサイズをロードしたコントロールに合わせる
            canvas.Width = image.Width;
            canvas.Height = image.Height;
            _canvasOriginalWidth = image.Width;
            _canvasOriginalHeight = image.Height;
        }


        void scrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
        {
            if ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.None)
            {
                changeCanvasScale(e.Delta);
                e.Handled = true;
            }
        }


        private void changeCanvasScale(int delta)
        {
            int originalZoomLevel = _zoomLevel;

            int zoomType = 0;
            if (checkBox.IsChecked.Value)
            {
                zoomType = 1;
            }

            double magnification = GetMagnification(zoomType, _zoomLevel);

            // 拡大操作時に中央に表示していたものがそのまま中央に表示されるようにする
            // そのためにまず中央に表示されている点の元の画像サイズの画像上での位置を求める
            double centerX = (scrollViewer.HorizontalOffset + scrollViewer.ViewportWidth / 2) / magnification;
            double centerY = (scrollViewer.VerticalOffset + scrollViewer.ViewportHeight / 2) / magnification;

            if (delta > 0)
            {
                _zoomLevel += 1;
            }
            else
            {
                _zoomLevel -= 1;
            }

            // 拡大率が異常になったら元の拡大率に戻す
            magnification = GetMagnification(zoomType, _zoomLevel);
            if (magnification <= 0 || double.IsInfinity(magnification) || double.IsNegativeInfinity(magnification))
            {
                _zoomLevel = originalZoomLevel;
                return;
            }

            _canvasTransformGroup.Children.Clear();
            _canvasTransformGroup.Children.Add(new ScaleTransform(magnification, magnification));
            canvas.RenderTransform = _canvasTransformGroup;
            canvas.Width = _canvasOriginalWidth * magnification;
            canvas.Height = _canvasOriginalHeight * magnification;

            // 中央に表示されていたものがそのまま中央に表示されるようにスクロールする
            scrollViewer.ScrollToHorizontalOffset(centerX * magnification - scrollViewer.ViewportWidth / 2);
            scrollViewer.ScrollToVerticalOffset(centerY * magnification - scrollViewer.ViewportHeight / 2);
        }

        /// <summary>
        /// 拡大レベルに応じた拡大率を得る
        /// </summary>
        /// <param name="zoomType">拡大縮小の方式 0:通常 0以外:縮小時の拡大率を拡大時の逆数で代用する</param>
        /// <param name="zoomLevel">拡大レベル 0だと等倍 正だと拡大 負だと縮小</param>
        /// <returns>拡大率</returns>
        private double GetMagnification(int zoomType, int zoomLevel)
        {
            double x = 0.1 / (2.0 * MAGNIFICATION_LEVEL_1 * Math.Tan(VIEW_ANGLE / 2));
            if (zoomType == 0)
            {
                return 1 / (1 - (x * zoomLevel) * Math.Tan(VIEW_ANGLE / 2) * 2);
            }
            else
            {
                double d = 1 / (1 - (x * Math.Abs(zoomLevel)) * Math.Tan(VIEW_ANGLE / 2) * 2);
                if (zoomLevel >= 0)
                {
                    return d;
                }
                else
                {
                    return 1 / d;
                }
            }

        }


        private void checkBox_Checked(object sender, RoutedEventArgs e)
        {
            _zoomLevel = 0;
        }


        private void checkBox_Unchecked(object sender, RoutedEventArgs e)
        {
            _zoomLevel = 0;
        }
    }
}