WPFのImageとscaleの関係のテスト→拡大縮小・パン移動・D&D・画像ビューア

コンピュータ

基本形

ファイル名:ImageScaleTest.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
    <PackageReference Include="OpenCvSharp4.WpfExtensions" Version="4.11.0.20250507" />
  </ItemGroup>

</Project>

ファイル名:MainWindow.xaml

<Window x:Class="ImageScaleTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ImageScaleTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Image x:Name="Image1" />
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Windows;

using Maywork.WPF.Helpers;


namespace ImageScaleTest;

public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();

        Image1.Source = OpenCvHelpers.CreateCrossImage();
    }
}

ファイル名:OpenCvHelper.cs

using System.Windows.Media.Imaging;
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;

namespace Maywork.WPF.Helpers;

public static class OpenCvHelpers
{
    public static BitmapSource CreateCrossImage()
    {
        // 1. 128x128のカラー画像(3チャンネル)を生成(背景:黒)
        using (Mat mat = new Mat(128, 128, MatType.CV_8UC3, Scalar.All(0)))
        {
            // 2. 左上から右下への線 (0,0) -> (127,127)
            // 引数: 画像, 開始点, 終了点, 色(BGR), 厚さ, 連結
            mat.Line(
                new Point(0, 0),
                new Point(127, 127),
                Scalar.Red, 1, LineTypes.AntiAlias);

            // 3. 右上から左下への線 (127,0) -> (0,127)
            mat.Line(
                new Point(127, 0),
                new Point(0, 127),
                Scalar.Red, 1, LineTypes.AntiAlias);

            // 4. WPFのImageコントロールで使える形式に変換
            return mat.ToBitmapSource();
        }
    }
}

Stretch="None"

Stretch="None"で、原寸128×128で表示
ScrollViewer追加

<Window x:Class="ImageScaleTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ImageScaleTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <ScrollViewer x:Name="Scroll1"
                      HorizontalScrollBarVisibility="Auto"
                      VerticalScrollBarVisibility="Auto">
            <Image x:Name="Image1"
                Stretch="None"/>
        </ScrollViewer>
    </Grid>
</Window>

Ctrl+マウスホイールで拡大縮小

ファイル名:MainWindow.xaml

<Window x:Class="ImageScaleTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ImageScaleTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <ScrollViewer x:Name="Scroll1"
                      HorizontalScrollBarVisibility="Auto"
                      VerticalScrollBarVisibility="Auto">
            <Canvas x:Name="Canvas1">
                <Image x:Name="Image1"
                       Stretch="None"/>
            </Canvas>
        </ScrollViewer>
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Maywork.WPF.Helpers;


namespace ImageScaleTest;

public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();

        Image1.Source = OpenCvHelpers.CreateCrossImage();

        // 初期倍率
        Image1.RenderTransform  = new ScaleTransform(1.0, 1.0);  // 倍率1倍
        // ImageとCanvasの解像度はセットで変更
        Canvas1.Width  = Image1.Source.Width;
        Canvas1.Height = Image1.Source.Height;

        // スクロールビューアでマウスホイールイベント
        Scroll1.PreviewMouseWheel += (sender, e) =>
        {
            // Ctrl キーが押されているか
            if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
            {
                // 直近の倍率取得
                var scale = Image1.RenderTransform as ScaleTransform;
                if (scale == null) return;  // 取得できんない場合諦める

                const double zoomFactor = 1.1;

                if (e.Delta > 0)
                {
                    // ホイール上(ズームインなど)
                    scale.ScaleX = scale.ScaleX * zoomFactor;
                }
                else
                {
                    // ホイール下(ズームアウトなど)
                    scale.ScaleX = scale.ScaleX / zoomFactor;
                }
                // 最大最小
                scale.ScaleX = Math.Clamp(scale.ScaleX, 0.1, 10.0);
                scale.ScaleY = scale.ScaleX;

                // Imageが変更されたらキャンバスサイズ調整
                Canvas1.Width  = Image1.Source.Width  * scale.ScaleX;
                Canvas1.Height = Image1.Source.Height * scale.ScaleY;

                // ScrollViewer に通常スクロールさせない
                e.Handled = true;
            }
        };
    }
}

拡大縮小だけなら、Canvasは不要ですが、

