WPFヘルパー: SelectionCropper.cs – 矩形選択

コンピュータ

SelectionCropper.cs

// 矩形選択
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 SelectionCropper
{
    #region Enable

    public static readonly DependencyProperty EnableProperty =
        DependencyProperty.RegisterAttached(
            "Enable",
            typeof(bool),
            typeof(SelectionCropper),
            new PropertyMetadata(false, OnEnableChanged));

    public static void SetEnable(DependencyObject obj, bool value)
        => obj.SetValue(EnableProperty, value);

    public static bool GetEnable(DependencyObject obj)
        => (bool)obj.GetValue(EnableProperty);

    #endregion

    #region CropCommand

    public static readonly DependencyProperty CropCommandProperty =
        DependencyProperty.RegisterAttached(
            "CropCommand",
            typeof(ICommand),
            typeof(SelectionCropper),
            new PropertyMetadata(null));

    public static void SetCropCommand(DependencyObject obj, ICommand value)
        => obj.SetValue(CropCommandProperty, value);

    public static ICommand? GetCropCommand(DependencyObject obj)
        => (ICommand?)obj.GetValue(CropCommandProperty);

    #endregion

    private static System.Windows.Shapes.Rectangle? _selectionRect;
    private static Point _start;
    private static bool _dragging;

    private static void OnEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not ScrollViewer sv)
            return;

        if ((bool)e.NewValue)
        {
            sv.Loaded += OnLoaded;
        }
    }

    private static void OnLoaded(object sender, RoutedEventArgs e)
    {
        if (sender is not ScrollViewer sv)
            return;

        var canvas = FindChild<Canvas>(sv);
        if (canvas == null)
            return;

        _selectionRect = new System.Windows.Shapes.Rectangle
        {
            Stroke = Brushes.Red,
            StrokeThickness = 1,
            Fill = new SolidColorBrush(Color.FromArgb(80, 255, 0, 0)),
            Visibility = Visibility.Collapsed
        };

        canvas.Children.Add(_selectionRect);

        canvas.MouseLeftButtonDown += (s, ev) =>
        {
            _start = ev.GetPosition(canvas);
            _dragging = true;

            Canvas.SetLeft(_selectionRect, _start.X);
            Canvas.SetTop(_selectionRect, _start.Y);
            _selectionRect.Width = 0;
            _selectionRect.Height = 0;
            _selectionRect.Visibility = Visibility.Visible;

            canvas.CaptureMouse();
        };

        canvas.MouseMove += (s, ev) =>
        {
            if (!_dragging) return;

            var pos = ev.GetPosition(canvas);

            double x = Math.Min(pos.X, _start.X);
            double y = Math.Min(pos.Y, _start.Y);
            double w = Math.Abs(pos.X - _start.X);
            double h = Math.Abs(pos.Y - _start.Y);

            Canvas.SetLeft(_selectionRect, x);
            Canvas.SetTop(_selectionRect, y);
            _selectionRect.Width = w;
            _selectionRect.Height = h;
        };

        canvas.MouseLeftButtonUp += (s, ev) =>
        {
            if (!_dragging) return;
            _dragging = false;
            canvas.ReleaseMouseCapture();

            ExecuteCrop(sv, canvas);
        };
    }
	public readonly record struct ImageSelectionRect(
		int X,
		int Y,
		int Width,
		int Height);
    private static void ExecuteCrop(ScrollViewer sv, Canvas canvas)
    {
		if (_selectionRect == null)
			return;

		var image = FindChild<Image>(canvas);
		if (image?.Source is not BitmapSource bmp)
			return;

		// TransformToVisualで座標変換だけする
		GeneralTransform t = canvas.TransformToVisual(image);

		double cx = Canvas.GetLeft(_selectionRect);
		double cy = Canvas.GetTop(_selectionRect);

		var p0 = t.Transform(new Point(cx, cy));
		var p1 = t.Transform(new Point(cx + _selectionRect.Width,
									cy + _selectionRect.Height));

		int x = (int)Math.Round(Math.Min(p0.X, p1.X));
		int y = (int)Math.Round(Math.Min(p0.Y, p1.Y));
		int w = (int)Math.Round(Math.Abs(p1.X - p0.X));
		int h = (int)Math.Round(Math.Abs(p1.Y - p0.Y));

		if (w <= 0 || h <= 0)
			return;

		var selection = new ImageSelectionRect(x, y, w, h);

		var cmd = GetCropCommand(sv);
		if (cmd?.CanExecute(selection) == true)
			cmd.Execute(selection);

		_selectionRect.Visibility = Visibility.Collapsed;
    }

    private static T? FindChild<T>(DependencyObject parent) where T : DependencyObject
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
        {
            var child = VisualTreeHelper.GetChild(parent, i);

            if (child is T t)
                return t;

            var result = FindChild<T>(child);
            if (result != null)
                return result;
        }
        return null;
    }
}

/*

// 使い方

XAML
xmlns:h="clr-namespace:Maywork.WPF.Helpers"

<Grid>
	<ScrollViewer
		h:SelectionCropper.Enable="True"
		h:SelectionCropper.CropCommand="{Binding CropCommand}" />
		<Canvas>
			<Image Source="{Binding CurrentImage}"/>
		</Canvas>
	</ScrollViewer>
</Grid>

public RelayCommand<ImageSelectionRect> CropCommand { get; }


ViewModel
public MainWindowViewModel()
{
    CropCommand = new RelayCommand<ImageSelectionRect>(OnCrop);
}

private void OnCrop(ImageSelectionRect rect)
{
    if (CurrentImage is not BitmapSource bmp)
        return;
	
	// rectで座標系の情報が渡されるので
	// ここでクロップなどの処理を行う
	Debug.Print($"X:{rect.X} Y:{rect.Y} W:{rect.Width} H:{rect.Height}");
}
*/
Download

コメント