WPFヘルパー:ImageScaleHelper.cs – 画像のズームとパンを提供するヘルパークラス

コンピュータ

ファイル名:ImageScaleHelper.cs

// 画像のズームとパンを提供するヘルパークラス

using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace Maywork.WPF.Helpers;

public static class ImageScaleHelper
{
    static readonly ConditionalWeakTable<Image, Controller> _table = [];

	// ダミーイメージを作る
    public static BitmapSource CreateDummyImage(int w, int h)
    {
        return BitmapSource.Create(
            w, h,
            96, 96,
            PixelFormats.Bgra32,
            null,
            new byte[w * h * 4],
            w * 4);
    }

	// アタッチ
    public static void Attach(
        ScrollViewer scroll,
        Canvas canvas,
        Image image)
    {
        if (_table.TryGetValue(image, out _))
            return;
		
		scroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
		scroll.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
		image.Stretch = Stretch.None;
        
        EnsureImageInitialized(image);

        var controller = new Controller(scroll, canvas, image);
        _table.Add(image, controller);
    }
	// 画像をセット
	public static void SetImage(Image image, BitmapSource source)
	{
		if (!_table.TryGetValue(image, out var controller))
			return; // Attachされていない

		controller.SetImage(source);		
	}
	// 初期化
    static void EnsureImageInitialized(Image image)
    {
        // Source が無ければダミーを入れる
        if (image.Source == null)
        {
            image.Source = CreateDummyImage(256, 256);
        }

        // RenderTransform を保証
        if (image.RenderTransform is not ScaleTransform)
        {
            image.RenderTransform = new ScaleTransform(1, 1);
        }
    }
	// コントローラー	
    sealed class Controller
    {
        readonly ScrollViewer _scroll;
        readonly Canvas _canvas;
        readonly Image _image;

        bool _panning;
        Point _panStartMouse;
        double _panStartH;
        double _panStartV;

        public Controller(
            ScrollViewer scroll,
            Canvas canvas,
            Image image)
        {
            _scroll = scroll;
            _canvas = canvas;
            _image  = image;

            _image.RenderTransform = new ScaleTransform(1, 1);

            _image.Loaded += (_, __) =>
            {
                _canvas.Width  = _image.Source.Width;
                _canvas.Height = _image.Source.Height;
                CenterScroll();
            };

            // ズーム
            _scroll.PreviewMouseWheel += OnMouseWheel;

            // パン(中ボタン)
            _scroll.PreviewMouseDown += OnMouseDown;
            _scroll.PreviewMouseMove += OnMouseMove;
            _scroll.PreviewMouseUp   += OnMouseUp;
        }

        /* ============================
         * ズーム(Ctrl + Wheel)
         * ============================ */
        void OnMouseWheel(object sender, MouseWheelEventArgs e)
        {
            if ((Keyboard.Modifiers & ModifierKeys.Control) == 0)
                return;

            var scale = (ScaleTransform)_image.RenderTransform;

            double oldW = _canvas.Width;
            double oldH = _canvas.Height;

            const double zoomFactor = 1.1;
            double factor = e.Delta > 0 ? zoomFactor : 1 / zoomFactor;

            scale.ScaleX = Math.Clamp(scale.ScaleX * factor, 0.1, 10.0);
            scale.ScaleY = scale.ScaleX;

            _canvas.Width  = _image.Source.Width  * scale.ScaleX;
            _canvas.Height = _image.Source.Height * scale.ScaleY;

            // 中心維持
            double cx = _scroll.HorizontalOffset + _scroll.ViewportWidth  / 2;
            double cy = _scroll.VerticalOffset   + _scroll.ViewportHeight / 2;

            double nx = cx * _canvas.Width  / oldW;
            double ny = cy * _canvas.Height / oldH;

            _scroll.ScrollToHorizontalOffset(nx - _scroll.ViewportWidth  / 2);
            _scroll.ScrollToVerticalOffset  (ny - _scroll.ViewportHeight / 2);

            e.Handled = true;
        }

        /* ============================
         * パン(ホイールボタン)
         * ============================ */
        void OnMouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton != MouseButton.Middle)
                return;

            _panning = true;
            _panStartMouse = e.GetPosition(_scroll);
            _panStartH = _scroll.HorizontalOffset;
            _panStartV = _scroll.VerticalOffset;

            _scroll.CaptureMouse();
            Mouse.OverrideCursor = Cursors.Hand;

            e.Handled = true;
        }

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

            Point p = e.GetPosition(_scroll);

            double dx = p.X - _panStartMouse.X;
            double dy = p.Y - _panStartMouse.Y;

            _scroll.ScrollToHorizontalOffset(_panStartH - dx);
            _scroll.ScrollToVerticalOffset  (_panStartV - dy);

            e.Handled = true;
        }

        void OnMouseUp(object sender, MouseButtonEventArgs e)
        {
            if (!_panning || e.ChangedButton != MouseButton.Middle)
                return;

            _panning = false;
            _scroll.ReleaseMouseCapture();
            Mouse.OverrideCursor = null;

            e.Handled = true;
        }

		// 中央へスクロール
        void CenterScroll()
        {
            _scroll.ScrollToHorizontalOffset(
                (_canvas.Width - _scroll.ViewportWidth) / 2);

            _scroll.ScrollToVerticalOffset(
                (_canvas.Height - _scroll.ViewportHeight) / 2);
        }

		// 画像をセット
		public void SetImage(BitmapSource source)
		{
			if (source == null)
				return;

			_image.Source = source;

			// 倍率リセット
			if (_image.RenderTransform is ScaleTransform scale)
			{
				scale.ScaleX = 1.0;
				scale.ScaleY = 1.0;
			}
			else
			{
				_image.RenderTransform = new ScaleTransform(1.0, 1.0);
			}

			// Canvasサイズ同期
			_canvas.Width  = source.Width;
			_canvas.Height = source.Height;

			// 中央へ
			CenterScroll();
		}

    }
}

/*

// 使用例

 XAML
<ScrollViewer x:Name="Scroll1">
	<Canvas x:Name="Canvas1">      
		<Image x:Name="Image1"/>
	</Canvas>
</ScrollViewer>

コードビハインド(C#)
public MainWindow()
{
	InitializeComponent();

	// ヘルパーへアタッチ
	ImageScaleHelper.Attach(Scroll1, Canvas1, Image1);

	// 画像のセット
	ImageScaleHelper.SetImage(Image1, new BitmapImage(new Uri("画像のURL")));

// 機能
Ctrl + マウスホイール : ズームイン/アウト
ホイールボタンのドラッグ : パン

*/
Download

コメント