それだとScrollViewerが仕事をしてくれない。

Canvasをかませて、Imageサイズが変更されたら、

Canvasの幅と高さを調整する。

Canvasサイズが表示領域より大きくなると、ScrollViewerが反応する。

ドラッグアンドドロップ対応

ファイル名:ImageHelper.cs

using System.Windows.Media.Imaging;

namespace Maywork.WPF.Helpers;

public static class ImageHelper
{
    public static BitmapSource ChangeDpi(BitmapSource src, double dpiX, double dpiY)
    {
        return BitmapSource.Create(
            src.PixelWidth,
            src.PixelHeight,
            dpiX,
            dpiY,
            src.Format,
            src.Palette,
            src.CopyPixelsToArray(),
            src.PixelWidth * (src.Format.BitsPerPixel / 8)
        );
    }
    public static byte[] CopyPixelsToArray(this BitmapSource src)
    {
        int stride = src.PixelWidth * (src.Format.BitsPerPixel / 8);
        byte[] pixels = new byte[stride * src.PixelHeight];
        src.CopyPixels(pixels, stride, 0);
        return pixels;
    }
    public static BitmapSource LoadImage96Dpi(string path)
    {
        var bmp = new BitmapImage();
        bmp.BeginInit();
        bmp.UriSource = new Uri(path);
        bmp.CacheOption = BitmapCacheOption.OnLoad;
        bmp.EndInit();
        bmp.Freeze();

        if (bmp.DpiX == 96 && bmp.DpiY == 96)
            return bmp;

        var img = ChangeDpi(bmp, 96, 96);
        img.Freeze();

        return img;
    }    
}

ファイル名:MainWindow.xaml

<Window x:Class="ImageScaleTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ImageScaleTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid x:Name="Grid0">
        <ScrollViewer x:Name="Scroll1"
                      HorizontalScrollBarVisibility="Auto"
                      VerticalScrollBarVisibility="Auto">
            <Canvas x:Name="Canvas1">   <!-- ScrollViewを反応させるためのCanvas -->
                <Image x:Name="Image1"
                       Stretch="None"/>
            </Canvas>
        </ScrollViewer>
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Maywork.WPF.Helpers;


namespace ImageScaleTest;

public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();

        Image1.Source = OpenCvHelpers.CreateCrossImage();

        // 初期倍率
        Image1.RenderTransform  = new ScaleTransform(1.0, 1.0);  // 倍率1倍
        // ImageとCanvasの解像度はセットで変更
        Canvas1.Width  = Image1.Source.Width;
        Canvas1.Height = Image1.Source.Height;

        // スクロールビューアでマウスホイールイベント
        Scroll1.PreviewMouseWheel += (sender, e) =>
        {
            // Ctrl キーが押されているか
            if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
            {
                // 直近の倍率取得
                var scale = Image1.RenderTransform as ScaleTransform;
                if (scale == null) return;  // 取得できない場合諦める

                const double zoomFactor = 1.1;

                if (e.Delta > 0)
                {
                    // ホイール上(ズームインなど)
                    scale.ScaleX = scale.ScaleX * zoomFactor;
                }
                else
                {
                    // ホイール下(ズームアウトなど)
                    scale.ScaleX = scale.ScaleX / zoomFactor;
                }
                // 最大最小
                scale.ScaleX = Math.Clamp(scale.ScaleX, 0.1, 10.0);
                scale.ScaleY = scale.ScaleX;

                // Imageが変更されたらキャンバスサイズ調整
                Canvas1.Width  = Image1.Source.Width  * scale.ScaleX;
                Canvas1.Height = Image1.Source.Height * scale.ScaleY;

                // ScrollViewer に通常スクロールさせない
                e.Handled = true;
            }
        };

        // ドラックアンドドロップ
        Wiring.AcceptFilesPreview(Grid0, files=>
        {
            var file = files.FirstOrDefault();
            if (file is null) return;

            // 画像ファイルのロード
            Image1.Source = ImageHelper.LoadImage96Dpi(file);
            // 初期倍率
            Image1.RenderTransform  = new ScaleTransform(1.0, 1.0);  // 倍率1倍
            // ImageとCanvasの解像度はセットで変更
            Canvas1.Width  = Image1.Source.Width;
            Canvas1.Height = Image1.Source.Height;

        }, ".jpeg", ".jpg", ".png", ".bmp", ".webp"); // ←対応画像拡張子指定
    }
}

