【WPF】图片剪裁-ImageCropping

背景

机缘巧合吧,当时在全网寻找图像剪裁工具,但大都不能满足需求,于是决定动手写。当然如果只是为了完成这么一个功能就没有必要记录了,主要是不依赖与第三方图像库,且实现方式简单,自我感觉很有纪念意义。

技术栈

  • C#
  • .NET6 可无缝升级后续版本
  • WPF
  • 无第三方库依赖

实现思路

  • 使用 Canvas 绘制选区,监听鼠标Down、Move事件,在事件中获取鼠标位置处理选框绘制
    • 获取鼠标左键 Down 时的屏幕坐标,记录为画框起始位置
    • 在鼠标左键并且移动的过程中获取鼠标的屏幕位置,记录为画框结束位置
    • Move 的过程中需要不断清除之前的画框,并重新绘制出新的画框,直到左键松开,绘制出最新的画框
  • 根据最新的画框位置,将屏幕位置转换成图像位置,从而获取到真实的图像选择区域
  • 通过 CroppedBitmap 取出备选区域的图像内容

核心代码

界面布局

<Grid Margin="5">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="2.5*" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <Border Background="WhiteSmoke" CornerRadius="4">
        <Canvas
            x:Name="MainCanvas"
            MouseDown="Canvas_MouseDown"
            MouseMove="Canvas_MouseMove">
            <Image x:Name="MainImage" Stretch="Uniform" />
        </Canvas>
    </Border>

    <Grid Grid.Column="1" Margin="5,0,0,0">
        <Grid.RowDefinitions>
            <RowDefinition Height="50" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
            <Button
                Width="100"
                Height="30"
                Margin="10,0"
                Background="DarkGreen"
                Click="Browse_Click"
                Content="浏览" />
            <Button
                Width="100"
                Height="30"
                Margin="10,0"
                Background="DarkBlue"
                Click="Save_Click"
                Content="另存" />
        </StackPanel>
        <Border
            Grid.Row="1"
            Background="WhiteSmoke"
            CornerRadius="4">
            <Image
                x:Name="ResultImage"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Stretch="Uniform" />
        </Border>
    </Grid>
</Grid>

Style

<Style TargetType="{x:Type Button}">
    <Setter Property="Foreground" Value="White" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Border
                    Width="{TemplateBinding Width}"
                    Height="{TemplateBinding Height}"
                    Background="{TemplateBinding Background}"
                    CornerRadius="4"
                    Opacity="{TemplateBinding Opacity}">
                    <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Opacity" Value="0.85" />
                        <Setter Property="Cursor" Value="Hand" />
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter Property="Background" Value="Gray" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

处理逻辑

using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Microsoft.Win32;

