XAMLを使わないWPF入門37「ドラック&ドロップ・ビヘイビア ー 画像の2値化」

コンピュータ

ファイル名:NoXAML37DragAndDrop.csproj


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

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

</Project>

ファイル名:App.cs


using System.Windows;
using NoXAML37DragAndDrop.ViewModels;
using NoXAML37DragAndDrop.Views;

namespace NoXAML37DragAndDrop;

public class App : Application
{
    [STAThread]
    public static void Main()
    {
        var app = new App();
        var vm = new MainViewModel();
        var win = new MainWindow { DataContext = vm };
        app.Run(win);
    }
}

ファイル名:AssemblyInfo.cs


using System.Windows;

[assembly:ThemeInfo(
    ResourceDictionaryLocation.None,            //where theme specific resource dictionaries are located
                                                //(used if a resource is not found in the page,
                                                // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly   //where the generic resource dictionary is located
                                                //(used if a resource is not found in the page,
                                                // app, or any theme specific resource dictionaries)
)]

ファイル名:Behaviors\DragDropBehavior.cs


using System.Linq;
using System.Windows;
using System.Windows.Input;

namespace NoXAML37DragAndDrop.Behaviors;

public static class DragDropBehavior
{
    public static readonly DependencyProperty DropCommandProperty =
        DependencyProperty.RegisterAttached(
            "DropCommand", typeof(ICommand), typeof(DragDropBehavior),
            new PropertyMetadata(null, OnDropCommandChanged));

    public static void SetDropCommand(DependencyObject obj, ICommand? value) => obj.SetValue(DropCommandProperty, value);
    public static ICommand? GetDropCommand(DependencyObject obj) => (ICommand?)obj.GetValue(DropCommandProperty);

    public static readonly DependencyProperty AllowedExtensionsProperty =
        DependencyProperty.RegisterAttached(
            "AllowedExtensions", typeof(string[]), typeof(DragDropBehavior),
            new PropertyMetadata(null));

    public static void SetAllowedExtensions(DependencyObject obj, string[]? value) => obj.SetValue(AllowedExtensionsProperty, value);
    public static string[]? GetAllowedExtensions(DependencyObject obj) => (string[]?)obj.GetValue(AllowedExtensionsProperty);

    private static void OnDropCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is UIElement ui)
        {
            ui.AllowDrop = true;
            ui.DragOver -= OnDragOver;
            ui.Drop -= OnDrop;
            if (e.NewValue is ICommand)
            {
                ui.DragOver += OnDragOver;
                ui.Drop += OnDrop;
            }
        }
    }

    private static void OnDragOver(object sender, DragEventArgs e)
    {
        var allowed = sender as DependencyObject is { } dobj ? GetAllowedExtensions(dobj) : null;

        if (e.Data.GetDataPresent(DataFormats.FileDrop))
        {
            var paths = (string[])e.Data.GetData(DataFormats.FileDrop);
            bool ok = paths.Any(p =>
            {
                if (allowed is null || allowed.Length == 0) return true;
                var ext = System.IO.Path.GetExtension(p).ToLowerInvariant();
                return allowed.Contains(ext);
            });

            e.Effects = ok ? DragDropEffects.Copy : DragDropEffects.None;
            e.Handled = true;
            return;
        }
        e.Effects = DragDropEffects.None; e.Handled = true;
    }

    private static void OnDrop(object sender, DragEventArgs e)
    {
        if (GetDropCommand((DependencyObject)sender) is not ICommand cmd) return;
        if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
        var files = (string[])e.Data.GetData(DataFormats.FileDrop);
        if (cmd.CanExecute(files)) cmd.Execute(files);
        e.Handled = true;
    }
}

ファイル名:Converters\BooleanToVisibilityConverter.cs


using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace NoXAML37DragAndDrop.Converters;

public sealed class BooleanToVisibilityConverter : IValueConverter
{
    public bool Invert { get; set; }
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        bool v = value is bool b && b;
        if (Invert) v = !v;
        return v ? Visibility.Visible : Visibility.Collapsed;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        => (value is Visibility vis && vis == Visibility.Visible) ^ Invert;
}

ファイル名:Models\ImageDocument.cs


using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace NoXAML37DragAndDrop.Models;

public sealed class ImageDocument
{
    public int Width { get; }
    public int Height { get; }
    public double DpiX { get; }
    public double DpiY { get; }
    public int Stride { get; } // bytes
    public byte[] OriginalPixels { get; } // BGRA32

    public ImageDocument(int width, int height, double dpiX, double dpiY, byte[] originalPixels)
    {
        Width = width; Height = height; DpiX = dpiX; DpiY = dpiY;
        Stride = width * 4;
        OriginalPixels = originalPixels;
    }