ファイル名:Wiring.cs

// イベントなどの配線関連のヘルパー群
using System.Windows;
using System.Windows.Input;

namespace Maywork.WPF.Helpers;
public static class Wiring
{
    /*
     * コントロールにファイルのドラッグアンドドロップするヘルパー(Preview版)
     * 受け入れ拡張子をオプション指定する機能あり。
     */
    public static void AcceptFilesPreview(
        FrameworkElement el,
        Action<string[]> onFiles,
        params string[] exts)
    {
        el.AllowDrop = true;

        DragEventHandler? over = null;
        DragEventHandler? drop = null;

        over = (_, e) =>
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
                e.Effects = DragDropEffects.Copy;
            else
                e.Effects = DragDropEffects.None;

            // このイベントはここで処理済みであることを通知する
            // (以降のコントロールへイベントを伝播させない)
            e.Handled = true;
        };

        drop = (_, e) =>
        {
            if (!e.Data.GetDataPresent(DataFormats.FileDrop))
                return; // ファイルドロップ以外は処理せず、次の要素へイベントを流す

            var files = (string[])e.Data.GetData(DataFormats.FileDrop)!;

            if (exts?.Length > 0)
            {
                files = files
                    .Where(f => exts.Any(x =>
                        f.EndsWith(x, StringComparison.OrdinalIgnoreCase)))
                    .ToArray();
            }

            if (files.Length > 0)
                onFiles(files);

            // 外部ファイルのドロップはここで責任を持って処理したことを示す
            // (以降の RoutedEvent の伝播を終了させる)
            e.Handled = true;
        };

        el.PreviewDragOver += over; // Preview(Tunnel)段階で受信
        el.PreviewDrop += drop;     // Preview(Tunnel)段階で受信
    }
}

ホイールボタンドラックでパン

using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Maywork.WPF.Helpers;


namespace ImageScaleTest;

public partial class MainWindow : Window
{
    // パンの状態管理クラス
    private sealed class PanState
    {
        public bool IsDragging = false;
        public Point StartPoint = new Point(0, 0);
        public double StartHOffset = 0.0d;
        public double StartVOffset = 0.0d;
    }
    PanState _panState = new();

    public MainWindow()
    {
        InitializeComponent();

        // イメージの初期化
        Image1.Source = OpenCvHelpers.CreateCrossImage();



        // 初期倍率
        Image1.RenderTransform  = new ScaleTransform(1.0, 1.0);  // 倍率1倍
        // ImageとCanvasの解像度はセットで変更
        Canvas1.Width  = Image1.Source.Width;
        Canvas1.Height = Image1.Source.Height;

        // スクロールビューアでマウスホイールイベント
        Scroll1.PreviewMouseWheel += (sender, e) =>
        {
            // Ctrl キーが押されているか
            if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
            {
                // 直近の倍率取得
                var scale = Image1.RenderTransform as ScaleTransform;
                if (scale == null) return;  // 取得できない場合諦める

                const double zoomFactor = 1.1;

                if (e.Delta > 0)
                {
                    // ホイール上(ズームインなど)
                    scale.ScaleX = scale.ScaleX * zoomFactor;
                }
                else
                {
                    // ホイール下(ズームアウトなど)
                    scale.ScaleX = scale.ScaleX / zoomFactor;
                }
                // 最大最小
                scale.ScaleX = Math.Clamp(scale.ScaleX, 0.1, 10.0);
                scale.ScaleY = scale.ScaleX;

                // Imageが変更されたらキャンバスサイズ調整
                Canvas1.Width  = Image1.Source.Width  * scale.ScaleX;
                Canvas1.Height = Image1.Source.Height * scale.ScaleY;

                // ScrollViewer に通常スクロールさせない
                e.Handled = true;
            }
        };

        // ドラックアンドドロップ
        Wiring.AcceptFilesPreview(Grid0, files=>
        {
            var file = files.FirstOrDefault();
            if (file is null) return;

            // 画像ファイルのロード
            Image1.Source = ImageHelper.LoadImage96Dpi(file);
            // 初期倍率
            Image1.RenderTransform  = new ScaleTransform(1.0, 1.0);  // 倍率1倍
            // ImageとCanvasの解像度はセットで変更
            Canvas1.Width  = Image1.Source.Width;
            Canvas1.Height = Image1.Source.Height;

        }, ".jpeg", ".jpg", ".png", ".bmp", ".webp"); // ←対応画像拡張子指定

        // スクロールのパン:マウスボタン押下
        Scroll1.PreviewMouseDown += (sender, e) =>
        {
            // ホイールボタン以外排除
            if (e.ChangedButton != MouseButton.Middle) return;
        
            _panState.IsDragging = true;
            _panState.StartPoint = e.GetPosition(Scroll1);
            _panState.StartHOffset = Scroll1.HorizontalOffset;
            _panState.StartVOffset = Scroll1.VerticalOffset;

            Scroll1.CaptureMouse();
            e.Handled = true;
        };

        // スクロールのパン:マウス移動
        Scroll1.PreviewMouseMove += (s, e) =>
        {
            if (!_panState.IsDragging)
                return;

            Point p = e.GetPosition(Scroll1);
            Vector delta = p - _panState.StartPoint;

            Scroll1.ScrollToHorizontalOffset(_panState.StartHOffset - delta.X);
            Scroll1.ScrollToVerticalOffset(_panState.StartVOffset - delta.Y);

            e.Handled = true;
        };

        // スクロールのパン:マウスボタン解放
        Scroll1.PreviewMouseUp += (s, e) =>
        {
            // ホイールボタン以外排除
            if (e.ChangedButton != MouseButton.Middle) return;

            EndPan();
            e.Handled = true;
        };

        // スクロールのパン:喪失
        Scroll1.LostMouseCapture += (s, e) => EndPan();
    }

