你还在为批量处理大量图片而头疼吗?设计师需要将几百张产品图片统一缩放,运营同学要批量压缩社交媒体素材,开发者要为移动端适配不同尺寸的图标...
今天,我将分享一个完整的C#批量图片处理解决方案,让你1分钟处理1000张图片,彻底告别重复劳动!
🎯 痛点分析:为什么需要批量图片处理?
在实际开发中,我们经常遇到这些场景:
手动处理这些任务不仅效率低下,还容易出错。今天我们就用C#打造一个专业级的批量处理工具!
💡 解决方案:基于SkiaSharp的高性能图片处理
🔧 技术选型
我们选择SkiaSharp作为图片处理库,原因如下:
- • 跨平台支持:Windows、Linux、macOS全覆盖
- • 高性能:基于Google Skia引擎,GPU加速
🛠️ 代码实战:构建批量处理核心功能
让我们逐步构建这个批量图片处理工具:
📁 文件选择与管理
private void BtnAddFiles_Click(object sender, EventArgs e)
{
using (var openDialog = new OpenFileDialog())
{
// 设置支持的图片格式过滤器
openDialog.Filter = "图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.tiff";
openDialog.Multiselect = true; // 允许多选
if (openDialog.ShowDialog() == DialogResult.OK)
{
foreach (stringfilein openDialog.FileNames)
{
// 避免重复添加同一文件
if (!fileListBox.Items.Contains(file))
{
fileListBox.Items.Add(file);
}
}
}
}
}
🎯 关键要点:
- • 使用
OpenFileDialog的Multiselect属性实现批量选择 - • 通过
Filter属性限制文件类型,提升用户体验
📤 输出目录设置
private void BtnOutputFolder_Click(object sender, EventArgs e)
{
using (var folderDialog = new FolderBrowserDialog())
{
if (folderDialog.ShowDialog() == DialogResult.OK)
{
outputFolder = folderDialog.SelectedPath;
// 实时更新状态栏,让用户知道当前设置
statusLabel.Text = $"输出文件夹: {outputFolder}";
}
}
}
🚀 异步批量处理核心逻辑
private async void BtnStart_Click(object sender, EventArgs e)
{
// 参数验证 - 确保用户输入完整
if (fileListBox.Items.Count == 0)
{
MessageBox.Show("请先添加要处理的文件!", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (string.IsNullOrEmpty(outputFolder))
{
MessageBox.Show("请选择输出文件夹!", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 防止重复点击
btnStart.Enabled = false;
// 初始化进度条
progressBar.Maximum = fileListBox.Items.Count;
progressBar.Value = 0;
// 获取用户设置的缩放比例和输出格式
var scale = (float)scaleNumeric.Value / 100f;
var format = GetBatchFormat();
// 异步处理,避免UI卡顿
await Task.Run(() => ProcessBatch(scale, format));
btnStart.Enabled = true;
MessageBox.Show("批量处理完成!", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
🔥 性能优化要点:
🎨 图片缩放算法实现
private void ProcessBatch(float scale, SKEncodedImageFormat format)
{
for (int i = 0; i < fileListBox.Items.Count; i++)
{
string inputFile = fileListBox.Items[i].ToString();
// 跨线程更新UI - 重要!
this.Invoke(new Action(() => {
statusLabel.Text = $"正在处理: {Path.GetFileName(inputFile)}";
progressBar.Value = i;
}));
try
{
// 使用using确保资源正确释放
using (var bitmap = SKBitmap.Decode(inputFile))
{
if (bitmap != null)
{
// 计算新尺寸
int newWidth = (int)(bitmap.Width * scale);
int newHeight = (int)(bitmap.Height * scale);
using (var scaledBitmap = new SKBitmap(newWidth, newHeight))
using (var canvas = new SKCanvas(scaledBitmap))
using (var paint = new SKPaint {
IsAntialias = true, // 抗锯齿
FilterQuality = SKFilterQuality.High // 高质量缩放
})
{
// 执行缩放绘制
canvas.DrawBitmap(bitmap,
new SKRect(0, 0, newWidth, newHeight), paint);
// 生成输出文件名
string outputFile = Path.Combine(outputFolder,
Path.GetFileNameWithoutExtension(inputFile) +
"_scaled" + GetExtension(format));
// 保存文件
using (var fileStream = new FileStream(outputFile, FileMode.Create))
using (var skStream = new SKManagedWStream(fileStream))
{
scaledBitmap.Encode(skStream, format, 95); // 95%质量
}
}
}
}
}
catch (Exception ex)
{
// 错误处理 - 单个文件失败不影响整体处理
this.Invoke(new Action(() => {
MessageBox.Show($"处理文件 {Path.GetFileName(inputFile)} 时发生错误:{ex.Message}",
"错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}));
}
}
// 更新最终状态
this.Invoke(new Action(() => {
progressBar.Value = fileListBox.Items.Count;
statusLabel.Text = "处理完成";
}));
}
💎 核心技术点:
- 1. 内存管理:使用
using语句确保SKBitmap等资源及时释放 - 2. 高质量缩放:
SKFilterQuality.High提供最佳视觉效果 - 3. 抗锯齿处理:
IsAntialias = true让缩放后的图片更平滑 - 4. 跨线程操作:
Invoke确保后台线程安全更新UI
🔄 格式转换支持
private SKEncodedImageFormat GetBatchFormat()
{
switch (outputFormatCombo.SelectedIndex)
{
case0: return SKEncodedImageFormat.Png; // 无损格式,适合图标
case1: return SKEncodedImageFormat.Jpeg; // 有损格式,适合照片
case2: return SKEncodedImageFormat.Bmp; // 位图格式
default: return SKEncodedImageFormat.Png;
}
}
private string GetExtension(SKEncodedImageFormat format)
{
switch (format)
{
case SKEncodedImageFormat.Png: return".png";
case SKEncodedImageFormat.Jpeg: return".jpg";
case SKEncodedImageFormat.Bmp: return".bmp";
default: return".png";
}
}
完整代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using SkiaSharp;
namespace AppGraphicsScaling
{
public partial class FrmBatchProcess : Form
{
private string outputFolder;
public FrmBatchProcess()
{
InitializeComponent();
}
private void BtnAddFiles_Click(object sender, EventArgs e)
{
using (var openDialog = new OpenFileDialog())
{
openDialog.Filter = "图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.tiff";
openDialog.Multiselect = true;
if (openDialog.ShowDialog() == DialogResult.OK)
{
foreach (stringfilein openDialog.FileNames)
{
if (!fileListBox.Items.Contains(file))
{
fileListBox.Items.Add(file);
}
}
}
}
}
private void BtnRemoveFile_Click(object sender, EventArgs e)
{
var selectedItems = fileListBox.SelectedItems.Cast<string>().ToList();
foreach (string item in selectedItems)
{
fileListBox.Items.Remove(item);
}
}
private void BtnClearAll_Click(object sender, EventArgs e)
{
fileListBox.Items.Clear();
}
private void BtnOutputFolder_Click(object sender, EventArgs e)
{
using (var folderDialog = new FolderBrowserDialog())
{
if (folderDialog.ShowDialog() == DialogResult.OK)
{
outputFolder = folderDialog.SelectedPath;
statusLabel.Text = $"输出文件夹: {outputFolder}";
}
}
}
private async void BtnStart_Click(object sender, EventArgs e)
{
if (fileListBox.Items.Count == 0)
{
MessageBox.Show("请先添加要处理的文件!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (string.IsNullOrEmpty(outputFolder))
{
MessageBox.Show("请选择输出文件夹!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
btnStart.Enabled = false;
progressBar.Maximum = fileListBox.Items.Count;
progressBar.Value = 0;
var scale = (float)scaleNumeric.Value / 100f;
var format = GetBatchFormat();
await Task.Run(() => ProcessBatch(scale, format));
btnStart.Enabled = true;
MessageBox.Show("批量处理完成!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void ProcessBatch(float scale, SKEncodedImageFormat format)
{
for (int i = 0; i < fileListBox.Items.Count; i++)
{
string inputFile = fileListBox.Items[i].ToString();
this.Invoke(new Action(() => {
statusLabel.Text = $"正在处理: {Path.GetFileName(inputFile)}";
progressBar.Value = i;
}));
try
{
using (var bitmap = SKBitmap.Decode(inputFile))
{
if (bitmap != null)
{
int newWidth = (int)(bitmap.Width * scale);
int newHeight = (int)(bitmap.Height * scale);
using (var scaledBitmap = new SKBitmap(newWidth, newHeight))
using (var canvas = new SKCanvas(scaledBitmap))
using (var paint = new SKPaint { IsAntialias = true, FilterQuality = SKFilterQuality.High })
{
canvas.DrawBitmap(bitmap, new SKRect(0, 0, newWidth, newHeight), paint);
string outputFile = Path.Combine(outputFolder,
Path.GetFileNameWithoutExtension(inputFile) + "_scaled" + GetExtension(format));
using (var fileStream = new FileStream(outputFile, FileMode.Create))
using (var skStream = new SKManagedWStream(fileStream))
{
scaledBitmap.Encode(skStream, format, 95);
}
}
}
}
}
catch (Exception ex)
{
this.Invoke(new Action(() => {
MessageBox.Show($"处理文件 {Path.GetFileName(inputFile)} 时发生错误:{ex.Message}",
"错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}));
}
}
this.Invoke(new Action(() => {
progressBar.Value = fileListBox.Items.Count;
statusLabel.Text = "处理完成";
}));
}
private SKEncodedImageFormat GetBatchFormat()
{
switch (outputFormatCombo.SelectedIndex)
{
case0: return SKEncodedImageFormat.Png;
case1: return SKEncodedImageFormat.Jpeg;
case2: return SKEncodedImageFormat.Bmp;
default: return SKEncodedImageFormat.Png;
}
}
private string GetExtension(SKEncodedImageFormat format)
{
switch (format)
{
case SKEncodedImageFormat.Png: return".png";
case SKEncodedImageFormat.Jpeg: return".jpg";
case SKEncodedImageFormat.Bmp: return".bmp";
default: return".png";
}
}
}
}

⚠️ 常见坑点提醒
1. 内存泄漏问题
// ❌ 错误写法 - 可能导致内存泄漏
var bitmap = SKBitmap.Decode(inputFile);
// 处理逻辑...
// 忘记释放资源
// ✅ 正确写法 - 自动释放资源
using (var bitmap = SKBitmap.Decode(inputFile))
{
// 处理逻辑...
} // 自动调用Dispose()
2. 跨线程UI更新
// ❌ 错误写法 - 会抛出跨线程异常
Task.Run(() => {
progressBar.Value = i; // 跨线程访问UI控件
});
// ✅ 正确写法 - 使用Invoke
Task.Run(() => {
this.Invoke(new Action(() => {
progressBar.Value = i;
}));
});
3. 大文件处理优化
对于超大图片文件,建议添加尺寸检查:
if (bitmap.Width > 10000 || bitmap.Height > 10000)
{
// 对超大图片进行分块处理或提示用户
MessageBox.Show("图片尺寸过大,建议先进行预处理");
continue;
}
💡 总结:三个关键收获
通过这个批量图片处理工具的实现,我们掌握了:
- 1. 🔧 SkiaSharp图片处理技术:高性能、跨平台的图片处理解决方案,支持多种格式和高质量缩放算法
- 2. ⚡ 异步编程最佳实践:使用async/await避免UI阻塞,通过Invoke安全更新界面,提升用户体验
- 3. 🛡️ 资源管理与异常处理:proper使用using语句管理内存,完善的错误处理机制确保程序稳定性
这个工具不仅解决了批量图片处理的痛点,更重要的是展示了C#在桌面应用开发中的强大能力。无论是企业级应用还是个人工具,都可以基于这个框架进行扩展。
阅读原文:原文链接
该文章在 2025/11/27 16:24:12 编辑过