    public BitmapSource ToBitmap(byte[] pixels)
    {
        var wb = new WriteableBitmap(Width, Height, DpiX, DpiY, PixelFormats.Bgra32, null);
        wb.WritePixels(new Int32Rect(0, 0, Width, Height), pixels, Stride, 0);
        return wb; // Freeze不要(UIスレッドでのみ使用)
    }

    public BitmapSource ToBitmapFromOriginal() => ToBitmap(OriginalPixels);
}

ファイル名:Services\ImageLoader.cs


using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using NoXAML37DragAndDrop.Models;

namespace NoXAML37DragAndDrop.Services;

public static class ImageLoader
{
    private static readonly string[] _exts = new[] { ".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".gif", ".webp" };

    public static bool IsSupported(string path)
        => File.Exists(path) && _exts.Contains(Path.GetExtension(path).ToLowerInvariant());

    public static ImageDocument Load(string path)
    {
        // ファイルロック回避のため OnLoad
        var bi = new BitmapImage();
        bi.BeginInit();
        bi.CacheOption = BitmapCacheOption.OnLoad;
        bi.UriSource = new Uri(Path.GetFullPath(path));
        bi.EndInit();
        bi.Freeze();

        var src = new FormatConvertedBitmap(bi, PixelFormats.Bgra32, null, 0);
        src.Freeze();

        int w = src.PixelWidth, h = src.PixelHeight, stride = w * 4;
        byte[] pixels = new byte[stride * h];
        src.CopyPixels(pixels, stride, 0);

        return new ImageDocument(w, h, src.DpiX, src.DpiY, pixels);
    }
}

ファイル名:Services\ImageProcessor.cs



namespace NoXAML37DragAndDrop.Services;

public static class ImageProcessor
{
    // BGRA32 → 2値(B/W)ピクセル配列を返す(A=255)
    public static byte[] Binarize(byte[] src, int width, int height, byte threshold)
    {
        int stride = width * 4;
        var dst = new byte[src.Length];

        for (int y = 0; y < height; y++)
        {
            int row = y * stride;
            for (int x = 0; x < width; x++)
            {
                int i = row + x * 4;
                byte b = src[i + 0], g = src[i + 1], r = src[i + 2];
                int luma = (int)(0.2126 * r + 0.7152 * g + 0.0722 * b + 0.5);
                byte v = (byte)(luma >= threshold ? 255 : 0);
                dst[i + 0] = v; dst[i + 1] = v; dst[i + 2] = v; dst[i + 3] = 255;
            }
        }
        return dst;
    }
}

ファイル名:ViewModels\MainViewModel.cs


using System.Windows.Media.Imaging;
using System.Windows.Input;
using NoXAML37DragAndDrop.Services;
using NoXAML37DragAndDrop.Models;
using System.Windows;

namespace NoXAML37DragAndDrop.ViewModels;

public sealed class MainViewModel : ObservableObject
{
    private ImageDocument? _doc;
    private BitmapSource? _currentBitmap;
    private bool _hasImage;
    private int _threshold = 128;

    public BitmapSource? CurrentBitmap
    {
        get => _currentBitmap;
        private set => SetProperty(ref _currentBitmap, value);
    }
    public Visibility HintVisibility => HasImage ? Visibility.Collapsed : Visibility.Visible;
    public bool HasImage
    {
        get => _hasImage;
        private set
        {
            if (SetProperty(ref _hasImage, value))
            {
                OnPropertyChanged(nameof(IsHintVisible));
                OnPropertyChanged(nameof(HintVisibility)); 
                BinarizeCommandCanRaise();
                ResetCommandCanRaise();
            }
        }
    }

    public bool IsHintVisible => !HasImage;

    public int Threshold
    {
        get => _threshold;
        set => SetProperty(ref _threshold, Math.Clamp(value, 0, 255));
    }

    public ICommand DropFilesCommand { get; }
    public RelayCommand BinarizeCommand { get; }
    public RelayCommand ResetCommand { get; }

    public MainViewModel()
    {
        DropFilesCommand = new RelayCommand(p =>
        {
            if (p is string[] files && files.Length > 0)
            {
                var first = files.FirstOrDefault(ImageLoader.IsSupported);
                if (first is null) return;
                try
                {
                    _doc = ImageLoader.Load(first);
                    CurrentBitmap = _doc.ToBitmapFromOriginal();
                    HasImage = true;
                }
                catch
                {
                    // 必要ならエラーハンドリングを追加
                }
            }
        });

        BinarizeCommand = new RelayCommand(_ =>
        {
            if (_doc is null) return;
            var bin = ImageProcessor.Binarize(_doc.OriginalPixels, _doc.Width, _doc.Height, (byte)Threshold);
            CurrentBitmap = _doc.ToBitmap(bin);
        }, _ => HasImage);

        ResetCommand = new RelayCommand(_ =>
        {
            if (_doc is null) return;
            CurrentBitmap = _doc.ToBitmapFromOriginal();
        }, _ => HasImage);
    }