    void EndPan()
    {
        if (!_panState.IsDragging) return;

        _panState.IsDragging = false;
        Scroll1.ReleaseMouseCapture();
    }

}

拡大縮小・パン移動・D&D・画像ビューアになりました。

これをUserControlにすると良いのかもしれませんが、

完成度のクオリティからすると、

当面は無理ですね。

ヘルパー化で再利用

ズームとパン機能をImageScaleHelper.csに切り出しました。

XAMLのレイアウトはある程度固定する必要がありますが、

Scroll・Canvas・Imageの組み合わせなので、

結構使える場面も多いと思います。

全ソース
ファイル名:ImageHelper.cs

using System.Windows.Media.Imaging;

namespace Maywork.WPF.Helpers;

public static class ImageHelper
{
    public static BitmapSource ChangeDpi(BitmapSource src, double dpiX, double dpiY)
    {
        return BitmapSource.Create(
            src.PixelWidth,
            src.PixelHeight,
            dpiX,
            dpiY,
            src.Format,
            src.Palette,
            src.CopyPixelsToArray(),
            src.PixelWidth * (src.Format.BitsPerPixel / 8)
        );
    }
    public static byte[] CopyPixelsToArray(this BitmapSource src)
    {
        int stride = src.PixelWidth * (src.Format.BitsPerPixel / 8);
        byte[] pixels = new byte[stride * src.PixelHeight];
        src.CopyPixels(pixels, stride, 0);
        return pixels;
    }
    public static BitmapSource LoadImage96Dpi(string path)
    {
        var bmp = new BitmapImage();
        bmp.BeginInit();
        bmp.UriSource = new Uri(path);
        bmp.CacheOption = BitmapCacheOption.OnLoad;
        bmp.EndInit();
        bmp.Freeze();

        if (bmp.DpiX == 96 && bmp.DpiY == 96)
            return bmp;

        var img = ChangeDpi(bmp, 96, 96);
        img.Freeze();

        return img;
    }    
}

ファイル名: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<ScrollViewer, Controller> _table = new();

    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(scroll, out _))
            return;
        
        EnsureImageInitialized(image);

        var controller = new Controller(scroll, canvas, image);
        _table.Add(scroll, controller);
    }

    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);
        }
    }
}

ファイル名:ImageScaleTest.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
    <PackageReference Include="OpenCvSharp4.WpfExtensions" Version="4.11.0.20250507" />
  </ItemGroup>

</Project>

