【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
预览



浙公网安备 33010602011771号