    private void BinarizeCommandCanRaise() => BinarizeCommand.RaiseCanExecuteChanged();
    private void ResetCommandCanRaise() => ResetCommand.RaiseCanExecuteChanged();
}

ファイル名:ViewModels\ObservableObject.cs


using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace NoXAML37DragAndDrop.ViewModels;

public abstract class ObservableObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

    protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? name = null)
    {
        if (Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(name);
        return true;
    }
}

ファイル名:ViewModels\RelayCommand.cs


using System.Windows.Input;

namespace NoXAML37DragAndDrop.ViewModels;

public sealed class RelayCommand : ICommand
{
    private readonly Action<object?> _exec;
    private readonly Func<object?, bool>? _can;
    public RelayCommand(Action<object?> exec, Func<object?, bool>? can = null)
    { _exec = exec; _can = can; }
    public bool CanExecute(object? p) => _can?.Invoke(p) ?? true;
    public void Execute(object? p) => _exec(p);
    public event EventHandler? CanExecuteChanged;
    public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

ファイル名:Views\MainWindow.cs


using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using NoXAML37DragAndDrop.Behaviors;
using NoXAML37DragAndDrop.ViewModels;

namespace NoXAML37DragAndDrop.Views;

public sealed class MainWindow : Window
{
    public MainWindow()
    {
        Title = "DnD → 表示 → 2値化(MVVM / NoXAML)";
        Width = 1000; Height = 700;

        var root = new Grid();
        root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
        root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });

        // === Top bar ===
        var bar = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(8), VerticalAlignment = VerticalAlignment.Center };

        bar.Children.Add(new TextBlock { Text = "閾値:", VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0,0,6,0) });

        var slider = new Slider { Minimum = 0, Maximum = 255, Width = 200, Margin = new Thickness(0,0,8,0) };
        slider.SetBinding(Slider.ValueProperty, new Binding(nameof(MainViewModel.Threshold)) { Mode = BindingMode.TwoWay });
        bar.Children.Add(slider);

        var binBtn = new Button { Content = "2値化", Margin = new Thickness(0,0,8,0), Padding = new Thickness(10,6,10,6) };
        binBtn.SetBinding(Button.CommandProperty, new Binding(nameof(MainViewModel.BinarizeCommand)));
        bar.Children.Add(binBtn);

        var resetBtn = new Button { Content = "元に戻す", Padding = new Thickness(10,6,10,6) };
        resetBtn.SetBinding(Button.CommandProperty, new Binding(nameof(MainViewModel.ResetCommand)));
        bar.Children.Add(resetBtn);

        Grid.SetRow(bar, 0);
        root.Children.Add(bar);

        // === Image area ===
        var border = new Border { BorderBrush = Brushes.LightGray, BorderThickness = new Thickness(1), Margin = new Thickness(8) };

        var displayGrid = new Grid();

        var img = new Image { Stretch = Stretch.Uniform, SnapsToDevicePixels = true };
        img.SetBinding(Image.SourceProperty, new Binding(nameof(MainViewModel.CurrentBitmap)));

        var hint = new TextBlock
        {
            Text = "ここに画像をドラッグ&ドロップ",
            Foreground = Brushes.Gray,
            HorizontalAlignment = HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center
        };
        var hintBinding = new Binding(nameof(MainViewModel.HintVisibility));
        hint.SetBinding(UIElement.VisibilityProperty, hintBinding);

        displayGrid.Children.Add(img);
        displayGrid.Children.Add(hint);

        border.Child = new ScrollViewer
        {
            Content = displayGrid,
            HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
            VerticalScrollBarVisibility = ScrollBarVisibility.Auto
        };

        // === Attach D&D behavior (bind to VM command) ===
        // 許可拡張子
        DragDropBehavior.SetAllowedExtensions(border, new[] { ".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".gif", ".webp" });
        // Drop -> ViewModel.DropFilesCommand
        var dropBinding = new Binding(nameof(MainViewModel.DropFilesCommand));
        BindingOperations.SetBinding(border, DragDropBehavior.DropCommandProperty, dropBinding);

        Grid.SetRow(border, 1);
        root.Children.Add(border);

        Content = root;
    }
}

コメント