ファイル名:MainWindow.xaml

<Window x:Class="ImageScaleTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ImageScaleTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid x:Name="Grid0">
        <ScrollViewer x:Name="Scroll1"
                      HorizontalScrollBarVisibility="Auto"
                      VerticalScrollBarVisibility="Auto">
            <Canvas x:Name="Canvas1">   <!-- ScrollViewを反応させるためのCanvas -->
                <Image x:Name="Image1"
                       Stretch="None"/>
            </Canvas>
        </ScrollViewer>
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Windows;
using System.Windows.Media;
using Maywork.WPF.Helpers;


namespace ImageScaleTest;

public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();

        // イメージの初期化
        Image1.Source = OpenCvHelpers.CreateCrossImage();

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

        // ドラックアンドドロップ
        Wiring.AcceptFilesPreview(Grid0, files=>
        {
            var file = files.FirstOrDefault();
            if (file is null) return;

            // 画像ファイルのロード
            Image1.Source = ImageHelper.LoadImage96Dpi(file);
            // 初期倍率
            Image1.RenderTransform  = new ScaleTransform(1.0, 1.0);  // 倍率1倍
            // ImageとCanvasの解像度はセットで変更
            Canvas1.Width  = Image1.Source.Width;
            Canvas1.Height = Image1.Source.Height;

        }, ".jpeg", ".jpg", ".png", ".bmp", ".webp"); // ←対応画像拡張子指定
    }
}

ファイル名:OpenCvHelper.cs

using System.Windows.Media.Imaging;
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;

namespace Maywork.WPF.Helpers;

public static class OpenCvHelpers
{
    public static BitmapSource CreateCrossImage()
    {
        // 1. 128x128のカラー画像(3チャンネル)を生成(背景:黒)
        using (Mat mat = new Mat(128, 128, MatType.CV_8UC3, Scalar.All(0)))
        {
            // 2. 左上から右下への線 (0,0) -> (127,127)
            // 引数: 画像, 開始点, 終了点, 色(BGR), 厚さ, 連結
            mat.Line(
                new Point(0, 0),
                new Point(127, 127),
                Scalar.Red, 1, LineTypes.AntiAlias);

            // 3. 右上から左下への線 (127,0) -> (0,127)
            mat.Line(
                new Point(127, 0),
                new Point(0, 127),
                Scalar.Red, 1, LineTypes.AntiAlias);

            // 4. WPFのImageコントロールで使える形式に変換
            return mat.ToBitmapSource();
        }
    }
}

ファイル名:Wiring.cs

// イベントなどの配線関連のヘルパー群
using System.Windows;
using System.Windows.Input;

namespace Maywork.WPF.Helpers;
public static class Wiring
{
    /*
     * コントロールにファイルのドラッグアンドドロップするヘルパー(Preview版)
     * 受け入れ拡張子をオプション指定する機能あり。
     */
    public static void AcceptFilesPreview(
        FrameworkElement el,
        Action<string[]> onFiles,
        params string[] exts)
    {
        el.AllowDrop = true;

        DragEventHandler? over = null;
        DragEventHandler? drop = null;

        over = (_, e) =>
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
                e.Effects = DragDropEffects.Copy;
            else
                e.Effects = DragDropEffects.None;

            // このイベントはここで処理済みであることを通知する
            // (以降のコントロールへイベントを伝播させない)
            e.Handled = true;
        };

        drop = (_, e) =>
        {
            if (!e.Data.GetDataPresent(DataFormats.FileDrop))
                return; // ファイルドロップ以外は処理せず、次の要素へイベントを流す

            var files = (string[])e.Data.GetData(DataFormats.FileDrop)!;

            if (exts?.Length > 0)
            {
                files = files
                    .Where(f => exts.Any(x =>
                        f.EndsWith(x, StringComparison.OrdinalIgnoreCase)))
                    .ToArray();
            }

            if (files.Length > 0)
                onFiles(files);

            // 外部ファイルのドロップはここで責任を持って処理したことを示す
            // (以降の RoutedEvent の伝播を終了させる)
            e.Handled = true;
        };

        el.PreviewDragOver += over; // Preview(Tunnel)段階で受信
        el.PreviewDrop += drop;     // Preview(Tunnel)段階で受信
    }
}

コメント