namespace ImageCropping
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private Point startPoint; // Canvas 起始点
        private System.Drawing.Point imageStartPoint; // Image 起始点
        private BitmapImage? bitmapImage;
        private CroppedBitmap? croppedBitmap;
        private bool isDragging = false; // 是否正在拖拽
        private int pixelWidth = 0;
        private int pixelHeight = 0;

        public MainWindow()
        {
            InitializeComponent();
            Init();
        }

        #region Events
        private void Canvas_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.LeftButton is MouseButtonState.Pressed)
            {
                startPoint = e.GetPosition(MainCanvas);
                var currentImagePoint = e.GetPosition(MainImage);

                imageStartPoint = GetPoint(currentImagePoint.X, currentImagePoint.Y);
                isDragging = true;
            }

            if (e.RightButton is MouseButtonState.Pressed)
            {
                // 清除之前的选框
                ClearSelectionRectangle();
            }
        }

        private void Canvas_MouseMove(object sender, MouseEventArgs e)
        {
            if (isDragging && e.LeftButton is MouseButtonState.Pressed)
            {
                var currentPoint = e.GetPosition(MainCanvas);
                var rect = new Rect(startPoint, currentPoint);


                DrawSelectionRectangle(rect);
            }
            else
            {
                if (isDragging && MainImage.ActualWidth is not 0)
                {
                    var currentImagePoint = e.GetPosition(MainImage);
                    FinalizeSelection(GetPoint(currentImagePoint.X, currentImagePoint.Y));
                    isDragging = false;
                }
            }
        }

        private void Browse_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                OpenFileDialog openFileDialog = new()
                {
                    Filter = "Image files (*.jpg;*.jpeg;*.png;*.bmp)|*.jpg;*.jpeg;*.png;*.bmp",
                    Multiselect = false
                };
                if (openFileDialog.ShowDialog() is true)
                {
                    ResetObjects();

                    string path = openFileDialog.FileName;
                    bitmapImage = new BitmapImage();
                    bitmapImage.BeginInit();
                    bitmapImage.UriSource = new Uri(path, UriKind.RelativeOrAbsolute);
                    bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
                    bitmapImage.EndInit();

                    MainImage.Source = bitmapImage;
                    pixelHeight = bitmapImage.PixelHeight;
                    pixelWidth = bitmapImage.PixelWidth;
                }
            }
            catch (Exception ex)
            {
                ExceptionHandling(ex);
            }
        }

        private void Save_Click(object sender, RoutedEventArgs e)
        {
            if (croppedBitmap!.Source is not null)
            {
                SaveFileDialog saveFileDialog = new()
                {
                    Filter = "Image files (*.jpg;*.jpeg;*.png;*.bmp)|*.jpg;*.jpeg;*.png;*.bmp",
                   
                };
                if (saveFileDialog.ShowDialog() is true)
                {
                    string filePath = saveFileDialog.FileName;
                    SaveImage(croppedBitmap, filePath);
                }
            }
        }

        #endregion

        #region Private function
        void Init()
        {
            croppedBitmap = new CroppedBitmap();

            Binding widthBinding = new("ActualWidth")
            {
                Source = MainCanvas,
            };
            MainImage.SetBinding(WidthProperty, widthBinding);

            Binding heightBinding = new("ActualHeight")
            {
                Source = MainCanvas
            };
            MainImage.SetBinding(HeightProperty, heightBinding);
        }

        /// <summary>
        /// 绘制选择矩形
        /// </summary>
        /// <param name="rect"></param>
        private void DrawSelectionRectangle(Rect rect)
        {
            ClearSelectionRectangle();

            // 创建选框
            var selectionRect = new Rectangle
            {
                Stroke = Brushes.Red,
                StrokeThickness = 1,
                Fill = Brushes.Transparent,
                RenderTransformOrigin = new Point(0.5, 0.5),
                Visibility = Visibility.Visible
            };

            Canvas.SetLeft(selectionRect, rect.X);
            Canvas.SetTop(selectionRect, rect.Y);
            selectionRect.Width = Math.Abs(rect.Width);
            selectionRect.Height = Math.Abs(rect.Height);

            MainCanvas.Children.Add(selectionRect);
        }

        /// <summary>
        /// 清除选框
        /// </summary>
        private void ClearSelectionRectangle()
        {
            // 查找并移除选框
            var selectionRect = MainCanvas.Children.OfType<Rectangle>()
                .FirstOrDefault(r => r.Stroke == Brushes.Red && r.Fill == Brushes.Transparent);
            if (selectionRect is not null)
            {
                MainCanvas.Children.Remove(selectionRect);
            }
        }

        /// <summary>
        /// 提取选取内容
        /// </summary>
        /// <param name="point"></param>
        private void FinalizeSelection(System.Drawing.Point point)
        {
            try
            {
                Int32Rect rect = imageStartPoint.X < point.X
                    ? new Int32Rect(imageStartPoint.X, imageStartPoint.Y, point.X - imageStartPoint.X, point.Y - imageStartPoint.Y)
                    : new Int32Rect(point.X, point.Y, imageStartPoint.X - point.X, imageStartPoint.Y - point.Y);
                if (!rect.IsEmpty)
                {
                    croppedBitmap = new(bitmapImage, rect);
                    ResultImage.Source = croppedBitmap;
                }
            }
            catch
            {

            }
        }

        /// <summary>
        /// 计算像素坐标
        /// </summary>
        /// <param name="pointX">控件坐标 X</param>
        /// <param name="pointY">控件坐标 Y</param>
        /// <returns>像素位置坐标</returns>
        private System.Drawing.Point GetPoint(double pointX, double pointY)
        {

            int pixelX = Convert.ToInt32(pointX * pixelWidth / MainImage.ActualWidth);
            int pixelY = Convert.ToInt32(pointY * pixelHeight / MainImage.ActualHeight);
            return new System.Drawing.Point(pixelX, pixelY);
        }

        /// <summary>
        /// 重置对象
        /// </summary>
        private void ResetObjects()
        {
            ClearSelectionRectangle();
            croppedBitmap = new CroppedBitmap();
            ResultImage.Source = new BitmapImage();
        }

        /// <summary>
        /// 保存文件
        /// </summary>
        /// <param name="bitmapSource">图像数据</param>
        /// <param name="filePath">文件路径</param>
        private static void SaveImage(BitmapSource bitmapSource, string filePath)
        {
            try
            {
                string directoryPath = System.IO.Path.GetDirectoryName(filePath)!;
                if (!Directory.Exists(directoryPath))
                    Directory.CreateDirectory(directoryPath);

                string fileExtension = System.IO.Path.GetExtension(filePath).ToLower();
                using var fileStream = new FileStream(filePath, FileMode.Create);
                var encoder = GetEncoder(fileExtension);
                encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
                encoder.Save(fileStream);
            }
            catch (Exception ex)
            {
                ExceptionHandling(ex);
            }
        }

        /// <summary>
        /// 获取编码器
        /// </summary>
        /// <param name="fileExtension">文件扩展名</param>
        /// <returns>编码器</returns>
        private static BitmapEncoder GetEncoder(string fileExtension)
        {
            return fileExtension switch
            {
                ".png" => new PngBitmapEncoder(),
                ".jpg" or ".jpeg" => new JpegBitmapEncoder(),
                ".bmp" => new BmpBitmapEncoder(),
                _ => throw new NotImplementedException(),
            };
        }

        /// <summary>
        /// 异常处理
        /// </summary>
        /// <param name="exception">捕获异常</param>
        private static void ExceptionHandling(Exception exception)
        {
            MessageBox.Show($"{exception.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
        }
        #endregion
    }
}

使用技巧

  • 在图像中按下鼠标左键框选选取,框选后选区内容即可在右侧预览。
  • Press the left mouse button in the image to select, and after selecting, the selected content can be previewed on the right side.
  • 在图像中点击鼠标右键即可取消选区
  • Right click on the image to deselect

预览

在这里插入图片描述
在这里插入图片描述

下载

ImageCropping

posted @ 2026-10-05 17:57  JacePan  阅读(8)  评论(0)    收藏  举报