ファイル名: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;
}
}
コメント