本文将介绍如何在 C# WinForms 项目中,利用 GDI+ 图形接口实现一个简单的“鼠标拖拽橡皮擦”效果。你可以把它应用在简易绘图程序、图片批注工具或自定义涂鸦组件中。
🎯 功能说明
我们实现的核心功能包括:
-
在 PictureBox 上加载或绘制底图;
-
按下鼠标左键并移动时,以鼠标当前位置为中心,清除一个小圆形区域(模拟橡皮擦);
-
实时更新显示,形成平滑擦除效果。
📐 实现思路
-
使用
PictureBox承载图像,在其Paint事件中用 GDI+ 绘制。 -
维护两个
Bitmap:-
baseImage:原始底图(不会被修改); -
drawingImage:当前显示画面(可被擦除)。
-
-
在鼠标拖动时,在
drawingImage上用Graphics.FillEllipse()填充背景色,覆盖原像素。 -
开启双缓冲,减少闪烁。
🔧 完整代码示例
1. 新建 WinForm 项目与界面
添加控件:
-
pictureBox(Dock = Fill) -
可选:按钮用于加载图片 / 重置画布
设置窗体属性:
public Form1()
{
InitializeComponent();
this.DoubleBuffered = true; // 窗体双缓冲
pictureBox.BackColor = Color.White;
}
2. 定义成员变量
private Bitmap baseImage; // 原始底图
private Bitmap drawingImage; // 正在绘制的画面
private bool isDrawing = false;
private Point lastPoint;
// 橡皮擦大小
private const int EraserSize = 20;
3. 初始化画布
提供两种方式:纯色画布或加载图片。
private void InitCanvas(int width, int height)
{
baseImage = new Bitmap(width, height);
using (var g = Graphics.FromImage(baseImage))
{
g.Clear(Color.White);
// 可在此绘制测试图形
g.DrawString("Try erasing here",
new Font("Arial", 16), Brushes.Black, 50, 50);
}
drawingImage = (Bitmap)baseImage.Clone();
pictureBox.Image = drawingImage;
}
// 若从文件加载:
private void LoadImage(string path)
{
baseImage?.Dispose();
baseImage = new Bitmap(path);
drawingImage?.Dispose();
drawingImage = (Bitmap)baseImage.Clone();
pictureBox.Image = drawingImage;
}
4. 鼠标事件处理
MouseDown
private void pictureBox_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
isDrawing = true;
lastPoint = e.Location;
DoErase(e.Location); // 立即擦除第一点
}
}
MouseMove
private void pictureBox_MouseMove(object sender, MouseEventArgs e)
{
if (!isDrawing) return;
// 简单线性插值,使连续擦除更平滑
var points = GetInterpolatedPoints(lastPoint, e.Location, EraserSize / 2f);
foreach (var pt in points)
DoErase(pt);
lastPoint = e.Location;
}
辅助方法:两点间插值
private List<Point> GetInterpolatedPoints(Point p1, Point p2, float stepDist)
{
var list = new List<Point>();
float dx = p2.X - p1.X;
float dy = p2.Y - p1.Y;
float dist = (float)Math.Sqrt(dx * dx + dy * dy);
int steps = Math.Max(1, (int)(dist / stepDist));
for (int i = 0; i <= steps; i++)
{
float t = (float)i / steps;
int x = (int)(p1.X + dx * t);
int y = (int)(p1.Y + dy * t);
list.Add(new Point(x, y));
}
return list;
}
MouseUp
private void pictureBox_MouseUp(object sender, MouseEventArgs e)
{
isDrawing = false;
}
5. 执行擦除操作
private void DoErase(Point loc)
{
if (drawingImage == null) return;
// 计算橡皮擦矩形范围
Rectangle eraseRect = new Rectangle(
loc.X - EraserSize / 2,
loc.Y - EraserSize / 2,
EraserSize,
EraserSize
);
// 限制在图片范围内
eraseRect.Intersect(new Rectangle(0, 0, drawingImage.Width, drawingImage.Height));
if (eraseRect.IsEmpty) return;
using (var g = Graphics.FromImage(drawingImage))
{
// 用背景色填充圆形,实现“擦除”
g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillEllipse(Brushes.White, eraseRect);
}
pictureBox.Invalidate(); // 触发重绘
}
6. Paint 事件(可选优化)
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
// 若已在 drawingImage 上绘制,此处可不做额外工作
// 如需叠加临时预览,可在这里加代码
}
✅ 运行效果
运行后,按住鼠标左键在图片上拖动,会看到一个白色圆形跟随鼠标移动,所到之处露出下方背景色(或底图的白色部分),形成擦除效果。
💡 扩展建议
-
透明 PNG 支持:若需真正透明擦除,可将
drawingImage设为带 Alpha 通道的PixelFormat.Format32bppArgb,并用Color.Transparent填充。 -
纹理橡皮擦:改用
TextureBrush替代纯色填充。 -
撤销/重做:记录每次擦除的区域或使用图层栈。
-
性能优化:频繁擦除时可限制刷新频率,或用
Region局部刷新。
📦 完整窗体类代码(可直接粘贴测试)
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace WinformEraserDemo
{
public partial class Form1 : Form
{
private Bitmap baseImage;
private Bitmap drawingImage;
private bool isDrawing = false;
private Point lastPoint;
private const int EraserSize = 25;
public Form1()
{
InitializeComponent();
DoubleBuffered = true;
SetupPictureBox();
InitCanvas(600, 400);
}
private void SetupPictureBox()
{
pictureBox = new PictureBox();
pictureBox.Dock = DockStyle.Fill;
pictureBox.BackColor = Color.LightGray;
Controls.Add(pictureBox);
pictureBox.MouseDown += pictureBox_MouseDown;
pictureBox.MouseMove += pictureBox_MouseMove;
pictureBox.MouseUp += pictureBox_MouseUp;
}
private void InitCanvas(int w, int h)
{
baseImage = new Bitmap(w, h);
using (var g = Graphics.FromImage(baseImage))
{
g.Clear(Color.White);
g.DrawRectangle(Pens.Blue, 100, 80, 300, 150);
g.DrawString("Drag to erase", new Font("Arial", 18), Brushes.Red, 120, 110);
}
drawingImage = (Bitmap)baseImage.Clone();
pictureBox.Image = drawingImage;
}
private void pictureBox_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
isDrawing = true;
lastPoint = e.Location;
DoErase(e.Location);
}
}
private void pictureBox_MouseMove(object sender, MouseEventArgs e)
{
if (!isDrawing) return;
var pts = GetInterpolatedPoints(lastPoint, e.Location, EraserSize / 3f);
foreach (var pt in pts)
DoErase(pt);
lastPoint = e.Location;
}
private void pictureBox_MouseUp(object sender, MouseEventArgs e)
{
isDrawing = false;
}
private List<Point> GetInterpolatedPoints(Point a, Point b, float step)
{
var list = new List<Point>();
float dx = b.X - a.X, dy = b.Y - a.Y;
float len = (float)Math.Sqrt(dx * dx + dy * dy);
int cnt = Math.Max(1, (int)(len / step));
for (int i = 0; i <= cnt; i++)
{
float t = (float)i / cnt;
list.Add(new Point((int)(a.X + dx * t), (int)(a.Y + dy * t)));
}
return list;
}
private void DoErase(Point pos)
{
if (drawingImage == null) return;
var rect = new Rectangle(pos.X - EraserSize / 2, pos.Y - EraserSize / 2, EraserSize, EraserSize);
rect.Intersect(new Rectangle(0, 0, drawingImage.Width, drawingImage.Height));
if (rect.IsEmpty) return;
using (var g = Graphics.FromImage(drawingImage))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillEllipse(Brushes.White, rect);
}
pictureBox.Invalidate(rect); // 局部刷新提升性能
}
// 重置画布
private void btnReset_Click(object sender, EventArgs e)
{
drawingImage?.Dispose();
drawingImage = (Bitmap)baseImage.Clone();
pictureBox.Image = drawingImage;
}
}
}