如何在C# WinForm中实现手绘图形自动转为规整图形?
实现手绘形状规整功能(转直线/完美正方形)
核心实现思路
- 记录用户在Panel A上的所有手绘轨迹点,而非直接绘制(避免重绘丢失,同时方便后续形状分析)
- 用户结束绘制(
MouseUp事件)时,对轨迹点做分析:- 若轨迹接近直线,计算拟合后的标准直线并绘制到Panel B
- 若轨迹接近正方形,生成边长一致、角度为90度的完美正方形并绘制到Panel B
- 用
Bitmap存储Panel A的手绘内容,解决窗口重绘导致的内容丢失问题
改造后的完整代码
public partial class frmPaint : Form { // 存储手绘轨迹的所有点 private List<Point> _drawPoints = new List<Point>(); // 保存Panel A的手绘内容,用于重绘 private Bitmap _drawBitmap; private Pen _penA = new Pen(Color.Red, 2); private Pen _eraser = new Pen(Color.White, 10); public frmPaint() { InitializeComponent(); // 初始化Bitmap,尺寸与Panel A一致 _drawBitmap = new Bitmap(panelDraw.Width, panelDraw.Height); // 初始化Panel B的白色背景 using (var g = panelResult.CreateGraphics()) { g.Clear(Color.White); } } private void panelDraw_MouseDown(object sender, MouseEventArgs e) { // 开始绘制时清空上一次的轨迹点 _drawPoints.Clear(); _drawPoints.Add(e.Location); // 更新画笔宽度 if (rbLineWidth5.Checked) _penA.Width = 5; if (rbLineWidth10.Checked) _penA.Width = 10; if (rbLineWidth15.Checked) _penA.Width = 15; } private void panelDraw_MouseMove(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { _drawPoints.Add(e.Location); // 在Bitmap上绘制当前轨迹段 using (var g = Graphics.FromImage(_drawBitmap)) { g.DrawLine(_penA, _drawPoints[^2], _drawPoints[^1]); } // 刷新Panel A显示最新内容 panelDraw.Invalidate(); } else if (e.Button == MouseButtons.Right) { _drawPoints.Add(e.Location); // 橡皮擦功能:用白色线条覆盖原有内容 using (var g = Graphics.FromImage(_drawBitmap)) { g.DrawLine(_eraser, _drawPoints[^2], _drawPoints[^1]); } panelDraw.Invalidate(); } } private void panelDraw_MouseUp(object sender, MouseEventArgs e) { // 绘制结束后,分析轨迹并在Panel B生成规整图形 AnalyzeAndDrawShape(); } private void panelDraw_Paint(object sender, PaintEventArgs e) { // 重绘时展示Bitmap中的内容,避免内容丢失 e.Graphics.DrawImage(_drawBitmap, 0, 0); } private void btnColor_Click(object sender, EventArgs e) { using (ColorDialog colorDialog = new ColorDialog()) { if (colorDialog.ShowDialog() == DialogResult.OK) _penA.Color = colorDialog.Color; } } // 分析轨迹形状,绘制规整后的图形到Panel B private void AnalyzeAndDrawShape() { if (_drawPoints.Count < 2) return; // 清空Panel B的原有内容 using (var g = panelResult.CreateGraphics()) { g.Clear(Color.White); } // 判断是否为近似直线 if (IsApproximateLine(_drawPoints, 10)) { var (start, end) = FitLine(_drawPoints); using (var g = panelResult.CreateGraphics()) { g.DrawLine(_penA, start, end); } } // 判断是否为近似正方形 else if (IsApproximateSquare(_drawPoints, 0.15)) { var squarePoints = GetPerfectSquare(_drawPoints); using (var g = panelResult.CreateGraphics()) { g.DrawPolygon(_penA, squarePoints); } } } // 判断轨迹是否接近直线,maxAvgDistance为允许的平均像素误差 private bool IsApproximateLine(List<Point> points, double maxAvgDistance) { if (points.Count < 2) return false; var (start, end) = FitLine(points); double totalDistance = 0; foreach (var p in points) { totalDistance += GetDistanceToLine(p, start, end); } double avgDistance = totalDistance / points.Count; return avgDistance <= maxAvgDistance; } // 用最小二乘法拟合直线,返回拟合后的起止点 private (Point Start, Point End) FitLine(List<Point> points) { int n = points.Count; double sumX = points.Sum(p => p.X); double sumY = points.Sum(p => p.Y); double sumXY = points.Sum(p => p.X * p.Y); double sumX2 = points.Sum(p => p.X * p.X); double slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); double intercept = (sumY - slope * sumX) / n; // 取轨迹的最小/最大X值作为直线端点 int minX = points.Min(p => p.X); int maxX = points.Max(p => p.X); Point start = new Point(minX, (int)(slope * minX + intercept)); Point end = new Point(maxX, (int)(slope * maxX + intercept)); return (start, end); } // 计算点到直线的垂直距离 private double GetDistanceToLine(Point p, Point lineStart, Point lineEnd) { double numerator = Math.Abs((lineEnd.Y - lineStart.Y) * p.X - (lineEnd.X - lineStart.X) * p.Y + lineEnd.X * lineStart.Y - lineEnd.Y * lineStart.X); double denominator = Math.Sqrt(Math.Pow(lineEnd.Y - lineStart.Y, 2) + Math.Pow(lineEnd.X - lineStart.X, 2)); return denominator == 0 ? 0 : numerator / denominator; } // 判断轨迹是否接近正方形,errorThreshold为边长比例的允许误差 private bool IsApproximateSquare(List<Point> points, double errorThreshold) { if (points.Count < 4) return false; // 获取轨迹的边界矩形 var rect = GetBoundingRectangle(points); double width = rect.Width; double height = rect.Height; // 宽高比接近1 double ratio = Math.Min(width, height) / Math.Max(width, height); if (ratio < 1 - errorThreshold) return false; // 验证轨迹点是否覆盖矩形的四个边区域 bool hasLeft = points.Any(p => Math.Abs(p.X - rect.Left) < 10); bool hasRight = points.Any(p => Math.Abs(p.X - rect.Right) < 10); bool hasTop = points.Any(p => Math.Abs(p.Y - rect.Top) < 10); bool hasBottom = points.Any(p => Math.Abs(p.Y - rect.Bottom) < 10); return hasLeft && hasRight && hasTop && hasBottom; } // 获取轨迹点的边界矩形 private Rectangle GetBoundingRectangle(List<Point> points) { int minX = points.Min(p => p.X); int maxX = points.Max(p => p.X); int minY = points.Min(p => p.Y); int maxY = points.Max(p => p.Y); return new Rectangle(minX, minY, maxX - minX, maxY - minY); } // 根据轨迹边界生成完美正方形 private Point[] GetPerfectSquare(List<Point> points) { var rect = GetBoundingRectangle(points); // 取边界矩形的最大边长作为正方形边长 int sideLength = Math.Max(rect.Width, rect.Height); int centerX = rect.Left + rect.Width / 2; int centerY = rect.Top + rect.Height / 2; // 生成以原矩形中心为中心的正方形 int left = centerX - sideLength / 2; int top = centerY - sideLength / 2; return new Point[] { new Point(left, top), new Point(left + sideLength, top), new Point(left + sideLength, top + sideLength), new Point(left, top + sideLength), new Point(left, top) // 闭合图形 }; } }
关键逻辑说明
- 轨迹持久化:用
List<Point>记录所有手绘点,配合Bitmap存储绘制内容,通过Paint事件实现重绘,解决原生CreateGraphics导致的内容丢失问题。 - 直线识别:通过最小二乘法拟合轨迹直线,计算所有点到直线的平均距离,判断是否符合直线特征。
- 正方形识别与规整:先获取轨迹的边界矩形,验证宽高比是否接近1,再确认轨迹点覆盖矩形四边;最后生成以原矩形中心为基准的完美正方形。
- 交互触发:在用户松开鼠标时触发形状分析,将规整后的图形实时绘制到Panel B。
内容的提问来源于stack exchange,提问作者Mardibak
相关产品推荐
相关产品推荐

